diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index 5b37c86887..856fac0635 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -165,10 +165,10 @@ public class JavaCommercialPaper implements Contract { if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Issue) { State output = single(outputs); if (!inputs.isEmpty()) { - throw new IllegalStateException("Failed Requirement: there is no input state"); + throw new IllegalStateException("Failed Requirement: output values sum to more than the inputs"); } if (output.faceValue.getQuantity() == 0) { - throw new IllegalStateException("Failed Requirement: the face value is not zero"); + throw new IllegalStateException("Failed Requirement: output values sum to more than the inputs"); } TimestampCommand timestampCommand = tx.getTimestampByName("Notary Service"); @@ -182,7 +182,7 @@ public class JavaCommercialPaper implements Contract { } if (!cmd.getSigners().contains(output.issuance.getParty().getOwningKey())) { - throw new IllegalStateException("Failed Requirement: the issuance is signed by the claimed issuer of the paper"); + throw new IllegalStateException("Failed Requirement: output states are issued by a command signer"); } } else { // Everything else (Move, Redeem) requires inputs (they are not first to be actioned) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index 27d41a59df..0299d65075 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -115,13 +115,13 @@ class CommercialPaper : Contract { val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped") requireThat { // Don't allow people to issue commercial paper under other entities identities. - "the issuance is signed by the claimed issuer of the paper" by + "output states are issued by a command signer" by (output.issuance.party.owningKey in command.signers) - "the face value is not zero" by (output.faceValue.quantity > 0) + "output values sum to more than the inputs" by (output.faceValue.quantity > 0) "the maturity date is not in the past" by (time < output.maturityDate) // Don't allow an existing CP state to be replaced by this issuance. // TODO: Consider how to handle the case of mistaken issuances, or other need to patch. - "there is no input state" by inputs.isEmpty() + "output values sum to more than the inputs" by inputs.isEmpty() } } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt index 59ae41fdbc..60a2638789 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt @@ -116,7 +116,7 @@ abstract class FungibleAsset : Contract { val assetCommands = tx.commands.select() 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 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) } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt index 1aa3538461..6021c10e1f 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Obligation.kt @@ -422,7 +422,7 @@ class Obligation

: Contract { 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 (obligor in issueCommand.signingParties) + "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 } } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index 0f56e5f8d2..df4cb63a3f 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -135,7 +135,7 @@ class CommercialPaperTestsGeneric { output { thisTest.getPaper() } command(DUMMY_PUBKEY_1) { thisTest.getIssueCommand() } timestamp(TEST_TX_TIME) - this `fails with` "signed by the claimed issuer" + this `fails with` "output states are issued by a command signer" } } @@ -145,7 +145,7 @@ class CommercialPaperTestsGeneric { output { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) } command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } timestamp(TEST_TX_TIME) - this `fails with` "face value is not zero" + this `fails with` "output values sum to more than the inputs" } } @@ -166,7 +166,7 @@ class CommercialPaperTestsGeneric { output { thisTest.getPaper() } command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } timestamp(TEST_TX_TIME) - this `fails with` "there is no input state" + this `fails with` "output values sum to more than the inputs" } } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt index a4adf2afbb..07922b937a 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt @@ -78,7 +78,7 @@ class CashTests { transaction { output { outState } command(DUMMY_PUBKEY_1) { Cash.Commands.Issue() } - this `fails with` "output deposits are owned by a command signer" + this `fails with` "output states are issued by a command signer" } transaction { output { diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt index 5eec7d84c9..023f28c34b 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt @@ -38,9 +38,9 @@ class ObligationTests { group: LedgerDSL ) = group.apply { unverifiedTransaction { - output("Alice's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY)) - output("Bob's $1,000,000 obligation to Alice", oneMillionDollars.OBLIGATION `between` Pair(BOB, ALICE_PUBKEY)) - output("MegaCorp's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, BOB_PUBKEY)) + output("Alice's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)) + output("Bob's $1,000,000 obligation to Alice", oneMillionDollars.OBLIGATION between Pair(BOB, ALICE_PUBKEY)) + output("MegaCorp's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, BOB_PUBKEY)) output("Alice's $1,000,000", 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY) } } @@ -96,7 +96,7 @@ class ObligationTests { transaction { output { outState } command(DUMMY_PUBKEY_1) { Obligation.Commands.Issue(outState.issuanceDef) } - this `fails with` "output deposits are owned by a command signer" + this `fails with` "output states are issued by a command signer" } transaction { output { @@ -212,8 +212,8 @@ class ObligationTests { /** 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 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) @@ -225,8 +225,8 @@ class ObligationTests { /** 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 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) @@ -235,14 +235,14 @@ class ObligationTests { assertEquals(1, tx.outputs.size) val actual = tx.outputs[0].data - assertEquals((1000000.DOLLARS `issued by` defaultIssuer).OBLIGATION `between` Pair(ALICE, BOB_PUBKEY), actual) + 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. */ @Test fun `generate payment net transaction`() { - val obligationAliceToBob = oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) - val obligationBobToAlice = oneMillionDollars.OBLIGATION `between` Pair(BOB, ALICE_PUBKEY) + 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().generatePaymentNetting(this, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) signWith(ALICE_KEY) @@ -255,8 +255,8 @@ class ObligationTests { /** Test generating a transaction to two obligations, where one is bigger than the other and therefore there is a remainder. */ @Test 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 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().generatePaymentNetting(this, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) signWith(ALICE_KEY) @@ -353,7 +353,7 @@ class ObligationTests { input("Alice's $1,000,000 obligation to Bob") input("Bob's $1,000,000 obligation to Alice") input("MegaCorp's $1,000,000 obligation to Bob") - output("change") { oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, BOB_PUBKEY) } + output("change") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, BOB_PUBKEY) } command(BOB_PUBKEY, MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) } timestamp(TEST_TX_TIME) this.verifies() @@ -367,7 +367,7 @@ class ObligationTests { transaction("Issuance") { input("Alice's $1,000,000 obligation to Bob") input("Bob's $1,000,000 obligation to Alice") - output("change") { (oneMillionDollars / 2).OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) } + output("change") { (oneMillionDollars / 2).OBLIGATION between Pair(ALICE, BOB_PUBKEY) } command(BOB_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) } timestamp(TEST_TX_TIME) this `fails with` "amounts owed on input and output must match" @@ -421,7 +421,7 @@ class ObligationTests { transaction("Issuance") { input("Bob's $1,000,000 obligation to Alice") input("MegaCorp's $1,000,000 obligation to Bob") - output("MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, ALICE_PUBKEY) } + output("MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, ALICE_PUBKEY) } command(ALICE_PUBKEY, BOB_PUBKEY, MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) } timestamp(TEST_TX_TIME) this.verifies() @@ -435,7 +435,7 @@ class ObligationTests { transaction("Issuance") { input("Bob's $1,000,000 obligation to Alice") input("MegaCorp's $1,000,000 obligation to Bob") - output("MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, ALICE_PUBKEY) } + output("MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, ALICE_PUBKEY) } command(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) } timestamp(TEST_TX_TIME) this `fails with` "all involved parties have signed" @@ -445,7 +445,7 @@ class ObligationTests { @Test fun `settlement`() { - // Try netting out two obligations + // Try settling an obligation ledger { obligationTestRoots(this) transaction("Settlement") { @@ -456,7 +456,33 @@ class ObligationTests { command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } this.verifies() } - this.verifies() + } + + // Try partial settling of an obligation + val halfAMillionDollars = 500000.DOLLARS `issued by` defaultIssuer + ledger { + transaction("Settlement") { + input(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)) + 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(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Amount(oneMillionDollars.quantity, USD)) } + command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } + this.verifies() + } + } + + // Make sure we can't settle an obligation that's defaulted + val defaultedObligation: Obligation.State = (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)).copy(lifecycle = Lifecycle.DEFAULTED) + ledger { + transaction("Settlement") { + 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(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Amount(oneMillionDollars.quantity, USD)) } + command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } + this `fails with` "all inputs are in the normal state" + } } } @@ -467,7 +493,7 @@ class ObligationTests { obligationTestRoots(this) 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) } + 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) } this `fails with` "there is a timestamp from the authority" } @@ -477,8 +503,8 @@ class ObligationTests { val pastTestTime = TEST_TX_TIME - Duration.ofDays(7) val futureTestTime = TEST_TX_TIME + Duration.ofDays(7) 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) } + 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) } timestamp(TEST_TX_TIME) this `fails with` "the due date has passed" @@ -487,8 +513,8 @@ class ObligationTests { // Try defaulting an obligation that is now in the past ledger { 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) } + 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) } timestamp(TEST_TX_TIME) this.verifies()