mirror of
https://github.com/corda/corda.git
synced 2025-01-01 02:36:44 +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)
|
instance.set(cashSelectionAlgo)
|
||||||
cashSelectionAlgo
|
cashSelectionAlgo
|
||||||
} ?: throw ClassNotFoundException("\nUnable to load compatible cash selection algorithm implementation for JDBC driver ($_metadata)." +
|
} ?: 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()
|
}.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -261,7 +261,7 @@ class PtCash : PtOnLedgerAsset<Currency, PtCash.Commands, PtCash.State>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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
|
* 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