contracts, node: Port CommercialPaperTests, TwoPartyTradeProtocolTests and GroupToGraphConversion to use new dsl

This commit is contained in:
Andras Slemmer 2016-07-04 17:15:02 +01:00
parent 9b36df607e
commit cde315aca9
3 changed files with 122 additions and 125 deletions

View File

@ -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<ICommercialPaperState>().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<TransactionDslInterpreter>.outputs(aliceGetsBack: Amount<Issued<Currency>>) {
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<ICommercialPaperState>().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 <T : ContractState> cashOutputsToWallet(vararg outputs: TransactionState<T>): Pair<LedgerTransaction, List<StateAndRef<T>>> {
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<Issued<Currency>> = 1000.DOLLARS `issued by` issuer,
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<ICommercialPaperState> {
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)
}
}
}
}

View File

@ -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<ContractState> {
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<ContractState> {
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<ContractState> {
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<ContractState> {
ledger {
runWithError(true, false, "at least one asset input")
}
}
@Test
fun `dependency with error on seller side`() {
transactionGroupFor<ContractState> {
ledger {
runWithError(false, true, "must be timestamped")
}
}
private fun TransactionGroupDSL<ContractState>.runWithError(bobError: Boolean, aliceError: Boolean,
private fun LedgerDsl<TransactionDslInterpreter, LedgerDslInterpreter<TransactionDslInterpreter>>.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<ContractState>.insertFakeTransactions(wtxToSign: List<WireTransaction>,
services: ServiceHub,
vararg extraKeys: KeyPair): Map<SecureHash, SignedTransaction> {
private fun insertFakeTransactions(
wtxToSign: List<WireTransaction>,
services: ServiceHub,
vararg extraKeys: KeyPair): Map<SecureHash, SignedTransaction> {
val signed: List<SignedTransaction> = 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<ContractState>.fillUpForBuyer(withError: Boolean,
owner: PublicKey = BOB_PUBKEY,
issuer: PartyAndReference = MEGA_CORP.ref(1)): Pair<Wallet, List<WireTransaction>> {
private fun LedgerDsl<TransactionDslInterpreter, LedgerDslInterpreter<TransactionDslInterpreter>>.fillUpForBuyer(
withError: Boolean,
owner: PublicKey = BOB_PUBKEY,
issuer: PartyAndReference = MEGA_CORP.ref(1)): Pair<Wallet, List<WireTransaction>> {
// 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<StateAndRef<Cash.State>>(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<ContractState>.fillUpForSeller(withError: Boolean,
owner: PublicKey,
amount: Amount<Issued<Currency>>,
notary: Party,
attachmentID: SecureHash?): Pair<Wallet, List<WireTransaction>> {
private fun LedgerDsl<TransactionDslInterpreter, LedgerDslInterpreter<TransactionDslInterpreter>>.fillUpForSeller(
withError: Boolean,
owner: PublicKey,
amount: Amount<Issued<Currency>>,
notary: Party,
attachmentID: SecureHash?): Pair<Wallet, List<WireTransaction>> {
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<StateAndRef<Cash.State>>(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<TxRecord>())
val records: MutableList<TxRecord> = Collections.synchronizedList(ArrayList<TxRecord>())
override fun addTransaction(transaction: SignedTransaction) {
records.add(TxRecord.Add(transaction))

View File

@ -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<in ContractState>) {
class GraphVisualiser(val dsl: LedgerDsl<TestTransactionDslInterpreter, TestLedgerDslInterpreter>) {
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<Node>("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<Node>(tx.outRef<ContractState>(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<Edge>("tx$txIndex-out$outIndex", txNode, node, true)
@ -56,8 +55,8 @@ class GraphVisualiser(val dsl: TransactionGroupDSL<in ContractState>) {
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<in ContractState>) {
}
})
}
}
}