From 1c8298a95a9022928bb2fa271816438c79e98e31 Mon Sep 17 00:00:00 2001 From: Ashutosh Meher Date: Tue, 29 Aug 2023 00:20:04 +0530 Subject: [PATCH] Refactor + BurnToken --- java-samples/shinny-tokens/README.md | 71 +++++-- .../tokens/contracts/GoldContract.java | 2 +- .../samples/tokens/states/GoldState.java | 20 +- .../tokens/states/GoldStateObserver.java | 2 +- .../workflows/BurnGoldTokenFLowArgs.java | 41 ++++ .../tokens/workflows/BurnGoldTokenFlow.java | 191 ++++++++++++++++++ .../workflows/IssueGoldTokenFlowArgs.java | 30 +++ ...kensFlow.java => IssueGoldTokensFlow.java} | 78 ++++--- .../tokens/workflows/ListGoldTokens.java | 2 +- .../workflows/MintGoldFlowInputArgs.java | 30 --- .../workflows/TransferGoldFlowInputArgs.java | 18 +- .../workflows/TransferGoldTokenFlow.java | 71 ++++--- 12 files changed, 408 insertions(+), 148 deletions(-) create mode 100644 java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/BurnGoldTokenFLowArgs.java create mode 100644 java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/BurnGoldTokenFlow.java create mode 100644 java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/IssueGoldTokenFlowArgs.java rename java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/{MintGoldTokensFlow.java => IssueGoldTokensFlow.java} (62%) delete mode 100644 java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/MintGoldFlowInputArgs.java diff --git a/java-samples/shinny-tokens/README.md b/java-samples/shinny-tokens/README.md index c9e41f2..a086b2c 100644 --- a/java-samples/shinny-tokens/README.md +++ b/java-samples/shinny-tokens/README.md @@ -13,9 +13,10 @@ using Next-Gen Corda. In this application, we will mint gold tokens and then transfer these tokens. In this app you can: -1. Write a flow to Create a Gold Asset/State on Ledger. `MintGoldTokensFlow` +1. Write a flow to Create a Gold Asset/State on Ledger. `IssueGoldTokensFlow` 2. List out the gold entries you had. `ListGoldTokens` -4. Claim and transfer the tokens to a new member. `TransferGoldTokenFlow` +3. Claim and transfer the tokens to a new member. `TransferGoldTokenFlow` +4. Burn tokens available with a member. `BurnGoldTokenFlow` ### Setting up @@ -36,20 +37,20 @@ Pick a VNode identity, and get its short hash. (Let's pick Alice.). Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Alice's hash) and request body: ``` { - "clientRequestId": "mint-1", - "flowClassName": "com.r3.developers.samples.tokens.workflows.MintGoldTokensFlow", + "clientRequestId": "issue-1", + "flowClassName": "com.r3.developers.samples.tokens.workflows.IssueGoldTokensFlow", "requestBody": { - "symbol":"GOLD", - "issuer":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB", - "value":"20" - } + "symbol": "GOLD", + "owner": "CN=Bob, OU=Test Dept, O=R3, L=London, C=GB", + "amount": "20" + } } ``` -After trigger the MintGoldTokensFlow flow, hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter the short hash(Alice's hash) and clientrequestid to view the flow result +After trigger the IssueGoldTokensFlow flow, hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter the short hash(Alice's hash) and clientrequestid to view the flow result #### Step 2: List the gold state -Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Alice's hash) and request body: +Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Bob's hash) and request body: ``` { "clientRequestId": "list-1", @@ -59,28 +60,27 @@ Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Ali ``` After trigger the ListGoldTokens flow, again, we need to hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and check the result. -As the screenshot shows, in the response body, we will see a list of the gold state we created. #### Step 3: Transfer the gold token with `TransferGoldTokenFlow` -In this step, Alice will transfer some tokens from his vault to Charlie. +In this step, Bob will transfer some tokens from his vault to Charlie. Goto `POST /flow/{holdingidentityshorthash}`, enter the identity short hash and request body. -Use Alice's holdingidentityshorthash to fire this post API. +Use Bob's holdingidentityshorthash to fire this post API. ``` { "clientRequestId": "transfer-1", "flowClassName": "com.r3.developers.samples.tokens.workflows.TransferGoldTokenFlow", "requestBody": { - "symbol":"GOLD", - "issuer":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB", - "newOwner":"CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB", - "value": "5" + "symbol": "GOLD", + "issuer": "CN=Alice, OU=Test Dept, O=R3, L=London, C=GB", + "receiver": "CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB", + "amount": "5" } } ``` And as for the result of this flow, go to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter the required fields. -#### Step 4: Confirm the token balances of Alice and Charlie -Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Alice's hash) and request body: +#### Step 4: Confirm the token balances of Bob and Charlie +Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Bob's hash) and request body: ``` { "clientRequestId": "list-2", @@ -98,10 +98,37 @@ Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Cha ``` And as for the result, you need to go to the Get API again and enter the short hash and client request ID. -Thus, we have concluded a full run through of the token app. +#### Step 5: Burn gold token with BurnGoldTokenFlow +Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Bob's hash) and request body: +``` +{ + "clientRequestId": "burn-1", + "flowClassName": "com.r3.developers.samples.tokens.workflows.BurnGoldTokenFlow", + "requestBody": { + "symbol": "GOLD", + "issuer": "CN=Alice, OU=Test Dept, O=R3, L=London, C=GB", + "amount": "5" + } +} +``` +Go to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter the required fields to check the result of +the flow. + +#### Step 4: Confirm the token balance of Bob + +Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Bob's hash) and request body: +``` +{ + "clientRequestId": "list-4", + "flowClassName": "com.r3.developers.samples.tokens.workflows.ListGoldTokens", + "requestBody": {} +} +``` + +And as for the result, you need to go to the Get API again and enter the short hash and client request ID. +Thus, we have concluded a full run through of the token app. # Additional Information -To read more about Token Selection API, you can visit the [docs](https://docs.r3.com/en/platform/corda/5.0-beta/developing/api/api-ledger-token-selection.html#tokens) and -read [this](https://r3-cev.atlassian.net/wiki/spaces/DR/pages/4435017960/Shiny+tokens+in+Next-Gen+Corda) blog. \ No newline at end of file +To read more about Token Selection API, you can visit the [docs](https://docs.r3.com/en/platform/corda/5.0/developing-applications/api/ledger/utxo-ledger/token-selection.html) \ No newline at end of file diff --git a/java-samples/shinny-tokens/contracts/src/main/java/com/r3/developers/samples/tokens/contracts/GoldContract.java b/java-samples/shinny-tokens/contracts/src/main/java/com/r3/developers/samples/tokens/contracts/GoldContract.java index 3c304b5..8cdcf27 100644 --- a/java-samples/shinny-tokens/contracts/src/main/java/com/r3/developers/samples/tokens/contracts/GoldContract.java +++ b/java-samples/shinny-tokens/contracts/src/main/java/com/r3/developers/samples/tokens/contracts/GoldContract.java @@ -11,7 +11,7 @@ public class GoldContract implements Contract { private final static Logger log = LoggerFactory.getLogger(GoldContract.class); - public static class Create implements Command { } + public static class Issue implements Command { } public static class Transfer implements Command { } diff --git a/java-samples/shinny-tokens/contracts/src/main/java/com/r3/developers/samples/tokens/states/GoldState.java b/java-samples/shinny-tokens/contracts/src/main/java/com/r3/developers/samples/tokens/states/GoldState.java index 29afc4a..915f596 100644 --- a/java-samples/shinny-tokens/contracts/src/main/java/com/r3/developers/samples/tokens/states/GoldState.java +++ b/java-samples/shinny-tokens/contracts/src/main/java/com/r3/developers/samples/tokens/states/GoldState.java @@ -4,6 +4,7 @@ import net.corda.v5.crypto.SecureHash; import net.corda.v5.ledger.utxo.BelongsToContract; import net.corda.v5.ledger.utxo.ContractState; +import org.jetbrains.annotations.NotNull; import java.math.BigDecimal; import java.security.PublicKey; @@ -13,17 +14,17 @@ public class GoldState implements ContractState { private SecureHash issuer; - private String symbol; - private BigDecimal value; private SecureHash owner; + private String symbol; + private BigDecimal amount; public List participants; - public GoldState(SecureHash issuer, String symbol, BigDecimal value, List participants, SecureHash owner) { + public GoldState(SecureHash issuer, SecureHash owner, String symbol, BigDecimal amount, List participants) { this.issuer = issuer; + this.owner = owner; this.symbol = symbol; - this.value = value; + this.amount = amount; this.participants = participants; - this.owner = owner; } public SecureHash getIssuer() { @@ -34,21 +35,18 @@ public String getSymbol() { return symbol; } - public BigDecimal getValue() { - return value; + public BigDecimal getAmount() { + return amount; } public SecureHash getOwner() { return owner; } + @NotNull @Override public List getParticipants() { return participants; } - - - - } diff --git a/java-samples/shinny-tokens/contracts/src/main/java/com/r3/developers/samples/tokens/states/GoldStateObserver.java b/java-samples/shinny-tokens/contracts/src/main/java/com/r3/developers/samples/tokens/states/GoldStateObserver.java index 99ad921..c76e179 100644 --- a/java-samples/shinny-tokens/contracts/src/main/java/com/r3/developers/samples/tokens/states/GoldStateObserver.java +++ b/java-samples/shinny-tokens/contracts/src/main/java/com/r3/developers/samples/tokens/states/GoldStateObserver.java @@ -23,6 +23,6 @@ public Class getStateType() { public UtxoToken onCommit(@NotNull GoldState state, @NotNull DigestService digestService) { //generate a pool with key - type, issuer and symbol to mint the tokens UtxoTokenPoolKey poolKey = new UtxoTokenPoolKey(GoldState.class.getName(), state.getIssuer(), state.getSymbol()); - return new UtxoToken(poolKey, state.getValue(), new UtxoTokenFilterFields(null, state.getOwner())); + return new UtxoToken(poolKey, state.getAmount(), new UtxoTokenFilterFields(null, state.getOwner())); } } diff --git a/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/BurnGoldTokenFLowArgs.java b/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/BurnGoldTokenFLowArgs.java new file mode 100644 index 0000000..d33f0f4 --- /dev/null +++ b/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/BurnGoldTokenFLowArgs.java @@ -0,0 +1,41 @@ +package com.r3.developers.samples.tokens.workflows; + +public class BurnGoldTokenFLowArgs { + + public BurnGoldTokenFLowArgs() { + } + + private String symbol; + private String issuer; + private String amount; + + public BurnGoldTokenFLowArgs(String symbol, String issuer, String amount) { + this.symbol = symbol; + this.issuer = issuer; + this.amount = amount; + } + + public String getSymbol() { + return symbol; + } + + public void setSymbol(String symbol) { + this.symbol = symbol; + } + + public String getIssuer() { + return issuer; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + public String getAmount() { + return amount; + } + + public void setAmount(String amount) { + this.amount = amount; + } +} diff --git a/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/BurnGoldTokenFlow.java b/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/BurnGoldTokenFlow.java new file mode 100644 index 0000000..99dba27 --- /dev/null +++ b/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/BurnGoldTokenFlow.java @@ -0,0 +1,191 @@ +package com.r3.developers.samples.tokens.workflows; + +import com.r3.developers.samples.tokens.contracts.GoldContract; +import com.r3.developers.samples.tokens.states.GoldState; +import net.corda.v5.application.crypto.DigestService; +import net.corda.v5.application.flows.ClientRequestBody; +import net.corda.v5.application.flows.ClientStartableFlow; +import net.corda.v5.application.flows.CordaInject; +import net.corda.v5.application.flows.FlowEngine; +import net.corda.v5.application.marshalling.JsonMarshallingService; +import net.corda.v5.application.membership.MemberLookup; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.base.exceptions.CordaRuntimeException; +import net.corda.v5.base.types.MemberX500Name; +import net.corda.v5.crypto.SecureHash; +import net.corda.v5.ledger.common.NotaryLookup; +import net.corda.v5.ledger.utxo.UtxoLedgerService; +import net.corda.v5.ledger.utxo.token.selection.ClaimedToken; +import net.corda.v5.ledger.utxo.token.selection.TokenClaim; +import net.corda.v5.ledger.utxo.token.selection.TokenClaimCriteria; +import net.corda.v5.ledger.utxo.token.selection.TokenSelection; +import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; +import net.corda.v5.ledger.utxo.transaction.UtxoTransactionBuilder; +import net.corda.v5.membership.MemberInfo; +import net.corda.v5.membership.NotaryInfo; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigDecimal; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.Objects.requireNonNull; +import static net.corda.v5.crypto.DigestAlgorithmName.SHA2_256; + +public class BurnGoldTokenFlow implements ClientStartableFlow { + + private final static Logger log = LoggerFactory.getLogger(BurnGoldTokenFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public MemberLookup memberLookup; + + @CordaInject + public NotaryLookup notaryLookup; + + // Token Selection API can be injected with CordaInject + @CordaInject + public TokenSelection tokenSelection; + + @CordaInject + public UtxoLedgerService ledgerService; + + @CordaInject + public DigestService digestService; + + @NotNull + @Override + @Suspendable + public String call(@NotNull ClientRequestBody requestBody) { + TokenClaim tokenClaim = null; + BigDecimal totalAmount = null; + BigDecimal change = null; + + try{ + BurnGoldTokenFLowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, BurnGoldTokenFLowArgs.class); + + MemberInfo myInfo = memberLookup.myInfo(); + + // Get the issuer of the token + MemberInfo issuerMember = requireNonNull( + memberLookup.lookup(MemberX500Name.parse(flowArgs.getIssuer())), + "MemberLookup can't find otherMember specified in flow arguments." + ); + + NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next(); + + // Create the token claim criteria by specifying the issuer and amount + TokenClaimCriteria tokenClaimCriteria = new TokenClaimCriteria( + GoldState.class.getName(), + getSecureHash(issuerMember.getName().getCommonName()), + notary.getName(), + flowArgs.getSymbol(), + new BigDecimal(flowArgs.getAmount()) + ); + + // tryClaim will check in the vault if there are tokens which can satisfy the expected amount. + // If yes all the fungible tokens are returned back. + // Remaining change will be returned back to the sender. + tokenClaim = tokenSelection.tryClaim(tokenClaimCriteria); + + if(tokenClaim == null) { + log.info("No tokens found for" + jsonMarshallingService.format(tokenClaimCriteria)); + return "No Tokens Found"; + } + + List claimedTokenList = tokenClaim.getClaimedTokens().stream().collect(Collectors.toList()); + + // calculate the change to be given back to the sender + totalAmount = claimedTokenList.stream().map(ClaimedToken::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + change = totalAmount.subtract(new BigDecimal(flowArgs.getAmount())); + + log.info("Found total " + totalAmount + " amount of tokens for " + jsonMarshallingService.format(tokenClaimCriteria)); + + UtxoTransactionBuilder txBuilder = null; + + + if(change.compareTo(BigDecimal.ZERO) > 0) { + // if there is change to be returned back to the sender, create a new gold state representing the original + // sender and the change. + GoldState goldStateChange = new GoldState( + getSecureHash(issuerMember.getName().getCommonName()), + getSecureHash(myInfo.getName().getCommonName()), + flowArgs.getSymbol(), change, + Collections.singletonList(myInfo.getLedgerKeys().get(0)) + ); + + txBuilder = ledgerService.createTransactionBuilder() + .setNotary(notary.getName()) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addInputStates(tokenClaim.getClaimedTokens().stream().map(ClaimedToken::getStateRef).collect(Collectors.toList())) + .addOutputStates(List.of(goldStateChange)) + .addCommand(new GoldContract.Transfer()) + .addSignatories(Collections.singletonList(myInfo.getLedgerKeys().get(0))); + } else { + // if there is no change, no need to create state representing the change to be given back to the sender. + txBuilder = ledgerService.createTransactionBuilder() + .setNotary(notary.getName()) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addInputStates(tokenClaim.getClaimedTokens().stream().map(ClaimedToken::getStateRef).collect(Collectors.toList())) + .addCommand(new GoldContract.Transfer()) + .addSignatories(Collections.singletonList(myInfo.getLedgerKeys().get(0))); + } + + UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction(); + UtxoSignedTransaction finalizedSignedTransaction = ledgerService.finalize( + signedTransaction, + Arrays.asList() + ).getTransaction(); + + String result = finalizedSignedTransaction.getId().toString(); + log.info("Success! Response: " + result); + + }catch (Exception e){ + log.warn("Failed to process utxo flow for request body " + requestBody + " because: " + e.getMessage()); + + log.info("Released the claim on the token states, indicating we spent none of them"); + // None of the tokens were used, so release all the claimed tokens + tokenClaim.useAndRelease(Arrays.asList()); + + throw new CordaRuntimeException(e.getMessage()); + }finally { + // Remove any used tokens from the cache and unlocks any remaining tokens for other flows to claim. + if(tokenClaim != null) { + log.info("Release the claim on the token states, indicating we spent them all"); + + tokenClaim.useAndRelease(tokenClaim.getClaimedTokens().stream().map(ClaimedToken::getStateRef).collect(Collectors.toList())); + + return "Total Available amount of Tokens : " + totalAmount + " change to be given back to the owner : " + change + " Total amount satisfied " + totalAmount.subtract(change); + + } + return "No Tokens Found"; + } + } + + @Suspendable + private SecureHash getSecureHash(String commonName) { + return digestService.hash(commonName.getBytes(), SHA2_256); + } +} + +/* +RequestBody for triggering the flow via REST: +{ + "clientRequestId": "burn-1", + "flowClassName": "com.r3.developers.samples.tokens.workflows.BurnGoldTokenFlow", + "requestBody": { + "symbol": "GOLD", + "issuer": "CN=Alice, OU=Test Dept, O=R3, L=London, C=GB", + "amount": "5" + } +} + */ + diff --git a/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/IssueGoldTokenFlowArgs.java b/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/IssueGoldTokenFlowArgs.java new file mode 100644 index 0000000..bcf743d --- /dev/null +++ b/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/IssueGoldTokenFlowArgs.java @@ -0,0 +1,30 @@ +package com.r3.developers.samples.tokens.workflows; + +// A class to hold the deserialized arguments required to start the flow. +public class IssueGoldTokenFlowArgs { + + // Serialisation service requires a default constructor + public IssueGoldTokenFlowArgs() {} + + private String symbol; + private String amount; + private String owner; + + public IssueGoldTokenFlowArgs(String symbol, String amount, String owner) { + this.symbol = symbol; + this.amount = amount; + this.owner = owner; + } + + public String getSymbol() { + return symbol; + } + + public String getAmount() { + return amount; + } + + public String getOwner() { + return owner; + } +} \ No newline at end of file diff --git a/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/MintGoldTokensFlow.java b/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/IssueGoldTokensFlow.java similarity index 62% rename from java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/MintGoldTokensFlow.java rename to java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/IssueGoldTokensFlow.java index d634ee4..01dc3cb 100644 --- a/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/MintGoldTokensFlow.java +++ b/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/IssueGoldTokensFlow.java @@ -3,11 +3,10 @@ import com.r3.developers.samples.tokens.contracts.GoldContract; import com.r3.developers.samples.tokens.states.GoldState; import net.corda.v5.application.crypto.DigestService; -import net.corda.v5.application.flows.ClientRequestBody; -import net.corda.v5.application.flows.ClientStartableFlow; -import net.corda.v5.application.flows.CordaInject; +import net.corda.v5.application.flows.*; import net.corda.v5.application.marshalling.JsonMarshallingService; import net.corda.v5.application.membership.MemberLookup; +import net.corda.v5.application.messaging.FlowMessaging; import net.corda.v5.base.annotations.Suspendable; import net.corda.v5.base.exceptions.CordaRuntimeException; import net.corda.v5.base.types.MemberX500Name; @@ -18,71 +17,68 @@ import net.corda.v5.ledger.utxo.transaction.UtxoTransactionBuilder; import net.corda.v5.membership.MemberInfo; import net.corda.v5.membership.NotaryInfo; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.math.BigDecimal; import java.time.Duration; import java.time.Instant; -import java.util.Arrays; +import java.util.Collections; import static java.util.Objects.requireNonNull; import static net.corda.v5.crypto.DigestAlgorithmName.SHA2_256; -// Alice will trigger this flow, by specifying the issuer. The issuer will issue tokens worth of given amount to Alice. -public class MintGoldTokensFlow implements ClientStartableFlow { - - private final static Logger log = LoggerFactory.getLogger(MintGoldTokensFlow.class); - +// Alice will trigger this flow to issue gold tokens to Bob. +public class IssueGoldTokensFlow implements ClientStartableFlow { + private final static Logger log = LoggerFactory.getLogger(IssueGoldTokensFlow.class); @CordaInject public JsonMarshallingService jsonMarshallingService; - @CordaInject public MemberLookup memberLookup; - - // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. @CordaInject public UtxoLedgerService ledgerService; - @CordaInject public NotaryLookup notaryLookup; - + @CordaInject + public FlowEngine flowEngine; @CordaInject public DigestService digestService; + @NotNull @Suspendable @Override - public String call( ClientRequestBody requestBody) { - - log.info("CreateNewChatFlow.call() called"); + public String call(ClientRequestBody requestBody) { try { // Obtain the deserialized input arguments to the flow from the requestBody. - MintGoldFlowInputArgs mintGoldInputRequest = requestBody.getRequestBodyAs(jsonMarshallingService, MintGoldFlowInputArgs.class); + IssueGoldTokenFlowArgs mintGoldInputRequest = + requestBody.getRequestBodyAs(jsonMarshallingService, IssueGoldTokenFlowArgs.class); // Get MemberInfos for the Vnode running the flow and the issuerMember. MemberInfo myInfo = memberLookup.myInfo(); - - MemberInfo issuerMember = requireNonNull( - memberLookup.lookup(MemberX500Name.parse(mintGoldInputRequest.getIssuer())), - "MemberLookup can't find issuerMember specified in flow arguments." + MemberInfo owner = requireNonNull( + memberLookup.lookup(MemberX500Name.parse(mintGoldInputRequest.getOwner())), + "MemberLookup can't find owner specified in flow arguments." ); + // Obtain the Notary + NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next(); - GoldState goldState = new GoldState(getSecureHash(issuerMember.getName().getCommonName()), + GoldState goldState = new GoldState( + getSecureHash(myInfo.getName().getCommonName()), + getSecureHash(owner.getName().getCommonName()), mintGoldInputRequest.getSymbol(), - new BigDecimal(mintGoldInputRequest.getValue()), - Arrays.asList(myInfo.getLedgerKeys().get(0)), - getSecureHash(myInfo.getName().getCommonName())); + new BigDecimal(mintGoldInputRequest.getAmount()), + Collections.singletonList(owner.getLedgerKeys().get(0)) + ); - // Obtain the Notary - NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next(); // Use UTXOTransactionBuilder to build up the draft transaction. UtxoTransactionBuilder txBuilder = ledgerService.createTransactionBuilder() .setNotary(notary.getName()) .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) .addOutputState(goldState) - .addCommand(new GoldContract.Create()) + .addCommand(new GoldContract.Issue()) .addSignatories(myInfo.getLedgerKeys().get(0)); // Convert the transaction builder to a UTXOSignedTransaction. Verifies the content of the @@ -90,15 +86,8 @@ public String call( ClientRequestBody requestBody) { // the current node. UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction(); - // Call FinalizeChatSubFlow which will finalise the transaction.There is no counter-party from whom - // we have to take signature and hence we will not pass any flow sessions. - UtxoSignedTransaction finalizedSignedTransaction = ledgerService.finalize( - signedTransaction, - Arrays.asList() - ).getTransaction(); - String result = finalizedSignedTransaction.getId().toString(); - log.info("Success! Response: " + result); - return result; + + return flowEngine.subFlow(new FinalizeMintSubFlow(signedTransaction, owner.getName())); } // Catch any exceptions, log them and rethrow the exception. catch (Exception e) { @@ -107,6 +96,7 @@ public String call( ClientRequestBody requestBody) { } } + @Suspendable private SecureHash getSecureHash(String commonName) { return digestService.hash(commonName.getBytes(), SHA2_256); } @@ -115,12 +105,12 @@ private SecureHash getSecureHash(String commonName) { /* RequestBody for triggering the flow via REST: { - "clientRequestId": "create-1", - "flowClassName": "com.r3.developers.samples.tokens.workflows.MintGoldTokensFlow", + "clientRequestId": "mint-1", + "flowClassName": "com.r3.developers.samples.tokens.workflows.IssueGoldTokensFlow", "requestBody": { - "symbol":"GOLD", - "issuer":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB", - "value":"20" - } + "symbol": "GOLD", + "owner": "CN=Bob, OU=Test Dept, O=R3, L=London, C=GB", + "amount": "20" + } } */ diff --git a/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/ListGoldTokens.java b/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/ListGoldTokens.java index 8694ff4..16b3e37 100644 --- a/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/ListGoldTokens.java +++ b/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/ListGoldTokens.java @@ -36,7 +36,7 @@ public String call(ClientRequestBody requestBody) { new GoldStateList( stateAndRef.getState().getContractState().getIssuer(), stateAndRef.getState().getContractState().getSymbol(), - stateAndRef.getState().getContractState().getValue(), + stateAndRef.getState().getContractState().getAmount(), stateAndRef.getState().getContractState().getOwner() ) ).collect(Collectors.toList()); diff --git a/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/MintGoldFlowInputArgs.java b/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/MintGoldFlowInputArgs.java deleted file mode 100644 index 1823483..0000000 --- a/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/MintGoldFlowInputArgs.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.r3.developers.samples.tokens.workflows; - -// A class to hold the deserialized arguments required to start the flow. -public class MintGoldFlowInputArgs { - - // Serialisation service requires a default constructor - public MintGoldFlowInputArgs() {} - - private String symbol; - private String issuer; - private String value; - - public MintGoldFlowInputArgs(String symbol, String issuer, String value) { - this.symbol = symbol; - this.issuer = issuer; - this.value = value; - } - - public String getSymbol() { - return symbol; - } - - public String getIssuer() { - return issuer; - } - - public String getValue() { - return value; - } -} \ No newline at end of file diff --git a/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/TransferGoldFlowInputArgs.java b/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/TransferGoldFlowInputArgs.java index f599dbf..5c50d88 100644 --- a/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/TransferGoldFlowInputArgs.java +++ b/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/TransferGoldFlowInputArgs.java @@ -8,14 +8,14 @@ public TransferGoldFlowInputArgs() {} private String symbol; private String issuer; - private String value; - private String newOwner; + private String amount; + private String receiver; - public TransferGoldFlowInputArgs(String symbol, String issuer, String value, String newOwner) { + public TransferGoldFlowInputArgs(String symbol, String issuer, String amount, String receiver) { this.symbol = symbol; this.issuer = issuer; - this.value = value; - this.newOwner = newOwner; + this.amount = amount; + this.receiver = receiver; } public String getSymbol() { @@ -26,11 +26,11 @@ public String getIssuer() { return issuer; } - public String getValue() { - return value; + public String getAmount() { + return amount; } - public String getNewOwner() { - return newOwner; + public String getReceiver() { + return receiver; } } \ No newline at end of file diff --git a/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/TransferGoldTokenFlow.java b/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/TransferGoldTokenFlow.java index cb05550..570cbd1 100644 --- a/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/TransferGoldTokenFlow.java +++ b/java-samples/shinny-tokens/workflows/src/main/java/com/r3/developers/samples/tokens/workflows/TransferGoldTokenFlow.java @@ -29,6 +29,7 @@ import java.math.BigDecimal; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -36,8 +37,8 @@ import static java.util.Objects.requireNonNull; import static net.corda.v5.crypto.DigestAlgorithmName.SHA2_256; -// This flow will be triggered by Alice to transfer some of his tokens to Charlie. The remaining -// amount of tokens will be given back as change to Alice. +// This flow will be triggered by Bob to transfer some of his tokens to Charlie. The remaining +// amount of tokens will be given back as change to Bob. public class TransferGoldTokenFlow implements ClientStartableFlow { private final static Logger log = LoggerFactory.getLogger(TransferGoldTokenFlow.class); @@ -73,13 +74,14 @@ public String call( ClientRequestBody requestBody) { try { - TransferGoldFlowInputArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, TransferGoldFlowInputArgs.class); + TransferGoldFlowInputArgs flowArgs = requestBody.getRequestBodyAs( + jsonMarshallingService, TransferGoldFlowInputArgs.class); MemberInfo myInfo = memberLookup.myInfo(); - // Take the new owner of the token whom Alice will transfer the token - MemberInfo newOwnerMember = requireNonNull( - memberLookup.lookup(MemberX500Name.parse(flowArgs.getNewOwner())), + // Take the receiver of the token whom Bob will transfer the token + MemberInfo receiver = requireNonNull( + memberLookup.lookup(MemberX500Name.parse(flowArgs.getReceiver())), "MemberLookup can't find otherMember specified in flow arguments." ); @@ -97,7 +99,7 @@ public String call( ClientRequestBody requestBody) { getSecureHash(issuerMember.getName().getCommonName()), notary.getName(), flowArgs.getSymbol(), - new BigDecimal(flowArgs.getValue()) + new BigDecimal(flowArgs.getAmount()) ); // tryClaim will check in the vault if there are tokens which can satisfy the expected amount. @@ -110,52 +112,59 @@ public String call( ClientRequestBody requestBody) { return "No Tokens Found"; } - List claimedTokenList = tokenClaim.getClaimedTokens().stream().collect(Collectors.toList()); + List claimedTokenList = new ArrayList<>(tokenClaim.getClaimedTokens()); // calculate the change to be given back to the sender - totalAmount = claimedTokenList.stream().map(ClaimedToken::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); - change = totalAmount.subtract(new BigDecimal(flowArgs.getValue())); + totalAmount = claimedTokenList.stream().map(ClaimedToken::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + change = totalAmount.subtract(new BigDecimal(flowArgs.getAmount())); - log.info("Found total " + totalAmount + " amount of tokens for " + jsonMarshallingService.format(tokenClaimCriteria)); + log.info("Found total " + totalAmount + " amount of tokens for " + + jsonMarshallingService.format(tokenClaimCriteria)); // create a new state representing the new owner and the expected amount. - GoldState goldStateNew = new GoldState(getSecureHash(issuerMember.getName().getCommonName()), - flowArgs.getSymbol(), new BigDecimal(flowArgs.getValue()), - Arrays.asList(newOwnerMember.getLedgerKeys().get(0)), - getSecureHash(newOwnerMember.getName().getCommonName())); + GoldState goldStateNew = new GoldState( + getSecureHash(issuerMember.getName().getCommonName()), + getSecureHash(receiver.getName().getCommonName()), + flowArgs.getSymbol(), new BigDecimal(flowArgs.getAmount()), + Arrays.asList(receiver.getLedgerKeys().get(0)) + ); UtxoTransactionBuilder txBuilder = null; if(change.compareTo(BigDecimal.ZERO) > 0) { - // if there is change to be returned back to the sender, create a new gold state representing the original - // sender and the change. + // if there is change to be returned back to the sender, create a new gold state + // representing the original sender and the change. GoldState goldStateChange = new GoldState(getSecureHash(issuerMember.getName().getCommonName()), + getSecureHash(myInfo.getName().getCommonName()), flowArgs.getSymbol(), change, - Arrays.asList(myInfo.getLedgerKeys().get(0)), - getSecureHash(newOwnerMember.getName().getCommonName())); + Arrays.asList(myInfo.getLedgerKeys().get(0)) + ); txBuilder = ledgerService.createTransactionBuilder() .setNotary(notary.getName()) .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) - .addInputStates(tokenClaim.getClaimedTokens().stream().map(ClaimedToken::getStateRef).collect(Collectors.toList())) - .addOutputStates(Arrays.asList(goldStateChange, goldStateNew)) + .addInputStates(tokenClaim.getClaimedTokens().stream().map(ClaimedToken::getStateRef) + .collect(Collectors.toList())) + .addOutputStates(goldStateChange, goldStateNew) .addCommand(new GoldContract.Transfer()) - .addSignatories(Arrays.asList(myInfo.getLedgerKeys().get(0), newOwnerMember.getLedgerKeys().get(0))); + .addSignatories(Arrays.asList(myInfo.getLedgerKeys().get(0), receiver.getLedgerKeys().get(0))); } else { // if there is no change, no need to create state representing the change to be given back to the sender. txBuilder = ledgerService.createTransactionBuilder() .setNotary(notary.getName()) .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) - .addInputStates(tokenClaim.getClaimedTokens().stream().map(ClaimedToken::getStateRef).collect(Collectors.toList())) + .addInputStates(tokenClaim.getClaimedTokens().stream().map(ClaimedToken::getStateRef) + .collect(Collectors.toList())) .addOutputStates(goldStateNew) .addCommand(new GoldContract.Transfer()) - .addSignatories(Arrays.asList(myInfo.getLedgerKeys().get(0), newOwnerMember.getLedgerKeys().get(0))); + .addSignatories(Arrays.asList(myInfo.getLedgerKeys().get(0), receiver.getLedgerKeys().get(0))); } UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction(); - flowEngine.subFlow(new FinalizeMintSubFlow(signedTransaction, newOwnerMember.getName())); + flowEngine.subFlow(new FinalizeMintSubFlow(signedTransaction, receiver.getName())); } catch (Exception e) { @@ -172,15 +181,19 @@ public String call( ClientRequestBody requestBody) { if(tokenClaim != null) { log.info("Release the claim on the token states, indicating we spent them all"); - tokenClaim.useAndRelease(tokenClaim.getClaimedTokens().stream().map(ClaimedToken::getStateRef).collect(Collectors.toList())); + tokenClaim.useAndRelease(tokenClaim.getClaimedTokens().stream().map(ClaimedToken::getStateRef) + .collect(Collectors.toList())); - return "Total Available amount of Tokens : " + totalAmount + " change to be given back to the owner : " + change + " Total amount satisfied " + totalAmount.subtract(change); + return "Total Available amount of Tokens : " + totalAmount + + " change to be given back to the owner : " + change + " Total amount satisfied " + + totalAmount.subtract(change); } return "No Tokens Found"; } } + @Suspendable private SecureHash getSecureHash(String commonName) { return digestService.hash(commonName.getBytes(), SHA2_256); } @@ -193,8 +206,8 @@ private SecureHash getSecureHash(String commonName) { "requestBody": { "symbol":"GOLD", "issuer":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB", - "newOwner":"CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB", - "value": "5" + "receiver":"CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB", + "amount": "5" } } */ \ No newline at end of file