State and Contract for Cash and CommercialPaper copied to perftestflows

This commit is contained in:
Christian Sailer 2017-10-10 13:10:21 +01:00
parent e22570a81d
commit 8ae92850c9
9 changed files with 1138 additions and 0 deletions

View File

@ -0,0 +1,43 @@
apply plugin: 'kotlin'
// Java Persistence API support: create no-arg constructor
// see: http://stackoverflow.com/questions/32038177/kotlin-with-jpa-default-constructor-hell
apply plugin: 'kotlin-jpa'
apply plugin: CanonicalizerPlugin
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'net.corda.plugins.cordformation'
//apply plugin: 'com.jfrog.artifactory'
description 'Corda performance test modules'
dependencies {
// Note the :perftestflows module is a CorDapp in its own right
// and CorDapps using :perftestflows features should use 'cordapp' not 'compile' linkage.
cordaCompile project(':core')
cordaCompile project(':confidential-identities')
testCompile project(':test-utils')
testCompile project(path: ':core', configuration: 'testArtifacts')
testCompile "junit:junit:$junit_version"
}
configurations {
testArtifacts.extendsFrom testRuntime
}
task testJar(type: Jar) {
classifier "tests"
from sourceSets.test.output
}
artifacts {
testArtifacts testJar
}
jar {
baseName 'corda-ptflows'
}
publish {
name jar.baseName
}

View File

@ -0,0 +1,23 @@
package net.corda.ptflows.contracts;
import net.corda.core.contracts.Amount;
import net.corda.core.contracts.ContractState;
import net.corda.core.contracts.Issued;
import net.corda.core.identity.AbstractParty;
import java.time.Instant;
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.
* Normally one would not duplicate an implementation in different languages for obvious reasons, but it demonstrates that
* ultimately either language can be used against a common test framework (and therefore can be used for real).
*/
public interface IPtCommercialPaperState extends ContractState {
IPtCommercialPaperState withOwner(AbstractParty newOwner);
IPtCommercialPaperState withFaceValue(Amount<Issued<Currency>> newFaceValue);
IPtCommercialPaperState withMaturityDate(Instant newMaturityDate);
}

View File

@ -0,0 +1,197 @@
package net.corda.ptflows.contracts
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.*
import net.corda.core.crypto.NullKeys.NULL_PARTY
import net.corda.core.utilities.toBase58String
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.Emoji
import net.corda.core.node.ServiceHub
import net.corda.core.schemas.MappedSchema
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.ptflows.contracts.asset.PtCash
import net.corda.ptflows.schemas.PtCommercialPaperSchemaV1
import net.corda.ptflows.utils.sumCashBy
import java.time.Instant
import java.util.*
/**
* This is an ultra-trivial implementation of commercial paper, which is essentially a simpler version of a corporate
* bond. It can be seen as a company-specific currency. A company issues CP with a particular face value, say $100,
* but sells it for less, say $90. The paper can be redeemed for cash at a given date in the future. Thus this example
* would have a 10% interest rate with a single repayment. Commercial paper is often rolled over (the maturity date
* is adjusted as if the paper was redeemed and immediately repurchased, but without having to front the cash).
*
* This contract is not intended to realistically model CP. It is here only to act as a next step up above cash in
* the prototyping phase. It is thus very incomplete.
*
* Open issues:
* - In this model, you cannot merge or split CP. Can you do this normally? We could model CP as a specialised form
* of cash, or reuse some of the cash code? Waiting on response from Ayoub and Rajar about whether CP can always
* be split/merged or only in secondary markets. Even if current systems can't do this, would it be a desirable
* feature to have anyway?
* - The funding steps of CP is totally ignored in this model.
* - No attention is paid to the existing roles of custodians, funding banks, etc.
* - There are regional variations on the CP concept, for instance, American CP requires a special "CUSIP number"
* which may need to be tracked. That, in turn, requires validation logic (there is a bean validator that knows how
* to do this in the Apache BVal project).
*/
val CP_PROGRAM_ID = "net.corda.ptflows.contracts.PtCommercialPaper"
// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance.
class PtCommercialPaper : Contract {
companion object {
const val CP_PROGRAM_ID: ContractClassName = "net.corda.ptflows.contracts.PtCommercialPaper"
}
data class State(
val issuance: PartyAndReference,
override val owner: AbstractParty,
val faceValue: Amount<Issued<Currency>>,
val maturityDate: Instant
) : OwnableState, QueryableState, IPtCommercialPaperState{
override val participants = listOf(owner)
override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner))
fun withoutOwner() = copy(owner = NULL_PARTY)
override fun toString() = "${Emoji.newspaper}CommercialPaper(of $faceValue redeemable on $maturityDate by '$issuance', owned by $owner)"
// Although kotlin is smart enough not to need these, as we are using the ICommercialPaperState, we need to declare them explicitly for use later,
override fun withOwner(newOwner: AbstractParty): IPtCommercialPaperState = copy(owner = newOwner)
override fun withFaceValue(newFaceValue: Amount<Issued<Currency>>): IPtCommercialPaperState = copy(faceValue = newFaceValue)
override fun withMaturityDate(newMaturityDate: Instant): IPtCommercialPaperState = copy(maturityDate = newMaturityDate)
/** Object Relational Mapping support. */
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(PtCommercialPaperSchemaV1)
/** Additional used schemas would be added here (eg. CommercialPaperV2, ...) */
/** Object Relational Mapping support. */
override fun generateMappedObject(schema: MappedSchema): PersistentState {
return when (schema) {
is PtCommercialPaperSchemaV1 -> PtCommercialPaperSchemaV1.PersistentCommercialPaperState(
issuanceParty = this.issuance.party.owningKey.toBase58String(),
issuanceRef = this.issuance.reference.bytes,
owner = this.owner.owningKey.toBase58String(),
maturity = this.maturityDate,
faceValue = this.faceValue.quantity,
currency = this.faceValue.token.product.currencyCode,
faceValueIssuerParty = this.faceValue.token.issuer.party.owningKey.toBase58String(),
faceValueIssuerRef = this.faceValue.token.issuer.reference.bytes
)
/** Additional schema mappings would be added here (eg. CommercialPaperV2, ...) */
else -> throw IllegalArgumentException("Unrecognised schema $schema")
}
}
/** @suppress */ infix fun `owned by`(owner: AbstractParty) = copy(owner = owner)
}
interface Commands : CommandData {
class Move : TypeOnlyCommandData(), Commands
class Redeem : TypeOnlyCommandData(), Commands
// We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP.
// However, nothing in the platform enforces that uniqueness: it's up to the issuer.
class Issue : TypeOnlyCommandData(), Commands
}
override fun verify(tx: LedgerTransaction) {
// Group by everything except owner: any modification to the CP at all is considered changing it fundamentally.
val groups = tx.groupStates(State::withoutOwner)
// There are two possible things that can be done with this CP. The first is trading it. The second is redeeming
// it for cash on or after the maturity date.
val command = tx.commands.requireSingleCommand<PtCommercialPaper.Commands>()
val timeWindow: TimeWindow? = tx.timeWindow
// Suppress compiler warning as 'key' is an unused variable when destructuring 'groups'.
@Suppress("UNUSED_VARIABLE")
for ((inputs, outputs, key) in groups) {
when (command.value) {
is Commands.Move -> {
val input = inputs.single()
requireThat {
"the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers)
"the state is propagated" using (outputs.size == 1)
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
// the input ignoring the owner field due to the grouping.
}
}
is Commands.Redeem -> {
// Redemption of the paper requires movement of on-ledger cash.
val input = inputs.single()
val received = tx.outputStates.sumCashBy(input.owner)
val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must have a time-window")
requireThat {
"the paper must have matured" using (time >= input.maturityDate)
"the received amount equals the face value" using (received == input.faceValue)
"the paper must be destroyed" using outputs.isEmpty()
"the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers)
}
}
is Commands.Issue -> {
val output = outputs.single()
val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances have a time-window")
requireThat {
// Don't allow people to issue commercial paper under other entities identities.
"output states are issued by a command signer" using
(output.issuance.party.owningKey in command.signers)
"output values sum to more than the inputs" using (output.faceValue.quantity > 0)
"the maturity date is not in the past" using (time < output.maturityDate)
// Don't allow an existing CP state to be replaced by this issuance.
// TODO: Consider how to handle the case of mistaken issuances, or other need to patch.
"output values sum to more than the inputs" using inputs.isEmpty()
}
}
// TODO: Think about how to evolve contracts over time with new commands.
else -> throw IllegalArgumentException("Unrecognised command")
}
}
}
/**
* Returns a transaction that issues commercial paper, owned by the issuing parties key. Does not update
* 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<Issued<Currency>>, maturityDate: Instant,
notary: Party): TransactionBuilder {
val state = State(issuance, issuance.party, faceValue, maturityDate)
return TransactionBuilder(notary = notary).withItems(StateAndContract(state, CP_PROGRAM_ID), Command(Commands.Issue(), issuance.party.owningKey))
}
/**
* Updates the given partial transaction with an input/output/command to reassign ownership of the paper.
*/
fun generateMove(tx: TransactionBuilder, paper: StateAndRef<State>, newOwner: AbstractParty) {
tx.addInputState(paper)
tx.addOutputState(paper.state.data.withOwner(newOwner), CP_PROGRAM_ID)
tx.addCommand(Commands.Move(), paper.state.data.owner.owningKey)
}
/**
* Intended to be called by the issuer of some commercial paper, when an owner has notified us that they wish
* to redeem the paper. We must therefore send enough money to the key that owns the paper to satisfy the face
* value, and then ensure the paper is removed from the ledger.
*
* @throws InsufficientBalanceException if the vault doesn't contain enough money to pay the redeemer.
*/
@Throws(InsufficientBalanceException::class)
@Suspendable
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, services: ServiceHub, ourIdentity: PartyAndCertificate) {
// Add the cash movement using the states in our vault.
PtCash.generateSpend(services, tx, paper.state.data.faceValue.withoutIssuer(), ourIdentity, paper.state.data.owner)
tx.addInputState(paper)
tx.addCommand(Commands.Redeem(), paper.state.data.owner.owningKey)
}
}

View File

@ -0,0 +1,415 @@
// So the static extension functions get put into a class with a better name than CashKt
package net.corda.ptflows.contracts.asset
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.*
import net.corda.core.contracts.Amount.Companion.sumOrThrow
import net.corda.core.crypto.NullKeys.NULL_PARTY
import net.corda.core.crypto.entropyToKeyPair
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.internal.Emoji
import net.corda.core.node.ServiceHub
import net.corda.core.schemas.MappedSchema
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.core.utilities.OpaqueBytes
import net.corda.core.utilities.toBase58String
import net.corda.ptflows.schemas.PtCashSchemaV1
import net.corda.ptflows.utils.sumCash
import net.corda.ptflows.utils.sumCashOrNull
import net.corda.ptflows.utils.sumCashOrZero
import java.math.BigInteger
import java.security.PublicKey
import java.sql.DatabaseMetaData
import java.util.*
import java.util.concurrent.atomic.AtomicReference
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Cash
//
/**
* Pluggable interface to allow for different cash selection provider implementations
* Default implementation [CashSelectionH2Impl] uses H2 database and a custom function within H2 to perform aggregation.
* Custom implementations must implement this interface and declare their implementation in
* META-INF/services/net.corda.contracts.asset.CashSelection
*/
interface PtCashSelection {
companion object {
val instance = AtomicReference<PtCashSelection>()
fun getInstance(metadata: () -> java.sql.DatabaseMetaData): PtCashSelection {
return instance.get() ?: {
val _metadata = metadata()
val cashSelectionAlgos = ServiceLoader.load(PtCashSelection::class.java).toList()
val cashSelectionAlgo = cashSelectionAlgos.firstOrNull { it.isCompatible(_metadata) }
cashSelectionAlgo?.let {
instance.set(cashSelectionAlgo)
cashSelectionAlgo
} ?: throw ClassNotFoundException("\nUnable to load compatible cash selection algorithm implementation for JDBC driver ($_metadata)." +
"\nPlease specify an implementation in META-INF/services/net.corda.finance.contracts.asset.CashSelection")
}.invoke()
}
}
/**
* Upon dynamically loading configured Cash Selection algorithms declared in META-INF/services
* this method determines whether the loaded implementation is compatible and usable with the currently
* loaded JDBC driver.
* Note: the first loaded implementation to pass this check will be used at run-time.
*/
fun isCompatible(metadata: DatabaseMetaData): Boolean
/**
* Query to gather Cash states that are available
* @param services The service hub to allow access to the database session
* @param amount The amount of currency desired (ignoring issues, but specifying the currency)
* @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer,
* 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.
* 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,
* otherwise what is available is returned unlocked for informational purposes.
*/
@Suspendable
fun unconsumedCashStatesForSpending(services: ServiceHub,
amount: Amount<Currency>,
onlyFromIssuerParties: Set<AbstractParty> = emptySet(),
notary: Party? = null,
lockId: UUID,
withIssuerRefs: Set<OpaqueBytes> = emptySet()): List<StateAndRef<PtCash.State>>
}
/**
* A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple
* input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour
* (a blend of issuer+depositRef) and you couldn't merge outputs of two colours together, but you COULD put them in
* the same transaction.
*
* The goal of this design is to ensure that money can be withdrawn from the ledger easily: if you receive some money
* via this contract, you always know where to go in order to extract it from the R3 ledger, no matter how many hands
* it has passed through in the intervening time.
*
* At the same time, other contracts that just want money and don't care much who is currently holding it in their
* vaults can ignore the issuer/depositRefs and just examine the amount fields.
*/
class PtCash : PtOnLedgerAsset<Currency, PtCash.Commands, PtCash.State>() {
override fun extractCommands(commands: Collection<CommandWithParties<CommandData>>): List<CommandWithParties<Commands>>
= commands.select<Commands>()
// DOCSTART 1
/** A state representing a cash claim against some party. */
data class State(
override val amount: Amount<Issued<Currency>>,
/** There must be a MoveCommand signed by this key to claim the amount. */
override val owner: AbstractParty
) : FungibleAsset<Currency>, QueryableState {
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: AbstractParty)
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
override val exitKeys = setOf(owner.owningKey, amount.token.issuer.party.owningKey)
override val participants = listOf(owner)
override fun withNewOwnerAndAmount(newAmount: Amount<Issued<Currency>>, newOwner: AbstractParty): FungibleAsset<Currency>
= copy(amount = amount.copy(newAmount.quantity), owner = newOwner)
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): State = copy(amount = amount.copy(token = amount.token.copy(issuer = deposit)))
/** Object Relational Mapping support. */
override fun generateMappedObject(schema: MappedSchema): PersistentState {
return when (schema) {
is PtCashSchemaV1 -> PtCashSchemaV1.PersistentCashState(
owner = this.owner,
pennies = this.amount.quantity,
currency = this.amount.token.product.currencyCode,
issuerParty = this.amount.token.issuer.party.owningKey.toBase58String(),
issuerRef = this.amount.token.issuer.reference.bytes
)
/** Additional schema mappings would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */
else -> throw IllegalArgumentException("Unrecognised schema $schema")
}
}
/** Object Relational Mapping support. */
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(PtCashSchemaV1)
/** Additional used schemas would be added here (eg. CashSchemaV2, CashSchemaV3, ...) */
}
// DOCEND 1
// Just for grouping
interface Commands : CommandData {
/**
* A command stating that money has been moved, optionally to fulfil another contract.
*
* @param contract the contract this move is for the attention of. Only that contract's verify function
* should take the moved states into account when considering whether it is valid. Typically this will be
* null.
*/
data class Move(override val contract: Class<out Contract>? = null) : MoveCommand
/**
* Allows new cash states to be issued into existence.
*/
class Issue : TypeOnlyCommandData()
/**
* 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<Issued<Currency>>) : CommandData
}
/**
* Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.
*/
fun generateIssue(tx: TransactionBuilder, tokenDef: Issued<Currency>, pennies: Long, owner: AbstractParty, notary: Party)
= generateIssue(tx, Amount(pennies, tokenDef), owner, notary)
/**
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
*/
fun generateIssue(tx: TransactionBuilder, amount: Amount<Issued<Currency>>, owner: AbstractParty, notary: Party)
= generateIssue(tx, TransactionState(State(amount, owner), PROGRAM_ID, notary), Commands.Issue())
override fun deriveState(txState: TransactionState<State>, amount: Amount<Issued<Currency>>, owner: AbstractParty)
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
override fun generateExitCommand(amount: Amount<Issued<Currency>>) = Commands.Exit(amount)
override fun generateMoveCommand() = Commands.Move()
override fun verify(tx: LedgerTransaction) {
// Each group is a set of input/output states with distinct (reference, currency) attributes. These types
// of cash are not fungible and must be kept separated for bookkeeping purposes.
val groups = tx.groupStates { it: State -> it.amount.token }
for ((inputs, outputs, key) in groups) {
// Either inputs or outputs could be empty.
val issuer = key.issuer
val currency = key.product
requireThat {
"there are no zero sized outputs" using (outputs.none { it.amount.quantity == 0L })
}
val issueCommand = tx.commands.select<Commands.Issue>().firstOrNull()
if (issueCommand != null) {
verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer)
} else {
val inputAmount = inputs.sumCashOrNull() ?: throw IllegalArgumentException("there is at least one cash input for this group")
val outputAmount = outputs.sumCashOrZero(Issued(issuer, currency))
// If we want to remove cash from the ledger, that must be signed for by the issuer.
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero.
val exitKeys: Set<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
val exitCommand = tx.commands.select<Commands.Exit>(parties = null, signers = exitKeys).filter { it.value.amount.token == key }.singleOrNull()
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, Issued(issuer, currency))
requireThat {
"there are no zero sized inputs" using inputs.none { it.amount.quantity == 0L }
"for reference ${issuer.reference} at issuer ${issuer.party} the amounts balance: ${inputAmount.quantity} - ${amountExitingLedger.quantity} != ${outputAmount.quantity}" using
(inputAmount == outputAmount + amountExitingLedger)
}
verifyMoveCommand<Commands.Move>(inputs, tx.commands)
}
}
}
private fun verifyIssueCommand(inputs: List<State>,
outputs: List<State>,
tx: LedgerTransaction,
issueCommand: CommandWithParties<Commands.Issue>,
currency: Currency,
issuer: PartyAndReference) {
// If we have an issue command, perform special processing: the group is allowed to have no inputs,
// and the output states must have a deposit reference owned by the signer.
//
// Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must
// sum to more than the inputs. An issuance of zero size is not allowed.
//
// Note that this means literally anyone with access to the network can issue cash claims of arbitrary
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some
// as-yet-unwritten identity service. See ADP-22 for discussion.
// The grouping ensures that all outputs have the same deposit reference and currency.
val inputAmount = inputs.sumCashOrZero(Issued(issuer, currency))
val outputAmount = outputs.sumCash()
val cashCommands = tx.commands.select<Commands.Issue>()
requireThat {
// TODO: This doesn't work with the trader demo, so use the underlying key instead
// "output states are issued by a command signer" by (issuer.party in issueCommand.signingParties)
"output states are issued by a command signer" using (issuer.party.owningKey in issueCommand.signers)
"output values sum to more than the inputs" using (outputAmount > inputAmount)
"there is only a single issue command" using (cashCommands.count() == 1)
}
}
companion object {
const val PROGRAM_ID: ContractClassName = "net.corda.finance.contracts.asset.Cash"
/**
* Generate a transaction that moves an amount of currency to the given party, and sends any change back to
* sole identity of the calling node. Fails for nodes with multiple identities.
*
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
*
* @param services The [ServiceHub] to provide access to the database session.
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
* to move the cash will be added on top.
* @param amount How much currency to send.
* @param to the recipient party.
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
* the resulting transaction for it to be valid.
* @throws InsufficientBalanceException when a cash spending transaction fails because
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
*/
@JvmStatic
@Throws(InsufficientBalanceException::class)
@Suspendable
@Deprecated("Our identity should be specified", replaceWith = ReplaceWith("generateSpend(services, tx, amount, to, ourIdentity, onlyFromParties)"))
fun generateSpend(services: ServiceHub,
tx: TransactionBuilder,
amount: Amount<Currency>,
to: AbstractParty,
onlyFromParties: Set<AbstractParty> = emptySet()) = generateSpend(services, tx, listOf(PartyAndAmount(to, amount)), services.myInfo.legalIdentitiesAndCerts.single(), onlyFromParties)
/**
* Generate a transaction that moves an amount of currency to the given party.
*
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
*
* @param services The [ServiceHub] to provide access to the database session.
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
* to move the cash will be added on top.
* @param amount How much currency to send.
* @param to the recipient party.
* @param ourIdentity well known identity to create a new confidential identity from, for sending change to.
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
* the resulting transaction for it to be valid.
* @throws InsufficientBalanceException when a cash spending transaction fails because
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
*/
@JvmStatic
@Throws(InsufficientBalanceException::class)
@Suspendable
fun generateSpend(services: ServiceHub,
tx: TransactionBuilder,
amount: Amount<Currency>,
ourIdentity: PartyAndCertificate,
to: AbstractParty,
onlyFromParties: Set<AbstractParty> = emptySet()): Pair<TransactionBuilder, List<PublicKey>> {
return generateSpend(services, tx, listOf(PartyAndAmount(to, amount)), ourIdentity, onlyFromParties)
}
/**
* Generate a transaction that moves money of the given amounts to the recipients specified, and sends any change
* back to sole identity of the calling node. Fails for nodes with multiple identities.
*
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
*
* @param services The [ServiceHub] to provide access to the database session.
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
* to move the cash will be added on top.
* @param payments A list of amounts to pay, and the party to send the payment to.
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
* the resulting transaction for it to be valid.
* @throws InsufficientBalanceException when a cash spending transaction fails because
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
*/
@JvmStatic
@Throws(InsufficientBalanceException::class)
@Suspendable
@Deprecated("Our identity should be specified", replaceWith = ReplaceWith("generateSpend(services, tx, amount, to, ourIdentity, onlyFromParties)"))
fun generateSpend(services: ServiceHub,
tx: TransactionBuilder,
payments: List<PartyAndAmount<Currency>>,
onlyFromParties: Set<AbstractParty> = emptySet()) = generateSpend(services, tx, payments, services.myInfo.legalIdentitiesAndCerts.single(), onlyFromParties)
/**
* Generate a transaction that moves money of the given amounts to the recipients specified.
*
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
*
* @param services The [ServiceHub] to provide access to the database session.
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
* to move the cash will be added on top.
* @param payments A list of amounts to pay, and the party to send the payment to.
* @param ourIdentity well known identity to create a new confidential identity from, for sending change to.
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
* the resulting transaction for it to be valid.
* @throws InsufficientBalanceException when a cash spending transaction fails because
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
*/
@JvmStatic
@Throws(InsufficientBalanceException::class)
@Suspendable
fun generateSpend(services: ServiceHub,
tx: TransactionBuilder,
payments: List<PartyAndAmount<Currency>>,
ourIdentity: PartyAndCertificate,
onlyFromParties: Set<AbstractParty> = emptySet()): Pair<TransactionBuilder, List<PublicKey>> {
fun deriveState(txState: TransactionState<State>, amt: Amount<Issued<Currency>>, owner: AbstractParty)
= txState.copy(data = txState.data.copy(amount = amt, owner = owner))
// Retrieve unspent and unlocked cash states that meet our spending criteria.
val totalAmount = payments.map { it.amount }.sumOrThrow()
val cashSelection = PtCashSelection.getInstance({ services.jdbcSession().metaData })
val acceptableCoins = cashSelection.unconsumedCashStatesForSpending(services, totalAmount, onlyFromParties, tx.notary, tx.lockId)
val revocationEnabled = false // Revocation is currently unsupported
// Generate a new identity that change will be sent to for confidentiality purposes. This means that a
// third party with a copy of the transaction (such as the notary) cannot identify who the change was
// sent to
val changeIdentity = services.keyManagementService.freshKeyAndCert(ourIdentity, revocationEnabled)
return PtOnLedgerAsset.generateSpend(tx, payments, acceptableCoins,
changeIdentity.party.anonymise(),
{ state, quantity, owner -> deriveState(state, quantity, owner) },
{ PtCash().generateMoveCommand() })
}
}
}
// Small DSL extensions.
/** @suppress */ infix fun PtCash.State.`owned by`(owner: AbstractParty) = ownedBy(owner)
/** @suppress */ infix fun PtCash.State.`issued by`(party: AbstractParty) = issuedBy(party)
/** @suppress */ infix fun PtCash.State.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
/** @suppress */ infix fun PtCash.State.`with deposit`(deposit: PartyAndReference): PtCash.State = withDeposit(deposit)
// Unit testing helpers. These could go in a separate file but it's hardly worth it for just a few functions.
/** A randomly generated key. */
val DUMMY_CASH_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) }
/** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */
val DUMMY_CASH_ISSUER by lazy { Party(CordaX500Name(organisation = "Snake Oil Issuer", locality = "London", country = "GB"), DUMMY_CASH_ISSUER_KEY.public).ref(1) }
/** An extension property that lets you write 100.DOLLARS.CASH */
val Amount<Currency>.CASH: PtCash.State get() = PtCash.State(Amount(quantity, Issued(DUMMY_CASH_ISSUER, token)), NULL_PARTY)
/** An extension property that lets you get a cash state from an issued token, under the [NULL_PARTY] */
val Amount<Issued<Currency>>.STATE: PtCash.State get() = PtCash.State(this, NULL_PARTY)

View File

@ -0,0 +1,323 @@
package net.corda.ptflows.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
import net.corda.core.utilities.trace
import java.security.PublicKey
import java.util.*
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Generic contract for assets on a ledger
//
/** A simple holder for a (possibly anonymous) [AbstractParty] and a quantity of tokens */
data class PartyAndAmount<T : Any>(val party: AbstractParty, val amount: Amount<T>)
/**
* An asset transaction may split and merge assets represented by a set of (issuer, depositRef) pairs, across multiple
* input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour (a blend of
* issuer+depositRef) and you couldn't merge outputs of two colours together, but you COULD put them in the same
* transaction.
*
* The goal of this design is to ensure that assets can be withdrawn from the ledger easily: if you receive some asset
* via this contract, you always know where to go in order to extract it from the R3 ledger, no matter how many hands
* it has passed through in the intervening time.
*
* At the same time, other contracts that just want assets and don't care much who is currently holding it can ignore
* the issuer/depositRefs and just examine the amount fields.
*/
abstract class PtOnLedgerAsset<T : Any, C : CommandData, S : FungibleAsset<T>> : Contract {
companion object {
val log = loggerFor<PtOnLedgerAsset<*, *, *>>()
/**
* Generate a transaction that moves an amount of currency to the given pubkey.
*
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
*
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
* to move the cash will be added on top.
* @param amount How much currency to send.
* @param to a key of the recipient.
* @param acceptableStates a list of acceptable input states to use.
* @param payChangeTo party to pay any change to; this is normally a confidential identity of the calling
* party.
* @param deriveState a function to derive an output state based on an input state, amount for the output
* and public key to pay to.
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
* the resulting transaction for it to be valid.
* @throws InsufficientBalanceException when a cash spending transaction fails because
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
*/
@Throws(InsufficientBalanceException::class)
@JvmStatic
fun <S : FungibleAsset<T>, T: Any> generateSpend(tx: TransactionBuilder,
amount: Amount<T>,
to: AbstractParty,
acceptableStates: List<StateAndRef<S>>,
payChangeTo: AbstractParty,
deriveState: (TransactionState<S>, Amount<Issued<T>>, AbstractParty) -> TransactionState<S>,
generateMoveCommand: () -> CommandData): Pair<TransactionBuilder, List<PublicKey>> {
return generateSpend(tx, listOf(PartyAndAmount(to, amount)), acceptableStates, payChangeTo, deriveState, generateMoveCommand)
}
/**
* Adds to the given transaction states that move amounts of a fungible asset to the given parties, using only
* the provided acceptable input states to find a solution (not all of them may be used in the end). A change
* output will be generated if the state amounts don't exactly fit.
*
* The fungible assets must all be of the same type and the amounts must be summable i.e. amounts of the same
* token.
*
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
* to move the cash will be added on top.
* @param amount How much currency to send.
* @param to a key of the recipient.
* @param acceptableStates a list of acceptable input states to use.
* @param payChangeTo party to pay any change to; this is normally a confidential identity of the calling
* party. We use a new confidential identity here so that the recipient is not identifiable.
* @param deriveState a function to derive an output state based on an input state, amount for the output
* and public key to pay to.
* @param T A type representing a token
* @param S A fungible asset state type
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
* the resulting transaction for it to be valid.
* @throws InsufficientBalanceException when a cash spending transaction fails because
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
*/
@Throws(InsufficientBalanceException::class)
@JvmStatic
fun <S : FungibleAsset<T>, T: Any> generateSpend(tx: TransactionBuilder,
payments: List<PartyAndAmount<T>>,
acceptableStates: List<StateAndRef<S>>,
payChangeTo: AbstractParty,
deriveState: (TransactionState<S>, Amount<Issued<T>>, AbstractParty) -> TransactionState<S>,
generateMoveCommand: () -> CommandData): Pair<TransactionBuilder, List<PublicKey>> {
// Discussion
//
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.
//
// First we must select a set of asset states (which for convenience we will call 'coins' here, as in bitcoinj).
// The input states can be considered our "vault", and may consist of different products, and with different
// issuers and deposits.
//
// Coin selection is a complex problem all by itself and many different approaches can be used. It is easily
// possible for different actors to use different algorithms and approaches that, for example, compete on
// privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of
// obfuscation and so on.
//
// Having selected input states of the correct asset, we must craft output states for the amount we're sending and
// the "change", which goes back to us. The change is required to make the amounts balance. We may need more
// than one change output in order to avoid merging assets from different deposits. The point of this design
// is to ensure that ledger entries are immutable and globally identifiable.
//
// Finally, we add the states to the provided partial transaction.
// TODO: We should be prepared to produce multiple transactions spending inputs from
// different notaries, or at least group states by notary and take the set with the
// highest total value.
// TODO: Check that re-running this on the same transaction multiple times does the right thing.
// The notary may be associated with a locked state only.
tx.notary = acceptableStates.firstOrNull()?.state?.notary
// Calculate the total amount we're sending (they must be all of a compatible token).
val totalSendAmount = payments.map { it.amount }.sumOrThrow()
// Select a subset of the available states we were given that sums up to >= totalSendAmount.
val (gathered, gatheredAmount) = gatherCoins(acceptableStates, totalSendAmount)
check(gatheredAmount >= totalSendAmount)
val keysUsed = gathered.map { it.state.data.owner.owningKey }
// Now calculate the output states. This is complicated by the fact that a single payment may require
// multiple output states, due to the need to keep states separated by issuer. We start by figuring out
// how much we've gathered for each issuer: this map will keep track of how much we've used from each
// as we work our way through the payments.
val statesGroupedByIssuer = gathered.groupBy { it.state.data.amount.token }
val remainingFromEachIssuer = statesGroupedByIssuer
.mapValues {
it.value.map {
it.state.data.amount
}.sumOrThrow()
}.toList().toMutableList()
val outputStates = mutableListOf<TransactionState<S>>()
for ((party, paymentAmount) in payments) {
var remainingToPay = paymentAmount.quantity
while (remainingToPay > 0) {
val (token, remainingFromCurrentIssuer) = remainingFromEachIssuer.last()
val templateState = statesGroupedByIssuer[token]!!.first().state
val delta = remainingFromCurrentIssuer.quantity - remainingToPay
when {
delta > 0 -> {
// The states from the current issuer more than covers this payment.
outputStates += deriveState(templateState, Amount(remainingToPay, token), party)
remainingFromEachIssuer[0] = Pair(token, Amount(delta, token))
remainingToPay = 0
}
delta == 0L -> {
// The states from the current issuer exactly covers this payment.
outputStates += deriveState(templateState, Amount(remainingToPay, token), party)
remainingFromEachIssuer.removeAt(remainingFromEachIssuer.lastIndex)
remainingToPay = 0
}
delta < 0 -> {
// The states from the current issuer don't cover this payment, so we'll have to use >1 output
// state to cover this payment.
outputStates += deriveState(templateState, remainingFromCurrentIssuer, party)
remainingFromEachIssuer.removeAt(remainingFromEachIssuer.lastIndex)
remainingToPay -= remainingFromCurrentIssuer.quantity
}
}
}
}
// Whatever values we have left over for each issuer must become change outputs.
for ((token, amount) in remainingFromEachIssuer) {
val templateState = statesGroupedByIssuer[token]!!.first().state
outputStates += deriveState(templateState, amount, payChangeTo)
}
for (state in gathered) tx.addInputState(state)
for (state in outputStates) tx.addOutputState(state)
// What if we already have a move command with the right keys? Filter it out here or in platform code?
tx.addCommand(generateMoveCommand(), keysUsed)
return Pair(tx, keysUsed)
}
/**
* Gather assets from the given list of states, sufficient to match or exceed the given amount.
*
* @param acceptableCoins list of states to use as inputs.
* @param amount the amount to gather states up to.
* @throws InsufficientBalanceException if there isn't enough value in the states to cover the requested amount.
*/
@Throws(InsufficientBalanceException::class)
private fun <S : FungibleAsset<T>, T : Any> gatherCoins(acceptableCoins: Collection<StateAndRef<S>>,
amount: Amount<T>): Pair<ArrayList<StateAndRef<S>>, Amount<T>> {
require(amount.quantity > 0) { "Cannot gather zero coins" }
val gathered = arrayListOf<StateAndRef<S>>()
var gatheredAmount = Amount(0, amount.token)
for (c in acceptableCoins) {
if (gatheredAmount >= amount) break
gathered.add(c)
gatheredAmount += Amount(c.state.data.amount.quantity, amount.token)
}
if (gatheredAmount < amount) {
log.trace { "Insufficient balance: requested $amount, available $gatheredAmount" }
throw InsufficientBalanceException(amount - gatheredAmount)
}
log.trace { "Gathered coins: requested $amount, available $gatheredAmount, change: ${gatheredAmount - amount}" }
return Pair(gathered, gatheredAmount)
}
/**
* Generate an transaction exiting fungible assets from the ledger.
*
* @param tx transaction builder to add states and commands to.
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
* the responsibility of the caller to check that they do not attempt to exit funds held by others.
* @return the public keys which must sign the transaction for it to be valid.
*/
@Throws(InsufficientBalanceException::class)
@JvmStatic
fun <S : FungibleAsset<T>, T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
assetStates: List<StateAndRef<S>>,
deriveState: (TransactionState<S>, Amount<Issued<T>>, AbstractParty) -> TransactionState<S>,
generateMoveCommand: () -> CommandData,
generateExitCommand: (Amount<Issued<T>>) -> CommandData): Set<PublicKey> {
val owner = assetStates.map { it.state.data.owner }.toSet().singleOrNull() ?: throw InsufficientBalanceException(amountIssued)
val currency = amountIssued.token.product
val amount = Amount(amountIssued.quantity, currency)
var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token }
tx.notary = acceptableCoins.firstOrNull()?.state?.notary
// TODO: We should be prepared to produce multiple transactions exiting inputs from
// different notaries, or at least group states by notary and take the set with the
// highest total value
acceptableCoins = acceptableCoins.filter { it.state.notary == tx.notary }
val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount)
val takeChangeFrom = gathered.lastOrNull()
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.amount.token)
} else {
null
}
val outputs = if (change != null) {
// Add a change output and adjust the last output downwards.
listOf(deriveState(gathered.last().state, change, owner))
} else emptyList()
for (state in gathered) tx.addInputState(state)
for (state in outputs) tx.addOutputState(state)
val moveKeys = gathered.map { it.state.data.owner.owningKey }
val exitKeys = gathered.flatMap { it.state.data.exitKeys }
tx.addCommand(generateMoveCommand(), moveKeys)
tx.addCommand(generateExitCommand(amountIssued), exitKeys)
return (moveKeys + exitKeys).toSet()
}
/**
* Puts together an issuance transaction for the specified state. Normally contracts will provide convenient
* wrappers around this function, which build the state for you, and those should be used in preference.
*/
@JvmStatic
fun <S : FungibleAsset<T>, T: Any> generateIssue(tx: TransactionBuilder,
transactionState: TransactionState<S>,
issueCommand: CommandData): Set<PublicKey> {
check(tx.inputStates().isEmpty())
check(tx.outputStates().map { it.data }.filterIsInstance(transactionState.javaClass).isEmpty())
require(transactionState.data.amount.quantity > 0)
val at = transactionState.data.amount.token.issuer
val commandSigner = at.party.owningKey
tx.addOutputState(transactionState)
tx.addCommand(issueCommand, commandSigner)
return setOf(commandSigner)
}
}
abstract fun extractCommands(commands: Collection<CommandWithParties<CommandData>>): Collection<CommandWithParties<C>>
/**
* Generate an transaction exiting assets from the ledger.
*
* @param tx transaction builder to add states and commands to.
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
* the responsibility of the caller to check that they do not exit funds held by others.
* @return the public keys which must sign the transaction for it to be valid.
*/
@Throws(InsufficientBalanceException::class)
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
assetStates: List<StateAndRef<S>>): Set<PublicKey> {
return generateExit(
tx,
amountIssued,
assetStates,
deriveState = { state, amount, owner -> deriveState(state, amount, owner) },
generateMoveCommand = { -> generateMoveCommand() },
generateExitCommand = { amount -> generateExitCommand(amount) }
)
}
abstract fun generateExitCommand(amount: Amount<Issued<T>>): CommandData
abstract fun generateMoveCommand(): MoveCommand
/**
* Derive a new transaction state based on the given example, with amount and owner modified. This allows concrete
* implementations to have fields in their state which we don't know about here, and we simply leave them untouched
* when sending out "change" from spending/exiting.
*/
abstract fun deriveState(txState: TransactionState<S>, amount: Amount<Issued<T>>, owner: AbstractParty): TransactionState<S>
}

View File

@ -0,0 +1,45 @@
package net.corda.ptflows.schemas
import net.corda.core.identity.AbstractParty
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.core.serialization.CordaSerializable
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Index
import javax.persistence.Table
/**
* An object used to fully qualify the [CashSchema] family name (i.e. independent of version).
*/
object PtCashSchema
/**
* First version of a cash contract ORM schema that maps all fields of the [Cash] contract state as it stood
* at the time of writing.
*/
@CordaSerializable
object PtCashSchemaV1 : MappedSchema(schemaFamily = PtCashSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCashState::class.java)) {
@Entity
@Table(name = "contract_cash_states",
indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"),
Index(name = "pennies_idx", columnList = "pennies")))
class PersistentCashState(
/** X500Name of owner party **/
@Column(name = "owner_name")
var owner: AbstractParty,
@Column(name = "pennies")
var pennies: Long,
@Column(name = "ccy_code", length = 3)
var currency: String,
@Column(name = "issuer_key")
var issuerParty: String,
@Column(name = "issuer_ref")
var issuerRef: ByteArray
) : PersistentState()
}

View File

@ -0,0 +1,52 @@
package net.corda.ptflows.schemas
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.core.serialization.CordaSerializable
import java.time.Instant
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.Index
import javax.persistence.Table
/**
* An object used to fully qualify the [CommercialPaperSchema] family name (i.e. independent of version).
*/
object PtCommercialPaperSchema
/**
* First version of a commercial paper contract ORM schema that maps all fields of the [CommercialPaper] contract state
* as it stood at the time of writing.
*/
@CordaSerializable
object PtCommercialPaperSchemaV1 : MappedSchema(schemaFamily = PtCommercialPaperSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCommercialPaperState::class.java)) {
@Entity
@Table(name = "cp_states",
indexes = arrayOf(Index(name = "ccy_code_index", columnList = "ccy_code"),
Index(name = "maturity_index", columnList = "maturity_instant"),
Index(name = "face_value_index", columnList = "face_value")))
class PersistentCommercialPaperState(
@Column(name = "issuance_key")
var issuanceParty: String,
@Column(name = "issuance_ref")
var issuanceRef: ByteArray,
@Column(name = "owner_key")
var owner: String,
@Column(name = "maturity_instant")
var maturity: Instant,
@Column(name = "face_value")
var faceValue: Long,
@Column(name = "ccy_code", length = 3)
var currency: String,
@Column(name = "face_value_issuer_key")
var faceValueIssuerParty: String,
@Column(name = "face_value_issuer_ref")
var faceValueIssuerRef: ByteArray
) : PersistentState()
}

View File

@ -0,0 +1,39 @@
package net.corda.ptflows.utils
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.*
/**
* 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<net.corda.ptflows.contracts.asset.PtCash.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<net.corda.ptflows.contracts.asset.PtCash.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<net.corda.ptflows.contracts.asset.PtCash.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<net.corda.ptflows.contracts.asset.PtCash.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)

View File

@ -46,4 +46,5 @@ include 'doorman'
include 'verify-enclave'
include 'sgx-jvm/hsm-tool'
include 'signing-server'
include 'perftestflows'