From 59b1248caca9a46a697b97a3e18b9d65f8840a07 Mon Sep 17 00:00:00 2001 From: Igor Wolkov Date: Sun, 21 Apr 2019 16:58:18 +0300 Subject: [PATCH 1/5] Split the Issue, Move and Redeem flows into the general and an extra custom flow --- .../sdk/token/workflow/flows/IssueToken.kt | 107 ++++++++++++++---- .../sdk/token/workflow/flows/MoveToken.kt | 107 ++++++++++++++---- .../sdk/token/workflow/flows/RedeemToken.kt | 89 ++++++++++----- .../corda/sdk/token/workflow/FlowHelpers.kt | 53 ++++++++- .../TokenFlowWithTradeConditionsTests.kt | 70 ++++++++++++ .../flows/IssueTokenWitTradeConditions.kt | 56 +++++++++ .../flows/MoveTokenWitTradeConditions.kt | 55 +++++++++ .../flows/RedeemTokenWitTradeConditions.kt | 50 ++++++++ .../statesAndContracts/TradeConditions.kt | 90 +++++++++++++++ 9 files changed, 603 insertions(+), 74 deletions(-) create mode 100644 workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/TokenFlowWithTradeConditionsTests.kt create mode 100644 workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueTokenWitTradeConditions.kt create mode 100644 workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/flows/MoveTokenWitTradeConditions.kt create mode 100644 workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/flows/RedeemTokenWitTradeConditions.kt create mode 100644 workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/statesAndContracts/TradeConditions.kt diff --git a/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt b/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt index 22ec5e94..98ff79cd 100644 --- a/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt +++ b/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt @@ -12,19 +12,15 @@ import com.r3.corda.sdk.token.contracts.utilities.withNotary import com.r3.corda.sdk.token.workflow.utilities.addPartyToDistributionList import net.corda.core.contracts.Amount import net.corda.core.contracts.TransactionState -import net.corda.core.flows.FinalityFlow -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowSession -import net.corda.core.flows.InitiatedBy -import net.corda.core.flows.InitiatingFlow -import net.corda.core.flows.ReceiveFinalityFlow -import net.corda.core.flows.StartableByRPC +import net.corda.core.contracts.requireThat +import net.corda.core.flows.* import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.StatesToRecord import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker +import java.security.PublicKey /** * This flow takes a bunch of parameters and is used to issue a token or an amount of some token on the ledger to @@ -61,9 +57,7 @@ import net.corda.core.utilities.ProgressTracker */ object IssueToken { - @InitiatingFlow - @StartableByRPC - class Initiator( + abstract class Primary( val token: T, val issueTo: AbstractParty, val notary: Party, @@ -72,26 +66,27 @@ object IssueToken { ) : FlowLogic() { companion object { object DIST_LIST : ProgressTracker.Step("Adding party to distribution list.") + object EXTRA_FLOW : ProgressTracker.Step("Starting extra flow") object SIGNING : ProgressTracker.Step("Signing transaction proposal.") object RECORDING : ProgressTracker.Step("Recording signed transaction.") { override fun childProgressTracker() = FinalityFlow.tracker() } - fun tracker() = ProgressTracker(DIST_LIST, SIGNING, RECORDING) + fun tracker() = ProgressTracker(DIST_LIST, EXTRA_FLOW, SIGNING, RECORDING) } override val progressTracker: ProgressTracker = tracker() @Suspendable - override fun call(): SignedTransaction { - // This is the identity which will be used to issue tokens. - // We also need a session for the other side. - val me: Party = ourIdentity - val holderParty = serviceHub.identityService.wellKnownPartyFromAnonymous(issueTo) - ?: throw IllegalArgumentException("Called IssueToken flow with anonymous party that node doesn't know about. " + - "Make sure that RequestConfidentialIdentity flow is called before.") - val holderSession = if (session == null) initiateFlow(holderParty) else session + abstract fun transactionExtra(me: Party, + holderParty: Party, + holderSession: FlowSession, + builder: TransactionBuilder): List + open fun issue(me: Party, + holderParty: Party, + holderSession: FlowSession, + builder: TransactionBuilder): Unit { // Create the issued token. We add this to the commands for grouping. val issuedToken: IssuedTokenType = token issuedBy me @@ -119,16 +114,43 @@ object IssueToken { // Create the transaction. val transactionState: TransactionState = heldToken withNotary notary - val utx: TransactionBuilder = TransactionBuilder(notary = notary).apply { + + builder.apply { addCommand(data = IssueTokenCommand(issuedToken), keys = listOf(me.owningKey)) addOutputState(state = transactionState) } + } + + @Suspendable + override fun call(): SignedTransaction { + // This is the identity which will be used to issue tokens. + // We also need a session for the other side. + val me: Party = ourIdentity + val holderParty = serviceHub.identityService.wellKnownPartyFromAnonymous(issueTo) + ?: throw IllegalArgumentException("Called IssueToken flow with anonymous party that node doesn't know about. " + + "Make sure that RequestConfidentialIdentity flow is called before.") + val holderSession = if (session == null) initiateFlow(holderParty) else session + + val builder = TransactionBuilder(notary = notary) + issue(me, holderParty, holderSession, builder) + + progressTracker.currentStep = EXTRA_FLOW + val extraKeys = transactionExtra(me, holderParty, holderSession, builder) + progressTracker.currentStep = SIGNING // Sign the transaction. Only Concrete Parties should be used here. - val stx: SignedTransaction = serviceHub.signInitialTransaction(utx) + val ptx: SignedTransaction = serviceHub.signInitialTransaction(builder, listOf(me.owningKey)) progressTracker.currentStep = RECORDING // Can issue to yourself, but finality flow doesn't take a session then. val sessions = if (me == holderParty) emptyList() else listOf(holderSession) + + val stx = ptx + + if (!serviceHub.myInfo.isLegalIdentity(holderSession.counterparty)) { + subFlow(CollectSignatureFlow(ptx, holderSession, extraKeys)) + } else { + listOf() + } + return subFlow(FinalityFlow(transaction = stx, progressTracker = RECORDING.childProgressTracker(), sessions = sessions @@ -136,15 +158,52 @@ object IssueToken { } } - @InitiatedBy(Initiator::class) - class Responder(val otherSession: FlowSession) : FlowLogic() { + abstract class Secondary(val otherSession: FlowSession) : FlowLogic() { + + @Suspendable + abstract fun checkTransaction(stx: SignedTransaction) + @Suspendable override fun call(): Unit { // We must do this check because FinalityFlow does not send locally and we want to be able to issue to ourselves. if (!serviceHub.myInfo.isLegalIdentity(otherSession.counterparty)) { + + val signTransactionFlow = object : SignTransactionFlow(otherSession) { + override fun checkTransaction(stx: SignedTransaction) = this@Secondary.checkTransaction(stx) + } + + val txId = subFlow(signTransactionFlow).id + // Resolve the issuance transaction. - subFlow(ReceiveFinalityFlow(otherSideSession = otherSession, statesToRecord = StatesToRecord.ONLY_RELEVANT)) + subFlow(ReceiveFinalityFlow(otherSideSession = otherSession, + statesToRecord = StatesToRecord.ONLY_RELEVANT, expectedTxId = txId)) } } } + + @InitiatingFlow + @StartableByRPC + class Initiator( + token: T, + issueTo: AbstractParty, + notary: Party, + amount: Amount? = null, + session: FlowSession? = null + ) : Primary(token, issueTo, notary, amount, session) { + + @Suspendable + override fun transactionExtra(me: Party, + holderParty: Party, + holderSession: FlowSession, + builder: TransactionBuilder): List { + return listOf() + } + } + + @InitiatedBy(Initiator::class) + class Responder(otherSession: FlowSession) : Secondary(otherSession) { + + @Suspendable + override fun checkTransaction(stx: SignedTransaction) = requireThat { } + } } diff --git a/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/MoveToken.kt b/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/MoveToken.kt index 780bdc5a..4de4fbb1 100644 --- a/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/MoveToken.kt +++ b/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/MoveToken.kt @@ -5,24 +5,19 @@ import com.r3.corda.sdk.token.contracts.types.TokenType import com.r3.corda.sdk.token.workflow.selection.TokenSelection import com.r3.corda.sdk.token.workflow.selection.generateMoveNonFungible import net.corda.core.contracts.Amount -import net.corda.core.flows.FinalityFlow -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowSession -import net.corda.core.flows.InitiatedBy -import net.corda.core.flows.InitiatingFlow -import net.corda.core.flows.ReceiveFinalityFlow -import net.corda.core.flows.StartableByRPC +import net.corda.core.contracts.requireThat +import net.corda.core.flows.* import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party import net.corda.core.node.StatesToRecord import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker +import java.security.PublicKey object MoveToken { - @InitiatingFlow - @StartableByRPC - class Initiator( + abstract class Primary( val ownedToken: T, val holder: AbstractParty, val amount: Amount? = null, @@ -30,49 +25,117 @@ object MoveToken { ) : FlowLogic() { companion object { object GENERATE_MOVE : ProgressTracker.Step("Generating tokens move.") + object EXTRA_FLOW : ProgressTracker.Step("Starting extra flow") object SIGNING : ProgressTracker.Step("Signing transaction proposal.") object RECORDING : ProgressTracker.Step("Recording signed transaction.") { override fun childProgressTracker() = FinalityFlow.tracker() } - fun tracker() = ProgressTracker(GENERATE_MOVE, SIGNING, RECORDING) + fun tracker() = ProgressTracker(GENERATE_MOVE, EXTRA_FLOW, SIGNING, RECORDING) } override val progressTracker: ProgressTracker = tracker() + @Suspendable + abstract fun transactionExtra(me: Party, + holderParty: Party, + holderSession: FlowSession, + builder: TransactionBuilder): List + + @Suspendable + open fun move(holderParty: Party, + holderSession: FlowSession): Pair> { + + return if (amount == null) { + generateMoveNonFungible(serviceHub.vaultService, ownedToken, holder) + } else { + val tokenSelection = TokenSelection(serviceHub) + tokenSelection.generateMove(TransactionBuilder(), amount, holder) + } + } + @Suspendable override fun call(): SignedTransaction { + val me: Party = ourIdentity val holderParty = serviceHub.identityService.wellKnownPartyFromAnonymous(holder) ?: throw IllegalArgumentException("Called MoveToken flow with anonymous party that node doesn't know about. " + "Make sure that RequestConfidentialIdentity flow is called before.") val holderSession = if (session == null) initiateFlow(holderParty) else session progressTracker.currentStep = GENERATE_MOVE - val (builder, keys) = if (amount == null) { - generateMoveNonFungible(serviceHub.vaultService, ownedToken, holder) - } else { - val tokenSelection = TokenSelection(serviceHub) - tokenSelection.generateMove(TransactionBuilder(), amount, holder) - } + val (builder, keys) = move(holderParty, holderSession) + + progressTracker.currentStep = EXTRA_FLOW + val extraKeys = transactionExtra(me, holderParty, holderSession, builder) progressTracker.currentStep = SIGNING // WARNING: At present, the recipient will not be signed up to updates from the token maintainer. - val stx: SignedTransaction = serviceHub.signInitialTransaction(builder, keys) + val ptx: SignedTransaction = serviceHub.signInitialTransaction(builder, keys) progressTracker.currentStep = RECORDING val sessions = if (ourIdentity == holderParty) emptyList() else listOf(holderSession) + + val stx = ptx + + if (!serviceHub.myInfo.isLegalIdentity(holderSession.counterparty)) { + subFlow(CollectSignatureFlow(ptx, holderSession, extraKeys)) + } else { + listOf() + } + return subFlow(FinalityFlow(transaction = stx, sessions = sessions)) } } - // TODO Don't really need it anymore as it calls only finality flow - @InitiatedBy(Initiator::class) - class Responder(val otherSession: FlowSession) : FlowLogic() { + abstract class Secondary(val otherSession: FlowSession) : FlowLogic() { + + @Suspendable + abstract fun checkTransaction(stx: SignedTransaction) + @Suspendable override fun call(): Unit { // Resolve the issuance transaction. if (!serviceHub.myInfo.isLegalIdentity(otherSession.counterparty)) { - subFlow(ReceiveFinalityFlow(otherSideSession = otherSession, statesToRecord = StatesToRecord.ONLY_RELEVANT)) + + val parties = serviceHub.identityService.wellKnownPartyFromAnonymous(otherSession.counterparty) + ?: throw IllegalArgumentException("Called MoveToken flow with anonymous party that node doesn't know about. " + + "Make sure that RequestConfidentialIdentity flow is called before.") + + parties + + val signTransactionFlow = object : SignTransactionFlow(otherSession) { + override fun checkTransaction(stx: SignedTransaction) = this@Secondary.checkTransaction(stx) + } + + val txId = subFlow(signTransactionFlow).id + + // Resolve the issuance transaction. + subFlow(ReceiveFinalityFlow(otherSideSession = otherSession, + statesToRecord = StatesToRecord.ONLY_RELEVANT, expectedTxId = txId)) } } } + + @InitiatingFlow + @StartableByRPC + class Initiator( + ownedToken: T, + holder: AbstractParty, + amount: Amount? = null, + session: FlowSession? = null + ) : Primary(ownedToken, holder, amount, session) { + + @Suspendable + override fun transactionExtra(me: Party, + holderParty: Party, + holderSession: FlowSession, + builder: TransactionBuilder): List { + return listOf() + } + } + + @InitiatedBy(Initiator::class) + class Responder(otherSession: FlowSession) : Secondary(otherSession) { + + @Suspendable + override fun checkTransaction(stx: SignedTransaction) = requireThat { } + } } \ No newline at end of file diff --git a/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/RedeemToken.kt b/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/RedeemToken.kt index 4c862ae0..888e8282 100644 --- a/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/RedeemToken.kt +++ b/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/RedeemToken.kt @@ -13,17 +13,7 @@ import net.corda.confidential.IdentitySyncFlow import net.corda.core.contracts.Amount import net.corda.core.contracts.CommandData import net.corda.core.contracts.StateAndRef -import net.corda.core.flows.CollectSignaturesFlow -import net.corda.core.flows.FinalityFlow -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowSession -import net.corda.core.flows.InitiatedBy -import net.corda.core.flows.InitiatingFlow -import net.corda.core.flows.ReceiveFinalityFlow -import net.corda.core.flows.ReceiveStateAndRefFlow -import net.corda.core.flows.SendStateAndRefFlow -import net.corda.core.flows.SignTransactionFlow -import net.corda.core.flows.StartableByRPC +import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.node.StatesToRecord import net.corda.core.node.services.IdentityService @@ -39,9 +29,7 @@ object RedeemToken { data class TokenRedeemNotification(val anonymous: Boolean, val amount: Amount?) // Called on owner side. - @InitiatingFlow - @StartableByRPC - class InitiateRedeem( + abstract class Primary( val ownedToken: T, val issuer: Party, val amount: Amount? = null, @@ -53,17 +41,20 @@ object RedeemToken { object SELECTING_STATES : ProgressTracker.Step("Selecting states to redeem.") object SEND_STATE_REF : ProgressTracker.Step("Sending states to the issuer for redeeming.") object SYNC_IDS : ProgressTracker.Step("Synchronising confidential identities.") + object EXTRA_FLOW : ProgressTracker.Step("Starting extra flow") object SIGNING_TX : ProgressTracker.Step("Signing transaction") object FINALISING_TX : ProgressTracker.Step("Finalising transaction") - fun tracker() = ProgressTracker(REDEEM_NOTIFICATION, CONF_ID, SELECTING_STATES, SEND_STATE_REF, SYNC_IDS, SIGNING_TX, FINALISING_TX) + fun tracker() = ProgressTracker(REDEEM_NOTIFICATION, CONF_ID, SELECTING_STATES, SEND_STATE_REF, SYNC_IDS, EXTRA_FLOW, SIGNING_TX, FINALISING_TX) } override val progressTracker: ProgressTracker = tracker() @Suspendable - override fun call(): SignedTransaction { - val issuerSession = initiateFlow(issuer) + abstract fun extraFlow(issuerSession: FlowSession): Unit + + @Suspendable + open fun redeemFlow(issuerSession: FlowSession) { progressTracker.currentStep = REDEEM_NOTIFICATION // Notify the recipient that we'll be sending them tokens for redeeming and advise them of anything they must do, e.g. @@ -101,9 +92,20 @@ object RedeemToken { val notary = firstState.notary val fakeWireTx = TransactionBuilder(notary = notary).withItems(*exitStateAndRefs.toTypedArray()).addCommand(DummyCommand(), ourIdentity.owningKey).toWireTransaction(serviceHub) subFlow(IdentitySyncFlow.Send(issuerSession, fakeWireTx)) + } + + @Suspendable + override fun call(): SignedTransaction { + + val issuerSession = initiateFlow(issuer) + + redeemFlow(issuerSession) + + progressTracker.currentStep = EXTRA_FLOW + extraFlow(issuerSession) + progressTracker.currentStep = SIGNING_TX subFlow(object : SignTransactionFlow(issuerSession) { - // TODO Add some additional checks. override fun checkTransaction(stx: SignedTransaction) = Unit }) @@ -116,10 +118,10 @@ object RedeemToken { private class DummyCommand : CommandData // Called on Issuer side. - @InitiatedBy(InitiateRedeem::class) - class IssuerResponder(val otherSession: FlowSession) : FlowLogic() { + abstract class Secondary(val otherSession: FlowSession) : FlowLogic() { + @Suspendable - override fun call(): SignedTransaction { + open fun redeem(): TransactionBuilder { // Receive a redeem notification from the party. It tells us if we need to sign up for token updates or // generate a confidential identity. val redeemNotification = otherSession.receive>().unwrap { it } @@ -132,20 +134,37 @@ object RedeemToken { val stateAndRefsToRedeem = subFlow(ReceiveStateAndRefFlow(otherSession)) // Synchronise identities. subFlow(IdentitySyncFlow.Receive(otherSession)) + + val notary = stateAndRefsToRedeem.first().state.notary + val builder = TransactionBuilder(notary = notary) + check(stateAndRefsToRedeem.isNotEmpty()) { "Received empty list of states to redeem." } + checkSameIssuer(stateAndRefsToRedeem, ourIdentity) checkSameNotary(stateAndRefsToRedeem) checkOwner(serviceHub.identityService, stateAndRefsToRedeem, otherSession.counterparty) - val notary = stateAndRefsToRedeem.first().state.notary - val txBuilder = TransactionBuilder(notary = notary) + if (redeemNotification.amount == null) { - generateExitNonFungible(txBuilder, stateAndRefsToRedeem.first() as StateAndRef>) + generateExitNonFungible(builder, stateAndRefsToRedeem.first() as StateAndRef>) } else { - TokenSelection(serviceHub).generateExit(txBuilder, stateAndRefsToRedeem as List>>, redeemNotification.amount, otherIdentity) + TokenSelection(serviceHub).generateExit(builder, stateAndRefsToRedeem as List>>, redeemNotification.amount, otherIdentity) } - val partialStx = serviceHub.signInitialTransaction(txBuilder, ourIdentity.owningKey) + + return builder + } + + @Suspendable + abstract fun extraFlow(builder: TransactionBuilder): Unit + + @Suspendable + override fun call(): SignedTransaction { + + val builder = redeem() + extraFlow(builder) + + val partialStx = serviceHub.signInitialTransaction(builder, ourIdentity.owningKey) val stx = subFlow(CollectSignaturesFlow(partialStx, listOf(otherSession))) return subFlow(FinalityFlow(transaction = stx, sessions = listOf(otherSession))) } @@ -190,4 +209,22 @@ object RedeemToken { "Received states that don't come from counterparty that initiated the flow." } } + + // Called on owner side. + @InitiatingFlow + @StartableByRPC + class InitiateRedeem( + ownedToken: T, + issuer: Party, + amount: Amount? = null, + anonymous: Boolean = true + ) : Primary(ownedToken, issuer, amount, anonymous) { + override fun extraFlow(issuerSession: FlowSession) {} + } + + // Called on Issuer side. + @InitiatedBy(InitiateRedeem::class) + class IssuerResponder(otherSession: FlowSession) : Secondary(otherSession) { + override fun extraFlow(builder: TransactionBuilder) {} + } } diff --git a/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/FlowHelpers.kt b/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/FlowHelpers.kt index 517509da..e8e4e667 100644 --- a/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/FlowHelpers.kt +++ b/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/FlowHelpers.kt @@ -26,7 +26,7 @@ fun StartedMockNode.issueTokens( issueTo: StartedMockNode, notary: StartedMockNode, amount: Amount? = null, - anonymous: Boolean = true + anonymous: Boolean = false ): CordaFuture { return transaction { if (anonymous) { @@ -51,7 +51,7 @@ fun StartedMockNode.moveTokens( token: T, owner: StartedMockNode, amount: Amount? = null, - anonymous: Boolean = true + anonymous: Boolean = false ): CordaFuture { return transaction { if (anonymous) { @@ -83,4 +83,53 @@ fun StartedMockNode.redeemTokens( amount = amount, anonymous = anonymous )) +} + +fun StartedMockNode.issueTokensWithTradeConditions( + token: T, + issueTo: StartedMockNode, + notary: StartedMockNode, + amount: Amount? = null, + conditions: String +): CordaFuture { + return transaction { + startFlow(IssueTokenWithTradeConditions.Initiator( + token = token, + issueTo = issueTo.legalIdentity(), + notary = notary.legalIdentity(), + amount = amount, + conditions = conditions + )) + } +} + +fun StartedMockNode.moveTokensWithTradeConditions( + token: T, + issueTo: StartedMockNode, + amount: Amount? = null, + conditions: String +): CordaFuture { + return transaction { + startFlow(MoveTokenWithTradeConditions.Initiator( + token = token, + issueTo = issueTo.legalIdentity(), + amount = amount, + conditions = conditions + )) + } +} + +fun StartedMockNode.redeemTokensWithTradeConditions( + token: T, + issuer: StartedMockNode, + amount: Amount? = null, + conditions: String +): CordaFuture { + return startFlow(RedeemTokenWithTradeConditions.InitiateRedeem( + ownedToken = token, + issuer = issuer.legalIdentity(), + amount = amount, + anonymous = false, + conditions = conditions + )) } \ No newline at end of file diff --git a/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/TokenFlowWithTradeConditionsTests.kt b/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/TokenFlowWithTradeConditionsTests.kt new file mode 100644 index 00000000..6d678c74 --- /dev/null +++ b/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/TokenFlowWithTradeConditionsTests.kt @@ -0,0 +1,70 @@ +package com.r3.corda.sdk.token.workflow + +import com.r3.corda.sdk.token.money.GBP +import com.r3.corda.sdk.token.workflow.statesAndContracts.TradeConditions +import com.r3.corda.sdk.token.workflow.utilities.ownedTokenAmountsByToken +import net.corda.core.node.services.queryBy +import net.corda.core.utilities.getOrThrow +import net.corda.testing.node.StartedMockNode +import org.assertj.core.api.Assertions +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +class TokenFlowWithTradeConditionsTests : MockNetworkTest(numberOfNodes = 3) { + + lateinit var A: StartedMockNode + lateinit var B: StartedMockNode + lateinit var I: StartedMockNode + + @Before + override fun initialiseNodes() { + A = nodes[0] + B = nodes[1] + I = nodes[2] + } + + @Test + fun `issue and move fixed tokens with trade conditions`() { + val conditions = "The best conditions ever." + val issueTokenTx = I.issueTokensWithTradeConditions(GBP, A, NOTARY, 100.GBP, conditions).getOrThrow() + A.watchForTransaction(issueTokenTx.id).getOrThrow() + val aTradeConditions = A.services.vaultService.queryBy().states.single().state.data + assertEquals(aTradeConditions.owner, A.legalIdentity()) + assertEquals(aTradeConditions.conditions, conditions) + + // Check to see that A was added to I's distribution list. + val moveTokenTx = A.moveTokensWithTradeConditions(GBP, B, 50.GBP, conditions).getOrThrow() + B.watchForTransaction(moveTokenTx.id).getOrThrow() + val bTradeConditions = B.services.vaultService.queryBy().states.single().state.data + assertEquals(bTradeConditions.owner, B.legalIdentity()) + assertEquals(bTradeConditions.conditions, conditions) + + println(moveTokenTx.tx) + } + + @Test + fun `redeem fungible with trade conditions happy pat`() { + val issueConditions = "The best issue conditions ever." + val redeemConditions = "The best redeem conditions ever." + val issueTokenTx = I.issueTokensWithTradeConditions(GBP, A, NOTARY, 100.GBP, issueConditions).getOrThrow() + A.watchForTransaction(issueTokenTx.id).getOrThrow() + A.redeemTokensWithTradeConditions(GBP, I, 100.GBP, redeemConditions).getOrThrow() + Assertions.assertThat(A.services.vaultService.ownedTokenAmountsByToken(GBP).states).isEmpty() + Assertions.assertThat(I.services.vaultService.ownedTokenAmountsByToken(GBP).states).isEmpty() + + assert(I.services.vaultService.queryBy().states.isEmpty()) + + + assertEquals(A.services.vaultService.queryBy().states.size, 2) + + val tradeConditions1 = A.services.vaultService.queryBy().states.get(0).state.data + assertEquals(tradeConditions1.owner, A.legalIdentity()) + assertEquals(tradeConditions1.conditions, issueConditions) + + val tradeConditions2 = A.services.vaultService.queryBy().states.get(1).state.data + assertEquals(tradeConditions2.owner, A.legalIdentity()) + assertEquals(tradeConditions2.conditions, redeemConditions) + } + +} \ No newline at end of file diff --git a/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueTokenWitTradeConditions.kt b/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueTokenWitTradeConditions.kt new file mode 100644 index 00000000..945752d5 --- /dev/null +++ b/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueTokenWitTradeConditions.kt @@ -0,0 +1,56 @@ +package com.r3.corda.sdk.token.workflow.flows + +import co.paralleluniverse.fibers.Suspendable +import com.r3.corda.sdk.token.contracts.types.TokenType +import com.r3.corda.sdk.token.workflow.statesAndContracts.TradeConditions +import com.r3.corda.sdk.token.workflow.statesAndContracts.TradeConditionsContract +import net.corda.core.contracts.Amount +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.contracts.requireThat +import net.corda.core.flows.* +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import java.security.PublicKey + +object IssueTokenWithTradeConditions { + @InitiatingFlow + @StartableByRPC + class Initiator( + token: T, + issueTo: AbstractParty, + notary: Party, + amount: Amount? = null, + val conditions: String, + session: FlowSession? = null + ) : IssueToken.Primary(token, issueTo, notary, amount, session) { + + @Suspendable + override fun transactionExtra(me: Party, + holderParty: Party, + holderSession: FlowSession, + builder: TransactionBuilder): List { + builder.apply { + addCommand(data = TradeConditionsContract.TradeCommand(), keys = listOf(holderParty.owningKey)) + addOutputState(state = TradeConditions(holderParty, conditions, UniqueIdentifier())) + } + + return listOf(holderParty.owningKey) + } + } + + @InitiatedBy(Initiator::class) + class Responder(otherSession: FlowSession) : IssueToken.Secondary(otherSession) { + + @Suspendable + override fun checkTransaction(stx: SignedTransaction) = requireThat { + val tradeConditions = stx.tx.outputsOfType().singleOrNull() ?: + throw FlowException("The transaction must contain trade conditions.") + + if(tradeConditions.conditions.contains("cheating")) + throw FlowException("The cheating trade conditions are unacceptable.") + + } + } +} \ No newline at end of file diff --git a/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/flows/MoveTokenWitTradeConditions.kt b/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/flows/MoveTokenWitTradeConditions.kt new file mode 100644 index 00000000..05ccad3a --- /dev/null +++ b/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/flows/MoveTokenWitTradeConditions.kt @@ -0,0 +1,55 @@ +package com.r3.corda.sdk.token.workflow.flows + +import co.paralleluniverse.fibers.Suspendable +import com.r3.corda.sdk.token.contracts.types.TokenType +import com.r3.corda.sdk.token.workflow.statesAndContracts.TradeConditions +import com.r3.corda.sdk.token.workflow.statesAndContracts.TradeConditionsContract +import net.corda.core.contracts.Amount +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.contracts.requireThat +import net.corda.core.flows.* +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import java.security.PublicKey + +object MoveTokenWithTradeConditions { + @InitiatingFlow + @StartableByRPC + class Initiator( + token: T, + issueTo: AbstractParty, + amount: Amount? = null, + val conditions: String, + session: FlowSession? = null + ) : MoveToken.Primary(token, issueTo, amount, session) { + + @Suspendable + override fun transactionExtra(me: Party, + holderParty: Party, + holderSession: FlowSession, + builder: TransactionBuilder): List { + builder.apply { + addCommand(data = TradeConditionsContract.TradeCommand(), keys = listOf(holderParty.owningKey)) + addOutputState(state = TradeConditions(holderParty, conditions, UniqueIdentifier())) + } + + return listOf(holderParty.owningKey) + } + } + + @InitiatedBy(Initiator::class) + class Responder(otherSession: FlowSession) : MoveToken.Secondary(otherSession) { + + @Suspendable + override fun checkTransaction(stx: SignedTransaction) = requireThat { + val tradeConditions = stx.tx.outputsOfType().singleOrNull() ?: + throw FlowException("The transaction must contain trade conditions.") + + if(tradeConditions.conditions.contains("cheating")) + throw FlowException("The cheating trade conditions are unacceptable.") + + } + } +} \ No newline at end of file diff --git a/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/flows/RedeemTokenWitTradeConditions.kt b/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/flows/RedeemTokenWitTradeConditions.kt new file mode 100644 index 00000000..e633d9b9 --- /dev/null +++ b/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/flows/RedeemTokenWitTradeConditions.kt @@ -0,0 +1,50 @@ +package com.r3.corda.sdk.token.workflow.flows + +import co.paralleluniverse.fibers.Suspendable +import com.r3.corda.sdk.token.contracts.types.TokenType +import com.r3.corda.sdk.token.workflow.statesAndContracts.TradeConditions +import com.r3.corda.sdk.token.workflow.statesAndContracts.TradeConditionsContract +import net.corda.core.contracts.Amount +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.unwrap + +object RedeemTokenWithTradeConditions { + + @InitiatingFlow + @StartableByRPC + class InitiateRedeem( + ownedToken: T, + issuer: Party, + amount: Amount? = null, + anonymous: Boolean = true, + val conditions: String + ) : RedeemToken.Primary(ownedToken, issuer, amount, anonymous) { + + @Suspendable + override fun extraFlow(issuerSession: FlowSession) { + issuerSession.send(TradeConditions(ourIdentity, conditions, UniqueIdentifier())) + } + } + + @InitiatedBy(InitiateRedeem::class) + class IssuerResponder(otherSession: FlowSession) : RedeemToken.Secondary(otherSession) { + + @Suspendable + override fun extraFlow(builder: TransactionBuilder) { + + val tradeConditions = otherSession.receive().unwrap { it } + + builder.apply { + addCommand(data = TradeConditionsContract.TradeCommand(), keys = listOf(tradeConditions.owner.owningKey)) + addOutputState(state = tradeConditions) + } + + } + } +} \ No newline at end of file diff --git a/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/statesAndContracts/TradeConditions.kt b/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/statesAndContracts/TradeConditions.kt new file mode 100644 index 00000000..6e8e41f0 --- /dev/null +++ b/workflow/src/test/kotlin/com/r3/corda/sdk/token/workflow/statesAndContracts/TradeConditions.kt @@ -0,0 +1,90 @@ +package com.r3.corda.sdk.token.workflow.statesAndContracts + +import net.corda.core.contracts.* +import net.corda.core.identity.AbstractParty +import net.corda.core.internal.castIfPossible +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import net.corda.core.schemas.QueryableState +import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.LedgerTransaction +import java.util.* +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Table + +@BelongsToContract(TradeConditionsContract::class) +data class TradeConditions(val owner: AbstractParty, + val conditions: String, + override val linearId: UniqueIdentifier) : LinearState, QueryableState { + + override val participants: List + get() = listOf(owner) + + override fun generateMappedObject(schema: MappedSchema): PersistentState { + return when (schema) { + is TradeConditionsSchemaV1 -> TradeConditionsSchemaV1.TradeConditions( + this.owner, + this.conditions, + this.linearId.id + ) + else -> throw IllegalArgumentException("The schema : $schema does not exist.") + } + } + + override fun supportedSchemas(): Iterable = listOf(TradeConditionsSchemaV1) +} + +object TradeConditionsSchema + +object TradeConditionsSchemaV1 : MappedSchema( + schemaFamily = TradeConditionsSchema.javaClass, + version = 1, + mappedTypes = listOf(TradeConditions::class.java) +) { + @Entity + @Table(name = "trade_conditions") + class TradeConditions( + @Column(name = "owner", nullable = false) + var owner: AbstractParty, + + @Column(name = "conditions", nullable = false) + var conditions: String, + + @Column(name = "trade_conditions_id") + var linearId: UUID + ) : PersistentState() +} + + +class TradeConditionsContract : Contract { + + companion object { + @JvmStatic + val CONTRACT_ID = "com.r3.corda.sdk.token.workflow.statesAndContracts.TradeConditionsContract" + } + + @CordaSerializable + interface TradeConditionsCommand : CommandData + + @CordaSerializable + class TradeCommand : TradeConditionsCommand, TypeOnlyCommandData() + + + override fun verify(tx: LedgerTransaction) { + + val command = tx.commands.mapNotNull { (TradeConditionsCommand::class.java).castIfPossible(it.value) }.single() + when (command) { + is TradeCommand -> handleTradeCommand(tx) + } + } + + private fun handleTradeCommand(tx: LedgerTransaction) { + val output = tx.outputsOfType().single() + + require(output.conditions.isNotEmpty()) { + "Trade conditions must be non-empty." + } + } +} + From ebbee92f2732e36f201b2417a6fedd9073c947cc Mon Sep 17 00:00:00 2001 From: przemolb Date: Sat, 8 Jun 2019 07:37:08 +0100 Subject: [PATCH 2/5] To trigger build on CI/CD Add just one space to trigger CI/CD build --- .../kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt b/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt index 98ff79cd..7cec9702 100644 --- a/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt +++ b/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt @@ -55,6 +55,7 @@ import java.security.PublicKey * TODO: Split into two flows. One for owned tokens and another for owned token amounts. * TODO: Profile and optimise this flow. */ + object IssueToken { abstract class Primary( From ea7bf1f3dea14d5d6fbe2f2d0b76354303f9000e Mon Sep 17 00:00:00 2001 From: przemolb Date: Sat, 8 Jun 2019 08:12:08 +0100 Subject: [PATCH 3/5] Again trying to trigger cicd --- .../kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt b/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt index 7cec9702..95ad364d 100644 --- a/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt +++ b/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt @@ -55,7 +55,7 @@ import java.security.PublicKey * TODO: Split into two flows. One for owned tokens and another for owned token amounts. * TODO: Profile and optimise this flow. */ - + object IssueToken { abstract class Primary( From 91f43522d57eadec3f74e458347764cc8a377c06 Mon Sep 17 00:00:00 2001 From: przemolb Date: Sat, 8 Jun 2019 09:36:16 +0100 Subject: [PATCH 4/5] Update IssueToken.kt --- .../kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt b/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt index 95ad364d..fa9cd4cd 100644 --- a/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt +++ b/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt @@ -55,7 +55,7 @@ import java.security.PublicKey * TODO: Split into two flows. One for owned tokens and another for owned token amounts. * TODO: Profile and optimise this flow. */ - + object IssueToken { abstract class Primary( From 7165ace59dbb2ffb645a311638198d4d82142ae9 Mon Sep 17 00:00:00 2001 From: przemolb Date: Thu, 13 Jun 2019 15:12:18 +0100 Subject: [PATCH 5/5] Update IssueToken.kt --- .../kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt b/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt index fa9cd4cd..7cec9702 100644 --- a/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt +++ b/workflow/src/main/kotlin/com/r3/corda/sdk/token/workflow/flows/IssueToken.kt @@ -55,7 +55,7 @@ import java.security.PublicKey * TODO: Split into two flows. One for owned tokens and another for owned token amounts. * TODO: Profile and optimise this flow. */ - + object IssueToken { abstract class Primary(