mirror of
https://github.com/corda/corda.git
synced 2025-03-22 12:05:59 +00:00
Merged in rnicoll-contract-minor (pull request #211)
Preparation work for contract clauses
This commit is contained in:
commit
8264e771f5
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user