R3 CordaのToken SDKを利用したDvP(Delivery versus Payment)


【本文】
R3 Cordaでは、Token SDKと呼ばれる証券をトークン化する際などにデザインしやすくなるようなライブラリがある。
Token SDKを用いたトークンの実装方法自体は割合webで簡単に見つかるので、今回は、実際に発行したトークンと通貨との交換をatomicに同時実行するためのDvP(Delivery versus Payment)の実装事例について、サンプルコードを紹介。

【実際のコード】

CordaでDvPを実現するためには、取引同士の2者間での複数回のやり取りが発生する。その為、多少は複雑なロジックとなる。
(取引同士のやり取りは、sessionを確立し、subflowを通じて実行される。)

DvPfromSTtoPT.kt
package com.template.flows

//import net.corda.core.flows.IdentitySyncFlow
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.*
import com.r3.corda.lib.tokens.contracts.states.EvolvableTokenType
import com.r3.corda.lib.tokens.contracts.states.FungibleToken
import com.r3.corda.lib.tokens.contracts.types.TokenType
import com.r3.corda.lib.tokens.contracts.utilities.of
import com.r3.corda.lib.tokens.money.FiatCurrency
import com.r3.corda.lib.tokens.workflows.flows.move.addMoveFungibleTokens
import com.r3.corda.lib.tokens.workflows.flows.move.addMoveTokens
import com.r3.corda.lib.tokens.workflows.internal.flows.distribution.UpdateDistributionListFlow
import com.r3.corda.lib.tokens.workflows.internal.flows.finality.ObserverAwareFinalityFlow
import com.r3.corda.lib.tokens.workflows.internal.flows.finality.ObserverAwareFinalityFlowHandler
import com.r3.corda.lib.tokens.workflows.internal.selection.TokenSelection
import com.r3.corda.lib.tokens.workflows.types.PartyAndAmount
import com.r3.corda.lib.tokens.workflows.utilities.getPreferredNotary
import com.r3.corda.lib.tokens.workflows.utilities.ourSigningKeys
import com.r3.corda.lib.tokens.workflows.utilities.toParty
import com.r3.corda.lib.tokens.workflows.utilities.tokenBalance
import com.template.states.TestTokenType
import net.corda.core.contracts.Amount
import net.corda.core.flows.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.unwrap
import java.io.File
import java.io.IOException
import java.lang.Object
import java.nio.*
import java.nio.file.Files.readAllLines
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
import java.util.UUID

import java.util.Date
import java.text.SimpleDateFormat

import kotlin.collections.List


/**
 *  DvP from Security Token to Fiatcurrency(payment token)
 */
@StartableByRPC
@InitiatingFlow
class DvPfromSTtoPTInitiateFlow(

) : FlowLogic<SignedTransaction>() {
    @Suspendable
    override fun call(): SignedTransaction {

        // read input file
        val inputfile: String = "<input file Path>"
        val text = mutableListOf<String>()
        File(inputfile).useLines { lines -> text.addAll(lines) }

        val eid = text[0]
        val quantityByST = text[1].toLong()
        val recipient = serviceHub.identityService.wellKnownPartyFromX500Name(CordaX500Name.parse(text[2]))!!.toParty(serviceHub)
        val currency = text[3]
        val quantityByPT = text[4].toLong()
        val exeDvPDateStr = text[5]

        val uuid = UUID.fromString(eid)
        val queryCriteria = QueryCriteria.LinearStateQueryCriteria(uuid = listOf(uuid))
        val tokenStateAndRef = serviceHub.vaultService.queryBy<EvolvableTokenType>(queryCriteria).states.single()
        val token = tokenStateAndRef.state.data.toPointer<EvolvableTokenType>()

        val evolvableTokenType: TestTokenType = tokenStateAndRef.state.data as TestTokenType

        // check redemptionDate
        val redemptionDate = SimpleDateFormat("yyyy/MM/dd").let{
            val parsed = it.parse(evolvableTokenType.redemptionDate)
            it.format(parsed)
        }

        println("redemptionDate : " + redemptionDate)

        val exeDvPDate = SimpleDateFormat("yyyy/MM/dd").let{
            val parsed = it.parse(exeDvPDateStr)
            it.format(parsed)
        }

        println("exeDvPDate : " + exeDvPDate)

        if (redemptionDate <= exeDvPDate) {
            val errorMsgForRedemptionDateOver = "DvP can not be done, because RedemptionDate has already came."
            println(errorMsgForRedemptionDateOver)
            throw IllegalArgumentException(errorMsgForRedemptionDateOver)
        }

        // checking sufficient amount for moving to Security Token
        val balance = serviceHub.vaultService.tokenBalance(token = token)
        val holdingSTQuantity = balance.quantity
        println("Payer holding Security Token : " + holdingSTQuantity + " of " + balance.token.tokenIdentifier)
        println("Payer should move to Recipient( " + recipient + " ) : " + quantityByST + " of " + balance.token.tokenIdentifier)

        if (quantityByST > holdingSTQuantity) {
            val errorMsgForInsufficientST = "Payer do not hold sufficient security token to recipient."
            println(errorMsgForInsufficientST)
            throw IllegalArgumentException(errorMsgForInsufficientST)
        }

        val amount =  quantityByST of token

        val notary = serviceHub.networkMapCache.notaryIdentities.first()
        val txBuilder = TransactionBuilder(notary)

        addMoveFungibleTokens(txBuilder, serviceHub, listOf(PartyAndAmount( recipient, amount)),ourIdentity)

        // Initiate new flow session. If this flow is supposed to be called as inline flow, then session should have been already passed.
        val session = initiateFlow(recipient)
        // Ask for input stateAndRefs - send notification with the amount to exchange.
        session.send(Trade(currency,quantityByPT))

        val inputs = subFlow(ReceiveStateAndRefFlow<FungibleToken>(session))
        // Receive outputs.
        val outputs = session.receive<List<FungibleToken>>().unwrap { it }

        addMoveTokens(txBuilder, inputs, outputs)

        // Because states on the transaction can have confidential identities on them, we need to sign them with corresponding keys.
        val ourSigningKeys = txBuilder.toLedgerTransaction(serviceHub).ourSigningKeys(serviceHub)
        val initialStx = serviceHub.signInitialTransaction(txBuilder, signingPubKeys = ourSigningKeys)
        // Collect signatures from trading counterpart.
        val stx = subFlow(CollectSignaturesFlow(initialStx, listOf(session), ourSigningKeys))

        // Update distribution list.
        subFlow(UpdateDistributionListFlow(stx))
        // Finalize transaction! If you want to have observers notified, you can pass optional observers sessions.
        val ftx = subFlow(ObserverAwareFinalityFlow(stx, listOf(session)))

        return ftx
    }

    @InitiatedBy(DvPfromSTtoPTInitiateFlow::class)
    class DvPfromSTtoPTFlowHandler(val otherSession: FlowSession) : FlowLogic<Unit>() {
        @Suspendable
        override fun call() {
            // Receive Trade.
            val trdata = otherSession.receive<Trade>().unwrap { it }
            val currency = trdata.currency
            val quantityByPT = trdata.quantityByPT

            val paymentToken = FiatCurrency.Companion.getInstance(currency)
            val amount = Amount(quantityByPT,paymentToken)

            // checking sufficient amount for paying to Payment Token
            val balance = serviceHub.vaultService.tokenBalance(token = paymentToken)
            val holdingPTQuantity = balance.quantity
            println("Payer holding Payment Token : " + holdingPTQuantity + " of " + balance.token.tokenIdentifier)
            println("Payer should settle to Recipient( " + otherSession.counterparty + " ) : " + quantityByPT + " of " + balance.token.tokenIdentifier)

            if (quantityByPT > holdingPTQuantity) {
                val errorMsgForInsufficientPT = "Payer do not hold sufficient payment token to recipient."
                println(errorMsgForInsufficientPT)
                throw IllegalArgumentException(errorMsgForInsufficientPT)
            }

            val changeHolder = ourIdentity

            val (inputs, outputs) = TokenSelection(serviceHub).generateMove(
                    lockId = runId.uuid,
                    partyAndAmounts = listOf(PartyAndAmount(otherSession.counterparty,amount)),
                    changeHolder = changeHolder
            )
            subFlow(SendStateAndRefFlow(otherSession, inputs))
            otherSession.send(outputs)

            subFlow(object : SignTransactionFlow(otherSession) {
                override fun checkTransaction(stx: SignedTransaction) {

                }
            })

            subFlow(ObserverAwareFinalityFlowHandler(otherSession))
        }
    }
}

@CordaSerializable
data class Trade(val currency: String,val quantityByPT: Long)

DvPfromSTtoPTのflowに関する簡単な説明
注)DvPに直接かかわる以外の説明は割愛。

  • Sec-Bよりトランザクションの型を作成し、Sec-Bより移転するトークンと取引相手(Sec-A)をトークン移転コマンドに定義
        val notary = serviceHub.networkMapCache.notaryIdentities.first()
        val txBuilder = TransactionBuilder(notary)

        addMoveFungibleTokens(txBuilder, serviceHub, listOf(PartyAndAmount( recipient, amount)),ourIdentity)
  • セッション相手のSec-Aに、Sec-AよりSec-Bに移転する為のJPYに関する情報をラッピングし、Sec-Aに送信。
        // Initiate new flow session. If this flow is supposed to be called as inline flow, then session should have been already passed.
        val session = initiateFlow(recipient)
        // Ask for input stateAndRefs - send notification with the amount to exchange.
        session.send(Trade(currency,quantityByPT))

-- (省略) --

@CordaSerializable
data class Trade(val currency: String,val quantityByPT: Long)
  • Sec-Aは、Sec-Bから送信された情報を基に、inputとoutputを作成し、Sec-Bに送信。
val (inputs, outputs) = TokenSelection(serviceHub).generateMove(
                    lockId = runId.uuid,
                    partyAndAmounts = listOf(PartyAndAmount(otherSession.counterparty,amount)),
                    changeHolder = changeHolder
            )
            subFlow(SendStateAndRefFlow(otherSession, inputs))
            otherSession.send(outputs)
  • Sec-Bにて、Sec-Aより送信されたinputとoutputをJPY移転コマンドに定義。
        val inputs = subFlow(ReceiveStateAndRefFlow<FungibleToken>(session))
        // Receive outputs.
        val outputs = session.receive<List<FungibleToken>>().unwrap { it }

        addMoveTokens(txBuilder, inputs, outputs)
  • Sec-Bよりトランザクションに署名しimutableなものにした後、Sec-Aにトランザクションを提案し、Sec-Aより署名を集める。
     val ourSigningKeys = txBuilder.toLedgerTransaction(serviceHub).ourSigningKeys(serviceHub)
        val initialStx = serviceHub.signInitialTransaction(txBuilder, signingPubKeys = ourSigningKeys)
        // Collect signatures from trading counterpart.
        val stx = subFlow(CollectSignaturesFlow(initialStx, listOf(session), ourSigningKeys))
  • Sec-AはSec-Bよりトランザクションを受け取り確認後、トランザクションに署名し、Sec-Bに再送信
            subFlow(object : SignTransactionFlow(otherSession) {
                override fun checkTransaction(stx: SignedTransaction) {

                }
  • Sec-Bはトークンの所有一覧を更新し、トランザクションをfinalizeする。
        // Update distribution list.
        subFlow(UpdateDistributionListFlow(stx))
        // Finalise transaction! If you want to have observers notified, you can pass optional observers sessions.
        val ftx = subFlow(ObserverAwareFinalityFlow(stx, listOf(session)))

        return ftx

DvPSTfromPTの実行結果

Sec-Bの実行結果として、printlnで出力したメッセージが以下。

実行結果(Sec-B)
Thu Oct 29 15:19:05 JST 2020>>> start DvPfromSTtoPTInitiateFlow
Starting
redemptionDate : 2021/01/15
exeDvPDate : 2021/01/13
Payer holding Security Token : 2000000 of 08d69270-1c30-49c2-abfe-73e9696e49a9
Payer should move to Recipient( O=Sec-A, L=Tokyo, C=JP ) : 50000 of 08d69270-1c30-49c2-abfe-73e9696e49a9
Collecting signatures from counterparties.
Verifying collected signatures.
Starting
Updating distribution list.
Starting
Done
Flow completed with result: SignedTransaction(id=FE596581A2A3E081045B60E4420AE32849C0AEAC599F5BDA784871279F6F9D97)

Sec-Aの実行結果として、printlnで出力したメッセージが以下。

実行結果(Sec-A)
Thu Oct 29 15:19:34 JST 2020>>> Payer holding Payment Token : 100000000 of JPY
Payer should settle to Recipient( O=Sec-B, L=Tokyo, C=JP ) : 10000 of JPY
  • (自作の)トランザクション確認ロジック(詳細は補足を参照)を利用してTxhashをキーにDvPのトランザクションを確認。
  • CordaはBitcoinと同じくUTXO型のため、一つのinputから複数のoutputに分割される。 (簡単に言うと、500円の品物を購入するために、10,000円札を出すと、500円はお店に渡り、おつりの9,500円分が自分に戻ってくる。この時の500円と9,500円が次のinputとなる。)
トランザクション確認
Thu Oct 29 15:30:10 JST 2020>>> start GetTx txhash: 'FE596581A2A3E081045B60E4420AE32849C0AEAC599F5BDA784871279F6F9D97'
Starting
Done
Flow completed with result: LedgerTransaction(
    id=FE596581A2A3E081045B60E4420AE32849C0AEAC599F5BDA784871279F6F9D97
    inputs=[StateAndRef(state=TransactionState(data=2000000 TokenPointer(class com.r3.corda.lib.tokens.contracts.states.EvolvableTokenType, 08d69270-1c30-49c2-abfe-73e9696e49a9) issued by Sec-A held by Sec-B, contract=com.r3.corda.lib.tokens.contracts.FungibleTokenContract, notary=O=Notary, L=Tokyo, C=JP, encumbrance=null, constraint=SignatureAttachmentConstraint(key=EC Public Key [4b:44:7c:7b:1b:38:a6:93:bd:9a:c4:1d:8b:46:b4:6b:77:ba:f8:0e]
            X: 38d226dcd0fa574316da478aa75225e6ce18f65cbd96e60bf3c8251b1965417
            Y: 56e5dcf7ccab21b712601ed0278501f2f33d0b5fdaa4c09e62639464e4910871
)), ref=1A28040CDA859E72655EB46C836D93116F43F7EA08AE1A3AFEF079227D54E369(0)), StateAndRef(state=TransactionState(data=100000000 TokenType(tokenIdentifier='JPY', fractionDigits=0) issued by Sec-A held by Sec-A, contract=com.r3.corda.lib.tokens.contracts.FungibleTokenContract, notary=O=Notary, L=Tokyo, C=JP, encumbrance=null, constraint=SignatureAttachmentConstraint(key=EC Public Key [4b:44:7c:7b:1b:38:a6:93:bd:9a:c4:1d:8b:46:b4:6b:77:ba:f8:0e]
            X: 38d226dcd0fa574316da478aa75225e6ce18f65cbd96e60bf3c8251b1965417
            Y: 56e5dcf7ccab21b712601ed0278501f2f33d0b5fdaa4c09e62639464e4910871
)), ref=E7D376BB8A69F241068BD5120025B76EBFB355904FCE84DD221CB7EFE0F0C6F7(0))]
    outputs=[TransactionState(data=50000 TokenPointer(class com.r3.corda.lib.tokens.contracts.states.EvolvableTokenType, 08d69270-1c30-49c2-abfe-73e9696e49a9) issued by Sec-A held by Sec-A, contract=com.r3.corda.lib.tokens.contracts.FungibleTokenContract, notary=O=Notary, L=Tokyo, C=JP, encumbrance=null, constraint=SignatureAttachmentConstraint(key=EC Public Key [4b:44:7c:7b:1b:38:a6:93:bd:9a:c4:1d:8b:46:b4:6b:77:ba:f8:0e]
            X: 38d226dcd0fa574316da478aa75225e6ce18f65cbd96e60bf3c8251b1965417
            Y: 56e5dcf7ccab21b712601ed0278501f2f33d0b5fdaa4c09e62639464e4910871
)), TransactionState(data=1950000 TokenPointer(class com.r3.corda.lib.tokens.contracts.states.EvolvableTokenType, 08d69270-1c30-49c2-abfe-73e9696e49a9) issued by Sec-A held by Sec-B, contract=com.r3.corda.lib.tokens.contracts.FungibleTokenContract, notary=O=Notary, L=Tokyo, C=JP, encumbrance=null, constraint=SignatureAttachmentConstraint(key=EC Public Key [4b:44:7c:7b:1b:38:a6:93:bd:9a:c4:1d:8b:46:b4:6b:77:ba:f8:0e]
            X: 38d226dcd0fa574316da478aa75225e6ce18f65cbd96e60bf3c8251b1965417
            Y: 56e5dcf7ccab21b712601ed0278501f2f33d0b5fdaa4c09e62639464e4910871
)), TransactionState(data=10000 TokenType(tokenIdentifier='JPY', fractionDigits=0) issued by Sec-A held by Sec-B, contract=com.r3.corda.lib.tokens.contracts.FungibleTokenContract, notary=O=Notary, L=Tokyo, C=JP, encumbrance=null, constraint=SignatureAttachmentConstraint(key=EC Public Key [4b:44:7c:7b:1b:38:a6:93:bd:9a:c4:1d:8b:46:b4:6b:77:ba:f8:0e]
            X: 38d226dcd0fa574316da478aa75225e6ce18f65cbd96e60bf3c8251b1965417
            Y: 56e5dcf7ccab21b712601ed0278501f2f33d0b5fdaa4c09e62639464e4910871
)), TransactionState(data=99990000 TokenType(tokenIdentifier='JPY', fractionDigits=0) issued by Sec-A held by Sec-A, contract=com.r3.corda.lib.tokens.contracts.FungibleTokenContract, notary=O=Notary, L=Tokyo, C=JP, encumbrance=null, constraint=SignatureAttachmentConstraint(key=EC Public Key [4b:44:7c:7b:1b:38:a6:93:bd:9a:c4:1d:8b:46:b4:6b:77:ba:f8:0e]
            X: 38d226dcd0fa574316da478aa75225e6ce18f65cbd96e60bf3c8251b1965417
            Y: 56e5dcf7ccab21b712601ed0278501f2f33d0b5fdaa4c09e62639464e4910871
))]
    commands=[CommandWithParties(signers=[net.i2p.crypto.eddsa.EdDSAPublicKey@d357ae0c], signingParties=[O=Sec-B, L=Tokyo, C=JP], value=com.r3.corda.lib.tokens.contracts.commands.MoveTokenCommand(token=TokenPointer(class com.r3.corda.lib.tokens.contracts.states.EvolvableTokenType, 08d69270-1c30-49c2-abfe-73e9696e49a9) issued by Sec-A, inputIndicies=[0], outputIndicies=[0, 1])), CommandWithParties(signers=[net.i2p.crypto.eddsa.EdDSAPublicKey@5e1468a7], signingParties=[O=Sec-A, L=Tokyo, C=JP], value=com.r3.corda.lib.tokens.contracts.commands.MoveTokenCommand(token=TokenType(tokenIdentifier='JPY', fractionDigits=0) issued by Sec-A, inputIndicies=[1], outputIndicies=[2, 3]))]
    attachments=[ContractAttachment(attachment=1C389ED9F4ADB894DBE4B874EE2F852DD682F37775C18F4F4D6CACC5019D8877, contracts='[com.template.TestTokenTypeContract, com.template.ExampleEvolvableTokenTypeContract]', uploader='app', signed='true', version='1'), ContractAttachment(attachment=60DEB9502AADDB16BB6A821F29F5692DC8BCDCCC2134F9D90C4A8BF71669EBA6, contracts='[com.r3.corda.lib.tokens.contracts.NonFungibleTokenContract, com.r3.corda.lib.tokens.contracts.FungibleTokenContract]', uploader='app', signed='true', version='1')]
    notary=O=Notary, L=Tokyo, C=JP
    timeWindow=null
    references=[StateAndRef(state=TransactionState(data=com.template.states.TestTokenType@b4abab, contract=com.template.TestTokenTypeContract, notary=O=Notary, L=Tokyo, C=JP, encumbrance=null, constraint=SignatureAttachmentConstraint(key=EC Public Key [4b:44:7c:7b:1b:38:a6:93:bd:9a:c4:1d:8b:46:b4:6b:77:ba:f8:0e]
            X: 38d226dcd0fa574316da478aa75225e6ce18f65cbd96e60bf3c8251b1965417
            Y: 56e5dcf7ccab21b712601ed0278501f2f33d0b5fdaa4c09e62639464e4910871
)), ref=E4465EC97A376BF203FD49FE7F2C6FF331867F443C0E8E248CEDE0D7A6598D3F(0))]
    networkParameters=NetworkParameters {
      minimumPlatformVersion=5
      notaries=[NotaryInfo(identity=O=Notary, L=Tokyo, C=JP, validating=false)]
      maxMessageSize=10485760
      maxTransactionSize=524288000
      whitelistedContractImplementations {
        com.r3.corda.lib.accounts.contracts.AccountInfoContract=[1CC4F4FC05F53987F70A76E8D2D9129474BCAAD1E7D4A011D115684FC7C1A83C]
      }
      eventHorizon=PT720H
      packageOwnership {

      }
      modifiedTime=2020-10-28T02:40:27.709Z
      epoch=1
  }
    privacySalt=[F855C6E62E83F714B46ECD659E3F05AA34A6E5363E3CB659929AD679D045CF23]
)

  • トランザクション内のinputを見ると、Sec-Bが保有する2,000,000分のトークン(08d69270-1c30-49c2-abfe-73e9696e49a9)のUTXOがセットされ、Sec-Aが保有する100,000,000分のJPY(日本円)がセットされている。
inputs
inputs=[StateAndRef(state=TransactionState(data=2000000 TokenPointer(class com.r3.corda.lib.tokens.contracts.states.EvolvableTokenType, 08d69270-1c30-49c2-abfe-73e9696e49a9) issued by Sec-A held by Sec-B, contract=com.r3.corda.lib.tokens.contracts.FungibleTokenContract, notary=O=Notary, L=Tokyo, C=JP, encumbrance=null, constraint=SignatureAttachmentConstraint(key=EC Public Key [4b:44:7c:7b:1b:38:a6:93:bd:9a:c4:1d:8b:46:b4:6b:77:ba:f8:0e]
            X: 38d226dcd0fa574316da478aa75225e6ce18f65cbd96e60bf3c8251b1965417
            Y: 56e5dcf7ccab21b712601ed0278501f2f33d0b5fdaa4c09e62639464e4910871
)), ref=1A28040CDA859E72655EB46C836D93116F43F7EA08AE1A3AFEF079227D54E369(0)), StateAndRef(state=TransactionState(data=100000000 TokenType(tokenIdentifier='JPY', fractionDigits=0) issued by Sec-A held by Sec-A, contract=com.r3.corda.lib.tokens.contracts.FungibleTokenContract, notary=O=Notary, L=Tokyo, C=JP, encumbrance=null, constraint=SignatureAttachmentConstraint(key=EC Public Key [4b:44:7c:7b:1b:38:a6:93:bd:9a:c4:1d:8b:46:b4:6b:77:ba:f8:0e]
            X: 38d226dcd0fa574316da478aa75225e6ce18f65cbd96e60bf3c8251b1965417
            Y: 56e5dcf7ccab21b712601ed0278501f2f33d0b5fdaa4c09e62639464e4910871
)), ref=E7D376BB8A69F241068BD5120025B76EBFB355904FCE84DD221CB7EFE0F0C6F7(0))]
  • outputsでは、DvP実行の結果、JPY:10,000分のUTXOがSec-AからSec-Bに移転し、JPY:99,990,000分のUTXOがSec-Aに戻ってきており、一方、トークン:50,000分のUTXOがSec-BからSec-Aに移転し、トークン:1,950,000分のUTXOがSec-Bに戻ってきていることが分かる。
outputs
outputs=[TransactionState(data=50000 TokenPointer(class com.r3.corda.lib.tokens.contracts.states.EvolvableTokenType, 08d69270-1c30-49c2-abfe-73e9696e49a9) issued by Sec-A held by Sec-A, contract=com.r3.corda.lib.tokens.contracts.FungibleTokenContract, notary=O=Notary, L=Tokyo, C=JP, encumbrance=null, constraint=SignatureAttachmentConstraint(key=EC Public Key [4b:44:7c:7b:1b:38:a6:93:bd:9a:c4:1d:8b:46:b4:6b:77:ba:f8:0e]
            X: 38d226dcd0fa574316da478aa75225e6ce18f65cbd96e60bf3c8251b1965417
            Y: 56e5dcf7ccab21b712601ed0278501f2f33d0b5fdaa4c09e62639464e4910871
)), TransactionState(data=1950000 TokenPointer(class com.r3.corda.lib.tokens.contracts.states.EvolvableTokenType, 08d69270-1c30-49c2-abfe-73e9696e49a9) issued by Sec-A held by Sec-B, contract=com.r3.corda.lib.tokens.contracts.FungibleTokenContract, notary=O=Notary, L=Tokyo, C=JP, encumbrance=null, constraint=SignatureAttachmentConstraint(key=EC Public Key [4b:44:7c:7b:1b:38:a6:93:bd:9a:c4:1d:8b:46:b4:6b:77:ba:f8:0e]
            X: 38d226dcd0fa574316da478aa75225e6ce18f65cbd96e60bf3c8251b1965417
            Y: 56e5dcf7ccab21b712601ed0278501f2f33d0b5fdaa4c09e62639464e4910871
)), TransactionState(data=10000 TokenType(tokenIdentifier='JPY', fractionDigits=0) issued by Sec-A held by Sec-B, contract=com.r3.corda.lib.tokens.contracts.FungibleTokenContract, notary=O=Notary, L=Tokyo, C=JP, encumbrance=null, constraint=SignatureAttachmentConstraint(key=EC Public Key [4b:44:7c:7b:1b:38:a6:93:bd:9a:c4:1d:8b:46:b4:6b:77:ba:f8:0e]
            X: 38d226dcd0fa574316da478aa75225e6ce18f65cbd96e60bf3c8251b1965417
            Y: 56e5dcf7ccab21b712601ed0278501f2f33d0b5fdaa4c09e62639464e4910871
)), TransactionState(data=99990000 TokenType(tokenIdentifier='JPY', fractionDigits=0) issued by Sec-A held by Sec-A, contract=com.r3.corda.lib.tokens.contracts.FungibleTokenContract, notary=O=Notary, L=Tokyo, C=JP, encumbrance=null, constraint=SignatureAttachmentConstraint(key=EC Public Key [4b:44:7c:7b:1b:38:a6:93:bd:9a:c4:1d:8b:46:b4:6b:77:ba:f8:0e]
            X: 38d226dcd0fa574316da478aa75225e6ce18f65cbd96e60bf3c8251b1965417
            Y: 56e5dcf7ccab21b712601ed0278501f2f33d0b5fdaa4c09e62639464e4910871
))]

上記のように、CordaでDvPを実現した場合、UTXO型と言うこともあり、複数のinputと複数のoutputに分かれる。

以上

補足

トランザクション確認ロジックは以下の通り。

package com.template.flows

import co.paralleluniverse.fibers.Suspendable
import com.r3.corda.lib.tokens.contracts.states.EvolvableTokenType
import com.r3.corda.lib.tokens.contracts.utilities.heldBy
import com.r3.corda.lib.tokens.contracts.utilities.issuedBy
import com.r3.corda.lib.tokens.contracts.utilities.of
import com.r3.corda.lib.tokens.workflows.flows.redeem.RedeemFungibleTokensFlow
import com.r3.corda.lib.tokens.workflows.flows.rpc.CreateEvolvableTokens
import com.r3.corda.lib.tokens.workflows.flows.rpc.IssueTokens
import com.r3.corda.lib.tokens.workflows.flows.rpc.MoveFungibleTokens
import com.r3.corda.lib.tokens.workflows.flows.rpc.RedeemFungibleTokens
import com.r3.corda.lib.tokens.workflows.internal.flows.distribution.UpdateDistributionListFlow
import com.r3.corda.lib.tokens.workflows.types.PartyAndAmount
import com.template.states.TestTokenType
import net.corda.core.contracts.TransactionState
import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
import java.util.*


/**
 *  Get transaction history
 */
@StartableByRPC
class GetTx(
        val txhash: SecureHash
) : FlowLogic<LedgerTransaction?>() {
    override val progressTracker = ProgressTracker()

    @Suspendable
    override fun call(): LedgerTransaction? {

        val stx = serviceHub.validatedTransactions.getTransaction(txhash)
        val ltx = stx?.toLedgerTransaction(serviceHub,true)

        return ltx
    }
}