mirror of
https://github.com/corda/corda.git
synced 2024-12-19 13:08:04 +00:00
Merged in rnicoll-clauses-commercial-paper (pull request #227)
Rebuild commercial paper contracts using clauses
This commit is contained in:
commit
1ec1642080
@ -3,8 +3,8 @@ package com.r3corda.contracts;
|
||||
import com.google.common.collect.*;
|
||||
import com.r3corda.contracts.asset.*;
|
||||
import com.r3corda.core.contracts.*;
|
||||
import static com.r3corda.core.contracts.ContractsDSL.requireThat;
|
||||
import com.r3corda.core.contracts.TransactionForContract.*;
|
||||
import com.r3corda.core.contracts.clauses.*;
|
||||
import com.r3corda.core.crypto.*;
|
||||
import kotlin.Unit;
|
||||
import org.jetbrains.annotations.*;
|
||||
@ -12,6 +12,7 @@ import org.jetbrains.annotations.*;
|
||||
import java.security.*;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.r3corda.core.contracts.ContractsDSL.*;
|
||||
import static kotlin.collections.CollectionsKt.*;
|
||||
@ -21,7 +22,7 @@ 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 class JavaCommercialPaper extends ClauseVerifier {
|
||||
//public static SecureHash JCP_PROGRAM_ID = SecureHash.sha256("java commercial paper (this should be a bytecode hash)");
|
||||
public static Contract JCP_PROGRAM_ID = new JavaCommercialPaper();
|
||||
|
||||
@ -118,62 +119,145 @@ public class JavaCommercialPaper implements Contract {
|
||||
}
|
||||
}
|
||||
|
||||
public static class Commands implements CommandData {
|
||||
public static class Move extends Commands {
|
||||
public interface Clause {
|
||||
abstract class AbstractGroup implements GroupClause<State, State> {
|
||||
@NotNull
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof Move;
|
||||
public MatchBehaviour getIfNotMatched() {
|
||||
return MatchBehaviour.CONTINUE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public MatchBehaviour getIfMatched() {
|
||||
return MatchBehaviour.END;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Redeem extends Commands {
|
||||
private final Party notary;
|
||||
|
||||
public Redeem(Party setNotary) {
|
||||
this.notary = setNotary;
|
||||
class Group extends GroupClauseVerifier<State, State> {
|
||||
@NotNull
|
||||
@Override
|
||||
public MatchBehaviour getIfMatched() {
|
||||
return MatchBehaviour.END;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof Redeem;
|
||||
public MatchBehaviour getIfNotMatched() {
|
||||
return MatchBehaviour.ERROR;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<GroupClause<State, State>> getClauses() {
|
||||
final List<GroupClause<State, State>> clauses = new ArrayList<>();
|
||||
|
||||
clauses.add(new Clause.Redeem());
|
||||
clauses.add(new Clause.Move());
|
||||
clauses.add(new Clause.Issue());
|
||||
|
||||
return clauses;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<InOutGroup<State, State>> extractGroups(@NotNull TransactionForContract tx) {
|
||||
return tx.groupStates(State.class, State::withoutOwner);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Issue extends Commands {
|
||||
private final Party notary;
|
||||
|
||||
public Issue(Party setNotary) {
|
||||
this.notary = setNotary;
|
||||
class Move extends AbstractGroup {
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||
return Collections.singleton(Commands.Move.class);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof Issue;
|
||||
public Set<CommandData> verify(@NotNull TransactionForContract tx,
|
||||
@NotNull List<? extends State> inputs,
|
||||
@NotNull List<? extends State> outputs,
|
||||
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
|
||||
@NotNull State token) {
|
||||
AuthenticatedObject<Commands.Move> cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class);
|
||||
// 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");
|
||||
|
||||
// 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");
|
||||
}
|
||||
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
|
||||
// the input ignoring the owner field due to the grouping.
|
||||
return Collections.singleton(cmd.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(@NotNull TransactionForContract 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.
|
||||
class Redeem extends AbstractGroup {
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||
return Collections.singleton(Commands.Redeem.class);
|
||||
}
|
||||
|
||||
List<InOutGroup<State, State>> groups = tx.groupStates(State.class, State::withoutOwner);
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<CommandData> verify(@NotNull TransactionForContract tx,
|
||||
@NotNull List<? extends State> inputs,
|
||||
@NotNull List<? extends State> outputs,
|
||||
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
|
||||
@NotNull State token) {
|
||||
AuthenticatedObject<Commands.Redeem> cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class);
|
||||
|
||||
// Find the command that instructs us what to do and check there's exactly one.
|
||||
// There should be only a single input due to aggregation above
|
||||
State input = single(inputs);
|
||||
|
||||
AuthenticatedObject<CommandData> cmd = requireSingleCommand(tx.getCommands(), JavaCommercialPaper.Commands.class);
|
||||
if (!cmd.getSigners().contains(input.getOwner()))
|
||||
throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP");
|
||||
|
||||
for (InOutGroup<State, State> group : groups) {
|
||||
List<State> inputs = group.getInputs();
|
||||
List<State> outputs = group.getOutputs();
|
||||
Party notary = cmd.getValue().notary;
|
||||
TimestampCommand timestampCommand = tx.getTimestampBy(notary);
|
||||
Instant time = null == timestampCommand
|
||||
? null
|
||||
: timestampCommand.getBefore();
|
||||
Amount<Issued<Currency>> received = CashKt.sumCashBy(tx.getOutputs(), input.getOwner());
|
||||
|
||||
// For now do not allow multiple pieces of CP to trade in a single transaction.
|
||||
if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Issue) {
|
||||
Commands.Issue issueCommand = (Commands.Issue) cmd.getValue();
|
||||
requireThat(require -> {
|
||||
require.by("must be timestamped", timestampCommand != null);
|
||||
require.by("received amount equals the face value: "
|
||||
+ received + " vs " + input.getFaceValue(), received.equals(input.getFaceValue()));
|
||||
require.by("the paper must have matured", time != null && !time.isBefore(input.getMaturityDate()));
|
||||
require.by("the received amount equals the face value", input.getFaceValue().equals(received));
|
||||
require.by("the paper must be destroyed", outputs.isEmpty());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
return Collections.singleton(cmd.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
class Issue extends AbstractGroup {
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||
return Collections.singleton(Commands.Issue.class);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<CommandData> verify(@NotNull TransactionForContract tx,
|
||||
@NotNull List<? extends State> inputs,
|
||||
@NotNull List<? extends State> outputs,
|
||||
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
|
||||
@NotNull State token) {
|
||||
AuthenticatedObject<Commands.Issue> cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class);
|
||||
State output = single(outputs);
|
||||
TimestampCommand timestampCommand = tx.getTimestampBy(issueCommand.notary);
|
||||
Party notary = cmd.getValue().notary;
|
||||
TimestampCommand timestampCommand = tx.getTimestampBy(notary);
|
||||
Instant time = null == timestampCommand
|
||||
? null
|
||||
: timestampCommand.getBefore();
|
||||
@ -186,44 +270,66 @@ public class JavaCommercialPaper implements Contract {
|
||||
require.by("output states are issued by a command signer", cmd.getSigners().contains(output.issuance.getParty().getOwningKey()));
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
} 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);
|
||||
|
||||
requireThat(require -> {
|
||||
require.by("the transaction is signed by the owner of the CP", cmd.getSigners().contains(input.getOwner()));
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Move) {
|
||||
requireThat(require -> {
|
||||
require.by("the state is propagated", outputs.size() == 1);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
|
||||
// the input ignoring the owner field due to the grouping.
|
||||
} else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) {
|
||||
TimestampCommand timestampCommand = tx.getTimestampBy(((Commands.Redeem) cmd.getValue()).notary);
|
||||
Instant time = null == timestampCommand
|
||||
? null
|
||||
: timestampCommand.getBefore();
|
||||
Amount<Issued<Currency>> received = CashKt.sumCashBy(tx.getOutputs(), input.getOwner());
|
||||
|
||||
requireThat(require -> {
|
||||
require.by("must be timestamped", timestampCommand != null);
|
||||
require.by("received amount equals the face value: "
|
||||
+ received + " vs " + input.getFaceValue(), received.equals(input.getFaceValue()));
|
||||
require.by("the paper must have matured", time != null && !time.isBefore(input.getMaturityDate()));
|
||||
require.by("the received amount equals the face value", input.getFaceValue().equals(received));
|
||||
require.by("the paper must be destroyed", outputs.isEmpty());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
return Collections.singleton(cmd.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface Commands extends CommandData {
|
||||
class Move implements Commands {
|
||||
@Override
|
||||
public boolean equals(Object obj) { return obj instanceof Move; }
|
||||
}
|
||||
|
||||
class Redeem implements Commands {
|
||||
private final Party notary;
|
||||
|
||||
public Redeem(Party setNotary) {
|
||||
this.notary = setNotary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) { return obj instanceof Redeem; }
|
||||
}
|
||||
|
||||
class Issue implements Commands {
|
||||
private final Party notary;
|
||||
|
||||
public Issue(Party setNotary) {
|
||||
this.notary = setNotary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Issue) {
|
||||
Issue other = (Issue)obj;
|
||||
return notary.equals(other.notary);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() { return notary.hashCode(); }
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<SingleClause> getClauses() {
|
||||
return Collections.singletonList(new Clause.Group());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) {
|
||||
return tx.getCommands()
|
||||
.stream()
|
||||
.filter((AuthenticatedObject<CommandData> command) -> { return command.getValue() instanceof Commands; })
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public SecureHash getLegalContractReference() {
|
||||
|
@ -3,11 +3,14 @@ package com.r3corda.contracts
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.contracts.asset.InsufficientBalanceException
|
||||
import com.r3corda.contracts.asset.sumCashBy
|
||||
import com.r3corda.contracts.clause.AbstractIssue
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.*
|
||||
import com.r3corda.core.crypto.NullPublicKey
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.toStringShort
|
||||
import com.r3corda.core.random63BitValue
|
||||
import com.r3corda.core.utilities.Emoji
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
@ -38,10 +41,21 @@ import java.util.*
|
||||
val CP_PROGRAM_ID = CommercialPaper()
|
||||
|
||||
// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance.
|
||||
class CommercialPaper : Contract {
|
||||
class CommercialPaper : ClauseVerifier() {
|
||||
// TODO: should reference the content of the legal agreement, not its URI
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper")
|
||||
|
||||
data class Terms(
|
||||
val asset: Issued<Currency>,
|
||||
val maturityDate: Instant
|
||||
)
|
||||
|
||||
override val clauses: List<SingleClause>
|
||||
get() = listOf(Clauses.Group())
|
||||
|
||||
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
|
||||
= tx.commands.select<Commands>()
|
||||
|
||||
data class State(
|
||||
val issuance: PartyAndReference,
|
||||
override val owner: PublicKey,
|
||||
@ -52,7 +66,9 @@ class CommercialPaper : Contract {
|
||||
override val participants: List<PublicKey>
|
||||
get() = listOf(owner)
|
||||
|
||||
fun withoutOwner() = copy(owner = NullPublicKey)
|
||||
val token: Issued<Terms>
|
||||
get() = Issued(issuance, Terms(faceValue.token, maturityDate))
|
||||
|
||||
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()})"
|
||||
|
||||
@ -64,73 +80,108 @@ class CommercialPaper : Contract {
|
||||
override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate)
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Move: TypeOnlyCommandData(), Commands
|
||||
data class Redeem(val notary: Party) : Commands
|
||||
// We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP.
|
||||
// However, nothing in the platform enforces that uniqueness: it's up to the issuer.
|
||||
data class Issue(val notary: Party) : Commands
|
||||
}
|
||||
interface Clauses {
|
||||
class Group : GroupClauseVerifier<State, Issued<Terms>>() {
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val clauses: List<GroupClause<State, Issued<Terms>>>
|
||||
get() = listOf(
|
||||
Redeem(),
|
||||
Move(),
|
||||
Issue())
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
// Group by everything except owner: any modification to the CP at all is considered changing it fundamentally.
|
||||
val groups = tx.groupStates() { it: State -> it.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.
|
||||
val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>()
|
||||
// If it's an issue, we can't take notary from inputs, so it must be specified in the command
|
||||
val cmdVal = command.value
|
||||
val timestamp: TimestampCommand? = when (cmdVal) {
|
||||
is Commands.Issue -> tx.getTimestampBy(cmdVal.notary)
|
||||
is Commands.Redeem -> tx.getTimestampBy(cmdVal.notary)
|
||||
else -> null
|
||||
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
|
||||
= tx.groupStates<State, Issued<Terms>> { it.token }
|
||||
}
|
||||
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
when (command.value) {
|
||||
is Commands.Move -> {
|
||||
val input = inputs.single()
|
||||
requireThat {
|
||||
"the transaction is signed by the owner of the CP" by (input.owner in command.signers)
|
||||
"the state is propagated" by (outputs.size == 1)
|
||||
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
|
||||
// the input ignoring the owner field due to the grouping.
|
||||
}
|
||||
}
|
||||
abstract class AbstractGroupClause: GroupClause<State, Issued<Terms>> {
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
}
|
||||
|
||||
// Redemption of the paper requires movement of on-ledger cash.
|
||||
is Commands.Redeem -> {
|
||||
val input = inputs.single()
|
||||
val received = tx.outputs.sumCashBy(input.owner)
|
||||
val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped")
|
||||
requireThat {
|
||||
"the paper must have matured" by (time >= input.maturityDate)
|
||||
"the received amount equals the face value" by (received == input.faceValue)
|
||||
"the paper must be destroyed" by outputs.isEmpty()
|
||||
"the transaction is signed by the owner of the CP" by (input.owner in command.signers)
|
||||
}
|
||||
}
|
||||
class Issue : AbstractIssue<State, Terms>(
|
||||
{ map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() },
|
||||
{ token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Issue::class.java)
|
||||
|
||||
is Commands.Issue -> {
|
||||
val output = outputs.single()
|
||||
val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped")
|
||||
requireThat {
|
||||
// Don't allow people to issue commercial paper under other entities identities.
|
||||
"output states are issued by a command signer" by
|
||||
(output.issuance.party.owningKey in command.signers)
|
||||
"output values sum to more than the inputs" by (output.faceValue.quantity > 0)
|
||||
"the maturity date is not in the past" by (time < output.maturityDate)
|
||||
// Don't allow an existing CP state to be replaced by this issuance.
|
||||
// TODO: Consider how to handle the case of mistaken issuances, or other need to patch.
|
||||
"output values sum to more than the inputs" by inputs.isEmpty()
|
||||
}
|
||||
}
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms>): Set<CommandData> {
|
||||
val consumedCommands = super.verify(tx, inputs, outputs, commands, token)
|
||||
val command = commands.requireSingleCommand<Commands.Issue>()
|
||||
// If it's an issue, we can't take notary from inputs, so it must be specified in the command
|
||||
val timestamp: TimestampCommand? = tx.getTimestampBy(command.value.notary)
|
||||
val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped")
|
||||
|
||||
// TODO: Think about how to evolve contracts over time with new commands.
|
||||
else -> throw IllegalArgumentException("Unrecognised command")
|
||||
require(outputs.all { time < it.maturityDate }) { "maturity date is not in the past" }
|
||||
|
||||
return consumedCommands
|
||||
}
|
||||
}
|
||||
|
||||
class Move: AbstractGroupClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Move::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms>): Set<CommandData> {
|
||||
val command = commands.requireSingleCommand<Commands.Move>()
|
||||
val input = inputs.single()
|
||||
requireThat {
|
||||
"the transaction is signed by the owner of the CP" by (input.owner in command.signers)
|
||||
"the state is propagated" by (outputs.size == 1)
|
||||
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
|
||||
// the input ignoring the owner field due to the grouping.
|
||||
}
|
||||
return setOf(command.value)
|
||||
}
|
||||
}
|
||||
|
||||
class Redeem(): AbstractGroupClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Redeem::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms>): Set<CommandData> {
|
||||
// TODO: This should filter commands down to those with compatible subjects (underlying product and maturity date)
|
||||
// before requiring a single command
|
||||
val command = commands.requireSingleCommand<Commands.Redeem>()
|
||||
// If it's an issue, we can't take notary from inputs, so it must be specified in the command
|
||||
val timestamp: TimestampCommand? = tx.getTimestampBy(command.value.notary)
|
||||
|
||||
val input = inputs.single()
|
||||
val received = tx.outputs.sumCashBy(input.owner)
|
||||
val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped")
|
||||
requireThat {
|
||||
"the paper must have matured" by (time >= input.maturityDate)
|
||||
"the received amount equals the face value" by (received == input.faceValue)
|
||||
"the paper must be destroyed" by outputs.isEmpty()
|
||||
"the transaction is signed by the owner of the CP" by (input.owner in command.signers)
|
||||
}
|
||||
|
||||
return setOf(command.value)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Move : TypeOnlyCommandData(), Commands
|
||||
data class Redeem(val notary: Party) : Commands
|
||||
data class Issue(val notary: Party, override val nonce: Long = random63BitValue()) : IssueCommand, Commands
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,116 @@
|
||||
package com.r3corda.contracts
|
||||
|
||||
import com.r3corda.contracts.asset.sumCashBy
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.NullPublicKey
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.toStringShort
|
||||
import com.r3corda.core.utilities.Emoji
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Legacy version of [CommercialPaper] that includes the full verification logic itself, rather than breaking it
|
||||
* into clauses. This is here just as an example for the contract tutorial.
|
||||
*/
|
||||
|
||||
val CP_LEGACY_PROGRAM_ID = CommercialPaperLegacy()
|
||||
|
||||
// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance.
|
||||
class CommercialPaperLegacy : Contract {
|
||||
// TODO: should reference the content of the legal agreement, not its URI
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper")
|
||||
|
||||
data class State(
|
||||
val issuance: PartyAndReference,
|
||||
override val owner: PublicKey,
|
||||
val faceValue: Amount<Issued<Currency>>,
|
||||
val maturityDate: Instant
|
||||
) : OwnableState, ICommercialPaperState {
|
||||
override val contract = CP_LEGACY_PROGRAM_ID
|
||||
override val participants: List<PublicKey>
|
||||
get() = listOf(owner)
|
||||
|
||||
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: PartyAndReference): ICommercialPaperState = copy(issuance = newIssuance)
|
||||
override fun withFaceValue(newFaceValue: Amount<Issued<Currency>>): ICommercialPaperState = copy(faceValue = newFaceValue)
|
||||
override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate)
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Move: TypeOnlyCommandData(), Commands
|
||||
data class Redeem(val notary: Party) : Commands
|
||||
// We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP.
|
||||
// However, nothing in the platform enforces that uniqueness: it's up to the issuer.
|
||||
data class Issue(val notary: Party) : Commands
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
// Group by everything except owner: any modification to the CP at all is considered changing it fundamentally.
|
||||
val groups = tx.groupStates() { it: State -> it.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.
|
||||
val command = tx.commands.requireSingleCommand<CommercialPaperLegacy.Commands>()
|
||||
// If it's an issue, we can't take notary from inputs, so it must be specified in the command
|
||||
val cmdVal = command.value
|
||||
val timestamp: TimestampCommand? = when (cmdVal) {
|
||||
is Commands.Issue -> tx.getTimestampBy(cmdVal.notary)
|
||||
is Commands.Redeem -> tx.getTimestampBy(cmdVal.notary)
|
||||
else -> null
|
||||
}
|
||||
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
when (command.value) {
|
||||
is Commands.Move -> {
|
||||
val input = inputs.single()
|
||||
requireThat {
|
||||
"the transaction is signed by the owner of the CP" by (input.owner in command.signers)
|
||||
"the state is propagated" by (outputs.size == 1)
|
||||
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
|
||||
// the input ignoring the owner field due to the grouping.
|
||||
}
|
||||
}
|
||||
|
||||
// Redemption of the paper requires movement of on-ledger cash.
|
||||
is Commands.Redeem -> {
|
||||
val input = inputs.single()
|
||||
val received = tx.outputs.sumCashBy(input.owner)
|
||||
val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped")
|
||||
requireThat {
|
||||
"the paper must have matured" by (time >= input.maturityDate)
|
||||
"the received amount equals the face value" by (received == input.faceValue)
|
||||
"the paper must be destroyed" by outputs.isEmpty()
|
||||
"the transaction is signed by the owner of the CP" by (input.owner in command.signers)
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.Issue -> {
|
||||
val output = outputs.single()
|
||||
val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped")
|
||||
requireThat {
|
||||
// Don't allow people to issue commercial paper under other entities identities.
|
||||
"output states are issued by a command signer" by
|
||||
(output.issuance.party.owningKey in command.signers)
|
||||
"output values sum to more than the inputs" by (output.faceValue.quantity > 0)
|
||||
"the maturity date is not in the past" by (time < output.maturityDate)
|
||||
// Don't allow an existing CP state to be replaced by this issuance.
|
||||
// TODO: Consider how to handle the case of mistaken issuances, or other need to patch.
|
||||
"output values sum to more than the inputs" by inputs.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Think about how to evolve contracts over time with new commands.
|
||||
else -> throw IllegalArgumentException("Unrecognised command")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -49,11 +49,24 @@ class KotlinCommercialPaperTest() : ICommercialPaperTestTemplate {
|
||||
override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move()
|
||||
}
|
||||
|
||||
class KotlinCommercialPaperLegacyTest() : ICommercialPaperTestTemplate {
|
||||
override fun getPaper(): ICommercialPaperState = CommercialPaperLegacy.State(
|
||||
issuance = MEGA_CORP.ref(123),
|
||||
owner = MEGA_CORP_PUBKEY,
|
||||
faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123),
|
||||
maturityDate = TEST_TX_TIME + 7.days
|
||||
)
|
||||
|
||||
override fun getIssueCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Issue(notary)
|
||||
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Redeem(notary)
|
||||
override fun getMoveCommand(): CommandData = CommercialPaperLegacy.Commands.Move()
|
||||
}
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class CommercialPaperTestsGeneric {
|
||||
companion object {
|
||||
@Parameterized.Parameters @JvmStatic
|
||||
fun data() = listOf(JavaCommercialPaperTest(), KotlinCommercialPaperTest())
|
||||
fun data() = listOf(JavaCommercialPaperTest(), KotlinCommercialPaperTest(), KotlinCommercialPaperLegacyTest())
|
||||
}
|
||||
|
||||
@Parameterized.Parameter
|
||||
|
@ -76,8 +76,8 @@ inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>
|
||||
}
|
||||
|
||||
// For Java
|
||||
fun List<AuthenticatedObject<CommandData>>.requireSingleCommand(klass: Class<out CommandData>) =
|
||||
filter { klass.isInstance(it.value) }.single()
|
||||
fun <C : CommandData> Collection<AuthenticatedObject<CommandData>>.requireSingleCommand(klass: Class<C>) =
|
||||
mapNotNull { @Suppress("UNCHECKED_CAST") if (klass.isInstance(it.value)) it as AuthenticatedObject<C> else null }.single()
|
||||
|
||||
/** Returns a timestamp that was signed by the given authority, or returns null if missing. */
|
||||
fun List<AuthenticatedObject<CommandData>>.getTimestampBy(timestampingAuthority: Party): TimestampCommand? {
|
||||
|
@ -7,7 +7,9 @@
|
||||
Writing a contract
|
||||
==================
|
||||
|
||||
This tutorial will take you through how the commercial paper contract works.
|
||||
This tutorial will take you through how the commercial paper contract works. This uses a simple contract structure of
|
||||
everything being in one contract class, while most actual contracts in Corda are broken into clauses (which we'll
|
||||
discuss in the next tutorial). You can see the full Kotlin version of this contract in the code as ``CommercialPaperLegacy``.
|
||||
|
||||
The code in this tutorial is available in both Kotlin and Java. You can quickly switch between them to get a feeling
|
||||
for how Kotlin syntax works.
|
||||
|
Loading…
Reference in New Issue
Block a user