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.
This commit is contained in:
Ross Nicoll 2016-05-11 14:18:54 +01:00
parent 3ee601360e
commit 25e2c4bc4d
5 changed files with 101 additions and 14 deletions

View File

@ -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.
*/

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}
}
}

View File

@ -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)
}
}