在不同节点上的帐户之间转移可替代令牌

Transfer fungible token between accounts on different nodes

我对 Corda 很陌生。我想创建一个没有 DVP 过程的像比特币这样的资产转移环境。我将我的代码基于 worldcupticketbooking 示例项目并修改 DVPAccountsHostedOnDifferentNodes class。我计划只发送与任何票无关的令牌。我 运行 代码,创建帐户,并向他们发行资产。但是当我尝试使用此命令在帐户之间转移令牌时

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

我收到以下错误:

✅ 开始

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

✅ 完成

☠ 逆流错误

这是我的代码,因为我无法调试它,所以我没能找到问题所在。谁能帮我看看这是怎么回事?

    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)))
        }
    
    }

我建议您查看 here 上的令牌示例。此示例简单地介绍了创建、发布和传输的步骤。

如果您的设计中没有任何dvp动作。我不建议您查看 worldcupticketbooking 示例,其中 dvp 使流程变得非常复杂。

实际上我建议您查看 tic-tac-thor 示例以熟悉帐户流程逻辑。然后尝试从 worldcupticketbooking 示例中模仿您的设计。

正如所讨论的,使用下面的代码在账户之间移动代币。您无需调用 DVP 流程。一个简单的MoveTokensUtilitiesKt.addMoveTokens应该就够了。

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;
    }
}