Merged in unified-test-dsl (pull request #203)

Unified test dsl
This commit is contained in:
Andras Slemmer 2016-07-06 13:30:59 +01:00
commit c38f99419f
13 changed files with 1023 additions and 814 deletions

View File

@ -129,7 +129,7 @@ task integrationTest(type: Test) {
testClassesDir = sourceSets.integrationTest.output.classesDir
classpath = sourceSets.integrationTest.runtimeClasspath
}
test.dependsOn(integrationTest)
test.finalizedBy(integrationTest)
tasks.withType(Test) {
reports.html.destination = file("${reporting.baseDir}/${name}")

View File

@ -2,6 +2,7 @@ package com.r3corda.contracts.asset;
import com.r3corda.core.contracts.PartyAndReference;
import com.r3corda.core.serialization.OpaqueBytes;
import kotlin.Unit;
import org.junit.Test;
import static com.r3corda.core.testing.JavaTestHelpers.*;
@ -20,39 +21,41 @@ public class CashTestsJava {
@Test
public void trivial() {
ledger(lg -> {
lg.transaction(tx -> {
tx.input(inState);
tx.failsWith("the amounts balance");
transaction(tx -> {
tx.input(inState);
tx.failsRequirement("the amounts balance");
tx.tweak(tw -> {
tw.output(new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), getDUMMY_PUBKEY_2()));
return tw.failsWith("the amounts balance");
});
tx.tweak(tw -> {
tw.output(new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), getDUMMY_PUBKEY_2()));
return tw.failsRequirement("the amounts balance");
});
tx.tweak(tw -> {
tw.output(outState);
// No command arguments
return tw.failsWith("required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command");
});
tx.tweak(tw -> {
tw.output(outState);
tw.command(getDUMMY_PUBKEY_2(), new Cash.Commands.Move());
return tw.failsWith("the owning keys are the same as the signing keys");
});
tx.tweak(tw -> {
tw.output(outState);
tw.output(issuedBy(outState, getMINI_CORP()));
tw.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
return tw.failsWith("at least one asset input");
});
tx.tweak(tw -> {
tw.output(outState);
// No command arguments
return tw.failsRequirement("required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command");
});
tx.tweak(tw -> {
tw.output(outState);
tw.arg(getDUMMY_PUBKEY_2(), new Cash.Commands.Move());
return tw.failsRequirement("the owning keys are the same as the signing keys");
});
tx.tweak(tw -> {
tw.output(outState);
tw.output(issuedBy(outState, getMINI_CORP()));
tw.arg(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
return tw.failsRequirement("at least one asset input");
});
// Simple reallocation works.
return tx.tweak(tw -> {
tw.output(outState);
tw.arg(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
return tw.accepts();
// Simple reallocation works.
return tx.tweak(tw -> {
tw.output(outState);
tw.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
return tw.verifies();
});
});
return Unit.INSTANCE;
});
}
}

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,113 @@ 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 {
unverifiedTransaction {
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)
this.verifies()
}
// 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() }
this.verifies()
}
// 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<EnforceVerifyOrFail, TransactionDSLInterpreter<EnforceVerifyOrFail>>.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"
}
this.verifies()
}
}
}
@Test
fun `key mismatch at issue`() {
transactionGroup {
transaction {
output { thisTest.getPaper() }
arg(DUMMY_PUBKEY_1) { thisTest.getIssueCommand() }
timestamp(TEST_TX_TIME)
}
expectFailureOfTx(1, "signed by the claimed issuer")
transaction {
output { thisTest.getPaper() }
command(DUMMY_PUBKEY_1) { thisTest.getIssueCommand() }
timestamp(TEST_TX_TIME)
this `fails with` "signed by the claimed issuer"
}
}
@Test
fun `face value is not zero`() {
transactionGroup {
transaction {
output { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) }
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
timestamp(TEST_TX_TIME)
}
expectFailureOfTx(1, "face value is not zero")
transaction {
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"
}
}
@Test
fun `maturity date not in the past`() {
transactionGroup {
transaction {
output { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) }
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
timestamp(TEST_TX_TIME)
}
expectFailureOfTx(1, "maturity date is not in the past")
transaction {
output { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) }
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
timestamp(TEST_TX_TIME)
this `fails with` "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")
}
transaction {
input("paper")
output { thisTest.getPaper() }
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
timestamp(TEST_TX_TIME)
}
expectFailureOfTx(1, "there is no input state")
transaction {
input(thisTest.getPaper())
output { thisTest.getPaper() }
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
timestamp(TEST_TX_TIME)
this `fails with` "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 +231,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

@ -200,12 +200,12 @@ class IRSTests {
@Test
fun ok() {
trade().verify()
trade().verifies()
}
@Test
fun `ok with groups`() {
tradegroups().verify()
tradegroups().verifies()
}
/**
@ -360,38 +360,40 @@ class IRSTests {
/**
* Generates a typical transactional history for an IRS.
*/
fun trade(): TransactionGroupDSL<InterestRateSwap.State> {
fun trade(): LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter> {
val ld = LocalDate.of(2016, 3, 8)
val bd = BigDecimal("0.0063518")
val txgroup: TransactionGroupDSL<InterestRateSwap.State> = transactionGroupFor() {
return ledger {
transaction("Agreement") {
output("irs post agreement") { singleIRS() }
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this.verifies()
}
transaction("Fix") {
input("irs post agreement")
val postAgreement = "irs post agreement".output<InterestRateSwap.State>()
output("irs post first fixing") {
"irs post agreement".output.data.copy(
"irs post agreement".output.data.fixedLeg,
"irs post agreement".output.data.floatingLeg,
"irs post agreement".output.data.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
"irs post agreement".output.data.common
postAgreement.data.copy(
postAgreement.data.fixedLeg,
postAgreement.data.floatingLeg,
postAgreement.data.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
postAgreement.data.common
)
}
arg(ORACLE_PUBKEY) {
command(ORACLE_PUBKEY) {
InterestRateSwap.Commands.Fix()
}
arg(ORACLE_PUBKEY) {
command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
}
timestamp(TEST_TX_TIME)
this.verifies()
}
}
return txgroup
}
@Test
@ -399,9 +401,9 @@ class IRSTests {
transaction {
input() { singleIRS() }
output("irs post agreement") { singleIRS() }
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "There are no in states for an agreement"
this `fails with` "There are no in states for an agreement"
}
}
@ -413,9 +415,9 @@ class IRSTests {
output() {
irs.copy(calculation = irs.calculation.copy(fixedLegPaymentSchedule = emptySchedule))
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "There are events in the fix schedule"
this `fails with` "There are events in the fix schedule"
}
}
@ -427,9 +429,9 @@ class IRSTests {
output() {
irs.copy(calculation = irs.calculation.copy(floatingLegPaymentSchedule = emptySchedule))
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "There are events in the float schedule"
this `fails with` "There are events in the float schedule"
}
}
@ -440,18 +442,18 @@ class IRSTests {
output() {
irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(quantity = 0)))
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "All notionals must be non zero"
this `fails with` "All notionals must be non zero"
}
transaction {
output() {
irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(quantity = 0)))
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "All notionals must be non zero"
this `fails with` "All notionals must be non zero"
}
}
@ -463,9 +465,9 @@ class IRSTests {
output() {
modifiedIRS
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "The fixed leg rate must be positive"
this `fails with` "The fixed leg rate must be positive"
}
}
@ -480,9 +482,9 @@ class IRSTests {
output() {
modifiedIRS
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "The currency of the notionals must be the same"
this `fails with` "The currency of the notionals must be the same"
}
}
@ -494,9 +496,9 @@ class IRSTests {
output() {
modifiedIRS
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "All leg notionals must be the same"
this `fails with` "All leg notionals must be the same"
}
}
@ -508,9 +510,9 @@ class IRSTests {
output() {
modifiedIRS1
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "The effective date is before the termination date for the fixed leg"
this `fails with` "The effective date is before the termination date for the fixed leg"
}
val modifiedIRS2 = irs.copy(floatingLeg = irs.floatingLeg.copy(terminationDate = irs.floatingLeg.effectiveDate.minusDays(1)))
@ -518,9 +520,9 @@ class IRSTests {
output() {
modifiedIRS2
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "The effective date is before the termination date for the floating leg"
this `fails with` "The effective date is before the termination date for the floating leg"
}
}
@ -533,9 +535,9 @@ class IRSTests {
output() {
modifiedIRS3
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "The termination dates are aligned"
this `fails with` "The termination dates are aligned"
}
@ -544,24 +546,23 @@ class IRSTests {
output() {
modifiedIRS4
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "The effective dates are aligned"
this `fails with` "The effective dates are aligned"
}
}
@Test
fun `various fixing tests`() {
val ld = LocalDate.of(2016, 3, 8)
val bd = BigDecimal("0.0063518")
transaction {
output("irs post agreement") { singleIRS() }
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this.accepts()
this.verifies()
}
val oldIRS = singleIRS(1)
@ -578,31 +579,31 @@ class IRSTests {
// Templated tweak for reference. A corrent fixing applied should be ok
tweak {
arg(ORACLE_PUBKEY) {
command(ORACLE_PUBKEY) {
InterestRateSwap.Commands.Fix()
}
timestamp(TEST_TX_TIME)
arg(ORACLE_PUBKEY) {
command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
}
output() { newIRS }
this.accepts()
this.verifies()
}
// This test makes sure that verify confirms the fixing was applied and there is a difference in the old and new
tweak {
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
timestamp(TEST_TX_TIME)
arg(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
command(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
output() { oldIRS }
this`fails requirement` "There is at least one difference in the IRS floating leg payment schedules"
this `fails with` "There is at least one difference in the IRS floating leg payment schedules"
}
// This tests tries to sneak in a change to another fixing (which may or may not be the latest one)
tweak {
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
timestamp(TEST_TX_TIME)
arg(ORACLE_PUBKEY) {
command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
}
@ -619,14 +620,14 @@ class IRSTests {
newIRS.common
)
}
this`fails requirement` "There is only one change in the IRS floating leg payment schedule"
this `fails with` "There is only one change in the IRS floating leg payment schedule"
}
// This tests modifies the payment currency for the fixing
tweak {
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
timestamp(TEST_TX_TIME)
arg(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
command(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key }
val modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.quantity, Currency.getInstance("JPY")))
@ -640,7 +641,7 @@ class IRSTests {
newIRS.common
)
}
this`fails requirement` "The fix payment has the same currency as the notional"
this `fails with` "The fix payment has the same currency as the notional"
}
}
}
@ -652,13 +653,13 @@ class IRSTests {
* result and the grouping won't work either.
* In reality, the only fields that should be in common will be the next fixing date and the reference rate.
*/
fun tradegroups(): TransactionGroupDSL<InterestRateSwap.State> {
fun tradegroups(): LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter> {
val ld1 = LocalDate.of(2016, 3, 8)
val bd1 = BigDecimal("0.0063518")
val irs = singleIRS()
val txgroup: TransactionGroupDSL<InterestRateSwap.State> = transactionGroupFor() {
return ledger {
transaction("Agreement") {
output("irs post agreement1") {
irs.copy(
@ -668,8 +669,9 @@ class IRSTests {
irs.common.copy(tradeID = "t1")
)
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this.verifies()
}
transaction("Agreement") {
@ -681,40 +683,43 @@ class IRSTests {
irs.common.copy(tradeID = "t2")
)
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this.verifies()
}
transaction("Fix") {
input("irs post agreement1")
input("irs post agreement2")
val postAgreement1 = "irs post agreement1".output<InterestRateSwap.State>()
output("irs post first fixing1") {
"irs post agreement1".output.data.copy(
"irs post agreement1".output.data.fixedLeg,
"irs post agreement1".output.data.floatingLeg,
"irs post agreement1".output.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
"irs post agreement1".output.data.common.copy(tradeID = "t1")
postAgreement1.data.copy(
postAgreement1.data.fixedLeg,
postAgreement1.data.floatingLeg,
postAgreement1.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
postAgreement1.data.common.copy(tradeID = "t1")
)
}
val postAgreement2 = "irs post agreement2".output<InterestRateSwap.State>()
output("irs post first fixing2") {
"irs post agreement2".output.data.copy(
"irs post agreement2".output.data.fixedLeg,
"irs post agreement2".output.data.floatingLeg,
"irs post agreement2".output.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
"irs post agreement2".output.data.common.copy(tradeID = "t2")
postAgreement2.data.copy(
postAgreement2.data.fixedLeg,
postAgreement2.data.floatingLeg,
postAgreement2.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
postAgreement2.data.common.copy(tradeID = "t2")
)
}
arg(ORACLE_PUBKEY) {
command(ORACLE_PUBKEY) {
InterestRateSwap.Commands.Fix()
}
arg(ORACLE_PUBKEY) {
command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1)
}
timestamp(TEST_TX_TIME)
this.verifies()
}
}
return txgroup
}
}

View File

@ -31,33 +31,33 @@ class CashTests {
fun trivial() {
transaction {
input { inState }
this `fails requirement` "the amounts balance"
this `fails with` "the amounts balance"
tweak {
output { outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer) }
this `fails requirement` "the amounts balance"
this `fails with` "the amounts balance"
}
tweak {
output { outState }
// No command arguments
this `fails requirement` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
}
tweak {
output { outState }
arg(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
this `fails requirement` "the owning keys are the same as the signing keys"
command(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
this `fails with` "the owning keys are the same as the signing keys"
}
tweak {
output { outState }
output { outState `issued by` MINI_CORP }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails requirement` "at least one asset input"
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails with` "at least one asset input"
}
// Simple reallocation works.
tweak {
output { outState }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this.accepts()
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this.verifies()
}
}
}
@ -68,17 +68,17 @@ class CashTests {
transaction {
input { DummyState() }
output { outState }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
command(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
this `fails requirement` "there is at least one asset input"
this `fails with` "there is at least one asset input"
}
// Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
// institution is allowed to issue as much cash as they want.
transaction {
output { outState }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Issue() }
this `fails requirement` "output deposits are owned by a command signer"
command(DUMMY_PUBKEY_1) { Cash.Commands.Issue() }
this `fails with` "output deposits are owned by a command signer"
}
transaction {
output {
@ -88,11 +88,11 @@ class CashTests {
)
}
tweak {
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue(0) }
this `fails requirement` "has a nonce"
command(MINI_CORP_PUBKEY) { Cash.Commands.Issue(0) }
this `fails with` "has a nonce"
}
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
this.accepts()
command(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
this.verifies()
}
// Test generation works.
@ -120,14 +120,14 @@ class CashTests {
// Move fails: not allowed to summon money.
tweak {
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails requirement` "at issuer MegaCorp the amounts balance"
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails with` "at issuer MegaCorp the amounts balance"
}
// Issue works.
tweak {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
this.accepts()
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
this.verifies()
}
}
@ -135,36 +135,36 @@ class CashTests {
transaction {
input { inState }
output { inState.copy(amount = inState.amount / 2) }
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
this `fails requirement` "output values sum to more than the inputs"
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
this `fails with` "output values sum to more than the inputs"
}
// Can't have an issue command that doesn't actually issue money.
transaction {
input { inState }
output { inState }
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
this `fails requirement` "output values sum to more than the inputs"
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
this `fails with` "output values sum to more than the inputs"
}
// Can't have any other commands if we have an issue command (because the issue command overrules them)
transaction {
input { inState }
output { inState.copy(amount = inState.amount * 2) }
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
tweak {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
this `fails requirement` "there is only a single issue command"
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
this `fails with` "there is only a single issue command"
}
tweak {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
this `fails requirement` "there is only a single issue command"
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
this `fails with` "there is only a single issue command"
}
tweak {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
this `fails requirement` "there is only a single issue command"
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
this `fails with` "there is only a single issue command"
}
this.accepts()
this.verifies()
}
}
@ -191,25 +191,25 @@ class CashTests {
fun testMergeSplit() {
// Splitting value works.
transaction {
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
tweak {
input { inState }
for (i in 1..4) output { inState.copy(amount = inState.amount / 4) }
this.accepts()
this.verifies()
}
// Merging 4 inputs into 2 outputs works.
tweak {
for (i in 1..4) input { inState.copy(amount = inState.amount / 4) }
output { inState.copy(amount = inState.amount / 2) }
output { inState.copy(amount = inState.amount / 2) }
this.accepts()
this.verifies()
}
// Merging 2 inputs into 1 works.
tweak {
input { inState.copy(amount = inState.amount / 2) }
input { inState.copy(amount = inState.amount / 2) }
output { inState }
this.accepts()
this.verifies()
}
}
}
@ -219,13 +219,13 @@ class CashTests {
transaction {
input { inState }
input { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
this `fails requirement` "zero sized inputs"
this `fails with` "zero sized inputs"
}
transaction {
input { inState }
output { inState }
output { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
this `fails requirement` "zero sized outputs"
this `fails with` "zero sized outputs"
}
}
@ -235,21 +235,21 @@ class CashTests {
transaction {
input { inState }
output { outState `issued by` MINI_CORP }
this `fails requirement` "at issuer MegaCorp the amounts balance"
this `fails with` "at issuer MegaCorp the amounts balance"
}
// Can't change deposit reference when splitting.
transaction {
input { inState }
output { outState.copy(amount = inState.amount / 2).editDepositRef(0) }
output { outState.copy(amount = inState.amount / 2).editDepositRef(1) }
this `fails requirement` "for deposit [01] at issuer MegaCorp the amounts balance"
this `fails with` "for deposit [01] at issuer MegaCorp the amounts balance"
}
// Can't mix currencies.
transaction {
input { inState }
output { outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer) }
output { outState.copy(amount = 200.POUNDS `issued by` defaultIssuer) }
this `fails requirement` "the amounts balance"
this `fails with` "the amounts balance"
}
transaction {
input { inState }
@ -260,22 +260,22 @@ class CashTests {
)
}
output { outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer) }
this `fails requirement` "the amounts balance"
this `fails with` "the amounts balance"
}
// Can't have superfluous input states from different issuers.
transaction {
input { inState }
input { inState `issued by` MINI_CORP }
output { outState }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails requirement` "at issuer MiniCorp the amounts balance"
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails with` "at issuer MiniCorp the amounts balance"
}
// Can't combine two different deposits at the same issuer.
transaction {
input { inState }
input { inState.editDepositRef(3) }
output { outState.copy(amount = inState.amount * 2).editDepositRef(3) }
this `fails requirement` "for deposit [01]"
this `fails with` "for deposit [01]"
}
}
@ -287,18 +287,18 @@ class CashTests {
output { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
tweak {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails requirement` "the amounts balance"
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails with` "the amounts balance"
}
tweak {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
this `fails requirement` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
tweak {
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this.accepts()
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this.verifies()
}
}
}
@ -310,15 +310,15 @@ class CashTests {
output { inState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) `issued by` MINI_CORP }
output { inState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails requirement` "at issuer MegaCorp the amounts balance"
this `fails with` "at issuer MegaCorp the amounts balance"
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
this `fails requirement` "at issuer MiniCorp the amounts balance"
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
this `fails with` "at issuer MiniCorp the amounts balance"
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)) }
this.accepts()
command(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)) }
this.verifies()
}
}
@ -332,20 +332,20 @@ class CashTests {
// Can't merge them together.
tweak {
output { inState.copy(owner = DUMMY_PUBKEY_2, amount = 2000.DOLLARS `issued by` defaultIssuer) }
this `fails requirement` "at issuer MegaCorp the amounts balance"
this `fails with` "at issuer MegaCorp the amounts balance"
}
// Missing MiniCorp deposit
tweak {
output { inState.copy(owner = DUMMY_PUBKEY_2) }
output { inState.copy(owner = DUMMY_PUBKEY_2) }
this `fails requirement` "at issuer MegaCorp the amounts balance"
this `fails with` "at issuer MegaCorp the amounts balance"
}
// This works.
output { inState.copy(owner = DUMMY_PUBKEY_2) }
output { inState.copy(owner = DUMMY_PUBKEY_2) `issued by` MINI_CORP }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this.accepts()
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this.verifies()
}
}
@ -358,9 +358,9 @@ class CashTests {
input { pounds }
output { inState `owned by` DUMMY_PUBKEY_2 }
output { pounds `owned by` DUMMY_PUBKEY_1 }
arg(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Cash.Commands.Move() }
command(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Cash.Commands.Move() }
this.accepts()
this.verifies()
}
}

View File

@ -1,11 +1,11 @@
package com.r3corda.contracts.asset
import com.r3corda.contracts.asset.*
import com.r3corda.contracts.asset.Obligation.Lifecycle
import com.r3corda.contracts.testing.*
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.testing.*
import com.r3corda.core.testing.JavaTestHelpers
import com.r3corda.core.utilities.nonEmptySetOf
import org.junit.Test
import java.security.PublicKey
@ -19,11 +19,10 @@ class ObligationTests {
val defaultUsd = USD `issued by` defaultIssuer
val oneMillionDollars = 1000000.DOLLARS `issued by` defaultIssuer
val trustedCashContract = nonEmptySetOf(SecureHash.Companion.randomSHA256() as SecureHash)
val megaIssuedDollars = nonEmptySetOf(Issued<Currency>(defaultIssuer, USD))
val megaIssuedPounds = nonEmptySetOf(Issued<Currency>(defaultIssuer, GBP))
val megaIssuedDollars = nonEmptySetOf(Issued(defaultIssuer, USD))
val megaIssuedPounds = nonEmptySetOf(Issued(defaultIssuer, GBP))
val fivePm = Instant.parse("2016-01-01T17:00:00.00Z")
val sixPm = Instant.parse("2016-01-01T18:00:00.00Z")
val notary = MEGA_CORP
val megaCorpDollarSettlement = Obligation.StateTemplate(trustedCashContract, megaIssuedDollars, fivePm)
val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuedProducts = megaIssuedPounds)
val inState = Obligation.State(
@ -35,43 +34,48 @@ class ObligationTests {
)
val outState = inState.copy(beneficiary = DUMMY_PUBKEY_2)
private fun obligationTestRoots(group: TransactionGroupDSL<Obligation.State<Currency>>) = group.Roots()
.transaction(oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `with notary` DUMMY_NOTARY label "Alice's $1,000,000 obligation to Bob")
.transaction(oneMillionDollars.OBLIGATION `between` Pair(BOB, ALICE_PUBKEY) `with notary` DUMMY_NOTARY label "Bob's $1,000,000 obligation to Alice")
.transaction(oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, BOB_PUBKEY) `with notary` DUMMY_NOTARY label "MegaCorp's $1,000,000 obligation to Bob")
.transaction(1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY label "Alice's $1,000,000")
private fun obligationTestRoots(
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", 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY)
}
}
@Test
fun trivial() {
transaction {
input { inState }
this `fails requirement` "the amounts balance"
this `fails with` "the amounts balance"
tweak {
output { outState.copy(quantity = 2000.DOLLARS.quantity) }
this `fails requirement` "the amounts balance"
this `fails with` "the amounts balance"
}
tweak {
output { outState }
// No command arguments
this `fails requirement` "required com.r3corda.contracts.asset.Obligation.Commands.Move command"
// No command commanduments
this `fails with` "required com.r3corda.contracts.asset.Obligation.Commands.Move command"
}
tweak {
output { outState }
arg(DUMMY_PUBKEY_2) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails requirement` "the owning keys are the same as the signing keys"
command(DUMMY_PUBKEY_2) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails with` "the owning keys are the same as the signing keys"
}
tweak {
output { outState }
output { outState `issued by` MINI_CORP }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails requirement` "at least one obligation input"
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails with` "at least one obligation input"
}
// Simple reallocation works.
tweak {
output { outState }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this.accepts()
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this.verifies()
}
}
}
@ -82,17 +86,17 @@ class ObligationTests {
transaction {
input { DummyState() }
output { outState }
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Move(outState.issuanceDef) }
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move(outState.issuanceDef) }
this `fails requirement` "there is at least one obligation input"
this `fails with` "there is at least one obligation input"
}
// Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
// institution is allowed to issue as much cash as they want.
transaction {
output { outState }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Issue(outState.issuanceDef) }
this `fails requirement` "output deposits are owned by a command signer"
command(DUMMY_PUBKEY_1) { Obligation.Commands.Issue(outState.issuanceDef) }
this `fails with` "output deposits are owned by a command signer"
}
transaction {
output {
@ -104,11 +108,11 @@ class ObligationTests {
)
}
tweak {
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement), 0) }
this `fails requirement` "has a nonce"
command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement), 0) }
this `fails with` "has a nonce"
}
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement)) }
this.accepts()
command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement)) }
this.verifies()
}
// Test generation works.
@ -133,14 +137,14 @@ class ObligationTests {
// Move fails: not allowed to summon money.
tweak {
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails requirement` "at obligor MegaCorp the amounts balance"
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails with` "at obligor MegaCorp the amounts balance"
}
// Issue works.
tweak {
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
this.accepts()
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
this.verifies()
}
}
@ -148,40 +152,40 @@ class ObligationTests {
transaction {
input { inState }
output { inState.copy(quantity = inState.amount.quantity / 2) }
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
this `fails requirement` "output values sum to more than the inputs"
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
this `fails with` "output values sum to more than the inputs"
}
// Can't have an issue command that doesn't actually issue money.
transaction {
input { inState }
output { inState }
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
this `fails requirement` "output values sum to more than the inputs"
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
this `fails with` "output values sum to more than the inputs"
}
// Can't have any other commands if we have an issue command (because the issue command overrules them)
transaction {
input { inState }
output { inState.copy(quantity = inState.amount.quantity * 2) }
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
tweak {
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
this `fails requirement` "only move/exit commands can be present along with other obligation commands"
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
this `fails with` "only move/exit commands can be present along with other obligation commands"
}
tweak {
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails requirement` "only move/exit commands can be present along with other obligation commands"
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails with` "only move/exit commands can be present along with other obligation commands"
}
tweak {
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.SetLifecycle(inState.issuanceDef, Lifecycle.DEFAULTED) }
this `fails requirement` "only move/exit commands can be present along with other obligation commands"
command(MEGA_CORP_PUBKEY) { Obligation.Commands.SetLifecycle(inState.issuanceDef, Lifecycle.DEFAULTED) }
this `fails with` "only move/exit commands can be present along with other obligation commands"
}
tweak {
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, inState.amount / 2) }
this `fails requirement` "only move/exit commands can be present along with other obligation commands"
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, inState.amount / 2) }
this `fails with` "only move/exit commands can be present along with other obligation commands"
}
this.accepts()
this.verifies()
}
}
@ -328,185 +332,194 @@ class ObligationTests {
@Test
fun `close-out netting`() {
// Try netting out two obligations
transactionGroupFor<Obligation.State<Currency>>() {
ledger {
obligationTestRoots(this)
transaction("Issuance") {
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
// Note we can sign with either key here
arg(ALICE_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
command(ALICE_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
timestamp(TEST_TX_TIME)
this.verifies()
}
}.verify()
this.verifies()
}
// Try netting out two obligations, with the third uninvolved obligation left
// as-is
transactionGroupFor<Obligation.State<Currency>>() {
ledger {
obligationTestRoots(this)
transaction("Issuance") {
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) }
arg(BOB_PUBKEY, MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
command(BOB_PUBKEY, MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
timestamp(TEST_TX_TIME)
this.verifies()
}
}.verify()
this.verifies()
}
// Try having outputs mis-match the inputs
transactionGroupFor<Obligation.State<Currency>>() {
ledger {
obligationTestRoots(this)
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) }
arg(BOB_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
command(BOB_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
timestamp(TEST_TX_TIME)
this `fails with` "amounts owed on input and output must match"
}
}.expectFailureOfTx(1, "amounts owed on input and output must match")
}
// Have the wrong signature on the transaction
transactionGroupFor<Obligation.State<Currency>>() {
ledger {
obligationTestRoots(this)
transaction("Issuance") {
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
timestamp(TEST_TX_TIME)
this `fails with` "any involved party has signed"
}
}.expectFailureOfTx(1, "any involved party has signed")
}
}
@Test
fun `payment netting`() {
// Try netting out two obligations
transactionGroupFor<Obligation.State<Currency>>() {
ledger {
obligationTestRoots(this)
transaction("Issuance") {
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
arg(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
command(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
timestamp(TEST_TX_TIME)
this.verifies()
}
}.verify()
this.verifies()
}
// Try netting out two obligations, but only provide one signature. Unlike close-out netting, we need both
// signatures for payment netting
transactionGroupFor<Obligation.State<Currency>>() {
ledger {
obligationTestRoots(this)
transaction("Issuance") {
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
arg(BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
command(BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
timestamp(TEST_TX_TIME)
this `fails with` "all involved parties have signed"
}
}.expectFailureOfTx(1, "all involved parties have signed")
}
// Multilateral netting, A -> B -> C which can net down to A -> C
transactionGroupFor<Obligation.State<Currency>>() {
ledger {
obligationTestRoots(this)
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) }
arg(ALICE_PUBKEY, BOB_PUBKEY, MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
command(ALICE_PUBKEY, BOB_PUBKEY, MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
timestamp(TEST_TX_TIME)
this.verifies()
}
}.verify()
this.verifies()
}
// Multilateral netting without the key of the receiving party
transactionGroupFor<Obligation.State<Currency>>() {
ledger {
obligationTestRoots(this)
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) }
arg(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
command(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
timestamp(TEST_TX_TIME)
this `fails with` "all involved parties have signed"
}
}.expectFailureOfTx(1, "all involved parties have signed")
}
}
@Test
fun `settlement`() {
// Try netting out two obligations
transactionGroupFor<Obligation.State<Currency>>() {
ledger {
obligationTestRoots(this)
transaction("Settlement") {
input("Alice's $1,000,000 obligation to Bob")
input("Alice's $1,000,000")
output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY }
arg(ALICE_PUBKEY) { Obligation.Commands.Settle<Currency>(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Amount(oneMillionDollars.quantity, USD)) }
arg(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Amount(oneMillionDollars.quantity, USD)) }
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
this.verifies()
}
}.verify()
this.verifies()
}
}
@Test
fun `payment default`() {
// Try defaulting an obligation without a timestamp
transactionGroupFor<Obligation.State<Currency>>() {
ledger {
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) }
arg(BOB_PUBKEY) { Obligation.Commands.SetLifecycle<Currency>(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), 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"
}
}.expectFailureOfTx(1, "there is a timestamp from the authority")
}
// Try defaulting an obligation due in the future
val pastTestTime = TEST_TX_TIME - Duration.ofDays(7)
val futureTestTime = TEST_TX_TIME + Duration.ofDays(7)
transactionGroupFor<Obligation.State<Currency>>() {
roots {
transaction(oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `at` futureTestTime `with notary` DUMMY_NOTARY label "Alice's $1,000,000 obligation to Bob")
}
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) `at` futureTestTime).copy(lifecycle = Lifecycle.DEFAULTED) }
arg(BOB_PUBKEY) { Obligation.Commands.SetLifecycle<Currency>(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF) `at` futureTestTime, Lifecycle.DEFAULTED) }
timestamp(TEST_TX_TIME)
}
}.expectFailureOfTx(1, "the due date has passed")
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) }
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"
}
// Try defaulting an obligation that is now in the past
transactionGroupFor<Obligation.State<Currency>>() {
roots {
transaction(oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `at` pastTestTime `with notary` DUMMY_NOTARY label "Alice's $1,000,000 obligation to Bob")
}
ledger {
transaction("Settlement") {
input("Alice's $1,000,000 obligation to Bob")
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) }
arg(BOB_PUBKEY) { Obligation.Commands.SetLifecycle<Currency>(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF) `at` pastTestTime, Lifecycle.DEFAULTED) }
command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF) `at` pastTestTime, Lifecycle.DEFAULTED) }
timestamp(TEST_TX_TIME)
this.verifies()
}
}.verify()
this.verifies()
}
}
@Test
fun testMergeSplit() {
// Splitting value works.
transaction {
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
tweak {
input { inState }
repeat(4) { output { inState.copy(quantity = inState.quantity / 4) } }
this.accepts()
this.verifies()
}
// Merging 4 inputs into 2 outputs works.
tweak {
repeat(4) { input { inState.copy(quantity = inState.quantity / 4) } }
output { inState.copy(quantity = inState.quantity / 2) }
output { inState.copy(quantity = inState.quantity / 2) }
this.accepts()
this.verifies()
}
// Merging 2 inputs into 1 works.
tweak {
input { inState.copy(quantity = inState.quantity / 2) }
input { inState.copy(quantity = inState.quantity / 2) }
output { inState }
this.accepts()
this.verifies()
}
}
}
@ -516,29 +529,30 @@ class ObligationTests {
transaction {
input { inState }
input { inState.copy(quantity = 0L) }
this `fails requirement` "zero sized inputs"
this `fails with` "zero sized inputs"
}
transaction {
input { inState }
output { inState }
output { inState.copy(quantity = 0L) }
this `fails requirement` "zero sized outputs"
this `fails with` "zero sized outputs"
}
}
@Test
fun trivialMismatches() {
// Can't change issuer.
transaction {
input { inState }
output { outState `issued by` MINI_CORP }
this `fails requirement` "at obligor MegaCorp the amounts balance"
this `fails with` "at obligor MegaCorp the amounts balance"
}
// Can't mix currencies.
transaction {
input { inState }
output { outState.copy(quantity = 80000, template = megaCorpDollarSettlement) }
output { outState.copy(quantity = 20000, template = megaCorpPoundSettlement) }
this `fails requirement` "the amounts balance"
this `fails with` "the amounts balance"
}
transaction {
input { inState }
@ -550,16 +564,16 @@ class ObligationTests {
)
}
output { outState.copy(quantity = 115000) }
this `fails requirement` "the amounts balance"
this `fails with` "the amounts balance"
}
// Can't have superfluous input states from different issuers.
transaction {
input { inState }
input { inState `issued by` MINI_CORP }
output { outState }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
this `fails requirement` "at obligor MiniCorp the amounts balance"
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
this `fails with` "at obligor MiniCorp the amounts balance"
}
}
@ -571,18 +585,18 @@ class ObligationTests {
output { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
tweak {
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, 100.DOLLARS) }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails requirement` "the amounts balance"
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 100.DOLLARS) }
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails with` "the amounts balance"
}
tweak {
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, 200.DOLLARS) }
this `fails requirement` "required com.r3corda.contracts.asset.Obligation.Commands.Move command"
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 200.DOLLARS) }
this `fails with` "required com.r3corda.contracts.asset.Obligation.Commands.Move command"
tweak {
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this.accepts()
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this.verifies()
}
}
}
@ -594,19 +608,19 @@ class ObligationTests {
output { inState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) `issued by` MINI_CORP }
output { inState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails requirement` "at obligor MegaCorp the amounts balance"
this `fails with` "at obligor MegaCorp the amounts balance"
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, 200.DOLLARS) }
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 200.DOLLARS) }
tweak {
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>((inState `issued by` MINI_CORP).issuanceDef, 0.DOLLARS) }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
this `fails requirement` "at obligor MiniCorp the amounts balance"
command(MINI_CORP_PUBKEY) { Obligation.Commands.Exit((inState `issued by` MINI_CORP).issuanceDef, 0.DOLLARS) }
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
this `fails with` "at obligor MiniCorp the amounts balance"
}
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>((inState `issued by` MINI_CORP).issuanceDef, 200.DOLLARS) }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
this.accepts()
command(MINI_CORP_PUBKEY) { Obligation.Commands.Exit((inState `issued by` MINI_CORP).issuanceDef, 200.DOLLARS) }
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
this.verifies()
}
}
@ -620,21 +634,21 @@ class ObligationTests {
// Can't merge them together.
tweak {
output { inState.copy(beneficiary = DUMMY_PUBKEY_2, quantity = 200000L) }
this `fails requirement` "at obligor MegaCorp the amounts balance"
this `fails with` "at obligor MegaCorp the amounts balance"
}
// Missing MiniCorp deposit
tweak {
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) }
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) }
this `fails requirement` "at obligor MegaCorp the amounts balance"
this `fails with` "at obligor MegaCorp the amounts balance"
}
// This works.
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) }
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) `issued by` MINI_CORP }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
this.accepts()
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
this.verifies()
}
}
@ -647,10 +661,10 @@ class ObligationTests {
input { pounds }
output { inState `owned by` DUMMY_PUBKEY_2 }
output { pounds `owned by` DUMMY_PUBKEY_1 }
arg(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Obligation.Commands.Move(inState.issuanceDef) }
arg(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Obligation.Commands.Move(pounds.issuanceDef) }
command(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Obligation.Commands.Move(inState.issuanceDef) }
command(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Obligation.Commands.Move(pounds.issuanceDef) }
this.accepts()
this.verifies()
}
}
@ -686,7 +700,7 @@ class ObligationTests {
fiveKDollarsFromMegaToMega.copy(template = megaCorpDollarSettlement.copy(acceptableContracts = nonEmptySetOf(SecureHash.Companion.randomSHA256()))).bilateralNetState)
// States must not be nettable if the trusted issuers differ
val miniCorpIssuer = nonEmptySetOf(Issued<Currency>(MINI_CORP.ref(1), USD))
val miniCorpIssuer = nonEmptySetOf(Issued(MINI_CORP.ref(1), USD))
assertNotEquals(fiveKDollarsFromMegaToMega.bilateralNetState,
fiveKDollarsFromMegaToMega.copy(template = megaCorpDollarSettlement.copy(acceptableIssuedProducts = miniCorpIssuer)).bilateralNetState)
}
@ -743,7 +757,7 @@ class ObligationTests {
val fiveKDollarsFromMegaToMini = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement,
5000.DOLLARS.quantity, MINI_CORP_PUBKEY)
val expected = mapOf(Pair(Pair(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), fiveKDollarsFromMegaToMini.amount))
val actual = extractAmountsDue<Currency>(USD, listOf(fiveKDollarsFromMegaToMini))
val actual = extractAmountsDue(USD, listOf(fiveKDollarsFromMegaToMini))
assertEquals(expected, actual)
}
@ -755,7 +769,7 @@ class ObligationTests {
Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP))
)
val expected: Map<Pair<PublicKey, PublicKey>, Amount<Currency>> = emptyMap() // Zero balances are stripped before returning
val actual = netAmountsDue<Currency>(balanced)
val actual = netAmountsDue(balanced)
assertEquals(expected, actual)
}
@ -769,7 +783,7 @@ class ObligationTests {
val expected = mapOf(
Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP))
)
var actual = netAmountsDue<Currency>(balanced)
val actual = netAmountsDue(balanced)
assertEquals(expected, actual)
}

View File

@ -0,0 +1,39 @@
package com.r3corda.core.testing
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.SecureHash
import java.io.InputStream
interface OutputStateLookup {
fun <S : ContractState> retrieveOutputStateAndRef(clazz: Class<S>, label: String): StateAndRef<S>
}
interface LedgerDSLInterpreter<R, out T : TransactionDSLInterpreter<R>> : OutputStateLookup {
fun transaction(transactionLabel: String?, dsl: TransactionDSL<R, T>.() -> R): WireTransaction
fun unverifiedTransaction(transactionLabel: String?, dsl: TransactionDSL<R, T>.() -> Unit): WireTransaction
fun tweak(dsl: LedgerDSL<R, T, LedgerDSLInterpreter<R, T>>.() -> Unit)
fun attachment(attachment: InputStream): SecureHash
fun verifies()
}
/**
* This is the class the top-level primitives deal with. It delegates all other primitives to the contained interpreter.
* This way we have a decoupling of the DSL "AST" and the interpretation(s) of it. Note how the delegation forces
* covariance of the TransactionInterpreter parameter.
*
* TODO (Kotlin 1.1): Use type synonyms to make the type params less unwieldy
*/
class LedgerDSL<R, out T : TransactionDSLInterpreter<R>, out L : LedgerDSLInterpreter<R, T>> (val interpreter: L) :
LedgerDSLInterpreter<R, TransactionDSLInterpreter<R>> by interpreter {
fun transaction(dsl: TransactionDSL<R, TransactionDSLInterpreter<R>>.() -> R) =
transaction(null, dsl)
fun unverifiedTransaction(dsl: TransactionDSL<R, TransactionDSLInterpreter<R>>.() -> Unit) =
unverifiedTransaction(null, dsl)
inline fun <reified S : ContractState> String.outputStateAndRef(): StateAndRef<S> =
retrieveOutputStateAndRef(S::class.java, this)
inline fun <reified S : ContractState> String.output(): TransactionState<S> =
outputStateAndRef<S>().state
fun String.outputRef(): StateRef = outputStateAndRef<ContractState>().ref
}

View File

@ -0,0 +1,354 @@
package com.r3corda.core.testing
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.signWithECDSA
import com.r3corda.core.node.services.IdentityService
import com.r3corda.core.node.services.StorageService
import com.r3corda.core.node.services.testing.MockStorageService
import com.r3corda.core.serialization.serialize
import java.io.InputStream
import java.security.KeyPair
import java.security.PublicKey
import java.util.*
fun transaction(
transactionLabel: String? = null,
dsl: TransactionDSL<
EnforceVerifyOrFail,
TransactionDSLInterpreter<EnforceVerifyOrFail>
>.() -> EnforceVerifyOrFail
) = JavaTestHelpers.transaction(transactionLabel, dsl)
fun ledger(
identityService: IdentityService = MOCK_IDENTITY_SERVICE,
storageService: StorageService = MockStorageService(),
dsl: LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.() -> Unit
) = JavaTestHelpers.ledger(identityService, storageService, dsl)
@Deprecated(
message = "ledger doesn't nest, use tweak",
replaceWith = ReplaceWith("tweak"),
level = DeprecationLevel.ERROR)
@Suppress("UNUSED_PARAMETER")
fun TransactionDSLInterpreter<EnforceVerifyOrFail>.ledger(
dsl: LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.() -> Unit) {
}
@Deprecated(
message = "transaction doesn't nest, use tweak",
replaceWith = ReplaceWith("tweak"),
level = DeprecationLevel.ERROR)
@Suppress("UNUSED_PARAMETER")
fun TransactionDSLInterpreter<EnforceVerifyOrFail>.transaction(
dsl: TransactionDSL<
EnforceVerifyOrFail,
TransactionDSLInterpreter<EnforceVerifyOrFail>
>.() -> EnforceVerifyOrFail) {
}
@Deprecated(
message = "ledger doesn't nest, use tweak",
replaceWith = ReplaceWith("tweak"),
level = DeprecationLevel.ERROR)
@Suppress("UNUSED_PARAMETER")
fun LedgerDSLInterpreter<EnforceVerifyOrFail, TransactionDSLInterpreter<EnforceVerifyOrFail>>.ledger(
dsl: LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.() -> Unit) {
}
/**
* If you jumped here from a compiler error make sure the last line of your test tests for a transaction verify or fail
* This is a dummy type that can only be instantiated by functions in this module. This way we can ensure that all tests
* will have as the last line either an accept or a failure test. The name is deliberately long to help make sense of
* the triggered diagnostic.
*/
sealed class EnforceVerifyOrFail {
internal object Token: EnforceVerifyOrFail()
}
/**
* This interpreter builds a transaction, and [TransactionDSL.verifies] that the resolved transaction is correct. Note
* that transactions corresponding to input states are not verified. Use [LedgerDSL.verifies] for that.
*/
data class TestTransactionDSLInterpreter(
override val ledgerInterpreter: TestLedgerDSLInterpreter,
private val inputStateRefs: ArrayList<StateRef> = arrayListOf(),
internal val outputStates: ArrayList<LabeledOutput> = arrayListOf(),
private val attachments: ArrayList<SecureHash> = arrayListOf(),
private val commands: ArrayList<Command> = arrayListOf(),
private val signers: LinkedHashSet<PublicKey> = LinkedHashSet(),
private val transactionType: TransactionType = TransactionType.General()
) : TransactionDSLInterpreter<EnforceVerifyOrFail>, OutputStateLookup by ledgerInterpreter {
private fun copy(): TestTransactionDSLInterpreter =
TestTransactionDSLInterpreter(
ledgerInterpreter = ledgerInterpreter,
inputStateRefs = ArrayList(inputStateRefs),
outputStates = ArrayList(outputStates),
attachments = ArrayList(attachments),
commands = ArrayList(commands),
signers = LinkedHashSet(signers),
transactionType = transactionType
)
internal fun toWireTransaction(): WireTransaction =
WireTransaction(
inputs = inputStateRefs,
outputs = outputStates.map { it.state },
attachments = attachments,
commands = commands,
signers = signers.toList(),
type = transactionType
)
override fun input(stateRef: StateRef) {
val notary = ledgerInterpreter.resolveStateRef<ContractState>(stateRef).notary
signers.add(notary.owningKey)
inputStateRefs.add(stateRef)
}
override fun _output(label: String?, notary: Party, contractState: ContractState) {
outputStates.add(LabeledOutput(label, TransactionState(contractState, notary)))
}
override fun attachment(attachmentId: SecureHash) {
attachments.add(attachmentId)
}
override fun _command(signers: List<PublicKey>, commandData: CommandData) {
this.signers.addAll(signers)
commands.add(Command(commandData, signers))
}
override fun verifies(): EnforceVerifyOrFail {
val resolvedTransaction = ledgerInterpreter.resolveWireTransaction(toWireTransaction())
resolvedTransaction.verify()
return EnforceVerifyOrFail.Token
}
override fun failsWith(expectedMessage: String?): EnforceVerifyOrFail {
val exceptionThrown = try {
this.verifies()
false
} catch (exception: Exception) {
if (expectedMessage != null) {
val exceptionMessage = exception.message
if (exceptionMessage == null) {
throw AssertionError(
"Expected exception containing '$expectedMessage' but raised exception had no message"
)
} else if (!exceptionMessage.toLowerCase().contains(expectedMessage.toLowerCase())) {
throw AssertionError(
"Expected exception containing '$expectedMessage' but raised exception was '$exception'"
)
}
}
true
}
if (!exceptionThrown) {
throw AssertionError("Expected exception but didn't get one")
}
return EnforceVerifyOrFail.Token
}
override fun tweak(
dsl: TransactionDSL<
EnforceVerifyOrFail,
TransactionDSLInterpreter<EnforceVerifyOrFail>
>.() -> EnforceVerifyOrFail
) = dsl(TransactionDSL(copy()))
}
class AttachmentResolutionException(attachmentId: SecureHash) :
Exception("Attachment with id $attachmentId not found")
data class TestLedgerDSLInterpreter private constructor (
private val identityService: IdentityService,
private val storageService: StorageService,
internal val labelToOutputStateAndRefs: HashMap<String, StateAndRef<ContractState>> = HashMap(),
private val transactionWithLocations: HashMap<SecureHash, WireTransactionWithLocation> = HashMap(),
private val nonVerifiedTransactionWithLocations: HashMap<SecureHash, WireTransactionWithLocation> = HashMap()
) : LedgerDSLInterpreter<EnforceVerifyOrFail, TestTransactionDSLInterpreter> {
val wireTransactions: List<WireTransaction> get() = transactionWithLocations.values.map { it.transaction }
// We specify [labelToOutputStateAndRefs] just so that Kotlin picks the primary constructor instead of cycling
constructor(identityService: IdentityService, storageService: StorageService) : this(
identityService, storageService, labelToOutputStateAndRefs = HashMap()
)
companion object {
private fun getCallerLocation(offset: Int): String {
val stackTraceElement = Thread.currentThread().stackTrace[3 + offset]
return stackTraceElement.toString()
}
}
internal data class WireTransactionWithLocation(
val label: String?,
val transaction: WireTransaction,
val location: String
)
class VerifiesFailed(transactionLocation: String, cause: Throwable) :
Exception("Transaction defined at ($transactionLocation) didn't verify: $cause", cause)
class TypeMismatch(requested: Class<*>, actual: Class<*>) :
Exception("Actual type $actual is not a subtype of requested type $requested")
internal fun copy(): TestLedgerDSLInterpreter =
TestLedgerDSLInterpreter(
identityService,
storageService,
labelToOutputStateAndRefs = HashMap(labelToOutputStateAndRefs),
transactionWithLocations = HashMap(transactionWithLocations),
nonVerifiedTransactionWithLocations = HashMap(nonVerifiedTransactionWithLocations)
)
internal fun resolveWireTransaction(wireTransaction: WireTransaction): TransactionForVerification {
return wireTransaction.run {
val authenticatedCommands = commands.map {
AuthenticatedObject(it.signers, it.signers.mapNotNull { identityService.partyFromKey(it) }, it.value)
}
val resolvedInputStates = inputs.map { resolveStateRef<ContractState>(it) }
val resolvedAttachments = attachments.map { resolveAttachment(it) }
TransactionForVerification(
inputs = resolvedInputStates,
outputs = outputs,
commands = authenticatedCommands,
origHash = wireTransaction.serialized.hash,
attachments = resolvedAttachments,
signers = signers.toList(),
type = type
)
}
}
internal inline fun <reified S : ContractState> resolveStateRef(stateRef: StateRef): TransactionState<S> {
val transactionWithLocation =
transactionWithLocations[stateRef.txhash] ?:
nonVerifiedTransactionWithLocations[stateRef.txhash] ?:
throw TransactionResolutionException(stateRef.txhash)
val output = transactionWithLocation.transaction.outputs[stateRef.index]
return if (S::class.java.isAssignableFrom(output.data.javaClass)) @Suppress("UNCHECKED_CAST") {
output as TransactionState<S>
} else {
throw TypeMismatch(requested = S::class.java, actual = output.data.javaClass)
}
}
internal fun resolveAttachment(attachmentId: SecureHash): Attachment =
storageService.attachments.openAttachment(attachmentId) ?: throw AttachmentResolutionException(attachmentId)
private fun <Return> interpretTransactionDsl(
dsl: TransactionDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter>.() -> Return
): TestTransactionDSLInterpreter {
val transactionInterpreter = TestTransactionDSLInterpreter(this)
dsl(TransactionDSL(transactionInterpreter))
return transactionInterpreter
}
fun toTransactionGroup(): TransactionGroup {
val ledgerTransactions = transactionWithLocations.map {
it.value.transaction.toLedgerTransaction(identityService, storageService.attachments)
}
val nonVerifiedLedgerTransactions = nonVerifiedTransactionWithLocations.map {
it.value.transaction.toLedgerTransaction(identityService, storageService.attachments)
}
return TransactionGroup(ledgerTransactions.toSet(), nonVerifiedLedgerTransactions.toSet())
}
fun transactionName(transactionHash: SecureHash): String? {
val transactionWithLocation = transactionWithLocations[transactionHash]
return if (transactionWithLocation != null) {
transactionWithLocation.label ?: "TX[${transactionWithLocation.location}]"
} else {
null
}
}
fun outputToLabel(state: ContractState): String? =
labelToOutputStateAndRefs.filter { it.value.state.data == state }.keys.firstOrNull()
private fun <R> recordTransactionWithTransactionMap(
transactionLabel: String?,
dsl: TransactionDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter>.() -> R,
transactionMap: HashMap<SecureHash, WireTransactionWithLocation> = HashMap()
): WireTransaction {
val transactionLocation = getCallerLocation(3)
val transactionInterpreter = interpretTransactionDsl(dsl)
// Create the WireTransaction
val wireTransaction = transactionInterpreter.toWireTransaction()
// Record the output states
transactionInterpreter.outputStates.forEachIndexed { index, labeledOutput ->
if (labeledOutput.label != null) {
labelToOutputStateAndRefs[labeledOutput.label] = wireTransaction.outRef(index)
}
}
transactionMap[wireTransaction.serialized.hash] =
WireTransactionWithLocation(transactionLabel, wireTransaction, transactionLocation)
return wireTransaction
}
override fun transaction(
transactionLabel: String?,
dsl: TransactionDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter>.() -> EnforceVerifyOrFail
) = recordTransactionWithTransactionMap(transactionLabel, dsl, transactionWithLocations)
override fun unverifiedTransaction(
transactionLabel: String?,
dsl: TransactionDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter>.() -> Unit
) = recordTransactionWithTransactionMap(transactionLabel, dsl, nonVerifiedTransactionWithLocations)
override fun tweak(
dsl: LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter,
LedgerDSLInterpreter<EnforceVerifyOrFail, TestTransactionDSLInterpreter>>.() -> Unit) =
dsl(LedgerDSL(copy()))
override fun attachment(attachment: InputStream): SecureHash {
return storageService.attachments.importAttachment(attachment)
}
override fun verifies() {
val transactionGroup = toTransactionGroup()
try {
transactionGroup.verify()
} catch (exception: TransactionVerificationException) {
throw VerifiesFailed(transactionWithLocations[exception.tx.origHash]?.location ?: "<unknown>", exception)
}
}
override fun <S : ContractState> retrieveOutputStateAndRef(clazz: Class<S>, label: String): StateAndRef<S> {
val stateAndRef = labelToOutputStateAndRefs[label]
if (stateAndRef == null) {
throw IllegalArgumentException("State with label '$label' was not found")
} else if (!clazz.isAssignableFrom(stateAndRef.state.data.javaClass)) {
throw TypeMismatch(requested = clazz, actual = stateAndRef.state.data.javaClass)
} else {
@Suppress("UNCHECKED_CAST")
return stateAndRef as StateAndRef<S>
}
}
}
fun signAll(transactionsToSign: List<WireTransaction>, extraKeys: Array<out KeyPair>) = transactionsToSign.map { wtx ->
val allPubKeys = wtx.signers.toMutableSet()
val bits = wtx.serialize()
require(bits == wtx.serialized)
val signatures = ArrayList<DigitalSignature.WithKey>()
for (key in ALL_TEST_KEYS + extraKeys) {
if (allPubKeys.contains(key.public)) {
signatures += key.signWithECDSA(bits)
allPubKeys -= key.public
}
}
SignedTransaction(bits, signatures)
}
fun LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.signAll(
transactionsToSign: List<WireTransaction> = this.interpreter.wireTransactions, vararg extraKeys: KeyPair) =
signAll(transactionsToSign, extraKeys)

View File

@ -7,18 +7,13 @@ import com.google.common.net.HostAndPort
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.*
import com.r3corda.core.node.services.IdentityService
import com.r3corda.core.node.services.StorageService
import com.r3corda.core.node.services.testing.MockIdentityService
import com.r3corda.core.node.services.testing.MockStorageService
import com.r3corda.core.seconds
import com.r3corda.core.serialization.serialize
import java.net.ServerSocket
import java.security.KeyPair
import java.security.PublicKey
import java.time.Instant
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.fail
/** If an exception is thrown by the body, rethrows the root cause exception. */
inline fun <R> rootCauseExceptions(body: () -> R): R {
@ -95,9 +90,23 @@ object JavaTestHelpers {
@JvmStatic fun generateStateRef() = StateRef(SecureHash.randomSHA256(), 0)
@JvmStatic fun transaction(body: TransactionForTest.() -> LastLineShouldTestForAcceptOrFailure): LastLineShouldTestForAcceptOrFailure {
return body(TransactionForTest())
@JvmStatic @JvmOverloads fun ledger(
identityService: IdentityService = MOCK_IDENTITY_SERVICE,
storageService: StorageService = MockStorageService(),
dsl: LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.() -> Unit
): LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter> {
val ledgerDsl = LedgerDSL(TestLedgerDSLInterpreter(identityService, storageService))
dsl(ledgerDsl)
return ledgerDsl
}
@JvmStatic @JvmOverloads fun transaction(
transactionLabel: String? = null,
dsl: TransactionDSL<
EnforceVerifyOrFail,
TransactionDSLInterpreter<EnforceVerifyOrFail>
>.() -> EnforceVerifyOrFail
) = ledger { transaction(transactionLabel, dsl) }
}
val TEST_TX_TIME = JavaTestHelpers.TEST_TX_TIME
@ -124,27 +133,6 @@ val MOCK_IDENTITY_SERVICE = JavaTestHelpers.MOCK_IDENTITY_SERVICE
fun generateStateRef() = JavaTestHelpers.generateStateRef()
fun transaction(body: TransactionForTest.() -> LastLineShouldTestForAcceptOrFailure) = JavaTestHelpers.transaction(body)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Defines a simple DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.
//
// Define a transaction like this:
//
// transaction {
// input { someExpression }
// output { someExpression }
// arg { someExpression }
//
// tweak {
// ... same thing but works with a copy of the parent, can add inputs/outputs/args just within this scope.
// }
//
// contract.accepts() -> should pass
// contract `fails requirement` "some substring of the error message"
// }
//
class LabeledOutput(val label: String?, val state: TransactionState<*>) {
override fun toString() = state.toString() + (if (label != null) " ($label)" else "")
override fun equals(other: Any?) = other is LabeledOutput && state.equals(other.state)
@ -153,298 +141,3 @@ class LabeledOutput(val label: String?, val state: TransactionState<*>) {
infix fun TransactionState<*>.label(label: String) = LabeledOutput(label, this)
abstract class AbstractTransactionForTest {
protected val attachments = ArrayList<SecureHash>()
protected val outStates = ArrayList<LabeledOutput>()
protected val commands = ArrayList<Command>()
protected val signers = LinkedHashSet<PublicKey>()
protected val type = TransactionType.General()
@JvmOverloads
open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, TransactionState(s(), DUMMY_NOTARY)).apply { outStates.add(this) }
@JvmOverloads
open fun output(label: String? = null, s: ContractState) = output(label) { s }
protected fun commandsToAuthenticatedObjects(): List<AuthenticatedObject<CommandData>> {
return commands.map { AuthenticatedObject(it.signers, it.signers.mapNotNull { MOCK_IDENTITY_SERVICE.partyFromKey(it) }, it.value) }
}
fun attachment(attachmentID: SecureHash) {
attachments.add(attachmentID)
}
fun arg(vararg keys: PublicKey, c: () -> CommandData) {
val keysList = listOf(*keys)
addCommand(Command(c(), keysList))
}
fun arg(key: PublicKey, c: CommandData) = arg(key) { c }
fun timestamp(time: Instant) {
val data = TimestampCommand(time, 30.seconds)
timestamp(data)
}
fun timestamp(data: TimestampCommand) {
addCommand(Command(data, DUMMY_NOTARY.owningKey))
}
fun addCommand(cmd: Command) {
signers.addAll(cmd.signers)
commands.add(cmd)
}
// Forbid patterns like: transaction { ... transaction { ... } }
@Deprecated("Cannot nest transactions, use tweak", level = DeprecationLevel.ERROR)
fun transaction(body: TransactionForTest.() -> LastLineShouldTestForAcceptOrFailure) {
}
}
/** If you jumped here from a compiler error make sure the last line of your test tests for a transaction accept or fail
* This is a dummy type that can only be instantiated by functions in this module. This way we can ensure that all tests
* will have as the last line either an accept or a failure test. The name is deliberately long to help make sense of
* the triggered diagnostic
*/
sealed class LastLineShouldTestForAcceptOrFailure {
internal object Token: LastLineShouldTestForAcceptOrFailure()
}
// Corresponds to the args to Contract.verify
// Note on defaults: try to avoid Kotlin defaults as they don't work from Java. Instead define overloads
open class TransactionForTest : AbstractTransactionForTest() {
private val inStates = arrayListOf<TransactionState<ContractState>>()
fun input(s: () -> ContractState) {
signers.add(DUMMY_NOTARY.owningKey)
inStates.add(TransactionState(s(), DUMMY_NOTARY))
}
fun input(s: ContractState) = input { s }
protected fun runCommandsAndVerify(time: Instant) {
val cmds = commandsToAuthenticatedObjects()
val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.Companion.randomSHA256(), signers.toList(), type)
tx.verify()
}
@JvmOverloads
fun accepts(time: Instant = TEST_TX_TIME): LastLineShouldTestForAcceptOrFailure {
runCommandsAndVerify(time)
return LastLineShouldTestForAcceptOrFailure.Token
}
@JvmOverloads
fun rejects(withMessage: String? = null, time: Instant = TEST_TX_TIME): LastLineShouldTestForAcceptOrFailure {
val r = try {
runCommandsAndVerify(time)
false
} catch (e: Exception) {
val m = e.message
if (m == null)
fail("Threw exception without a message")
else
if (withMessage != null && !m.toLowerCase().contains(withMessage.toLowerCase())) throw AssertionError("Error was actually: $m", e)
true
}
if (!r) throw AssertionError("Expected exception but didn't get one")
return LastLineShouldTestForAcceptOrFailure.Token
}
/**
* Used to confirm that the test, when (implicitly) run against the .verify() method, fails with the text of the message
*/
infix fun `fails requirement`(msg: String): LastLineShouldTestForAcceptOrFailure = rejects(msg)
fun failsRequirement(msg: String) = this.`fails requirement`(msg)
// Use this to create transactions where the output of this transaction is automatically used as an input of
// the next.
fun chain(vararg outputLabels: String, body: TransactionForTest.() -> LastLineShouldTestForAcceptOrFailure): TransactionForTest {
val states = outStates.mapNotNull {
val l = it.label
if (l != null && outputLabels.contains(l))
it.state
else
null
}
val tx = TransactionForTest()
tx.inStates.addAll(states)
tx.body()
return tx
}
// Allow customisation of partial transactions.
fun tweak(body: TransactionForTest.() -> LastLineShouldTestForAcceptOrFailure): LastLineShouldTestForAcceptOrFailure {
val tx = TransactionForTest()
tx.inStates.addAll(inStates)
tx.outStates.addAll(outStates)
tx.commands.addAll(commands)
tx.signers.addAll(tx.inStates.map { it.notary.owningKey })
tx.signers.addAll(commands.flatMap { it.signers })
return tx.body()
}
override fun toString(): String {
return """transaction {
inputs: $inStates
outputs: $outStates
commands $commands
}"""
}
override fun equals(other: Any?) = this === other || (other is TransactionForTest && inStates == other.inStates && outStates == other.outStates && commands == other.commands)
override fun hashCode(): Int {
var result = inStates.hashCode()
result += 31 * result + outStates.hashCode()
result += 31 * result + commands.hashCode()
return result
}
}
class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
open inner class WireTransactionDSL : AbstractTransactionForTest() {
private val inStates = ArrayList<StateRef>()
fun input(label: String) {
val notaryKey = label.output.notary.owningKey
signers.add(notaryKey)
inStates.add(label.outputRef)
}
fun toWireTransaction() = WireTransaction(inStates, attachments, outStates.map { it.state }, commands, signers.toList(), type)
}
val String.output: TransactionState<T>
get() = labelToOutputs[this] ?: throw IllegalArgumentException("State with label '$this' was not found")
val String.outputRef: StateRef get() = labelToRefs[this] ?: throw IllegalArgumentException("Unknown label \"$this\"")
fun <C : ContractState> lookup(label: String): StateAndRef<C> {
val output = label.output
val newOutput = TransactionState(output.data as C, output.notary)
return StateAndRef(newOutput, label.outputRef)
}
private inner class InternalWireTransactionDSL : WireTransactionDSL() {
fun finaliseAndInsertLabels(): WireTransaction {
val wtx = toWireTransaction()
for ((index, labelledState) in outStates.withIndex()) {
if (labelledState.label != null) {
labelToRefs[labelledState.label] = StateRef(wtx.id, index)
if (stateType.isInstance(labelledState.state.data)) {
labelToOutputs[labelledState.label] = labelledState.state as TransactionState<T>
}
outputsToLabels[labelledState.state] = labelledState.label
}
}
return wtx
}
}
private val rootTxns = ArrayList<WireTransaction>()
private val labelToRefs = HashMap<String, StateRef>()
private val labelToOutputs = HashMap<String, TransactionState<T>>()
private val outputsToLabels = HashMap<TransactionState<*>, String>()
fun labelForState(output: TransactionState<*>): String? = outputsToLabels[output]
inner class Roots {
fun transaction(vararg outputStates: LabeledOutput): Roots {
val outs = outputStates.map { it.state }
val wtx = WireTransaction(emptyList(), emptyList(), outs, emptyList(), emptyList(), TransactionType.General())
for ((index, state) in outputStates.withIndex()) {
val label = state.label!!
labelToRefs[label] = StateRef(wtx.id, index)
outputsToLabels[state.state] = label
labelToOutputs[label] = state.state as TransactionState<T>
}
rootTxns.add(wtx)
return this
}
/**
* Note: Don't delete, this is intended to trigger compiler diagnostic when the DSL primitive is used in the wrong place
*/
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
fun roots(body: Roots.() -> Unit) {
}
/**
* Note: Don't delete, this is intended to trigger compiler diagnostic when the DSL primitive is used in the wrong place
*/
@Deprecated("Use the vararg form of transaction inside roots", level = DeprecationLevel.ERROR)
fun transaction(body: WireTransactionDSL.() -> Unit) {
}
}
fun roots(body: Roots.() -> Unit) = Roots().apply { body() }
val txns = ArrayList<WireTransaction>()
private val txnToLabelMap = HashMap<SecureHash, String>()
@JvmOverloads
fun transaction(label: String? = null, body: WireTransactionDSL.() -> Unit): WireTransaction {
val forTest = InternalWireTransactionDSL()
forTest.body()
val wtx = forTest.finaliseAndInsertLabels()
txns.add(wtx)
if (label != null)
txnToLabelMap[wtx.id] = label
return wtx
}
fun labelForTransaction(tx: WireTransaction): String? = txnToLabelMap[tx.id]
fun labelForTransaction(tx: LedgerTransaction): String? = txnToLabelMap[tx.id]
/**
* Note: Don't delete, this is intended to trigger compiler diagnostic when the DSL primitive is used in the wrong place
*/
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
fun transactionGroup(body: TransactionGroupDSL<T>.() -> Unit) {
}
fun toTransactionGroup() = TransactionGroup(
txns.map { it.toLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments) }.toSet(),
rootTxns.map { it.toLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments) }.toSet()
)
class Failed(val index: Int, cause: Throwable) : Exception("Transaction $index didn't verify", cause)
fun verify() {
val group = toTransactionGroup()
try {
group.verify()
} catch (e: TransactionVerificationException) {
// Let the developer know the index of the transaction that failed.
val wtx: WireTransaction = txns.find { it.id == e.tx.origHash }!!
throw Failed(txns.indexOf(wtx) + 1, e)
}
}
fun expectFailureOfTx(index: Int, message: String): Exception {
val e = assertFailsWith(Failed::class) {
verify()
}
assertEquals(index, e.index)
if (!(e.cause?.message ?: "") .contains(message))
throw AssertionError("Exception should have said '$message' but was actually: ${e.cause?.message}", e.cause)
return e
}
fun signAll(txnsToSign: List<WireTransaction> = txns, vararg extraKeys: KeyPair): List<SignedTransaction> {
return txnsToSign.map { wtx ->
val allPubKeys = wtx.signers.toMutableSet()
val bits = wtx.serialize()
require(bits == wtx.serialized)
val sigs = ArrayList<DigitalSignature.WithKey>()
for (key in ALL_TEST_KEYS + extraKeys) {
if (allPubKeys.contains(key.public)) {
sigs += key.signWithECDSA(bits)
allPubKeys -= key.public
}
}
SignedTransaction(bits, sigs)
}
}
}
inline fun <reified T : ContractState> transactionGroupFor(body: TransactionGroupDSL<T>.() -> Unit) = TransactionGroupDSL<T>(T::class.java).apply { this.body() }
fun transactionGroup(body: TransactionGroupDSL<ContractState>.() -> Unit) = TransactionGroupDSL(ContractState::class.java).apply { this.body() }

View File

@ -0,0 +1,88 @@
package com.r3corda.core.testing
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.seconds
import java.security.PublicKey
import java.time.Instant
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Defines a simple DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.
//
// Define a transaction like this:
//
// ledger {
// transaction {
// input { someExpression }
// output { someExpression }
// command { someExpression }
//
// tweak {
// ... same thing but works with a copy of the parent, can add inputs/outputs/commands just within this scope.
// }
//
// contract.verifies() -> verify() should pass
// contract `fails with` "some substring of the error message"
// }
// }
//
/**
* The [TransactionDSLInterpreter] defines the interface DSL interpreters should satisfy. No
* overloading/default valuing should be done here, only the basic functions that are required to implement everything.
* Same goes for functions requiring reflection e.g. [OutputStateLookup.retrieveOutputStateAndRef]
* Put convenience functions in [TransactionDSL] instead. There are some cases where the overloads would clash with the
* Interpreter interface, in these cases define a "backing" function in the interface instead (e.g. [_command]).
*
* This way the responsibility of providing a nice frontend DSL and the implementation(s) are separated.
*/
interface TransactionDSLInterpreter<R> : OutputStateLookup {
val ledgerInterpreter: LedgerDSLInterpreter<R, TransactionDSLInterpreter<R>>
fun input(stateRef: StateRef)
fun _output(label: String?, notary: Party, contractState: ContractState)
fun attachment(attachmentId: SecureHash)
fun _command(signers: List<PublicKey>, commandData: CommandData)
fun verifies(): R
fun failsWith(expectedMessage: String?): R
fun tweak(
dsl: TransactionDSL<R, TransactionDSLInterpreter<R>>.() -> R
): R
}
class TransactionDSL<R, out T : TransactionDSLInterpreter<R>> (val interpreter: T) :
TransactionDSLInterpreter<R> by interpreter {
fun input(stateLabel: String) = input(retrieveOutputStateAndRef(ContractState::class.java, stateLabel).ref)
/**
* Adds the passed in state as a non-verified transaction output to the ledger and adds that as an input.
*/
fun input(state: ContractState) {
val transaction = ledgerInterpreter.unverifiedTransaction(null) {
output { state }
}
input(transaction.outRef<ContractState>(0).ref)
}
fun input(stateClosure: () -> ContractState) = input(stateClosure())
@JvmOverloads
fun output(label: String? = null, notary: Party = DUMMY_NOTARY, contractStateClosure: () -> ContractState) =
_output(label, notary, contractStateClosure())
@JvmOverloads
fun output(label: String? = null, contractState: ContractState) =
_output(label, DUMMY_NOTARY, contractState)
fun command(vararg signers: PublicKey, commandDataClosure: () -> CommandData) =
_command(listOf(*signers), commandDataClosure())
fun command(signer: PublicKey, commandData: CommandData) = _command(listOf(signer), commandData)
@JvmOverloads
fun timestamp(time: Instant, notary: PublicKey = DUMMY_NOTARY.owningKey) =
timestamp(TimestampCommand(time, 30.seconds), notary)
@JvmOverloads
fun timestamp(data: TimestampCommand, notary: PublicKey = DUMMY_NOTARY.owningKey) = command(notary, data)
fun fails() = failsWith(null)
infix fun `fails with`(msg: String) = failsWith(msg)
}

View File

@ -47,33 +47,36 @@ class TransactionGroupTests {
@Test
fun success() {
transactionGroup {
roots {
transaction(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY label "£1000")
ledger {
unverifiedTransaction {
output("£1000") { A_THOUSAND_POUNDS }
}
transaction {
input("£1000")
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY }
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
this.verifies()
}
transaction {
input("alice's £1000")
arg(ALICE_PUBKEY) { TestCash.Commands.Move() }
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Exit(1000.POUNDS) }
command(ALICE_PUBKEY) { TestCash.Commands.Move() }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Exit(1000.POUNDS) }
this.verifies()
}
verify()
this.verifies()
}
}
@Test
fun conflict() {
transactionGroup {
ledger {
val t = transaction {
output("cash") { A_THOUSAND_POUNDS }
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() }
this.verifies()
}
val conflict1 = transaction {
@ -81,10 +84,11 @@ class TransactionGroupTests {
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB_PUBKEY
output { HALF }
output { HALF }
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
this.verifies()
}
verify()
verifies()
// Alice tries to double spend back to herself.
val conflict2 = transaction {
@ -92,13 +96,14 @@ class TransactionGroupTests {
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE_PUBKEY
output { HALF }
output { HALF }
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
this.verifies()
}
assertNotEquals(conflict1, conflict2)
val e = assertFailsWith(TransactionConflictException::class) {
verify()
verifies()
}
assertEquals(StateRef(t.id, 0), e.conflictRef)
assertEquals(setOf(conflict1.id, conflict2.id), setOf(e.tx1.id, e.tx2.id))
@ -108,79 +113,83 @@ class TransactionGroupTests {
@Test
fun disconnected() {
// Check that if we have a transaction in the group that doesn't connect to anything else, it's rejected.
val tg = transactionGroup {
val tg = ledger {
transaction {
output("cash") { A_THOUSAND_POUNDS }
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() }
this.verifies()
}
transaction {
input("cash")
output { A_THOUSAND_POUNDS `owned by` BOB_PUBKEY }
this.verifies()
}
}
// We have to do this manually without the DSL because transactionGroup { } won't let us create a tx that
// points nowhere.
val input = StateAndRef(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY, generateStateRef())
tg.txns += TransactionType.General.Builder().apply {
addInputState(input)
addOutputState(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY)
addCommand(TestCash.Commands.Move(), BOB_PUBKEY)
}.toWireTransaction()
val e = assertFailsWith(TransactionResolutionException::class) {
tg.verify()
tg.apply {
transaction {
assertFailsWith(TransactionResolutionException::class) {
input(input.ref)
}
this.verifies()
}
}
assertEquals(e.hash, input.ref.txhash)
}
@Test
fun duplicatedInputs() {
// Check that a transaction cannot refer to the same input more than once.
transactionGroup {
roots {
transaction(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY label "£1000")
ledger {
unverifiedTransaction {
output("£1000") { A_THOUSAND_POUNDS }
}
transaction {
input("£1000")
input("£1000")
output { A_THOUSAND_POUNDS.copy(amount = A_THOUSAND_POUNDS.amount * 2) }
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
this.verifies()
}
assertFailsWith(TransactionConflictException::class) {
verify()
verifies()
}
}
}
@Test
fun signGroup() {
val signedTxns: List<SignedTransaction> = transactionGroup {
ledger {
transaction {
output("£1000") { A_THOUSAND_POUNDS }
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() }
this.verifies()
}
transaction {
input("£1000")
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY }
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
this.verifies()
}
transaction {
input("alice's £1000")
arg(ALICE_PUBKEY) { TestCash.Commands.Move() }
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Exit(1000.POUNDS) }
command(ALICE_PUBKEY) { TestCash.Commands.Move() }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Exit(1000.POUNDS) }
this.verifies()
}
}.signAll()
// Now go through the conversion -> verification path with them.
val ltxns = signedTxns.map {
it.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments)
}.toSet()
TransactionGroup(ltxns, emptySet()).verify()
val signedTxns: List<SignedTransaction> = signAll()
// Now go through the conversion -> verification path with them.
val ltxns = signedTxns.map {
it.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments)
}.toSet()
TransactionGroup(ltxns, emptySet()).verify()
}
}
}

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,10 +249,11 @@ class TwoPartyTradeProtocolTests {
@Test
fun `check dependencies of sale asset are resolved`() {
transactionGroupFor<ContractState> {
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)
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)
ledger(storageService = aliceNode.storage) {
// Insert a prospectus type attachment into the commercial paper transaction.
val stream = ByteArrayOutputStream()
@ -258,7 +262,7 @@ class TwoPartyTradeProtocolTests {
it.write("Our commercial paper is top notch stuff".toByteArray())
it.closeEntry()
}
val attachmentID = aliceNode.storage.attachments.importAttachment(ByteArrayInputStream(stream.toByteArray()))
val attachmentID = attachment(ByteArrayInputStream(stream.toByteArray()))
val issuer = MEGA_CORP.ref(1)
val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public, issuer).second
@ -275,7 +279,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,20 +354,23 @@ 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,
expectedMessageSubstring: String) {
private fun LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.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)
val bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY)
@ -385,7 +392,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,10 +418,11 @@ 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> {
val signed: List<SignedTransaction> = signAll(wtxToSign, *extraKeys)
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
if (validatedTransactions is RecordingTransactionStorage) {
@ -423,9 +431,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<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.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,52 +443,64 @@ 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)
if (withError) {
this.fails()
} else {
this.verifies()
}
}
// Bob gets some cash onto the ledger from BoE
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() }
this.verifies()
}
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() }
this.verifies()
}
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<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.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)
if (withError) {
this.fails()
} else {
this.verifies()
}
}
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<EnforceVerifyOrFail, 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>) {
}
})
}
}
}