From 6c6ed3a75836af44a1bf7bb88e1e8807cc5db002 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 24 Feb 2017 16:15:03 +0000 Subject: [PATCH] Move transaction generation to OnLedgerAsset Move functions for generating transactions into OnLedgerAsset from various locations in the code (VaultService, AbstractConserveAmount, etc.) to unify the code paths and reduce duplication. --- .../net/corda/core/contracts/FungibleAsset.kt | 5 + docs/source/changelog.rst | 3 + .../kotlin/net/corda/contracts/asset/Cash.kt | 11 +- .../contracts/asset/CommodityContract.kt | 11 +- .../net/corda/contracts/asset/Obligation.kt | 10 +- .../corda/contracts/asset/OnLedgerAsset.kt | 194 +++++++++++++++++- .../clause/AbstractConserveAmount.kt | 66 +----- .../node/services/vault/NodeVaultService.kt | 107 +--------- 8 files changed, 217 insertions(+), 190 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt index 99aafbbd1d..1e65bea48c 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt @@ -1,7 +1,12 @@ package net.corda.core.contracts +import net.corda.core.crypto.Party import net.corda.core.flows.FlowException +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.trace import java.security.PublicKey +import java.util.* class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException("Insufficient balance, missing $amountMissing") diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index c259a9bb45..87f169c6f1 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -14,6 +14,9 @@ UNRELEASED * Starting a flow no longer enables progress tracking by default. To enable it, you must now invoke your flow using one of the new ``CordaRPCOps.startTrackedFlow`` functions. ``FlowHandle`` is now an interface, and its ``progress: Observable`` field has been moved to the ``FlowProgressHandle`` child interface. Hence developers no longer need to invoke ``notUsed`` on their flows' unwanted progress-tracking observables. + * Moved ``generateSpend`` and ``generateExit`` functions into ``OnLedgerAsset`` from the vault and + ``AbstractConserveAmount`` clauses respectively. + * DemoBench is now installed as ``Corda DemoBench`` instead of ``DemoBench``. Milestone 10.0 diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt index 4b160d1681..d893abfe6f 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt @@ -53,7 +53,6 @@ class Cash : OnLedgerAsset() { * that is inconsistent with the legal contract. */ override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") - override val conserveClause: AbstractConserveAmount = Clauses.ConserveAmount() override fun extractCommands(commands: Collection>): List> = commands.select() @@ -152,14 +151,8 @@ class Cash : OnLedgerAsset() { /** * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. */ - fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: PublicKey, notary: Party) { - check(tx.inputStates().isEmpty()) - check(tx.outputStates().map { it.data }.sumCashOrNull() == null) - require(amount.quantity > 0) - val at = amount.token.issuer - tx.addOutputState(TransactionState(State(amount, owner), notary)) - tx.addCommand(generateIssueCommand(), at.party.owningKey) - } + fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: PublicKey, notary: Party) + = generateIssue(tx, TransactionState(State(amount, owner), notary), generateIssueCommand()) override fun deriveState(txState: TransactionState, amount: Amount>, owner: PublicKey) = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt b/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt index e8d31c23f8..ca577de309 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt @@ -47,8 +47,6 @@ class CommodityContract : OnLedgerAsset = Clauses.ConserveAmount() - /** * The clauses for this contract are essentially: * @@ -153,13 +151,8 @@ class CommodityContract : OnLedgerAsset>, owner: PublicKey, notary: Party) { - check(tx.inputStates().isEmpty()) - check(tx.outputStates().map { it.data }.sumCashOrNull() == null) - val at = amount.token.issuer - tx.addOutputState(TransactionState(State(amount, owner), notary)) - tx.addCommand(generateIssueCommand(), at.party.owningKey) - } + fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: PublicKey, notary: Party) + = generateIssue(tx, TransactionState(State(amount, owner), notary), generateIssueCommand()) override fun deriveState(txState: TransactionState, amount: Amount>, owner: PublicKey) diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt index 795ecf8d49..15a04fb1b1 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt @@ -458,7 +458,7 @@ class Obligation

: Contract { @Suppress("unused") fun generateExit(tx: TransactionBuilder, amountIssued: Amount>>, assetStates: List>>): PublicKey - = Clauses.ConserveAmount

().generateExit(tx, amountIssued, assetStates, + = OnLedgerAsset.generateExit(tx, amountIssued, assetStates, deriveState = { state, amount, owner -> state.copy(data = state.data.move(amount, owner)) }, generateMoveCommand = { -> Commands.Move() }, generateExitCommand = { amount -> Commands.Exit(amount) } @@ -472,12 +472,8 @@ class Obligation

: Contract { issuanceDef: Terms

, pennies: Long, beneficiary: PublicKey, - notary: Party) { - check(tx.inputStates().isEmpty()) - check(tx.outputStates().map { it.data }.sumObligationsOrNull

() == null) - tx.addOutputState(State(Lifecycle.NORMAL, obligor.toAnonymous(), issuanceDef, pennies, beneficiary), notary) - tx.addCommand(Commands.Issue(), obligor.owningKey) - } + notary: Party) + = OnLedgerAsset.generateIssue(tx, TransactionState(State(Lifecycle.NORMAL, obligor.toAnonymous(), issuanceDef, pennies, beneficiary), notary), Commands.Issue()) fun generatePaymentNetting(tx: TransactionBuilder, issued: Issued>, diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt b/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt index 8550ad17bd..47f286dece 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt @@ -3,7 +3,10 @@ package net.corda.contracts.asset import net.corda.contracts.clause.AbstractConserveAmount import net.corda.core.contracts.* import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.trace import java.security.PublicKey +import java.util.* ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // @@ -24,8 +27,195 @@ import java.security.PublicKey * the issuer/depositRefs and just examine the amount fields. */ abstract class OnLedgerAsset> : Contract { + companion object { + val log = loggerFor>() + + /** + * Generate a transaction that moves an amount of currency to the given pubkey. + * + * Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset] + * + * @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed + * to move the cash will be added on top. + * @param amount How much currency to send. + * @param to a key of the recipient. + * @param acceptableStates a list of acceptable input states to use. + * @param deriveState a function to derive an output state based on an input state, amount for the output + * and public key to pay to. + * @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign + * the resulting transaction for it to be valid. + * @throws InsufficientBalanceException when a cash spending transaction fails because + * there is insufficient quantity for a given currency (and optionally set of Issuer Parties). + */ + @Throws(InsufficientBalanceException::class) + @JvmStatic + fun , T: Any> generateSpend(tx: TransactionBuilder, + amount: Amount, + to: PublicKey, + acceptableStates: List>, + deriveState: (TransactionState, Amount>, PublicKey) -> TransactionState, + generateMoveCommand: () -> CommandData): Pair> { + // Discussion + // + // This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline. + // + // First we must select a set of asset states (which for convenience we will call 'coins' here, as in bitcoinj). + // The input states can be considered our "vault", and may consist of different products, and with different + // issuers and deposits. + // + // Coin selection is a complex problem all by itself and many different approaches can be used. It is easily + // possible for different actors to use different algorithms and approaches that, for example, compete on + // privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of + // obfuscation and so on. + // + // Having selected input states of the correct asset, we must craft output states for the amount we're sending and + // the "change", which goes back to us. The change is required to make the amounts balance. We may need more + // than one change output in order to avoid merging assets from different deposits. The point of this design + // is to ensure that ledger entries are immutable and globally identifiable. + // + // Finally, we add the states to the provided partial transaction. + + // TODO: We should be prepared to produce multiple transactions spending inputs from + // different notaries, or at least group states by notary and take the set with the + // highest total value. + + // notary may be associated with locked state only + tx.notary = acceptableStates.firstOrNull()?.state?.notary + + val (gathered, gatheredAmount) = gatherCoins(acceptableStates, amount) + + val takeChangeFrom = gathered.firstOrNull() + val change = if (takeChangeFrom != null && gatheredAmount > amount) { + Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.amount.token) + } else { + null + } + val keysUsed = gathered.map { it.state.data.owner } + + val states = gathered.groupBy { it.state.data.amount.token.issuer }.map { + val coins = it.value + val totalAmount = coins.map { it.state.data.amount }.sumOrThrow() + deriveState(coins.first().state, totalAmount, to) + }.sortedBy { it.data.amount.quantity } + + val outputs = if (change != null) { + // Just copy a key across as the change key. In real life of course, this works but leaks private data. + // In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow + // value flows through the transaction graph. + val existingOwner = gathered.first().state.data.owner + // Add a change output and adjust the last output downwards. + states.subList(0, states.lastIndex) + + states.last().let { + val spent = it.data.amount.withoutIssuer() - change.withoutIssuer() + deriveState(it, Amount(spent.quantity, it.data.amount.token), it.data.owner) + } + + states.last().let { + deriveState(it, Amount(change.quantity, it.data.amount.token), existingOwner) + } + } else states + + for (state in gathered) tx.addInputState(state) + for (state in outputs) tx.addOutputState(state) + + // What if we already have a move command with the right keys? Filter it out here or in platform code? + tx.addCommand(generateMoveCommand(), keysUsed) + + return Pair(tx, keysUsed) + } + + /** + * Gather assets from the given list of states, sufficient to match or exceed the given amount. + * + * @param acceptableCoins list of states to use as inputs. + * @param amount the amount to gather states up to. + * @throws InsufficientBalanceException if there isn't enough value in the states to cover the requested amount. + */ + @Throws(InsufficientBalanceException::class) + private fun , T : Any> gatherCoins(acceptableCoins: Collection>, + amount: Amount): Pair>, Amount> { + require(amount.quantity > 0) { "Cannot gather zero coins" } + val gathered = arrayListOf>() + var gatheredAmount = Amount(0, amount.token) + for (c in acceptableCoins) { + if (gatheredAmount >= amount) break + gathered.add(c) + gatheredAmount += Amount(c.state.data.amount.quantity, amount.token) + } + + if (gatheredAmount < amount) { + log.trace { "Insufficient balance: requested $amount, available $gatheredAmount" } + throw InsufficientBalanceException(amount - gatheredAmount) + } + + log.trace { "Gathered coins: requested $amount, available $gatheredAmount, change: ${gatheredAmount - amount}" } + + return Pair(gathered, gatheredAmount) + } + + /** + * Generate an transaction exiting fungible assets from the ledger. + * + * @param tx transaction builder to add states and commands to. + * @param amountIssued the amount to be exited, represented as a quantity of issued currency. + * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is + * the responsibility of the caller to check that they do not attempt to exit funds held by others. + * @return the public key of the assets issuer, who must sign the transaction for it to be valid. + */ + @Throws(InsufficientBalanceException::class) + @JvmStatic + fun , T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount>, + assetStates: List>, + deriveState: (TransactionState, Amount>, PublicKey) -> TransactionState, + generateMoveCommand: () -> CommandData, + generateExitCommand: (Amount>) -> CommandData): PublicKey { + val owner = assetStates.map { it.state.data.owner }.toSet().singleOrNull() ?: throw InsufficientBalanceException(amountIssued) + val currency = amountIssued.token.product + val amount = Amount(amountIssued.quantity, currency) + var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token } + tx.notary = acceptableCoins.firstOrNull()?.state?.notary + // TODO: We should be prepared to produce multiple transactions exiting inputs from + // different notaries, or at least group states by notary and take the set with the + // highest total value + acceptableCoins = acceptableCoins.filter { it.state.notary == tx.notary } + + val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount) + val takeChangeFrom = gathered.lastOrNull() + val change = if (takeChangeFrom != null && gatheredAmount > amount) { + Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.amount.token) + } else { + null + } + + val outputs = if (change != null) { + // Add a change output and adjust the last output downwards. + listOf(deriveState(gathered.last().state, change, owner)) + } else emptyList() + + for (state in gathered) tx.addInputState(state) + for (state in outputs) tx.addOutputState(state) + tx.addCommand(generateMoveCommand(), gathered.map { it.state.data.owner }) + tx.addCommand(generateExitCommand(amountIssued), gathered.flatMap { it.state.data.exitKeys }) + return amountIssued.token.issuer.party.owningKey + } + + /** + * Puts together an issuance transaction for the specified state. Normally contracts will provide convenient + * wrappers around this function, which build the state for you, and those should be used in preference. + */ + @JvmStatic + fun , T: Any> generateIssue(tx: TransactionBuilder, + transactionState: TransactionState, + issueCommand: CommandData) { + check(tx.inputStates().isEmpty()) + check(tx.outputStates().map { it.data }.filterIsInstance(transactionState.javaClass).isEmpty()) + require(transactionState.data.amount.quantity > 0) + val at = transactionState.data.amount.token.issuer + tx.addOutputState(transactionState) + tx.addCommand(issueCommand, at.party.owningKey) + } + } + abstract fun extractCommands(commands: Collection>): Collection> - abstract val conserveClause: AbstractConserveAmount /** * Generate an transaction exiting assets from the ledger. @@ -41,7 +231,7 @@ abstract class OnLedgerAsset> : C @Throws(InsufficientBalanceException::class) fun generateExit(tx: TransactionBuilder, amountIssued: Amount>, assetStates: List>): PublicKey { - return conserveClause.generateExit( + return generateExit( tx, amountIssued, assetStates, diff --git a/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt b/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt index 6308075fd4..dde9354f9d 100644 --- a/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt +++ b/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt @@ -1,12 +1,11 @@ package net.corda.contracts.clause +import net.corda.contracts.asset.OnLedgerAsset import net.corda.core.contracts.* import net.corda.core.contracts.clauses.Clause import net.corda.core.transactions.TransactionBuilder -import java.security.PublicKey import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.trace -import java.util.* +import java.security.PublicKey /** * Standardised clause for checking input/output balances of fungible assets. Requires that a @@ -19,34 +18,6 @@ abstract class AbstractConserveAmount, C : CommandData, T : val log = loggerFor>() } - /** - * Gather assets from the given list of states, sufficient to match or exceed the given amount. - * - * @param acceptableCoins list of states to use as inputs. - * @param amount the amount to gather states up to. - * @throws InsufficientBalanceException if there isn't enough value in the states to cover the requested amount. - */ - @Throws(InsufficientBalanceException::class) - private fun gatherCoins(acceptableCoins: Collection>, - amount: Amount): Pair>, Amount> { - require(amount.quantity > 0) { "Cannot gather zero coins" } - val gathered = arrayListOf>() - var gatheredAmount = Amount(0, amount.token) - for (c in acceptableCoins) { - if (gatheredAmount >= amount) break - gathered.add(c) - gatheredAmount += Amount(c.state.data.amount.quantity, amount.token) - } - - if (gatheredAmount < amount) { - log.trace { "Insufficient balance: requested $amount, available $gatheredAmount" } - throw InsufficientBalanceException(amount - gatheredAmount) - } - - log.trace { "Gathered coins: requested $amount, available $gatheredAmount, change: ${gatheredAmount - amount}" } - return Pair(gathered, gatheredAmount) - } - /** * Generate an transaction exiting fungible assets from the ledger. * @@ -56,41 +27,14 @@ abstract class AbstractConserveAmount, C : CommandData, T : * the responsibility of the caller to check that they do not attempt to exit funds held by others. * @return the public key of the assets issuer, who must sign the transaction for it to be valid. */ + @Deprecated("This function will be removed in a future milestone", ReplaceWith("OnLedgerAsset.generateExit()")) @Throws(InsufficientBalanceException::class) fun generateExit(tx: TransactionBuilder, amountIssued: Amount>, assetStates: List>, deriveState: (TransactionState, Amount>, PublicKey) -> TransactionState, generateMoveCommand: () -> CommandData, - generateExitCommand: (Amount>) -> CommandData): PublicKey { - val owner = assetStates.map { it.state.data.owner }.toSet().singleOrNull() ?: throw InsufficientBalanceException(amountIssued) - val currency = amountIssued.token.product - val amount = Amount(amountIssued.quantity, currency) - var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token } - tx.notary = acceptableCoins.firstOrNull()?.state?.notary - // TODO: We should be prepared to produce multiple transactions exiting inputs from - // different notaries, or at least group states by notary and take the set with the - // highest total value - acceptableCoins = acceptableCoins.filter { it.state.notary == tx.notary } - - val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount) - val takeChangeFrom = gathered.lastOrNull() - val change = if (takeChangeFrom != null && gatheredAmount > amount) { - Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.amount.token) - } else { - null - } - - val outputs = if (change != null) { - // Add a change output and adjust the last output downwards. - listOf(deriveState(gathered.last().state, change, owner)) - } else emptyList() - - for (state in gathered) tx.addInputState(state) - for (state in outputs) tx.addOutputState(state) - tx.addCommand(generateMoveCommand(), gathered.map { it.state.data.owner }) - tx.addCommand(generateExitCommand(amountIssued), gathered.flatMap { it.state.data.exitKeys }) - return amountIssued.token.issuer.party.owningKey - } + generateExitCommand: (Amount>) -> CommandData): PublicKey + = OnLedgerAsset.generateExit(tx, amountIssued, assetStates, deriveState, generateMoveCommand, generateExitCommand) override fun verify(tx: TransactionForContract, inputs: List, diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index b725871d59..ddfb1f843e 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -10,6 +10,8 @@ import io.requery.kotlin.isNull import io.requery.kotlin.notNull import io.requery.query.RowExpression import net.corda.contracts.asset.Cash +import net.corda.contracts.asset.OnLedgerAsset +import net.corda.contracts.clause.AbstractConserveAmount import net.corda.core.ThreadBox import net.corda.core.bufferUntilSubscribed import net.corda.core.contracts.* @@ -448,115 +450,16 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P amount: Amount, to: PublicKey, onlyFromParties: Set?): Pair> { - // Discussion - // - // This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline. - // - // First we must select a set of asset states (which for convenience we will call 'coins' here, as in bitcoinj). - // The input states can be considered our "vault", and may consist of different products, and with different - // issuers and deposits. - // - // Coin selection is a complex problem all by itself and many different approaches can be used. It is easily - // possible for different actors to use different algorithms and approaches that, for example, compete on - // privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of - // obfuscation and so on. - // - // Having selected input states of the correct asset, we must craft output states for the amount we're sending and - // the "change", which goes back to us. The change is required to make the amounts balance. We may need more - // than one change output in order to avoid merging assets from different deposits. The point of this design - // is to ensure that ledger entries are immutable and globally identifiable. - // - // Finally, we add the states to the provided partial transaction. - // Retrieve unspent and unlocked cash states that meet our spending criteria. val acceptableCoins = unconsumedStatesForSpending(amount, onlyFromParties, tx.notary, tx.lockId) - - // TODO: We should be prepared to produce multiple transactions spending inputs from - // different notaries, or at least group states by notary and take the set with the - // highest total value. - - // notary may be associated with locked state only - tx.notary = acceptableCoins.firstOrNull()?.state?.notary - - val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount) - - val takeChangeFrom = gathered.firstOrNull() - val change = if (takeChangeFrom != null && gatheredAmount > amount) { - Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.amount.token) - } else { - null - } - val keysUsed = gathered.map { it.state.data.owner } - - val states = gathered.groupBy { it.state.data.amount.token.issuer }.map { - val coins = it.value - val totalAmount = coins.map { it.state.data.amount }.sumOrThrow() - deriveState(coins.first().state, totalAmount, to) - }.sortedBy { it.data.amount.quantity } - - val outputs = if (change != null) { - // Just copy a key across as the change key. In real life of course, this works but leaks private data. - // In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow - // value flows through the transaction graph. - val existingOwner = gathered.first().state.data.owner - // Add a change output and adjust the last output downwards. - states.subList(0, states.lastIndex) + - states.last().let { - val spent = it.data.amount.withoutIssuer() - change.withoutIssuer() - deriveState(it, Amount(spent.quantity, it.data.amount.token), it.data.owner) - } + - states.last().let { - deriveState(it, Amount(change.quantity, it.data.amount.token), existingOwner) - } - } else states - - for (state in gathered) tx.addInputState(state) - for (state in outputs) tx.addOutputState(state) - - // What if we already have a move command with the right keys? Filter it out here or in platform code? - tx.addCommand(Cash().generateMoveCommand(), keysUsed) - - // update Vault - // notify(tx.toWireTransaction()) - // Vault update must be completed AFTER transaction is recorded to ledger storage!!! - // (this is accomplished within the recordTransaction function) - - return Pair(tx, keysUsed) + return OnLedgerAsset.generateSpend(tx, amount, to, acceptableCoins, + { state, amount, owner -> deriveState(state, amount, owner) }, + { Cash().generateMoveCommand() }) } private fun deriveState(txState: TransactionState, amount: Amount>, owner: PublicKey) = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) - /** - * Gather assets from the given list of states, sufficient to match or exceed the given amount. - * - * @param acceptableCoins list of states to use as inputs. - * @param amount the amount to gather states up to. - * @throws InsufficientBalanceException if there isn't enough value in the states to cover the requested amount. - */ - // TODO: Merge this with the function in [AbstractConserveAmount] - @Throws(InsufficientBalanceException::class) - private fun gatherCoins(acceptableCoins: Collection>, - amount: Amount): Pair>, Amount> { - require(amount.quantity > 0) { "Cannot gather zero coins" } - val gathered = arrayListOf>() - var gatheredAmount = Amount(0, amount.token) - for (c in acceptableCoins) { - if (gatheredAmount >= amount) break - gathered.add(c) - gatheredAmount += Amount(c.state.data.amount.quantity, amount.token) - } - - if (gatheredAmount < amount) { - log.trace("Insufficient balance: requested $amount, available $gatheredAmount (total balance ${cashBalances[amount.token]})") - throw InsufficientBalanceException(amount - gatheredAmount) - } - - log.trace("Gathered coins: requested $amount, available $gatheredAmount, change: ${gatheredAmount - amount}") - - return Pair(gathered, gatheredAmount) - } - private fun makeUpdate(tx: WireTransaction, ourKeys: Set): Vault.Update { val ourNewStates = tx.outputs. filter { isRelevant(it.data, ourKeys) }.