mirror of
https://github.com/corda/corda.git
synced 2025-01-18 02:39:51 +00:00
Cash contract: don't allow merging of two different origin deposits together.
This commit is contained in:
parent
8f46fb4406
commit
0c6c2df483
25
src/Cash.kt
25
src/Cash.kt
@ -4,8 +4,19 @@ import java.util.*
|
|||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Cash
|
// Cash
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// (a blend of issuer+depositRef) and you couldn't merge outputs of two colours together, but you COULD put them in
|
||||||
|
// the same transaction.
|
||||||
|
//
|
||||||
|
// The goal of this design is to ensure that money can be withdrawn from the ledger easily: if you receive some money
|
||||||
|
// via this contract, you always know where to go in order to extract it from the R3 ledger via a regular wire transfer,
|
||||||
|
// no matter how many hands it has passed through in the intervening time.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
// TODO: Think about state merging: when does it make sense to merge multiple cash states from the same issuer?
|
|
||||||
// TODO: Does multi-currency also make sense? Probably?
|
// TODO: Does multi-currency also make sense? Probably?
|
||||||
// TODO: Implement a generate function.
|
// TODO: Implement a generate function.
|
||||||
|
|
||||||
@ -51,11 +62,12 @@ class CashContract : Contract {
|
|||||||
"all outputs use the currency of the inputs" by cashOutputs.all { it.amount.currency == currency }
|
"all outputs use the currency of the inputs" by cashOutputs.all { it.amount.currency == currency }
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each issuer 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 ((issuer, inputs) in cashInputs.groupBy { it.issuingInstitution }) {
|
for ((pair, inputs) in cashInputs.groupBy { Pair(it.issuingInstitution, it.depositReference) }) {
|
||||||
val outputs = cashOutputs.filter { it.issuingInstitution == issuer }
|
val (issuer, depositRef) = pair
|
||||||
|
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()
|
||||||
@ -64,11 +76,8 @@ class CashContract : Contract {
|
|||||||
val issuerCommand = args.filter { it.signingInstitution == issuer }.map { it.command as? ExitCashCommand }.filterNotNull().singleOrNull()
|
val issuerCommand = args.filter { it.signingInstitution == issuer }.map { it.command as? ExitCashCommand }.filterNotNull().singleOrNull()
|
||||||
val amountExitingLedger = issuerCommand?.amount ?: Amount(0, inputAmount.currency)
|
val amountExitingLedger = issuerCommand?.amount ?: Amount(0, inputAmount.currency)
|
||||||
|
|
||||||
val depositReference = inputs.first().depositReference
|
|
||||||
|
|
||||||
requireThat {
|
requireThat {
|
||||||
"for issuer ${issuer.name} the amounts balance" by (inputAmount == outputAmount + amountExitingLedger)
|
"for deposit $depositRef at issuer ${issuer.name} the amounts balance" by (inputAmount == outputAmount + amountExitingLedger)
|
||||||
"for issuer ${issuer.name} the deposit references are the same" by outputs.all { it.depositReference == depositReference }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,14 +94,14 @@ class CashTests {
|
|||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { outState.copy(issuingInstitution = MINI_CORP) }
|
output { outState.copy(issuingInstitution = MINI_CORP) }
|
||||||
contract `fails requirement` "for 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.copy(depositReference = OpaqueBytes.of(0), amount = inState.amount / 2) }
|
||||||
output { outState.copy(depositReference = OpaqueBytes.of(1), amount = inState.amount / 2) }
|
output { outState.copy(depositReference = OpaqueBytes.of(1), amount = inState.amount / 2) }
|
||||||
contract `fails requirement` "the deposit references are the same"
|
contract `fails requirement` "for deposit [01] at issuer MegaCorp the amounts balance"
|
||||||
}
|
}
|
||||||
// Can't mix currencies.
|
// Can't mix currencies.
|
||||||
transaction {
|
transaction {
|
||||||
@ -121,11 +121,19 @@ class CashTests {
|
|||||||
output { outState.copy(amount = 1150.DOLLARS) }
|
output { outState.copy(amount = 1150.DOLLARS) }
|
||||||
contract `fails requirement` "all inputs use the same currency"
|
contract `fails requirement` "all inputs use the same currency"
|
||||||
}
|
}
|
||||||
|
// Can't have superfluous input states from different issuers.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
input { inState.copy(issuingInstitution = MINI_CORP) }
|
input { inState.copy(issuingInstitution = MINI_CORP) }
|
||||||
output { outState }
|
output { outState }
|
||||||
contract `fails requirement` "for issuer MiniCorp the amounts balance"
|
contract `fails requirement` "at issuer MiniCorp the amounts balance"
|
||||||
|
}
|
||||||
|
// Can't combine two different deposits at the same issuer.
|
||||||
|
transaction {
|
||||||
|
input { inState }
|
||||||
|
input { inState.copy(depositReference = OpaqueBytes.of(3)) }
|
||||||
|
output { outState.copy(amount = inState.amount * 2, depositReference = OpaqueBytes.of(3)) }
|
||||||
|
contract `fails requirement` "for deposit [01]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,10 +169,10 @@ class CashTests {
|
|||||||
|
|
||||||
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
||||||
|
|
||||||
contract `fails requirement` "for issuer MegaCorp the amounts balance"
|
contract `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||||
|
|
||||||
arg(MEGA_CORP_KEY) { ExitCashCommand(200.DOLLARS) }
|
arg(MEGA_CORP_KEY) { ExitCashCommand(200.DOLLARS) }
|
||||||
contract `fails requirement` "for issuer MiniCorp the amounts balance"
|
contract `fails requirement` "at issuer MiniCorp the amounts balance"
|
||||||
|
|
||||||
arg(MINI_CORP_KEY) { ExitCashCommand(200.DOLLARS) }
|
arg(MINI_CORP_KEY) { ExitCashCommand(200.DOLLARS) }
|
||||||
contract.accepts()
|
contract.accepts()
|
||||||
@ -181,13 +189,13 @@ class CashTests {
|
|||||||
// Can't merge them together.
|
// Can't merge them together.
|
||||||
transaction {
|
transaction {
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2, amount = 2000.DOLLARS) }
|
output { inState.copy(owner = DUMMY_PUBKEY_2, amount = 2000.DOLLARS) }
|
||||||
contract `fails requirement` "for issuer MegaCorp the amounts balance"
|
contract `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||||
}
|
}
|
||||||
// Missing MiniCorp deposit
|
// Missing MiniCorp deposit
|
||||||
transaction {
|
transaction {
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
||||||
contract `fails requirement` "for issuer MegaCorp the amounts balance"
|
contract `fails requirement` "at issuer MegaCorp the amounts balance"
|
||||||
}
|
}
|
||||||
|
|
||||||
// This works.
|
// This works.
|
||||||
|
Loading…
Reference in New Issue
Block a user