mirror of
https://github.com/corda/corda.git
synced 2024-12-19 04:57:58 +00:00
Rebuild asset contracts using clauses
This commit is contained in:
parent
ea051d57be
commit
cba3aab96e
@ -1,6 +1,10 @@
|
||||
package com.r3corda.contracts.asset
|
||||
|
||||
import com.r3corda.contracts.clause.AbstractConserveAmount
|
||||
import com.r3corda.contracts.clause.AbstractIssue
|
||||
import com.r3corda.contracts.clause.NoZeroSizedOutputs
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.*
|
||||
import com.r3corda.core.crypto.*
|
||||
import com.r3corda.core.node.services.Wallet
|
||||
import com.r3corda.core.utilities.Emoji
|
||||
@ -28,7 +32,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 : FungibleAsset<Currency>() {
|
||||
class Cash : ClauseVerifier() {
|
||||
/**
|
||||
* TODO:
|
||||
* 1) hash should be of the contents, not the URI
|
||||
@ -40,6 +44,34 @@ class Cash : FungibleAsset<Currency>() {
|
||||
* 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 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(
|
||||
NoZeroSizedOutputs<State, Currency>(),
|
||||
Issue(),
|
||||
ConserveAmount())
|
||||
|
||||
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Currency>>>
|
||||
= 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 ConserveAmount : AbstractConserveAmount<State, Currency>()
|
||||
}
|
||||
|
||||
/** A state representing a cash claim against some party */
|
||||
data class State(
|
||||
@ -47,12 +79,10 @@ class Cash : FungibleAsset<Currency>() {
|
||||
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
override val owner: PublicKey
|
||||
) : FungibleAsset.State<Currency> {
|
||||
) : FungibleAsset<Currency> {
|
||||
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: PublicKey)
|
||||
: this(Amount(amount.quantity, Issued<Currency>(deposit, amount.token)), owner)
|
||||
: this(Amount(amount.quantity, Issued<Currency>(deposit, amount.token)), owner)
|
||||
|
||||
override val productAmount: Amount<Currency>
|
||||
get() = Amount(amount.quantity, amount.token.product)
|
||||
override val deposit: PartyAndReference
|
||||
get() = amount.token.issuer
|
||||
override val exitKeys: Collection<PublicKey>
|
||||
@ -63,8 +93,8 @@ class Cash : FungibleAsset<Currency>() {
|
||||
override val participants: List<PublicKey>
|
||||
get() = listOf(owner)
|
||||
|
||||
override fun move(newAmount: Amount<Currency>, newOwner: PublicKey): FungibleAsset.State<Currency>
|
||||
= copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner)
|
||||
override fun move(newAmount: Amount<Issued<Currency>>, newOwner: PublicKey): FungibleAsset<Currency>
|
||||
= copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner)
|
||||
|
||||
override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})"
|
||||
|
||||
@ -72,7 +102,7 @@ class Cash : FungibleAsset<Currency>() {
|
||||
}
|
||||
|
||||
// Just for grouping
|
||||
interface Commands : CommandData {
|
||||
interface Commands : FungibleAsset.Commands {
|
||||
/**
|
||||
* A command stating that money has been moved, optionally to fulfil another contract.
|
||||
*
|
||||
|
@ -1,41 +1,38 @@
|
||||
package com.r3corda.contracts.asset
|
||||
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Cash-like
|
||||
//
|
||||
|
||||
class InsufficientBalanceException(val amountMissing: Amount<Currency>) : Exception()
|
||||
|
||||
/**
|
||||
* Superclass for contracts representing assets which are fungible, countable and issued by a specific party. States
|
||||
* contain assets which are equivalent (such as cash of the same currency), so records of their existence can
|
||||
* be merged or split as needed where the issuer is the same. For instance, dollars issued by the Fed are fungible and
|
||||
* countable (in cents), barrels of West Texas crude are fungible and countable (oil from two small containers
|
||||
* can be poured into one large container), shares of the same class in a specific company are fungible and
|
||||
* countable, and so on.
|
||||
* Interface for contract states representing assets which are fungible, countable and issued by a
|
||||
* specific party. States contain assets which are equivalent (such as cash of the same currency),
|
||||
* so records of their existence can be merged or split as needed where the issuer is the same. For
|
||||
* instance, dollars issued by the Fed are fungible and countable (in cents), barrels of West Texas
|
||||
* crude are fungible and countable (oil from two small containers can be poured into one large
|
||||
* container), shares of the same class in a specific company are fungible and countable, and so on.
|
||||
*
|
||||
* See [Cash] for an example subclass that implements currency.
|
||||
* See [Cash] for an example contract that implements currency using state objects that implement
|
||||
* this interface.
|
||||
*
|
||||
* @param T a type that represents the asset in question. This should describe the basic type of the asset
|
||||
* (GBP, USD, oil, shares in company <X>, etc.) and any additional metadata (issuer, grade, class, etc.).
|
||||
*/
|
||||
abstract class FungibleAsset<T> : Contract {
|
||||
/** A state representing a cash claim against some party */
|
||||
interface State<T> : FungibleAssetState<T, Issued<T>> {
|
||||
/** Where the underlying currency backing this ledger entry can be found (propagated) */
|
||||
val deposit: PartyAndReference
|
||||
val amount: Amount<Issued<T>>
|
||||
/** There must be an ExitCommand signed by these keys to destroy the amount */
|
||||
val exitKeys: Collection<PublicKey>
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
override val owner: PublicKey
|
||||
}
|
||||
interface FungibleAsset<T> : OwnableState {
|
||||
/**
|
||||
* Where the underlying asset backing this ledger entry can be found. The reference
|
||||
* is only intended for use by the issuer, and is not intended to be meaningful to others.
|
||||
*/
|
||||
val deposit: PartyAndReference
|
||||
val issuanceDef: Issued<T>
|
||||
val amount: Amount<Issued<T>>
|
||||
/** There must be an ExitCommand signed by these keys to destroy the amount */
|
||||
val exitKeys: Collection<PublicKey>
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
override val owner: PublicKey
|
||||
fun move(newAmount: Amount<Issued<T>>, newOwner: PublicKey): FungibleAsset<T>
|
||||
|
||||
// Just for grouping
|
||||
interface Commands : CommandData {
|
||||
@ -53,92 +50,14 @@ abstract class FungibleAsset<T> : Contract {
|
||||
*/
|
||||
interface Exit<T> : Commands { val amount: Amount<Issued<T>> }
|
||||
}
|
||||
|
||||
/** This is the function EVERYONE runs */
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
// Each group is a set of input/output states with distinct issuance definitions. These assets are not fungible
|
||||
// and must be kept separated for bookkeeping purposes.
|
||||
val groups = tx.groupStates() { it: FungibleAsset.State<T> -> it.issuanceDef }
|
||||
|
||||
for ((inputs, outputs, token) in groups) {
|
||||
// Either inputs or outputs could be empty.
|
||||
val deposit = token.issuer
|
||||
val issuer = deposit.party
|
||||
|
||||
requireThat {
|
||||
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
|
||||
}
|
||||
|
||||
val issueCommand = tx.commands.select<Commands.Issue>().firstOrNull()
|
||||
if (issueCommand != null) {
|
||||
verifyIssueCommand(inputs, outputs, tx, issueCommand, token, issuer)
|
||||
} else {
|
||||
val inputAmount = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for this group")
|
||||
val outputAmount = outputs.sumFungibleOrZero(token)
|
||||
|
||||
// If we want to remove assets from the ledger, that must be signed for by the issuer.
|
||||
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero.
|
||||
val exitCommand = tx.commands.select<Commands.Exit<T>>(party = issuer).singleOrNull()
|
||||
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, token)
|
||||
|
||||
requireThat {
|
||||
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
|
||||
"for deposit ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by
|
||||
(inputAmount == outputAmount + amountExitingLedger)
|
||||
}
|
||||
|
||||
verifyMoveCommand<Commands.Move>(inputs, tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyIssueCommand(inputs: List<State<T>>,
|
||||
outputs: List<State<T>>,
|
||||
tx: TransactionForContract,
|
||||
issueCommand: AuthenticatedObject<Commands.Issue>,
|
||||
token: Issued<T>,
|
||||
issuer: Party) {
|
||||
// If we have an issue command, perform special processing: the group is allowed to have no inputs,
|
||||
// and the output states must have a deposit reference owned by the signer.
|
||||
//
|
||||
// Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must
|
||||
// sum to more than the inputs. An issuance of zero size is not allowed.
|
||||
//
|
||||
// Note that this means literally anyone with access to the network can issue asset claims of arbitrary
|
||||
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some
|
||||
// external mechanism (such as locally defined rules on which parties are trustworthy).
|
||||
|
||||
// The grouping ensures that all outputs have the same deposit reference and token.
|
||||
val inputAmount = inputs.sumFungibleOrZero(token)
|
||||
val outputAmount = outputs.sumFungible<T>()
|
||||
val assetCommands = tx.commands.select<FungibleAsset.Commands>()
|
||||
requireThat {
|
||||
"the issue command has a nonce" by (issueCommand.value.nonce != 0L)
|
||||
"output states are issued by a command signer" by (issuer in issueCommand.signingParties)
|
||||
"output values sum to more than the inputs" by (outputAmount > inputAmount)
|
||||
"there is only a single issue command" by (assetCommands.count() == 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Small DSL extensions.
|
||||
|
||||
/**
|
||||
* Sums the asset states in the list belonging to a single owner, throwing an exception
|
||||
* if there are none, or if any of the asset states cannot be added together (i.e. are
|
||||
* different tokens).
|
||||
*/
|
||||
fun <T> Iterable<ContractState>.sumFungibleBy(owner: PublicKey) = filterIsInstance<FungibleAsset.State<T>>().filter { it.owner == owner }.map { it.amount }.sumOrThrow()
|
||||
|
||||
/**
|
||||
* Sums the asset states in the list, throwing an exception if there are none, or if any of the asset
|
||||
* states cannot be added together (i.e. are different tokens).
|
||||
*/
|
||||
fun <T> Iterable<ContractState>.sumFungible() = filterIsInstance<FungibleAsset.State<T>>().map { it.amount }.sumOrThrow()
|
||||
|
||||
/** Sums the asset states in the list, returning null if there are none. */
|
||||
fun <T> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset.State<T>>().map { it.amount }.sumOrNull()
|
||||
fun <T> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrNull()
|
||||
|
||||
/** Sums the asset states in the list, returning zero of the given token if there are none. */
|
||||
fun <T> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset.State<T>>().map { it.amount }.sumOrZero(token)
|
||||
fun <T> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrZero(token)
|
||||
|
||||
|
@ -1,14 +0,0 @@
|
||||
package com.r3corda.contracts.asset
|
||||
|
||||
import com.r3corda.core.contracts.Amount
|
||||
import com.r3corda.core.contracts.OwnableState
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* Common elements of cash contract states.
|
||||
*/
|
||||
interface FungibleAssetState<T, I> : OwnableState {
|
||||
val issuanceDef: I
|
||||
val productAmount: Amount<T>
|
||||
fun move(newAmount: Amount<T>, newOwner: PublicKey): FungibleAssetState<T, I>
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
package com.r3corda.contracts.asset
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import com.r3corda.contracts.asset.Obligation.Lifecycle.NORMAL
|
||||
import com.r3corda.contracts.clause.*
|
||||
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
|
||||
@ -29,7 +30,7 @@ val OBLIGATION_PROGRAM_ID = Obligation<Currency>()
|
||||
*
|
||||
* @param P the product the obligation is for payment of.
|
||||
*/
|
||||
class Obligation<P> : Contract {
|
||||
class Obligation<P> : ClauseVerifier() {
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
@ -42,6 +43,199 @@ class Obligation<P> : Contract {
|
||||
* 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>()),
|
||||
Clauses.Group<P>())
|
||||
|
||||
interface Clauses {
|
||||
/**
|
||||
* 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 fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<Obligation.State<P>, Issued<Terms<P>>>>
|
||||
= tx.groupStates<Obligation.State<P>, Issued<Terms<P>>> { it.issuanceDef }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic move/exit clause for fungible assets
|
||||
*/
|
||||
class ConserveAmount<P> : AbstractConserveAmount<State<P>, Terms<P>>()
|
||||
|
||||
/**
|
||||
* Clause for supporting netting of obligations.
|
||||
*/
|
||||
class Net<P> : NetClause<P>()
|
||||
|
||||
/**
|
||||
* 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 fun verify(tx: TransactionForContract,
|
||||
inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms<P>>): Set<CommandData> {
|
||||
val command = commands.requireSingleCommand<Commands.SetLifecycle>()
|
||||
Obligation<P>().verifySetLifecycleCommand(inputs, outputs, tx, command)
|
||||
return setOf(command.value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obligation-specific clause for settling an outstanding obligation by witnessing
|
||||
* 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 fun verify(tx: TransactionForContract,
|
||||
inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms<P>>): Set<CommandData> {
|
||||
val command = commands.requireSingleCommand<Commands.Settle<P>>()
|
||||
val obligor = token.issuer.party
|
||||
val template = token.product
|
||||
val inputAmount: Amount<Issued<Terms<P>>> = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
|
||||
val outputAmount: Amount<Issued<Terms<P>>> = outputs.sumObligationsOrZero(token)
|
||||
|
||||
// Sum up all asset state objects that are moving and fulfil our requirements
|
||||
|
||||
// The fungible asset contract verification handles ensuring there's inputs enough to cover the output states,
|
||||
// we only care about counting how much is output in this transaction. We then calculate the difference in
|
||||
// settlement amounts between the transaction inputs and outputs, and the two must match. No elimination is
|
||||
// done of amounts paid in by each beneficiary, as it's presumed the beneficiaries have enough sense to do that
|
||||
// themselves. Therefore if someone actually signed the following transaction (using cash just for an example):
|
||||
//
|
||||
// Inputs:
|
||||
// £1m cash owned by B
|
||||
// £1m owed from A to B
|
||||
// Outputs:
|
||||
// £1m cash owned by B
|
||||
// Commands:
|
||||
// Settle (signed by A)
|
||||
// Move (signed by B)
|
||||
//
|
||||
// That would pass this check. Ensuring they do not is best addressed in the transaction generation stage.
|
||||
val assetStates = tx.outputs.filterIsInstance<FungibleAsset<*>>()
|
||||
val acceptableAssetStates = assetStates
|
||||
// TODO: This filter is nonsense, because it just checks there is an asset contract loaded, we need to
|
||||
// verify the asset contract is the asset contract we expect.
|
||||
// Something like:
|
||||
// attachments.mustHaveOneOf(key.acceptableAssetContract)
|
||||
.filter { it.contract.legalContractReference in template.acceptableContracts }
|
||||
// Restrict the states to those of the correct issuance definition (this normally
|
||||
// covers issued product and obligor, but is opaque to us)
|
||||
.filter { it.issuanceDef in template.acceptableIssuedProducts }
|
||||
// Catch that there's nothing useful here, so we can dump out a useful error
|
||||
requireThat {
|
||||
"there are fungible asset state outputs" by (assetStates.size > 0)
|
||||
"there are defined acceptable fungible asset states" by (acceptableAssetStates.size > 0)
|
||||
}
|
||||
|
||||
val amountReceivedByOwner = acceptableAssetStates.groupBy { it.owner }
|
||||
// Note we really do want to search all commands, because we want move commands of other contracts, not just
|
||||
// this one.
|
||||
val moveCommands = tx.commands.select<MoveCommand>()
|
||||
var totalPenniesSettled = 0L
|
||||
val requiredSigners = inputs.map { it.deposit.party.owningKey }.toSet()
|
||||
|
||||
for ((beneficiary, obligations) in inputs.groupBy { it.owner }) {
|
||||
val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull<P>()
|
||||
if (settled != null) {
|
||||
val debt = obligations.sumObligationsOrZero(token)
|
||||
require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" }
|
||||
totalPenniesSettled += settled.quantity
|
||||
}
|
||||
}
|
||||
|
||||
// Insist that we can be the only contract consuming inputs, to ensure no other contract can think it's being
|
||||
// settled as well
|
||||
requireThat {
|
||||
"all move commands relate to this contract" by (moveCommands.map { it.value.contractHash }
|
||||
.all { it == null || it == Obligation<P>().legalContractReference })
|
||||
// Settle commands exclude all other commands, so we don't need to check for contracts moving at the same
|
||||
// time.
|
||||
"amounts paid must match recipients to settle" by inputs.map { it.owner }.containsAll(amountReceivedByOwner.keys)
|
||||
"signatures are present from all obligors" by command.signers.containsAll(requiredSigners)
|
||||
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
|
||||
"at obligor ${obligor.name} the obligations after settlement balance" by
|
||||
(inputAmount == outputAmount + Amount(totalPenniesSettled, token))
|
||||
}
|
||||
return setOf(command.value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obligation-specific clause for verifying that all states are in
|
||||
* normal lifecycle. In a group clause set, this must be run after
|
||||
* any lifecycle change clause, which is the only clause that involve
|
||||
* 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 fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
|
||||
= verify(
|
||||
tx.inputs.filterIsInstance<State<P>>(),
|
||||
tx.outputs.filterIsInstance<State<P>>()
|
||||
)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms<P>>): Set<CommandData>
|
||||
= verify(inputs, outputs)
|
||||
|
||||
fun verify(inputs: List<State<P>>,
|
||||
outputs: List<State<P>>): Set<CommandData> {
|
||||
requireThat {
|
||||
"all inputs are in the normal state " by inputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||
"all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||
}
|
||||
return emptySet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents where in its lifecycle a contract state is, which in turn controls the commands that can be applied
|
||||
@ -58,42 +252,12 @@ class Obligation<P> : Contract {
|
||||
DEFAULTED
|
||||
}
|
||||
|
||||
/**
|
||||
* Common interface for the state subsets used when determining nettability of two or more states. Exposes the
|
||||
* underlying issued thing.
|
||||
*/
|
||||
interface NetState<P> {
|
||||
val template: StateTemplate<P>
|
||||
}
|
||||
|
||||
/**
|
||||
* Subset of state, containing the elements which must match for two obligation transactions to be nettable.
|
||||
* If two obligation state objects produce equal bilateral net states, they are considered safe to net directly.
|
||||
* Bilateral states are used in close-out netting.
|
||||
*/
|
||||
data class BilateralNetState<P>(
|
||||
val partyKeys: Set<PublicKey>,
|
||||
override val template: StateTemplate<P>
|
||||
) : NetState<P>
|
||||
|
||||
/**
|
||||
* Subset of state, containing the elements which must match for two or more obligation transactions to be candidates
|
||||
* for netting (this does not include the checks to enforce that everyone's amounts received are the same at the end,
|
||||
* which is handled under the verify() function).
|
||||
* In comparison to [BilateralNetState], this doesn't include the parties' keys, as ensuring balances match on
|
||||
* input and output is handled elsewhere.
|
||||
* Used in cases where all parties (or their proxies) are signing, such as central clearing.
|
||||
*/
|
||||
data class MultilateralNetState<P>(
|
||||
override val template: StateTemplate<P>
|
||||
) : NetState<P>
|
||||
|
||||
/**
|
||||
* Subset of state, containing the elements specified when issuing a new settlement contract.
|
||||
*
|
||||
* @param P the product the obligation is for payment of.
|
||||
*/
|
||||
data class StateTemplate<P>(
|
||||
data class Terms<P>(
|
||||
/** The hash of the asset contract we're willing to accept in payment for this debt. */
|
||||
val acceptableContracts: NonEmptySet<SecureHash>,
|
||||
/** The parties whose assets we are willing to accept in payment for this debt. */
|
||||
@ -107,21 +271,9 @@ class Obligation<P> : Contract {
|
||||
get() = acceptableIssuedProducts.map { it.product }.toSet().single()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subset of state, containing the elements specified when issuing a new settlement contract.
|
||||
* TODO: This needs to be something common to contracts that we can be obliged to pay, and moved
|
||||
* out into core accordingly.
|
||||
*
|
||||
* @param P the product the obligation is for payment of.
|
||||
*/
|
||||
data class IssuanceDefinition<P>(
|
||||
val obligor: Party,
|
||||
val template: StateTemplate<P>
|
||||
)
|
||||
|
||||
/**
|
||||
* A state representing the obligation of one party (obligor) to deliver a specified number of
|
||||
* units of an underlying asset (described as issuanceDef.acceptableIssuedProducts) to the beneficiary
|
||||
* units of an underlying asset (described as token.acceptableIssuedProducts) to the beneficiary
|
||||
* no later than the specified time.
|
||||
*
|
||||
* @param P the product the obligation is for payment of.
|
||||
@ -130,32 +282,28 @@ class Obligation<P> : Contract {
|
||||
var lifecycle: Lifecycle = Lifecycle.NORMAL,
|
||||
/** Where the debt originates from (obligor) */
|
||||
val obligor: Party,
|
||||
val template: StateTemplate<P>,
|
||||
val template: Terms<P>,
|
||||
val quantity: Long,
|
||||
/** The public key of the entity the contract pays to */
|
||||
val beneficiary: PublicKey
|
||||
) : FungibleAssetState<P, IssuanceDefinition<P>>, BilateralNettableState<State<P>> {
|
||||
val amount: Amount<P>
|
||||
get() = Amount(quantity, template.product)
|
||||
val aggregateState: IssuanceDefinition<P>
|
||||
get() = issuanceDef
|
||||
override val productAmount: Amount<P>
|
||||
get() = amount
|
||||
) : FungibleAsset<Obligation.Terms<P>>, NettableState<State<P>, MultilateralNetState<P>> {
|
||||
override val amount: Amount<Issued<Terms<P>>>
|
||||
get() = Amount(quantity, issuanceDef)
|
||||
override val contract = OBLIGATION_PROGRAM_ID
|
||||
val acceptableContracts: NonEmptySet<SecureHash>
|
||||
get() = template.acceptableContracts
|
||||
val acceptableIssuanceDefinitions: NonEmptySet<*>
|
||||
get() = template.acceptableIssuedProducts
|
||||
override val deposit: PartyAndReference
|
||||
get() = amount.token.issuer
|
||||
override val exitKeys: Collection<PublicKey>
|
||||
get() = setOf(owner)
|
||||
val dueBefore: Instant
|
||||
get() = template.dueBefore
|
||||
override val issuanceDef: IssuanceDefinition<P>
|
||||
get() = IssuanceDefinition(obligor, template)
|
||||
override val issuanceDef: Issued<Terms<P>>
|
||||
get() = Issued(obligor.ref(0), template)
|
||||
override val participants: List<PublicKey>
|
||||
get() = listOf(obligor.owningKey, beneficiary)
|
||||
override val owner: PublicKey
|
||||
get() = beneficiary
|
||||
|
||||
override fun move(newAmount: Amount<P>, newOwner: PublicKey): State<P>
|
||||
override fun move(newAmount: Amount<Issued<Terms<P>>>, newOwner: PublicKey): State<P>
|
||||
= copy(quantity = newAmount.quantity, beneficiary = newOwner)
|
||||
|
||||
override fun toString() = when (lifecycle) {
|
||||
@ -168,7 +316,7 @@ class Obligation<P> : Contract {
|
||||
check(lifecycle == Lifecycle.NORMAL)
|
||||
return BilateralNetState(setOf(obligor.owningKey, beneficiary), template)
|
||||
}
|
||||
val multilateralNetState: MultilateralNetState<P>
|
||||
override val multilateralNetState: MultilateralNetState<P>
|
||||
get() {
|
||||
check(lifecycle == Lifecycle.NORMAL)
|
||||
return MultilateralNetState(template)
|
||||
@ -188,21 +336,16 @@ class Obligation<P> : Contract {
|
||||
}
|
||||
}
|
||||
|
||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(issuanceDef), copy(beneficiary = newOwner))
|
||||
}
|
||||
|
||||
/** Interface for commands that apply to states grouped by issuance definition */
|
||||
interface IssuanceCommands<P> : CommandData {
|
||||
val aggregateState: IssuanceDefinition<P>
|
||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(beneficiary = newOwner))
|
||||
}
|
||||
|
||||
// Just for grouping
|
||||
interface Commands : CommandData {
|
||||
interface Commands : FungibleAsset.Commands {
|
||||
/**
|
||||
* Net two or more obligation states together in a close-out netting style. Limited to bilateral netting
|
||||
* as only the beneficiary (not the obligor) needs to sign.
|
||||
*/
|
||||
data class Net(val type: NetType) : Commands
|
||||
data class Net(val type: NetType) : Obligation.Commands
|
||||
|
||||
/**
|
||||
* A command stating that a debt has been moved, optionally to fulfil another contract.
|
||||
@ -211,30 +354,26 @@ class Obligation<P> : Contract {
|
||||
* should take the moved states into account when considering whether it is valid. Typically this will be
|
||||
* null.
|
||||
*/
|
||||
data class Move<P>(override val aggregateState: IssuanceDefinition<P>,
|
||||
override val contractHash: SecureHash? = null) : Commands, IssuanceCommands<P>, MoveCommand
|
||||
data class Move(override val contractHash: SecureHash? = null) : Commands, FungibleAsset.Commands.Move
|
||||
|
||||
/**
|
||||
* Allows new obligation states to be issued into existence: the nonce ("number used once") ensures the
|
||||
* transaction has a unique ID even when there are no inputs.
|
||||
*/
|
||||
data class Issue<P>(override val aggregateState: IssuanceDefinition<P>,
|
||||
val nonce: Long = random63BitValue()) : Commands, IssuanceCommands<P>
|
||||
data class Issue(override val nonce: Long = random63BitValue()) : FungibleAsset.Commands.Issue, Commands
|
||||
|
||||
/**
|
||||
* A command stating that the obligor is settling some or all of the amount owed by transferring a suitable
|
||||
* state object to the beneficiary. If this reduces the balance to zero, the state object is destroyed.
|
||||
* @see [MoveCommand].
|
||||
*/
|
||||
data class Settle<P>(override val aggregateState: IssuanceDefinition<P>,
|
||||
val amount: Amount<P>) : Commands, IssuanceCommands<P>
|
||||
data class Settle<P>(val amount: Amount<Issued<Terms<P>>>) : Commands
|
||||
|
||||
/**
|
||||
* A command stating that the beneficiary is moving the contract into the defaulted state as it has not been settled
|
||||
* by the due date, or resetting a defaulted contract back to the issued state.
|
||||
*/
|
||||
data class SetLifecycle<P>(override val aggregateState: IssuanceDefinition<P>,
|
||||
val lifecycle: Lifecycle) : Commands, IssuanceCommands<P> {
|
||||
data class SetLifecycle(val lifecycle: Lifecycle) : Commands {
|
||||
val inverse: Lifecycle
|
||||
get() = when (lifecycle) {
|
||||
Lifecycle.NORMAL -> Lifecycle.DEFAULTED
|
||||
@ -246,140 +385,20 @@ class Obligation<P> : Contract {
|
||||
* A command stating that the debt is being released by the beneficiary. Normally would indicate
|
||||
* either settlement outside of the ledger, or that the obligor is unable to pay.
|
||||
*/
|
||||
data class Exit<P>(override val aggregateState: IssuanceDefinition<P>,
|
||||
val amount: Amount<P>) : Commands, IssuanceCommands<P>
|
||||
data class Exit<P>(override val amount: Amount<Issued<Terms<P>>>) : Commands, FungibleAsset.Commands.Exit<Terms<P>>
|
||||
}
|
||||
|
||||
/** This is the function EVERYONE runs */
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
val commands = tx.commands.select<Commands>()
|
||||
|
||||
// Net commands are special, and cross issuance definitions, so handle them first
|
||||
val netCommands = commands.select<Commands.Net>()
|
||||
if (netCommands.isNotEmpty()) {
|
||||
val netCommand = netCommands.single()
|
||||
val groups = when (netCommand.value.type) {
|
||||
NetType.CLOSE_OUT -> tx.groupStates { it: State<P> -> it.bilateralNetState }
|
||||
NetType.PAYMENT -> tx.groupStates { it: State<P> -> it.multilateralNetState }
|
||||
}
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
verifyNetCommand(inputs, outputs, netCommand, key)
|
||||
}
|
||||
} else {
|
||||
val commandGroups = tx.groupCommands<IssuanceCommands<P>, IssuanceDefinition<P>> { it.value.aggregateState }
|
||||
// Each group is a set of input/output states with distinct issuance definitions. These types
|
||||
// of settlement are not fungible and must be kept separated for bookkeeping purposes.
|
||||
val groups = tx.groupStates() { it: State<P> -> it.aggregateState }
|
||||
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
// Either inputs or outputs could be empty.
|
||||
val obligor = key.obligor
|
||||
|
||||
requireThat {
|
||||
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
|
||||
}
|
||||
|
||||
verifyCommandGroup(tx, commandGroups[key] ?: emptyList(), inputs, outputs, obligor, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyCommandGroup(tx: TransactionForContract,
|
||||
commands: List<AuthenticatedObject<IssuanceCommands<P>>>,
|
||||
inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
obligor: Party,
|
||||
key: IssuanceDefinition<P>) {
|
||||
// We've already pre-grouped by product amongst other fields, and verified above that every state specifies
|
||||
// at least one acceptable issuance definition, so we can just use the first issuance definition to
|
||||
// determine product
|
||||
val issued = key.template.acceptableIssuedProducts.first()
|
||||
|
||||
// Issue, default, net and settle commands are all single commands (there's only ever one of them, and
|
||||
// they exclude all other commands).
|
||||
val issueCommand = commands.select<Commands.Issue<P>>().firstOrNull()
|
||||
val setLifecycleCommand = commands.select<Commands.SetLifecycle<P>>().firstOrNull()
|
||||
val settleCommand = commands.select<Commands.Settle<P>>().firstOrNull()
|
||||
|
||||
if (commands.size != 1) {
|
||||
// Only commands can be move/exit
|
||||
require(commands.map { it.value }.all { it is Commands.Move || it is Commands.Exit })
|
||||
{ "only move/exit commands can be present along with other obligation commands" }
|
||||
}
|
||||
|
||||
// Issue, default and net commands are special, and do not follow normal input/output summing rules, so
|
||||
// deal with them first
|
||||
if (setLifecycleCommand != null) {
|
||||
verifySetLifecycleCommand(inputs, outputs, tx, setLifecycleCommand)
|
||||
} else {
|
||||
// Only the default command processes inputs/outputs that are not in the normal state
|
||||
// TODO: Need to be able to exit defaulted amounts
|
||||
requireThat {
|
||||
"all inputs are in the normal state " by inputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||
"all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||
}
|
||||
if (issueCommand != null) {
|
||||
verifyIssueCommand(inputs, outputs, issueCommand, issued, obligor)
|
||||
} else if (settleCommand != null) {
|
||||
// Perhaps through an abundance of caution, settlement is enforced as its own command.
|
||||
// This could perhaps be merged into verifyBalanceChange() later, however doing so introduces a lot
|
||||
// of scope for making it more opaque what's going on in a transaction and whether it's as expected
|
||||
// by all parties.
|
||||
verifySettleCommand(inputs, outputs, tx, settleCommand, issued, obligor, key)
|
||||
} else {
|
||||
verifyBalanceChange(inputs, outputs, commands, issued.product, obligor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify simple lifecycle changes for settlement contracts, handling exit and move commands.
|
||||
*
|
||||
* @param commands a list of commands filtered to those matching issuance definition for the provided inputs and
|
||||
* outputs.
|
||||
*/
|
||||
private fun verifyBalanceChange(inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
commands: List<AuthenticatedObject<IssuanceCommands<P>>>,
|
||||
product: P,
|
||||
obligor: Party) {
|
||||
// Sum up how much settlement owed there is in the inputs, and the difference in outputs. The difference should
|
||||
// be matched by exit commands representing the extracted amount.
|
||||
|
||||
val inputAmount = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
|
||||
val outputAmount = outputs.sumObligationsOrZero(product)
|
||||
|
||||
val exitCommands = commands.select<Commands.Exit<P>>()
|
||||
val requiredExitSignatures = HashSet<PublicKey>()
|
||||
val amountExitingLedger: Amount<P> = if (exitCommands.isNotEmpty()) {
|
||||
require(exitCommands.size == 1) { "There can only be one exit command" }
|
||||
val exitCommand = exitCommands.single()
|
||||
// If we want to remove debt from the ledger, that must be signed for by the beneficiary. For now we require exit
|
||||
// commands to be signed by all input beneficiarys, unlocking the full input amount, rather than trying to detangle
|
||||
// exactly who exited what.
|
||||
requiredExitSignatures.addAll(inputs.map { it.beneficiary })
|
||||
exitCommand.value.amount
|
||||
} else {
|
||||
Amount(0, product)
|
||||
}
|
||||
|
||||
requireThat {
|
||||
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
|
||||
"at obligor ${obligor.name} the amounts balance" by
|
||||
(inputAmount == outputAmount + amountExitingLedger)
|
||||
}
|
||||
|
||||
verifyMoveCommand<Commands.Move<P>>(inputs, commands)
|
||||
}
|
||||
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
|
||||
= tx.commands.select<Obligation.Commands>()
|
||||
|
||||
/**
|
||||
* A default command mutates inputs and produces identical outputs, except that the lifecycle changes.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected fun verifySetLifecycleCommand(inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
protected fun verifySetLifecycleCommand(inputs: List<FungibleAsset<Terms<P>>>,
|
||||
outputs: List<FungibleAsset<Terms<P>>>,
|
||||
tx: TransactionForContract,
|
||||
setLifecycleCommand: AuthenticatedObject<Commands.SetLifecycle<P>>) {
|
||||
setLifecycleCommand: AuthenticatedObject<Commands.SetLifecycle>) {
|
||||
// Default must not change anything except lifecycle, so number of inputs and outputs must match
|
||||
// exactly.
|
||||
require(inputs.size == outputs.size) { "Number of inputs and outputs must match" }
|
||||
@ -391,162 +410,27 @@ class Obligation<P> : Contract {
|
||||
|
||||
// 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()) {
|
||||
val actualOutput = outputs[stateIdx]
|
||||
val deadline = input.dueBefore
|
||||
val timestamp: TimestampCommand? = tx.timestamp
|
||||
val expectedOutput: State<P> = input.copy(lifecycle = expectedOutputLifecycle)
|
||||
if (input is State<P>) {
|
||||
val actualOutput = outputs[stateIdx]
|
||||
val deadline = input.dueBefore
|
||||
val timestamp: TimestampCommand? = tx.timestamp
|
||||
val expectedOutput: State<P> = input.copy(lifecycle = expectedOutputLifecycle)
|
||||
|
||||
requireThat {
|
||||
"there is a timestamp from the authority" by (timestamp != null)
|
||||
"the due date has passed" by (timestamp!!.after?.isAfter(deadline) ?: false)
|
||||
"input state lifecycle is correct" by (input.lifecycle == expectedInputLifecycle)
|
||||
"output state corresponds exactly to input state, with lifecycle changed" by (expectedOutput == actualOutput)
|
||||
requireThat {
|
||||
"there is a timestamp from the authority" by (timestamp != null)
|
||||
"the due date has passed" by (timestamp!!.after?.isAfter(deadline) ?: false)
|
||||
"input state lifecycle is correct" by (input.lifecycle == expectedInputLifecycle)
|
||||
"output state corresponds exactly to input state, with lifecycle changed" by (expectedOutput == actualOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
val owningPubKeys = inputs.map { it.beneficiary }.toSet()
|
||||
val owningPubKeys = inputs.filter { it is State<P> }.map { (it as State<P>).beneficiary }.toSet()
|
||||
val keysThatSigned = setLifecycleCommand.signers.toSet()
|
||||
requireThat {
|
||||
"the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected fun verifyIssueCommand(inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
issueCommand: AuthenticatedObject<Commands.Issue<P>>,
|
||||
issued: Issued<P>,
|
||||
obligor: Party) {
|
||||
// If we have an issue command, perform special processing: the group is must have no inputs,
|
||||
// and that signatures are present for all obligors.
|
||||
|
||||
val inputAmount: Amount<P> = inputs.sumObligationsOrZero(issued.product)
|
||||
val outputAmount: Amount<P> = outputs.sumObligations<P>()
|
||||
requireThat {
|
||||
"the issue command has a nonce" by (issueCommand.value.nonce != 0L)
|
||||
"output states are issued by a command signer" by (obligor in issueCommand.signingParties)
|
||||
"output values sum to more than the inputs" by (outputAmount > inputAmount)
|
||||
"valid settlement issuance definition is not this issuance definition" by inputs.none { it.issuanceDef in it.acceptableIssuanceDefinitions }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a netting command. This handles both close-out and payment netting.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected fun verifyNetCommand(inputs: Iterable<State<P>>,
|
||||
outputs: Iterable<State<P>>,
|
||||
command: AuthenticatedObject<Commands.Net>,
|
||||
netState: NetState<P>) {
|
||||
// TODO: Can we merge this with the checks for aggregated commands?
|
||||
requireThat {
|
||||
"all inputs are in the normal state " by inputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||
"all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||
}
|
||||
|
||||
val template = netState.template
|
||||
val product = template.product
|
||||
// Create two maps of balances from obligors to beneficiaries, one for input states, the other for output states.
|
||||
val inputBalances = extractAmountsDue(product, inputs)
|
||||
val outputBalances = extractAmountsDue(product, outputs)
|
||||
|
||||
// Sum the columns of the matrices. This will yield the net amount payable to/from each party to/from all other participants.
|
||||
// The two summaries must match, reflecting that the amounts owed match on both input and output.
|
||||
requireThat {
|
||||
"all input states use the same template" by (inputs.all { it.template == template })
|
||||
"all output states use the same template" by (outputs.all { it.template == template })
|
||||
"amounts owed on input and output must match" by (sumAmountsDue(inputBalances) == sumAmountsDue(outputBalances))
|
||||
}
|
||||
|
||||
// TODO: Handle proxies nominated by parties, i.e. a central clearing service
|
||||
val involvedParties = inputs.map { it.beneficiary }.union(inputs.map { it.obligor.owningKey }).toSet()
|
||||
when (command.value.type) {
|
||||
// For close-out netting, allow any involved party to sign
|
||||
NetType.CLOSE_OUT -> require(command.signers.intersect(involvedParties).isNotEmpty()) { "any involved party has signed" }
|
||||
// Require signatures from all parties (this constraint can be changed for other contracts, and is used as a
|
||||
// placeholder while exact requirements are established), or fail the transaction.
|
||||
NetType.PAYMENT -> require(command.signers.containsAll(involvedParties)) { "all involved parties have signed" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify settlement of state objects.
|
||||
*/
|
||||
private fun verifySettleCommand(inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
tx: TransactionForContract,
|
||||
command: AuthenticatedObject<Commands.Settle<P>>,
|
||||
issued: Issued<P>,
|
||||
obligor: Party,
|
||||
key: IssuanceDefinition<P>) {
|
||||
val template = key.template
|
||||
val inputAmount: Amount<P> = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
|
||||
val outputAmount: Amount<P> = outputs.sumObligationsOrZero(issued.product)
|
||||
|
||||
// Sum up all asset state objects that are moving and fulfil our requirements
|
||||
|
||||
// The fungible asset contract verification handles ensuring there's inputs enough to cover the output states,
|
||||
// we only care about counting how much is output in this transaction. We then calculate the difference in
|
||||
// settlement amounts between the transaction inputs and outputs, and the two must match. No elimination is
|
||||
// done of amounts paid in by each beneficiary, as it's presumed the beneficiaries have enough sense to do that
|
||||
// themselves. Therefore if someone actually signed the following transaction (using cash just for an example):
|
||||
//
|
||||
// Inputs:
|
||||
// £1m cash owned by B
|
||||
// £1m owed from A to B
|
||||
// Outputs:
|
||||
// £1m cash owned by B
|
||||
// Commands:
|
||||
// Settle (signed by A)
|
||||
// Move (signed by B)
|
||||
//
|
||||
// That would pass this check. Ensuring they do not is best addressed in the transaction generation stage.
|
||||
val assetStates = tx.outputs.filterIsInstance<FungibleAssetState<*, *>>()
|
||||
val acceptableAssetStates = assetStates
|
||||
// TODO: This filter is nonsense, because it just checks there is an asset contract loaded, we need to
|
||||
// verify the asset contract is the asset contract we expect.
|
||||
// Something like:
|
||||
// attachments.mustHaveOneOf(key.acceptableAssetContract)
|
||||
.filter { it.contract.legalContractReference in template.acceptableContracts }
|
||||
// Restrict the states to those of the correct issuance definition (this normally
|
||||
// covers issued product and obligor, but is opaque to us)
|
||||
.filter { it.issuanceDef in template.acceptableIssuedProducts }
|
||||
// Catch that there's nothing useful here, so we can dump out a useful error
|
||||
requireThat {
|
||||
"there are fungible asset state outputs" by (assetStates.size > 0)
|
||||
"there are defined acceptable fungible asset states" by (acceptableAssetStates.size > 0)
|
||||
}
|
||||
|
||||
val amountReceivedByOwner = acceptableAssetStates.groupBy { it.owner }
|
||||
// Note we really do want to search all commands, because we want move commands of other contracts, not just
|
||||
// this one.
|
||||
val moveCommands = tx.commands.select<MoveCommand>()
|
||||
var totalPenniesSettled = 0L
|
||||
val requiredSigners = inputs.map { it.obligor.owningKey }.toSet()
|
||||
|
||||
for ((beneficiary, obligations) in inputs.groupBy { it.beneficiary }) {
|
||||
val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull<P>()
|
||||
if (settled != null) {
|
||||
val debt = obligations.sumObligationsOrZero(issued)
|
||||
require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" }
|
||||
totalPenniesSettled += settled.quantity
|
||||
}
|
||||
}
|
||||
|
||||
// Insist that we can be the only contract consuming inputs, to ensure no other contract can think it's being
|
||||
// settled as well
|
||||
requireThat {
|
||||
"all move commands relate to this contract" by (moveCommands.map { it.value.contractHash }
|
||||
.all { it == null || it == legalContractReference })
|
||||
"contract does not try to consume itself" by (moveCommands.map { it.value }.filterIsInstance<Commands.Move<P>>()
|
||||
.none { it.aggregateState == key })
|
||||
"amounts paid must match recipients to settle" by inputs.map { it.beneficiary }.containsAll(amountReceivedByOwner.keys)
|
||||
"signatures are present from all obligors" by command.signers.containsAll(requiredSigners)
|
||||
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
|
||||
"at obligor ${obligor.name} the obligations after settlement balance" by
|
||||
(inputAmount == outputAmount + Amount(totalPenniesSettled, issued.product))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a transaction performing close-out netting of two or more states.
|
||||
*
|
||||
@ -577,19 +461,18 @@ class Obligation<P> : Contract {
|
||||
*/
|
||||
fun generateIssue(tx: TransactionBuilder,
|
||||
obligor: Party,
|
||||
issuanceDef: StateTemplate<P>,
|
||||
issuanceDef: Terms<P>,
|
||||
pennies: Long,
|
||||
beneficiary: PublicKey,
|
||||
notary: Party) {
|
||||
check(tx.inputStates().isEmpty())
|
||||
check(tx.outputStates().map { it.data }.sumObligationsOrNull<P>() == null)
|
||||
val aggregateState = IssuanceDefinition(obligor, issuanceDef)
|
||||
tx.addOutputState(State(Lifecycle.NORMAL, obligor, issuanceDef, pennies, beneficiary), notary)
|
||||
tx.addCommand(Commands.Issue(aggregateState), obligor.owningKey)
|
||||
tx.addCommand(Commands.Issue(), obligor.owningKey)
|
||||
}
|
||||
|
||||
fun generatePaymentNetting(tx: TransactionBuilder,
|
||||
issued: Issued<P>,
|
||||
issued: Issued<Obligation.Terms<P>>,
|
||||
notary: Party,
|
||||
vararg states: State<P>) {
|
||||
requireThat {
|
||||
@ -647,7 +530,7 @@ class Obligation<P> : Contract {
|
||||
tx.addOutputState(outState, notary)
|
||||
partiesUsed.add(stateAndRef.state.data.beneficiary)
|
||||
}
|
||||
tx.addCommand(Commands.SetLifecycle(aggregateState, lifecycle), partiesUsed.distinct())
|
||||
tx.addCommand(Commands.SetLifecycle(lifecycle), partiesUsed.distinct())
|
||||
}
|
||||
tx.setTime(issuanceDef.dueBefore, notary, issuanceDef.timeTolerance)
|
||||
}
|
||||
@ -662,7 +545,7 @@ class Obligation<P> : Contract {
|
||||
*/
|
||||
fun generateSettle(tx: TransactionBuilder,
|
||||
statesAndRefs: Iterable<StateAndRef<State<P>>>,
|
||||
assetStatesAndRefs: Iterable<StateAndRef<FungibleAssetState<P, *>>>,
|
||||
assetStatesAndRefs: Iterable<StateAndRef<FungibleAsset<P>>>,
|
||||
moveCommand: MoveCommand,
|
||||
notary: Party) {
|
||||
val states = statesAndRefs.map { it.state }
|
||||
@ -682,28 +565,31 @@ class Obligation<P> : Contract {
|
||||
// on each side together
|
||||
|
||||
val issuanceDef = getIssuanceDefinitionOrThrow(statesAndRefs.map { it.state.data })
|
||||
val template = issuanceDef.template
|
||||
val obligationTotal: Amount<P> = states.map { it.data }.sumObligations<P>()
|
||||
val template: Terms<P> = issuanceDef.product
|
||||
val obligationTotal: Amount<P> = Amount(states.map { it.data }.sumObligations<P>().quantity, template.product)
|
||||
var obligationRemaining: Amount<P> = obligationTotal
|
||||
val assetSigners = HashSet<PublicKey>()
|
||||
|
||||
statesAndRefs.forEach { tx.addInputState(it) }
|
||||
|
||||
// Move the assets to the new beneficiary
|
||||
assetStatesAndRefs.forEach {
|
||||
assetStatesAndRefs.forEach { ref ->
|
||||
if (obligationRemaining.quantity > 0L) {
|
||||
val assetState = it.state
|
||||
tx.addInputState(it)
|
||||
if (obligationRemaining >= assetState.data.productAmount) {
|
||||
tx.addOutputState(assetState.data.move(assetState.data.productAmount, obligationOwner), notary)
|
||||
obligationRemaining -= assetState.data.productAmount
|
||||
tx.addInputState(ref)
|
||||
|
||||
val assetState = ref.state.data
|
||||
val amount: Amount<P> = Amount(assetState.amount.quantity, assetState.amount.token.product)
|
||||
if (obligationRemaining >= amount) {
|
||||
tx.addOutputState(assetState.move(assetState.amount, obligationOwner), notary)
|
||||
obligationRemaining -= amount
|
||||
} else {
|
||||
val change = Amount(obligationRemaining.quantity, assetState.amount.token)
|
||||
// Split the state in two, sending the change back to the previous beneficiary
|
||||
tx.addOutputState(assetState.data.move(obligationRemaining, obligationOwner), notary)
|
||||
tx.addOutputState(assetState.data.move(assetState.data.productAmount - obligationRemaining, assetState.data.owner), notary)
|
||||
tx.addOutputState(assetState.move(change, obligationOwner), notary)
|
||||
tx.addOutputState(assetState.move(assetState.amount - change, assetState.owner), notary)
|
||||
obligationRemaining -= Amount(0L, obligationRemaining.token)
|
||||
}
|
||||
assetSigners.add(assetState.data.owner)
|
||||
assetSigners.add(assetState.owner)
|
||||
}
|
||||
}
|
||||
|
||||
@ -716,15 +602,15 @@ class Obligation<P> : Contract {
|
||||
|
||||
// Add the asset move command and obligation settle
|
||||
tx.addCommand(moveCommand, assetSigners.toList())
|
||||
tx.addCommand(Commands.Settle(issuanceDef, obligationTotal - obligationRemaining), obligationOwner)
|
||||
tx.addCommand(Commands.Settle(Amount((obligationTotal - obligationRemaining).quantity, issuanceDef)), obligationIssuer.owningKey)
|
||||
}
|
||||
|
||||
/** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */
|
||||
private fun getIssuanceDefinitionOrThrow(states: Iterable<State<P>>): IssuanceDefinition<P> =
|
||||
private fun getIssuanceDefinitionOrThrow(states: Iterable<State<P>>): Issued<Terms<P>> =
|
||||
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>>): StateTemplate<P> =
|
||||
private fun getTemplateOrThrow(states: Iterable<State<P>>): Terms<P> =
|
||||
states.map { it.template }.distinct().single()
|
||||
}
|
||||
|
||||
@ -734,13 +620,13 @@ class Obligation<P> : Contract {
|
||||
*
|
||||
* @return a map of obligor/beneficiary pairs to the balance due.
|
||||
*/
|
||||
fun <P> extractAmountsDue(product: P, states: Iterable<Obligation.State<P>>): Map<Pair<PublicKey, PublicKey>, Amount<P>> {
|
||||
val balances = HashMap<Pair<PublicKey, PublicKey>, Amount<P>>()
|
||||
fun <P> extractAmountsDue(product: Obligation.Terms<P>, states: Iterable<Obligation.State<P>>): Map<Pair<PublicKey, PublicKey>, Amount<Obligation.Terms<P>>> {
|
||||
val balances = HashMap<Pair<PublicKey, PublicKey>, Amount<Obligation.Terms<P>>>()
|
||||
|
||||
states.forEach { state ->
|
||||
val key = Pair(state.obligor.owningKey, state.beneficiary)
|
||||
val balance = balances[key] ?: Amount(0L, product)
|
||||
balances[key] = balance + state.productAmount
|
||||
balances[key] = balance + Amount(state.amount.quantity, state.amount.token.product)
|
||||
}
|
||||
|
||||
return balances
|
||||
@ -804,19 +690,18 @@ fun <P> sumAmountsDue(balances: Map<Pair<PublicKey, PublicKey>, Amount<P>>): Map
|
||||
}
|
||||
|
||||
/** Sums the obligation states in the list, throwing an exception if there are none. All state objects in the list are presumed to be nettable. */
|
||||
fun <P> Iterable<ContractState>.sumObligations(): Amount<P>
|
||||
fun <P> Iterable<ContractState>.sumObligations(): Amount<Issued<Obligation.Terms<P>>>
|
||||
= filterIsInstance<Obligation.State<P>>().map { it.amount }.sumOrThrow()
|
||||
|
||||
/** Sums the obligation states in the list, returning null if there are none. */
|
||||
fun <P> Iterable<ContractState>.sumObligationsOrNull(): Amount<P>?
|
||||
fun <P> Iterable<ContractState>.sumObligationsOrNull(): Amount<Issued<Obligation.Terms<P>>>?
|
||||
= filterIsInstance<Obligation.State<P>>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrNull()
|
||||
|
||||
/** Sums the obligation states in the list, returning zero of the given product if there are none. */
|
||||
fun <P> Iterable<ContractState>.sumObligationsOrZero(product: P): Amount<P>
|
||||
= filterIsInstance<Obligation.State<P>>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(product)
|
||||
fun <P> Iterable<ContractState>.sumObligationsOrZero(issuanceDef: Issued<Obligation.Terms<P>>): Amount<Issued<Obligation.Terms<P>>>
|
||||
= filterIsInstance<Obligation.State<P>>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(issuanceDef)
|
||||
|
||||
infix fun <T> Obligation.State<T>.at(dueBefore: Instant) = copy(template = template.copy(dueBefore = dueBefore))
|
||||
infix fun <T> Obligation.IssuanceDefinition<T>.at(dueBefore: Instant) = copy(template = template.copy(dueBefore = dueBefore))
|
||||
infix fun <T> Obligation.State<T>.between(parties: Pair<Party, PublicKey>) = copy(obligor = parties.first, beneficiary = parties.second)
|
||||
infix fun <T> Obligation.State<T>.`owned by`(owner: PublicKey) = copy(beneficiary = owner)
|
||||
infix fun <T> Obligation.State<T>.`issued by`(party: Party) = copy(obligor = party)
|
||||
@ -824,7 +709,7 @@ infix fun <T> Obligation.State<T>.`issued by`(party: Party) = copy(obligor = par
|
||||
fun <T> Obligation.State<T>.ownedBy(owner: PublicKey) = copy(beneficiary = owner)
|
||||
fun <T> Obligation.State<T>.issuedBy(party: Party) = copy(obligor = party)
|
||||
|
||||
val Issued<Currency>.OBLIGATION_DEF: Obligation.StateTemplate<Currency>
|
||||
get() = Obligation.StateTemplate(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME)
|
||||
val Issued<Currency>.OBLIGATION_DEF: Obligation.Terms<Currency>
|
||||
get() = Obligation.Terms(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME)
|
||||
val Amount<Issued<Currency>>.OBLIGATION: Obligation.State<Currency>
|
||||
get() = Obligation.State(Obligation.Lifecycle.NORMAL, MINI_CORP, token.OBLIGATION_DEF, quantity, NullPublicKey)
|
||||
|
@ -13,7 +13,7 @@ import java.security.PublicKey
|
||||
* Move command is provided, and errors if absent. Must be the last clause under a grouping clause;
|
||||
* errors on no-match, ends on match.
|
||||
*/
|
||||
abstract class AbstractConserveAmount<C: MoveCommand, S: FungibleAsset.State<T>, T: Any> : GroupClause<S, Issued<T>> {
|
||||
abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause<S, Issued<T>> {
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
|
@ -6,6 +6,13 @@ import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
|
||||
/**
|
||||
* Standard issue clause for contracts that issue fungible assets.
|
||||
*
|
||||
* @param S the type of contract state which is being issued.
|
||||
* @param T the token underlying the issued state.
|
||||
* @param sum function to convert a list of states into an amount of the token. Must error if there are no states in
|
||||
* the list.
|
||||
* @param sumOrZero function to convert a list of states into an amount of the token, and returns zero if there are
|
||||
* no states in the list. Takes in an instance of the token definition for constructing the zero amount if needed.
|
||||
*/
|
||||
abstract class AbstractIssue<S: ContractState, T: Any>(
|
||||
val sum: List<S>.() -> Amount<Issued<T>>,
|
||||
|
@ -0,0 +1,97 @@
|
||||
package com.r3corda.contracts.clause
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import com.r3corda.contracts.asset.Obligation
|
||||
import com.r3corda.contracts.asset.extractAmountsDue
|
||||
import com.r3corda.contracts.asset.sumAmountsDue
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
import com.r3corda.core.contracts.clauses.SingleClause
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* Common interface for the state subsets used when determining nettability of two or more states. Exposes the
|
||||
* underlying issued thing.
|
||||
*/
|
||||
interface NetState<P> {
|
||||
val template: Obligation.Terms<P>
|
||||
}
|
||||
|
||||
/**
|
||||
* Subset of state, containing the elements which must match for two obligation transactions to be nettable.
|
||||
* If two obligation state objects produce equal bilateral net states, they are considered safe to net directly.
|
||||
* Bilateral states are used in close-out netting.
|
||||
*/
|
||||
data class BilateralNetState<P>(
|
||||
val partyKeys: Set<PublicKey>,
|
||||
override val template: Obligation.Terms<P>
|
||||
) : NetState<P>
|
||||
|
||||
/**
|
||||
* Subset of state, containing the elements which must match for two or more obligation transactions to be candidates
|
||||
* for netting (this does not include the checks to enforce that everyone's amounts received are the same at the end,
|
||||
* which is handled under the verify() function).
|
||||
* In comparison to [BilateralNetState], this doesn't include the parties' keys, as ensuring balances match on
|
||||
* input and output is handled elsewhere.
|
||||
* Used in cases where all parties (or their proxies) are signing, such as central clearing.
|
||||
*/
|
||||
data class MultilateralNetState<P>(
|
||||
override val template: Obligation.Terms<P>
|
||||
) : NetState<P>
|
||||
|
||||
/**
|
||||
* Clause for netting contract states. Currently only supports obligation contract.
|
||||
*/
|
||||
// TODO: Make this usable for any nettable contract states
|
||||
open class NetClause<P> : SingleClause {
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Obligation.Commands.Net::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
|
||||
val command = commands.requireSingleCommand<Obligation.Commands.Net>()
|
||||
val groups = when (command.value.type) {
|
||||
NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State<P> -> it.bilateralNetState }
|
||||
NetType.PAYMENT -> tx.groupStates { it: Obligation.State<P> -> it.multilateralNetState }
|
||||
}
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
verifyNetCommand(inputs, outputs, command, key)
|
||||
}
|
||||
return setOf(command.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a netting command. This handles both close-out and payment netting.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public fun verifyNetCommand(inputs: List<Obligation.State<P>>,
|
||||
outputs: List<Obligation.State<P>>,
|
||||
command: AuthenticatedObject<Obligation.Commands.Net>,
|
||||
netState: NetState<P>) {
|
||||
val template = netState.template
|
||||
// Create two maps of balances from obligors to beneficiaries, one for input states, the other for output states.
|
||||
val inputBalances = extractAmountsDue(template, inputs)
|
||||
val outputBalances = extractAmountsDue(template, outputs)
|
||||
|
||||
// Sum the columns of the matrices. This will yield the net amount payable to/from each party to/from all other participants.
|
||||
// The two summaries must match, reflecting that the amounts owed match on both input and output.
|
||||
requireThat {
|
||||
"all input states use the same template" by (inputs.all { it.template == template })
|
||||
"all output states use the same template" by (outputs.all { it.template == template })
|
||||
"amounts owed on input and output must match" by (sumAmountsDue(inputBalances) == sumAmountsDue(outputBalances))
|
||||
}
|
||||
|
||||
// TODO: Handle proxies nominated by parties, i.e. a central clearing service
|
||||
val involvedParties = inputs.map { it.beneficiary }.union(inputs.map { it.obligor.owningKey }).toSet()
|
||||
when (command.value.type) {
|
||||
// For close-out netting, allow any involved party to sign
|
||||
NetType.CLOSE_OUT -> require(command.signers.intersect(involvedParties).isNotEmpty()) { "any involved party has signed" }
|
||||
// Require signatures from all parties (this constraint can be changed for other contracts, and is used as a
|
||||
// placeholder while exact requirements are established), or fail the transaction.
|
||||
NetType.PAYMENT -> require(command.signers.containsAll(involvedParties)) { "all involved parties have signed" }
|
||||
}
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
* Clause for fungible asset contracts, which enforces that no output state should have
|
||||
* a balance of zero.
|
||||
*/
|
||||
open class NoZeroSizedOutputs<S: FungibleAsset.State<T>, T: Any> : GroupClause<S, Issued<T>> {
|
||||
open class NoZeroSizedOutputs<S: FungibleAsset<T>, T: Any> : GroupClause<S, Issued<T>> {
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
|
@ -150,15 +150,15 @@ class CashTests {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
this `fails with` "there is only a single issue command"
|
||||
this `fails with` "List has more than one element."
|
||||
}
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
this `fails with` "there is only a single issue command"
|
||||
this `fails with` "All commands must be matched at end of execution."
|
||||
}
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
|
||||
this `fails with` "there is only a single issue command"
|
||||
this `fails with` "All commands must be matched at end of execution."
|
||||
}
|
||||
this.verifies()
|
||||
}
|
||||
@ -238,7 +238,7 @@ class CashTests {
|
||||
input { inState }
|
||||
output { outState.copy(amount = inState.amount / 2).editDepositRef(0) }
|
||||
output { outState.copy(amount = inState.amount / 2).editDepositRef(1) }
|
||||
this `fails with` "for deposit [01] at issuer MegaCorp the amounts balance"
|
||||
this `fails with` "for reference [01] at issuer MegaCorp the amounts balance"
|
||||
}
|
||||
// Can't mix currencies.
|
||||
transaction {
|
||||
@ -271,7 +271,7 @@ class CashTests {
|
||||
input { inState }
|
||||
input { inState.editDepositRef(3) }
|
||||
output { outState.copy(amount = inState.amount * 2).editDepositRef(3) }
|
||||
this `fails with` "for deposit [01]"
|
||||
this `fails with` "for reference [01]"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,12 +3,13 @@ package com.r3corda.contracts.asset
|
||||
import com.r3corda.contracts.asset.Obligation.Lifecycle
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.serialization.OpaqueBytes
|
||||
import com.r3corda.core.testing.*
|
||||
import com.r3corda.core.utilities.nonEmptySetOf
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
@ -16,15 +17,15 @@ import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ObligationTests {
|
||||
val defaultIssuer = MEGA_CORP.ref(1)
|
||||
val defaultUsd = USD `issued by` defaultIssuer
|
||||
val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
|
||||
val defaultIssuer = MEGA_CORP.ref(defaultRef)
|
||||
val oneMillionDollars = 1000000.DOLLARS `issued by` defaultIssuer
|
||||
val trustedCashContract = nonEmptySetOf(SecureHash.Companion.randomSHA256() as SecureHash)
|
||||
val megaIssuedDollars = nonEmptySetOf(Issued(defaultIssuer, USD))
|
||||
val megaIssuedPounds = nonEmptySetOf(Issued(defaultIssuer, GBP))
|
||||
val fivePm = Instant.parse("2016-01-01T17:00:00.00Z")
|
||||
val sixPm = Instant.parse("2016-01-01T18:00:00.00Z")
|
||||
val megaCorpDollarSettlement = Obligation.StateTemplate(trustedCashContract, megaIssuedDollars, fivePm)
|
||||
val megaIssuedDollars = nonEmptySetOf(Issued<Currency>(defaultIssuer, USD))
|
||||
val megaIssuedPounds = nonEmptySetOf(Issued<Currency>(defaultIssuer, GBP))
|
||||
val fivePm = TEST_TX_TIME.truncatedTo(ChronoUnit.DAYS).plus(17, ChronoUnit.HOURS)
|
||||
val sixPm = fivePm.plus(1, ChronoUnit.HOURS)
|
||||
val megaCorpDollarSettlement = Obligation.Terms(trustedCashContract, megaIssuedDollars, fivePm)
|
||||
val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuedProducts = megaIssuedPounds)
|
||||
val inState = Obligation.State(
|
||||
lifecycle = Lifecycle.NORMAL,
|
||||
@ -58,24 +59,24 @@ class ObligationTests {
|
||||
}
|
||||
tweak {
|
||||
output { outState }
|
||||
// No command commanduments
|
||||
this `fails with` "required com.r3corda.contracts.asset.Obligation.Commands.Move command"
|
||||
// No command arguments
|
||||
this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
|
||||
}
|
||||
tweak {
|
||||
output { outState }
|
||||
command(DUMMY_PUBKEY_2) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||
command(DUMMY_PUBKEY_2) { Obligation.Commands.Move() }
|
||||
this `fails with` "the owning keys are the same as the signing keys"
|
||||
}
|
||||
tweak {
|
||||
output { outState }
|
||||
output { outState `issued by` MINI_CORP }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||
this `fails with` "at least one obligation input"
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() }
|
||||
this `fails with` "at least one asset input"
|
||||
}
|
||||
// Simple reallocation works.
|
||||
tweak {
|
||||
output { outState }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() }
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -87,16 +88,16 @@ class ObligationTests {
|
||||
transaction {
|
||||
input { DummyState() }
|
||||
output { outState }
|
||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move(outState.issuanceDef) }
|
||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() }
|
||||
|
||||
this `fails with` "there is at least one obligation input"
|
||||
this `fails with` "there is at least one asset input"
|
||||
}
|
||||
|
||||
// Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
|
||||
// institution is allowed to issue as much cash as they want.
|
||||
transaction {
|
||||
output { outState }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Issue(outState.issuanceDef) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Issue() }
|
||||
this `fails with` "output states are issued by a command signer"
|
||||
}
|
||||
transaction {
|
||||
@ -109,10 +110,10 @@ class ObligationTests {
|
||||
)
|
||||
}
|
||||
tweak {
|
||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement), 0) }
|
||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(0) }
|
||||
this `fails with` "has a nonce"
|
||||
}
|
||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement)) }
|
||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
@ -128,7 +129,7 @@ class ObligationTests {
|
||||
template = megaCorpDollarSettlement
|
||||
)
|
||||
assertEquals(ptx.outputStates()[0].data, expected)
|
||||
assertTrue(ptx.commands()[0].value is Obligation.Commands.Issue<*>)
|
||||
assertTrue(ptx.commands()[0].value is Obligation.Commands.Issue)
|
||||
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0])
|
||||
|
||||
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
|
||||
@ -138,13 +139,13 @@ class ObligationTests {
|
||||
|
||||
// Move fails: not allowed to summon money.
|
||||
tweak {
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||
this `fails with` "at obligor MegaCorp the amounts balance"
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() }
|
||||
this `fails with` "for reference [00] at issuer MegaCorp the amounts balance"
|
||||
}
|
||||
|
||||
// Issue works.
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -153,7 +154,7 @@ class ObligationTests {
|
||||
transaction {
|
||||
input { inState }
|
||||
output { inState.copy(quantity = inState.amount.quantity / 2) }
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||
this `fails with` "output values sum to more than the inputs"
|
||||
}
|
||||
|
||||
@ -161,30 +162,26 @@ class ObligationTests {
|
||||
transaction {
|
||||
input { inState }
|
||||
output { inState }
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
|
||||
this `fails with` "output values sum to more than the inputs"
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||
this `fails with` ""
|
||||
}
|
||||
|
||||
// Can't have any other commands if we have an issue command (because the issue command overrules them)
|
||||
transaction {
|
||||
input { inState }
|
||||
output { inState.copy(quantity = inState.amount.quantity * 2) }
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
|
||||
this `fails with` "only move/exit commands can be present along with other obligation commands"
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||
this `fails with` "List has more than one element."
|
||||
}
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||
this `fails with` "only move/exit commands can be present along with other obligation commands"
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move() }
|
||||
this `fails with` "All commands must be matched at end of execution."
|
||||
}
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.SetLifecycle(inState.issuanceDef, Lifecycle.DEFAULTED) }
|
||||
this `fails with` "only move/exit commands can be present along with other obligation commands"
|
||||
}
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, inState.amount / 2) }
|
||||
this `fails with` "only move/exit commands can be present along with other obligation commands"
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.amount / 2) }
|
||||
this `fails with` "All commands must be matched at end of execution."
|
||||
}
|
||||
this.verifies()
|
||||
}
|
||||
@ -245,7 +242,7 @@ class ObligationTests {
|
||||
val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)
|
||||
val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE_PUBKEY)
|
||||
val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||
Obligation<Currency>().generatePaymentNetting(this, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
|
||||
Obligation<Currency>().generatePaymentNetting(this, obligationAliceToBob.issuanceDef, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
|
||||
signWith(ALICE_KEY)
|
||||
signWith(BOB_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
@ -259,7 +256,7 @@ class ObligationTests {
|
||||
val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)
|
||||
val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE_PUBKEY)
|
||||
val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||
Obligation<Currency>().generatePaymentNetting(this, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
|
||||
Obligation<Currency>().generatePaymentNetting(this, obligationAliceToBob.issuanceDef, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
|
||||
signWith(ALICE_KEY)
|
||||
signWith(BOB_KEY)
|
||||
}.toSignedTransaction().tx
|
||||
@ -453,7 +450,7 @@ class ObligationTests {
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Alice's $1,000,000")
|
||||
output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Amount(oneMillionDollars.quantity, USD)) }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle<Currency>(Amount(oneMillionDollars.quantity, inState.issuanceDef)) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
|
||||
this.verifies()
|
||||
}
|
||||
@ -467,7 +464,7 @@ class ObligationTests {
|
||||
input(500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY)
|
||||
output("Alice's $5,000,000 obligation to Bob") { halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) }
|
||||
output("Bob's $500,000") { 500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle<Currency>(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Amount(oneMillionDollars.quantity, USD)) }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle<Currency>(Amount(oneMillionDollars.quantity, inState.issuanceDef)) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
|
||||
this.verifies()
|
||||
}
|
||||
@ -480,7 +477,7 @@ class ObligationTests {
|
||||
input(defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob
|
||||
input(1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY)
|
||||
output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle<Currency>(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Amount(oneMillionDollars.quantity, USD)) }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle<Currency>(Amount(oneMillionDollars.quantity, inState.issuanceDef)) }
|
||||
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
|
||||
this `fails with` "all inputs are in the normal state"
|
||||
}
|
||||
@ -495,7 +492,7 @@ class ObligationTests {
|
||||
transaction("Settlement") {
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)).copy(lifecycle = Lifecycle.DEFAULTED) }
|
||||
command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Lifecycle.DEFAULTED) }
|
||||
command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) }
|
||||
this `fails with` "there is a timestamp from the authority"
|
||||
}
|
||||
}
|
||||
@ -506,7 +503,7 @@ class ObligationTests {
|
||||
transaction("Settlement") {
|
||||
input(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) `at` futureTestTime)
|
||||
output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) `at` futureTestTime).copy(lifecycle = Lifecycle.DEFAULTED) }
|
||||
command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF) `at` futureTestTime, Lifecycle.DEFAULTED) }
|
||||
command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "the due date has passed"
|
||||
}
|
||||
@ -516,7 +513,7 @@ class ObligationTests {
|
||||
transaction("Settlement") {
|
||||
input(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) `at` pastTestTime)
|
||||
output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) `at` pastTestTime).copy(lifecycle = Lifecycle.DEFAULTED) }
|
||||
command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF) `at` pastTestTime, Lifecycle.DEFAULTED) }
|
||||
command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
@ -528,7 +525,7 @@ class ObligationTests {
|
||||
fun testMergeSplit() {
|
||||
// Splitting value works.
|
||||
transaction {
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() }
|
||||
tweak {
|
||||
input { inState }
|
||||
repeat(4) { output { inState.copy(quantity = inState.quantity / 4) } }
|
||||
@ -572,7 +569,7 @@ class ObligationTests {
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState `issued by` MINI_CORP }
|
||||
this `fails with` "at obligor MegaCorp the amounts balance"
|
||||
this `fails with` "for reference [00] at issuer MegaCorp the amounts balance"
|
||||
}
|
||||
// Can't mix currencies.
|
||||
transaction {
|
||||
@ -598,55 +595,54 @@ class ObligationTests {
|
||||
input { inState }
|
||||
input { inState `issued by` MINI_CORP }
|
||||
output { outState }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
||||
this `fails with` "at obligor MiniCorp the amounts balance"
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() }
|
||||
this `fails with` "for reference [00] at issuer MiniCorp the amounts balance"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exitLedger() {
|
||||
fun `exit single product obligation`() {
|
||||
// Single input/output straightforward case.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
|
||||
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 100.DOLLARS) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit<Currency>(Amount(100.DOLLARS.quantity, inState.issuanceDef)) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() }
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 200.DOLLARS) }
|
||||
this `fails with` "required com.r3corda.contracts.asset.Obligation.Commands.Move command"
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit<Currency>(Amount(200.DOLLARS.quantity, inState.issuanceDef)) }
|
||||
this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
|
||||
|
||||
tweak {
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() }
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Multi-issuer case.
|
||||
|
||||
}
|
||||
@Test
|
||||
fun `exit multiple product obligations`() {
|
||||
// Multi-product case.
|
||||
transaction {
|
||||
input { inState }
|
||||
input { inState `issued by` MINI_CORP }
|
||||
input { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedPounds)) }
|
||||
input { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedDollars)) }
|
||||
|
||||
output { inState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) `issued by` MINI_CORP }
|
||||
output { inState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
|
||||
output { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedPounds), quantity = inState.quantity - 200.POUNDS.quantity) }
|
||||
output { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedDollars), quantity = inState.quantity - 200.DOLLARS.quantity) }
|
||||
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() }
|
||||
|
||||
this `fails with` "at obligor MegaCorp the amounts balance"
|
||||
this `fails with` "for reference [00] at issuer MegaCorp the amounts balance"
|
||||
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 200.DOLLARS) }
|
||||
tweak {
|
||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Exit((inState `issued by` MINI_CORP).issuanceDef, 0.DOLLARS) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
||||
this `fails with` "at obligor MiniCorp the amounts balance"
|
||||
}
|
||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Exit((inState `issued by` MINI_CORP).issuanceDef, 200.DOLLARS) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.issuanceDef.copy(product = megaCorpDollarSettlement))) }
|
||||
this `fails with` "for reference [00] at issuer MegaCorp the amounts balance"
|
||||
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.POUNDS.quantity, inState.issuanceDef.copy(product = megaCorpPoundSettlement))) }
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -661,20 +657,19 @@ class ObligationTests {
|
||||
// Can't merge them together.
|
||||
tweak {
|
||||
output { inState.copy(beneficiary = DUMMY_PUBKEY_2, quantity = 200000L) }
|
||||
this `fails with` "at obligor MegaCorp the amounts balance"
|
||||
this `fails with` "for reference [00] at issuer MegaCorp the amounts balance"
|
||||
}
|
||||
// Missing MiniCorp deposit
|
||||
tweak {
|
||||
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) }
|
||||
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) }
|
||||
this `fails with` "at obligor MegaCorp the amounts balance"
|
||||
this `fails with` "for reference [00] at issuer MegaCorp the amounts balance"
|
||||
}
|
||||
|
||||
// This works.
|
||||
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) }
|
||||
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) `issued by` MINI_CORP }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
||||
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() }
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
@ -688,8 +683,7 @@ class ObligationTests {
|
||||
input { pounds }
|
||||
output { inState `owned by` DUMMY_PUBKEY_2 }
|
||||
output { pounds `owned by` DUMMY_PUBKEY_1 }
|
||||
command(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||
command(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Obligation.Commands.Move(pounds.issuanceDef) }
|
||||
command(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Obligation.Commands.Move() }
|
||||
|
||||
this.verifies()
|
||||
}
|
||||
@ -754,7 +748,7 @@ class ObligationTests {
|
||||
|
||||
@Test
|
||||
fun `adding two settlement contracts nets them`() {
|
||||
val megaCorpDollarSettlement = Obligation.StateTemplate(trustedCashContract, megaIssuedDollars, fivePm)
|
||||
val megaCorpDollarSettlement = Obligation.Terms(trustedCashContract, megaIssuedDollars, fivePm)
|
||||
val fiveKDollarsFromMegaToMini = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement,
|
||||
5000.DOLLARS.quantity, MINI_CORP_PUBKEY)
|
||||
val oneKDollarsFromMiniToMega = Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpDollarSettlement,
|
||||
@ -780,11 +774,12 @@ class ObligationTests {
|
||||
|
||||
@Test
|
||||
fun `extracting amounts due between parties from a list of states`() {
|
||||
val megaCorpDollarSettlement = Obligation.StateTemplate(trustedCashContract, megaIssuedDollars, fivePm)
|
||||
val megaCorpDollarSettlement = Obligation.Terms(trustedCashContract, megaIssuedDollars, fivePm)
|
||||
val fiveKDollarsFromMegaToMini = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement,
|
||||
5000.DOLLARS.quantity, MINI_CORP_PUBKEY)
|
||||
val expected = mapOf(Pair(Pair(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), fiveKDollarsFromMegaToMini.amount))
|
||||
val actual = extractAmountsDue(USD, listOf(fiveKDollarsFromMegaToMini))
|
||||
val amount = fiveKDollarsFromMegaToMini.amount
|
||||
val expected = mapOf(Pair(Pair(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), Amount(amount.quantity, amount.token.product)))
|
||||
val actual = extractAmountsDue<Currency>(megaCorpDollarSettlement, listOf(fiveKDollarsFromMegaToMini))
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@ -810,7 +805,7 @@ class ObligationTests {
|
||||
val expected = mapOf(
|
||||
Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP))
|
||||
)
|
||||
val actual = netAmountsDue(balanced)
|
||||
val actual = netAmountsDue<Currency>(balanced)
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ interface NamedByHash {
|
||||
/**
|
||||
* Interface for state objects that support being netted with other state objects.
|
||||
*/
|
||||
interface BilateralNettableState<T: BilateralNettableState<T>> {
|
||||
interface BilateralNettableState<N: BilateralNettableState<N>> {
|
||||
/**
|
||||
* Returns an object used to determine if two states can be subject to close-out netting. If two states return
|
||||
* equal objects, they can be close out netted together.
|
||||
@ -34,9 +34,23 @@ interface BilateralNettableState<T: BilateralNettableState<T>> {
|
||||
* Perform bilateral netting of this state with another state. The two states must be compatible (as in
|
||||
* bilateralNetState objects are equal).
|
||||
*/
|
||||
fun net(other: T): T
|
||||
fun net(other: N): N
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for state objects that support being netted with other state objects.
|
||||
*/
|
||||
interface MultilateralNettableState<T: Any> {
|
||||
/**
|
||||
* Returns an object used to determine if two states can be subject to close-out netting. If two states return
|
||||
* equal objects, they can be close out netted together.
|
||||
*/
|
||||
val multilateralNetState: T
|
||||
}
|
||||
|
||||
interface NettableState<N: BilateralNettableState<N>, T: Any>: BilateralNettableState<N>,
|
||||
MultilateralNettableState<T>
|
||||
|
||||
/**
|
||||
* A contract state (or just "state") contains opaque data used by a contract program. It can be thought of as a disk
|
||||
* file that the program can use to persist data across transactions. States are immutable: once created they are never
|
||||
|
@ -96,9 +96,6 @@ data class TransactionForContract(val inputs: List<ContractState>,
|
||||
@Deprecated("This property was renamed to outputs", ReplaceWith("outputs"))
|
||||
val outStates: List<ContractState> get() = outputs
|
||||
|
||||
inline fun <reified T: CommandData, K> groupCommands(keySelector: (AuthenticatedObject<T>) -> K): Map<K, List<AuthenticatedObject<T>>>
|
||||
= commands.select<T>().groupBy(keySelector)
|
||||
|
||||
/**
|
||||
* Given a type and a function that returns a grouping key, associates inputs and outputs together so that they
|
||||
* can be processed as one. The grouping key is any arbitrary object that can act as a map key (so must implement
|
||||
|
@ -23,10 +23,10 @@ interface GroupClause<S : ContractState, T : Any> : Clause, GroupVerify<S, T>
|
||||
|
||||
abstract class GroupClauseVerifier<S : ContractState, T : Any> : SingleClause {
|
||||
abstract val clauses: List<GroupClause<S, T>>
|
||||
override val requiredCommands: Set<Class<CommandData>>
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = emptySet()
|
||||
|
||||
abstract fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<out S, T>>
|
||||
abstract fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<S, T>>
|
||||
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
|
||||
val groups = extractGroups(tx)
|
||||
|
Loading…
Reference in New Issue
Block a user