From 7ab94650a68f18a86dc8568e97ea59a735163a93 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Tue, 10 Oct 2017 17:00:36 +0100 Subject: [PATCH] Cash flows and unit tests --- .../corda/ptflows/flows/AbstractPtCashFlow.kt | 53 ++++++++ .../ptflows/flows/PtCashConfigDataFlow.kt | 40 ++++++ .../net/corda/ptflows/flows/PtCashExitFlow.kt | 82 ++++++++++++ .../flows/PtCashIssueAndPaymentFlow.kt | 49 +++++++ .../corda/ptflows/flows/PtCashIssueFlow.kt | 52 ++++++++ .../corda/ptflows/flows/PtCashPaymentFlow.kt | 74 +++++++++++ .../contracts/flows/CashExitFlowTests.kt | 79 ++++++++++++ .../contracts/flows/CashIssueFlowTests.kt | 69 ++++++++++ .../contracts/flows/CashPaymentFlowTests.kt | 121 ++++++++++++++++++ 9 files changed, 619 insertions(+) create mode 100644 perftestflows/src/main/kotlin/net/corda/ptflows/flows/AbstractPtCashFlow.kt create mode 100644 perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashConfigDataFlow.kt create mode 100644 perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashExitFlow.kt create mode 100644 perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashIssueAndPaymentFlow.kt create mode 100644 perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashIssueFlow.kt create mode 100644 perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashPaymentFlow.kt create mode 100644 perftestflows/src/test/kotlin/net/corda/ptflows/contracts/flows/CashExitFlowTests.kt create mode 100644 perftestflows/src/test/kotlin/net/corda/ptflows/contracts/flows/CashIssueFlowTests.kt create mode 100644 perftestflows/src/test/kotlin/net/corda/ptflows/contracts/flows/CashPaymentFlowTests.kt diff --git a/perftestflows/src/main/kotlin/net/corda/ptflows/flows/AbstractPtCashFlow.kt b/perftestflows/src/main/kotlin/net/corda/ptflows/flows/AbstractPtCashFlow.kt new file mode 100644 index 0000000000..31e256f721 --- /dev/null +++ b/perftestflows/src/main/kotlin/net/corda/ptflows/flows/AbstractPtCashFlow.kt @@ -0,0 +1,53 @@ +package net.corda.ptflows.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Amount +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowException +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.NotaryException +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.ProgressTracker +import java.util.* + +/** + * Initiates a flow that produces an Issue/Move or Exit Cash transaction. + */ +abstract class AbstractPtCashFlow(override val progressTracker: ProgressTracker) : FlowLogic() { + companion object { + object GENERATING_ID : ProgressTracker.Step("Generating anonymous identities") + object GENERATING_TX : ProgressTracker.Step("Generating transaction") + object SIGNING_TX : ProgressTracker.Step("Signing transaction") + object FINALISING_TX : ProgressTracker.Step("Finalising transaction") + + fun tracker() = ProgressTracker(GENERATING_ID, GENERATING_TX, SIGNING_TX, FINALISING_TX) + } + + @Suspendable + protected fun finaliseTx(tx: SignedTransaction, extraParticipants: Set, message: String): SignedTransaction { + try { + return subFlow(FinalityFlow(tx, extraParticipants)) + } catch (e: NotaryException) { + throw PtCashException(message, e) + } + } + + /** + * Combined signed transaction and identity lookup map, which is the resulting data from regular cash flows. + * Specialised flows for unit tests differ from this. + * + * @param stx the signed transaction. + * @param recipient the identity used for the other side of the transaction, where applicable (i.e. this is + * null for exit transactions). For anonymous transactions this is the confidential identity generated for the + * transaction, otherwise this is the well known identity. + */ + @CordaSerializable + data class Result(val stx: SignedTransaction, val recipient: AbstractParty?) + + abstract class AbstractRequest(val amount: Amount) +} + +class PtCashException(message: String, cause: Throwable) : FlowException(message, cause) \ No newline at end of file diff --git a/perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashConfigDataFlow.kt b/perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashConfigDataFlow.kt new file mode 100644 index 0000000000..a235ff6a51 --- /dev/null +++ b/perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashConfigDataFlow.kt @@ -0,0 +1,40 @@ +package net.corda.ptflows.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowException +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.core.serialization.CordaSerializable +import net.corda.ptflows.CHF +import net.corda.ptflows.EUR +import net.corda.ptflows.GBP +import net.corda.ptflows.USD +import java.util.* + +/** + * Flow to obtain cash cordapp app configuration. + */ +@StartableByRPC +class PtCashConfigDataFlow : FlowLogic() { + companion object { + private val supportedCurrencies = listOf(USD, GBP, CHF, EUR) + } + + @Suspendable + override fun call(): PtCashConfiguration { + val issuableCurrencies = supportedCurrencies.mapNotNull { + try { + // Currently it uses checkFlowPermission to determine the list of issuable currency as a temporary hack. + // TODO: get the config from proper configuration source. + checkFlowPermission("corda.issuer.$it", emptyMap()) + it + } catch (e: FlowException) { + null + } + } + return PtCashConfiguration(issuableCurrencies, supportedCurrencies) + } +} + +@CordaSerializable +data class PtCashConfiguration(val issuableCurrencies: List, val supportedCurrencies: List) diff --git a/perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashExitFlow.kt b/perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashExitFlow.kt new file mode 100644 index 0000000000..2aa360137f --- /dev/null +++ b/perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashExitFlow.kt @@ -0,0 +1,82 @@ +package net.corda.ptflows.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Amount +import net.corda.core.contracts.InsufficientBalanceException +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.DEFAULT_PAGE_NUM +import net.corda.core.node.services.vault.PageSpecification +import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria +import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.ProgressTracker +import net.corda.ptflows.contracts.asset.PtCash +import net.corda.ptflows.contracts.asset.PtCashSelection +import net.corda.ptflows.issuedBy +import java.util.* + +/** + * Initiates a flow that produces an cash exit transaction. + * + * @param amount the amount of a currency to remove from the ledger. + * @param issuerRef the reference on the issued currency. Added to the node's legal identity to determine the + * issuer. + */ +@StartableByRPC +class PtCashExitFlow(private val amount: Amount, + private val issuerRef: OpaqueBytes, + progressTracker: ProgressTracker) : AbstractPtCashFlow(progressTracker) { + constructor(amount: Amount, issueRef: OpaqueBytes) : this(amount, issueRef, tracker()) + constructor(request: ExitRequest) : this(request.amount, request.issueRef, tracker()) + + companion object { + fun tracker() = ProgressTracker(GENERATING_TX, SIGNING_TX, FINALISING_TX) + } + + /** + * @return the signed transaction, and a mapping of parties to new anonymous identities generated + * (for this flow this map is always empty). + */ + @Suspendable + @Throws(PtCashException::class) + override fun call(): AbstractPtCashFlow.Result { + progressTracker.currentStep = GENERATING_TX + val builder = TransactionBuilder(notary = null) + val issuer = ourIdentity.ref(issuerRef) + val exitStates = PtCashSelection + .getInstance { serviceHub.jdbcSession().metaData } + .unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference)) + val signers = try { + PtCash().generateExit( + builder, + amount.issuedBy(issuer), + exitStates) + } catch (e: InsufficientBalanceException) { + throw PtCashException("Exiting more cash than exists", e) + } + + // Work out who the owners of the burnt states were (specify page size so we don't silently drop any if > DEFAULT_PAGE_SIZE) + val inputStates = serviceHub.vaultQueryService.queryBy(VaultQueryCriteria(stateRefs = builder.inputStates()), + PageSpecification(pageNumber = DEFAULT_PAGE_NUM, pageSize = builder.inputStates().size)).states + + // TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them + // count as a reason to fail? + val participants: Set = inputStates + .mapNotNull { serviceHub.identityService.wellKnownPartyFromAnonymous(it.state.data.owner) } + .toSet() + // Sign transaction + progressTracker.currentStep = SIGNING_TX + val tx = serviceHub.signInitialTransaction(builder, signers) + + // Commit the transaction + progressTracker.currentStep = FINALISING_TX + val notarised = finaliseTx(tx, participants, "Unable to notarise exit") + return Result(notarised, null) + } + + @CordaSerializable + class ExitRequest(amount: Amount, val issueRef: OpaqueBytes) : AbstractRequest(amount) +} diff --git a/perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashIssueAndPaymentFlow.kt b/perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashIssueAndPaymentFlow.kt new file mode 100644 index 0000000000..74cca5b215 --- /dev/null +++ b/perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashIssueAndPaymentFlow.kt @@ -0,0 +1,49 @@ +package net.corda.ptflows.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Amount +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.serialization.CordaSerializable +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 issuerBankPartyRef a reference to put on the issued currency. + * @param notary the notary to set on the output states. + */ +@StartableByRPC +class PtCashIssueAndPaymentFlow(val amount: Amount, + val issueRef: OpaqueBytes, + val recipient: Party, + val anonymous: Boolean, + val notary: Party, + progressTracker: ProgressTracker) : AbstractPtCashFlow(progressTracker) { + constructor(amount: Amount, + issueRef: OpaqueBytes, + recipient: Party, + anonymous: Boolean, + notary: Party) : this(amount, issueRef, recipient, anonymous, notary, tracker()) + constructor(request: IssueAndPaymentRequest) : this(request.amount, request.issueRef, request.recipient, request.anonymous, request.notary, tracker()) + + @Suspendable + override fun call(): Result { + subFlow(PtCashIssueFlow(amount, issueRef, notary)) + return subFlow(PtCashPaymentFlow(amount, recipient, anonymous)) + } + + @CordaSerializable + class IssueAndPaymentRequest(amount: Amount, + val issueRef: OpaqueBytes, + val recipient: Party, + val notary: Party, + val anonymous: Boolean) : AbstractRequest(amount) +} \ No newline at end of file diff --git a/perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashIssueFlow.kt b/perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashIssueFlow.kt new file mode 100644 index 0000000000..e249f5090e --- /dev/null +++ b/perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashIssueFlow.kt @@ -0,0 +1,52 @@ +package net.corda.ptflows.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Amount +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.ProgressTracker +import net.corda.ptflows.contracts.asset.PtCash +import net.corda.ptflows.issuedBy +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 issuerBankPartyRef a reference to put on the issued currency. + * @param notary the notary to set on the output states. + */ +@StartableByRPC +class PtCashIssueFlow(private val amount: Amount, + private val issuerBankPartyRef: OpaqueBytes, + private val notary: Party, + progressTracker: ProgressTracker) : AbstractPtCashFlow(progressTracker) { + constructor(amount: Amount, + issuerBankPartyRef: OpaqueBytes, + notary: Party) : this(amount, issuerBankPartyRef, notary, tracker()) + constructor(request: IssueRequest) : this(request.amount, request.issueRef, request.notary, tracker()) + + @Suspendable + override fun call(): AbstractPtCashFlow.Result { + progressTracker.currentStep = GENERATING_TX + val builder = TransactionBuilder(notary) + val issuer = ourIdentity.ref(issuerBankPartyRef) + val signers = PtCash().generateIssue(builder, amount.issuedBy(issuer), ourIdentity, notary) + progressTracker.currentStep = SIGNING_TX + val tx = serviceHub.signInitialTransaction(builder, signers) + progressTracker.currentStep = FINALISING_TX + // There is no one to send the tx to as we're the only participants + val notarised = finaliseTx(tx, emptySet(), "Unable to notarise issue") + return Result(notarised, ourIdentity) + } + + @CordaSerializable + class IssueRequest(amount: Amount, val issueRef: OpaqueBytes, val notary: Party) : AbstractRequest(amount) +} diff --git a/perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashPaymentFlow.kt b/perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashPaymentFlow.kt new file mode 100644 index 0000000000..bb92fea5ce --- /dev/null +++ b/perftestflows/src/main/kotlin/net/corda/ptflows/flows/PtCashPaymentFlow.kt @@ -0,0 +1,74 @@ +package net.corda.ptflows.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.confidential.SwapIdentitiesFlow +import net.corda.core.contracts.Amount +import net.corda.core.contracts.InsufficientBalanceException +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.Party +import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker +import net.corda.ptflows.contracts.asset.PtCash +import java.util.* + +/** + * Initiates a flow that sends cash to a recipient. + * + * @param amount the amount of a currency to pay to the recipient. + * @param recipient the party to pay the currency to. + * @param issuerConstraint if specified, the payment will be made using only cash issued by the given parties. + * @param anonymous whether to anonymous the recipient party. Should be true for normal usage, but may be false + * for testing purposes. + */ +@StartableByRPC +open class PtCashPaymentFlow( + val amount: Amount, + val recipient: Party, + val anonymous: Boolean, + progressTracker: ProgressTracker, + val issuerConstraint: Set = emptySet()) : AbstractPtCashFlow(progressTracker) { + /** A straightforward constructor that constructs spends using cash states of any issuer. */ + constructor(amount: Amount, recipient: Party) : this(amount, recipient, true, tracker()) + /** A straightforward constructor that constructs spends using cash states of any issuer. */ + constructor(amount: Amount, recipient: Party, anonymous: Boolean) : this(amount, recipient, anonymous, tracker()) + constructor(request: PaymentRequest) : this(request.amount, request.recipient, request.anonymous, tracker(), request.issuerConstraint) + + @Suspendable + override fun call(): AbstractPtCashFlow.Result { + progressTracker.currentStep = GENERATING_ID + val txIdentities = if (anonymous) { + subFlow(SwapIdentitiesFlow(recipient)) + } else { + emptyMap() + } + val anonymousRecipient = txIdentities[recipient] ?: recipient + progressTracker.currentStep = GENERATING_TX + val builder = TransactionBuilder(notary = null) + // TODO: Have some way of restricting this to states the caller controls + val (spendTX, keysForSigning) = try { + PtCash.generateSpend(serviceHub, + builder, + amount, + ourIdentityAndCert, + anonymousRecipient, + issuerConstraint) + } catch (e: InsufficientBalanceException) { + throw PtCashException("Insufficient cash for spend: ${e.message}", e) + } + + 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, anonymousRecipient) + } + + @CordaSerializable + class PaymentRequest(amount: Amount, + val recipient: Party, + val anonymous: Boolean, + val issuerConstraint: Set = emptySet()) : AbstractRequest(amount) +} diff --git a/perftestflows/src/test/kotlin/net/corda/ptflows/contracts/flows/CashExitFlowTests.kt b/perftestflows/src/test/kotlin/net/corda/ptflows/contracts/flows/CashExitFlowTests.kt new file mode 100644 index 0000000000..69986b6f44 --- /dev/null +++ b/perftestflows/src/test/kotlin/net/corda/ptflows/contracts/flows/CashExitFlowTests.kt @@ -0,0 +1,79 @@ +package net.corda.ptflows.contracts.flows + +import net.corda.core.identity.Party +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.ptflows.flows.PtCashException +import net.corda.ptflows.flows.PtCashExitFlow +import net.corda.ptflows.flows.PtCashIssueFlow +import net.corda.ptflows.DOLLARS +import net.corda.ptflows.`issued by` +import net.corda.ptflows.contracts.asset.PtCash +import net.corda.node.internal.StartedNode +import net.corda.testing.chooseIdentity +import net.corda.testing.getDefaultNotary +import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin +import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockNetwork.MockNode +import net.corda.testing.setCordappPackages +import net.corda.testing.unsetCordappPackages +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class CashExitFlowTests { + 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 notaryNode: StartedNode + private lateinit var notary: Party + + @Before + fun start() { + setCordappPackages("net.corda.ptflows.contracts.asset") + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + val nodes = mockNet.createSomeNodes(1) + notaryNode = nodes.notaryNode + bankOfCordaNode = nodes.partyNodes[0] + bankOfCorda = bankOfCordaNode.info.chooseIdentity() + + mockNet.runNetwork() + notary = bankOfCordaNode.services.getDefaultNotary() + val future = bankOfCordaNode.services.startFlow(PtCashIssueFlow(initialBalance, ref, notary)).resultFuture + mockNet.runNetwork() + future.getOrThrow() + } + + @After + fun cleanUp() { + mockNet.stopNodes() + unsetCordappPackages() + } + + @Test + fun `exit some cash`() { + val exitAmount = 500.DOLLARS + val future = bankOfCordaNode.services.startFlow(PtCashExitFlow(exitAmount, ref)).resultFuture + mockNet.runNetwork() + val exitTx = future.getOrThrow().stx.tx + val expected = (initialBalance - exitAmount).`issued by`(bankOfCorda.ref(ref)) + assertEquals(1, exitTx.inputs.size) + assertEquals(1, exitTx.outputs.size) + val output = exitTx.outputsOfType().single() + assertEquals(expected, output.amount) + } + + @Test + fun `exit zero cash`() { + val expected = 0.DOLLARS + val future = bankOfCordaNode.services.startFlow(PtCashExitFlow(expected, ref)).resultFuture + mockNet.runNetwork() + assertFailsWith { + future.getOrThrow() + } + } +} diff --git a/perftestflows/src/test/kotlin/net/corda/ptflows/contracts/flows/CashIssueFlowTests.kt b/perftestflows/src/test/kotlin/net/corda/ptflows/contracts/flows/CashIssueFlowTests.kt new file mode 100644 index 0000000000..318b1cc53b --- /dev/null +++ b/perftestflows/src/test/kotlin/net/corda/ptflows/contracts/flows/CashIssueFlowTests.kt @@ -0,0 +1,69 @@ +package net.corda.ptflows.contracts.flows + +import net.corda.core.identity.Party +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.ptflows.DOLLARS +import net.corda.ptflows.`issued by` +import net.corda.ptflows.contracts.asset.PtCash +import net.corda.node.internal.StartedNode +import net.corda.ptflows.flows.PtCashIssueFlow +import net.corda.testing.chooseIdentity +import net.corda.testing.getDefaultNotary +import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin +import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockNetwork.MockNode +import net.corda.testing.setCordappPackages +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class CashIssueFlowTests { + private lateinit var mockNet : MockNetwork + private lateinit var bankOfCordaNode: StartedNode + private lateinit var bankOfCorda: Party + private lateinit var notaryNode: StartedNode + private lateinit var notary: Party + + @Before + fun start() { + setCordappPackages("net.corda.ptflows.contracts.asset") + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + val nodes = mockNet.createSomeNodes(1) + notaryNode = nodes.notaryNode + bankOfCordaNode = nodes.partyNodes[0] + bankOfCorda = bankOfCordaNode.info.chooseIdentity() + + mockNet.runNetwork() + notary = bankOfCordaNode.services.getDefaultNotary() + } + + @After + fun cleanUp() { + mockNet.stopNodes() + } + + @Test + fun `issue some cash`() { + val expected = 500.DOLLARS + val ref = OpaqueBytes.of(0x01) + val future = bankOfCordaNode.services.startFlow(PtCashIssueFlow(expected, ref, notary)).resultFuture + mockNet.runNetwork() + val issueTx = future.getOrThrow().stx + val output = issueTx.tx.outputsOfType().single() + assertEquals(expected.`issued by`(bankOfCorda.ref(ref)), output.amount) + } + + @Test + fun `issue zero cash`() { + val expected = 0.DOLLARS + val ref = OpaqueBytes.of(0x01) + val future = bankOfCordaNode.services.startFlow(PtCashIssueFlow(expected, ref, notary)).resultFuture + mockNet.runNetwork() + assertFailsWith { + future.getOrThrow() + } + } +} diff --git a/perftestflows/src/test/kotlin/net/corda/ptflows/contracts/flows/CashPaymentFlowTests.kt b/perftestflows/src/test/kotlin/net/corda/ptflows/contracts/flows/CashPaymentFlowTests.kt new file mode 100644 index 0000000000..7a2ee8eaf0 --- /dev/null +++ b/perftestflows/src/test/kotlin/net/corda/ptflows/contracts/flows/CashPaymentFlowTests.kt @@ -0,0 +1,121 @@ +package net.corda.ptflows.contracts.flows + +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.ptflows.DOLLARS +import net.corda.ptflows.`issued by` +import net.corda.ptflows.contracts.asset.PtCash +import net.corda.ptflows.flows.PtCashException +import net.corda.ptflows.flows.PtCashIssueFlow +import net.corda.ptflows.flows.PtCashPaymentFlow +import net.corda.node.internal.StartedNode +import net.corda.testing.chooseIdentity +import net.corda.testing.expect +import net.corda.testing.expectEvents +import net.corda.testing.getDefaultNotary +import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin +import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockNetwork.MockNode +import net.corda.testing.setCordappPackages +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class CashPaymentFlowTests { + 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 notaryNode: StartedNode + private lateinit var notary: Party + + @Before + fun start() { + setCordappPackages("net.corda.ptflows.contracts.asset") + mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + val nodes = mockNet.createSomeNodes(1) + notaryNode = nodes.notaryNode + bankOfCordaNode = nodes.partyNodes[0] + bankOfCorda = bankOfCordaNode.info.chooseIdentity() + notary = notaryNode.services.getDefaultNotary() + val future = bankOfCordaNode.services.startFlow(PtCashIssueFlow(initialBalance, ref, notary)).resultFuture + mockNet.runNetwork() + future.getOrThrow() + } + + @After + fun cleanUp() { + mockNet.stopNodes() + } + + @Test + fun `pay some cash`() { + val payTo = notaryNode.info.chooseIdentity() + val expectedPayment = 500.DOLLARS + val expectedChange = 1500.DOLLARS + + bankOfCordaNode.database.transaction { + // Register for vault updates + val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) + val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy(criteria) + val (_, vaultUpdatesBankClient) = notaryNode.services.vaultQueryService.trackBy(criteria) + + val future = bankOfCordaNode.services.startFlow(PtCashPaymentFlow(expectedPayment, + payTo)).resultFuture + mockNet.runNetwork() + future.getOrThrow() + + // Check Bank of Corda vault updates - we take in some issued cash and split it into $500 to the notary + // and $1,500 back to us, so we expect to consume one state, produce one state for our own vault + vaultUpdatesBoc.expectEvents { + expect { update -> + require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" } + require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" } + val changeState = update.produced.single().state.data + assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), changeState.amount) + } + } + + // 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) + } + } + } + } + + @Test + fun `pay more than we have`() { + val payTo = notaryNode.info.chooseIdentity() + val expected = 4000.DOLLARS + val future = bankOfCordaNode.services.startFlow(PtCashPaymentFlow(expected, + payTo)).resultFuture + mockNet.runNetwork() + assertFailsWith { + future.getOrThrow() + } + } + + @Test + fun `pay zero cash`() { + val payTo = notaryNode.info.chooseIdentity() + val expected = 0.DOLLARS + val future = bankOfCordaNode.services.startFlow(PtCashPaymentFlow(expected, + payTo)).resultFuture + mockNet.runNetwork() + assertFailsWith { + future.getOrThrow() + } + } +}