mirror of
https://github.com/corda/corda.git
synced 2025-01-18 02:39:51 +00:00
More moving around and renaming, to have a consistent style between contracts.
This commit is contained in:
parent
0856894047
commit
5022f11d9e
@ -11,37 +11,8 @@ import java.time.Instant
|
||||
// Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode.
|
||||
val CASH_PROGRAM_ID = SecureHash.sha256("cash")
|
||||
|
||||
/** A state representing a claim on the cash reserves of some institution */
|
||||
data class CashState(
|
||||
/** Where the underlying currency backing this ledger entry can be found (propagated) */
|
||||
val deposit: InstitutionReference,
|
||||
|
||||
val amount: Amount,
|
||||
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
val owner: PublicKey
|
||||
) : ContractState {
|
||||
override val programRef = CASH_PROGRAM_ID
|
||||
override fun toString() = "Cash($amount at $deposit owned by $owner)"
|
||||
}
|
||||
|
||||
/** A command proving ownership of some input states, the signature covers the output states. */
|
||||
class MoveCashCommand : Command {
|
||||
override fun equals(other: Any?) = other is MoveCashCommand
|
||||
override fun hashCode() = 0
|
||||
}
|
||||
/** A command stating that money has been withdrawn from the shared ledger and is now accounted for in some other way */
|
||||
class ExitCashCommand(val amount: Amount) : Command {
|
||||
override fun equals(other: Any?) = other is ExitCashCommand && other.amount == amount
|
||||
override fun hashCode() = amount.hashCode()
|
||||
}
|
||||
|
||||
class InsufficientBalanceException(val amountMissing: Amount) : Exception()
|
||||
|
||||
// Small DSL extension.
|
||||
fun Iterable<ContractState>.sumCashBy(owner: PublicKey) = this.filterIsInstance<CashState>().filter { it.owner == owner }.map { it.amount }.sum()
|
||||
fun Iterable<ContractState>.sumCash() = this.filterIsInstance<CashState>().map { it.amount }.sum()
|
||||
|
||||
/**
|
||||
* A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple
|
||||
* input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour
|
||||
@ -55,10 +26,39 @@ fun Iterable<ContractState>.sumCash() = this.filterIsInstance<CashState>().map {
|
||||
* At the same time, other contracts that just want money and don't care much who is currently holding it in their
|
||||
* vaults can ignore the issuer/depositRefs and just examine the amount fields.
|
||||
*/
|
||||
object CashContract : Contract {
|
||||
object Cash : Contract {
|
||||
/** A state representing a claim on the cash reserves of some institution */
|
||||
data class State(
|
||||
/** Where the underlying currency backing this ledger entry can be found (propagated) */
|
||||
val deposit: InstitutionReference,
|
||||
|
||||
val amount: Amount,
|
||||
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
val owner: PublicKey
|
||||
) : ContractState {
|
||||
override val programRef = CASH_PROGRAM_ID
|
||||
override fun toString() = "Cash($amount at $deposit owned by $owner)"
|
||||
}
|
||||
|
||||
|
||||
sealed class Commands {
|
||||
/** A command proving ownership of some input states, the signature covers the output states. */
|
||||
class Move : Command {
|
||||
override fun equals(other: Any?) = other is Move
|
||||
override fun hashCode() = 0
|
||||
}
|
||||
|
||||
/** A command stating that money has been withdrawn from the shared ledger and is now accounted for in some other way */
|
||||
class Exit(val amount: Amount) : Command {
|
||||
override fun equals(other: Any?) = other is Exit && other.amount == amount
|
||||
override fun hashCode() = amount.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
/** This is the function EVERYONE runs */
|
||||
override fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSigned<Command>>, time: Instant) {
|
||||
val cashInputs = inStates.filterIsInstance<CashState>()
|
||||
val cashInputs = inStates.filterIsInstance<Cash.State>()
|
||||
|
||||
requireThat {
|
||||
"there is at least one cash input" by cashInputs.isNotEmpty()
|
||||
@ -69,7 +69,7 @@ object CashContract : Contract {
|
||||
val currency = cashInputs.first().amount.currency
|
||||
|
||||
// Select all the output states that are cash states. There may be zero if all money is being withdrawn.
|
||||
val cashOutputs = outStates.filterIsInstance<CashState>()
|
||||
val cashOutputs = outStates.filterIsInstance<Cash.State>()
|
||||
requireThat {
|
||||
"all outputs use the currency of the inputs" by cashOutputs.all { it.amount.currency == currency }
|
||||
}
|
||||
@ -84,7 +84,7 @@ object CashContract : Contract {
|
||||
val inputAmount = inputs.map { it.amount }.sum()
|
||||
val outputAmount = outputs.map { it.amount }.sumOrZero(currency)
|
||||
|
||||
val issuerCommand = args.select<ExitCashCommand>(institution = deposit.institution).singleOrNull()
|
||||
val issuerCommand = args.select<Commands.Exit>(institution = deposit.institution).singleOrNull()
|
||||
val amountExitingLedger = issuerCommand?.value?.amount ?: Amount(0, inputAmount.currency)
|
||||
|
||||
requireThat {
|
||||
@ -98,7 +98,7 @@ object CashContract : Contract {
|
||||
// see a signature from each of those keys. The actual signatures have been verified against the transaction
|
||||
// data by the platform before execution.
|
||||
val owningPubKeys = cashInputs.map { it.owner }.toSortedSet()
|
||||
val keysThatSigned = args.select<MoveCashCommand>().map { it.signer }.toSortedSet()
|
||||
val keysThatSigned = args.select<Commands.Move>().map { it.signer }.toSortedSet()
|
||||
requireThat {
|
||||
"the owning keys are the same as the signing keys" by (owningPubKeys == keysThatSigned)
|
||||
}
|
||||
@ -111,7 +111,7 @@ object CashContract : Contract {
|
||||
|
||||
/** Generate a transaction that consumes one or more of the given input states to move money to the given pubkey. */
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
fun craftSpend(amount: Amount, to: PublicKey, wallet: List<CashState>): TransactionForTest {
|
||||
fun craftSpend(amount: Amount, to: PublicKey, wallet: List<Cash.State>): TransactionForTest {
|
||||
// Discussion
|
||||
//
|
||||
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.
|
||||
@ -135,9 +135,9 @@ object CashContract : Contract {
|
||||
// is put into the transaction, which is finally returned.
|
||||
|
||||
val currency = amount.currency
|
||||
val coinsOfCurrency = wallet.asSequence().filter { it.amount.currency == currency }
|
||||
val coinsOfCurrency = wallet.filter { it.amount.currency == currency }
|
||||
|
||||
val gathered = arrayListOf<CashState>()
|
||||
val gathered = arrayListOf<State>()
|
||||
var gatheredAmount = Amount(0, currency)
|
||||
for (c in coinsOfCurrency) {
|
||||
if (gatheredAmount >= amount) break
|
||||
@ -154,7 +154,7 @@ object CashContract : Contract {
|
||||
val states = gathered.groupBy { it.deposit }.map {
|
||||
val (deposit, coins) = it
|
||||
val totalAmount = coins.map { it.amount }.sum()
|
||||
CashState(deposit, totalAmount, to)
|
||||
State(deposit, totalAmount, to)
|
||||
}
|
||||
|
||||
val outputs = if (change.pennies > 0) {
|
||||
@ -165,12 +165,16 @@ object CashContract : Contract {
|
||||
// Add a change output and adjust the last output downwards.
|
||||
states.subList(0, states.lastIndex) +
|
||||
states.last().let { it.copy(amount = it.amount - change) } +
|
||||
CashState(gathered.last().deposit, change, changeKey)
|
||||
State(gathered.last().deposit, change, changeKey)
|
||||
} else states
|
||||
|
||||
// Finally, generate the commands. Pretend to sign here, real signatures aren't done yet.
|
||||
val commands = keysUsed.map { VerifiedSigned(it, null, MoveCashCommand()) }
|
||||
val commands = keysUsed.map { VerifiedSigned(it, null, Commands.Move()) }
|
||||
|
||||
return TransactionForTest(gathered.toArrayList(), outputs.toArrayList(), commands.toArrayList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Small DSL extension.
|
||||
fun Iterable<ContractState>.sumCashBy(owner: PublicKey) = filterIsInstance<Cash.State>().filter { it.owner == owner }.map { it.amount }.sum()
|
||||
fun Iterable<ContractState>.sumCash() = filterIsInstance<Cash.State>().map { it.amount }.sum()
|
||||
|
@ -21,13 +21,13 @@ import java.time.Instant
|
||||
|
||||
val CP_PROGRAM_ID = SecureHash.sha256("comedy-paper")
|
||||
|
||||
// TODO: Generalise the notion of an owned object into a superclass/supercontract. Consider composition vs inheritance.
|
||||
// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance.
|
||||
object ComedyPaper : Contract {
|
||||
data class State(
|
||||
val issuance: InstitutionReference,
|
||||
val owner: PublicKey,
|
||||
val faceValue: Amount,
|
||||
val maturityDate: Instant
|
||||
val issuance: InstitutionReference,
|
||||
val owner: PublicKey,
|
||||
val faceValue: Amount,
|
||||
val maturityDate: Instant
|
||||
) : ContractState {
|
||||
override val programRef = CP_PROGRAM_ID
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import contracts.*
|
||||
import contracts.Cash
|
||||
import contracts.InsufficientBalanceException
|
||||
import core.*
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
@ -9,16 +10,16 @@ import kotlin.test.assertFailsWith
|
||||
// 2. There must be at least one input state (note: not "one of the type the contract wants")
|
||||
|
||||
class CashTests {
|
||||
val inState = CashState(
|
||||
val inState = Cash.State(
|
||||
deposit = InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)),
|
||||
amount = 1000.DOLLARS,
|
||||
owner = DUMMY_PUBKEY_1
|
||||
)
|
||||
val outState = inState.copy(owner = DUMMY_PUBKEY_2)
|
||||
val contract = CashContract
|
||||
val contract = Cash
|
||||
|
||||
fun CashState.editInstitution(institution: Institution) = copy(deposit = deposit.copy(institution = institution))
|
||||
fun CashState.editDepositRef(ref: Byte) = copy(deposit = deposit.copy(reference = OpaqueBytes.of(ref)))
|
||||
fun Cash.State.editInstitution(institution: Institution) = copy(deposit = deposit.copy(institution = institution))
|
||||
fun Cash.State.editDepositRef(ref: Byte) = copy(deposit = deposit.copy(reference = OpaqueBytes.of(ref)))
|
||||
|
||||
@Test
|
||||
fun trivial() {
|
||||
@ -39,7 +40,7 @@ class CashTests {
|
||||
}
|
||||
transaction {
|
||||
output { outState }
|
||||
arg(DUMMY_PUBKEY_2) { MoveCashCommand() }
|
||||
arg(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
|
||||
contract `fails requirement` "the owning keys are the same as the signing keys"
|
||||
}
|
||||
transaction {
|
||||
@ -50,7 +51,7 @@ class CashTests {
|
||||
// Simple reallocation works.
|
||||
transaction {
|
||||
output { outState }
|
||||
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
contract.accepts()
|
||||
}
|
||||
}
|
||||
@ -60,7 +61,7 @@ class CashTests {
|
||||
fun testMergeSplit() {
|
||||
// Splitting value works.
|
||||
transaction {
|
||||
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
transaction {
|
||||
input { inState }
|
||||
for (i in 1..4) output { inState.copy(amount = inState.amount / 4) }
|
||||
@ -150,16 +151,16 @@ class CashTests {
|
||||
output { outState.copy(amount = inState.amount - 200.DOLLARS) }
|
||||
|
||||
transaction {
|
||||
arg(MEGA_CORP_KEY) { ExitCashCommand(100.DOLLARS) }
|
||||
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(100.DOLLARS) }
|
||||
contract `fails requirement` "the amounts balance"
|
||||
}
|
||||
|
||||
transaction {
|
||||
arg(MEGA_CORP_KEY) { ExitCashCommand(200.DOLLARS) }
|
||||
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
|
||||
contract `fails requirement` "the owning keys are the same as the signing keys" // No move command.
|
||||
|
||||
transaction {
|
||||
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
contract.accepts()
|
||||
}
|
||||
}
|
||||
@ -172,14 +173,14 @@ class CashTests {
|
||||
output { inState.copy(amount = inState.amount - 200.DOLLARS).editInstitution(MINI_CORP) }
|
||||
output { inState.copy(amount = inState.amount - 200.DOLLARS) }
|
||||
|
||||
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
|
||||
contract `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||
|
||||
arg(MEGA_CORP_KEY) { ExitCashCommand(200.DOLLARS) }
|
||||
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
|
||||
contract `fails requirement` "at issuer MiniCorp the amounts balance"
|
||||
|
||||
arg(MINI_CORP_KEY) { ExitCashCommand(200.DOLLARS) }
|
||||
arg(MINI_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
|
||||
contract.accepts()
|
||||
}
|
||||
}
|
||||
@ -206,7 +207,7 @@ class CashTests {
|
||||
// This works.
|
||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
||||
output { inState.copy(owner = DUMMY_PUBKEY_2).editInstitution(MINI_CORP) }
|
||||
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
contract.accepts()
|
||||
}
|
||||
|
||||
@ -223,10 +224,10 @@ class CashTests {
|
||||
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
|
||||
val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2
|
||||
val WALLET = listOf(
|
||||
CashState(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 100.DOLLARS, OUR_PUBKEY_1),
|
||||
CashState(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 400.DOLLARS, OUR_PUBKEY_1),
|
||||
CashState(InstitutionReference(MINI_CORP, OpaqueBytes.of(1)), 80.DOLLARS, OUR_PUBKEY_1),
|
||||
CashState(InstitutionReference(MINI_CORP, OpaqueBytes.of(2)), 80.SWISS_FRANCS, OUR_PUBKEY_1)
|
||||
Cash.State(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 100.DOLLARS, OUR_PUBKEY_1),
|
||||
Cash.State(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 400.DOLLARS, OUR_PUBKEY_1),
|
||||
Cash.State(InstitutionReference(MINI_CORP, OpaqueBytes.of(1)), 80.DOLLARS, OUR_PUBKEY_1),
|
||||
Cash.State(InstitutionReference(MINI_CORP, OpaqueBytes.of(2)), 80.SWISS_FRANCS, OUR_PUBKEY_1)
|
||||
)
|
||||
|
||||
@Test
|
||||
@ -235,7 +236,7 @@ class CashTests {
|
||||
transaction {
|
||||
input { WALLET[0] }
|
||||
output { WALLET[0].copy(owner = THEIR_PUBKEY_1) }
|
||||
arg(OUR_PUBKEY_1) { MoveCashCommand() }
|
||||
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
|
||||
},
|
||||
contract.craftSpend(100.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
||||
)
|
||||
@ -248,7 +249,7 @@ class CashTests {
|
||||
input { WALLET[0] }
|
||||
output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS) }
|
||||
output { WALLET[0].copy(owner = OUR_PUBKEY_1, amount = 90.DOLLARS) }
|
||||
arg(OUR_PUBKEY_1) { MoveCashCommand() }
|
||||
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
|
||||
},
|
||||
contract.craftSpend(10.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
||||
)
|
||||
@ -261,7 +262,7 @@ class CashTests {
|
||||
input { WALLET[0] }
|
||||
input { WALLET[1] }
|
||||
output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS) }
|
||||
arg(OUR_PUBKEY_1) { MoveCashCommand() }
|
||||
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
|
||||
},
|
||||
contract.craftSpend(500.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
||||
)
|
||||
@ -276,7 +277,7 @@ class CashTests {
|
||||
input { WALLET[2] }
|
||||
output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS) }
|
||||
output { WALLET[2].copy(owner = THEIR_PUBKEY_1) }
|
||||
arg(OUR_PUBKEY_1) { MoveCashCommand() }
|
||||
arg(OUR_PUBKEY_1) { Cash.Commands.Move() }
|
||||
},
|
||||
contract.craftSpend(580.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user