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.
This commit is contained in:
Ross Nicoll
2017-02-24 16:15:03 +00:00
parent 325f3f791f
commit 6c6ed3a758
8 changed files with 217 additions and 190 deletions

View File

@ -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<Currency>,
to: PublicKey,
onlyFromParties: Set<AbstractParty>?): Pair<TransactionBuilder, List<PublicKey>> {
// 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<Cash.State>(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<Cash.State>, amount: Amount<Issued<Currency>>, 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<StateAndRef<Cash.State>>,
amount: Amount<Currency>): Pair<ArrayList<StateAndRef<Cash.State>>, Amount<Currency>> {
require(amount.quantity > 0) { "Cannot gather zero coins" }
val gathered = arrayListOf<StateAndRef<Cash.State>>()
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<PublicKey>): Vault.Update {
val ourNewStates = tx.outputs.
filter { isRelevant(it.data, ourKeys) }.