diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt index 5485a7ab45..cea84f6cc0 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt @@ -154,6 +154,7 @@ class Cash : OnLedgerAsset() { fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: CompositeKey, notary: Party) { check(tx.inputStates().isEmpty()) check(tx.outputStates().map { it.data }.sumCashOrNull() == null) + require(amount.quantity > 0) val at = amount.token.issuer tx.addOutputState(TransactionState(State(amount, owner), notary)) tx.addCommand(generateIssueCommand(), at.party.owningKey) diff --git a/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt b/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt index 67f0099766..6ff4d283f3 100644 --- a/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt +++ b/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt @@ -29,6 +29,7 @@ abstract class AbstractConserveAmount, C : CommandData, T : @Throws(InsufficientBalanceException::class) private fun gatherCoins(acceptableCoins: Collection>, amount: Amount): Pair>, Amount> { + require(amount.quantity > 0) { "Cannot gather zero coins" } val gathered = arrayListOf>() var gatheredAmount = Amount(0, amount.token) for (c in acceptableCoins) { diff --git a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt new file mode 100644 index 0000000000..7baac1e431 --- /dev/null +++ b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt @@ -0,0 +1,72 @@ +package net.corda.flows + +import net.corda.contracts.asset.Cash +import net.corda.core.contracts.DOLLARS +import net.corda.core.contracts.`issued by` +import net.corda.core.crypto.Party +import net.corda.core.getOrThrow +import net.corda.core.serialization.OpaqueBytes +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 +import kotlin.test.assertFailsWith + +class CashExitFlowTests { + private val net = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + private val initialBalance = 2000.DOLLARS + private val ref = OpaqueBytes.of(0x01) + private lateinit var bankOfCordaNode: MockNode + private lateinit var bankOfCorda: Party + private lateinit var notaryNode: MockNode + private lateinit var notary: Party + + @Before + fun start() { + val nodes = net.createTwoNodes() + notaryNode = nodes.first + bankOfCordaNode = nodes.second + notary = notaryNode.info.notaryIdentity + bankOfCorda = bankOfCordaNode.info.legalIdentity + + net.runNetwork() + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, + bankOfCorda, + notary)).resultFuture + net.runNetwork() + future.getOrThrow() + } + + @After + fun cleanUp() { + net.stopNodes() + } + + @Test + fun `exit some cash`() { + val exitAmount = 500.DOLLARS + val future = bankOfCordaNode.services.startFlow(CashExitFlow(exitAmount, + ref)).resultFuture + net.runNetwork() + val exitTx = future.getOrThrow().tx + val expected = (initialBalance - exitAmount).`issued by`(bankOfCorda.ref(ref)) + assertEquals(1, exitTx.inputs.size) + assertEquals(1, exitTx.outputs.size) + val output = exitTx.outputs.map { it.data }.filterIsInstance().single() + assertEquals(expected, output.amount) + } + + @Test + fun `exit zero cash`() { + val expected = 0.DOLLARS + val future = bankOfCordaNode.services.startFlow(CashExitFlow(expected, + ref)).resultFuture + net.runNetwork() + assertFailsWith { + future.getOrThrow() + } + } +} diff --git a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt new file mode 100644 index 0000000000..f98ecfc1d3 --- /dev/null +++ b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt @@ -0,0 +1,65 @@ +package net.corda.flows + +import net.corda.contracts.asset.Cash +import net.corda.core.contracts.DOLLARS +import net.corda.core.contracts.`issued by` +import net.corda.core.crypto.Party +import net.corda.core.getOrThrow +import net.corda.core.serialization.OpaqueBytes +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 +import kotlin.test.assertFailsWith + +class CashIssueFlowTests { + private val net = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + private lateinit var bankOfCordaNode: MockNode + private lateinit var bankOfCorda: Party + private lateinit var notaryNode: MockNode + private lateinit var notary: Party + + @Before + fun start() { + val nodes = net.createTwoNodes() + notaryNode = nodes.first + bankOfCordaNode = nodes.second + notary = notaryNode.info.notaryIdentity + bankOfCorda = bankOfCordaNode.info.legalIdentity + + net.runNetwork() + } + + @After + fun cleanUp() { + net.stopNodes() + } + + @Test + fun `issue some cash`() { + val expected = 500.DOLLARS + val ref = OpaqueBytes.of(0x01) + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, + bankOfCorda, + notary)).resultFuture + net.runNetwork() + val issueTx = future.getOrThrow() + val output = issueTx.tx.outputs.single().data as Cash.State + assertEquals(expected.`issued by`(bankOfCorda.ref(ref)), output.amount) + } + + @Test + fun `issue zero cash`() { + val expected = 0.DOLLARS + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, OpaqueBytes.of(0x01), + bankOfCorda, + notary)).resultFuture + net.runNetwork() + assertFailsWith { + future.getOrThrow() + } + } +} diff --git a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt new file mode 100644 index 0000000000..de4b15415c --- /dev/null +++ b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt @@ -0,0 +1,86 @@ +package net.corda.flows + +import net.corda.contracts.asset.Cash +import net.corda.core.contracts.DOLLARS +import net.corda.core.contracts.`issued by` +import net.corda.core.crypto.Party +import net.corda.core.getOrThrow +import net.corda.core.serialization.OpaqueBytes +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 java.util.concurrent.ExecutionException +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class CashPaymentFlowTests { + private val net = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + private val initialBalance = 2000.DOLLARS + private val ref = OpaqueBytes.of(0x01) + private lateinit var bankOfCordaNode: MockNode + private lateinit var bankOfCorda: Party + private lateinit var notaryNode: MockNode + private lateinit var notary: Party + + @Before + fun start() { + val nodes = net.createTwoNodes() + notaryNode = nodes.first + bankOfCordaNode = nodes.second + notary = notaryNode.info.notaryIdentity + bankOfCorda = bankOfCordaNode.info.legalIdentity + + net.runNetwork() + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, + bankOfCorda, + notary)).resultFuture + net.runNetwork() + future.getOrThrow() + } + + @After + fun cleanUp() { + net.stopNodes() + } + + @Test + fun `pay some cash`() { + val payTo = notaryNode.info.legalIdentity + val expected = 500.DOLLARS + val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expected, + payTo)).resultFuture + net.runNetwork() + val paymentTx = future.getOrThrow() + val states = paymentTx.tx.outputs.map { it.data }.filterIsInstance() + val ourState = states.single { it.owner != payTo.owningKey } + val paymentState = states.single { it.owner == payTo.owningKey } + assertEquals(expected.`issued by`(bankOfCorda.ref(ref)), paymentState.amount) + } + + @Test + fun `pay more than we have`() { + val payTo = notaryNode.info.legalIdentity + val expected = 4000.DOLLARS + val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expected, + payTo)).resultFuture + net.runNetwork() + assertFailsWith { + future.getOrThrow() + } + } + + @Test + fun `pay zero cash`() { + val payTo = notaryNode.info.legalIdentity + val expected = 0.DOLLARS + val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expected, + payTo)).resultFuture + net.runNetwork() + assertFailsWith { + future.getOrThrow() + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 74163bd403..ae36165e32 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -528,9 +528,11 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P * @param amount the amount to gather states up to. * @throws InsufficientBalanceException if there isn't enough value in the states to cover the requested amount. */ + // TODO: Merge this with the function in [AbstractConserveAmount] @Throws(InsufficientBalanceException::class) private fun gatherCoins(acceptableCoins: Collection>, amount: Amount): Pair>, Amount> { + require(amount.quantity > 0) { "Cannot gather zero coins" } val gathered = arrayListOf>() var gatheredAmount = Amount(0, amount.token) for (c in acceptableCoins) {