mirror of
https://github.com/corda/corda.git
synced 2025-01-19 11:16:54 +00:00
Merged in rnicoll-commodity-cleanup (pull request #256)
Clean up contracts
This commit is contained in:
commit
15eef7adab
@ -6,7 +6,6 @@ import com.r3corda.contracts.asset.sumCashBy
|
||||
import com.r3corda.contracts.clause.AbstractIssue
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.*
|
||||
import com.r3corda.core.crypto.NullPublicKey
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.toStringShort
|
||||
@ -50,8 +49,7 @@ class CommercialPaper : ClauseVerifier() {
|
||||
val maturityDate: Instant
|
||||
)
|
||||
|
||||
override val clauses: List<SingleClause>
|
||||
get() = listOf(Clauses.Group())
|
||||
override val clauses = listOf(Clauses.Group())
|
||||
|
||||
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
|
||||
= tx.commands.select<Commands>()
|
||||
@ -82,32 +80,27 @@ class CommercialPaper : ClauseVerifier() {
|
||||
|
||||
interface Clauses {
|
||||
class Group : GroupClauseVerifier<State, Issued<Terms>>() {
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val clauses: List<GroupClause<State, Issued<Terms>>>
|
||||
get() = listOf(
|
||||
Redeem(),
|
||||
Move(),
|
||||
Issue())
|
||||
override val ifNotMatched = MatchBehaviour.ERROR
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
override val clauses = listOf(
|
||||
Redeem(),
|
||||
Move(),
|
||||
Issue()
|
||||
)
|
||||
|
||||
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
|
||||
= tx.groupStates<State, Issued<Terms>> { it.token }
|
||||
}
|
||||
|
||||
abstract class AbstractGroupClause: GroupClause<State, Issued<Terms>> {
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val ifNotMatched = MatchBehaviour.CONTINUE
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
}
|
||||
|
||||
class Issue : AbstractIssue<State, Terms>(
|
||||
{ map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() },
|
||||
{ token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Issue::class.java)
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
@ -127,8 +120,7 @@ class CommercialPaper : ClauseVerifier() {
|
||||
}
|
||||
|
||||
class Move: AbstractGroupClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Move::class.java)
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Move::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
|
@ -184,7 +184,7 @@ class FloatingRatePaymentEvent(date: LocalDate,
|
||||
* This is just a representation of a vanilla Fixed vs Floating (same currency) IRS in the R3 prototype model.
|
||||
*/
|
||||
class InterestRateSwap() : ClauseVerifier() {
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("is_this_the_text_of_the_contract ? TBD")
|
||||
override val legalContractReference = SecureHash.sha256("is_this_the_text_of_the_contract ? TBD")
|
||||
|
||||
/**
|
||||
* This Common area contains all the information that is not leg specific.
|
||||
@ -457,10 +457,8 @@ class InterestRateSwap() : ClauseVerifier() {
|
||||
* helper functions for the clauses.
|
||||
*/
|
||||
abstract class AbstractIRSClause : GroupClause<State, String> {
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
override val ifNotMatched = MatchBehaviour.CONTINUE
|
||||
|
||||
// These functions may make more sense to use for basket types, but for now let's leave them here
|
||||
fun checkLegDates(legs: Array<CommonLeg>) {
|
||||
@ -504,27 +502,20 @@ class InterestRateSwap() : ClauseVerifier() {
|
||||
}
|
||||
|
||||
class Group : GroupClauseVerifier<State, String>() {
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
override val ifNotMatched = MatchBehaviour.ERROR
|
||||
|
||||
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, String>> {
|
||||
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, String>>
|
||||
// Group by Trade ID for in / out states
|
||||
return tx.groupStates() { state: InterestRateSwap.State -> state.common.tradeID }
|
||||
}
|
||||
= tx.groupStates() { state -> state.common.tradeID }
|
||||
|
||||
override val clauses: List<GroupClause<State, String>>
|
||||
get() = listOf(Agree(), Fix(), Pay(), Mature())
|
||||
override val clauses = listOf(Agree(), Fix(), Pay(), Mature())
|
||||
}
|
||||
|
||||
class Timestamped : SingleClause {
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = emptySet()
|
||||
override val ifMatched = MatchBehaviour.CONTINUE
|
||||
override val ifNotMatched = MatchBehaviour.ERROR
|
||||
override val requiredCommands = emptySet<Class<out CommandData>>()
|
||||
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
|
||||
// TODO: This needs to either be the notary used for the inputs, or otherwise
|
||||
@ -537,8 +528,7 @@ class InterestRateSwap() : ClauseVerifier() {
|
||||
}
|
||||
|
||||
class Agree : AbstractIRSClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Agree::class.java)
|
||||
override val requiredCommands = setOf(Commands.Agree::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
@ -574,8 +564,7 @@ class InterestRateSwap() : ClauseVerifier() {
|
||||
}
|
||||
|
||||
class Fix : AbstractIRSClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Refix::class.java)
|
||||
override val requiredCommands = setOf(Commands.Refix::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
@ -620,8 +609,7 @@ class InterestRateSwap() : ClauseVerifier() {
|
||||
}
|
||||
|
||||
class Pay : AbstractIRSClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Pay::class.java)
|
||||
override val requiredCommands = setOf(Commands.Pay::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
@ -637,8 +625,7 @@ class InterestRateSwap() : ClauseVerifier() {
|
||||
}
|
||||
|
||||
class Mature : AbstractIRSClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Mature::class.java)
|
||||
override val requiredCommands = setOf(Commands.Mature::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
@ -748,7 +735,7 @@ class InterestRateSwap() : ClauseVerifier() {
|
||||
/**
|
||||
* Just makes printing it out a bit better for those who don't have 80000 column wide monitors.
|
||||
*/
|
||||
fun prettyPrint(): String = toString().replace(",", "\n")
|
||||
fun prettyPrint() = toString().replace(",", "\n")
|
||||
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ val CASH_PROGRAM_ID = Cash()
|
||||
* At the same time, other contracts that just want money and don't care much who is currently holding it in their
|
||||
* vaults can ignore the issuer/depositRefs and just examine the amount fields.
|
||||
*/
|
||||
class Cash : ClauseVerifier() {
|
||||
class Cash : OnLedgerAsset<Currency, Cash.State>() {
|
||||
/**
|
||||
* TODO:
|
||||
* 1) hash should be of the contents, not the URI
|
||||
@ -45,19 +45,16 @@ class Cash : ClauseVerifier() {
|
||||
* 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 clauses: List<SingleClause>
|
||||
get() = listOf(Clauses.Group())
|
||||
override val conserveClause: AbstractConserveAmount<State, Currency> = Clauses.ConserveAmount()
|
||||
override val clauses = listOf(Clauses.Group())
|
||||
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
|
||||
= tx.commands.select<Cash.Commands>()
|
||||
|
||||
interface Clauses {
|
||||
class Group : GroupClauseVerifier<State, Issued<Currency>>() {
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
override val clauses: List<GroupClause<State, Issued<Currency>>>
|
||||
get() = listOf(
|
||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
|
||||
override val clauses = listOf(
|
||||
NoZeroSizedOutputs<State, Currency>(),
|
||||
Issue(),
|
||||
ConserveAmount())
|
||||
@ -66,9 +63,11 @@ class Cash : ClauseVerifier() {
|
||||
= tx.groupStates<State, Issued<Currency>> { it.issuanceDef }
|
||||
}
|
||||
|
||||
class Issue : AbstractIssue<State, Currency>({ sumCash() }, { token -> sumCashOrZero(token) }) {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Issue::class.java)
|
||||
class Issue : AbstractIssue<State, Currency>(
|
||||
sum = { sumCash() },
|
||||
sumOrZero = { sumCashOrZero(it) }
|
||||
) {
|
||||
override val requiredCommands = setOf(Commands.Issue::class.java)
|
||||
}
|
||||
|
||||
class ConserveAmount : AbstractConserveAmount<State, Currency>()
|
||||
@ -84,15 +83,11 @@ class Cash : ClauseVerifier() {
|
||||
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: PublicKey)
|
||||
: this(Amount(amount.quantity, Issued<Currency>(deposit, amount.token)), owner)
|
||||
|
||||
override val deposit: PartyAndReference
|
||||
get() = amount.token.issuer
|
||||
override val exitKeys: Collection<PublicKey>
|
||||
get() = setOf(deposit.party.owningKey)
|
||||
override val deposit = amount.token.issuer
|
||||
override val exitKeys = setOf(deposit.party.owningKey)
|
||||
override val contract = CASH_PROGRAM_ID
|
||||
override val issuanceDef: Issued<Currency>
|
||||
get() = amount.token
|
||||
override val participants: List<PublicKey>
|
||||
get() = listOf(owner)
|
||||
override val issuanceDef = amount.token
|
||||
override val participants = listOf(owner)
|
||||
|
||||
override fun move(newAmount: Amount<Issued<Currency>>, newOwner: PublicKey): FungibleAsset<Currency>
|
||||
= copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner)
|
||||
@ -126,75 +121,6 @@ class Cash : ClauseVerifier() {
|
||||
data class Exit(override val amount: Amount<Issued<Currency>>) : Commands, FungibleAsset.Commands.Exit<Currency>
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an transaction exiting cash 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 changeKey the key to send any change to. This needs to be explicitly stated as the input states are not
|
||||
* necessarily owned by us.
|
||||
* @param cashStates the cash 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 exit funds held by others.
|
||||
* @return the public key of the cash issuer, who must sign the transaction for it to be valid.
|
||||
*/
|
||||
// TODO: I'm not at all sure we should support exiting funds others hold, but that's a bigger discussion around the
|
||||
// contract logic, not just this function.
|
||||
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<Currency>>,
|
||||
changeKey: PublicKey, cashStates: List<StateAndRef<State>>): PublicKey {
|
||||
val currency = amountIssued.token.product
|
||||
val issuer = amountIssued.token.issuer.party
|
||||
val amount = Amount(amountIssued.quantity, currency)
|
||||
var acceptableCoins = cashStates.filter { ref -> ref.state.data.amount.token == amountIssued.token }
|
||||
val 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 == notary }
|
||||
|
||||
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, Amount(amount.quantity, currency))
|
||||
val takeChangeFrom = gathered.lastOrNull()
|
||||
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
|
||||
Amount<Issued<Currency>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val outputs: List<TransactionState<Cash.State>> = if (change != null) {
|
||||
// Add a change output and adjust the last output downwards.
|
||||
listOf(TransactionState(State(amountIssued.token.issuer, Amount(change.quantity, currency), changeKey),
|
||||
notary!!))
|
||||
} else emptyList()
|
||||
|
||||
for (state in gathered) tx.addInputState(state)
|
||||
for (state in outputs) tx.addOutputState(state)
|
||||
tx.addCommand(Commands.Exit(amountIssued), amountIssued.token.issuer.party.owningKey)
|
||||
return amountIssued.token.issuer.party.owningKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather coins 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: List<StateAndRef<State>>,
|
||||
amount: Amount<Currency>): Pair<ArrayList<StateAndRef<State>>, Amount<Currency>> {
|
||||
val gathered = arrayListOf<StateAndRef<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)
|
||||
throw InsufficientBalanceException(amount - gatheredAmount)
|
||||
|
||||
return Pair(gathered, gatheredAmount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.
|
||||
*/
|
||||
@ -208,83 +134,15 @@ class Cash : ClauseVerifier() {
|
||||
check(tx.inputStates().isEmpty())
|
||||
check(tx.outputStates().map { it.data }.sumCashOrNull() == null)
|
||||
val at = amount.token.issuer
|
||||
tx.addOutputState(TransactionState(Cash.State(amount, owner), notary))
|
||||
tx.addCommand(Cash.Commands.Issue(), at.party.owningKey)
|
||||
tx.addOutputState(TransactionState(State(amount, owner), notary))
|
||||
tx.addCommand(generateIssueCommand(), at.party.owningKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a transaction that consumes one or more of the given input states to move money to the given pubkey.
|
||||
* Note that the wallet list is not updated: it's up to you to do that.
|
||||
*
|
||||
* @param onlyFromParties if non-null, the wallet will be filtered to only include cash states issued by the set
|
||||
* of given parties. This can be useful if the party you're trying to pay has expectations
|
||||
* about which type of cash claims they are willing to accept.
|
||||
*/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
fun generateSpend(tx: TransactionBuilder, amount: Amount<Currency>, to: PublicKey,
|
||||
cashStates: List<StateAndRef<State>>, onlyFromParties: Set<Party>? = null): 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 cash states (which for convenience we will call 'coins' here, as in bitcoinj).
|
||||
// The input states can be considered our "wallet", and may consist of coins of different currencies, and from
|
||||
// different institutions 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 coins of the right currency, 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 coins 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.
|
||||
|
||||
val currency = amount.token
|
||||
val acceptableCoins = run {
|
||||
val ofCurrency = cashStates.filter { it.state.data.amount.token.product == currency }
|
||||
if (onlyFromParties != null)
|
||||
ofCurrency.filter { it.state.data.deposit.party in onlyFromParties }
|
||||
else
|
||||
ofCurrency
|
||||
}
|
||||
|
||||
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount)
|
||||
val takeChangeFrom = gathered.firstOrNull()
|
||||
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
|
||||
Amount<Issued<Currency>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val keysUsed = gathered.map { it.state.data.owner }.toSet()
|
||||
|
||||
val states = gathered.groupBy { it.state.data.deposit }.map {
|
||||
val coins = it.value
|
||||
val totalAmount = coins.map { it.state.data.amount }.sumOrThrow()
|
||||
TransactionState(State(totalAmount, to), coins.first().state.notary)
|
||||
}
|
||||
|
||||
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 changeKey = gathered.first().state.data.owner
|
||||
// Add a change output and adjust the last output downwards.
|
||||
states.subList(0, states.lastIndex) +
|
||||
states.last().let { TransactionState(it.data.copy(amount = it.data.amount - change), it.notary) } +
|
||||
TransactionState(State(change, changeKey), gathered.last().state.notary)
|
||||
} 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?
|
||||
val keysList = keysUsed.toList()
|
||||
tx.addCommand(Commands.Move(), keysList)
|
||||
return keysList
|
||||
}
|
||||
override fun deriveState(txState: TransactionState<State>, amount: Amount<Issued<Currency>>, owner: PublicKey)
|
||||
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
|
||||
override fun generateExitCommand(amount: Amount<Issued<Currency>>) = Commands.Exit(amount)
|
||||
override fun generateIssueCommand() = Commands.Issue()
|
||||
override fun generateMoveCommand() = Commands.Move()
|
||||
}
|
||||
|
||||
// Small DSL extensions.
|
||||
@ -298,7 +156,7 @@ fun Iterable<ContractState>.sumCashBy(owner: PublicKey): Amount<Issued<Currency>
|
||||
|
||||
/**
|
||||
* Sums the cash states in the list, throwing an exception if there are none, or if any of the cash
|
||||
* states cannot be added together (i.e. are different currencies).
|
||||
* states cannot be added together (i.e. are different currencies or issuers).
|
||||
*/
|
||||
fun Iterable<ContractState>.sumCash(): Amount<Issued<Currency>> = filterIsInstance<Cash.State>().map { it.amount }.sumOrThrow()
|
||||
|
||||
|
@ -9,8 +9,6 @@ import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.newSecureRandom
|
||||
import com.r3corda.core.crypto.toStringShort
|
||||
import com.r3corda.core.node.services.Wallet
|
||||
import com.r3corda.core.utilities.Emoji
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
@ -31,7 +29,7 @@ val COMMODITY_PROGRAM_ID = CommodityContract()
|
||||
* internal accounting by the issuer (it might be, for example, a warehouse and/or location within a warehouse).
|
||||
*/
|
||||
// TODO: Need to think about expiry of commodities, how to require payment of storage costs, etc.
|
||||
class CommodityContract : ClauseVerifier() {
|
||||
class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
|
||||
/**
|
||||
* TODO:
|
||||
* 1) hash should be of the contents, not the URI
|
||||
@ -44,6 +42,8 @@ class CommodityContract : ClauseVerifier() {
|
||||
*/
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html")
|
||||
|
||||
override val conserveClause: AbstractConserveAmount<State, Commodity> = Clauses.ConserveAmount()
|
||||
|
||||
/**
|
||||
* The clauses for this contract are essentially:
|
||||
*
|
||||
@ -63,35 +63,34 @@ class CommodityContract : ClauseVerifier() {
|
||||
* The group clause does not depend on any commands being present, so something has gone terribly wrong if
|
||||
* it doesn't match.
|
||||
*/
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
override val ifNotMatched = MatchBehaviour.ERROR
|
||||
/**
|
||||
* The group clause is the only top level clause, so end after processing it. If there are any commands left
|
||||
* after this clause has run, the clause verifier will trigger an error.
|
||||
*/
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
// Subclauses to run on each group
|
||||
override val clauses: List<GroupClause<State, Issued<Commodity>>>
|
||||
get() = listOf(
|
||||
NoZeroSizedOutputs<State, Commodity>(),
|
||||
Issue(),
|
||||
ConserveAmount()
|
||||
)
|
||||
override val clauses = listOf(
|
||||
NoZeroSizedOutputs<State, Commodity>(),
|
||||
Issue(),
|
||||
ConserveAmount()
|
||||
)
|
||||
|
||||
/**
|
||||
* Group commodity states by issuance definition (issuer and underlying commodity).
|
||||
*/
|
||||
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Commodity>>>
|
||||
override fun extractGroups(tx: TransactionForContract)
|
||||
= tx.groupStates<State, Issued<Commodity>> { it.issuanceDef }
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard issue clause, specialised to match the commodity issue command.
|
||||
*/
|
||||
class Issue : AbstractIssue<State, Commodity>({ -> sumCommodities() }, { token: Issued<Commodity> -> sumCommoditiesOrZero(token) }) {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Issue::class.java)
|
||||
class Issue : AbstractIssue<State, Commodity>(
|
||||
sum = { sumCommodities() },
|
||||
sumOrZero = { sumCommoditiesOrZero(it) }
|
||||
) {
|
||||
override val requiredCommands = setOf(Commands.Issue::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,15 +109,11 @@ class CommodityContract : ClauseVerifier() {
|
||||
constructor(deposit: PartyAndReference, amount: Amount<Commodity>, owner: PublicKey)
|
||||
: this(Amount(amount.quantity, Issued<Commodity>(deposit, amount.token)), owner)
|
||||
|
||||
override val deposit: PartyAndReference
|
||||
get() = amount.token.issuer
|
||||
override val deposit = amount.token.issuer
|
||||
override val contract = COMMODITY_PROGRAM_ID
|
||||
override val exitKeys: Collection<PublicKey>
|
||||
get() = Collections.singleton(owner)
|
||||
override val issuanceDef: Issued<Commodity>
|
||||
get() = amount.token
|
||||
override val participants: List<PublicKey>
|
||||
get() = listOf(owner)
|
||||
override val exitKeys = Collections.singleton(owner)
|
||||
override val issuanceDef = amount.token
|
||||
override val participants = listOf(owner)
|
||||
|
||||
override fun move(newAmount: Amount<Issued<Commodity>>, newOwner: PublicKey): FungibleAsset<Commodity>
|
||||
= copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner)
|
||||
@ -151,8 +146,7 @@ class CommodityContract : ClauseVerifier() {
|
||||
*/
|
||||
data class Exit(override val amount: Amount<Issued<Commodity>>) : Commands, FungibleAsset.Commands.Exit<Commodity>
|
||||
}
|
||||
override val clauses: List<SingleClause>
|
||||
get() = listOf(Clauses.Group())
|
||||
override val clauses = listOf(Clauses.Group())
|
||||
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
|
||||
= tx.commands.select<CommodityContract.Commands>()
|
||||
|
||||
@ -167,107 +161,18 @@ class CommodityContract : ClauseVerifier() {
|
||||
*/
|
||||
fun generateIssue(tx: TransactionBuilder, amount: Amount<Issued<Commodity>>, owner: PublicKey, notary: Party) {
|
||||
check(tx.inputStates().isEmpty())
|
||||
check(tx.outputStates().map { it.data }.sumFungibleOrNull<Commodity>() == null)
|
||||
check(tx.outputStates().map { it.data }.sumCashOrNull() == null)
|
||||
val at = amount.token.issuer
|
||||
tx.addOutputState(TransactionState(CommodityContract.State(amount, owner), notary))
|
||||
tx.addCommand(Commands.Issue(), at.party.owningKey)
|
||||
tx.addOutputState(TransactionState(State(amount, owner), notary))
|
||||
tx.addCommand(generateIssueCommand(), at.party.owningKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a transaction that consumes one or more of the given input states to move money to the given pubkey.
|
||||
* Note that the wallet list is not updated: it's up to you to do that.
|
||||
*/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
fun generateSpend(tx: TransactionBuilder, amount: Amount<Issued<Commodity>>, to: PublicKey,
|
||||
commodityStates: List<StateAndRef<State>>): List<PublicKey> =
|
||||
generateSpend(tx, Amount(amount.quantity, amount.token.product), to, commodityStates,
|
||||
setOf(amount.token.issuer.party))
|
||||
|
||||
/**
|
||||
* Generate a transaction that consumes one or more of the given input states to move money to the given pubkey.
|
||||
* Note that the wallet list is not updated: it's up to you to do that.
|
||||
*
|
||||
* @param onlyFromParties if non-null, the wallet will be filtered to only include commodity states issued by the set
|
||||
* of given parties. This can be useful if the party you're trying to pay has expectations
|
||||
* about which type of commodity claims they are willing to accept.
|
||||
*/
|
||||
// TODO: These spend functions should be shared with [Cash], possibly through some common superclass
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
fun generateSpend(tx: TransactionBuilder, amount: Amount<Commodity>, to: PublicKey,
|
||||
commodityStates: List<StateAndRef<State>>, onlyFromParties: Set<Party>? = null): 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 commodity states (which for convenience we will call 'coins' here, as in bitcoinj).
|
||||
// The input states can be considered our "wallet", and may consist of coins of different currencies, and from
|
||||
// different institutions 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 coins of the right currency, 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 coins 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.
|
||||
|
||||
val currency = amount.token
|
||||
val acceptableCoins = run {
|
||||
val ofCurrency = commodityStates.filter { it.state.data.amount.token.product == currency }
|
||||
if (onlyFromParties != null)
|
||||
ofCurrency.filter { it.state.data.deposit.party in onlyFromParties }
|
||||
else
|
||||
ofCurrency
|
||||
}
|
||||
|
||||
val gathered = arrayListOf<StateAndRef<State>>()
|
||||
var gatheredAmount = Amount(0, currency)
|
||||
var takeChangeFrom: StateAndRef<State>? = null
|
||||
for (c in acceptableCoins) {
|
||||
if (gatheredAmount >= amount) break
|
||||
gathered.add(c)
|
||||
gatheredAmount += Amount(c.state.data.amount.quantity, currency)
|
||||
takeChangeFrom = c
|
||||
}
|
||||
|
||||
if (gatheredAmount < amount)
|
||||
throw InsufficientBalanceException(amount - gatheredAmount)
|
||||
|
||||
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
|
||||
Amount<Issued<Commodity>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val keysUsed = gathered.map { it.state.data.owner }.toSet()
|
||||
|
||||
val states = gathered.groupBy { it.state.data.deposit }.map {
|
||||
val coins = it.value
|
||||
val totalAmount = coins.map { it.state.data.amount }.sumOrThrow()
|
||||
TransactionState(State(totalAmount, to), coins.first().state.notary)
|
||||
}
|
||||
|
||||
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 changeKey = gathered.first().state.data.owner
|
||||
// Add a change output and adjust the last output downwards.
|
||||
states.subList(0, states.lastIndex) +
|
||||
states.last().let { TransactionState(it.data.copy(amount = it.data.amount - change), it.notary) } +
|
||||
TransactionState(State(change, changeKey), gathered.last().state.notary)
|
||||
} 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?
|
||||
val keysList = keysUsed.toList()
|
||||
tx.addCommand(Commands.Move(), keysList)
|
||||
return keysList
|
||||
}
|
||||
override fun deriveState(txState: TransactionState<State>, amount: Amount<Issued<Commodity>>, owner: PublicKey)
|
||||
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
|
||||
override fun generateExitCommand(amount: Amount<Issued<Commodity>>) = Commands.Exit(amount)
|
||||
override fun generateIssueCommand() = Commands.Issue()
|
||||
override fun generateMoveCommand() = Commands.Move()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -43,8 +43,7 @@ class Obligation<P> : ClauseVerifier() {
|
||||
* that is inconsistent with the legal contract.
|
||||
*/
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.example.gov/cash-settlement.html")
|
||||
override val clauses: List<SingleClause>
|
||||
get() = listOf(InterceptorClause(Clauses.VerifyLifecycle<P>(), Clauses.Net<P>()),
|
||||
override val clauses = listOf(InterceptorClause(Clauses.VerifyLifecycle<P>(), Clauses.Net<P>()),
|
||||
Clauses.Group<P>())
|
||||
|
||||
interface Clauses {
|
||||
@ -52,18 +51,16 @@ class Obligation<P> : ClauseVerifier() {
|
||||
* Parent clause for clauses that operate on grouped states (those which are fungible).
|
||||
*/
|
||||
class Group<P> : GroupClauseVerifier<State<P>, Issued<Terms<P>>>() {
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
override val clauses: List<GroupClause<State<P>, Issued<Terms<P>>>>
|
||||
get() = listOf(
|
||||
NoZeroSizedOutputs<State<P>, Terms<P>>(),
|
||||
SetLifecycle<P>(),
|
||||
VerifyLifecycle<P>(),
|
||||
Settle<P>(),
|
||||
Issue(),
|
||||
ConserveAmount())
|
||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
|
||||
override val clauses = listOf(
|
||||
NoZeroSizedOutputs<State<P>, Terms<P>>(),
|
||||
SetLifecycle<P>(),
|
||||
VerifyLifecycle<P>(),
|
||||
Settle<P>(),
|
||||
Issue(),
|
||||
ConserveAmount()
|
||||
)
|
||||
|
||||
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<Obligation.State<P>, Issued<Terms<P>>>>
|
||||
= tx.groupStates<Obligation.State<P>, Issued<Terms<P>>> { it.issuanceDef }
|
||||
@ -73,8 +70,7 @@ class Obligation<P> : ClauseVerifier() {
|
||||
* Generic issuance clause
|
||||
*/
|
||||
class Issue<P> : AbstractIssue<State<P>, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Obligation.Commands.Issue::class.java)
|
||||
override val requiredCommands = setOf(Obligation.Commands.Issue::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,12 +87,9 @@ class Obligation<P> : ClauseVerifier() {
|
||||
* Obligation-specific clause for changing the lifecycle of one or more states.
|
||||
*/
|
||||
class SetLifecycle<P> : GroupClause<State<P>, Issued<Terms<P>>> {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.SetLifecycle::class.java)
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
override val requiredCommands = setOf(Commands.SetLifecycle::class.java)
|
||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State<P>>,
|
||||
@ -114,12 +107,9 @@ class Obligation<P> : ClauseVerifier() {
|
||||
* change of ownership of other states to fulfil
|
||||
*/
|
||||
class Settle<P> : GroupClause<State<P>, Issued<Terms<P>>> {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Settle::class.java)
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
override val requiredCommands = setOf(Commands.Settle::class.java)
|
||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State<P>>,
|
||||
@ -208,12 +198,9 @@ class Obligation<P> : ClauseVerifier() {
|
||||
* non-standard lifecycle states on input/output.
|
||||
*/
|
||||
class VerifyLifecycle<P> : SingleClause, GroupClause<State<P>, Issued<Terms<P>>> {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = emptySet()
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
override val requiredCommands: Set<Class<out CommandData>> = emptySet()
|
||||
override val ifMatched: MatchBehaviour = MatchBehaviour.CONTINUE
|
||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
|
||||
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
|
||||
= verify(
|
||||
@ -407,8 +394,8 @@ class Obligation<P> : ClauseVerifier() {
|
||||
|
||||
// If we have an default command, perform special processing: issued contracts can only be defaulted
|
||||
// after the due date, and default/reset can only be done by the beneficiary
|
||||
val expectedInputLifecycle: Lifecycle = setLifecycleCommand.value.inverse
|
||||
val expectedOutputLifecycle: Lifecycle = setLifecycleCommand.value.lifecycle
|
||||
val expectedInputLifecycle = setLifecycleCommand.value.inverse
|
||||
val expectedOutputLifecycle = setLifecycleCommand.value.lifecycle
|
||||
|
||||
// Check that we're past the deadline for ALL involved inputs, and that the output states correspond 1:1
|
||||
for ((stateIdx, input) in inputs.withIndex()) {
|
||||
@ -416,7 +403,7 @@ class Obligation<P> : ClauseVerifier() {
|
||||
val actualOutput = outputs[stateIdx]
|
||||
val deadline = input.dueBefore
|
||||
val timestamp: TimestampCommand? = tx.timestamp
|
||||
val expectedOutput: State<P> = input.copy(lifecycle = expectedOutputLifecycle)
|
||||
val expectedOutput = input.copy(lifecycle = expectedOutputLifecycle)
|
||||
|
||||
requireThat {
|
||||
"there is a timestamp from the authority" by (timestamp != null)
|
||||
@ -458,6 +445,24 @@ class Obligation<P> : ClauseVerifier() {
|
||||
tx.addCommand(Commands.Net(NetType.PAYMENT), signer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an transaction exiting an obligation 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 changeKey the key to send any change to. This needs to be explicitly stated as the input states are not
|
||||
* necessarily owned by us.
|
||||
* @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 exit funds held by others.
|
||||
* @return the public key of the assets issuer, who must sign the transaction for it to be valid.
|
||||
*/
|
||||
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<Terms<P>>>,
|
||||
changeKey: PublicKey, assetStates: List<StateAndRef<Obligation.State<P>>>): PublicKey
|
||||
= Clauses.ConserveAmount<P>().generateExit(tx, amountIssued, changeKey, assetStates,
|
||||
deriveState = { state, amount, owner -> state.copy(data = state.data.move(amount, owner)) },
|
||||
generateExitCommand = { amount -> Commands.Exit<P>(amount) }
|
||||
)
|
||||
|
||||
/**
|
||||
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
|
||||
*/
|
||||
@ -515,7 +520,7 @@ class Obligation<P> : ClauseVerifier() {
|
||||
lifecycle: Lifecycle,
|
||||
notary: Party) {
|
||||
val states = statesAndRefs.map { it.state.data }
|
||||
val issuanceDef = getTemplateOrThrow(states)
|
||||
val issuanceDef = getTermsOrThrow(states)
|
||||
val existingLifecycle = when (lifecycle) {
|
||||
Lifecycle.DEFAULTED -> Lifecycle.NORMAL
|
||||
Lifecycle.NORMAL -> Lifecycle.DEFAULTED
|
||||
@ -580,7 +585,7 @@ class Obligation<P> : ClauseVerifier() {
|
||||
tx.addInputState(ref)
|
||||
|
||||
val assetState = ref.state.data
|
||||
val amount: Amount<P> = Amount(assetState.amount.quantity, assetState.amount.token.product)
|
||||
val amount = Amount(assetState.amount.quantity, assetState.amount.token.product)
|
||||
if (obligationRemaining >= amount) {
|
||||
tx.addOutputState(assetState.move(assetState.amount, obligationOwner), notary)
|
||||
obligationRemaining -= amount
|
||||
@ -612,7 +617,7 @@ class Obligation<P> : ClauseVerifier() {
|
||||
states.map { it.issuanceDef }.distinct().single()
|
||||
|
||||
/** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */
|
||||
private fun getTemplateOrThrow(states: Iterable<State<P>>): Terms<P> =
|
||||
private fun getTermsOrThrow(states: Iterable<State<P>>) =
|
||||
states.map { it.template }.distinct().single()
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,78 @@
|
||||
package com.r3corda.contracts.asset
|
||||
|
||||
import com.r3corda.contracts.clause.AbstractConserveAmount
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.*
|
||||
import com.r3corda.core.crypto.*
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generic contract for assets on a ledger
|
||||
//
|
||||
|
||||
/**
|
||||
* An asset transaction may split and merge assets represented by a set of (issuer, depositRef) pairs, across multiple
|
||||
* input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour (a blend of
|
||||
* issuer+depositRef) and you couldn't merge outputs of two colours together, but you COULD put them in the same
|
||||
* transaction.
|
||||
*
|
||||
* The goal of this design is to ensure that assets can be withdrawn from the ledger easily: if you receive some asset
|
||||
* via this contract, you always know where to go in order to extract it from the R3 ledger, no matter how many hands
|
||||
* it has passed through in the intervening time.
|
||||
*
|
||||
* At the same time, other contracts that just want assets and don't care much who is currently holding it can ignore
|
||||
* the issuer/depositRefs and just examine the amount fields.
|
||||
*/
|
||||
abstract class OnLedgerAsset<T : Any, S : FungibleAsset<T>> : ClauseVerifier() {
|
||||
abstract val conserveClause: AbstractConserveAmount<S, T>
|
||||
|
||||
/**
|
||||
* Generate an transaction exiting 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 changeKey the key to send any change to. This needs to be explicitly stated as the input states are not
|
||||
* necessarily owned by us.
|
||||
* @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 exit funds held by others.
|
||||
* @return the public key of the assets issuer, who must sign the transaction for it to be valid.
|
||||
*/
|
||||
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
|
||||
changeKey: PublicKey, assetStates: List<StateAndRef<S>>): PublicKey
|
||||
= conserveClause.generateExit(tx, amountIssued, changeKey, assetStates,
|
||||
deriveState = { state, amount, owner -> deriveState(state, amount, owner) },
|
||||
generateExitCommand = { amount -> generateExitCommand(amount) }
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* Generate a transaction that consumes one or more of the given input states to move assets to the given pubkey.
|
||||
* Note that the wallet list is not updated: it's up to you to do that.
|
||||
*
|
||||
* @param onlyFromParties if non-null, the wallet will be filtered to only include asset states issued by the set
|
||||
* of given parties. This can be useful if the party you're trying to pay has expectations
|
||||
* about which type of asset claims they are willing to accept.
|
||||
*/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
fun generateSpend(tx: TransactionBuilder,
|
||||
amount: Amount<T>,
|
||||
to: PublicKey,
|
||||
assetsStates: List<StateAndRef<S>>,
|
||||
onlyFromParties: Set<Party>? = null): List<PublicKey>
|
||||
= conserveClause.generateSpend(tx, amount, to, assetsStates, onlyFromParties,
|
||||
deriveState = { state, amount, owner -> deriveState(state, amount, owner) },
|
||||
generateMoveCommand = { generateMoveCommand() })
|
||||
|
||||
abstract fun generateExitCommand(amount: Amount<Issued<T>>): FungibleAsset.Commands.Exit<T>
|
||||
abstract fun generateIssueCommand(): FungibleAsset.Commands.Issue
|
||||
abstract fun generateMoveCommand(): FungibleAsset.Commands.Move
|
||||
|
||||
/**
|
||||
* Derive a new transaction state based on the given example, with amount and owner modified. This allows concrete
|
||||
* implementations to have fields in their state which we don't know about here, and we simply leave them untouched
|
||||
* when sending out "change" from spending/exiting.
|
||||
*/
|
||||
abstract fun deriveState(txState: TransactionState<S>, amount: Amount<Issued<T>>, owner: PublicKey): TransactionState<S>
|
||||
}
|
@ -1,12 +1,15 @@
|
||||
package com.r3corda.contracts.clause
|
||||
|
||||
import com.r3corda.contracts.asset.FungibleAsset
|
||||
import com.r3corda.contracts.asset.InsufficientBalanceException
|
||||
import com.r3corda.contracts.asset.sumFungibleOrNull
|
||||
import com.r3corda.contracts.asset.sumFungibleOrZero
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.GroupClause
|
||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
import com.r3corda.core.crypto.Party
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Standardised clause for checking input/output balances of fungible assets. Requires that a
|
||||
@ -21,6 +24,152 @@ abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = emptySet()
|
||||
|
||||
/**
|
||||
* 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>> {
|
||||
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)
|
||||
throw InsufficientBalanceException(amount - gatheredAmount)
|
||||
|
||||
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 changeKey the key to send any change to. This needs to be explicitly stated as the input states are not
|
||||
* necessarily owned by us.
|
||||
* @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 exit funds held by others.
|
||||
* @return the public key of the assets issuer, who must sign the transaction for it to be valid.
|
||||
*/
|
||||
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
|
||||
changeKey: PublicKey, assetStates: List<StateAndRef<S>>,
|
||||
deriveState: (TransactionState<S>, Amount<Issued<T>>, PublicKey) -> TransactionState<S>,
|
||||
generateExitCommand: (Amount<Issued<T>>) -> CommandData): PublicKey {
|
||||
val currency = amountIssued.token.product
|
||||
val amount = Amount(amountIssued.quantity, currency)
|
||||
var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token }
|
||||
val 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 == notary }
|
||||
|
||||
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, Amount(amount.quantity, currency))
|
||||
val takeChangeFrom = gathered.lastOrNull()
|
||||
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
|
||||
Amount<Issued<T>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val outputs = if (change != null) {
|
||||
// Add a change output and adjust the last output downwards.
|
||||
listOf(deriveState(gathered.last().state, change, changeKey))
|
||||
} else emptyList()
|
||||
|
||||
for (state in gathered) tx.addInputState(state)
|
||||
for (state in outputs) tx.addOutputState(state)
|
||||
tx.addCommand(generateExitCommand(amountIssued), amountIssued.token.issuer.party.owningKey)
|
||||
return amountIssued.token.issuer.party.owningKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a transaction that consumes one or more of the given input states to move assets to the given pubkey.
|
||||
* Note that the wallet list is not updated: it's up to you to do that.
|
||||
*
|
||||
* @param onlyFromParties if non-null, the wallet will be filtered to only include asset states issued by the set
|
||||
* of given parties. This can be useful if the party you're trying to pay has expectations
|
||||
* about which type of asset claims they are willing to accept.
|
||||
*/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
fun generateSpend(tx: TransactionBuilder,
|
||||
amount: Amount<T>,
|
||||
to: PublicKey,
|
||||
assetsStates: List<StateAndRef<S>>,
|
||||
onlyFromParties: Set<Party>? = null,
|
||||
deriveState: (TransactionState<S>, Amount<Issued<T>>, PublicKey) -> TransactionState<S>,
|
||||
generateMoveCommand: () -> CommandData): 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 "wallet", 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.
|
||||
|
||||
val currency = amount.token
|
||||
val acceptableCoins = run {
|
||||
val ofCurrency = assetsStates.filter { it.state.data.amount.token.product == currency }
|
||||
if (onlyFromParties != null)
|
||||
ofCurrency.filter { it.state.data.deposit.party in onlyFromParties }
|
||||
else
|
||||
ofCurrency
|
||||
}
|
||||
|
||||
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount)
|
||||
val takeChangeFrom = gathered.firstOrNull()
|
||||
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
|
||||
Amount<Issued<T>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val keysUsed = gathered.map { it.state.data.owner }.toSet()
|
||||
|
||||
val states = gathered.groupBy { it.state.data.deposit }.map {
|
||||
val coins = it.value
|
||||
val totalAmount = coins.map { it.state.data.amount }.sumOrThrow()
|
||||
deriveState(coins.first().state, totalAmount, to)
|
||||
}
|
||||
|
||||
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 changeKey = gathered.first().state.data.owner
|
||||
// Add a change output and adjust the last output downwards.
|
||||
states.subList(0, states.lastIndex) +
|
||||
states.last().let { deriveState(it, it.data.amount - change, it.data.owner) } +
|
||||
deriveState(gathered.last().state, change, changeKey)
|
||||
} 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?
|
||||
val keysList = keysUsed.toList()
|
||||
tx.addCommand(generateMoveCommand(), keysList)
|
||||
return keysList
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
|
Loading…
Reference in New Issue
Block a user