diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/Obligation.kt b/contracts/src/main/kotlin/com/r3corda/contracts/Obligation.kt index e5c25dcb14..6ce0119de3 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/Obligation.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/Obligation.kt @@ -1,7 +1,8 @@ package com.r3corda.contracts import com.google.common.annotations.VisibleForTesting -import com.r3corda.contracts.cash.* +import com.r3corda.contracts.cash.FungibleAssetState +import com.r3corda.contracts.cash.sumFungibleOrNull import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash @@ -18,12 +19,10 @@ import java.util.* val OBLIGATION_PROGRAM_ID = Obligation() /** - * A cash settlement contract commits the obligor to delivering a specified amount of cash (represented as the [Cash] - * contract) at a specified future point in time. Similarly to cash, settlement transactions may split and merge - * contracts across multiple input and output states. - * - * The goal of this design is to handle money owed, and these contracts are expected to be netted/merged, with - * settlement only for any remainder amount. + * An obligation contract commits the obligor to delivering a specified amount of a fungible asset (for example the + * [Cash] contract) at a specified future point in time. Settlement transactions may split and merge contracts across + * multiple input and output states. The goal of this design is to handle amounts owed, and these contracts are expected + * to be netted/merged, with settlement only for any remainder amount. * * @param P the product the obligation is for payment of. */ @@ -92,9 +91,9 @@ class Obligation

: Contract { * @param P the product the obligation is for payment of. */ data class StateTemplate

( - /** The hash of the cash contract we're willing to accept in payment for this debt. */ + /** The hash of the asset contract we're willing to accept in payment for this debt. */ val acceptableContracts: NonEmptySet, - /** The parties whose cash we are willing to accept in payment for this debt. */ + /** The parties whose assets we are willing to accept in payment for this debt. */ val acceptableIssuedProducts: NonEmptySet>, /** When the contract must be settled by. */ @@ -119,7 +118,7 @@ class Obligation

: Contract { /** * A state representing the obligation of one party (obligor) to deliver a specified number of - * units of an underlying asset (described as issuanceDef.acceptableCashIssuance) to the beneficiary + * units of an underlying asset (described as issuanceDef.acceptableIssuedProducts) to the beneficiary * no later than the specified time. * * @param P the product the obligation is for payment of. @@ -197,7 +196,7 @@ class Obligation

: Contract { // Just for grouping interface Commands : CommandData { /** - * Net two or more cash settlement states together in a close-out netting style. Limited to bilateral netting + * 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 @@ -222,7 +221,7 @@ class Obligation

: Contract { /** * 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 [Cash.Commands.Move] + * @see [MoveCommand] */ data class Settle

(override val aggregateState: IssuanceDefinition

, val amount: Amount

) : Commands, IssuanceCommands

@@ -288,9 +287,9 @@ class Obligation

: Contract { outputs: List>, obligor: Party, key: IssuanceDefinition

) { - // We've already pre-grouped by currency amongst other fields, and verified above that every state specifies + // 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 currency + // 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 @@ -482,13 +481,13 @@ class Obligation

: Contract { val inputAmount: Amount

= inputs.sumObligationsOrNull

() ?: throw IllegalArgumentException("there is at least one obligation input for this group") val outputAmount: Amount

= outputs.sumObligationsOrZero(issued.product) - // Sum up all cash state objects that are moving and fulfil our requirements + // Sum up all asset state objects that are moving and fulfil our requirements - // The cash contract verification handles ensuring there's inputs enough to cover the output states, we only - // care about counting how much cash is output in this transaction. We then calculate the difference in + // 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 beneficiarys have enough sense to do that themselves. - // Therefore if someone actually signed the following transaction: + // 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 @@ -500,23 +499,23 @@ class Obligation

: Contract { // Move (signed by B) // // That would pass this check. Ensuring they do not is best addressed in the transaction generation stage. - val cashStates = tx.outputs.filterIsInstance>() - val acceptableCashStates = cashStates - // TODO: This filter is nonsense, because it just checks there is a cash contract loaded, we need to - // verify the cash contract is the cash contract we expect. + val assetStates = tx.outputs.filterIsInstance>() + 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.acceptableCashContract) + // attachments.mustHaveOneOf(key.acceptableAssetContract) .filter { it.contract.legalContractReference in template.acceptableContracts } // Restrict the states to those of the correct issuance definition (this normally - // covers currency and obligor, but is opaque to us) + // 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 cash state outputs" by (cashStates.size > 0) - "there are defined acceptable cash states" by (acceptableCashStates.size > 0) + "there are fungible asset state outputs" by (assetStates.size > 0) + "there are defined acceptable fungible asset states" by (acceptableAssetStates.size > 0) } - val amountReceivedByOwner = acceptableCashStates.groupBy { it.owner } + 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() @@ -524,7 +523,7 @@ class Obligation

: Contract { val requiredSigners = inputs.map { it.obligor.owningKey }.toSet() for ((beneficiary, obligations) in inputs.groupBy { it.beneficiary }) { - val settled = amountReceivedByOwner[beneficiary]?.sumCashOrNull() + val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull

() if (settled != null) { val debt = obligations.sumObligationsOrZero(issued) require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" } @@ -655,20 +654,22 @@ class Obligation

: Contract { /** * @param statesAndRefs a list of state objects, which MUST all have the same aggregate state. This is done as * only a single settlement command can be present in a transaction, to avoid potential problems with allocating - * cash to different obligation issuances. - * @param cashStatesAndRefs a list of cash state objects, which MUST all be in the same currency. It is strongly - * encouraged that these all have the same beneficiary. + * assets to different obligation issuances. + * @param assetStatesAndRefs a list of fungible asset state objects, which MUST all be of the same issued product. + * It is strongly encouraged that these all have the same beneficiary. + * @param moveCommand the command used to move the asset state objects to their new owner. */ fun generateSettle(tx: TransactionBuilder, statesAndRefs: Iterable>>, - cashStatesAndRefs: Iterable>>, + assetStatesAndRefs: Iterable>>, + moveCommand: MoveCommand, notary: Party) { val states = statesAndRefs.map { it.state } val obligationIssuer = states.first().data.obligor val obligationOwner = states.first().data.beneficiary requireThat { - "all cash states use the same notary" by (cashStatesAndRefs.all { it.state.notary == notary }) + "all fungible asset states use the same notary" by (assetStatesAndRefs.all { it.state.notary == notary }) "all obligation states are in the normal state" by (statesAndRefs.all { it.state.data.lifecycle == Lifecycle.NORMAL }) "all obligation states use the same notary" by (statesAndRefs.all { it.state.notary == notary }) "all obligation states have the same obligor" by (statesAndRefs.all { it.state.data.obligor == obligationIssuer }) @@ -676,32 +677,32 @@ class Obligation

: Contract { } // TODO: A much better (but more complex) solution would be to have two iterators, one for obligations, - // one for cash, and step through each in a semi-synced manner. For now however we just bundle all the states + // one for the assets, and step through each in a semi-synced manner. For now however we just bundle all the states // on each side together val issuanceDef = getIssuanceDefinitionOrThrow(statesAndRefs.map { it.state.data }) val template = issuanceDef.template val obligationTotal: Amount

= states.map { it.data }.sumObligations

() var obligationRemaining: Amount

= obligationTotal - val cashSigners = HashSet() + val assetSigners = HashSet() statesAndRefs.forEach { tx.addInputState(it) } - // Move the cash to the new beneficiary - cashStatesAndRefs.forEach { + // Move the assets to the new beneficiary + assetStatesAndRefs.forEach { if (obligationRemaining.quantity > 0L) { - val cashState = it.state + val assetState = it.state tx.addInputState(it) - if (obligationRemaining >= cashState.data.productAmount) { - tx.addOutputState(cashState.data.move(cashState.data.productAmount, obligationOwner), notary) - obligationRemaining -= cashState.data.productAmount + if (obligationRemaining >= assetState.data.productAmount) { + tx.addOutputState(assetState.data.move(assetState.data.productAmount, obligationOwner), notary) + obligationRemaining -= assetState.data.productAmount } else { // Split the state in two, sending the change back to the previous beneficiary - tx.addOutputState(cashState.data.move(obligationRemaining, obligationOwner), notary) - tx.addOutputState(cashState.data.move(cashState.data.productAmount - obligationRemaining, cashState.data.owner), notary) + tx.addOutputState(assetState.data.move(obligationRemaining, obligationOwner), notary) + tx.addOutputState(assetState.data.move(assetState.data.productAmount - obligationRemaining, assetState.data.owner), notary) obligationRemaining -= Amount(0L, obligationRemaining.token) } - cashSigners.add(cashState.data.owner) + assetSigners.add(assetState.data.owner) } } @@ -712,8 +713,8 @@ class Obligation

: Contract { // Destroy all of the states } - // Add the cash move command and obligation settle - tx.addCommand(Cash.Commands.Move(), cashSigners.toList()) + // Add the asset move command and obligation settle + tx.addCommand(moveCommand, assetSigners.toList()) tx.addCommand(Commands.Settle(issuanceDef, obligationTotal - obligationRemaining), obligationOwner) } @@ -801,17 +802,15 @@ fun

sumAmountsDue(balances: Map, Amount

>): Map return sum } -/** Sums the cash states in the list, throwing an exception if there are none. - * All cash states in the list are presumed to be nettable. - */ +/** 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

Iterable.sumObligations(): Amount

= filterIsInstance>().map { it.amount }.sumOrThrow() -/** Sums the cash settlement states in the list, returning null if there are none. */ +/** Sums the obligation states in the list, returning null if there are none. */ fun

Iterable.sumObligationsOrNull(): Amount

? = filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrNull() -/** Sums the cash settlement states in the list, returning zero of the given currency if there are none. */ +/** Sums the obligation states in the list, returning zero of the given product if there are none. */ fun

Iterable.sumObligationsOrZero(product: P): Amount

= filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(product) diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt index c9ba546142..f7144334bd 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt @@ -317,7 +317,7 @@ class ObligationTests { // Now generate a transaction settling the obligation val settleTx = TransactionType.General.Builder(DUMMY_NOTARY).apply { - Obligation().generateSettle(this, listOf(obligationTx.outRef(0)), listOf(cashTx.outRef(0)), DUMMY_NOTARY) + Obligation().generateSettle(this, listOf(obligationTx.outRef(0)), listOf(cashTx.outRef(0)), Cash.Commands.Move(), DUMMY_NOTARY) signWith(DUMMY_NOTARY_KEY) signWith(MINI_CORP_KEY) }.toSignedTransaction().tx