mirror of
https://github.com/corda/corda.git
synced 2024-12-24 07:06:44 +00:00
commit
c38f99419f
@ -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}")
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
354
core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt
Normal file
354
core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt
Normal 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)
|
@ -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() }
|
||||
|
@ -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)
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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>) {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user