diff --git a/perftestflows/src/main/java/net/corda/ptflows/contracts/JavaPtCommercialPaper.java b/perftestflows/src/main/java/net/corda/ptflows/contracts/JavaPtCommercialPaper.java new file mode 100644 index 0000000000..ce9c2d263f --- /dev/null +++ b/perftestflows/src/main/java/net/corda/ptflows/contracts/JavaPtCommercialPaper.java @@ -0,0 +1,267 @@ +package net.corda.ptflows.contracts; + + +import co.paralleluniverse.fibers.Suspendable; +import kotlin.Unit; +import net.corda.core.contracts.*; +import net.corda.core.crypto.NullKeys.NullPublicKey; +import net.corda.core.identity.AbstractParty; +import net.corda.core.identity.AnonymousParty; +import net.corda.core.identity.Party; +import net.corda.core.identity.PartyAndCertificate; +import net.corda.core.node.ServiceHub; +import net.corda.core.transactions.LedgerTransaction; +import net.corda.core.transactions.TransactionBuilder; +import net.corda.ptflows.contracts.asset.PtCash; +import net.corda.ptflows.utils.StateSummingUtilitiesKt; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Instant; +import java.util.Collections; +import java.util.Currency; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; +import static net.corda.core.contracts.ContractsDSL.requireThat; + +/** + * This is a Java version of the CommercialPaper contract (chosen because it's simple). This demonstrates how the + * use of Kotlin for implementation of the framework does not impose the same language choice on contract developers. + */ +@SuppressWarnings("unused") +public class JavaPtCommercialPaper implements Contract { + static final String JCP_PROGRAM_ID = "net.corda.ptflows.contracts.JavaPtCommercialPaper"; + + @SuppressWarnings("unused") + public static class State implements OwnableState, IPtCommercialPaperState { + private PartyAndReference issuance; + private AbstractParty owner; + private Amount> faceValue; + private Instant maturityDate; + + public State() { + } // For serialization + + public State(PartyAndReference issuance, AbstractParty owner, Amount> faceValue, + Instant maturityDate) { + this.issuance = issuance; + this.owner = owner; + this.faceValue = faceValue; + this.maturityDate = maturityDate; + } + + public State copy() { + return new State(this.issuance, this.owner, this.faceValue, this.maturityDate); + } + + public IPtCommercialPaperState withOwner(AbstractParty newOwner) { + return new State(this.issuance, newOwner, this.faceValue, this.maturityDate); + } + + @NotNull + @Override + public CommandAndState withNewOwner(@NotNull AbstractParty newOwner) { + return new CommandAndState(new Commands.Move(), new State(this.issuance, newOwner, this.faceValue, this.maturityDate)); + } + + public IPtCommercialPaperState withFaceValue(Amount> newFaceValue) { + return new State(this.issuance, this.owner, newFaceValue, this.maturityDate); + } + + public IPtCommercialPaperState withMaturityDate(Instant newMaturityDate) { + return new State(this.issuance, this.owner, this.faceValue, newMaturityDate); + } + + public PartyAndReference getIssuance() { + return issuance; + } + + @NotNull + public AbstractParty getOwner() { + return owner; + } + + public Amount> getFaceValue() { + return faceValue; + } + + public Instant getMaturityDate() { + return maturityDate; + } + + @Override + public boolean equals(Object that) { + if (this == that) return true; + if (that == null || getClass() != that.getClass()) return false; + + State state = (State) that; + + if (issuance != null ? !issuance.equals(state.issuance) : state.issuance != null) return false; + if (owner != null ? !owner.equals(state.owner) : state.owner != null) return false; + if (faceValue != null ? !faceValue.equals(state.faceValue) : state.faceValue != null) return false; + if (maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null) + return false; + return true; + } + + @Override + public int hashCode() { + int result = issuance != null ? issuance.hashCode() : 0; + result = 31 * result + (owner != null ? owner.hashCode() : 0); + result = 31 * result + (faceValue != null ? faceValue.hashCode() : 0); + result = 31 * result + (maturityDate != null ? maturityDate.hashCode() : 0); + return result; + } + + State withoutOwner() { + return new State(issuance, new AnonymousParty(NullPublicKey.INSTANCE), faceValue, maturityDate); + } + + @NotNull + @Override + public List getParticipants() { + return Collections.singletonList(this.owner); + } + } + + public interface Commands extends CommandData { + class Move implements Commands { + @Override + public boolean equals(Object obj) { + return obj instanceof Move; + } + } + + class Redeem implements Commands { + @Override + public boolean equals(Object obj) { + return obj instanceof Redeem; + } + } + + class Issue implements Commands { + @Override + public boolean equals(Object obj) { + return obj instanceof Issue; + } + } + } + + @NotNull + private List> extractCommands(@NotNull LedgerTransaction tx) { + return tx.getCommands() + .stream() + .filter((CommandWithParties command) -> command.getValue() instanceof Commands) + .map((CommandWithParties command) -> new CommandWithParties<>(command.getSigners(), command.getSigningParties(), (Commands) command.getValue())) + .collect(Collectors.toList()); + } + + @Override + public void verify(@NotNull LedgerTransaction tx) throws IllegalArgumentException { + + // Group by everything except owner: any modification to the CP at all is considered changing it fundamentally. + final List> groups = tx.groupStates(State.class, State::withoutOwner); + + // There are two possible things that can be done with this CP. The first is trading it. The second is redeeming + // it for cash on or after the maturity date. + final List> commands = tx.getCommands().stream().filter( + it -> it.getValue() instanceof Commands + ).collect(Collectors.toList()); + final CommandWithParties command = onlyElementOf(commands); + final TimeWindow timeWindow = tx.getTimeWindow(); + + for (final LedgerTransaction.InOutGroup group : groups) { + final List inputs = group.getInputs(); + final List outputs = group.getOutputs(); + if (command.getValue() instanceof Commands.Move) { + final CommandWithParties cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class); + // There should be only a single input due to aggregation above + final State input = onlyElementOf(inputs); + + if (!cmd.getSigners().contains(input.getOwner().getOwningKey())) + throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); + + // Check the output CP state is the same as the input state, ignoring the owner field. + if (outputs.size() != 1) { + throw new IllegalStateException("the state is propagated"); + } + } else if (command.getValue() instanceof Commands.Redeem) { + final CommandWithParties cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class); + + // There should be only a single input due to aggregation above + final State input = onlyElementOf(inputs); + + if (!cmd.getSigners().contains(input.getOwner().getOwningKey())) + throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); + + final Instant time = null == timeWindow + ? null + : timeWindow.getUntilTime(); + final Amount> received = StateSummingUtilitiesKt.sumCashBy(tx.getOutputStates(), input.getOwner()); + + requireThat(require -> { + require.using("must be timestamped", timeWindow != null); + require.using("received amount equals the face value: " + + received + " vs " + input.getFaceValue(), received.equals(input.getFaceValue())); + require.using("the paper must have matured", time != null && !time.isBefore(input.getMaturityDate())); + require.using("the received amount equals the face value", input.getFaceValue().equals(received)); + require.using("the paper must be destroyed", outputs.isEmpty()); + return Unit.INSTANCE; + }); + } else if (command.getValue() instanceof Commands.Issue) { + final CommandWithParties cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class); + final State output = onlyElementOf(outputs); + final Instant time = null == timeWindow + ? null + : timeWindow.getUntilTime(); + + requireThat(require -> { + require.using("output values sum to more than the inputs", inputs.isEmpty()); + require.using("output values sum to more than the inputs", output.faceValue.getQuantity() > 0); + require.using("must be timestamped", timeWindow != null); + require.using("the maturity date is not in the past", time != null && time.isBefore(output.getMaturityDate())); + require.using("output states are issued by a command signer", cmd.getSigners().contains(output.issuance.getParty().getOwningKey())); + return Unit.INSTANCE; + }); + } + } + } + + public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount> faceValue, @Nullable Instant maturityDate, @NotNull Party notary, Integer encumbrance) { + State state = new State(issuance, issuance.getParty(), faceValue, maturityDate); + TransactionState output = new TransactionState<>(state, JCP_PROGRAM_ID, notary, encumbrance); + return new TransactionBuilder(notary).withItems(output, new Command<>(new Commands.Issue(), issuance.getParty().getOwningKey())); + } + + public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount> faceValue, @Nullable Instant maturityDate, @NotNull Party notary) { + return generateIssue(issuance, faceValue, maturityDate, notary, null); + } + + @Suspendable + public void generateRedeem(final TransactionBuilder tx, + final StateAndRef paper, + final ServiceHub services, + final PartyAndCertificate ourIdentity) throws InsufficientBalanceException { + PtCash.generateSpend(services, tx, Structures.withoutIssuer(paper.getState().getData().getFaceValue()), ourIdentity, paper.getState().getData().getOwner(), Collections.emptySet()); + tx.addInputState(paper); + tx.addCommand(new Command<>(new Commands.Redeem(), paper.getState().getData().getOwner().getOwningKey())); + } + + public void generateMove(TransactionBuilder tx, StateAndRef paper, AbstractParty newOwner) { + tx.addInputState(paper); + tx.addOutputState(new TransactionState<>(new State(paper.getState().getData().getIssuance(), newOwner, paper.getState().getData().getFaceValue(), paper.getState().getData().getMaturityDate()), JCP_PROGRAM_ID, paper.getState().getNotary(), paper.getState().getEncumbrance())); + tx.addCommand(new Command<>(new Commands.Move(), paper.getState().getData().getOwner().getOwningKey())); + } + + private static T onlyElementOf(Iterable iterable) { + Iterator iter = iterable.iterator(); + T item = iter.next(); + if (iter.hasNext()) { + throw new IllegalArgumentException("Iterable has more than one element!"); + } + return item; + } +} diff --git a/perftestflows/src/test/kotlin/net/corda/ptflows/contracts/PtCommercialPaperTests.kt b/perftestflows/src/test/kotlin/net/corda/ptflows/contracts/PtCommercialPaperTests.kt new file mode 100644 index 0000000000..d6b11c698b --- /dev/null +++ b/perftestflows/src/test/kotlin/net/corda/ptflows/contracts/PtCommercialPaperTests.kt @@ -0,0 +1,317 @@ +package net.corda.ptflows.contracts + +import net.corda.core.contracts.* +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.Party +import net.corda.core.node.services.Vault +import net.corda.core.node.services.VaultService +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.days +import net.corda.core.utilities.seconds +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` +import net.corda.ptflows.contracts.asset.* +import net.corda.testing.* +import net.corda.ptflows.contracts.asset.fillWithSomeTestCash +import net.corda.testing.node.MockServices +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.time.Instant +import java.util.* +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +// TODO: The generate functions aren't tested by these tests: add them. + +interface IPtCommercialPaperTestTemplate { + fun getPaper(): IPtCommercialPaperState + fun getIssueCommand(notary: Party): CommandData + fun getRedeemCommand(notary: Party): CommandData + fun getMoveCommand(): CommandData + fun getContract(): ContractClassName +} + +class JavaCommercialPaperTest : IPtCommercialPaperTestTemplate { + override fun getPaper(): IPtCommercialPaperState = JavaPtCommercialPaper.State( + MEGA_CORP.ref(123), + MEGA_CORP, + 1000.DOLLARS `issued by` MEGA_CORP.ref(123), + TEST_TX_TIME + 7.days + ) + + override fun getIssueCommand(notary: Party): CommandData = JavaPtCommercialPaper.Commands.Issue() + override fun getRedeemCommand(notary: Party): CommandData = JavaPtCommercialPaper.Commands.Redeem() + override fun getMoveCommand(): CommandData = JavaPtCommercialPaper.Commands.Move() + override fun getContract() = JavaPtCommercialPaper.JCP_PROGRAM_ID +} + +class KotlinCommercialPaperTest : IPtCommercialPaperTestTemplate { + override fun getPaper(): IPtCommercialPaperState = PtCommercialPaper.State( + issuance = MEGA_CORP.ref(123), + owner = MEGA_CORP, + faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123), + maturityDate = TEST_TX_TIME + 7.days + ) + + override fun getIssueCommand(notary: Party): CommandData = PtCommercialPaper.Commands.Issue() + override fun getRedeemCommand(notary: Party): CommandData = PtCommercialPaper.Commands.Redeem() + override fun getMoveCommand(): CommandData = PtCommercialPaper.Commands.Move() + override fun getContract() = PtCommercialPaper.CP_PROGRAM_ID +} + +class KotlinCommercialPaperLegacyTest : IPtCommercialPaperTestTemplate { + override fun getPaper(): IPtCommercialPaperState = PtCommercialPaper.State( + issuance = MEGA_CORP.ref(123), + owner = MEGA_CORP, + faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123), + maturityDate = TEST_TX_TIME + 7.days + ) + + override fun getIssueCommand(notary: Party): CommandData = PtCommercialPaper.Commands.Issue() + override fun getRedeemCommand(notary: Party): CommandData = PtCommercialPaper.Commands.Redeem() + override fun getMoveCommand(): CommandData = PtCommercialPaper.Commands.Move() + override fun getContract() = PtCommercialPaper.CP_PROGRAM_ID +} + +@RunWith(Parameterized::class) +class CommercialPaperTestsGeneric { + companion object { + @Parameterized.Parameters @JvmStatic + fun data() = listOf(JavaCommercialPaperTest(), KotlinCommercialPaperTest(), KotlinCommercialPaperLegacyTest()) + } + + @Parameterized.Parameter + lateinit var thisTest: IPtCommercialPaperTestTemplate + + val issuer = MEGA_CORP.ref(123) + + @Test + fun `trade lifecycle test`() { + val someProfits = 1200.DOLLARS `issued by` issuer + ledger { + unverifiedTransaction { + attachment(PtCash.PROGRAM_ID) + output(PtCash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE) + output(PtCash.PROGRAM_ID, "some profits", someProfits.STATE `owned by` MEGA_CORP) + } + + // Some CP is issued onto the ledger by MegaCorp. + transaction("Issuance") { + attachments(CP_PROGRAM_ID, JavaPtCommercialPaper.JCP_PROGRAM_ID) + output(thisTest.getContract(), "paper") { thisTest.getPaper() } + command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) } + timeWindow(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") { + attachments(PtCash.PROGRAM_ID, JavaPtCommercialPaper.JCP_PROGRAM_ID) + input("paper") + input("alice's $900") + output(PtCash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP } + output(thisTest.getContract(), "alice's paper") { "paper".output().withOwner(ALICE) } + command(ALICE_PUBKEY) { PtCash.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") { + attachments(CP_PROGRAM_ID, JavaPtCommercialPaper.JCP_PROGRAM_ID) + input("alice's paper") + input("some profits") + + fun TransactionDSL.outputs(aliceGetsBack: Amount>) { + output(PtCash.PROGRAM_ID, "Alice's profit") { aliceGetsBack.STATE `owned by` ALICE } + output(PtCash.PROGRAM_ID, "Change") { (someProfits - aliceGetsBack).STATE `owned by` MEGA_CORP } + } + + command(MEGA_CORP_PUBKEY) { PtCash.Commands.Move() } + command(ALICE_PUBKEY) { thisTest.getRedeemCommand(DUMMY_NOTARY) } + + tweak { + outputs(700.DOLLARS `issued by` issuer) + timeWindow(TEST_TX_TIME + 8.days) + this `fails with` "received amount equals the face value" + } + outputs(1000.DOLLARS `issued by` issuer) + + + tweak { + timeWindow(TEST_TX_TIME + 2.days) + this `fails with` "must have matured" + } + timeWindow(TEST_TX_TIME + 8.days) + + tweak { + output(thisTest.getContract()) { "paper".output() } + this `fails with` "must be destroyed" + } + + this.verifies() + } + } + } + + @Test + fun `key mismatch at issue`() { + transaction { + attachment(CP_PROGRAM_ID) + attachment(JavaPtCommercialPaper.JCP_PROGRAM_ID) + output(thisTest.getContract()) { thisTest.getPaper() } + command(MINI_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) } + timeWindow(TEST_TX_TIME) + this `fails with` "output states are issued by a command signer" + } + } + + @Test + fun `face value is not zero`() { + transaction { + attachment(CP_PROGRAM_ID) + attachment(JavaPtCommercialPaper.JCP_PROGRAM_ID) + output(thisTest.getContract()) { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) } + command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) } + timeWindow(TEST_TX_TIME) + this `fails with` "output values sum to more than the inputs" + } + } + + @Test + fun `maturity date not in the past`() { + transaction { + attachment(CP_PROGRAM_ID) + attachment(JavaPtCommercialPaper.JCP_PROGRAM_ID) + output(thisTest.getContract()) { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) } + command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) } + timeWindow(TEST_TX_TIME) + this `fails with` "maturity date is not in the past" + } + } + + @Test + fun `issue cannot replace an existing state`() { + transaction { + attachment(CP_PROGRAM_ID) + attachment(JavaPtCommercialPaper.JCP_PROGRAM_ID) + input(thisTest.getContract(), thisTest.getPaper()) + output(thisTest.getContract()) { thisTest.getPaper() } + command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) } + timeWindow(TEST_TX_TIME) + this `fails with` "output values sum to more than the inputs" + } + } + + /** + * Unit test requires two separate Database instances to represent each of the two + * transaction participants (enforces uniqueness of vault content in lieu of partipant identity) + */ + + private lateinit var bigCorpServices: MockServices + private lateinit var bigCorpVault: Vault + private lateinit var bigCorpVaultService: VaultService + + private lateinit var aliceServices: MockServices + private lateinit var aliceVaultService: VaultService + private lateinit var alicesVault: Vault + + private val notaryServices = MockServices(DUMMY_NOTARY_KEY) + private val issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY) + + private lateinit var moveTX: SignedTransaction + + // @Test + @Ignore + fun `issue move and then redeem`() { + setCordappPackages("net.corda.finance.contracts") + initialiseTestSerialization() + val aliceDatabaseAndServices = MockServices.makeTestDatabaseAndMockServices(keys = listOf(ALICE_KEY)) + val databaseAlice = aliceDatabaseAndServices.first + aliceServices = aliceDatabaseAndServices.second + aliceVaultService = aliceServices.vaultService + + databaseAlice.transaction { + alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, issuerServices, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = DUMMY_CASH_ISSUER) + aliceVaultService = aliceServices.vaultService + } + + val bigCorpDatabaseAndServices = MockServices.makeTestDatabaseAndMockServices(keys = listOf(BIG_CORP_KEY)) + val databaseBigCorp = bigCorpDatabaseAndServices.first + bigCorpServices = bigCorpDatabaseAndServices.second + bigCorpVaultService = bigCorpServices.vaultService + + databaseBigCorp.transaction { + bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, issuerServices, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = DUMMY_CASH_ISSUER) + bigCorpVaultService = bigCorpServices.vaultService + } + + // Propagate the cash transactions to each side. + aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.validatedTransactions.getTransaction(it.ref.txhash)!! }) + bigCorpServices.recordTransactions(alicesVault.states.map { aliceServices.validatedTransactions.getTransaction(it.ref.txhash)!! }) + + // BigCorpâ„¢ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. + val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER + val issuance = bigCorpServices.myInfo.chooseIdentity().ref(1) + val issueBuilder = PtCommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY) + issueBuilder.setTimeWindow(TEST_TX_TIME, 30.seconds) + val issuePtx = bigCorpServices.signInitialTransaction(issueBuilder) + val issueTx = notaryServices.addSignature(issuePtx) + + databaseAlice.transaction { + // Alice pays $9000 to BigCorp to own some of their debt. + moveTX = run { + val builder = TransactionBuilder(DUMMY_NOTARY) + PtCash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public)) + PtCommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public)) + val ptx = aliceServices.signInitialTransaction(builder) + val ptx2 = bigCorpServices.addSignature(ptx) + val stx = notaryServices.addSignature(ptx2) + stx + } + } + + databaseBigCorp.transaction { + // Verify the txns are valid and insert into both sides. + listOf(issueTx, moveTX).forEach { + it.toLedgerTransaction(aliceServices).verify() + aliceServices.recordTransactions(it) + bigCorpServices.recordTransactions(it) + } + } + + databaseBigCorp.transaction { + fun makeRedeemTX(time: Instant): Pair { + val builder = TransactionBuilder(DUMMY_NOTARY) + builder.setTimeWindow(time, 30.seconds) + PtCommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpServices, bigCorpServices.myInfo.chooseIdentityAndCert()) + val ptx = aliceServices.signInitialTransaction(builder) + val ptx2 = bigCorpServices.addSignature(ptx) + val stx = notaryServices.addSignature(ptx2) + return Pair(stx, builder.lockId) + } + + val redeemTX = makeRedeemTX(TEST_TX_TIME + 10.days) + val tooEarlyRedemption = redeemTX.first + val tooEarlyRedemptionLockId = redeemTX.second + val e = assertFailsWith(TransactionVerificationException::class) { + tooEarlyRedemption.toLedgerTransaction(aliceServices).verify() + } + // manually release locks held by this failing transaction + aliceServices.vaultService.softLockRelease(tooEarlyRedemptionLockId) + assertTrue(e.cause!!.message!!.contains("paper must have matured")) + + val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days).first + validRedemption.toLedgerTransaction(aliceServices).verify() + // soft lock not released after success either!!! (as transaction not recorded) + } + resetTestSerialization() + } +} + + diff --git a/perftestflows/src/test/kotlin/net/corda/ptflows/contract/asset/PtCashTests.kt b/perftestflows/src/test/kotlin/net/corda/ptflows/contracts/asset/PtCashTests.kt similarity index 99% rename from perftestflows/src/test/kotlin/net/corda/ptflows/contract/asset/PtCashTests.kt rename to perftestflows/src/test/kotlin/net/corda/ptflows/contracts/asset/PtCashTests.kt index 2f75481982..52e7a3fa3e 100644 --- a/perftestflows/src/test/kotlin/net/corda/ptflows/contract/asset/PtCashTests.kt +++ b/perftestflows/src/test/kotlin/net/corda/ptflows/contracts/asset/PtCashTests.kt @@ -1,4 +1,4 @@ -package net.corda.ptflows.contract.asset +package net.corda.ptflows.contracts.asset import net.corda.core.contracts.* @@ -22,7 +22,6 @@ import net.corda.ptflows.utils.sumCashOrNull import net.corda.ptflows.utils.sumCashOrZero import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.CordaPersistence -import net.corda.ptflows.contracts.asset.* import net.corda.testing.* import net.corda.testing.contracts.DummyState import net.corda.testing.contracts.calculateRandomlySizedAmounts