mirror of
https://github.com/corda/corda.git
synced 2024-12-20 05:28:21 +00:00
Cash contract: Introduce a DepositPointer abstraction for easier grouping
In this current model, you cannot mix up money from different deposits: they must always be kept separate.
This commit is contained in:
parent
fd67a85c29
commit
a09120d445
22
src/Cash.kt
22
src/Cash.kt
@ -11,13 +11,16 @@ import java.util.*
|
|||||||
// Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode.
|
// 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")
|
val CASH_PROGRAM_ID = SecureHash.sha256("cash")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to some money being stored by an institution e.g. in a vault or (more likely) on their normal ledger.
|
||||||
|
* The deposit reference is intended to be encrypted so it's meaningless to anyone other than the institution.
|
||||||
|
*/
|
||||||
|
data class DepositPointer(val institution: Institution, val reference: OpaqueBytes)
|
||||||
|
|
||||||
/** A state representing a claim on the cash reserves of some institution */
|
/** A state representing a claim on the cash reserves of some institution */
|
||||||
data class CashState(
|
data class CashState(
|
||||||
/** The institution that has this original cash deposit (propagated) */
|
/** Where the underlying currency backing this ledger entry can be found (propagated) */
|
||||||
val issuingInstitution: Institution,
|
val deposit: DepositPointer,
|
||||||
|
|
||||||
/** Whatever internal ID the bank needs in order to locate that deposit, may be encrypted (propagated) */
|
|
||||||
val depositReference: OpaqueBytes,
|
|
||||||
|
|
||||||
val amount: Amount,
|
val amount: Amount,
|
||||||
|
|
||||||
@ -68,19 +71,18 @@ class CashContract : Contract {
|
|||||||
// For each deposit that's represented in the inputs, group the inputs together and verify that the outputs
|
// For each deposit that's represented in the inputs, group the inputs together and verify that the outputs
|
||||||
// balance, taking into account a possible exit command from that issuer.
|
// balance, taking into account a possible exit command from that issuer.
|
||||||
var outputsLeft = cashOutputs.size
|
var outputsLeft = cashOutputs.size
|
||||||
for ((pair, inputs) in cashInputs.groupBy { Pair(it.issuingInstitution, it.depositReference) }) {
|
for ((deposit, inputs) in cashInputs.groupBy { it.deposit }) {
|
||||||
val (issuer, depositRef) = pair
|
val outputs = cashOutputs.filter { it.deposit == deposit }
|
||||||
val outputs = cashOutputs.filter { it.issuingInstitution == issuer && it.depositReference == depositRef }
|
|
||||||
outputsLeft -= outputs.size
|
outputsLeft -= outputs.size
|
||||||
|
|
||||||
val inputAmount = inputs.map { it.amount }.sum()
|
val inputAmount = inputs.map { it.amount }.sum()
|
||||||
val outputAmount = outputs.map { it.amount }.sumOrZero(currency)
|
val outputAmount = outputs.map { it.amount }.sumOrZero(currency)
|
||||||
|
|
||||||
val issuerCommand = args.filter { it.signingInstitution == issuer }.map { it.command as? ExitCashCommand }.filterNotNull().singleOrNull()
|
val issuerCommand = args.filter { it.signingInstitution == deposit.institution }.map { it.command as? ExitCashCommand }.filterNotNull().singleOrNull()
|
||||||
val amountExitingLedger = issuerCommand?.amount ?: Amount(0, inputAmount.currency)
|
val amountExitingLedger = issuerCommand?.amount ?: Amount(0, inputAmount.currency)
|
||||||
|
|
||||||
requireThat {
|
requireThat {
|
||||||
"for deposit $depositRef at issuer ${issuer.name} the amounts balance" by (inputAmount == outputAmount + amountExitingLedger)
|
"for deposit ${deposit.reference} at issuer ${deposit.institution.name} the amounts balance" by (inputAmount == outputAmount + amountExitingLedger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,15 +8,16 @@ import kotlin.test.assertFailsWith
|
|||||||
|
|
||||||
class CashTests {
|
class CashTests {
|
||||||
val inState = CashState(
|
val inState = CashState(
|
||||||
issuingInstitution = MEGA_CORP,
|
deposit = DepositPointer(MEGA_CORP, OpaqueBytes.of(1)),
|
||||||
depositReference = OpaqueBytes.of(1),
|
|
||||||
amount = 1000.DOLLARS,
|
amount = 1000.DOLLARS,
|
||||||
owner = DUMMY_PUBKEY_1
|
owner = DUMMY_PUBKEY_1
|
||||||
)
|
)
|
||||||
val outState = inState.copy(owner = DUMMY_PUBKEY_2)
|
val outState = inState.copy(owner = DUMMY_PUBKEY_2)
|
||||||
|
|
||||||
val contract = CashContract()
|
val contract = CashContract()
|
||||||
|
|
||||||
|
fun CashState.editInstitution(institution: Institution) = copy(deposit = deposit.copy(institution = institution))
|
||||||
|
fun CashState.editDepositRef(ref: Byte) = copy(deposit = deposit.copy(reference = OpaqueBytes.of(ref)))
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun trivial() {
|
fun trivial() {
|
||||||
transaction {
|
transaction {
|
||||||
@ -41,7 +42,7 @@ class CashTests {
|
|||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
output { outState }
|
output { outState }
|
||||||
output { outState.copy(issuingInstitution = MINI_CORP) }
|
output { outState.editInstitution(MINI_CORP) }
|
||||||
contract `fails requirement` "no output states are unaccounted for"
|
contract `fails requirement` "no output states are unaccounted for"
|
||||||
}
|
}
|
||||||
// Simple reallocation works.
|
// Simple reallocation works.
|
||||||
@ -95,14 +96,14 @@ class CashTests {
|
|||||||
// Can't change issuer.
|
// Can't change issuer.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { outState.copy(issuingInstitution = MINI_CORP) }
|
output { outState.editInstitution(MINI_CORP) }
|
||||||
contract `fails requirement` "at issuer MegaCorp the amounts balance"
|
contract `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||||
}
|
}
|
||||||
// Can't change deposit reference when splitting.
|
// Can't change deposit reference when splitting.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { outState.copy(depositReference = OpaqueBytes.of(0), amount = inState.amount / 2) }
|
output { outState.editDepositRef(0).copy(amount = inState.amount / 2) }
|
||||||
output { outState.copy(depositReference = OpaqueBytes.of(1), amount = inState.amount / 2) }
|
output { outState.editDepositRef(1).copy(amount = inState.amount / 2) }
|
||||||
contract `fails requirement` "for deposit [01] at issuer MegaCorp the amounts balance"
|
contract `fails requirement` "for deposit [01] at issuer MegaCorp the amounts balance"
|
||||||
}
|
}
|
||||||
// Can't mix currencies.
|
// Can't mix currencies.
|
||||||
@ -126,15 +127,15 @@ class CashTests {
|
|||||||
// Can't have superfluous input states from different issuers.
|
// Can't have superfluous input states from different issuers.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
input { inState.copy(issuingInstitution = MINI_CORP) }
|
input { inState.editInstitution(MINI_CORP) }
|
||||||
output { outState }
|
output { outState }
|
||||||
contract `fails requirement` "at issuer MiniCorp the amounts balance"
|
contract `fails requirement` "at issuer MiniCorp the amounts balance"
|
||||||
}
|
}
|
||||||
// Can't combine two different deposits at the same issuer.
|
// Can't combine two different deposits at the same issuer.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
input { inState.copy(depositReference = OpaqueBytes.of(3)) }
|
input { inState.editDepositRef(3) }
|
||||||
output { outState.copy(amount = inState.amount * 2, depositReference = OpaqueBytes.of(3)) }
|
output { outState.copy(amount = inState.amount * 2).editDepositRef(3) }
|
||||||
contract `fails requirement` "for deposit [01]"
|
contract `fails requirement` "for deposit [01]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -164,9 +165,9 @@ class CashTests {
|
|||||||
// Multi-issuer case.
|
// Multi-issuer case.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
input { inState.copy(issuingInstitution = MINI_CORP) }
|
input { inState.editInstitution(MINI_CORP) }
|
||||||
|
|
||||||
output { inState.copy(issuingInstitution = MINI_CORP, amount = inState.amount - 200.DOLLARS) }
|
output { inState.copy(amount = inState.amount - 200.DOLLARS).editInstitution(MINI_CORP) }
|
||||||
output { inState.copy(amount = inState.amount - 200.DOLLARS) }
|
output { inState.copy(amount = inState.amount - 200.DOLLARS) }
|
||||||
|
|
||||||
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
||||||
@ -186,7 +187,7 @@ class CashTests {
|
|||||||
transaction {
|
transaction {
|
||||||
// Gather 2000 dollars from two different issuers.
|
// Gather 2000 dollars from two different issuers.
|
||||||
input { inState }
|
input { inState }
|
||||||
input { inState.copy(issuingInstitution = MINI_CORP) }
|
input { inState.editInstitution(MINI_CORP) }
|
||||||
|
|
||||||
// Can't merge them together.
|
// Can't merge them together.
|
||||||
transaction {
|
transaction {
|
||||||
@ -202,7 +203,7 @@ class CashTests {
|
|||||||
|
|
||||||
// This works.
|
// This works.
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
||||||
output { inState.copy(issuingInstitution = MINI_CORP, owner = DUMMY_PUBKEY_2) }
|
output { inState.copy(owner = DUMMY_PUBKEY_2).editInstitution(MINI_CORP) }
|
||||||
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
||||||
contract.accepts()
|
contract.accepts()
|
||||||
}
|
}
|
||||||
@ -216,10 +217,10 @@ class CashTests {
|
|||||||
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
|
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
|
||||||
val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2
|
val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2
|
||||||
val WALLET = listOf(
|
val WALLET = listOf(
|
||||||
CashState(issuingInstitution = MEGA_CORP, depositReference = OpaqueBytes.of(1), amount = 100.DOLLARS, owner = OUR_PUBKEY_1),
|
CashState(DepositPointer(MEGA_CORP, OpaqueBytes.of(1)), 100.DOLLARS, OUR_PUBKEY_1),
|
||||||
CashState(issuingInstitution = MEGA_CORP, depositReference = OpaqueBytes.of(2), amount = 400.DOLLARS, owner = OUR_PUBKEY_1),
|
CashState(DepositPointer(MEGA_CORP, OpaqueBytes.of(2)), 400.DOLLARS, OUR_PUBKEY_1),
|
||||||
CashState(issuingInstitution = MINI_CORP, depositReference = OpaqueBytes.of(1), amount = 80.DOLLARS, owner = OUR_PUBKEY_1),
|
CashState(DepositPointer(MINI_CORP, OpaqueBytes.of(1)), 80.DOLLARS, OUR_PUBKEY_1),
|
||||||
CashState(issuingInstitution = MINI_CORP, depositReference = OpaqueBytes.of(2), amount = 80.SWISS_FRANCS, owner = OUR_PUBKEY_1)
|
CashState(DepositPointer(MINI_CORP, OpaqueBytes.of(2)), 80.SWISS_FRANCS, OUR_PUBKEY_1)
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
Loading…
Reference in New Issue
Block a user