Search code examples
kotlintokencordaaccount

Transfer fungible token between accounts on different nodes


I'm so new to Corda. I want to create an asset transfer environment like bitcoin without the DVP process. I based my code to worldcupticketbooking sample project and modify DVPAccountsHostedOnDifferentNodes class. I plan just sending the token without relation to any ticket. I run the code, create accounts, and issue assets to them. But when I try to transfer token between accounts with this command

flow start MoveTokensBetweenAccounts senderAccountName: buyer3, receiverAccountName: buyer1, costOfTicket: 10, currency: USD

I get the error below:

✅ Starting

➡️   Receiving transaction proposal for signing.
🚫   Verifying transaction proposal.
🚫   Signing transaction proposal.

✅ Done

☠ Counter-flow errored

Here is my code, since I cannot debug it I fail to find what the problem is. Can someone help me what is wrong with this?

    InitiatingFlow
    @StartableByRPC
    class MoveTokensBetweenAccounts(private val senderAccountName:String,
    private val receiverAccountName:String,
    private val costOfTicket: Long,
    private val currency: String) : FlowLogic<String>(){
        @Suspendable
        override fun call():String {
    
            //get sender info and account
            val senderInfo = accountService.accountInfo(senderAccountName)[0].state.data
            val senderAcct = subFlow(RequestKeyForAccount(senderInfo))
    
            //get receiver info and account
            val receiverInfo = accountService.accountInfo(receiverAccountName).single().state.data
            val receiverAcct = subFlow(RequestKeyForAccount(receiverInfo))
    
            //sender will create generate a move tokens state and send this state with new holder(seller) to receiver
            val amount = Amount(costOfTicket, getInstance(currency))
    
            val receiverSession = initiateFlow(receiverInfo.host)
    
            //send uuid, buyer,seller account name to seller
            receiverSession.send(senderAccountName)
            receiverSession.send(receiverAccountName)
    
            //sender Query for token balance.
            val queryCriteria = heldTokenAmountCriteria(getInstance(currency), senderAcct).and(sumTokenCriteria())
    
            val sum = serviceHub.vaultService.queryBy(FungibleToken::class.java, queryCriteria).component5()
            if (sum.size == 0) throw FlowException("$senderAccountName has 0 token balance. Please ask the Bank to issue some cash.") else {
                val tokenBalance = sum[0] as Long
                if (tokenBalance < costOfTicket) throw FlowException("Available token balance of $senderAccountName is less than the cost of the ticket. Please ask the Bank to issue some cash if you wish to buy the ticket ")
            }
    
    
            //the tokens to move to new account which is the seller account
            val partyAndAmount:Pair<AbstractParty, Amount<TokenType>> = Pair(receiverAcct, amount)
    
            //let's use the DatabaseTokenSelection to get the tokens from the db
            val tokenSelection = DatabaseTokenSelection(serviceHub, MAX_RETRIES_DEFAULT,
                    RETRY_SLEEP_DEFAULT, RETRY_CAP_DEFAULT, PAGE_SIZE_DEFAULT)
    
            //call generateMove which gives us 2 stateandrefs with tokens having new owner as seller.
            val inputsAndOutputs = tokenSelection
                    .generateMove(Arrays.asList(partyAndAmount), senderAcct, TokenQueryBy(), runId.uuid)
    
            //send the generated inputsAndOutputs to the seller
            subFlow(SendStateAndRefFlow(receiverSession, inputsAndOutputs.first))
            receiverSession.send(inputsAndOutputs.second)
    
            //sync following keys with seller - buyeraccounts, selleraccounts which we generated above using RequestKeyForAccount, and IMP: also share the anonymouse keys
            //created by the above token move method for the holder.
            val signers: MutableList<AbstractParty> = ArrayList()
            signers.add(senderAcct)
            signers.add(receiverAcct)
    
            val inputs = inputsAndOutputs.first
            for ((state) in inputs) {
                signers.add(state.data.holder)
            }
    
            //Sync our associated keys with the conterparties.
            subFlow(SyncKeyMappingFlow(receiverSession, signers))
    
            //this is the handler for synckeymapping called by seller. seller must also have created some keys not known to us - buyer
            subFlow(SyncKeyMappingFlowHandler(receiverSession))
    
            //recieve the data from counter session in tx formatt.
            subFlow(object : SignTransactionFlow(receiverSession) {
                @Throws(FlowException::class)
                override fun checkTransaction(stx: SignedTransaction) {
                    // Custom Logic to validate transaction.
                }
            })
    
            val stx = subFlow(ReceiveFinalityFlow(receiverSession))
    
            return ("The ticket is sold to $receiverAccountName"+ "\ntxID: "+stx.id)
        }
    }
    
    @InitiatedBy(MoveTokensBetweenAccounts::class)
    class MoveTokensBetweenAccountsResponder(val otherSide: FlowSession) : FlowLogic<SignedTransaction>() {
        @Suspendable
        override fun call():SignedTransaction {
            //get all the details from the seller
            //val tokenId: String = otherSide.receive(String::class.java).unwrap { it }
            val senderAccountName: String = otherSide.receive(String::class.java).unwrap { it }
            val receiverAccountName: String = otherSide.receive(String::class.java).unwrap{ it }
    
            val inputs = subFlow(ReceiveStateAndRefFlow<FungibleToken>(otherSide))
            val moneyReceived: List<FungibleToken> = otherSide.receive(List::class.java).unwrap{ it } as List<FungibleToken>
    
            //call SyncKeyMappingHandler for SyncKey Mapping called at buyers side
            subFlow(SyncKeyMappingFlowHandler(otherSide))
    
            //Get buyers and sellers account infos
            val senderAccountInfo = accountService.accountInfo(senderAccountName)[0].state.data
            val receiverAccountInfo = accountService.accountInfo(receiverAccountName)[0].state.data
    
            //Generate new keys for buyers and sellers
            val senderAccount = subFlow(RequestKeyForAccount(senderAccountInfo))
            val receiverAccount = subFlow(RequestKeyForAccount(receiverAccountInfo))
    
            //building transaction
            val notary = serviceHub.networkMapCache.notaryIdentities[0]
            val txBuilder = TransactionBuilder(notary)
    
            //part2 of DVP is to transfer cash - fungible token from buyer to seller and return the change to buyer
            addMoveTokens(txBuilder, inputs, moneyReceived)
    
            //add signers
            val signers: MutableList<AbstractParty> = ArrayList()
            signers.add(senderAccount)
            signers.add(receiverAccount)
    
            for ((state) in inputs) {
                signers.add(state.data.holder)
            }
    
            //sync keys with buyer, again sync for similar members
            subFlow(SyncKeyMappingFlow(otherSide, signers))
    
            //call filterMyKeys to get the my signers for seller node and pass in as a 4th parameter to CollectSignaturesFlow.
            //by doing this we tell CollectSignaturesFlow that these are the signers which have already signed the transaction
            val commandWithPartiesList: List<CommandWithParties<CommandData>> = txBuilder.toLedgerTransaction(serviceHub).commands
    
            val mySigners: MutableList<PublicKey> = ArrayList()
            commandWithPartiesList.map {
                val signer = (serviceHub.keyManagementService.filterMyKeys(it.signers) as ArrayList<PublicKey>)
                if(signer.size >0){
                    mySigners.add(signer[0]) }
            }
    
            val selfSignedTransaction = serviceHub.signInitialTransaction(txBuilder, mySigners)
            val fullySignedTx = subFlow(CollectSignaturesFlow(selfSignedTransaction, listOf(otherSide), mySigners))
    
            //call FinalityFlow for finality
            return subFlow(FinalityFlow(fullySignedTx, Arrays.asList(otherSide)))
        }
    
    }

Solution

  • As discussed use the below code to move tokens between Accounts. You do not need to call the DVP flow. A simple MoveTokensUtilitiesKt.addMoveTokens should be enough.

    package com.t20worldcup.flows;
    import co.paralleluniverse.fibers.Suspendable;
    import com.r3.corda.lib.accounts.contracts.states.AccountInfo;
    import com.r3.corda.lib.accounts.workflows.UtilitiesKt;
    import com.r3.corda.lib.accounts.workflows.flows.RequestKeyForAccount;
    import com.r3.corda.lib.tokens.contracts.states.FungibleToken;
    import com.r3.corda.lib.tokens.contracts.types.TokenType;
    import com.r3.corda.lib.tokens.selection.TokenQueryBy;
    import com.r3.corda.lib.tokens.selection.database.config.DatabaseSelectionConfigKt;
    import com.r3.corda.lib.tokens.selection.database.selector.DatabaseTokenSelection;
    import com.r3.corda.lib.tokens.workflows.flows.move.MoveTokensUtilitiesKt;
    import com.r3.corda.lib.tokens.workflows.utilities.QueryUtilitiesKt;
    import kotlin.Pair;
    import net.corda.core.contracts.Amount;
    import net.corda.core.contracts.CommandData;
    import net.corda.core.contracts.CommandWithParties;
    import net.corda.core.contracts.StateAndRef;
    import net.corda.core.flows.*;
    import net.corda.core.identity.AbstractParty;
    import net.corda.core.identity.AnonymousParty;
    import net.corda.core.identity.Party;
    import net.corda.core.node.services.vault.QueryCriteria;
    import net.corda.core.transactions.SignedTransaction;
    import net.corda.core.transactions.TransactionBuilder;
    import java.security.PublicKey;
    import java.util.*;
    @StartableByRPC
    @InitiatingFlow
    public class MoveTokensBetweenAccounts extends FlowLogic<String> {
        private final String buyerAccountName;
        private final String sellerAccountName;
        private final String currency;
        private final Long costOfTicket;
        public MoveTokensBetweenAccounts(String buyerAccountName, String sellerAccountName, String currency, Long costOfTicket) {
            this.buyerAccountName = buyerAccountName;
            this.sellerAccountName = sellerAccountName;
            this.currency = currency;
            this.costOfTicket = costOfTicket;
        }
        @Override
        @Suspendable
        public String call() throws FlowException {
            //Get buyers and sellers account infos
            AccountInfo buyerAccountInfo = UtilitiesKt.getAccountService(this).accountInfo(buyerAccountName).get(0).getState().getData();
            AccountInfo sellerAccountInfo = UtilitiesKt.getAccountService(this).accountInfo(sellerAccountName).get(0).getState().getData();
            //Generate new keys for buyers and sellers
            //make sure to sync these keys with the counterparty by calling SyncKeyMappingFlow as below
            AnonymousParty buyerAccount = subFlow(new RequestKeyForAccount(buyerAccountInfo));//mappng saved locally
            AnonymousParty sellerAccount = subFlow(new RequestKeyForAccount(sellerAccountInfo));//mappiing requested from counterparty. does the counterparty save i dont think so
            //buyer will create generate a move tokens state and send this state with new holder(seller) to seller
            Amount<TokenType> amount = new Amount(costOfTicket, getInstance(currency));
            //Buyer Query for token balance.
            QueryCriteria queryCriteria = QueryUtilitiesKt.heldTokenAmountCriteria(this.getInstance(currency), buyerAccount).and(QueryUtilitiesKt.sumTokenCriteria());
            List<Object> sum = getServiceHub().getVaultService().queryBy(FungibleToken.class, queryCriteria).component5();
            if(sum.size() == 0)
                throw new FlowException(buyerAccountName + " has 0 token balance. Please ask the Bank to issue some cash.");
            else {
                Long tokenBalance = (Long) sum.get(0);
                if(tokenBalance < costOfTicket)
                    throw new FlowException("Available token balance of " + buyerAccountName+ " is less than the cost of the ticket. Please ask the Bank to issue some cash if you wish to buy the ticket ");
            }
            //the tokens to move to new account which is the seller account
            Pair<AbstractParty, Amount<TokenType>> partyAndAmount = new Pair(sellerAccount, amount);
            //let's use the DatabaseTokenSelection to get the tokens from the db
            DatabaseTokenSelection tokenSelection = new DatabaseTokenSelection(
                    getServiceHub(),
                    DatabaseSelectionConfigKt.MAX_RETRIES_DEFAULT,
                    DatabaseSelectionConfigKt.RETRY_SLEEP_DEFAULT,
                    DatabaseSelectionConfigKt.RETRY_CAP_DEFAULT,
                    DatabaseSelectionConfigKt.PAGE_SIZE_DEFAULT
            );
            //call generateMove which gives us 2 stateandrefs with tokens having new owner as seller.
            Pair<List<StateAndRef<FungibleToken>>, List<FungibleToken>> inputsAndOutputs =
                    tokenSelection.generateMove(Arrays.asList(partyAndAmount), buyerAccount, new TokenQueryBy(), getRunId().getUuid());
            Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
            TransactionBuilder transactionBuilder = new TransactionBuilder(notary);
            MoveTokensUtilitiesKt.addMoveTokens(transactionBuilder, inputsAndOutputs.getFirst(), inputsAndOutputs.getSecond());
            Set<PublicKey> mySigners = new HashSet<>();
            List<CommandWithParties<CommandData>> commandWithPartiesList  = transactionBuilder.toLedgerTransaction(getServiceHub()).getCommands();
            for(CommandWithParties<CommandData> commandDataCommandWithParties : commandWithPartiesList) {
                if(((ArrayList<PublicKey>)(getServiceHub().getKeyManagementService().filterMyKeys(commandDataCommandWithParties.getSigners()))).size() > 0) {
                    mySigners.add(((ArrayList<PublicKey>)getServiceHub().getKeyManagementService().filterMyKeys(commandDataCommandWithParties.getSigners())).get(0));
                }
            }
            FlowSession sellerSession = initiateFlow(sellerAccountInfo.getHost());
           //sign the transaction with the signers we got by calling filterMyKeys
            SignedTransaction selfSignedTransaction = getServiceHub().signInitialTransaction(transactionBuilder, mySigners);
            //call FinalityFlow for finality
            subFlow(new FinalityFlow(selfSignedTransaction, Arrays.asList(sellerSession)));
            return null;
        }
        public TokenType getInstance(String currencyCode) {
            Currency currency = Currency.getInstance(currencyCode);
            return new TokenType(currency.getCurrencyCode(), 0);
        }
    }
    @InitiatedBy(MoveTokensBetweenAccounts.class)
    class MoveTokensBetweenAccountsResponder extends FlowLogic<Void> {
        private final FlowSession otherSide;
        public MoveTokensBetweenAccountsResponder(FlowSession otherSide) {
            this.otherSide = otherSide;
        }
        @Override
        @Suspendable
        public Void call() throws FlowException {
            subFlow(new ReceiveFinalityFlow(otherSide));
            return null;
        }
    }