mirror of
https://github.com/corda/corda.git
synced 2024-12-20 21:43:14 +00:00
Add a concept of token size to Amount<T> so that conversion to/from indicative and displayable BigDecimal works sensibly
Add an AmountTransfer type to express the concept of asset flows. Unify the currency amount creators and fix a few old style display conversions in teh explorer cash dialogs. Modifications according to PR comments. Change TransferAmount display string as it may not always be a payment. Update docs
This commit is contained in:
parent
f116c02149
commit
cedfc4e1ad
@ -4,6 +4,7 @@ package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@ -31,11 +32,13 @@ fun commodity(code: String) = Commodity.getInstance(code)!!
|
||||
@JvmField val RUB = currency("RUB")
|
||||
@JvmField val FCOJ = commodity("FCOJ") // Frozen concentrated orange juice, yum!
|
||||
|
||||
fun DOLLARS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, USD)
|
||||
fun DOLLARS(amount: Double): Amount<Currency> = Amount((amount * 100).toLong(), USD)
|
||||
fun POUNDS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, GBP)
|
||||
fun SWISS_FRANCS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, CHF)
|
||||
fun FCOJ(amount: Int): Amount<Commodity> = Amount(amount.toLong() * 100, FCOJ)
|
||||
fun <T: Any>AMOUNT(amount: Int, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token)
|
||||
fun <T: Any>AMOUNT(amount: Double, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount), token)
|
||||
fun DOLLARS(amount: Int): Amount<Currency> = AMOUNT(amount, USD)
|
||||
fun DOLLARS(amount: Double): Amount<Currency> = AMOUNT(amount, USD)
|
||||
fun POUNDS(amount: Int): Amount<Currency> = AMOUNT(amount, GBP)
|
||||
fun SWISS_FRANCS(amount: Int): Amount<Currency> = AMOUNT(amount, CHF)
|
||||
fun FCOJ(amount: Int): Amount<Commodity> = AMOUNT(amount, FCOJ)
|
||||
|
||||
val Int.DOLLARS: Amount<Currency> get() = DOLLARS(this)
|
||||
val Double.DOLLARS: Amount<Currency> get() = DOLLARS(this)
|
||||
@ -48,7 +51,7 @@ infix fun Commodity.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
|
||||
infix fun Amount<Currency>.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
|
||||
infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
|
||||
infix fun Commodity.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
|
||||
infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantity, token.issuedBy(deposit))
|
||||
infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit))
|
||||
|
||||
//// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
@ -11,16 +11,25 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
import java.math.RoundingMode
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This interface is used by [Amount] to determine the conversion ratio from
|
||||
* indicative/displayed asset amounts in [BigDecimal] to fungible tokens represented by Amount objects.
|
||||
*/
|
||||
interface TokenizableAssetInfo {
|
||||
val displayTokenSize: BigDecimal
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* representable units. The nominal quantity represented by each individual token is equal to the [displayTokenSize].
|
||||
* The scale property of the [displayTokenSize] should correctly reflect the displayed decimal places and is used
|
||||
* when rounding conversions from indicative/displayed amounts in [BigDecimal] to Amount occur via the Amount.fromDecimal method.
|
||||
*
|
||||
* 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
|
||||
@ -28,25 +37,62 @@ import java.util.*
|
||||
* 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,
|
||||
* 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 quantity the number of tokens as a Long value.
|
||||
* @param displayTokenSize the nominal display unit size of a single token,
|
||||
* potentially with trailing decimal display places if the scale parameter is non-zero.
|
||||
* @param T the type of the token, for example [Currency].
|
||||
* T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required.
|
||||
*
|
||||
* TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Amount<T: Any>(val quantity: Long, val token: T) : Comparable<Amount<T>> {
|
||||
data class Amount<T: Any>(val quantity: Long, val displayTokenSize: BigDecimal,val token: T) : Comparable<Amount<T>> {
|
||||
companion object {
|
||||
/**
|
||||
* Build a currency amount from a decimal representation. For example, with an input of "12.34" GBP,
|
||||
* returns an amount with a quantity of "1234".
|
||||
* Build an Amount from a decimal representation. For example, with an input of "12.34 GBP",
|
||||
* returns an amount with a quantity of "1234" tokens. The displayTokenSize as determined via
|
||||
* getDisplayTokenSize is used to determine the conversion scaling.
|
||||
* e.g. Bonds might be in nominal amounts of 100, currencies in 0.01 penny units.
|
||||
*
|
||||
* @see Amount<Currency>.toDecimal
|
||||
* @throws ArithmeticException if the intermediate calculations cannot be converted to an unsigned 63-bit token amount.
|
||||
*/
|
||||
fun fromDecimal(quantity: BigDecimal, currency: Currency) : Amount<Currency> {
|
||||
val longQuantity = quantity.movePointRight(currency.defaultFractionDigits).toLong()
|
||||
return Amount(longQuantity, currency)
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun <T : Any> fromDecimal(displayQuantity: BigDecimal, token: T, rounding: RoundingMode = RoundingMode.FLOOR): Amount<T> {
|
||||
val tokenSize = getDisplayTokenSize(token)
|
||||
val tokenCount = displayQuantity.divide(tokenSize).setScale(0, rounding).longValueExact()
|
||||
return Amount(tokenCount, tokenSize, token)
|
||||
}
|
||||
|
||||
/**
|
||||
* For a particular token returns a zero sized Amount<T>
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : Any> zero(token: T): Amount<T> {
|
||||
val tokenSize = getDisplayTokenSize(token)
|
||||
return Amount(0L, tokenSize, token)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines the representation of one Token quantity in BigDecimal. For Currency and Issued<Currency>
|
||||
* the definitions is taken from Currency defaultFractionDigits property e.g. 2 for USD, or 0 for JPY
|
||||
* so that the automatic token size is the conventional minimum penny amount.
|
||||
* For other possible token types the asset token should implement TokenizableAssetInfo to
|
||||
* correctly report the designed nominal amount.
|
||||
*/
|
||||
fun getDisplayTokenSize(token: Any): BigDecimal {
|
||||
if (token is TokenizableAssetInfo) {
|
||||
return token.displayTokenSize
|
||||
}
|
||||
if (token is Currency) {
|
||||
return BigDecimal.ONE.scaleByPowerOfTen(-token.defaultFractionDigits)
|
||||
}
|
||||
if (token is Issued<*>) {
|
||||
return getDisplayTokenSize(token.product)
|
||||
}
|
||||
return BigDecimal.ONE
|
||||
}
|
||||
|
||||
private val currencySymbols: Map<String, Currency> = mapOf(
|
||||
@ -111,44 +157,93 @@ data class Amount<T: Any>(val quantity: Long, val token: T) : Comparable<Amount<
|
||||
}
|
||||
|
||||
init {
|
||||
// Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain
|
||||
// contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance.
|
||||
// If you want to express a negative amount, for now, use a long.
|
||||
// Amount represents a static balance of physical assets as managed by the distributed ledger and is not allowed
|
||||
// to become negative a rule further maintained by the Contract verify method.
|
||||
// N.B. If concepts such as an account overdraft are required this should be modelled separately via Obligations,
|
||||
// or similar second order smart contract concepts.
|
||||
require(quantity >= 0) { "Negative amounts are not allowed: $quantity" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the amount using the given decimal value as quantity. Any fractional part
|
||||
* is discarded. To convert and use the fractional part, see [fromDecimal].
|
||||
* Automatic conversion constructor from number of tokens to an Amount using getDisplayTokenSize to determine
|
||||
* the displayTokenSize.
|
||||
*
|
||||
* @param tokenQuantity the number of tokens represented.
|
||||
* @param token the type of the token, for example a [Currency] object.
|
||||
*/
|
||||
constructor(quantity: BigDecimal, token: T) : this(quantity.toLong(), token)
|
||||
constructor(quantity: BigInteger, token: T) : this(quantity.toLong(), token)
|
||||
constructor(tokenQuantity: Long, token: T) : this(tokenQuantity, getDisplayTokenSize(token), token)
|
||||
|
||||
/**
|
||||
* A checked addition operator is supported to simplify aggregation of Amounts.
|
||||
* @throws ArithmeticException if there is overflow of Amount tokens during the summation
|
||||
* Mixing non-identical token types will throw [IllegalArgumentException]
|
||||
*/
|
||||
operator fun plus(other: Amount<T>): Amount<T> {
|
||||
checkToken(other)
|
||||
return Amount(Math.addExact(quantity, other.quantity), token)
|
||||
return Amount(Math.addExact(quantity, other.quantity), displayTokenSize, token)
|
||||
}
|
||||
|
||||
/**
|
||||
* A checked addition operator is supported to simplify netting of Amounts.
|
||||
* If this leads to the Amount going negative this will throw [IllegalArgumentException].
|
||||
* @throws ArithmeticException if there is Numeric underflow
|
||||
* Mixing non-identical token types will throw [IllegalArgumentException]
|
||||
*/
|
||||
operator fun minus(other: Amount<T>): Amount<T> {
|
||||
checkToken(other)
|
||||
return Amount(Math.subtractExact(quantity, other.quantity), token)
|
||||
return Amount(Math.subtractExact(quantity, other.quantity), displayTokenSize, token)
|
||||
}
|
||||
|
||||
private fun checkToken(other: Amount<T>) {
|
||||
require(other.token == token) { "Token mismatch: ${other.token} vs $token" }
|
||||
require(other.displayTokenSize == displayTokenSize) { "Token size mismatch: ${other.displayTokenSize} vs $displayTokenSize" }
|
||||
}
|
||||
|
||||
operator fun div(other: Long): Amount<T> = Amount(quantity / other, token)
|
||||
operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(quantity, other), token)
|
||||
operator fun div(other: Int): Amount<T> = Amount(quantity / other, token)
|
||||
operator fun times(other: Int): Amount<T> = Amount(Math.multiplyExact(quantity, other.toLong()), token)
|
||||
/**
|
||||
* The multiplication operator is supported to allow easy calculation for multiples of a primitive Amount.
|
||||
* Note this is not a conserving operation, so it may not always be correct modelling of proper token behaviour.
|
||||
* N.B. Division is not supported as fractional tokens are not representable by an Amount.
|
||||
*/
|
||||
operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(quantity, other), displayTokenSize, token)
|
||||
|
||||
operator fun times(other: Int): Amount<T> = Amount(Math.multiplyExact(quantity, other.toLong()), displayTokenSize, token)
|
||||
|
||||
/**
|
||||
* This method provides a token conserving divide mechanism.
|
||||
* @param partitions the number of amounts to divide the current quantity into.
|
||||
* @result Returns [partitions] separate Amount objects which sum to the same quantity as this Amount
|
||||
* and differ by no more than a single token in size.
|
||||
*/
|
||||
fun splitEvenly(partitions: Int): List<Amount<T>> {
|
||||
require(partitions >= 1) { "Must split amount into one, or more pieces" }
|
||||
val commonTokensPerPartition = quantity.div(partitions)
|
||||
val residualTokens = quantity - (commonTokensPerPartition * partitions)
|
||||
val splitAmount = Amount(commonTokensPerPartition, displayTokenSize, token)
|
||||
val splitAmountPlusOne = Amount(commonTokensPerPartition + 1L, displayTokenSize, token)
|
||||
return (0..partitions - 1).map { if (it < residualTokens) splitAmountPlusOne else splitAmount }.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a currency [Amount] to a decimal representation. For example, with an amount with a quantity
|
||||
* of "1234" GBP, returns "12.34". The precise representation is controlled by the displayTokenSize,
|
||||
* which determines the size of a single token and controls the trailing decimal places via it's scale property.
|
||||
*
|
||||
* @see Amount.Companion.fromDecimal
|
||||
*/
|
||||
fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantity, 0) * displayTokenSize
|
||||
|
||||
|
||||
/**
|
||||
* Convert a currency [Amount] to a display string representation.
|
||||
*
|
||||
* For example, with an amount with a quantity of "1234" GBP, returns "12.34 GBP".
|
||||
* The result of fromDecimal is used to control the numerical formatting and
|
||||
* the token specifier appended is taken from token.toString.
|
||||
*
|
||||
* @see Amount.Companion.fromDecimal
|
||||
*/
|
||||
override fun toString(): String {
|
||||
val bd = if (token is Currency)
|
||||
BigDecimal(quantity).movePointLeft(token.defaultFractionDigits)
|
||||
else
|
||||
BigDecimal(quantity)
|
||||
return bd.toPlainString() + " " + token
|
||||
return toDecimal().toPlainString() + " " + token
|
||||
}
|
||||
|
||||
override fun compareTo(other: Amount<T>): Int {
|
||||
@ -157,17 +252,207 @@ data class Amount<T: Any>(val quantity: Long, val token: T) : Comparable<Amount<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a currency [Amount] to a decimal representation. For example, with an amount with a quantity
|
||||
* of "1234" GBP, returns "12.34".
|
||||
*
|
||||
* @see Amount.Companion.fromDecimal
|
||||
*/
|
||||
fun Amount<Currency>.toDecimal() : BigDecimal = BigDecimal(quantity).movePointLeft(token.defaultFractionDigits)
|
||||
|
||||
|
||||
fun <T: Any> Iterable<Amount<T>>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
|
||||
fun <T: Any> Iterable<Amount<T>>.sumOrThrow() = reduce { left, right -> left + right }
|
||||
fun <T: Any> Iterable<Amount<T>>.sumOrZero(currency: T) = if (iterator().hasNext()) sumOrThrow() else Amount(0, currency)
|
||||
fun <T: Any> Iterable<Amount<T>>.sumOrZero(token: T) = if (iterator().hasNext()) sumOrThrow() else Amount.zero(token)
|
||||
|
||||
|
||||
/**
|
||||
* Simple data class to associate the origin, owner, or holder of a particular Amount object.
|
||||
* @param source the holder of the Amount.
|
||||
* @param amount the Amount of asset available.
|
||||
* @param ref is an optional field used for housekeeping in the caller.
|
||||
* e.g. to point back at the original Vault state objects.
|
||||
* @see SourceAndAmount.apply which processes a list of SourceAndAmount objects
|
||||
* and calculates the resulting Amount distribution as a new list of SourceAndAmount objects.
|
||||
*/
|
||||
data class SourceAndAmount<T : Any, P : Any>(val source: P, val amount: Amount<T>, val ref: Any? = null)
|
||||
|
||||
/**
|
||||
* This class represents a possibly negative transfer of tokens from one vault state to another, possibly at a future date.
|
||||
*
|
||||
* @param quantityDelta is a signed Long value representing the exchanged number of tokens. If positive then
|
||||
* it represents the movement of Math.abs(quantityDelta) tokens away from source and receipt of Math.abs(quantityDelta)
|
||||
* at the destination. If the quantityDelta is negative then the source will receive Math.abs(quantityDelta) tokens
|
||||
* and the destination will lose Math.abs(quantityDelta) tokens.
|
||||
* Where possible the source and destination should be coded to ensure a positive quantityDelta,
|
||||
* but in various scenarios it may be more consistent to allow positive and negative values.
|
||||
* For example it is common for a bank to code asset flows as gains and losses from its perspective i.e. always the destination.
|
||||
* @param token represents the type of asset token as would be used to construct Amount<T> objects.
|
||||
* @param source is the [Party], [Account], [CompositeKey], or other identifier of the token source if quantityDelta is positive,
|
||||
* or the token sink if quantityDelta is negative. The type P should support value equality.
|
||||
* @param destination is the [Party], [Account], [CompositeKey], or other identifier of the token sink if quantityDelta is positive,
|
||||
* or the token source if quantityDelta is negative. The type P should support value equality.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
|
||||
val token: T,
|
||||
val source: P,
|
||||
val destination: P) {
|
||||
companion object {
|
||||
/**
|
||||
* Construct an AmountTransfer object from an indicative/displayable BigDecimal source, applying rounding as specified.
|
||||
* The token size is determined from the token type and is the same as for [Amount] of the same token.
|
||||
* @param displayQuantityDelta is the signed amount to transfer between source and destination in displayable units.
|
||||
* Positive values mean transfers from source to destination. Negative values mean transfers from destination to source.
|
||||
* @param token defines the asset being represented in the transfer. The token should implement [TokenizableAssetInfo] if custom
|
||||
* conversion logic is required.
|
||||
* @param source The payer of the transfer if displayQuantityDelta is positive, the payee if displayQuantityDelta is negative
|
||||
* @param destination The payee of the transfer if displayQuantityDelta is positive, the payer if displayQuantityDelta is negative
|
||||
* @param rounding The mode of rounding to apply after scaling to integer token units.
|
||||
*/
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun <T : Any, P : Any> fromDecimal(displayQuantityDelta: BigDecimal,
|
||||
token: T,
|
||||
source: P,
|
||||
destination: P,
|
||||
rounding: RoundingMode = RoundingMode.DOWN): AmountTransfer<T, P> {
|
||||
val tokenSize = Amount.getDisplayTokenSize(token)
|
||||
val deltaTokenCount = displayQuantityDelta.divide(tokenSize).setScale(0, rounding).longValueExact()
|
||||
return AmountTransfer(deltaTokenCount, token, source, destination)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to make a zero size AmountTransfer
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : Any, P : Any> zero(token: T,
|
||||
source: P,
|
||||
destination: P): AmountTransfer<T, P> = AmountTransfer(0L, token, source, destination)
|
||||
}
|
||||
|
||||
init {
|
||||
require(source != destination) { "The source and destination cannot be the same ($source)" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Add together two [AmountTransfer] objects to produce the single equivalent net flow.
|
||||
* The addition only applies to AmountTransfer objects with the same token type.
|
||||
* Also the pair of parties must be aligned, although source destination may be
|
||||
* swapped in the second item.
|
||||
* @throws ArithmeticException if there is underflow, or overflow in the summations.
|
||||
*/
|
||||
operator fun plus(other: AmountTransfer<T, P>): AmountTransfer<T, P> {
|
||||
require(other.token == token) { "Token mismatch: ${other.token} vs $token" }
|
||||
require((other.source == source && other.destination == destination)
|
||||
|| (other.source == destination && other.destination == source)) {
|
||||
"Only AmountTransfer between the same two parties can be aggregated/netted"
|
||||
}
|
||||
return if (other.source == source) {
|
||||
AmountTransfer(Math.addExact(quantityDelta, other.quantityDelta), token, source, destination)
|
||||
} else {
|
||||
AmountTransfer(Math.subtractExact(quantityDelta, other.quantityDelta), token, source, destination)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the quantityDelta to a displayable format BigDecimal value. The conversion ratio is the same as for
|
||||
* [Amount] of the same token type.
|
||||
*/
|
||||
fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantityDelta, 0) * Amount.getDisplayTokenSize(token)
|
||||
|
||||
fun copy(quantityDelta: Long = this.quantityDelta,
|
||||
token: T = this.token,
|
||||
source: P = this.source,
|
||||
destination: P = this.destination): AmountTransfer<T, P> = AmountTransfer(quantityDelta, token, source, destination)
|
||||
|
||||
/**
|
||||
* Checks value equality of AmountTransfer objects, but also matches the reversed source and destination equivalent.
|
||||
*/
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as AmountTransfer<*, *>
|
||||
|
||||
if (token != other.token) return false
|
||||
if (source == other.source) {
|
||||
if (destination != other.destination) return false
|
||||
if (quantityDelta != other.quantityDelta) return false
|
||||
return true
|
||||
} else if (source == other.destination) {
|
||||
if (destination != other.source) return false
|
||||
if (quantityDelta != -other.quantityDelta) return false
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* HashCode ensures that reversed source and destination equivalents will hash to the same value.
|
||||
*/
|
||||
override fun hashCode(): Int {
|
||||
var result = Math.abs(quantityDelta).hashCode() // ignore polarity reversed values
|
||||
result = 31 * result + token.hashCode()
|
||||
result = 31 * result + (source.hashCode() xor destination.hashCode()) // XOR to ensure the same hash for swapped source and destination
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Transfer from $source to $destination of ${this.toDecimal().toPlainString()} $token"
|
||||
}
|
||||
|
||||
/**
|
||||
* Novation is a common financial operation in which a bilateral exchange is modified so that the same
|
||||
* relative asset exchange happens, but with each party exchanging versus a central counterparty, or clearing house.
|
||||
*
|
||||
* @param centralParty The central party to face the exchange against.
|
||||
* @return Returns two new AmountTransfers each between one of the original parties and the centralParty.
|
||||
* The net total exchange is the same as in the original input.
|
||||
*/
|
||||
fun novate(centralParty: P): Pair<AmountTransfer<T, P>, AmountTransfer<T, P>> = Pair(copy(destination = centralParty), copy(source = centralParty))
|
||||
|
||||
/**
|
||||
* Applies this AmountTransfer to a list of [SourceAndAmount] objects representing balances.
|
||||
* The list can be heterogeneous in terms of token types and parties, so long as there is sufficient balance
|
||||
* of the correct token type held with the party paying for the transfer.
|
||||
* @param balances The source list of [SourceAndAmount] objects containing the funds to satisfy the exchange.
|
||||
* @param newRef An optional marker object which is attached to any new [SourceAndAmount] objects created in the output.
|
||||
* i.e. To the new payment destination entry and to any residual change output.
|
||||
* @return The returned list is a copy of the original list, except that funds needed to cover the exchange
|
||||
* will have been removed and a new output and possibly residual amount entry will be added at the end of the list.
|
||||
* @throws ArithmeticException if there is underflow in the summations.
|
||||
*/
|
||||
fun apply(balances: List<SourceAndAmount<T, P>>, newRef: Any? = null): List<SourceAndAmount<T, P>> {
|
||||
val (payer, payee) = if (quantityDelta >= 0L) Pair(source, destination) else Pair(destination, source)
|
||||
val transfer = Math.abs(quantityDelta)
|
||||
var residual = transfer
|
||||
val outputs = mutableListOf<SourceAndAmount<T, P>>()
|
||||
var remaining: SourceAndAmount<T, P>? = null
|
||||
var newAmount: SourceAndAmount<T, P>? = null
|
||||
for (balance in balances) {
|
||||
if (balance.source != payer
|
||||
|| balance.amount.token != token
|
||||
|| residual == 0L) {
|
||||
// Just copy across unmodified.
|
||||
outputs += balance
|
||||
} else if (balance.amount.quantity < residual) {
|
||||
// Consume the payers amount and do not copy across.
|
||||
residual -= balance.amount.quantity
|
||||
} else {
|
||||
// Calculate any residual spend left on the payers balance.
|
||||
if (balance.amount.quantity > residual) {
|
||||
remaining = SourceAndAmount(payer, balance.amount.copy(quantity = Math.subtractExact(balance.amount.quantity, residual)), newRef)
|
||||
}
|
||||
// Build the new output payment to the payee.
|
||||
newAmount = SourceAndAmount(payee, balance.amount.copy(quantity = transfer), newRef)
|
||||
// Clear the residual.
|
||||
residual = 0L
|
||||
}
|
||||
}
|
||||
require(residual == 0L) { "Insufficient funds. Unable to process $this" }
|
||||
if (remaining != null) {
|
||||
outputs += remaining
|
||||
}
|
||||
outputs += newAmount!!
|
||||
return outputs
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
@ -546,7 +831,10 @@ enum class NetType {
|
||||
@CordaSerializable
|
||||
data class Commodity(val commodityCode: String,
|
||||
val displayName: String,
|
||||
val defaultFractionDigits: Int = 0) {
|
||||
val defaultFractionDigits: Int = 0) : TokenizableAssetInfo {
|
||||
override val displayTokenSize: BigDecimal
|
||||
get() = BigDecimal.ONE.scaleByPowerOfTen(-defaultFractionDigits)
|
||||
|
||||
companion object {
|
||||
private val registry = mapOf(
|
||||
// Simple example commodity, as in http://www.investopedia.com/university/commodities/commodities14.asp
|
||||
|
@ -2,7 +2,12 @@ package net.corda.core.contracts
|
||||
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
import java.util.stream.Collectors
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Tests of the [Amount] class.
|
||||
@ -18,10 +23,24 @@ class AmountTests {
|
||||
@Test
|
||||
fun decimalConversion() {
|
||||
val quantity = 1234L
|
||||
val amount = Amount(quantity, GBP)
|
||||
val expected = BigDecimal("12.34")
|
||||
assertEquals(expected, amount.toDecimal())
|
||||
assertEquals(amount, Amount.fromDecimal(amount.toDecimal(), amount.token))
|
||||
val amountGBP = Amount(quantity, GBP)
|
||||
val expectedGBP = BigDecimal("12.34")
|
||||
assertEquals(expectedGBP, amountGBP.toDecimal())
|
||||
assertEquals(amountGBP, Amount.fromDecimal(amountGBP.toDecimal(), amountGBP.token))
|
||||
val amountJPY = Amount(quantity, JPY)
|
||||
val expectedJPY = BigDecimal("1234")
|
||||
assertEquals(expectedJPY, amountJPY.toDecimal())
|
||||
assertEquals(amountJPY, Amount.fromDecimal(amountJPY.toDecimal(), amountJPY.token))
|
||||
val testAsset = TestAsset("GB0009997999")
|
||||
val amountBond = Amount(quantity, testAsset)
|
||||
val expectedBond = BigDecimal("123400")
|
||||
assertEquals(expectedBond, amountBond.toDecimal())
|
||||
assertEquals(amountBond, Amount.fromDecimal(amountBond.toDecimal(), amountBond.token))
|
||||
}
|
||||
|
||||
data class TestAsset(val name: String) : TokenizableAssetInfo {
|
||||
override val displayTokenSize: BigDecimal = BigDecimal("100")
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -39,4 +58,124 @@ class AmountTests {
|
||||
assertEquals("5000 JPY", Amount.parseCurrency("¥5000").toString())
|
||||
assertEquals("50.12 USD", Amount.parseCurrency("$50.12").toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun split() {
|
||||
for (baseQuantity in 0..1000) {
|
||||
val baseAmount = Amount(baseQuantity.toLong(), GBP)
|
||||
for (partitionCount in 1..100) {
|
||||
val splits = baseAmount.splitEvenly(partitionCount)
|
||||
assertEquals(partitionCount, splits.size)
|
||||
assertEquals(baseAmount, splits.sumOrZero(baseAmount.token))
|
||||
val min = splits.min()!!
|
||||
val max = splits.max()!!
|
||||
assertTrue(max.quantity - min.quantity <= 1L, "Amount quantities should differ by at most one token")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun amountTransfersEquality() {
|
||||
val partyA = "A"
|
||||
val partyB = "B"
|
||||
val partyC = "C"
|
||||
val baseSize = BigDecimal("123.45")
|
||||
val transferA = AmountTransfer.fromDecimal(baseSize, GBP, partyA, partyB)
|
||||
assertEquals(baseSize, transferA.toDecimal())
|
||||
val transferB = AmountTransfer.fromDecimal(baseSize.negate(), GBP, partyB, partyA)
|
||||
assertEquals(baseSize.negate(), transferB.toDecimal())
|
||||
val transferC = AmountTransfer.fromDecimal(BigDecimal("123.40"), GBP, partyA, partyB)
|
||||
val transferD = AmountTransfer.fromDecimal(baseSize, USD, partyA, partyB)
|
||||
val transferE = AmountTransfer.fromDecimal(baseSize, GBP, partyA, partyC)
|
||||
assertEquals(transferA, transferA)
|
||||
assertEquals(transferA.hashCode(), transferA.hashCode())
|
||||
assertEquals(transferA, transferB)
|
||||
assertEquals(transferA.hashCode(), transferB.hashCode())
|
||||
assertNotEquals(transferC, transferA)
|
||||
assertNotEquals(transferC.hashCode(), transferA.hashCode())
|
||||
assertNotEquals(transferD, transferA)
|
||||
assertNotEquals(transferD.hashCode(), transferA.hashCode())
|
||||
assertNotEquals(transferE, transferA)
|
||||
assertNotEquals(transferE.hashCode(), transferA.hashCode())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun amountTransferAggregation() {
|
||||
val partyA = "A"
|
||||
val partyB = "B"
|
||||
val partyC = "C"
|
||||
val baseSize = BigDecimal("123.45")
|
||||
val simpleTransfer = AmountTransfer.fromDecimal(baseSize, GBP, partyA, partyB)
|
||||
val flippedTransfer = AmountTransfer.fromDecimal(baseSize.negate(), GBP, partyB, partyA)
|
||||
val doubleSizeTransfer = AmountTransfer.fromDecimal(baseSize.multiply(BigDecimal("2")), GBP, partyA, partyB)
|
||||
val differentTokenTransfer = AmountTransfer.fromDecimal(baseSize, USD, partyA, partyB)
|
||||
val differentPartyTransfer = AmountTransfer.fromDecimal(baseSize, GBP, partyA, partyC)
|
||||
val negativeTransfer = AmountTransfer.fromDecimal(baseSize.negate(), GBP, partyA, partyB)
|
||||
val zeroTransfer = AmountTransfer.zero(GBP, partyA, partyB)
|
||||
val sumFlipped1 = simpleTransfer + flippedTransfer
|
||||
val sumFlipped2 = flippedTransfer + simpleTransfer
|
||||
assertEquals(doubleSizeTransfer, sumFlipped1)
|
||||
assertEquals(doubleSizeTransfer, sumFlipped2)
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
simpleTransfer + differentTokenTransfer
|
||||
}
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
simpleTransfer + differentPartyTransfer
|
||||
}
|
||||
val sumsToZero = simpleTransfer + negativeTransfer
|
||||
assertEquals(zeroTransfer, sumsToZero)
|
||||
val sumFlippedToZero = flippedTransfer + negativeTransfer
|
||||
assertEquals(zeroTransfer, sumFlippedToZero)
|
||||
val sumUntilNegative = (flippedTransfer + negativeTransfer) + negativeTransfer
|
||||
assertEquals(negativeTransfer, sumUntilNegative)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun amountTransferApply() {
|
||||
val partyA = "A"
|
||||
val partyB = "B"
|
||||
val partyC = "C"
|
||||
val sourceAccounts = listOf(
|
||||
SourceAndAmount(partyA, DOLLARS(123), 1),
|
||||
SourceAndAmount(partyB, DOLLARS(100), 2),
|
||||
SourceAndAmount(partyC, DOLLARS(123), 3),
|
||||
SourceAndAmount(partyB, DOLLARS(100), 4),
|
||||
SourceAndAmount(partyA, POUNDS(256), 5),
|
||||
SourceAndAmount(partyB, POUNDS(256), 6)
|
||||
)
|
||||
val collector = Collectors.toMap<SourceAndAmount<Currency, String>, Pair<String, Currency>, BigDecimal>({ Pair(it.source, it.amount.token)}, {it.amount.toDecimal()}, { x,y -> x + y})
|
||||
val originalTotals = sourceAccounts.stream().collect(collector)
|
||||
|
||||
val smallTransfer = AmountTransfer.fromDecimal(BigDecimal("10"), USD, partyA, partyB)
|
||||
val accountsAfterSmallTransfer = smallTransfer.apply(sourceAccounts, 10)
|
||||
val newTotals = accountsAfterSmallTransfer.stream().collect(collector)
|
||||
assertEquals(originalTotals[Pair(partyA, USD)]!! - BigDecimal("10.00"), newTotals[Pair(partyA, USD)])
|
||||
assertEquals(originalTotals[Pair(partyB, USD)]!! + BigDecimal("10.00"), newTotals[Pair(partyB, USD)])
|
||||
assertEquals(originalTotals[Pair(partyC, USD)], newTotals[Pair(partyC, USD)])
|
||||
assertEquals(originalTotals[Pair(partyA, GBP)], newTotals[Pair(partyA, GBP)])
|
||||
assertEquals(originalTotals[Pair(partyB, GBP)], newTotals[Pair(partyB, GBP)])
|
||||
|
||||
val largeTransfer = AmountTransfer.fromDecimal(BigDecimal("150"), USD, partyB, partyC)
|
||||
val accountsAfterLargeTransfer = largeTransfer.apply(sourceAccounts, 10)
|
||||
val newTotals2 = accountsAfterLargeTransfer.stream().collect(collector)
|
||||
assertEquals(originalTotals[Pair(partyA, USD)], newTotals2[Pair(partyA, USD)])
|
||||
assertEquals(originalTotals[Pair(partyB, USD)]!! - BigDecimal("150.00"), newTotals2[Pair(partyB, USD)])
|
||||
assertEquals(originalTotals[Pair(partyC, USD)]!! + BigDecimal("150.00"), newTotals2[Pair(partyC, USD)])
|
||||
assertEquals(originalTotals[Pair(partyA, GBP)], newTotals2[Pair(partyA, GBP)])
|
||||
assertEquals(originalTotals[Pair(partyB, GBP)], newTotals2[Pair(partyB, GBP)])
|
||||
|
||||
val tooLargeTransfer = AmountTransfer.fromDecimal(BigDecimal("150"), USD, partyA, partyB)
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
tooLargeTransfer.apply(sourceAccounts)
|
||||
}
|
||||
val emptyingTransfer = AmountTransfer.fromDecimal(BigDecimal("123"), USD, partyA, partyB)
|
||||
val accountsAfterEmptyingTransfer = emptyingTransfer.apply(sourceAccounts, 10)
|
||||
val newTotals3 = accountsAfterEmptyingTransfer.stream().collect(collector)
|
||||
assertEquals(null, newTotals3[Pair(partyA, USD)])
|
||||
assertEquals(originalTotals[Pair(partyB, USD)]!! + BigDecimal("123.00"), newTotals3[Pair(partyB, USD)])
|
||||
assertEquals(originalTotals[Pair(partyC, USD)], newTotals3[Pair(partyC, USD)])
|
||||
assertEquals(originalTotals[Pair(partyA, GBP)], newTotals3[Pair(partyA, GBP)])
|
||||
assertEquals(originalTotals[Pair(partyB, GBP)], newTotals3[Pair(partyB, GBP)])
|
||||
|
||||
}
|
||||
}
|
@ -11,7 +11,8 @@ The `Amount <api/net.corda.core.contracts/-amount/index.html>`_ class is used to
|
||||
fungible asset. It is a generic class which wraps around a type used to define the underlying product, called
|
||||
the *token*. For instance it can be the standard JDK type ``Currency``, or an ``Issued`` instance, or this can be
|
||||
a more complex type such as an obligation contract issuance definition (which in turn contains a token definition
|
||||
for whatever the obligation is to be settled in).
|
||||
for whatever the obligation is to be settled in). Custom token types should implement ``TokenizableAssetInfo`` to allow the
|
||||
``Amount`` conversion helpers ``fromDecimal`` and ``toDecimal`` to calculate the correct ``displayTokenSize``.
|
||||
|
||||
.. note:: Fungible is used here to mean that instances of an asset is interchangeable for any other identical instance,
|
||||
and that they can be split/merged. For example a £5 note can reasonably be exchanged for any other £5 note, and
|
||||
@ -30,18 +31,27 @@ Here are some examples:
|
||||
// A quantity of a product governed by specific obligation terms
|
||||
Amount<Obligation.Terms<P>>
|
||||
|
||||
``Amount`` represents quantities as integers. For currencies the quantity represents pennies, cents or whatever
|
||||
else the smallest integer amount for that currency is. You cannot use ``Amount`` to represent negative quantities
|
||||
or fractional quantities: if you wish to do this then you must use a different type e.g. ``BigDecimal``. ``Amount``
|
||||
defines methods to do addition and subtraction and these methods verify that the tokens on both sides of the operator
|
||||
are equal (these are operator overloads in Kotlin and can be used as regular methods from Java). There are also
|
||||
methods to do multiplication and division by integer amounts.
|
||||
``Amount`` represents quantities as integers. You cannot use ``Amount`` to represent negative quantities,
|
||||
or fractional quantities: if you wish to do this then you must use a different type, typically ``BigDecimal``.
|
||||
For currencies the quantity represents pennies, cents, or whatever else is the smallest integer amount for that currency,
|
||||
but for other assets it might mean anything e.g. 1000 tonnes of coal, or kilowatt-hours. The precise conversion ratio
|
||||
to displayable amounts is via the ``displayTokenSize`` property, which is the ``BigDecimal`` numeric representation of
|
||||
a single token as it would be written. ``Amount`` also defines methods to do overflow/underflow checked addition and subtraction
|
||||
(these are operator overloads in Kotlin and can be used as regular methods from Java). More complex calculations should typically
|
||||
be done in ``BigDecimal`` and converted back to ensure due consideration of rounding and to ensure token conservation.
|
||||
|
||||
``Issued`` refers to a product (which can be cash, a cash-like thing, assets, or generally anything else that's
|
||||
quantifiable with integer quantities) and an associated ``PartyAndReference`` that describes the issuer of that contract.
|
||||
An issued product typically follows a lifecycle which includes issuance, movement and exiting from the ledger (for example,
|
||||
see the ``Cash`` contract and its associated *state* and *commands*)
|
||||
|
||||
To represent movements of ``Amount`` tokens use the ``AmountTransfer`` type, which records the quantity and perspective
|
||||
of a transfer. Positive values will indicate a movement of tokens from a ``source`` e.g. a ``Party``, or ``CompositeKey``
|
||||
to a ``destination``. Negative values can be used to indicate a retrograde motion of tokens from ``destination``
|
||||
to ``source``. ``AmountTransfer`` supports addition (as a Kotlin operator, or Java method) to provide netting
|
||||
and aggregation of flows. The ``apply`` method can be used to process a list of attributed ``Amount`` objects in a
|
||||
``List<SourceAndAmount>`` to carry out the actual transfer.
|
||||
|
||||
Financial states
|
||||
----------------
|
||||
In additional to the common state types, a number of interfaces extend ``ContractState`` to model financial state such as:
|
||||
|
@ -205,7 +205,7 @@ class CashTests {
|
||||
// Can't use an issue command to lower the amount.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { inState.copy(amount = inState.amount / 2) }
|
||||
output { inState.copy(amount = inState.amount.splitEvenly(2).first()) }
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
this `fails with` "output values sum to more than the inputs"
|
||||
}
|
||||
@ -232,7 +232,7 @@ class CashTests {
|
||||
this `fails with` "The following commands were not matched at the end of execution"
|
||||
}
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount.splitEvenly(2).first()) }
|
||||
this `fails with` "The following commands were not matched at the end of execution"
|
||||
}
|
||||
this.verifies()
|
||||
@ -265,20 +265,22 @@ class CashTests {
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
tweak {
|
||||
input { inState }
|
||||
for (i in 1..4) output { inState.copy(amount = inState.amount / 4) }
|
||||
val splits4 = inState.amount.splitEvenly(4)
|
||||
for (i in 0..3) output { inState.copy(amount = splits4[i]) }
|
||||
this.verifies()
|
||||
}
|
||||
// Merging 4 inputs into 2 outputs works.
|
||||
tweak {
|
||||
for (i in 1..4) input { inState.copy(amount = inState.amount / 4) }
|
||||
output { inState.copy(amount = inState.amount / 2) }
|
||||
output { inState.copy(amount = inState.amount / 2) }
|
||||
val splits2 = inState.amount.splitEvenly(2)
|
||||
val splits4 = inState.amount.splitEvenly(4)
|
||||
for (i in 0..3) input { inState.copy(amount = splits4[i]) }
|
||||
for (i in 0..1) output { inState.copy(amount = splits2[i]) }
|
||||
this.verifies()
|
||||
}
|
||||
// Merging 2 inputs into 1 works.
|
||||
tweak {
|
||||
input { inState.copy(amount = inState.amount / 2) }
|
||||
input { inState.copy(amount = inState.amount / 2) }
|
||||
val splits2 = inState.amount.splitEvenly(2)
|
||||
for (i in 0..1) input { inState.copy(amount = splits2[i]) }
|
||||
output { inState }
|
||||
this.verifies()
|
||||
}
|
||||
@ -310,9 +312,9 @@ class CashTests {
|
||||
}
|
||||
// Can't change deposit reference when splitting.
|
||||
transaction {
|
||||
val splits2 = inState.amount.splitEvenly(2)
|
||||
input { inState }
|
||||
output { outState.copy(amount = inState.amount / 2).editDepositRef(0) }
|
||||
output { outState.copy(amount = inState.amount / 2).editDepositRef(1) }
|
||||
for (i in 0..1) output { outState.copy(amount = splits2[i]).editDepositRef(i.toByte()) }
|
||||
this `fails with` "the amounts balance"
|
||||
}
|
||||
// Can't mix currencies.
|
||||
@ -513,7 +515,7 @@ class CashTests {
|
||||
val wtx = makeExit(50.DOLLARS, MEGA_CORP, 1)
|
||||
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
||||
assertEquals(1, wtx.outputs.size)
|
||||
assertEquals(WALLET[0].state.data.copy(amount = WALLET[0].state.data.amount / 2), wtx.outputs[0].data)
|
||||
assertEquals(WALLET[0].state.data.copy(amount = WALLET[0].state.data.amount.splitEvenly(2).first()), wtx.outputs[0].data)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -183,7 +183,7 @@ class ObligationTests {
|
||||
this `fails with` "The following commands were not matched at the end of execution"
|
||||
}
|
||||
tweak {
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.amount / 2) }
|
||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.amount.splitEvenly(2).first()) }
|
||||
this `fails with` "The following commands were not matched at the end of execution"
|
||||
}
|
||||
this.verifies()
|
||||
@ -368,7 +368,7 @@ class ObligationTests {
|
||||
transaction("Issuance") {
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
output("change") { (oneMillionDollars / 2).OBLIGATION between Pair(ALICE, BOB_PUBKEY) }
|
||||
output("change") { (oneMillionDollars.splitEvenly(2).first()).OBLIGATION between Pair(ALICE, BOB_PUBKEY) }
|
||||
command(BOB_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "amounts owed on input and output must match"
|
||||
|
@ -12,6 +12,6 @@ import java.util.*
|
||||
object AmountFormatter {
|
||||
// TODO replace this once we settled on how we do formatting
|
||||
val boring = object : Formatter<Amount<Currency>> {
|
||||
override fun format(value: Amount<Currency>) = "${value.quantity} ${value.token}"
|
||||
override fun format(value: Amount<Currency>) = value.toString()
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import net.corda.client.jfx.utils.isNotNull
|
||||
import net.corda.client.jfx.utils.map
|
||||
import net.corda.client.jfx.utils.unique
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.sumOrNull
|
||||
import net.corda.core.contracts.withoutIssuer
|
||||
import net.corda.core.crypto.AbstractParty
|
||||
import net.corda.core.crypto.Party
|
||||
@ -144,10 +145,10 @@ class NewTransaction : Fragment() {
|
||||
when (it) {
|
||||
executeButton -> when (transactionTypeCB.value) {
|
||||
CashTransaction.Issue -> {
|
||||
CashFlowCommand.IssueCash(Amount(amount.value, currencyChoiceBox.value), issueRef, partyBChoiceBox.value.legalIdentity, notaries.first().notaryIdentity)
|
||||
CashFlowCommand.IssueCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), issueRef, partyBChoiceBox.value.legalIdentity, notaries.first().notaryIdentity)
|
||||
}
|
||||
CashTransaction.Pay -> CashFlowCommand.PayCash(Amount(amount.value, currencyChoiceBox.value), partyBChoiceBox.value.legalIdentity)
|
||||
CashTransaction.Exit -> CashFlowCommand.ExitCash(Amount(amount.value, currencyChoiceBox.value), issueRef)
|
||||
CashTransaction.Pay -> CashFlowCommand.PayCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), partyBChoiceBox.value.legalIdentity)
|
||||
CashTransaction.Exit -> CashFlowCommand.ExitCash(Amount.fromDecimal(amount.value, currencyChoiceBox.value), issueRef)
|
||||
else -> null
|
||||
}
|
||||
else -> null
|
||||
@ -208,8 +209,8 @@ class NewTransaction : Fragment() {
|
||||
availableAmount.textProperty()
|
||||
.bind(Bindings.createStringBinding({
|
||||
val filteredCash = cash.filtered { it.token.issuer.party as AbstractParty == issuer.value && it.token.product == currencyChoiceBox.value }
|
||||
.map { it.withoutIssuer().quantity }
|
||||
"${filteredCash.sum()} ${currencyChoiceBox.value?.currencyCode} Available"
|
||||
.map { it.withoutIssuer() }.sumOrNull()
|
||||
"${filteredCash ?: "None"} Available"
|
||||
}, arrayOf(currencyChoiceBox.valueProperty(), issuerChoiceBox.valueProperty())))
|
||||
// Amount
|
||||
amountLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
|
Loading…
Reference in New Issue
Block a user