diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/Cash.kt b/contracts/src/main/kotlin/com/r3corda/contracts/Cash.kt index b78687eb46..1c24eaf4fb 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/Cash.kt @@ -259,10 +259,17 @@ class Cash : Contract { // Small DSL extensions. -/** Sums the cash states in the list that are owned by the given key, throwing an exception if there are none. */ +/** + * Sums the cash states in the list belonging to a single owner, throwing an exception + * if there are none, or if any of the cash states cannot be added together (i.e. are + * different currencies). + */ fun Iterable<ContractState>.sumCashBy(owner: PublicKey) = filterIsInstance<Cash.State>().filter { it.owner == owner }.map { it.amount }.sumOrThrow() -/** Sums the cash states in the list, throwing an exception if there are none. */ +/** + * Sums the cash states in the list, throwing an exception if there are none, or if any of the cash + * states cannot be added together (i.e. are different currencies). + */ fun Iterable<ContractState>.sumCash() = filterIsInstance<Cash.State>().map { it.amount }.sumOrThrow() /** Sums the cash states in the list, returning null if there are none. */ diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CashTests.kt index 0b84ac1377..d034124a20 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CashTests.kt @@ -1,6 +1,10 @@ import com.r3corda.contracts.Cash import com.r3corda.contracts.DummyContract import com.r3corda.contracts.InsufficientBalanceException +import com.r3corda.contracts.sumCash +import com.r3corda.contracts.sumCashBy +import com.r3corda.contracts.sumCashOrNull +import com.r3corda.contracts.sumCashOrZero import com.r3corda.contracts.testing.`issued by` import com.r3corda.contracts.testing.`owned by` import com.r3corda.core.contracts.* @@ -14,6 +18,7 @@ import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals +import kotlin.test.assertNull import kotlin.test.assertTrue class CashTests { @@ -170,6 +175,25 @@ class CashTests { } } + /** + * Test that the issuance builder rejects building into a transaction with existing + * cash inputs. + */ + @Test(expected = IllegalStateException::class) + fun `reject issuance with inputs`() { + // Issue some cash + var ptx = TransactionBuilder() + + Cash().generateIssue(ptx, 100.DOLLARS, MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + ptx.signWith(MINI_CORP_KEY) + val tx = ptx.toSignedTransaction() + + // Include the previously issued cash in a new issuance command + ptx = TransactionBuilder() + ptx.addInputState(tx.tx.outRef<Cash.State>(0).ref) + Cash().generateIssue(ptx, 100.DOLLARS, MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + } + @Test fun testMergeSplit() { // Splitting value works. @@ -456,4 +480,59 @@ class CashTests { assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, fiveThousandDollarsFromMega.copy(deposit = MEGA_CORP.ref(1)).issuanceDef) assertNotEquals(fiveThousandDollarsFromMega.copy(deposit = MEGA_CORP.ref(1)).issuanceDef, fiveThousandDollarsFromMega.issuanceDef) } + + @Test + fun `summing by owner`() { + val states = listOf( + Cash.State(MEGA_CORP.ref(1), 1000.DOLLARS, MINI_CORP_PUBKEY, DUMMY_NOTARY), + Cash.State(MEGA_CORP.ref(1), 2000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY), + Cash.State(MEGA_CORP.ref(1), 4000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + ) + assertEquals(6000.DOLLARS, states.sumCashBy(MEGA_CORP_PUBKEY)) + } + + @Test(expected = UnsupportedOperationException::class) + fun `summing by owner throws`() { + val states = listOf( + Cash.State(MEGA_CORP.ref(1), 2000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY), + Cash.State(MEGA_CORP.ref(1), 4000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + ) + states.sumCashBy(MINI_CORP_PUBKEY) + } + + @Test + fun `summing no currencies`() { + val states = emptyList<Cash.State>() + assertEquals(0.POUNDS, states.sumCashOrZero(GBP)) + assertNull(states.sumCashOrNull()) + } + + @Test(expected = UnsupportedOperationException::class) + fun `summing no currencies throws`() { + val states = emptyList<Cash.State>() + states.sumCash() + } + + @Test + fun `summing a single currency`() { + val states = listOf( + Cash.State(MEGA_CORP.ref(1), 1000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY), + Cash.State(MEGA_CORP.ref(1), 2000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY), + Cash.State(MEGA_CORP.ref(1), 4000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + ) + // Test that summing everything produces the total number of dollars + var expected = 7000.DOLLARS + var actual = states.sumCash() + assertEquals(expected, actual) + } + + @Test(expected = IllegalArgumentException::class) + fun `summing multiple currencies`() { + val states = listOf( + Cash.State(MEGA_CORP.ref(1), 1000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY), + Cash.State(MEGA_CORP.ref(1), 4000.POUNDS, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + ) + // Test that summing everything fails because we're mixing units + states.sumCash() + } }