Merge branch 'master' into sofus-generic-contract

This commit is contained in:
sofusmortensen
2016-07-10 12:13:32 +02:00
1173 changed files with 28584 additions and 5130 deletions

View File

@ -1,819 +0,0 @@
package com.r3corda.contracts
import com.google.common.annotations.VisibleForTesting
import com.r3corda.contracts.cash.*
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.random63BitValue
import com.r3corda.core.utilities.Emoji
import com.r3corda.core.utilities.NonEmptySet
import java.security.PublicKey
import java.time.Duration
import java.time.Instant
import java.util.*
// 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>()
/**
* A cash settlement contract commits the issuer to delivering a specified amount of cash (represented as the [Cash]
* contract) at a specified future point in time. Similarly to cash, settlement transactions may split and merge
* contracts across multiple input and output states.
*
* The goal of this design is to handle money owed, and these contracts are expected to be netted/merged, with
* settlement only for any remainder amount.
*
* @param P the product the obligation is for payment of.
*/
class Obligation<P> : Contract {
/**
* TODO:
* 1) hash should be of the contents, not the URI
* 2) allow the content to be specified at time of instance creation?
*
* Motivation: it's the difference between a state object referencing a programRef, which references a
* legalContractReference and a state object which directly references both. The latter allows the legal wording
* to evolve without requiring code changes. But creates a risk that users create objects governed by a program
* that is inconsistent with the legal contract
*/
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.example.gov/cash-settlement.html")
/**
* 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
* represented by absence of the state on transaction output.
*/
enum class Lifecycle {
/** Default lifecycle state for a contract, in which it can be settled normally */
NORMAL,
/**
* Indicates the contract has not been settled by its due date. Once in the defaulted state,
* it can only be reverted to [NORMAL] state by the owner.
*/
DEFAULTED
}
/**
* Common interface for the state subsets used when determining nettability of two or more states. Exposes the
* underlying issued thing.
*/
interface NetState<P> {
val issued: Issued<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>(
val partyKeys: Set<PublicKey>,
val issuanceDef: StateTemplate<P>
) : NetState<P> {
override val issued: Issued<P>
get() = issuanceDef.issued
}
/**
* 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>(
val issuanceDef: StateTemplate<P>
) : NetState<P> {
override val issued: Issued<P>
get() = issuanceDef.issued
}
/**
* Subset of state, containing the elements specified when issuing a new settlement contract.
*
* @param P the product the obligation is for payment of.
*/
data class StateTemplate<P>(
/** The hash of the cash contract we're willing to accept in payment for this debt. */
val acceptableContracts: NonEmptySet<SecureHash>,
/** The parties whose cash we are willing to accept in payment for this debt. */
val acceptableIssuanceDefinitions: NonEmptySet<Issued<P>>,
/** When the contract must be settled by. */
val dueBefore: Instant,
val timeTolerance: Duration = Duration.ofSeconds(30)
) {
val issued: Issued<P>
get() = acceptableIssuanceDefinitions.toSet().single()
}
/**
* Subset of state, containing the elements specified when issuing a new settlement contract.
* TODO: This needs to be something common to contracts that we can be obliged to pay, and moved
* out into core accordingly.
*
* @param P the product the obligation is for payment of.
*/
data class IssuanceDefinition<P>(
val issuer: Party,
val template: StateTemplate<P>
) {
val currency: P
get() = template.issued.product
val issued: Issued<P>
get() = template.issued
}
/**
* A state representing the obligation of one party (issuer) to deliver a specified number of
* units of an underlying asset (described as issuanceDef.acceptableCashIssuance) to the owner
* no later than the specified time.
*
* @param P the product the obligation is for payment of.
*/
data class State<P>(
var lifecycle: Lifecycle = Lifecycle.NORMAL,
/** Where the debt originates from (issuer) */
val issuer: Party,
val template: StateTemplate<P>,
val quantity: Long,
/** The public key of the entity the contract pays to */
override val owner: PublicKey
) : FungibleAssetState<P, IssuanceDefinition<P>>, BilateralNettableState<State<P>> {
override val amount: Amount<Issued<P>>
get() = Amount(quantity, template.issued)
override val contract = OBLIGATION_PROGRAM_ID
val acceptableContracts: NonEmptySet<SecureHash>
get() = template.acceptableContracts
val acceptableIssuanceDefinitions: NonEmptySet<*>
get() = template.acceptableIssuanceDefinitions
val dueBefore: Instant
get() = template.dueBefore
override val issuanceDef: IssuanceDefinition<P>
get() = IssuanceDefinition(issuer, template)
override val participants: List<PublicKey>
get() = listOf(issuer.owningKey, owner)
override fun move(amount: Amount<Issued<P>>, owner: PublicKey): Obligation.State<P>
= copy(quantity = amount.quantity, owner = owner)
override fun toString() = when (lifecycle) {
Lifecycle.NORMAL -> "${Emoji.bagOfCash}Debt($amount due $dueBefore to ${owner.toStringShort()})"
Lifecycle.DEFAULTED -> "${Emoji.bagOfCash}Debt($amount unpaid by $dueBefore to ${owner.toStringShort()})"
}
override val bilateralNetState: BilateralNetState<P>
get() {
check(lifecycle == Lifecycle.NORMAL)
return BilateralNetState(setOf(issuer.owningKey, owner), template)
}
val multilateralNetState: MultilateralNetState<P>
get() {
check(lifecycle == Lifecycle.NORMAL)
return MultilateralNetState(template)
}
override fun net(other: State<P>): State<P> {
val netA = bilateralNetState
val netB = other.bilateralNetState
require(netA == netB) { "net substates of the two state objects must be identical" }
if (issuer.owningKey == other.issuer.owningKey) {
// Both sides are from the same issuer to owner
return copy(quantity = quantity + other.quantity)
} else {
// Issuer and owner are backwards
return copy(quantity = quantity - other.quantity)
}
}
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(issuanceDef), copy(owner = newOwner))
}
/** Interface for commands that apply to aggregated states */
interface AggregateCommands<P> : CommandData {
val aggregateState: IssuanceDefinition<P>
}
// Just for grouping
interface Commands : CommandData {
/**
* Net two or more cash settlement states together in a close-out netting style. Limited to bilateral netting
* as only the owner (not the issuer) needs to sign.
*/
data class Net(val type: NetType) : Commands
/**
* A command stating that a debt has been moved, optionally to fulfil another contract.
*
* @param contractHash the hash of contract's code, which indicates to that contract that the
* obligation states moved in this transaction are for their sole attention.
* This is a single value to ensure the same state(s) cannot be used to settle multiple contracts.
* May be null, if this is not relevant to any other contract in the same transaction.
*/
data class Move<P>(override val aggregateState: IssuanceDefinition<P>,
override val contractHash: SecureHash? = null) : Commands, AggregateCommands<P>, MoveCommand
/**
* Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction
* has a unique ID even when there are no inputs.
*/
data class Issue<P>(override val aggregateState: IssuanceDefinition<P>,
val nonce: Long = random63BitValue()) : Commands, AggregateCommands<P>
/**
* A command stating that the issuer is settling some or all of the amount owed by paying in a suitable cash
* contract. If this reduces the balance to zero, the contract moves to the settled state.
* @see [Cash.Commands.Move]
*/
data class Settle<P>(override val aggregateState: IssuanceDefinition<P>,
val amount: Amount<Issued<P>>) : Commands, AggregateCommands<P>
/**
* A command stating that the owner is moving the contract into the defaulted state as it has not been settled
* by the due date, or resetting a defaulted contract back to the issued state.
*/
data class SetLifecycle<P>(override val aggregateState: IssuanceDefinition<P>,
val lifecycle: Lifecycle) : Commands, AggregateCommands<P>
/**
* A command stating that the debt is being released by the owner. Normally would indicate
* either settlement outside of the ledger, or that the issuer is unable to pay.
*/
data class Exit<P>(override val aggregateState: IssuanceDefinition<P>,
val amount: Amount<Issued<P>>) : Commands, AggregateCommands<P>
}
/** This is the function EVERYONE runs */
override fun verify(tx: TransactionForContract) {
val commands = tx.commands.select<Obligation.Commands>()
// Net commands are special, and cross issuance definitions, so handle them first
val netCommands = commands.select<Obligation.Commands.Net>()
if (netCommands.isNotEmpty()) {
val netCommand = netCommands.single()
val groups = when (netCommand.value.type) {
NetType.CLOSE_OUT -> tx.groupStates { it: State<P> -> it.bilateralNetState }
NetType.PAYMENT -> tx.groupStates { it: State<P> -> it.multilateralNetState }
}
for ((inputs, outputs, key) in groups) {
verifyNetCommand(inputs, outputs, netCommand, key)
}
} else {
val commandGroups = tx.groupCommands<AggregateCommands<P>, IssuanceDefinition<P>> { it.value.aggregateState }
// Each group is a set of input/output states with distinct issuance definitions. These types
// of settlement are not fungible and must be kept separated for bookkeeping purposes.
val groups = tx.groupStates() { it: State<P> -> it.issuanceDef }
for ((inputs, outputs, key) in groups) {
// Either inputs or outputs could be empty.
val issuer = key.issuer
val commands = commandGroups[key] ?: emptyList()
requireThat {
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
}
verifyCommandGroup(tx, commands, inputs, outputs, issuer, key)
}
}
}
private fun verifyCommandGroup(tx: TransactionForContract,
commands: List<AuthenticatedObject<AggregateCommands<P>>>,
inputs: List<State<P>>,
outputs: List<State<P>>,
issuer: Party,
key: IssuanceDefinition<P>) {
// We've already pre-grouped by currency amongst other fields, and verified above that every state specifies
// at least one acceptable cash issuance definition, so we can just use the first issuance definition to
// determine currency
val currency = key.template.acceptableIssuanceDefinitions.first()
// Issue, default, net and settle commands are all single commands (there's only ever one of them, and
// they exclude all other commands).
val issueCommand = commands.select<Commands.Issue<P>>().firstOrNull()
val defaultCommand = commands.select<Commands.SetLifecycle<P>>().firstOrNull()
val settleCommand = commands.select<Commands.Settle<P>>().firstOrNull()
if (commands.size != 1) {
// Only commands can be move/exit
require(commands.map { it.value }.all { it is Commands.Move || it is Commands.Exit })
{ "only move/exit commands can be present along with other obligation commands" }
}
// Issue, default and net commands are special, and do not follow normal input/output summing rules, so
// deal with them first
if (defaultCommand != null) {
verifyDefaultCommand(inputs, outputs, tx, defaultCommand)
} else {
// Only the default command processes inputs/outputs that are not in the normal state
// TODO: Need to be able to exit defaulted amounts
requireThat {
"all inputs are in the normal state " by inputs.all { it.lifecycle == Lifecycle.NORMAL }
"all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL }
}
if (issueCommand != null) {
verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer)
} else if (settleCommand != null) {
// Perhaps through an abundance of caution, settlement is enforced as its own command.
// This could perhaps be merged into verifyBalanceChange() later, however doing so introduces a lot
// of scope for making it more opaque what's going on in a transaction and whether it's as expected
// by all parties.
verifySettleCommand(inputs, outputs, tx, settleCommand, currency, issuer, key)
} else {
verifyBalanceChange(inputs, outputs, commands, currency, issuer)
}
}
}
/**
* Verify simple lifecycle changes for settlement contracts, handling exit and move commands.
*
* @param commands a list of commands filtered to those matching issuance definition for the provided inputs and
* outputs.
*/
private fun verifyBalanceChange(inputs: List<State<P>>,
outputs: List<State<P>>,
commands: List<AuthenticatedObject<AggregateCommands<P>>>,
currency: Issued<P>,
issuer: Party) {
// Sum up how much settlement owed there is in the inputs, and the difference in outputs. The difference should
// be matched by exit commands representing the extracted amount.
val inputAmount = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
val outputAmount = outputs.sumObligationsOrZero(currency)
val exitCommands = commands.select<Commands.Exit<P>>()
val requiredExitSignatures = HashSet<PublicKey>()
val amountExitingLedger: Amount<Issued<P>> = if (exitCommands.isNotEmpty()) {
require(exitCommands.size == 1) { "There can only be one exit command" }
val exitCommand = exitCommands.single()
// If we want to remove debt from the ledger, that must be signed for by the owner. For now we require exit
// commands to be signed by all input owners, unlocking the full input amount, rather than trying to detangle
// exactly who exited what.
requiredExitSignatures.addAll(inputs.map { it.owner })
exitCommand.value.amount
} else {
Amount(0, currency)
}
requireThat {
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
"at issuer ${issuer.name} the amounts balance" by
(inputAmount == outputAmount + amountExitingLedger)
}
verifyMoveCommand<Commands.Move<P>>(inputs, commands)
}
/**
* A default command mutates inputs and produces identical outputs, except that the lifecycle changes.
*/
@VisibleForTesting
protected fun verifyDefaultCommand(inputs: List<State<P>>,
outputs: List<State<P>>,
tx: TransactionForContract,
setLifecycleCommand: AuthenticatedObject<Commands.SetLifecycle<P>>) {
// Default must not change anything except lifecycle, so number of inputs and outputs must match
// exactly.
require(inputs.size == outputs.size) { "Number of inputs and outputs must match" }
// If we have an default command, perform special processing: issued contracts can only be defaulted
// after the due date, and default/reset can only be done by the owner
val expectedOutputState: Lifecycle = setLifecycleCommand.value.lifecycle
val expectedInputState: Lifecycle
expectedInputState = when (expectedOutputState) {
Lifecycle.DEFAULTED -> Lifecycle.NORMAL
Lifecycle.NORMAL -> Lifecycle.DEFAULTED
}
// Check that we're past the deadline for ALL involved inputs, and that the output states correspond 1:1
for ((stateIdx, input) in inputs.withIndex()) {
val actualOutput = outputs[stateIdx]
val deadline = input.dueBefore
val timestamp: TimestampCommand? = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A")
val expectedOutput: State<P> = input.copy(lifecycle = expectedOutputState)
requireThat {
"there is a timestamp from the authority" by (timestamp != null)
"the due date has passed" by (timestamp?.after?.isBefore(deadline) ?: false)
"input state lifecycle is correct" by (input.lifecycle == expectedInputState)
"output state corresponds exactly to input state, with lifecycle changed" by (expectedOutput == actualOutput)
}
}
val owningPubKeys = inputs.map { it.owner }.toSet()
val keysThatSigned = setLifecycleCommand.signers.toSet()
requireThat {
"the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys)
}
}
@VisibleForTesting
protected fun verifyIssueCommand(inputs: List<State<P>>,
outputs: List<State<P>>,
tx: TransactionForContract,
issueCommand: AuthenticatedObject<Commands.Issue<P>>,
currency: Issued<P>,
issuer: Party) {
// If we have an issue command, perform special processing: the group is must have no inputs,
// and that signatures are present for all issuers.
val inputAmount = inputs.sumObligationsOrZero(currency)
val outputAmount = outputs.sumObligations<P>()
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)
"valid settlement issuance definition is not this issuance definition" by inputs.none { it.issuanceDef in it.acceptableIssuanceDefinitions }
}
}
/**
* Verify a netting command. This handles both close-out and payment netting.
*/
@VisibleForTesting
protected fun verifyNetCommand(inputs: Iterable<State<P>>,
outputs: Iterable<State<P>>,
command: AuthenticatedObject<Commands.Net>,
netState: NetState<P>) {
// TODO: Can we merge this with the checks for aggregated commands?
requireThat {
"all inputs are in the normal state " by inputs.all { it.lifecycle == Lifecycle.NORMAL }
"all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL }
}
val token = netState.issued
// Create two maps of balances from issuers to owners, one for input states, the other for output states.
val inputBalances = extractAmountsDue(token, inputs)
val outputBalances = extractAmountsDue(token, 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 expected token" by (inputs.all { it.issuanceDef.issued == token })
"all output states use the expected token" by (outputs.all { it.issuanceDef.issued == token })
"amounts owed on input and output must match" by (sumAmountsDue(inputBalances) == sumAmountsDue(outputBalances))
}
// TODO: Handle proxies nominated by parties, i.e. a central clearing service
val involvedParties = inputs.map { it.owner }.union(inputs.map { it.issuer.owningKey }).toSet()
when (command.value.type) {
// For close-out netting, allow any involved party to sign
NetType.CLOSE_OUT -> require(involvedParties.intersect(command.signers).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" }
}
}
/**
* Verify settlement of state objects.
*/
private fun verifySettleCommand(inputs: List<State<P>>,
outputs: List<State<P>>,
tx: TransactionForContract,
command: AuthenticatedObject<Commands.Settle<P>>,
currency: Issued<P>,
issuer: Party,
key: IssuanceDefinition<P>) {
val template = key.template
val inputAmount = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
val outputAmount = outputs.sumObligationsOrZero(currency)
// Sum up all cash contracts that are moving and fulfil our requirements
// The cash contract verification handles ensuring there's inputs enough to cover the output states, we only
// care about counting how much cash 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 owner, as it's presumed the owners have enough sense to do that themselves.
// Therefore if someone actually signed the following transaction:
//
// Inputs:
// £1m cash owned by B
// £1m owed from A to B
// Outputs:
// £1m cash owned by B
// Commands:
// Settle (signed by B)
// Move (signed by B)
//
// That would pass this check. Ensuring they do not is best addressed in the transaction generation stage.
val cashStates = tx.outStates.filterIsInstance<FungibleAssetState<*, *>>()
val acceptableCashStates = cashStates
// TODO: This filter is nonsense, because it just checks there is a cash contract loaded, we need to
// verify the cash contract is the cash contract we expect.
// Something like:
// attachments.mustHaveOneOf(key.acceptableCashContract)
.filter { it.contract.legalContractReference in template.acceptableContracts }
// Restrict the states to those of the correct issuance definition (this normally
// covers currency and issuer, but is opaque to us)
.filter { it.issuanceDef in template.acceptableIssuanceDefinitions }
// Catch that there's nothing useful here, so we can dump out a useful error
requireThat {
"there are cash state outputs" by (cashStates.size > 0)
"there are defined acceptable cash states" by (acceptableCashStates.size > 0)
}
val amountReceivedByOwner = acceptableCashStates.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.issuer.owningKey }.toSet()
for ((owner, obligations) in inputs.groupBy { it.owner }) {
val settled = amountReceivedByOwner[owner]?.sumCashOrNull()
if (settled != null) {
val debt = obligations.sumObligationsOrZero(currency)
require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" }
totalPenniesSettled += settled.quantity
}
}
// Insist that we can be the only contract consuming inputs, to ensure no other contract can think it's being
// settled as well
requireThat {
"all move commands relate to this contract" by (moveCommands.map { it.value.contractHash }
.all { it == null || it == legalContractReference })
"contract does not try to consume itself" by (moveCommands.map { it.value }.filterIsInstance<Commands.Move<P>>()
.none { it.aggregateState.issued in template.acceptableIssuanceDefinitions })
"amounts paid must match recipients to settle" by inputs.map { it.owner }.containsAll(amountReceivedByOwner.keys)
"signatures are present from all issuers" by command.signers.containsAll(requiredSigners)
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
"at issuer ${issuer.name} the obligations after settlement balance" by
(inputAmount == outputAmount + Amount(totalPenniesSettled, currency))
}
}
/**
* Generate a transaction performing close-out netting of two or more states.
*
* @param signer the party who will sign the transaction. Must be one of the issuer or owner.
* @param states two or more states, which must be compatible for bilateral netting (same issuance definitions,
* and same parties involved).
*/
fun generateCloseOutNetting(tx: TransactionBuilder,
signer: PublicKey,
vararg states: State<P>) {
val netState = states.firstOrNull()?.bilateralNetState
requireThat {
"at least two states are provided" by (states.size >= 2)
"all states are in the normal lifecycle state " by (states.all { it.lifecycle == Lifecycle.NORMAL })
"all states must be bilateral nettable" by (states.all { it.bilateralNetState == netState })
"signer is in the state parties" by (signer in netState!!.partyKeys)
}
tx.addOutputState(states.reduce { stateA, stateB -> stateA.net(stateB) })
tx.addCommand(Commands.Net(NetType.PAYMENT), signer)
}
/**
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
*/
fun generateIssue(tx: TransactionBuilder,
issuer: Party,
issuanceDef: StateTemplate<P>,
pennies: Long,
owner: PublicKey,
notary: Party) {
check(tx.inputStates().isEmpty())
check(tx.outputStates().map { it.data }.sumObligationsOrNull<P>() == null)
val aggregateState = IssuanceDefinition(issuer, issuanceDef)
tx.addOutputState(State(Lifecycle.NORMAL, issuer, issuanceDef, pennies, owner), notary)
tx.addCommand(Commands.Issue(aggregateState), issuer.owningKey)
}
fun generatePaymentNetting(tx: TransactionBuilder,
currency: Issued<P>,
notary: Party,
vararg states: State<P>) {
requireThat {
"all states are in the normal lifecycle state " by (states.all { it.lifecycle == Lifecycle.NORMAL })
}
val groups = states.groupBy { it.multilateralNetState }
val partyLookup = HashMap<PublicKey, Party>()
val signers = states.map { it.owner }.union(states.map { it.issuer.owningKey }).toSet()
// Create a lookup table of the party that each public key represents.
states.map { it.issuer }.forEach { partyLookup.put(it.owningKey, it) }
for ((netState, groupStates) in groups) {
// Extract the net balances
val netBalances = netAmountsDue(extractAmountsDue(currency, states.asIterable()))
netBalances
// Convert the balances into obligation state objects
.map { entry ->
State(Lifecycle.NORMAL, partyLookup[entry.key.first]!!,
netState.issuanceDef, entry.value.quantity, entry.key.second)
}
// Add the new states to the TX
.forEach { tx.addOutputState(it, notary) }
tx.addCommand(Commands.Net(NetType.PAYMENT), signers.toList())
}
}
/**
* Generate a transaction changing the lifecycle of one or more state objects.
*
* @param statesAndRefs a list of state objects, which MUST all have the same issuance definition. This avoids
* potential complications arising from different deadlines applying to different states.
*/
fun generateSetLifecycle(tx: TransactionBuilder,
statesAndRefs: List<StateAndRef<State<P>>>,
lifecycle: Lifecycle,
notary: Party) {
val states = statesAndRefs.map { it.state.data }
val issuanceDef = getTemplateOrThrow(states)
val existingLifecycle = when (lifecycle) {
Lifecycle.DEFAULTED -> Lifecycle.NORMAL
Lifecycle.NORMAL -> Lifecycle.DEFAULTED
}
require(states.all { it.lifecycle == existingLifecycle }) { "initial lifecycle must be ${existingLifecycle} for all input states" }
// Produce a new set of states
val groups = statesAndRefs.groupBy { it.state.data.issuanceDef }
for ((aggregateState, stateAndRefs) in groups) {
val partiesUsed = ArrayList<PublicKey>()
stateAndRefs.forEach { stateAndRef ->
val outState = stateAndRef.state.data.copy(lifecycle = lifecycle)
tx.addInputState(stateAndRef)
tx.addOutputState(outState, notary)
partiesUsed.add(stateAndRef.state.data.owner)
}
tx.addCommand(Commands.SetLifecycle(aggregateState, lifecycle), partiesUsed.distinct())
}
tx.setTime(issuanceDef.dueBefore, notary, issuanceDef.timeTolerance)
}
/**
* @param statesAndRefs a list of state objects, which MUST all have the same aggregate state. This is done as
* only a single settlement command can be present in a transaction, to avoid potential problems with allocating
* cash to different obligation issuances.
* @param cashStatesAndRefs a list of cash state objects, which MUST all be in the same currency. It is strongly
* encouraged that these all have the same owner.
*/
fun generateSettle(tx: TransactionBuilder,
statesAndRefs: Iterable<StateAndRef<State<P>>>,
cashStatesAndRefs: Iterable<StateAndRef<FungibleAssetState<P, *>>>,
notary: Party) {
val states = statesAndRefs.map { it.state }
val notary = states.first().notary
val obligationIssuer = states.first().data.issuer
val obligationOwner = states.first().data.owner
requireThat {
"all cash states use the same notary" by (cashStatesAndRefs.all { it.state.notary == notary })
"all obligation states are in the normal state" by (statesAndRefs.all { it.state.data.lifecycle == Lifecycle.NORMAL })
"all obligation states use the same notary" by (statesAndRefs.all { it.state.notary == notary })
"all obligation states have the same issuer" by (statesAndRefs.all { it.state.data.issuer == obligationIssuer })
"all obligation states have the same owner" by (statesAndRefs.all { it.state.data.owner == obligationOwner })
}
// TODO: A much better (but more complex) solution would be to have two iterators, one for obligations,
// one for cash, and step through each in a semi-synced manner. For now however we just bundle all the states
// on each side together
val issuanceDef = getIssuanceDefinitionOrThrow(statesAndRefs.map { it.state.data })
val template = issuanceDef.template
val obligationTotal: Amount<Issued<P>> = states.map { it.data }.sumObligations<P>()
var obligationRemaining: Amount<Issued<P>> = obligationTotal
val cashSigners = HashSet<PublicKey>()
statesAndRefs.forEach { tx.addInputState(it) }
// Move the cash to the new owner
cashStatesAndRefs.forEach {
if (obligationRemaining.quantity > 0L) {
val cashState = it.state
tx.addInputState(it)
if (obligationRemaining >= cashState.data.amount) {
tx.addOutputState(cashState.data.move(cashState.data.amount, obligationOwner), notary)
obligationRemaining -= cashState.data.amount
} else {
// Split the state in two, sending the change back to the previous owner
tx.addOutputState(cashState.data.move(obligationRemaining, obligationOwner), notary)
tx.addOutputState(cashState.data.move(cashState.data.amount - obligationRemaining, cashState.data.owner), notary)
obligationRemaining -= Amount(0L, obligationRemaining.token)
}
cashSigners.add(cashState.data.owner)
}
}
// If we haven't cleared the full obligation, add the remainder as an output
if (obligationRemaining.quantity > 0L) {
tx.addOutputState(State(Lifecycle.NORMAL, obligationIssuer, template, obligationRemaining.quantity, obligationOwner), notary)
} else {
// Destroy all of the states
}
// Add the cash move command and obligation settle
tx.addCommand(Cash.Commands.Move(), cashSigners.toList())
tx.addCommand(Commands.Settle(issuanceDef, obligationTotal - obligationRemaining), obligationOwner)
}
/** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */
private fun getIssuanceDefinitionOrThrow(states: Iterable<State<P>>): IssuanceDefinition<P> =
states.map { it.issuanceDef }.distinct().single()
/** Get the common issuance definition for one or more states, or throw an IllegalArgumentException. */
private fun getTemplateOrThrow(states: Iterable<State<P>>): StateTemplate<P> =
states.map { it.template }.distinct().single()
}
/**
* Convert a list of settlement states into total from each issuer to a owner.
*
* @return a map of issuer/owner pairs to the balance due.
*/
fun <P> extractAmountsDue(currency: Issued<P>, states: Iterable<Obligation.State<P>>): Map<Pair<PublicKey, PublicKey>, Amount<Issued<P>>> {
val balances = HashMap<Pair<PublicKey, PublicKey>, Amount<Issued<P>>>()
states.forEach { state ->
val key = Pair(state.issuer.owningKey, state.owner)
val balance = balances[key] ?: Amount(0L, currency)
balances[key] = balance + state.amount
}
return balances
}
/**
* Net off the amounts due between parties.
*/
fun <P> netAmountsDue(balances: Map<Pair<PublicKey, PublicKey>, Amount<Issued<P>>>): Map<Pair<PublicKey, PublicKey>, Amount<Issued<P>>> {
val nettedBalances = HashMap<Pair<PublicKey, PublicKey>, Amount<Issued<P>>>()
balances.forEach { balance ->
val (issuer, owner) = balance.key
val oppositeKey = Pair(owner, issuer)
val opposite = (balances[oppositeKey] ?: Amount(0L, balance.value.token))
// Drop zero balances
if (balance.value > opposite) {
nettedBalances[balance.key] = (balance.value - opposite)
} else if (opposite > balance.value) {
nettedBalances[oppositeKey] = (opposite - balance.value)
}
}
return nettedBalances
}
/**
* Calculate the total balance movement for each party in the transaction, based off a summary of balances between
* each issuer and owner.
*
* @param balances payments due, indexed by issuer and owner. Zero balances are stripped from the map before being
* returned.
*/
fun <P> sumAmountsDue(balances: Map<Pair<PublicKey, PublicKey>, Amount<P>>): Map<PublicKey, Long> {
val sum = HashMap<PublicKey, Long>()
// Fill the map with zeroes initially
balances.keys.forEach {
sum[it.first] = 0L
sum[it.second] = 0L
}
for ((key, amount) in balances) {
val (issuer, owner) = key
// Subtract it from the issuer
sum[issuer] = sum[issuer]!! - amount.quantity
// Add it to the owner
sum[owner] = sum[owner]!! + amount.quantity
}
// Strip zero balances
val iterator = sum.iterator()
while (iterator.hasNext()) {
val (key, amount) = iterator.next()
if (amount == 0L) {
iterator.remove()
}
}
return sum
}
/** Sums the cash states in the list, throwing an exception if there are none.
* All cash states in the list are presumed to be nettable.
*/
fun <P> Iterable<ContractState>.sumObligations() = filterIsInstance<Obligation.State<P>>().map { it.amount }.sumOrThrow()
/** Sums the cash settlement states in the list, returning null if there are none. */
fun <P> Iterable<ContractState>.sumObligationsOrNull()
= filterIsInstance<Obligation.State<P>>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrNull()
/** Sums the cash settlement states in the list, returning zero of the given currency if there are none. */
fun <P> Iterable<ContractState>.sumObligationsOrZero(currency: Issued<P>)
= filterIsInstance<Obligation.State<P>>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(currency)

View File

@ -1,33 +0,0 @@
package com.r3corda.contracts.testing
import com.r3corda.contracts.Obligation
import com.r3corda.contracts.cash.Cash
import com.r3corda.core.contracts.Amount
import com.r3corda.core.contracts.Issued
import com.r3corda.core.crypto.NullPublicKey
import com.r3corda.core.crypto.Party
import com.r3corda.core.testing.MINI_CORP
import com.r3corda.core.utilities.nonEmptySetOf
import java.security.PublicKey
import java.time.Instant
import java.util.*
object JavaExperimental {
@JvmStatic fun <T> at(state: Obligation.State<T>, dueBefore: Instant) = state.copy(template = state.template.copy(dueBefore = dueBefore))
@JvmStatic fun <T> between(state: Obligation.State<T>, parties: Pair<Party, PublicKey>) = state.copy(issuer = parties.first, owner = parties.second)
@JvmStatic fun <T> ownedBy(state: Obligation.State<T>, owner: PublicKey) = state.copy(owner = owner)
@JvmStatic fun <T> issuedBy(state: Obligation.State<T>, party: Party) = state.copy(issuer = party)
@JvmStatic fun OBLIGATION_DEF(issued: Issued<Currency>)
= Obligation.StateTemplate(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(issued), Instant.parse("2020-01-01T17:00:00Z"))
@JvmStatic fun OBLIGATION(amount: Amount<Issued<Currency>>) = Obligation.State(Obligation.Lifecycle.NORMAL, MINI_CORP,
OBLIGATION_DEF(amount.token), amount.quantity, NullPublicKey)
}
infix fun <T> Obligation.State<T>.`at`(dueBefore: Instant) = JavaExperimental.at(this, dueBefore)
infix fun <T> Obligation.State<T>.`between`(parties: Pair<Party, PublicKey>) = JavaExperimental.between(this, parties)
infix fun <T> Obligation.State<T>.`owned by`(owner: PublicKey) = JavaExperimental.ownedBy(this, owner)
infix fun <T> Obligation.State<T>.`issued by`(party: Party) = JavaExperimental.issuedBy(this, party)
// Allows you to write 100.DOLLARS.OBLIGATION
val Issued<Currency>.OBLIGATION_DEF: Obligation.StateTemplate<Currency> get() = JavaExperimental.OBLIGATION_DEF(this)
val Amount<Issued<Currency>>.OBLIGATION: Obligation.State<Currency> get() = JavaExperimental.OBLIGATION(this)

View File

@ -1,746 +0,0 @@
package com.r3corda.contracts
import com.r3corda.contracts.cash.Cash
import com.r3corda.contracts.Obligation.Lifecycle
import com.r3corda.contracts.testing.*
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.seconds
import com.r3corda.core.testing.*
import com.r3corda.core.utilities.nonEmptySetOf
import org.junit.Test
import java.security.PublicKey
import java.time.Instant
import java.util.*
import kotlin.test.*
class ObligationTests {
val defaultIssuer = MEGA_CORP.ref(1)
val defaultUsd = USD `issued by` defaultIssuer
val oneMillionDollars = 1000000.DOLLARS `issued by` defaultIssuer
val trustedCashContract = nonEmptySetOf(SecureHash.randomSHA256() as SecureHash)
val megaIssuedDollars = nonEmptySetOf(Issued<Currency>(defaultIssuer, USD))
val megaIssuedPounds = nonEmptySetOf(Issued<Currency>(defaultIssuer, GBP))
val fivePm = Instant.parse("2016-01-01T17:00:00.00Z")
val sixPm = Instant.parse("2016-01-01T18:00:00.00Z")
val notary = MEGA_CORP
val megaCorpDollarSettlement = Obligation.StateTemplate(trustedCashContract, megaIssuedDollars, fivePm)
val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuanceDefinitions = megaIssuedPounds)
val inState = Obligation.State(
lifecycle = Lifecycle.NORMAL,
issuer = MEGA_CORP,
template = megaCorpDollarSettlement,
quantity = 1000.DOLLARS.quantity,
owner = DUMMY_PUBKEY_1
)
val outState = inState.copy(owner = DUMMY_PUBKEY_2)
private fun obligationTestRoots(group: TransactionGroupDSL<Obligation.State<Currency>>) = group.Roots()
.transaction(oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `with notary` DUMMY_NOTARY label "Alice's $1,000,000 obligation to Bob")
.transaction(oneMillionDollars.OBLIGATION `between` Pair(BOB, ALICE_PUBKEY) `with notary` DUMMY_NOTARY label "Bob's $1,000,000 obligation to Alice")
.transaction(oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, BOB_PUBKEY) `with notary` DUMMY_NOTARY label "MegaCorp's $1,000,000 obligation to Bob")
.transaction(1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY label "Alice's $1,000,000")
@Test
fun trivial() {
transaction {
input { inState }
this `fails requirement` "the amounts balance"
tweak {
output { outState.copy(quantity = 2000.DOLLARS.quantity) }
this `fails requirement` "the amounts balance"
}
tweak {
output { outState }
// No command arguments
this `fails requirement` "required com.r3corda.contracts.Obligation.Commands.Move command"
}
tweak {
output { outState }
arg(DUMMY_PUBKEY_2) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails requirement` "the owning keys are the same as the signing keys"
}
tweak {
output { outState }
output { outState `issued by` MINI_CORP }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails requirement` "at least one obligation input"
}
// Simple reallocation works.
tweak {
output { outState }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this.accepts()
}
}
}
@Test
fun `issue debt`() {
// Check we can't "move" debt into existence.
transaction {
input { DummyContract.State() }
output { outState }
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Move(outState.issuanceDef) }
this `fails requirement` "there is 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
// institution is allowed to issue as much cash as they want.
transaction {
output { outState }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Issue(outState.issuanceDef) }
this `fails requirement` "output deposits are owned by a command signer"
}
transaction {
output {
Obligation.State(
issuer = MINI_CORP,
quantity = 1000.DOLLARS.quantity,
owner = DUMMY_PUBKEY_1,
template = megaCorpDollarSettlement
)
}
tweak {
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement), 0) }
this `fails requirement` "has a nonce"
}
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement)) }
this.accepts()
}
// Test generation works.
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
Obligation<Currency>().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
assertTrue(ptx.inputStates().isEmpty())
val s = ptx.outputStates()[0].data as Obligation.State<Currency>
assertEquals(100.DOLLARS `issued by` MEGA_CORP.ref(1), s.amount)
assertEquals(MINI_CORP, s.issuer)
assertEquals(DUMMY_PUBKEY_1, s.owner)
assertTrue(ptx.commands()[0].value is Obligation.Commands.Issue<*>)
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0])
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
transaction {
input { inState }
output { inState.copy(quantity = inState.amount.quantity * 2) }
// Move fails: not allowed to summon money.
tweak {
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails requirement` "at issuer MegaCorp the amounts balance"
}
// Issue works.
tweak {
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
this.accepts()
}
}
// Can't use an issue command to lower the amount.
transaction {
input { inState }
output { inState.copy(quantity = inState.amount.quantity / 2) }
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
this `fails requirement` "output values sum to more than the inputs"
}
// Can't have an issue command that doesn't actually issue money.
transaction {
input { inState }
output { inState }
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
this `fails requirement` "output values sum to more than the inputs"
}
// Can't have any other commands if we have an issue command (because the issue command overrules them)
transaction {
input { inState }
output { inState.copy(quantity = inState.amount.quantity * 2) }
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
tweak {
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
this `fails requirement` "only move/exit commands can be present along with other obligation commands"
}
tweak {
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails requirement` "only move/exit commands can be present along with other obligation commands"
}
tweak {
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.SetLifecycle(inState.issuanceDef, Lifecycle.DEFAULTED) }
this `fails requirement` "only move/exit commands can be present along with other obligation commands"
}
tweak {
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, inState.amount / 2) }
this `fails requirement` "only move/exit commands can be present along with other obligation commands"
}
this.accepts()
}
}
/**
* Test that the issuance builder rejects building into a transaction with existing
* cash inputs.
*/
@Test(expected = IllegalStateException::class)
fun `reject issuance with inputs`() {
// Issue some obligation
var ptx = TransactionType.General.Builder(DUMMY_NOTARY)
Obligation<Currency>().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY)
ptx.signWith(MINI_CORP_KEY)
val tx = ptx.toSignedTransaction()
// Include the previously issued obligation in a new issuance command
ptx = TransactionType.General.Builder(DUMMY_NOTARY)
ptx.addInputState(tx.tx.outRef<Obligation.State<Currency>>(0))
Obligation<Currency>().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY)
}
/** Test generating a transaction to net two obligations of the same size, and therefore there are no outputs. */
@Test
fun `generate payment net transaction`() {
val obligationAliceToBob = oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY)
val obligationBobToAlice = oneMillionDollars.OBLIGATION `between` Pair(BOB, ALICE_PUBKEY)
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
Obligation<Currency>().generatePaymentNetting(ptx, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
assertEquals(0, ptx.outputStates().size)
}
/** Test generating a transaction to two obligations, where one is bigger than the other and therefore there is a remainder. */
@Test
fun `generate payment net transaction with remainder`() {
val obligationAliceToBob = oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY)
val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION `between` Pair(BOB, ALICE_PUBKEY)
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
Obligation<Currency>().generatePaymentNetting(ptx, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
assertEquals(1, ptx.outputStates().size)
val out = ptx.outputStates().single().data as Obligation.State<Currency>
assertEquals(1000000.DOLLARS.quantity, out.quantity)
assertEquals(BOB, out.issuer)
assertEquals(ALICE_PUBKEY, out.owner)
}
/** Test generating a transaction to mark outputs as having defaulted. */
@Test
fun `generate set lifecycle`() {
// Issue some obligation
val dueBefore = Instant.parse("2010-01-01T17:00:00Z")
// Generate a transaction issuing the obligation
var tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
Obligation<Currency>().generateIssue(this, MINI_CORP, megaCorpDollarSettlement.copy(dueBefore = dueBefore), 100.DOLLARS.quantity,
owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY)
signWith(MINI_CORP_KEY)
}.toSignedTransaction()
var stateAndRef = tx.tx.outRef<Obligation.State<Currency>>(0)
// Now generate a transaction marking the obligation as having defaulted
tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
Obligation<Currency>().generateSetLifecycle(this, listOf(stateAndRef), Obligation.Lifecycle.DEFAULTED, DUMMY_NOTARY)
signWith(MINI_CORP_KEY)
}.toSignedTransaction(false)
assertEquals(1, tx.tx.outputs.size)
assertEquals(stateAndRef.state.data.copy(lifecycle = Obligation.Lifecycle.DEFAULTED), tx.tx.outputs[0].data)
// And set it back
stateAndRef = tx.tx.outRef<Obligation.State<Currency>>(0)
tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
Obligation<Currency>().generateSetLifecycle(this, listOf(stateAndRef), Obligation.Lifecycle.NORMAL, DUMMY_NOTARY)
signWith(MINI_CORP_KEY)
}.toSignedTransaction(false)
assertEquals(1, tx.tx.outputs.size)
assertEquals(stateAndRef.state.data.copy(lifecycle = Obligation.Lifecycle.NORMAL), tx.tx.outputs[0].data)
}
/** Test generating a transaction to settle an obligation. */
@Test
fun `generate settlement transaction`() {
var ptx: TransactionBuilder
// Generate a transaction to issue the cash we'll need
ptx = TransactionType.General.Builder(DUMMY_NOTARY)
Cash().generateIssue(ptx, 100.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY)
ptx.signWith(MEGA_CORP_KEY)
val cashTx = ptx.toSignedTransaction().tx
// Generate a transaction issuing the obligation
ptx = TransactionType.General.Builder(DUMMY_NOTARY)
Obligation<Currency>().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY)
ptx.signWith(MINI_CORP_KEY)
val obligationTx = ptx.toSignedTransaction().tx
// Now generate a transaction settling the obligation
ptx = TransactionType.General.Builder(DUMMY_NOTARY)
val stateAndRef = obligationTx.outRef<Obligation.State<Currency>>(0)
Obligation<Currency>().generateSettle(ptx, listOf(obligationTx.outRef(0)), listOf(cashTx.outRef(0)), DUMMY_NOTARY)
assertEquals(2, ptx.inputStates().size)
assertEquals(1, ptx.outputStates().size)
}
@Test
fun `close-out netting`() {
// Try netting out two obligations
transactionGroupFor<Obligation.State<Currency>>() {
obligationTestRoots(this)
transaction("Issuance") {
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
// Note we can sign with either key here
arg(ALICE_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
timestamp(TEST_TX_TIME)
}
}.verify()
// Try netting out two obligations, with the third uninvolved obligation left
// as-is
transactionGroupFor<Obligation.State<Currency>>() {
obligationTestRoots(this)
transaction("Issuance") {
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
input("MegaCorp's $1,000,000 obligation to Bob")
output("change") { oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, BOB_PUBKEY) }
arg(BOB_PUBKEY, MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
timestamp(TEST_TX_TIME)
}
}.verify()
// Try having outputs mis-match the inputs
transactionGroupFor<Obligation.State<Currency>>() {
obligationTestRoots(this)
transaction("Issuance") {
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
output("change") { (oneMillionDollars / 2).OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) }
arg(BOB_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
timestamp(TEST_TX_TIME)
}
}.expectFailureOfTx(1, "amounts owed on input and output must match")
// Have the wrong signature on the transaction
transactionGroupFor<Obligation.State<Currency>>() {
obligationTestRoots(this)
transaction("Issuance") {
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
timestamp(TEST_TX_TIME)
}
}.expectFailureOfTx(1, "any involved party has signed")
}
@Test
fun `payment netting`() {
// Try netting out two obligations
transactionGroupFor<Obligation.State<Currency>>() {
obligationTestRoots(this)
transaction("Issuance") {
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
arg(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
timestamp(TEST_TX_TIME)
}
}.verify()
// Try netting out two obligations, but only provide one signature. Unlike close-out netting, we need both
// signatures for payment netting
transactionGroupFor<Obligation.State<Currency>>() {
obligationTestRoots(this)
transaction("Issuance") {
input("Alice's $1,000,000 obligation to Bob")
input("Bob's $1,000,000 obligation to Alice")
arg(BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
timestamp(TEST_TX_TIME)
}
}.expectFailureOfTx(1, "all involved parties have signed")
// Multilateral netting, A -> B -> C which can net down to A -> C
transactionGroupFor<Obligation.State<Currency>>() {
obligationTestRoots(this)
transaction("Issuance") {
input("Bob's $1,000,000 obligation to Alice")
input("MegaCorp's $1,000,000 obligation to Bob")
output("MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, ALICE_PUBKEY) }
arg(ALICE_PUBKEY, BOB_PUBKEY, MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
timestamp(TEST_TX_TIME)
}
}.verify()
// Multilateral netting without the key of the receiving party
transactionGroupFor<Obligation.State<Currency>>() {
obligationTestRoots(this)
transaction("Issuance") {
input("Bob's $1,000,000 obligation to Alice")
input("MegaCorp's $1,000,000 obligation to Bob")
output("MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, ALICE_PUBKEY) }
arg(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) }
timestamp(TEST_TX_TIME)
}
}.expectFailureOfTx(1, "all involved parties have signed")
}
@Test
fun `settlement`() {
// Try netting out two obligations
transactionGroupFor<Obligation.State<Currency>>() {
obligationTestRoots(this)
transaction("Settlement") {
input("Alice's $1,000,000 obligation to Bob")
input("Alice's $1,000,000")
output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY }
arg(ALICE_PUBKEY) { Obligation.Commands.Settle<Currency>(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), oneMillionDollars) }
arg(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
}
}.verify()
}
@Test
fun `payment default`() {
// Try defaulting an obligation without a timestamp
transactionGroupFor<Obligation.State<Currency>>() {
obligationTestRoots(this)
transaction("Settlement") {
input("Alice's $1,000,000 obligation to Bob")
output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY)).copy(lifecycle = Obligation.Lifecycle.DEFAULTED) }
arg(BOB_PUBKEY) { Obligation.Commands.SetLifecycle<Currency>(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Obligation.Lifecycle.DEFAULTED) }
}
}.expectFailureOfTx(1, "there is a timestamp from the authority")
// Try defaulting an obligation
transactionGroupFor<Obligation.State<Currency>>() {
obligationTestRoots(this)
transaction("Settlement") {
input("Alice's $1,000,000 obligation to Bob")
output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY)).copy(lifecycle = Obligation.Lifecycle.DEFAULTED) }
arg(BOB_PUBKEY) { Obligation.Commands.SetLifecycle<Currency>(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Obligation.Lifecycle.DEFAULTED) }
timestamp(TEST_TX_TIME)
}
}.verify()
}
@Test
fun testMergeSplit() {
// Splitting value works.
transaction {
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
tweak {
input { inState }
for (i in 1..4) output { inState.copy(quantity = inState.quantity / 4) }
this.accepts()
}
// Merging 4 inputs into 2 outputs works.
tweak {
for (i in 1..4) input { inState.copy(quantity = inState.quantity / 4) }
output { inState.copy(quantity = inState.quantity / 2) }
output { inState.copy(quantity = inState.quantity / 2) }
this.accepts()
}
// Merging 2 inputs into 1 works.
tweak {
input { inState.copy(quantity = inState.quantity / 2) }
input { inState.copy(quantity = inState.quantity / 2) }
output { inState }
this.accepts()
}
}
}
@Test
fun zeroSizedValues() {
transaction {
input { inState }
input { inState.copy(quantity = 0L) }
this `fails requirement` "zero sized inputs"
}
transaction {
input { inState }
output { inState }
output { inState.copy(quantity = 0L) }
this `fails requirement` "zero sized outputs"
}
}
@Test
fun trivialMismatches() {
// Can't change issuer.
transaction {
input { inState }
output { outState `issued by` MINI_CORP }
this `fails requirement` "at issuer MegaCorp the amounts balance"
}
// Can't mix currencies.
transaction {
input { inState }
output { outState.copy(quantity = 80000, template = megaCorpDollarSettlement) }
output { outState.copy(quantity = 20000, template = megaCorpPoundSettlement) }
this `fails requirement` "the amounts balance"
}
transaction {
input { inState }
input {
inState.copy(
quantity = 15000,
template = megaCorpPoundSettlement,
owner = DUMMY_PUBKEY_2
)
}
output { outState.copy(quantity = 115000) }
this `fails requirement` "the amounts balance"
}
// Can't have superfluous input states from different issuers.
transaction {
input { inState }
input { inState `issued by` MINI_CORP }
output { outState }
arg(DUMMY_PUBKEY_1) {Obligation.Commands.Move(inState.issuanceDef) }
arg(DUMMY_PUBKEY_1) {Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
this `fails requirement` "at issuer MiniCorp the amounts balance"
}
}
@Test
fun exitLedger() {
// Single input/output straightforward case.
transaction {
input { inState }
output { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
tweak {
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, 100.DOLLARS `issued by` defaultIssuer) }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails requirement` "the amounts balance"
}
tweak {
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, 200.DOLLARS `issued by` defaultIssuer) }
this `fails requirement` "required com.r3corda.contracts.Obligation.Commands.Move command"
tweak {
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this.accepts()
}
}
}
// Multi-issuer case.
transaction {
input { inState }
input { inState `issued by` MINI_CORP }
output { inState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) `issued by` MINI_CORP }
output { inState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
this `fails requirement` "at issuer MegaCorp the amounts balance"
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, 200.DOLLARS `issued by` defaultIssuer) }
tweak {
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>((inState `issued by` MINI_CORP).issuanceDef, 0.DOLLARS `issued by` defaultIssuer) }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
this `fails requirement` "at issuer MiniCorp the amounts balance"
}
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>((inState `issued by` MINI_CORP).issuanceDef, 200.DOLLARS `issued by` defaultIssuer) }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
this.accepts()
}
}
@Test
fun multiIssuer() {
transaction {
// Gather 2000 dollars from two different issuers.
input { inState }
input { inState `issued by` MINI_CORP }
// Can't merge them together.
tweak {
output { inState.copy(owner = DUMMY_PUBKEY_2, quantity = 200000L) }
this `fails requirement` "at issuer MegaCorp the amounts balance"
}
// Missing MiniCorp deposit
tweak {
output { inState.copy(owner = DUMMY_PUBKEY_2) }
output { inState.copy(owner = DUMMY_PUBKEY_2) }
this `fails requirement` "at issuer MegaCorp the amounts balance"
}
// This works.
output { inState.copy(owner = DUMMY_PUBKEY_2) }
output { inState.copy(owner = DUMMY_PUBKEY_2) `issued by` MINI_CORP }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
this.accepts()
}
}
@Test
fun multiCurrency() {
// Check we can do an atomic currency trade tx.
transaction {
val pounds = Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpPoundSettlement, 658.POUNDS.quantity, DUMMY_PUBKEY_2)
input { inState `owned by` DUMMY_PUBKEY_1 }
input { pounds }
output { inState `owned by` DUMMY_PUBKEY_2 }
output { pounds `owned by` DUMMY_PUBKEY_1 }
arg(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Obligation.Commands.Move(inState.issuanceDef) }
arg(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Obligation.Commands.Move(pounds.issuanceDef) }
this.accepts()
}
}
@Test
fun `nettability of settlement contracts`() {
val fiveKDollarsFromMegaToMega = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement,
5000.DOLLARS.quantity, MEGA_CORP_PUBKEY)
val twoKDollarsFromMegaToMini = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement,
2000.DOLLARS.quantity, MINI_CORP_PUBKEY)
val oneKDollarsFromMiniToMega = Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpDollarSettlement,
1000.DOLLARS.quantity, MEGA_CORP_PUBKEY)
// Obviously states must be nettable with themselves
assertEquals(fiveKDollarsFromMegaToMega.bilateralNetState, fiveKDollarsFromMegaToMega.bilateralNetState)
assertEquals(oneKDollarsFromMiniToMega.bilateralNetState, oneKDollarsFromMiniToMega.bilateralNetState)
// States must be nettable if the two involved parties are the same, irrespective of which way around
assertEquals(twoKDollarsFromMegaToMini.bilateralNetState, oneKDollarsFromMiniToMega.bilateralNetState)
// States must not be nettable if they do not have the same pair of parties
assertNotEquals(fiveKDollarsFromMegaToMega.bilateralNetState, twoKDollarsFromMegaToMini.bilateralNetState)
assertNotEquals(fiveKDollarsFromMegaToMega.bilateralNetState, oneKDollarsFromMiniToMega.bilateralNetState)
// States must not be nettable if the currency differs
assertNotEquals(oneKDollarsFromMiniToMega.bilateralNetState, oneKDollarsFromMiniToMega.copy(template = megaCorpPoundSettlement).bilateralNetState)
// States must not be nettable if the settlement time differs
assertNotEquals(fiveKDollarsFromMegaToMega.bilateralNetState,
fiveKDollarsFromMegaToMega.copy(template = megaCorpDollarSettlement.copy(dueBefore = sixPm)).bilateralNetState)
// States must not be nettable if the cash contract differs
assertNotEquals(fiveKDollarsFromMegaToMega.bilateralNetState,
fiveKDollarsFromMegaToMega.copy(template = megaCorpDollarSettlement.copy(acceptableContracts = nonEmptySetOf(SecureHash.randomSHA256()))).bilateralNetState)
// States must not be nettable if the trusted issuers differ
val miniCorpIssuer = nonEmptySetOf(Issued<Currency>(MINI_CORP.ref(1), USD))
assertNotEquals(fiveKDollarsFromMegaToMega.bilateralNetState,
fiveKDollarsFromMegaToMega.copy(template = megaCorpDollarSettlement.copy(acceptableIssuanceDefinitions = miniCorpIssuer)).bilateralNetState)
}
@Test(expected = IllegalStateException::class)
fun `states cannot be netted if not in the normal state`() {
inState.copy(lifecycle = Lifecycle.DEFAULTED).bilateralNetState
}
/**
* Confirm that extraction of issuance definition works correctly.
*/
@Test
fun `extraction of issuance defintion`() {
val fiveKDollarsFromMegaToMega = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement,
5000.DOLLARS.quantity, MEGA_CORP_PUBKEY)
val oneKDollarsFromMiniToMega = Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpDollarSettlement,
1000.DOLLARS.quantity, MEGA_CORP_PUBKEY)
// Issuance definitions must match the input
assertEquals(fiveKDollarsFromMegaToMega.template, megaCorpDollarSettlement)
assertEquals(oneKDollarsFromMiniToMega.template, megaCorpDollarSettlement)
}
@Test
fun `adding two settlement contracts nets them`() {
val megaCorpDollarSettlement = Obligation.StateTemplate(trustedCashContract, megaIssuedDollars, fivePm)
val fiveKDollarsFromMegaToMini = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement,
5000.DOLLARS.quantity, MINI_CORP_PUBKEY)
val oneKDollarsFromMiniToMega = Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpDollarSettlement,
1000.DOLLARS.quantity, MEGA_CORP_PUBKEY)
var actual = fiveKDollarsFromMegaToMini.net(fiveKDollarsFromMegaToMini.copy(quantity = 2000.DOLLARS.quantity))
// Both pay from mega to mini, so we add directly
var expected = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement, 7000.DOLLARS.quantity,
MINI_CORP_PUBKEY)
assertEquals(expected, actual)
// Reversing the direction should mean adding the second state subtracts from the first
actual = fiveKDollarsFromMegaToMini.net(oneKDollarsFromMiniToMega)
expected = fiveKDollarsFromMegaToMini.copy(quantity = 4000.DOLLARS.quantity)
assertEquals(expected, actual)
// Trying to add an incompatible state must throw an error
assertFailsWith(IllegalArgumentException::class) {
fiveKDollarsFromMegaToMini.net(Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpDollarSettlement, 1000.DOLLARS.quantity,
MINI_CORP_PUBKEY))
}
}
@Test
fun `extracting amounts due between parties from a list of states`() {
val megaCorpDollarSettlement = Obligation.StateTemplate(trustedCashContract, megaIssuedDollars, fivePm)
val fiveKDollarsFromMegaToMini = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement,
5000.DOLLARS.quantity, MINI_CORP_PUBKEY)
val expected = mapOf(Pair(Pair(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), fiveKDollarsFromMegaToMini.amount))
val actual = extractAmountsDue<Currency>(defaultUsd, listOf(fiveKDollarsFromMegaToMini))
assertEquals(expected, actual)
}
@Test
fun `netting equal balances due between parties`() {
// Now try it with two balances, which cancel each other out
val balanced = mapOf(
Pair(Pair(ALICE_PUBKEY, BOB_PUBKEY), Amount(100000000, GBP)),
Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP))
)
val expected: Map<PublicKey, Long> = emptyMap() // Zero balances are stripped before returning
val actual = sumAmountsDue(balanced)
assertEquals(expected, actual)
}
@Test
fun `netting difference balances due between parties`() {
// Now try it with two balances, which cancel each other out
val balanced = mapOf(
Pair(Pair(ALICE_PUBKEY, BOB_PUBKEY), Amount(100000000, GBP) `issued by` defaultIssuer),
Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(200000000, GBP) `issued by` defaultIssuer)
)
val expected = mapOf(
Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP) `issued by` defaultIssuer)
)
var actual = netAmountsDue<Currency>(balanced)
assertEquals(expected, actual)
}
@Test
fun `summing empty balances due between parties`() {
val empty = emptyMap<Pair<PublicKey, PublicKey>, Amount<Currency>>()
val expected = emptyMap<PublicKey, Long>()
val actual = sumAmountsDue(empty)
assertEquals(expected, actual)
}
@Test
fun `summing balances due between parties`() {
val simple = mapOf(Pair(Pair(ALICE_PUBKEY, BOB_PUBKEY), Amount(100000000, GBP)))
val expected = mapOf(Pair(ALICE_PUBKEY, -100000000L), Pair(BOB_PUBKEY, 100000000L))
val actual = sumAmountsDue(simple)
assertEquals(expected, actual)
}
@Test
fun `summing balances due between parties which net to zero`() {
// Now try it with two balances, which cancel each other out
val balanced = mapOf(
Pair(Pair(ALICE_PUBKEY, BOB_PUBKEY), Amount(100000000, GBP)),
Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP))
)
val expected: Map<PublicKey, Long> = emptyMap() // Zero balances are stripped before returning
val actual = sumAmountsDue(balanced)
assertEquals(expected, actual)
}
}

View File

@ -34,20 +34,20 @@ class FXSwap {
transaction {
output { inState }
this `fails requirement` "transaction has a single command"
this `fails with` "transaction has a single command"
tweak {
arg(roadRunner.owningKey) { UniversalContract.Commands.Issue() }
this `fails requirement` "the transaction is signed by all liable parties"
command(roadRunner.owningKey) { UniversalContract.Commands.Issue() }
this `fails with` "the transaction is signed by all liable parties"
}
tweak {
arg(wileECoyote.owningKey) { UniversalContract.Commands.Issue() }
this `fails requirement` "the transaction is signed by all liable parties"
command(wileECoyote.owningKey) { UniversalContract.Commands.Issue() }
this `fails with` "the transaction is signed by all liable parties"
}
arg(wileECoyote.owningKey, roadRunner.owningKey) { UniversalContract.Commands.Issue() }
command(wileECoyote.owningKey, roadRunner.owningKey) { UniversalContract.Commands.Issue() }
this.accepts()
this.verifies()
}
}
@ -59,13 +59,13 @@ class FXSwap {
output { outState2 }
tweak {
arg(wileECoyote.owningKey) { UniversalContract.Commands.Action("some undefined name") }
this `fails requirement` "action must be defined"
command(wileECoyote.owningKey) { UniversalContract.Commands.Action("some undefined name") }
this `fails with` "action must be defined"
}
arg(wileECoyote.owningKey) { UniversalContract.Commands.Action("execute") }
command(wileECoyote.owningKey) { UniversalContract.Commands.Action("execute") }
this.accepts()
this.verifies()
}
}
@ -76,8 +76,8 @@ class FXSwap {
output { outState1 }
output { outState2 }
arg(porkyPig.owningKey) { UniversalContract.Commands.Action("execute") }
this `fails requirement` "action must be authorized"
command(porkyPig.owningKey) { UniversalContract.Commands.Action("execute") }
this `fails with` "action must be authorized"
}
}
@ -87,8 +87,8 @@ class FXSwap {
input { inState }
output { outState1 }
arg(roadRunner.owningKey) { UniversalContract.Commands.Action("execute") }
this `fails requirement` "output state must match action result state"
command(roadRunner.owningKey) { UniversalContract.Commands.Action("execute") }
this `fails with` "output state must match action result state"
}
}
}

View File

@ -47,16 +47,16 @@ class ZeroCouponBond {
transaction {
output { inState }
this `fails requirement` "transaction has a single command"
this `fails with` "transaction has a single command"
tweak {
arg(roadRunner.owningKey) { UniversalContract.Commands.Issue() }
this `fails requirement` "the transaction is signed by all liable parties"
command(roadRunner.owningKey) { UniversalContract.Commands.Issue() }
this `fails with` "the transaction is signed by all liable parties"
}
arg(wileECoyote.owningKey) { UniversalContract.Commands.Issue() }
command(wileECoyote.owningKey) { UniversalContract.Commands.Issue() }
this.accepts()
this.verifies()
}
}
@ -67,13 +67,13 @@ class ZeroCouponBond {
output { outState }
tweak {
arg(wileECoyote.owningKey) { UniversalContract.Commands.Action("some undefined name") }
this `fails requirement` "action must be defined"
command(wileECoyote.owningKey) { UniversalContract.Commands.Action("some undefined name") }
this `fails with` "action must be defined"
}
arg(wileECoyote.owningKey) { UniversalContract.Commands.Action("execute") }
command(wileECoyote.owningKey) { UniversalContract.Commands.Action("execute") }
this.accepts()
this.verifies()
}
}
@ -83,8 +83,8 @@ class ZeroCouponBond {
input { inState }
output { outState }
arg(porkyPig.owningKey) { UniversalContract.Commands.Action("execute") }
this `fails requirement` "action must be authorized"
command(porkyPig.owningKey) { UniversalContract.Commands.Action("execute") }
this `fails with` "action must be authorized"
}
}
@ -94,8 +94,8 @@ class ZeroCouponBond {
input { inState }
output { outStateWrong }
arg(roadRunner.owningKey) { UniversalContract.Commands.Action("execute") }
this `fails requirement` "output state must match action result state"
command(roadRunner.owningKey) { UniversalContract.Commands.Action("execute") }
this `fails with` "output state must match action result state"
}
}
@ -106,26 +106,26 @@ class ZeroCouponBond {
tweak {
output { outStateMove }
arg(roadRunner.owningKey) {
command(roadRunner.owningKey) {
UniversalContract.Commands.Move(roadRunner, porkyPig)
}
this `fails requirement` "the transaction is signed by all liable parties"
this `fails with` "the transaction is signed by all liable parties"
}
tweak {
output { inState }
arg(roadRunner.owningKey, porkyPig.owningKey, wileECoyote.owningKey) {
command(roadRunner.owningKey, porkyPig.owningKey, wileECoyote.owningKey) {
UniversalContract.Commands.Move(roadRunner, porkyPig)
}
this `fails requirement` "output state does not reflect move command"
this `fails with` "output state does not reflect move command"
}
output { outStateMove}
arg(roadRunner.owningKey, porkyPig.owningKey, wileECoyote.owningKey) {
command(roadRunner.owningKey, porkyPig.owningKey, wileECoyote.owningKey) {
UniversalContract.Commands.Move(roadRunner, porkyPig)
}
this.accepts()
this.verifies()
}
}