Merged in rnicoll-clauses-commercial-paper (pull request #227)

Rebuild commercial paper contracts using clauses
This commit is contained in:
Ross Nicoll 2016-07-13 17:04:30 +01:00
commit 1ec1642080
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.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() {

View File

@ -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
}
/**

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()
}
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

View File

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

View File

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