mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
Merged in rnicoll-obligation-main (pull request #185)
Move Obligation contract into contracts module
This commit is contained in:
commit
23c3112660
@ -18,7 +18,7 @@ import java.util.*
|
|||||||
val OBLIGATION_PROGRAM_ID = Obligation<Currency>()
|
val OBLIGATION_PROGRAM_ID = Obligation<Currency>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cash settlement contract commits the issuer to delivering a specified amount of cash (represented as the [Cash]
|
* A cash settlement contract commits the obligor 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
|
* contract) at a specified future point in time. Similarly to cash, settlement transactions may split and merge
|
||||||
* contracts across multiple input and output states.
|
* contracts across multiple input and output states.
|
||||||
*
|
*
|
||||||
@ -51,7 +51,7 @@ class Obligation<P> : Contract {
|
|||||||
NORMAL,
|
NORMAL,
|
||||||
/**
|
/**
|
||||||
* Indicates the contract has not been settled by its due date. Once in the defaulted state,
|
* 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.
|
* it can only be reverted to [NORMAL] state by the beneficiary.
|
||||||
*/
|
*/
|
||||||
DEFAULTED
|
DEFAULTED
|
||||||
}
|
}
|
||||||
@ -61,7 +61,7 @@ class Obligation<P> : Contract {
|
|||||||
* underlying issued thing.
|
* underlying issued thing.
|
||||||
*/
|
*/
|
||||||
interface NetState<P> {
|
interface NetState<P> {
|
||||||
val issued: Issued<P>
|
val template: StateTemplate<P>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,11 +71,8 @@ class Obligation<P> : Contract {
|
|||||||
*/
|
*/
|
||||||
data class BilateralNetState<P>(
|
data class BilateralNetState<P>(
|
||||||
val partyKeys: Set<PublicKey>,
|
val partyKeys: Set<PublicKey>,
|
||||||
val issuanceDef: StateTemplate<P>
|
override val template: StateTemplate<P>
|
||||||
) : NetState<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
|
* Subset of state, containing the elements which must match for two or more obligation transactions to be candidates
|
||||||
@ -86,11 +83,8 @@ class Obligation<P> : Contract {
|
|||||||
* Used in cases where all parties (or their proxies) are signing, such as central clearing.
|
* Used in cases where all parties (or their proxies) are signing, such as central clearing.
|
||||||
*/
|
*/
|
||||||
data class MultilateralNetState<P>(
|
data class MultilateralNetState<P>(
|
||||||
val issuanceDef: StateTemplate<P>
|
override val template: StateTemplate<P>
|
||||||
) : NetState<P> {
|
) : NetState<P>
|
||||||
override val issued: Issued<P>
|
|
||||||
get() = issuanceDef.issued
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subset of state, containing the elements specified when issuing a new settlement contract.
|
* Subset of state, containing the elements specified when issuing a new settlement contract.
|
||||||
@ -101,14 +95,14 @@ class Obligation<P> : Contract {
|
|||||||
/** The hash of the cash contract we're willing to accept in payment for this debt. */
|
/** The hash of the cash contract we're willing to accept in payment for this debt. */
|
||||||
val acceptableContracts: NonEmptySet<SecureHash>,
|
val acceptableContracts: NonEmptySet<SecureHash>,
|
||||||
/** The parties whose cash we are willing to accept in payment for this debt. */
|
/** The parties whose cash we are willing to accept in payment for this debt. */
|
||||||
val acceptableIssuanceDefinitions: NonEmptySet<Issued<P>>,
|
val acceptableIssuedProducts: NonEmptySet<Issued<P>>,
|
||||||
|
|
||||||
/** When the contract must be settled by. */
|
/** When the contract must be settled by. */
|
||||||
val dueBefore: Instant,
|
val dueBefore: Instant,
|
||||||
val timeTolerance: Duration = Duration.ofSeconds(30)
|
val timeTolerance: Duration = Duration.ofSeconds(30)
|
||||||
) {
|
) {
|
||||||
val issued: Issued<P>
|
val product: P
|
||||||
get() = acceptableIssuanceDefinitions.toSet().single()
|
get() = acceptableIssuedProducts.map { it.product }.toSet().single()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -119,57 +113,58 @@ class Obligation<P> : Contract {
|
|||||||
* @param P the product the obligation is for payment of.
|
* @param P the product the obligation is for payment of.
|
||||||
*/
|
*/
|
||||||
data class IssuanceDefinition<P>(
|
data class IssuanceDefinition<P>(
|
||||||
val issuer: Party,
|
val obligor: Party,
|
||||||
val template: StateTemplate<P>
|
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
|
* A state representing the obligation of one party (obligor) to deliver a specified number of
|
||||||
* units of an underlying asset (described as issuanceDef.acceptableCashIssuance) to the owner
|
* units of an underlying asset (described as issuanceDef.acceptableCashIssuance) to the beneficiary
|
||||||
* no later than the specified time.
|
* no later than the specified time.
|
||||||
*
|
*
|
||||||
* @param P the product the obligation is for payment of.
|
* @param P the product the obligation is for payment of.
|
||||||
*/
|
*/
|
||||||
data class State<P>(
|
data class State<P>(
|
||||||
var lifecycle: Lifecycle = Lifecycle.NORMAL,
|
var lifecycle: Lifecycle = Lifecycle.NORMAL,
|
||||||
/** Where the debt originates from (issuer) */
|
/** Where the debt originates from (obligor) */
|
||||||
val issuer: Party,
|
val obligor: Party,
|
||||||
val template: StateTemplate<P>,
|
val template: StateTemplate<P>,
|
||||||
val quantity: Long,
|
val quantity: Long,
|
||||||
/** The public key of the entity the contract pays to */
|
/** The public key of the entity the contract pays to */
|
||||||
override val owner: PublicKey
|
val beneficiary: PublicKey
|
||||||
) : FungibleAssetState<P, IssuanceDefinition<P>>, BilateralNettableState<State<P>> {
|
) : FungibleAssetState<P, IssuanceDefinition<P>>, BilateralNettableState<State<P>> {
|
||||||
override val amount: Amount<Issued<P>>
|
val amount: Amount<P>
|
||||||
get() = Amount(quantity, template.issued)
|
get() = Amount(quantity, template.product)
|
||||||
|
val aggregateState: IssuanceDefinition<P>
|
||||||
|
get() = issuanceDef
|
||||||
|
override val productAmount: Amount<P>
|
||||||
|
get() = amount
|
||||||
override val contract = OBLIGATION_PROGRAM_ID
|
override val contract = OBLIGATION_PROGRAM_ID
|
||||||
val acceptableContracts: NonEmptySet<SecureHash>
|
val acceptableContracts: NonEmptySet<SecureHash>
|
||||||
get() = template.acceptableContracts
|
get() = template.acceptableContracts
|
||||||
val acceptableIssuanceDefinitions: NonEmptySet<*>
|
val acceptableIssuanceDefinitions: NonEmptySet<*>
|
||||||
get() = template.acceptableIssuanceDefinitions
|
get() = template.acceptableIssuedProducts
|
||||||
val dueBefore: Instant
|
val dueBefore: Instant
|
||||||
get() = template.dueBefore
|
get() = template.dueBefore
|
||||||
override val issuanceDef: IssuanceDefinition<P>
|
override val issuanceDef: IssuanceDefinition<P>
|
||||||
get() = IssuanceDefinition(issuer, template)
|
get() = IssuanceDefinition(obligor, template)
|
||||||
override val participants: List<PublicKey>
|
override val participants: List<PublicKey>
|
||||||
get() = listOf(issuer.owningKey, owner)
|
get() = listOf(obligor.owningKey, beneficiary)
|
||||||
|
override val owner: PublicKey
|
||||||
|
get() = beneficiary
|
||||||
|
|
||||||
override fun move(amount: Amount<Issued<P>>, owner: PublicKey): Obligation.State<P>
|
override fun move(amount: Amount<P>, beneficiary: PublicKey): Obligation.State<P>
|
||||||
= copy(quantity = amount.quantity, owner = owner)
|
= copy(quantity = amount.quantity, beneficiary = beneficiary)
|
||||||
|
|
||||||
override fun toString() = when (lifecycle) {
|
override fun toString() = when (lifecycle) {
|
||||||
Lifecycle.NORMAL -> "${Emoji.bagOfCash}Debt($amount due $dueBefore to ${owner.toStringShort()})"
|
Lifecycle.NORMAL -> "${Emoji.bagOfCash}Debt($amount due $dueBefore to ${beneficiary.toStringShort()})"
|
||||||
Lifecycle.DEFAULTED -> "${Emoji.bagOfCash}Debt($amount unpaid by $dueBefore to ${owner.toStringShort()})"
|
Lifecycle.DEFAULTED -> "${Emoji.bagOfCash}Debt($amount unpaid by $dueBefore to ${beneficiary.toStringShort()})"
|
||||||
}
|
}
|
||||||
|
|
||||||
override val bilateralNetState: BilateralNetState<P>
|
override val bilateralNetState: BilateralNetState<P>
|
||||||
get() {
|
get() {
|
||||||
check(lifecycle == Lifecycle.NORMAL)
|
check(lifecycle == Lifecycle.NORMAL)
|
||||||
return BilateralNetState(setOf(issuer.owningKey, owner), template)
|
return BilateralNetState(setOf(obligor.owningKey, beneficiary), template)
|
||||||
}
|
}
|
||||||
val multilateralNetState: MultilateralNetState<P>
|
val multilateralNetState: MultilateralNetState<P>
|
||||||
get() {
|
get() {
|
||||||
@ -182,20 +177,20 @@ class Obligation<P> : Contract {
|
|||||||
val netB = other.bilateralNetState
|
val netB = other.bilateralNetState
|
||||||
require(netA == netB) { "net substates of the two state objects must be identical" }
|
require(netA == netB) { "net substates of the two state objects must be identical" }
|
||||||
|
|
||||||
if (issuer.owningKey == other.issuer.owningKey) {
|
if (obligor.owningKey == other.obligor.owningKey) {
|
||||||
// Both sides are from the same issuer to owner
|
// Both sides are from the same obligor to beneficiary
|
||||||
return copy(quantity = quantity + other.quantity)
|
return copy(quantity = quantity + other.quantity)
|
||||||
} else {
|
} else {
|
||||||
// Issuer and owner are backwards
|
// Issuer and beneficiary are backwards
|
||||||
return copy(quantity = quantity - other.quantity)
|
return copy(quantity = quantity - other.quantity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(issuanceDef), copy(owner = newOwner))
|
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(issuanceDef), copy(beneficiary = newOwner))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Interface for commands that apply to aggregated states */
|
/** Interface for commands that apply to states grouped by issuance definition */
|
||||||
interface AggregateCommands<P> : CommandData {
|
interface IssuanceCommands<P> : CommandData {
|
||||||
val aggregateState: IssuanceDefinition<P>
|
val aggregateState: IssuanceDefinition<P>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,49 +198,54 @@ class Obligation<P> : Contract {
|
|||||||
interface Commands : CommandData {
|
interface Commands : CommandData {
|
||||||
/**
|
/**
|
||||||
* Net two or more cash settlement states together in a close-out netting style. Limited to bilateral netting
|
* 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.
|
* as only the beneficiary (not the obligor) needs to sign.
|
||||||
*/
|
*/
|
||||||
data class Net(val type: NetType) : Commands
|
data class Net(val type: NetType) : Commands
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command stating that a debt has been moved, optionally to fulfil another contract.
|
* 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
|
* @param contractHash the contract this move is for the attention of. Only that contract's verify function
|
||||||
* obligation states moved in this transaction are for their sole attention.
|
* should take the moved states into account when considering whether it is valid. Typically this will be
|
||||||
* This is a single value to ensure the same state(s) cannot be used to settle multiple contracts.
|
* null.
|
||||||
* 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>,
|
data class Move<P>(override val aggregateState: IssuanceDefinition<P>,
|
||||||
override val contractHash: SecureHash? = null) : Commands, AggregateCommands<P>, MoveCommand
|
override val contractHash: SecureHash? = null) : Commands, IssuanceCommands<P>, MoveCommand
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction
|
* Allows new obligation states to be issued into existence: the nonce ("number used once") ensures the
|
||||||
* has a unique ID even when there are no inputs.
|
* transaction has a unique ID even when there are no inputs.
|
||||||
*/
|
*/
|
||||||
data class Issue<P>(override val aggregateState: IssuanceDefinition<P>,
|
data class Issue<P>(override val aggregateState: IssuanceDefinition<P>,
|
||||||
val nonce: Long = random63BitValue()) : Commands, AggregateCommands<P>
|
val nonce: Long = random63BitValue()) : Commands, IssuanceCommands<P>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command stating that the issuer is settling some or all of the amount owed by paying in a suitable cash
|
* A command stating that the obligor is settling some or all of the amount owed by transferring a suitable
|
||||||
* contract. If this reduces the balance to zero, the contract moves to the settled state.
|
* state object to the beneficiary. If this reduces the balance to zero, the state object is destroyed.
|
||||||
* @see [Cash.Commands.Move]
|
* @see [Cash.Commands.Move]
|
||||||
*/
|
*/
|
||||||
data class Settle<P>(override val aggregateState: IssuanceDefinition<P>,
|
data class Settle<P>(override val aggregateState: IssuanceDefinition<P>,
|
||||||
val amount: Amount<Issued<P>>) : Commands, AggregateCommands<P>
|
val amount: Amount<P>) : Commands, IssuanceCommands<P>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command stating that the owner is moving the contract into the defaulted state as it has not been settled
|
* A command stating that the beneficiary 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.
|
* by the due date, or resetting a defaulted contract back to the issued state.
|
||||||
*/
|
*/
|
||||||
data class SetLifecycle<P>(override val aggregateState: IssuanceDefinition<P>,
|
data class SetLifecycle<P>(override val aggregateState: IssuanceDefinition<P>,
|
||||||
val lifecycle: Lifecycle) : Commands, AggregateCommands<P>
|
val lifecycle: Lifecycle) : Commands, IssuanceCommands<P> {
|
||||||
|
val inverse: Lifecycle
|
||||||
|
get() = when (lifecycle) {
|
||||||
|
Lifecycle.NORMAL -> Lifecycle.DEFAULTED
|
||||||
|
Lifecycle.DEFAULTED -> Lifecycle.NORMAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command stating that the debt is being released by the owner. Normally would indicate
|
* A command stating that the debt is being released by the beneficiary. Normally would indicate
|
||||||
* either settlement outside of the ledger, or that the issuer is unable to pay.
|
* either settlement outside of the ledger, or that the obligor is unable to pay.
|
||||||
*/
|
*/
|
||||||
data class Exit<P>(override val aggregateState: IssuanceDefinition<P>,
|
data class Exit<P>(override val aggregateState: IssuanceDefinition<P>,
|
||||||
val amount: Amount<Issued<P>>) : Commands, AggregateCommands<P>
|
val amount: Amount<P>) : Commands, IssuanceCommands<P>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This is the function EVERYONE runs */
|
/** This is the function EVERYONE runs */
|
||||||
@ -264,40 +264,40 @@ class Obligation<P> : Contract {
|
|||||||
verifyNetCommand(inputs, outputs, netCommand, key)
|
verifyNetCommand(inputs, outputs, netCommand, key)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val commandGroups = tx.groupCommands<AggregateCommands<P>, IssuanceDefinition<P>> { it.value.aggregateState }
|
val commandGroups = tx.groupCommands<IssuanceCommands<P>, IssuanceDefinition<P>> { it.value.aggregateState }
|
||||||
// Each group is a set of input/output states with distinct issuance definitions. These types
|
// 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.
|
// of settlement are not fungible and must be kept separated for bookkeeping purposes.
|
||||||
val groups = tx.groupStates() { it: State<P> -> it.issuanceDef }
|
val groups = tx.groupStates() { it: State<P> -> it.aggregateState }
|
||||||
|
|
||||||
for ((inputs, outputs, key) in groups) {
|
for ((inputs, outputs, key) in groups) {
|
||||||
// Either inputs or outputs could be empty.
|
// Either inputs or outputs could be empty.
|
||||||
val issuer = key.issuer
|
val obligor = key.obligor
|
||||||
val commands = commandGroups[key] ?: emptyList()
|
val commands = commandGroups[key] ?: emptyList()
|
||||||
|
|
||||||
requireThat {
|
requireThat {
|
||||||
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
|
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyCommandGroup(tx, commands, inputs, outputs, issuer, key)
|
verifyCommandGroup(tx, commands, inputs, outputs, obligor, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyCommandGroup(tx: TransactionForContract,
|
private fun verifyCommandGroup(tx: TransactionForContract,
|
||||||
commands: List<AuthenticatedObject<AggregateCommands<P>>>,
|
commands: List<AuthenticatedObject<IssuanceCommands<P>>>,
|
||||||
inputs: List<State<P>>,
|
inputs: List<State<P>>,
|
||||||
outputs: List<State<P>>,
|
outputs: List<State<P>>,
|
||||||
issuer: Party,
|
obligor: Party,
|
||||||
key: IssuanceDefinition<P>) {
|
key: IssuanceDefinition<P>) {
|
||||||
// We've already pre-grouped by currency amongst other fields, and verified above that every state specifies
|
// 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
|
// at least one acceptable issuance definition, so we can just use the first issuance definition to
|
||||||
// determine currency
|
// determine currency
|
||||||
val currency = key.template.acceptableIssuanceDefinitions.first()
|
val issued = key.template.acceptableIssuedProducts.first()
|
||||||
|
|
||||||
// Issue, default, net and settle commands are all single commands (there's only ever one of them, and
|
// Issue, default, net and settle commands are all single commands (there's only ever one of them, and
|
||||||
// they exclude all other commands).
|
// they exclude all other commands).
|
||||||
val issueCommand = commands.select<Commands.Issue<P>>().firstOrNull()
|
val issueCommand = commands.select<Commands.Issue<P>>().firstOrNull()
|
||||||
val defaultCommand = commands.select<Commands.SetLifecycle<P>>().firstOrNull()
|
val setLifecycleCommand = commands.select<Commands.SetLifecycle<P>>().firstOrNull()
|
||||||
val settleCommand = commands.select<Commands.Settle<P>>().firstOrNull()
|
val settleCommand = commands.select<Commands.Settle<P>>().firstOrNull()
|
||||||
|
|
||||||
if (commands.size != 1) {
|
if (commands.size != 1) {
|
||||||
@ -308,8 +308,8 @@ class Obligation<P> : Contract {
|
|||||||
|
|
||||||
// Issue, default and net commands are special, and do not follow normal input/output summing rules, so
|
// Issue, default and net commands are special, and do not follow normal input/output summing rules, so
|
||||||
// deal with them first
|
// deal with them first
|
||||||
if (defaultCommand != null) {
|
if (setLifecycleCommand != null) {
|
||||||
verifyDefaultCommand(inputs, outputs, tx, defaultCommand)
|
verifySetLifecycleCommand(inputs, outputs, tx, setLifecycleCommand)
|
||||||
} else {
|
} else {
|
||||||
// Only the default command processes inputs/outputs that are not in the normal state
|
// Only the default command processes inputs/outputs that are not in the normal state
|
||||||
// TODO: Need to be able to exit defaulted amounts
|
// TODO: Need to be able to exit defaulted amounts
|
||||||
@ -318,15 +318,15 @@ class Obligation<P> : Contract {
|
|||||||
"all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL }
|
"all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||||
}
|
}
|
||||||
if (issueCommand != null) {
|
if (issueCommand != null) {
|
||||||
verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer)
|
verifyIssueCommand(inputs, outputs, tx, issueCommand, issued, obligor)
|
||||||
} else if (settleCommand != null) {
|
} else if (settleCommand != null) {
|
||||||
// Perhaps through an abundance of caution, settlement is enforced as its own command.
|
// 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
|
// 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
|
// of scope for making it more opaque what's going on in a transaction and whether it's as expected
|
||||||
// by all parties.
|
// by all parties.
|
||||||
verifySettleCommand(inputs, outputs, tx, settleCommand, currency, issuer, key)
|
verifySettleCommand(inputs, outputs, tx, settleCommand, issued, obligor, key)
|
||||||
} else {
|
} else {
|
||||||
verifyBalanceChange(inputs, outputs, commands, currency, issuer)
|
verifyBalanceChange(inputs, outputs, commands, issued.product, obligor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -339,32 +339,32 @@ class Obligation<P> : Contract {
|
|||||||
*/
|
*/
|
||||||
private fun verifyBalanceChange(inputs: List<State<P>>,
|
private fun verifyBalanceChange(inputs: List<State<P>>,
|
||||||
outputs: List<State<P>>,
|
outputs: List<State<P>>,
|
||||||
commands: List<AuthenticatedObject<AggregateCommands<P>>>,
|
commands: List<AuthenticatedObject<IssuanceCommands<P>>>,
|
||||||
currency: Issued<P>,
|
product: P,
|
||||||
issuer: Party) {
|
obligor: Party) {
|
||||||
// Sum up how much settlement owed there is in the inputs, and the difference in outputs. The difference should
|
// 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.
|
// 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 inputAmount = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
|
||||||
val outputAmount = outputs.sumObligationsOrZero(currency)
|
val outputAmount = outputs.sumObligationsOrZero(product)
|
||||||
|
|
||||||
val exitCommands = commands.select<Commands.Exit<P>>()
|
val exitCommands = commands.select<Commands.Exit<P>>()
|
||||||
val requiredExitSignatures = HashSet<PublicKey>()
|
val requiredExitSignatures = HashSet<PublicKey>()
|
||||||
val amountExitingLedger: Amount<Issued<P>> = if (exitCommands.isNotEmpty()) {
|
val amountExitingLedger: Amount<P> = if (exitCommands.isNotEmpty()) {
|
||||||
require(exitCommands.size == 1) { "There can only be one exit command" }
|
require(exitCommands.size == 1) { "There can only be one exit command" }
|
||||||
val exitCommand = exitCommands.single()
|
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
|
// If we want to remove debt from the ledger, that must be signed for by the beneficiary. For now we require exit
|
||||||
// commands to be signed by all input owners, unlocking the full input amount, rather than trying to detangle
|
// commands to be signed by all input beneficiarys, unlocking the full input amount, rather than trying to detangle
|
||||||
// exactly who exited what.
|
// exactly who exited what.
|
||||||
requiredExitSignatures.addAll(inputs.map { it.owner })
|
requiredExitSignatures.addAll(inputs.map { it.beneficiary })
|
||||||
exitCommand.value.amount
|
exitCommand.value.amount
|
||||||
} else {
|
} else {
|
||||||
Amount(0, currency)
|
Amount(0, product)
|
||||||
}
|
}
|
||||||
|
|
||||||
requireThat {
|
requireThat {
|
||||||
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
|
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
|
||||||
"at issuer ${issuer.name} the amounts balance" by
|
"at obligor ${obligor.name} the amounts balance" by
|
||||||
(inputAmount == outputAmount + amountExitingLedger)
|
(inputAmount == outputAmount + amountExitingLedger)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,39 +375,36 @@ class Obligation<P> : Contract {
|
|||||||
* A default command mutates inputs and produces identical outputs, except that the lifecycle changes.
|
* A default command mutates inputs and produces identical outputs, except that the lifecycle changes.
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
protected fun verifyDefaultCommand(inputs: List<State<P>>,
|
protected fun verifySetLifecycleCommand(inputs: List<State<P>>,
|
||||||
outputs: List<State<P>>,
|
outputs: List<State<P>>,
|
||||||
tx: TransactionForContract,
|
tx: TransactionForContract,
|
||||||
setLifecycleCommand: AuthenticatedObject<Commands.SetLifecycle<P>>) {
|
setLifecycleCommand: AuthenticatedObject<Commands.SetLifecycle<P>>) {
|
||||||
// Default must not change anything except lifecycle, so number of inputs and outputs must match
|
// Default must not change anything except lifecycle, so number of inputs and outputs must match
|
||||||
// exactly.
|
// exactly.
|
||||||
require(inputs.size == outputs.size) { "Number of inputs and outputs must match" }
|
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
|
// 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
|
// after the due date, and default/reset can only be done by the beneficiary
|
||||||
val expectedOutputState: Lifecycle = setLifecycleCommand.value.lifecycle
|
val expectedInputLifecycle: Lifecycle = setLifecycleCommand.value.inverse
|
||||||
val expectedInputState: Lifecycle
|
val expectedOutputLifecycle: Lifecycle = setLifecycleCommand.value.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
|
// 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()) {
|
for ((stateIdx, input) in inputs.withIndex()) {
|
||||||
val actualOutput = outputs[stateIdx]
|
val actualOutput = outputs[stateIdx]
|
||||||
val deadline = input.dueBefore
|
val deadline = input.dueBefore
|
||||||
|
// TODO: Determining correct timestamp authority needs rework now that timestamping service is part of
|
||||||
|
// notary.
|
||||||
val timestamp: TimestampCommand? = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A")
|
val timestamp: TimestampCommand? = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A")
|
||||||
val expectedOutput: State<P> = input.copy(lifecycle = expectedOutputState)
|
val expectedOutput: State<P> = input.copy(lifecycle = expectedOutputLifecycle)
|
||||||
|
|
||||||
requireThat {
|
requireThat {
|
||||||
"there is a timestamp from the authority" by (timestamp != null)
|
"there is a timestamp from the authority" by (timestamp != null)
|
||||||
"the due date has passed" by (timestamp?.after?.isBefore(deadline) ?: false)
|
"the due date has passed" by (timestamp!!.after?.isAfter(deadline) ?: false)
|
||||||
"input state lifecycle is correct" by (input.lifecycle == expectedInputState)
|
"input state lifecycle is correct" by (input.lifecycle == expectedInputLifecycle)
|
||||||
"output state corresponds exactly to input state, with lifecycle changed" by (expectedOutput == actualOutput)
|
"output state corresponds exactly to input state, with lifecycle changed" by (expectedOutput == actualOutput)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val owningPubKeys = inputs.map { it.owner }.toSet()
|
val owningPubKeys = inputs.map { it.beneficiary }.toSet()
|
||||||
val keysThatSigned = setLifecycleCommand.signers.toSet()
|
val keysThatSigned = setLifecycleCommand.signers.toSet()
|
||||||
requireThat {
|
requireThat {
|
||||||
"the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys)
|
"the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys)
|
||||||
@ -419,16 +416,16 @@ class Obligation<P> : Contract {
|
|||||||
outputs: List<State<P>>,
|
outputs: List<State<P>>,
|
||||||
tx: TransactionForContract,
|
tx: TransactionForContract,
|
||||||
issueCommand: AuthenticatedObject<Commands.Issue<P>>,
|
issueCommand: AuthenticatedObject<Commands.Issue<P>>,
|
||||||
currency: Issued<P>,
|
issued: Issued<P>,
|
||||||
issuer: Party) {
|
obligor: Party) {
|
||||||
// If we have an issue command, perform special processing: the group is must have no inputs,
|
// If we have an issue command, perform special processing: the group is must have no inputs,
|
||||||
// and that signatures are present for all issuers.
|
// and that signatures are present for all obligors.
|
||||||
|
|
||||||
val inputAmount = inputs.sumObligationsOrZero(currency)
|
val inputAmount: Amount<P> = inputs.sumObligationsOrZero(issued.product)
|
||||||
val outputAmount = outputs.sumObligations<P>()
|
val outputAmount: Amount<P> = outputs.sumObligations<P>()
|
||||||
requireThat {
|
requireThat {
|
||||||
"the issue command has a nonce" by (issueCommand.value.nonce != 0L)
|
"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 deposits are owned by a command signer" by (obligor in issueCommand.signingParties)
|
||||||
"output values sum to more than the inputs" by (outputAmount > inputAmount)
|
"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 }
|
"valid settlement issuance definition is not this issuance definition" by inputs.none { it.issuanceDef in it.acceptableIssuanceDefinitions }
|
||||||
}
|
}
|
||||||
@ -448,24 +445,25 @@ class Obligation<P> : Contract {
|
|||||||
"all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL }
|
"all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||||
}
|
}
|
||||||
|
|
||||||
val token = netState.issued
|
val template = netState.template
|
||||||
// Create two maps of balances from issuers to owners, one for input states, the other for output states.
|
val product = template.product
|
||||||
val inputBalances = extractAmountsDue(token, inputs)
|
// Create two maps of balances from obligors to beneficiaries, one for input states, the other for output states.
|
||||||
val outputBalances = extractAmountsDue(token, outputs)
|
val inputBalances = extractAmountsDue(product, inputs)
|
||||||
|
val outputBalances = extractAmountsDue(product, outputs)
|
||||||
|
|
||||||
// Sum the columns of the matrices. This will yield the net amount payable to/from each party to/from all other participants.
|
// 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.
|
// The two summaries must match, reflecting that the amounts owed match on both input and output.
|
||||||
requireThat {
|
requireThat {
|
||||||
"all input states use the expected token" by (inputs.all { it.issuanceDef.issued == token })
|
"all input states use the same template" by (inputs.all { it.template == template })
|
||||||
"all output states use the expected token" by (outputs.all { it.issuanceDef.issued == token })
|
"all output states use the same template" by (outputs.all { it.template == template })
|
||||||
"amounts owed on input and output must match" by (sumAmountsDue(inputBalances) == sumAmountsDue(outputBalances))
|
"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
|
// 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()
|
val involvedParties = inputs.map { it.beneficiary }.union(inputs.map { it.obligor.owningKey }).toSet()
|
||||||
when (command.value.type) {
|
when (command.value.type) {
|
||||||
// For close-out netting, allow any involved party to sign
|
// For close-out netting, allow any involved party to sign
|
||||||
NetType.CLOSE_OUT -> require(involvedParties.intersect(command.signers).isNotEmpty()) { "any involved party has signed" }
|
NetType.CLOSE_OUT -> require(command.signers.intersect(involvedParties).isNotEmpty()) { "any involved party has signed" }
|
||||||
// Require signatures from all parties (this constraint can be changed for other contracts, and is used as a
|
// 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.
|
// placeholder while exact requirements are established), or fail the transaction.
|
||||||
NetType.PAYMENT -> require(command.signers.containsAll(involvedParties)) { "all involved parties have signed" }
|
NetType.PAYMENT -> require(command.signers.containsAll(involvedParties)) { "all involved parties have signed" }
|
||||||
@ -479,19 +477,19 @@ class Obligation<P> : Contract {
|
|||||||
outputs: List<State<P>>,
|
outputs: List<State<P>>,
|
||||||
tx: TransactionForContract,
|
tx: TransactionForContract,
|
||||||
command: AuthenticatedObject<Commands.Settle<P>>,
|
command: AuthenticatedObject<Commands.Settle<P>>,
|
||||||
currency: Issued<P>,
|
issued: Issued<P>,
|
||||||
issuer: Party,
|
obligor: Party,
|
||||||
key: IssuanceDefinition<P>) {
|
key: IssuanceDefinition<P>) {
|
||||||
val template = key.template
|
val template = key.template
|
||||||
val inputAmount = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
|
val inputAmount: Amount<P> = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
|
||||||
val outputAmount = outputs.sumObligationsOrZero(currency)
|
val outputAmount: Amount<P> = outputs.sumObligationsOrZero(issued.product)
|
||||||
|
|
||||||
// Sum up all cash contracts that are moving and fulfil our requirements
|
// Sum up all cash state objects that are moving and fulfil our requirements
|
||||||
|
|
||||||
// The cash contract verification handles ensuring there's inputs enough to cover the output states, we only
|
// 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
|
// 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
|
// 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.
|
// done of amounts paid in by each beneficiary, as it's presumed the beneficiarys have enough sense to do that themselves.
|
||||||
// Therefore if someone actually signed the following transaction:
|
// Therefore if someone actually signed the following transaction:
|
||||||
//
|
//
|
||||||
// Inputs:
|
// Inputs:
|
||||||
@ -500,11 +498,11 @@ class Obligation<P> : Contract {
|
|||||||
// Outputs:
|
// Outputs:
|
||||||
// £1m cash owned by B
|
// £1m cash owned by B
|
||||||
// Commands:
|
// Commands:
|
||||||
// Settle (signed by B)
|
// Settle (signed by A)
|
||||||
// Move (signed by B)
|
// Move (signed by B)
|
||||||
//
|
//
|
||||||
// That would pass this check. Ensuring they do not is best addressed in the transaction generation stage.
|
// That would pass this check. Ensuring they do not is best addressed in the transaction generation stage.
|
||||||
val cashStates = tx.outStates.filterIsInstance<FungibleAssetState<*, *>>()
|
val cashStates = tx.outputs.filterIsInstance<FungibleAssetState<*, *>>()
|
||||||
val acceptableCashStates = cashStates
|
val acceptableCashStates = cashStates
|
||||||
// TODO: This filter is nonsense, because it just checks there is a cash contract loaded, we need to
|
// 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.
|
// verify the cash contract is the cash contract we expect.
|
||||||
@ -512,8 +510,8 @@ class Obligation<P> : Contract {
|
|||||||
// attachments.mustHaveOneOf(key.acceptableCashContract)
|
// attachments.mustHaveOneOf(key.acceptableCashContract)
|
||||||
.filter { it.contract.legalContractReference in template.acceptableContracts }
|
.filter { it.contract.legalContractReference in template.acceptableContracts }
|
||||||
// Restrict the states to those of the correct issuance definition (this normally
|
// Restrict the states to those of the correct issuance definition (this normally
|
||||||
// covers currency and issuer, but is opaque to us)
|
// covers currency and obligor, but is opaque to us)
|
||||||
.filter { it.issuanceDef in template.acceptableIssuanceDefinitions }
|
.filter { it.issuanceDef in template.acceptableIssuedProducts }
|
||||||
// Catch that there's nothing useful here, so we can dump out a useful error
|
// Catch that there's nothing useful here, so we can dump out a useful error
|
||||||
requireThat {
|
requireThat {
|
||||||
"there are cash state outputs" by (cashStates.size > 0)
|
"there are cash state outputs" by (cashStates.size > 0)
|
||||||
@ -525,12 +523,12 @@ class Obligation<P> : Contract {
|
|||||||
// this one.
|
// this one.
|
||||||
val moveCommands = tx.commands.select<MoveCommand>()
|
val moveCommands = tx.commands.select<MoveCommand>()
|
||||||
var totalPenniesSettled = 0L
|
var totalPenniesSettled = 0L
|
||||||
val requiredSigners = inputs.map { it.issuer.owningKey }.toSet()
|
val requiredSigners = inputs.map { it.obligor.owningKey }.toSet()
|
||||||
|
|
||||||
for ((owner, obligations) in inputs.groupBy { it.owner }) {
|
for ((beneficiary, obligations) in inputs.groupBy { it.beneficiary }) {
|
||||||
val settled = amountReceivedByOwner[owner]?.sumCashOrNull()
|
val settled = amountReceivedByOwner[beneficiary]?.sumCashOrNull()
|
||||||
if (settled != null) {
|
if (settled != null) {
|
||||||
val debt = obligations.sumObligationsOrZero(currency)
|
val debt = obligations.sumObligationsOrZero(issued)
|
||||||
require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" }
|
require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" }
|
||||||
totalPenniesSettled += settled.quantity
|
totalPenniesSettled += settled.quantity
|
||||||
}
|
}
|
||||||
@ -542,19 +540,19 @@ class Obligation<P> : Contract {
|
|||||||
"all move commands relate to this contract" by (moveCommands.map { it.value.contractHash }
|
"all move commands relate to this contract" by (moveCommands.map { it.value.contractHash }
|
||||||
.all { it == null || it == legalContractReference })
|
.all { it == null || it == legalContractReference })
|
||||||
"contract does not try to consume itself" by (moveCommands.map { it.value }.filterIsInstance<Commands.Move<P>>()
|
"contract does not try to consume itself" by (moveCommands.map { it.value }.filterIsInstance<Commands.Move<P>>()
|
||||||
.none { it.aggregateState.issued in template.acceptableIssuanceDefinitions })
|
.none { it.aggregateState == key })
|
||||||
"amounts paid must match recipients to settle" by inputs.map { it.owner }.containsAll(amountReceivedByOwner.keys)
|
"amounts paid must match recipients to settle" by inputs.map { it.beneficiary }.containsAll(amountReceivedByOwner.keys)
|
||||||
"signatures are present from all issuers" by command.signers.containsAll(requiredSigners)
|
"signatures are present from all obligors" by command.signers.containsAll(requiredSigners)
|
||||||
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
|
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
|
||||||
"at issuer ${issuer.name} the obligations after settlement balance" by
|
"at obligor ${obligor.name} the obligations after settlement balance" by
|
||||||
(inputAmount == outputAmount + Amount(totalPenniesSettled, currency))
|
(inputAmount == outputAmount + Amount(totalPenniesSettled, issued.product))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a transaction performing close-out netting of two or more states.
|
* 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 signer the party who will sign the transaction. Must be one of the obligor or beneficiary.
|
||||||
* @param states two or more states, which must be compatible for bilateral netting (same issuance definitions,
|
* @param states two or more states, which must be compatible for bilateral netting (same issuance definitions,
|
||||||
* and same parties involved).
|
* and same parties involved).
|
||||||
*/
|
*/
|
||||||
@ -570,7 +568,9 @@ class Obligation<P> : Contract {
|
|||||||
"signer is in the state parties" by (signer in netState!!.partyKeys)
|
"signer is in the state parties" by (signer in netState!!.partyKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.addOutputState(states.reduce { stateA, stateB -> stateA.net(stateB) })
|
val out = states.reduce { stateA, stateB -> stateA.net(stateB) }
|
||||||
|
if (out.quantity > 0L)
|
||||||
|
tx.addOutputState(out)
|
||||||
tx.addCommand(Commands.Net(NetType.PAYMENT), signer)
|
tx.addCommand(Commands.Net(NetType.PAYMENT), signer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -578,20 +578,20 @@ class Obligation<P> : Contract {
|
|||||||
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
|
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
|
||||||
*/
|
*/
|
||||||
fun generateIssue(tx: TransactionBuilder,
|
fun generateIssue(tx: TransactionBuilder,
|
||||||
issuer: Party,
|
obligor: Party,
|
||||||
issuanceDef: StateTemplate<P>,
|
issuanceDef: StateTemplate<P>,
|
||||||
pennies: Long,
|
pennies: Long,
|
||||||
owner: PublicKey,
|
beneficiary: PublicKey,
|
||||||
notary: Party) {
|
notary: Party) {
|
||||||
check(tx.inputStates().isEmpty())
|
check(tx.inputStates().isEmpty())
|
||||||
check(tx.outputStates().map { it.data }.sumObligationsOrNull<P>() == null)
|
check(tx.outputStates().map { it.data }.sumObligationsOrNull<P>() == null)
|
||||||
val aggregateState = IssuanceDefinition(issuer, issuanceDef)
|
val aggregateState = IssuanceDefinition(obligor, issuanceDef)
|
||||||
tx.addOutputState(State(Lifecycle.NORMAL, issuer, issuanceDef, pennies, owner), notary)
|
tx.addOutputState(State(Lifecycle.NORMAL, obligor, issuanceDef, pennies, beneficiary), notary)
|
||||||
tx.addCommand(Commands.Issue(aggregateState), issuer.owningKey)
|
tx.addCommand(Commands.Issue(aggregateState), obligor.owningKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generatePaymentNetting(tx: TransactionBuilder,
|
fun generatePaymentNetting(tx: TransactionBuilder,
|
||||||
currency: Issued<P>,
|
issued: Issued<P>,
|
||||||
notary: Party,
|
notary: Party,
|
||||||
vararg states: State<P>) {
|
vararg states: State<P>) {
|
||||||
requireThat {
|
requireThat {
|
||||||
@ -599,20 +599,20 @@ class Obligation<P> : Contract {
|
|||||||
}
|
}
|
||||||
val groups = states.groupBy { it.multilateralNetState }
|
val groups = states.groupBy { it.multilateralNetState }
|
||||||
val partyLookup = HashMap<PublicKey, Party>()
|
val partyLookup = HashMap<PublicKey, Party>()
|
||||||
val signers = states.map { it.owner }.union(states.map { it.issuer.owningKey }).toSet()
|
val signers = states.map { it.beneficiary }.union(states.map { it.obligor.owningKey }).toSet()
|
||||||
|
|
||||||
// Create a lookup table of the party that each public key represents.
|
// Create a lookup table of the party that each public key represents.
|
||||||
states.map { it.issuer }.forEach { partyLookup.put(it.owningKey, it) }
|
states.map { it.obligor }.forEach { partyLookup.put(it.owningKey, it) }
|
||||||
|
|
||||||
for ((netState, groupStates) in groups) {
|
for ((netState, groupStates) in groups) {
|
||||||
// Extract the net balances
|
// Extract the net balances
|
||||||
val netBalances = netAmountsDue(extractAmountsDue(currency, states.asIterable()))
|
val netBalances = netAmountsDue(extractAmountsDue(issued.product, states.asIterable()))
|
||||||
|
|
||||||
netBalances
|
netBalances
|
||||||
// Convert the balances into obligation state objects
|
// Convert the balances into obligation state objects
|
||||||
.map { entry ->
|
.map { entry ->
|
||||||
State(Lifecycle.NORMAL, partyLookup[entry.key.first]!!,
|
State(Lifecycle.NORMAL, partyLookup[entry.key.first]!!,
|
||||||
netState.issuanceDef, entry.value.quantity, entry.key.second)
|
netState.template, entry.value.quantity, entry.key.second)
|
||||||
}
|
}
|
||||||
// Add the new states to the TX
|
// Add the new states to the TX
|
||||||
.forEach { tx.addOutputState(it, notary) }
|
.forEach { tx.addOutputState(it, notary) }
|
||||||
@ -647,7 +647,7 @@ class Obligation<P> : Contract {
|
|||||||
val outState = stateAndRef.state.data.copy(lifecycle = lifecycle)
|
val outState = stateAndRef.state.data.copy(lifecycle = lifecycle)
|
||||||
tx.addInputState(stateAndRef)
|
tx.addInputState(stateAndRef)
|
||||||
tx.addOutputState(outState, notary)
|
tx.addOutputState(outState, notary)
|
||||||
partiesUsed.add(stateAndRef.state.data.owner)
|
partiesUsed.add(stateAndRef.state.data.beneficiary)
|
||||||
}
|
}
|
||||||
tx.addCommand(Commands.SetLifecycle(aggregateState, lifecycle), partiesUsed.distinct())
|
tx.addCommand(Commands.SetLifecycle(aggregateState, lifecycle), partiesUsed.distinct())
|
||||||
}
|
}
|
||||||
@ -659,23 +659,22 @@ class Obligation<P> : Contract {
|
|||||||
* only a single settlement command can be present in a transaction, to avoid potential problems with allocating
|
* only a single settlement command can be present in a transaction, to avoid potential problems with allocating
|
||||||
* cash to different obligation issuances.
|
* cash to different obligation issuances.
|
||||||
* @param cashStatesAndRefs a list of cash state objects, which MUST all be in the same currency. It is strongly
|
* @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.
|
* encouraged that these all have the same beneficiary.
|
||||||
*/
|
*/
|
||||||
fun generateSettle(tx: TransactionBuilder,
|
fun generateSettle(tx: TransactionBuilder,
|
||||||
statesAndRefs: Iterable<StateAndRef<State<P>>>,
|
statesAndRefs: Iterable<StateAndRef<State<P>>>,
|
||||||
cashStatesAndRefs: Iterable<StateAndRef<FungibleAssetState<P, *>>>,
|
cashStatesAndRefs: Iterable<StateAndRef<FungibleAssetState<P, *>>>,
|
||||||
notary: Party) {
|
notary: Party) {
|
||||||
val states = statesAndRefs.map { it.state }
|
val states = statesAndRefs.map { it.state }
|
||||||
val notary = states.first().notary
|
val obligationIssuer = states.first().data.obligor
|
||||||
val obligationIssuer = states.first().data.issuer
|
val obligationOwner = states.first().data.beneficiary
|
||||||
val obligationOwner = states.first().data.owner
|
|
||||||
|
|
||||||
requireThat {
|
requireThat {
|
||||||
"all cash states use the same notary" by (cashStatesAndRefs.all { it.state.notary == notary })
|
"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 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 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 obligor" by (statesAndRefs.all { it.state.data.obligor == obligationIssuer })
|
||||||
"all obligation states have the same owner" by (statesAndRefs.all { it.state.data.owner == obligationOwner })
|
"all obligation states have the same beneficiary" by (statesAndRefs.all { it.state.data.beneficiary == obligationOwner })
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: A much better (but more complex) solution would be to have two iterators, one for obligations,
|
// TODO: A much better (but more complex) solution would be to have two iterators, one for obligations,
|
||||||
@ -684,24 +683,24 @@ class Obligation<P> : Contract {
|
|||||||
|
|
||||||
val issuanceDef = getIssuanceDefinitionOrThrow(statesAndRefs.map { it.state.data })
|
val issuanceDef = getIssuanceDefinitionOrThrow(statesAndRefs.map { it.state.data })
|
||||||
val template = issuanceDef.template
|
val template = issuanceDef.template
|
||||||
val obligationTotal: Amount<Issued<P>> = states.map { it.data }.sumObligations<P>()
|
val obligationTotal: Amount<P> = states.map { it.data }.sumObligations<P>()
|
||||||
var obligationRemaining: Amount<Issued<P>> = obligationTotal
|
var obligationRemaining: Amount<P> = obligationTotal
|
||||||
val cashSigners = HashSet<PublicKey>()
|
val cashSigners = HashSet<PublicKey>()
|
||||||
|
|
||||||
statesAndRefs.forEach { tx.addInputState(it) }
|
statesAndRefs.forEach { tx.addInputState(it) }
|
||||||
|
|
||||||
// Move the cash to the new owner
|
// Move the cash to the new beneficiary
|
||||||
cashStatesAndRefs.forEach {
|
cashStatesAndRefs.forEach {
|
||||||
if (obligationRemaining.quantity > 0L) {
|
if (obligationRemaining.quantity > 0L) {
|
||||||
val cashState = it.state
|
val cashState = it.state
|
||||||
tx.addInputState(it)
|
tx.addInputState(it)
|
||||||
if (obligationRemaining >= cashState.data.amount) {
|
if (obligationRemaining >= cashState.data.productAmount) {
|
||||||
tx.addOutputState(cashState.data.move(cashState.data.amount, obligationOwner), notary)
|
tx.addOutputState(cashState.data.move(cashState.data.productAmount, obligationOwner), notary)
|
||||||
obligationRemaining -= cashState.data.amount
|
obligationRemaining -= cashState.data.productAmount
|
||||||
} else {
|
} else {
|
||||||
// Split the state in two, sending the change back to the previous owner
|
// Split the state in two, sending the change back to the previous beneficiary
|
||||||
tx.addOutputState(cashState.data.move(obligationRemaining, obligationOwner), notary)
|
tx.addOutputState(cashState.data.move(obligationRemaining, obligationOwner), notary)
|
||||||
tx.addOutputState(cashState.data.move(cashState.data.amount - obligationRemaining, cashState.data.owner), notary)
|
tx.addOutputState(cashState.data.move(cashState.data.productAmount - obligationRemaining, cashState.data.owner), notary)
|
||||||
obligationRemaining -= Amount(0L, obligationRemaining.token)
|
obligationRemaining -= Amount(0L, obligationRemaining.token)
|
||||||
}
|
}
|
||||||
cashSigners.add(cashState.data.owner)
|
cashSigners.add(cashState.data.owner)
|
||||||
@ -731,17 +730,17 @@ class Obligation<P> : Contract {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a list of settlement states into total from each issuer to a owner.
|
* Convert a list of settlement states into total from each obligor to a beneficiary.
|
||||||
*
|
*
|
||||||
* @return a map of issuer/owner pairs to the balance due.
|
* @return a map of obligor/beneficiary pairs to the balance due.
|
||||||
*/
|
*/
|
||||||
fun <P> extractAmountsDue(currency: Issued<P>, states: Iterable<Obligation.State<P>>): Map<Pair<PublicKey, PublicKey>, Amount<Issued<P>>> {
|
fun <P> extractAmountsDue(product: P, states: Iterable<Obligation.State<P>>): Map<Pair<PublicKey, PublicKey>, Amount<P>> {
|
||||||
val balances = HashMap<Pair<PublicKey, PublicKey>, Amount<Issued<P>>>()
|
val balances = HashMap<Pair<PublicKey, PublicKey>, Amount<P>>()
|
||||||
|
|
||||||
states.forEach { state ->
|
states.forEach { state ->
|
||||||
val key = Pair(state.issuer.owningKey, state.owner)
|
val key = Pair(state.obligor.owningKey, state.beneficiary)
|
||||||
val balance = balances[key] ?: Amount(0L, currency)
|
val balance = balances[key] ?: Amount(0L, product)
|
||||||
balances[key] = balance + state.amount
|
balances[key] = balance + state.productAmount
|
||||||
}
|
}
|
||||||
|
|
||||||
return balances
|
return balances
|
||||||
@ -750,12 +749,12 @@ fun <P> extractAmountsDue(currency: Issued<P>, states: Iterable<Obligation.State
|
|||||||
/**
|
/**
|
||||||
* Net off the amounts due between parties.
|
* 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>>> {
|
fun <P> netAmountsDue(balances: Map<Pair<PublicKey, PublicKey>, Amount<P>>): Map<Pair<PublicKey, PublicKey>, Amount<P>> {
|
||||||
val nettedBalances = HashMap<Pair<PublicKey, PublicKey>, Amount<Issued<P>>>()
|
val nettedBalances = HashMap<Pair<PublicKey, PublicKey>, Amount<P>>()
|
||||||
|
|
||||||
balances.forEach { balance ->
|
balances.forEach { balance ->
|
||||||
val (issuer, owner) = balance.key
|
val (obligor, beneficiary) = balance.key
|
||||||
val oppositeKey = Pair(owner, issuer)
|
val oppositeKey = Pair(beneficiary, obligor)
|
||||||
val opposite = (balances[oppositeKey] ?: Amount(0L, balance.value.token))
|
val opposite = (balances[oppositeKey] ?: Amount(0L, balance.value.token))
|
||||||
// Drop zero balances
|
// Drop zero balances
|
||||||
if (balance.value > opposite) {
|
if (balance.value > opposite) {
|
||||||
@ -770,9 +769,9 @@ fun <P> netAmountsDue(balances: Map<Pair<PublicKey, PublicKey>, Amount<Issued<P>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the total balance movement for each party in the transaction, based off a summary of balances between
|
* Calculate the total balance movement for each party in the transaction, based off a summary of balances between
|
||||||
* each issuer and owner.
|
* each obligor and beneficiary.
|
||||||
*
|
*
|
||||||
* @param balances payments due, indexed by issuer and owner. Zero balances are stripped from the map before being
|
* @param balances payments due, indexed by obligor and beneficiary. Zero balances are stripped from the map before being
|
||||||
* returned.
|
* returned.
|
||||||
*/
|
*/
|
||||||
fun <P> sumAmountsDue(balances: Map<Pair<PublicKey, PublicKey>, Amount<P>>): Map<PublicKey, Long> {
|
fun <P> sumAmountsDue(balances: Map<Pair<PublicKey, PublicKey>, Amount<P>>): Map<PublicKey, Long> {
|
||||||
@ -785,11 +784,11 @@ fun <P> sumAmountsDue(balances: Map<Pair<PublicKey, PublicKey>, Amount<P>>): Map
|
|||||||
}
|
}
|
||||||
|
|
||||||
for ((key, amount) in balances) {
|
for ((key, amount) in balances) {
|
||||||
val (issuer, owner) = key
|
val (obligor, beneficiary) = key
|
||||||
// Subtract it from the issuer
|
// Subtract it from the obligor
|
||||||
sum[issuer] = sum[issuer]!! - amount.quantity
|
sum[obligor] = sum[obligor]!! - amount.quantity
|
||||||
// Add it to the owner
|
// Add it to the beneficiary
|
||||||
sum[owner] = sum[owner]!! + amount.quantity
|
sum[beneficiary] = sum[beneficiary]!! + amount.quantity
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip zero balances
|
// Strip zero balances
|
||||||
@ -807,13 +806,14 @@ fun <P> sumAmountsDue(balances: Map<Pair<PublicKey, PublicKey>, Amount<P>>): Map
|
|||||||
/** Sums the cash states in the list, throwing an exception if there are none.
|
/** 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.
|
* All cash states in the list are presumed to be nettable.
|
||||||
*/
|
*/
|
||||||
fun <P> Iterable<ContractState>.sumObligations() = filterIsInstance<Obligation.State<P>>().map { it.amount }.sumOrThrow()
|
fun <P> Iterable<ContractState>.sumObligations(): Amount<P>
|
||||||
|
= filterIsInstance<Obligation.State<P>>().map { it.amount }.sumOrThrow()
|
||||||
|
|
||||||
/** Sums the cash settlement states in the list, returning null if there are none. */
|
/** Sums the cash settlement states in the list, returning null if there are none. */
|
||||||
fun <P> Iterable<ContractState>.sumObligationsOrNull()
|
fun <P> Iterable<ContractState>.sumObligationsOrNull(): Amount<P>?
|
||||||
= filterIsInstance<Obligation.State<P>>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrNull()
|
= 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. */
|
/** 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>)
|
fun <P> Iterable<ContractState>.sumObligationsOrZero(product: P): Amount<P>
|
||||||
= filterIsInstance<Obligation.State<P>>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(currency)
|
= filterIsInstance<Obligation.State<P>>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(product)
|
||||||
|
|
@ -54,6 +54,9 @@ class Cash : FungibleAsset<Currency>() {
|
|||||||
) : FungibleAsset.State<Currency> {
|
) : FungibleAsset.State<Currency> {
|
||||||
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: PublicKey)
|
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: PublicKey)
|
||||||
: this(Amount(amount.quantity, Issued<Currency>(deposit, amount.token)), owner)
|
: this(Amount(amount.quantity, Issued<Currency>(deposit, amount.token)), owner)
|
||||||
|
|
||||||
|
override val productAmount: Amount<Currency>
|
||||||
|
get() = Amount(amount.quantity, amount.token.product)
|
||||||
override val deposit: PartyAndReference
|
override val deposit: PartyAndReference
|
||||||
get() = amount.token.issuer
|
get() = amount.token.issuer
|
||||||
override val contract = CASH_PROGRAM_ID
|
override val contract = CASH_PROGRAM_ID
|
||||||
@ -62,8 +65,8 @@ class Cash : FungibleAsset<Currency>() {
|
|||||||
override val participants: List<PublicKey>
|
override val participants: List<PublicKey>
|
||||||
get() = listOf(owner)
|
get() = listOf(owner)
|
||||||
|
|
||||||
override fun move(amount: Amount<Issued<Currency>>, owner: PublicKey): FungibleAsset.State<Currency>
|
override fun move(newAmount: Amount<Currency>, newOwner: PublicKey): FungibleAsset.State<Currency>
|
||||||
= copy(amount = amount, owner = owner)
|
= copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner)
|
||||||
|
|
||||||
override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})"
|
override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})"
|
||||||
|
|
||||||
@ -75,9 +78,9 @@ class Cash : FungibleAsset<Currency>() {
|
|||||||
/**
|
/**
|
||||||
* A command stating that money has been moved, optionally to fulfil another contract.
|
* A command stating that money has been moved, optionally to fulfil another contract.
|
||||||
*
|
*
|
||||||
* @param contractHash the hash of the contract this cash is settling, to ensure one cash contract cannot be
|
* @param contractHash the contract this move is for the attention of. Only that contract's verify function
|
||||||
* used to settle multiple contracts. May be null, if this is not relevant to any other contract in the
|
* should take the moved states into account when considering whether it is valid. Typically this will be
|
||||||
* same transaction
|
* null.
|
||||||
*/
|
*/
|
||||||
data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands
|
data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ abstract class FungibleAsset<T> : Contract {
|
|||||||
interface State<T> : FungibleAssetState<T, Issued<T>> {
|
interface State<T> : FungibleAssetState<T, Issued<T>> {
|
||||||
/** Where the underlying currency backing this ledger entry can be found (propagated) */
|
/** Where the underlying currency backing this ledger entry can be found (propagated) */
|
||||||
val deposit: PartyAndReference
|
val deposit: PartyAndReference
|
||||||
override val amount: Amount<Issued<T>>
|
val amount: Amount<Issued<T>>
|
||||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||||
override val owner: PublicKey
|
override val owner: PublicKey
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,6 @@ import java.security.PublicKey
|
|||||||
*/
|
*/
|
||||||
interface FungibleAssetState<T, I> : OwnableState {
|
interface FungibleAssetState<T, I> : OwnableState {
|
||||||
val issuanceDef: I
|
val issuanceDef: I
|
||||||
val amount: Amount<Issued<T>>
|
val productAmount: Amount<T>
|
||||||
fun move(amount: Amount<Issued<T>>, owner: PublicKey): FungibleAssetState<T, I>
|
fun move(amount: Amount<T>, owner: PublicKey): FungibleAssetState<T, I>
|
||||||
}
|
}
|
@ -14,7 +14,11 @@ import com.r3corda.core.contracts.TransactionState
|
|||||||
import com.r3corda.core.crypto.NullPublicKey
|
import com.r3corda.core.crypto.NullPublicKey
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.generateKeyPair
|
import com.r3corda.core.crypto.generateKeyPair
|
||||||
|
import com.r3corda.core.testing.MINI_CORP
|
||||||
|
import com.r3corda.core.testing.TEST_TX_TIME
|
||||||
|
import com.r3corda.core.utilities.nonEmptySetOf
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
// In a real system this would be a persistent map of hash to bytecode and we'd instantiate the object as needed inside
|
// In a real system this would be a persistent map of hash to bytecode and we'd instantiate the object as needed inside
|
||||||
@ -56,6 +60,12 @@ object JavaTestHelpers {
|
|||||||
@JvmStatic fun withNotary(state: Cash.State, notary: Party) = TransactionState(state, notary)
|
@JvmStatic fun withNotary(state: Cash.State, notary: Party) = TransactionState(state, notary)
|
||||||
@JvmStatic fun withDeposit(state: Cash.State, deposit: PartyAndReference) = state.copy(amount = state.amount.copy(token = state.amount.token.copy(issuer = deposit)))
|
@JvmStatic fun withDeposit(state: Cash.State, deposit: PartyAndReference) = state.copy(amount = state.amount.copy(token = state.amount.token.copy(issuer = deposit)))
|
||||||
|
|
||||||
|
@JvmStatic fun <T> at(state: Obligation.State<T>, dueBefore: Instant) = state.copy(template = state.template.copy(dueBefore = dueBefore))
|
||||||
|
@JvmStatic fun <T> at(issuanceDef: Obligation.IssuanceDefinition<T>, dueBefore: Instant) = issuanceDef.copy(template = issuanceDef.template.copy(dueBefore = dueBefore))
|
||||||
|
@JvmStatic fun <T> between(state: Obligation.State<T>, parties: Pair<Party, PublicKey>) = state.copy(obligor = parties.first, beneficiary = parties.second)
|
||||||
|
@JvmStatic fun <T> ownedBy(state: Obligation.State<T>, owner: PublicKey) = state.copy(beneficiary = owner)
|
||||||
|
@JvmStatic fun <T> issuedBy(state: Obligation.State<T>, party: Party) = state.copy(obligor = party)
|
||||||
|
|
||||||
@JvmStatic fun ownedBy(state: CommercialPaper.State, owner: PublicKey) = state.copy(owner = owner)
|
@JvmStatic fun ownedBy(state: CommercialPaper.State, owner: PublicKey) = state.copy(owner = owner)
|
||||||
@JvmStatic fun withNotary(state: CommercialPaper.State, notary: Party) = TransactionState(state, notary)
|
@JvmStatic fun withNotary(state: CommercialPaper.State, notary: Party) = TransactionState(state, notary)
|
||||||
@JvmStatic fun ownedBy(state: ICommercialPaperState, new_owner: PublicKey) = state.withOwner(new_owner)
|
@JvmStatic fun ownedBy(state: ICommercialPaperState, new_owner: PublicKey) = state.withOwner(new_owner)
|
||||||
@ -66,6 +76,12 @@ object JavaTestHelpers {
|
|||||||
Amount<Issued<Currency>>(amount.quantity, Issued<Currency>(DUMMY_CASH_ISSUER, amount.token)),
|
Amount<Issued<Currency>>(amount.quantity, Issued<Currency>(DUMMY_CASH_ISSUER, amount.token)),
|
||||||
NullPublicKey)
|
NullPublicKey)
|
||||||
@JvmStatic fun STATE(amount: Amount<Issued<Currency>>) = Cash.State(amount, NullPublicKey)
|
@JvmStatic fun STATE(amount: Amount<Issued<Currency>>) = Cash.State(amount, NullPublicKey)
|
||||||
|
|
||||||
|
// Allows you to write 100.DOLLARS.OBLIGATION
|
||||||
|
@JvmStatic fun OBLIGATION_DEF(issued: Issued<Currency>)
|
||||||
|
= Obligation.StateTemplate(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(issued), TEST_TX_TIME)
|
||||||
|
@JvmStatic fun OBLIGATION(amount: Amount<Issued<Currency>>) = Obligation.State(Obligation.Lifecycle.NORMAL, MINI_CORP,
|
||||||
|
OBLIGATION_DEF(amount.token), amount.quantity, NullPublicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -75,6 +91,12 @@ infix fun Cash.State.`issued by`(deposit: PartyAndReference) = JavaTestHelpers.i
|
|||||||
infix fun Cash.State.`with notary`(notary: Party) = JavaTestHelpers.withNotary(this, notary)
|
infix fun Cash.State.`with notary`(notary: Party) = JavaTestHelpers.withNotary(this, notary)
|
||||||
infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = JavaTestHelpers.withDeposit(this, deposit)
|
infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = JavaTestHelpers.withDeposit(this, deposit)
|
||||||
|
|
||||||
|
infix fun <T> Obligation.State<T>.`at`(dueBefore: Instant) = JavaTestHelpers.at(this, dueBefore)
|
||||||
|
infix fun <T> Obligation.IssuanceDefinition<T>.`at`(dueBefore: Instant) = JavaTestHelpers.at(this, dueBefore)
|
||||||
|
infix fun <T> Obligation.State<T>.`between`(parties: Pair<Party, PublicKey>) = JavaTestHelpers.between(this, parties)
|
||||||
|
infix fun <T> Obligation.State<T>.`owned by`(owner: PublicKey) = JavaTestHelpers.ownedBy(this, owner)
|
||||||
|
infix fun <T> Obligation.State<T>.`issued by`(party: Party) = JavaTestHelpers.issuedBy(this, party)
|
||||||
|
|
||||||
infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = JavaTestHelpers.ownedBy(this, owner)
|
infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = JavaTestHelpers.ownedBy(this, owner)
|
||||||
infix fun CommercialPaper.State.`with notary`(notary: Party) = JavaTestHelpers.withNotary(this, notary)
|
infix fun CommercialPaper.State.`with notary`(notary: Party) = JavaTestHelpers.withNotary(this, notary)
|
||||||
infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = JavaTestHelpers.ownedBy(this, new_owner)
|
infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = JavaTestHelpers.ownedBy(this, new_owner)
|
||||||
@ -87,3 +109,6 @@ val DUMMY_CASH_ISSUER = Party("Snake Oil Issuer", DUMMY_CASH_ISSUER_KEY.public).
|
|||||||
val Amount<Currency>.CASH: Cash.State get() = JavaTestHelpers.CASH(this)
|
val Amount<Currency>.CASH: Cash.State get() = JavaTestHelpers.CASH(this)
|
||||||
val Amount<Issued<Currency>>.STATE: Cash.State get() = JavaTestHelpers.STATE(this)
|
val Amount<Issued<Currency>>.STATE: Cash.State get() = JavaTestHelpers.STATE(this)
|
||||||
|
|
||||||
|
/** Allows you to write 100.DOLLARS.CASH */
|
||||||
|
val Issued<Currency>.OBLIGATION_DEF: Obligation.StateTemplate<Currency> get() = JavaTestHelpers.OBLIGATION_DEF(this)
|
||||||
|
val Amount<Issued<Currency>>.OBLIGATION: Obligation.State<Currency> get() = JavaTestHelpers.OBLIGATION(this)
|
||||||
|
@ -5,11 +5,11 @@ import com.r3corda.contracts.Obligation.Lifecycle
|
|||||||
import com.r3corda.contracts.testing.*
|
import com.r3corda.contracts.testing.*
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
import com.r3corda.core.seconds
|
|
||||||
import com.r3corda.core.testing.*
|
import com.r3corda.core.testing.*
|
||||||
import com.r3corda.core.utilities.nonEmptySetOf
|
import com.r3corda.core.utilities.nonEmptySetOf
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.test.*
|
import kotlin.test.*
|
||||||
@ -25,15 +25,15 @@ class ObligationTests {
|
|||||||
val sixPm = Instant.parse("2016-01-01T18:00:00.00Z")
|
val sixPm = Instant.parse("2016-01-01T18:00:00.00Z")
|
||||||
val notary = MEGA_CORP
|
val notary = MEGA_CORP
|
||||||
val megaCorpDollarSettlement = Obligation.StateTemplate(trustedCashContract, megaIssuedDollars, fivePm)
|
val megaCorpDollarSettlement = Obligation.StateTemplate(trustedCashContract, megaIssuedDollars, fivePm)
|
||||||
val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuanceDefinitions = megaIssuedPounds)
|
val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuedProducts = megaIssuedPounds)
|
||||||
val inState = Obligation.State(
|
val inState = Obligation.State(
|
||||||
lifecycle = Lifecycle.NORMAL,
|
lifecycle = Lifecycle.NORMAL,
|
||||||
issuer = MEGA_CORP,
|
obligor = MEGA_CORP,
|
||||||
template = megaCorpDollarSettlement,
|
template = megaCorpDollarSettlement,
|
||||||
quantity = 1000.DOLLARS.quantity,
|
quantity = 1000.DOLLARS.quantity,
|
||||||
owner = DUMMY_PUBKEY_1
|
beneficiary = DUMMY_PUBKEY_1
|
||||||
)
|
)
|
||||||
val outState = inState.copy(owner = DUMMY_PUBKEY_2)
|
val outState = inState.copy(beneficiary = DUMMY_PUBKEY_2)
|
||||||
|
|
||||||
private fun obligationTestRoots(group: TransactionGroupDSL<Obligation.State<Currency>>) = group.Roots()
|
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(ALICE, BOB_PUBKEY) `with notary` DUMMY_NOTARY label "Alice's $1,000,000 obligation to Bob")
|
||||||
@ -97,9 +97,9 @@ class ObligationTests {
|
|||||||
transaction {
|
transaction {
|
||||||
output {
|
output {
|
||||||
Obligation.State(
|
Obligation.State(
|
||||||
issuer = MINI_CORP,
|
obligor = MINI_CORP,
|
||||||
quantity = 1000.DOLLARS.quantity,
|
quantity = 1000.DOLLARS.quantity,
|
||||||
owner = DUMMY_PUBKEY_1,
|
beneficiary = DUMMY_PUBKEY_1,
|
||||||
template = megaCorpDollarSettlement
|
template = megaCorpDollarSettlement
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -114,12 +114,12 @@ class ObligationTests {
|
|||||||
// Test generation works.
|
// Test generation works.
|
||||||
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
|
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
|
||||||
Obligation<Currency>().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
|
Obligation<Currency>().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
|
||||||
owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
beneficiary = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
||||||
assertTrue(ptx.inputStates().isEmpty())
|
assertTrue(ptx.inputStates().isEmpty())
|
||||||
val s = ptx.outputStates()[0].data as Obligation.State<Currency>
|
val s = ptx.outputStates()[0].data as Obligation.State<Currency>
|
||||||
assertEquals(100.DOLLARS `issued by` MEGA_CORP.ref(1), s.amount)
|
assertEquals(100.DOLLARS, s.amount)
|
||||||
assertEquals(MINI_CORP, s.issuer)
|
assertEquals(MINI_CORP, s.obligor)
|
||||||
assertEquals(DUMMY_PUBKEY_1, s.owner)
|
assertEquals(DUMMY_PUBKEY_1, s.beneficiary)
|
||||||
assertTrue(ptx.commands()[0].value is Obligation.Commands.Issue<*>)
|
assertTrue(ptx.commands()[0].value is Obligation.Commands.Issue<*>)
|
||||||
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0])
|
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0])
|
||||||
|
|
||||||
@ -131,7 +131,7 @@ class ObligationTests {
|
|||||||
// Move fails: not allowed to summon money.
|
// Move fails: not allowed to summon money.
|
||||||
tweak {
|
tweak {
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||||
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
this `fails requirement` "at obligor MegaCorp the amounts balance"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue works.
|
// Issue works.
|
||||||
@ -189,18 +189,46 @@ class ObligationTests {
|
|||||||
@Test(expected = IllegalStateException::class)
|
@Test(expected = IllegalStateException::class)
|
||||||
fun `reject issuance with inputs`() {
|
fun `reject issuance with inputs`() {
|
||||||
// Issue some obligation
|
// Issue some obligation
|
||||||
var ptx = TransactionType.General.Builder(DUMMY_NOTARY)
|
val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||||
|
Obligation<Currency>().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
|
||||||
Obligation<Currency>().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
|
beneficiary = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY)
|
||||||
owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY)
|
signWith(MINI_CORP_KEY)
|
||||||
ptx.signWith(MINI_CORP_KEY)
|
}.toSignedTransaction()
|
||||||
val tx = ptx.toSignedTransaction()
|
|
||||||
|
|
||||||
// Include the previously issued obligation in a new issuance command
|
// Include the previously issued obligation in a new issuance command
|
||||||
ptx = TransactionType.General.Builder(DUMMY_NOTARY)
|
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
|
||||||
ptx.addInputState(tx.tx.outRef<Obligation.State<Currency>>(0))
|
ptx.addInputState(tx.tx.outRef<Obligation.State<Currency>>(0))
|
||||||
Obligation<Currency>().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
|
Obligation<Currency>().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
|
||||||
owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY)
|
beneficiary = 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 close-out net transaction`() {
|
||||||
|
val obligationAliceToBob = oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY)
|
||||||
|
val obligationBobToAlice = oneMillionDollars.OBLIGATION `between` Pair(BOB, ALICE_PUBKEY)
|
||||||
|
val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||||
|
Obligation<Currency>().generateCloseOutNetting(this, ALICE_PUBKEY, obligationAliceToBob, obligationBobToAlice)
|
||||||
|
signWith(ALICE_KEY)
|
||||||
|
signWith(DUMMY_NOTARY_KEY)
|
||||||
|
}.toSignedTransaction().tx
|
||||||
|
assertEquals(0, tx.outputs.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Test generating a transaction to net two obligations of the different sizes, and confirm the balance is correct. */
|
||||||
|
@Test
|
||||||
|
fun `generate close-out net transaction with remainder`() {
|
||||||
|
val obligationAliceToBob = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION `between` Pair(ALICE, BOB_PUBKEY)
|
||||||
|
val obligationBobToAlice = oneMillionDollars.OBLIGATION `between` Pair(BOB, ALICE_PUBKEY)
|
||||||
|
val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||||
|
Obligation<Currency>().generateCloseOutNetting(this, ALICE_PUBKEY, obligationAliceToBob, obligationBobToAlice)
|
||||||
|
signWith(ALICE_KEY)
|
||||||
|
signWith(DUMMY_NOTARY_KEY)
|
||||||
|
}.toSignedTransaction().tx
|
||||||
|
assertEquals(1, tx.outputs.size)
|
||||||
|
|
||||||
|
val actual = tx.outputs[0].data
|
||||||
|
assertEquals((1000000.DOLLARS `issued by` defaultIssuer).OBLIGATION `between` Pair(ALICE, BOB_PUBKEY), actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Test generating a transaction to net two obligations of the same size, and therefore there are no outputs. */
|
/** Test generating a transaction to net two obligations of the same size, and therefore there are no outputs. */
|
||||||
@ -208,9 +236,13 @@ class ObligationTests {
|
|||||||
fun `generate payment net transaction`() {
|
fun `generate payment net transaction`() {
|
||||||
val obligationAliceToBob = oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY)
|
val obligationAliceToBob = oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY)
|
||||||
val obligationBobToAlice = oneMillionDollars.OBLIGATION `between` Pair(BOB, ALICE_PUBKEY)
|
val obligationBobToAlice = oneMillionDollars.OBLIGATION `between` Pair(BOB, ALICE_PUBKEY)
|
||||||
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
|
val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||||
Obligation<Currency>().generatePaymentNetting(ptx, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
|
Obligation<Currency>().generatePaymentNetting(this, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
|
||||||
assertEquals(0, ptx.outputStates().size)
|
signWith(ALICE_KEY)
|
||||||
|
signWith(BOB_KEY)
|
||||||
|
signWith(DUMMY_NOTARY_KEY)
|
||||||
|
}.toSignedTransaction().tx
|
||||||
|
assertEquals(0, tx.outputs.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Test generating a transaction to two obligations, where one is bigger than the other and therefore there is a remainder. */
|
/** Test generating a transaction to two obligations, where one is bigger than the other and therefore there is a remainder. */
|
||||||
@ -218,25 +250,27 @@ class ObligationTests {
|
|||||||
fun `generate payment net transaction with remainder`() {
|
fun `generate payment net transaction with remainder`() {
|
||||||
val obligationAliceToBob = oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY)
|
val obligationAliceToBob = oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY)
|
||||||
val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION `between` Pair(BOB, ALICE_PUBKEY)
|
val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION `between` Pair(BOB, ALICE_PUBKEY)
|
||||||
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
|
val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||||
Obligation<Currency>().generatePaymentNetting(ptx, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
|
Obligation<Currency>().generatePaymentNetting(this, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
|
||||||
assertEquals(1, ptx.outputStates().size)
|
signWith(ALICE_KEY)
|
||||||
val out = ptx.outputStates().single().data as Obligation.State<Currency>
|
signWith(BOB_KEY)
|
||||||
assertEquals(1000000.DOLLARS.quantity, out.quantity)
|
}.toSignedTransaction().tx
|
||||||
assertEquals(BOB, out.issuer)
|
assertEquals(1, tx.outputs.size)
|
||||||
assertEquals(ALICE_PUBKEY, out.owner)
|
val expected = obligationBobToAlice.copy(quantity = obligationBobToAlice.quantity - obligationAliceToBob.quantity)
|
||||||
|
val actual = tx.outputs[0].data
|
||||||
|
assertEquals(expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Test generating a transaction to mark outputs as having defaulted. */
|
/** Test generating a transaction to mark outputs as having defaulted. */
|
||||||
@Test
|
@Test
|
||||||
fun `generate set lifecycle`() {
|
fun `generate set lifecycle`() {
|
||||||
// Issue some obligation
|
// We don't actually verify the states, this is just here to make things look sensible
|
||||||
val dueBefore = Instant.parse("2010-01-01T17:00:00Z")
|
val dueBefore = TEST_TX_TIME - Duration.ofDays(7)
|
||||||
|
|
||||||
// Generate a transaction issuing the obligation
|
// Generate a transaction issuing the obligation
|
||||||
var tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
var tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||||
Obligation<Currency>().generateIssue(this, MINI_CORP, megaCorpDollarSettlement.copy(dueBefore = dueBefore), 100.DOLLARS.quantity,
|
Obligation<Currency>().generateIssue(this, MINI_CORP, megaCorpDollarSettlement.copy(dueBefore = dueBefore), 100.DOLLARS.quantity,
|
||||||
owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY)
|
beneficiary = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY)
|
||||||
signWith(MINI_CORP_KEY)
|
signWith(MINI_CORP_KEY)
|
||||||
}.toSignedTransaction()
|
}.toSignedTransaction()
|
||||||
var stateAndRef = tx.tx.outRef<Obligation.State<Currency>>(0)
|
var stateAndRef = tx.tx.outRef<Obligation.State<Currency>>(0)
|
||||||
@ -245,44 +279,47 @@ class ObligationTests {
|
|||||||
tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||||
Obligation<Currency>().generateSetLifecycle(this, listOf(stateAndRef), Obligation.Lifecycle.DEFAULTED, DUMMY_NOTARY)
|
Obligation<Currency>().generateSetLifecycle(this, listOf(stateAndRef), Obligation.Lifecycle.DEFAULTED, DUMMY_NOTARY)
|
||||||
signWith(MINI_CORP_KEY)
|
signWith(MINI_CORP_KEY)
|
||||||
}.toSignedTransaction(false)
|
signWith(DUMMY_NOTARY_KEY)
|
||||||
|
}.toSignedTransaction()
|
||||||
assertEquals(1, tx.tx.outputs.size)
|
assertEquals(1, tx.tx.outputs.size)
|
||||||
assertEquals(stateAndRef.state.data.copy(lifecycle = Obligation.Lifecycle.DEFAULTED), tx.tx.outputs[0].data)
|
assertEquals(stateAndRef.state.data.copy(lifecycle = Obligation.Lifecycle.DEFAULTED), tx.tx.outputs[0].data)
|
||||||
|
assertTrue(tx.verify().isEmpty())
|
||||||
|
|
||||||
// And set it back
|
// And set it back
|
||||||
stateAndRef = tx.tx.outRef<Obligation.State<Currency>>(0)
|
stateAndRef = tx.tx.outRef<Obligation.State<Currency>>(0)
|
||||||
tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||||
Obligation<Currency>().generateSetLifecycle(this, listOf(stateAndRef), Obligation.Lifecycle.NORMAL, DUMMY_NOTARY)
|
Obligation<Currency>().generateSetLifecycle(this, listOf(stateAndRef), Obligation.Lifecycle.NORMAL, DUMMY_NOTARY)
|
||||||
signWith(MINI_CORP_KEY)
|
signWith(MINI_CORP_KEY)
|
||||||
}.toSignedTransaction(false)
|
signWith(DUMMY_NOTARY_KEY)
|
||||||
|
}.toSignedTransaction()
|
||||||
assertEquals(1, tx.tx.outputs.size)
|
assertEquals(1, tx.tx.outputs.size)
|
||||||
assertEquals(stateAndRef.state.data.copy(lifecycle = Obligation.Lifecycle.NORMAL), tx.tx.outputs[0].data)
|
assertEquals(stateAndRef.state.data.copy(lifecycle = Obligation.Lifecycle.NORMAL), tx.tx.outputs[0].data)
|
||||||
|
assertTrue(tx.verify().isEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Test generating a transaction to settle an obligation. */
|
/** Test generating a transaction to settle an obligation. */
|
||||||
@Test
|
@Test
|
||||||
fun `generate settlement transaction`() {
|
fun `generate settlement transaction`() {
|
||||||
var ptx: TransactionBuilder
|
val cashTx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||||
|
Cash().generateIssue(this, 100.DOLLARS `issued by` defaultIssuer, MINI_CORP_PUBKEY, DUMMY_NOTARY)
|
||||||
// Generate a transaction to issue the cash we'll need
|
signWith(MEGA_CORP_KEY)
|
||||||
ptx = TransactionType.General.Builder(DUMMY_NOTARY)
|
}.toSignedTransaction().tx
|
||||||
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
|
// Generate a transaction issuing the obligation
|
||||||
ptx = TransactionType.General.Builder(DUMMY_NOTARY)
|
val obligationTx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||||
Obligation<Currency>().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
|
Obligation<Currency>().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
|
||||||
owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY)
|
beneficiary = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY)
|
||||||
ptx.signWith(MINI_CORP_KEY)
|
signWith(MINI_CORP_KEY)
|
||||||
val obligationTx = ptx.toSignedTransaction().tx
|
}.toSignedTransaction().tx
|
||||||
|
|
||||||
// Now generate a transaction settling the obligation
|
// Now generate a transaction settling the obligation
|
||||||
ptx = TransactionType.General.Builder(DUMMY_NOTARY)
|
val settleTx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
|
||||||
val stateAndRef = obligationTx.outRef<Obligation.State<Currency>>(0)
|
Obligation<Currency>().generateSettle(this, listOf(obligationTx.outRef(0)), listOf(cashTx.outRef(0)), DUMMY_NOTARY)
|
||||||
Obligation<Currency>().generateSettle(ptx, listOf(obligationTx.outRef(0)), listOf(cashTx.outRef(0)), DUMMY_NOTARY)
|
signWith(DUMMY_NOTARY_KEY)
|
||||||
assertEquals(2, ptx.inputStates().size)
|
signWith(MINI_CORP_KEY)
|
||||||
assertEquals(1, ptx.outputStates().size)
|
}.toSignedTransaction().tx
|
||||||
|
assertEquals(2, settleTx.inputs.size)
|
||||||
|
assertEquals(1, settleTx.outputs.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -397,7 +434,7 @@ class ObligationTests {
|
|||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Alice's $1,000,000")
|
input("Alice's $1,000,000")
|
||||||
output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY }
|
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) { Obligation.Commands.Settle<Currency>(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Amount(oneMillionDollars.quantity, USD)) }
|
||||||
arg(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
|
arg(ALICE_PUBKEY) { Cash.Commands.Move(Obligation<Currency>().legalContractReference) }
|
||||||
}
|
}
|
||||||
}.verify()
|
}.verify()
|
||||||
@ -415,13 +452,30 @@ class ObligationTests {
|
|||||||
}
|
}
|
||||||
}.expectFailureOfTx(1, "there is a timestamp from the authority")
|
}.expectFailureOfTx(1, "there is a timestamp from the authority")
|
||||||
|
|
||||||
// Try defaulting an obligation
|
// Try defaulting an obligation due in the future
|
||||||
|
val pastTestTime = TEST_TX_TIME - Duration.ofDays(7)
|
||||||
|
val futureTestTime = TEST_TX_TIME + Duration.ofDays(7)
|
||||||
transactionGroupFor<Obligation.State<Currency>>() {
|
transactionGroupFor<Obligation.State<Currency>>() {
|
||||||
obligationTestRoots(this)
|
roots {
|
||||||
|
transaction(oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `at` futureTestTime `with notary` DUMMY_NOTARY label "Alice's $1,000,000 obligation to Bob")
|
||||||
|
}
|
||||||
transaction("Settlement") {
|
transaction("Settlement") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
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) }
|
output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `at` futureTestTime).copy(lifecycle = Obligation.Lifecycle.DEFAULTED) }
|
||||||
arg(BOB_PUBKEY) { Obligation.Commands.SetLifecycle<Currency>(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF), Obligation.Lifecycle.DEFAULTED) }
|
arg(BOB_PUBKEY) { Obligation.Commands.SetLifecycle<Currency>(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF) `at` futureTestTime, Obligation.Lifecycle.DEFAULTED) }
|
||||||
|
timestamp(TEST_TX_TIME)
|
||||||
|
}
|
||||||
|
}.expectFailureOfTx(1, "the due date has passed")
|
||||||
|
|
||||||
|
// Try defaulting an obligation that is now in the past
|
||||||
|
transactionGroupFor<Obligation.State<Currency>>() {
|
||||||
|
roots {
|
||||||
|
transaction(oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `at` pastTestTime `with notary` DUMMY_NOTARY label "Alice's $1,000,000 obligation to Bob")
|
||||||
|
}
|
||||||
|
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) `at` pastTestTime).copy(lifecycle = Obligation.Lifecycle.DEFAULTED) }
|
||||||
|
arg(BOB_PUBKEY) { Obligation.Commands.SetLifecycle<Currency>(Obligation.IssuanceDefinition(ALICE, defaultUsd.OBLIGATION_DEF) `at` pastTestTime, Obligation.Lifecycle.DEFAULTED) }
|
||||||
timestamp(TEST_TX_TIME)
|
timestamp(TEST_TX_TIME)
|
||||||
}
|
}
|
||||||
}.verify()
|
}.verify()
|
||||||
@ -434,12 +488,12 @@ class ObligationTests {
|
|||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||||
tweak {
|
tweak {
|
||||||
input { inState }
|
input { inState }
|
||||||
for (i in 1..4) output { inState.copy(quantity = inState.quantity / 4) }
|
repeat(4) { output { inState.copy(quantity = inState.quantity / 4) } }
|
||||||
this.accepts()
|
this.accepts()
|
||||||
}
|
}
|
||||||
// Merging 4 inputs into 2 outputs works.
|
// Merging 4 inputs into 2 outputs works.
|
||||||
tweak {
|
tweak {
|
||||||
for (i in 1..4) input { inState.copy(quantity = inState.quantity / 4) }
|
repeat(4) { input { inState.copy(quantity = inState.quantity / 4) } }
|
||||||
output { inState.copy(quantity = inState.quantity / 2) }
|
output { inState.copy(quantity = inState.quantity / 2) }
|
||||||
output { inState.copy(quantity = inState.quantity / 2) }
|
output { inState.copy(quantity = inState.quantity / 2) }
|
||||||
this.accepts()
|
this.accepts()
|
||||||
@ -474,7 +528,7 @@ class ObligationTests {
|
|||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { inState }
|
||||||
output { outState `issued by` MINI_CORP }
|
output { outState `issued by` MINI_CORP }
|
||||||
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
this `fails requirement` "at obligor MegaCorp the amounts balance"
|
||||||
}
|
}
|
||||||
// Can't mix currencies.
|
// Can't mix currencies.
|
||||||
transaction {
|
transaction {
|
||||||
@ -489,7 +543,7 @@ class ObligationTests {
|
|||||||
inState.copy(
|
inState.copy(
|
||||||
quantity = 15000,
|
quantity = 15000,
|
||||||
template = megaCorpPoundSettlement,
|
template = megaCorpPoundSettlement,
|
||||||
owner = DUMMY_PUBKEY_2
|
beneficiary = DUMMY_PUBKEY_2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
output { outState.copy(quantity = 115000) }
|
output { outState.copy(quantity = 115000) }
|
||||||
@ -502,7 +556,7 @@ class ObligationTests {
|
|||||||
output { outState }
|
output { outState }
|
||||||
arg(DUMMY_PUBKEY_1) {Obligation.Commands.Move(inState.issuanceDef) }
|
arg(DUMMY_PUBKEY_1) {Obligation.Commands.Move(inState.issuanceDef) }
|
||||||
arg(DUMMY_PUBKEY_1) {Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
arg(DUMMY_PUBKEY_1) {Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
||||||
this `fails requirement` "at issuer MiniCorp the amounts balance"
|
this `fails requirement` "at obligor MiniCorp the amounts balance"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -514,13 +568,13 @@ class ObligationTests {
|
|||||||
output { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
|
output { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) }
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, 100.DOLLARS `issued by` defaultIssuer) }
|
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, 100.DOLLARS) }
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||||
this `fails requirement` "the amounts balance"
|
this `fails requirement` "the amounts balance"
|
||||||
}
|
}
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, 200.DOLLARS `issued by` defaultIssuer) }
|
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, 200.DOLLARS) }
|
||||||
this `fails requirement` "required com.r3corda.contracts.Obligation.Commands.Move command"
|
this `fails requirement` "required com.r3corda.contracts.Obligation.Commands.Move command"
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
@ -539,15 +593,15 @@ class ObligationTests {
|
|||||||
|
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||||
|
|
||||||
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
this `fails requirement` "at obligor MegaCorp the amounts balance"
|
||||||
|
|
||||||
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, 200.DOLLARS `issued by` defaultIssuer) }
|
arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>(inState.issuanceDef, 200.DOLLARS) }
|
||||||
tweak {
|
tweak {
|
||||||
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>((inState `issued by` MINI_CORP).issuanceDef, 0.DOLLARS `issued by` defaultIssuer) }
|
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>((inState `issued by` MINI_CORP).issuanceDef, 0.DOLLARS) }
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
||||||
this `fails requirement` "at issuer MiniCorp the amounts balance"
|
this `fails requirement` "at obligor MiniCorp the amounts balance"
|
||||||
}
|
}
|
||||||
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>((inState `issued by` MINI_CORP).issuanceDef, 200.DOLLARS `issued by` defaultIssuer) }
|
arg(MINI_CORP_PUBKEY) { Obligation.Commands.Exit<Currency>((inState `issued by` MINI_CORP).issuanceDef, 200.DOLLARS) }
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
||||||
this.accepts()
|
this.accepts()
|
||||||
}
|
}
|
||||||
@ -562,19 +616,19 @@ class ObligationTests {
|
|||||||
|
|
||||||
// Can't merge them together.
|
// Can't merge them together.
|
||||||
tweak {
|
tweak {
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2, quantity = 200000L) }
|
output { inState.copy(beneficiary = DUMMY_PUBKEY_2, quantity = 200000L) }
|
||||||
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
this `fails requirement` "at obligor MegaCorp the amounts balance"
|
||||||
}
|
}
|
||||||
// Missing MiniCorp deposit
|
// Missing MiniCorp deposit
|
||||||
tweak {
|
tweak {
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) }
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) }
|
||||||
this `fails requirement` "at issuer MegaCorp the amounts balance"
|
this `fails requirement` "at obligor MegaCorp the amounts balance"
|
||||||
}
|
}
|
||||||
|
|
||||||
// This works.
|
// This works.
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2) }
|
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) }
|
||||||
output { inState.copy(owner = DUMMY_PUBKEY_2) `issued by` MINI_CORP }
|
output { inState.copy(beneficiary = DUMMY_PUBKEY_2) `issued by` MINI_CORP }
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
|
||||||
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move((inState `issued by` MINI_CORP).issuanceDef) }
|
||||||
this.accepts()
|
this.accepts()
|
||||||
@ -631,7 +685,7 @@ class ObligationTests {
|
|||||||
// States must not be nettable if the trusted issuers differ
|
// States must not be nettable if the trusted issuers differ
|
||||||
val miniCorpIssuer = nonEmptySetOf(Issued<Currency>(MINI_CORP.ref(1), USD))
|
val miniCorpIssuer = nonEmptySetOf(Issued<Currency>(MINI_CORP.ref(1), USD))
|
||||||
assertNotEquals(fiveKDollarsFromMegaToMega.bilateralNetState,
|
assertNotEquals(fiveKDollarsFromMegaToMega.bilateralNetState,
|
||||||
fiveKDollarsFromMegaToMega.copy(template = megaCorpDollarSettlement.copy(acceptableIssuanceDefinitions = miniCorpIssuer)).bilateralNetState)
|
fiveKDollarsFromMegaToMega.copy(template = megaCorpDollarSettlement.copy(acceptableIssuedProducts = miniCorpIssuer)).bilateralNetState)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = IllegalStateException::class)
|
@Test(expected = IllegalStateException::class)
|
||||||
@ -686,7 +740,7 @@ class ObligationTests {
|
|||||||
val fiveKDollarsFromMegaToMini = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement,
|
val fiveKDollarsFromMegaToMini = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement,
|
||||||
5000.DOLLARS.quantity, MINI_CORP_PUBKEY)
|
5000.DOLLARS.quantity, MINI_CORP_PUBKEY)
|
||||||
val expected = mapOf(Pair(Pair(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), fiveKDollarsFromMegaToMini.amount))
|
val expected = mapOf(Pair(Pair(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), fiveKDollarsFromMegaToMini.amount))
|
||||||
val actual = extractAmountsDue<Currency>(defaultUsd, listOf(fiveKDollarsFromMegaToMini))
|
val actual = extractAmountsDue<Currency>(USD, listOf(fiveKDollarsFromMegaToMini))
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -697,8 +751,8 @@ class ObligationTests {
|
|||||||
Pair(Pair(ALICE_PUBKEY, BOB_PUBKEY), Amount(100000000, GBP)),
|
Pair(Pair(ALICE_PUBKEY, BOB_PUBKEY), Amount(100000000, GBP)),
|
||||||
Pair(Pair(BOB_PUBKEY, ALICE_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 expected: Map<Pair<PublicKey, PublicKey>, Amount<Currency>> = emptyMap() // Zero balances are stripped before returning
|
||||||
val actual = sumAmountsDue(balanced)
|
val actual = netAmountsDue<Currency>(balanced)
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -706,11 +760,11 @@ class ObligationTests {
|
|||||||
fun `netting difference balances due between parties`() {
|
fun `netting difference balances due between parties`() {
|
||||||
// Now try it with two balances, which cancel each other out
|
// Now try it with two balances, which cancel each other out
|
||||||
val balanced = mapOf(
|
val balanced = mapOf(
|
||||||
Pair(Pair(ALICE_PUBKEY, BOB_PUBKEY), Amount(100000000, GBP) `issued by` defaultIssuer),
|
Pair(Pair(ALICE_PUBKEY, BOB_PUBKEY), Amount(100000000, GBP)),
|
||||||
Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(200000000, GBP) `issued by` defaultIssuer)
|
Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(200000000, GBP))
|
||||||
)
|
)
|
||||||
val expected = mapOf(
|
val expected = mapOf(
|
||||||
Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP) `issued by` defaultIssuer)
|
Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP))
|
||||||
)
|
)
|
||||||
var actual = netAmountsDue<Currency>(balanced)
|
var actual = netAmountsDue<Currency>(balanced)
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
@ -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)
|
|
Loading…
Reference in New Issue
Block a user