mirror of
https://github.com/corda/corda.git
synced 2025-01-13 16:30:25 +00:00
SQL coin selection (#186)
* Copy SQL server cash selection and all other required changes from finance to perftestcordapp * Copy SQL server cash selection and all other required changes from finance to perftestcordapp * Add cash selection to manifest
This commit is contained in:
parent
2512b29b26
commit
205663d37f
@ -23,6 +23,7 @@ import com.r3.corda.enterprise.perftestcordapp.utils.sumCash
|
||||
import com.r3.corda.enterprise.perftestcordapp.utils.sumCashOrNull
|
||||
import com.r3.corda.enterprise.perftestcordapp.utils.sumCashOrZero
|
||||
import com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import java.math.BigInteger
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
@ -81,7 +82,7 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
owner = this.owner,
|
||||
pennies = this.amount.quantity,
|
||||
currency = this.amount.token.product.currencyCode,
|
||||
issuerParty = this.amount.token.issuer.party.owningKey.toBase58String(),
|
||||
issuerPartyHash = this.amount.token.issuer.party.owningKey.toStringShort(),
|
||||
issuerRef = this.amount.token.issuer.reference.bytes
|
||||
)
|
||||
/** Additional schema mappings would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */
|
||||
@ -341,10 +342,12 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
|
||||
// Unit testing helpers. These could go in a separate file but it's hardly worth it for just a few functions.
|
||||
|
||||
/** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */
|
||||
val DUMMY_CASH_ISSUER_NAME = CordaX500Name(organisation = "Snake Oil Issuer", locality = "London", country = "GB")
|
||||
/** A randomly generated key. */
|
||||
val DUMMY_CASH_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) }
|
||||
/** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */
|
||||
val DUMMY_CASH_ISSUER by lazy { Party(CordaX500Name(organisation = "Snake Oil Issuer", locality = "London", country = "GB"), DUMMY_CASH_ISSUER_KEY.public).ref(1) }
|
||||
val DUMMY_CASH_ISSUER by lazy { Party(DUMMY_CASH_ISSUER_NAME, DUMMY_CASH_ISSUER_KEY.public).ref(1) }
|
||||
/** An extension property that lets you write 100.DOLLARS.CASH */
|
||||
val Amount<Currency>.CASH: Cash.State get() = Cash.State(Amount(quantity, Issued(DUMMY_CASH_ISSUER, token)), NULL_PARTY)
|
||||
/** An extension property that lets you get a cash state from an issued token, under the [NULL_PARTY] */
|
||||
|
@ -27,7 +27,9 @@ import kotlin.concurrent.withLock
|
||||
* Custom implementations must implement this interface and declare their implementation in
|
||||
* META-INF/services/net.corda.contracts.asset.CashSelection
|
||||
*/
|
||||
abstract class AbstractCashSelection {
|
||||
// TODO: make parameters configurable when we get CorDapp configuration.
|
||||
abstract class AbstractCashSelection(private val maxRetries: Int = 8, private val retrySleep: Int = 100,
|
||||
private val retryCap: Int = 2000) {
|
||||
companion object {
|
||||
val instance = AtomicReference<AbstractCashSelection>()
|
||||
|
||||
@ -44,14 +46,10 @@ abstract class AbstractCashSelection {
|
||||
}.invoke()
|
||||
}
|
||||
|
||||
val log = loggerFor<AbstractCashSelection>()
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
// coin selection retry loop counter, sleep (msecs) and lock for selecting states
|
||||
// TODO: make parameters configurable when we get CorDapp configuration.
|
||||
private val MAX_RETRIES = 8
|
||||
private val RETRY_SLEEP = 100
|
||||
private val RETRY_CAP = 2000
|
||||
private val spendLock: ReentrantLock = ReentrantLock()
|
||||
|
||||
/**
|
||||
@ -64,7 +62,7 @@ abstract class AbstractCashSelection {
|
||||
|
||||
/**
|
||||
* A vendor specific query(ies) to gather Cash states that are available.
|
||||
* @param statement The service hub to allow access to the database session
|
||||
* @param connection The service hub to allow access to the database session
|
||||
* @param amount The amount of currency desired (ignoring issues, but specifying the currency)
|
||||
* @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.
|
||||
@ -72,13 +70,14 @@ abstract class AbstractCashSelection {
|
||||
* with this notary are included.
|
||||
* @param onlyFromIssuerParties Optional issuer parties to match against.
|
||||
* @param withIssuerRefs Optional issuer references to match against.
|
||||
* @return JDBC ResultSet with the matching states that were found. If sufficient funds were found these will be locked,
|
||||
* @param withResultSet Function that contains the business logic. The JDBC ResultSet with 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.
|
||||
* @return The result of the withResultSet function
|
||||
*/
|
||||
abstract fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
|
||||
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet
|
||||
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean
|
||||
|
||||
override abstract fun toString() : String
|
||||
override abstract fun toString(): String
|
||||
|
||||
/**
|
||||
* Query to gather Cash states that are available and retry if they are temporarily unavailable.
|
||||
@ -103,13 +102,13 @@ abstract class AbstractCashSelection {
|
||||
withIssuerRefs: Set<OpaqueBytes> = emptySet()): List<StateAndRef<Cash.State>> {
|
||||
val stateAndRefs = mutableListOf<StateAndRef<Cash.State>>()
|
||||
|
||||
for (retryCount in 1..MAX_RETRIES) {
|
||||
for (retryCount in 1..maxRetries) {
|
||||
if (!attemptSpend(services, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs, stateAndRefs)) {
|
||||
log.warn("Coin selection failed on attempt $retryCount")
|
||||
// TODO: revisit the back off strategy for contended spending.
|
||||
if (retryCount != MAX_RETRIES) {
|
||||
if (retryCount != maxRetries) {
|
||||
stateAndRefs.clear()
|
||||
val durationMillis = (minOf(RETRY_SLEEP.shl(retryCount), RETRY_CAP / 2) * (1.0 + Math.random())).toInt()
|
||||
val durationMillis = (minOf(retrySleep.shl(retryCount), retryCap / 2) * (1.0 + Math.random())).toInt()
|
||||
FlowLogic.sleep(durationMillis.millis)
|
||||
} else {
|
||||
log.warn("Insufficient spendable states identified for $amount")
|
||||
@ -127,34 +126,40 @@ abstract class AbstractCashSelection {
|
||||
try {
|
||||
// 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 rs = executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs)
|
||||
stateAndRefs.clear()
|
||||
return executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs) { rs ->
|
||||
stateAndRefs.clear()
|
||||
|
||||
var totalPennies = 0L
|
||||
val stateRefs = mutableSetOf<StateRef>()
|
||||
while (rs.next()) {
|
||||
val txHash = SecureHash.parse(rs.getString(1))
|
||||
val index = rs.getInt(2)
|
||||
val pennies = rs.getLong(3)
|
||||
totalPennies = rs.getLong(4)
|
||||
val rowLockId = rs.getString(5)
|
||||
stateRefs.add(StateRef(txHash, index))
|
||||
log.trace { "ROW: $rowLockId ($lockId): ${StateRef(txHash, index)} : $pennies ($totalPennies)" }
|
||||
var totalPennies = 0L
|
||||
val stateRefs = mutableSetOf<StateRef>()
|
||||
while (rs.next()) {
|
||||
val txHash = SecureHash.parse(rs.getString(1))
|
||||
val index = rs.getInt(2)
|
||||
val pennies = rs.getLong(3)
|
||||
totalPennies = rs.getLong(4)
|
||||
val rowLockId = rs.getString(5)
|
||||
stateRefs.add(StateRef(txHash, index))
|
||||
log.trace { "ROW: $rowLockId ($lockId): ${StateRef(txHash, index)} : $pennies ($totalPennies)" }
|
||||
}
|
||||
|
||||
if (stateRefs.isNotEmpty()) {
|
||||
// TODO: future implementation to retrieve contract states from a Vault BLOB store
|
||||
stateAndRefs.addAll(services.loadStates(stateRefs) as Collection<StateAndRef<Cash.State>>)
|
||||
}
|
||||
|
||||
val success = stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity
|
||||
if (success) {
|
||||
// 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())
|
||||
} else {
|
||||
log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}")
|
||||
}
|
||||
success
|
||||
}
|
||||
if (stateRefs.isNotEmpty())
|
||||
// TODO: future implementation to retrieve contract states from a Vault BLOB store
|
||||
stateAndRefs.addAll(services.loadStates(stateRefs) as Collection<StateAndRef<Cash.State>>)
|
||||
|
||||
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 true
|
||||
}
|
||||
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]
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.*
|
||||
@ -13,7 +14,7 @@ class CashSelectionH2Impl : AbstractCashSelection() {
|
||||
|
||||
companion object {
|
||||
const val JDBC_DRIVER_NAME = "H2 JDBC Driver"
|
||||
val log = loggerFor<CashSelectionH2Impl>()
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
override fun isCompatible(metadata: DatabaseMetaData): Boolean {
|
||||
@ -22,16 +23,14 @@ class CashSelectionH2Impl : AbstractCashSelection() {
|
||||
|
||||
override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME"
|
||||
|
||||
|
||||
// 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)
|
||||
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
|
||||
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : ResultSet {
|
||||
connection.createStatement().execute("CALL SET(@t, 0);")
|
||||
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
|
||||
connection.createStatement().use { it.execute("CALL SET(@t, CAST(0 AS BIGINT));") }
|
||||
|
||||
val selectJoin = """
|
||||
SELECT vs.transaction_id, vs.output_index, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
|
||||
@ -44,24 +43,27 @@ class CashSelectionH2Impl : AbstractCashSelection() {
|
||||
(if (notary != null)
|
||||
" AND vs.notary_name = ?" else "") +
|
||||
(if (onlyFromIssuerParties.isNotEmpty())
|
||||
" AND ccs.issuer_key IN (?)" else "") +
|
||||
" AND ccs.issuer_key_hash IN (?)" else "") +
|
||||
(if (withIssuerRefs.isNotEmpty())
|
||||
" AND ccs.issuer_ref IN (?)" else "")
|
||||
|
||||
// Use prepared statement for protection against SQL Injection (http://www.h2database.com/html/advanced.html#sql_injection)
|
||||
val psSelectJoin = connection.prepareStatement(selectJoin)
|
||||
var pIndex = 0
|
||||
psSelectJoin.setString(++pIndex, amount.token.currencyCode)
|
||||
psSelectJoin.setLong(++pIndex, amount.quantity)
|
||||
psSelectJoin.setString(++pIndex, lockId.toString())
|
||||
if (notary != null)
|
||||
psSelectJoin.setString(++pIndex, notary.name.toString())
|
||||
if (onlyFromIssuerParties.isNotEmpty())
|
||||
psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any}.toTypedArray() )
|
||||
if (withIssuerRefs.isNotEmpty())
|
||||
psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes.toHexString() as Any }.toTypedArray())
|
||||
log.debug { psSelectJoin.toString() }
|
||||
connection.prepareStatement(selectJoin).use { psSelectJoin ->
|
||||
var pIndex = 0
|
||||
psSelectJoin.setString(++pIndex, amount.token.currencyCode)
|
||||
psSelectJoin.setLong(++pIndex, amount.quantity)
|
||||
psSelectJoin.setString(++pIndex, lockId.toString())
|
||||
if (notary != null)
|
||||
psSelectJoin.setString(++pIndex, notary.name.toString())
|
||||
if (onlyFromIssuerParties.isNotEmpty())
|
||||
psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toStringShort() as Any }.toTypedArray())
|
||||
if (withIssuerRefs.isNotEmpty())
|
||||
psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray())
|
||||
log.debug { psSelectJoin.toString() }
|
||||
|
||||
return psSelectJoin.executeQuery()
|
||||
psSelectJoin.executeQuery().use { rs ->
|
||||
return withResultSet(rs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import java.sql.Connection
|
||||
import java.sql.DatabaseMetaData
|
||||
import java.sql.ResultSet
|
||||
import java.util.*
|
||||
|
||||
class CashSelectionMySQLImpl : AbstractCashSelection() {
|
||||
|
||||
companion object {
|
||||
const val JDBC_DRIVER_NAME = "MySQL JDBC Driver"
|
||||
}
|
||||
|
||||
override fun isCompatible(metadata: DatabaseMetaData): Boolean {
|
||||
return metadata.driverName == JDBC_DRIVER_NAME
|
||||
}
|
||||
|
||||
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
|
||||
TODO("MySQL cash selection not implemented")
|
||||
}
|
||||
|
||||
override fun toString() = "${this::class.java} for ${CashSelectionH2Impl.JDBC_DRIVER_NAME}"
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.*
|
||||
import java.sql.Connection
|
||||
import java.sql.DatabaseMetaData
|
||||
import java.sql.ResultSet
|
||||
import java.util.*
|
||||
|
||||
class CashSelectionPostgreSQLImpl : AbstractCashSelection() {
|
||||
|
||||
companion object {
|
||||
val JDBC_DRIVER_NAME = "PostgreSQL JDBC Driver"
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
override fun isCompatible(metadata: DatabaseMetaData): Boolean {
|
||||
return metadata.driverName == JDBC_DRIVER_NAME
|
||||
}
|
||||
|
||||
override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME"
|
||||
|
||||
// This is using PostgreSQL window functions for selecting a minimum set of rows that match a request amount of coins:
|
||||
// 1) This may also be possible with user-defined functions (e.g. using PL/pgSQL)
|
||||
// 2) The window function accumulated column (`total`) does not include the current row (starts from 0) and cannot
|
||||
// appear in the WHERE clause, hence restricting row selection and adjusting the returned total in the outer query.
|
||||
// 3) Currently (version 9.6), FOR UPDATE cannot be specified with window functions
|
||||
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
|
||||
val selectJoin = """SELECT nested.transaction_id, nested.output_index, nested.pennies,
|
||||
nested.total+nested.pennies as total_pennies, nested.lock_id
|
||||
FROM
|
||||
(SELECT vs.transaction_id, vs.output_index, ccs.pennies,
|
||||
coalesce((SUM(ccs.pennies) OVER (PARTITION BY 1 ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING)), 0)
|
||||
AS total, 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 = ?
|
||||
AND (vs.lock_id = ? OR vs.lock_id is null)
|
||||
""" +
|
||||
(if (notary != null)
|
||||
" AND vs.notary_name = ?" else "") +
|
||||
(if (onlyFromIssuerParties.isNotEmpty())
|
||||
" AND ccs.issuer_key = ANY (?)" else "") +
|
||||
(if (withIssuerRefs.isNotEmpty())
|
||||
" AND ccs.issuer_ref = ANY (?)" else "") +
|
||||
""")
|
||||
nested WHERE nested.total < ?
|
||||
"""
|
||||
|
||||
connection.prepareStatement(selectJoin).use { statement ->
|
||||
statement.setString(1, amount.token.toString())
|
||||
statement.setString(2, lockId.toString())
|
||||
var paramOffset = 0
|
||||
if (notary != null) {
|
||||
statement.setString(3, notary.name.toString())
|
||||
paramOffset += 1
|
||||
}
|
||||
if (onlyFromIssuerParties.isNotEmpty()) {
|
||||
val issuerKeys = connection.createArrayOf("VARCHAR", onlyFromIssuerParties.map
|
||||
{ it.owningKey.toBase58String() }.toTypedArray())
|
||||
statement.setArray(3 + paramOffset, issuerKeys)
|
||||
paramOffset += 1
|
||||
}
|
||||
if (withIssuerRefs.isNotEmpty()) {
|
||||
val issuerRefs = connection.createArrayOf("BYTEA", withIssuerRefs.map
|
||||
{ it.bytes }.toTypedArray())
|
||||
statement.setArray(3 + paramOffset, issuerRefs)
|
||||
paramOffset += 1
|
||||
}
|
||||
statement.setLong(3 + paramOffset, amount.quantity)
|
||||
log.debug { statement.toString() }
|
||||
|
||||
statement.executeQuery().use { rs ->
|
||||
return withResultSet(rs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.toBase58String
|
||||
import java.sql.Connection
|
||||
import java.sql.DatabaseMetaData
|
||||
import java.sql.ResultSet
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* SQL Server / SQL Azure
|
||||
*/
|
||||
class CashSelectionSQLServerImpl : AbstractCashSelection(maxRetries = 16, retrySleep = 1000, retryCap = 5000) {
|
||||
|
||||
companion object {
|
||||
val JDBC_DRIVER_NAME = "Microsoft JDBC Driver 6.2 for SQL Server"
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
override fun isCompatible(metadata: DatabaseMetaData): Boolean {
|
||||
return metadata.driverName == JDBC_DRIVER_NAME
|
||||
}
|
||||
|
||||
override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME"
|
||||
|
||||
override fun executeQuery(connection: Connection, amount: Amount<Currency>, lockId: UUID, notary: Party?,
|
||||
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, withResultSet: (ResultSet) -> Boolean): Boolean {
|
||||
|
||||
val selectJoin = """
|
||||
WITH row(transaction_id, output_index, pennies, total, lock_id) AS
|
||||
(
|
||||
SELECT vs.transaction_id, vs.output_index, ccs.pennies,
|
||||
SUM(ccs.pennies) OVER (ORDER BY ccs.transaction_id RANGE UNBOUNDED PRECEDING), vs.lock_id
|
||||
FROM contract_pt_cash_states AS ccs, vault_states AS vs
|
||||
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
|
||||
AND vs.state_status = 0
|
||||
AND ccs.ccy_code = ?
|
||||
AND (vs.lock_id = ? OR vs.lock_id is null)""" +
|
||||
(if (notary != null)
|
||||
" AND vs.notary_name = ?" else "") +
|
||||
(if (onlyFromIssuerParties.isNotEmpty())
|
||||
" AND ccs.issuer_key IN (?)" else "") +
|
||||
(if (withIssuerRefs.isNotEmpty())
|
||||
" AND ccs.issuer_ref IN (?)" else "") +
|
||||
""")
|
||||
SELECT row.transaction_id, row.output_index, row.pennies, row.total, row.lock_id
|
||||
FROM row where row.total <= ? + row.pennies"""
|
||||
|
||||
// Use prepared statement for protection against SQL Injection
|
||||
connection.prepareStatement(selectJoin).use { statement ->
|
||||
var pIndex = 0
|
||||
statement.setString(++pIndex, amount.token.currencyCode)
|
||||
statement.setString(++pIndex, lockId.toString())
|
||||
if (notary != null)
|
||||
statement.setString(++pIndex, notary.name.toString())
|
||||
if (onlyFromIssuerParties.isNotEmpty())
|
||||
statement.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any }.toTypedArray())
|
||||
if (withIssuerRefs.isNotEmpty())
|
||||
statement.setObject(++pIndex, withIssuerRefs.map { it.bytes as Any }.toTypedArray())
|
||||
statement.setLong(++pIndex, amount.quantity)
|
||||
log.debug(selectJoin)
|
||||
|
||||
statement.executeQuery().use { rs ->
|
||||
return withResultSet(rs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,10 +5,10 @@ import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Index
|
||||
import javax.persistence.Table
|
||||
import net.corda.core.utilities.MAX_HASH_HEX_SIZE
|
||||
import net.corda.core.contracts.MAX_ISSUER_REF_SIZE
|
||||
import org.hibernate.annotations.Type
|
||||
import javax.persistence.*
|
||||
|
||||
/**
|
||||
* An object used to fully qualify the [CashSchema] family name (i.e. independent of version).
|
||||
@ -36,10 +36,11 @@ object CashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version
|
||||
@Column(name = "ccy_code", length = 3)
|
||||
var currency: String,
|
||||
|
||||
@Column(name = "issuer_key")
|
||||
var issuerParty: String,
|
||||
@Column(name = "issuer_key_hash", length = MAX_HASH_HEX_SIZE)
|
||||
var issuerPartyHash: String,
|
||||
|
||||
@Column(name = "issuer_ref")
|
||||
@Column(name = "issuer_ref", length = MAX_ISSUER_REF_SIZE)
|
||||
@Type(type = "corda-wrapper-binary")
|
||||
var issuerRef: ByteArray
|
||||
) : PersistentState()
|
||||
}
|
||||
|
@ -1,2 +1,4 @@
|
||||
com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.CashSelectionH2Impl
|
||||
|
||||
com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.CashSelectionMySQLImpl
|
||||
com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.CashSelectionPostgreSQLImpl
|
||||
com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.CashSelectionSQLServerImpl
|
||||
|
@ -226,7 +226,7 @@ class CommercialPaperTestsGeneric {
|
||||
aliceVaultService = aliceServices.vaultService
|
||||
|
||||
databaseAlice.transaction {
|
||||
alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, issuerServices, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = DUMMY_CASH_ISSUER)
|
||||
alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, issuerServices, issuedBy = DUMMY_CASH_ISSUER)
|
||||
aliceVaultService = aliceServices.vaultService
|
||||
}
|
||||
|
||||
@ -239,7 +239,7 @@ class CommercialPaperTestsGeneric {
|
||||
bigCorpVaultService = bigCorpServices.vaultService
|
||||
|
||||
databaseBigCorp.transaction {
|
||||
bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, issuerServices, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = DUMMY_CASH_ISSUER)
|
||||
bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, issuerServices, issuedBy = DUMMY_CASH_ISSUER)
|
||||
bigCorpVaultService = bigCorpServices.vaultService
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
package com.r3.corda.enterprise.perftestcordapp.contracts.asset
|
||||
|
||||
|
||||
import com.nhaarman.mockito_kotlin.argThat
|
||||
import com.nhaarman.mockito_kotlin.doNothing
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import com.r3.corda.enterprise.perftestcordapp.*
|
||||
import com.r3.corda.enterprise.perftestcordapp.utils.sumCash
|
||||
@ -18,7 +20,6 @@ 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
|
||||
@ -27,15 +28,14 @@ import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.contracts.VaultFiller.Companion.calculateRandomlySizedAmounts
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
|
||||
import net.corda.testing.node.makeTestIdentityService
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito.doReturn
|
||||
import java.security.KeyPair
|
||||
import java.util.*
|
||||
import kotlin.test.*
|
||||
|
||||
@ -53,37 +53,29 @@ import kotlin.test.*
|
||||
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<Cash.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 = Cash()
|
||||
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)
|
||||
val issuance = TransactionBuilder(null as Party?)
|
||||
cash.generateIssue(issuance, Amount(howMuch.quantity, Issued(issuedBy.copy(reference = ref), howMuch.token)), anonParty, outputNotary)
|
||||
|
||||
return@map issuerServices.signInitialTransaction(issuance, issuedBy.party.owningKey)
|
||||
}
|
||||
|
||||
recordTransactions(transactions)
|
||||
val transaction = issuerServices.signInitialTransaction(issuance, issuedBy.party.owningKey)
|
||||
recordTransactions(listOf(transaction))
|
||||
|
||||
// Get all the StateRefs of all the generated transactions.
|
||||
val states = transactions.flatMap { stx ->
|
||||
stx.tx.outputs.indices.map { i -> stx.tx.outRef<Cash.State>(i) }
|
||||
}
|
||||
|
||||
val states = transaction.tx.outputs.indices.map { i -> transaction.tx.outRef<Cash.State>(i) }
|
||||
return Vault(states)
|
||||
}
|
||||
|
||||
class CashTests {
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
private val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
|
||||
private val defaultIssuer = MEGA_CORP.ref(defaultRef)
|
||||
private val inState = Cash.State(
|
||||
@ -98,38 +90,59 @@ class CashTests {
|
||||
amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref))))
|
||||
)
|
||||
|
||||
private lateinit var ourServices: MockServices
|
||||
private lateinit var miniCorpServices: MockServices
|
||||
private lateinit var megaCorpServices: MockServices
|
||||
val vault: VaultService get() = miniCorpServices.vaultService
|
||||
lateinit var database: CordaPersistence
|
||||
private lateinit var vaultStatesUnconsumed: List<StateAndRef<Cash.State>>
|
||||
|
||||
private lateinit var ourIdentity: AbstractParty
|
||||
private lateinit var miniCorpAnonymised: AnonymousParty
|
||||
private val CHARLIE_ANONYMISED = CHARLIE_IDENTITY.party.anonymise()
|
||||
|
||||
private lateinit var WALLET: List<StateAndRef<Cash.State>>
|
||||
|
||||
@Before
|
||||
fun setUp() = withTestSerialization {
|
||||
fun setUp() {
|
||||
LogHelper.setLevel(NodeVaultService::class)
|
||||
megaCorpServices = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset","com.r3.corda.enterprise.perftestcordapp.schemas"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY)
|
||||
megaCorpServices = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY)
|
||||
miniCorpServices = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), rigorousMock<IdentityServiceInternal>().also {
|
||||
doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == MINI_CORP.name })
|
||||
}, MINI_CORP.name, MINI_CORP_KEY)
|
||||
val notaryServices = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
val databaseAndServices = makeTestDatabaseAndMockServices(
|
||||
listOf(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY),
|
||||
makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)),
|
||||
listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset", "com.r3.corda.enterprise.perftestcordapp.schemas"),
|
||||
CordaX500Name("Me", "London", "GB"))
|
||||
cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"),
|
||||
initialIdentityName = CordaX500Name(organisation = "Me", locality = "London", country = "GB"),
|
||||
keys = listOf(generateKeyPair()),
|
||||
identityService = makeTestIdentityService(listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY)))
|
||||
database = databaseAndServices.first
|
||||
miniCorpServices = databaseAndServices.second
|
||||
ourServices = databaseAndServices.second
|
||||
|
||||
// Set up and register identities
|
||||
ourIdentity = ourServices.myInfo.singleIdentity()
|
||||
miniCorpAnonymised = miniCorpServices.myInfo.singleIdentityAndCert().party.anonymise()
|
||||
(miniCorpServices.myInfo.legalIdentitiesAndCerts + megaCorpServices.myInfo.legalIdentitiesAndCerts + notaryServices.myInfo.legalIdentitiesAndCerts).forEach { identity ->
|
||||
ourServices.identityService.verifyAndRegisterIdentity(identity)
|
||||
}
|
||||
|
||||
// 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)
|
||||
ourServices.fillWithSomeTestCash(issuerServices = megaCorpServices, ownedBy = ourIdentity, issuedBy = MEGA_CORP.ref(1), howMuch = 100.DOLLARS)
|
||||
ourServices.fillWithSomeTestCash(issuerServices = megaCorpServices, ownedBy = ourIdentity, issuedBy = MEGA_CORP.ref(1), howMuch = 400.DOLLARS)
|
||||
ourServices.fillWithSomeTestCash(issuerServices = miniCorpServices, ownedBy = ourIdentity, issuedBy = MINI_CORP.ref(1), howMuch = 80.DOLLARS)
|
||||
ourServices.fillWithSomeTestCash(issuerServices = miniCorpServices, ownedBy = ourIdentity, issuedBy = MINI_CORP.ref(1), howMuch = 80.SWISS_FRANCS)
|
||||
}
|
||||
|
||||
database.transaction {
|
||||
vaultStatesUnconsumed = miniCorpServices.vaultService.queryBy<Cash.State>().states
|
||||
vaultStatesUnconsumed = ourServices.vaultService.queryBy<Cash.State>().states
|
||||
}
|
||||
WALLET = listOf(
|
||||
makeCash(100.DOLLARS, MEGA_CORP),
|
||||
makeCash(400.DOLLARS, MEGA_CORP),
|
||||
makeCash(80.DOLLARS, MINI_CORP),
|
||||
makeCash(80.SWISS_FRANCS, MINI_CORP, 2)
|
||||
)
|
||||
}
|
||||
|
||||
@After
|
||||
@ -138,11 +151,10 @@ class CashTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun trivial() = withTestSerialization {
|
||||
fun trivial() {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
|
||||
tweak {
|
||||
output(Cash.PROGRAM_ID, outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
@ -172,25 +184,22 @@ class CashTests {
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue by move`() = withTestSerialization {
|
||||
fun `issue by move`() {
|
||||
// Check we can't "move" money into existence.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID, DummyState())
|
||||
output(Cash.PROGRAM_ID, outState)
|
||||
command(MINI_CORP_PUBKEY, Cash.Commands.Move())
|
||||
|
||||
this `fails with` "there is at least one cash input for this group"
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun issue() = withTestSerialization {
|
||||
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 {
|
||||
@ -204,17 +213,14 @@ class CashTests {
|
||||
output(Cash.PROGRAM_ID,
|
||||
Cash.State(
|
||||
amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34),
|
||||
owner = AnonymousParty(ALICE_PUBKEY)
|
||||
)
|
||||
)
|
||||
owner = AnonymousParty(ALICE_PUBKEY)))
|
||||
command(MINI_CORP_PUBKEY, Cash.Commands.Issue())
|
||||
this.verifies()
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateIssueRaw() = withTestSerialization {
|
||||
fun generateIssueRaw() {
|
||||
// Test generation works.
|
||||
val tx: WireTransaction = TransactionBuilder(notary = null).apply {
|
||||
Cash().generateIssue(this, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = AnonymousParty(ALICE_PUBKEY), notary = DUMMY_NOTARY)
|
||||
@ -229,7 +235,7 @@ class CashTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateIssueFromAmount() = withTestSerialization {
|
||||
fun generateIssueFromAmount() {
|
||||
// Test issuance from an issued amount
|
||||
val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34)
|
||||
val tx: WireTransaction = TransactionBuilder(notary = null).apply {
|
||||
@ -240,13 +246,12 @@ class CashTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `extended issue examples`() = withTestSerialization {
|
||||
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(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID, issuerInState)
|
||||
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2))
|
||||
|
||||
// Move fails: not allowed to summon money.
|
||||
tweak {
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
@ -290,7 +295,6 @@ class CashTests {
|
||||
}
|
||||
this.verifies()
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
/**
|
||||
@ -298,7 +302,7 @@ class CashTests {
|
||||
* cash inputs.
|
||||
*/
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun `reject issuance with inputs`() = withTestSerialization {
|
||||
fun `reject issuance with inputs`() {
|
||||
// Issue some cash
|
||||
var ptx = TransactionBuilder(DUMMY_NOTARY)
|
||||
|
||||
@ -309,11 +313,10 @@ class CashTests {
|
||||
ptx = TransactionBuilder(DUMMY_NOTARY)
|
||||
ptx.addInputState(tx.tx.outRef<Cash.State>(0))
|
||||
Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY)
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMergeSplit() = withTestSerialization {
|
||||
fun testMergeSplit() {
|
||||
// Splitting value works.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
@ -340,11 +343,10 @@ class CashTests {
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun zeroSizedValues() = withTestSerialization {
|
||||
fun zeroSizedValues() {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
@ -360,11 +362,10 @@ class CashTests {
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "zero sized outputs"
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun trivialMismatches() = withTestSerialization {
|
||||
fun trivialMismatches() {
|
||||
// Can't change issuer.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
@ -387,7 +388,7 @@ class CashTests {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
output(Cash.PROGRAM_ID, outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer))
|
||||
output(Cash.PROGRAM_ID, outState.copy(amount = 200.POUNDS `issued by` defaultIssuer))
|
||||
output(Cash.PROGRAM_ID, outState.copy(amount = 200.POUNDS `issued by` defaultIssuer))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
@ -397,9 +398,7 @@ class CashTests {
|
||||
input(Cash.PROGRAM_ID,
|
||||
inState.copy(
|
||||
amount = 150.POUNDS `issued by` defaultIssuer,
|
||||
owner = AnonymousParty(BOB_PUBKEY)
|
||||
)
|
||||
)
|
||||
owner = AnonymousParty(BOB_PUBKEY)))
|
||||
output(Cash.PROGRAM_ID, outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer))
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
@ -422,17 +421,15 @@ class CashTests {
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "for reference [01]"
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exitLedger() = withTestSerialization {
|
||||
fun exitLedger() {
|
||||
// Single input/output straightforward case.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID, issuerInState)
|
||||
output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)))
|
||||
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer))
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
|
||||
@ -449,35 +446,28 @@ class CashTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exit ledger with multiple issuers`() = withTestSerialization {
|
||||
fun `exit ledger with multiple issuers`() {
|
||||
// Multi-issuer case.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
input(Cash.PROGRAM_ID, issuerInState)
|
||||
input(Cash.PROGRAM_ID, issuerInState.copy(owner = MINI_CORP) issuedBy MINI_CORP)
|
||||
|
||||
output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) issuedBy MINI_CORP)
|
||||
output(Cash.PROGRAM_ID, issuerInState.copy(owner = MINI_CORP, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)))
|
||||
|
||||
command(listOf(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), Cash.Commands.Move())
|
||||
|
||||
this `fails with` "the amounts balance"
|
||||
|
||||
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
|
||||
this `fails with` "the amounts balance"
|
||||
|
||||
command(MINI_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)))
|
||||
this.verifies()
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exit cash not held by its issuer`() = withTestSerialization {
|
||||
fun `exit cash not held by its issuer`() {
|
||||
// Single input/output straightforward case.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
@ -487,18 +477,16 @@ class CashTests {
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multiIssuer() = withTestSerialization {
|
||||
fun multiIssuer() {
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
// Gather 2000 dollars from two different issuers.
|
||||
input(Cash.PROGRAM_ID, inState)
|
||||
input(Cash.PROGRAM_ID, inState issuedBy MINI_CORP)
|
||||
command(ALICE_PUBKEY, Cash.Commands.Move())
|
||||
|
||||
// Can't merge them together.
|
||||
tweak {
|
||||
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY), amount = 2000.DOLLARS `issued by` defaultIssuer))
|
||||
@ -516,11 +504,10 @@ class CashTests {
|
||||
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)) issuedBy MINI_CORP)
|
||||
this.verifies()
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multiCurrency() = withTestSerialization {
|
||||
fun multiCurrency() {
|
||||
// Check we can do an atomic currency trade tx.
|
||||
transaction {
|
||||
attachment(Cash.PROGRAM_ID)
|
||||
@ -530,36 +517,20 @@ class CashTests {
|
||||
output(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(BOB_PUBKEY))
|
||||
output(Cash.PROGRAM_ID, pounds ownedBy AnonymousParty(ALICE_PUBKEY))
|
||||
command(listOf(ALICE_PUBKEY, BOB_PUBKEY), Cash.Commands.Move())
|
||||
|
||||
this.verifies()
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Spend tx generation
|
||||
|
||||
private val OUR_KEY: KeyPair by lazy { generateKeyPair() }
|
||||
private val OUR_IDENTITY_1: AbstractParty get() = AnonymousParty(OUR_KEY.public)
|
||||
private val OUR_IDENTITY_AND_CERT = getTestPartyAndCertificate(CordaX500Name(organisation = "Me", locality = "London", country = "GB"), OUR_KEY.public)
|
||||
|
||||
private val THEIR_IDENTITY_1 = AnonymousParty(MINI_CORP_PUBKEY)
|
||||
private val THEIR_IDENTITY_2 = AnonymousParty(CHARLIE_PUBKEY)
|
||||
|
||||
private fun makeCash(amount: Amount<Currency>, issuer: AbstractParty, depositRef: Byte = 1) =
|
||||
StateAndRef(
|
||||
TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), OUR_IDENTITY_1), Cash.PROGRAM_ID, DUMMY_NOTARY),
|
||||
TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, DUMMY_NOTARY),
|
||||
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
|
||||
)
|
||||
|
||||
private 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.
|
||||
*/
|
||||
@ -570,19 +541,21 @@ class CashTests {
|
||||
return tx.toWireTransaction(serviceHub)
|
||||
}
|
||||
|
||||
private fun makeSpend(amount: Amount<Currency>, dest: AbstractParty): WireTransaction {
|
||||
private fun makeSpend(services: ServiceHub, amount: Amount<Currency>, dest: AbstractParty): WireTransaction {
|
||||
val ourIdentity = services.myInfo.singleIdentityAndCert()
|
||||
val changeIdentity = services.keyManagementService.freshKeyAndCert(ourIdentity, false)
|
||||
val tx = TransactionBuilder(DUMMY_NOTARY)
|
||||
database.transaction {
|
||||
Cash.generateSpend(miniCorpServices, tx, amount, OUR_IDENTITY_AND_CERT, dest)
|
||||
Cash.generateSpend(services, tx, amount, changeIdentity, dest)
|
||||
}
|
||||
return tx.toWireTransaction(miniCorpServices)
|
||||
return tx.toWireTransaction(services)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try exiting an amount which matches a single state.
|
||||
*/
|
||||
@Test
|
||||
fun generateSimpleExit() = withTestSerialization {
|
||||
fun generateSimpleExit() {
|
||||
val wtx = makeExit(miniCorpServices, 100.DOLLARS, MEGA_CORP, 1)
|
||||
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
||||
assertEquals(0, wtx.outputs.size)
|
||||
@ -597,7 +570,7 @@ class CashTests {
|
||||
* Try exiting an amount smaller than the smallest available input state, and confirm change is generated correctly.
|
||||
*/
|
||||
@Test
|
||||
fun generatePartialExit() = withTestSerialization {
|
||||
fun generatePartialExit() {
|
||||
val wtx = makeExit(miniCorpServices, 50.DOLLARS, MEGA_CORP, 1)
|
||||
val actualInput = wtx.inputs.single()
|
||||
// Filter the available inputs and confirm exactly one has been used
|
||||
@ -614,80 +587,76 @@ class CashTests {
|
||||
* Try exiting a currency we don't have.
|
||||
*/
|
||||
@Test
|
||||
fun generateAbsentExit() = withTestSerialization {
|
||||
fun generateAbsentExit() {
|
||||
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 1) }
|
||||
Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Try exiting with a reference mis-match.
|
||||
*/
|
||||
@Test
|
||||
fun generateInvalidReferenceExit() = withTestSerialization {
|
||||
fun generateInvalidReferenceExit() {
|
||||
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 2) }
|
||||
Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Try exiting an amount greater than the maximum available.
|
||||
*/
|
||||
@Test
|
||||
fun generateInsufficientExit() = withTestSerialization {
|
||||
fun generateInsufficientExit() {
|
||||
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 1000.DOLLARS, MEGA_CORP, 1) }
|
||||
Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Try exiting for an owner with no states
|
||||
*/
|
||||
@Test
|
||||
fun generateOwnerWithNoStatesExit() = withTestSerialization {
|
||||
fun generateOwnerWithNoStatesExit() {
|
||||
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, CHARLIE, 1) }
|
||||
Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Try exiting when vault is empty
|
||||
*/
|
||||
@Test
|
||||
fun generateExitWithEmptyVault() = withTestSerialization {
|
||||
fun generateExitWithEmptyVault() {
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
val tx = TransactionBuilder(DUMMY_NOTARY)
|
||||
Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList(), OUR_IDENTITY_1)
|
||||
Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList(), ourIdentity)
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSimpleDirectSpend() = withTestSerialization {
|
||||
fun generateSimpleDirectSpend() {
|
||||
val wtx =
|
||||
database.transaction {
|
||||
makeSpend(100.DOLLARS, THEIR_IDENTITY_1)
|
||||
makeSpend(ourServices, 100.DOLLARS, miniCorpAnonymised)
|
||||
}
|
||||
database.transaction {
|
||||
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 Cash.Commands.Move }.signers[0])
|
||||
assertEquals(vaultState.state.data.copy(owner = miniCorpAnonymised), wtx.getOutput(0))
|
||||
assertEquals(ourIdentity.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSimpleSpendWithParties() = withTestSerialization {
|
||||
fun generateSimpleSpendWithParties() {
|
||||
val changeIdentity = ourServices.keyManagementService.freshKeyAndCert(ourServices.myInfo.singleIdentityAndCert(), false)
|
||||
database.transaction {
|
||||
|
||||
val tx = TransactionBuilder(DUMMY_NOTARY)
|
||||
Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, OUR_IDENTITY_AND_CERT, ALICE, setOf(MINI_CORP))
|
||||
Cash.generateSpend(ourServices, tx, 80.DOLLARS, changeIdentity, ALICE, setOf(MINI_CORP))
|
||||
|
||||
assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSimpleSpendWithChange() = withTestSerialization {
|
||||
fun generateSimpleSpendWithChange() {
|
||||
val wtx =
|
||||
database.transaction {
|
||||
makeSpend(10.DOLLARS, THEIR_IDENTITY_1)
|
||||
makeSpend(ourServices, 10.DOLLARS, miniCorpAnonymised)
|
||||
}
|
||||
database.transaction {
|
||||
val vaultState = vaultStatesUnconsumed.elementAt(0)
|
||||
@ -700,35 +669,35 @@ class CashTests {
|
||||
}
|
||||
}
|
||||
val changeOwner = (likelyChangeState as Cash.State).owner
|
||||
assertEquals(1, miniCorpServices.keyManagementService.filterMyKeys(setOf(changeOwner.owningKey)).toList().size)
|
||||
assertEquals(1, ourServices.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(owner = miniCorpAnonymised, 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 Cash.Commands.Move }.signers[0])
|
||||
assertEquals(ourIdentity.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSpendWithTwoInputs() = withTestSerialization {
|
||||
fun generateSpendWithTwoInputs() {
|
||||
val wtx =
|
||||
database.transaction {
|
||||
makeSpend(500.DOLLARS, THEIR_IDENTITY_1)
|
||||
makeSpend(ourServices, 500.DOLLARS, miniCorpAnonymised)
|
||||
}
|
||||
database.transaction {
|
||||
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 Cash.Commands.Move }.signers[0])
|
||||
assertEquals(vaultState0.state.data.copy(owner = miniCorpAnonymised, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.getOutput(0))
|
||||
assertEquals(ourIdentity.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSpendMixedDeposits() = withTestSerialization {
|
||||
fun generateSpendMixedDeposits() {
|
||||
val wtx =
|
||||
database.transaction {
|
||||
val wtx = makeSpend(580.DOLLARS, THEIR_IDENTITY_1)
|
||||
val wtx = makeSpend(ourServices, 580.DOLLARS, miniCorpAnonymised)
|
||||
assertEquals(3, wtx.inputs.size)
|
||||
wtx
|
||||
}
|
||||
@ -739,26 +708,25 @@ class CashTests {
|
||||
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 Cash.Commands.Move }.signers[0])
|
||||
assertEquals(vaultState0.state.data.copy(owner = miniCorpAnonymised, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data)
|
||||
assertEquals(vaultState2.state.data.copy(owner = miniCorpAnonymised), wtx.outputs[0].data)
|
||||
assertEquals(ourIdentity.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSpendInsufficientBalance() = withTestSerialization {
|
||||
fun generateSpendInsufficientBalance() {
|
||||
database.transaction {
|
||||
|
||||
val e: InsufficientBalanceException = assertFailsWith("balance") {
|
||||
makeSpend(1000.DOLLARS, THEIR_IDENTITY_1)
|
||||
makeSpend(ourServices, 1000.DOLLARS, miniCorpAnonymised)
|
||||
}
|
||||
assertEquals((1000 - 580).DOLLARS, e.amountMissing)
|
||||
|
||||
assertFailsWith(InsufficientBalanceException::class) {
|
||||
makeSpend(81.SWISS_FRANCS, THEIR_IDENTITY_1)
|
||||
makeSpend(ourServices, 81.SWISS_FRANCS, miniCorpAnonymised)
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
/**
|
||||
@ -846,7 +814,7 @@ class CashTests {
|
||||
|
||||
// Double spend.
|
||||
@Test
|
||||
fun chainCashDoubleSpendFailsWith() = withTestSerialization {
|
||||
fun chainCashDoubleSpendFailsWith() {
|
||||
val mockService = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), rigorousMock<IdentityServiceInternal>().also {
|
||||
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
|
||||
}, MEGA_CORP.name, MEGA_CORP_KEY)
|
||||
@ -856,9 +824,7 @@ class CashTests {
|
||||
output(Cash.PROGRAM_ID, "MEGA_CORP cash",
|
||||
Cash.State(
|
||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||
owner = MEGA_CORP
|
||||
)
|
||||
)
|
||||
owner = MEGA_CORP))
|
||||
}
|
||||
|
||||
transaction {
|
||||
@ -883,20 +849,20 @@ class CashTests {
|
||||
|
||||
this.verifies()
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multiSpend() = withTestSerialization {
|
||||
fun multiSpend() {
|
||||
val tx = TransactionBuilder(DUMMY_NOTARY)
|
||||
database.transaction {
|
||||
val changeIdentity = ourServices.keyManagementService.freshKeyAndCert(ourServices.myInfo.singleIdentityAndCert(), false)
|
||||
val payments = listOf(
|
||||
PartyAndAmount(THEIR_IDENTITY_1, 400.DOLLARS),
|
||||
PartyAndAmount(THEIR_IDENTITY_2, 150.DOLLARS)
|
||||
PartyAndAmount(miniCorpAnonymised, 400.DOLLARS),
|
||||
PartyAndAmount(CHARLIE_ANONYMISED, 150.DOLLARS)
|
||||
)
|
||||
Cash.generateSpend(miniCorpServices, tx, payments)
|
||||
Cash.generateSpend(ourServices, tx, payments, changeIdentity)
|
||||
}
|
||||
val wtx = tx.toWireTransaction(miniCorpServices)
|
||||
val wtx = tx.toWireTransaction(ourServices)
|
||||
fun out(i: Int) = wtx.getOutput(i) as Cash.State
|
||||
assertEquals(4, wtx.outputs.size)
|
||||
assertEquals(80.DOLLARS, out(0).amount.withoutIssuer())
|
||||
|
@ -174,8 +174,7 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) {
|
||||
bobNode.internals.disableDBCloseOnStop()
|
||||
|
||||
val cashStates = bobNode.database.transaction {
|
||||
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, notary, 3, 3,
|
||||
issuedBy = issuer)
|
||||
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, notary, issuedBy = issuer)
|
||||
}
|
||||
|
||||
val alicesFakePaper = aliceNode.database.transaction {
|
||||
|
Loading…
Reference in New Issue
Block a user