From 25e2c4bc4dd5847a938158efb0e3a2031bac2d48 Mon Sep 17 00:00:00 2001 From: Ross Nicoll <ross.nicoll@r3cev.com> Date: Wed, 11 May 2016 14:18:54 +0100 Subject: [PATCH] Add issuance definition for cash contract Add issuance definition for cash contract, as well as common interfaces to support later extensions. The issuance definition encapsulates the core values for state objects when issued, and essentially acts as the Ricardian contract for Corda states. --- contracts/src/main/kotlin/contracts/Cash.kt | 29 +++++++++--- .../contracts/cash/CashIssuanceDefinition.kt | 15 +++++++ .../kotlin/contracts/cash/CommonCashState.kt | 15 +++++++ core/src/main/kotlin/core/Structures.kt | 11 ++++- src/test/kotlin/contracts/CashTests.kt | 45 +++++++++++++++---- 5 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 contracts/src/main/kotlin/contracts/cash/CashIssuanceDefinition.kt create mode 100644 contracts/src/main/kotlin/contracts/cash/CommonCashState.kt diff --git a/contracts/src/main/kotlin/contracts/Cash.kt b/contracts/src/main/kotlin/contracts/Cash.kt index 45cd1efbbb..0e129adcff 100644 --- a/contracts/src/main/kotlin/contracts/Cash.kt +++ b/contracts/src/main/kotlin/contracts/Cash.kt @@ -1,5 +1,7 @@ package contracts +import contracts.cash.CashIssuanceDefinition +import contracts.cash.CommonCashState import core.* import core.crypto.SecureHash import core.crypto.toStringShort @@ -45,17 +47,27 @@ class Cash : Contract { */ override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") + data class IssuanceDefinition( + /** Where the underlying currency backing this ledger entry can be found (propagated) */ + override val deposit: PartyAndReference, + + override val currency: Currency + ) : CashIssuanceDefinition + /** A state representing a cash claim against some party */ data class State( /** Where the underlying currency backing this ledger entry can be found (propagated) */ - val deposit: PartyAndReference, + override val deposit: PartyAndReference, - val amount: Amount, + override val amount: Amount, /** There must be a MoveCommand signed by this key to claim the amount */ override val owner: PublicKey - ) : OwnableState { + ) : CommonCashState<Cash.IssuanceDefinition> { + override val issuanceDef: Cash.IssuanceDefinition + get() = Cash.IssuanceDefinition(deposit, amount.currency) override val contract = CASH_PROGRAM_ID + override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})" override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) @@ -82,11 +94,12 @@ class Cash : Contract { override fun verify(tx: TransactionForVerification) { // Each group is a set of input/output states with distinct (deposit, currency) attributes. These types // of cash are not fungible and must be kept separated for bookkeeping purposes. - val groups = tx.groupStates() { it: Cash.State -> Pair(it.deposit, it.amount.currency) } + val groups = tx.groupStates() { it: Cash.State -> it.issuanceDef } for ((inputs, outputs, key) in groups) { // Either inputs or outputs could be empty. - val (deposit, currency) = key + val deposit = key.deposit + val currency = key.currency val issuer = deposit.party requireThat { @@ -144,6 +157,12 @@ class Cash : Contract { } } + /** + * Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey. + */ + fun generateIssue(tx: TransactionBuilder, issuanceDef: CashIssuanceDefinition, pennies: Long, owner: PublicKey) + = generateIssue(tx, Amount(pennies, issuanceDef.currency), issuanceDef.deposit, owner) + /** * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. */ diff --git a/contracts/src/main/kotlin/contracts/cash/CashIssuanceDefinition.kt b/contracts/src/main/kotlin/contracts/cash/CashIssuanceDefinition.kt new file mode 100644 index 0000000000..9519289a04 --- /dev/null +++ b/contracts/src/main/kotlin/contracts/cash/CashIssuanceDefinition.kt @@ -0,0 +1,15 @@ +package contracts.cash + +import core.IssuanceDefinition +import core.PartyAndReference +import java.util.* + +/** + * Subset of cash-like contract state, containing the issuance definition. If these definitions match for two + * contracts' states, those states can be aggregated. + */ +interface CashIssuanceDefinition : IssuanceDefinition { + /** Where the underlying currency backing this ledger entry can be found (propagated) */ + val deposit: PartyAndReference + val currency: Currency +} \ No newline at end of file diff --git a/contracts/src/main/kotlin/contracts/cash/CommonCashState.kt b/contracts/src/main/kotlin/contracts/cash/CommonCashState.kt new file mode 100644 index 0000000000..cce0e1871b --- /dev/null +++ b/contracts/src/main/kotlin/contracts/cash/CommonCashState.kt @@ -0,0 +1,15 @@ +package contracts.cash + +import core.Amount +import core.OwnableState +import core.PartyAndReference + +/** + * Common elements of cash contract states. + */ +interface CommonCashState<I : CashIssuanceDefinition> : OwnableState { + val issuanceDef: I + /** Where the underlying currency backing this ledger entry can be found (propagated) */ + val deposit: PartyAndReference + val amount: Amount +} \ No newline at end of file diff --git a/core/src/main/kotlin/core/Structures.kt b/core/src/main/kotlin/core/Structures.kt index 328243ae93..8c9a8e9b1c 100644 --- a/core/src/main/kotlin/core/Structures.kt +++ b/core/src/main/kotlin/core/Structures.kt @@ -29,6 +29,15 @@ interface ContractState { val contract: Contract } +/** + * Marker interface for data classes that represent the issuance state for a contract. These are intended as templates + * from which the state object is initialised. + */ +interface IssuanceDefinition + +/** + * A contract state that can have a single owner. + */ interface OwnableState : ContractState { /** There must be a MoveCommand signed by this key to claim the amount */ val owner: PublicKey @@ -189,4 +198,4 @@ interface Attachment : NamedByHash { } throw FileNotFoundException() } -} \ No newline at end of file +} diff --git a/src/test/kotlin/contracts/CashTests.kt b/src/test/kotlin/contracts/CashTests.kt index 29cac0a74e..bc54a2f859 100644 --- a/src/test/kotlin/contracts/CashTests.kt +++ b/src/test/kotlin/contracts/CashTests.kt @@ -1,14 +1,7 @@ -/* - * Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members - * pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms - * set forth therein. - * - * All other rights reserved. - */ - import contracts.Cash import contracts.DummyContract import contracts.InsufficientBalanceException +import contracts.cash.CashIssuanceDefinition import core.* import core.crypto.SecureHash import core.serialization.OpaqueBytes @@ -18,6 +11,7 @@ import java.security.PublicKey import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals import kotlin.test.assertTrue class CashTests { @@ -110,6 +104,13 @@ class CashTests { assertTrue(ptx.commands()[0].value is Cash.Commands.Issue) assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0]) + // Test issuance from the issuance definition + val issuanceDef = Cash.IssuanceDefinition(MINI_CORP.ref(12, 34), USD) + val templatePtx = TransactionBuilder() + Cash().generateIssue(templatePtx, issuanceDef, 100.DOLLARS.pennies, owner = DUMMY_PUBKEY_1) + assertTrue(templatePtx.inputStates().isEmpty()) + assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0]) + // We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer. transaction { input { inState } @@ -423,4 +424,32 @@ class CashTests { makeSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1) } } + + /** + * Confirm that aggregation of states is correctly modelled. + */ + @Test + fun aggregation() { + val fiveThousandDollarsFromMega = Cash.State(MEGA_CORP.ref(2), 5000.DOLLARS, MEGA_CORP_PUBKEY) + val twoThousandDollarsFromMega = Cash.State(MEGA_CORP.ref(2), 2000.DOLLARS, MINI_CORP_PUBKEY) + val oneThousandDollarsFromMini = Cash.State(MINI_CORP.ref(3), 1000.DOLLARS, MEGA_CORP_PUBKEY) + + // Obviously it must be possible to aggregate states with themselves + assertEquals(fiveThousandDollarsFromMega.issuanceDef, fiveThousandDollarsFromMega.issuanceDef) + + // Owner is not considered when calculating whether it is possible to aggregate states + assertEquals(fiveThousandDollarsFromMega.issuanceDef, twoThousandDollarsFromMega.issuanceDef) + + // States cannot be aggregated if the deposit differs + assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, oneThousandDollarsFromMini.issuanceDef) + assertNotEquals(twoThousandDollarsFromMega.issuanceDef, oneThousandDollarsFromMini.issuanceDef) + + // States cannot be aggregated if the currency differs + assertNotEquals(oneThousandDollarsFromMini.issuanceDef, + Cash.State(MINI_CORP.ref(3), 1000.POUNDS, MEGA_CORP_PUBKEY).issuanceDef) + + // States cannot be aggregated if the reference differs + assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, fiveThousandDollarsFromMega.copy(deposit = MEGA_CORP.ref(1)).issuanceDef) + assertNotEquals(fiveThousandDollarsFromMega.copy(deposit = MEGA_CORP.ref(1)).issuanceDef, fiveThousandDollarsFromMega.issuanceDef) + } }