mirror of
https://github.com/corda/corda.git
synced 2025-02-01 00:45:59 +00:00
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:
parent
325f3f791f
commit
6c6ed3a758
@ -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")
|
||||
|
||||
|
@ -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
|
||||
|
@ -53,7 +53,6 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
* 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<State, Commands, Currency> = Clauses.ConserveAmount()
|
||||
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Cash.Commands>>
|
||||
= commands.select<Cash.Commands>()
|
||||
|
||||
@ -152,14 +151,8 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
/**
|
||||
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
|
||||
*/
|
||||
fun generateIssue(tx: TransactionBuilder, amount: Amount<Issued<Currency>>, 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<Issued<Currency>>, owner: PublicKey, notary: Party)
|
||||
= generateIssue(tx, TransactionState(State(amount, owner), notary), generateIssueCommand())
|
||||
|
||||
override fun deriveState(txState: TransactionState<State>, amount: Amount<Issued<Currency>>, owner: PublicKey)
|
||||
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
|
||||
|
@ -47,8 +47,6 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, C
|
||||
*/
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html")
|
||||
|
||||
override val conserveClause: AbstractConserveAmount<State, Commands, Commodity> = Clauses.ConserveAmount()
|
||||
|
||||
/**
|
||||
* The clauses for this contract are essentially:
|
||||
*
|
||||
@ -153,13 +151,8 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, C
|
||||
/**
|
||||
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
|
||||
*/
|
||||
fun generateIssue(tx: TransactionBuilder, amount: Amount<Issued<Commodity>>, 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<Issued<Commodity>>, owner: PublicKey, notary: Party)
|
||||
= generateIssue(tx, TransactionState(State(amount, owner), notary), generateIssueCommand())
|
||||
|
||||
|
||||
override fun deriveState(txState: TransactionState<State>, amount: Amount<Issued<Commodity>>, owner: PublicKey)
|
||||
|
@ -458,7 +458,7 @@ class Obligation<P : Any> : Contract {
|
||||
@Suppress("unused")
|
||||
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<Terms<P>>>,
|
||||
assetStates: List<StateAndRef<Obligation.State<P>>>): PublicKey
|
||||
= Clauses.ConserveAmount<P>().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<P : Any> : Contract {
|
||||
issuanceDef: Terms<P>,
|
||||
pennies: Long,
|
||||
beneficiary: PublicKey,
|
||||
notary: Party) {
|
||||
check(tx.inputStates().isEmpty())
|
||||
check(tx.outputStates().map { it.data }.sumObligationsOrNull<P>() == 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<Obligation.Terms<P>>,
|
||||
|
@ -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<T : Any, C : CommandData, S : FungibleAsset<T>> : Contract {
|
||||
companion object {
|
||||
val log = loggerFor<OnLedgerAsset<*, *, *>>()
|
||||
|
||||
/**
|
||||
* 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 <S : FungibleAsset<T>, T: Any> generateSpend(tx: TransactionBuilder,
|
||||
amount: Amount<T>,
|
||||
to: PublicKey,
|
||||
acceptableStates: List<StateAndRef<S>>,
|
||||
deriveState: (TransactionState<S>, Amount<Issued<T>>, PublicKey) -> TransactionState<S>,
|
||||
generateMoveCommand: () -> CommandData): 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.
|
||||
|
||||
// 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 <S : FungibleAsset<T>, T : Any> gatherCoins(acceptableCoins: Collection<StateAndRef<S>>,
|
||||
amount: Amount<T>): Pair<ArrayList<StateAndRef<S>>, Amount<T>> {
|
||||
require(amount.quantity > 0) { "Cannot gather zero coins" }
|
||||
val gathered = arrayListOf<StateAndRef<S>>()
|
||||
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 <S : FungibleAsset<T>, T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
|
||||
assetStates: List<StateAndRef<S>>,
|
||||
deriveState: (TransactionState<S>, Amount<Issued<T>>, PublicKey) -> TransactionState<S>,
|
||||
generateMoveCommand: () -> CommandData,
|
||||
generateExitCommand: (Amount<Issued<T>>) -> 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 <S : FungibleAsset<T>, T: Any> generateIssue(tx: TransactionBuilder,
|
||||
transactionState: TransactionState<S>,
|
||||
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<AuthenticatedObject<CommandData>>): Collection<AuthenticatedObject<C>>
|
||||
abstract val conserveClause: AbstractConserveAmount<S, C, T>
|
||||
|
||||
/**
|
||||
* Generate an transaction exiting assets from the ledger.
|
||||
@ -41,7 +231,7 @@ abstract class OnLedgerAsset<T : Any, C : CommandData, S : FungibleAsset<T>> : C
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
|
||||
assetStates: List<StateAndRef<S>>): PublicKey {
|
||||
return conserveClause.generateExit(
|
||||
return generateExit(
|
||||
tx,
|
||||
amountIssued,
|
||||
assetStates,
|
||||
|
@ -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<S : FungibleAsset<T>, C : CommandData, T :
|
||||
val log = loggerFor<AbstractConserveAmount<*, *, *>>()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<StateAndRef<S>>,
|
||||
amount: Amount<T>): Pair<ArrayList<StateAndRef<S>>, Amount<T>> {
|
||||
require(amount.quantity > 0) { "Cannot gather zero coins" }
|
||||
val gathered = arrayListOf<StateAndRef<S>>()
|
||||
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<S : FungibleAsset<T>, 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<Issued<T>>,
|
||||
assetStates: List<StateAndRef<S>>,
|
||||
deriveState: (TransactionState<S>, Amount<Issued<T>>, PublicKey) -> TransactionState<S>,
|
||||
generateMoveCommand: () -> CommandData,
|
||||
generateExitCommand: (Amount<Issued<T>>) -> 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<Issued<T>>) -> CommandData): PublicKey
|
||||
= OnLedgerAsset.generateExit(tx, amountIssued, assetStates, deriveState, generateMoveCommand, generateExitCommand)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
|
@ -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) }.
|
||||
|
Loading…
x
Reference in New Issue
Block a user