mirror of
https://github.com/corda/corda.git
synced 2024-12-20 05:28:21 +00:00
Merged in rnicoll-generic-amount (pull request #122)
Genericise Amount class
This commit is contained in:
commit
d8940ca88c
@ -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);
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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 .
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
}
|
@ -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)
|
||||
|
@ -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>() {
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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 /////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
@ -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)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
@ -25,7 +26,7 @@ class TransactionSerializationTests {
|
||||
|
||||
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
|
||||
@ -34,7 +35,7 @@ class TransactionSerializationTests {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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() }
|
||||
}
|
@ -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,7 +96,7 @@
|
||||
"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"
|
||||
},
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user