Added unit tests for commercial paper

This commit is contained in:
Christian Sailer 2017-10-10 14:58:55 +01:00
parent 22bf2b1c1d
commit 1cb4f56609
3 changed files with 585 additions and 2 deletions

View File

@ -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<Issued<Currency>> faceValue;
private Instant maturityDate;
public State() {
} // For serialization
public State(PartyAndReference issuance, AbstractParty owner, Amount<Issued<Currency>> 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<Issued<Currency>> 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<Issued<Currency>> 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<AbstractParty> 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<CommandWithParties<Commands>> extractCommands(@NotNull LedgerTransaction tx) {
return tx.getCommands()
.stream()
.filter((CommandWithParties<CommandData> command) -> command.getValue() instanceof Commands)
.map((CommandWithParties<CommandData> 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<LedgerTransaction.InOutGroup<State, State>> 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<CommandWithParties<CommandData>> commands = tx.getCommands().stream().filter(
it -> it.getValue() instanceof Commands
).collect(Collectors.toList());
final CommandWithParties<CommandData> command = onlyElementOf(commands);
final TimeWindow timeWindow = tx.getTimeWindow();
for (final LedgerTransaction.InOutGroup<State, State> group : groups) {
final List<State> inputs = group.getInputs();
final List<State> outputs = group.getOutputs();
if (command.getValue() instanceof Commands.Move) {
final CommandWithParties<Commands.Move> 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<Commands.Redeem> 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<Issued<Currency>> 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<Commands.Issue> 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<Issued<Currency>> 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<Issued<Currency>> faceValue, @Nullable Instant maturityDate, @NotNull Party notary) {
return generateIssue(issuance, faceValue, maturityDate, notary, null);
}
@Suspendable
public void generateRedeem(final TransactionBuilder tx,
final StateAndRef<State> 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<State> 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> T onlyElementOf(Iterable<T> iterable) {
Iterator<T> iter = iterable.iterator();
T item = iter.next();
if (iter.hasNext()) {
throw new IllegalArgumentException("Iterable has more than one element!");
}
return item;
}
}

View File

@ -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<IPtCommercialPaperState>().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<TransactionDSLInterpreter>.outputs(aliceGetsBack: Amount<Issued<Currency>>) {
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<IPtCommercialPaperState>() }
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<ContractState>
private lateinit var bigCorpVaultService: VaultService
private lateinit var aliceServices: MockServices
private lateinit var aliceVaultService: VaultService
private lateinit var alicesVault: Vault<ContractState>
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<SignedTransaction, UUID> {
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()
}
}

View File

@ -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