Merged in rnicoll-ricardian (pull request #90)

Add issuance definition for cash contract
This commit is contained in:
Ross Nicoll 2016-05-13 14:57:46 +01:00
commit 3f3ab74f1e
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)
}
}