mirror of
https://github.com/corda/corda.git
synced 2025-06-21 00:23:09 +00:00
Improve the contracts API and Cash contract a bit, and beef up the unit tests.
Better usage of generics in groupStates so the group exposes the grouping key that was used, this avoids constructs like `val issuer = outputs[0].deposit.party` which is a bit ugly.
This commit is contained in:
@ -138,13 +138,13 @@ public class JavaCommercialPaper implements Contract {
|
||||
// Issuance, trading (aka moving in this prototype) and redeeming.
|
||||
// Each command has it's own set of restrictions which the verify function ... verifies.
|
||||
|
||||
List<InOutGroup<State>> groups = tx.groupStates(State.class, State::withoutOwner);
|
||||
List<InOutGroup<State, State>> groups = tx.groupStates(State.class, State::withoutOwner);
|
||||
|
||||
// Find the command that instructs us what to do and check there's exactly one.
|
||||
|
||||
AuthenticatedObject<CommandData> cmd = requireSingleCommand(tx.getCommands(), JavaCommercialPaper.Commands.class);
|
||||
|
||||
for (InOutGroup<State> group : groups) {
|
||||
for (InOutGroup<State, State> group : groups) {
|
||||
List<State> inputs = group.getInputs();
|
||||
List<State> outputs = group.getOutputs();
|
||||
|
||||
@ -164,7 +164,7 @@ public class JavaCommercialPaper implements Contract {
|
||||
|
||||
Instant time = timestampCommand.getBefore();
|
||||
|
||||
if (!time.isBefore(output.maturityDate)) {
|
||||
if (time == null || !time.isBefore(output.maturityDate)) {
|
||||
throw new IllegalStateException("Failed Requirement: the maturity date is not in the past");
|
||||
}
|
||||
|
||||
@ -197,7 +197,7 @@ public class JavaCommercialPaper implements Contract {
|
||||
|
||||
if (!received.equals(input.getFaceValue()))
|
||||
throw new IllegalStateException("Failed Requirement: received amount equals the face value");
|
||||
if (time.isBefore(input.getMaturityDate()))
|
||||
if (time == null || time.isBefore(input.getMaturityDate()))
|
||||
throw new IllegalStateException("Failed requirement: the paper must have matured");
|
||||
if (!input.getFaceValue().equals(received))
|
||||
throw new IllegalStateException("Failed requirement: the received amount equals the face value");
|
||||
|
@ -82,53 +82,50 @@ class Cash : Contract {
|
||||
override fun verify(tx: TransactionForVerification) {
|
||||
// Each group is a set of input/output states with distinct (deposit, currency) attributes. These types
|
||||
// of cash are not fungible and must be kept separated for bookkeeping purposes.
|
||||
val groups = tx.groupStates<Cash.State>() { Pair(it.deposit, it.amount.currency) }
|
||||
val groups = tx.groupStates() { it: Cash.State -> Pair(it.deposit, it.amount.currency) }
|
||||
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
// Either inputs or outputs could be empty.
|
||||
val (deposit, currency) = key
|
||||
val issuer = deposit.party
|
||||
|
||||
for ((inputs, outputs) in groups) {
|
||||
requireThat {
|
||||
"all outputs represent at least one penny" by outputs.none { it.amount.pennies == 0L }
|
||||
"there are no zero sized outputs" by outputs.none { it.amount.pennies == 0L }
|
||||
}
|
||||
|
||||
|
||||
val issueCommand = tx.commands.select<Commands.Issue>().singleOrNull()
|
||||
if (issueCommand != null) {
|
||||
if (issueCommand != null && outputs.isNotEmpty()) {
|
||||
// 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. 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
|
||||
// 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.
|
||||
val outputsInstitution = outputs.map { it.deposit.party }.distinct().singleOrNull()
|
||||
if (outputsInstitution != null) {
|
||||
requireThat {
|
||||
"the issue command has a nonce" by (issueCommand.value.nonce != 0L)
|
||||
"output deposits are owned by a command signer" by
|
||||
outputs.all { issueCommand.signingParties.contains(it.deposit.party) }
|
||||
"there are no inputs in this group" by inputs.isEmpty()
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
// There was an issue command, but it wasn't signed for this group. It may apply to other
|
||||
// groups.
|
||||
|
||||
// The grouping ensures that all outputs have the same deposit reference and currency.
|
||||
val inputAmount = inputs.sumCashOrZero(currency)
|
||||
val outputAmount = outputs.sumCash()
|
||||
requireThat {
|
||||
"the issue command has a nonce" by (issueCommand.value.nonce != 0L)
|
||||
"output deposits are owned by a command signer" by (issuer in issueCommand.signingParties)
|
||||
"output values sum to more than the inputs" by (outputAmount > inputAmount)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
val inputAmount = inputs.sumCashOrNull() ?: throw IllegalArgumentException("there is at least one cash input for this group")
|
||||
val outputAmount = outputs.sumCashOrZero(inputAmount.currency)
|
||||
val outputAmount = outputs.sumCashOrZero(currency)
|
||||
|
||||
val deposit = inputs.first().deposit
|
||||
// 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 exitCommand = tx.commands.select<Commands.Exit>(party = issuer).singleOrNull()
|
||||
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, currency)
|
||||
|
||||
requireThat {
|
||||
"there is at least one cash input" by inputs.isNotEmpty()
|
||||
"there are no zero sized inputs" by inputs.none { it.amount.pennies == 0L }
|
||||
"there are no zero sized outputs" by outputs.none { it.amount.pennies == 0L }
|
||||
"all outputs in this group use the currency of the inputs" by
|
||||
outputs.all { it.amount.currency == inputAmount.currency }
|
||||
}
|
||||
|
||||
val exitCommand = tx.commands.select<Commands.Exit>(party = deposit.party).singleOrNull()
|
||||
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, inputAmount.currency)
|
||||
|
||||
requireThat {
|
||||
"for deposit ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by
|
||||
(inputAmount == outputAmount + amountExitingLedger)
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ class CommercialPaper : Contract {
|
||||
|
||||
override fun verify(tx: TransactionForVerification) {
|
||||
// Group by everything except owner: any modification to the CP at all is considered changing it fundamentally.
|
||||
val groups = tx.groupStates<State>() { it.withoutOwner() }
|
||||
val groups = tx.groupStates() { it: State -> it.withoutOwner() }
|
||||
|
||||
// There are two possible things that can be done with this CP. The first is trading it. The second is redeeming
|
||||
// it for cash on or after the maturity date.
|
||||
@ -78,39 +78,43 @@ class CommercialPaper : Contract {
|
||||
// who or what is a trusted authority.
|
||||
val timestamp: TimestampCommand? = tx.commands.getTimestampByName("Mock Company 0", "Timestamping Service", "Bank A")
|
||||
|
||||
for (group in groups) {
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
when (command.value) {
|
||||
is Commands.Move -> {
|
||||
val input = group.inputs.single()
|
||||
val input = inputs.single()
|
||||
requireThat {
|
||||
"the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
|
||||
"the state is propagated" by (group.outputs.size == 1)
|
||||
"the transaction is signed by the owner of the CP" by (input.owner in command.signers)
|
||||
"the state is propagated" by (outputs.size == 1)
|
||||
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
|
||||
// the input ignoring the owner field due to the grouping.
|
||||
}
|
||||
}
|
||||
|
||||
// Redemption of the paper requires movement of on-ledger cash.
|
||||
is Commands.Redeem -> {
|
||||
val input = group.inputs.single()
|
||||
val input = inputs.single()
|
||||
val received = tx.outStates.sumCashBy(input.owner)
|
||||
val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped")
|
||||
requireThat {
|
||||
"the paper must have matured" by (time > input.maturityDate)
|
||||
"the paper must have matured" by (time >= input.maturityDate)
|
||||
"the received amount equals the face value" by (received == input.faceValue)
|
||||
"the paper must be destroyed" by group.outputs.isEmpty()
|
||||
"the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
|
||||
"the paper must be destroyed" by outputs.isEmpty()
|
||||
"the transaction is signed by the owner of the CP" by (input.owner in command.signers)
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.Issue -> {
|
||||
val output = group.outputs.single()
|
||||
val output = outputs.single()
|
||||
val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped")
|
||||
requireThat {
|
||||
// Don't allow people to issue commercial paper under other entities identities.
|
||||
"the issuance is signed by the claimed issuer of the paper" by
|
||||
(command.signers.contains(output.issuance.party.owningKey))
|
||||
(output.issuance.party.owningKey in command.signers)
|
||||
"the face value is not zero" by (output.faceValue.pennies > 0)
|
||||
"the maturity date is not in the past" by (time < output.maturityDate)
|
||||
// Don't allow an existing CP state to be replaced by this issuance.
|
||||
"there is no input state" by group.inputs.isEmpty()
|
||||
// TODO: Consider how to handle the case of mistaken issuances, or other need to patch.
|
||||
"there is no input state" by inputs.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user