mirror of
https://github.com/corda/corda.git
synced 2025-02-08 03:50:34 +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
|
package net.corda.core.contracts
|
||||||
|
|
||||||
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.security.PublicKey
|
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. */
|
/** 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,
|
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.select(signer: PublicKey? = null,
|
||||||
party: Party? = null) =
|
party: AbstractParty? = null) =
|
||||||
filter { it.value is T }.
|
filter { it.value is T }.
|
||||||
filter { if (signer == null) true else signer in it.signers }.
|
filter { if (signer == null) true else signer in it.signers }.
|
||||||
filter { if (party == null) true else party in it.signingParties }.
|
filter { if (party == null) true else party in it.signingParties }.
|
||||||
|
@ -7,10 +7,6 @@ import kotlin.Pair;
|
|||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
import net.corda.contracts.asset.CashKt;
|
import net.corda.contracts.asset.CashKt;
|
||||||
import net.corda.core.contracts.*;
|
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.SecureHash;
|
||||||
import net.corda.core.crypto.testing.NullPublicKey;
|
import net.corda.core.crypto.testing.NullPublicKey;
|
||||||
import net.corda.core.identity.AbstractParty;
|
import net.corda.core.identity.AbstractParty;
|
||||||
@ -23,10 +19,8 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Currency;
|
import java.util.Currency;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
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 (issuance != null ? !issuance.equals(state.issuance) : state.issuance != null) return false;
|
||||||
if (owner != null ? !owner.equals(state.owner) : state.owner != 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 (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;
|
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 {
|
public interface Commands extends CommandData {
|
||||||
class Move implements Commands {
|
class Move implements Commands {
|
||||||
@Override
|
@Override
|
||||||
@ -303,7 +166,75 @@ public class JavaCommercialPaper implements Contract {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void verify(@NotNull LedgerTransaction tx) throws IllegalArgumentException {
|
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
|
@NotNull
|
||||||
|
@ -2,14 +2,9 @@ package net.corda.contracts
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.contracts.asset.sumCashBy
|
import net.corda.contracts.asset.sumCashBy
|
||||||
import net.corda.contracts.clause.AbstractIssue
|
|
||||||
import net.corda.core.contracts.*
|
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.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.crypto.toBase58String
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.Party
|
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
|
* 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).
|
* to do this in the Apache BVal project).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
val CP_PROGRAM_ID = CommercialPaper()
|
val CP_PROGRAM_ID = CommercialPaper()
|
||||||
|
|
||||||
// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance.
|
// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance.
|
||||||
@ -53,13 +47,6 @@ class CommercialPaper : Contract {
|
|||||||
// TODO: should reference the content of the legal agreement, not its URI
|
// TODO: should reference the content of the legal agreement, not its URI
|
||||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper")
|
override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper")
|
||||||
|
|
||||||
data class Terms(
|
|
||||||
val asset: Issued<Currency>,
|
|
||||||
val maturityDate: Instant
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun verify(tx: LedgerTransaction) = verifyClause(tx, Clauses.Group(), tx.commands.select<Commands>())
|
|
||||||
|
|
||||||
data class State(
|
data class State(
|
||||||
val issuance: PartyAndReference,
|
val issuance: PartyAndReference,
|
||||||
override val owner: AbstractParty,
|
override val owner: AbstractParty,
|
||||||
@ -67,13 +54,10 @@ class CommercialPaper : Contract {
|
|||||||
val maturityDate: Instant
|
val maturityDate: Instant
|
||||||
) : OwnableState, QueryableState, ICommercialPaperState {
|
) : OwnableState, QueryableState, ICommercialPaperState {
|
||||||
override val contract = CP_PROGRAM_ID
|
override val contract = CP_PROGRAM_ID
|
||||||
override val participants: List<AbstractParty>
|
override val participants = listOf(owner)
|
||||||
get() = listOf(owner)
|
|
||||||
|
|
||||||
val token: Issued<Terms>
|
|
||||||
get() = Issued(issuance, Terms(faceValue.token, maturityDate))
|
|
||||||
|
|
||||||
override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner))
|
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)"
|
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,
|
// 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 withFaceValue(newFaceValue: Amount<Issued<Currency>>): ICommercialPaperState = copy(faceValue = newFaceValue)
|
||||||
override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate)
|
override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate)
|
||||||
|
|
||||||
// DOCSTART VaultIndexedQueryCriteria
|
|
||||||
/** Object Relational Mapping support. */
|
/** Object Relational Mapping support. */
|
||||||
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(CommercialPaperSchemaV1)
|
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(CommercialPaperSchemaV1)
|
||||||
/** Additional used schemas would be added here (eg. CommercialPaperV2, ...) */
|
/** Additional used schemas would be added here (eg. CommercialPaperV2, ...) */
|
||||||
@ -104,49 +87,31 @@ class CommercialPaper : Contract {
|
|||||||
else -> throw IllegalArgumentException("Unrecognised schema $schema")
|
else -> throw IllegalArgumentException("Unrecognised schema $schema")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// DOCEND VaultIndexedQueryCriteria
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Clauses {
|
interface Commands : CommandData {
|
||||||
class Group : GroupClauseVerifier<State, Commands, Issued<Terms>>(
|
class Move : TypeOnlyCommandData(), Commands
|
||||||
AnyOf(
|
|
||||||
Redeem(),
|
class Redeem : TypeOnlyCommandData(), Commands
|
||||||
Move(),
|
// We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP.
|
||||||
Issue())) {
|
// However, nothing in the platform enforces that uniqueness: it's up to the issuer.
|
||||||
override fun groupStates(tx: LedgerTransaction): List<LedgerTransaction.InOutGroup<State, Issued<Terms>>>
|
class Issue : TypeOnlyCommandData(), Commands
|
||||||
= tx.groupStates<State, Issued<Terms>> { it.token }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Issue : AbstractIssue<State, Commands, Terms>(
|
override fun verify(tx: LedgerTransaction) {
|
||||||
{ map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() },
|
// Group by everything except owner: any modification to the CP at all is considered changing it fundamentally.
|
||||||
{ token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) {
|
val groups = tx.groupStates(State::withoutOwner)
|
||||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
|
||||||
|
|
||||||
override fun verify(tx: LedgerTransaction,
|
// There are two possible things that can be done with this CP. The first is trading it. The second is redeeming
|
||||||
inputs: List<State>,
|
// it for cash on or after the maturity date.
|
||||||
outputs: List<State>,
|
val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>()
|
||||||
commands: List<AuthenticatedObject<Commands>>,
|
val timeWindow: TimeWindow? = tx.timeWindow
|
||||||
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" }
|
// Suppress compiler warning as 'key' is an unused variable when destructuring 'groups'.
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
return consumedCommands
|
for ((inputs, outputs, key) in groups) {
|
||||||
}
|
when (command.value) {
|
||||||
}
|
is Commands.Move -> {
|
||||||
|
|
||||||
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()
|
val input = inputs.single()
|
||||||
requireThat {
|
requireThat {
|
||||||
"the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers)
|
"the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers)
|
||||||
@ -154,23 +119,10 @@ class CommercialPaper : Contract {
|
|||||||
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
|
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
|
||||||
// the input ignoring the owner field due to the grouping.
|
// the input ignoring the owner field due to the grouping.
|
||||||
}
|
}
|
||||||
return setOf(command.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Redeem : Clause<State, Commands, Issued<Terms>>() {
|
is Commands.Redeem -> {
|
||||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Redeem::class.java)
|
// Redemption of the paper requires movement of on-ledger cash.
|
||||||
|
|
||||||
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 input = inputs.single()
|
||||||
val received = tx.outputStates.sumCashBy(input.owner)
|
val received = tx.outputStates.sumCashBy(input.owner)
|
||||||
val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must have a time-window")
|
val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must have a time-window")
|
||||||
@ -180,17 +132,28 @@ class CommercialPaper : Contract {
|
|||||||
"the paper must be destroyed" using outputs.isEmpty()
|
"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)
|
"the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers)
|
||||||
}
|
}
|
||||||
|
|
||||||
return setOf(command.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Commands : CommandData {
|
// TODO: Think about how to evolve contracts over time with new commands.
|
||||||
data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands
|
else -> throw IllegalArgumentException("Unrecognised command")
|
||||||
class Redeem : TypeOnlyCommandData(), Commands
|
}
|
||||||
data class Issue(override val nonce: Long = random63BitValue()) : IssueCommand, Commands
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -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
|
* 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.
|
* 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 {
|
fun generateIssue(issuance: PartyAndReference, faceValue: Amount<Issued<Currency>>, maturityDate: Instant,
|
||||||
val state = TransactionState(State(issuance, issuance.party, faceValue, maturityDate), notary)
|
notary: Party): TransactionBuilder {
|
||||||
return TransactionBuilder(notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey))
|
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) {
|
fun generateMove(tx: TransactionBuilder, paper: StateAndRef<State>, newOwner: AbstractParty) {
|
||||||
tx.addInputState(paper)
|
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)
|
tx.addCommand(Commands.Move(), paper.state.data.owner.owningKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,15 +187,12 @@ class CommercialPaper : Contract {
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: VaultService) {
|
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: VaultService) {
|
||||||
// Add the cash movement using the states in our vault.
|
// 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, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner)
|
||||||
vault.generateSpend(tx, amount, paper.state.data.owner)
|
|
||||||
tx.addInputState(paper)
|
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.`owned by`(owner: AbstractParty) = copy(owner = owner)
|
||||||
infix fun CommercialPaper.State.`with notary`(notary: Party) = TransactionState(this, notary)
|
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
|
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.*
|
||||||
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.SecureHash
|
||||||
import net.corda.core.crypto.entropyToKeyPair
|
import net.corda.core.crypto.entropyToKeyPair
|
||||||
import net.corda.core.crypto.newSecureRandom
|
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.crypto.toBase58String
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.Emoji
|
||||||
import net.corda.core.schemas.MappedSchema
|
import net.corda.core.schemas.MappedSchema
|
||||||
import net.corda.core.schemas.PersistentState
|
import net.corda.core.schemas.PersistentState
|
||||||
import net.corda.core.schemas.QueryableState
|
import net.corda.core.schemas.QueryableState
|
||||||
import net.corda.core.serialization.CordaSerializable
|
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.internal.Emoji
|
|
||||||
import net.corda.schemas.CashSchemaV1
|
import net.corda.schemas.CashSchemaV1
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
|
import java.security.PublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -61,33 +54,11 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
|||||||
*/
|
*/
|
||||||
// DOCSTART 2
|
// DOCSTART 2
|
||||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html")
|
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html")
|
||||||
|
|
||||||
// DOCEND 2
|
// DOCEND 2
|
||||||
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Cash.Commands>>
|
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Cash.Commands>>
|
||||||
= commands.select<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
|
// DOCSTART 1
|
||||||
/** A state representing a cash claim against some party. */
|
/** A state representing a cash claim against some party. */
|
||||||
data class State(
|
data class State(
|
||||||
@ -174,8 +145,73 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
|||||||
override fun generateIssueCommand() = Commands.Issue()
|
override fun generateIssueCommand() = Commands.Issue()
|
||||||
override fun generateMoveCommand() = Commands.Move()
|
override fun generateMoveCommand() = Commands.Move()
|
||||||
|
|
||||||
override fun verify(tx: LedgerTransaction)
|
override fun verify(tx: LedgerTransaction) {
|
||||||
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
|
// 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.
|
// Small DSL extensions.
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
package net.corda.contracts.asset
|
package net.corda.contracts.asset
|
||||||
|
|
||||||
import net.corda.contracts.Commodity
|
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.*
|
||||||
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.SecureHash
|
||||||
import net.corda.core.crypto.newSecureRandom
|
import net.corda.core.crypto.newSecureRandom
|
||||||
import net.corda.core.identity.AbstractParty
|
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")
|
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 */
|
/** A state representing a commodity claim against some party */
|
||||||
data class State(
|
data class State(
|
||||||
override val amount: Amount<Issued<Commodity>>,
|
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>
|
data class Exit(override val amount: Amount<Issued<Commodity>>) : Commands, FungibleAsset.Commands.Exit<Commodity>
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun verify(tx: LedgerTransaction)
|
override fun verify(tx: LedgerTransaction) {
|
||||||
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
|
// 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>>
|
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Commands>>
|
||||||
= commands.select<CommodityContract.Commands>()
|
= commands.select<CommodityContract.Commands>()
|
||||||
|
@ -5,9 +5,7 @@ import net.corda.contracts.NetCommand
|
|||||||
import net.corda.contracts.NetType
|
import net.corda.contracts.NetType
|
||||||
import net.corda.contracts.NettableState
|
import net.corda.contracts.NettableState
|
||||||
import net.corda.contracts.asset.Obligation.Lifecycle.NORMAL
|
import net.corda.contracts.asset.Obligation.Lifecycle.NORMAL
|
||||||
import net.corda.contracts.clause.*
|
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.contracts.clauses.*
|
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.entropyToKeyPair
|
import net.corda.core.crypto.entropyToKeyPair
|
||||||
import net.corda.core.crypto.random63BitValue
|
import net.corda.core.crypto.random63BitValue
|
||||||
@ -30,6 +28,37 @@ import kotlin.collections.component1
|
|||||||
import kotlin.collections.component2
|
import kotlin.collections.component2
|
||||||
import kotlin.collections.set
|
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.
|
// 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>()
|
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")
|
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
|
* 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
|
* 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>>
|
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>(
|
override fun verify(tx: LedgerTransaction) {
|
||||||
Clauses.Net<Commands, P>(),
|
val netCommand = tx.commands.select<Commands.Net>().firstOrNull()
|
||||||
Clauses.Group<P>()
|
if (netCommand != null) {
|
||||||
), tx.commands.select<Obligation.Commands>())
|
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.
|
* A default command mutates inputs and produces identical outputs, except that the lifecycle changes.
|
||||||
|
@ -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 -> {
|
tx.tweak(tw -> {
|
||||||
tw.output(outState);
|
tw.output(outState);
|
||||||
// No command arguments
|
// 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 -> {
|
tx.tweak(tw -> {
|
||||||
tw.output(outState);
|
tw.output(outState);
|
||||||
@ -49,7 +49,7 @@ public class CashTestsJava {
|
|||||||
// with different overloads (for some reason).
|
// with different overloads (for some reason).
|
||||||
tw.output(CashKt.issuedBy(outState, getMINI_CORP()));
|
tw.output(CashKt.issuedBy(outState, getMINI_CORP()));
|
||||||
tw.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
|
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.
|
// Simple reallocation works.
|
||||||
|
@ -60,16 +60,16 @@ class KotlinCommercialPaperTest : ICommercialPaperTestTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class KotlinCommercialPaperLegacyTest : ICommercialPaperTestTemplate {
|
class KotlinCommercialPaperLegacyTest : ICommercialPaperTestTemplate {
|
||||||
override fun getPaper(): ICommercialPaperState = CommercialPaperLegacy.State(
|
override fun getPaper(): ICommercialPaperState = CommercialPaper.State(
|
||||||
issuance = MEGA_CORP.ref(123),
|
issuance = MEGA_CORP.ref(123),
|
||||||
owner = MEGA_CORP,
|
owner = MEGA_CORP,
|
||||||
faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123),
|
faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123),
|
||||||
maturityDate = TEST_TX_TIME + 7.days
|
maturityDate = TEST_TX_TIME + 7.days
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun getIssueCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Issue()
|
override fun getIssueCommand(notary: Party): CommandData = CommercialPaper.Commands.Issue()
|
||||||
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Redeem()
|
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaper.Commands.Redeem()
|
||||||
override fun getMoveCommand(): CommandData = CommercialPaperLegacy.Commands.Move()
|
override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move()
|
||||||
}
|
}
|
||||||
|
|
||||||
@RunWith(Parameterized::class)
|
@RunWith(Parameterized::class)
|
||||||
|
@ -1,28 +1,21 @@
|
|||||||
package net.corda.contracts.asset
|
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.*
|
||||||
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.SecureHash
|
||||||
import net.corda.core.crypto.newSecureRandom
|
import net.corda.core.crypto.newSecureRandom
|
||||||
import net.corda.core.crypto.toBase58String
|
import net.corda.core.crypto.toBase58String
|
||||||
import net.corda.core.identity.AbstractParty
|
import net.corda.core.identity.AbstractParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
|
import net.corda.core.internal.Emoji
|
||||||
import net.corda.core.schemas.MappedSchema
|
import net.corda.core.schemas.MappedSchema
|
||||||
import net.corda.core.schemas.PersistentState
|
import net.corda.core.schemas.PersistentState
|
||||||
import net.corda.core.schemas.QueryableState
|
import net.corda.core.schemas.QueryableState
|
||||||
import net.corda.core.serialization.CordaSerializable
|
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.internal.Emoji
|
|
||||||
import net.corda.schemas.SampleCashSchemaV1
|
import net.corda.schemas.SampleCashSchemaV1
|
||||||
import net.corda.schemas.SampleCashSchemaV2
|
import net.corda.schemas.SampleCashSchemaV2
|
||||||
import net.corda.schemas.SampleCashSchemaV3
|
import net.corda.schemas.SampleCashSchemaV3
|
||||||
|
import java.security.PublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class DummyFungibleContract : OnLedgerAsset<Currency, DummyFungibleContract.Commands, DummyFungibleContract.State>() {
|
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>>
|
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<DummyFungibleContract.Commands>>
|
||||||
= commands.select<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(
|
data class State(
|
||||||
override val amount: Amount<Issued<Currency>>,
|
override val amount: Amount<Issued<Currency>>,
|
||||||
|
|
||||||
@ -129,7 +99,69 @@ class DummyFungibleContract : OnLedgerAsset<Currency, DummyFungibleContract.Comm
|
|||||||
override fun generateIssueCommand() = Commands.Issue()
|
override fun generateIssueCommand() = Commands.Issue()
|
||||||
override fun generateMoveCommand() = Commands.Move()
|
override fun generateMoveCommand() = Commands.Move()
|
||||||
|
|
||||||
override fun verify(tx: LedgerTransaction)
|
override fun verify(tx: LedgerTransaction) {
|
||||||
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
|
|
||||||
|
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,6 +70,7 @@ class CashTests : TestDependencyInjectionBase() {
|
|||||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
||||||
vaultService.notifyAll(txs.map { it.tx })
|
vaultService.notifyAll(txs.map { it.tx })
|
||||||
}
|
}
|
||||||
|
|
||||||
override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
|
override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +101,7 @@ class CashTests : TestDependencyInjectionBase() {
|
|||||||
tweak {
|
tweak {
|
||||||
output { outState }
|
output { outState }
|
||||||
// No command arguments
|
// 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 {
|
tweak {
|
||||||
output { outState }
|
output { outState }
|
||||||
@ -111,7 +112,7 @@ class CashTests : TestDependencyInjectionBase() {
|
|||||||
output { outState }
|
output { outState }
|
||||||
output { outState `issued by` MINI_CORP }
|
output { outState `issued by` MINI_CORP }
|
||||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
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.
|
// Simple reallocation works.
|
||||||
tweak {
|
tweak {
|
||||||
@ -130,7 +131,7 @@ class CashTests : TestDependencyInjectionBase() {
|
|||||||
output { outState }
|
output { outState }
|
||||||
command(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
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() }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||||
tweak {
|
tweak {
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||||
this `fails with` "List has more than one element."
|
this `fails with` "there is only a single issue command"
|
||||||
}
|
|
||||||
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.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
@ -372,7 +365,7 @@ class CashTests : TestDependencyInjectionBase() {
|
|||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
|
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 {
|
tweak {
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
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.days
|
||||||
import net.corda.core.utilities.hours
|
import net.corda.core.utilities.hours
|
||||||
import net.corda.testing.*
|
import net.corda.testing.*
|
||||||
import org.junit.After
|
|
||||||
import net.corda.testing.contracts.DummyState
|
import net.corda.testing.contracts.DummyState
|
||||||
import net.corda.testing.node.MockServices
|
import net.corda.testing.node.MockServices
|
||||||
|
import org.junit.After
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
@ -77,7 +77,7 @@ class ObligationTests {
|
|||||||
tweak {
|
tweak {
|
||||||
output { outState }
|
output { outState }
|
||||||
// No command arguments
|
// 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 {
|
tweak {
|
||||||
output { outState }
|
output { outState }
|
||||||
@ -88,7 +88,7 @@ class ObligationTests {
|
|||||||
output { outState }
|
output { outState }
|
||||||
output { outState `issued by` MINI_CORP }
|
output { outState `issued by` MINI_CORP }
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
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.
|
// Simple reallocation works.
|
||||||
tweak {
|
tweak {
|
||||||
@ -107,7 +107,7 @@ class ObligationTests {
|
|||||||
output { outState }
|
output { outState }
|
||||||
command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() }
|
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
|
// 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() }
|
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||||
tweak {
|
tweak {
|
||||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue() }
|
||||||
this `fails with` "List has more than one element."
|
this `fails with` "there is only a single issue command"
|
||||||
}
|
|
||||||
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.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
@ -668,7 +660,7 @@ class ObligationTests {
|
|||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token)) }
|
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 {
|
tweak {
|
||||||
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
command(CHARLIE.owningKey) { Obligation.Commands.Move() }
|
||||||
|
@ -490,7 +490,7 @@ class TwoPartyTradeFlowTests {
|
|||||||
fun `dependency with error on buyer side`() {
|
fun `dependency with error on buyer side`() {
|
||||||
mockNet = MockNetwork(false)
|
mockNet = MockNetwork(false)
|
||||||
ledger(initialiseSerialization = 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`() {
|
fun `dependency with error on seller side`() {
|
||||||
mockNet = MockNetwork(false)
|
mockNet = MockNetwork(false)
|
||||||
ledger(initialiseSerialization = false) {
|
ledger(initialiseSerialization = false) {
|
||||||
runWithError(false, true, "Issuances must have a time-window")
|
runWithError(false, true, "Issuances have a time-window")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user