mirror of
https://github.com/corda/corda.git
synced 2025-01-12 07:52:38 +00:00
commit
c38f99419f
@ -129,7 +129,7 @@ task integrationTest(type: Test) {
|
|||||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||||
}
|
}
|
||||||
test.dependsOn(integrationTest)
|
test.finalizedBy(integrationTest)
|
||||||
|
|
||||||
tasks.withType(Test) {
|
tasks.withType(Test) {
|
||||||
reports.html.destination = file("${reporting.baseDir}/${name}")
|
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.contracts.PartyAndReference;
|
||||||
import com.r3corda.core.serialization.OpaqueBytes;
|
import com.r3corda.core.serialization.OpaqueBytes;
|
||||||
|
import kotlin.Unit;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import static com.r3corda.core.testing.JavaTestHelpers.*;
|
import static com.r3corda.core.testing.JavaTestHelpers.*;
|
||||||
@ -20,39 +21,41 @@ public class CashTestsJava {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void trivial() {
|
public void trivial() {
|
||||||
|
ledger(lg -> {
|
||||||
|
lg.transaction(tx -> {
|
||||||
|
tx.input(inState);
|
||||||
|
tx.failsWith("the amounts balance");
|
||||||
|
|
||||||
transaction(tx -> {
|
tx.tweak(tw -> {
|
||||||
tx.input(inState);
|
tw.output(new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), getDUMMY_PUBKEY_2()));
|
||||||
tx.failsRequirement("the amounts balance");
|
return tw.failsWith("the amounts balance");
|
||||||
|
});
|
||||||
|
|
||||||
tx.tweak(tw -> {
|
tx.tweak(tw -> {
|
||||||
tw.output(new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), getDUMMY_PUBKEY_2()));
|
tw.output(outState);
|
||||||
return tw.failsRequirement("the amounts balance");
|
// 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 -> {
|
// Simple reallocation works.
|
||||||
tw.output(outState);
|
return tx.tweak(tw -> {
|
||||||
// No command arguments
|
tw.output(outState);
|
||||||
return tw.failsRequirement("required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command");
|
tw.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
|
||||||
});
|
return tw.verifies();
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
|
return Unit.INSTANCE;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,10 @@ import kotlin.test.assertFailsWith
|
|||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
interface ICommercialPaperTestTemplate {
|
interface ICommercialPaperTestTemplate {
|
||||||
open fun getPaper(): ICommercialPaperState
|
fun getPaper(): ICommercialPaperState
|
||||||
open fun getIssueCommand(): CommandData
|
fun getIssueCommand(): CommandData
|
||||||
open fun getRedeemCommand(): CommandData
|
fun getRedeemCommand(): CommandData
|
||||||
open fun getMoveCommand(): CommandData
|
fun getMoveCommand(): CommandData
|
||||||
}
|
}
|
||||||
|
|
||||||
class JavaCommercialPaperTest() : ICommercialPaperTestTemplate {
|
class JavaCommercialPaperTest() : ICommercialPaperTestTemplate {
|
||||||
@ -63,81 +63,113 @@ class CommercialPaperTestsGeneric {
|
|||||||
val issuer = MEGA_CORP.ref(123)
|
val issuer = MEGA_CORP.ref(123)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun ok() {
|
fun `trade lifecycle test`() {
|
||||||
trade().verify()
|
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
|
// Some CP is issued onto the ledger by MegaCorp.
|
||||||
fun `not matured at redemption`() {
|
transaction("Issuance") {
|
||||||
trade(redemptionTime = TEST_TX_TIME + 2.days).expectFailureOfTx(3, "must have matured")
|
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
|
@Test
|
||||||
fun `key mismatch at issue`() {
|
fun `key mismatch at issue`() {
|
||||||
transactionGroup {
|
transaction {
|
||||||
transaction {
|
output { thisTest.getPaper() }
|
||||||
output { thisTest.getPaper() }
|
command(DUMMY_PUBKEY_1) { thisTest.getIssueCommand() }
|
||||||
arg(DUMMY_PUBKEY_1) { thisTest.getIssueCommand() }
|
timestamp(TEST_TX_TIME)
|
||||||
timestamp(TEST_TX_TIME)
|
this `fails with` "signed by the claimed issuer"
|
||||||
}
|
|
||||||
|
|
||||||
expectFailureOfTx(1, "signed by the claimed issuer")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `face value is not zero`() {
|
fun `face value is not zero`() {
|
||||||
transactionGroup {
|
transaction {
|
||||||
transaction {
|
output { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) }
|
||||||
output { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) }
|
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
||||||
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
timestamp(TEST_TX_TIME)
|
||||||
timestamp(TEST_TX_TIME)
|
this `fails with` "face value is not zero"
|
||||||
}
|
|
||||||
|
|
||||||
expectFailureOfTx(1, "face value is not zero")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `maturity date not in the past`() {
|
fun `maturity date not in the past`() {
|
||||||
transactionGroup {
|
transaction {
|
||||||
transaction {
|
output { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) }
|
||||||
output { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) }
|
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
||||||
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
timestamp(TEST_TX_TIME)
|
||||||
timestamp(TEST_TX_TIME)
|
this `fails with` "maturity date is not in the past"
|
||||||
}
|
|
||||||
|
|
||||||
expectFailureOfTx(1, "maturity date is not in the past")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `issue cannot replace an existing state`() {
|
fun `issue cannot replace an existing state`() {
|
||||||
transactionGroup {
|
transaction {
|
||||||
roots {
|
input(thisTest.getPaper())
|
||||||
transaction(thisTest.getPaper() `with notary` DUMMY_NOTARY label "paper")
|
output { thisTest.getPaper() }
|
||||||
}
|
command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
||||||
transaction {
|
timestamp(TEST_TX_TIME)
|
||||||
input("paper")
|
this `fails with` "there is no input state"
|
||||||
output { thisTest.getPaper() }
|
|
||||||
arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
|
|
||||||
timestamp(TEST_TX_TIME)
|
|
||||||
}
|
|
||||||
|
|
||||||
expectFailureOfTx(1, "there is no input state")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `did not receive enough money at redemption`() {
|
|
||||||
trade(aliceGetsBack = 700.DOLLARS `issued by` issuer).expectFailureOfTx(3, "received amount equals the face value")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `paper must be destroyed by redemption`() {
|
|
||||||
trade(destroyPaperAtRedemption = false).expectFailureOfTx(3, "must be destroyed")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T : ContractState> cashOutputsToWallet(vararg outputs: TransactionState<T>): Pair<LedgerTransaction, List<StateAndRef<T>>> {
|
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())
|
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)) })
|
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()
|
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
|
@Test
|
||||||
fun ok() {
|
fun ok() {
|
||||||
trade().verify()
|
trade().verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ok with groups`() {
|
fun `ok with groups`() {
|
||||||
tradegroups().verify()
|
tradegroups().verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -360,38 +360,40 @@ class IRSTests {
|
|||||||
/**
|
/**
|
||||||
* Generates a typical transactional history for an IRS.
|
* 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 ld = LocalDate.of(2016, 3, 8)
|
||||||
val bd = BigDecimal("0.0063518")
|
val bd = BigDecimal("0.0063518")
|
||||||
|
|
||||||
val txgroup: TransactionGroupDSL<InterestRateSwap.State> = transactionGroupFor() {
|
return ledger {
|
||||||
transaction("Agreement") {
|
transaction("Agreement") {
|
||||||
output("irs post agreement") { singleIRS() }
|
output("irs post agreement") { singleIRS() }
|
||||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||||
timestamp(TEST_TX_TIME)
|
timestamp(TEST_TX_TIME)
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction("Fix") {
|
transaction("Fix") {
|
||||||
input("irs post agreement")
|
input("irs post agreement")
|
||||||
|
val postAgreement = "irs post agreement".output<InterestRateSwap.State>()
|
||||||
output("irs post first fixing") {
|
output("irs post first fixing") {
|
||||||
"irs post agreement".output.data.copy(
|
postAgreement.data.copy(
|
||||||
"irs post agreement".output.data.fixedLeg,
|
postAgreement.data.fixedLeg,
|
||||||
"irs post agreement".output.data.floatingLeg,
|
postAgreement.data.floatingLeg,
|
||||||
"irs post agreement".output.data.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
|
postAgreement.data.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
|
||||||
"irs post agreement".output.data.common
|
postAgreement.data.common
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
arg(ORACLE_PUBKEY) {
|
command(ORACLE_PUBKEY) {
|
||||||
InterestRateSwap.Commands.Fix()
|
InterestRateSwap.Commands.Fix()
|
||||||
}
|
}
|
||||||
arg(ORACLE_PUBKEY) {
|
command(ORACLE_PUBKEY) {
|
||||||
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
|
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
|
||||||
}
|
}
|
||||||
timestamp(TEST_TX_TIME)
|
timestamp(TEST_TX_TIME)
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return txgroup
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -399,9 +401,9 @@ class IRSTests {
|
|||||||
transaction {
|
transaction {
|
||||||
input() { singleIRS() }
|
input() { singleIRS() }
|
||||||
output("irs post agreement") { singleIRS() }
|
output("irs post agreement") { singleIRS() }
|
||||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||||
timestamp(TEST_TX_TIME)
|
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() {
|
output() {
|
||||||
irs.copy(calculation = irs.calculation.copy(fixedLegPaymentSchedule = emptySchedule))
|
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)
|
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() {
|
output() {
|
||||||
irs.copy(calculation = irs.calculation.copy(floatingLegPaymentSchedule = emptySchedule))
|
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)
|
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() {
|
output() {
|
||||||
irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(quantity = 0)))
|
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)
|
timestamp(TEST_TX_TIME)
|
||||||
this `fails requirement` "All notionals must be non zero"
|
this `fails with` "All notionals must be non zero"
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
output() {
|
output() {
|
||||||
irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(quantity = 0)))
|
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)
|
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() {
|
output() {
|
||||||
modifiedIRS
|
modifiedIRS
|
||||||
}
|
}
|
||||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||||
timestamp(TEST_TX_TIME)
|
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() {
|
output() {
|
||||||
modifiedIRS
|
modifiedIRS
|
||||||
}
|
}
|
||||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||||
timestamp(TEST_TX_TIME)
|
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() {
|
output() {
|
||||||
modifiedIRS
|
modifiedIRS
|
||||||
}
|
}
|
||||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||||
timestamp(TEST_TX_TIME)
|
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() {
|
output() {
|
||||||
modifiedIRS1
|
modifiedIRS1
|
||||||
}
|
}
|
||||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||||
timestamp(TEST_TX_TIME)
|
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)))
|
val modifiedIRS2 = irs.copy(floatingLeg = irs.floatingLeg.copy(terminationDate = irs.floatingLeg.effectiveDate.minusDays(1)))
|
||||||
@ -518,9 +520,9 @@ class IRSTests {
|
|||||||
output() {
|
output() {
|
||||||
modifiedIRS2
|
modifiedIRS2
|
||||||
}
|
}
|
||||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||||
timestamp(TEST_TX_TIME)
|
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() {
|
output() {
|
||||||
modifiedIRS3
|
modifiedIRS3
|
||||||
}
|
}
|
||||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||||
timestamp(TEST_TX_TIME)
|
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() {
|
output() {
|
||||||
modifiedIRS4
|
modifiedIRS4
|
||||||
}
|
}
|
||||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||||
timestamp(TEST_TX_TIME)
|
timestamp(TEST_TX_TIME)
|
||||||
this `fails requirement` "The effective dates are aligned"
|
this `fails with` "The effective dates are aligned"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `various fixing tests`() {
|
fun `various fixing tests`() {
|
||||||
|
|
||||||
val ld = LocalDate.of(2016, 3, 8)
|
val ld = LocalDate.of(2016, 3, 8)
|
||||||
val bd = BigDecimal("0.0063518")
|
val bd = BigDecimal("0.0063518")
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
output("irs post agreement") { singleIRS() }
|
output("irs post agreement") { singleIRS() }
|
||||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||||
timestamp(TEST_TX_TIME)
|
timestamp(TEST_TX_TIME)
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
val oldIRS = singleIRS(1)
|
val oldIRS = singleIRS(1)
|
||||||
@ -578,31 +579,31 @@ class IRSTests {
|
|||||||
|
|
||||||
// Templated tweak for reference. A corrent fixing applied should be ok
|
// Templated tweak for reference. A corrent fixing applied should be ok
|
||||||
tweak {
|
tweak {
|
||||||
arg(ORACLE_PUBKEY) {
|
command(ORACLE_PUBKEY) {
|
||||||
InterestRateSwap.Commands.Fix()
|
InterestRateSwap.Commands.Fix()
|
||||||
}
|
}
|
||||||
timestamp(TEST_TX_TIME)
|
timestamp(TEST_TX_TIME)
|
||||||
arg(ORACLE_PUBKEY) {
|
command(ORACLE_PUBKEY) {
|
||||||
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
|
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
|
||||||
}
|
}
|
||||||
output() { newIRS }
|
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
|
// This test makes sure that verify confirms the fixing was applied and there is a difference in the old and new
|
||||||
tweak {
|
tweak {
|
||||||
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
|
command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
|
||||||
timestamp(TEST_TX_TIME)
|
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 }
|
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)
|
// This tests tries to sneak in a change to another fixing (which may or may not be the latest one)
|
||||||
tweak {
|
tweak {
|
||||||
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
|
command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
|
||||||
timestamp(TEST_TX_TIME)
|
timestamp(TEST_TX_TIME)
|
||||||
arg(ORACLE_PUBKEY) {
|
command(ORACLE_PUBKEY) {
|
||||||
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
|
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -619,14 +620,14 @@ class IRSTests {
|
|||||||
newIRS.common
|
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
|
// This tests modifies the payment currency for the fixing
|
||||||
tweak {
|
tweak {
|
||||||
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
|
command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
|
||||||
timestamp(TEST_TX_TIME)
|
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 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")))
|
val modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.quantity, Currency.getInstance("JPY")))
|
||||||
@ -640,7 +641,7 @@ class IRSTests {
|
|||||||
newIRS.common
|
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.
|
* 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.
|
* 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 ld1 = LocalDate.of(2016, 3, 8)
|
||||||
val bd1 = BigDecimal("0.0063518")
|
val bd1 = BigDecimal("0.0063518")
|
||||||
|
|
||||||
val irs = singleIRS()
|
val irs = singleIRS()
|
||||||
|
|
||||||
val txgroup: TransactionGroupDSL<InterestRateSwap.State> = transactionGroupFor() {
|
return ledger {
|
||||||
transaction("Agreement") {
|
transaction("Agreement") {
|
||||||
output("irs post agreement1") {
|
output("irs post agreement1") {
|
||||||
irs.copy(
|
irs.copy(
|
||||||
@ -668,8 +669,9 @@ class IRSTests {
|
|||||||
irs.common.copy(tradeID = "t1")
|
irs.common.copy(tradeID = "t1")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||||
timestamp(TEST_TX_TIME)
|
timestamp(TEST_TX_TIME)
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction("Agreement") {
|
transaction("Agreement") {
|
||||||
@ -681,40 +683,43 @@ class IRSTests {
|
|||||||
irs.common.copy(tradeID = "t2")
|
irs.common.copy(tradeID = "t2")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||||
timestamp(TEST_TX_TIME)
|
timestamp(TEST_TX_TIME)
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction("Fix") {
|
transaction("Fix") {
|
||||||
input("irs post agreement1")
|
input("irs post agreement1")
|
||||||
input("irs post agreement2")
|
input("irs post agreement2")
|
||||||
|
val postAgreement1 = "irs post agreement1".output<InterestRateSwap.State>()
|
||||||
output("irs post first fixing1") {
|
output("irs post first fixing1") {
|
||||||
"irs post agreement1".output.data.copy(
|
postAgreement1.data.copy(
|
||||||
"irs post agreement1".output.data.fixedLeg,
|
postAgreement1.data.fixedLeg,
|
||||||
"irs post agreement1".output.data.floatingLeg,
|
postAgreement1.data.floatingLeg,
|
||||||
"irs post agreement1".output.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
|
postAgreement1.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
|
||||||
"irs post agreement1".output.data.common.copy(tradeID = "t1")
|
postAgreement1.data.common.copy(tradeID = "t1")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val postAgreement2 = "irs post agreement2".output<InterestRateSwap.State>()
|
||||||
output("irs post first fixing2") {
|
output("irs post first fixing2") {
|
||||||
"irs post agreement2".output.data.copy(
|
postAgreement2.data.copy(
|
||||||
"irs post agreement2".output.data.fixedLeg,
|
postAgreement2.data.fixedLeg,
|
||||||
"irs post agreement2".output.data.floatingLeg,
|
postAgreement2.data.floatingLeg,
|
||||||
"irs post agreement2".output.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
|
postAgreement2.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
|
||||||
"irs post agreement2".output.data.common.copy(tradeID = "t2")
|
postAgreement2.data.common.copy(tradeID = "t2")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
arg(ORACLE_PUBKEY) {
|
command(ORACLE_PUBKEY) {
|
||||||
InterestRateSwap.Commands.Fix()
|
InterestRateSwap.Commands.Fix()
|
||||||
}
|
}
|
||||||
arg(ORACLE_PUBKEY) {
|
command(ORACLE_PUBKEY) {
|
||||||
Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1)
|
Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1)
|
||||||
}
|
}
|
||||||
timestamp(TEST_TX_TIME)
|
timestamp(TEST_TX_TIME)
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return txgroup
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,33 +31,33 @@ class CashTests {
|
|||||||
fun trivial() {
|
fun trivial() {
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
this `fails requirement` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
output { outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer) }
|
output { outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer) }
|
||||||
this `fails requirement` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
output { outState }
|
output { outState }
|
||||||
// No command arguments
|
// 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 {
|
tweak {
|
||||||
output { outState }
|
output { outState }
|
||||||
arg(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
|
command(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
|
||||||
this `fails requirement` "the owning keys are the same as the signing keys"
|
this `fails with` "the owning keys are the same as the signing keys"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
output { outState }
|
output { outState }
|
||||||
output { outState `issued by` MINI_CORP }
|
output { outState `issued by` MINI_CORP }
|
||||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
this `fails requirement` "at least one asset input"
|
this `fails with` "at least one asset input"
|
||||||
}
|
}
|
||||||
// Simple reallocation works.
|
// Simple reallocation works.
|
||||||
tweak {
|
tweak {
|
||||||
output { outState }
|
output { outState }
|
||||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,17 +68,17 @@ class CashTests {
|
|||||||
transaction {
|
transaction {
|
||||||
input { DummyState() }
|
input { DummyState() }
|
||||||
output { outState }
|
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
|
// 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.
|
// institution is allowed to issue as much cash as they want.
|
||||||
transaction {
|
transaction {
|
||||||
output { outState }
|
output { outState }
|
||||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Issue() }
|
command(DUMMY_PUBKEY_1) { Cash.Commands.Issue() }
|
||||||
this `fails requirement` "output deposits are owned by a command signer"
|
this `fails with` "output deposits are owned by a command signer"
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
output {
|
output {
|
||||||
@ -88,11 +88,11 @@ class CashTests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue(0) }
|
command(MINI_CORP_PUBKEY) { Cash.Commands.Issue(0) }
|
||||||
this `fails requirement` "has a nonce"
|
this `fails with` "has a nonce"
|
||||||
}
|
}
|
||||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
command(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test generation works.
|
// Test generation works.
|
||||||
@ -120,14 +120,14 @@ class CashTests {
|
|||||||
|
|
||||||
// Move fails: not allowed to summon money.
|
// Move fails: not allowed to summon money.
|
||||||
tweak {
|
tweak {
|
||||||
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue works.
|
// Issue works.
|
||||||
tweak {
|
tweak {
|
||||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,36 +135,36 @@ class CashTests {
|
|||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { inState.copy(amount = inState.amount / 2) }
|
output { inState.copy(amount = inState.amount / 2) }
|
||||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||||
this `fails requirement` "output values sum to more than the inputs"
|
this `fails with` "output values sum to more than the inputs"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Can't have an issue command that doesn't actually issue money.
|
// Can't have an issue command that doesn't actually issue money.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { inState }
|
output { inState }
|
||||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||||
this `fails requirement` "output values sum to more than the inputs"
|
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)
|
// Can't have any other commands if we have an issue command (because the issue command overrules them)
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { inState.copy(amount = inState.amount * 2) }
|
output { inState.copy(amount = inState.amount * 2) }
|
||||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||||
tweak {
|
tweak {
|
||||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||||
this `fails requirement` "there is only a single issue command"
|
this `fails with` "there is only a single issue command"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||||
this `fails requirement` "there is only a single issue command"
|
this `fails with` "there is only a single issue command"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
|
||||||
this `fails requirement` "there is only a single issue command"
|
this `fails with` "there is only a single issue command"
|
||||||
}
|
}
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,25 +191,25 @@ class CashTests {
|
|||||||
fun testMergeSplit() {
|
fun testMergeSplit() {
|
||||||
// Splitting value works.
|
// Splitting value works.
|
||||||
transaction {
|
transaction {
|
||||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
tweak {
|
tweak {
|
||||||
input { inState }
|
input { inState }
|
||||||
for (i in 1..4) output { inState.copy(amount = inState.amount / 4) }
|
for (i in 1..4) output { inState.copy(amount = inState.amount / 4) }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
// Merging 4 inputs into 2 outputs works.
|
// Merging 4 inputs into 2 outputs works.
|
||||||
tweak {
|
tweak {
|
||||||
for (i in 1..4) input { inState.copy(amount = inState.amount / 4) }
|
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) }
|
||||||
output { inState.copy(amount = inState.amount / 2) }
|
output { inState.copy(amount = inState.amount / 2) }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
// Merging 2 inputs into 1 works.
|
// Merging 2 inputs into 1 works.
|
||||||
tweak {
|
tweak {
|
||||||
input { inState.copy(amount = inState.amount / 2) }
|
input { inState.copy(amount = inState.amount / 2) }
|
||||||
input { inState.copy(amount = inState.amount / 2) }
|
input { inState.copy(amount = inState.amount / 2) }
|
||||||
output { inState }
|
output { inState }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -219,13 +219,13 @@ class CashTests {
|
|||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
input { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
|
input { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
|
||||||
this `fails requirement` "zero sized inputs"
|
this `fails with` "zero sized inputs"
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { inState }
|
output { inState }
|
||||||
output { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
|
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 {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { outState `issued by` MINI_CORP }
|
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.
|
// Can't change deposit reference when splitting.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { outState.copy(amount = inState.amount / 2).editDepositRef(0) }
|
output { outState.copy(amount = inState.amount / 2).editDepositRef(0) }
|
||||||
output { outState.copy(amount = inState.amount / 2).editDepositRef(1) }
|
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.
|
// Can't mix currencies.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer) }
|
output { outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer) }
|
||||||
output { outState.copy(amount = 200.POUNDS `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 {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
@ -260,22 +260,22 @@ class CashTests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
output { outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer) }
|
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.
|
// Can't have superfluous input states from different issuers.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
input { inState `issued by` MINI_CORP }
|
input { inState `issued by` MINI_CORP }
|
||||||
output { outState }
|
output { outState }
|
||||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
this `fails requirement` "at issuer MiniCorp the amounts balance"
|
this `fails with` "at issuer MiniCorp the amounts balance"
|
||||||
}
|
}
|
||||||
// Can't combine two different deposits at the same issuer.
|
// Can't combine two different deposits at the same issuer.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
input { inState.editDepositRef(3) }
|
input { inState.editDepositRef(3) }
|
||||||
output { outState.copy(amount = inState.amount * 2).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)) }
|
output { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) }
|
||||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
this `fails requirement` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
|
||||||
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 {
|
tweak {
|
||||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
this.accepts()
|
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)) `issued by` MINI_CORP }
|
||||||
output { inState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
|
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) }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
|
||||||
this `fails requirement` "at issuer MiniCorp the amounts balance"
|
this `fails with` "at issuer MiniCorp the amounts balance"
|
||||||
|
|
||||||
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)) }
|
command(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)) }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,20 +332,20 @@ class CashTests {
|
|||||||
// Can't merge them together.
|
// Can't merge them together.
|
||||||
tweak {
|
tweak {
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2, amount = 2000.DOLLARS `issued by` defaultIssuer) }
|
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
|
// Missing MiniCorp deposit
|
||||||
tweak {
|
tweak {
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
||||||
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.
|
// This works.
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2) `issued by` MINI_CORP }
|
output { inState.copy(owner = DUMMY_PUBKEY_2) `issued by` MINI_CORP }
|
||||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,9 +358,9 @@ class CashTests {
|
|||||||
input { pounds }
|
input { pounds }
|
||||||
output { inState `owned by` DUMMY_PUBKEY_2 }
|
output { inState `owned by` DUMMY_PUBKEY_2 }
|
||||||
output { pounds `owned by` DUMMY_PUBKEY_1 }
|
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
|
package com.r3corda.contracts.asset
|
||||||
|
|
||||||
import com.r3corda.contracts.asset.*
|
|
||||||
import com.r3corda.contracts.asset.Obligation.Lifecycle
|
import com.r3corda.contracts.asset.Obligation.Lifecycle
|
||||||
import com.r3corda.contracts.testing.*
|
import com.r3corda.contracts.testing.*
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
import com.r3corda.core.testing.*
|
import com.r3corda.core.testing.*
|
||||||
|
import com.r3corda.core.testing.JavaTestHelpers
|
||||||
import com.r3corda.core.utilities.nonEmptySetOf
|
import com.r3corda.core.utilities.nonEmptySetOf
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -19,11 +19,10 @@ class ObligationTests {
|
|||||||
val defaultUsd = USD `issued by` defaultIssuer
|
val defaultUsd = USD `issued by` defaultIssuer
|
||||||
val oneMillionDollars = 1000000.DOLLARS `issued by` defaultIssuer
|
val oneMillionDollars = 1000000.DOLLARS `issued by` defaultIssuer
|
||||||
val trustedCashContract = nonEmptySetOf(SecureHash.Companion.randomSHA256() as SecureHash)
|
val trustedCashContract = nonEmptySetOf(SecureHash.Companion.randomSHA256() as SecureHash)
|
||||||
val megaIssuedDollars = nonEmptySetOf(Issued<Currency>(defaultIssuer, USD))
|
val megaIssuedDollars = nonEmptySetOf(Issued(defaultIssuer, USD))
|
||||||
val megaIssuedPounds = nonEmptySetOf(Issued<Currency>(defaultIssuer, GBP))
|
val megaIssuedPounds = nonEmptySetOf(Issued(defaultIssuer, GBP))
|
||||||
val fivePm = Instant.parse("2016-01-01T17:00:00.00Z")
|
val fivePm = Instant.parse("2016-01-01T17:00:00.00Z")
|
||||||
val sixPm = Instant.parse("2016-01-01T18: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 megaCorpDollarSettlement = Obligation.StateTemplate(trustedCashContract, megaIssuedDollars, fivePm)
|
||||||
val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuedProducts = megaIssuedPounds)
|
val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuedProducts = megaIssuedPounds)
|
||||||
val inState = Obligation.State(
|
val inState = Obligation.State(
|
||||||
@ -35,43 +34,48 @@ class ObligationTests {
|
|||||||
)
|
)
|
||||||
val outState = inState.copy(beneficiary = DUMMY_PUBKEY_2)
|
val outState = inState.copy(beneficiary = DUMMY_PUBKEY_2)
|
||||||
|
|
||||||
private fun obligationTestRoots(group: TransactionGroupDSL<Obligation.State<Currency>>) = group.Roots()
|
private fun obligationTestRoots(
|
||||||
.transaction(oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `with notary` DUMMY_NOTARY label "Alice's $1,000,000 obligation to Bob")
|
group: LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>
|
||||||
.transaction(oneMillionDollars.OBLIGATION `between` Pair(BOB, ALICE_PUBKEY) `with notary` DUMMY_NOTARY label "Bob's $1,000,000 obligation to Alice")
|
) = group.apply {
|
||||||
.transaction(oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, BOB_PUBKEY) `with notary` DUMMY_NOTARY label "MegaCorp's $1,000,000 obligation to Bob")
|
unverifiedTransaction {
|
||||||
.transaction(1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY label "Alice's $1,000,000")
|
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
|
@Test
|
||||||
fun trivial() {
|
fun trivial() {
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
this `fails requirement` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
output { outState.copy(quantity = 2000.DOLLARS.quantity) }
|
output { outState.copy(quantity = 2000.DOLLARS.quantity) }
|
||||||
this `fails requirement` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
output { outState }
|
output { outState }
|
||||||
// No command arguments
|
// No command commanduments
|
||||||
this `fails requirement` "required com.r3corda.contracts.asset.Obligation.Commands.Move command"
|
this `fails with` "required com.r3corda.contracts.asset.Obligation.Commands.Move command"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
output { outState }
|
output { outState }
|
||||||
arg(DUMMY_PUBKEY_2) { Obligation.Commands.Move(inState.issuanceDef) }
|
command(DUMMY_PUBKEY_2) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||||
this `fails requirement` "the owning keys are the same as the signing keys"
|
this `fails with` "the owning keys are the same as the signing keys"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
output { outState }
|
output { outState }
|
||||||
output { outState `issued by` MINI_CORP }
|
output { outState `issued by` MINI_CORP }
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||||
this `fails requirement` "at least one obligation input"
|
this `fails with` "at least one obligation input"
|
||||||
}
|
}
|
||||||
// Simple reallocation works.
|
// Simple reallocation works.
|
||||||
tweak {
|
tweak {
|
||||||
output { outState }
|
output { outState }
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,17 +86,17 @@ class ObligationTests {
|
|||||||
transaction {
|
transaction {
|
||||||
input { DummyState() }
|
input { DummyState() }
|
||||||
output { outState }
|
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
|
// 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.
|
// institution is allowed to issue as much cash as they want.
|
||||||
transaction {
|
transaction {
|
||||||
output { outState }
|
output { outState }
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Issue(outState.issuanceDef) }
|
command(DUMMY_PUBKEY_1) { Obligation.Commands.Issue(outState.issuanceDef) }
|
||||||
this `fails requirement` "output deposits are owned by a command signer"
|
this `fails with` "output deposits are owned by a command signer"
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
output {
|
output {
|
||||||
@ -104,11 +108,11 @@ class ObligationTests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement), 0) }
|
command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement), 0) }
|
||||||
this `fails requirement` "has a nonce"
|
this `fails with` "has a nonce"
|
||||||
}
|
}
|
||||||
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement)) }
|
command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement)) }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test generation works.
|
// Test generation works.
|
||||||
@ -133,14 +137,14 @@ class ObligationTests {
|
|||||||
|
|
||||||
// Move fails: not allowed to summon money.
|
// Move fails: not allowed to summon money.
|
||||||
tweak {
|
tweak {
|
||||||
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue works.
|
// Issue works.
|
||||||
tweak {
|
tweak {
|
||||||
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
|
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,40 +152,40 @@ class ObligationTests {
|
|||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { inState.copy(quantity = inState.amount.quantity / 2) }
|
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) }
|
||||||
this `fails requirement` "output values sum to more than the inputs"
|
this `fails with` "output values sum to more than the inputs"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Can't have an issue command that doesn't actually issue money.
|
// Can't have an issue command that doesn't actually issue money.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { inState }
|
output { inState }
|
||||||
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
|
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
|
||||||
this `fails requirement` "output values sum to more than the inputs"
|
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)
|
// Can't have any other commands if we have an issue command (because the issue command overrules them)
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { inState.copy(quantity = inState.amount.quantity * 2) }
|
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 {
|
tweak {
|
||||||
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
|
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
|
||||||
this `fails requirement` "only move/exit commands can be present along with other obligation commands"
|
this `fails with` "only move/exit commands can be present along with other obligation commands"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Move(inState.issuanceDef) }
|
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||||
this `fails requirement` "only move/exit commands can be present along with other obligation commands"
|
this `fails with` "only move/exit commands can be present along with other obligation commands"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.SetLifecycle(inState.issuanceDef, Lifecycle.DEFAULTED) }
|
command(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"
|
this `fails with` "only move/exit commands can be present along with other obligation commands"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, inState.amount / 2) }
|
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, inState.amount / 2) }
|
||||||
this `fails requirement` "only move/exit commands can be present along with other obligation commands"
|
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
|
@Test
|
||||||
fun `close-out netting`() {
|
fun `close-out netting`() {
|
||||||
// Try netting out two obligations
|
// Try netting out two obligations
|
||||||
transactionGroupFor<Obligation.State<Currency>>() {
|
ledger {
|
||||||
obligationTestRoots(this)
|
obligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
input("Bob's $1,000,000 obligation to Alice")
|
||||||
// Note we can sign with either key here
|
// 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)
|
timestamp(TEST_TX_TIME)
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
}.verify()
|
this.verifies()
|
||||||
|
}
|
||||||
|
|
||||||
// Try netting out two obligations, with the third uninvolved obligation left
|
// Try netting out two obligations, with the third uninvolved obligation left
|
||||||
// as-is
|
// as-is
|
||||||
transactionGroupFor<Obligation.State<Currency>>() {
|
ledger {
|
||||||
obligationTestRoots(this)
|
obligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
input("Bob's $1,000,000 obligation to Alice")
|
||||||
input("MegaCorp's $1,000,000 obligation to Bob")
|
input("MegaCorp's $1,000,000 obligation to Bob")
|
||||||
output("change") { oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, BOB_PUBKEY) }
|
output("change") { oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, BOB_PUBKEY) }
|
||||||
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)
|
timestamp(TEST_TX_TIME)
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
}.verify()
|
this.verifies()
|
||||||
|
}
|
||||||
|
|
||||||
// Try having outputs mis-match the inputs
|
// Try having outputs mis-match the inputs
|
||||||
transactionGroupFor<Obligation.State<Currency>>() {
|
ledger {
|
||||||
obligationTestRoots(this)
|
obligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
input("Bob's $1,000,000 obligation to Alice")
|
||||||
output("change") { (oneMillionDollars / 2).OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) }
|
output("change") { (oneMillionDollars / 2).OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) }
|
||||||
arg(BOB_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
|
command(BOB_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
|
||||||
timestamp(TEST_TX_TIME)
|
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
|
// Have the wrong signature on the transaction
|
||||||
transactionGroupFor<Obligation.State<Currency>>() {
|
ledger {
|
||||||
obligationTestRoots(this)
|
obligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
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)
|
timestamp(TEST_TX_TIME)
|
||||||
|
this `fails with` "any involved party has signed"
|
||||||
}
|
}
|
||||||
}.expectFailureOfTx(1, "any involved party has signed")
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `payment netting`() {
|
fun `payment netting`() {
|
||||||
// Try netting out two obligations
|
// Try netting out two obligations
|
||||||
transactionGroupFor<Obligation.State<Currency>>() {
|
ledger {
|
||||||
obligationTestRoots(this)
|
obligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
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)
|
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
|
// Try netting out two obligations, but only provide one signature. Unlike close-out netting, we need both
|
||||||
// signatures for payment netting
|
// signatures for payment netting
|
||||||
transactionGroupFor<Obligation.State<Currency>>() {
|
ledger {
|
||||||
obligationTestRoots(this)
|
obligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
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)
|
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
|
// Multilateral netting, A -> B -> C which can net down to A -> C
|
||||||
transactionGroupFor<Obligation.State<Currency>>() {
|
ledger {
|
||||||
obligationTestRoots(this)
|
obligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
input("Bob's $1,000,000 obligation to Alice")
|
||||||
input("MegaCorp's $1,000,000 obligation to Bob")
|
input("MegaCorp's $1,000,000 obligation to Bob")
|
||||||
output("MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, ALICE_PUBKEY) }
|
output("MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, ALICE_PUBKEY) }
|
||||||
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)
|
timestamp(TEST_TX_TIME)
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
}.verify()
|
this.verifies()
|
||||||
|
}
|
||||||
|
|
||||||
// Multilateral netting without the key of the receiving party
|
// Multilateral netting without the key of the receiving party
|
||||||
transactionGroupFor<Obligation.State<Currency>>() {
|
ledger {
|
||||||
obligationTestRoots(this)
|
obligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
input("Bob's $1,000,000 obligation to Alice")
|
||||||
input("MegaCorp's $1,000,000 obligation to Bob")
|
input("MegaCorp's $1,000,000 obligation to Bob")
|
||||||
output("MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, ALICE_PUBKEY) }
|
output("MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, ALICE_PUBKEY) }
|
||||||
arg(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
|
command(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
|
||||||
timestamp(TEST_TX_TIME)
|
timestamp(TEST_TX_TIME)
|
||||||
|
this `fails with` "all involved parties have signed"
|
||||||
}
|
}
|
||||||
}.expectFailureOfTx(1, "all involved parties have signed")
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `settlement`() {
|
fun `settlement`() {
|
||||||
// Try netting out two obligations
|
// Try netting out two obligations
|
||||||
transactionGroupFor<Obligation.State<Currency>>() {
|
ledger {
|
||||||
obligationTestRoots(this)
|
obligationTestRoots(this)
|
||||||
transaction("Settlement") {
|
transaction("Settlement") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Alice's $1,000,000")
|
input("Alice's $1,000,000")
|
||||||
output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY }
|
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)) }
|
command(ALICE_PUBKEY) { Obligation.Commands.Settle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Amount(oneMillionDollars.quantity, USD)) }
|
||||||
arg(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
|
command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
}.verify()
|
this.verifies()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `payment default`() {
|
fun `payment default`() {
|
||||||
// Try defaulting an obligation without a timestamp
|
// Try defaulting an obligation without a timestamp
|
||||||
transactionGroupFor<Obligation.State<Currency>>() {
|
ledger {
|
||||||
obligationTestRoots(this)
|
obligationTestRoots(this)
|
||||||
transaction("Settlement") {
|
transaction("Settlement") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY)).copy(lifecycle = Lifecycle.DEFAULTED) }
|
output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY)).copy(lifecycle = Lifecycle.DEFAULTED) }
|
||||||
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
|
// Try defaulting an obligation due in the future
|
||||||
val pastTestTime = TEST_TX_TIME - Duration.ofDays(7)
|
val pastTestTime = TEST_TX_TIME - Duration.ofDays(7)
|
||||||
val futureTestTime = TEST_TX_TIME + Duration.ofDays(7)
|
val futureTestTime = TEST_TX_TIME + Duration.ofDays(7)
|
||||||
transactionGroupFor<Obligation.State<Currency>>() {
|
transaction("Settlement") {
|
||||||
roots {
|
input(oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `at` futureTestTime)
|
||||||
transaction(oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `at` futureTestTime `with notary` DUMMY_NOTARY label "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) }
|
||||||
}
|
command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF) `at` futureTestTime, Lifecycle.DEFAULTED) }
|
||||||
transaction("Settlement") {
|
timestamp(TEST_TX_TIME)
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
this `fails with` "the due date has passed"
|
||||||
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")
|
|
||||||
|
|
||||||
// Try defaulting an obligation that is now in the past
|
// Try defaulting an obligation that is now in the past
|
||||||
transactionGroupFor<Obligation.State<Currency>>() {
|
ledger {
|
||||||
roots {
|
|
||||||
transaction(oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `at` pastTestTime `with notary` DUMMY_NOTARY label "Alice's $1,000,000 obligation to Bob")
|
|
||||||
}
|
|
||||||
transaction("Settlement") {
|
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) }
|
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)
|
timestamp(TEST_TX_TIME)
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
}.verify()
|
this.verifies()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMergeSplit() {
|
fun testMergeSplit() {
|
||||||
// Splitting value works.
|
// Splitting value works.
|
||||||
transaction {
|
transaction {
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||||
tweak {
|
tweak {
|
||||||
input { inState }
|
input { inState }
|
||||||
repeat(4) { output { inState.copy(quantity = inState.quantity / 4) } }
|
repeat(4) { output { inState.copy(quantity = inState.quantity / 4) } }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
// Merging 4 inputs into 2 outputs works.
|
// Merging 4 inputs into 2 outputs works.
|
||||||
tweak {
|
tweak {
|
||||||
repeat(4) { input { inState.copy(quantity = inState.quantity / 4) } }
|
repeat(4) { input { inState.copy(quantity = inState.quantity / 4) } }
|
||||||
output { inState.copy(quantity = inState.quantity / 2) }
|
output { inState.copy(quantity = inState.quantity / 2) }
|
||||||
output { inState.copy(quantity = inState.quantity / 2) }
|
output { inState.copy(quantity = inState.quantity / 2) }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
// Merging 2 inputs into 1 works.
|
// Merging 2 inputs into 1 works.
|
||||||
tweak {
|
tweak {
|
||||||
input { inState.copy(quantity = inState.quantity / 2) }
|
input { inState.copy(quantity = inState.quantity / 2) }
|
||||||
input { inState.copy(quantity = inState.quantity / 2) }
|
input { inState.copy(quantity = inState.quantity / 2) }
|
||||||
output { inState }
|
output { inState }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -516,29 +529,30 @@ class ObligationTests {
|
|||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
input { inState.copy(quantity = 0L) }
|
input { inState.copy(quantity = 0L) }
|
||||||
this `fails requirement` "zero sized inputs"
|
this `fails with` "zero sized inputs"
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { inState }
|
output { inState }
|
||||||
output { inState.copy(quantity = 0L) }
|
output { inState.copy(quantity = 0L) }
|
||||||
this `fails requirement` "zero sized outputs"
|
this `fails with` "zero sized outputs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun trivialMismatches() {
|
fun trivialMismatches() {
|
||||||
// Can't change issuer.
|
// Can't change issuer.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { outState `issued by` MINI_CORP }
|
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.
|
// Can't mix currencies.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { outState.copy(quantity = 80000, template = megaCorpDollarSettlement) }
|
output { outState.copy(quantity = 80000, template = megaCorpDollarSettlement) }
|
||||||
output { outState.copy(quantity = 20000, template = megaCorpPoundSettlement) }
|
output { outState.copy(quantity = 20000, template = megaCorpPoundSettlement) }
|
||||||
this `fails requirement` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
@ -550,16 +564,16 @@ class ObligationTests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
output { outState.copy(quantity = 115000) }
|
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.
|
// Can't have superfluous input states from different issuers.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
input { inState `issued by` MINI_CORP }
|
input { inState `issued by` MINI_CORP }
|
||||||
output { outState }
|
output { outState }
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
||||||
this `fails requirement` "at obligor MiniCorp the amounts balance"
|
this `fails with` "at obligor MiniCorp the amounts balance"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -571,18 +585,18 @@ class ObligationTests {
|
|||||||
output { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
|
output { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, 100.DOLLARS) }
|
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 100.DOLLARS) }
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||||
this `fails requirement` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, 200.DOLLARS) }
|
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, 200.DOLLARS) }
|
||||||
this `fails requirement` "required com.r3corda.contracts.asset.Obligation.Commands.Move command"
|
this `fails with` "required com.r3corda.contracts.asset.Obligation.Commands.Move command"
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||||
this.accepts()
|
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) `issued by` MINI_CORP }
|
||||||
output { inState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
|
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 {
|
tweak {
|
||||||
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>((inState `issued by` MINI_CORP).issuanceDef, 0.DOLLARS) }
|
command(MINI_CORP_PUBKEY) { Obligation.Commands.Exit((inState `issued by` MINI_CORP).issuanceDef, 0.DOLLARS) }
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
||||||
this `fails requirement` "at obligor MiniCorp the amounts balance"
|
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) }
|
command(MINI_CORP_PUBKEY) { Obligation.Commands.Exit((inState `issued by` MINI_CORP).issuanceDef, 200.DOLLARS) }
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -620,21 +634,21 @@ class ObligationTests {
|
|||||||
// Can't merge them together.
|
// Can't merge them together.
|
||||||
tweak {
|
tweak {
|
||||||
output { inState.copy(beneficiary = DUMMY_PUBKEY_2, quantity = 200000L) }
|
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
|
// Missing MiniCorp deposit
|
||||||
tweak {
|
tweak {
|
||||||
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) }
|
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) }
|
||||||
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.
|
// This works.
|
||||||
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) }
|
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) }
|
||||||
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) `issued by` MINI_CORP }
|
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) `issued by` MINI_CORP }
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
command(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -647,10 +661,10 @@ class ObligationTests {
|
|||||||
input { pounds }
|
input { pounds }
|
||||||
output { inState `owned by` DUMMY_PUBKEY_2 }
|
output { inState `owned by` DUMMY_PUBKEY_2 }
|
||||||
output { pounds `owned by` DUMMY_PUBKEY_1 }
|
output { pounds `owned by` DUMMY_PUBKEY_1 }
|
||||||
arg(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Obligation.Commands.Move(inState.issuanceDef) }
|
command(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(pounds.issuanceDef) }
|
||||||
|
|
||||||
this.accepts()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -686,7 +700,7 @@ class ObligationTests {
|
|||||||
fiveKDollarsFromMegaToMega.copy(template = megaCorpDollarSettlement.copy(acceptableContracts = nonEmptySetOf(SecureHash.Companion.randomSHA256()))).bilateralNetState)
|
fiveKDollarsFromMegaToMega.copy(template = megaCorpDollarSettlement.copy(acceptableContracts = nonEmptySetOf(SecureHash.Companion.randomSHA256()))).bilateralNetState)
|
||||||
|
|
||||||
// States must not be nettable if the trusted issuers differ
|
// 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,
|
assertNotEquals(fiveKDollarsFromMegaToMega.bilateralNetState,
|
||||||
fiveKDollarsFromMegaToMega.copy(template = megaCorpDollarSettlement.copy(acceptableIssuedProducts = miniCorpIssuer)).bilateralNetState)
|
fiveKDollarsFromMegaToMega.copy(template = megaCorpDollarSettlement.copy(acceptableIssuedProducts = miniCorpIssuer)).bilateralNetState)
|
||||||
}
|
}
|
||||||
@ -743,7 +757,7 @@ class ObligationTests {
|
|||||||
val fiveKDollarsFromMegaToMini = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement,
|
val fiveKDollarsFromMegaToMini = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement,
|
||||||
5000.DOLLARS.quantity, MINI_CORP_PUBKEY)
|
5000.DOLLARS.quantity, MINI_CORP_PUBKEY)
|
||||||
val expected = mapOf(Pair(Pair(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), fiveKDollarsFromMegaToMini.amount))
|
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)
|
assertEquals(expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -755,7 +769,7 @@ class ObligationTests {
|
|||||||
Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP))
|
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 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)
|
assertEquals(expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -769,7 +783,7 @@ class ObligationTests {
|
|||||||
val expected = mapOf(
|
val expected = mapOf(
|
||||||
Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP))
|
Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP))
|
||||||
)
|
)
|
||||||
var actual = netAmountsDue<Currency>(balanced)
|
val actual = netAmountsDue(balanced)
|
||||||
assertEquals(expected, actual)
|
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.contracts.*
|
||||||
import com.r3corda.core.crypto.*
|
import com.r3corda.core.crypto.*
|
||||||
import com.r3corda.core.node.services.IdentityService
|
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.MockIdentityService
|
||||||
import com.r3corda.core.node.services.testing.MockStorageService
|
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.net.ServerSocket
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Instant
|
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. */
|
/** If an exception is thrown by the body, rethrows the root cause exception. */
|
||||||
inline fun <R> rootCauseExceptions(body: () -> R): R {
|
inline fun <R> rootCauseExceptions(body: () -> R): R {
|
||||||
@ -95,9 +90,23 @@ object JavaTestHelpers {
|
|||||||
|
|
||||||
@JvmStatic fun generateStateRef() = StateRef(SecureHash.randomSHA256(), 0)
|
@JvmStatic fun generateStateRef() = StateRef(SecureHash.randomSHA256(), 0)
|
||||||
|
|
||||||
@JvmStatic fun transaction(body: TransactionForTest.() -> LastLineShouldTestForAcceptOrFailure): LastLineShouldTestForAcceptOrFailure {
|
@JvmStatic @JvmOverloads fun ledger(
|
||||||
return body(TransactionForTest())
|
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
|
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 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<*>) {
|
class LabeledOutput(val label: String?, val state: TransactionState<*>) {
|
||||||
override fun toString() = state.toString() + (if (label != null) " ($label)" else "")
|
override fun toString() = state.toString() + (if (label != null) " ($label)" else "")
|
||||||
override fun equals(other: Any?) = other is LabeledOutput && state.equals(other.state)
|
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)
|
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
|
@Test
|
||||||
fun success() {
|
fun success() {
|
||||||
transactionGroup {
|
ledger {
|
||||||
roots {
|
unverifiedTransaction {
|
||||||
transaction(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY label "£1000")
|
output("£1000") { A_THOUSAND_POUNDS }
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
input("£1000")
|
input("£1000")
|
||||||
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY }
|
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 {
|
transaction {
|
||||||
input("alice's £1000")
|
input("alice's £1000")
|
||||||
arg(ALICE_PUBKEY) { TestCash.Commands.Move() }
|
command(ALICE_PUBKEY) { TestCash.Commands.Move() }
|
||||||
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Exit(1000.POUNDS) }
|
command(MINI_CORP_PUBKEY) { TestCash.Commands.Exit(1000.POUNDS) }
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
verify()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun conflict() {
|
fun conflict() {
|
||||||
transactionGroup {
|
ledger {
|
||||||
val t = transaction {
|
val t = transaction {
|
||||||
output("cash") { A_THOUSAND_POUNDS }
|
output("cash") { A_THOUSAND_POUNDS }
|
||||||
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() }
|
command(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() }
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
val conflict1 = transaction {
|
val conflict1 = transaction {
|
||||||
@ -81,10 +84,11 @@ class TransactionGroupTests {
|
|||||||
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB_PUBKEY
|
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB_PUBKEY
|
||||||
output { HALF }
|
output { HALF }
|
||||||
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.
|
// Alice tries to double spend back to herself.
|
||||||
val conflict2 = transaction {
|
val conflict2 = transaction {
|
||||||
@ -92,13 +96,14 @@ class TransactionGroupTests {
|
|||||||
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE_PUBKEY
|
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE_PUBKEY
|
||||||
output { HALF }
|
output { HALF }
|
||||||
output { HALF }
|
output { HALF }
|
||||||
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
|
command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertNotEquals(conflict1, conflict2)
|
assertNotEquals(conflict1, conflict2)
|
||||||
|
|
||||||
val e = assertFailsWith(TransactionConflictException::class) {
|
val e = assertFailsWith(TransactionConflictException::class) {
|
||||||
verify()
|
verifies()
|
||||||
}
|
}
|
||||||
assertEquals(StateRef(t.id, 0), e.conflictRef)
|
assertEquals(StateRef(t.id, 0), e.conflictRef)
|
||||||
assertEquals(setOf(conflict1.id, conflict2.id), setOf(e.tx1.id, e.tx2.id))
|
assertEquals(setOf(conflict1.id, conflict2.id), setOf(e.tx1.id, e.tx2.id))
|
||||||
@ -108,79 +113,83 @@ class TransactionGroupTests {
|
|||||||
@Test
|
@Test
|
||||||
fun disconnected() {
|
fun disconnected() {
|
||||||
// Check that if we have a transaction in the group that doesn't connect to anything else, it's rejected.
|
// 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 {
|
transaction {
|
||||||
output("cash") { A_THOUSAND_POUNDS }
|
output("cash") { A_THOUSAND_POUNDS }
|
||||||
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() }
|
command(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() }
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
input("cash")
|
input("cash")
|
||||||
output { A_THOUSAND_POUNDS `owned by` BOB_PUBKEY }
|
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())
|
val input = StateAndRef(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY, generateStateRef())
|
||||||
tg.txns += TransactionType.General.Builder().apply {
|
tg.apply {
|
||||||
addInputState(input)
|
transaction {
|
||||||
addOutputState(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY)
|
assertFailsWith(TransactionResolutionException::class) {
|
||||||
addCommand(TestCash.Commands.Move(), BOB_PUBKEY)
|
input(input.ref)
|
||||||
}.toWireTransaction()
|
}
|
||||||
|
this.verifies()
|
||||||
val e = assertFailsWith(TransactionResolutionException::class) {
|
}
|
||||||
tg.verify()
|
|
||||||
}
|
}
|
||||||
assertEquals(e.hash, input.ref.txhash)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun duplicatedInputs() {
|
fun duplicatedInputs() {
|
||||||
// Check that a transaction cannot refer to the same input more than once.
|
// Check that a transaction cannot refer to the same input more than once.
|
||||||
transactionGroup {
|
ledger {
|
||||||
roots {
|
unverifiedTransaction {
|
||||||
transaction(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY label "£1000")
|
output("£1000") { A_THOUSAND_POUNDS }
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
input("£1000")
|
input("£1000")
|
||||||
input("£1000")
|
input("£1000")
|
||||||
output { A_THOUSAND_POUNDS.copy(amount = A_THOUSAND_POUNDS.amount * 2) }
|
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) {
|
assertFailsWith(TransactionConflictException::class) {
|
||||||
verify()
|
verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun signGroup() {
|
fun signGroup() {
|
||||||
val signedTxns: List<SignedTransaction> = transactionGroup {
|
ledger {
|
||||||
transaction {
|
transaction {
|
||||||
output("£1000") { A_THOUSAND_POUNDS }
|
output("£1000") { A_THOUSAND_POUNDS }
|
||||||
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() }
|
command(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() }
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
input("£1000")
|
input("£1000")
|
||||||
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY }
|
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 {
|
transaction {
|
||||||
input("alice's £1000")
|
input("alice's £1000")
|
||||||
arg(ALICE_PUBKEY) { TestCash.Commands.Move() }
|
command(ALICE_PUBKEY) { TestCash.Commands.Move() }
|
||||||
arg(MINI_CORP_PUBKEY) { TestCash.Commands.Exit(1000.POUNDS) }
|
command(MINI_CORP_PUBKEY) { TestCash.Commands.Exit(1000.POUNDS) }
|
||||||
|
this.verifies()
|
||||||
}
|
}
|
||||||
}.signAll()
|
|
||||||
|
|
||||||
// Now go through the conversion -> verification path with them.
|
val signedTxns: List<SignedTransaction> = signAll()
|
||||||
val ltxns = signedTxns.map {
|
|
||||||
it.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments)
|
// Now go through the conversion -> verification path with them.
|
||||||
}.toSet()
|
val ltxns = signedTxns.map {
|
||||||
TransactionGroup(ltxns, emptySet()).verify()
|
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
|
// we run in the unit test thread exclusively to speed things up, ensure deterministic results and
|
||||||
// allow interruption half way through.
|
// allow interruption half way through.
|
||||||
net = MockNetwork(false, true)
|
net = MockNetwork(false, true)
|
||||||
transactionGroupFor<ContractState> {
|
|
||||||
|
ledger {
|
||||||
|
|
||||||
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||||
val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY)
|
val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY)
|
||||||
val bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY)
|
val bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY)
|
||||||
@ -113,7 +115,7 @@ class TwoPartyTradeProtocolTests {
|
|||||||
aliceNode.smm,
|
aliceNode.smm,
|
||||||
notaryNode.info,
|
notaryNode.info,
|
||||||
bobNode.info.identity,
|
bobNode.info.identity,
|
||||||
lookup("alice's paper"),
|
"alice's paper".outputStateAndRef(),
|
||||||
1000.DOLLARS `issued by` issuer,
|
1000.DOLLARS `issued by` issuer,
|
||||||
ALICE_KEY,
|
ALICE_KEY,
|
||||||
buyerSessionID
|
buyerSessionID
|
||||||
@ -133,7 +135,8 @@ class TwoPartyTradeProtocolTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `shutdown and restore`() {
|
fun `shutdown and restore`() {
|
||||||
transactionGroupFor<ContractState> {
|
|
||||||
|
ledger {
|
||||||
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||||
val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY)
|
val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY)
|
||||||
var bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY)
|
var bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY)
|
||||||
@ -155,7 +158,7 @@ class TwoPartyTradeProtocolTests {
|
|||||||
aliceNode.smm,
|
aliceNode.smm,
|
||||||
notaryNode.info,
|
notaryNode.info,
|
||||||
bobNode.info.identity,
|
bobNode.info.identity,
|
||||||
lookup("alice's paper"),
|
"alice's paper".outputStateAndRef(),
|
||||||
1000.DOLLARS `issued by` issuer,
|
1000.DOLLARS `issued by` issuer,
|
||||||
ALICE_KEY,
|
ALICE_KEY,
|
||||||
buyerSessionID
|
buyerSessionID
|
||||||
@ -246,10 +249,11 @@ class TwoPartyTradeProtocolTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `check dependencies of sale asset are resolved`() {
|
fun `check dependencies of sale asset are resolved`() {
|
||||||
transactionGroupFor<ContractState> {
|
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||||
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
val aliceNode = makeNodeWithTracking(notaryNode.info, ALICE.name, ALICE_KEY)
|
||||||
val aliceNode = makeNodeWithTracking(notaryNode.info, ALICE.name, ALICE_KEY)
|
val bobNode = makeNodeWithTracking(notaryNode.info, BOB.name, BOB_KEY)
|
||||||
val bobNode = makeNodeWithTracking(notaryNode.info, BOB.name, BOB_KEY)
|
|
||||||
|
ledger(storageService = aliceNode.storage) {
|
||||||
|
|
||||||
// Insert a prospectus type attachment into the commercial paper transaction.
|
// Insert a prospectus type attachment into the commercial paper transaction.
|
||||||
val stream = ByteArrayOutputStream()
|
val stream = ByteArrayOutputStream()
|
||||||
@ -258,7 +262,7 @@ class TwoPartyTradeProtocolTests {
|
|||||||
it.write("Our commercial paper is top notch stuff".toByteArray())
|
it.write("Our commercial paper is top notch stuff".toByteArray())
|
||||||
it.closeEntry()
|
it.closeEntry()
|
||||||
}
|
}
|
||||||
val attachmentID = aliceNode.storage.attachments.importAttachment(ByteArrayInputStream(stream.toByteArray()))
|
val attachmentID = attachment(ByteArrayInputStream(stream.toByteArray()))
|
||||||
|
|
||||||
val issuer = MEGA_CORP.ref(1)
|
val issuer = MEGA_CORP.ref(1)
|
||||||
val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public, issuer).second
|
val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public, issuer).second
|
||||||
@ -275,7 +279,7 @@ class TwoPartyTradeProtocolTests {
|
|||||||
aliceNode.smm,
|
aliceNode.smm,
|
||||||
notaryNode.info,
|
notaryNode.info,
|
||||||
bobNode.info.identity,
|
bobNode.info.identity,
|
||||||
lookup("alice's paper"),
|
"alice's paper".outputStateAndRef(),
|
||||||
1000.DOLLARS `issued by` issuer,
|
1000.DOLLARS `issued by` issuer,
|
||||||
ALICE_KEY,
|
ALICE_KEY,
|
||||||
buyerSessionID
|
buyerSessionID
|
||||||
@ -350,20 +354,23 @@ class TwoPartyTradeProtocolTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `dependency with error on buyer side`() {
|
fun `dependency with error on buyer side`() {
|
||||||
transactionGroupFor<ContractState> {
|
ledger {
|
||||||
runWithError(true, false, "at least one asset input")
|
runWithError(true, false, "at least one asset input")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `dependency with error on seller side`() {
|
fun `dependency with error on seller side`() {
|
||||||
transactionGroupFor<ContractState> {
|
ledger {
|
||||||
runWithError(false, true, "must be timestamped")
|
runWithError(false, true, "must be timestamped")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TransactionGroupDSL<ContractState>.runWithError(bobError: Boolean, aliceError: Boolean,
|
private fun LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.runWithError(
|
||||||
expectedMessageSubstring: String) {
|
bobError: Boolean,
|
||||||
|
aliceError: Boolean,
|
||||||
|
expectedMessageSubstring: String
|
||||||
|
) {
|
||||||
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||||
val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY)
|
val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY)
|
||||||
val bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY)
|
val bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY)
|
||||||
@ -385,7 +392,7 @@ class TwoPartyTradeProtocolTests {
|
|||||||
aliceNode.smm,
|
aliceNode.smm,
|
||||||
notaryNode.info,
|
notaryNode.info,
|
||||||
bobNode.info.identity,
|
bobNode.info.identity,
|
||||||
lookup("alice's paper"),
|
"alice's paper".outputStateAndRef(),
|
||||||
1000.DOLLARS `issued by` issuer,
|
1000.DOLLARS `issued by` issuer,
|
||||||
ALICE_KEY,
|
ALICE_KEY,
|
||||||
buyerSessionID
|
buyerSessionID
|
||||||
@ -411,10 +418,11 @@ class TwoPartyTradeProtocolTests {
|
|||||||
assertTrue(e.cause!!.cause!!.message!!.contains(expectedMessageSubstring))
|
assertTrue(e.cause!!.cause!!.message!!.contains(expectedMessageSubstring))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TransactionGroupDSL<ContractState>.insertFakeTransactions(wtxToSign: List<WireTransaction>,
|
private fun insertFakeTransactions(
|
||||||
services: ServiceHub,
|
wtxToSign: List<WireTransaction>,
|
||||||
vararg extraKeys: KeyPair): Map<SecureHash, SignedTransaction> {
|
services: ServiceHub,
|
||||||
val signed: List<SignedTransaction> = signAll(wtxToSign, *extraKeys)
|
vararg extraKeys: KeyPair): Map<SecureHash, SignedTransaction> {
|
||||||
|
val signed: List<SignedTransaction> = signAll(wtxToSign, extraKeys)
|
||||||
services.recordTransactions(signed)
|
services.recordTransactions(signed)
|
||||||
val validatedTransactions = services.storageService.validatedTransactions
|
val validatedTransactions = services.storageService.validatedTransactions
|
||||||
if (validatedTransactions is RecordingTransactionStorage) {
|
if (validatedTransactions is RecordingTransactionStorage) {
|
||||||
@ -423,9 +431,10 @@ class TwoPartyTradeProtocolTests {
|
|||||||
return signed.associateBy { it.id }
|
return signed.associateBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TransactionGroupDSL<ContractState>.fillUpForBuyer(withError: Boolean,
|
private fun LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.fillUpForBuyer(
|
||||||
owner: PublicKey = BOB_PUBKEY,
|
withError: Boolean,
|
||||||
issuer: PartyAndReference = MEGA_CORP.ref(1)): Pair<Wallet, List<WireTransaction>> {
|
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
|
// Bob (Buyer) has some cash he got from the Bank of Elbonia, Alice (Seller) has some commercial paper she
|
||||||
// wants to sell to Bob.
|
// 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 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 }
|
output("elbonian money 2") { 1000.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY }
|
||||||
if (!withError)
|
if (!withError)
|
||||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||||
timestamp(TEST_TX_TIME)
|
timestamp(TEST_TX_TIME)
|
||||||
|
if (withError) {
|
||||||
|
this.fails()
|
||||||
|
} else {
|
||||||
|
this.verifies()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bob gets some cash onto the ledger from BoE
|
// Bob gets some cash onto the ledger from BoE
|
||||||
val bc1 = transaction {
|
val bc1 = transaction {
|
||||||
input("elbonian money 1")
|
input("elbonian money 1")
|
||||||
output("bob cash 1") { 800.DOLLARS.CASH `issued by` issuer `owned by` owner }
|
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 {
|
val bc2 = transaction {
|
||||||
input("elbonian money 2")
|
input("elbonian money 2")
|
||||||
output("bob cash 2") { 300.DOLLARS.CASH `issued by` issuer `owned by` owner }
|
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.
|
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))
|
return Pair(wallet, listOf(eb1, bc1, bc2))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TransactionGroupDSL<ContractState>.fillUpForSeller(withError: Boolean,
|
private fun LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.fillUpForSeller(
|
||||||
owner: PublicKey,
|
withError: Boolean,
|
||||||
amount: Amount<Issued<Currency>>,
|
owner: PublicKey,
|
||||||
notary: Party,
|
amount: Amount<Issued<Currency>>,
|
||||||
attachmentID: SecureHash?): Pair<Wallet, List<WireTransaction>> {
|
notary: Party,
|
||||||
|
attachmentID: SecureHash?): Pair<Wallet, List<WireTransaction>> {
|
||||||
val ap = transaction {
|
val ap = transaction {
|
||||||
output("alice's paper") {
|
output("alice's paper") {
|
||||||
CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), owner, amount, TEST_TX_TIME + 7.days)
|
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)
|
if (!withError)
|
||||||
arg(notary.owningKey) { TimestampCommand(TEST_TX_TIME, 30.seconds) }
|
command(notary.owningKey) { TimestampCommand(TEST_TX_TIME, 30.seconds) }
|
||||||
if (attachmentID != null)
|
if (attachmentID != null)
|
||||||
attachment(attachmentID)
|
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))
|
return Pair(wallet, listOf(ap))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RecordingTransactionStorage(val delegate: TransactionStorage) : TransactionStorage {
|
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) {
|
override fun addTransaction(transaction: SignedTransaction) {
|
||||||
records.add(TxRecord.Add(transaction))
|
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.CommandData
|
||||||
import com.r3corda.core.contracts.ContractState
|
import com.r3corda.core.contracts.ContractState
|
||||||
import com.r3corda.core.contracts.TransactionState
|
|
||||||
import com.r3corda.core.crypto.SecureHash
|
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.Edge
|
||||||
import org.graphstream.graph.Node
|
import org.graphstream.graph.Node
|
||||||
import org.graphstream.graph.implementations.SingleGraph
|
import org.graphstream.graph.implementations.SingleGraph
|
||||||
import kotlin.reflect.memberProperties
|
import kotlin.reflect.memberProperties
|
||||||
|
|
||||||
class GraphVisualiser(val dsl: TransactionGroupDSL<in ContractState>) {
|
class GraphVisualiser(val dsl: LedgerDSL<EnforceVerifyOrFail, TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>) {
|
||||||
companion object {
|
companion object {
|
||||||
val css = GraphVisualiser::class.java.getResourceAsStream("graph.css").bufferedReader().readText()
|
val css = GraphVisualiser::class.java.getResourceAsStream("graph.css").bufferedReader().readText()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convert(): SingleGraph {
|
fun convert(): SingleGraph {
|
||||||
val tg = dsl.toTransactionGroup()
|
val tg = dsl.interpreter.toTransactionGroup()
|
||||||
val graph = createGraph("Transaction group", css)
|
val graph = createGraph("Transaction group", css)
|
||||||
|
|
||||||
// Map all the transactions, including the bogus non-verified ones (with no inputs) to graph nodes.
|
// 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()) {
|
for ((txIndex, tx) in (tg.transactions + tg.nonVerifiedRoots).withIndex()) {
|
||||||
val txNode = graph.addNode<Node>("tx$txIndex")
|
val txNode = graph.addNode<Node>("tx$txIndex")
|
||||||
if (tx !in tg.nonVerifiedRoots)
|
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"
|
txNode.styleClass = "tx"
|
||||||
|
|
||||||
// Now create a vertex for each output state.
|
// Now create a vertex for each output state.
|
||||||
for (outIndex in tx.outputs.indices) {
|
for (outIndex in tx.outputs.indices) {
|
||||||
val node = graph.addNode<Node>(tx.outRef<ContractState>(outIndex).ref.toString())
|
val node = graph.addNode<Node>(tx.outRef<ContractState>(outIndex).ref.toString())
|
||||||
val state = tx.outputs[outIndex]
|
val state = tx.outputs[outIndex]
|
||||||
node.label = stateToLabel(state)
|
node.label = stateToLabel(state.data)
|
||||||
node.styleClass = stateToCSSClass(state.data) + ",state"
|
node.styleClass = stateToCSSClass(state.data) + ",state"
|
||||||
node.setAttribute("state", state)
|
node.setAttribute("state", state)
|
||||||
val edge = graph.addEdge<Edge>("tx$txIndex-out$outIndex", txNode, node, true)
|
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
|
return graph
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stateToLabel(state: TransactionState<*>): String {
|
private fun stateToLabel(state: ContractState): String {
|
||||||
return dsl.labelForState(state) ?: stateToTypeName(state.data)
|
return dsl.interpreter.outputToLabel(state) ?: stateToTypeName(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun commandToTypeName(state: CommandData) = state.javaClass.canonicalName.removePrefix("contracts.").replace('$', '.')
|
private fun commandToTypeName(state: CommandData) = state.javaClass.canonicalName.removePrefix("contracts.").replace('$', '.')
|
||||||
|
Loading…
Reference in New Issue
Block a user