PtCashTests and missing bits of implementation to make them work

This commit is contained in:
Christian Sailer 2017-10-10 14:32:18 +01:00
parent 8ae92850c9
commit 22bf2b1c1d
8 changed files with 2089 additions and 2 deletions

View File

@ -0,0 +1,34 @@
@file:JvmName("PtCurrencies")
package net.corda.ptflows
import net.corda.core.contracts.Amount
import net.corda.core.contracts.Issued
import net.corda.core.contracts.PartyAndReference
import java.math.BigDecimal
import java.util.*
@JvmField val USD: Currency = Currency.getInstance("USD")
@JvmField val GBP: Currency = Currency.getInstance("GBP")
@JvmField val EUR: Currency = Currency.getInstance("EUR")
@JvmField val CHF: Currency = Currency.getInstance("CHF")
@JvmField val JPY: Currency = Currency.getInstance("JPY")
@JvmField val RUB: Currency = Currency.getInstance("RUB")
fun <T : Any> AMOUNT(amount: Int, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token)
fun <T : Any> AMOUNT(amount: Double, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount), token)
fun DOLLARS(amount: Int): Amount<Currency> = AMOUNT(amount, USD)
fun DOLLARS(amount: Double): Amount<Currency> = AMOUNT(amount, USD)
fun POUNDS(amount: Int): Amount<Currency> = AMOUNT(amount, GBP)
fun SWISS_FRANCS(amount: Int): Amount<Currency> = AMOUNT(amount, CHF)
val Int.DOLLARS: Amount<Currency> get() = DOLLARS(this)
val Double.DOLLARS: Amount<Currency> get() = DOLLARS(this)
val Int.POUNDS: Amount<Currency> get() = POUNDS(this)
val Int.SWISS_FRANCS: Amount<Currency> get() = SWISS_FRANCS(this)
infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
infix fun Amount<Currency>.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit))

View File

@ -53,7 +53,7 @@ interface PtCashSelection {
instance.set(cashSelectionAlgo)
cashSelectionAlgo
} ?: throw ClassNotFoundException("\nUnable to load compatible cash selection algorithm implementation for JDBC driver ($_metadata)." +
"\nPlease specify an implementation in META-INF/services/net.corda.finance.contracts.asset.CashSelection")
"\nPlease specify an implementation in META-INF/services/net.corda.ptflows.contracts.asset.PtCashSelection")
}.invoke()
}
}
@ -261,7 +261,7 @@ class PtCash : PtOnLedgerAsset<Currency, PtCash.Commands, PtCash.State>() {
}
companion object {
const val PROGRAM_ID: ContractClassName = "net.corda.finance.contracts.asset.Cash"
const val PROGRAM_ID: ContractClassName = "net.corda.ptflows.contracts.asset.PtCash"
/**
* Generate a transaction that moves an amount of currency to the given party, and sends any change back to

View File

@ -0,0 +1,151 @@
package net.corda.ptflows.contracts.asset.cash.selection
import co.paralleluniverse.fibers.Suspendable
import co.paralleluniverse.strands.Strand
import net.corda.core.contracts.Amount
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TransactionState
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.StatesNotAvailableException
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.deserialize
import net.corda.core.utilities.*
import net.corda.ptflows.contracts.asset.PtCash
import net.corda.ptflows.contracts.asset.PtCashSelection
import java.sql.DatabaseMetaData
import java.sql.SQLException
import java.util.*
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
class PtCashSelectionH2Impl : PtCashSelection {
companion object {
const val JDBC_DRIVER_NAME = "H2 JDBC Driver"
val log = loggerFor<PtCashSelectionH2Impl>()
}
override fun isCompatible(metadata: DatabaseMetaData): Boolean {
return metadata.driverName == JDBC_DRIVER_NAME
}
// coin selection retry loop counter, sleep (msecs) and lock for selecting states
private val MAX_RETRIES = 5
private val RETRY_SLEEP = 100
private val spendLock: ReentrantLock = ReentrantLock()
/**
* An optimised query to gather Cash states that are available and retry if they are temporarily unavailable.
* @param services The service hub to allow access to the database session
* @param amount The amount of currency desired (ignoring issues, but specifying the currency)
* @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer,
* otherwise the set of eligible states wil be filtered to only include those from these issuers.
* @param notary If null the notary source is ignored, if specified then only states marked
* with this notary are included.
* @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states.
* Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes.
* @param withIssuerRefs If not empty the specific set of issuer references to match against.
* @return The matching states that were found. If sufficient funds were found these will be locked,
* otherwise what is available is returned unlocked for informational purposes.
*/
@Suspendable
override fun unconsumedCashStatesForSpending(services: ServiceHub,
amount: Amount<Currency>,
onlyFromIssuerParties: Set<AbstractParty>,
notary: Party?,
lockId: UUID,
withIssuerRefs: Set<OpaqueBytes>): List<StateAndRef<PtCash.State>> {
val issuerKeysStr = onlyFromIssuerParties.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }.dropLast(1)
val issuerRefsStr = withIssuerRefs.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }.dropLast(1)
val stateAndRefs = mutableListOf<StateAndRef<PtCash.State>>()
// We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins:
// 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the
// running total of such an accumulator
// 2) H2 uses session variables to perform this accumulator function:
// http://www.h2database.com/html/functions.html#set
// 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries)
for (retryCount in 1..MAX_RETRIES) {
spendLock.withLock {
val statement = services.jdbcSession().createStatement()
try {
statement.execute("CALL SET(@t, 0);")
// we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null)
// the softLockReserve update will detect whether we try to lock states locked by others
val selectJoin = """
SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
FROM vault_states AS vs, contract_cash_states AS ccs
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
AND vs.state_status = 0
AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity}
AND (vs.lock_id = '$lockId' OR vs.lock_id is null)
""" +
(if (notary != null)
" AND vs.notary_name = '${notary.name}'" else "") +
(if (onlyFromIssuerParties.isNotEmpty())
" AND ccs.issuer_key IN ($issuerKeysStr)" else "") +
(if (withIssuerRefs.isNotEmpty())
" AND ccs.issuer_ref IN ($issuerRefsStr)" else "")
// Retrieve spendable state refs
val rs = statement.executeQuery(selectJoin)
stateAndRefs.clear()
log.debug(selectJoin)
var totalPennies = 0L
while (rs.next()) {
val txHash = SecureHash.parse(rs.getString(1))
val index = rs.getInt(2)
val stateRef = StateRef(txHash, index)
val state = rs.getBytes(3).deserialize<TransactionState<PtCash.State>>(context = SerializationDefaults.STORAGE_CONTEXT)
val pennies = rs.getLong(4)
totalPennies = rs.getLong(5)
val rowLockId = rs.getString(6)
stateAndRefs.add(StateAndRef(state, stateRef))
log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" }
}
if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) {
// we should have a minimum number of states to satisfy our selection `amount` criteria
log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs")
// With the current single threaded state machine available states are guaranteed to lock.
// TODO However, we will have to revisit these methods in the future multi-threaded.
services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet())
return stateAndRefs
}
log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}")
// retry as more states may become available
} catch (e: SQLException) {
log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId]
$e.
""")
} catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine
stateAndRefs.clear()
log.warn(e.message)
// retry only if there are locked states that may become available again (or consumed with change)
} finally {
statement.close()
}
}
log.warn("Coin selection failed on attempt $retryCount")
// TODO: revisit the back off strategy for contended spending.
if (retryCount != MAX_RETRIES) {
Strand.sleep(RETRY_SLEEP * retryCount.toLong())
}
}
log.warn("Insufficient spendable states identified for $amount")
return stateAndRefs
}
}

View File

@ -0,0 +1,2 @@
net.corda.ptflows.contracts.asset.cash.selection.PtCashSelectionH2Impl

View File

@ -0,0 +1 @@
2015-01-01,2015-04-03,2015-04-06,2015-05-04,2015-05-25,2015-08-31,2015-12-25,2015-12-28,2016-01-01,2016-03-25,2016-03-28,2016-05-02,2016-05-30,2016-08-29,2016-12-26,2016-12-27,2017-01-02,2017-04-14,2017-04-17,2017-05-01,2017-05-29,2017-08-28,2017-12-25,2017-12-26,2018-01-01,2018-03-30,2018-04-02,2018-05-07,2018-05-28,2018-08-27,2018-12-25,2018-12-26,2019-01-01,2019-04-19,2019-04-22,2019-05-06,2019-05-27,2019-08-26,2019-12-25,2019-12-26,2020-01-01,2020-04-10,2020-04-13,2020-05-04,2020-05-25,2020-08-31,2020-12-25,2020-12-28,2021-01-01,2021-04-02,2021-04-05,2021-05-03,2021-05-31,2021-08-30,2021-12-27,2021-12-28,2022-01-03,2022-04-15,2022-04-18,2022-05-02,2022-05-30,2022-08-29,2022-12-26,2022-12-27,2023-01-02,2023-04-07,2023-04-10,2023-05-01,2023-05-29,2023-08-28,2023-12-25,2023-12-26,2024-01-01,2024-03-29,2024-04-01,2024-05-06,2024-05-27,2024-08-26,2024-12-25,2024-12-26

View File

@ -0,0 +1 @@
2015-01-01,2015-01-19,2015-02-16,2015-02-18,2015-05-25,2015-07-03,2015-09-07,2015-10-12,2015-11-11,2015-11-26,2015-12-25,2016-01-01,2016-01-18,2016-02-10,2016-02-15,2016-05-30,2016-07-04,2016-09-05,2016-10-10,2016-11-11,2016-11-24,2016-12-26,2017-01-02,2017-01-16,2017-02-20,2017-03-01,2017-05-29,2017-07-04,2017-09-04,2017-10-09,2017-11-10,2017-11-23,2017-12-25,2018-01-01,2018-01-15,2018-02-14,2018-02-19,2018-05-28,2018-07-04,2018-09-03,2018-10-08,2018-11-12,2018-11-22,2018-12-25,2019-01-01,2019-01-21,2019-02-18,2019-03-06,2019-05-27,2019-07-04,2019-09-02,2019-10-14,2019-11-11,2019-11-28,2019-12-25,2020-01-01,2020-01-20,2020-02-17,2020-02-26,2020-05-25,2020-07-03,2020-09-07,2020-10-12,2020-11-11,2020-11-26,2020-12-25,2021-01-01,2021-01-18,2021-02-15,2021-02-17,2021-05-31,2021-07-05,2021-09-06,2021-10-11,2021-11-11,2021-11-25,2021-12-24,2022-01-17,2022-02-21,2022-03-02,2022-05-30,2022-07-04,2022-09-05,2022-10-10,2022-11-11,2022-11-24,2022-12-26,2023-01-02,2023-01-16,2023-02-20,2023-02-22,2023-05-29,2023-07-04,2023-09-04,2023-10-09,2023-11-10,2023-11-23,2023-12-25,2024-01-01,2024-01-15,2024-02-14,2024-02-19,2024-05-27,2024-07-04,2024-09-02,2024-10-14,2024-11-11,2024-11-28,2024-12-25

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,896 @@
package net.corda.ptflows.contract.asset
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.Vault
import net.corda.core.node.services.VaultService
import net.corda.core.node.services.queryBy
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.ptflows.*
import net.corda.ptflows.utils.sumCash
import net.corda.ptflows.utils.sumCashBy
import net.corda.ptflows.utils.sumCashOrNull
import net.corda.ptflows.utils.sumCashOrZero
import net.corda.node.services.vault.NodeVaultService
import net.corda.node.utilities.CordaPersistence
import net.corda.ptflows.contracts.asset.*
import net.corda.testing.*
import net.corda.testing.contracts.DummyState
import net.corda.testing.contracts.calculateRandomlySizedAmounts
import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.security.KeyPair
import java.util.*
import kotlin.test.*
/**
* Creates a random set of between (by default) 3 and 10 cash states that add up to the given amount and adds them
* to the vault. This is intended for unit tests. The cash is issued by [DUMMY_CASH_ISSUER] and owned by the legal
* identity key from the storage service.
*
* The service hub needs to provide at least a key management service and a storage service.
*
* @param issuerServices service hub of the issuer node, which will be used to sign the transaction.
* @param outputNotary the notary to use for output states. The transaction is NOT signed by this notary.
* @return a vault object that represents the generated states (it will NOT be the full vault from the service hub!).
*/
fun ServiceHub.fillWithSomeTestCash(howMuch: Amount<Currency>,
issuerServices: ServiceHub = this,
outputNotary: Party = DUMMY_NOTARY,
atLeastThisManyStates: Int = 3,
atMostThisManyStates: Int = 10,
rng: Random = Random(),
ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })),
ownedBy: AbstractParty? = null,
issuedBy: PartyAndReference = DUMMY_CASH_ISSUER): Vault<PtCash.State> {
val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng)
val myKey = ownedBy?.owningKey ?: myInfo.chooseIdentity().owningKey
val anonParty = AnonymousParty(myKey)
// We will allocate one state to one transaction, for simplicities sake.
val cash = PtCash()
val transactions: List<SignedTransaction> = amounts.map { pennies ->
val issuance = TransactionBuilder(null as Party?)
cash.generateIssue(issuance, Amount(pennies, Issued(issuedBy.copy(reference = ref), howMuch.token)), anonParty, outputNotary)
return@map issuerServices.signInitialTransaction(issuance, issuedBy.party.owningKey)
}
recordTransactions(transactions)
// Get all the StateRefs of all the generated transactions.
val states = transactions.flatMap { stx ->
stx.tx.outputs.indices.map { i -> stx.tx.outRef<PtCash.State>(i) }
}
return Vault(states)
}
class PtCashTests : TestDependencyInjectionBase() {
val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
val defaultIssuer = MEGA_CORP.ref(defaultRef)
val inState = PtCash.State(
amount = 1000.DOLLARS `issued by` defaultIssuer,
owner = AnonymousParty(ALICE_PUBKEY)
)
// Input state held by the issuer
val issuerInState = inState.copy(owner = defaultIssuer.party)
val outState = issuerInState.copy(owner = AnonymousParty(BOB_PUBKEY))
fun PtCash.State.editDepositRef(ref: Byte) = copy(
amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref))))
)
lateinit var miniCorpServices: MockServices
lateinit var megaCorpServices: MockServices
val vault: VaultService get() = miniCorpServices.vaultService
lateinit var database: CordaPersistence
lateinit var vaultStatesUnconsumed: List<StateAndRef<PtCash.State>>
@Before
fun setUp() {
LogHelper.setLevel(NodeVaultService::class)
megaCorpServices = MockServices(listOf("net.corda.ptflows.contracts.asset"), MEGA_CORP_KEY)
val databaseAndServices = makeTestDatabaseAndMockServices(cordappPackages = listOf("net.corda.ptflows.contracts.asset"), keys = listOf(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY))
database = databaseAndServices.first
miniCorpServices = databaseAndServices.second
// Create some cash. Any attempt to spend >$500 will require multiple issuers to be involved.
database.transaction {
miniCorpServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
ownedBy = OUR_IDENTITY_1, issuedBy = MEGA_CORP.ref(1), issuerServices = megaCorpServices)
miniCorpServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
ownedBy = OUR_IDENTITY_1, issuedBy = MEGA_CORP.ref(1), issuerServices = megaCorpServices)
miniCorpServices.fillWithSomeTestCash(howMuch = 80.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
ownedBy = OUR_IDENTITY_1, issuedBy = MINI_CORP.ref(1), issuerServices = miniCorpServices)
miniCorpServices.fillWithSomeTestCash(howMuch = 80.SWISS_FRANCS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
ownedBy = OUR_IDENTITY_1, issuedBy = MINI_CORP.ref(1), issuerServices = miniCorpServices)
}
database.transaction {
vaultStatesUnconsumed = miniCorpServices.vaultQueryService.queryBy<PtCash.State>().states
}
resetTestSerialization()
}
@After
fun tearDown() {
database.close()
}
@Test
fun trivial() {
transaction {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { inState }
tweak {
output(PtCash.PROGRAM_ID) { outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer) }
command(ALICE_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "the amounts balance"
}
tweak {
output(PtCash.PROGRAM_ID) { outState }
command(ALICE_PUBKEY) { DummyCommandData }
// Invalid command
this `fails with` "required net.corda.ptflows.contracts.asset.PtCash.Commands.Move command"
}
tweak {
output(PtCash.PROGRAM_ID) { outState }
command(BOB_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "the owning keys are a subset of the signing keys"
}
tweak {
output(PtCash.PROGRAM_ID) { outState }
output(PtCash.PROGRAM_ID) { outState `issued by` MINI_CORP }
command(ALICE_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "at least one cash input"
}
// Simple reallocation works.
tweak {
output(PtCash.PROGRAM_ID) { outState }
command(ALICE_PUBKEY) { PtCash.Commands.Move() }
this.verifies()
}
}
}
@Test
fun `issue by move`() {
// Check we can't "move" money into existence.
transaction {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { DummyState() }
output(PtCash.PROGRAM_ID) { outState }
command(MINI_CORP_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "there is at least one cash input for this group"
}
}
@Test
fun issue() {
// 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 {
attachment(PtCash.PROGRAM_ID)
output(PtCash.PROGRAM_ID) { outState }
command(ALICE_PUBKEY) { PtCash.Commands.Issue() }
this `fails with` "output states are issued by a command signer"
}
transaction {
attachment(PtCash.PROGRAM_ID)
output(PtCash.PROGRAM_ID) {
PtCash.State(
amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34),
owner = AnonymousParty(ALICE_PUBKEY)
)
}
command(MINI_CORP_PUBKEY) { PtCash.Commands.Issue() }
this.verifies()
}
}
@Test
fun generateIssueRaw() {
initialiseTestSerialization()
// Test generation works.
val tx: WireTransaction = TransactionBuilder(notary = null).apply {
PtCash().generateIssue(this, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = AnonymousParty(ALICE_PUBKEY), notary = DUMMY_NOTARY)
}.toWireTransaction(miniCorpServices)
assertTrue(tx.inputs.isEmpty())
val s = tx.outputsOfType<PtCash.State>().single()
assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount)
assertEquals(MINI_CORP as AbstractParty, s.amount.token.issuer.party)
assertEquals(AnonymousParty(ALICE_PUBKEY), s.owner)
assertTrue(tx.commands[0].value is PtCash.Commands.Issue)
assertEquals(MINI_CORP_PUBKEY, tx.commands[0].signers[0])
}
@Test
fun generateIssueFromAmount() {
initialiseTestSerialization()
// Test issuance from an issued amount
val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34)
val tx: WireTransaction = TransactionBuilder(notary = null).apply {
PtCash().generateIssue(this, amount, owner = AnonymousParty(ALICE_PUBKEY), notary = DUMMY_NOTARY)
}.toWireTransaction(miniCorpServices)
assertTrue(tx.inputs.isEmpty())
assertEquals(tx.outputs[0], tx.outputs[0])
}
@Test
fun `extended issue examples`() {
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
transaction {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { issuerInState }
output(PtCash.PROGRAM_ID) { inState.copy(amount = inState.amount * 2) }
// Move fails: not allowed to summon money.
tweak {
command(ALICE_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "the amounts balance"
}
// Issue works.
tweak {
command(MEGA_CORP_PUBKEY) { PtCash.Commands.Issue() }
this.verifies()
}
}
// Can't use an issue command to lower the amount.
transaction {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { inState }
output(PtCash.PROGRAM_ID) { inState.copy(amount = inState.amount.splitEvenly(2).first()) }
command(MEGA_CORP_PUBKEY) { PtCash.Commands.Issue() }
this `fails with` "output values sum to more than the inputs"
}
// Can't have an issue command that doesn't actually issue money.
transaction {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { inState }
output(PtCash.PROGRAM_ID) { inState }
command(MEGA_CORP_PUBKEY) { PtCash.Commands.Issue() }
this `fails with` "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 {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { inState }
output(PtCash.PROGRAM_ID) { inState.copy(amount = inState.amount * 2) }
command(MEGA_CORP_PUBKEY) { PtCash.Commands.Issue() }
tweak {
command(MEGA_CORP_PUBKEY) { PtCash.Commands.Issue() }
this `fails with` "there is only a single issue command"
}
this.verifies()
}
}
/**
* Test that the issuance builder rejects building into a transaction with existing
* cash inputs.
*/
@Test(expected = IllegalStateException::class)
fun `reject issuance with inputs`() {
initialiseTestSerialization()
// Issue some cash
var ptx = TransactionBuilder(DUMMY_NOTARY)
PtCash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY)
val tx = miniCorpServices.signInitialTransaction(ptx)
// Include the previously issued cash in a new issuance command
ptx = TransactionBuilder(DUMMY_NOTARY)
ptx.addInputState(tx.tx.outRef<PtCash.State>(0))
PtCash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY)
}
@Test
fun testMergeSplit() {
// Splitting value works.
transaction {
attachment(PtCash.PROGRAM_ID)
command(ALICE_PUBKEY) { PtCash.Commands.Move() }
tweak {
input(PtCash.PROGRAM_ID) { inState }
val splits4 = inState.amount.splitEvenly(4)
for (i in 0..3) output(PtCash.PROGRAM_ID) { inState.copy(amount = splits4[i]) }
this.verifies()
}
// Merging 4 inputs into 2 outputs works.
tweak {
val splits2 = inState.amount.splitEvenly(2)
val splits4 = inState.amount.splitEvenly(4)
for (i in 0..3) input(PtCash.PROGRAM_ID) { inState.copy(amount = splits4[i]) }
for (i in 0..1) output(PtCash.PROGRAM_ID) { inState.copy(amount = splits2[i]) }
this.verifies()
}
// Merging 2 inputs into 1 works.
tweak {
val splits2 = inState.amount.splitEvenly(2)
for (i in 0..1) input(PtCash.PROGRAM_ID) { inState.copy(amount = splits2[i]) }
output(PtCash.PROGRAM_ID) { inState }
this.verifies()
}
}
}
@Test
fun zeroSizedValues() {
transaction {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { inState }
input(PtCash.PROGRAM_ID) { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
command(ALICE_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "zero sized inputs"
}
transaction {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { inState }
output(PtCash.PROGRAM_ID) { inState }
output(PtCash.PROGRAM_ID) { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
command(ALICE_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "zero sized outputs"
}
}
@Test
fun trivialMismatches() {
// Can't change issuer.
transaction {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { inState }
output(PtCash.PROGRAM_ID) { outState `issued by` MINI_CORP }
command(ALICE_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "the amounts balance"
}
// Can't change deposit reference when splitting.
transaction {
attachment(PtCash.PROGRAM_ID)
val splits2 = inState.amount.splitEvenly(2)
input(PtCash.PROGRAM_ID) { inState }
for (i in 0..1) output(PtCash.PROGRAM_ID) { outState.copy(amount = splits2[i]).editDepositRef(i.toByte()) }
command(ALICE_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "the amounts balance"
}
// Can't mix currencies.
transaction {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { inState }
output(PtCash.PROGRAM_ID) { outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer) }
output(PtCash.PROGRAM_ID) { outState.copy(amount = 200.POUNDS `issued by` defaultIssuer) }
command(ALICE_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "the amounts balance"
}
transaction {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { inState }
input(PtCash.PROGRAM_ID) {
inState.copy(
amount = 150.POUNDS `issued by` defaultIssuer,
owner = AnonymousParty(BOB_PUBKEY)
)
}
output(PtCash.PROGRAM_ID) { outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer) }
command(ALICE_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "the amounts balance"
}
// Can't have superfluous input states from different issuers.
transaction {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { inState }
input(PtCash.PROGRAM_ID) { inState `issued by` MINI_CORP }
output(PtCash.PROGRAM_ID) { outState }
command(ALICE_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "the amounts balance"
}
// Can't combine two different deposits at the same issuer.
transaction {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { inState }
input(PtCash.PROGRAM_ID) { inState.editDepositRef(3) }
output(PtCash.PROGRAM_ID) { outState.copy(amount = inState.amount * 2).editDepositRef(3) }
command(ALICE_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "for reference [01]"
}
}
@Test
fun exitLedger() {
// Single input/output straightforward case.
transaction {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { issuerInState }
output(PtCash.PROGRAM_ID) { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
tweak {
command(MEGA_CORP_PUBKEY) { PtCash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) }
command(MEGA_CORP_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "the amounts balance"
}
tweak {
command(MEGA_CORP_PUBKEY) { PtCash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
this `fails with` "required net.corda.ptflows.contracts.asset.PtCash.Commands.Move command"
tweak {
command(MEGA_CORP_PUBKEY) { PtCash.Commands.Move() }
this.verifies()
}
}
}
}
@Test
fun `exit ledger with multiple issuers`() {
// Multi-issuer case.
transaction {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { issuerInState }
input(PtCash.PROGRAM_ID) { issuerInState.copy(owner = MINI_CORP) `issued by` MINI_CORP }
output(PtCash.PROGRAM_ID) { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) `issued by` MINI_CORP }
output(PtCash.PROGRAM_ID) { issuerInState.copy(owner = MINI_CORP, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "the amounts balance"
command(MEGA_CORP_PUBKEY) { PtCash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
this `fails with` "the amounts balance"
command(MINI_CORP_PUBKEY) { PtCash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)) }
this.verifies()
}
}
@Test
fun `exit cash not held by its issuer`() {
// Single input/output straightforward case.
transaction {
attachment(PtCash.PROGRAM_ID)
input(PtCash.PROGRAM_ID) { inState }
output(PtCash.PROGRAM_ID) { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
command(MEGA_CORP_PUBKEY) { PtCash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
command(ALICE_PUBKEY) { PtCash.Commands.Move() }
this `fails with` "the amounts balance"
}
}
@Test
fun multiIssuer() {
transaction {
attachment(PtCash.PROGRAM_ID)
// Gather 2000 dollars from two different issuers.
input(PtCash.PROGRAM_ID) { inState }
input(PtCash.PROGRAM_ID) { inState `issued by` MINI_CORP }
command(ALICE_PUBKEY) { PtCash.Commands.Move() }
// Can't merge them together.
tweak {
output(PtCash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY), amount = 2000.DOLLARS `issued by` defaultIssuer) }
this `fails with` "the amounts balance"
}
// Missing MiniCorp deposit
tweak {
output(PtCash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) }
output(PtCash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) }
this `fails with` "the amounts balance"
}
// This works.
output(PtCash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) }
output(PtCash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) `issued by` MINI_CORP }
this.verifies()
}
}
@Test
fun multiCurrency() {
// Check we can do an atomic currency trade tx.
transaction {
attachment(PtCash.PROGRAM_ID)
val pounds = PtCash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), AnonymousParty(BOB_PUBKEY))
input(PtCash.PROGRAM_ID) { inState `owned by` AnonymousParty(ALICE_PUBKEY) }
input(PtCash.PROGRAM_ID) { pounds }
output(PtCash.PROGRAM_ID) { inState `owned by` AnonymousParty(BOB_PUBKEY) }
output(PtCash.PROGRAM_ID) { pounds `owned by` AnonymousParty(ALICE_PUBKEY) }
command(ALICE_PUBKEY, BOB_PUBKEY) { PtCash.Commands.Move() }
this.verifies()
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Spend tx generation
val OUR_KEY: KeyPair by lazy { generateKeyPair() }
val OUR_IDENTITY_1: AbstractParty get() = AnonymousParty(OUR_KEY.public)
val THEIR_IDENTITY_1 = AnonymousParty(MINI_CORP_PUBKEY)
val THEIR_IDENTITY_2 = AnonymousParty(CHARLIE_PUBKEY)
fun makeCash(amount: Amount<Currency>, corp: Party, depositRef: Byte = 1) =
StateAndRef(
TransactionState<PtCash.State>(PtCash.State(amount `issued by` corp.ref(depositRef), OUR_IDENTITY_1), PtCash.PROGRAM_ID, 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)
)
/**
* Generate an exit transaction, removing some amount of cash from the ledger.
*/
private fun makeExit(amount: Amount<Currency>, corp: Party, depositRef: Byte = 1): WireTransaction {
val tx = TransactionBuilder(DUMMY_NOTARY)
PtCash().generateExit(tx, Amount(amount.quantity, Issued(corp.ref(depositRef), amount.token)), WALLET)
return tx.toWireTransaction(miniCorpServices)
}
private fun makeSpend(amount: Amount<Currency>, dest: AbstractParty): WireTransaction {
val tx = TransactionBuilder(DUMMY_NOTARY)
database.transaction {
PtCash.generateSpend(miniCorpServices, tx, amount, dest)
}
return tx.toWireTransaction(miniCorpServices)
}
/**
* Try exiting an amount which matches a single state.
*/
@Test
fun generateSimpleExit() {
initialiseTestSerialization()
val wtx = makeExit(100.DOLLARS, MEGA_CORP, 1)
assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(0, wtx.outputs.size)
val expectedMove = PtCash.Commands.Move()
val expectedExit = PtCash.Commands.Exit(Amount(10000, Issued(MEGA_CORP.ref(1), USD)))
assertEquals(listOf(expectedMove, expectedExit), wtx.commands.map { it.value })
}
/**
* Try exiting an amount smaller than the smallest available input state, and confirm change is generated correctly.
*/
@Test
fun generatePartialExit() {
initialiseTestSerialization()
val wtx = makeExit(50.DOLLARS, MEGA_CORP, 1)
assertEquals(WALLET[0].ref, wtx.inputs[0])
assertEquals(1, wtx.outputs.size)
assertEquals(WALLET[0].state.data.copy(amount = WALLET[0].state.data.amount.splitEvenly(2).first()), wtx.getOutput(0))
}
/**
* Try exiting a currency we don't have.
*/
@Test
fun generateAbsentExit() {
initialiseTestSerialization()
assertFailsWith<InsufficientBalanceException> { makeExit(100.POUNDS, MEGA_CORP, 1) }
}
/**
* Try exiting with a reference mis-match.
*/
@Test
fun generateInvalidReferenceExit() {
initialiseTestSerialization()
assertFailsWith<InsufficientBalanceException> { makeExit(100.POUNDS, MEGA_CORP, 2) }
}
/**
* Try exiting an amount greater than the maximum available.
*/
@Test
fun generateInsufficientExit() {
initialiseTestSerialization()
assertFailsWith<InsufficientBalanceException> { makeExit(1000.DOLLARS, MEGA_CORP, 1) }
}
/**
* Try exiting for an owner with no states
*/
@Test
fun generateOwnerWithNoStatesExit() {
initialiseTestSerialization()
assertFailsWith<InsufficientBalanceException> { makeExit(100.POUNDS, CHARLIE, 1) }
}
/**
* Try exiting when vault is empty
*/
@Test
fun generateExitWithEmptyVault() {
initialiseTestSerialization()
assertFailsWith<InsufficientBalanceException> {
val tx = TransactionBuilder(DUMMY_NOTARY)
PtCash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList())
}
}
@Test
fun generateSimpleDirectSpend() {
initialiseTestSerialization()
val wtx =
database.transaction {
makeSpend(100.DOLLARS, THEIR_IDENTITY_1)
}
database.transaction {
@Suppress("UNCHECKED_CAST")
val vaultState = vaultStatesUnconsumed.elementAt(0)
assertEquals(vaultState.ref, wtx.inputs[0])
assertEquals(vaultState.state.data.copy(owner = THEIR_IDENTITY_1), wtx.getOutput(0))
assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is PtCash.Commands.Move }.signers[0])
}
}
@Test
fun generateSimpleSpendWithParties() {
initialiseTestSerialization()
database.transaction {
val tx = TransactionBuilder(DUMMY_NOTARY)
PtCash.generateSpend(miniCorpServices, tx, 80.DOLLARS, ALICE, setOf(MINI_CORP))
assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0])
}
}
@Test
fun generateSimpleSpendWithChange() {
initialiseTestSerialization()
val wtx =
database.transaction {
makeSpend(10.DOLLARS, THEIR_IDENTITY_1)
}
database.transaction {
@Suppress("UNCHECKED_CAST")
val vaultState = vaultStatesUnconsumed.elementAt(0)
val changeAmount = 90.DOLLARS `issued by` defaultIssuer
val likelyChangeState = wtx.outputs.map(TransactionState<*>::data).filter { state ->
if (state is PtCash.State) {
state.amount == changeAmount
} else {
false
}
}.single()
val changeOwner = (likelyChangeState as PtCash.State).owner
assertEquals(1, miniCorpServices.keyManagementService.filterMyKeys(setOf(changeOwner.owningKey)).toList().size)
assertEquals(vaultState.ref, wtx.inputs[0])
assertEquals(vaultState.state.data.copy(owner = THEIR_IDENTITY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
assertEquals(vaultState.state.data.copy(amount = changeAmount, owner = changeOwner), wtx.outputs[1].data)
assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is PtCash.Commands.Move }.signers[0])
}
}
@Test
fun generateSpendWithTwoInputs() {
initialiseTestSerialization()
val wtx =
database.transaction {
makeSpend(500.DOLLARS, THEIR_IDENTITY_1)
}
database.transaction {
@Suppress("UNCHECKED_CAST")
val vaultState0 = vaultStatesUnconsumed.elementAt(0)
val vaultState1 = vaultStatesUnconsumed.elementAt(1)
assertEquals(vaultState0.ref, wtx.inputs[0])
assertEquals(vaultState1.ref, wtx.inputs[1])
assertEquals(vaultState0.state.data.copy(owner = THEIR_IDENTITY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.getOutput(0))
assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is PtCash.Commands.Move }.signers[0])
}
}
@Test
fun generateSpendMixedDeposits() {
initialiseTestSerialization()
val wtx =
database.transaction {
val wtx = makeSpend(580.DOLLARS, THEIR_IDENTITY_1)
assertEquals(3, wtx.inputs.size)
wtx
}
database.transaction {
val vaultState0: StateAndRef<PtCash.State> = vaultStatesUnconsumed.elementAt(0)
val vaultState1: StateAndRef<PtCash.State> = vaultStatesUnconsumed.elementAt(1)
val vaultState2: StateAndRef<PtCash.State> = vaultStatesUnconsumed.elementAt(2)
assertEquals(vaultState0.ref, wtx.inputs[0])
assertEquals(vaultState1.ref, wtx.inputs[1])
assertEquals(vaultState2.ref, wtx.inputs[2])
assertEquals(vaultState0.state.data.copy(owner = THEIR_IDENTITY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data)
assertEquals(vaultState2.state.data.copy(owner = THEIR_IDENTITY_1), wtx.outputs[0].data)
assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is PtCash.Commands.Move }.signers[0])
}
}
@Test
fun generateSpendInsufficientBalance() {
initialiseTestSerialization()
database.transaction {
val e: InsufficientBalanceException = assertFailsWith("balance") {
makeSpend(1000.DOLLARS, THEIR_IDENTITY_1)
}
assertEquals((1000 - 580).DOLLARS, e.amountMissing)
assertFailsWith(InsufficientBalanceException::class) {
makeSpend(81.SWISS_FRANCS, THEIR_IDENTITY_1)
}
}
}
/**
* Confirm that aggregation of states is correctly modelled.
*/
@Test
fun aggregation() {
val fiveThousandDollarsFromMega = PtCash.State(5000.DOLLARS `issued by` MEGA_CORP.ref(2), MEGA_CORP)
val twoThousandDollarsFromMega = PtCash.State(2000.DOLLARS `issued by` MEGA_CORP.ref(2), MINI_CORP)
val oneThousandDollarsFromMini = PtCash.State(1000.DOLLARS `issued by` MINI_CORP.ref(3), MEGA_CORP)
// Obviously it must be possible to aggregate states with themselves
assertEquals(fiveThousandDollarsFromMega.amount.token, fiveThousandDollarsFromMega.amount.token)
// Owner is not considered when calculating whether it is possible to aggregate states
assertEquals(fiveThousandDollarsFromMega.amount.token, twoThousandDollarsFromMega.amount.token)
// States cannot be aggregated if the deposit differs
assertNotEquals(fiveThousandDollarsFromMega.amount.token, oneThousandDollarsFromMini.amount.token)
assertNotEquals(twoThousandDollarsFromMega.amount.token, oneThousandDollarsFromMini.amount.token)
// States cannot be aggregated if the currency differs
assertNotEquals(oneThousandDollarsFromMini.amount.token,
PtCash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP).amount.token)
// States cannot be aggregated if the reference differs
assertNotEquals(fiveThousandDollarsFromMega.amount.token, (fiveThousandDollarsFromMega `with deposit` defaultIssuer).amount.token)
assertNotEquals((fiveThousandDollarsFromMega `with deposit` defaultIssuer).amount.token, fiveThousandDollarsFromMega.amount.token)
}
@Test
fun `summing by owner`() {
val states = listOf(
PtCash.State(1000.DOLLARS `issued by` defaultIssuer, MINI_CORP),
PtCash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
PtCash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP)
)
assertEquals(6000.DOLLARS `issued by` defaultIssuer, states.sumCashBy(MEGA_CORP))
}
@Test(expected = UnsupportedOperationException::class)
fun `summing by owner throws`() {
val states = listOf(
PtCash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
PtCash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP)
)
states.sumCashBy(MINI_CORP)
}
@Test
fun `summing no currencies`() {
val states = emptyList<PtCash.State>()
assertEquals(0.POUNDS `issued by` defaultIssuer, states.sumCashOrZero(GBP `issued by` defaultIssuer))
assertNull(states.sumCashOrNull())
}
@Test(expected = UnsupportedOperationException::class)
fun `summing no currencies throws`() {
val states = emptyList<PtCash.State>()
states.sumCash()
}
@Test
fun `summing a single currency`() {
val states = listOf(
PtCash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
PtCash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
PtCash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP)
)
// Test that summing everything produces the total number of dollars
val expected = 7000.DOLLARS `issued by` defaultIssuer
val actual = states.sumCash()
assertEquals(expected, actual)
}
@Test(expected = IllegalArgumentException::class)
fun `summing multiple currencies`() {
val states = listOf(
PtCash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
PtCash.State(4000.POUNDS `issued by` defaultIssuer, MEGA_CORP)
)
// Test that summing everything fails because we're mixing units
states.sumCash()
}
// Double spend.
@Test
fun chainCashDoubleSpendFailsWith() {
val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), MEGA_CORP_KEY)
ledger(mockService) {
unverifiedTransaction {
attachment(PtCash.PROGRAM_ID)
output(PtCash.PROGRAM_ID, "MEGA_CORP cash") {
PtCash.State(
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
owner = MEGA_CORP
)
}
}
transaction {
attachment(PtCash.PROGRAM_ID)
input("MEGA_CORP cash")
output(PtCash.PROGRAM_ID, "MEGA_CORP cash 2", "MEGA_CORP cash".output<PtCash.State>().copy(owner = AnonymousParty(ALICE_PUBKEY)) )
command(MEGA_CORP_PUBKEY) { PtCash.Commands.Move() }
this.verifies()
}
tweak {
transaction {
attachment(PtCash.PROGRAM_ID)
input("MEGA_CORP cash")
// We send it to another pubkey so that the transaction is not identical to the previous one
output(PtCash.PROGRAM_ID, "MEGA_CORP cash 3", "MEGA_CORP cash".output<PtCash.State>().copy(owner = ALICE))
command(MEGA_CORP_PUBKEY) { PtCash.Commands.Move() }
this.verifies()
}
this.fails()
}
this.verifies()
}
}
@Test
fun multiSpend() {
initialiseTestSerialization()
val tx = TransactionBuilder(DUMMY_NOTARY)
database.transaction {
val payments = listOf(
PartyAndAmount(THEIR_IDENTITY_1, 400.DOLLARS),
PartyAndAmount(THEIR_IDENTITY_2, 150.DOLLARS)
)
PtCash.generateSpend(miniCorpServices, tx, payments)
}
val wtx = tx.toWireTransaction(miniCorpServices)
fun out(i: Int) = wtx.getOutput(i) as PtCash.State
assertEquals(4, wtx.outputs.size)
assertEquals(80.DOLLARS, out(0).amount.withoutIssuer())
assertEquals(320.DOLLARS, out(1).amount.withoutIssuer())
assertEquals(150.DOLLARS, out(2).amount.withoutIssuer())
assertEquals(30.DOLLARS, out(3).amount.withoutIssuer())
assertEquals(MINI_CORP, out(0).amount.token.issuer.party)
assertEquals(MEGA_CORP, out(1).amount.token.issuer.party)
assertEquals(MEGA_CORP, out(2).amount.token.issuer.party)
assertEquals(MEGA_CORP, out(3).amount.token.issuer.party)
}
}