/* * 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 core.TransactionForVerification.*; import core.crypto.*; import org.jetbrains.annotations.*; import java.security.*; import java.time.*; 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. * */ public class JavaCommercialPaper implements Contract { public static SecureHash JCP_PROGRAM_ID = SecureHash.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; private Instant maturityDate; public State() {} // For serialization public State(PartyReference issuance, PublicKey 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 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() { return issuance; } public PublicKey getOwner() { return owner; } public Amount getFaceValue() { return faceValue; } public Instant getMaturityDate() { return maturityDate; } @NotNull @Override public SecureHash getProgramRef() { return SecureHash.Companion.sha256("java commercial paper (this should be a bytecode hash)"); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; State state = (State) o; 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; return !(maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null); } @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; } public State withoutOwner() { return new State(issuance, NullPublicKey.INSTANCE, faceValue, maturityDate); } } public static class Commands implements core.CommandData { public static class Move extends Commands { @Override public boolean equals(Object obj) { return obj instanceof Move; } } public static class Redeem extends Commands { @Override public boolean equals(Object obj) { return obj instanceof Redeem; } } public static class Issue extends Commands { @Override public boolean equals(Object obj) { return obj instanceof Issue; } } } @Override public void verify(@NotNull TransactionForVerification tx) { // 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(), 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. 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"); } 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("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"); } } } } @NotNull @Override public SecureHash getLegalContractReference() { // TODO: Should return hash of the contract's contents, not its URI return SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); } public TransactionBuilder generateIssue(@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 generateRedeem(TransactionBuilder tx, StateAndRef paper, List> wallet) throws InsufficientBalanceException { new Cash().generateSpend(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 generateMove(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())); } }