mirror of
https://github.com/corda/corda.git
synced 2025-01-21 03:55:00 +00:00
Removing clauses (#1195)
* Removing clauses * Removing clauses from JavaCommercialPaper * Addressing review comments
This commit is contained in:
parent
28610868c4
commit
0b33214fea
@ -2,6 +2,7 @@
|
||||
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import java.math.BigDecimal
|
||||
import java.security.PublicKey
|
||||
@ -71,7 +72,7 @@ inline fun <R> requireThat(body: Requirements.() -> R) = Requirements.body()
|
||||
|
||||
/** Filters the command list by type, party and public key all at once. */
|
||||
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.select(signer: PublicKey? = null,
|
||||
party: Party? = null) =
|
||||
party: AbstractParty? = null) =
|
||||
filter { it.value is T }.
|
||||
filter { if (signer == null) true else signer in it.signers }.
|
||||
filter { if (party == null) true else party in it.signingParties }.
|
||||
|
@ -7,10 +7,6 @@ import kotlin.Pair;
|
||||
import kotlin.Unit;
|
||||
import net.corda.contracts.asset.CashKt;
|
||||
import net.corda.core.contracts.*;
|
||||
import net.corda.core.contracts.clauses.AnyOf;
|
||||
import net.corda.core.contracts.clauses.Clause;
|
||||
import net.corda.core.contracts.clauses.ClauseVerifier;
|
||||
import net.corda.core.contracts.clauses.GroupClauseVerifier;
|
||||
import net.corda.core.crypto.SecureHash;
|
||||
import net.corda.core.crypto.testing.NullPublicKey;
|
||||
import net.corda.core.identity.AbstractParty;
|
||||
@ -23,10 +19,8 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Currency;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
||||
@ -113,7 +107,8 @@ public class JavaCommercialPaper implements Contract {
|
||||
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;
|
||||
if (maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null) return false;
|
||||
if (maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -137,138 +132,6 @@ public class JavaCommercialPaper implements Contract {
|
||||
}
|
||||
}
|
||||
|
||||
public interface Clauses {
|
||||
@SuppressWarnings("unused")
|
||||
class Group extends GroupClauseVerifier<State, Commands, State> {
|
||||
// This complains because we're passing generic types into a varargs, but it is valid so we suppress the
|
||||
// warning.
|
||||
@SuppressWarnings("unchecked")
|
||||
Group() {
|
||||
super(new AnyOf<>(
|
||||
new Clauses.Redeem(),
|
||||
new Clauses.Move(),
|
||||
new Clauses.Issue()
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<LedgerTransaction.InOutGroup<State, State>> groupStates(@NotNull LedgerTransaction tx) {
|
||||
return tx.groupStates(State.class, State::withoutOwner);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
class Move extends Clause<State, Commands, State> {
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||
return Collections.singleton(Commands.Move.class);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<Commands> verify(@NotNull LedgerTransaction tx,
|
||||
@NotNull List<? extends State> inputs,
|
||||
@NotNull List<? extends State> outputs,
|
||||
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
|
||||
State groupingKey) {
|
||||
AuthenticatedObject<Commands.Move> cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class);
|
||||
// There should be only a single input due to aggregation above
|
||||
State input = Iterables.getOnlyElement(inputs);
|
||||
|
||||
if (!cmd.getSigners().contains(input.getOwner().getOwningKey()))
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
class Redeem extends Clause<State, Commands, State> {
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||
return Collections.singleton(Commands.Redeem.class);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<Commands> verify(@NotNull LedgerTransaction tx,
|
||||
@NotNull List<? extends State> inputs,
|
||||
@NotNull List<? extends State> outputs,
|
||||
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
|
||||
State groupingKey) {
|
||||
AuthenticatedObject<Commands.Redeem> cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class);
|
||||
|
||||
// There should be only a single input due to aggregation above
|
||||
State input = Iterables.getOnlyElement(inputs);
|
||||
|
||||
if (!cmd.getSigners().contains(input.getOwner().getOwningKey()))
|
||||
throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP");
|
||||
|
||||
TimeWindow timeWindow = tx.getTimeWindow();
|
||||
Instant time = null == timeWindow
|
||||
? null
|
||||
: timeWindow.getUntilTime();
|
||||
Amount<Issued<Currency>> received = CashKt.sumCashBy(tx.getOutputs().stream().map(TransactionState::getData).collect(Collectors.toList()), input.getOwner());
|
||||
|
||||
requireThat(require -> {
|
||||
require.using("must be timestamped", timeWindow != null);
|
||||
require.using("received amount equals the face value: "
|
||||
+ received + " vs " + input.getFaceValue(), received.equals(input.getFaceValue()));
|
||||
require.using("the paper must have matured", time != null && !time.isBefore(input.getMaturityDate()));
|
||||
require.using("the received amount equals the face value", input.getFaceValue().equals(received));
|
||||
require.using("the paper must be destroyed", outputs.isEmpty());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
return Collections.singleton(cmd.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
class Issue extends Clause<State, Commands, State> {
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||
return Collections.singleton(Commands.Issue.class);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<Commands> verify(@NotNull LedgerTransaction tx,
|
||||
@NotNull List<? extends State> inputs,
|
||||
@NotNull List<? extends State> outputs,
|
||||
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
|
||||
State groupingKey) {
|
||||
AuthenticatedObject<Commands.Issue> cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class);
|
||||
State output = Iterables.getOnlyElement(outputs);
|
||||
TimeWindow timeWindowCommand = tx.getTimeWindow();
|
||||
Instant time = null == timeWindowCommand
|
||||
? null
|
||||
: timeWindowCommand.getUntilTime();
|
||||
|
||||
requireThat(require -> {
|
||||
require.using("output values sum to more than the inputs", inputs.isEmpty());
|
||||
require.using("output values sum to more than the inputs", output.faceValue.getQuantity() > 0);
|
||||
require.using("must be timestamped", timeWindowCommand != null);
|
||||
require.using("the maturity date is not in the past", time != null && time.isBefore(output.getMaturityDate()));
|
||||
require.using("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
|
||||
@ -303,7 +166,75 @@ public class JavaCommercialPaper implements Contract {
|
||||
|
||||
@Override
|
||||
public void verify(@NotNull LedgerTransaction tx) throws IllegalArgumentException {
|
||||
ClauseVerifier.verifyClause(tx, new Clauses.Group(), extractCommands(tx));
|
||||
|
||||
// Group by everything except owner: any modification to the CP at all is considered changing it fundamentally.
|
||||
final List<LedgerTransaction.InOutGroup<State, State>> groups = tx.groupStates(State.class, State::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.
|
||||
final List<AuthenticatedObject<CommandData>> commands = tx.getCommands().stream().filter(
|
||||
it -> {
|
||||
return it.getValue() instanceof Commands;
|
||||
}
|
||||
).collect(Collectors.toList());
|
||||
final AuthenticatedObject<CommandData> command = Iterables.getOnlyElement(commands);
|
||||
final TimeWindow timeWindow = tx.getTimeWindow();
|
||||
|
||||
for (final LedgerTransaction.InOutGroup<State, State> group : groups) {
|
||||
final List<State> inputs = group.getInputs();
|
||||
final List<State> outputs = group.getOutputs();
|
||||
if (command.getValue() instanceof Commands.Move) {
|
||||
final AuthenticatedObject<Commands.Move> cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class);
|
||||
// There should be only a single input due to aggregation above
|
||||
final State input = Iterables.getOnlyElement(inputs);
|
||||
|
||||
if (!cmd.getSigners().contains(input.getOwner().getOwningKey()))
|
||||
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");
|
||||
}
|
||||
} else if (command.getValue() instanceof Commands.Redeem) {
|
||||
final AuthenticatedObject<Commands.Redeem> cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class);
|
||||
|
||||
// There should be only a single input due to aggregation above
|
||||
final State input = Iterables.getOnlyElement(inputs);
|
||||
|
||||
if (!cmd.getSigners().contains(input.getOwner().getOwningKey()))
|
||||
throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP");
|
||||
|
||||
final Instant time = null == timeWindow
|
||||
? null
|
||||
: timeWindow.getUntilTime();
|
||||
final Amount<Issued<Currency>> received = CashKt.sumCashBy(tx.getOutputs().stream().map(TransactionState::getData).collect(Collectors.toList()), input.getOwner());
|
||||
|
||||
requireThat(require -> {
|
||||
require.using("must be timestamped", timeWindow != null);
|
||||
require.using("received amount equals the face value: "
|
||||
+ received + " vs " + input.getFaceValue(), received.equals(input.getFaceValue()));
|
||||
require.using("the paper must have matured", time != null && !time.isBefore(input.getMaturityDate()));
|
||||
require.using("the received amount equals the face value", input.getFaceValue().equals(received));
|
||||
require.using("the paper must be destroyed", outputs.isEmpty());
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
} else if (command.getValue() instanceof Commands.Issue) {
|
||||
final AuthenticatedObject<Commands.Issue> cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class);
|
||||
final State output = Iterables.getOnlyElement(outputs);
|
||||
final Instant time = null == timeWindow
|
||||
? null
|
||||
: timeWindow.getUntilTime();
|
||||
|
||||
requireThat(require -> {
|
||||
require.using("output values sum to more than the inputs", inputs.isEmpty());
|
||||
require.using("output values sum to more than the inputs", output.faceValue.getQuantity() > 0);
|
||||
require.using("must be timestamped", timeWindow != null);
|
||||
require.using("the maturity date is not in the past", time != null && time.isBefore(output.getMaturityDate()));
|
||||
require.using("output states are issued by a command signer", cmd.getSigners().contains(output.issuance.getParty().getOwningKey()));
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
@ -2,14 +2,9 @@ package net.corda.contracts
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.sumCashBy
|
||||
import net.corda.contracts.clause.AbstractIssue
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.clauses.AnyOf
|
||||
import net.corda.core.contracts.clauses.Clause
|
||||
import net.corda.core.contracts.clauses.GroupClauseVerifier
|
||||
import net.corda.core.contracts.clauses.verifyClause
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.crypto.testing.NULL_PARTY
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
@ -45,7 +40,6 @@ import java.util.*
|
||||
* which may need to be tracked. That, in turn, requires validation logic (there is a bean validator that knows how
|
||||
* to do this in the Apache BVal project).
|
||||
*/
|
||||
|
||||
val CP_PROGRAM_ID = CommercialPaper()
|
||||
|
||||
// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance.
|
||||
@ -53,13 +47,6 @@ class CommercialPaper : 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 Terms(
|
||||
val asset: Issued<Currency>,
|
||||
val maturityDate: Instant
|
||||
)
|
||||
|
||||
override fun verify(tx: LedgerTransaction) = verifyClause(tx, Clauses.Group(), tx.commands.select<Commands>())
|
||||
|
||||
data class State(
|
||||
val issuance: PartyAndReference,
|
||||
override val owner: AbstractParty,
|
||||
@ -67,13 +54,10 @@ class CommercialPaper : Contract {
|
||||
val maturityDate: Instant
|
||||
) : OwnableState, QueryableState, ICommercialPaperState {
|
||||
override val contract = CP_PROGRAM_ID
|
||||
override val participants: List<AbstractParty>
|
||||
get() = listOf(owner)
|
||||
|
||||
val token: Issued<Terms>
|
||||
get() = Issued(issuance, Terms(faceValue.token, maturityDate))
|
||||
override val participants = listOf(owner)
|
||||
|
||||
override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner))
|
||||
fun withoutOwner() = copy(owner = NULL_PARTY)
|
||||
override fun toString() = "${Emoji.newspaper}CommercialPaper(of $faceValue redeemable on $maturityDate by '$issuance', owned by $owner)"
|
||||
|
||||
// Although kotlin is smart enough not to need these, as we are using the ICommercialPaperState, we need to declare them explicitly for use later,
|
||||
@ -82,7 +66,6 @@ class CommercialPaper : Contract {
|
||||
override fun withFaceValue(newFaceValue: Amount<Issued<Currency>>): ICommercialPaperState = copy(faceValue = newFaceValue)
|
||||
override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate)
|
||||
|
||||
// DOCSTART VaultIndexedQueryCriteria
|
||||
/** Object Relational Mapping support. */
|
||||
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(CommercialPaperSchemaV1)
|
||||
/** Additional used schemas would be added here (eg. CommercialPaperV2, ...) */
|
||||
@ -100,97 +83,77 @@ class CommercialPaper : Contract {
|
||||
faceValueIssuerParty = this.faceValue.token.issuer.party.owningKey.toBase58String(),
|
||||
faceValueIssuerRef = this.faceValue.token.issuer.reference.bytes
|
||||
)
|
||||
/** Additional schema mappings would be added here (eg. CommercialPaperV2, ...) */
|
||||
/** Additional schema mappings would be added here (eg. CommercialPaperV2, ...) */
|
||||
else -> throw IllegalArgumentException("Unrecognised schema $schema")
|
||||
}
|
||||
}
|
||||
// DOCEND VaultIndexedQueryCriteria
|
||||
}
|
||||
|
||||
interface Clauses {
|
||||
class Group : GroupClauseVerifier<State, Commands, Issued<Terms>>(
|
||||
AnyOf(
|
||||
Redeem(),
|
||||
Move(),
|
||||
Issue())) {
|
||||
override fun groupStates(tx: LedgerTransaction): List<LedgerTransaction.InOutGroup<State, Issued<Terms>>>
|
||||
= tx.groupStates<State, Issued<Terms>> { it.token }
|
||||
}
|
||||
|
||||
class Issue : AbstractIssue<State, Commands, 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>> = setOf(Commands.Issue::class.java)
|
||||
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Issued<Terms>?): Set<Commands> {
|
||||
val consumedCommands = super.verify(tx, inputs, outputs, commands, groupingKey)
|
||||
commands.requireSingleCommand<Commands.Issue>()
|
||||
val timeWindow = tx.timeWindow
|
||||
val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances must have a time-window")
|
||||
|
||||
require(outputs.all { time < it.maturityDate }) { "maturity date is not in the past" }
|
||||
|
||||
return consumedCommands
|
||||
}
|
||||
}
|
||||
|
||||
class Move : Clause<State, Commands, Issued<Terms>>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Move::class.java)
|
||||
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Issued<Terms>?): Set<Commands> {
|
||||
val command = commands.requireSingleCommand<Commands.Move>()
|
||||
val input = inputs.single()
|
||||
requireThat {
|
||||
"the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers)
|
||||
"the state is propagated" using (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 : Clause<State, Commands, Issued<Terms>>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Redeem::class.java)
|
||||
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Issued<Terms>?): Set<Commands> {
|
||||
// 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>()
|
||||
val timeWindow = tx.timeWindow
|
||||
|
||||
val input = inputs.single()
|
||||
val received = tx.outputStates.sumCashBy(input.owner)
|
||||
val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must have a time-window")
|
||||
requireThat {
|
||||
"the paper must have matured" using (time >= input.maturityDate)
|
||||
"the received amount equals the face value" using (received == input.faceValue)
|
||||
"the paper must be destroyed" using outputs.isEmpty()
|
||||
"the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers)
|
||||
}
|
||||
|
||||
return setOf(command.value)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands
|
||||
class Move : TypeOnlyCommandData(), Commands
|
||||
|
||||
class Redeem : TypeOnlyCommandData(), Commands
|
||||
data class Issue(override val nonce: Long = random63BitValue()) : IssueCommand, 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.
|
||||
class Issue : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
// Group by everything except owner: any modification to the CP at all is considered changing it fundamentally.
|
||||
val groups = tx.groupStates(State::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>()
|
||||
val timeWindow: TimeWindow? = tx.timeWindow
|
||||
|
||||
// Suppress compiler warning as 'key' is an unused variable when destructuring 'groups'.
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
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" using (input.owner.owningKey in command.signers)
|
||||
"the state is propagated" using (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.
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.Redeem -> {
|
||||
// Redemption of the paper requires movement of on-ledger cash.
|
||||
val input = inputs.single()
|
||||
val received = tx.outputStates.sumCashBy(input.owner)
|
||||
val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must have a time-window")
|
||||
requireThat {
|
||||
"the paper must have matured" using (time >= input.maturityDate)
|
||||
"the received amount equals the face value" using (received == input.faceValue)
|
||||
"the paper must be destroyed" using outputs.isEmpty()
|
||||
"the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers)
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.Issue -> {
|
||||
val output = outputs.single()
|
||||
val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances have a time-window")
|
||||
requireThat {
|
||||
// Don't allow people to issue commercial paper under other entities identities.
|
||||
"output states are issued by a command signer" using
|
||||
(output.issuance.party.owningKey in command.signers)
|
||||
"output values sum to more than the inputs" using (output.faceValue.quantity > 0)
|
||||
"the maturity date is not in the past" using (time < output.maturityDate)
|
||||
// Don't allow an existing CP state to be replaced by this issuance.
|
||||
// TODO: this has a weird/incorrect assertion string because it doesn't quite match the logic in the clause version.
|
||||
// TODO: Consider how to handle the case of mistaken issuances, or other need to patch.
|
||||
"output values sum to more than the inputs" using inputs.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Think about how to evolve contracts over time with new commands.
|
||||
else -> throw IllegalArgumentException("Unrecognised command")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -198,9 +161,10 @@ class CommercialPaper : Contract {
|
||||
* an existing transaction because you aren't able to issue multiple pieces of CP in a single transaction
|
||||
* at the moment: this restriction is not fundamental and may be lifted later.
|
||||
*/
|
||||
fun generateIssue(issuance: PartyAndReference, faceValue: Amount<Issued<Currency>>, maturityDate: Instant, notary: Party): TransactionBuilder {
|
||||
val state = TransactionState(State(issuance, issuance.party, faceValue, maturityDate), notary)
|
||||
return TransactionBuilder(notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey))
|
||||
fun generateIssue(issuance: PartyAndReference, faceValue: Amount<Issued<Currency>>, maturityDate: Instant,
|
||||
notary: Party): TransactionBuilder {
|
||||
val state = State(issuance, issuance.party, faceValue, maturityDate)
|
||||
return TransactionBuilder(notary = notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -208,7 +172,7 @@ class CommercialPaper : Contract {
|
||||
*/
|
||||
fun generateMove(tx: TransactionBuilder, paper: StateAndRef<State>, newOwner: AbstractParty) {
|
||||
tx.addInputState(paper)
|
||||
tx.addOutputState(TransactionState(paper.state.data.copy(owner = newOwner), paper.state.notary))
|
||||
tx.addOutputState(paper.state.data.withOwner(newOwner))
|
||||
tx.addCommand(Commands.Move(), paper.state.data.owner.owningKey)
|
||||
}
|
||||
|
||||
@ -223,15 +187,12 @@ class CommercialPaper : Contract {
|
||||
@Suspendable
|
||||
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: VaultService) {
|
||||
// Add the cash movement using the states in our vault.
|
||||
val amount = paper.state.data.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) }
|
||||
vault.generateSpend(tx, amount, paper.state.data.owner)
|
||||
vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner)
|
||||
tx.addInputState(paper)
|
||||
tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.data.owner.owningKey)
|
||||
tx.addCommand(Commands.Redeem(), paper.state.data.owner.owningKey)
|
||||
}
|
||||
}
|
||||
|
||||
infix fun CommercialPaper.State.`owned by`(owner: AbstractParty) = copy(owner = owner)
|
||||
infix fun CommercialPaper.State.`with notary`(notary: Party) = TransactionState(this, notary)
|
||||
infix fun ICommercialPaperState.`owned by`(newOwner: AbstractParty) = withOwner(newOwner)
|
||||
|
||||
|
||||
infix fun ICommercialPaperState.`owned by`(newOwner: AbstractParty) = withOwner(newOwner)
|
@ -1,136 +0,0 @@
|
||||
package net.corda.contracts
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.sumCashBy
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.testing.NULL_PARTY
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
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: AbstractParty,
|
||||
val faceValue: Amount<Issued<Currency>>,
|
||||
val maturityDate: Instant
|
||||
) : OwnableState, ICommercialPaperState {
|
||||
override val contract = CP_LEGACY_PROGRAM_ID
|
||||
override val participants = listOf(owner)
|
||||
|
||||
fun withoutOwner() = copy(owner = NULL_PARTY)
|
||||
override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner))
|
||||
override fun toString() = "${Emoji.newspaper}CommercialPaper(of $faceValue redeemable on $maturityDate by '$issuance', owned by $owner)"
|
||||
|
||||
// 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: AbstractParty): ICommercialPaperState = copy(owner = newOwner)
|
||||
|
||||
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
|
||||
|
||||
class Redeem : TypeOnlyCommandData(), 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.
|
||||
class Issue : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
// Group by everything except owner: any modification to the CP at all is considered changing it fundamentally.
|
||||
val groups = tx.groupStates(State::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>()
|
||||
val timeWindow: TimeWindow? = tx.timeWindow
|
||||
|
||||
// Suppress compiler warning as 'key' is an unused variable when destructuring 'groups'.
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
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" using (input.owner.owningKey in command.signers)
|
||||
"the state is propagated" using (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.
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.Redeem -> {
|
||||
// Redemption of the paper requires movement of on-ledger cash.
|
||||
val input = inputs.single()
|
||||
val received = tx.outputStates.sumCashBy(input.owner)
|
||||
val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must have a time-window")
|
||||
requireThat {
|
||||
"the paper must have matured" using (time >= input.maturityDate)
|
||||
"the received amount equals the face value" using (received == input.faceValue)
|
||||
"the paper must be destroyed" using outputs.isEmpty()
|
||||
"the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers)
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.Issue -> {
|
||||
val output = outputs.single()
|
||||
val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances have a time-window")
|
||||
requireThat {
|
||||
// Don't allow people to issue commercial paper under other entities identities.
|
||||
"output states are issued by a command signer" using
|
||||
(output.issuance.party.owningKey in command.signers)
|
||||
"output values sum to more than the inputs" using (output.faceValue.quantity > 0)
|
||||
"the maturity date is not in the past" using (time < output.maturityDate)
|
||||
// Don't allow an existing CP state to be replaced by this issuance.
|
||||
// TODO: this has a weird/incorrect assertion string because it doesn't quite match the logic in the clause version.
|
||||
// TODO: Consider how to handle the case of mistaken issuances, or other need to patch.
|
||||
"output values sum to more than the inputs" using inputs.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Think about how to evolve contracts over time with new commands.
|
||||
else -> throw IllegalArgumentException("Unrecognised command")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun generateIssue(issuance: PartyAndReference, faceValue: Amount<Issued<Currency>>, maturityDate: Instant,
|
||||
notary: Party): TransactionBuilder {
|
||||
val state = State(issuance, issuance.party, faceValue, maturityDate)
|
||||
return TransactionBuilder(notary = notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey))
|
||||
}
|
||||
|
||||
fun generateMove(tx: TransactionBuilder, paper: StateAndRef<State>, newOwner: AbstractParty) {
|
||||
tx.addInputState(paper)
|
||||
tx.addOutputState(paper.state.data.withOwner(newOwner))
|
||||
tx.addCommand(Command(Commands.Move(), paper.state.data.owner.owningKey))
|
||||
}
|
||||
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
@Suspendable
|
||||
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: VaultService) {
|
||||
// Add the cash movement using the states in our vault.
|
||||
vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner)
|
||||
tx.addInputState(paper)
|
||||
tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner.owningKey))
|
||||
}
|
||||
}
|
@ -1,13 +1,6 @@
|
||||
package net.corda.contracts.asset
|
||||
|
||||
import net.corda.contracts.clause.AbstractConserveAmount
|
||||
import net.corda.contracts.clause.AbstractIssue
|
||||
import net.corda.contracts.clause.NoZeroSizedOutputs
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.clauses.AllOf
|
||||
import net.corda.core.contracts.clauses.FirstOf
|
||||
import net.corda.core.contracts.clauses.GroupClauseVerifier
|
||||
import net.corda.core.contracts.clauses.verifyClause
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.entropyToKeyPair
|
||||
import net.corda.core.crypto.newSecureRandom
|
||||
@ -15,16 +8,16 @@ import net.corda.core.crypto.testing.NULL_PARTY
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.QueryableState
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.schemas.CashSchemaV1
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.math.BigInteger
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -61,33 +54,11 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
*/
|
||||
// DOCSTART 2
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html")
|
||||
|
||||
// DOCEND 2
|
||||
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Cash.Commands>>
|
||||
= commands.select<Cash.Commands>()
|
||||
|
||||
interface Clauses {
|
||||
class Group : GroupClauseVerifier<State, Commands, Issued<Currency>>(AllOf<State, Commands, Issued<Currency>>(
|
||||
NoZeroSizedOutputs<State, Commands, Currency>(),
|
||||
FirstOf<State, Commands, Issued<Currency>>(
|
||||
Issue(),
|
||||
ConserveAmount())
|
||||
)
|
||||
) {
|
||||
override fun groupStates(tx: LedgerTransaction): List<LedgerTransaction.InOutGroup<State, Issued<Currency>>>
|
||||
= tx.groupStates<State, Issued<Currency>> { it.amount.token }
|
||||
}
|
||||
|
||||
class Issue : AbstractIssue<State, Commands, Currency>(
|
||||
sum = { sumCash() },
|
||||
sumOrZero = { sumCashOrZero(it) }
|
||||
) {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class ConserveAmount : AbstractConserveAmount<State, Commands, Currency>()
|
||||
}
|
||||
|
||||
// DOCSTART 1
|
||||
/** A state representing a cash claim against some party. */
|
||||
data class State(
|
||||
@ -120,7 +91,7 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
issuerParty = this.amount.token.issuer.party.owningKey.toBase58String(),
|
||||
issuerRef = this.amount.token.issuer.reference.bytes
|
||||
)
|
||||
/** Additional schema mappings would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */
|
||||
/** Additional schema mappings would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */
|
||||
else -> throw IllegalArgumentException("Unrecognised schema $schema")
|
||||
}
|
||||
}
|
||||
@ -165,7 +136,7 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
|
||||
*/
|
||||
fun generateIssue(tx: TransactionBuilder, amount: Amount<Issued<Currency>>, owner: AbstractParty, notary: Party)
|
||||
= generateIssue(tx, TransactionState(State(amount, owner), notary), generateIssueCommand())
|
||||
= generateIssue(tx, TransactionState(State(amount, owner), notary), generateIssueCommand())
|
||||
|
||||
override fun deriveState(txState: TransactionState<State>, amount: Amount<Issued<Currency>>, owner: AbstractParty)
|
||||
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
|
||||
@ -174,8 +145,73 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
override fun generateIssueCommand() = Commands.Issue()
|
||||
override fun generateMoveCommand() = Commands.Move()
|
||||
|
||||
override fun verify(tx: LedgerTransaction)
|
||||
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
// Each group is a set of input/output states with distinct (reference, currency) attributes. These types
|
||||
// of cash are not fungible and must be kept separated for bookkeeping purposes.
|
||||
val groups = tx.groupStates { it: Cash.State -> it.amount.token }
|
||||
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
// Either inputs or outputs could be empty.
|
||||
val issuer = key.issuer
|
||||
val currency = key.product
|
||||
|
||||
requireThat {
|
||||
"there are no zero sized outputs" using (outputs.none { it.amount.quantity == 0L })
|
||||
}
|
||||
|
||||
val issueCommand = tx.commands.select<Commands.Issue>().firstOrNull()
|
||||
if (issueCommand != null) {
|
||||
verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer)
|
||||
} else {
|
||||
val inputAmount = inputs.sumCashOrNull() ?: throw IllegalArgumentException("there is at least one cash input for this group")
|
||||
val outputAmount = outputs.sumCashOrZero(Issued(issuer, currency))
|
||||
|
||||
// If we want to remove cash from the ledger, that must be signed for by the issuer.
|
||||
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero.
|
||||
val exitKeys: Set<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
|
||||
val exitCommand = tx.commands.select<Commands.Exit>(parties = null, signers = exitKeys).filter { it.value.amount.token == key }.singleOrNull()
|
||||
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, Issued(issuer, currency))
|
||||
|
||||
requireThat {
|
||||
"there are no zero sized inputs" using inputs.none { it.amount.quantity == 0L }
|
||||
"for reference ${issuer.reference} at issuer ${issuer.party} the amounts balance: ${inputAmount.quantity} - ${amountExitingLedger.quantity} != ${outputAmount.quantity}" using
|
||||
(inputAmount == outputAmount + amountExitingLedger)
|
||||
}
|
||||
|
||||
verifyMoveCommand<Commands.Move>(inputs, tx.commands)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyIssueCommand(inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
tx: LedgerTransaction,
|
||||
issueCommand: AuthenticatedObject<Commands.Issue>,
|
||||
currency: Currency,
|
||||
issuer: PartyAndReference) {
|
||||
// If we have an issue command, perform special processing: the group is allowed to have no inputs,
|
||||
// and the output states must have a deposit reference owned by the signer.
|
||||
//
|
||||
// Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must
|
||||
// sum to more than the inputs. An issuance of zero size is not allowed.
|
||||
//
|
||||
// Note that this means literally anyone with access to the network can issue cash claims of arbitrary
|
||||
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some
|
||||
// as-yet-unwritten identity service. See ADP-22 for discussion.
|
||||
|
||||
// The grouping ensures that all outputs have the same deposit reference and currency.
|
||||
val inputAmount = inputs.sumCashOrZero(Issued(issuer, currency))
|
||||
val outputAmount = outputs.sumCash()
|
||||
val cashCommands = tx.commands.select<Commands.Issue>()
|
||||
requireThat {
|
||||
"the issue command has a nonce" using (issueCommand.value.nonce != 0L)
|
||||
// TODO: This doesn't work with the trader demo, so use the underlying key instead
|
||||
// "output states are issued by a command signer" by (issuer.party in issueCommand.signingParties)
|
||||
"output states are issued by a command signer" using (issuer.party.owningKey in issueCommand.signers)
|
||||
"output values sum to more than the inputs" using (outputAmount > inputAmount)
|
||||
"there is only a single issue command" using (cashCommands.count() == 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Small DSL extensions.
|
||||
|
@ -1,13 +1,7 @@
|
||||
package net.corda.contracts.asset
|
||||
|
||||
import net.corda.contracts.Commodity
|
||||
import net.corda.contracts.clause.AbstractConserveAmount
|
||||
import net.corda.contracts.clause.AbstractIssue
|
||||
import net.corda.contracts.clause.NoZeroSizedOutputs
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.clauses.AnyOf
|
||||
import net.corda.core.contracts.clauses.GroupClauseVerifier
|
||||
import net.corda.core.contracts.clauses.verifyClause
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.newSecureRandom
|
||||
import net.corda.core.identity.AbstractParty
|
||||
@ -49,48 +43,6 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, C
|
||||
*/
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html")
|
||||
|
||||
/**
|
||||
* The clauses for this contract are essentially:
|
||||
*
|
||||
* 1. Group all commodity input and output states in a transaction by issued commodity, and then for each group:
|
||||
* a. Check there are no zero sized output states in the group, and throw an error if so.
|
||||
* b. Check for an issuance command, and do standard issuance checks if so, THEN STOP. Otherwise:
|
||||
* c. Check for a move command (required) and an optional exit command, and that input and output totals are correctly
|
||||
* conserved (output = input - exit)
|
||||
*/
|
||||
interface Clauses {
|
||||
/**
|
||||
* Grouping clause to extract input and output states into matched groups and then run a set of clauses over
|
||||
* each group.
|
||||
*/
|
||||
class Group : GroupClauseVerifier<State, Commands, Issued<Commodity>>(AnyOf(
|
||||
NoZeroSizedOutputs<State, Commands, Commodity>(),
|
||||
Issue(),
|
||||
ConserveAmount())) {
|
||||
/**
|
||||
* Group commodity states by issuance definition (issuer and underlying commodity).
|
||||
*/
|
||||
override fun groupStates(tx: LedgerTransaction)
|
||||
= tx.groupStates<State, Issued<Commodity>> { it.amount.token }
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard issue clause, specialised to match the commodity issue command.
|
||||
*/
|
||||
class Issue : AbstractIssue<State, Commands, Commodity>(
|
||||
sum = { sumCommodities() },
|
||||
sumOrZero = { sumCommoditiesOrZero(it) }
|
||||
) {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard clause for conserving the amount from input to output.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class ConserveAmount : AbstractConserveAmount<State, Commands, Commodity>()
|
||||
}
|
||||
|
||||
/** A state representing a commodity claim against some party */
|
||||
data class State(
|
||||
override val amount: Amount<Issued<Commodity>>,
|
||||
@ -138,8 +90,71 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, C
|
||||
data class Exit(override val amount: Amount<Issued<Commodity>>) : Commands, FungibleAsset.Commands.Exit<Commodity>
|
||||
}
|
||||
|
||||
override fun verify(tx: LedgerTransaction)
|
||||
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
// Each group is a set of input/output states with distinct (reference, commodity) attributes. These types
|
||||
// of commodity are not fungible and must be kept separated for bookkeeping purposes.
|
||||
val groups = tx.groupStates { it: CommodityContract.State -> it.amount.token }
|
||||
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
// Either inputs or outputs could be empty.
|
||||
val issuer = key.issuer
|
||||
val commodity = key.product
|
||||
val party = issuer.party
|
||||
|
||||
requireThat {
|
||||
"there are no zero sized outputs" using ( outputs.none { it.amount.quantity == 0L } )
|
||||
}
|
||||
|
||||
val issueCommand = tx.commands.select<Commands.Issue>().firstOrNull()
|
||||
if (issueCommand != null) {
|
||||
verifyIssueCommand(inputs, outputs, tx, issueCommand, commodity, issuer)
|
||||
} else {
|
||||
val inputAmount = inputs.sumCommoditiesOrNull() ?: throw IllegalArgumentException("there is at least one commodity input for this group")
|
||||
val outputAmount = outputs.sumCommoditiesOrZero(Issued(issuer, commodity))
|
||||
|
||||
// If we want to remove commodity from the ledger, that must be signed for by the issuer.
|
||||
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero.
|
||||
val exitCommand = tx.commands.select<Commands.Exit>(party = party).singleOrNull()
|
||||
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, Issued(issuer, commodity))
|
||||
|
||||
requireThat {
|
||||
"there are no zero sized inputs" using ( inputs.none { it.amount.quantity == 0L } )
|
||||
"for reference ${issuer.reference} at issuer ${party.nameOrNull()} the amounts balance" using
|
||||
(inputAmount == outputAmount + amountExitingLedger)
|
||||
}
|
||||
|
||||
verifyMoveCommand<Commands.Move>(inputs, tx.commands)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyIssueCommand(inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
tx: LedgerTransaction,
|
||||
issueCommand: AuthenticatedObject<Commands.Issue>,
|
||||
commodity: Commodity,
|
||||
issuer: PartyAndReference) {
|
||||
// If we have an issue command, perform special processing: the group is allowed to have no inputs,
|
||||
// and the output states must have a deposit reference owned by the signer.
|
||||
//
|
||||
// Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must
|
||||
// sum to more than the inputs. An issuance of zero size is not allowed.
|
||||
//
|
||||
// Note that this means literally anyone with access to the network can issue cash claims of arbitrary
|
||||
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some
|
||||
// as-yet-unwritten identity service. See ADP-22 for discussion.
|
||||
|
||||
// The grouping ensures that all outputs have the same deposit reference and currency.
|
||||
val inputAmount = inputs.sumCommoditiesOrZero(Issued(issuer, commodity))
|
||||
val outputAmount = outputs.sumCommodities()
|
||||
val commodityCommands = tx.commands.select<CommodityContract.Commands>()
|
||||
requireThat {
|
||||
"the issue command has a nonce" using (issueCommand.value.nonce != 0L)
|
||||
"output deposits are owned by a command signer" using (issuer.party in issueCommand.signingParties)
|
||||
"output values sum to more than the inputs" using (outputAmount > inputAmount)
|
||||
"there is only a single issue command" using (commodityCommands.count() == 1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Commands>>
|
||||
= commands.select<CommodityContract.Commands>()
|
||||
|
@ -5,9 +5,7 @@ import net.corda.contracts.NetCommand
|
||||
import net.corda.contracts.NetType
|
||||
import net.corda.contracts.NettableState
|
||||
import net.corda.contracts.asset.Obligation.Lifecycle.NORMAL
|
||||
import net.corda.contracts.clause.*
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.clauses.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.entropyToKeyPair
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
@ -30,6 +28,37 @@ import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.set
|
||||
|
||||
/**
|
||||
* Common interface for the state subsets used when determining nettability of two or more states. Exposes the
|
||||
* underlying issued thing.
|
||||
*/
|
||||
interface NetState<P : Any> {
|
||||
val template: Obligation.Terms<P>
|
||||
}
|
||||
|
||||
/**
|
||||
* Subset of state, containing the elements which must match for two obligation transactions to be nettable.
|
||||
* If two obligation state objects produce equal bilateral net states, they are considered safe to net directly.
|
||||
* Bilateral states are used in close-out netting.
|
||||
*/
|
||||
data class BilateralNetState<P : Any>(
|
||||
val partyKeys: Set<AbstractParty>,
|
||||
override val template: Obligation.Terms<P>
|
||||
) : NetState<P>
|
||||
|
||||
/**
|
||||
* Subset of state, containing the elements which must match for two or more obligation transactions to be candidates
|
||||
* for netting (this does not include the checks to enforce that everyone's amounts received are the same at the end,
|
||||
* which is handled under the verify() function).
|
||||
* In comparison to [BilateralNetState], this doesn't include the parties' keys, as ensuring balances match on
|
||||
* input and output is handled elsewhere.
|
||||
* Used in cases where all parties (or their proxies) are signing, such as central clearing.
|
||||
*/
|
||||
data class MultilateralNetState<P : Any>(
|
||||
override val template: Obligation.Terms<P>
|
||||
) : NetState<P>
|
||||
|
||||
|
||||
// Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode.
|
||||
val OBLIGATION_PROGRAM_ID = Obligation<Currency>()
|
||||
|
||||
@ -55,186 +84,6 @@ class Obligation<P : Any> : Contract {
|
||||
*/
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.example.gov/cash-settlement.html")
|
||||
|
||||
interface Clauses {
|
||||
/**
|
||||
* Parent clause for clauses that operate on grouped states (those which are fungible).
|
||||
*/
|
||||
class Group<P : Any> : GroupClauseVerifier<State<P>, Commands, Issued<Terms<P>>>(
|
||||
AllOf(
|
||||
NoZeroSizedOutputs<State<P>, Commands, Terms<P>>(),
|
||||
FirstOf(
|
||||
SetLifecycle<P>(),
|
||||
AllOf(
|
||||
VerifyLifecycle<State<P>, Commands, Issued<Terms<P>>, P>(),
|
||||
FirstOf(
|
||||
Settle<P>(),
|
||||
Issue(),
|
||||
ConserveAmount()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
override fun groupStates(tx: LedgerTransaction): List<LedgerTransaction.InOutGroup<Obligation.State<P>, Issued<Terms<P>>>>
|
||||
= tx.groupStates<Obligation.State<P>, Issued<Terms<P>>> { it.amount.token }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic issuance clause
|
||||
*/
|
||||
class Issue<P : Any> : AbstractIssue<State<P>, Commands, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic move/exit clause for fungible assets
|
||||
*/
|
||||
class ConserveAmount<P : Any> : AbstractConserveAmount<State<P>, Commands, Terms<P>>()
|
||||
|
||||
/**
|
||||
* Clause for supporting netting of obligations.
|
||||
*/
|
||||
class Net<C : CommandData, P : Any> : NetClause<C, P>() {
|
||||
val lifecycleClause = Clauses.VerifyLifecycle<ContractState, C, Unit, P>()
|
||||
override fun toString(): String = "Net obligations"
|
||||
|
||||
override fun verify(tx: LedgerTransaction, inputs: List<ContractState>, outputs: List<ContractState>, commands: List<AuthenticatedObject<C>>, groupingKey: Unit?): Set<C> {
|
||||
lifecycleClause.verify(tx, inputs, outputs, commands, groupingKey)
|
||||
return super.verify(tx, inputs, outputs, commands, groupingKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obligation-specific clause for changing the lifecycle of one or more states.
|
||||
*/
|
||||
class SetLifecycle<P : Any> : Clause<State<P>, Commands, Issued<Terms<P>>>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.SetLifecycle::class.java)
|
||||
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Issued<Terms<P>>?): Set<Commands> {
|
||||
val command = commands.requireSingleCommand<Commands.SetLifecycle>()
|
||||
Obligation<P>().verifySetLifecycleCommand(inputs, outputs, tx, command)
|
||||
return setOf(command.value)
|
||||
}
|
||||
|
||||
override fun toString(): String = "Set obligation lifecycle"
|
||||
}
|
||||
|
||||
/**
|
||||
* Obligation-specific clause for settling an outstanding obligation by witnessing
|
||||
* change of ownership of other states to fulfil
|
||||
*/
|
||||
class Settle<P : Any> : Clause<State<P>, Commands, Issued<Terms<P>>>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Settle::class.java)
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Issued<Terms<P>>?): Set<Commands> {
|
||||
require(groupingKey != null)
|
||||
val command = commands.requireSingleCommand<Commands.Settle<P>>()
|
||||
val obligor = groupingKey!!.issuer.party
|
||||
val template = groupingKey.product
|
||||
val inputAmount: Amount<Issued<Terms<P>>> = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
|
||||
val outputAmount: Amount<Issued<Terms<P>>> = outputs.sumObligationsOrZero(groupingKey)
|
||||
|
||||
// Sum up all asset state objects that are moving and fulfil our requirements
|
||||
|
||||
// The fungible asset contract verification handles ensuring there's inputs enough to cover the output states,
|
||||
// we only care about counting how much is output in this transaction. We then calculate the difference in
|
||||
// settlement amounts between the transaction inputs and outputs, and the two must match. No elimination is
|
||||
// done of amounts paid in by each beneficiary, as it's presumed the beneficiaries have enough sense to do that
|
||||
// themselves. Therefore if someone actually signed the following transaction (using cash just for an example):
|
||||
//
|
||||
// Inputs:
|
||||
// £1m cash owned by B
|
||||
// £1m owed from A to B
|
||||
// Outputs:
|
||||
// £1m cash owned by B
|
||||
// Commands:
|
||||
// Settle (signed by A)
|
||||
// Move (signed by B)
|
||||
//
|
||||
// That would pass this check. Ensuring they do not is best addressed in the transaction generation stage.
|
||||
val assetStates = tx.outputsOfType<FungibleAsset<*>>()
|
||||
val acceptableAssetStates = assetStates
|
||||
// TODO: This filter is nonsense, because it just checks there is an asset contract loaded, we need to
|
||||
// verify the asset contract is the asset contract we expect.
|
||||
// Something like:
|
||||
// attachments.mustHaveOneOf(key.acceptableAssetContract)
|
||||
.filter { it.contract.legalContractReference in template.acceptableContracts }
|
||||
// Restrict the states to those of the correct issuance definition (this normally
|
||||
// covers issued product and obligor, but is opaque to us)
|
||||
.filter { it.amount.token in template.acceptableIssuedProducts }
|
||||
// Catch that there's nothing useful here, so we can dump out a useful error
|
||||
requireThat {
|
||||
"there are fungible asset state outputs" using (assetStates.isNotEmpty())
|
||||
"there are defined acceptable fungible asset states" using (acceptableAssetStates.isNotEmpty())
|
||||
}
|
||||
|
||||
val amountReceivedByOwner = acceptableAssetStates.groupBy { it.owner }
|
||||
// Note we really do want to search all commands, because we want move commands of other contracts, not just
|
||||
// this one.
|
||||
val moveCommands = tx.commands.select<MoveCommand>()
|
||||
var totalPenniesSettled = 0L
|
||||
val requiredSigners = inputs.map { it.amount.token.issuer.party.owningKey }.toSet()
|
||||
|
||||
for ((beneficiary, obligations) in inputs.groupBy { it.owner }) {
|
||||
val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull<P>()
|
||||
if (settled != null) {
|
||||
val debt = obligations.sumObligationsOrZero(groupingKey)
|
||||
require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" }
|
||||
totalPenniesSettled += settled.quantity
|
||||
}
|
||||
}
|
||||
|
||||
val totalAmountSettled = Amount(totalPenniesSettled, command.value.amount.token)
|
||||
requireThat {
|
||||
// Insist that we can be the only contract consuming inputs, to ensure no other contract can think it's being
|
||||
// settled as well
|
||||
"all move commands relate to this contract" using (moveCommands.map { it.value.contractHash }
|
||||
.all { it == null || it == Obligation<P>().legalContractReference })
|
||||
// Settle commands exclude all other commands, so we don't need to check for contracts moving at the same
|
||||
// time.
|
||||
"amounts paid must match recipients to settle" using inputs.map { it.owner }.containsAll(amountReceivedByOwner.keys)
|
||||
"amount in settle command ${command.value.amount} matches settled total $totalAmountSettled" using (command.value.amount == totalAmountSettled)
|
||||
"signatures are present from all obligors" using command.signers.containsAll(requiredSigners)
|
||||
"there are no zero sized inputs" using inputs.none { it.amount.quantity == 0L }
|
||||
"at obligor $obligor the obligations after settlement balance" using
|
||||
(inputAmount == outputAmount + Amount(totalPenniesSettled, groupingKey))
|
||||
}
|
||||
return setOf(command.value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obligation-specific clause for verifying that all states are in
|
||||
* normal lifecycle. In a group clause set, this must be run after
|
||||
* any lifecycle change clause, which is the only clause that involve
|
||||
* non-standard lifecycle states on input/output.
|
||||
*/
|
||||
class VerifyLifecycle<S : ContractState, C : CommandData, T : Any, P : Any> : Clause<S, C, T>() {
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: T?): Set<C>
|
||||
= verify(inputs.filterIsInstance<State<P>>(), outputs.filterIsInstance<State<P>>())
|
||||
|
||||
private fun verify(inputs: List<State<P>>,
|
||||
outputs: List<State<P>>): Set<C> {
|
||||
requireThat {
|
||||
"all inputs are in the normal state " using inputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||
"all outputs are in the normal state " using outputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||
}
|
||||
return emptySet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents where in its lifecycle a contract state is, which in turn controls the commands that can be applied
|
||||
* to the state. Most states will not leave the [NORMAL] lifecycle. Note that settled (as an end lifecycle) is
|
||||
@ -386,10 +235,209 @@ class Obligation<P : Any> : Contract {
|
||||
data class Exit<P : Any>(override val amount: Amount<Issued<Terms<P>>>) : Commands, FungibleAsset.Commands.Exit<Terms<P>>
|
||||
}
|
||||
|
||||
override fun verify(tx: LedgerTransaction) = verifyClause<Commands>(tx, FirstOf<ContractState, Commands, Unit>(
|
||||
Clauses.Net<Commands, P>(),
|
||||
Clauses.Group<P>()
|
||||
), tx.commands.select<Obligation.Commands>())
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val netCommand = tx.commands.select<Commands.Net>().firstOrNull()
|
||||
if (netCommand != null) {
|
||||
verifyLifecycleCommand(tx.inputStates, tx.outputStates)
|
||||
verifyNetCommand(tx, netCommand)
|
||||
} else {
|
||||
val groups = tx.groupStates { it: Obligation.State<P> -> it.amount.token }
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
requireThat {
|
||||
"there are no zero sized outputs" using (outputs.none { it.amount.quantity == 0L })
|
||||
}
|
||||
val setLifecycleCommand = tx.commands.select<Commands.SetLifecycle>().firstOrNull()
|
||||
if (setLifecycleCommand != null) {
|
||||
verifySetLifecycleCommand(inputs, outputs, tx, setLifecycleCommand)
|
||||
} else {
|
||||
verifyLifecycleCommand(inputs, outputs)
|
||||
val settleCommand = tx.commands.select<Commands.Settle<P>>().firstOrNull()
|
||||
if (settleCommand != null) {
|
||||
verifySettleCommand(tx, inputs, outputs, settleCommand, key)
|
||||
} else {
|
||||
val issueCommand = tx.commands.select<Commands.Issue>().firstOrNull()
|
||||
if (issueCommand != null) {
|
||||
verifyIssueCommand(tx, inputs, outputs, issueCommand, key)
|
||||
} else {
|
||||
conserveAmount(tx, inputs, outputs, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun conserveAmount(tx: LedgerTransaction,
|
||||
inputs: List<FungibleAsset<Terms<P>>>,
|
||||
outputs: List<FungibleAsset<Terms<P>>>,
|
||||
key: Issued<Terms<P>>) {
|
||||
val issuer = key.issuer
|
||||
val terms = key.product
|
||||
val inputAmount = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
|
||||
val outputAmount = outputs.sumObligationsOrZero(Issued(issuer, terms))
|
||||
|
||||
// If we want to remove obligations from the ledger, that must be signed for by the issuer.
|
||||
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero.
|
||||
val exitKeys: Set<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
|
||||
val exitCommand = tx.commands.select<Commands.Exit<P>>(parties = null, signers = exitKeys).filter { it.value.amount.token == key }.singleOrNull()
|
||||
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, Issued(issuer, terms))
|
||||
|
||||
requireThat {
|
||||
"there are no zero sized inputs" using (inputs.none { it.amount.quantity == 0L })
|
||||
"for reference ${issuer.reference} at issuer ${issuer.party.nameOrNull()} the amounts balance" using
|
||||
(inputAmount == outputAmount + amountExitingLedger)
|
||||
}
|
||||
|
||||
verifyMoveCommand<Commands.Move>(inputs, tx.commands)
|
||||
}
|
||||
|
||||
private fun verifyIssueCommand(tx: LedgerTransaction,
|
||||
inputs: List<FungibleAsset<Terms<P>>>,
|
||||
outputs: List<FungibleAsset<Terms<P>>>,
|
||||
issueCommand: AuthenticatedObject<Commands.Issue>,
|
||||
key: Issued<Terms<P>>) {
|
||||
// If we have an issue command, perform special processing: the group is allowed to have no inputs,
|
||||
// and the output states must have a deposit reference owned by the signer.
|
||||
//
|
||||
// Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must
|
||||
// sum to more than the inputs. An issuance of zero size is not allowed.
|
||||
//
|
||||
// Note that this means literally anyone with access to the network can issue cash claims of arbitrary
|
||||
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some
|
||||
// as-yet-unwritten identity service. See ADP-22 for discussion.
|
||||
|
||||
// The grouping ensures that all outputs have the same deposit reference and currency.
|
||||
val issuer = key.issuer
|
||||
val terms = key.product
|
||||
val inputAmount = inputs.sumObligationsOrZero(Issued(issuer, terms))
|
||||
val outputAmount = outputs.sumObligations<P>()
|
||||
val issueCommands = tx.commands.select<Commands.Issue>()
|
||||
requireThat {
|
||||
"the issue command has a nonce" using (issueCommand.value.nonce != 0L)
|
||||
"output states are issued by a command signer" using (issuer.party in issueCommand.signingParties)
|
||||
"output values sum to more than the inputs" using (outputAmount > inputAmount)
|
||||
"there is only a single issue command" using (issueCommands.count() == 1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifySettleCommand(tx: LedgerTransaction,
|
||||
inputs: List<FungibleAsset<Terms<P>>>,
|
||||
outputs: List<FungibleAsset<Terms<P>>>,
|
||||
command: AuthenticatedObject<Commands.Settle<P>>,
|
||||
groupingKey: Issued<Terms<P>>) {
|
||||
val obligor = groupingKey.issuer.party
|
||||
val template = groupingKey.product
|
||||
val inputAmount: Amount<Issued<Terms<P>>> = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
|
||||
val outputAmount: Amount<Issued<Terms<P>>> = outputs.sumObligationsOrZero(groupingKey)
|
||||
|
||||
// Sum up all asset state objects that are moving and fulfil our requirements
|
||||
|
||||
// The fungible asset contract verification handles ensuring there's inputs enough to cover the output states,
|
||||
// we only care about counting how much is output in this transaction. We then calculate the difference in
|
||||
// settlement amounts between the transaction inputs and outputs, and the two must match. No elimination is
|
||||
// done of amounts paid in by each beneficiary, as it's presumed the beneficiaries have enough sense to do that
|
||||
// themselves. Therefore if someone actually signed the following transaction (using cash just for an example):
|
||||
//
|
||||
// Inputs:
|
||||
// £1m cash owned by B
|
||||
// £1m owed from A to B
|
||||
// Outputs:
|
||||
// £1m cash owned by B
|
||||
// Commands:
|
||||
// Settle (signed by A)
|
||||
// Move (signed by B)
|
||||
//
|
||||
// That would pass this check. Ensuring they do not is best addressed in the transaction generation stage.
|
||||
val assetStates = tx.outputsOfType<FungibleAsset<*>>()
|
||||
val acceptableAssetStates = assetStates
|
||||
// TODO: This filter is nonsense, because it just checks there is an asset contract loaded, we need to
|
||||
// verify the asset contract is the asset contract we expect.
|
||||
// Something like:
|
||||
// attachments.mustHaveOneOf(key.acceptableAssetContract)
|
||||
.filter { it.contract.legalContractReference in template.acceptableContracts }
|
||||
// Restrict the states to those of the correct issuance definition (this normally
|
||||
// covers issued product and obligor, but is opaque to us)
|
||||
.filter { it.amount.token in template.acceptableIssuedProducts }
|
||||
// Catch that there's nothing useful here, so we can dump out a useful error
|
||||
requireThat {
|
||||
"there are fungible asset state outputs" using (assetStates.isNotEmpty())
|
||||
"there are defined acceptable fungible asset states" using (acceptableAssetStates.isNotEmpty())
|
||||
}
|
||||
|
||||
val amountReceivedByOwner = acceptableAssetStates.groupBy { it.owner }
|
||||
// Note we really do want to search all commands, because we want move commands of other contracts, not just
|
||||
// this one.
|
||||
val moveCommands = tx.commands.select<MoveCommand>()
|
||||
var totalPenniesSettled = 0L
|
||||
val requiredSigners = inputs.map { it.amount.token.issuer.party.owningKey }.toSet()
|
||||
|
||||
for ((beneficiary, obligations) in inputs.groupBy { it.owner }) {
|
||||
val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull<P>()
|
||||
if (settled != null) {
|
||||
val debt = obligations.sumObligationsOrZero(groupingKey)
|
||||
require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" }
|
||||
totalPenniesSettled += settled.quantity
|
||||
}
|
||||
}
|
||||
|
||||
val totalAmountSettled = Amount(totalPenniesSettled, command.value.amount.token)
|
||||
requireThat {
|
||||
// Insist that we can be the only contract consuming inputs, to ensure no other contract can think it's being
|
||||
// settled as well
|
||||
"all move commands relate to this contract" using (moveCommands.map { it.value.contractHash }
|
||||
.all { it == null || it == Obligation<P>().legalContractReference })
|
||||
// Settle commands exclude all other commands, so we don't need to check for contracts moving at the same
|
||||
// time.
|
||||
"amounts paid must match recipients to settle" using inputs.map { it.owner }.containsAll(amountReceivedByOwner.keys)
|
||||
"amount in settle command ${command.value.amount} matches settled total $totalAmountSettled" using (command.value.amount == totalAmountSettled)
|
||||
"signatures are present from all obligors" using command.signers.containsAll(requiredSigners)
|
||||
"there are no zero sized inputs" using inputs.none { it.amount.quantity == 0L }
|
||||
"at obligor $obligor the obligations after settlement balance" using
|
||||
(inputAmount == outputAmount + Amount(totalPenniesSettled, groupingKey))
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyLifecycleCommand(inputs: List<ContractState>, outputs: List<ContractState>) {
|
||||
val filteredInputs = inputs.filterIsInstance<State<P>>()
|
||||
val filteredOutputs = outputs.filterIsInstance<State<P>>()
|
||||
requireThat {
|
||||
"all inputs are in the normal state " using filteredInputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||
"all outputs are in the normal state " using filteredOutputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyNetCommand(tx: LedgerTransaction, command: AuthenticatedObject<NetCommand>) {
|
||||
val groups = when (command.value.type) {
|
||||
NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State<P> -> it.bilateralNetState }
|
||||
NetType.PAYMENT -> tx.groupStates { it: Obligation.State<P> -> it.multilateralNetState }
|
||||
}
|
||||
for ((groupInputs, groupOutputs, key) in groups) {
|
||||
|
||||
val template = key.template
|
||||
// Create two maps of balances from obligors to beneficiaries, one for input states, the other for output states.
|
||||
val inputBalances = extractAmountsDue(template, groupInputs)
|
||||
val outputBalances = extractAmountsDue(template, groupOutputs)
|
||||
|
||||
// Sum the columns of the matrices. This will yield the net amount payable to/from each party to/from all other participants.
|
||||
// The two summaries must match, reflecting that the amounts owed match on both input and output.
|
||||
requireThat {
|
||||
"all input states use the same template" using (groupInputs.all { it.template == template })
|
||||
"all output states use the same template" using (groupOutputs.all { it.template == template })
|
||||
"amounts owed on input and output must match" using (sumAmountsDue(inputBalances) == sumAmountsDue
|
||||
(outputBalances))
|
||||
}
|
||||
|
||||
// TODO: Handle proxies nominated by parties, i.e. a central clearing service
|
||||
val involvedParties: Set<PublicKey> = groupInputs.map { it.beneficiary.owningKey }.union(groupInputs.map { it.obligor.owningKey }).toSet()
|
||||
when (command.value.type) {
|
||||
// For close-out netting, allow any involved party to sign
|
||||
NetType.CLOSE_OUT -> require(command.signers.intersect(involvedParties).isNotEmpty()) { "any involved party has signed" }
|
||||
// Require signatures from all parties (this constraint can be changed for other contracts, and is used as a
|
||||
// placeholder while exact requirements are established), or fail the transaction.
|
||||
NetType.PAYMENT -> require(command.signers.containsAll(involvedParties)) { "all involved parties have signed" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A default command mutates inputs and produces identical outputs, except that the lifecycle changes.
|
||||
@ -488,11 +536,11 @@ class Obligation<P : Any> : Contract {
|
||||
* @param notary the notary for this transaction's outputs.
|
||||
*/
|
||||
fun generateCashIssue(tx: TransactionBuilder,
|
||||
obligor: AbstractParty,
|
||||
amount: Amount<Issued<Currency>>,
|
||||
dueBefore: Instant,
|
||||
beneficiary: AbstractParty,
|
||||
notary: Party) {
|
||||
obligor: AbstractParty,
|
||||
amount: Amount<Issued<Currency>>,
|
||||
dueBefore: Instant,
|
||||
beneficiary: AbstractParty,
|
||||
notary: Party) {
|
||||
val issuanceDef = Terms(NonEmptySet.of(Cash().legalContractReference), NonEmptySet.of(amount.token), dueBefore)
|
||||
OnLedgerAsset.generateIssue(tx, TransactionState(State(Lifecycle.NORMAL, obligor, issuanceDef, amount.quantity, beneficiary), notary), Commands.Issue())
|
||||
}
|
||||
@ -514,7 +562,7 @@ class Obligation<P : Any> : Contract {
|
||||
pennies: Long,
|
||||
beneficiary: AbstractParty,
|
||||
notary: Party)
|
||||
= OnLedgerAsset.generateIssue(tx, TransactionState(State(Lifecycle.NORMAL, obligor, issuanceDef, pennies, beneficiary), notary), Commands.Issue())
|
||||
= OnLedgerAsset.generateIssue(tx, TransactionState(State(Lifecycle.NORMAL, obligor, issuanceDef, pennies, beneficiary), notary), Commands.Issue())
|
||||
|
||||
fun generatePaymentNetting(tx: TransactionBuilder,
|
||||
issued: Issued<Obligation.Terms<P>>,
|
||||
@ -682,7 +730,7 @@ fun <P : Any> extractAmountsDue(product: Obligation.Terms<P>, states: Iterable<O
|
||||
/**
|
||||
* Net off the amounts due between parties.
|
||||
*/
|
||||
fun <P: AbstractParty, T : Any> netAmountsDue(balances: Map<Pair<P, P>, Amount<T>>): Map<Pair<P, P>, Amount<T>> {
|
||||
fun <P : AbstractParty, T : Any> netAmountsDue(balances: Map<Pair<P, P>, Amount<T>>): Map<Pair<P, P>, Amount<T>> {
|
||||
val nettedBalances = HashMap<Pair<P, P>, Amount<T>>()
|
||||
|
||||
balances.forEach { balance ->
|
||||
@ -709,7 +757,7 @@ fun <P: AbstractParty, T : Any> netAmountsDue(balances: Map<Pair<P, P>, Amount<T
|
||||
* @param P type of party to operate on.
|
||||
* @param T token that balances represent
|
||||
*/
|
||||
fun <P: AbstractParty, T : Any> sumAmountsDue(balances: Map<Pair<P, P>, Amount<T>>): Map<P, Long> {
|
||||
fun <P : AbstractParty, T : Any> sumAmountsDue(balances: Map<Pair<P, P>, Amount<T>>): Map<P, Long> {
|
||||
val sum = HashMap<P, Long>()
|
||||
|
||||
// Fill the map with zeroes initially
|
||||
|
@ -1,71 +0,0 @@
|
||||
package net.corda.contracts.clause
|
||||
|
||||
import net.corda.contracts.asset.OnLedgerAsset
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.clauses.Clause
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* Standardised clause for checking input/output balances of fungible assets. Requires that a
|
||||
* Move command is provided, and errors if absent. Must be the last clause under a grouping clause;
|
||||
* errors on no-match, ends on match.
|
||||
*/
|
||||
abstract class AbstractConserveAmount<S : FungibleAsset<T>, C : CommandData, T : Any> : Clause<S, C, Issued<T>>() {
|
||||
|
||||
private companion object {
|
||||
val log = loggerFor<AbstractConserveAmount<*, *, *>>()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an transaction exiting fungible assets from the ledger.
|
||||
*
|
||||
* @param tx transaction builder to add states and commands to.
|
||||
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
|
||||
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
|
||||
* the responsibility of the caller to check that they do not attempt to exit funds held by others.
|
||||
* @return the public keys which must sign the transaction for it to be valid.
|
||||
*/
|
||||
@Deprecated("This function will be removed in a future milestone", ReplaceWith("OnLedgerAsset.generateExit()"))
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
|
||||
assetStates: List<StateAndRef<S>>,
|
||||
deriveState: (TransactionState<S>, Amount<Issued<T>>, AbstractParty) -> TransactionState<S>,
|
||||
generateMoveCommand: () -> CommandData,
|
||||
generateExitCommand: (Amount<Issued<T>>) -> CommandData): Set<PublicKey>
|
||||
= OnLedgerAsset.generateExit(tx, amountIssued, assetStates, deriveState, generateMoveCommand, generateExitCommand)
|
||||
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: Issued<T>?): Set<C> {
|
||||
require(groupingKey != null) { "Conserve amount clause can only be used on grouped states" }
|
||||
val matchedCommands = commands.filter { command -> command.value is FungibleAsset.Commands.Move || command.value is FungibleAsset.Commands.Exit<*> }
|
||||
val inputAmount: Amount<Issued<T>> = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for group $groupingKey")
|
||||
val deposit = groupingKey!!.issuer
|
||||
val outputAmount: Amount<Issued<T>> = outputs.sumFungibleOrZero(groupingKey)
|
||||
|
||||
// If we want to remove assets from the ledger, that must be signed for by the issuer and owner.
|
||||
val exitKeys: Set<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
|
||||
val exitCommand = matchedCommands.select<FungibleAsset.Commands.Exit<T>>(parties = null, signers = exitKeys).filter { it.value.amount.token == groupingKey }.singleOrNull()
|
||||
val amountExitingLedger: Amount<Issued<T>> = exitCommand?.value?.amount ?: Amount(0, groupingKey)
|
||||
|
||||
requireThat {
|
||||
"there are no zero sized inputs" using inputs.none { it.amount.quantity == 0L }
|
||||
"for reference ${deposit.reference} at issuer ${deposit.party} the amounts balance: ${inputAmount.quantity} - ${amountExitingLedger.quantity} != ${outputAmount.quantity}" using
|
||||
(inputAmount == outputAmount + amountExitingLedger)
|
||||
}
|
||||
|
||||
verifyMoveCommand<FungibleAsset.Commands.Move>(inputs, commands)
|
||||
|
||||
// This is safe because we've taken the commands from a collection of C objects at the start
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return matchedCommands.map { it.value }.toSet()
|
||||
}
|
||||
|
||||
override fun toString(): String = "Conserve amount between inputs and outputs"
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package net.corda.contracts.clause
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.clauses.Clause
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
|
||||
/**
|
||||
* Standard issue clause for contracts that issue fungible assets.
|
||||
*
|
||||
* @param S the type of contract state which is being issued.
|
||||
* @param T the token underlying the issued state.
|
||||
* @param sum function to convert a list of states into an amount of the token. Must error if there are no states in
|
||||
* the list.
|
||||
* @param sumOrZero function to convert a list of states into an amount of the token, and returns zero if there are
|
||||
* no states in the list. Takes in an instance of the token definition for constructing the zero amount if needed.
|
||||
*/
|
||||
abstract class AbstractIssue<in S : ContractState, C : CommandData, T : Any>(
|
||||
val sum: List<S>.() -> Amount<Issued<T>>,
|
||||
val sumOrZero: List<S>.(token: Issued<T>) -> Amount<Issued<T>>
|
||||
) : Clause<S, C, Issued<T>>() {
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: Issued<T>?): Set<C> {
|
||||
require(groupingKey != null)
|
||||
// TODO: Take in matched commands as a parameter
|
||||
val issueCommand = commands.requireSingleCommand<IssueCommand>()
|
||||
|
||||
// If we have an issue command, perform special processing: the group is allowed to have no inputs,
|
||||
// and the output states must have a deposit reference owned by the signer.
|
||||
//
|
||||
// Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must
|
||||
// sum to more than the inputs. An issuance of zero size is not allowed.
|
||||
//
|
||||
// Note that this means literally anyone with access to the network can issue asset claims of arbitrary
|
||||
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some
|
||||
// external mechanism (such as locally defined rules on which parties are trustworthy).
|
||||
|
||||
// The grouping already ensures that all outputs have the same deposit reference and token.
|
||||
val issuer = groupingKey!!.issuer.party
|
||||
val inputAmount = inputs.sumOrZero(groupingKey)
|
||||
val outputAmount = outputs.sum()
|
||||
requireThat {
|
||||
"the issue command has a nonce" using (issueCommand.value.nonce != 0L)
|
||||
// TODO: This doesn't work with the trader demo, so use the underlying key instead
|
||||
// "output states are issued by a command signer" by (issuer in issueCommand.signingParties)
|
||||
"output states are issued by a command signer" using (issuer.owningKey in issueCommand.signers)
|
||||
"output values sum to more than the inputs" using (outputAmount > inputAmount)
|
||||
}
|
||||
|
||||
// This is safe because we've taken the command from a collection of C objects at the start
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return setOf(issueCommand.value as C)
|
||||
}
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
package net.corda.contracts.clause
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import net.corda.contracts.NetCommand
|
||||
import net.corda.contracts.NetType
|
||||
import net.corda.contracts.asset.Obligation
|
||||
import net.corda.contracts.asset.extractAmountsDue
|
||||
import net.corda.contracts.asset.sumAmountsDue
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.clauses.Clause
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* Common interface for the state subsets used when determining nettability of two or more states. Exposes the
|
||||
* underlying issued thing.
|
||||
*/
|
||||
interface NetState<P : Any> {
|
||||
val template: Obligation.Terms<P>
|
||||
}
|
||||
|
||||
/**
|
||||
* Subset of state, containing the elements which must match for two obligation transactions to be nettable.
|
||||
* If two obligation state objects produce equal bilateral net states, they are considered safe to net directly.
|
||||
* Bilateral states are used in close-out netting.
|
||||
*/
|
||||
data class BilateralNetState<P : Any>(
|
||||
val partyKeys: Set<AbstractParty>,
|
||||
override val template: Obligation.Terms<P>
|
||||
) : NetState<P>
|
||||
|
||||
/**
|
||||
* Subset of state, containing the elements which must match for two or more obligation transactions to be candidates
|
||||
* for netting (this does not include the checks to enforce that everyone's amounts received are the same at the end,
|
||||
* which is handled under the verify() function).
|
||||
* In comparison to [BilateralNetState], this doesn't include the parties' keys, as ensuring balances match on
|
||||
* input and output is handled elsewhere.
|
||||
* Used in cases where all parties (or their proxies) are signing, such as central clearing.
|
||||
*/
|
||||
data class MultilateralNetState<P : Any>(
|
||||
override val template: Obligation.Terms<P>
|
||||
) : NetState<P>
|
||||
|
||||
/**
|
||||
* Clause for netting contract states. Currently only supports obligation contract.
|
||||
*/
|
||||
// TODO: Make this usable for any nettable contract states
|
||||
open class NetClause<C : CommandData, P : Any> : Clause<ContractState, C, Unit>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Obligation.Commands.Net::class.java)
|
||||
|
||||
@Suppress("ConvertLambdaToReference")
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: Unit?): Set<C> {
|
||||
val matchedCommands: List<AuthenticatedObject<C>> = commands.filter { it.value is NetCommand }
|
||||
val command = matchedCommands.requireSingleCommand<Obligation.Commands.Net>()
|
||||
val groups = when (command.value.type) {
|
||||
NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State<P> -> it.bilateralNetState }
|
||||
NetType.PAYMENT -> tx.groupStates { it: Obligation.State<P> -> it.multilateralNetState }
|
||||
}
|
||||
for ((groupInputs, groupOutputs, key) in groups) {
|
||||
verifyNetCommand(groupInputs, groupOutputs, command, key)
|
||||
}
|
||||
return matchedCommands.map { it.value }.toSet()
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a netting command. This handles both close-out and payment netting.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun verifyNetCommand(inputs: List<Obligation.State<P>>,
|
||||
outputs: List<Obligation.State<P>>,
|
||||
command: AuthenticatedObject<NetCommand>,
|
||||
netState: NetState<P>) {
|
||||
val template = netState.template
|
||||
// Create two maps of balances from obligors to beneficiaries, one for input states, the other for output states.
|
||||
val inputBalances = extractAmountsDue(template, inputs)
|
||||
val outputBalances = extractAmountsDue(template, outputs)
|
||||
|
||||
// Sum the columns of the matrices. This will yield the net amount payable to/from each party to/from all other participants.
|
||||
// The two summaries must match, reflecting that the amounts owed match on both input and output.
|
||||
requireThat {
|
||||
"all input states use the same template" using (inputs.all { it.template == template })
|
||||
"all output states use the same template" using (outputs.all { it.template == template })
|
||||
"amounts owed on input and output must match" using (sumAmountsDue(inputBalances) == sumAmountsDue
|
||||
(outputBalances))
|
||||
}
|
||||
|
||||
// TODO: Handle proxies nominated by parties, i.e. a central clearing service
|
||||
val involvedParties: Set<PublicKey> = inputs.map { it.beneficiary.owningKey }.union(inputs.map { it.obligor.owningKey }).toSet()
|
||||
when (command.value.type) {
|
||||
// For close-out netting, allow any involved party to sign
|
||||
NetType.CLOSE_OUT -> require(command.signers.intersect(involvedParties).isNotEmpty()) { "any involved party has signed" }
|
||||
// Require signatures from all parties (this constraint can be changed for other contracts, and is used as a
|
||||
// placeholder while exact requirements are established), or fail the transaction.
|
||||
NetType.PAYMENT -> require(command.signers.containsAll(involvedParties)) { "all involved parties have signed" }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package net.corda.contracts.clause
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.clauses.Clause
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
|
||||
/**
|
||||
* Clause for fungible asset contracts, which enforces that no output state should have
|
||||
* a balance of zero.
|
||||
*/
|
||||
open class NoZeroSizedOutputs<in S : FungibleAsset<T>, C : CommandData, T : Any> : Clause<S, C, Issued<T>>() {
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: Issued<T>?): Set<C> {
|
||||
requireThat {
|
||||
"there are no zero sized outputs" using outputs.none { it.amount.quantity == 0L }
|
||||
}
|
||||
return emptySet()
|
||||
}
|
||||
|
||||
override fun toString(): String = "No zero sized outputs"
|
||||
}
|
@ -36,7 +36,7 @@ public class CashTestsJava {
|
||||
tx.tweak(tw -> {
|
||||
tw.output(outState);
|
||||
// No command arguments
|
||||
return tw.failsWith("required net.corda.core.contracts.FungibleAsset.Commands.Move command");
|
||||
return tw.failsWith("required net.corda.contracts.asset.Cash.Commands.Move command");
|
||||
});
|
||||
tx.tweak(tw -> {
|
||||
tw.output(outState);
|
||||
@ -49,7 +49,7 @@ public class CashTestsJava {
|
||||
// with different overloads (for some reason).
|
||||
tw.output(CashKt.issuedBy(outState, getMINI_CORP()));
|
||||
tw.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
|
||||
return tw.failsWith("at least one asset input");
|
||||
return tw.failsWith("at least one cash input");
|
||||
});
|
||||
|
||||
// Simple reallocation works.
|
||||
|
@ -60,16 +60,16 @@ class KotlinCommercialPaperTest : ICommercialPaperTestTemplate {
|
||||
}
|
||||
|
||||
class KotlinCommercialPaperLegacyTest : ICommercialPaperTestTemplate {
|
||||
override fun getPaper(): ICommercialPaperState = CommercialPaperLegacy.State(
|
||||
override fun getPaper(): ICommercialPaperState = CommercialPaper.State(
|
||||
issuance = MEGA_CORP.ref(123),
|
||||
owner = MEGA_CORP,
|
||||
faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123),
|
||||
maturityDate = TEST_TX_TIME + 7.days
|
||||
)
|
||||
|
||||
override fun getIssueCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Issue()
|
||||
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Redeem()
|
||||
override fun getMoveCommand(): CommandData = CommercialPaperLegacy.Commands.Move()
|
||||
override fun getIssueCommand(notary: Party): CommandData = CommercialPaper.Commands.Issue()
|
||||
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaper.Commands.Redeem()
|
||||
override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move()
|
||||
}
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
|
@ -1,28 +1,21 @@
|
||||
package net.corda.contracts.asset
|
||||
|
||||
import net.corda.contracts.clause.AbstractConserveAmount
|
||||
import net.corda.contracts.clause.AbstractIssue
|
||||
import net.corda.contracts.clause.NoZeroSizedOutputs
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.clauses.AllOf
|
||||
import net.corda.core.contracts.clauses.FirstOf
|
||||
import net.corda.core.contracts.clauses.GroupClauseVerifier
|
||||
import net.corda.core.contracts.clauses.verifyClause
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.newSecureRandom
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.QueryableState
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.schemas.SampleCashSchemaV1
|
||||
import net.corda.schemas.SampleCashSchemaV2
|
||||
import net.corda.schemas.SampleCashSchemaV3
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
class DummyFungibleContract : OnLedgerAsset<Currency, DummyFungibleContract.Commands, DummyFungibleContract.State>() {
|
||||
@ -31,29 +24,6 @@ class DummyFungibleContract : OnLedgerAsset<Currency, DummyFungibleContract.Comm
|
||||
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<DummyFungibleContract.Commands>>
|
||||
= commands.select<DummyFungibleContract.Commands>()
|
||||
|
||||
interface Clauses {
|
||||
class Group : GroupClauseVerifier<State, Commands, Issued<Currency>>(AllOf<State, Commands, Issued<Currency>>(
|
||||
NoZeroSizedOutputs<State, Commands, Currency>(),
|
||||
FirstOf<State, Commands, Issued<Currency>>(
|
||||
Issue(),
|
||||
ConserveAmount())
|
||||
)
|
||||
) {
|
||||
override fun groupStates(tx: LedgerTransaction): List<LedgerTransaction.InOutGroup<State, Issued<Currency>>>
|
||||
= tx.groupStates<State, Issued<Currency>> { it.amount.token }
|
||||
}
|
||||
|
||||
class Issue : AbstractIssue<State, Commands, Currency>(
|
||||
sum = { sumCash() },
|
||||
sumOrZero = { sumCashOrZero(it) }
|
||||
) {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class ConserveAmount : AbstractConserveAmount<State, Commands, Currency>()
|
||||
}
|
||||
|
||||
data class State(
|
||||
override val amount: Amount<Issued<Currency>>,
|
||||
|
||||
@ -129,7 +99,69 @@ class DummyFungibleContract : OnLedgerAsset<Currency, DummyFungibleContract.Comm
|
||||
override fun generateIssueCommand() = Commands.Issue()
|
||||
override fun generateMoveCommand() = Commands.Move()
|
||||
|
||||
override fun verify(tx: LedgerTransaction)
|
||||
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
|
||||
val groups = tx.groupStates { it: State -> it.amount.token }
|
||||
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
// Either inputs or outputs could be empty.
|
||||
val issuer = key.issuer
|
||||
val currency = key.product
|
||||
|
||||
requireThat {
|
||||
"there are no zero sized outputs" using (outputs.none { it.amount.quantity == 0L })
|
||||
}
|
||||
|
||||
val issueCommand = tx.commands.select<Commands.Issue>().firstOrNull()
|
||||
if (issueCommand != null) {
|
||||
verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer)
|
||||
} else {
|
||||
val inputAmount = inputs.sumCashOrNull() ?: throw IllegalArgumentException("there is at least one input for this group")
|
||||
val outputAmount = outputs.sumCashOrZero(Issued(issuer, currency))
|
||||
|
||||
val exitKeys: Set<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
|
||||
val exitCommand = tx.commands.select<Commands.Exit>(parties = null, signers = exitKeys).filter { it.value.amount.token == key }.singleOrNull()
|
||||
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, Issued(issuer, currency))
|
||||
|
||||
requireThat {
|
||||
"there are no zero sized inputs" using inputs.none { it.amount.quantity == 0L }
|
||||
"for reference ${issuer.reference} at issuer ${issuer.party} the amounts balance: ${inputAmount.quantity} - ${amountExitingLedger.quantity} != ${outputAmount.quantity}" using
|
||||
(inputAmount == outputAmount + amountExitingLedger)
|
||||
}
|
||||
|
||||
verifyMoveCommand<Commands.Move>(inputs, tx.commands)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyIssueCommand(inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
tx: LedgerTransaction,
|
||||
issueCommand: AuthenticatedObject<Commands.Issue>,
|
||||
currency: Currency,
|
||||
issuer: PartyAndReference) {
|
||||
// If we have an issue command, perform special processing: the group is allowed to have no inputs,
|
||||
// and the output states must have a deposit reference owned by the signer.
|
||||
//
|
||||
// Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must
|
||||
// sum to more than the inputs. An issuance of zero size is not allowed.
|
||||
//
|
||||
// Note that this means literally anyone with access to the network can issue cash claims of arbitrary
|
||||
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some
|
||||
// as-yet-unwritten identity service. See ADP-22 for discussion.
|
||||
|
||||
// The grouping ensures that all outputs have the same deposit reference and currency.
|
||||
val inputAmount = inputs.sumCashOrZero(Issued(issuer, currency))
|
||||
val outputAmount = outputs.sumCash()
|
||||
val cashCommands = tx.commands.select<Commands.Issue>()
|
||||
requireThat {
|
||||
"the issue command has a nonce" using (issueCommand.value.nonce != 0L)
|
||||
// TODO: This doesn't work with the trader demo, so use the underlying key instead
|
||||
// "output states are issued by a command signer" by (issuer.party in issueCommand.signingParties)
|
||||
"output states are issued by a command signer" using (issuer.party.owningKey in issueCommand.signers)
|
||||
"output values sum to more than the inputs" using (outputAmount > inputAmount)
|
||||
"there is only a single issue command" using (cashCommands.count() == 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,8 @@ class CashTests : TestDependencyInjectionBase() {
|
||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
||||
vaultService.notifyAll(txs.map { it.tx })
|
||||
}
|
||||
override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
|
||||
|
||||
override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
|
||||
}
|
||||
|
||||
miniCorpServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
||||
@ -100,7 +101,7 @@ class CashTests : TestDependencyInjectionBase() {
|
||||
tweak {
|
||||
output { outState }
|
||||
// No command arguments
|
||||
this `fails with` "required net.corda.core.contracts.FungibleAsset.Commands.Move command"
|
||||
this `fails with` "required net.corda.contracts.asset.Cash.Commands.Move command"
|
||||
}
|
||||
tweak {
|
||||
output { outState }
|
||||
@ -111,7 +112,7 @@ class CashTests : TestDependencyInjectionBase() {
|
||||
output { outState }
|
||||
output { outState `issued by` MINI_CORP }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
this `fails with` "at least one asset input"
|
||||
this `fails with` "at least one cash input"
|
||||
}
|
||||
// Simple reallocation works.
|
||||
tweak {
|
||||
@ -130,7 +131,7 @@ class CashTests : TestDependencyInjectionBase() {
|
||||
output { outState }
|
||||
command(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
|
||||
this `fails with` "there is at least one asset input"
|
||||
this `fails with` "there is at least one cash input for this group"
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,15 +231,7 @@ class CashTests : TestDependencyInjectionBase() {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
this `fails with` "List has more than one element."
|
||||
}
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
this `fails with` "The following commands were not matched at the end of execution"
|
||||
}
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount.splitEvenly(2).first()) }
|
||||
this `fails with` "The following commands were not matched at the end of execution"
|
||||
this `fails with` "there is only a single issue command"
|
||||
}
|
||||
this.verifies()
|
||||
}
|
||||
@ -372,7 +365,7 @@ class CashTests : TestDependencyInjectionBase() {
|
||||
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
|
||||
this `fails with` "required net.corda.core.contracts.FungibleAsset.Commands.Move command"
|
||||
this `fails with` "required net.corda.contracts.asset.Cash.Commands.Move command"
|
||||
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
|
@ -14,9 +14,9 @@ import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.hours
|
||||
import net.corda.testing.*
|
||||
import org.junit.After
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.junit.After
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
@ -77,7 +77,7 @@ class ObligationTests {
|
||||
tweak {
|
||||
output { outState }
|
||||
// No command arguments
|
||||
this `fails with` "required net.corda.core.contracts.FungibleAsset.Commands.Move command"
|
||||
this `fails with` "required net.corda.contracts.asset.Obligation.Commands.Move command"
|
||||
}
|
||||
tweak {
|
||||
output { outState }
|
||||
@ -88,7 +88,7 @@ class ObligationTests {
|
||||
output { outState }
|
||||
output { outState `issued by` MINI_CORP }
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||
this `fails with` "at least one asset input"
|
||||
this `fails with` "at least one obligation input"
|
||||
}
|
||||
// Simple reallocation works.
|
||||
tweak {
|
||||
@ -107,7 +107,7 @@ class ObligationTests {
|
||||
output { outState }
|
||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() }
|
||||
|
||||
this `fails with` "there is at least one asset input"
|
||||
this `fails with` "at least one obligation input"
|
||||
}
|
||||
|
||||
// Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
|
||||
@ -193,15 +193,7 @@ class ObligationTests {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||
this `fails with` "List has more than one element."
|
||||
}
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move() }
|
||||
this `fails with` "The following commands were not matched at the end of execution"
|
||||
}
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.amount.splitEvenly(2).first()) }
|
||||
this `fails with` "The following commands were not matched at the end of execution"
|
||||
this `fails with` "there is only a single issue command"
|
||||
}
|
||||
this.verifies()
|
||||
}
|
||||
@ -668,7 +660,7 @@ class ObligationTests {
|
||||
|
||||
tweak {
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token)) }
|
||||
this `fails with` "required net.corda.core.contracts.FungibleAsset.Commands.Move command"
|
||||
this `fails with` "required net.corda.contracts.asset.Obligation.Commands.Move command"
|
||||
|
||||
tweak {
|
||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||
|
@ -490,7 +490,7 @@ class TwoPartyTradeFlowTests {
|
||||
fun `dependency with error on buyer side`() {
|
||||
mockNet = MockNetwork(false)
|
||||
ledger(initialiseSerialization = false) {
|
||||
runWithError(true, false, "at least one asset input")
|
||||
runWithError(true, false, "at least one cash input")
|
||||
}
|
||||
}
|
||||
|
||||
@ -498,7 +498,7 @@ class TwoPartyTradeFlowTests {
|
||||
fun `dependency with error on seller side`() {
|
||||
mockNet = MockNetwork(false)
|
||||
ledger(initialiseSerialization = false) {
|
||||
runWithError(false, true, "Issuances must have a time-window")
|
||||
runWithError(false, true, "Issuances have a time-window")
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user