mirror of
https://github.com/corda/corda.git
synced 2025-06-01 15:10:54 +00:00
458 lines
17 KiB
Kotlin
458 lines
17 KiB
Kotlin
import contracts.Cash
|
|
import contracts.DummyContract
|
|
import contracts.InsufficientBalanceException
|
|
import contracts.cash.CashIssuanceDefinition
|
|
import core.*
|
|
import core.crypto.SecureHash
|
|
import core.serialization.OpaqueBytes
|
|
import core.testutils.*
|
|
import org.junit.Test
|
|
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 {
|
|
val inState = Cash.State(
|
|
deposit = MEGA_CORP.ref(1),
|
|
amount = 1000.DOLLARS,
|
|
owner = DUMMY_PUBKEY_1,
|
|
notary = DUMMY_NOTARY
|
|
)
|
|
val outState = inState.copy(owner = DUMMY_PUBKEY_2)
|
|
|
|
fun Cash.State.editDepositRef(ref: Byte) = copy(deposit = deposit.copy(reference = OpaqueBytes.of(ref)))
|
|
|
|
@Test
|
|
fun trivial() {
|
|
transaction {
|
|
input { inState }
|
|
this `fails requirement` "the amounts balance"
|
|
|
|
tweak {
|
|
output { outState.copy(amount = 2000.DOLLARS) }
|
|
this `fails requirement` "the amounts balance"
|
|
}
|
|
tweak {
|
|
output { outState }
|
|
// No command arguments
|
|
this `fails requirement` "required contracts.Cash.Commands.Move command"
|
|
}
|
|
tweak {
|
|
output { outState }
|
|
arg(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
|
|
this `fails requirement` "the owning keys are the same as the signing keys"
|
|
}
|
|
tweak {
|
|
output { outState }
|
|
output { outState `issued by` MINI_CORP }
|
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
|
this `fails requirement` "at least one cash input"
|
|
}
|
|
// Simple reallocation works.
|
|
tweak {
|
|
output { outState }
|
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
|
this.accepts()
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun issueMoney() {
|
|
// Check we can't "move" money into existence.
|
|
transaction {
|
|
input { DummyContract.State(notary = DUMMY_NOTARY) }
|
|
output { outState }
|
|
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
|
|
|
this `fails requirement` "there is at least one cash input"
|
|
}
|
|
|
|
// Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
|
|
// institution is allowed to issue as much cash as they want.
|
|
transaction {
|
|
output { outState }
|
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Issue() }
|
|
this `fails requirement` "output deposits are owned by a command signer"
|
|
}
|
|
transaction {
|
|
output {
|
|
Cash.State(
|
|
amount = 1000.DOLLARS,
|
|
owner = DUMMY_PUBKEY_1,
|
|
deposit = MINI_CORP.ref(12, 34),
|
|
notary = DUMMY_NOTARY
|
|
)
|
|
}
|
|
tweak {
|
|
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue(0) }
|
|
this `fails requirement` "has a nonce"
|
|
}
|
|
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
|
this.accepts()
|
|
}
|
|
|
|
// Test generation works.
|
|
val ptx = TransactionBuilder()
|
|
Cash().generateIssue(ptx, 100.DOLLARS, MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
|
assertTrue(ptx.inputStates().isEmpty())
|
|
val s = ptx.outputStates()[0] as Cash.State
|
|
assertEquals(100.DOLLARS, s.amount)
|
|
assertEquals(MINI_CORP, s.deposit.party)
|
|
assertEquals(DUMMY_PUBKEY_1, s.owner)
|
|
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 }
|
|
output { inState.copy(amount = inState.amount * 2) }
|
|
|
|
// Move fails: not allowed to summon money.
|
|
tweak {
|
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
|
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
|
}
|
|
|
|
// Issue works.
|
|
tweak {
|
|
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
|
this.accepts()
|
|
}
|
|
}
|
|
|
|
// Can't use an issue command to lower the amount.
|
|
transaction {
|
|
input { inState }
|
|
output { inState.copy(amount = inState.amount / 2) }
|
|
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
|
this `fails requirement` "output values sum to more than the inputs"
|
|
}
|
|
|
|
// Can't have an issue command that doesn't actually issue money.
|
|
transaction {
|
|
input { inState }
|
|
output { inState }
|
|
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
|
this `fails requirement` "output values sum to more than the inputs"
|
|
}
|
|
|
|
// Can't have any other commands if we have an issue command (because the issue command overrules them)
|
|
transaction {
|
|
input { inState }
|
|
output { inState.copy(amount = inState.amount * 2) }
|
|
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
|
tweak {
|
|
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
|
this `fails requirement` "there is only a single issue command"
|
|
}
|
|
tweak {
|
|
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
|
this `fails requirement` "there is only a single issue command"
|
|
}
|
|
tweak {
|
|
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
|
|
this `fails requirement` "there is only a single issue command"
|
|
}
|
|
this.accepts()
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun testMergeSplit() {
|
|
// Splitting value works.
|
|
transaction {
|
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
|
tweak {
|
|
input { inState }
|
|
for (i in 1..4) output { inState.copy(amount = inState.amount / 4) }
|
|
this.accepts()
|
|
}
|
|
// Merging 4 inputs into 2 outputs works.
|
|
tweak {
|
|
for (i in 1..4) input { inState.copy(amount = inState.amount / 4) }
|
|
output { inState.copy(amount = inState.amount / 2) }
|
|
output { inState.copy(amount = inState.amount / 2) }
|
|
this.accepts()
|
|
}
|
|
// Merging 2 inputs into 1 works.
|
|
tweak {
|
|
input { inState.copy(amount = inState.amount / 2) }
|
|
input { inState.copy(amount = inState.amount / 2) }
|
|
output { inState }
|
|
this.accepts()
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun zeroSizedValues() {
|
|
transaction {
|
|
input { inState }
|
|
input { inState.copy(amount = 0.DOLLARS) }
|
|
this `fails requirement` "zero sized inputs"
|
|
}
|
|
transaction {
|
|
input { inState }
|
|
output { inState }
|
|
output { inState.copy(amount = 0.DOLLARS) }
|
|
this `fails requirement` "zero sized outputs"
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun trivialMismatches() {
|
|
// Can't change issuer.
|
|
transaction {
|
|
input { inState }
|
|
output { outState `issued by` MINI_CORP }
|
|
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
|
}
|
|
// Can't change deposit reference when splitting.
|
|
transaction {
|
|
input { inState }
|
|
output { outState.editDepositRef(0).copy(amount = inState.amount / 2) }
|
|
output { outState.editDepositRef(1).copy(amount = inState.amount / 2) }
|
|
this `fails requirement` "for deposit [01] at issuer MegaCorp the amounts balance"
|
|
}
|
|
// Can't mix currencies.
|
|
transaction {
|
|
input { inState }
|
|
output { outState.copy(amount = 800.DOLLARS) }
|
|
output { outState.copy(amount = 200.POUNDS) }
|
|
this `fails requirement` "the amounts balance"
|
|
}
|
|
transaction {
|
|
input { inState }
|
|
input {
|
|
inState.copy(
|
|
amount = 150.POUNDS,
|
|
owner = DUMMY_PUBKEY_2
|
|
)
|
|
}
|
|
output { outState.copy(amount = 1150.DOLLARS) }
|
|
this `fails requirement` "the amounts balance"
|
|
}
|
|
// Can't have superfluous input states from different issuers.
|
|
transaction {
|
|
input { inState }
|
|
input { inState `issued by` MINI_CORP }
|
|
output { outState }
|
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
|
this `fails requirement` "at issuer MiniCorp the amounts balance"
|
|
}
|
|
// Can't combine two different deposits at the same issuer.
|
|
transaction {
|
|
input { inState }
|
|
input { inState.editDepositRef(3) }
|
|
output { outState.copy(amount = inState.amount * 2).editDepositRef(3) }
|
|
this `fails requirement` "for deposit [01]"
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun exitLedger() {
|
|
// Single input/output straightforward case.
|
|
transaction {
|
|
input { inState }
|
|
output { outState.copy(amount = inState.amount - 200.DOLLARS) }
|
|
|
|
tweak {
|
|
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS) }
|
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
|
this `fails requirement` "the amounts balance"
|
|
}
|
|
|
|
tweak {
|
|
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) }
|
|
this `fails requirement` "required contracts.Cash.Commands.Move command"
|
|
|
|
tweak {
|
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
|
this.accepts()
|
|
}
|
|
}
|
|
}
|
|
// Multi-issuer case.
|
|
transaction {
|
|
input { inState }
|
|
input { inState `issued by` MINI_CORP }
|
|
|
|
output { inState.copy(amount = inState.amount - 200.DOLLARS) `issued by` MINI_CORP }
|
|
output { inState.copy(amount = inState.amount - 200.DOLLARS) }
|
|
|
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
|
|
|
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
|
|
|
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) }
|
|
this `fails requirement` "at issuer MiniCorp the amounts balance"
|
|
|
|
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) }
|
|
this.accepts()
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun multiIssuer() {
|
|
transaction {
|
|
// Gather 2000 dollars from two different issuers.
|
|
input { inState }
|
|
input { inState `issued by` MINI_CORP }
|
|
|
|
// Can't merge them together.
|
|
tweak {
|
|
output { inState.copy(owner = DUMMY_PUBKEY_2, amount = 2000.DOLLARS) }
|
|
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
|
}
|
|
// Missing MiniCorp deposit
|
|
tweak {
|
|
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
|
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
|
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
|
}
|
|
|
|
// This works.
|
|
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
|
output { inState.copy(owner = DUMMY_PUBKEY_2) `issued by` MINI_CORP }
|
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
|
this.accepts()
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun multiCurrency() {
|
|
// Check we can do an atomic currency trade tx.
|
|
transaction {
|
|
val pounds = Cash.State(MINI_CORP.ref(3, 4, 5), 658.POUNDS, DUMMY_PUBKEY_2, DUMMY_NOTARY)
|
|
input { inState `owned by` DUMMY_PUBKEY_1 }
|
|
input { pounds }
|
|
output { inState `owned by` DUMMY_PUBKEY_2 }
|
|
output { pounds `owned by` DUMMY_PUBKEY_1 }
|
|
arg(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Cash.Commands.Move() }
|
|
|
|
this.accepts()
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Spend tx generation
|
|
|
|
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
|
|
val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2
|
|
|
|
fun makeCash(amount: Amount, corp: Party, depositRef: Byte = 1) =
|
|
StateAndRef(
|
|
Cash.State(corp.ref(depositRef), amount, OUR_PUBKEY_1, DUMMY_NOTARY),
|
|
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
|
|
)
|
|
|
|
val WALLET = listOf(
|
|
makeCash(100.DOLLARS, MEGA_CORP),
|
|
makeCash(400.DOLLARS, MEGA_CORP),
|
|
makeCash(80.DOLLARS, MINI_CORP),
|
|
makeCash(80.SWISS_FRANCS, MINI_CORP, 2)
|
|
)
|
|
|
|
fun makeSpend(amount: Amount, dest: PublicKey): WireTransaction {
|
|
val tx = TransactionBuilder()
|
|
Cash().generateSpend(tx, amount, dest, WALLET)
|
|
return tx.toWireTransaction()
|
|
}
|
|
|
|
@Test
|
|
fun generateSimpleDirectSpend() {
|
|
val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1)
|
|
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
|
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0])
|
|
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
|
}
|
|
|
|
@Test
|
|
fun generateSimpleSpendWithParties() {
|
|
val tx = TransactionBuilder()
|
|
Cash().generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, WALLET, setOf(MINI_CORP))
|
|
assertEquals(WALLET[2].ref, tx.inputStates()[0])
|
|
}
|
|
|
|
@Test
|
|
fun generateSimpleSpendWithChange() {
|
|
val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1)
|
|
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
|
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS), wtx.outputs[0])
|
|
assertEquals(WALLET[0].state.copy(amount = 90.DOLLARS), wtx.outputs[1])
|
|
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
|
}
|
|
|
|
@Test
|
|
fun generateSpendWithTwoInputs() {
|
|
val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1)
|
|
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
|
assertEquals(WALLET[1].ref, wtx.inputs[1])
|
|
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputs[0])
|
|
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
|
}
|
|
|
|
@Test
|
|
fun generateSpendMixedDeposits() {
|
|
val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1)
|
|
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
|
assertEquals(WALLET[1].ref, wtx.inputs[1])
|
|
assertEquals(WALLET[2].ref, wtx.inputs[2])
|
|
assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputs[0])
|
|
assertEquals(WALLET[2].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1])
|
|
assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
|
}
|
|
|
|
@Test
|
|
fun generateSpendInsufficientBalance() {
|
|
val e: InsufficientBalanceException = assertFailsWith("balance") {
|
|
makeSpend(1000.DOLLARS, THEIR_PUBKEY_1)
|
|
}
|
|
assertEquals((1000 - 580).DOLLARS, e.amountMissing)
|
|
|
|
assertFailsWith(InsufficientBalanceException::class) {
|
|
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)
|
|
}
|
|
}
|