mirror of
https://github.com/corda/corda.git
synced 2024-12-28 16:58:55 +00:00
PtCashTests and missing bits of implementation to make them work
This commit is contained in:
parent
8ae92850c9
commit
22bf2b1c1d
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
net.corda.ptflows.contracts.asset.cash.selection.PtCashSelectionH2Impl
|
||||
|
@ -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
|
@ -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
|
1002
perftestflows/src/main/resources/finance/utils/cities.txt
Normal file
1002
perftestflows/src/main/resources/finance/utils/cities.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user