From 201803ff3610d099f5f79631bfe405f976188e8d Mon Sep 17 00:00:00 2001 From: Richard Green Date: Tue, 2 Feb 2016 17:23:43 +0000 Subject: [PATCH] Now runs Java CommercialPaper unit tests Acted upon comments from last pull request. Added an interface to enable the usage of the same tests for both the Kotlin and Java example CommercialPaper class - did appropriate refactoring to enable. Added javadoc, removed public modifier from interfaces. Various fixes from code review comments. --- .../java/contracts/ICommercialPaperState.java | 25 ++++ src/main/kotlin/contracts/CommercialPaper.kt | 8 +- .../kotlin/contracts/JavaCommercialPaper.java | 133 +++++++++++++----- ...ests.kt => CommercialPaperTestsGeneric.kt} | 42 +++--- .../contracts/CommercialPaperTestsJava.kt | 39 +++++ .../contracts/CommercialPaperTestsKotlin.kt | 39 +++++ src/test/kotlin/core/testutils/TestUtils.kt | 4 +- 7 files changed, 234 insertions(+), 56 deletions(-) create mode 100644 src/main/java/contracts/ICommercialPaperState.java rename src/test/kotlin/contracts/{CommercialPaperTests.kt => CommercialPaperTestsGeneric.kt} (89%) create mode 100644 src/test/kotlin/contracts/CommercialPaperTestsJava.kt create mode 100644 src/test/kotlin/contracts/CommercialPaperTestsKotlin.kt diff --git a/src/main/java/contracts/ICommercialPaperState.java b/src/main/java/contracts/ICommercialPaperState.java new file mode 100644 index 0000000000..868b30be69 --- /dev/null +++ b/src/main/java/contracts/ICommercialPaperState.java @@ -0,0 +1,25 @@ +/* + * Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members + * pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms + * set forth therein. + * + * All other rights reserved. + */ +package contracts; + +import core.*; + +import java.security.*; +import java.time.*; + +/* This is an interface solely created to demonstrate that the same kotlin tests can be run against + * either a Java implementation of the CommercialPaper or a kotlin implementation. + * Normally one would not duplicate an implementation in different languages for obvious reasons, but it demonstrates that + * ultimately either language can be used against a common test framework (and therefore can be used for real). + */ +public interface ICommercialPaperState extends ContractState { + ICommercialPaperState withOwner(PublicKey newOwner); + ICommercialPaperState withIssuance(PartyReference newIssuance); + ICommercialPaperState withFaceValue(Amount newFaceValue); + ICommercialPaperState withMaturityDate(Instant newMaturityDate); +} diff --git a/src/main/kotlin/contracts/CommercialPaper.kt b/src/main/kotlin/contracts/CommercialPaper.kt index b870e0ffa6..16344b95b2 100644 --- a/src/main/kotlin/contracts/CommercialPaper.kt +++ b/src/main/kotlin/contracts/CommercialPaper.kt @@ -47,12 +47,18 @@ class CommercialPaper : Contract { override val owner: PublicKey, val faceValue: Amount, val maturityDate: Instant - ) : OwnableState { + ) : OwnableState, ICommercialPaperState { override val programRef = CP_PROGRAM_ID fun withoutOwner() = copy(owner = NullPublicKey) override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) override fun toString() = "${Emoji.newspaper}CommercialPaper(of $faceValue redeemable on $maturityDate by '$issuance', owned by ${owner.toStringShort()})" + + // Although kotlin is smart enough not to need these, as we are using the ICommercialPaperState, we need to declare them explicitly for use later, + override fun withOwner(newOwner: PublicKey): ICommercialPaperState = copy(owner = newOwner) + override fun withIssuance(newIssuance: PartyReference): ICommercialPaperState = copy(issuance = newIssuance) + override fun withFaceValue(newFaceValue: Amount): ICommercialPaperState = copy(faceValue = newFaceValue) + override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate) } interface Commands : CommandData { diff --git a/src/main/kotlin/contracts/JavaCommercialPaper.java b/src/main/kotlin/contracts/JavaCommercialPaper.java index 740602be2d..bcbeb7ff9d 100644 --- a/src/main/kotlin/contracts/JavaCommercialPaper.java +++ b/src/main/kotlin/contracts/JavaCommercialPaper.java @@ -19,14 +19,16 @@ import java.util.*; import static core.ContractsDSLKt.*; import static kotlin.collections.CollectionsKt.*; + /** * 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. * - * NOTE: For illustration only. Not unit tested. */ public class JavaCommercialPaper implements Contract { - public static class State implements ContractState { + public static core.SecureHash JCP_PROGRAM_ID = SecureHash.Companion.sha256("java commercial paper (this should be a bytecode hash)"); + + public static class State implements ContractState, ICommercialPaperState { private PartyReference issuance; private PublicKey owner; private Amount faceValue; @@ -42,8 +44,23 @@ public class JavaCommercialPaper implements Contract { } public State copy() { - State ret = new State(this.issuance, this.owner, this.faceValue, this.maturityDate); - return ret; + return new State(this.issuance, this.owner, this.faceValue, this.maturityDate); + } + + public ICommercialPaperState withOwner(PublicKey newOwner) { + return new State(this.issuance, newOwner, this.faceValue, this.maturityDate); + } + + public ICommercialPaperState withIssuance(PartyReference newIssuance) { + return new State(newIssuance, this.owner, this.faceValue, this.maturityDate); + } + + public ICommercialPaperState withFaceValue(Amount newFaceValue) { + return new State(this.issuance, this.owner, newFaceValue, this.maturityDate); + } + + public ICommercialPaperState withMaturityDate(Instant newMaturityDate) { + return new State(this.issuance, this.owner, this.faceValue, newMaturityDate); } public PartyReference getIssuance() { @@ -120,46 +137,78 @@ public class JavaCommercialPaper implements Contract { @Override public void verify(@NotNull TransactionForVerification tx) { - // There are two possible things that can be done with CP. The first is trading it. The second is redeeming it - // for cash on or after the maturity date. + // There are three possible things that can be done with CP. + // Issuance, trading (aka moving in this prototype) and redeeming. + // Each command has it's own set of restrictions which the verify function ... verifies. + List> groups = tx.groupStates(State.class, State::withoutOwner); // Find the command that instructs us what to do and check there's exactly one. - AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.class); - TimestampCommand timestampCommand = tx.getTimestampBy(DummyTimestampingAuthority.INSTANCE.getIdentity()); - if (timestampCommand == null) - throw new IllegalArgumentException("must be timestamped"); - Instant time = timestampCommand.getMidpoint(); + AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), JavaCommercialPaper.Commands.class); for (InOutGroup group : groups) { List inputs = group.getInputs(); List outputs = group.getOutputs(); - // For now do not allow multiple pieces of CP to trade in a single transaction. Study this more! - State input = single(filterIsInstance(inputs, State.class)); - - if (!cmd.getSigners().contains(input.getOwner())) - throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); - - if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Move) { - // Check the output CP state is the same as the input state, ignoring the owner field. + // For now do not allow multiple pieces of CP to trade in a single transaction. + if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Issue) { State output = single(outputs); + if (!inputs.isEmpty()) { + throw new IllegalStateException("Failed Requirement: there is no input state"); + } + if (output.faceValue.getPennies() == 0) { + throw new IllegalStateException("Failed Requirement: the face value is not zero"); + } - if (!output.getFaceValue().equals(input.getFaceValue()) || - !output.getIssuance().equals(input.getIssuance()) || - !output.getMaturityDate().equals(input.getMaturityDate())) - throw new IllegalStateException("Failed requirement: the output state is the same as the input state except for owner"); - } else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) { - Amount received = CashKt.sumCashOrNull(inputs); - if (received == null) - throw new IllegalStateException("Failed requirement: no cash being redeemed"); - if (input.getMaturityDate().isAfter(time)) - throw new IllegalStateException("Failed requirement: the paper must have matured"); - if (!input.getFaceValue().equals(received)) - throw new IllegalStateException("Failed requirement: the received amount equals the face value"); - if (!outputs.isEmpty()) - throw new IllegalStateException("Failed requirement: the paper must be destroyed"); + TimestampCommand timestampCommand = tx.getTimestampBy(DummyTimestampingAuthority.INSTANCE.getIdentity()); + if (timestampCommand == null) + throw new IllegalArgumentException("Failed Requirement: must be timestamped"); + + Instant time = timestampCommand.getBefore(); + + if (! time.isBefore(output.maturityDate)) { + throw new IllegalStateException("Failed Requirement: the maturity date is not in the past"); + } + + if (!cmd.getSigners().contains(output.issuance.getParty().getOwningKey())) { + throw new IllegalStateException("Failed Requirement: the issuance is signed by the claimed issuer of the paper"); + } + } + else { // Everything else (Move, Redeem) requires inputs (they are not first to be actioned) + // There should be only a single input due to aggregation above + State input = single(inputs); + + if (!cmd.getSigners().contains(input.getOwner())) + throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); + + if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Move) { + // Check the output CP state is the same as the input state, ignoring the owner field. + State output = single(outputs); + + if (!output.getFaceValue().equals(input.getFaceValue()) || + !output.getIssuance().equals(input.getIssuance()) || + !output.getMaturityDate().equals(input.getMaturityDate())) + throw new IllegalStateException("Failed requirement: the output state is the same as the input state except for owner"); + } + else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) + { + TimestampCommand timestampCommand = tx.getTimestampBy(DummyTimestampingAuthority.INSTANCE.getIdentity()); + if (timestampCommand == null) + throw new IllegalArgumentException("Failed Requirement: must be timestamped"); + Instant time = timestampCommand.getBefore(); + + Amount received = CashKt.sumCashBy(tx.getOutStates(), input.getOwner()); + + if (! received.equals(input.getFaceValue())) + throw new IllegalStateException(String.format("Failed Requirement: received amount equals the face value")); + if (time.isBefore(input.getMaturityDate())) + throw new IllegalStateException("Failed requirement: the paper must have matured"); + if (!input.getFaceValue().equals(received)) + throw new IllegalStateException("Failed requirement: the received amount equals the face value"); + if (!outputs.isEmpty()) + throw new IllegalStateException("Failed requirement: the paper must be destroyed"); + } } } } @@ -170,4 +219,22 @@ public class JavaCommercialPaper implements Contract { // TODO: Should return hash of the contract's contents, not its URI return SecureHash.Companion.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); } + + public TransactionBuilder craftIssue(@NotNull PartyReference issuance, @NotNull Amount faceValue, @Nullable Instant maturityDate) { + State state = new State(issuance,issuance.getParty().getOwningKey(), faceValue, maturityDate); + return new TransactionBuilder().withItems(state, new Command( new Commands.Issue(), issuance.getParty().getOwningKey())); + } + + public void craftRedeem(TransactionBuilder tx, StateAndRef paper, List> wallet) throws InsufficientBalanceException { + new Cash().craftSpend(tx, paper.getState().getFaceValue(), paper.getState().getOwner(), wallet, null); + tx.addInputState(paper.getRef()); + tx.addCommand(new Command( new Commands.Redeem(), paper.getState().getOwner())); + } + + public void craftMove(TransactionBuilder tx, StateAndRef paper, PublicKey newOwner) { + tx.addInputState(paper.getRef()); + tx.addOutputState(new State(paper.getState().getIssuance(), newOwner, paper.getState().getFaceValue(), paper.getState().getMaturityDate())); + tx.addCommand(new Command(new Commands.Move(), paper.getState().getOwner())); + + } } diff --git a/src/test/kotlin/contracts/CommercialPaperTests.kt b/src/test/kotlin/contracts/CommercialPaperTestsGeneric.kt similarity index 89% rename from src/test/kotlin/contracts/CommercialPaperTests.kt rename to src/test/kotlin/contracts/CommercialPaperTestsGeneric.kt index 26a173ecaa..ca8a312786 100644 --- a/src/test/kotlin/contracts/CommercialPaperTests.kt +++ b/src/test/kotlin/contracts/CommercialPaperTestsGeneric.kt @@ -18,13 +18,17 @@ import java.time.ZoneOffset import kotlin.test.assertFailsWith import kotlin.test.assertTrue -class CommercialPaperTests { - val PAPER_1 = CommercialPaper.State( - issuance = MEGA_CORP.ref(123), - owner = MEGA_CORP_PUBKEY, - faceValue = 1000.DOLLARS, - maturityDate = TEST_TX_TIME + 7.days - ) +interface ICommercialPaperTestTemplate { + open fun getPaper() : ICommercialPaperState + open fun getIssueCommand() : CommandData + open fun getRedeemCommand() : CommandData + open fun getMoveCommand() : CommandData +} + +open class CommercialPaperTestsGeneric(templateToTest: ICommercialPaperTestTemplate) { + + val thisTest = templateToTest + val PAPER_1 = thisTest.getPaper() @Test fun ok() { @@ -41,7 +45,7 @@ class CommercialPaperTests { transactionGroup { transaction { output { PAPER_1 } - arg(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() } + arg(DUMMY_PUBKEY_1) { thisTest.getIssueCommand() } timestamp(TEST_TX_TIME) } @@ -53,8 +57,8 @@ class CommercialPaperTests { fun `face value is not zero`() { transactionGroup { transaction { - output { PAPER_1.copy(faceValue = 0.DOLLARS) } - arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + output { PAPER_1.withFaceValue(0.DOLLARS) } + arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } timestamp(TEST_TX_TIME) } @@ -66,8 +70,8 @@ class CommercialPaperTests { fun `maturity date not in the past`() { transactionGroup { transaction { - output { PAPER_1.copy(maturityDate = TEST_TX_TIME - 10.days) } - arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + output { PAPER_1.withMaturityDate(TEST_TX_TIME - 10.days) } + arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } timestamp(TEST_TX_TIME) } @@ -106,7 +110,7 @@ class CommercialPaperTests { transaction { input("paper") output { PAPER_1 } - arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } timestamp(TEST_TX_TIME) } @@ -189,7 +193,7 @@ class CommercialPaperTests { // Generate a trade lifecycle with various parameters. fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days, aliceGetsBack: Amount = 1000.DOLLARS, - destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL { + destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL { val someProfits = 1200.DOLLARS return transactionGroupFor() { roots { @@ -200,7 +204,7 @@ class CommercialPaperTests { // Some CP is issued onto the ledger by MegaCorp. transaction("Issuance") { output("paper") { PAPER_1 } - arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } + arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } timestamp(TEST_TX_TIME) } @@ -212,7 +216,7 @@ class CommercialPaperTests { output("borrowed $900") { 900.DOLLARS.CASH `owned by` MEGA_CORP_PUBKEY } output("alice's paper") { "paper".output `owned by` ALICE } arg(ALICE) { Cash.Commands.Move() } - arg(MEGA_CORP_PUBKEY) { CommercialPaper.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 @@ -227,14 +231,10 @@ class CommercialPaperTests { output { "paper".output } arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } - arg(ALICE) { CommercialPaper.Commands.Redeem() } + arg(ALICE) { thisTest.getRedeemCommand() } timestamp(redemptionTime) } } } } - -fun main(args: Array) { - CommercialPaperTests().trade().visualise() -} diff --git a/src/test/kotlin/contracts/CommercialPaperTestsJava.kt b/src/test/kotlin/contracts/CommercialPaperTestsJava.kt new file mode 100644 index 0000000000..dfc4bb1ed2 --- /dev/null +++ b/src/test/kotlin/contracts/CommercialPaperTestsJava.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members + * pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms + * set forth therein. + * + * All other rights reserved. + */ + +package contracts + +import core.CommandData +import core.DOLLARS +import core.days +import core.testutils.MEGA_CORP +import core.testutils.MEGA_CORP_PUBKEY +import core.testutils.TEST_TX_TIME + +fun getJavaCommericalPaper() : ICommercialPaperState { + return JavaCommercialPaper.State( + MEGA_CORP.ref(123), + MEGA_CORP_PUBKEY, + 1000.DOLLARS, + TEST_TX_TIME + 7.days + ) +} + +open class JavaCommercialPaperTest() : ICommercialPaperTestTemplate { + override fun getPaper() : ICommercialPaperState = getJavaCommericalPaper() + override fun getIssueCommand() : CommandData = JavaCommercialPaper.Commands.Issue() + override fun getRedeemCommand() : CommandData = JavaCommercialPaper.Commands.Redeem() + override fun getMoveCommand() : CommandData = JavaCommercialPaper.Commands.Move() +} + +class CommercialPaperTestsJava() : CommercialPaperTestsGeneric(JavaCommercialPaperTest()) { } + + +fun main(args: Array) { + CommercialPaperTestsJava().trade().visualise() +} diff --git a/src/test/kotlin/contracts/CommercialPaperTestsKotlin.kt b/src/test/kotlin/contracts/CommercialPaperTestsKotlin.kt new file mode 100644 index 0000000000..975701b1ec --- /dev/null +++ b/src/test/kotlin/contracts/CommercialPaperTestsKotlin.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members + * pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms + * set forth therein. + * + * All other rights reserved. + */ + +package contracts + +import core.CommandData +import core.DOLLARS +import core.days +import core.testutils.MEGA_CORP +import core.testutils.MEGA_CORP_PUBKEY +import core.testutils.TEST_TX_TIME + +fun getKotlinCommercialPaper() : ICommercialPaperState { + return CommercialPaper.State( + issuance = MEGA_CORP.ref(123), + owner = MEGA_CORP_PUBKEY, + faceValue = 1000.DOLLARS, + maturityDate = TEST_TX_TIME + 7.days + ) +} + +open class KotlinCommercialPaperTest() : ICommercialPaperTestTemplate { + override fun getPaper() : ICommercialPaperState = getKotlinCommercialPaper() + override fun getIssueCommand() : CommandData = CommercialPaper.Commands.Issue() + override fun getRedeemCommand() : CommandData = CommercialPaper.Commands.Redeem() + override fun getMoveCommand() : CommandData = CommercialPaper.Commands.Move() +} + +class CommercialPaperTestsKotlin() : CommercialPaperTestsGeneric( KotlinCommercialPaperTest()) { } + +fun main(args: Array) { + CommercialPaperTestsKotlin().trade().visualise() +} + diff --git a/src/test/kotlin/core/testutils/TestUtils.kt b/src/test/kotlin/core/testutils/TestUtils.kt index ea8271977a..37219b93ec 100644 --- a/src/test/kotlin/core/testutils/TestUtils.kt +++ b/src/test/kotlin/core/testutils/TestUtils.kt @@ -53,6 +53,7 @@ val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z") val TEST_PROGRAM_MAP: Map = mapOf( CASH_PROGRAM_ID to Cash(), CP_PROGRAM_ID to CommercialPaper(), + JavaCommercialPaper.JCP_PROGRAM_ID to JavaCommercialPaper(), CROWDFUND_PROGRAM_ID to CrowdFund(), DUMMY_PROGRAM_ID to DummyContract ) @@ -79,7 +80,8 @@ val TEST_PROGRAM_MAP: Map = mapOf( // TODO: Make it impossible to forget to test either a failure or an accept for each transaction{} block infix fun Cash.State.`owned by`(owner: PublicKey) = this.copy(owner = owner) -infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = this.copy(owner = owner) +infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = this.withOwner(new_owner) + // Allows you to write 100.DOLLARS.CASH val Amount.CASH: Cash.State get() = Cash.State(MINI_CORP.ref(1,2,3), this, NullPublicKey)