Merged in rg_work_in_progress (pull request #16)

Updated as comments per previous pull request, now generic tests run both Kotlin and Java CommercialPaper class tests.
This commit is contained in:
Richard Green 2016-02-10 15:04:13 +00:00
commit ea2b18eb41
7 changed files with 234 additions and 56 deletions

View File

@ -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);
}

View File

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

View File

@ -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<InOutGroup<State>> groups = tx.groupStates(State.class, State::withoutOwner);
// Find the command that instructs us what to do and check there's exactly one.
AuthenticatedObject<CommandData> 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<CommandData> cmd = requireSingleCommand(tx.getCommands(), JavaCommercialPaper.Commands.class);
for (InOutGroup<State> group : groups) {
List<State> inputs = group.getInputs();
List<State> 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<State> paper, List<StateAndRef<Cash.State>> 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<State> 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()));
}
}

View File

@ -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<CommercialPaper.State> {
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<ICommercialPaperState> {
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<String>) {
CommercialPaperTests().trade().visualise()
}

View File

@ -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<String>) {
CommercialPaperTestsJava().trade().visualise()
}

View File

@ -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<String>) {
CommercialPaperTestsKotlin().trade().visualise()
}

View File

@ -53,6 +53,7 @@ val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
val TEST_PROGRAM_MAP: Map<SecureHash, Contract> = 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<SecureHash, Contract> = 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)