From 0b7678b8ecd13906c305e414e3df4cae672351f3 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Fri, 24 Nov 2017 14:25:56 +0000 Subject: [PATCH] Flow to issue cash and pay using the returned tx id rather than a vault query to find the new state --- .../flows/CashIssueAndPaymentNoSelection.kt | 62 +++++++++++++ .../CashIssueAndPaymentNoSelectionFlowTest.kt | 87 +++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentNoSelection.kt create mode 100644 perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentNoSelectionFlowTest.kt diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentNoSelection.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentNoSelection.kt new file mode 100644 index 0000000000..560c296f99 --- /dev/null +++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentNoSelection.kt @@ -0,0 +1,62 @@ +package com.r3.corda.enterprise.perftestcordapp.flows + +import co.paralleluniverse.fibers.Suspendable +import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash +import com.r3.corda.enterprise.perftestcordapp.contracts.asset.OnLedgerAsset +import com.r3.corda.enterprise.perftestcordapp.contracts.asset.PartyAndAmount +import net.corda.core.contracts.* +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.ProgressTracker +import java.util.* + +/** + * Initiates a flow that self-issues cash (which should then be sent to recipient(s) using a payment transaction). + * + * We issue cash only to ourselves so that all KYC/AML checks on payments are enforced consistently, rather than risk + * checks for issuance and payments differing. Outside of test scenarios it would be extremely unusual to issue cash + * and immediately transfer it, so impact of this limitation is considered minimal. + * + * @param amount the amount of currency to issue. + * @param issueRef a reference to put on the issued currency. + * @param recipient payee Party + * @param anonymous whether to anonymise before the transaction + * @param notary the notary to set on the output states. + */ +@StartableByRPC +class CashIssueAndPaymentNoSelection(val amount: Amount, + val issueRef: OpaqueBytes, + val recipient: Party, + val anonymous: Boolean, + val notary: Party, + progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { + constructor(request: CashIssueAndPaymentFlow.IssueAndPaymentRequest) : this(request.amount, request.issueRef, request.recipient, request.anonymous, request.notary, tracker()) + + @Suspendable + override fun call(): Result { + fun deriveState(txState: TransactionState, amt: Amount>, owner: AbstractParty) + = txState.copy(data = txState.data.copy(amount = amt, owner = owner)) + + val issueResult = subFlow(CashIssueFlow(amount, issueRef, notary)) + val cashStateAndRef = issueResult.stx.tx.outRef(0) + val changeIdentity = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false) + val builder = TransactionBuilder(notary) + val (spendTx, keysForSigning) = OnLedgerAsset.generateSpend(builder, listOf(PartyAndAmount(recipient, amount)), listOf(cashStateAndRef), + changeIdentity.party.anonymise(), + { state, quantity, owner -> deriveState(state, quantity, owner) }, + { Cash().generateMoveCommand() }) + + progressTracker.currentStep = SIGNING_TX + val tx = serviceHub.signInitialTransaction(spendTx, keysForSigning) + + progressTracker.currentStep = FINALISING_TX + val notarised = finaliseTx(tx, setOf(recipient), "Unable to notarise spend") + return Result(notarised, recipient) + } + + constructor(amount: Amount, issueRef: OpaqueBytes, payTo: Party, anonymous: Boolean, notary: Party) : this(amount, issueRef, payTo, anonymous, notary, tracker()) + +} \ No newline at end of file diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentNoSelectionFlowTest.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentNoSelectionFlowTest.kt new file mode 100644 index 0000000000..bfc42dd480 --- /dev/null +++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentNoSelectionFlowTest.kt @@ -0,0 +1,87 @@ +package com.r3.corda.enterprise.perftestcordapp.flows + +import com.r3.corda.enterprise.perftestcordapp.DOLLARS +import com.r3.corda.enterprise.perftestcordapp.`issued by` +import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash +import net.corda.core.identity.Party +import net.corda.core.node.services.Vault +import net.corda.core.node.services.trackBy +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.node.internal.StartedNode +import net.corda.testing.* +import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin +import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockNetwork.MockNode +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +class CashIssueAndPayNoSelectionTests { + private lateinit var mockNet: MockNetwork + private val initialBalance = 2000.DOLLARS + private val ref = OpaqueBytes.of(0x01) + private lateinit var bankOfCordaNode: StartedNode + private lateinit var bankOfCorda: Party + private lateinit var aliceNode: StartedNode + private lateinit var notary: Party + + @Before + fun start() { + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset")) + bankOfCordaNode = mockNet.createPartyNode(BOC.name) + aliceNode = mockNet.createPartyNode(ALICE.name) + bankOfCorda = bankOfCordaNode.info.chooseIdentity() + mockNet.runNetwork() + notary = mockNet.defaultNotaryIdentity + } + + @After + fun cleanUp() { + mockNet.stopNodes() + } + + @Test + fun `issue and pay some cash`() { + val payTo = aliceNode.info.chooseIdentity() + val expectedPayment = 500.DOLLARS + + bankOfCordaNode.database.transaction { + // Register for vault updates + val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) + val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultService.trackBy(criteria) + val (_, vaultUpdatesBankClient) = aliceNode.services.vaultService.trackBy(criteria) + + val future = bankOfCordaNode.services.startFlow(CashIssueAndPaymentNoSelection(expectedPayment, OpaqueBytes.of(1), payTo, false, notary)).resultFuture + mockNet.runNetwork() + future.getOrThrow() + + // Check bank of corda vault - should see two consecutive updates of issuing $500 + // and paying $500 to alice + vaultUpdatesBoc.expectEvents { + sequence( + expect { update -> + require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" } + val changeState = update.produced.single().state.data + assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), changeState.amount) + }, + expect { update -> + require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" } + } + ) + } + + // Check notary node vault updates + vaultUpdatesBankClient.expectEvents { + expect { (consumed, produced) -> + require(consumed.isEmpty()) { consumed.size } + require(produced.size == 1) { produced.size } + val paymentState = produced.single().state.data + assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount) + } + } + } + } +}