Merged in rnicoll-generic-amount (pull request #122)

Genericise Amount class
This commit is contained in:
Ross Nicoll 2016-06-01 12:04:46 +01:00
commit d8940ca88c
22 changed files with 121 additions and 109 deletions

View File

@ -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<Currency> newFaceValue);
ICommercialPaperState withMaturityDate(Instant newMaturityDate);
}

View File

@ -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<Currency>,
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<Currency>): 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<Currency>, 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))
}

View File

@ -42,7 +42,7 @@ class CrowdFund : Contract {
data class Campaign(
val owner: PublicKey,
val name: String,
val target: Amount,
val target: Amount<Currency>,
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<Currency> get() = pledges.map { it.amount }.sumOrZero(campaign.target.token)
}
data class Pledge(
val owner: PublicKey,
val amount: Amount
val amount: Amount<Currency>
)
@ -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<Currency>, 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))

View File

@ -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<Currency>
}
/**
@ -59,22 +59,22 @@ abstract class RatePaymentEvent(date: LocalDate,
val accrualEndDate: LocalDate,
val dayCountBasisDay: DayCountBasisDay,
val dayCountBasisYear: DayCountBasisYear,
val notional: Amount,
val notional: Amount<Currency>,
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<Currency> = flow
abstract val flow: Amount
abstract val flow: Amount<Currency>
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<Currency>,
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<Currency> get() =
Amount<Currency>(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<Currency>,
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<Currency> 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<Currency>(0, notional.token)
return Amount<Currency>(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<Currency> = 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<Currency>,
val threshold: Amount<Currency>,
val minimumTransferAmount: Amount<Currency>,
val rounding: Amount<Currency>,
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<Currency>,
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<Currency>,
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<Currency> = 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<Currency>,
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<Currency> = 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 .
}
}

View File

@ -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<Currency>.times(other: RatioUnit): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.token)
//operator fun Amount<Currency>.times(other: FixedRate): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency)
//fun Amount<Currency>.times(other: InterestRateSwap.RatioUnit): Amount<Currency> = Amount<Currency>((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()

View File

@ -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<Currency>) : 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<Currency>,
/** 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<Cash.IssuanceDefinition> {
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<Currency>) : 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<Currency>, 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<Currency>, to: PublicKey,
cashStates: List<StateAndRef<State>>, onlyFromParties: Set<Party>? = null): List<PublicKey> {
// 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

View File

@ -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<I : CashIssuanceDefinition> : 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<Currency>
}

View File

@ -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<Currency>.CASH: Cash.State get() = Cash.State(MINI_CORP.ref(1, 2, 3), this, NullPublicKey, DUMMY_NOTARY)

View File

@ -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<Currency>) : 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<OwnableState>,
val price: Amount,
val price: Amount<Currency>,
val sellerOwnerKey: PublicKey,
val sessionID: Long
)
@ -63,7 +64,7 @@ object TwoPartyTradeProtocol {
open class Seller(val otherSide: SingleMessageRecipient,
val notaryNode: NodeInfo,
val assetToSell: StateAndRef<OwnableState>,
val price: Amount,
val price: Amount<Currency>,
val myKeyPair: KeyPair,
val buyerSessionID: Long,
override val progressTracker: ProgressTracker = Seller.tracker()) : ProtocolLogic<SignedTransaction>() {
@ -173,7 +174,7 @@ object TwoPartyTradeProtocol {
open class Buyer(val otherSide: SingleMessageRecipient,
val notary: Party,
val acceptablePrice: Amount,
val acceptablePrice: Amount<Currency>,
val typeToBuy: Class<out OwnableState>,
val sessionID: Long) : ProtocolLogic<SignedTransaction>() {

View File

@ -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<Currency> = 1000.DOLLARS,
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<ICommercialPaperState> {
val someProfits = 1200.DOLLARS
return transactionGroupFor() {

View File

@ -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

View File

@ -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<Currency>, 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<Currency>, dest: PublicKey): WireTransaction {
val tx = TransactionBuilder()
Cash().generateSpend(tx, amount, dest, WALLET)
return tx.toWireTransaction()

View File

@ -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<Currency> get() = Amount(this.toLong() * 100, USD)
val Int.POUNDS: Amount<Currency> get() = Amount(this.toLong() * 100, GBP)
val Int.SWISS_FRANCS: Amount<Currency> get() = Amount(this.toLong() * 100, CHF)
val Double.DOLLARS: Amount get() = Amount((this * 100).toLong(), USD)
val Double.DOLLARS: Amount<Currency> get() = Amount((this * 100).toLong(), USD)
//// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -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<Amount> {
data class Amount<T>(val pennies: Long, val token: T) : Comparable<Amount<T>> {
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<Amount
require(pennies >= 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<T>): Amount<T> {
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<T>): Amount<T> {
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<T>) {
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<T> = Amount(pennies / other, token)
operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(pennies, other), token)
operator fun div(other: Int): Amount<T> = Amount(pennies / other, token)
operator fun times(other: Int): Amount<T> = 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<T>): Int {
checkCurrency(other)
return pennies.compareTo(other.pennies)
}
}
fun Iterable<Amount>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
fun Iterable<Amount>.sumOrThrow() = reduce { left, right -> left + right }
fun Iterable<Amount>.sumOrZero(currency: Currency) = if (iterator().hasNext()) sumOrThrow() else Amount(0, currency)
fun <T> Iterable<Amount<T>>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
fun <T> Iterable<Amount<T>>.sumOrThrow() = reduce { left, right -> left + right }
fun <T> Iterable<Amount<T>>.sumOrZero(currency: T) = if (iterator().hasNext()) sumOrThrow() else Amount<T>(0, currency)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//

View File

@ -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<Currency, Amount>
abstract val cashBalances: Map<Currency, Amount<Currency>>
}
/**
@ -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<Currency, Amount>
val cashBalances: Map<Currency, Amount<Currency>>
/**
* Returns a snapshot of the heads of LinearStates

View File

@ -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<Currency>,
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<Currency>) : Commands
}
}

View File

@ -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<Currency>,
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<Currency>) : Commands
}
}

View File

@ -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

View File

@ -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<Currency, Amount> get() = mutex.locked { wallet }.cashBalances
override val cashBalances: Map<Currency, Amount<Currency>> 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<Currency>, 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<Currency>, 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

View File

@ -22,11 +22,11 @@ class WalletImpl(override val states: List<StateAndRef<ContractState>>) : 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<Currency, Amount> get() = states.
override val cashBalances: Map<Currency, Amount<Currency>> 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<Currency, List<Amount>> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) }
groupBy { it.currency }.
groupBy { it.token }.
// Collapse to Map<Currency, Amount> by summing all the amounts of the same currency together.
mapValues { it.value.sumOrThrow() }
}

View File

@ -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"
}
}

View File

@ -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<OwnableState>, price: Amount,
otherSide: SingleMessageRecipient, assetToSell: StateAndRef<OwnableState>, price: Amount<Currency>,
myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture<SignedTransaction> {
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<out OwnableState>,
otherSide: SingleMessageRecipient, acceptablePrice: Amount<Currency>, typeToBuy: Class<out OwnableState>,
sessionID: Long): ListenableFuture<SignedTransaction> {
val buyer = TwoPartyTradeProtocol.Buyer(otherSide, notaryNode.identity, acceptablePrice, typeToBuy, sessionID)
return smm.add("${TwoPartyTradeProtocol.TRADE_TOPIC}.buyer", buyer)