From f4f0e160d2be9519425a56808c31929823139493 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Tue, 31 May 2016 11:54:03 +0100 Subject: [PATCH] Genericise Cash contract to support non-Currency things Split the verification and commands for the Cash contract into a new AbstractCashLike class, and make Cash a concrete implementation of that class, specialised for dealing with Currency as the underlying token. --- .../contracts/JavaCommercialPaper.java | 2 +- .../com/r3corda/contracts/CommercialPaper.kt | 2 +- .../kotlin/com/r3corda/contracts/CrowdFund.kt | 2 +- .../main/kotlin/com/r3corda/contracts/IRS.kt | 8 +- .../kotlin/com/r3corda/contracts/IRSUtils.kt | 2 +- ...finition.kt => AssetIssuanceDefinition.kt} | 6 +- .../kotlin/com/r3corda/contracts/cash/Cash.kt | 95 ++--------- .../r3corda/contracts/cash/FungibleAsset.kt | 148 ++++++++++++++++++ ...mmonCashState.kt => FungibleAssetState.kt} | 4 +- .../kotlin/com/r3corda/contracts/IRSTests.kt | 22 +-- .../com/r3corda/contracts/cash/CashTests.kt | 10 +- .../r3corda/core/contracts/FinanceTypes.kt | 20 +-- docs/source/release-notes.rst | 2 + .../node/services/wallet/NodeWalletService.kt | 6 +- .../r3corda/node/internal/testing/trade.json | 14 +- .../messaging/TwoPartyTradeProtocolTests.kt | 2 +- scripts/example-irs-trade.json | 16 +- 17 files changed, 221 insertions(+), 140 deletions(-) rename contracts/src/main/kotlin/com/r3corda/contracts/cash/{CashIssuanceDefinition.kt => AssetIssuanceDefinition.kt} (67%) create mode 100644 contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt rename contracts/src/main/kotlin/com/r3corda/contracts/cash/{CommonCashState.kt => FungibleAssetState.kt} (78%) diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index d427299188..f27919b99f 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -168,7 +168,7 @@ public class JavaCommercialPaper implements Contract { if (!inputs.isEmpty()) { throw new IllegalStateException("Failed Requirement: there is no input state"); } - if (output.faceValue.getPennies() == 0) { + if (output.faceValue.getQuantity() == 0) { throw new IllegalStateException("Failed Requirement: the face value is not zero"); } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index 36ae1ca5d5..5250499827 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -117,7 +117,7 @@ class CommercialPaper : Contract { // Don't allow people to issue commercial paper under other entities identities. "the issuance is signed by the claimed issuer of the paper" by (output.issuance.party.owningKey in command.signers) - "the face value is not zero" by (output.faceValue.pennies > 0) + "the face value is not zero" by (output.faceValue.quantity > 0) "the maturity date is not in the past" by (time < output.maturityDate) // Don't allow an existing CP state to be replaced by this issuance. // TODO: Consider how to handle the case of mistaken issuances, or other need to patch. diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CrowdFund.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CrowdFund.kt index 3e5da7a21c..8112248316 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CrowdFund.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CrowdFund.kt @@ -88,7 +88,7 @@ class CrowdFund : Contract { "there is no input state" by tx.inStates.filterIsInstance().isEmpty() "the transaction is signed by the owner of the crowdsourcing" by (command.signers.contains(outputCrowdFund.campaign.owner)) "the output registration is empty of pledges" by (outputCrowdFund.pledges.isEmpty()) - "the output registration has a non-zero target" by (outputCrowdFund.campaign.target.pennies > 0) + "the output registration has a non-zero target" by (outputCrowdFund.campaign.target.quantity > 0) "the output registration has a name" by (outputCrowdFund.campaign.name.isNotBlank()) "the output registration has a closing time in the future" by (time < outputCrowdFund.campaign.closingTime) "the output registration has an open state" by (!outputCrowdFund.closed) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt index 84ed6ca3ce..eb662a3cd0 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt @@ -112,7 +112,7 @@ class FixedRatePaymentEvent(date: LocalDate, } override val flow: Amount get() = - Amount(dayCountFactor.times(BigDecimal(notional.pennies)).times(rate.ratioUnit!!.value).toLong(), notional.token) + Amount(dayCountFactor.times(BigDecimal(notional.quantity)).times(rate.ratioUnit!!.value).toLong(), notional.token) override fun toString(): String = "FixedRatePaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate : $flow" @@ -138,7 +138,7 @@ class FloatingRatePaymentEvent(date: LocalDate, override val flow: Amount get() { // TODO: Should an uncalculated amount return a zero ? null ? etc. val v = rate.ratioUnit?.value ?: return Amount(0, notional.token) - return Amount(dayCountFactor.times(BigDecimal(notional.pennies)).times(v).toLong(), notional.token) + return Amount(dayCountFactor.times(BigDecimal(notional.quantity)).times(v).toLong(), notional.token) } override fun toString(): String = "FloatingPaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate (fix on $fixingDate): $flow" @@ -456,7 +456,7 @@ class InterestRateSwap() : Contract { fun checkLegAmounts(legs: Array) { requireThat { - "The notional is non zero" by legs.any { it.notional.pennies > (0).toLong() } + "The notional is non zero" by legs.any { it.notional.quantity > (0).toLong() } "The notional for all legs must be the same" by legs.all { it.notional == legs[0].notional } } for (leg: CommonLeg in legs) { @@ -505,7 +505,7 @@ class InterestRateSwap() : Contract { "There are no in states for an agreement" by inputs.isEmpty() "There are events in the fix schedule" by (irs.calculation.fixedLegPaymentSchedule.size > 0) "There are events in the float schedule" by (irs.calculation.floatingLegPaymentSchedule.size > 0) - "All notionals must be non zero" by (irs.fixedLeg.notional.pennies > 0 && irs.floatingLeg.notional.pennies > 0) + "All notionals must be non zero" by (irs.fixedLeg.notional.quantity > 0 && irs.floatingLeg.notional.quantity > 0) "The fixed leg rate must be positive" by (irs.fixedLeg.fixedRate.isPositive()) "The currency of the notionals must be the same" by (irs.fixedLeg.notional.token == irs.floatingLeg.notional.token) "All leg notionals must be the same" by (irs.fixedLeg.notional == irs.floatingLeg.notional) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRSUtils.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRSUtils.kt index 806858f071..6ac160849a 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRSUtils.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/IRSUtils.kt @@ -95,7 +95,7 @@ class ReferenceRate(val oracle: String, val tenor: Tenor, val name: String) : Fl } // TODO: For further discussion. -operator fun Amount.times(other: RatioUnit): Amount = Amount((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.token) +operator fun Amount.times(other: RatioUnit): Amount = Amount((BigDecimal(this.quantity).multiply(other.value)).longValueExact(), this.token) //operator fun Amount.times(other: FixedRate): Amount = Amount((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency) //fun Amount.times(other: InterestRateSwap.RatioUnit): Amount = Amount((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/CashIssuanceDefinition.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/AssetIssuanceDefinition.kt similarity index 67% rename from contracts/src/main/kotlin/com/r3corda/contracts/cash/CashIssuanceDefinition.kt rename to contracts/src/main/kotlin/com/r3corda/contracts/cash/AssetIssuanceDefinition.kt index eaade3fd4d..906ba85a14 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/CashIssuanceDefinition.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/AssetIssuanceDefinition.kt @@ -8,8 +8,8 @@ import java.util.* * Subset of cash-like contract state, containing the issuance definition. If these definitions match for two * contracts' states, those states can be aggregated. */ -interface CashIssuanceDefinition : IssuanceDefinition { - /** Where the underlying currency backing this ledger entry can be found (propagated) */ +interface AssetIssuanceDefinition : IssuanceDefinition { + /** Where the underlying asset backing this ledger entry can be found (propagated) */ val deposit: PartyAndReference - val currency: Currency + val token: T } \ No newline at end of file diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt index 58cb78327d..7ccfee0f95 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt @@ -18,8 +18,6 @@ import java.util.* val CASH_PROGRAM_ID = Cash() //SecureHash.sha256("cash") -class InsufficientBalanceException(val amountMissing: Amount) : Exception() - /** * A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple * input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour @@ -33,7 +31,7 @@ class InsufficientBalanceException(val amountMissing: Amount) : Except * At the same time, other contracts that just want money and don't care much who is currently holding it in their * vaults can ignore the issuer/depositRefs and just examine the amount fields. */ -class Cash : Contract { +class Cash : FungibleAsset() { /** * TODO: * 1) hash should be of the contents, not the URI @@ -46,12 +44,12 @@ class Cash : Contract { */ override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") - data class IssuanceDefinition( + data class IssuanceDefinition( /** Where the underlying currency backing this ledger entry can be found (propagated) */ override val deposit: PartyAndReference, - override val currency: Currency - ) : CashIssuanceDefinition + override val token: T + ) : AssetIssuanceDefinition /** A state representing a cash claim against some party */ data class State( @@ -64,9 +62,9 @@ class Cash : Contract { override val owner: PublicKey, override val notary: Party - ) : CommonCashState { - override val issuanceDef: Cash.IssuanceDefinition - get() = Cash.IssuanceDefinition(deposit, amount.token) + ) : FungibleAsset.State { + override val issuanceDef: IssuanceDefinition + get() = IssuanceDefinition(deposit, amount.token) override val contract = CASH_PROGRAM_ID override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})" @@ -76,93 +74,26 @@ class Cash : Contract { // Just for grouping interface Commands : CommandData { - class Move() : TypeOnlyCommandData(), Commands + class Move() : TypeOnlyCommandData(), FungibleAsset.Commands.Move /** * Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction * has a unique ID even when there are no inputs. */ - data class Issue(val nonce: Long = SecureRandom.getInstanceStrong().nextLong()) : Commands + data class Issue(override val nonce: Long = SecureRandom.getInstanceStrong().nextLong()) : FungibleAsset.Commands.Issue /** * A command stating that money has been withdrawn from the shared ledger and is now accounted for * in some other way. */ - data class Exit(val amount: Amount) : Commands - } - - /** This is the function EVERYONE runs */ - override fun verify(tx: TransactionForVerification) { - // Each group is a set of input/output states with distinct (deposit, currency) attributes. These types - // of cash are not fungible and must be kept separated for bookkeeping purposes. - val groups = tx.groupStates() { it: Cash.State -> it.issuanceDef } - - for ((inputs, outputs, key) in groups) { - // Either inputs or outputs could be empty. - val deposit = key.deposit - val currency = key.currency - val issuer = deposit.party - - requireThat { - "there are no zero sized outputs" by outputs.none { it.amount.pennies == 0L } - } - - val issueCommand = tx.commands.select().firstOrNull() - if (issueCommand != null) { - verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer) - } else { - val inputAmount = inputs.sumCashOrNull() ?: throw IllegalArgumentException("there is at least one cash input for this group") - val outputAmount = outputs.sumCashOrZero(currency) - - // If we want to remove cash from the ledger, that must be signed for by the issuer. - // A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero. - val exitCommand = tx.commands.select(party = issuer).singleOrNull() - val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, currency) - - requireThat { - "there are no zero sized inputs" by inputs.none { it.amount.pennies == 0L } - "for deposit ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by - (inputAmount == outputAmount + amountExitingLedger) - } - - verifyMoveCommands(inputs, tx) - } - } - } - - private fun verifyIssueCommand(inputs: List, - outputs: List, - tx: TransactionForVerification, - issueCommand: AuthenticatedObject, - currency: Currency, - issuer: Party) { - // If we have an issue command, perform special processing: the group is allowed to have no inputs, - // and the output states must have a deposit reference owned by the signer. - // - // Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must - // sum to more than the inputs. An issuance of zero size is not allowed. - // - // Note that this means literally anyone with access to the network can issue cash claims of arbitrary - // amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some - // as-yet-unwritten identity service. See ADP-22 for discussion. - - // The grouping ensures that all outputs have the same deposit reference and currency. - val inputAmount = inputs.sumCashOrZero(currency) - val outputAmount = outputs.sumCash() - val cashCommands = tx.commands.select() - requireThat { - "the issue command has a nonce" by (issueCommand.value.nonce != 0L) - "output deposits are owned by a command signer" by (issuer in issueCommand.signingParties) - "output values sum to more than the inputs" by (outputAmount > inputAmount) - "there is only a single issue command" by (cashCommands.count() == 1) - } + data class Exit(override val amount: Amount) : Commands, FungibleAsset.Commands.Exit } /** * Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey. */ - fun generateIssue(tx: TransactionBuilder, issuanceDef: CashIssuanceDefinition, pennies: Long, owner: PublicKey, notary: Party) - = generateIssue(tx, Amount(pennies, issuanceDef.currency), issuanceDef.deposit, owner, notary) + fun generateIssue(tx: TransactionBuilder, issuanceDef: AssetIssuanceDefinition, pennies: Long, owner: PublicKey, notary: Party) + = generateIssue(tx, Amount(pennies, issuanceDef.token), issuanceDef.deposit, owner, notary) /** * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. @@ -234,7 +165,7 @@ class Cash : Contract { State(deposit, totalAmount, to, coins.first().state.notary) } - val outputs = if (change.pennies > 0) { + val outputs = if (change.quantity > 0) { // Just copy a key across as the change key. In real life of course, this works but leaks private data. // In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow // value flows through the transaction graph. diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt new file mode 100644 index 0000000000..ac79efb7b2 --- /dev/null +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt @@ -0,0 +1,148 @@ +package com.r3corda.contracts.cash + +import com.r3corda.core.contracts.* +import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.SecureHash +import com.r3corda.core.crypto.toStringShort +import com.r3corda.core.utilities.Emoji +import java.security.PublicKey +import java.security.SecureRandom +import java.util.* + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Cash-like +// + +class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception() + +/** + * Superclass for contracts representing assets which are fungible, countable and issued by a specific party. States + * contain assets which are equivalent (such as cash of the same currency), so records of their existence can + * be merged or split as needed where the issuer is the same. For instance, dollars issued by the Fed are fungible and + * countable (in cents), barrels of West Texas crude are fungible and countable (oil from two small containers + * can be poured into one large container), shares of the same class in a specific company are fungible and + * countable, and so on. + * + * See [Cash] for an example subclass that implements currency. + * + * @param T a type that represents the asset in question. This should describe the basic type of the asset + * (GBP, USD, oil, shares in company , etc.) and any additional metadata (issuer, grade, class, etc.) + */ +abstract class FungibleAsset : Contract { + /** A state representing a claim against some party */ + interface State : FungibleAssetState> { + /** Where the underlying asset backing this ledger entry can be found (propagated) */ + override val deposit: PartyAndReference + override val amount: Amount + /** There must be a MoveCommand signed by this key to claim the amount */ + override val owner: PublicKey + override val notary: Party + } + + // Just for grouping + interface Commands : CommandData { + interface Move : Commands + + /** + * Allows new asset states to be issued into existence: the nonce ("number used once") ensures the transaction + * has a unique ID even when there are no inputs. + */ + interface Issue : Commands { val nonce: Long } + + /** + * A command stating that money has been withdrawn from the shared ledger and is now accounted for + * in some other way. + */ + interface Exit : Commands { val amount: Amount } + } + + /** This is the function EVERYONE runs */ + override fun verify(tx: TransactionForVerification) { + // Each group is a set of input/output states with distinct issuance definitions. These assets are not fungible + // and must be kept separated for bookkeeping purposes. + val groups = tx.groupStates() { it: FungibleAsset.State -> it.issuanceDef } + + for ((inputs, outputs, key) in groups) { + // Either inputs or outputs could be empty. + val deposit = key.deposit + val token = key.token + val issuer = deposit.party + + requireThat { + "there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L } + } + + val issueCommand = tx.commands.select().firstOrNull() + if (issueCommand != null) { + verifyIssueCommand(inputs, outputs, tx, issueCommand, token, issuer) + } else { + val inputAmount = inputs.sumFungibleOrNull() ?: throw IllegalArgumentException("there is at least one asset input for this group") + val outputAmount = outputs.sumFungibleOrZero(token) + + // If we want to remove assets from the ledger, that must be signed for by the issuer. + // A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero. + val exitCommand = tx.commands.select>(party = issuer).singleOrNull() + val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, token) + + requireThat { + "there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L } + "for deposit ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by + (inputAmount == outputAmount + amountExitingLedger) + } + + verifyMoveCommands(inputs, tx) + } + } + } + + private fun verifyIssueCommand(inputs: List>, + outputs: List>, + tx: TransactionForVerification, + issueCommand: AuthenticatedObject, + token: T, + issuer: Party) { + // If we have an issue command, perform special processing: the group is allowed to have no inputs, + // and the output states must have a deposit reference owned by the signer. + // + // Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must + // sum to more than the inputs. An issuance of zero size is not allowed. + // + // Note that this means literally anyone with access to the network can issue asset claims of arbitrary + // amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some + // external mechanism (such as locally defined rules on which parties are trustworthy). + + // The grouping ensures that all outputs have the same deposit reference and token. + val inputAmount = inputs.sumFungibleOrZero(token) + val outputAmount = outputs.sumFungible() + val assetCommands = tx.commands.select() + requireThat { + "the issue command has a nonce" by (issueCommand.value.nonce != 0L) + "output deposits are owned by a command signer" by (issuer in issueCommand.signingParties) + "output values sum to more than the inputs" by (outputAmount > inputAmount) + "there is only a single issue command" by (assetCommands.count() == 1) + } + } +} + + +// Small DSL extensions. + +/** + * Sums the asset states in the list belonging to a single owner, throwing an exception + * if there are none, or if any of the asset states cannot be added together (i.e. are + * different tokens). + */ +fun Iterable.sumFungibleBy(owner: PublicKey) = filterIsInstance>().filter { it.owner == owner }.map { it.amount }.sumOrThrow() + +/** + * Sums the asset states in the list, throwing an exception if there are none, or if any of the asset + * states cannot be added together (i.e. are different tokens). + */ +fun Iterable.sumFungible() = filterIsInstance>().map { it.amount }.sumOrThrow() + +/** Sums the asset states in the list, returning null if there are none. */ +fun Iterable.sumFungibleOrNull() = filterIsInstance>().map { it.amount }.sumOrNull() + +/** Sums the asset states in the list, returning zero of the given token if there are none. */ +fun Iterable.sumFungibleOrZero(token: T) = filterIsInstance>().map { it.amount }.sumOrZero(token) \ No newline at end of file diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/CommonCashState.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAssetState.kt similarity index 78% rename from contracts/src/main/kotlin/com/r3corda/contracts/cash/CommonCashState.kt rename to contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAssetState.kt index 9b07eca974..73ac45231a 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/CommonCashState.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAssetState.kt @@ -8,9 +8,9 @@ import java.util.Currency /** * Common elements of cash contract states. */ -interface CommonCashState : OwnableState { +interface FungibleAssetState> : OwnableState { val issuanceDef: I /** Where the underlying currency backing this ledger entry can be found (propagated) */ val deposit: PartyAndReference - val amount: Amount + val amount: Amount } \ No newline at end of file diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt index d5c621bdc8..0d94e41e5e 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt @@ -340,18 +340,18 @@ class IRSTests { fun `expression calculation testing`() { val dummyIRS = singleIRS() val stuffToPrint: ArrayList = arrayListOf( - "fixedLeg.notional.pennies", + "fixedLeg.notional.quantity", "fixedLeg.fixedRate.ratioUnit", "fixedLeg.fixedRate.ratioUnit.value", - "floatingLeg.notional.pennies", + "floatingLeg.notional.quantity", "fixedLeg.fixedRate", "currentBusinessDate", "calculation.floatingLegPaymentSchedule.get(currentBusinessDate)", "fixedLeg.notional.token.currencyCode", - "fixedLeg.notional.pennies * 10", - "fixedLeg.notional.pennies * fixedLeg.fixedRate.ratioUnit.value", + "fixedLeg.notional.quantity * 10", + "fixedLeg.notional.quantity * fixedLeg.fixedRate.ratioUnit.value", "(fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360 ", - "(fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value))" + "(fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value))" // "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate" // "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value", //"( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))", @@ -450,7 +450,7 @@ class IRSTests { val irs = singleIRS() transaction { output() { - irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(pennies = 0))) + irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(quantity = 0))) } arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } timestamp(TEST_TX_TIME) @@ -459,7 +459,7 @@ class IRSTests { transaction { output() { - irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(pennies = 0))) + irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(quantity = 0))) } arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } timestamp(TEST_TX_TIME) @@ -487,7 +487,7 @@ class IRSTests { @Test fun `ensure same currency notionals`() { val irs = singleIRS() - val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.fixedLeg.notional.pennies, Currency.getInstance("JPY")))) + val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.fixedLeg.notional.quantity, Currency.getInstance("JPY")))) transaction { output() { modifiedIRS @@ -501,7 +501,7 @@ class IRSTests { @Test fun `ensure notional amounts are equal`() { val irs = singleIRS() - val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.floatingLeg.notional.pennies + 1, irs.floatingLeg.notional.token))) + val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.floatingLeg.notional.quantity + 1, irs.floatingLeg.notional.token))) transaction { output() { modifiedIRS @@ -619,7 +619,7 @@ class IRSTests { val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.first() val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey] - var modifiedFirstResetValue = firstResetValue!!.copy(notional = Amount(firstResetValue.notional.pennies, Currency.getInstance("JPY"))) + var modifiedFirstResetValue = firstResetValue!!.copy(notional = Amount(firstResetValue.notional.quantity, Currency.getInstance("JPY"))) output() { newIRS.copy( @@ -640,7 +640,7 @@ class IRSTests { arg(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) } val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key } - var modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.pennies, Currency.getInstance("JPY"))) + var modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.quantity, Currency.getInstance("JPY"))) output() { newIRS.copy( diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt index 511f04cffa..9abe855298 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt @@ -41,7 +41,7 @@ class CashTests { tweak { output { outState } // No command arguments - this `fails requirement` "required com.r3corda.contracts.cash.Cash.Commands.Move command" + this `fails requirement` "required com.r3corda.contracts.cash.FungibleAsset.Commands.Move command" } tweak { output { outState } @@ -52,7 +52,7 @@ class CashTests { output { outState } output { outState `issued by` MINI_CORP } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } - this `fails requirement` "at least one cash input" + this `fails requirement` "at least one asset input" } // Simple reallocation works. tweak { @@ -71,7 +71,7 @@ class CashTests { output { outState } arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() } - this `fails requirement` "there is at least one cash input" + this `fails requirement` "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 @@ -112,7 +112,7 @@ class CashTests { // Test issuance from the issuance definition val issuanceDef = Cash.IssuanceDefinition(MINI_CORP.ref(12, 34), USD) val templatePtx = TransactionBuilder() - Cash().generateIssue(templatePtx, issuanceDef, 100.DOLLARS.pennies, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) + Cash().generateIssue(templatePtx, issuanceDef, 100.DOLLARS.quantity, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) assertTrue(templatePtx.inputStates().isEmpty()) assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0]) @@ -297,7 +297,7 @@ class CashTests { tweak { arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) } - this `fails requirement` "required com.r3corda.contracts.cash.Cash.Commands.Move command" + this `fails requirement` "required com.r3corda.contracts.cash.FungibleAsset.Commands.Move command" tweak { arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt b/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt index 5ae40a4e17..0ec4c99d3c 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt @@ -33,40 +33,40 @@ import java.util.* * * @param T the type of the token, for example [Currency]. */ -data class Amount(val pennies: Long, val token: T) : Comparable> { +data class Amount(val quantity: Long, val token: T) : Comparable> { init { // Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain // contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance. // If you want to express a negative amount, for now, use a long. - require(pennies >= 0) { "Negative amounts are not allowed: $pennies" } + require(quantity >= 0) { "Negative amounts are not allowed: $quantity" } } constructor(amount: BigDecimal, currency: T) : this(amount.toLong(), currency) operator fun plus(other: Amount): Amount { checkCurrency(other) - return Amount(Math.addExact(pennies, other.pennies), token) + return Amount(Math.addExact(quantity, other.quantity), token) } operator fun minus(other: Amount): Amount { checkCurrency(other) - return Amount(Math.subtractExact(pennies, other.pennies), token) + return Amount(Math.subtractExact(quantity, other.quantity), token) } private fun checkCurrency(other: Amount) { require(other.token == token) { "Currency mismatch: ${other.token} vs $token" } } - operator fun div(other: Long): Amount = Amount(pennies / other, token) - operator fun times(other: Long): Amount = Amount(Math.multiplyExact(pennies, other), token) - operator fun div(other: Int): Amount = Amount(pennies / other, token) - operator fun times(other: Int): Amount = Amount(Math.multiplyExact(pennies, other.toLong()), token) + operator fun div(other: Long): Amount = Amount(quantity / other, token) + operator fun times(other: Long): Amount = Amount(Math.multiplyExact(quantity, other), token) + operator fun div(other: Int): Amount = Amount(quantity / other, token) + operator fun times(other: Int): Amount = Amount(Math.multiplyExact(quantity, other.toLong()), token) - override fun toString(): String = (BigDecimal(pennies).divide(BigDecimal(100))).setScale(2).toPlainString() + override fun toString(): String = (BigDecimal(quantity).divide(BigDecimal(100))).setScale(2).toPlainString() override fun compareTo(other: Amount): Int { checkCurrency(other) - return pennies.compareTo(other.pennies) + return quantity.compareTo(other.quantity) } } diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 98c30ba2c2..f8dcde9f73 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -10,6 +10,8 @@ Here are changes in git master that haven't yet made it to a snapshot release: * The cash contract has moved from com.r3corda.contracts to com.r3corda.contracts.cash. * Amount class is now generic, to support non-currency types (such as assets, or currency with additional information). +* Refactored the Cash contract to have a new FungibleAsset superclass, to model all countable assets that can be merged + and split (currency, barrels of oil, etc.) Milestone 0 diff --git a/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt b/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt index ce0a5a656d..9e1e55e519 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt @@ -130,7 +130,7 @@ class NodeWalletService(private val services: ServiceHubInternal) : SingletonSer m.register("WalletBalances.${balance.key}Pennies", newMetric) newMetric } - metric.pennies = balance.value.pennies + metric.pennies = balance.value.quantity } } @@ -172,7 +172,7 @@ class NodeWalletService(private val services: ServiceHubInternal) : SingletonSer private fun calculateRandomlySizedAmounts(howMuch: Amount, min: Int, max: Int, rng: Random): LongArray { val numStates = min + Math.floor(rng.nextDouble() * (max - min)).toInt() val amounts = LongArray(numStates) - val baseSize = howMuch.pennies / numStates + val baseSize = howMuch.quantity / numStates var filledSoFar = 0L for (i in 0..numStates - 1) { if (i < numStates - 1) { @@ -181,7 +181,7 @@ class NodeWalletService(private val services: ServiceHubInternal) : SingletonSer filledSoFar += baseSize } else { // Handle inexact rounding. - amounts[i] = howMuch.pennies - filledSoFar + amounts[i] = howMuch.quantity - filledSoFar } } return amounts diff --git a/node/src/main/resources/com/r3corda/node/internal/testing/trade.json b/node/src/main/resources/com/r3corda/node/internal/testing/trade.json index b433102ef0..0d5d125897 100644 --- a/node/src/main/resources/com/r3corda/node/internal/testing/trade.json +++ b/node/src/main/resources/com/r3corda/node/internal/testing/trade.json @@ -2,7 +2,7 @@ "fixedLeg": { "fixedRatePayer": "Bank A", "notional": { - "pennies": 2500000000, + "quantity": 2500000000, "token": "USD" }, "paymentFrequency": "SemiAnnual", @@ -27,7 +27,7 @@ "floatingLeg": { "floatingRatePayer": "Bank B", "notional": { - "pennies": 2500000000, + "quantity": 2500000000, "token": "USD" }, "paymentFrequency": "Quarterly", @@ -56,7 +56,7 @@ } }, "calculation": { - "expression": "( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))", + "expression": "( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))", "floatingLegPaymentSchedule": { }, "fixedLegPaymentSchedule": { @@ -67,19 +67,19 @@ "eligibleCurrency": "EUR", "eligibleCreditSupport": "Cash in an Eligible Currency", "independentAmounts": { - "pennies": 0, + "quantity": 0, "token": "EUR" }, "threshold": { - "pennies": 0, + "quantity": 0, "token": "EUR" }, "minimumTransferAmount": { - "pennies": 25000000, + "quantity": 25000000, "token": "EUR" }, "rounding": { - "pennies": 1000000, + "quantity": 1000000, "token": "EUR" }, "valuationDate": "Every Local Business Day", diff --git a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt index 81edf7a2b2..3b9e41d300 100644 --- a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt @@ -350,7 +350,7 @@ class TwoPartyTradeProtocolTests { @Test fun `dependency with error on buyer side`() { transactionGroupFor { - runWithError(true, false, "at least one cash input") + runWithError(true, false, "at least one asset input") } } diff --git a/scripts/example-irs-trade.json b/scripts/example-irs-trade.json index 737b32780e..81a68d5817 100644 --- a/scripts/example-irs-trade.json +++ b/scripts/example-irs-trade.json @@ -2,7 +2,7 @@ "fixedLeg": { "fixedRatePayer": "Bank A", "notional": { - "pennies": 2500000000, + "quantity": 2500000000, "currency": "USD" }, "paymentFrequency": "SemiAnnual", @@ -27,7 +27,7 @@ "floatingLeg": { "floatingRatePayer": "Bank B", "notional": { - "pennies": 2500000000, + "quantity": 2500000000, "currency": "USD" }, "paymentFrequency": "Quarterly", @@ -56,7 +56,7 @@ } }, "calculation": { - "expression": "( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))", + "expression": "( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))", "floatingLegPaymentSchedule": { }, "fixedLegPaymentSchedule": { @@ -67,19 +67,19 @@ "eligibleCurrency": "EUR", "eligibleCreditSupport": "Cash in an Eligible Currency", "independentAmounts": { - "pennies": 0, + "quantity": 0, "currency": "EUR" }, "threshold": { - "pennies": 0, + "quantity": 0, "currency": "EUR" }, "minimumTransferAmount": { - "pennies": 25000000, + "quantity": 25000000, "currency": "EUR" }, "rounding": { - "pennies": 1000000, + "quantity": 1000000, "currency": "EUR" }, "valuationDate": "Every Local Business Day", @@ -101,4 +101,4 @@ "hashLegalDocs": "put hash here" }, "notary": "Bank A" -} \ No newline at end of file +}