From d7b367965fa9e320dd07b20ad4d8a07cb032eac2 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Tue, 31 May 2016 11:35:38 +0100 Subject: [PATCH] Genericise Amount class Make the Amount class generic so it doesn't have to represent a quantity of a currency, but can handle other things such as assets as well, or extended detail (for example a currency-issuer tuple). --- .../contracts/ICommercialPaperState.java | 3 +- .../com/r3corda/contracts/CommercialPaper.kt | 7 +-- .../kotlin/com/r3corda/contracts/CrowdFund.kt | 10 ++-- .../main/kotlin/com/r3corda/contracts/IRS.kt | 50 +++++++++---------- .../kotlin/com/r3corda/contracts/IRSUtils.kt | 7 +-- .../kotlin/com/r3corda/contracts/cash/Cash.kt | 16 +++--- .../r3corda/contracts/cash/CommonCashState.kt | 3 +- .../r3corda/contracts/testing/TestUtils.kt | 2 +- .../protocols/TwoPartyTradeProtocol.kt | 9 ++-- .../r3corda/contracts/CommercialPaperTests.kt | 3 +- .../kotlin/com/r3corda/contracts/IRSTests.kt | 6 +-- .../com/r3corda/contracts/cash/CashTests.kt | 4 +- .../r3corda/core/contracts/ContractsDSL.kt | 8 +-- .../r3corda/core/contracts/FinanceTypes.kt | 48 +++++++++--------- .../r3corda/core/node/services/Services.kt | 4 +- .../core/contracts/TransactionGroupTests.kt | 5 +- .../TransactionSerializationTests.kt | 11 ++-- docs/source/release-notes.rst | 1 + .../node/services/wallet/NodeWalletService.kt | 8 +-- .../node/services/wallet/WalletImpl.kt | 4 +- .../r3corda/node/internal/testing/trade.json | 16 +++--- .../messaging/TwoPartyTradeProtocolTests.kt | 5 +- 22 files changed, 121 insertions(+), 109 deletions(-) diff --git a/contracts/src/main/java/com/r3corda/contracts/ICommercialPaperState.java b/contracts/src/main/java/com/r3corda/contracts/ICommercialPaperState.java index 86329e4540..aa40a2bd78 100644 --- a/contracts/src/main/java/com/r3corda/contracts/ICommercialPaperState.java +++ b/contracts/src/main/java/com/r3corda/contracts/ICommercialPaperState.java @@ -6,6 +6,7 @@ import com.r3corda.core.contracts.PartyAndReference; import java.security.*; import java.time.*; +import java.util.Currency; /* This is an interface solely created to demonstrate that the same kotlin tests can be run against * either a Java implementation of the CommercialPaper or a kotlin implementation. @@ -17,7 +18,7 @@ public interface ICommercialPaperState extends ContractState { ICommercialPaperState withIssuance(PartyAndReference newIssuance); - ICommercialPaperState withFaceValue(Amount newFaceValue); + ICommercialPaperState withFaceValue(Amount newFaceValue); ICommercialPaperState withMaturityDate(Instant newMaturityDate); } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index 1a0f8ddc85..36ae1ca5d5 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -12,6 +12,7 @@ import com.r3corda.core.crypto.toStringShort import com.r3corda.core.utilities.Emoji import java.security.PublicKey import java.time.Instant +import java.util.Currency /** * This is an ultra-trivial implementation of commercial paper, which is essentially a simpler version of a corporate @@ -45,7 +46,7 @@ class CommercialPaper : Contract { data class State( val issuance: PartyAndReference, override val owner: PublicKey, - val faceValue: Amount, + val faceValue: Amount, val maturityDate: Instant, override val notary: Party ) : OwnableState, ICommercialPaperState { @@ -59,7 +60,7 @@ class CommercialPaper : Contract { override fun withOwner(newOwner: PublicKey): ICommercialPaperState = copy(owner = newOwner) override fun withIssuance(newIssuance: PartyAndReference): ICommercialPaperState = copy(issuance = newIssuance) - override fun withFaceValue(newFaceValue: Amount): ICommercialPaperState = copy(faceValue = newFaceValue) + override fun withFaceValue(newFaceValue: Amount): ICommercialPaperState = copy(faceValue = newFaceValue) override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate) } @@ -135,7 +136,7 @@ class CommercialPaper : Contract { * an existing transaction because you aren't able to issue multiple pieces of CP in a single transaction * at the moment: this restriction is not fundamental and may be lifted later. */ - fun generateIssue(issuance: PartyAndReference, faceValue: Amount, maturityDate: Instant, notary: Party): TransactionBuilder { + fun generateIssue(issuance: PartyAndReference, faceValue: Amount, maturityDate: Instant, notary: Party): TransactionBuilder { val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate, notary) return TransactionBuilder().withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CrowdFund.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CrowdFund.kt index 4e39361ce4..3e5da7a21c 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CrowdFund.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CrowdFund.kt @@ -42,7 +42,7 @@ class CrowdFund : Contract { data class Campaign( val owner: PublicKey, val name: String, - val target: Amount, + val target: Amount, val closingTime: Instant ) { override fun toString() = "Crowdsourcing($target sought by $owner by $closingTime)" @@ -56,12 +56,12 @@ class CrowdFund : Contract { ) : ContractState { override val contract = CROWDFUND_PROGRAM_ID - val pledgedAmount: Amount get() = pledges.map { it.amount }.sumOrZero(campaign.target.currency) + val pledgedAmount: Amount get() = pledges.map { it.amount }.sumOrZero(campaign.target.token) } data class Pledge( val owner: PublicKey, - val amount: Amount + val amount: Amount ) @@ -101,7 +101,7 @@ class CrowdFund : Contract { requireThat { "campaign details have not changed" by (inputCrowdFund.campaign == outputCrowdFund.campaign) "the campaign is still open" by (inputCrowdFund.campaign.closingTime >= time) - "the pledge must be in the same currency as the goal" by (pledgedCash.currency == outputCrowdFund.campaign.target.currency) + "the pledge must be in the same currency as the goal" by (pledgedCash.token == outputCrowdFund.campaign.target.token) "the pledged total has increased by the value of the pledge" by (outputCrowdFund.pledgedAmount == inputCrowdFund.pledgedAmount + pledgedCash) "the output registration has an open state" by (!outputCrowdFund.closed) } @@ -141,7 +141,7 @@ class CrowdFund : Contract { * Returns a transaction that registers a crowd-funding campaing, owned by the issuing institution's key. Does not update * an existing transaction because it's not possible to register multiple campaigns in a single transaction */ - fun generateRegister(owner: PartyAndReference, fundingTarget: Amount, fundingName: String, closingTime: Instant, notary: Party): TransactionBuilder { + fun generateRegister(owner: PartyAndReference, fundingTarget: Amount, fundingName: String, closingTime: Instant, notary: Party): TransactionBuilder { val campaign = Campaign(owner = owner.party.owningKey, name = fundingName, target = fundingTarget, closingTime = closingTime) val state = State(campaign, notary) return TransactionBuilder().withItems(state, Command(Commands.Register(), owner.party.owningKey)) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt index 4d51c62436..84ed6ca3ce 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt @@ -44,7 +44,7 @@ open class Event(val date: LocalDate) { * Top level PaymentEvent class - represents an obligation to pay an amount on a given date, which may be either in the past or the future. */ abstract class PaymentEvent(date: LocalDate) : Event(date) { - abstract fun calculate(): Amount + abstract fun calculate(): Amount } /** @@ -59,22 +59,22 @@ abstract class RatePaymentEvent(date: LocalDate, val accrualEndDate: LocalDate, val dayCountBasisDay: DayCountBasisDay, val dayCountBasisYear: DayCountBasisYear, - val notional: Amount, + val notional: Amount, val rate: Rate) : PaymentEvent(date) { companion object { val CSVHeader = "AccrualStartDate,AccrualEndDate,DayCountFactor,Days,Date,Ccy,Notional,Rate,Flow" } - override fun calculate(): Amount = flow + override fun calculate(): Amount = flow - abstract val flow: Amount + abstract val flow: Amount val days: Int get() = calculateDaysBetween(accrualStartDate, accrualEndDate, dayCountBasisYear, dayCountBasisDay) // TODO : Fix below (use daycount convention for division, not hardcoded 360 etc) val dayCountFactor: BigDecimal get() = (BigDecimal(days).divide(BigDecimal(360.0), 8, RoundingMode.HALF_UP)).setScale(4, RoundingMode.HALF_UP) - open fun asCSV() = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.currency},${notional},$rate,$flow" + open fun asCSV() = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.token},${notional},$rate,$flow" override fun equals(other: Any?): Boolean { if (this === other) return true @@ -104,15 +104,15 @@ class FixedRatePaymentEvent(date: LocalDate, accrualEndDate: LocalDate, dayCountBasisDay: DayCountBasisDay, dayCountBasisYear: DayCountBasisYear, - notional: Amount, + notional: Amount, rate: Rate) : RatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay, dayCountBasisYear, notional, rate) { companion object { val CSVHeader = RatePaymentEvent.CSVHeader } - override val flow: Amount get() = - Amount(dayCountFactor.times(BigDecimal(notional.pennies)).times(rate.ratioUnit!!.value).toLong(), notional.currency) + override val flow: Amount get() = + Amount(dayCountFactor.times(BigDecimal(notional.pennies)).times(rate.ratioUnit!!.value).toLong(), notional.token) override fun toString(): String = "FixedRatePaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate : $flow" @@ -128,22 +128,22 @@ class FloatingRatePaymentEvent(date: LocalDate, dayCountBasisDay: DayCountBasisDay, dayCountBasisYear: DayCountBasisYear, val fixingDate: LocalDate, - notional: Amount, + notional: Amount, rate: Rate) : RatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay, dayCountBasisYear, notional, rate) { companion object { val CSVHeader = RatePaymentEvent.CSVHeader + ",FixingDate" } - override val flow: Amount get() { + override val flow: Amount get() { // TODO: Should an uncalculated amount return a zero ? null ? etc. - val v = rate.ratioUnit?.value ?: return Amount(0, notional.currency) - return Amount(dayCountFactor.times(BigDecimal(notional.pennies)).times(v).toLong(), notional.currency) + val v = rate.ratioUnit?.value ?: return Amount(0, notional.token) + return Amount(dayCountFactor.times(BigDecimal(notional.pennies)).times(v).toLong(), notional.token) } override fun toString(): String = "FloatingPaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate (fix on $fixingDate): $flow" - override fun asCSV(): String = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.currency},${notional},$fixingDate,$rate,$flow" + override fun asCSV(): String = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.token},${notional},$fixingDate,$rate,$flow" /** * Used for making immutables @@ -169,7 +169,7 @@ class FloatingRatePaymentEvent(date: LocalDate, dayCountBasisDay: DayCountBasisDay = this.dayCountBasisDay, dayCountBasisYear: DayCountBasisYear = this.dayCountBasisYear, fixingDate: LocalDate = this.fixingDate, - notional: Amount = this.notional, + notional: Amount = this.notional, rate: Rate = this.rate) = FloatingRatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay, dayCountBasisYear, fixingDate, notional, rate) } @@ -191,10 +191,10 @@ class InterestRateSwap() : Contract { val baseCurrency: Currency, val eligibleCurrency: Currency, val eligibleCreditSupport: String, - val independentAmounts: Amount, - val threshold: Amount, - val minimumTransferAmount: Amount, - val rounding: Amount, + val independentAmounts: Amount, + val threshold: Amount, + val minimumTransferAmount: Amount, + val rounding: Amount, val valuationDate: String, val notificationTime: String, val resolutionTime: String, @@ -246,7 +246,7 @@ class InterestRateSwap() : Contract { } abstract class CommonLeg( - val notional: Amount, + val notional: Amount, val paymentFrequency: Frequency, val effectiveDate: LocalDate, val effectiveDateAdjustment: DateRollConvention?, @@ -296,7 +296,7 @@ class InterestRateSwap() : Contract { open class FixedLeg( var fixedRatePayer: Party, - notional: Amount, + notional: Amount, paymentFrequency: Frequency, effectiveDate: LocalDate, effectiveDateAdjustment: DateRollConvention?, @@ -335,7 +335,7 @@ class InterestRateSwap() : Contract { // Can't autogenerate as not a data class :-( fun copy(fixedRatePayer: Party = this.fixedRatePayer, - notional: Amount = this.notional, + notional: Amount = this.notional, paymentFrequency: Frequency = this.paymentFrequency, effectiveDate: LocalDate = this.effectiveDate, effectiveDateAdjustment: DateRollConvention? = this.effectiveDateAdjustment, @@ -357,7 +357,7 @@ class InterestRateSwap() : Contract { open class FloatingLeg( var floatingRatePayer: Party, - notional: Amount, + notional: Amount, paymentFrequency: Frequency, effectiveDate: LocalDate, effectiveDateAdjustment: DateRollConvention?, @@ -415,7 +415,7 @@ class InterestRateSwap() : Contract { fun copy(floatingRatePayer: Party = this.floatingRatePayer, - notional: Amount = this.notional, + notional: Amount = this.notional, paymentFrequency: Frequency = this.paymentFrequency, effectiveDate: LocalDate = this.effectiveDate, effectiveDateAdjustment: DateRollConvention? = this.effectiveDateAdjustment, @@ -507,7 +507,7 @@ class InterestRateSwap() : Contract { "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) "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.currency == irs.floatingLeg.notional.currency) + "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) "The effective date is before the termination date for the fixed leg" by (irs.fixedLeg.effectiveDate < irs.fixedLeg.terminationDate) @@ -553,7 +553,7 @@ class InterestRateSwap() : Contract { "The changed payments dates are aligned" by (oldFloatingRatePaymentEvent.date == newFixedRatePaymentEvent.date) "The new payment has the correct rate" by (newFixedRatePaymentEvent.rate.ratioUnit!!.value == fixValue.value) "The fixing is for the next required date" by (prevIrs.calculation.nextFixingDate() == fixValue.of.forDay) - "The fix payment has the same currency as the notional" by (newFixedRatePaymentEvent.flow.currency == irs.floatingLeg.notional.currency) + "The fix payment has the same currency as the notional" by (newFixedRatePaymentEvent.flow.token == irs.floatingLeg.notional.token) // "The fixing is not in the future " by (fixCommand) // The oracle should not have signed this . } } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRSUtils.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRSUtils.kt index b52a152187..806858f071 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRSUtils.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/IRSUtils.kt @@ -3,6 +3,7 @@ package com.r3corda.contracts import com.r3corda.core.contracts.Amount import com.r3corda.core.contracts.Tenor import java.math.BigDecimal +import java.util.Currency // Things in here will move to the general utils class when we've hammered out various discussions regarding amounts, dates, oracle etc. @@ -94,9 +95,9 @@ 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.currency) -//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) +operator fun Amount.times(other: RatioUnit): Amount = Amount((BigDecimal(this.pennies).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) operator fun kotlin.Int.times(other: FixedRate): Int = BigDecimal(this).multiply(other.ratioUnit!!.value).intValueExact() operator fun Int.times(other: Rate): Int = BigDecimal(this).multiply(other.ratioUnit!!.value).intValueExact() 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 b64d2106b6..58cb78327d 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt @@ -18,7 +18,7 @@ import java.util.* val CASH_PROGRAM_ID = Cash() //SecureHash.sha256("cash") -class InsufficientBalanceException(val amountMissing: Amount) : Exception() +class InsufficientBalanceException(val amountMissing: Amount) : Exception() /** * A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple @@ -58,7 +58,7 @@ class Cash : Contract { /** Where the underlying currency backing this ledger entry can be found (propagated) */ override val deposit: PartyAndReference, - override val amount: Amount, + override val amount: Amount, /** There must be a MoveCommand signed by this key to claim the amount */ override val owner: PublicKey, @@ -66,7 +66,7 @@ class Cash : Contract { override val notary: Party ) : CommonCashState { override val issuanceDef: Cash.IssuanceDefinition - get() = Cash.IssuanceDefinition(deposit, amount.currency) + get() = Cash.IssuanceDefinition(deposit, amount.token) override val contract = CASH_PROGRAM_ID override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})" @@ -88,7 +88,7 @@ class Cash : Contract { * 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 + data class Exit(val amount: Amount) : Commands } /** This is the function EVERYONE runs */ @@ -167,7 +167,7 @@ class Cash : Contract { /** * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. */ - fun generateIssue(tx: TransactionBuilder, amount: Amount, at: PartyAndReference, owner: PublicKey, notary: Party) { + fun generateIssue(tx: TransactionBuilder, amount: Amount, at: PartyAndReference, owner: PublicKey, notary: Party) { check(tx.inputStates().isEmpty()) check(tx.outputStates().sumCashOrNull() == null) tx.addOutputState(Cash.State(at, amount, owner, notary)) @@ -183,7 +183,7 @@ class Cash : Contract { * about which type of cash claims they are willing to accept. */ @Throws(InsufficientBalanceException::class) - fun generateSpend(tx: TransactionBuilder, amount: Amount, to: PublicKey, + fun generateSpend(tx: TransactionBuilder, amount: Amount, to: PublicKey, cashStates: List>, onlyFromParties: Set? = null): List { // Discussion // @@ -205,9 +205,9 @@ class Cash : Contract { // // Finally, we add the states to the provided partial transaction. - val currency = amount.currency + val currency = amount.token val acceptableCoins = run { - val ofCurrency = cashStates.filter { it.state.amount.currency == currency } + val ofCurrency = cashStates.filter { it.state.amount.token == currency } if (onlyFromParties != null) ofCurrency.filter { it.state.deposit.party in onlyFromParties } else diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/CommonCashState.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/CommonCashState.kt index b0a4995004..9b07eca974 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/CommonCashState.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/CommonCashState.kt @@ -3,6 +3,7 @@ package com.r3corda.contracts.cash import com.r3corda.core.contracts.Amount import com.r3corda.core.contracts.OwnableState import com.r3corda.core.contracts.PartyAndReference +import java.util.Currency /** * Common elements of cash contract states. @@ -11,5 +12,5 @@ interface CommonCashState : 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/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt index 246be01388..cf033f45fb 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt @@ -52,4 +52,4 @@ infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = this.copy(owner = infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = this.withOwner(new_owner) // Allows you to write 100.DOLLARS.CASH -val Amount.CASH: Cash.State get() = Cash.State(MINI_CORP.ref(1, 2, 3), this, NullPublicKey, DUMMY_NOTARY) +val Amount.CASH: Cash.State get() = Cash.State(MINI_CORP.ref(1, 2, 3), this, NullPublicKey, DUMMY_NOTARY) diff --git a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt index 46b6f6f4e8..93be08d510 100644 --- a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt +++ b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt @@ -17,6 +17,7 @@ import com.r3corda.core.utilities.trace import java.security.KeyPair import java.security.PublicKey import java.security.SignatureException +import java.util.Currency /** * This asset trading protocol implements a "delivery vs payment" type swap. It has two parties (B and S for buyer @@ -44,7 +45,7 @@ import java.security.SignatureException object TwoPartyTradeProtocol { val TRADE_TOPIC = "platform.trade" - class UnacceptablePriceException(val givenPrice: Amount) : Exception() + class UnacceptablePriceException(val givenPrice: Amount) : Exception() class AssetMismatchException(val expectedTypeName: String, val typeName: String) : Exception() { override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName" } @@ -52,7 +53,7 @@ object TwoPartyTradeProtocol { // This object is serialised to the network and is the first protocol message the seller sends to the buyer. class SellerTradeInfo( val assetForSale: StateAndRef, - val price: Amount, + val price: Amount, val sellerOwnerKey: PublicKey, val sessionID: Long ) @@ -63,7 +64,7 @@ object TwoPartyTradeProtocol { open class Seller(val otherSide: SingleMessageRecipient, val notaryNode: NodeInfo, val assetToSell: StateAndRef, - val price: Amount, + val price: Amount, val myKeyPair: KeyPair, val buyerSessionID: Long, override val progressTracker: ProgressTracker = Seller.tracker()) : ProtocolLogic() { @@ -173,7 +174,7 @@ object TwoPartyTradeProtocol { open class Buyer(val otherSide: SingleMessageRecipient, val notary: Party, - val acceptablePrice: Amount, + val acceptablePrice: Amount, val typeToBuy: Class, val sessionID: Long) : ProtocolLogic() { diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index 5ce1959d2d..c1a902b1a6 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -14,6 +14,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized import java.time.Instant +import java.util.Currency import kotlin.test.assertFailsWith import kotlin.test.assertTrue @@ -204,7 +205,7 @@ class CommercialPaperTestsGeneric { // Generate a trade lifecycle with various parameters. fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days, - aliceGetsBack: Amount = 1000.DOLLARS, + aliceGetsBack: Amount = 1000.DOLLARS, destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL { val someProfits = 1200.DOLLARS return transactionGroupFor() { diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt index 2c977dd2a4..d5c621bdc8 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt @@ -347,10 +347,10 @@ class IRSTests { "fixedLeg.fixedRate", "currentBusinessDate", "calculation.floatingLegPaymentSchedule.get(currentBusinessDate)", - "fixedLeg.notional.currency.currencyCode", + "fixedLeg.notional.token.currencyCode", "fixedLeg.notional.pennies * 10", "fixedLeg.notional.pennies * fixedLeg.fixedRate.ratioUnit.value", - "(fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360 ", + "(fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360 ", "(fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value))" // "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate" // "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value", @@ -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.currency))) + val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.floatingLeg.notional.pennies + 1, irs.floatingLeg.notional.token))) transaction { output() { modifiedIRS 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 1152d576d8..511f04cffa 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt @@ -374,7 +374,7 @@ class CashTests { val OUR_PUBKEY_1 = DUMMY_PUBKEY_1 val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2 - fun makeCash(amount: Amount, corp: Party, depositRef: Byte = 1) = + fun makeCash(amount: Amount, corp: Party, depositRef: Byte = 1) = StateAndRef( Cash.State(corp.ref(depositRef), amount, OUR_PUBKEY_1, DUMMY_NOTARY), StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) @@ -387,7 +387,7 @@ class CashTests { makeCash(80.SWISS_FRANCS, MINI_CORP, 2) ) - fun makeSpend(amount: Amount, dest: PublicKey): WireTransaction { + fun makeSpend(amount: Amount, dest: PublicKey): WireTransaction { val tx = TransactionBuilder() Cash().generateSpend(tx, amount, dest, WALLET) return tx.toWireTransaction() diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt index be45d98a5b..479ccd2eee 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt @@ -24,11 +24,11 @@ val USD = currency("USD") val GBP = currency("GBP") val CHF = currency("CHF") -val Int.DOLLARS: Amount get() = Amount(this.toLong() * 100, USD) -val Int.POUNDS: Amount get() = Amount(this.toLong() * 100, GBP) -val Int.SWISS_FRANCS: Amount get() = Amount(this.toLong() * 100, CHF) +val Int.DOLLARS: Amount get() = Amount(this.toLong() * 100, USD) +val Int.POUNDS: Amount get() = Amount(this.toLong() * 100, GBP) +val Int.SWISS_FRANCS: Amount get() = Amount(this.toLong() * 100, CHF) -val Double.DOLLARS: Amount get() = Amount((this * 100).toLong(), USD) +val Double.DOLLARS: Amount get() = Amount((this * 100).toLong(), USD) //// Requirements ///////////////////////////////////////////////////////////////////////////////////////////////////// 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 39484a167d..5ae40a4e17 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/FinanceTypes.kt @@ -16,22 +16,24 @@ import java.time.format.DateTimeFormatter import java.util.* /** - * Amount represents a positive quantity of currency, measured in pennies, which are the smallest representable units. + * Amount represents a positive quantity of some token (currency, asset, etc.), measured in quantity of the smallest + * representable units. Note that quantity is not necessarily 1/100ths of a currency unit, but are the actual smallest + * amount used in whatever underlying thing the amount represents. * - * Note that "pennies" are not necessarily 1/100ths of a currency unit, but are the actual smallest amount used in - * whatever currency the amount represents. - * - * Amounts of different currencies *do not mix* and attempting to add or subtract two amounts of different currencies + * Amounts of different tokens *do not mix* and attempting to add or subtract two amounts of different currencies * will throw [IllegalArgumentException]. Amounts may not be negative. Amounts are represented internally using a signed * 64 bit value, therefore, the maximum expressable amount is 2^63 - 1 == Long.MAX_VALUE. Addition, subtraction and * multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer * overflow. * - * TODO: It may make sense to replace this with convenience extensions over the JSR 354 MonetaryAmount interface - * TODO: Should amount be abstracted to cover things like quantities of a stock, bond, commercial paper etc? Probably. + * TODO: It may make sense to replace this with convenience extensions over the JSR 354 MonetaryAmount interface, + * in particular for use during calculations. This may also resolve... * TODO: Think about how positive-only vs positive-or-negative amounts can be represented in the type system. + * TODO: Add either a scaling factor, or a variant for use in calculations + * + * @param T the type of the token, for example [Currency]. */ -data class Amount(val pennies: Long, val currency: Currency) : Comparable { +data class Amount(val pennies: 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. @@ -39,38 +41,38 @@ data class Amount(val pennies: Long, val currency: Currency) : Comparable= 0) { "Negative amounts are not allowed: $pennies" } } - constructor(amount: BigDecimal, currency: Currency) : this(amount.toLong(), currency) + constructor(amount: BigDecimal, currency: T) : this(amount.toLong(), currency) - operator fun plus(other: Amount): Amount { + operator fun plus(other: Amount): Amount { checkCurrency(other) - return Amount(Math.addExact(pennies, other.pennies), currency) + return Amount(Math.addExact(pennies, other.pennies), token) } - operator fun minus(other: Amount): Amount { + operator fun minus(other: Amount): Amount { checkCurrency(other) - return Amount(Math.subtractExact(pennies, other.pennies), currency) + return Amount(Math.subtractExact(pennies, other.pennies), token) } - private fun checkCurrency(other: Amount) { - require(other.currency == currency) { "Currency mismatch: ${other.currency} vs $currency" } + private fun checkCurrency(other: Amount) { + require(other.token == token) { "Currency mismatch: ${other.token} vs $token" } } - operator fun div(other: Long): Amount = Amount(pennies / other, currency) - operator fun times(other: Long): Amount = Amount(Math.multiplyExact(pennies, other), currency) - operator fun div(other: Int): Amount = Amount(pennies / other, currency) - operator fun times(other: Int): Amount = Amount(Math.multiplyExact(pennies, other.toLong()), currency) + 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) override fun toString(): String = (BigDecimal(pennies).divide(BigDecimal(100))).setScale(2).toPlainString() - override fun compareTo(other: Amount): Int { + override fun compareTo(other: Amount): Int { checkCurrency(other) return pennies.compareTo(other.pennies) } } -fun Iterable.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow() -fun Iterable.sumOrThrow() = reduce { left, right -> left + right } -fun Iterable.sumOrZero(currency: Currency) = if (iterator().hasNext()) sumOrThrow() else Amount(0, currency) +fun Iterable>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow() +fun Iterable>.sumOrThrow() = reduce { left, right -> left + right } +fun Iterable>.sumOrZero(currency: T) = if (iterator().hasNext()) sumOrThrow() else Amount(0, currency) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // diff --git a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt index c4febb7089..0535668b86 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/services/Services.kt @@ -37,7 +37,7 @@ abstract class Wallet { * Returns a map of how much cash we have in each currency, ignoring details like issuer. Note: currencies for * which we have no cash evaluate to null (not present in map), not 0. */ - abstract val cashBalances: Map + abstract val cashBalances: Map> } /** @@ -57,7 +57,7 @@ interface WalletService { * Returns a snapshot of how much cash we have in each currency, ignoring details like issuer. Note: currencies for * which we have no cash evaluate to null, not 0. */ - val cashBalances: Map + val cashBalances: Map> /** * Returns a snapshot of the heads of LinearStates diff --git a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt index 4a8b18ba67..c2e60ed365 100644 --- a/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/contracts/TransactionGroupTests.kt @@ -7,6 +7,7 @@ import com.r3corda.core.testing.* import org.junit.Test import java.security.PublicKey import java.security.SecureRandom +import java.util.Currency import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNotEquals @@ -24,7 +25,7 @@ class TransactionGroupTests { data class State( val deposit: PartyAndReference, - val amount: Amount, + val amount: Amount, override val owner: PublicKey, override val notary: Party) : OwnableState { override val contract: Contract = TEST_PROGRAM_ID @@ -33,7 +34,7 @@ class TransactionGroupTests { interface Commands : CommandData { class Move() : TypeOnlyCommandData(), Commands data class Issue(val nonce: Long = SecureRandom.getInstanceStrong().nextLong()) : Commands - data class Exit(val amount: Amount) : Commands + data class Exit(val amount: Amount) : Commands } } diff --git a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt index 6946036829..1ab212e742 100644 --- a/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/serialization/TransactionSerializationTests.kt @@ -11,6 +11,7 @@ import org.junit.Test import java.security.PublicKey import java.security.SecureRandom import java.security.SignatureException +import java.util.Currency import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -24,17 +25,17 @@ class TransactionSerializationTests { } data class State( - val deposit: PartyAndReference, - val amount: Amount, - override val owner: PublicKey, - override val notary: Party) : OwnableState { + val deposit: PartyAndReference, + val amount: Amount, + override val owner: PublicKey, + override val notary: Party) : OwnableState { override val contract: Contract = TEST_PROGRAM_ID override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) } interface Commands : CommandData { class Move() : TypeOnlyCommandData(), Commands data class Issue(val nonce: Long = SecureRandom.getInstanceStrong().nextLong()) : Commands - data class Exit(val amount: Amount) : Commands + data class Exit(val amount: Amount) : Commands } } diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 547e927203..98c30ba2c2 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -9,6 +9,7 @@ Unreleased 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). 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 04216dd6ea..6f73708568 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 @@ -39,7 +39,7 @@ class NodeWalletService(private val services: ServiceHubInternal) : WalletServic * Returns a snapshot of how much cash we have in each currency, ignoring details like issuer. Note: currencies for * which we have no cash evaluate to null, not 0. */ - override val cashBalances: Map get() = mutex.locked { wallet }.cashBalances + override val cashBalances: Map> get() = mutex.locked { wallet }.cashBalances /** * Returns a snapshot of the heads of LinearStates @@ -143,7 +143,7 @@ class NodeWalletService(private val services: ServiceHubInternal) : WalletServic * * TODO: Move this out of NodeWalletService */ - fun fillWithSomeTestCash(notary: Party, howMuch: Amount, atLeastThisManyStates: Int = 3, + fun fillWithSomeTestCash(notary: Party, howMuch: Amount, atLeastThisManyStates: Int = 3, atMostThisManyStates: Int = 10, rng: Random = Random()) { val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng) @@ -159,7 +159,7 @@ class NodeWalletService(private val services: ServiceHubInternal) : WalletServic val issuance = TransactionBuilder() val freshKey = services.keyManagementService.freshKey() - cash.generateIssue(issuance, Amount(pennies, howMuch.currency), depositRef, freshKey.public, notary) + cash.generateIssue(issuance, Amount(pennies, howMuch.token), depositRef, freshKey.public, notary) issuance.signWith(myKey) return@map issuance.toSignedTransaction(true) @@ -168,7 +168,7 @@ class NodeWalletService(private val services: ServiceHubInternal) : WalletServic services.recordTransactions(transactions) } - private fun calculateRandomlySizedAmounts(howMuch: Amount, min: Int, max: Int, rng: Random): LongArray { + 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 diff --git a/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt b/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt index c400ca62b6..01e0667b3a 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt @@ -22,11 +22,11 @@ class WalletImpl(override val states: List>) : Wallet * Returns a map of how much cash we have in each currency, ignoring details like issuer. Note: currencies for * which we have no cash evaluate to null (not present in map), not 0. */ - override val cashBalances: Map get() = states. + override val cashBalances: Map> get() = states. // Select the states we own which are cash, ignore the rest, take the amounts. mapNotNull { (it.state as? Cash.State)?.amount }. // Turn into a Map> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) } - groupBy { it.currency }. + groupBy { it.token }. // Collapse to Map by summing all the amounts of the same currency together. mapValues { it.value.sumOrThrow() } } \ No newline at end of file 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 4fe23edd56..b433102ef0 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 @@ -3,7 +3,7 @@ "fixedRatePayer": "Bank A", "notional": { "pennies": 2500000000, - "currency": "USD" + "token": "USD" }, "paymentFrequency": "SemiAnnual", "effectiveDate": "2016-03-11", @@ -28,7 +28,7 @@ "floatingRatePayer": "Bank B", "notional": { "pennies": 2500000000, - "currency": "USD" + "token": "USD" }, "paymentFrequency": "Quarterly", "effectiveDate": "2016-03-11", @@ -68,19 +68,19 @@ "eligibleCreditSupport": "Cash in an Eligible Currency", "independentAmounts": { "pennies": 0, - "currency": "EUR" + "token": "EUR" }, "threshold": { "pennies": 0, - "currency": "EUR" + "token": "EUR" }, "minimumTransferAmount": { "pennies": 25000000, - "currency": "EUR" + "token": "EUR" }, "rounding": { "pennies": 1000000, - "currency": "EUR" + "token": "EUR" }, "valuationDate": "Every Local Business Day", "notificationTime": "2:00pm London", @@ -96,9 +96,9 @@ "addressForTransfers": "", "exposure": {}, "localBusinessDay": [ "London" , "NewYork" ], - "dailyInterestAmount": "(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360", + "dailyInterestAmount": "(CashAmount * InterestRate ) / (fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360", "tradeID": "tradeXXX", "hashLegalDocs": "put hash here" }, "notary": "Notary Service" -} \ No newline at end of file +} 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 519ee494aa..6b9d7c2c61 100644 --- a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt @@ -38,6 +38,7 @@ import java.io.ByteArrayOutputStream import java.nio.file.Path import java.security.KeyPair import java.security.PublicKey +import java.util.Currency import java.util.concurrent.ExecutionException import java.util.jar.JarOutputStream import java.util.zip.ZipEntry @@ -56,14 +57,14 @@ class TwoPartyTradeProtocolTests { lateinit var net: MockNetwork private fun runSeller(smm: StateMachineManager, notary: NodeInfo, - otherSide: SingleMessageRecipient, assetToSell: StateAndRef, price: Amount, + otherSide: SingleMessageRecipient, assetToSell: StateAndRef, price: Amount, myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture { val seller = TwoPartyTradeProtocol.Seller(otherSide, notary, assetToSell, price, myKeyPair, buyerSessionID) return smm.add("${TwoPartyTradeProtocol.TRADE_TOPIC}.seller", seller) } private fun runBuyer(smm: StateMachineManager, notaryNode: NodeInfo, - otherSide: SingleMessageRecipient, acceptablePrice: Amount, typeToBuy: Class, + otherSide: SingleMessageRecipient, acceptablePrice: Amount, typeToBuy: Class, sessionID: Long): ListenableFuture { val buyer = TwoPartyTradeProtocol.Buyer(otherSide, notaryNode.identity, acceptablePrice, typeToBuy, sessionID) return smm.add("${TwoPartyTradeProtocol.TRADE_TOPIC}.buyer", buyer)