Move some extension methods for summing to new locations.

This improves the Java API and makes it more idiomatic. The methods
were not moved to be static methods of the relevant types in all cases
due to a bad interaction with a Kotlin auto-completion bug, and because
static methods on interfaces are new in Java 8 and Kotlin is not yet
emitting Java 8 bytecode.

Also, introduce a packages.md file so packages can be documented.
This commit is contained in:
Mike Hearn 2017-08-17 13:12:20 +02:00
parent ec83735ea3
commit d22cdac2dd
22 changed files with 195 additions and 121 deletions

View File

@ -13,6 +13,7 @@ import java.util.*
* indicative/displayed asset amounts in [BigDecimal] to fungible tokens represented by Amount objects.
*/
interface TokenizableAssetInfo {
/** The nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero. */
val displayTokenSize: BigDecimal
}
@ -28,16 +29,14 @@ interface TokenizableAssetInfo {
* multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer
* overflow.
*
* @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.
* @property quantity the number of tokens as a Long value.
* @property displayTokenSize the nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero.
* @property token an instance of type T, usually a singleton.
* @param T the type of the token, for example [Currency]. T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required.
*/
@CordaSerializable
data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal, val token: T) : Comparable<Amount<T>> {
// TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application.
companion object {
/**
* Build an Amount from a decimal representation. For example, with an input of "12.34 GBP",
@ -73,6 +72,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
* For other possible token types the asset token should implement TokenizableAssetInfo to
* correctly report the designed nominal amount.
*/
@JvmStatic
fun getDisplayTokenSize(token: Any): BigDecimal {
if (token is TokenizableAssetInfo) {
return token.displayTokenSize
@ -85,6 +85,28 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
}
return BigDecimal.ONE
}
/**
* If the given iterable of [Amount]s yields any elements, sum them, throwing an [IllegalArgumentException] if
* any of the token types are mismatched; if the iterator yields no elements, return null.
*/
@JvmStatic
fun <T : Any> Iterable<Amount<T>>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
/**
* Sums the amounts yielded by the given iterable, throwing an [IllegalArgumentException] if any of the token
* types are mismatched.
*/
@JvmStatic
fun <T : Any> Iterable<Amount<T>>.sumOrThrow() = reduce { left, right -> left + right }
/**
* If the given iterable of [Amount]s yields any elements, sum them, throwing an [IllegalArgumentException] if
* any of the token types are mismatched; if the iterator yields no elements, return a zero amount of the given
* token type.
*/
@JvmStatic
fun <T : Any> Iterable<Amount<T>>.sumOrZero(token: T) = if (iterator().hasNext()) sumOrThrow() else Amount.zero(token)
}
init {
@ -106,8 +128,9 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
/**
* A checked addition operator is supported to simplify aggregation of Amounts.
* Mixing non-identical token types will throw [IllegalArgumentException].
*
* @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)
@ -117,8 +140,9 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
/**
* A checked addition operator is supported to simplify netting of Amounts.
* If this leads to the Amount going negative this will throw [IllegalArgumentException].
* Mixing non-identical token types 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)
@ -137,6 +161,11 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
*/
operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(quantity, other), displayTokenSize, 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: Int): Amount<T> = Amount(Math.multiplyExact(quantity, other.toLong()), displayTokenSize, token)
/**
@ -159,7 +188,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
* 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
* @see Amount.fromDecimal
*/
fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantity, 0) * displayTokenSize
@ -171,29 +200,27 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
* 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
* @see Amount.fromDecimal
*/
override fun toString(): String {
return toDecimal().toPlainString() + " " + token
}
/** @suppress */
override fun compareTo(other: Amount<T>): Int {
checkToken(other)
return quantity.compareTo(other.quantity)
}
}
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(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.
*
* @param P Any class type that can disambiguate where the amount came from.
* @param T The token type of the underlying [Amount].
* @property source the holder of the Amount.
* @property amount the Amount of asset available.
* @property 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.
@ -203,17 +230,17 @@ data class SourceAndAmount<T : Any, out P : Any>(val source: P, val amount: Amou
/**
* 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
* @property 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], [CompositeKey], or other identifier of the token source if quantityDelta is positive,
* @property token represents the type of asset token as would be used to construct Amount<T> objects.
* @property source is the [Party], [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], [CompositeKey], or other identifier of the token sink if quantityDelta is positive,
* @property destination is the [Party], [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
@ -245,9 +272,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
return AmountTransfer(deltaTokenCount, token, source, destination)
}
/**
* Helper to make a zero size AmountTransfer
*/
/** Helper to make a zero size AmountTransfer. */
@JvmStatic
fun <T : Any, P : Any> zero(token: T,
source: P,
@ -284,6 +309,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
*/
fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantityDelta, 0) * Amount.getDisplayTokenSize(token)
/** @suppress */
fun copy(quantityDelta: Long = this.quantityDelta,
token: T = this.token,
source: P = this.source,
@ -313,7 +339,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
}
/**
* HashCode ensures that reversed source and destination equivalents will hash to the same value.
* This hash code function 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
@ -322,18 +348,20 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
return result
}
/** @suppress */
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.
* Returns a list of 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. 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 a list of 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.
*/
@Suppress("UNUSED")
fun novate(centralParty: P): List<AmountTransfer<T, P>> = listOf(copy(destination = centralParty), copy(source = centralParty))
/**
@ -343,7 +371,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
* @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
* @return 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.
*/

View File

@ -3,6 +3,8 @@ package net.corda.core.contracts
import net.corda.core.flows.FlowException
import net.corda.core.identity.AbstractParty
import java.security.PublicKey
import net.corda.core.contracts.Amount.Companion.sumOrNull
import net.corda.core.contracts.Amount.Companion.sumOrZero
class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException("Insufficient balance, missing $amountMissing")
@ -51,12 +53,3 @@ interface FungibleAsset<T : Any> : OwnableState {
}
}
}
// Small DSL extensions.
/** Sums the asset states in the list, returning null if there are none. */
fun <T : Any> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrNull()
/** Sums the asset states in the list, returning zero of the given token if there are none. */
fun <T : Any> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrZero(token)

View File

@ -1,6 +1,7 @@
package net.corda.core.contracts
import net.corda.finance.*
import net.corda.core.contracts.Amount.Companion.sumOrZero
import org.junit.Test
import java.math.BigDecimal
import java.util.*

View File

@ -18,6 +18,7 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/javadoc")
processConfigurations = ['compile']
sourceDirs = files('../core/src/main/kotlin', '../client/jfx/src/main/kotlin', '../client/mock/src/main/kotlin', '../client/rpc/src/main/kotlin', '../node/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin')
includes = ['packages.md']
}
task buildDocs(dependsOn: ['apidocs', 'makeDocs'])

3
docs/packages.md Normal file
View File

@ -0,0 +1,3 @@
# Package net.corda.finance.utils
A collection of utilities for summing financial states, for example, summing obligations to get total debts.

View File

@ -45,6 +45,12 @@ UNRELEASED
* Remove `IssuerFlow` as it allowed nodes to request arbitrary amounts of cash to be issued from any remote node. Use
`CashIssueFlow` instead.
* Some utility/extension functions (``sumOrThrow``, ``sumOrNull``, ``sumOrZero`` on ``Amount`` and ``Commodity``)
have moved to be static methods on the classes themselves. This improves the API for Java users who no longer
have to see or known about file-level FooKt style classes generated by the Kotlin compile, but means that IntelliJ
no longer auto-suggests these extension functions in completion unless you add import lines for them yourself
(this is Kotlin IDE bug KT-15286).
Milestone 14
------------

View File

@ -3,10 +3,8 @@ package net.corda.contracts;
import co.paralleluniverse.fibers.Suspendable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import kotlin.Pair;
import kotlin.Unit;
import net.corda.contracts.asset.Cash;
import net.corda.contracts.asset.CashKt;
import kotlin.*;
import net.corda.contracts.asset.*;
import net.corda.core.contracts.*;
import net.corda.core.crypto.SecureHash;
import net.corda.core.crypto.testing.NullPublicKey;
@ -16,6 +14,7 @@ import net.corda.core.identity.Party;
import net.corda.core.node.ServiceHub;
import net.corda.core.transactions.LedgerTransaction;
import net.corda.core.transactions.TransactionBuilder;
import net.corda.finance.utils.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -207,7 +206,7 @@ public class JavaCommercialPaper implements Contract {
final Instant time = null == timeWindow
? null
: timeWindow.getUntilTime();
final Amount<Issued<Currency>> received = CashKt.sumCashBy(tx.getOutputs().stream().map(TransactionState::getData).collect(Collectors.toList()), input.getOwner());
final Amount<Issued<Currency>> received = StateSumming.sumCashBy(tx.getOutputStates(), input.getOwner());
requireThat(require -> {
require.using("must be timestamped", timeWindow != null);

View File

@ -2,7 +2,6 @@ package net.corda.contracts
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.sumCashBy
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.testing.NULL_PARTY
@ -16,6 +15,7 @@ import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.QueryableState
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.finance.utils.sumCashBy
import net.corda.schemas.CommercialPaperSchemaV1
import java.time.Instant
import java.util.*
@ -88,6 +88,9 @@ class CommercialPaper : Contract {
else -> throw IllegalArgumentException("Unrecognised schema $schema")
}
}
/** @suppress */ infix fun `owned by`(owner: AbstractParty) = copy(owner = owner)
/** @suppress */ infix fun `with notary`(notary: Party) = TransactionState(this, notary)
}
interface Commands : CommandData {
@ -191,8 +194,4 @@ class CommercialPaper : Contract {
tx.addInputState(paper)
tx.addCommand(Commands.Redeem(), paper.state.data.owner.owningKey)
}
}
infix fun CommercialPaper.State.`owned by`(owner: AbstractParty) = copy(owner = owner)
infix fun CommercialPaper.State.`with notary`(notary: Party) = TransactionState(this, notary)
infix fun ICommercialPaperState.`owned by`(newOwner: AbstractParty) = withOwner(newOwner)
}

View File

@ -1,3 +1,4 @@
@file:JvmName("CashUtilities") // So the static extension functions get put into a class with a better name than CashKt
package net.corda.contracts.asset
import co.paralleluniverse.fibers.Suspendable
@ -25,6 +26,9 @@ import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.toHexString
import net.corda.core.utilities.toNonEmptySet
import net.corda.core.utilities.trace
import net.corda.finance.utils.sumCash
import net.corda.finance.utils.sumCashOrNull
import net.corda.finance.utils.sumCashOrZero
import net.corda.schemas.CashSchemaV1
import org.bouncycastle.asn1.x500.X500Name
import java.math.BigInteger
@ -94,6 +98,10 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
override fun toString() = "${Emoji.bagOfCash}Cash($amount at ${amount.token.issuer} owned by $owner)"
override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner))
fun ownedBy(owner: AbstractParty) = copy(owner = owner)
fun issuedBy(party: AbstractParty) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = amount.token.issuer.copy(party = party))))
fun issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = deposit)))
fun withDeposit(deposit: PartyAndReference): Cash.State = copy(amount = amount.copy(token = amount.token.copy(issuer = deposit)))
/** Object Relational Mapping support. */
override fun generateMappedObject(schema: MappedSchema): PersistentState {
@ -278,7 +286,7 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
* otherwise the set of eligible states wil be filtered to only include those from these issuers.
* @param notary If null the notary source is ignored, if specified then only states marked
* with this notary are included.
* @param lockId The [FlowLogic.runId.uuid] of the flow, which is used to soft reserve the states.
* @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states.
* Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes.
* @param withIssuerRefs If not empty the specific set of issuer references to match against.
* @return The matching states that were found. If sufficient funds were found these will be locked,
@ -387,36 +395,10 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
// Small DSL extensions.
/**
* Sums the cash states in the list belonging to a single owner, throwing an exception
* if there are none, or if any of the cash states cannot be added together (i.e. are
* different currencies or issuers).
*/
fun Iterable<ContractState>.sumCashBy(owner: AbstractParty): Amount<Issued<Currency>> = filterIsInstance<Cash.State>().filter { it.owner == owner }.map { it.amount }.sumOrThrow()
/**
* Sums the cash states in the list, throwing an exception if there are none, or if any of the cash
* states cannot be added together (i.e. are different currencies or issuers).
*/
fun Iterable<ContractState>.sumCash(): Amount<Issued<Currency>> = filterIsInstance<Cash.State>().map { it.amount }.sumOrThrow()
/** Sums the cash states in the list, returning null if there are none. */
fun Iterable<ContractState>.sumCashOrNull(): Amount<Issued<Currency>>? = filterIsInstance<Cash.State>().map { it.amount }.sumOrNull()
/** Sums the cash states in the list, returning zero of the given currency+issuer if there are none. */
fun Iterable<ContractState>.sumCashOrZero(currency: Issued<Currency>): Amount<Issued<Currency>> {
return filterIsInstance<Cash.State>().map { it.amount }.sumOrZero(currency)
}
fun Cash.State.ownedBy(owner: AbstractParty) = copy(owner = owner)
fun Cash.State.issuedBy(party: AbstractParty) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = amount.token.issuer.copy(party = party))))
fun Cash.State.issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = deposit)))
fun Cash.State.withDeposit(deposit: PartyAndReference): Cash.State = copy(amount = amount.copy(token = amount.token.copy(issuer = deposit)))
infix fun Cash.State.`owned by`(owner: AbstractParty) = ownedBy(owner)
infix fun Cash.State.`issued by`(party: AbstractParty) = issuedBy(party)
infix fun Cash.State.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = withDeposit(deposit)
/** @suppress */ infix fun Cash.State.`owned by`(owner: AbstractParty) = ownedBy(owner)
/** @suppress */ infix fun Cash.State.`issued by`(party: AbstractParty) = issuedBy(party)
/** @suppress */ infix fun Cash.State.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
/** @suppress */ infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = withDeposit(deposit)
// Unit testing helpers. These could go in a separate file but it's hardly worth it for just a few functions.

View File

@ -9,6 +9,9 @@ import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.finance.utils.sumCommodities
import net.corda.finance.utils.sumCommoditiesOrNull
import net.corda.finance.utils.sumCommoditiesOrZero
import java.util.*
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -178,16 +181,4 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, C
override fun generateExitCommand(amount: Amount<Issued<Commodity>>) = Commands.Exit(amount)
override fun generateIssueCommand() = Commands.Issue()
override fun generateMoveCommand() = Commands.Move()
}
/**
* Sums the cash states in the list, throwing an exception if there are none, or if any of the cash
* states cannot be added together (i.e. are different currencies).
*/
fun Iterable<ContractState>.sumCommodities() = filterIsInstance<CommodityContract.State>().map { it.amount }.sumOrThrow()
/** Sums the cash states in the list, returning null if there are none. */
@Suppress("unused") fun Iterable<ContractState>.sumCommoditiesOrNull() = filterIsInstance<CommodityContract.State>().map { it.amount }.sumOrNull()
/** Sums the cash states in the list, returning zero of the given currency if there are none. */
fun Iterable<ContractState>.sumCommoditiesOrZero(currency: Issued<Commodity>) = filterIsInstance<CommodityContract.State>().map { it.amount }.sumOrZero(currency)
}

View File

@ -18,6 +18,10 @@ import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.seconds
import net.corda.finance.utils.sumFungibleOrNull
import net.corda.finance.utils.sumObligations
import net.corda.finance.utils.sumObligationsOrNull
import net.corda.finance.utils.sumObligationsOrZero
import org.bouncycastle.asn1.x500.X500Name
import java.math.BigInteger
import java.security.PublicKey
@ -71,7 +75,6 @@ val OBLIGATION_PROGRAM_ID = Obligation<Currency>()
* @param P the product the obligation is for payment of.
*/
class Obligation<P : Any> : Contract {
/**
* TODO:
* 1) hash should be of the contents, not the URI
@ -792,18 +795,6 @@ fun <P : AbstractParty, T : Any> sumAmountsDue(balances: Map<Pair<P, P>, Amount<
return sum
}
/** Sums the obligation states in the list, throwing an exception if there are none. All state objects in the list are presumed to be nettable. */
fun <P : Any> Iterable<ContractState>.sumObligations(): Amount<Issued<Obligation.Terms<P>>>
= filterIsInstance<Obligation.State<P>>().map { it.amount }.sumOrThrow()
/** Sums the obligation states in the list, returning null if there are none. */
fun <P : Any> Iterable<ContractState>.sumObligationsOrNull(): Amount<Issued<Obligation.Terms<P>>>?
= filterIsInstance<Obligation.State<P>>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrNull()
/** Sums the obligation states in the list, returning zero of the given product if there are none. */
fun <P : Any> Iterable<ContractState>.sumObligationsOrZero(issuanceDef: Issued<Obligation.Terms<P>>): Amount<Issued<Obligation.Terms<P>>>
= filterIsInstance<Obligation.State<P>>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(issuanceDef)
infix fun <T : Any> Obligation.State<T>.at(dueBefore: Instant) = copy(template = template.copy(dueBefore = dueBefore))
infix fun <T : Any> Obligation.State<T>.between(parties: Pair<AbstractParty, AbstractParty>) = copy(obligor = parties.first, beneficiary = parties.second)
infix fun <T : Any> Obligation.State<T>.`owned by`(owner: AbstractParty) = copy(beneficiary = owner)

View File

@ -1,6 +1,7 @@
package net.corda.contracts.asset
import net.corda.core.contracts.*
import net.corda.core.contracts.Amount.Companion.sumOrThrow
import net.corda.core.identity.AbstractParty
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.loggerFor

View File

@ -0,0 +1,73 @@
@file:JvmName("StateSumming")
package net.corda.finance.utils
import net.corda.contracts.Commodity
import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.CommodityContract
import net.corda.contracts.asset.Obligation
import net.corda.core.contracts.Amount
import net.corda.core.contracts.Amount.Companion.sumOrNull
import net.corda.core.contracts.Amount.Companion.sumOrThrow
import net.corda.core.contracts.Amount.Companion.sumOrZero
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.FungibleAsset
import net.corda.core.contracts.Issued
import net.corda.core.identity.AbstractParty
import java.util.*
/** A collection of utilities for summing states */
/**
* Sums the cash states in the list belonging to a single owner, throwing an exception
* if there are none, or if any of the cash states cannot be added together (i.e. are
* different currencies or issuers).
*/
fun Iterable<ContractState>.sumCashBy(owner: AbstractParty): Amount<Issued<Currency>> = filterIsInstance<Cash.State>().filter { it.owner == owner }.map { it.amount }.sumOrThrow()
/**
* Sums the cash states in the list, throwing an exception if there are none, or if any of the cash
* states cannot be added together (i.e. are different currencies or issuers).
*/
fun Iterable<ContractState>.sumCash(): Amount<Issued<Currency>> = filterIsInstance<Cash.State>().map { it.amount }.sumOrThrow()
/** Sums the cash states in the list, returning null if there are none. */
fun Iterable<ContractState>.sumCashOrNull(): Amount<Issued<Currency>>? = filterIsInstance<Cash.State>().map { it.amount }.sumOrNull()
/** Sums the cash states in the list, returning zero of the given currency+issuer if there are none. */
fun Iterable<ContractState>.sumCashOrZero(currency: Issued<Currency>): Amount<Issued<Currency>> {
return filterIsInstance<Cash.State>().map { it.amount }.sumOrZero(currency)
}
/** Sums the asset states in the list, returning null if there are none. */
fun <T : Any> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrNull()
/** Sums the asset states in the list, returning zero of the given token if there are none. */
fun <T : Any> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrZero(token)
/**
* Sums the cash states in the list, throwing an exception if there are none, or if any of the cash
* states cannot be added together (i.e. are different currencies).
*/
fun Iterable<ContractState>.sumCommodities() = filterIsInstance<CommodityContract.State>().map { it.amount }.sumOrThrow()
/** Sums the cash states in the list, returning null if there are none. */
@Suppress("unused")
fun Iterable<ContractState>.sumCommoditiesOrNull() = filterIsInstance<CommodityContract.State>().map { it.amount }.sumOrNull()
/** Sums the cash states in the list, returning zero of the given currency if there are none. */
fun Iterable<ContractState>.sumCommoditiesOrZero(currency: Issued<Commodity>) = filterIsInstance<CommodityContract.State>().map { it.amount }.sumOrZero(currency)
/**
* Sums the obligation states in the list, throwing an exception if there are none. All state objects in the
* list are presumed to be nettable.
*/
fun <P : Any> Iterable<ContractState>.sumObligations(): Amount<Issued<Obligation.Terms<P>>>
= filterIsInstance<Obligation.State<P>>().map { it.amount }.sumOrThrow()
/** Sums the obligation states in the list, returning null if there are none. */
fun <P : Any> Iterable<ContractState>.sumObligationsOrNull(): Amount<Issued<Obligation.Terms<P>>>?
= filterIsInstance<Obligation.State<P>>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrNull()
/** Sums the obligation states in the list, returning zero of the given product if there are none. */
fun <P : Any> Iterable<ContractState>.sumObligationsOrZero(issuanceDef: Issued<Obligation.Terms<P>>): Amount<Issued<Obligation.Terms<P>>>
= filterIsInstance<Obligation.State<P>>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(issuanceDef)

View File

@ -2,7 +2,6 @@ package net.corda.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.sumCashBy
import net.corda.core.contracts.Amount
import net.corda.core.contracts.OwnableState
import net.corda.core.contracts.StateAndRef
@ -18,6 +17,7 @@ import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.seconds
import net.corda.core.utilities.unwrap
import net.corda.finance.utils.sumCashBy
import java.security.PublicKey
import java.util.*

View File

@ -45,7 +45,7 @@ public class CashTestsJava {
tw.output(outState);
// issuedBy() can't be directly imported because it conflicts with other identically named functions
// with different overloads (for some reason).
tw.output(CashKt.issuedBy(outState, getMINI_CORP()));
tw.output(outState.issuedBy(getMINI_CORP()));
tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move());
return tw.failsWith("at least one cash input");
});

View File

@ -107,7 +107,7 @@ class CommercialPaperTestsGeneric {
input("paper")
input("alice's $900")
output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP }
output("alice's paper") { "paper".output<ICommercialPaperState>() `owned by` ALICE }
output("alice's paper") { "paper".output<ICommercialPaperState>().withOwner(ALICE) }
command(ALICE_PUBKEY) { Cash.Commands.Move() }
command(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() }
this.verifies()

View File

@ -12,6 +12,9 @@ import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.QueryableState
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.finance.utils.sumCash
import net.corda.finance.utils.sumCashOrNull
import net.corda.finance.utils.sumCashOrZero
import net.corda.schemas.SampleCashSchemaV1
import net.corda.schemas.SampleCashSchemaV2
import net.corda.schemas.SampleCashSchemaV3

View File

@ -12,6 +12,10 @@ import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.finance.*
import net.corda.finance.utils.sumCash
import net.corda.finance.utils.sumCashBy
import net.corda.finance.utils.sumCashOrNull
import net.corda.finance.utils.sumCashOrZero
import net.corda.node.services.vault.NodeVaultService
import net.corda.node.utilities.CordaPersistence
import net.corda.testing.*

View File

@ -3,7 +3,7 @@ package net.corda.node.services.vault;
import com.google.common.collect.ImmutableSet;
import kotlin.Pair;
import net.corda.contracts.DealState;
import net.corda.contracts.asset.Cash;
import net.corda.contracts.asset.*;
import net.corda.core.contracts.*;
import net.corda.core.crypto.EncodingUtils;
import net.corda.core.identity.AbstractParty;
@ -34,13 +34,12 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER;
import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY;
import static net.corda.core.node.services.vault.QueryCriteriaUtils.DEFAULT_PAGE_NUM;
import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZE;
import static net.corda.core.utilities.ByteArrays.toHexString;
import static net.corda.testing.CoreTestUtils.*;
import static net.corda.testing.TestConstants.*;
import static net.corda.contracts.asset.CashUtilities.*;
import static net.corda.testing.node.MockServicesKt.makeTestDatabaseAndMockServices;
import static net.corda.testing.node.MockServicesKt.makeTestIdentityService;
import static org.assertj.core.api.Assertions.assertThat;
@ -57,7 +56,6 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
ArrayList<KeyPair> keys = new ArrayList<>();
keys.add(getMEGA_CORP_KEY());
keys.add(getDUMMY_NOTARY_KEY());
IdentityService identitySvc = makeTestIdentityService();
Pair<CordaPersistence, MockServices> databaseAndServices = makeTestDatabaseAndMockServices(Collections.EMPTY_SET, keys, () -> identitySvc);
issuerServices = new MockServices(getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY());
@ -127,7 +125,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
Amount<Currency> amount = new Amount<>(100, Currency.getInstance("USD"));
VaultFiller.fillWithSomeTestCash(services,
new Amount<>(100, Currency.getInstance("USD")),
new Amount<Currency>(100, Currency.getInstance("USD")),
issuerServices,
TestConstants.getDUMMY_NOTARY(),
3,
@ -135,7 +133,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
new Random(),
new OpaqueBytes("1".getBytes()),
null,
getDUMMY_CASH_ISSUER());
CashUtilities.getDUMMY_CASH_ISSUER());
VaultFiller.consumeCash(services, amount, getDUMMY_NOTARY());

View File

@ -13,6 +13,7 @@ import net.corda.core.transactions.SignedTransaction
import net.corda.finance.DOLLARS
import net.corda.finance.POUNDS
import net.corda.finance.SWISS_FRANCS
import net.corda.finance.utils.sumCash
import net.corda.node.services.schema.HibernateObserver
import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.services.vault.VaultSchemaV1

View File

@ -4,7 +4,6 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY
import net.corda.contracts.asset.sumCash
import net.corda.contracts.getCashBalance
import net.corda.core.contracts.*
import net.corda.core.crypto.generateKeyPair
@ -21,6 +20,7 @@ import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.toNonEmptySet
import net.corda.finance.*
import net.corda.finance.utils.sumCash
import net.corda.node.utilities.CordaPersistence
import net.corda.testing.*
import net.corda.testing.contracts.fillWithSomeTestCash

View File

@ -18,7 +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.Amount.Companion.sumOrNull
import net.corda.core.contracts.withoutIssuer
import net.corda.core.flows.FlowException
import net.corda.core.identity.Party