mirror of
https://github.com/corda/corda.git
synced 2024-12-29 09:18:58 +00:00
Added unit tests for commercial paper
This commit is contained in:
parent
22bf2b1c1d
commit
1cb4f56609
@ -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;
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user