Merged in rnicoll-contract-minor (pull request #211)

Preparation work for contract clauses
This commit is contained in:
Ross Nicoll 2016-07-11 11:36:45 +01:00
commit 8264e771f5
7 changed files with 62 additions and 36 deletions

View File

@ -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)

View File

@ -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()
}
}

View File

@ -116,7 +116,7 @@ abstract class FungibleAsset<T> : Contract {
val assetCommands = tx.commands.select<FungibleAsset.Commands>()
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)
}

View File

@ -422,7 +422,7 @@ class Obligation<P> : Contract {
val outputAmount: Amount<P> = outputs.sumObligations<P>()
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 }
}

View File

@ -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"
}
}

View File

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

View File

@ -38,9 +38,9 @@ class ObligationTests {
group: LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>
) = 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<Currency>().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<Currency>().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<Currency>().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<Currency>().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<Currency>().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<Currency>(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Amount(oneMillionDollars.quantity, USD)) }
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
this.verifies()
}
}
// Make sure we can't settle an obligation that's defaulted
val defaultedObligation: Obligation.State<Currency> = (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<Currency>(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Amount(oneMillionDollars.quantity, USD)) }
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().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()