diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index e7f0fb3083..e9a5f1b136 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -17,10 +17,10 @@ import kotlin.test.assertFailsWith import kotlin.test.assertTrue interface ICommercialPaperTestTemplate { - open fun getPaper(): ICommercialPaperState - open fun getIssueCommand(): CommandData - open fun getRedeemCommand(): CommandData - open fun getMoveCommand(): CommandData + fun getPaper(): ICommercialPaperState + fun getIssueCommand(): CommandData + fun getRedeemCommand(): CommandData + fun getMoveCommand(): CommandData } class JavaCommercialPaperTest() : ICommercialPaperTestTemplate { @@ -63,81 +63,122 @@ class CommercialPaperTestsGeneric { val issuer = MEGA_CORP.ref(123) @Test - fun ok() { - trade().verify() - } + fun `trade lifecycle test`() { + val someProfits = 1200.DOLLARS `issued by` issuer + ledger { + nonVerifiedTransaction { + output("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY) + output("some profits", someProfits.STATE `owned by` MEGA_CORP_PUBKEY) + } - @Test - fun `not matured at redemption`() { - trade(redemptionTime = TEST_TX_TIME + 2.days).expectFailureOfTx(3, "must have matured") + // Some CP is issued onto the ledger by MegaCorp. + transaction("Issuance") { + output("paper") { thisTest.getPaper() } + command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } + timestamp(TEST_TX_TIME) + } + + // The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days, + // that sounds a bit too good to be true! + transaction("Trade") { + input("paper") + input("alice's $900") + output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } + output("alice's paper") { "paper".output().data `owned by` ALICE_PUBKEY } + command(ALICE_PUBKEY) { Cash.Commands.Move() } + command(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() } + } + + // Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200 + // as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change. + transaction("Redemption") { + input("alice's paper") + input("some profits") + + fun TransactionDsl.outputs(aliceGetsBack: Amount>) { + output("Alice's profit") { aliceGetsBack.STATE `owned by` ALICE_PUBKEY } + output("Change") { (someProfits - aliceGetsBack).STATE `owned by` MEGA_CORP_PUBKEY } + } + + command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } + command(ALICE_PUBKEY) { thisTest.getRedeemCommand() } + + tweak { + outputs(700.DOLLARS `issued by` issuer) + timestamp(TEST_TX_TIME + 8.days) + this `fails with` "received amount equals the face value" + } + outputs(1000.DOLLARS `issued by` issuer) + + + tweak { + timestamp(TEST_TX_TIME + 2.days) + this `fails with` "must have matured" + } + timestamp(TEST_TX_TIME + 8.days) + + tweak { + output { "paper".output().data } + this `fails with` "must be destroyed" + } + + verifies() + } + } } @Test fun `key mismatch at issue`() { - transactionGroup { + ledger { transaction { output { thisTest.getPaper() } - arg(DUMMY_PUBKEY_1) { thisTest.getIssueCommand() } + command(DUMMY_PUBKEY_1) { thisTest.getIssueCommand() } timestamp(TEST_TX_TIME) + this `fails with` "signed by the claimed issuer" } - - expectFailureOfTx(1, "signed by the claimed issuer") } } @Test fun `face value is not zero`() { - transactionGroup { + ledger { transaction { output { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) } - arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } + command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } timestamp(TEST_TX_TIME) + this `fails with` "face value is not zero" } - - expectFailureOfTx(1, "face value is not zero") } } @Test fun `maturity date not in the past`() { - transactionGroup { + ledger { transaction { output { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) } - arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } + command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } timestamp(TEST_TX_TIME) + this `fails with` "maturity date is not in the past" } - - expectFailureOfTx(1, "maturity date is not in the past") } } @Test fun `issue cannot replace an existing state`() { - transactionGroup { - roots { - transaction(thisTest.getPaper() `with notary` DUMMY_NOTARY label "paper") + ledger { + nonVerifiedTransaction { + output("paper") { thisTest.getPaper() } } transaction { input("paper") output { thisTest.getPaper() } - arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } + command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } timestamp(TEST_TX_TIME) + this `fails with` "there is no input state" } - - expectFailureOfTx(1, "there is no input state") } } - @Test - fun `did not receive enough money at redemption`() { - trade(aliceGetsBack = 700.DOLLARS `issued by` issuer).expectFailureOfTx(3, "received amount equals the face value") - } - - @Test - fun `paper must be destroyed by redemption`() { - trade(destroyPaperAtRedemption = false).expectFailureOfTx(3, "must be destroyed") - } - fun cashOutputsToWallet(vararg outputs: TransactionState): Pair>> { val ltx = LedgerTransaction(emptyList(), listOf(*outputs), emptyList(), emptyList(), SecureHash.randomSHA256(), emptyList(), TransactionType.General()) return Pair(ltx, outputs.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) }) @@ -199,52 +240,4 @@ class CommercialPaperTestsGeneric { TransactionGroup(setOf(issueTX, moveTX, validRedemption), setOf(corpWalletTX, alicesWalletTX)).verify() } - - // Generate a trade lifecycle with various parameters. - fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days, - aliceGetsBack: Amount> = 1000.DOLLARS `issued by` issuer, - destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL { - val someProfits = 1200.DOLLARS `issued by` issuer - return transactionGroupFor() { - roots { - transaction(900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY label "alice's $900") - transaction(someProfits.STATE `owned by` MEGA_CORP_PUBKEY `with notary` DUMMY_NOTARY label "some profits") - } - - // Some CP is issued onto the ledger by MegaCorp. - transaction("Issuance") { - output("paper") { thisTest.getPaper() } - arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } - timestamp(TEST_TX_TIME) - } - - // The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days, - // that sounds a bit too good to be true! - transaction("Trade") { - input("paper") - input("alice's $900") - output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } - output("alice's paper") { "paper".output.data `owned by` ALICE_PUBKEY } - arg(ALICE_PUBKEY) { Cash.Commands.Move() } - arg(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() } - } - - // Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200 - // as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change. - transaction("Redemption") { - input("alice's paper") - input("some profits") - - output("Alice's profit") { aliceGetsBack.STATE `owned by` ALICE_PUBKEY } - output("Change") { (someProfits - aliceGetsBack).STATE `owned by` MEGA_CORP_PUBKEY } - if (!destroyPaperAtRedemption) - output { "paper".output.data } - - arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } - arg(ALICE_PUBKEY) { thisTest.getRedeemCommand() } - - timestamp(redemptionTime) - } - } - } } diff --git a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt index 1b8de5b263..bdc365d599 100644 --- a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt @@ -86,7 +86,9 @@ class TwoPartyTradeProtocolTests { // we run in the unit test thread exclusively to speed things up, ensure deterministic results and // allow interruption half way through. net = MockNetwork(false, true) - transactionGroupFor { + + ledger { + val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY) val bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY) @@ -113,7 +115,7 @@ class TwoPartyTradeProtocolTests { aliceNode.smm, notaryNode.info, bobNode.info.identity, - lookup("alice's paper"), + "alice's paper".outputStateAndRef(), 1000.DOLLARS `issued by` issuer, ALICE_KEY, buyerSessionID @@ -133,7 +135,8 @@ class TwoPartyTradeProtocolTests { @Test fun `shutdown and restore`() { - transactionGroupFor { + + ledger { val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY) var bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY) @@ -155,7 +158,7 @@ class TwoPartyTradeProtocolTests { aliceNode.smm, notaryNode.info, bobNode.info.identity, - lookup("alice's paper"), + "alice's paper".outputStateAndRef(), 1000.DOLLARS `issued by` issuer, ALICE_KEY, buyerSessionID @@ -246,7 +249,7 @@ class TwoPartyTradeProtocolTests { @Test fun `check dependencies of sale asset are resolved`() { - transactionGroupFor { + ledger { val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) val aliceNode = makeNodeWithTracking(notaryNode.info, ALICE.name, ALICE_KEY) val bobNode = makeNodeWithTracking(notaryNode.info, BOB.name, BOB_KEY) @@ -275,7 +278,7 @@ class TwoPartyTradeProtocolTests { aliceNode.smm, notaryNode.info, bobNode.info.identity, - lookup("alice's paper"), + "alice's paper".outputStateAndRef(), 1000.DOLLARS `issued by` issuer, ALICE_KEY, buyerSessionID @@ -350,19 +353,19 @@ class TwoPartyTradeProtocolTests { @Test fun `dependency with error on buyer side`() { - transactionGroupFor { + ledger { runWithError(true, false, "at least one asset input") } } @Test fun `dependency with error on seller side`() { - transactionGroupFor { + ledger { runWithError(false, true, "must be timestamped") } } - private fun TransactionGroupDSL.runWithError(bobError: Boolean, aliceError: Boolean, + private fun LedgerDsl>.runWithError(bobError: Boolean, aliceError: Boolean, expectedMessageSubstring: String) { val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY) @@ -385,7 +388,7 @@ class TwoPartyTradeProtocolTests { aliceNode.smm, notaryNode.info, bobNode.info.identity, - lookup("alice's paper"), + "alice's paper".outputStateAndRef(), 1000.DOLLARS `issued by` issuer, ALICE_KEY, buyerSessionID @@ -411,9 +414,10 @@ class TwoPartyTradeProtocolTests { assertTrue(e.cause!!.cause!!.message!!.contains(expectedMessageSubstring)) } - private fun TransactionGroupDSL.insertFakeTransactions(wtxToSign: List, - services: ServiceHub, - vararg extraKeys: KeyPair): Map { + private fun insertFakeTransactions( + wtxToSign: List, + services: ServiceHub, + vararg extraKeys: KeyPair): Map { val signed: List = signAll(wtxToSign, *extraKeys) services.recordTransactions(signed) val validatedTransactions = services.storageService.validatedTransactions @@ -423,9 +427,10 @@ class TwoPartyTradeProtocolTests { return signed.associateBy { it.id } } - private fun TransactionGroupDSL.fillUpForBuyer(withError: Boolean, - owner: PublicKey = BOB_PUBKEY, - issuer: PartyAndReference = MEGA_CORP.ref(1)): Pair> { + private fun LedgerDsl>.fillUpForBuyer( + withError: Boolean, + owner: PublicKey = BOB_PUBKEY, + issuer: PartyAndReference = MEGA_CORP.ref(1)): Pair> { // Bob (Buyer) has some cash he got from the Bank of Elbonia, Alice (Seller) has some commercial paper she // wants to sell to Bob. @@ -434,7 +439,7 @@ class TwoPartyTradeProtocolTests { output("elbonian money 1") { 800.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } output("elbonian money 2") { 1000.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } if (!withError) - arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() } + command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() } timestamp(TEST_TX_TIME) } @@ -442,44 +447,44 @@ class TwoPartyTradeProtocolTests { val bc1 = transaction { input("elbonian money 1") output("bob cash 1") { 800.DOLLARS.CASH `issued by` issuer `owned by` owner } - arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } + command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } } val bc2 = transaction { input("elbonian money 2") output("bob cash 2") { 300.DOLLARS.CASH `issued by` issuer `owned by` owner } output { 700.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } // Change output. - arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } + command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } } - val wallet = Wallet(listOf>(lookup("bob cash 1"), lookup("bob cash 2"))) + val wallet = Wallet(listOf("bob cash 1".outputStateAndRef(), "bob cash 2".outputStateAndRef())) return Pair(wallet, listOf(eb1, bc1, bc2)) } - private fun TransactionGroupDSL.fillUpForSeller(withError: Boolean, - owner: PublicKey, - amount: Amount>, - notary: Party, - attachmentID: SecureHash?): Pair> { + private fun LedgerDsl>.fillUpForSeller( + withError: Boolean, + owner: PublicKey, + amount: Amount>, + notary: Party, + attachmentID: SecureHash?): Pair> { val ap = transaction { output("alice's paper") { CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), owner, amount, TEST_TX_TIME + 7.days) } - arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } if (!withError) - arg(notary.owningKey) { TimestampCommand(TEST_TX_TIME, 30.seconds) } + command(notary.owningKey) { TimestampCommand(TEST_TX_TIME, 30.seconds) } if (attachmentID != null) attachment(attachmentID) } - val wallet = Wallet(listOf>(lookup("alice's paper"))) + val wallet = Wallet(listOf("alice's paper".outputStateAndRef())) return Pair(wallet, listOf(ap)) } - class RecordingTransactionStorage(val delegate: TransactionStorage) : TransactionStorage { - val records = Collections.synchronizedList(ArrayList()) + val records: MutableList = Collections.synchronizedList(ArrayList()) override fun addTransaction(transaction: SignedTransaction) { records.add(TxRecord.Add(transaction)) diff --git a/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt b/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt index 9cb29a4e18..ae7ca111b2 100644 --- a/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt +++ b/node/src/test/kotlin/com/r3corda/node/visualiser/GroupToGraphConversion.kt @@ -2,35 +2,34 @@ package com.r3corda.node.visualiser import com.r3corda.core.contracts.CommandData import com.r3corda.core.contracts.ContractState -import com.r3corda.core.contracts.TransactionState import com.r3corda.core.crypto.SecureHash -import com.r3corda.core.testing.TransactionGroupDSL +import com.r3corda.core.testing.* import org.graphstream.graph.Edge import org.graphstream.graph.Node import org.graphstream.graph.implementations.SingleGraph import kotlin.reflect.memberProperties -class GraphVisualiser(val dsl: TransactionGroupDSL) { +class GraphVisualiser(val dsl: LedgerDsl) { companion object { val css = GraphVisualiser::class.java.getResourceAsStream("graph.css").bufferedReader().readText() } fun convert(): SingleGraph { - val tg = dsl.toTransactionGroup() + val tg = dsl.interpreter.toTransactionGroup() val graph = createGraph("Transaction group", css) // Map all the transactions, including the bogus non-verified ones (with no inputs) to graph nodes. for ((txIndex, tx) in (tg.transactions + tg.nonVerifiedRoots).withIndex()) { val txNode = graph.addNode("tx$txIndex") if (tx !in tg.nonVerifiedRoots) - txNode.label = dsl.labelForTransaction(tx).let { it ?: "TX ${tx.id.prefixChars()}" } + txNode.label = dsl.interpreter.transactionName(tx.id).let { it ?: "TX[${tx.id.prefixChars()}]" } txNode.styleClass = "tx" // Now create a vertex for each output state. for (outIndex in tx.outputs.indices) { val node = graph.addNode(tx.outRef(outIndex).ref.toString()) val state = tx.outputs[outIndex] - node.label = stateToLabel(state) + node.label = stateToLabel(state.data) node.styleClass = stateToCSSClass(state.data) + ",state" node.setAttribute("state", state) val edge = graph.addEdge("tx$txIndex-out$outIndex", txNode, node, true) @@ -56,8 +55,8 @@ class GraphVisualiser(val dsl: TransactionGroupDSL) { return graph } - private fun stateToLabel(state: TransactionState<*>): String { - return dsl.labelForState(state) ?: stateToTypeName(state.data) + private fun stateToLabel(state: ContractState): String { + return dsl.interpreter.outputToLabel(state) ?: stateToTypeName(state) } private fun commandToTypeName(state: CommandData) = state.javaClass.canonicalName.removePrefix("contracts.").replace('$', '.') @@ -73,4 +72,4 @@ class GraphVisualiser(val dsl: TransactionGroupDSL) { } }) } -} \ No newline at end of file +}