>, owner: PublicKey): Obligation.State
- = copy(quantity = amount.quantity, owner = owner)
+ override fun move(newAmount: Amount
, newOwner: PublicKey): State
+ = copy(quantity = newAmount.quantity, beneficiary = newOwner)
override fun toString() = when (lifecycle) {
- Lifecycle.NORMAL -> "${Emoji.bagOfCash}Debt($amount due $dueBefore to ${owner.toStringShort()})"
- Lifecycle.DEFAULTED -> "${Emoji.bagOfCash}Debt($amount unpaid by $dueBefore to ${owner.toStringShort()})"
+ Lifecycle.NORMAL -> "${Emoji.bagOfCash}Debt($amount due $dueBefore to ${beneficiary.toStringShort()})"
+ Lifecycle.DEFAULTED -> "${Emoji.bagOfCash}Debt($amount unpaid by $dueBefore to ${beneficiary.toStringShort()})"
}
override val bilateralNetState: BilateralNetState
get() {
check(lifecycle == Lifecycle.NORMAL)
- return BilateralNetState(setOf(issuer.owningKey, owner), template)
+ return BilateralNetState(setOf(obligor.owningKey, beneficiary), template)
}
val multilateralNetState: MultilateralNetState
get() {
@@ -182,78 +176,83 @@ class Obligation
: Contract {
val netB = other.bilateralNetState
require(netA == netB) { "net substates of the two state objects must be identical" }
- if (issuer.owningKey == other.issuer.owningKey) {
- // Both sides are from the same issuer to owner
+ if (obligor.owningKey == other.obligor.owningKey) {
+ // Both sides are from the same obligor to beneficiary
return copy(quantity = quantity + other.quantity)
} else {
- // Issuer and owner are backwards
+ // Issuer and beneficiary are backwards
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 AggregateCommands
: CommandData {
+ /** Interface for commands that apply to states grouped by issuance definition */
+ interface IssuanceCommands
: CommandData {
val aggregateState: IssuanceDefinition
}
// Just for grouping
interface Commands : CommandData {
/**
- * Net two or more cash settlement states together in a close-out netting style. Limited to bilateral netting
- * as only the owner (not the issuer) needs to sign.
+ * Net two or more obligation states together in a close-out netting style. Limited to bilateral netting
+ * as only the beneficiary (not the obligor) needs to sign.
*/
data class Net(val type: NetType) : Commands
/**
* A command stating that a debt has been moved, optionally to fulfil another contract.
*
- * @param contractHash the hash of contract's code, which indicates to that contract that the
- * obligation states moved in this transaction are for their sole attention.
- * This is a single value to ensure the same state(s) cannot be used to settle multiple contracts.
- * May be null, if this is not relevant to any other contract in the same transaction.
+ * @param contractHash the contract this move is for the attention of. Only that contract's verify function
+ * should take the moved states into account when considering whether it is valid. Typically this will be
+ * null.
*/
data class Move
(override val aggregateState: IssuanceDefinition
,
- override val contractHash: SecureHash? = null) : Commands, AggregateCommands
, MoveCommand
+ override val contractHash: SecureHash? = null) : Commands, IssuanceCommands
, MoveCommand
/**
- * Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction
- * has a unique ID even when there are no inputs.
+ * Allows new obligation states to be issued into existence: the nonce ("number used once") ensures the
+ * transaction has a unique ID even when there are no inputs.
*/
data class Issue
(override val aggregateState: IssuanceDefinition
,
- val nonce: Long = random63BitValue()) : Commands, AggregateCommands
+ val nonce: Long = random63BitValue()) : Commands, IssuanceCommands
/**
- * A command stating that the issuer is settling some or all of the amount owed by paying in a suitable cash
- * contract. If this reduces the balance to zero, the contract moves to the settled state.
- * @see [Cash.Commands.Move]
+ * A command stating that the obligor is settling some or all of the amount owed by transferring a suitable
+ * state object to the beneficiary. If this reduces the balance to zero, the state object is destroyed.
+ * @see [MoveCommand]
*/
data class Settle
(override val aggregateState: IssuanceDefinition
,
- val amount: Amount>) : Commands, AggregateCommands
+ val amount: Amount
) : Commands, IssuanceCommands
/**
- * 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.
*/
data class SetLifecycle
(override val aggregateState: IssuanceDefinition
,
- val lifecycle: Lifecycle) : Commands, AggregateCommands
+ val lifecycle: Lifecycle) : Commands, IssuanceCommands
{
+ 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
- * either settlement outside of the ledger, or that the issuer is unable to pay.
+ * A command stating that the debt is being released by the beneficiary. Normally would indicate
+ * either settlement outside of the ledger, or that the obligor is unable to pay.
*/
data class Exit
(override val aggregateState: IssuanceDefinition
,
- val amount: Amount>) : Commands, AggregateCommands
+ val amount: Amount
) : Commands, IssuanceCommands
}
/** This is the function EVERYONE runs */
override fun verify(tx: TransactionForContract) {
- val commands = tx.commands.select()
+ val commands = tx.commands.select()
// Net commands are special, and cross issuance definitions, so handle them first
- val netCommands = commands.select()
+ val netCommands = commands.select()
if (netCommands.isNotEmpty()) {
val netCommand = netCommands.single()
val groups = when (netCommand.value.type) {
@@ -264,40 +263,39 @@ class Obligation : Contract {
verifyNetCommand(inputs, outputs, netCommand, key)
}
} else {
- val commandGroups = tx.groupCommands, IssuanceDefinition> { it.value.aggregateState }
+ val commandGroups = tx.groupCommands, IssuanceDefinition> { it.value.aggregateState }
// Each group is a set of input/output states with distinct issuance definitions. These types
// of settlement are not fungible and must be kept separated for bookkeeping purposes.
- val groups = tx.groupStates() { it: State
-> it.issuanceDef }
+ val groups = tx.groupStates() { it: State
-> it.aggregateState }
for ((inputs, outputs, key) in groups) {
// Either inputs or outputs could be empty.
- val issuer = key.issuer
- val commands = commandGroups[key] ?: emptyList()
+ val obligor = key.obligor
requireThat {
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
}
- verifyCommandGroup(tx, commands, inputs, outputs, issuer, key)
+ verifyCommandGroup(tx, commandGroups[key] ?: emptyList(), inputs, outputs, obligor, key)
}
}
}
private fun verifyCommandGroup(tx: TransactionForContract,
- commands: List>>,
+ commands: List>>,
inputs: List>,
outputs: List>,
- issuer: Party,
+ obligor: Party,
key: IssuanceDefinition) {
- // We've already pre-grouped by currency amongst other fields, and verified above that every state specifies
- // at least one acceptable cash issuance definition, so we can just use the first issuance definition to
- // determine currency
- val currency = key.template.acceptableIssuanceDefinitions.first()
+ // We've already pre-grouped by product amongst other fields, and verified above that every state specifies
+ // at least one acceptable issuance definition, so we can just use the first issuance definition to
+ // determine product
+ val issued = key.template.acceptableIssuedProducts.first()
// Issue, default, net and settle commands are all single commands (there's only ever one of them, and
// they exclude all other commands).
val issueCommand = commands.select>().firstOrNull()
- val defaultCommand = commands.select>().firstOrNull()
+ val setLifecycleCommand = commands.select>().firstOrNull()
val settleCommand = commands.select>().firstOrNull()
if (commands.size != 1) {
@@ -308,8 +306,8 @@ class Obligation : Contract {
// Issue, default and net commands are special, and do not follow normal input/output summing rules, so
// deal with them first
- if (defaultCommand != null) {
- verifyDefaultCommand(inputs, outputs, tx, defaultCommand)
+ if (setLifecycleCommand != null) {
+ verifySetLifecycleCommand(inputs, outputs, tx, setLifecycleCommand)
} else {
// Only the default command processes inputs/outputs that are not in the normal state
// TODO: Need to be able to exit defaulted amounts
@@ -318,15 +316,15 @@ class Obligation
: Contract {
"all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL }
}
if (issueCommand != null) {
- verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer)
+ verifyIssueCommand(inputs, outputs, issueCommand, issued, obligor)
} else if (settleCommand != null) {
// Perhaps through an abundance of caution, settlement is enforced as its own command.
// This could perhaps be merged into verifyBalanceChange() later, however doing so introduces a lot
// of scope for making it more opaque what's going on in a transaction and whether it's as expected
// by all parties.
- verifySettleCommand(inputs, outputs, tx, settleCommand, currency, issuer, key)
+ verifySettleCommand(inputs, outputs, tx, settleCommand, issued, obligor, key)
} else {
- verifyBalanceChange(inputs, outputs, commands, currency, issuer)
+ verifyBalanceChange(inputs, outputs, commands, issued.product, obligor)
}
}
}
@@ -339,32 +337,32 @@ class Obligation
: Contract {
*/
private fun verifyBalanceChange(inputs: List>,
outputs: List>,
- commands: List>>,
- currency: Issued,
- issuer: Party) {
+ commands: List>>,
+ product: P,
+ obligor: Party) {
// Sum up how much settlement owed there is in the inputs, and the difference in outputs. The difference should
// be matched by exit commands representing the extracted amount.
val inputAmount = inputs.sumObligationsOrNull() ?: 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>()
val requiredExitSignatures = HashSet()
- val amountExitingLedger: Amount> = if (exitCommands.isNotEmpty()) {
+ val amountExitingLedger: Amount = if (exitCommands.isNotEmpty()) {
require(exitCommands.size == 1) { "There can only be one exit command" }
val exitCommand = exitCommands.single()
- // If we want to remove debt from the ledger, that must be signed for by the owner. For now we require exit
- // commands to be signed by all input owners, unlocking the full input amount, rather than trying to detangle
+ // 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 beneficiarys, unlocking the full input amount, rather than trying to detangle
// exactly who exited what.
- requiredExitSignatures.addAll(inputs.map { it.owner })
+ requiredExitSignatures.addAll(inputs.map { it.beneficiary })
exitCommand.value.amount
} else {
- Amount(0, currency)
+ Amount(0, product)
}
requireThat {
"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)
}
@@ -375,39 +373,36 @@ class Obligation
: Contract {
* A default command mutates inputs and produces identical outputs, except that the lifecycle changes.
*/
@VisibleForTesting
- protected fun verifyDefaultCommand(inputs: List>,
- outputs: List>,
- tx: TransactionForContract,
- setLifecycleCommand: AuthenticatedObject>) {
+ protected fun verifySetLifecycleCommand(inputs: List>,
+ outputs: List>,
+ tx: TransactionForContract,
+ setLifecycleCommand: AuthenticatedObject>) {
// Default must not change anything except lifecycle, so number of inputs and outputs must match
// exactly.
require(inputs.size == outputs.size) { "Number of inputs and outputs must match" }
// If we have an default command, perform special processing: issued contracts can only be defaulted
- // after the due date, and default/reset can only be done by the owner
- val expectedOutputState: Lifecycle = setLifecycleCommand.value.lifecycle
- val expectedInputState: Lifecycle
-
- expectedInputState = when (expectedOutputState) {
- Lifecycle.DEFAULTED -> Lifecycle.NORMAL
- Lifecycle.NORMAL -> Lifecycle.DEFAULTED
- }
+ // after the due date, and default/reset can only be done by the beneficiary
+ val expectedInputLifecycle: Lifecycle = setLifecycleCommand.value.inverse
+ val expectedOutputLifecycle: Lifecycle = setLifecycleCommand.value.lifecycle
// Check that we're past the deadline for ALL involved inputs, and that the output states correspond 1:1
for ((stateIdx, input) in inputs.withIndex()) {
val actualOutput = outputs[stateIdx]
val deadline = input.dueBefore
+ // 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 expectedOutput: State = input.copy(lifecycle = expectedOutputState)
+ val expectedOutput: State
= input.copy(lifecycle = expectedOutputLifecycle)
requireThat {
"there is a timestamp from the authority" by (timestamp != null)
- "the due date has passed" by (timestamp?.after?.isBefore(deadline) ?: false)
- "input state lifecycle is correct" by (input.lifecycle == expectedInputState)
+ "the due date has passed" by (timestamp!!.after?.isAfter(deadline) ?: false)
+ "input state lifecycle is correct" by (input.lifecycle == expectedInputLifecycle)
"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()
requireThat {
"the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys)
@@ -417,18 +412,17 @@ class Obligation
: Contract {
@VisibleForTesting
protected fun verifyIssueCommand(inputs: List>,
outputs: List>,
- tx: TransactionForContract,
issueCommand: AuthenticatedObject>,
- currency: Issued,
- issuer: Party) {
+ issued: Issued
,
+ obligor: Party) {
// If we have an issue command, perform special processing: the group is must have no inputs,
- // and that signatures are present for all issuers.
+ // and that signatures are present for all obligors.
- val inputAmount = inputs.sumObligationsOrZero(currency)
- val outputAmount = outputs.sumObligations
()
+ val inputAmount: Amount
= inputs.sumObligationsOrZero(issued.product)
+ val outputAmount: Amount
= outputs.sumObligations
()
requireThat {
"the issue command has a nonce" by (issueCommand.value.nonce != 0L)
- "output deposits are owned by a command signer" by (issuer in issueCommand.signingParties)
+ "output deposits are owned by a command signer" by (obligor in issueCommand.signingParties)
"output values sum to more than the inputs" by (outputAmount > inputAmount)
"valid settlement issuance definition is not this issuance definition" by inputs.none { it.issuanceDef in it.acceptableIssuanceDefinitions }
}
@@ -448,24 +442,25 @@ class Obligation
: Contract {
"all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL }
}
- val token = netState.issued
- // Create two maps of balances from issuers to owners, one for input states, the other for output states.
- val inputBalances = extractAmountsDue(token, inputs)
- val outputBalances = extractAmountsDue(token, outputs)
+ val template = netState.template
+ val product = template.product
+ // Create two maps of balances from obligors to beneficiaries, one for input states, the other for output states.
+ 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.
// The two summaries must match, reflecting that the amounts owed match on both input and output.
requireThat {
- "all input states use the expected token" by (inputs.all { it.issuanceDef.issued == token })
- "all output states use the expected token" by (outputs.all { it.issuanceDef.issued == token })
+ "all input states use the same template" by (inputs.all { it.template == template })
+ "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))
}
// 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) {
// 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
// placeholder while exact requirements are established), or fail the transaction.
NetType.PAYMENT -> require(command.signers.containsAll(involvedParties)) { "all involved parties have signed" }
@@ -479,20 +474,20 @@ class Obligation
: Contract {
outputs: List>,
tx: TransactionForContract,
command: AuthenticatedObject>,
- currency: Issued,
- issuer: Party,
+ issued: Issued
,
+ obligor: Party,
key: IssuanceDefinition
) {
val template = key.template
- val inputAmount = inputs.sumObligationsOrNull
() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
- val outputAmount = outputs.sumObligationsOrZero(currency)
+ val inputAmount: Amount
= inputs.sumObligationsOrNull
() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
+ val outputAmount: Amount
= outputs.sumObligationsOrZero(issued.product)
- // Sum up all cash contracts that are moving and fulfil our requirements
+ // Sum up all asset 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
- // care about counting how much cash is output in this transaction. We then calculate the difference in
+ // The fungible asset contract verification handles ensuring there's inputs enough to cover the output states,
+ // we only care about counting how much is output in this transaction. We then calculate the difference in
// settlement amounts between the transaction inputs and outputs, and the two must match. No elimination is
- // done of amounts paid in by each owner, as it's presumed the owners have enough sense to do that themselves.
- // Therefore if someone actually signed the following transaction:
+ // done of amounts paid in by each beneficiary, as it's presumed the beneficiaries have enough sense to do that
+ // themselves. Therefore if someone actually signed the following transaction (using cash just for an example):
//
// Inputs:
// £1m cash owned by B
@@ -500,37 +495,37 @@ class Obligation
: Contract {
// Outputs:
// £1m cash owned by B
// Commands:
- // Settle (signed by B)
+ // Settle (signed by A)
// Move (signed by B)
//
// That would pass this check. Ensuring they do not is best addressed in the transaction generation stage.
- val cashStates = tx.outStates.filterIsInstance>()
- val acceptableCashStates = cashStates
- // TODO: This filter is nonsense, because it just checks there is a cash contract loaded, we need to
- // verify the cash contract is the cash contract we expect.
+ val assetStates = tx.outputs.filterIsInstance>()
+ val acceptableAssetStates = assetStates
+ // TODO: This filter is nonsense, because it just checks there is an asset contract loaded, we need to
+ // verify the asset contract is the asset contract we expect.
// Something like:
- // attachments.mustHaveOneOf(key.acceptableCashContract)
+ // attachments.mustHaveOneOf(key.acceptableAssetContract)
.filter { it.contract.legalContractReference in template.acceptableContracts }
// Restrict the states to those of the correct issuance definition (this normally
- // covers currency and issuer, but is opaque to us)
- .filter { it.issuanceDef in template.acceptableIssuanceDefinitions }
+ // covers issued product and obligor, but is opaque to us)
+ .filter { it.issuanceDef in template.acceptableIssuedProducts }
// Catch that there's nothing useful here, so we can dump out a useful error
requireThat {
- "there are cash state outputs" by (cashStates.size > 0)
- "there are defined acceptable cash states" by (acceptableCashStates.size > 0)
+ "there are fungible asset state outputs" by (assetStates.size > 0)
+ "there are defined acceptable fungible asset states" by (acceptableAssetStates.size > 0)
}
- val amountReceivedByOwner = acceptableCashStates.groupBy { it.owner }
+ val amountReceivedByOwner = acceptableAssetStates.groupBy { it.owner }
// Note we really do want to search all commands, because we want move commands of other contracts, not just
// this one.
val moveCommands = tx.commands.select()
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 }) {
- val settled = amountReceivedByOwner[owner]?.sumCashOrNull()
+ for ((beneficiary, obligations) in inputs.groupBy { it.beneficiary }) {
+ val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull()
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" }
totalPenniesSettled += settled.quantity
}
@@ -542,19 +537,19 @@ class Obligation
: Contract {
"all move commands relate to this contract" by (moveCommands.map { it.value.contractHash }
.all { it == null || it == legalContractReference })
"contract does not try to consume itself" by (moveCommands.map { it.value }.filterIsInstance>()
- .none { it.aggregateState.issued in template.acceptableIssuanceDefinitions })
- "amounts paid must match recipients to settle" by inputs.map { it.owner }.containsAll(amountReceivedByOwner.keys)
- "signatures are present from all issuers" by command.signers.containsAll(requiredSigners)
+ .none { it.aggregateState == key })
+ "amounts paid must match recipients to settle" by inputs.map { it.beneficiary }.containsAll(amountReceivedByOwner.keys)
+ "signatures are present from all obligors" by command.signers.containsAll(requiredSigners)
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
- "at issuer ${issuer.name} the obligations after settlement balance" by
- (inputAmount == outputAmount + Amount(totalPenniesSettled, currency))
+ "at obligor ${obligor.name} the obligations after settlement balance" by
+ (inputAmount == outputAmount + Amount(totalPenniesSettled, issued.product))
}
}
/**
* 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,
* and same parties involved).
*/
@@ -570,7 +565,9 @@ class Obligation : Contract {
"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)
}
@@ -578,20 +575,20 @@ class Obligation
: Contract {
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
*/
fun generateIssue(tx: TransactionBuilder,
- issuer: Party,
+ obligor: Party,
issuanceDef: StateTemplate
,
pennies: Long,
- owner: PublicKey,
+ beneficiary: PublicKey,
notary: Party) {
check(tx.inputStates().isEmpty())
check(tx.outputStates().map { it.data }.sumObligationsOrNull
() == null)
- val aggregateState = IssuanceDefinition(issuer, issuanceDef)
- tx.addOutputState(State(Lifecycle.NORMAL, issuer, issuanceDef, pennies, owner), notary)
- tx.addCommand(Commands.Issue(aggregateState), issuer.owningKey)
+ val aggregateState = IssuanceDefinition(obligor, issuanceDef)
+ tx.addOutputState(State(Lifecycle.NORMAL, obligor, issuanceDef, pennies, beneficiary), notary)
+ tx.addCommand(Commands.Issue(aggregateState), obligor.owningKey)
}
fun generatePaymentNetting(tx: TransactionBuilder,
- currency: Issued
,
+ issued: Issued
,
notary: Party,
vararg states: State
) {
requireThat {
@@ -599,20 +596,20 @@ class Obligation
: Contract {
}
val groups = states.groupBy { it.multilateralNetState }
val partyLookup = HashMap()
- 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.
- 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) {
// Extract the net balances
- val netBalances = netAmountsDue(extractAmountsDue(currency, states.asIterable()))
+ val netBalances = netAmountsDue(extractAmountsDue(issued.product, states.asIterable()))
netBalances
// Convert the balances into obligation state objects
.map { entry ->
State(Lifecycle.NORMAL, partyLookup[entry.key.first]!!,
- netState.issuanceDef, entry.value.quantity, entry.key.second)
+ netState.template, entry.value.quantity, entry.key.second)
}
// Add the new states to the TX
.forEach { tx.addOutputState(it, notary) }
@@ -647,7 +644,7 @@ class Obligation : Contract {
val outState = stateAndRef.state.data.copy(lifecycle = lifecycle)
tx.addInputState(stateAndRef)
tx.addOutputState(outState, notary)
- partiesUsed.add(stateAndRef.state.data.owner)
+ partiesUsed.add(stateAndRef.state.data.beneficiary)
}
tx.addCommand(Commands.SetLifecycle(aggregateState, lifecycle), partiesUsed.distinct())
}
@@ -657,54 +654,55 @@ class Obligation
: Contract {
/**
* @param statesAndRefs a list of state objects, which MUST all have the same aggregate state. This is done as
* only a single settlement command can be present in a transaction, to avoid potential problems with allocating
- * cash to different obligation issuances.
- * @param cashStatesAndRefs a list of cash state objects, which MUST all be in the same currency. It is strongly
- * encouraged that these all have the same owner.
+ * assets to different obligation issuances.
+ * @param assetStatesAndRefs a list of fungible asset state objects, which MUST all be of the same issued product.
+ * It is strongly encouraged that these all have the same beneficiary.
+ * @param moveCommand the command used to move the asset state objects to their new owner.
*/
fun generateSettle(tx: TransactionBuilder,
statesAndRefs: Iterable>>,
- cashStatesAndRefs: Iterable>>,
+ assetStatesAndRefs: Iterable>>,
+ moveCommand: MoveCommand,
notary: Party) {
val states = statesAndRefs.map { it.state }
- val notary = states.first().notary
- val obligationIssuer = states.first().data.issuer
- val obligationOwner = states.first().data.owner
+ val obligationIssuer = states.first().data.obligor
+ val obligationOwner = states.first().data.beneficiary
requireThat {
- "all cash states use the same notary" by (cashStatesAndRefs.all { it.state.notary == notary })
+ "all fungible asset states use the same notary" by (assetStatesAndRefs.all { it.state.notary == notary })
"all obligation states are in the normal state" by (statesAndRefs.all { it.state.data.lifecycle == Lifecycle.NORMAL })
"all obligation states use the same notary" by (statesAndRefs.all { it.state.notary == notary })
- "all obligation states have the same issuer" by (statesAndRefs.all { it.state.data.issuer == obligationIssuer })
- "all obligation states have the same owner" by (statesAndRefs.all { it.state.data.owner == obligationOwner })
+ "all obligation states have the same obligor" by (statesAndRefs.all { it.state.data.obligor == obligationIssuer })
+ "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,
- // one for cash, and step through each in a semi-synced manner. For now however we just bundle all the states
+ // one for the assets, and step through each in a semi-synced manner. For now however we just bundle all the states
// on each side together
val issuanceDef = getIssuanceDefinitionOrThrow(statesAndRefs.map { it.state.data })
val template = issuanceDef.template
- val obligationTotal: Amount> = states.map { it.data }.sumObligations()
- var obligationRemaining: Amount> = obligationTotal
- val cashSigners = HashSet()
+ val obligationTotal: Amount = states.map { it.data }.sumObligations
()
+ var obligationRemaining: Amount
= obligationTotal
+ val assetSigners = HashSet()
statesAndRefs.forEach { tx.addInputState(it) }
- // Move the cash to the new owner
- cashStatesAndRefs.forEach {
+ // Move the assets to the new beneficiary
+ assetStatesAndRefs.forEach {
if (obligationRemaining.quantity > 0L) {
- val cashState = it.state
+ val assetState = it.state
tx.addInputState(it)
- if (obligationRemaining >= cashState.data.amount) {
- tx.addOutputState(cashState.data.move(cashState.data.amount, obligationOwner), notary)
- obligationRemaining -= cashState.data.amount
+ if (obligationRemaining >= assetState.data.productAmount) {
+ tx.addOutputState(assetState.data.move(assetState.data.productAmount, obligationOwner), notary)
+ obligationRemaining -= assetState.data.productAmount
} else {
- // Split the state in two, sending the change back to the previous owner
- tx.addOutputState(cashState.data.move(obligationRemaining, obligationOwner), notary)
- tx.addOutputState(cashState.data.move(cashState.data.amount - obligationRemaining, cashState.data.owner), notary)
+ // Split the state in two, sending the change back to the previous beneficiary
+ tx.addOutputState(assetState.data.move(obligationRemaining, obligationOwner), notary)
+ tx.addOutputState(assetState.data.move(assetState.data.productAmount - obligationRemaining, assetState.data.owner), notary)
obligationRemaining -= Amount(0L, obligationRemaining.token)
}
- cashSigners.add(cashState.data.owner)
+ assetSigners.add(assetState.data.owner)
}
}
@@ -715,8 +713,8 @@ class Obligation : Contract {
// Destroy all of the states
}
- // Add the cash move command and obligation settle
- tx.addCommand(Cash.Commands.Move(), cashSigners.toList())
+ // Add the asset move command and obligation settle
+ tx.addCommand(moveCommand, assetSigners.toList())
tx.addCommand(Commands.Settle(issuanceDef, obligationTotal - obligationRemaining), obligationOwner)
}
@@ -731,17 +729,17 @@ class Obligation
: 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
extractAmountsDue(currency: Issued
, states: Iterable>): Map, Amount>> {
- val balances = HashMap, Amount>>()
+fun extractAmountsDue(product: P, states: Iterable>): Map, Amount> {
+ val balances = HashMap, Amount>()
states.forEach { state ->
- val key = Pair(state.issuer.owningKey, state.owner)
- val balance = balances[key] ?: Amount(0L, currency)
- balances[key] = balance + state.amount
+ val key = Pair(state.obligor.owningKey, state.beneficiary)
+ val balance = balances[key] ?: Amount(0L, product)
+ balances[key] = balance + state.productAmount
}
return balances
@@ -750,12 +748,12 @@ fun
extractAmountsDue(currency: Issued
, states: Iterable netAmountsDue(balances: Map, Amount>>): Map, Amount>> {
- val nettedBalances = HashMap, Amount>>()
+fun netAmountsDue(balances: Map, Amount>): Map, Amount> {
+ val nettedBalances = HashMap, Amount>()
balances.forEach { balance ->
- val (issuer, owner) = balance.key
- val oppositeKey = Pair(owner, issuer)
+ val (obligor, beneficiary) = balance.key
+ val oppositeKey = Pair(beneficiary, obligor)
val opposite = (balances[oppositeKey] ?: Amount(0L, balance.value.token))
// Drop zero balances
if (balance.value > opposite) {
@@ -770,9 +768,9 @@ fun
netAmountsDue(balances: Map, Amount
/**
* 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.
*/
fun sumAmountsDue(balances: Map, Amount>): Map {
@@ -785,17 +783,17 @@ fun sumAmountsDue(balances: Map, Amount>): Map
}
for ((key, amount) in balances) {
- val (issuer, owner) = key
- // Subtract it from the issuer
- sum[issuer] = sum[issuer]!! - amount.quantity
- // Add it to the owner
- sum[owner] = sum[owner]!! + amount.quantity
+ val (obligor, beneficiary) = key
+ // Subtract it from the obligor
+ sum[obligor] = sum[obligor]!! - amount.quantity
+ // Add it to the beneficiary
+ sum[beneficiary] = sum[beneficiary]!! + amount.quantity
}
// Strip zero balances
val iterator = sum.iterator()
while (iterator.hasNext()) {
- val (key, amount) = iterator.next()
+ val amount = iterator.next().value
if (amount == 0L) {
iterator.remove()
}
@@ -804,16 +802,15 @@ fun
sumAmountsDue(balances: Map, Amount>): Map
return sum
}
-/** Sums the cash states in the list, throwing an exception if there are none.
- * All cash states in the list are presumed to be nettable.
- */
-fun
Iterable.sumObligations() = filterIsInstance>().map { it.amount }.sumOrThrow()
+/** Sums the obligation states in the list, throwing an exception if there are none. All state objects in the list are presumed to be nettable. */
+fun Iterable.sumObligations(): Amount
+ = filterIsInstance>().map { it.amount }.sumOrThrow()
-/** Sums the cash settlement states in the list, returning null if there are none. */
-fun Iterable.sumObligationsOrNull()
+/** Sums the obligation states in the list, returning null if there are none. */
+fun Iterable.sumObligationsOrNull(): Amount?
= filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrNull()
-/** Sums the cash settlement states in the list, returning zero of the given currency if there are none. */
-fun Iterable.sumObligationsOrZero(currency: Issued)
- = filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(currency)
+/** Sums the obligation states in the list, returning zero of the given product if there are none. */
+fun Iterable.sumObligationsOrZero(product: P): Amount
+ = filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(product)
diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt
index d7f3dfc526..bd13e1178e 100644
--- a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt
+++ b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt
@@ -1,20 +1,26 @@
package com.r3corda.contracts.testing
import com.r3corda.contracts.*
-import com.r3corda.contracts.cash.CASH_PROGRAM_ID
-import com.r3corda.contracts.cash.Cash
+import com.r3corda.contracts.asset.CASH_PROGRAM_ID
+import com.r3corda.contracts.asset.Cash
+import com.r3corda.contracts.asset.Obligation
import com.r3corda.core.contracts.Amount
import com.r3corda.core.contracts.Contract
+import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.DUMMY_PROGRAM_ID
import com.r3corda.core.contracts.DummyContract
+import com.r3corda.core.contracts.DummyState
import com.r3corda.core.contracts.PartyAndReference
import com.r3corda.core.contracts.Issued
-import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.TransactionState
import com.r3corda.core.crypto.NullPublicKey
import com.r3corda.core.crypto.Party
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.time.Instant
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
@@ -27,7 +33,7 @@ val TEST_PROGRAM_MAP: Map> = mapOf(
IRS_PROGRAM_ID to InterestRateSwap::class.java
)
-fun generateState() = DummyContract.State(Random().nextInt())
+fun generateState() = DummyState(Random().nextInt())
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
@@ -56,6 +62,12 @@ object JavaTestHelpers {
@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 at(state: Obligation.State, dueBefore: Instant) = state.copy(template = state.template.copy(dueBefore = dueBefore))
+ @JvmStatic fun at(issuanceDef: Obligation.IssuanceDefinition, dueBefore: Instant) = issuanceDef.copy(template = issuanceDef.template.copy(dueBefore = dueBefore))
+ @JvmStatic fun between(state: Obligation.State, parties: Pair) = state.copy(obligor = parties.first, beneficiary = parties.second)
+ @JvmStatic fun ownedBy(state: Obligation.State, owner: PublicKey) = state.copy(beneficiary = owner)
+ @JvmStatic fun issuedBy(state: Obligation.State, party: Party) = state.copy(obligor = party)
+
@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 ownedBy(state: ICommercialPaperState, new_owner: PublicKey) = state.withOwner(new_owner)
@@ -66,6 +78,12 @@ object JavaTestHelpers {
Amount>(amount.quantity, Issued(DUMMY_CASH_ISSUER, amount.token)),
NullPublicKey)
@JvmStatic fun STATE(amount: Amount>) = Cash.State(amount, NullPublicKey)
+
+ // Allows you to write 100.DOLLARS.OBLIGATION
+ @JvmStatic fun OBLIGATION_DEF(issued: Issued)
+ = Obligation.StateTemplate(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(issued), TEST_TX_TIME)
+ @JvmStatic fun OBLIGATION(amount: Amount>) = Obligation.State(Obligation.Lifecycle.NORMAL, MINI_CORP,
+ OBLIGATION_DEF(amount.token), amount.quantity, NullPublicKey)
}
@@ -75,6 +93,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 deposit`(deposit: PartyAndReference): Cash.State = JavaTestHelpers.withDeposit(this, deposit)
+infix fun Obligation.State.`at`(dueBefore: Instant) = JavaTestHelpers.at(this, dueBefore)
+infix fun Obligation.IssuanceDefinition.`at`(dueBefore: Instant) = JavaTestHelpers.at(this, dueBefore)
+infix fun Obligation.State.`between`(parties: Pair) = JavaTestHelpers.between(this, parties)
+infix fun Obligation.State.`owned by`(owner: PublicKey) = JavaTestHelpers.ownedBy(this, owner)
+infix fun Obligation.State.`issued by`(party: Party) = JavaTestHelpers.issuedBy(this, party)
+
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 ICommercialPaperState.`owned by`(new_owner: PublicKey) = JavaTestHelpers.ownedBy(this, new_owner)
@@ -87,3 +111,6 @@ val DUMMY_CASH_ISSUER = Party("Snake Oil Issuer", DUMMY_CASH_ISSUER_KEY.public).
val Amount.CASH: Cash.State get() = JavaTestHelpers.CASH(this)
val Amount>.STATE: Cash.State get() = JavaTestHelpers.STATE(this)
+/** Allows you to write 100.DOLLARS.CASH */
+val Issued.OBLIGATION_DEF: Obligation.StateTemplate get() = JavaTestHelpers.OBLIGATION_DEF(this)
+val Amount>.OBLIGATION: Obligation.State get() = JavaTestHelpers.OBLIGATION(this)
diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/testing/WalletFiller.kt b/contracts/src/main/kotlin/com/r3corda/contracts/testing/WalletFiller.kt
index ec7bec2404..456b8dd391 100644
--- a/contracts/src/main/kotlin/com/r3corda/contracts/testing/WalletFiller.kt
+++ b/contracts/src/main/kotlin/com/r3corda/contracts/testing/WalletFiller.kt
@@ -1,7 +1,7 @@
@file:JvmName("WalletFiller")
package com.r3corda.contracts.testing
-import com.r3corda.contracts.cash.Cash
+import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.Amount
import com.r3corda.core.contracts.Issued
import com.r3corda.core.contracts.SignedTransaction
@@ -66,6 +66,7 @@ private fun calculateRandomlySizedAmounts(howMuch: Amount, min: Int, m
val numStates = min + Math.floor(rng.nextDouble() * (max - min)).toInt()
val amounts = LongArray(numStates)
val baseSize = howMuch.quantity / numStates
+ check(baseSize > 0) { baseSize }
var filledSoFar = 0L
for (i in 0..numStates - 1) {
if (i < numStates - 1) {
@@ -76,6 +77,7 @@ private fun calculateRandomlySizedAmounts(howMuch: Amount, min: Int, m
// Handle inexact rounding.
amounts[i] = howMuch.quantity - filledSoFar
}
+ check(amounts[i] >= 0) { amounts[i] }
}
check(amounts.sum() == howMuch.quantity)
return amounts
diff --git a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt
index 514f230675..7efd145934 100644
--- a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt
+++ b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt
@@ -1,13 +1,12 @@
package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable
-import com.r3corda.contracts.cash.Cash
-import com.r3corda.contracts.cash.sumCashBy
+import com.r3corda.contracts.asset.Cash
+import com.r3corda.contracts.asset.sumCashBy
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.signWithECDSA
-import com.r3corda.core.messaging.SingleMessageRecipient
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.random63BitValue
@@ -43,7 +42,8 @@ import java.util.*
* To see an example of how to use this class, look at the unit tests.
*/
object TwoPartyTradeProtocol {
- val TRADE_TOPIC = "platform.trade"
+
+ val TOPIC = "platform.trade"
class UnacceptablePriceException(val givenPrice: Amount>) : Exception()
class AssetMismatchException(val expectedTypeName: String, val typeName: String) : Exception() {
@@ -61,7 +61,7 @@ object TwoPartyTradeProtocol {
class SignaturesFromSeller(val sellerSig: DigitalSignature.WithKey,
val notarySig: DigitalSignature.LegallyIdentifiable)
- open class Seller(val otherSide: SingleMessageRecipient,
+ open class Seller(val otherSide: Party,
val notaryNode: NodeInfo,
val assetToSell: StateAndRef,
val price: Amount>,
@@ -83,6 +83,8 @@ object TwoPartyTradeProtocol {
fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, NOTARY, SENDING_SIGS)
}
+ override val topic: String get() = TOPIC
+
@Suspendable
override fun call(): SignedTransaction {
val partialTX: SignedTransaction = receiveAndCheckProposedTransaction()
@@ -109,7 +111,7 @@ object TwoPartyTradeProtocol {
// Make the first message we'll send to kick off the protocol.
val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public, sessionID)
- val maybeSTX = sendAndReceive(TRADE_TOPIC, otherSide, buyerSessionID, sessionID, hello)
+ val maybeSTX = sendAndReceive(otherSide, buyerSessionID, sessionID, hello)
progressTracker.currentStep = VERIFYING
@@ -167,12 +169,12 @@ object TwoPartyTradeProtocol {
logger.trace { "Built finished transaction, sending back to secondary!" }
- send(TRADE_TOPIC, otherSide, buyerSessionID, SignaturesFromSeller(ourSignature, notarySignature))
+ send(otherSide, buyerSessionID, SignaturesFromSeller(ourSignature, notarySignature))
return fullySigned
}
}
- open class Buyer(val otherSide: SingleMessageRecipient,
+ open class Buyer(val otherSide: Party,
val notary: Party,
val acceptablePrice: Amount>,
val typeToBuy: Class,
@@ -186,6 +188,7 @@ object TwoPartyTradeProtocol {
object SWAPPING_SIGNATURES : ProgressTracker.Step("Swapping signatures with the seller")
+ override val topic: String get() = TOPIC
override val progressTracker = ProgressTracker(RECEIVING, VERIFYING, SIGNING, SWAPPING_SIGNATURES)
@Suspendable
@@ -196,8 +199,6 @@ object TwoPartyTradeProtocol {
val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest)
val stx = signWithOurKeys(cashSigningPubKeys, ptx)
- // exitProcess(0)
-
val signatures = swapSignaturesWithSeller(stx, tradeRequest.sessionID)
logger.trace { "Got signatures from seller, verifying ... " }
@@ -216,7 +217,7 @@ object TwoPartyTradeProtocol {
private fun receiveAndValidateTradeRequest(): SellerTradeInfo {
progressTracker.currentStep = RECEIVING
// Wait for a trade request to come in on our pre-provided session ID.
- val maybeTradeRequest = receive(TRADE_TOPIC, sessionID)
+ val maybeTradeRequest = receive(sessionID)
progressTracker.currentStep = VERIFYING
maybeTradeRequest.validate {
@@ -247,7 +248,7 @@ object TwoPartyTradeProtocol {
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
- return sendAndReceive(TRADE_TOPIC, otherSide, theirSessionID, sessionID, stx).validate { it }
+ return sendAndReceive(otherSide, theirSessionID, sessionID, stx).validate { it }
}
private fun signWithOurKeys(cashSigningPubKeys: List, ptx: TransactionBuilder): SignedTransaction {
diff --git a/contracts/src/test/java/com/r3corda/contracts/asset/CashTestsJava.java b/contracts/src/test/java/com/r3corda/contracts/asset/CashTestsJava.java
new file mode 100644
index 0000000000..36fced1951
--- /dev/null
+++ b/contracts/src/test/java/com/r3corda/contracts/asset/CashTestsJava.java
@@ -0,0 +1,61 @@
+package com.r3corda.contracts.asset;
+
+import com.r3corda.core.contracts.PartyAndReference;
+import com.r3corda.core.serialization.OpaqueBytes;
+import kotlin.Unit;
+import org.junit.Test;
+
+import static com.r3corda.core.testing.JavaTestHelpers.*;
+import static com.r3corda.core.contracts.JavaTestHelpers.*;
+import static com.r3corda.contracts.testing.JavaTestHelpers.*;
+
+/**
+ * This is an incomplete Java replica of CashTests.kt to show how to use the Java test DSL
+ */
+public class CashTestsJava {
+
+ private OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{1});;
+ private PartyAndReference defaultIssuer = getMEGA_CORP().ref(defaultRef);
+ private Cash.State inState = new Cash.State(issuedBy(DOLLARS(1000), defaultIssuer), getDUMMY_PUBKEY_1());
+ private Cash.State outState = new Cash.State(inState.getAmount(), getDUMMY_PUBKEY_2());
+
+ @Test
+ public void trivial() {
+ ledger(lg -> {
+ lg.transaction(tx -> {
+ tx.input(inState);
+ tx.failsWith("the amounts balance");
+
+ tx.tweak(tw -> {
+ tw.output(new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), getDUMMY_PUBKEY_2()));
+ return tw.failsWith("the amounts balance");
+ });
+
+ tx.tweak(tw -> {
+ tw.output(outState);
+ // No command arguments
+ return tw.failsWith("required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command");
+ });
+ tx.tweak(tw -> {
+ tw.output(outState);
+ tw.command(getDUMMY_PUBKEY_2(), new Cash.Commands.Move());
+ return tw.failsWith("the owning keys are the same as the signing keys");
+ });
+ tx.tweak(tw -> {
+ tw.output(outState);
+ tw.output(issuedBy(outState, getMINI_CORP()));
+ tw.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
+ return tw.failsWith("at least one asset input");
+ });
+
+ // Simple reallocation works.
+ return tx.tweak(tw -> {
+ tw.output(outState);
+ tw.command(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
+ return tw.verifies();
+ });
+ });
+ return Unit.INSTANCE;
+ });
+ }
+}
diff --git a/contracts/src/test/java/com/r3corda/contracts/cash/CashTestsJava.java b/contracts/src/test/java/com/r3corda/contracts/cash/CashTestsJava.java
deleted file mode 100644
index 63772ab0dc..0000000000
--- a/contracts/src/test/java/com/r3corda/contracts/cash/CashTestsJava.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.r3corda.contracts.cash;
-
-import com.r3corda.core.contracts.PartyAndReference;
-import com.r3corda.core.serialization.OpaqueBytes;
-import org.junit.Test;
-
-import static com.r3corda.core.testing.JavaTestHelpers.*;
-import static com.r3corda.core.contracts.JavaTestHelpers.*;
-import static com.r3corda.contracts.testing.JavaTestHelpers.*;
-
-/**
- * This is an incomplete Java replica of CashTests.kt to show how to use the Java test DSL
- */
-public class CashTestsJava {
-
- private OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{1});;
- private PartyAndReference defaultIssuer = getMEGA_CORP().ref(defaultRef);
- private Cash.State inState = new Cash.State(issuedBy(DOLLARS(1000), defaultIssuer), getDUMMY_PUBKEY_1());
- private Cash.State outState = new Cash.State(inState.getAmount(), getDUMMY_PUBKEY_2());
-
- @Test
- public void trivial() {
-
- transaction(tx -> {
- tx.input(inState);
- tx.failsRequirement("the amounts balance");
-
- tx.tweak(tw -> {
- tw.output(new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), getDUMMY_PUBKEY_2()));
- return tw.failsRequirement("the amounts balance");
- });
-
- tx.tweak(tw -> {
- tw.output(outState);
- // No command arguments
- return tw.failsRequirement("required com.r3corda.contracts.cash.FungibleAsset.Commands.Move command");
- });
- tx.tweak(tw -> {
- tw.output(outState);
- tw.arg(getDUMMY_PUBKEY_2(), new Cash.Commands.Move());
- return tw.failsRequirement("the owning keys are the same as the signing keys");
- });
- tx.tweak(tw -> {
- tw.output(outState);
- tw.output(issuedBy(outState, getMINI_CORP()));
- tw.arg(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
- return tw.failsRequirement("at least one asset input");
- });
-
- // Simple reallocation works.
- return tx.tweak(tw -> {
- tw.output(outState);
- tw.arg(getDUMMY_PUBKEY_1(), new Cash.Commands.Move());
- return tw.accepts();
- });
- });
- }
-}
diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt
index 9b69908e1e..0f56e5f8d2 100644
--- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt
+++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt
@@ -1,6 +1,6 @@
package com.r3corda.contracts
-import com.r3corda.contracts.cash.Cash
+import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.testing.*
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.SecureHash
@@ -12,15 +12,15 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.time.Instant
-import java.util.Currency
+import java.util.*
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
interface ICommercialPaperTestTemplate {
- open fun getPaper(): ICommercialPaperState
- open fun getIssueCommand(): CommandData
- open fun getRedeemCommand(): CommandData
- open fun getMoveCommand(): CommandData
+ fun getPaper(): ICommercialPaperState
+ fun getIssueCommand(): CommandData
+ fun getRedeemCommand(): CommandData
+ fun getMoveCommand(): CommandData
}
class JavaCommercialPaperTest() : ICommercialPaperTestTemplate {
@@ -63,81 +63,113 @@ class CommercialPaperTestsGeneric {
val issuer = MEGA_CORP.ref(123)
@Test
- fun ok() {
- trade().verify()
- }
+ fun `trade lifecycle test`() {
+ val someProfits = 1200.DOLLARS `issued by` issuer
+ ledger {
+ unverifiedTransaction {
+ output("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY)
+ output("some profits", someProfits.STATE `owned by` MEGA_CORP_PUBKEY)
+ }
- @Test
- fun `not matured at redemption`() {
- trade(redemptionTime = TEST_TX_TIME + 2.days).expectFailureOfTx(3, "must have matured")
+ // Some CP is issued onto the ledger by MegaCorp.
+ transaction("Issuance") {
+ output("paper") { thisTest.getPaper() }
+ command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
+ timestamp(TEST_TX_TIME)
+ this.verifies()
+ }
+
+ // The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days,
+ // that sounds a bit too good to be true!
+ transaction("Trade") {
+ input("paper")
+ input("alice's $900")
+ output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY }
+ output("alice's paper") { "paper".output().data `owned by` ALICE_PUBKEY }
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ command(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() }
+ this.verifies()
+ }
+
+ // Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
+ // as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change.
+ transaction("Redemption") {
+ input("alice's paper")
+ input("some profits")
+
+ fun TransactionDSL>.outputs(aliceGetsBack: Amount>) {
+ output("Alice's profit") { aliceGetsBack.STATE `owned by` ALICE_PUBKEY }
+ output("Change") { (someProfits - aliceGetsBack).STATE `owned by` MEGA_CORP_PUBKEY }
+ }
+
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
+ command(ALICE_PUBKEY) { thisTest.getRedeemCommand() }
+
+ tweak {
+ outputs(700.DOLLARS `issued by` issuer)
+ timestamp(TEST_TX_TIME + 8.days)
+ this `fails with` "received amount equals the face value"
+ }
+ outputs(1000.DOLLARS `issued by` issuer)
+
+
+ tweak {
+ timestamp(TEST_TX_TIME + 2.days)
+ this `fails with` "must have matured"
+ }
+ timestamp(TEST_TX_TIME + 8.days)
+
+ tweak {
+ output { "paper".output().data }
+ this `fails with` "must be destroyed"
+ }
+
+ this.verifies()
+ }
+ }
}
@Test
fun `key mismatch at issue`() {
- transactionGroup {
- transaction {
- output { thisTest.getPaper() }
- arg(DUMMY_PUBKEY_1) { thisTest.getIssueCommand() }
- timestamp(TEST_TX_TIME)
- }
-
- expectFailureOfTx(1, "signed by the claimed issuer")
+ transaction {
+ output { thisTest.getPaper() }
+ command(DUMMY_PUBKEY_1) { thisTest.getIssueCommand() }
+ timestamp(TEST_TX_TIME)
+ this `fails with` "signed by the claimed issuer"
}
}
@Test
fun `face value is not zero`() {
- transactionGroup {
- transaction {
- output { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) }
- arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
- timestamp(TEST_TX_TIME)
- }
-
- expectFailureOfTx(1, "face value is not zero")
+ transaction {
+ output { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) }
+ command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
+ timestamp(TEST_TX_TIME)
+ this `fails with` "face value is not zero"
}
}
@Test
fun `maturity date not in the past`() {
- transactionGroup {
- transaction {
- output { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) }
- arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
- timestamp(TEST_TX_TIME)
- }
-
- expectFailureOfTx(1, "maturity date is not in the past")
+ transaction {
+ output { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) }
+ command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
+ timestamp(TEST_TX_TIME)
+ this `fails with` "maturity date is not in the past"
}
}
@Test
fun `issue cannot replace an existing state`() {
- transactionGroup {
- roots {
- transaction(thisTest.getPaper() `with notary` DUMMY_NOTARY label "paper")
- }
- transaction {
- input("paper")
- output { thisTest.getPaper() }
- arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
- timestamp(TEST_TX_TIME)
- }
-
- expectFailureOfTx(1, "there is no input state")
+ transaction {
+ input(thisTest.getPaper())
+ output { thisTest.getPaper() }
+ command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
+ timestamp(TEST_TX_TIME)
+ this `fails with` "there is no input state"
}
}
- @Test
- fun `did not receive enough money at redemption`() {
- trade(aliceGetsBack = 700.DOLLARS `issued by` issuer).expectFailureOfTx(3, "received amount equals the face value")
- }
-
- @Test
- fun `paper must be destroyed by redemption`() {
- trade(destroyPaperAtRedemption = false).expectFailureOfTx(3, "must be destroyed")
- }
-
fun cashOutputsToWallet(vararg outputs: TransactionState): Pair>> {
val ltx = LedgerTransaction(emptyList(), listOf(*outputs), emptyList(), emptyList(), SecureHash.randomSHA256(), emptyList(), TransactionType.General())
return Pair(ltx, outputs.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) })
@@ -199,52 +231,4 @@ class CommercialPaperTestsGeneric {
TransactionGroup(setOf(issueTX, moveTX, validRedemption), setOf(corpWalletTX, alicesWalletTX)).verify()
}
-
- // Generate a trade lifecycle with various parameters.
- fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days,
- aliceGetsBack: Amount> = 1000.DOLLARS `issued by` issuer,
- destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL {
- val someProfits = 1200.DOLLARS `issued by` issuer
- return transactionGroupFor() {
- roots {
- transaction(900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY label "alice's $900")
- transaction(someProfits.STATE `owned by` MEGA_CORP_PUBKEY `with notary` DUMMY_NOTARY label "some profits")
- }
-
- // Some CP is issued onto the ledger by MegaCorp.
- transaction("Issuance") {
- output("paper") { thisTest.getPaper() }
- arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() }
- timestamp(TEST_TX_TIME)
- }
-
- // The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days,
- // that sounds a bit too good to be true!
- transaction("Trade") {
- input("paper")
- input("alice's $900")
- output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY }
- output("alice's paper") { "paper".output.data `owned by` ALICE_PUBKEY }
- arg(ALICE_PUBKEY) { Cash.Commands.Move() }
- arg(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() }
- }
-
- // Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
- // as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change.
- transaction("Redemption") {
- input("alice's paper")
- input("some profits")
-
- output("Alice's profit") { aliceGetsBack.STATE `owned by` ALICE_PUBKEY }
- output("Change") { (someProfits - aliceGetsBack).STATE `owned by` MEGA_CORP_PUBKEY }
- if (!destroyPaperAtRedemption)
- output { "paper".output.data }
-
- arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
- arg(ALICE_PUBKEY) { thisTest.getRedeemCommand() }
-
- timestamp(redemptionTime)
- }
- }
- }
}
diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt
index 6075cff0ed..e771e25140 100644
--- a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt
+++ b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt
@@ -200,12 +200,12 @@ class IRSTests {
@Test
fun ok() {
- trade().verify()
+ trade().verifies()
}
@Test
fun `ok with groups`() {
- tradegroups().verify()
+ tradegroups().verifies()
}
/**
@@ -360,38 +360,40 @@ class IRSTests {
/**
* Generates a typical transactional history for an IRS.
*/
- fun trade(): TransactionGroupDSL {
+ fun trade(): LedgerDSL {
val ld = LocalDate.of(2016, 3, 8)
val bd = BigDecimal("0.0063518")
- val txgroup: TransactionGroupDSL = transactionGroupFor() {
+ return ledger {
transaction("Agreement") {
output("irs post agreement") { singleIRS() }
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
+ this.verifies()
}
transaction("Fix") {
input("irs post agreement")
+ val postAgreement = "irs post agreement".output()
output("irs post first fixing") {
- "irs post agreement".output.data.copy(
- "irs post agreement".output.data.fixedLeg,
- "irs post agreement".output.data.floatingLeg,
- "irs post agreement".output.data.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
- "irs post agreement".output.data.common
+ postAgreement.data.copy(
+ postAgreement.data.fixedLeg,
+ postAgreement.data.floatingLeg,
+ postAgreement.data.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
+ postAgreement.data.common
)
}
- arg(ORACLE_PUBKEY) {
+ command(ORACLE_PUBKEY) {
InterestRateSwap.Commands.Fix()
}
- arg(ORACLE_PUBKEY) {
+ command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
}
timestamp(TEST_TX_TIME)
+ this.verifies()
}
}
- return txgroup
}
@Test
@@ -399,9 +401,9 @@ class IRSTests {
transaction {
input() { singleIRS() }
output("irs post agreement") { singleIRS() }
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
- this `fails requirement` "There are no in states for an agreement"
+ this `fails with` "There are no in states for an agreement"
}
}
@@ -413,9 +415,9 @@ class IRSTests {
output() {
irs.copy(calculation = irs.calculation.copy(fixedLegPaymentSchedule = emptySchedule))
}
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
- this `fails requirement` "There are events in the fix schedule"
+ this `fails with` "There are events in the fix schedule"
}
}
@@ -427,9 +429,9 @@ class IRSTests {
output() {
irs.copy(calculation = irs.calculation.copy(floatingLegPaymentSchedule = emptySchedule))
}
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
- this `fails requirement` "There are events in the float schedule"
+ this `fails with` "There are events in the float schedule"
}
}
@@ -440,18 +442,18 @@ class IRSTests {
output() {
irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(quantity = 0)))
}
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
- this `fails requirement` "All notionals must be non zero"
+ this `fails with` "All notionals must be non zero"
}
transaction {
output() {
irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(quantity = 0)))
}
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
- this `fails requirement` "All notionals must be non zero"
+ this `fails with` "All notionals must be non zero"
}
}
@@ -463,9 +465,9 @@ class IRSTests {
output() {
modifiedIRS
}
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
- this `fails requirement` "The fixed leg rate must be positive"
+ this `fails with` "The fixed leg rate must be positive"
}
}
@@ -480,9 +482,9 @@ class IRSTests {
output() {
modifiedIRS
}
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
- this `fails requirement` "The currency of the notionals must be the same"
+ this `fails with` "The currency of the notionals must be the same"
}
}
@@ -494,9 +496,9 @@ class IRSTests {
output() {
modifiedIRS
}
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
- this `fails requirement` "All leg notionals must be the same"
+ this `fails with` "All leg notionals must be the same"
}
}
@@ -508,9 +510,9 @@ class IRSTests {
output() {
modifiedIRS1
}
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
- this `fails requirement` "The effective date is before the termination date for the fixed leg"
+ this `fails with` "The effective date is before the termination date for the fixed leg"
}
val modifiedIRS2 = irs.copy(floatingLeg = irs.floatingLeg.copy(terminationDate = irs.floatingLeg.effectiveDate.minusDays(1)))
@@ -518,9 +520,9 @@ class IRSTests {
output() {
modifiedIRS2
}
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
- this `fails requirement` "The effective date is before the termination date for the floating leg"
+ this `fails with` "The effective date is before the termination date for the floating leg"
}
}
@@ -533,9 +535,9 @@ class IRSTests {
output() {
modifiedIRS3
}
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
- this `fails requirement` "The termination dates are aligned"
+ this `fails with` "The termination dates are aligned"
}
@@ -544,24 +546,23 @@ class IRSTests {
output() {
modifiedIRS4
}
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
- this `fails requirement` "The effective dates are aligned"
+ this `fails with` "The effective dates are aligned"
}
}
@Test
fun `various fixing tests`() {
-
val ld = LocalDate.of(2016, 3, 8)
val bd = BigDecimal("0.0063518")
transaction {
output("irs post agreement") { singleIRS() }
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
- this.accepts()
+ this.verifies()
}
val oldIRS = singleIRS(1)
@@ -578,31 +579,31 @@ class IRSTests {
// Templated tweak for reference. A corrent fixing applied should be ok
tweak {
- arg(ORACLE_PUBKEY) {
+ command(ORACLE_PUBKEY) {
InterestRateSwap.Commands.Fix()
}
timestamp(TEST_TX_TIME)
- arg(ORACLE_PUBKEY) {
+ command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
}
output() { newIRS }
- this.accepts()
+ this.verifies()
}
// This test makes sure that verify confirms the fixing was applied and there is a difference in the old and new
tweak {
- arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
+ command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
timestamp(TEST_TX_TIME)
- arg(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
+ command(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
output() { oldIRS }
- this`fails requirement` "There is at least one difference in the IRS floating leg payment schedules"
+ this `fails with` "There is at least one difference in the IRS floating leg payment schedules"
}
// This tests tries to sneak in a change to another fixing (which may or may not be the latest one)
tweak {
- arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
+ command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
timestamp(TEST_TX_TIME)
- arg(ORACLE_PUBKEY) {
+ command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
}
@@ -619,14 +620,14 @@ class IRSTests {
newIRS.common
)
}
- this`fails requirement` "There is only one change in the IRS floating leg payment schedule"
+ this `fails with` "There is only one change in the IRS floating leg payment schedule"
}
// This tests modifies the payment currency for the fixing
tweak {
- arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
+ command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
timestamp(TEST_TX_TIME)
- arg(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
+ command(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key }
val modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.quantity, Currency.getInstance("JPY")))
@@ -640,7 +641,7 @@ class IRSTests {
newIRS.common
)
}
- this`fails requirement` "The fix payment has the same currency as the notional"
+ this `fails with` "The fix payment has the same currency as the notional"
}
}
}
@@ -652,13 +653,13 @@ class IRSTests {
* result and the grouping won't work either.
* In reality, the only fields that should be in common will be the next fixing date and the reference rate.
*/
- fun tradegroups(): TransactionGroupDSL {
+ fun tradegroups(): LedgerDSL {
val ld1 = LocalDate.of(2016, 3, 8)
val bd1 = BigDecimal("0.0063518")
val irs = singleIRS()
- val txgroup: TransactionGroupDSL = transactionGroupFor() {
+ return ledger {
transaction("Agreement") {
output("irs post agreement1") {
irs.copy(
@@ -668,8 +669,9 @@ class IRSTests {
irs.common.copy(tradeID = "t1")
)
}
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
+ this.verifies()
}
transaction("Agreement") {
@@ -681,40 +683,43 @@ class IRSTests {
irs.common.copy(tradeID = "t2")
)
}
- arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
+ command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
+ this.verifies()
}
transaction("Fix") {
input("irs post agreement1")
input("irs post agreement2")
+ val postAgreement1 = "irs post agreement1".output()
output("irs post first fixing1") {
- "irs post agreement1".output.data.copy(
- "irs post agreement1".output.data.fixedLeg,
- "irs post agreement1".output.data.floatingLeg,
- "irs post agreement1".output.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
- "irs post agreement1".output.data.common.copy(tradeID = "t1")
+ postAgreement1.data.copy(
+ postAgreement1.data.fixedLeg,
+ postAgreement1.data.floatingLeg,
+ postAgreement1.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
+ postAgreement1.data.common.copy(tradeID = "t1")
)
}
+ val postAgreement2 = "irs post agreement2".output()
output("irs post first fixing2") {
- "irs post agreement2".output.data.copy(
- "irs post agreement2".output.data.fixedLeg,
- "irs post agreement2".output.data.floatingLeg,
- "irs post agreement2".output.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
- "irs post agreement2".output.data.common.copy(tradeID = "t2")
+ postAgreement2.data.copy(
+ postAgreement2.data.fixedLeg,
+ postAgreement2.data.floatingLeg,
+ postAgreement2.data.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
+ postAgreement2.data.common.copy(tradeID = "t2")
)
}
- arg(ORACLE_PUBKEY) {
+ command(ORACLE_PUBKEY) {
InterestRateSwap.Commands.Fix()
}
- arg(ORACLE_PUBKEY) {
+ command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1)
}
timestamp(TEST_TX_TIME)
+ this.verifies()
}
}
- return txgroup
}
}
diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt
similarity index 80%
rename from contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt
rename to contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt
index 0b0e355720..a4adf2afbb 100644
--- a/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt
+++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt
@@ -1,4 +1,4 @@
-package com.r3corda.contracts.cash
+package com.r3corda.contracts.asset
import com.r3corda.contracts.testing.`issued by`
import com.r3corda.contracts.testing.`owned by`
@@ -31,33 +31,33 @@ class CashTests {
fun trivial() {
transaction {
input { inState }
- this `fails requirement` "the amounts balance"
+ this `fails with` "the amounts balance"
tweak {
output { outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer) }
- this `fails requirement` "the amounts balance"
+ this `fails with` "the amounts balance"
}
tweak {
output { outState }
// No command arguments
- this `fails requirement` "required com.r3corda.contracts.cash.FungibleAsset.Commands.Move command"
+ this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
}
tweak {
output { outState }
- arg(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
- this `fails requirement` "the owning keys are the same as the signing keys"
+ command(DUMMY_PUBKEY_2) { Cash.Commands.Move() }
+ this `fails with` "the owning keys are the same as the signing keys"
}
tweak {
output { outState }
output { outState `issued by` MINI_CORP }
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
- this `fails requirement` "at least one asset input"
+ command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
+ this `fails with` "at least one asset input"
}
// Simple reallocation works.
tweak {
output { outState }
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
- this.accepts()
+ command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
+ this.verifies()
}
}
}
@@ -66,19 +66,19 @@ class CashTests {
fun issueMoney() {
// Check we can't "move" money into existence.
transaction {
- input { DummyContract.State() }
+ input { DummyState() }
output { outState }
- arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
+ command(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
- this `fails requirement` "there is at least one asset input"
+ this `fails with` "there is at least one asset input"
}
// Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
// institution is allowed to issue as much cash as they want.
transaction {
output { outState }
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Issue() }
- this `fails requirement` "output deposits are owned by a command signer"
+ command(DUMMY_PUBKEY_1) { Cash.Commands.Issue() }
+ this `fails with` "output deposits are owned by a command signer"
}
transaction {
output {
@@ -88,11 +88,11 @@ class CashTests {
)
}
tweak {
- arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue(0) }
- this `fails requirement` "has a nonce"
+ command(MINI_CORP_PUBKEY) { Cash.Commands.Issue(0) }
+ this `fails with` "has a nonce"
}
- arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
- this.accepts()
+ command(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
+ this.verifies()
}
// Test generation works.
@@ -120,14 +120,14 @@ class CashTests {
// Move fails: not allowed to summon money.
tweak {
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
- this `fails requirement` "at issuer MegaCorp the amounts balance"
+ command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
+ this `fails with` "at issuer MegaCorp the amounts balance"
}
// Issue works.
tweak {
- arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
- this.accepts()
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
+ this.verifies()
}
}
@@ -135,36 +135,36 @@ class CashTests {
transaction {
input { inState }
output { inState.copy(amount = inState.amount / 2) }
- arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
- this `fails requirement` "output values sum to more than the inputs"
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
+ this `fails with` "output values sum to more than the inputs"
}
// Can't have an issue command that doesn't actually issue money.
transaction {
input { inState }
output { inState }
- arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
- this `fails requirement` "output values sum to more than the inputs"
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
+ this `fails with` "output values sum to more than the inputs"
}
// Can't have any other commands if we have an issue command (because the issue command overrules them)
transaction {
input { inState }
output { inState.copy(amount = inState.amount * 2) }
- arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
tweak {
- arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
- this `fails requirement` "there is only a single issue command"
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
+ this `fails with` "there is only a single issue command"
}
tweak {
- arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
- this `fails requirement` "there is only a single issue command"
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
+ this `fails with` "there is only a single issue command"
}
tweak {
- arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
- this `fails requirement` "there is only a single issue command"
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
+ this `fails with` "there is only a single issue command"
}
- this.accepts()
+ this.verifies()
}
}
@@ -191,25 +191,25 @@ class CashTests {
fun testMergeSplit() {
// Splitting value works.
transaction {
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
+ command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
tweak {
input { inState }
for (i in 1..4) output { inState.copy(amount = inState.amount / 4) }
- this.accepts()
+ this.verifies()
}
// Merging 4 inputs into 2 outputs works.
tweak {
for (i in 1..4) input { inState.copy(amount = inState.amount / 4) }
output { inState.copy(amount = inState.amount / 2) }
output { inState.copy(amount = inState.amount / 2) }
- this.accepts()
+ this.verifies()
}
// Merging 2 inputs into 1 works.
tweak {
input { inState.copy(amount = inState.amount / 2) }
input { inState.copy(amount = inState.amount / 2) }
output { inState }
- this.accepts()
+ this.verifies()
}
}
}
@@ -219,13 +219,13 @@ class CashTests {
transaction {
input { inState }
input { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
- this `fails requirement` "zero sized inputs"
+ this `fails with` "zero sized inputs"
}
transaction {
input { inState }
output { inState }
output { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
- this `fails requirement` "zero sized outputs"
+ this `fails with` "zero sized outputs"
}
}
@@ -235,21 +235,21 @@ class CashTests {
transaction {
input { inState }
output { outState `issued by` MINI_CORP }
- this `fails requirement` "at issuer MegaCorp the amounts balance"
+ this `fails with` "at issuer MegaCorp the amounts balance"
}
// Can't change deposit reference when splitting.
transaction {
input { inState }
output { outState.copy(amount = inState.amount / 2).editDepositRef(0) }
output { outState.copy(amount = inState.amount / 2).editDepositRef(1) }
- this `fails requirement` "for deposit [01] at issuer MegaCorp the amounts balance"
+ this `fails with` "for deposit [01] at issuer MegaCorp the amounts balance"
}
// Can't mix currencies.
transaction {
input { inState }
output { outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer) }
output { outState.copy(amount = 200.POUNDS `issued by` defaultIssuer) }
- this `fails requirement` "the amounts balance"
+ this `fails with` "the amounts balance"
}
transaction {
input { inState }
@@ -260,22 +260,22 @@ class CashTests {
)
}
output { outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer) }
- this `fails requirement` "the amounts balance"
+ this `fails with` "the amounts balance"
}
// Can't have superfluous input states from different issuers.
transaction {
input { inState }
input { inState `issued by` MINI_CORP }
output { outState }
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
- this `fails requirement` "at issuer MiniCorp the amounts balance"
+ command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
+ this `fails with` "at issuer MiniCorp the amounts balance"
}
// Can't combine two different deposits at the same issuer.
transaction {
input { inState }
input { inState.editDepositRef(3) }
output { outState.copy(amount = inState.amount * 2).editDepositRef(3) }
- this `fails requirement` "for deposit [01]"
+ this `fails with` "for deposit [01]"
}
}
@@ -287,18 +287,18 @@ class CashTests {
output { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
tweak {
- arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) }
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
- this `fails requirement` "the amounts balance"
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) }
+ command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
+ this `fails with` "the amounts balance"
}
tweak {
- arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
- this `fails requirement` "required com.r3corda.contracts.cash.FungibleAsset.Commands.Move command"
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
+ this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
tweak {
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
- this.accepts()
+ command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
+ this.verifies()
}
}
}
@@ -310,15 +310,15 @@ class CashTests {
output { inState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) `issued by` MINI_CORP }
output { inState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
+ command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
- this `fails requirement` "at issuer MegaCorp the amounts balance"
+ this `fails with` "at issuer MegaCorp the amounts balance"
- arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
- this `fails requirement` "at issuer MiniCorp the amounts balance"
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
+ this `fails with` "at issuer MiniCorp the amounts balance"
- arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)) }
- this.accepts()
+ command(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)) }
+ this.verifies()
}
}
@@ -332,20 +332,20 @@ class CashTests {
// Can't merge them together.
tweak {
output { inState.copy(owner = DUMMY_PUBKEY_2, amount = 2000.DOLLARS `issued by` defaultIssuer) }
- this `fails requirement` "at issuer MegaCorp the amounts balance"
+ this `fails with` "at issuer MegaCorp the amounts balance"
}
// Missing MiniCorp deposit
tweak {
output { inState.copy(owner = DUMMY_PUBKEY_2) }
output { inState.copy(owner = DUMMY_PUBKEY_2) }
- this `fails requirement` "at issuer MegaCorp the amounts balance"
+ this `fails with` "at issuer MegaCorp the amounts balance"
}
// This works.
output { inState.copy(owner = DUMMY_PUBKEY_2) }
output { inState.copy(owner = DUMMY_PUBKEY_2) `issued by` MINI_CORP }
- arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
- this.accepts()
+ command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
+ this.verifies()
}
}
@@ -358,9 +358,9 @@ class CashTests {
input { pounds }
output { inState `owned by` DUMMY_PUBKEY_2 }
output { pounds `owned by` DUMMY_PUBKEY_1 }
- arg(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Cash.Commands.Move() }
+ command(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Cash.Commands.Move() }
- this.accepts()
+ this.verifies()
}
}
diff --git a/experimental/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt
similarity index 57%
rename from experimental/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt
rename to contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt
index 84be8b37df..5eec7d84c9 100644
--- a/experimental/src/test/kotlin/com/r3corda/contracts/ObligationTests.kt
+++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/ObligationTests.kt
@@ -1,15 +1,15 @@
-package com.r3corda.contracts
+package com.r3corda.contracts.asset
-import com.r3corda.contracts.cash.Cash
-import com.r3corda.contracts.Obligation.Lifecycle
+import com.r3corda.contracts.asset.Obligation.Lifecycle
import com.r3corda.contracts.testing.*
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.SecureHash
-import com.r3corda.core.seconds
import com.r3corda.core.testing.*
+import com.r3corda.core.testing.JavaTestHelpers
import com.r3corda.core.utilities.nonEmptySetOf
import org.junit.Test
import java.security.PublicKey
+import java.time.Duration
import java.time.Instant
import java.util.*
import kotlin.test.*
@@ -18,60 +18,64 @@ class ObligationTests {
val defaultIssuer = MEGA_CORP.ref(1)
val defaultUsd = USD `issued by` defaultIssuer
val oneMillionDollars = 1000000.DOLLARS `issued by` defaultIssuer
- val trustedCashContract = nonEmptySetOf(SecureHash.randomSHA256() as SecureHash)
- val megaIssuedDollars = nonEmptySetOf(Issued(defaultIssuer, USD))
- val megaIssuedPounds = nonEmptySetOf(Issued(defaultIssuer, GBP))
+ val trustedCashContract = nonEmptySetOf(SecureHash.Companion.randomSHA256() as SecureHash)
+ val megaIssuedDollars = nonEmptySetOf(Issued(defaultIssuer, USD))
+ val megaIssuedPounds = nonEmptySetOf(Issued(defaultIssuer, GBP))
val fivePm = Instant.parse("2016-01-01T17:00:00.00Z")
val sixPm = Instant.parse("2016-01-01T18:00:00.00Z")
- val notary = MEGA_CORP
val megaCorpDollarSettlement = Obligation.StateTemplate(trustedCashContract, megaIssuedDollars, fivePm)
- val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuanceDefinitions = megaIssuedPounds)
+ val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuedProducts = megaIssuedPounds)
val inState = Obligation.State(
lifecycle = Lifecycle.NORMAL,
- issuer = MEGA_CORP,
+ obligor = MEGA_CORP,
template = megaCorpDollarSettlement,
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>) = group.Roots()
- .transaction(oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY) `with notary` DUMMY_NOTARY label "Alice's $1,000,000 obligation to Bob")
- .transaction(oneMillionDollars.OBLIGATION `between` Pair(BOB, ALICE_PUBKEY) `with notary` DUMMY_NOTARY label "Bob's $1,000,000 obligation to Alice")
- .transaction(oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, BOB_PUBKEY) `with notary` DUMMY_NOTARY label "MegaCorp's $1,000,000 obligation to Bob")
- .transaction(1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY label "Alice's $1,000,000")
+ private fun obligationTestRoots(
+ group: LedgerDSL
+ ) = group.apply {
+ unverifiedTransaction {
+ output("Alice's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY))
+ output("Bob's $1,000,000 obligation to Alice", oneMillionDollars.OBLIGATION `between` Pair(BOB, ALICE_PUBKEY))
+ output("MegaCorp's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION `between` Pair(MEGA_CORP, BOB_PUBKEY))
+ output("Alice's $1,000,000", 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY)
+ }
+ }
@Test
fun trivial() {
transaction {
input { inState }
- this `fails requirement` "the amounts balance"
+ this `fails with` "the amounts balance"
tweak {
output { outState.copy(quantity = 2000.DOLLARS.quantity) }
- this `fails requirement` "the amounts balance"
+ this `fails with` "the amounts balance"
}
tweak {
output { outState }
- // No command arguments
- this `fails requirement` "required com.r3corda.contracts.Obligation.Commands.Move command"
+ // No command commanduments
+ this `fails with` "required com.r3corda.contracts.asset.Obligation.Commands.Move command"
}
tweak {
output { outState }
- arg(DUMMY_PUBKEY_2) { Obligation.Commands.Move(inState.issuanceDef) }
- this `fails requirement` "the owning keys are the same as the signing keys"
+ command(DUMMY_PUBKEY_2) { Obligation.Commands.Move(inState.issuanceDef) }
+ this `fails with` "the owning keys are the same as the signing keys"
}
tweak {
output { outState }
output { outState `issued by` MINI_CORP }
- arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
- this `fails requirement` "at least one obligation input"
+ command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
+ this `fails with` "at least one obligation input"
}
// Simple reallocation works.
tweak {
output { outState }
- arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
- this.accepts()
+ command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
+ this.verifies()
}
}
}
@@ -80,46 +84,49 @@ class ObligationTests {
fun `issue debt`() {
// Check we can't "move" debt into existence.
transaction {
- input { DummyContract.State() }
+ input { DummyState() }
output { outState }
- arg(MINI_CORP_PUBKEY) { Obligation.Commands.Move(outState.issuanceDef) }
+ command(MINI_CORP_PUBKEY) { Obligation.Commands.Move(outState.issuanceDef) }
- this `fails requirement` "there is at least one obligation input"
+ this `fails with` "there is at least one obligation input"
}
// Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
// institution is allowed to issue as much cash as they want.
transaction {
output { outState }
- arg(DUMMY_PUBKEY_1) { Obligation.Commands.Issue(outState.issuanceDef) }
- this `fails requirement` "output deposits are owned by a command signer"
+ command(DUMMY_PUBKEY_1) { Obligation.Commands.Issue(outState.issuanceDef) }
+ this `fails with` "output deposits are owned by a command signer"
}
transaction {
output {
Obligation.State(
- issuer = MINI_CORP,
+ obligor = MINI_CORP,
quantity = 1000.DOLLARS.quantity,
- owner = DUMMY_PUBKEY_1,
+ beneficiary = DUMMY_PUBKEY_1,
template = megaCorpDollarSettlement
)
}
tweak {
- arg(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement), 0) }
- this `fails requirement` "has a nonce"
+ command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement), 0) }
+ this `fails with` "has a nonce"
}
- arg(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement)) }
- this.accepts()
+ command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(Obligation.IssuanceDefinition(MINI_CORP, megaCorpDollarSettlement)) }
+ this.verifies()
}
// Test generation works.
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
Obligation().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())
- val s = ptx.outputStates()[0].data as Obligation.State
- assertEquals(100.DOLLARS `issued by` MEGA_CORP.ref(1), s.amount)
- assertEquals(MINI_CORP, s.issuer)
- assertEquals(DUMMY_PUBKEY_1, s.owner)
+ val expected = Obligation.State(
+ obligor = MINI_CORP,
+ quantity = 100.DOLLARS.quantity,
+ beneficiary = DUMMY_PUBKEY_1,
+ template = megaCorpDollarSettlement
+ )
+ assertEquals(ptx.outputStates()[0].data, expected)
assertTrue(ptx.commands()[0].value is Obligation.Commands.Issue<*>)
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0])
@@ -130,14 +137,14 @@ class ObligationTests {
// Move fails: not allowed to summon money.
tweak {
- arg(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
- this `fails requirement` "at issuer MegaCorp the amounts balance"
+ command(DUMMY_PUBKEY_1) { Obligation.Commands.Move(inState.issuanceDef) }
+ this `fails with` "at obligor MegaCorp the amounts balance"
}
// Issue works.
tweak {
- arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
- this.accepts()
+ command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
+ this.verifies()
}
}
@@ -145,40 +152,40 @@ class ObligationTests {
transaction {
input { inState }
output { inState.copy(quantity = inState.amount.quantity / 2) }
- arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
- this `fails requirement` "output values sum to more than the inputs"
+ command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
+ this `fails with` "output values sum to more than the inputs"
}
// Can't have an issue command that doesn't actually issue money.
transaction {
input { inState }
output { inState }
- arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
- this `fails requirement` "output values sum to more than the inputs"
+ command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
+ this `fails with` "output values sum to more than the inputs"
}
// Can't have any other commands if we have an issue command (because the issue command overrules them)
transaction {
input { inState }
output { inState.copy(quantity = inState.amount.quantity * 2) }
- arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
+ command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
tweak {
- arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
- this `fails requirement` "only move/exit commands can be present along with other obligation commands"
+ command(MEGA_CORP_PUBKEY) { Obligation.Commands.Issue(inState.issuanceDef) }
+ this `fails with` "only move/exit commands can be present along with other obligation commands"
}
tweak {
- arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Move(inState.issuanceDef) }
- this `fails requirement` "only move/exit commands can be present along with other obligation commands"
+ command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move(inState.issuanceDef) }
+ this `fails with` "only move/exit commands can be present along with other obligation commands"
}
tweak {
- arg(MEGA_CORP_PUBKEY) { Obligation.Commands.SetLifecycle(inState.issuanceDef, Lifecycle.DEFAULTED) }
- this `fails requirement` "only move/exit commands can be present along with other obligation commands"
+ command(MEGA_CORP_PUBKEY) { Obligation.Commands.SetLifecycle(inState.issuanceDef, Lifecycle.DEFAULTED) }
+ this `fails with` "only move/exit commands can be present along with other obligation commands"
}
tweak {
- arg(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, inState.amount / 2) }
- this `fails requirement` "only move/exit commands can be present along with other obligation commands"
+ command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.issuanceDef, inState.amount / 2) }
+ this `fails with` "only move/exit commands can be present along with other obligation commands"
}
- this.accepts()
+ this.verifies()
}
}
@@ -189,18 +196,46 @@ class ObligationTests {
@Test(expected = IllegalStateException::class)
fun `reject issuance with inputs`() {
// Issue some obligation
- var ptx = TransactionType.General.Builder(DUMMY_NOTARY)
-
- Obligation().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
- owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY)
- ptx.signWith(MINI_CORP_KEY)
- val tx = ptx.toSignedTransaction()
+ val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
+ Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
+ beneficiary = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY)
+ signWith(MINI_CORP_KEY)
+ }.toSignedTransaction()
// 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>(0))
Obligation().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().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().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. */
@@ -208,9 +243,13 @@ class ObligationTests {
fun `generate payment net transaction`() {
val obligationAliceToBob = oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY)
val obligationBobToAlice = oneMillionDollars.OBLIGATION `between` Pair(BOB, ALICE_PUBKEY)
- val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
- Obligation().generatePaymentNetting(ptx, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
- assertEquals(0, ptx.outputStates().size)
+ val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
+ Obligation().generatePaymentNetting(this, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
+ 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. */
@@ -218,238 +257,269 @@ class ObligationTests {
fun `generate payment net transaction with remainder`() {
val obligationAliceToBob = oneMillionDollars.OBLIGATION `between` Pair(ALICE, BOB_PUBKEY)
val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION `between` Pair(BOB, ALICE_PUBKEY)
- val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
- Obligation().generatePaymentNetting(ptx, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
- assertEquals(1, ptx.outputStates().size)
- val out = ptx.outputStates().single().data as Obligation.State
- assertEquals(1000000.DOLLARS.quantity, out.quantity)
- assertEquals(BOB, out.issuer)
- assertEquals(ALICE_PUBKEY, out.owner)
+ val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
+ Obligation().generatePaymentNetting(this, defaultUsd, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice)
+ signWith(ALICE_KEY)
+ signWith(BOB_KEY)
+ }.toSignedTransaction().tx
+ assertEquals(1, tx.outputs.size)
+ 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
fun `generate set lifecycle`() {
- // Issue some obligation
- val dueBefore = Instant.parse("2010-01-01T17:00:00Z")
+ // We don't actually verify the states, this is just here to make things look sensible
+ val dueBefore = TEST_TX_TIME - Duration.ofDays(7)
// Generate a transaction issuing the obligation
var tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
Obligation().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)
}.toSignedTransaction()
var stateAndRef = tx.tx.outRef>(0)
// Now generate a transaction marking the obligation as having defaulted
tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
- Obligation().generateSetLifecycle(this, listOf(stateAndRef), Obligation.Lifecycle.DEFAULTED, DUMMY_NOTARY)
+ Obligation().generateSetLifecycle(this, listOf(stateAndRef), Lifecycle.DEFAULTED, DUMMY_NOTARY)
signWith(MINI_CORP_KEY)
- }.toSignedTransaction(false)
+ signWith(DUMMY_NOTARY_KEY)
+ }.toSignedTransaction()
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 = Lifecycle.DEFAULTED), tx.tx.outputs[0].data)
+ assertTrue(tx.verify().isEmpty())
// And set it back
stateAndRef = tx.tx.outRef>(0)
tx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
- Obligation().generateSetLifecycle(this, listOf(stateAndRef), Obligation.Lifecycle.NORMAL, DUMMY_NOTARY)
+ Obligation().generateSetLifecycle(this, listOf(stateAndRef), Lifecycle.NORMAL, DUMMY_NOTARY)
signWith(MINI_CORP_KEY)
- }.toSignedTransaction(false)
+ signWith(DUMMY_NOTARY_KEY)
+ }.toSignedTransaction()
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 = Lifecycle.NORMAL), tx.tx.outputs[0].data)
+ assertTrue(tx.verify().isEmpty())
}
/** Test generating a transaction to settle an obligation. */
@Test
fun `generate settlement transaction`() {
- var ptx: TransactionBuilder
-
- // Generate a transaction to issue the cash we'll need
- ptx = TransactionType.General.Builder(DUMMY_NOTARY)
- Cash().generateIssue(ptx, 100.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY)
- ptx.signWith(MEGA_CORP_KEY)
- val cashTx = ptx.toSignedTransaction().tx
+ val cashTx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
+ Cash().generateIssue(this, 100.DOLLARS `issued by` defaultIssuer, MINI_CORP_PUBKEY, DUMMY_NOTARY)
+ signWith(MEGA_CORP_KEY)
+ }.toSignedTransaction().tx
// Generate a transaction issuing the obligation
- ptx = TransactionType.General.Builder(DUMMY_NOTARY)
- Obligation().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
- owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY)
- ptx.signWith(MINI_CORP_KEY)
- val obligationTx = ptx.toSignedTransaction().tx
+ val obligationTx = TransactionType.General.Builder(DUMMY_NOTARY).apply {
+ Obligation