2015-11-03 12:37:19 +00:00
|
|
|
import org.junit.Test
|
2015-11-03 16:38:11 +00:00
|
|
|
import kotlin.test.assertEquals
|
|
|
|
import kotlin.test.assertFailsWith
|
2015-11-03 12:37:19 +00:00
|
|
|
|
2015-11-03 12:49:28 +00:00
|
|
|
// TODO: Some basic invariants should be enforced by the platform before contract execution:
|
|
|
|
// 1. No duplicate input states
|
|
|
|
// 2. There must be at least one input state (note: not "one of the type the contract wants")
|
|
|
|
|
2015-11-03 12:37:19 +00:00
|
|
|
class CashTests {
|
|
|
|
val inState = CashState(
|
|
|
|
issuingInstitution = MEGA_CORP,
|
2015-11-03 15:00:43 +00:00
|
|
|
depositReference = OpaqueBytes.of(1),
|
2015-11-03 12:37:19 +00:00
|
|
|
amount = 1000.DOLLARS,
|
|
|
|
owner = DUMMY_PUBKEY_1
|
|
|
|
)
|
|
|
|
val outState = inState.copy(owner = DUMMY_PUBKEY_2)
|
2015-11-03 12:49:28 +00:00
|
|
|
|
|
|
|
val contract = CashContract()
|
2015-11-03 12:37:19 +00:00
|
|
|
|
|
|
|
@Test
|
|
|
|
fun trivial() {
|
2015-11-03 12:49:28 +00:00
|
|
|
transaction {
|
|
|
|
contract `fails requirement` "there is at least one cash input"
|
|
|
|
|
|
|
|
input { inState }
|
|
|
|
contract `fails requirement` "the amounts balance"
|
|
|
|
|
2015-11-03 12:37:19 +00:00
|
|
|
transaction {
|
2015-11-03 12:49:28 +00:00
|
|
|
output { outState.copy(amount = 2000.DOLLARS )}
|
|
|
|
contract `fails requirement` "the amounts balance"
|
2015-11-03 12:37:19 +00:00
|
|
|
}
|
|
|
|
transaction {
|
2015-11-03 12:49:28 +00:00
|
|
|
output { outState }
|
|
|
|
// No command arguments
|
|
|
|
contract `fails requirement` "the owning keys are the same as the signing keys"
|
2015-11-03 12:37:19 +00:00
|
|
|
}
|
|
|
|
transaction {
|
2015-11-03 12:49:28 +00:00
|
|
|
output { outState }
|
|
|
|
arg(DUMMY_PUBKEY_2) { MoveCashCommand() }
|
|
|
|
contract `fails requirement` "the owning keys are the same as the signing keys"
|
2015-11-03 12:37:19 +00:00
|
|
|
}
|
|
|
|
transaction {
|
2015-11-03 12:49:28 +00:00
|
|
|
output { outState }
|
2015-11-03 12:37:19 +00:00
|
|
|
output { outState.copy(issuingInstitution = MINI_CORP) }
|
2015-11-03 12:49:28 +00:00
|
|
|
contract `fails requirement` "no output states are unaccounted for"
|
2015-11-03 12:37:19 +00:00
|
|
|
}
|
2015-11-03 12:49:28 +00:00
|
|
|
// Simple reallocation works.
|
2015-11-03 12:37:19 +00:00
|
|
|
transaction {
|
2015-11-03 12:49:28 +00:00
|
|
|
output { outState }
|
|
|
|
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
|
|
|
contract.accepts()
|
2015-11-03 12:37:19 +00:00
|
|
|
}
|
2015-11-03 12:49:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
fun testMergeSplit() {
|
|
|
|
// Splitting value works.
|
|
|
|
transaction {
|
|
|
|
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
2015-11-03 12:37:19 +00:00
|
|
|
transaction {
|
|
|
|
input { inState }
|
2015-11-03 12:49:28 +00:00
|
|
|
for (i in 1..4) output { inState.copy(amount = inState.amount / 4) }
|
|
|
|
contract.accepts()
|
2015-11-03 12:37:19 +00:00
|
|
|
}
|
2015-11-03 12:49:28 +00:00
|
|
|
// Merging 4 inputs into 2 outputs works.
|
2015-11-03 12:37:19 +00:00
|
|
|
transaction {
|
2015-11-03 12:49:28 +00:00
|
|
|
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) }
|
|
|
|
contract.accepts()
|
2015-11-03 12:37:19 +00:00
|
|
|
}
|
2015-11-03 12:49:28 +00:00
|
|
|
// Merging 2 inputs into 1 works.
|
2015-11-03 12:37:19 +00:00
|
|
|
transaction {
|
2015-11-03 12:49:28 +00:00
|
|
|
input { inState.copy(amount = inState.amount / 2) }
|
|
|
|
input { inState.copy(amount = inState.amount / 2) }
|
|
|
|
output { inState }
|
|
|
|
contract.accepts()
|
2015-11-03 12:37:19 +00:00
|
|
|
}
|
2015-11-03 12:49:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
fun zeroSizedInputs() {
|
|
|
|
transaction {
|
|
|
|
input { inState }
|
|
|
|
input { inState.copy(amount = 0.DOLLARS) }
|
|
|
|
contract `fails requirement` "zero sized inputs"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
fun trivialMismatches() {
|
|
|
|
// Can't change issuer.
|
|
|
|
transaction {
|
|
|
|
input { inState }
|
|
|
|
output { outState.copy(issuingInstitution = MINI_CORP) }
|
2015-11-03 15:09:02 +00:00
|
|
|
contract `fails requirement` "at issuer MegaCorp the amounts balance"
|
2015-11-03 12:49:28 +00:00
|
|
|
}
|
|
|
|
// Can't change deposit reference when splitting.
|
|
|
|
transaction {
|
|
|
|
input { inState }
|
2015-11-03 15:00:43 +00:00
|
|
|
output { outState.copy(depositReference = OpaqueBytes.of(0), amount = inState.amount / 2) }
|
|
|
|
output { outState.copy(depositReference = OpaqueBytes.of(1), amount = inState.amount / 2) }
|
2015-11-03 15:09:02 +00:00
|
|
|
contract `fails requirement` "for deposit [01] at issuer MegaCorp the amounts balance"
|
2015-11-03 12:49:28 +00:00
|
|
|
}
|
|
|
|
// Can't mix currencies.
|
|
|
|
transaction {
|
|
|
|
input { inState }
|
|
|
|
output { outState.copy(amount = 800.DOLLARS) }
|
|
|
|
output { outState.copy(amount = 200.POUNDS) }
|
|
|
|
contract `fails requirement` "all outputs use the currency of the inputs"
|
|
|
|
}
|
|
|
|
transaction {
|
|
|
|
input { inState }
|
|
|
|
input {
|
|
|
|
inState.copy(
|
|
|
|
amount = 150.POUNDS,
|
|
|
|
owner = DUMMY_PUBKEY_2
|
|
|
|
)
|
2015-11-03 12:37:19 +00:00
|
|
|
}
|
2015-11-03 12:49:28 +00:00
|
|
|
output { outState.copy(amount = 1150.DOLLARS) }
|
|
|
|
contract `fails requirement` "all inputs use the same currency"
|
|
|
|
}
|
2015-11-03 15:09:02 +00:00
|
|
|
// Can't have superfluous input states from different issuers.
|
2015-11-03 12:49:28 +00:00
|
|
|
transaction {
|
|
|
|
input { inState }
|
|
|
|
input { inState.copy(issuingInstitution = MINI_CORP) }
|
|
|
|
output { outState }
|
2015-11-03 15:09:02 +00:00
|
|
|
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]"
|
2015-11-03 12:37:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
fun exitLedger() {
|
2015-11-03 12:49:28 +00:00
|
|
|
// Single input/output straightforward case.
|
|
|
|
transaction {
|
|
|
|
input { inState }
|
|
|
|
output { outState.copy(amount = inState.amount - 200.DOLLARS) }
|
|
|
|
|
2015-11-03 12:37:19 +00:00
|
|
|
transaction {
|
2015-11-03 12:49:28 +00:00
|
|
|
arg(MEGA_CORP_KEY) { ExitCashCommand(100.DOLLARS) }
|
|
|
|
contract `fails requirement` "the amounts balance"
|
|
|
|
}
|
2015-11-03 12:37:19 +00:00
|
|
|
|
2015-11-03 12:49:28 +00:00
|
|
|
transaction {
|
|
|
|
arg(MEGA_CORP_KEY) { ExitCashCommand(200.DOLLARS) }
|
|
|
|
contract `fails requirement` "the owning keys are the same as the signing keys" // No move command.
|
2015-11-03 12:37:19 +00:00
|
|
|
|
|
|
|
transaction {
|
2015-11-03 12:49:28 +00:00
|
|
|
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
|
|
|
contract.accepts()
|
2015-11-03 12:37:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-11-03 12:49:28 +00:00
|
|
|
// Multi-issuer case.
|
|
|
|
transaction {
|
|
|
|
input { inState }
|
|
|
|
input { inState.copy(issuingInstitution = MINI_CORP) }
|
|
|
|
|
|
|
|
output { inState.copy(issuingInstitution = MINI_CORP, amount = inState.amount - 200.DOLLARS) }
|
|
|
|
output { inState.copy(amount = inState.amount - 200.DOLLARS) }
|
|
|
|
|
|
|
|
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
|
|
|
|
2015-11-03 15:09:02 +00:00
|
|
|
contract `fails requirement` "at issuer MegaCorp the amounts balance"
|
2015-11-03 12:49:28 +00:00
|
|
|
|
|
|
|
arg(MEGA_CORP_KEY) { ExitCashCommand(200.DOLLARS) }
|
2015-11-03 15:09:02 +00:00
|
|
|
contract `fails requirement` "at issuer MiniCorp the amounts balance"
|
2015-11-03 12:49:28 +00:00
|
|
|
|
|
|
|
arg(MINI_CORP_KEY) { ExitCashCommand(200.DOLLARS) }
|
|
|
|
contract.accepts()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
fun multiIssuer() {
|
|
|
|
transaction {
|
|
|
|
// Gather 2000 dollars from two different issuers.
|
|
|
|
input { inState }
|
|
|
|
input { inState.copy(issuingInstitution = MINI_CORP) }
|
|
|
|
|
|
|
|
// Can't merge them together.
|
|
|
|
transaction {
|
|
|
|
output { inState.copy(owner = DUMMY_PUBKEY_2, amount = 2000.DOLLARS) }
|
2015-11-03 15:09:02 +00:00
|
|
|
contract `fails requirement` "at issuer MegaCorp the amounts balance"
|
2015-11-03 12:49:28 +00:00
|
|
|
}
|
|
|
|
// Missing MiniCorp deposit
|
|
|
|
transaction {
|
|
|
|
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
|
|
|
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
2015-11-03 15:09:02 +00:00
|
|
|
contract `fails requirement` "at issuer MegaCorp the amounts balance"
|
2015-11-03 12:49:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// This works.
|
|
|
|
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
|
|
|
output { inState.copy(issuingInstitution = MINI_CORP, owner = DUMMY_PUBKEY_2) }
|
|
|
|
arg(DUMMY_PUBKEY_1) { MoveCashCommand() }
|
|
|
|
contract.accepts()
|
|
|
|
}
|
|
|
|
|
|
|
|
transaction {
|
|
|
|
input { inState }
|
|
|
|
input { inState }
|
|
|
|
}
|
2015-11-03 12:37:19 +00:00
|
|
|
}
|
2015-11-03 16:38:11 +00:00
|
|
|
|
|
|
|
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
|
|
|
|
val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2
|
|
|
|
val WALLET = listOf(
|
|
|
|
CashState(issuingInstitution = MEGA_CORP, depositReference = OpaqueBytes.of(1), amount = 100.DOLLARS, owner = OUR_PUBKEY_1),
|
|
|
|
CashState(issuingInstitution = MEGA_CORP, depositReference = OpaqueBytes.of(2), amount = 400.DOLLARS, owner = OUR_PUBKEY_1),
|
|
|
|
CashState(issuingInstitution = MINI_CORP, depositReference = OpaqueBytes.of(1), amount = 80.DOLLARS, owner = OUR_PUBKEY_1),
|
|
|
|
CashState(issuingInstitution = MINI_CORP, depositReference = OpaqueBytes.of(2), amount = 80.SWISS_FRANCS, owner = OUR_PUBKEY_1)
|
|
|
|
)
|
|
|
|
|
|
|
|
@Test
|
|
|
|
fun craftSimpleDirectSpend() {
|
|
|
|
assertEquals(
|
|
|
|
transaction {
|
|
|
|
input { WALLET[0] }
|
|
|
|
output { WALLET[0].copy(owner = THEIR_PUBKEY_1) }
|
|
|
|
arg(OUR_PUBKEY_1) { MoveCashCommand() }
|
|
|
|
},
|
|
|
|
contract.craftSpend(100.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
fun craftSimpleSpendWithChange() {
|
|
|
|
assertEquals(
|
|
|
|
transaction {
|
|
|
|
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() }
|
|
|
|
},
|
|
|
|
contract.craftSpend(10.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
fun craftSpendWithTwoInputs() {
|
|
|
|
assertEquals(
|
|
|
|
transaction {
|
|
|
|
input { WALLET[0] }
|
|
|
|
input { WALLET[1] }
|
|
|
|
output { WALLET[0].copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS) }
|
|
|
|
arg(OUR_PUBKEY_1) { MoveCashCommand() }
|
|
|
|
},
|
|
|
|
contract.craftSpend(500.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
fun craftSpendMixedDeposits() {
|
|
|
|
assertEquals(
|
|
|
|
transaction {
|
|
|
|
input { WALLET[0] }
|
|
|
|
input { WALLET[1] }
|
|
|
|
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() }
|
|
|
|
},
|
|
|
|
contract.craftSpend(580.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
@Test
|
|
|
|
fun craftSpendInsufficientBalance() {
|
|
|
|
assertFailsWith(InsufficientBalanceException::class) {
|
|
|
|
contract.craftSpend(1000.DOLLARS, THEIR_PUBKEY_1, WALLET)
|
|
|
|
}
|
|
|
|
assertFailsWith(InsufficientBalanceException::class) {
|
|
|
|
contract.craftSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1, WALLET)
|
|
|
|
}
|
|
|
|
}
|
2015-11-03 12:37:19 +00:00
|
|
|
}
|