mirror of
https://github.com/corda/corda.git
synced 2024-12-19 04:57:58 +00:00
Add obligation contract
Add a new Obligation contract, modelling an obligation to send an amount of something (currently limited to cash) by some future point. Obligation contracts introduce the concept of one contract being aware of other contracts, and common interfaces for state objects so other contracts can interpret them meaningfully.
This commit is contained in:
parent
f975c5181b
commit
388c26dd35
@ -0,0 +1,24 @@
|
|||||||
|
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.*
|
||||||
|
|
||||||
|
infix fun <T> Obligation.State<T>.`at`(dueBefore: Instant) = copy(template = template.copy(dueBefore = dueBefore))
|
||||||
|
infix fun <T> Obligation.State<T>.`between`(parties: Pair<Party, PublicKey>) = copy(issuer = parties.first, owner = parties.second)
|
||||||
|
infix fun <T> Obligation.State<T>.`owned by`(owner: PublicKey) = copy(owner = owner)
|
||||||
|
infix fun <T> Obligation.State<T>.`issued by`(party: Party) = copy(issuer = party)
|
||||||
|
|
||||||
|
// Allows you to write 100.DOLLARS.OBLIGATION
|
||||||
|
val Issued<Currency>.OBLIGATION_DEF: Obligation.StateTemplate<Currency> get() = Obligation.StateTemplate(nonEmptySetOf(Cash().legalContractReference),
|
||||||
|
nonEmptySetOf(this), Instant.parse("2020-01-01T17:00:00Z"))
|
||||||
|
val Amount<Issued<Currency>>.OBLIGATION: Obligation.State<Currency> get() = Obligation.State(Obligation.Lifecycle.NORMAL, MINI_CORP,
|
||||||
|
this.token.OBLIGATION_DEF, this.quantity, NullPublicKey)
|
819
experimental/src/main/kotlin/com/r3corda/contracts/Obligation.kt
Normal file
819
experimental/src/main/kotlin/com/r3corda/contracts/Obligation.kt
Normal file
@ -0,0 +1,819 @@
|
|||||||
|
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)
|
||||||
|
|
@ -0,0 +1,746 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user