Rebuild commercial paper contracts using clauses

This commit is contained in:
Ross Nicoll 2016-07-12 13:47:32 +01:00
parent 0ab4d90bf1
commit c32af56b41
6 changed files with 422 additions and 134 deletions

View File

@ -3,8 +3,8 @@ package com.r3corda.contracts;
import com.google.common.collect.*; import com.google.common.collect.*;
import com.r3corda.contracts.asset.*; import com.r3corda.contracts.asset.*;
import com.r3corda.core.contracts.*; import com.r3corda.core.contracts.*;
import static com.r3corda.core.contracts.ContractsDSL.requireThat;
import com.r3corda.core.contracts.TransactionForContract.*; import com.r3corda.core.contracts.TransactionForContract.*;
import com.r3corda.core.contracts.clauses.*;
import com.r3corda.core.crypto.*; import com.r3corda.core.crypto.*;
import kotlin.Unit; import kotlin.Unit;
import org.jetbrains.annotations.*; import org.jetbrains.annotations.*;
@ -12,6 +12,7 @@ import org.jetbrains.annotations.*;
import java.security.*; import java.security.*;
import java.time.*; import java.time.*;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import static com.r3corda.core.contracts.ContractsDSL.*; import static com.r3corda.core.contracts.ContractsDSL.*;
import static kotlin.collections.CollectionsKt.*; 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 * 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. * 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 SecureHash JCP_PROGRAM_ID = SecureHash.sha256("java commercial paper (this should be a bytecode hash)");
public static Contract JCP_PROGRAM_ID = new JavaCommercialPaper(); public static Contract JCP_PROGRAM_ID = new JavaCommercialPaper();
@ -118,93 +119,108 @@ public class JavaCommercialPaper implements Contract {
} }
} }
public static class Commands implements CommandData { public interface Clause {
public static class Move extends Commands { abstract class AbstractGroup implements GroupClause<State, State> {
@NotNull
@Override @Override
public boolean equals(Object obj) { public MatchBehaviour getIfNotMatched() {
return obj instanceof Move; return MatchBehaviour.CONTINUE;
}
}
public static class Redeem extends Commands {
private final Party notary;
public Redeem(Party setNotary) {
this.notary = setNotary;
} }
@NotNull
@Override @Override
public boolean equals(Object obj) { public MatchBehaviour getIfMatched() {
return obj instanceof Redeem; return MatchBehaviour.END;
} }
} }
public static class Issue extends Commands { class Group extends GroupClauseVerifier<State, State> {
private final Party notary; @NotNull
public Issue(Party setNotary) {
this.notary = setNotary;
}
@Override @Override
public boolean equals(Object obj) { public MatchBehaviour getIfMatched() {
return obj instanceof Issue; return MatchBehaviour.END;
}
}
} }
@NotNull
@Override @Override
public void verify(@NotNull TransactionForContract tx) { public MatchBehaviour getIfNotMatched() {
// There are three possible things that can be done with CP. return MatchBehaviour.ERROR;
// 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, State>> groups = tx.groupStates(State.class, State::withoutOwner); @NotNull
@Override
public List<GroupClause<State, State>> getClauses() {
final List<GroupClause<State, State>> clauses = new ArrayList<>();
// Find the command that instructs us what to do and check there's exactly one. clauses.add(new Clause.Redeem());
clauses.add(new Clause.Move());
clauses.add(new Clause.Issue());
AuthenticatedObject<CommandData> cmd = requireSingleCommand(tx.getCommands(), JavaCommercialPaper.Commands.class); return clauses;
}
for (InOutGroup<State, State> group : groups) { @NotNull
List<State> inputs = group.getInputs(); @Override
List<State> outputs = group.getOutputs(); public List<InOutGroup<State, State>> extractGroups(@NotNull TransactionForContract tx) {
return tx.groupStates(State.class, State::withoutOwner);
}
}
// For now do not allow multiple pieces of CP to trade in a single transaction. class Move extends AbstractGroup {
if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Issue) { @NotNull
Commands.Issue issueCommand = (Commands.Issue) cmd.getValue(); @Override
State output = single(outputs); public Set<Class<? extends CommandData>> getRequiredCommands() {
TimestampCommand timestampCommand = tx.getTimestampBy(issueCommand.notary); return Collections.singleton(Commands.Move.class);
Instant time = null == timestampCommand }
? null
: timestampCommand.getBefore();
requireThat(require -> { @NotNull
require.by("output values sum to more than the inputs", inputs.isEmpty()); @Override
require.by("output values sum to more than the inputs", output.faceValue.getQuantity() > 0); public Set<CommandData> verify(@NotNull TransactionForContract tx,
require.by("must be timestamped", timestampCommand != null); @NotNull List<? extends State> inputs,
require.by("the maturity date is not in the past", time != null && time.isBefore(output.getMaturityDate())); @NotNull List<? extends State> outputs,
require.by("output states are issued by a command signer", cmd.getSigners().contains(output.issuance.getParty().getOwningKey())); @NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
return Unit.INSTANCE; @NotNull State token) {
}); AuthenticatedObject<Commands.Move> cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class);
} 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 // There should be only a single input due to aggregation above
State input = single(inputs); State input = single(inputs);
requireThat(require -> { if (!cmd.getSigners().contains(input.getOwner()))
require.by("the transaction is signed by the owner of the CP", cmd.getSigners().contains(input.getOwner())); throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP");
return Unit.INSTANCE;
});
if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Move) { // Check the output CP state is the same as the input state, ignoring the owner field.
requireThat(require -> { if (outputs.size() != 1) {
require.by("the state is propagated", outputs.size() == 1); throw new IllegalStateException("the state is propagated");
return Unit.INSTANCE; }
});
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to // 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. // the input ignoring the owner field due to the grouping.
} else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) { return Collections.singleton(cmd.getValue());
TimestampCommand timestampCommand = tx.getTimestampBy(((Commands.Redeem) cmd.getValue()).notary); }
}
class Redeem extends AbstractGroup {
@NotNull
@Override
public Set<Class<? extends CommandData>> getRequiredCommands() {
return Collections.singleton(Commands.Redeem.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.Redeem> cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.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");
Party notary = cmd.getValue().notary;
TimestampCommand timestampCommand = tx.getTimestampBy(notary);
Instant time = null == timestampCommand Instant time = null == timestampCommand
? null ? null
: timestampCommand.getBefore(); : timestampCommand.getBefore();
@ -219,9 +235,99 @@ public class JavaCommercialPaper implements Contract {
require.by("the paper must be destroyed", outputs.isEmpty()); require.by("the paper must be destroyed", outputs.isEmpty());
return Unit.INSTANCE; 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);
Party notary = cmd.getValue().notary;
TimestampCommand timestampCommand = tx.getTimestampBy(notary);
Instant time = null == timestampCommand
? null
: timestampCommand.getBefore();
requireThat(require -> {
require.by("output values sum to more than the inputs", inputs.isEmpty());
require.by("output values sum to more than the inputs", output.faceValue.getQuantity() > 0);
require.by("must be timestamped", timestampCommand != null);
require.by("the maturity date is not in the past", time != null && time.isBefore(output.getMaturityDate()));
require.by("output states are issued by a command signer", cmd.getSigners().contains(output.issuance.getParty().getOwningKey()));
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 @NotNull

View File

@ -3,11 +3,14 @@ package com.r3corda.contracts
import com.r3corda.contracts.asset.Cash import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.InsufficientBalanceException import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.contracts.asset.sumCashBy import com.r3corda.contracts.asset.sumCashBy
import com.r3corda.contracts.clause.AbstractIssue
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.clauses.*
import com.r3corda.core.crypto.NullPublicKey import com.r3corda.core.crypto.NullPublicKey
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.random63BitValue
import com.r3corda.core.utilities.Emoji import com.r3corda.core.utilities.Emoji
import java.security.PublicKey import java.security.PublicKey
import java.time.Instant import java.time.Instant
@ -38,10 +41,21 @@ import java.util.*
val CP_PROGRAM_ID = CommercialPaper() val CP_PROGRAM_ID = CommercialPaper()
// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance. // 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 // 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") 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( data class State(
val issuance: PartyAndReference, val issuance: PartyAndReference,
override val owner: PublicKey, override val owner: PublicKey,
@ -52,7 +66,9 @@ class CommercialPaper : Contract {
override val participants: List<PublicKey> override val participants: List<PublicKey>
get() = listOf(owner) 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 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()})" override fun toString() = "${Emoji.newspaper}CommercialPaper(of $faceValue redeemable on $maturityDate by '$issuance', owned by ${owner.toStringShort()})"
@ -64,32 +80,62 @@ class CommercialPaper : Contract {
override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate) override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate)
} }
interface Commands : CommandData { interface Clauses {
class Move: TypeOnlyCommandData(), Commands class Group : GroupClauseVerifier<State, Issued<Terms>>() {
data class Redeem(val notary: Party) : Commands override val ifNotMatched: MatchBehaviour
// We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP. get() = MatchBehaviour.ERROR
// However, nothing in the platform enforces that uniqueness: it's up to the issuer. override val ifMatched: MatchBehaviour
data class Issue(val notary: Party) : Commands get() = MatchBehaviour.END
override val clauses: List<GroupClause<State, Issued<Terms>>>
get() = listOf(
Redeem(),
Move(),
Issue())
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
= tx.groupStates<State, Issued<Terms>> { it.token }
} }
override fun verify(tx: TransactionForContract) { abstract class AbstractGroupClause: GroupClause<State, Issued<Terms>> {
// Group by everything except owner: any modification to the CP at all is considered changing it fundamentally. override val ifNotMatched: MatchBehaviour
val groups = tx.groupStates() { it: State -> it.withoutOwner() } get() = MatchBehaviour.CONTINUE
override val ifMatched: MatchBehaviour
get() = MatchBehaviour.END
}
// There are two possible things that can be done with this CP. The first is trading it. The second is redeeming class Issue : AbstractIssue<State, Terms>(
// it for cash on or after the maturity date. { map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() },
val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>() { token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Issue::class.java)
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 // 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? = tx.getTimestampBy(command.value.notary)
val timestamp: TimestampCommand? = when (cmdVal) { val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped")
is Commands.Issue -> tx.getTimestampBy(cmdVal.notary)
is Commands.Redeem -> tx.getTimestampBy(cmdVal.notary) require(outputs.all { time < it.maturityDate }) { "maturity date is not in the past" }
else -> null
return consumedCommands
}
} }
for ((inputs, outputs, key) in groups) { class Move: AbstractGroupClause() {
when (command.value) { override val requiredCommands: Set<Class<out CommandData>>
is Commands.Move -> { 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() val input = inputs.single()
requireThat { requireThat {
"the transaction is signed by the owner of the CP" by (input.owner in command.signers) "the transaction is signed by the owner of the CP" by (input.owner in command.signers)
@ -97,10 +143,25 @@ class CommercialPaper : Contract {
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to // 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. // the input ignoring the owner field due to the grouping.
} }
return setOf(command.value)
}
} }
// Redemption of the paper requires movement of on-ledger cash. class Redeem(): AbstractGroupClause() {
is Commands.Redeem -> { 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 input = inputs.single()
val received = tx.outputs.sumCashBy(input.owner) val received = tx.outputs.sumCashBy(input.owner)
val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped") val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped")
@ -110,27 +171,17 @@ class CommercialPaper : Contract {
"the paper must be destroyed" by outputs.isEmpty() "the paper must be destroyed" by outputs.isEmpty()
"the transaction is signed by the owner of the CP" by (input.owner in command.signers) "the transaction is signed by the owner of the CP" by (input.owner in command.signers)
} }
return setOf(command.value)
} }
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. interface Commands : CommandData {
else -> throw IllegalArgumentException("Unrecognised command") class Move : TypeOnlyCommandData(), Commands
} data class Redeem(val notary: Party) : Commands
} data class Issue(val notary: Party, override val nonce: Long = random63BitValue()) : IssueCommand, Commands
} }
/** /**

View File

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

View File

@ -49,11 +49,24 @@ class KotlinCommercialPaperTest() : ICommercialPaperTestTemplate {
override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move() 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) @RunWith(Parameterized::class)
class CommercialPaperTestsGeneric { class CommercialPaperTestsGeneric {
companion object { companion object {
@Parameterized.Parameters @JvmStatic @Parameterized.Parameters @JvmStatic
fun data() = listOf(JavaCommercialPaperTest(), KotlinCommercialPaperTest()) fun data() = listOf(JavaCommercialPaperTest(), KotlinCommercialPaperTest(), KotlinCommercialPaperLegacyTest())
} }
@Parameterized.Parameter @Parameterized.Parameter

View File

@ -76,8 +76,8 @@ inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>
} }
// For Java // For Java
fun List<AuthenticatedObject<CommandData>>.requireSingleCommand(klass: Class<out CommandData>) = fun <C : CommandData> Collection<AuthenticatedObject<CommandData>>.requireSingleCommand(klass: Class<C>) =
filter { klass.isInstance(it.value) }.single() 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. */ /** Returns a timestamp that was signed by the given authority, or returns null if missing. */
fun List<AuthenticatedObject<CommandData>>.getTimestampBy(timestampingAuthority: Party): TimestampCommand? { fun List<AuthenticatedObject<CommandData>>.getTimestampBy(timestampingAuthority: Party): TimestampCommand? {

View File

@ -7,7 +7,9 @@
Writing a contract 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 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. for how Kotlin syntax works.