|
| 1 | +package net.corda.examples.spaceships.flows; |
| 2 | + |
| 3 | +import co.paralleluniverse.fibers.Suspendable; |
| 4 | +import com.r3.corda.lib.tokens.contracts.states.FungibleToken; |
| 5 | +import com.r3.corda.lib.tokens.contracts.states.NonFungibleToken; |
| 6 | +import com.r3.corda.lib.tokens.contracts.types.TokenPointer; |
| 7 | +import com.r3.corda.lib.tokens.contracts.types.TokenType; |
| 8 | +import com.r3.corda.lib.tokens.selection.database.selector.DatabaseTokenSelection; |
| 9 | +import com.r3.corda.lib.tokens.workflows.flows.move.MoveTokensUtilities; |
| 10 | +import com.r3.corda.lib.tokens.workflows.internal.flows.distribution.UpdateDistributionListFlow; |
| 11 | +import com.r3.corda.lib.tokens.workflows.internal.flows.finality.ObserverAwareFinalityFlow; |
| 12 | +import com.r3.corda.lib.tokens.workflows.internal.flows.finality.ObserverAwareFinalityFlowHandler; |
| 13 | +import com.r3.corda.lib.tokens.workflows.utilities.NotaryUtilities; |
| 14 | +import com.r3.corda.lib.tokens.workflows.utilities.QueryUtilities; |
| 15 | +import kotlin.Pair; |
| 16 | +import net.corda.core.contracts.Amount; |
| 17 | +import net.corda.core.contracts.StateAndRef; |
| 18 | +import net.corda.core.flows.*; |
| 19 | +import net.corda.core.identity.Party; |
| 20 | +import net.corda.core.node.StatesToRecord; |
| 21 | +import net.corda.core.node.services.VaultService; |
| 22 | +import net.corda.core.transactions.SignedTransaction; |
| 23 | +import net.corda.core.transactions.TransactionBuilder; |
| 24 | +import net.corda.examples.spaceships.states.SpaceshipTokenType; |
| 25 | +import org.jetbrains.annotations.NotNull; |
| 26 | + |
| 27 | +import java.util.Collections; |
| 28 | +import java.util.List; |
| 29 | +import java.util.Set; |
| 30 | +import java.util.UUID; |
| 31 | +import java.util.stream.Collectors; |
| 32 | + |
| 33 | +/** |
| 34 | + * The contained flows complete an atomic swap (single transaction exchange between buyer and seller) of a spaceship |
| 35 | + * To achieve this the steps are: |
| 36 | + * 1. Buyer requests the value/sale price of the ship he wants to purchase identified by UUID |
| 37 | + * 2. Seller queries his inventory of ships and grabs the value of the associated UUID and sends to Buyer |
| 38 | + * 3. Buyer checks to see if he has the money to pay - if not throws exception which ends the flow. |
| 39 | + * 4. Seller sends the transaction of ownership (the tx that created his ownership token) and sends it to Buyer |
| 40 | + * 5. Buyer creates transaction with the required currency tokens going to seller, and the ship token going to buyer |
| 41 | + * 6. Transaction is signed and finalised |
| 42 | + */ |
| 43 | +public interface BuySpaceshipFlows { |
| 44 | + |
| 45 | + @InitiatingFlow |
| 46 | + @StartableByRPC |
| 47 | + class BuySpaceshipInitiator extends FlowLogic<SignedTransaction> { |
| 48 | + |
| 49 | + private final String shipId; |
| 50 | + private final Party seller; |
| 51 | + |
| 52 | + public BuySpaceshipInitiator(String shipId, Party seller) { |
| 53 | + this.shipId = shipId; |
| 54 | + this.seller = seller; |
| 55 | + } |
| 56 | + |
| 57 | + @Suspendable |
| 58 | + @Override |
| 59 | + @SuppressWarnings("unchecked") |
| 60 | + public SignedTransaction call() throws FlowException { |
| 61 | + Party notary = NotaryUtilities.getPreferredNotary(getServiceHub()); |
| 62 | + FlowSession sellerSession = initiateFlow(seller); |
| 63 | + VaultService vaultService = getServiceHub().getVaultService(); |
| 64 | + boolean processSale = false; |
| 65 | + Amount<TokenType> paymentAmount; // The amount we wish to pay (in any currency - we will check against conversion rates) |
| 66 | + |
| 67 | + // (STEP 1 - Request Token ship value) see how much the seller's spaceship costs |
| 68 | + Amount<TokenType> shipValue = sellerSession.sendAndReceive(Amount.class, shipId).unwrap(it -> it); |
| 69 | + |
| 70 | + paymentAmount = shipValue; // first we will try to pay in the native currency of the ship's value |
| 71 | + |
| 72 | + // check if we have enough funds to afford this currency and amount |
| 73 | + // tokenBalance returns Amount<TokenType> which represents the quantity of this TokenType (currency) in our vault |
| 74 | + int fundsAvailable = QueryUtilities.tokenBalance(vaultService, shipValue.getToken()).compareTo(paymentAmount); |
| 75 | + |
| 76 | + if (fundsAvailable >= 0) { // we have enough funds and will go through with the purchase |
| 77 | + processSale = true; |
| 78 | + } else { // we do NOT have enough, in THAT tokenType but check if we can pay in some other currency using exchange rate |
| 79 | + |
| 80 | + // creates a set of all tokenTypes we are holding |
| 81 | + Set<TokenType> heldTokenTypes = vaultService.queryBy(FungibleToken.class).getStates().stream() |
| 82 | + .map(it -> it.getState().getData().getTokenType()) |
| 83 | + .collect(Collectors.toSet()); |
| 84 | + heldTokenTypes.remove(shipValue.getToken()); // remove this as it's already been checked |
| 85 | + |
| 86 | + // iterate over all the different TokenTypes (currencies) we hold to see if any have enough value against |
| 87 | + // the listed exchange rate (defined in FlowHelpers interface) |
| 88 | + for (TokenType currentTokenType : heldTokenTypes) { |
| 89 | + paymentAmount = FlowHelpers.exchangeCurrency(shipValue, currentTokenType); |
| 90 | + int funds = QueryUtilities.tokenBalance(vaultService, currentTokenType).compareTo(paymentAmount); |
| 91 | + if (funds >= 0) { |
| 92 | + processSale = true; |
| 93 | + break; |
| 94 | + } |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + if (!processSale) throw new FlowException("Insufficient Funds to Buy the spaceship " + shipId); |
| 99 | + |
| 100 | + /** |
| 101 | + * If the above exception isn't thrown, then we have enough tokens in 'some' currency to complete the sale |
| 102 | + * |
| 103 | + * (Step 2 - receive the transaction which generated the ShipToken and it's associated definition) |
| 104 | + * |
| 105 | + * This token is what the seller holds (the tx proposal will transfer it to the us/buyer) Note: we need the full transaction |
| 106 | + * because it also includes the reference state which the TokenPointer/TokenType refers to - both are required |
| 107 | + * for addMoveNonFungibleTokens when using an EvolvableTokenType. |
| 108 | + */ |
| 109 | + SignedTransaction shipTokenTransaction = subFlow(new ReceiveTransactionFlow(sellerSession)); |
| 110 | + |
| 111 | + /** |
| 112 | + * We record the transaction in OUR vault so that the TransactionBuilder can access it. |
| 113 | + * Important: make sure you have the argument StatesToRecord.ALL_VISIBLE as the default recordTransactions will only |
| 114 | + * record states in the transaction which are RELEVANT (i.e. which we participated in) this is not 'our' transaction |
| 115 | + * so we need to that argument ALL_VISIBLE. |
| 116 | + */ |
| 117 | + getServiceHub().recordTransactions(StatesToRecord.ALL_VISIBLE, Collections.singletonList(shipTokenTransaction)); |
| 118 | + NonFungibleToken shipNFT = shipTokenTransaction.getCoreTransaction().outputsOfType(NonFungibleToken.class).get(0); |
| 119 | + TokenPointer<SpaceshipTokenType> shipTokenPointer = (TokenPointer<SpaceshipTokenType>) shipNFT.getTokenType(); |
| 120 | + |
| 121 | + // Gather the paymentAmount in Tokens on our side (the tx proposal will transfer these to the seller) |
| 122 | + Pair<List<StateAndRef<FungibleToken>>, List<FungibleToken>> selectedTokens = new DatabaseTokenSelection(getServiceHub()) |
| 123 | + // here we are generating input and output states which send the correct amount to the seller, and any change back to buyer |
| 124 | + .generateMove(Collections.singletonList(new Pair<>(seller, paymentAmount)), getOurIdentity()); |
| 125 | + |
| 126 | + // Build the transaction which transfers the curreny tokens AND the spaceship token, in a single transaction |
| 127 | + TransactionBuilder txBuilder = new TransactionBuilder(notary); |
| 128 | + MoveTokensUtilities.addMoveNonFungibleTokens(txBuilder, getServiceHub(), shipTokenPointer, getOurIdentity()); |
| 129 | + MoveTokensUtilities.addMoveTokens(txBuilder, selectedTokens.getFirst(), selectedTokens.getSecond()); |
| 130 | + |
| 131 | + SignedTransaction ptx = getServiceHub().signInitialTransaction(txBuilder); |
| 132 | + SignedTransaction stx = subFlow(new CollectSignaturesFlow(ptx, Collections.singletonList(sellerSession))); |
| 133 | + |
| 134 | + // update the distribution list |
| 135 | + subFlow(new UpdateDistributionListFlow(stx)); |
| 136 | + return subFlow(new ObserverAwareFinalityFlow(stx, Collections.singletonList(sellerSession))); |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + @InitiatedBy(BuySpaceshipInitiator.class) |
| 141 | + class BuySpaceshipResponder extends FlowLogic<Void> { |
| 142 | + private final FlowSession counterpartySession; |
| 143 | + |
| 144 | + public BuySpaceshipResponder(FlowSession counterpartySession) { |
| 145 | + this.counterpartySession = counterpartySession; |
| 146 | + } |
| 147 | + |
| 148 | + @Suspendable |
| 149 | + @Override |
| 150 | + public Void call() throws FlowException { |
| 151 | + // receive request for value of the given shipId |
| 152 | + String shipId = counterpartySession.receive(String.class).unwrap(it -> it); |
| 153 | + UUID shipUUID = UUID.fromString(shipId); |
| 154 | + |
| 155 | +// // Get the state definition from vault to grab the value |
| 156 | + SpaceshipTokenType spaceshipTokenType = FlowHelpers.uuidToSpaceShipTokenType(getServiceHub().getVaultService(), shipUUID); |
| 157 | + |
| 158 | + // (Step 1 - Respond with value) send back value |
| 159 | + counterpartySession.send(spaceshipTokenType.getValue()); |
| 160 | + |
| 161 | + StateAndRef<NonFungibleToken> spaceshipNFTStateAndRef = QueryUtilities.heldTokensByToken(getServiceHub().getVaultService(), spaceshipTokenType.toPointer()) |
| 162 | + .getStates().get(0); |
| 163 | + |
| 164 | + // (Step 2 - Send the corresponding TX representing our ownership so the buyer can build and propose the full transaction) |
| 165 | + SignedTransaction shipTokenTransaction = getServiceHub().getValidatedTransactions().getTransaction(spaceshipNFTStateAndRef.getRef().getTxhash()); |
| 166 | + assert shipTokenTransaction != null; |
| 167 | + subFlow(new SendTransactionFlow(counterpartySession, shipTokenTransaction)); |
| 168 | + |
| 169 | + SignedTransaction stx = subFlow(new SignTransactionFlow(counterpartySession) { |
| 170 | + @Override |
| 171 | + protected void checkTransaction(@NotNull SignedTransaction stx) throws FlowException { |
| 172 | + // No checks for simplicity - but here you can check that you will actually receive |
| 173 | + // the correct amount of tokens before signing (among other possible checks). |
| 174 | + } |
| 175 | + }); |
| 176 | + |
| 177 | + subFlow(new ObserverAwareFinalityFlowHandler(counterpartySession)); |
| 178 | + return null; |
| 179 | + } |
| 180 | + } |
| 181 | +} |
0 commit comments