From 9994d129f2022bbd61b168be329fb32a70105d69 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 24 Jun 2016 15:34:40 +0100 Subject: [PATCH 1/2] General cleanup based on first round of feedback * Rename AggregateCommands to IssuanceCommands * Reorder comparisons to be consistent * Rename verifyDefaultCommand to verifySetLifecycleCommand * Rename currency to issued/product * Add note about needing to rethink timestamping * Rename issuer to obligor, and owner to beneficiary * Move lifecycle inversion code into SetLifecycle command * Correct comments regarding cash states * Rework description of contractHash parameter * Fixes 'netting equal balances due between parties', and add further netting tests * Separate calculations involving issued products and the underlying product * Use signed transactions in obligation tests * Add verification tests for changing lifecycle --- .../kotlin/com/r3corda/contracts/cash/Cash.kt | 13 +- .../r3corda/contracts/cash/FungibleAsset.kt | 2 +- .../contracts/cash/FungibleAssetState.kt | 4 +- .../com/r3corda/contracts/Obligation.kt | 366 +++++++++--------- .../testing/ExperimentalTestUtils.kt | 16 +- .../com/r3corda/contracts/ObligationTests.kt | 220 +++++++---- 6 files changed, 341 insertions(+), 280 deletions(-) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt index 2d15cebd7e..606f430014 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt @@ -54,6 +54,9 @@ class Cash : FungibleAsset() { ) : FungibleAsset.State { constructor(deposit: PartyAndReference, amount: Amount, owner: PublicKey) : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) + + override val productAmount: Amount + get() = Amount(amount.quantity, amount.token.product) override val deposit: PartyAndReference get() = amount.token.issuer override val contract = CASH_PROGRAM_ID @@ -62,8 +65,8 @@ class Cash : FungibleAsset() { override val participants: List get() = listOf(owner) - override fun move(amount: Amount>, owner: PublicKey): FungibleAsset.State - = copy(amount = amount, owner = owner) + override fun move(newAmount: Amount, newOwner: PublicKey): FungibleAsset.State + = copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner) override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})" @@ -75,9 +78,9 @@ class Cash : FungibleAsset() { /** * A command stating that money has been moved, optionally to fulfil another contract. * - * @param contractHash the hash of the contract this cash is settling, to ensure one cash contract cannot be - * used to settle multiple contracts. May be null, if this is not relevant to any other contract in the - * same transaction + * @param contractHash the contract this move is for the attention of. Only that contract's verify function + * should take the moved states into account when considering whether it is valid. Typically this will be + * null. */ data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt index 56061adab2..869ddea6d5 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt @@ -34,7 +34,7 @@ abstract class FungibleAsset : Contract { interface State : FungibleAssetState> { /** Where the underlying currency backing this ledger entry can be found (propagated) */ val deposit: PartyAndReference - override val amount: Amount> + val amount: Amount> /** There must be a MoveCommand signed by this key to claim the amount */ override val owner: PublicKey } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAssetState.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAssetState.kt index 17fa51adb2..b2641d5d99 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAssetState.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAssetState.kt @@ -10,6 +10,6 @@ import java.security.PublicKey */ interface FungibleAssetState : OwnableState { val issuanceDef: I - val amount: Amount> - fun move(amount: Amount>, owner: PublicKey): FungibleAssetState + val productAmount: Amount + fun move(amount: Amount, owner: PublicKey): FungibleAssetState } \ No newline at end of file diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/Obligation.kt b/experimental/src/main/kotlin/com/r3corda/contracts/Obligation.kt index 3635785735..cb1504a428 100644 --- a/experimental/src/main/kotlin/com/r3corda/contracts/Obligation.kt +++ b/experimental/src/main/kotlin/com/r3corda/contracts/Obligation.kt @@ -18,7 +18,7 @@ import java.util.* val OBLIGATION_PROGRAM_ID = Obligation() /** - * A cash settlement contract commits the issuer to delivering a specified amount of cash (represented as the [Cash] + * 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. * @@ -51,7 +51,7 @@ class Obligation

: Contract { NORMAL, /** * Indicates the contract has not been settled by its due date. Once in the defaulted state, - * it can only be reverted to [NORMAL] state by the owner. + * it can only be reverted to [NORMAL] state by the beneficiary. */ DEFAULTED } @@ -61,7 +61,7 @@ class Obligation

: Contract { * underlying issued thing. */ interface NetState

{ - val issued: Issued

+ val template: StateTemplate

} /** @@ -71,11 +71,8 @@ class Obligation

: Contract { */ data class BilateralNetState

( val partyKeys: Set, - val issuanceDef: StateTemplate

- ) : NetState

{ - override val issued: Issued

- get() = issuanceDef.issued - } + override val template: StateTemplate

+ ) : NetState

/** * Subset of state, containing the elements which must match for two or more obligation transactions to be candidates @@ -86,11 +83,8 @@ class Obligation

: Contract { * Used in cases where all parties (or their proxies) are signing, such as central clearing. */ data class MultilateralNetState

( - val issuanceDef: StateTemplate

- ) : NetState

{ - override val issued: Issued

- get() = issuanceDef.issued - } + override val template: StateTemplate

+ ) : NetState

/** * Subset of state, containing the elements specified when issuing a new settlement contract. @@ -101,14 +95,14 @@ class Obligation

: Contract { /** The hash of the cash 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. */ - val acceptableIssuanceDefinitions: NonEmptySet>, + val acceptableIssuedProducts: NonEmptySet>, /** When the contract must be settled by. */ val dueBefore: Instant, val timeTolerance: Duration = Duration.ofSeconds(30) ) { - val issued: Issued

- get() = acceptableIssuanceDefinitions.toSet().single() + val product: P + get() = acceptableIssuedProducts.map { it.product }.toSet().single() } /** @@ -119,57 +113,58 @@ class Obligation

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

( - val issuer: Party, + val obligor: Party, val template: StateTemplate

- ) { - val currency: P - get() = template.issued.product - val issued: Issued

- get() = template.issued - } + ) /** - * A state representing the obligation of one party (issuer) to deliver a specified number of - * units of an underlying asset (described as issuanceDef.acceptableCashIssuance) to the owner + * 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 * no later than the specified time. * * @param P the product the obligation is for payment of. */ data class State

( var lifecycle: Lifecycle = Lifecycle.NORMAL, - /** Where the debt originates from (issuer) */ - val issuer: Party, + /** Where the debt originates from (obligor) */ + val obligor: Party, val template: StateTemplate

, val quantity: Long, /** The public key of the entity the contract pays to */ - override val owner: PublicKey + val beneficiary: PublicKey ) : FungibleAssetState>, BilateralNettableState> { - override val amount: Amount> - get() = Amount(quantity, template.issued) + val amount: Amount

+ get() = Amount(quantity, template.product) + val aggregateState: IssuanceDefinition

+ get() = issuanceDef + override val productAmount: Amount

+ get() = amount override val contract = OBLIGATION_PROGRAM_ID val acceptableContracts: NonEmptySet get() = template.acceptableContracts val acceptableIssuanceDefinitions: NonEmptySet<*> - get() = template.acceptableIssuanceDefinitions + get() = template.acceptableIssuedProducts val dueBefore: Instant get() = template.dueBefore override val issuanceDef: IssuanceDefinition

- get() = IssuanceDefinition(issuer, template) + get() = IssuanceDefinition(obligor, template) override val participants: List - get() = listOf(issuer.owningKey, owner) + get() = listOf(obligor.owningKey, beneficiary) + override val owner: PublicKey + get() = beneficiary - override fun move(amount: Amount>, owner: PublicKey): Obligation.State

- = copy(quantity = amount.quantity, owner = owner) + override fun move(amount: Amount

, beneficiary: PublicKey): Obligation.State

+ = copy(quantity = amount.quantity, beneficiary = beneficiary) override fun toString() = when (lifecycle) { - Lifecycle.NORMAL -> "${Emoji.bagOfCash}Debt($amount due $dueBefore to ${owner.toStringShort()})" - Lifecycle.DEFAULTED -> "${Emoji.bagOfCash}Debt($amount unpaid by $dueBefore to ${owner.toStringShort()})" + Lifecycle.NORMAL -> "${Emoji.bagOfCash}Debt($amount due $dueBefore to ${beneficiary.toStringShort()})" + Lifecycle.DEFAULTED -> "${Emoji.bagOfCash}Debt($amount unpaid by $dueBefore to ${beneficiary.toStringShort()})" } override val bilateralNetState: BilateralNetState

get() { check(lifecycle == Lifecycle.NORMAL) - return BilateralNetState(setOf(issuer.owningKey, owner), template) + return BilateralNetState(setOf(obligor.owningKey, beneficiary), template) } val multilateralNetState: MultilateralNetState

get() { @@ -182,20 +177,20 @@ class Obligation

: Contract { val netB = other.bilateralNetState require(netA == netB) { "net substates of the two state objects must be identical" } - if (issuer.owningKey == other.issuer.owningKey) { - // Both sides are from the same issuer to owner + if (obligor.owningKey == other.obligor.owningKey) { + // Both sides are from the same obligor to beneficiary return copy(quantity = quantity + other.quantity) } else { - // Issuer and owner are backwards + // Issuer and beneficiary are backwards return copy(quantity = quantity - other.quantity) } } - override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(issuanceDef), copy(owner = newOwner)) + override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(issuanceDef), copy(beneficiary = newOwner)) } - /** Interface for commands that apply to aggregated states */ - interface AggregateCommands

: CommandData { + /** Interface for commands that apply to states grouped by issuance definition */ + interface IssuanceCommands

: CommandData { val aggregateState: IssuanceDefinition

} @@ -203,49 +198,54 @@ class Obligation

: Contract { interface Commands : CommandData { /** * Net two or more cash settlement states together in a close-out netting style. Limited to bilateral netting - * as only the owner (not the issuer) needs to sign. + * as only the beneficiary (not the obligor) needs to sign. */ data class Net(val type: NetType) : Commands /** * A command stating that a debt has been moved, optionally to fulfil another contract. * - * @param contractHash the hash of contract's code, which indicates to that contract that the - * obligation states moved in this transaction are for their sole attention. - * This is a single value to ensure the same state(s) cannot be used to settle multiple contracts. - * May be null, if this is not relevant to any other contract in the same transaction. + * @param contractHash the contract this move is for the attention of. Only that contract's verify function + * should take the moved states into account when considering whether it is valid. Typically this will be + * null. */ data class Move

(override val aggregateState: IssuanceDefinition

, - override val contractHash: SecureHash? = null) : Commands, AggregateCommands

, MoveCommand + override val contractHash: SecureHash? = null) : Commands, IssuanceCommands

, MoveCommand /** - * Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction - * has a unique ID even when there are no inputs. + * 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

(override val aggregateState: IssuanceDefinition

, - val nonce: Long = random63BitValue()) : Commands, AggregateCommands

+ val nonce: Long = random63BitValue()) : Commands, IssuanceCommands

/** - * A command stating that the issuer is settling some or all of the amount owed by paying in a suitable cash - * contract. If this reduces the balance to zero, the contract moves to the settled state. + * 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] */ data class Settle

(override val aggregateState: IssuanceDefinition

, - val amount: Amount>) : Commands, AggregateCommands

+ val amount: Amount

) : Commands, IssuanceCommands

/** - * A command stating that the owner is moving the contract into the defaulted state as it has not been settled + * 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

(override val aggregateState: IssuanceDefinition

, - val lifecycle: Lifecycle) : Commands, AggregateCommands

+ val lifecycle: Lifecycle) : Commands, IssuanceCommands

{ + val inverse: Lifecycle + get() = when (lifecycle) { + Lifecycle.NORMAL -> Lifecycle.DEFAULTED + Lifecycle.DEFAULTED -> Lifecycle.NORMAL + } + } /** - * A command stating that the debt is being released by the owner. Normally would indicate - * either settlement outside of the ledger, or that the issuer is unable to pay. + * 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

(override val aggregateState: IssuanceDefinition

, - val amount: Amount>) : Commands, AggregateCommands

+ val amount: Amount

) : Commands, IssuanceCommands

} /** This is the function EVERYONE runs */ @@ -264,40 +264,40 @@ class Obligation

: Contract { verifyNetCommand(inputs, outputs, netCommand, key) } } else { - val commandGroups = tx.groupCommands, IssuanceDefinition

> { it.value.aggregateState } + val commandGroups = tx.groupCommands, IssuanceDefinition

> { 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

-> it.issuanceDef } + val groups = tx.groupStates() { it: State

-> it.aggregateState } for ((inputs, outputs, key) in groups) { // Either inputs or outputs could be empty. - val issuer = key.issuer + val obligor = key.obligor val commands = commandGroups[key] ?: emptyList() requireThat { "there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L } } - verifyCommandGroup(tx, commands, inputs, outputs, issuer, key) + verifyCommandGroup(tx, commands, inputs, outputs, obligor, key) } } } private fun verifyCommandGroup(tx: TransactionForContract, - commands: List>>, + commands: List>>, inputs: List>, outputs: List>, - issuer: Party, + obligor: Party, key: IssuanceDefinition

) { // We've already pre-grouped by currency amongst other fields, and verified above that every state specifies - // at least one acceptable cash issuance definition, so we can just use the first issuance definition to + // at least one acceptable issuance definition, so we can just use the first issuance definition to // determine currency - val currency = key.template.acceptableIssuanceDefinitions.first() + 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>().firstOrNull() - val defaultCommand = commands.select>().firstOrNull() + val setLifecycleCommand = commands.select>().firstOrNull() val settleCommand = commands.select>().firstOrNull() if (commands.size != 1) { @@ -308,8 +308,8 @@ class Obligation

: Contract { // Issue, default and net commands are special, and do not follow normal input/output summing rules, so // deal with them first - if (defaultCommand != null) { - verifyDefaultCommand(inputs, outputs, tx, defaultCommand) + 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 @@ -318,15 +318,15 @@ class Obligation

: Contract { "all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL } } if (issueCommand != null) { - verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer) + verifyIssueCommand(inputs, outputs, tx, 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, currency, issuer, key) + verifySettleCommand(inputs, outputs, tx, settleCommand, issued, obligor, key) } else { - verifyBalanceChange(inputs, outputs, commands, currency, issuer) + verifyBalanceChange(inputs, outputs, commands, issued.product, obligor) } } } @@ -339,32 +339,32 @@ class Obligation

: Contract { */ private fun verifyBalanceChange(inputs: List>, outputs: List>, - commands: List>>, - currency: Issued

, - issuer: Party) { + commands: List>>, + 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

() ?: throw IllegalArgumentException("there is at least one obligation input for this group") - val outputAmount = outputs.sumObligationsOrZero(currency) + val outputAmount = outputs.sumObligationsOrZero(product) val exitCommands = commands.select>() val requiredExitSignatures = HashSet() - val amountExitingLedger: Amount> = if (exitCommands.isNotEmpty()) { + val amountExitingLedger: Amount

= 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 owner. For now we require exit - // commands to be signed by all input owners, unlocking the full input amount, rather than trying to detangle + // 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.owner }) + requiredExitSignatures.addAll(inputs.map { it.beneficiary }) exitCommand.value.amount } else { - Amount(0, currency) + Amount(0, product) } requireThat { "there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L } - "at issuer ${issuer.name} the amounts balance" by + "at obligor ${obligor.name} the amounts balance" by (inputAmount == outputAmount + amountExitingLedger) } @@ -375,39 +375,36 @@ class Obligation

: Contract { * A default command mutates inputs and produces identical outputs, except that the lifecycle changes. */ @VisibleForTesting - protected fun verifyDefaultCommand(inputs: List>, - outputs: List>, - tx: TransactionForContract, - setLifecycleCommand: AuthenticatedObject>) { + protected fun verifySetLifecycleCommand(inputs: List>, + outputs: List>, + tx: TransactionForContract, + setLifecycleCommand: AuthenticatedObject>) { // 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" } // If we have an default command, perform special processing: issued contracts can only be defaulted - // after the due date, and default/reset can only be done by the owner - val expectedOutputState: Lifecycle = setLifecycleCommand.value.lifecycle - val expectedInputState: Lifecycle - - expectedInputState = when (expectedOutputState) { - Lifecycle.DEFAULTED -> Lifecycle.NORMAL - Lifecycle.NORMAL -> Lifecycle.DEFAULTED - } + // after the due date, and default/reset can only be done by the beneficiary + val expectedInputLifecycle: Lifecycle = setLifecycleCommand.value.inverse + val expectedOutputLifecycle: Lifecycle = setLifecycleCommand.value.lifecycle // 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 + // TODO: Determining correct timestamp authority needs rework now that timestamping service is part of + // notary. val timestamp: TimestampCommand? = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A") - val expectedOutput: State

= input.copy(lifecycle = expectedOutputState) + val expectedOutput: State

= input.copy(lifecycle = expectedOutputLifecycle) requireThat { "there is a timestamp from the authority" by (timestamp != null) - "the due date has passed" by (timestamp?.after?.isBefore(deadline) ?: false) - "input state lifecycle is correct" by (input.lifecycle == expectedInputState) + "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.owner }.toSet() + val owningPubKeys = inputs.map { it.beneficiary }.toSet() val keysThatSigned = setLifecycleCommand.signers.toSet() requireThat { "the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys) @@ -419,16 +416,16 @@ class Obligation

: Contract { outputs: List>, tx: TransactionForContract, issueCommand: AuthenticatedObject>, - currency: Issued

, - issuer: Party) { + issued: Issued

, + 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 issuers. + // and that signatures are present for all obligors. - val inputAmount = inputs.sumObligationsOrZero(currency) - val outputAmount = outputs.sumObligations

() + val inputAmount: Amount

= inputs.sumObligationsOrZero(issued.product) + val outputAmount: Amount

= outputs.sumObligations

() requireThat { "the issue command has a nonce" by (issueCommand.value.nonce != 0L) - "output deposits are owned by a command signer" by (issuer in issueCommand.signingParties) + "output deposits are owned 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 } } @@ -448,24 +445,25 @@ class Obligation

: Contract { "all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL } } - val token = netState.issued - // Create two maps of balances from issuers to owners, one for input states, the other for output states. - val inputBalances = extractAmountsDue(token, inputs) - val outputBalances = extractAmountsDue(token, outputs) + 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 expected token" by (inputs.all { it.issuanceDef.issued == token }) - "all output states use the expected token" by (outputs.all { it.issuanceDef.issued == token }) + "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.owner }.union(inputs.map { it.issuer.owningKey }).toSet() + 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(involvedParties.intersect(command.signers).isNotEmpty()) { "any involved party has signed" } + 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" } @@ -479,19 +477,19 @@ class Obligation

: Contract { outputs: List>, tx: TransactionForContract, command: AuthenticatedObject>, - currency: Issued

, - issuer: Party, + issued: Issued

, + obligor: Party, key: IssuanceDefinition

) { val template = key.template - val inputAmount = inputs.sumObligationsOrNull

() ?: throw IllegalArgumentException("there is at least one obligation input for this group") - val outputAmount = outputs.sumObligationsOrZero(currency) + 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 contracts that are moving and fulfil our requirements + // Sum up all cash 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 // settlement amounts between the transaction inputs and outputs, and the two must match. No elimination is - // done of amounts paid in by each owner, as it's presumed the owners have enough sense to do that themselves. + // 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: // // Inputs: @@ -500,11 +498,11 @@ class Obligation

: Contract { // Outputs: // £1m cash owned by B // Commands: - // Settle (signed by B) + // 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 cashStates = tx.outStates.filterIsInstance>() + 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. @@ -512,8 +510,8 @@ class Obligation

: Contract { // attachments.mustHaveOneOf(key.acceptableCashContract) .filter { it.contract.legalContractReference in template.acceptableContracts } // Restrict the states to those of the correct issuance definition (this normally - // covers currency and issuer, but is opaque to us) - .filter { it.issuanceDef in template.acceptableIssuanceDefinitions } + // covers currency 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) @@ -525,12 +523,12 @@ class Obligation

: Contract { // this one. val moveCommands = tx.commands.select() var totalPenniesSettled = 0L - val requiredSigners = inputs.map { it.issuer.owningKey }.toSet() + val requiredSigners = inputs.map { it.obligor.owningKey }.toSet() - for ((owner, obligations) in inputs.groupBy { it.owner }) { - val settled = amountReceivedByOwner[owner]?.sumCashOrNull() + for ((beneficiary, obligations) in inputs.groupBy { it.beneficiary }) { + val settled = amountReceivedByOwner[beneficiary]?.sumCashOrNull() if (settled != null) { - val debt = obligations.sumObligationsOrZero(currency) + val debt = obligations.sumObligationsOrZero(issued) require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" } totalPenniesSettled += settled.quantity } @@ -542,19 +540,19 @@ class Obligation

: Contract { "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>() - .none { it.aggregateState.issued in template.acceptableIssuanceDefinitions }) - "amounts paid must match recipients to settle" by inputs.map { it.owner }.containsAll(amountReceivedByOwner.keys) - "signatures are present from all issuers" by command.signers.containsAll(requiredSigners) + .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 issuer ${issuer.name} the obligations after settlement balance" by - (inputAmount == outputAmount + Amount(totalPenniesSettled, currency)) + "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. * - * @param signer the party who will sign the transaction. Must be one of the issuer or owner. + * @param signer the party who will sign the transaction. Must be one of the obligor or beneficiary. * @param states two or more states, which must be compatible for bilateral netting (same issuance definitions, * and same parties involved). */ @@ -570,7 +568,9 @@ class Obligation

: Contract { "signer is in the state parties" by (signer in netState!!.partyKeys) } - tx.addOutputState(states.reduce { stateA, stateB -> stateA.net(stateB) }) + val out = states.reduce { stateA, stateB -> stateA.net(stateB) } + if (out.quantity > 0L) + tx.addOutputState(out) tx.addCommand(Commands.Net(NetType.PAYMENT), signer) } @@ -578,20 +578,20 @@ class Obligation

: Contract { * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. */ fun generateIssue(tx: TransactionBuilder, - issuer: Party, + obligor: Party, issuanceDef: StateTemplate

, pennies: Long, - owner: PublicKey, + beneficiary: PublicKey, notary: Party) { check(tx.inputStates().isEmpty()) check(tx.outputStates().map { it.data }.sumObligationsOrNull

() == null) - val aggregateState = IssuanceDefinition(issuer, issuanceDef) - tx.addOutputState(State(Lifecycle.NORMAL, issuer, issuanceDef, pennies, owner), notary) - tx.addCommand(Commands.Issue(aggregateState), issuer.owningKey) + val aggregateState = IssuanceDefinition(obligor, issuanceDef) + tx.addOutputState(State(Lifecycle.NORMAL, obligor, issuanceDef, pennies, beneficiary), notary) + tx.addCommand(Commands.Issue(aggregateState), obligor.owningKey) } fun generatePaymentNetting(tx: TransactionBuilder, - currency: Issued

, + issued: Issued

, notary: Party, vararg states: State

) { requireThat { @@ -599,20 +599,20 @@ class Obligation

: Contract { } val groups = states.groupBy { it.multilateralNetState } val partyLookup = HashMap() - val signers = states.map { it.owner }.union(states.map { it.issuer.owningKey }).toSet() + val signers = states.map { it.beneficiary }.union(states.map { it.obligor.owningKey }).toSet() // Create a lookup table of the party that each public key represents. - states.map { it.issuer }.forEach { partyLookup.put(it.owningKey, it) } + states.map { it.obligor }.forEach { partyLookup.put(it.owningKey, it) } for ((netState, groupStates) in groups) { // Extract the net balances - val netBalances = netAmountsDue(extractAmountsDue(currency, states.asIterable())) + val netBalances = netAmountsDue(extractAmountsDue(issued.product, states.asIterable())) netBalances // Convert the balances into obligation state objects .map { entry -> State(Lifecycle.NORMAL, partyLookup[entry.key.first]!!, - netState.issuanceDef, entry.value.quantity, entry.key.second) + netState.template, entry.value.quantity, entry.key.second) } // Add the new states to the TX .forEach { tx.addOutputState(it, notary) } @@ -647,7 +647,7 @@ class Obligation

: Contract { val outState = stateAndRef.state.data.copy(lifecycle = lifecycle) tx.addInputState(stateAndRef) tx.addOutputState(outState, notary) - partiesUsed.add(stateAndRef.state.data.owner) + partiesUsed.add(stateAndRef.state.data.beneficiary) } tx.addCommand(Commands.SetLifecycle(aggregateState, lifecycle), partiesUsed.distinct()) } @@ -659,23 +659,22 @@ class Obligation

: Contract { * 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 owner. + * encouraged that these all have the same beneficiary. */ fun generateSettle(tx: TransactionBuilder, statesAndRefs: Iterable>>, cashStatesAndRefs: Iterable>>, notary: Party) { val states = statesAndRefs.map { it.state } - val notary = states.first().notary - val obligationIssuer = states.first().data.issuer - val obligationOwner = states.first().data.owner + 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 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 issuer" by (statesAndRefs.all { it.state.data.issuer == obligationIssuer }) - "all obligation states have the same owner" by (statesAndRefs.all { it.state.data.owner == obligationOwner }) + "all obligation states have the same obligor" by (statesAndRefs.all { it.state.data.obligor == obligationIssuer }) + "all obligation states have the same beneficiary" by (statesAndRefs.all { it.state.data.beneficiary == obligationOwner }) } // TODO: A much better (but more complex) solution would be to have two iterators, one for obligations, @@ -684,24 +683,24 @@ class Obligation

: Contract { 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 obligationTotal: Amount

= states.map { it.data }.sumObligations

() + var obligationRemaining: Amount

= obligationTotal val cashSigners = HashSet() statesAndRefs.forEach { tx.addInputState(it) } - // Move the cash to the new owner + // Move the cash to the new beneficiary cashStatesAndRefs.forEach { if (obligationRemaining.quantity > 0L) { val cashState = it.state tx.addInputState(it) - if (obligationRemaining >= cashState.data.amount) { - tx.addOutputState(cashState.data.move(cashState.data.amount, obligationOwner), notary) - obligationRemaining -= cashState.data.amount + if (obligationRemaining >= cashState.data.productAmount) { + tx.addOutputState(cashState.data.move(cashState.data.productAmount, obligationOwner), notary) + obligationRemaining -= cashState.data.productAmount } else { - // Split the state in two, sending the change back to the previous owner + // 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.amount - obligationRemaining, cashState.data.owner), notary) + tx.addOutputState(cashState.data.move(cashState.data.productAmount - obligationRemaining, cashState.data.owner), notary) obligationRemaining -= Amount(0L, obligationRemaining.token) } cashSigners.add(cashState.data.owner) @@ -731,17 +730,17 @@ class Obligation

: Contract { /** - * Convert a list of settlement states into total from each issuer to a owner. + * Convert a list of settlement states into total from each obligor to a beneficiary. * - * @return a map of issuer/owner pairs to the balance due. + * @return a map of obligor/beneficiary pairs to the balance due. */ -fun

extractAmountsDue(currency: Issued

, states: Iterable>): Map, Amount>> { - val balances = HashMap, Amount>>() +fun

extractAmountsDue(product: P, states: Iterable>): Map, Amount

> { + val balances = HashMap, Amount

>() states.forEach { state -> - val key = Pair(state.issuer.owningKey, state.owner) - val balance = balances[key] ?: Amount(0L, currency) - balances[key] = balance + state.amount + val key = Pair(state.obligor.owningKey, state.beneficiary) + val balance = balances[key] ?: Amount(0L, product) + balances[key] = balance + state.productAmount } return balances @@ -750,12 +749,12 @@ fun

extractAmountsDue(currency: Issued

, states: Iterable netAmountsDue(balances: Map, Amount>>): Map, Amount>> { - val nettedBalances = HashMap, Amount>>() +fun

netAmountsDue(balances: Map, Amount

>): Map, Amount

> { + val nettedBalances = HashMap, Amount

>() balances.forEach { balance -> - val (issuer, owner) = balance.key - val oppositeKey = Pair(owner, issuer) + val (obligor, beneficiary) = balance.key + val oppositeKey = Pair(beneficiary, obligor) val opposite = (balances[oppositeKey] ?: Amount(0L, balance.value.token)) // Drop zero balances if (balance.value > opposite) { @@ -770,9 +769,9 @@ fun

netAmountsDue(balances: Map, Amount /** * Calculate the total balance movement for each party in the transaction, based off a summary of balances between - * each issuer and owner. + * each obligor and beneficiary. * - * @param balances payments due, indexed by issuer and owner. Zero balances are stripped from the map before being + * @param balances payments due, indexed by obligor and beneficiary. Zero balances are stripped from the map before being * returned. */ fun

sumAmountsDue(balances: Map, Amount

>): Map { @@ -785,11 +784,11 @@ fun

sumAmountsDue(balances: Map, Amount

>): Map } for ((key, amount) in balances) { - val (issuer, owner) = key - // Subtract it from the issuer - sum[issuer] = sum[issuer]!! - amount.quantity - // Add it to the owner - sum[owner] = sum[owner]!! + amount.quantity + val (obligor, beneficiary) = key + // Subtract it from the obligor + sum[obligor] = sum[obligor]!! - amount.quantity + // Add it to the beneficiary + sum[beneficiary] = sum[beneficiary]!! + amount.quantity } // Strip zero balances @@ -807,13 +806,14 @@ fun

sumAmountsDue(balances: Map, Amount

>): Map /** 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. */ -fun

Iterable.sumObligations() = filterIsInstance>().map { it.amount }.sumOrThrow() +fun

Iterable.sumObligations(): Amount

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

Iterable.sumObligationsOrNull() +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. */ -fun

Iterable.sumObligationsOrZero(currency: Issued

) - = filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(currency) +fun

Iterable.sumObligationsOrZero(product: P): Amount

+ = filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(product) diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/testing/ExperimentalTestUtils.kt b/experimental/src/main/kotlin/com/r3corda/contracts/testing/ExperimentalTestUtils.kt index d88cb52867..112e5f03d6 100644 --- a/experimental/src/main/kotlin/com/r3corda/contracts/testing/ExperimentalTestUtils.kt +++ b/experimental/src/main/kotlin/com/r3corda/contracts/testing/ExperimentalTestUtils.kt @@ -7,6 +7,7 @@ import com.r3corda.core.contracts.Issued import com.r3corda.core.crypto.NullPublicKey import com.r3corda.core.crypto.Party import com.r3corda.core.testing.MINI_CORP +import com.r3corda.core.testing.TEST_TX_TIME import com.r3corda.core.utilities.nonEmptySetOf import java.security.PublicKey import java.time.Instant @@ -14,20 +15,23 @@ import java.util.* object JavaExperimental { @JvmStatic fun at(state: Obligation.State, dueBefore: Instant) = state.copy(template = state.template.copy(dueBefore = dueBefore)) - @JvmStatic fun between(state: Obligation.State, parties: Pair) = state.copy(issuer = parties.first, owner = parties.second) - @JvmStatic fun ownedBy(state: Obligation.State, owner: PublicKey) = state.copy(owner = owner) - @JvmStatic fun issuedBy(state: Obligation.State, party: Party) = state.copy(issuer = party) + @JvmStatic fun at(issuanceDef: Obligation.IssuanceDefinition, dueBefore: Instant) = issuanceDef.copy(template = issuanceDef.template.copy(dueBefore = dueBefore)) + @JvmStatic fun between(state: Obligation.State, parties: Pair) = state.copy(obligor = parties.first, beneficiary = parties.second) + @JvmStatic fun ownedBy(state: Obligation.State, owner: PublicKey) = state.copy(beneficiary = owner) + @JvmStatic fun issuedBy(state: Obligation.State, party: Party) = state.copy(obligor = party) + // Allows you to write 100.DOLLARS.OBLIGATION @JvmStatic fun OBLIGATION_DEF(issued: Issued) - = Obligation.StateTemplate(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(issued), Instant.parse("2020-01-01T17:00:00Z")) + = Obligation.StateTemplate(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(issued), TEST_TX_TIME) @JvmStatic fun OBLIGATION(amount: Amount>) = Obligation.State(Obligation.Lifecycle.NORMAL, MINI_CORP, - OBLIGATION_DEF(amount.token), amount.quantity, NullPublicKey) + OBLIGATION_DEF(amount.token), amount.quantity, NullPublicKey) } infix fun Obligation.State.`at`(dueBefore: Instant) = JavaExperimental.at(this, dueBefore) +infix fun Obligation.IssuanceDefinition.`at`(dueBefore: Instant) = JavaExperimental.at(this, dueBefore) infix fun Obligation.State.`between`(parties: Pair) = JavaExperimental.between(this, parties) infix fun Obligation.State.`owned by`(owner: PublicKey) = JavaExperimental.ownedBy(this, owner) infix fun Obligation.State.`issued by`(party: Party) = JavaExperimental.issuedBy(this, party) -// Allows you to write 100.DOLLARS.OBLIGATION +/** Allows you to write 100.DOLLARS.CASH */ val Issued.OBLIGATION_DEF: Obligation.StateTemplate get() = JavaExperimental.OBLIGATION_DEF(this) val Amount>.OBLIGATION: Obligation.State get() = JavaExperimental.OBLIGATION(this) diff --git a/experimental/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt b/experimental/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt index 84be8b37df..6f264bc5df 100644 --- a/experimental/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt +++ b/experimental/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt @@ -5,11 +5,11 @@ import com.r3corda.contracts.Obligation.Lifecycle import com.r3corda.contracts.testing.* import com.r3corda.core.contracts.* import com.r3corda.core.crypto.SecureHash -import com.r3corda.core.seconds 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.util.* import kotlin.test.* @@ -25,15 +25,15 @@ class ObligationTests { val sixPm = Instant.parse("2016-01-01T18:00:00.00Z") val notary = MEGA_CORP val megaCorpDollarSettlement = Obligation.StateTemplate(trustedCashContract, megaIssuedDollars, fivePm) - val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuanceDefinitions = megaIssuedPounds) + val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuedProducts = megaIssuedPounds) val inState = Obligation.State( lifecycle = Lifecycle.NORMAL, - issuer = MEGA_CORP, + obligor = MEGA_CORP, template = megaCorpDollarSettlement, quantity = 1000.DOLLARS.quantity, - owner = DUMMY_PUBKEY_1 + beneficiary = DUMMY_PUBKEY_1 ) - val outState = inState.copy(owner = DUMMY_PUBKEY_2) + val outState = inState.copy(beneficiary = DUMMY_PUBKEY_2) private fun obligationTestRoots(group: TransactionGroupDSL>) = group.Roots() .transaction(oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `with notary` DUMMY_NOTARY label "Alice's $1,000,000 obligation to Bob") @@ -97,9 +97,9 @@ class ObligationTests { transaction { output { Obligation.State( - issuer = MINI_CORP, + obligor = MINI_CORP, quantity = 1000.DOLLARS.quantity, - owner = DUMMY_PUBKEY_1, + beneficiary = DUMMY_PUBKEY_1, template = megaCorpDollarSettlement ) } @@ -114,12 +114,12 @@ class ObligationTests { // Test generation works. val ptx = TransactionType.General.Builder(DUMMY_NOTARY) Obligation().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, - owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) + beneficiary = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) assertTrue(ptx.inputStates().isEmpty()) val s = ptx.outputStates()[0].data as Obligation.State - assertEquals(100.DOLLARS `issued by` MEGA_CORP.ref(1), s.amount) - assertEquals(MINI_CORP, s.issuer) - assertEquals(DUMMY_PUBKEY_1, s.owner) + assertEquals(100.DOLLARS, s.amount) + assertEquals(MINI_CORP, s.obligor) + assertEquals(DUMMY_PUBKEY_1, s.beneficiary) assertTrue(ptx.commands()[0].value is Obligation.Commands.Issue<*>) assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0]) @@ -131,7 +131,7 @@ class ObligationTests { // Move fails: not allowed to summon money. tweak { arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) } - this `fails requirement` "at issuer MegaCorp the amounts balance" + this `fails requirement` "at obligor MegaCorp the amounts balance" } // Issue works. @@ -189,18 +189,46 @@ class ObligationTests { @Test(expected = IllegalStateException::class) fun `reject issuance with inputs`() { // Issue some obligation - var ptx = TransactionType.General.Builder(DUMMY_NOTARY) - - Obligation().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, - owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) - ptx.signWith(MINI_CORP_KEY) - val tx = ptx.toSignedTransaction() + val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { + Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, + beneficiary = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + signWith(MINI_CORP_KEY) + }.toSignedTransaction() // Include the previously issued obligation in a new issuance command - ptx = TransactionType.General.Builder(DUMMY_NOTARY) + val ptx = TransactionType.General.Builder(DUMMY_NOTARY) ptx.addInputState(tx.tx.outRef>(0)) Obligation().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, - owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + beneficiary = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + } + + /** Test generating a transaction to net two obligations of the same size, and therefore there are no outputs. */ + @Test + fun `generate close-out net transaction`() { + 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().generateCloseOutNetting(this, ALICE_PUBKEY, obligationAliceToBob, obligationBobToAlice) + signWith(ALICE_KEY) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction().tx + assertEquals(0, tx.outputs.size) + } + + /** Test generating a transaction to net two obligations of the different sizes, and confirm the balance is correct. */ + @Test + fun `generate close-out net transaction with remainder`() { + val obligationAliceToBob = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) + val obligationBobToAlice = oneMillionDollars.OBLIGATION `between` Pair(BOB, ALICE_PUBKEY) + val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { + Obligation().generateCloseOutNetting(this, ALICE_PUBKEY, obligationAliceToBob, obligationBobToAlice) + signWith(ALICE_KEY) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction().tx + assertEquals(1, tx.outputs.size) + + val actual = tx.outputs[0].data + assertEquals((1000000.DOLLARS `issued by` defaultIssuer).OBLIGATION `between` Pair(ALICE, BOB_PUBKEY), actual) } /** Test generating a transaction to net two obligations of the same size, and therefore there are no outputs. */ @@ -208,9 +236,13 @@ class ObligationTests { fun `generate payment net transaction`() { val obligationAliceToBob = oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) val obligationBobToAlice = oneMillionDollars.OBLIGATION `between` Pair(BOB, ALICE_PUBKEY) - val ptx = TransactionType.General.Builder(DUMMY_NOTARY) - Obligation().generatePaymentNetting(ptx, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) - assertEquals(0, ptx.outputStates().size) + val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { + Obligation().generatePaymentNetting(this, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) + signWith(ALICE_KEY) + signWith(BOB_KEY) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction().tx + assertEquals(0, tx.outputs.size) } /** Test generating a transaction to two obligations, where one is bigger than the other and therefore there is a remainder. */ @@ -218,25 +250,27 @@ class ObligationTests { fun `generate payment net transaction with remainder`() { val obligationAliceToBob = oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION `between` Pair(BOB, ALICE_PUBKEY) - val ptx = TransactionType.General.Builder(DUMMY_NOTARY) - Obligation().generatePaymentNetting(ptx, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) - assertEquals(1, ptx.outputStates().size) - val out = ptx.outputStates().single().data as Obligation.State - assertEquals(1000000.DOLLARS.quantity, out.quantity) - assertEquals(BOB, out.issuer) - assertEquals(ALICE_PUBKEY, out.owner) + val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { + Obligation().generatePaymentNetting(this, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) + signWith(ALICE_KEY) + signWith(BOB_KEY) + }.toSignedTransaction().tx + assertEquals(1, tx.outputs.size) + val expected = obligationBobToAlice.copy(quantity = obligationBobToAlice.quantity - obligationAliceToBob.quantity) + val actual = tx.outputs[0].data + assertEquals(expected, actual) } /** Test generating a transaction to mark outputs as having defaulted. */ @Test fun `generate set lifecycle`() { - // Issue some obligation - val dueBefore = Instant.parse("2010-01-01T17:00:00Z") + // We don't actually verify the states, this is just here to make things look sensible + val dueBefore = TEST_TX_TIME - Duration.ofDays(7) // Generate a transaction issuing the obligation var tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement.copy(dueBefore = dueBefore), 100.DOLLARS.quantity, - owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + beneficiary = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) signWith(MINI_CORP_KEY) }.toSignedTransaction() var stateAndRef = tx.tx.outRef>(0) @@ -245,44 +279,47 @@ class ObligationTests { tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateSetLifecycle(this, listOf(stateAndRef), Obligation.Lifecycle.DEFAULTED, DUMMY_NOTARY) signWith(MINI_CORP_KEY) - }.toSignedTransaction(false) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction() assertEquals(1, tx.tx.outputs.size) assertEquals(stateAndRef.state.data.copy(lifecycle = Obligation.Lifecycle.DEFAULTED), tx.tx.outputs[0].data) + assertTrue(tx.verify().isEmpty()) // And set it back stateAndRef = tx.tx.outRef>(0) tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateSetLifecycle(this, listOf(stateAndRef), Obligation.Lifecycle.NORMAL, DUMMY_NOTARY) signWith(MINI_CORP_KEY) - }.toSignedTransaction(false) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction() assertEquals(1, tx.tx.outputs.size) assertEquals(stateAndRef.state.data.copy(lifecycle = Obligation.Lifecycle.NORMAL), tx.tx.outputs[0].data) + assertTrue(tx.verify().isEmpty()) } /** Test generating a transaction to settle an obligation. */ @Test fun `generate settlement transaction`() { - var ptx: TransactionBuilder - - // Generate a transaction to issue the cash we'll need - ptx = TransactionType.General.Builder(DUMMY_NOTARY) - Cash().generateIssue(ptx, 100.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY) - ptx.signWith(MEGA_CORP_KEY) - val cashTx = ptx.toSignedTransaction().tx + val cashTx = TransactionType.General.Builder(DUMMY_NOTARY).apply { + Cash().generateIssue(this, 100.DOLLARS `issued by` defaultIssuer, MINI_CORP_PUBKEY, DUMMY_NOTARY) + signWith(MEGA_CORP_KEY) + }.toSignedTransaction().tx // Generate a transaction issuing the obligation - ptx = TransactionType.General.Builder(DUMMY_NOTARY) - Obligation().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, - owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) - ptx.signWith(MINI_CORP_KEY) - val obligationTx = ptx.toSignedTransaction().tx + val obligationTx = TransactionType.General.Builder(DUMMY_NOTARY).apply { + Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, + beneficiary = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + signWith(MINI_CORP_KEY) + }.toSignedTransaction().tx // Now generate a transaction settling the obligation - ptx = TransactionType.General.Builder(DUMMY_NOTARY) - val stateAndRef = obligationTx.outRef>(0) - Obligation().generateSettle(ptx, listOf(obligationTx.outRef(0)), listOf(cashTx.outRef(0)), DUMMY_NOTARY) - assertEquals(2, ptx.inputStates().size) - assertEquals(1, ptx.outputStates().size) + val settleTx = TransactionType.General.Builder(DUMMY_NOTARY).apply { + Obligation().generateSettle(this, listOf(obligationTx.outRef(0)), listOf(cashTx.outRef(0)), DUMMY_NOTARY) + signWith(DUMMY_NOTARY_KEY) + signWith(MINI_CORP_KEY) + }.toSignedTransaction().tx + assertEquals(2, settleTx.inputs.size) + assertEquals(1, settleTx.outputs.size) } @Test @@ -397,7 +434,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 } - arg(ALICE_PUBKEY) { Obligation.Commands.Settle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), oneMillionDollars) } + arg(ALICE_PUBKEY) { Obligation.Commands.Settle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Amount(oneMillionDollars.quantity, USD)) } arg(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } } }.verify() @@ -415,13 +452,30 @@ class ObligationTests { } }.expectFailureOfTx(1, "there is a timestamp from the authority") - // Try defaulting an obligation + // Try defaulting an obligation due in the future + val pastTestTime = TEST_TX_TIME - Duration.ofDays(7) + val futureTestTime = TEST_TX_TIME + Duration.ofDays(7) transactionGroupFor>() { - obligationTestRoots(this) + roots { + transaction(oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `at` futureTestTime `with notary` DUMMY_NOTARY label "Alice's $1,000,000 obligation to Bob") + } 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 = Obligation.Lifecycle.DEFAULTED) } - arg(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Obligation.Lifecycle.DEFAULTED) } + output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `at` futureTestTime).copy(lifecycle = Obligation.Lifecycle.DEFAULTED) } + arg(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF) `at` futureTestTime, Obligation.Lifecycle.DEFAULTED) } + timestamp(TEST_TX_TIME) + } + }.expectFailureOfTx(1, "the due date has passed") + + // Try defaulting an obligation that is now in the past + transactionGroupFor>() { + roots { + transaction(oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `at` pastTestTime `with notary` DUMMY_NOTARY label "Alice's $1,000,000 obligation to Bob") + } + 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) `at` pastTestTime).copy(lifecycle = Obligation.Lifecycle.DEFAULTED) } + arg(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF) `at` pastTestTime, Obligation.Lifecycle.DEFAULTED) } timestamp(TEST_TX_TIME) } }.verify() @@ -434,12 +488,12 @@ class ObligationTests { arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) } tweak { input { inState } - for (i in 1..4) output { inState.copy(quantity = inState.quantity / 4) } + repeat(4) { output { inState.copy(quantity = inState.quantity / 4) } } this.accepts() } // Merging 4 inputs into 2 outputs works. tweak { - for (i in 1..4) input { inState.copy(quantity = inState.quantity / 4) } + repeat(4) { input { inState.copy(quantity = inState.quantity / 4) } } output { inState.copy(quantity = inState.quantity / 2) } output { inState.copy(quantity = inState.quantity / 2) } this.accepts() @@ -474,7 +528,7 @@ class ObligationTests { transaction { input { inState } output { outState `issued by` MINI_CORP } - this `fails requirement` "at issuer MegaCorp the amounts balance" + this `fails requirement` "at obligor MegaCorp the amounts balance" } // Can't mix currencies. transaction { @@ -489,7 +543,7 @@ class ObligationTests { inState.copy( quantity = 15000, template = megaCorpPoundSettlement, - owner = DUMMY_PUBKEY_2 + beneficiary = DUMMY_PUBKEY_2 ) } output { outState.copy(quantity = 115000) } @@ -502,7 +556,7 @@ class ObligationTests { output { outState } arg(DUMMY_PUBKEY_1) {Obligation.Commands.Move(inState.issuanceDef) } arg(DUMMY_PUBKEY_1) {Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) } - this `fails requirement` "at issuer MiniCorp the amounts balance" + this `fails requirement` "at obligor MiniCorp the amounts balance" } } @@ -514,13 +568,13 @@ class ObligationTests { output { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) } tweak { - arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 100.DOLLARS `issued by` defaultIssuer) } + arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 100.DOLLARS) } arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) } this `fails requirement` "the amounts balance" } tweak { - arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 200.DOLLARS `issued by` defaultIssuer) } + arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 200.DOLLARS) } this `fails requirement` "required com.r3corda.contracts.Obligation.Commands.Move command" tweak { @@ -539,15 +593,15 @@ class ObligationTests { arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) } - this `fails requirement` "at issuer MegaCorp the amounts balance" + this `fails requirement` "at obligor MegaCorp the amounts balance" - arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 200.DOLLARS `issued by` defaultIssuer) } + arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 200.DOLLARS) } tweak { - arg(MINI_CORP_PUBKEY) { Obligation.Commands.Exit((inState `issued by` MINI_CORP).issuanceDef, 0.DOLLARS `issued by` defaultIssuer) } + arg(MINI_CORP_PUBKEY) { Obligation.Commands.Exit((inState `issued by` MINI_CORP).issuanceDef, 0.DOLLARS) } arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) } - this `fails requirement` "at issuer MiniCorp the amounts balance" + this `fails requirement` "at obligor MiniCorp the amounts balance" } - arg(MINI_CORP_PUBKEY) { Obligation.Commands.Exit((inState `issued by` MINI_CORP).issuanceDef, 200.DOLLARS `issued by` defaultIssuer) } + arg(MINI_CORP_PUBKEY) { Obligation.Commands.Exit((inState `issued by` MINI_CORP).issuanceDef, 200.DOLLARS) } arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) } this.accepts() } @@ -562,19 +616,19 @@ class ObligationTests { // Can't merge them together. tweak { - output { inState.copy(owner = DUMMY_PUBKEY_2, quantity = 200000L) } - this `fails requirement` "at issuer MegaCorp the amounts balance" + output { inState.copy(beneficiary = DUMMY_PUBKEY_2, quantity = 200000L) } + this `fails requirement` "at obligor MegaCorp the amounts balance" } // Missing MiniCorp deposit tweak { - output { inState.copy(owner = DUMMY_PUBKEY_2) } - output { inState.copy(owner = DUMMY_PUBKEY_2) } - this `fails requirement` "at issuer MegaCorp the amounts balance" + output { inState.copy(beneficiary = DUMMY_PUBKEY_2) } + output { inState.copy(beneficiary = DUMMY_PUBKEY_2) } + this `fails requirement` "at obligor MegaCorp the amounts balance" } // This works. - output { inState.copy(owner = DUMMY_PUBKEY_2) } - output { inState.copy(owner = DUMMY_PUBKEY_2) `issued by` MINI_CORP } + output { inState.copy(beneficiary = DUMMY_PUBKEY_2) } + output { inState.copy(beneficiary = DUMMY_PUBKEY_2) `issued by` MINI_CORP } arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) } arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) } this.accepts() @@ -631,7 +685,7 @@ class ObligationTests { // States must not be nettable if the trusted issuers differ val miniCorpIssuer = nonEmptySetOf(Issued(MINI_CORP.ref(1), USD)) assertNotEquals(fiveKDollarsFromMegaToMega.bilateralNetState, - fiveKDollarsFromMegaToMega.copy(template = megaCorpDollarSettlement.copy(acceptableIssuanceDefinitions = miniCorpIssuer)).bilateralNetState) + fiveKDollarsFromMegaToMega.copy(template = megaCorpDollarSettlement.copy(acceptableIssuedProducts = miniCorpIssuer)).bilateralNetState) } @Test(expected = IllegalStateException::class) @@ -686,7 +740,7 @@ class ObligationTests { 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(defaultUsd, listOf(fiveKDollarsFromMegaToMini)) + val actual = extractAmountsDue(USD, listOf(fiveKDollarsFromMegaToMini)) assertEquals(expected, actual) } @@ -697,8 +751,8 @@ class ObligationTests { Pair(Pair(ALICE_PUBKEY, BOB_PUBKEY), Amount(100000000, GBP)), Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP)) ) - val expected: Map = emptyMap() // Zero balances are stripped before returning - val actual = sumAmountsDue(balanced) + val expected: Map, Amount> = emptyMap() // Zero balances are stripped before returning + val actual = netAmountsDue(balanced) assertEquals(expected, actual) } @@ -706,11 +760,11 @@ class ObligationTests { fun `netting difference balances due between parties`() { // Now try it with two balances, which cancel each other out val balanced = mapOf( - Pair(Pair(ALICE_PUBKEY, BOB_PUBKEY), Amount(100000000, GBP) `issued by` defaultIssuer), - Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(200000000, GBP) `issued by` defaultIssuer) + Pair(Pair(ALICE_PUBKEY, BOB_PUBKEY), Amount(100000000, GBP)), + Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(200000000, GBP)) ) val expected = mapOf( - Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP) `issued by` defaultIssuer) + Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP)) ) var actual = netAmountsDue(balanced) assertEquals(expected, actual) From 228513671d1728938809f46a009ee145525148a8 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Thu, 23 Jun 2016 15:03:13 +0100 Subject: [PATCH 2/2] Move Obligation contract into contracts module --- .../com/r3corda/contracts/Obligation.kt | 0 .../r3corda/contracts/testing/TestUtils.kt | 25 +++++++++++++ .../com/r3corda/contracts/ObligationTests.kt | 0 .../testing/ExperimentalTestUtils.kt | 37 ------------------- 4 files changed, 25 insertions(+), 37 deletions(-) rename {experimental => contracts}/src/main/kotlin/com/r3corda/contracts/Obligation.kt (100%) rename {experimental => contracts}/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt (100%) delete mode 100644 experimental/src/main/kotlin/com/r3corda/contracts/testing/ExperimentalTestUtils.kt diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/Obligation.kt b/contracts/src/main/kotlin/com/r3corda/contracts/Obligation.kt similarity index 100% rename from experimental/src/main/kotlin/com/r3corda/contracts/Obligation.kt rename to contracts/src/main/kotlin/com/r3corda/contracts/Obligation.kt diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt index d7f3dfc526..f0f0ef36f0 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt @@ -14,7 +14,11 @@ import com.r3corda.core.contracts.TransactionState import com.r3corda.core.crypto.NullPublicKey import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.generateKeyPair +import com.r3corda.core.testing.MINI_CORP +import com.r3corda.core.testing.TEST_TX_TIME +import com.r3corda.core.utilities.nonEmptySetOf import java.security.PublicKey +import java.time.Instant import java.util.* // In a real system this would be a persistent map of hash to bytecode and we'd instantiate the object as needed inside @@ -56,6 +60,12 @@ object JavaTestHelpers { @JvmStatic fun withNotary(state: Cash.State, notary: Party) = TransactionState(state, notary) @JvmStatic fun withDeposit(state: Cash.State, deposit: PartyAndReference) = state.copy(amount = state.amount.copy(token = state.amount.token.copy(issuer = deposit))) + @JvmStatic fun at(state: Obligation.State, dueBefore: Instant) = state.copy(template = state.template.copy(dueBefore = dueBefore)) + @JvmStatic fun at(issuanceDef: Obligation.IssuanceDefinition, dueBefore: Instant) = issuanceDef.copy(template = issuanceDef.template.copy(dueBefore = dueBefore)) + @JvmStatic fun between(state: Obligation.State, parties: Pair) = state.copy(obligor = parties.first, beneficiary = parties.second) + @JvmStatic fun ownedBy(state: Obligation.State, owner: PublicKey) = state.copy(beneficiary = owner) + @JvmStatic fun issuedBy(state: Obligation.State, party: Party) = state.copy(obligor = party) + @JvmStatic fun ownedBy(state: CommercialPaper.State, owner: PublicKey) = state.copy(owner = owner) @JvmStatic fun withNotary(state: CommercialPaper.State, notary: Party) = TransactionState(state, notary) @JvmStatic fun ownedBy(state: ICommercialPaperState, new_owner: PublicKey) = state.withOwner(new_owner) @@ -66,6 +76,12 @@ object JavaTestHelpers { Amount>(amount.quantity, Issued(DUMMY_CASH_ISSUER, amount.token)), NullPublicKey) @JvmStatic fun STATE(amount: Amount>) = Cash.State(amount, NullPublicKey) + + // Allows you to write 100.DOLLARS.OBLIGATION + @JvmStatic fun OBLIGATION_DEF(issued: Issued) + = Obligation.StateTemplate(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(issued), TEST_TX_TIME) + @JvmStatic fun OBLIGATION(amount: Amount>) = Obligation.State(Obligation.Lifecycle.NORMAL, MINI_CORP, + OBLIGATION_DEF(amount.token), amount.quantity, NullPublicKey) } @@ -75,6 +91,12 @@ infix fun Cash.State.`issued by`(deposit: PartyAndReference) = JavaTestHelpers.i infix fun Cash.State.`with notary`(notary: Party) = JavaTestHelpers.withNotary(this, notary) infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = JavaTestHelpers.withDeposit(this, deposit) +infix fun Obligation.State.`at`(dueBefore: Instant) = JavaTestHelpers.at(this, dueBefore) +infix fun Obligation.IssuanceDefinition.`at`(dueBefore: Instant) = JavaTestHelpers.at(this, dueBefore) +infix fun Obligation.State.`between`(parties: Pair) = JavaTestHelpers.between(this, parties) +infix fun Obligation.State.`owned by`(owner: PublicKey) = JavaTestHelpers.ownedBy(this, owner) +infix fun Obligation.State.`issued by`(party: Party) = JavaTestHelpers.issuedBy(this, party) + infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = JavaTestHelpers.ownedBy(this, owner) infix fun CommercialPaper.State.`with notary`(notary: Party) = JavaTestHelpers.withNotary(this, notary) infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = JavaTestHelpers.ownedBy(this, new_owner) @@ -87,3 +109,6 @@ val DUMMY_CASH_ISSUER = Party("Snake Oil Issuer", DUMMY_CASH_ISSUER_KEY.public). val Amount.CASH: Cash.State get() = JavaTestHelpers.CASH(this) val Amount>.STATE: Cash.State get() = JavaTestHelpers.STATE(this) +/** Allows you to write 100.DOLLARS.CASH */ +val Issued.OBLIGATION_DEF: Obligation.StateTemplate get() = JavaTestHelpers.OBLIGATION_DEF(this) +val Amount>.OBLIGATION: Obligation.State get() = JavaTestHelpers.OBLIGATION(this) diff --git a/experimental/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt similarity index 100% rename from experimental/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt rename to contracts/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/testing/ExperimentalTestUtils.kt b/experimental/src/main/kotlin/com/r3corda/contracts/testing/ExperimentalTestUtils.kt deleted file mode 100644 index 112e5f03d6..0000000000 --- a/experimental/src/main/kotlin/com/r3corda/contracts/testing/ExperimentalTestUtils.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.r3corda.contracts.testing - -import com.r3corda.contracts.Obligation -import com.r3corda.contracts.cash.Cash -import com.r3corda.core.contracts.Amount -import com.r3corda.core.contracts.Issued -import com.r3corda.core.crypto.NullPublicKey -import com.r3corda.core.crypto.Party -import com.r3corda.core.testing.MINI_CORP -import com.r3corda.core.testing.TEST_TX_TIME -import com.r3corda.core.utilities.nonEmptySetOf -import java.security.PublicKey -import java.time.Instant -import java.util.* - -object JavaExperimental { - @JvmStatic fun at(state: Obligation.State, dueBefore: Instant) = state.copy(template = state.template.copy(dueBefore = dueBefore)) - @JvmStatic fun at(issuanceDef: Obligation.IssuanceDefinition, dueBefore: Instant) = issuanceDef.copy(template = issuanceDef.template.copy(dueBefore = dueBefore)) - @JvmStatic fun between(state: Obligation.State, parties: Pair) = state.copy(obligor = parties.first, beneficiary = parties.second) - @JvmStatic fun ownedBy(state: Obligation.State, owner: PublicKey) = state.copy(beneficiary = owner) - @JvmStatic fun issuedBy(state: Obligation.State, party: Party) = state.copy(obligor = party) - - // Allows you to write 100.DOLLARS.OBLIGATION - @JvmStatic fun OBLIGATION_DEF(issued: Issued) - = Obligation.StateTemplate(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(issued), TEST_TX_TIME) - @JvmStatic fun OBLIGATION(amount: Amount>) = Obligation.State(Obligation.Lifecycle.NORMAL, MINI_CORP, - OBLIGATION_DEF(amount.token), amount.quantity, NullPublicKey) -} -infix fun Obligation.State.`at`(dueBefore: Instant) = JavaExperimental.at(this, dueBefore) -infix fun Obligation.IssuanceDefinition.`at`(dueBefore: Instant) = JavaExperimental.at(this, dueBefore) -infix fun Obligation.State.`between`(parties: Pair) = JavaExperimental.between(this, parties) -infix fun Obligation.State.`owned by`(owner: PublicKey) = JavaExperimental.ownedBy(this, owner) -infix fun Obligation.State.`issued by`(party: Party) = JavaExperimental.issuedBy(this, party) - -/** Allows you to write 100.DOLLARS.CASH */ -val Issued.OBLIGATION_DEF: Obligation.StateTemplate get() = JavaExperimental.OBLIGATION_DEF(this) -val Amount>.OBLIGATION: Obligation.State get() = JavaExperimental.OBLIGATION(this)