From 8ae92850c9c6751ed1d24001982ee3b3133009d7 Mon Sep 17 00:00:00 2001 From: Christian Sailer Date: Tue, 10 Oct 2017 13:10:21 +0100 Subject: [PATCH] State and Contract for Cash and CommercialPaper copied to perftestflows --- perftestflows/build.gradle | 43 ++ .../contracts/IPtCommercialPaperState.java | 23 + .../ptflows/contracts/PtCommercialPaper.kt | 197 +++++++++ .../corda/ptflows/contracts/asset/PtCash.kt | 415 ++++++++++++++++++ .../contracts/asset/PtOnLedgerAsset.kt | 323 ++++++++++++++ .../corda/ptflows/schemas/PtCashSchemaV1.kt | 45 ++ .../schemas/PtCommercialPaperSchemaV1.kt | 52 +++ .../ptflows/utils/StateSummingUtilities.kt | 39 ++ settings.gradle | 1 + 9 files changed, 1138 insertions(+) create mode 100644 perftestflows/build.gradle create mode 100644 perftestflows/src/main/java/net/corda/ptflows/contracts/IPtCommercialPaperState.java create mode 100644 perftestflows/src/main/kotlin/net/corda/ptflows/contracts/PtCommercialPaper.kt create mode 100644 perftestflows/src/main/kotlin/net/corda/ptflows/contracts/asset/PtCash.kt create mode 100644 perftestflows/src/main/kotlin/net/corda/ptflows/contracts/asset/PtOnLedgerAsset.kt create mode 100644 perftestflows/src/main/kotlin/net/corda/ptflows/schemas/PtCashSchemaV1.kt create mode 100644 perftestflows/src/main/kotlin/net/corda/ptflows/schemas/PtCommercialPaperSchemaV1.kt create mode 100644 perftestflows/src/main/kotlin/net/corda/ptflows/utils/StateSummingUtilities.kt diff --git a/perftestflows/build.gradle b/perftestflows/build.gradle new file mode 100644 index 0000000000..d05eafb3ac --- /dev/null +++ b/perftestflows/build.gradle @@ -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 +} diff --git a/perftestflows/src/main/java/net/corda/ptflows/contracts/IPtCommercialPaperState.java b/perftestflows/src/main/java/net/corda/ptflows/contracts/IPtCommercialPaperState.java new file mode 100644 index 0000000000..edf6362ba9 --- /dev/null +++ b/perftestflows/src/main/java/net/corda/ptflows/contracts/IPtCommercialPaperState.java @@ -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> newFaceValue); + + IPtCommercialPaperState withMaturityDate(Instant newMaturityDate); +} diff --git a/perftestflows/src/main/kotlin/net/corda/ptflows/contracts/PtCommercialPaper.kt b/perftestflows/src/main/kotlin/net/corda/ptflows/contracts/PtCommercialPaper.kt new file mode 100644 index 0000000000..7c4825ecc1 --- /dev/null +++ b/perftestflows/src/main/kotlin/net/corda/ptflows/contracts/PtCommercialPaper.kt @@ -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>, + 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>): IPtCommercialPaperState = copy(faceValue = newFaceValue) + override fun withMaturityDate(newMaturityDate: Instant): IPtCommercialPaperState = copy(maturityDate = newMaturityDate) + + /** Object Relational Mapping support. */ + override fun supportedSchemas(): Iterable = 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() + 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>, 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, 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, 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) + } +} \ No newline at end of file diff --git a/perftestflows/src/main/kotlin/net/corda/ptflows/contracts/asset/PtCash.kt b/perftestflows/src/main/kotlin/net/corda/ptflows/contracts/asset/PtCash.kt new file mode 100644 index 0000000000..768a17208a --- /dev/null +++ b/perftestflows/src/main/kotlin/net/corda/ptflows/contracts/asset/PtCash.kt @@ -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() + + 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, + onlyFromIssuerParties: Set = emptySet(), + notary: Party? = null, + lockId: UUID, + withIssuerRefs: Set = emptySet()): List> +} + +/** + * 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() { + override fun extractCommands(commands: Collection>): List> + = commands.select() + + // DOCSTART 1 + /** A state representing a cash claim against some party. */ + data class State( + override val amount: Amount>, + + /** There must be a MoveCommand signed by this key to claim the amount. */ + override val owner: AbstractParty + ) : FungibleAsset, QueryableState { + constructor(deposit: PartyAndReference, amount: Amount, 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>, newOwner: AbstractParty): FungibleAsset + = 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 = 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? = 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>) : 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, 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>, owner: AbstractParty, notary: Party) + = generateIssue(tx, TransactionState(State(amount, owner), PROGRAM_ID, notary), Commands.Issue()) + + override fun deriveState(txState: TransactionState, amount: Amount>, owner: AbstractParty) + = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) + + override fun generateExitCommand(amount: Amount>) = 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().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 = inputs.flatMap { it.exitKeys }.toSet() + val exitCommand = tx.commands.select(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(inputs, tx.commands) + } + } + } + + private fun verifyIssueCommand(inputs: List, + outputs: List, + tx: LedgerTransaction, + issueCommand: CommandWithParties, + 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() + 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, + to: AbstractParty, + onlyFromParties: Set = 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, + ourIdentity: PartyAndCertificate, + to: AbstractParty, + onlyFromParties: Set = emptySet()): Pair> { + 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>, + onlyFromParties: Set = 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>, + ourIdentity: PartyAndCertificate, + onlyFromParties: Set = emptySet()): Pair> { + fun deriveState(txState: TransactionState, amt: Amount>, 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.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>.STATE: PtCash.State get() = PtCash.State(this, NULL_PARTY) diff --git a/perftestflows/src/main/kotlin/net/corda/ptflows/contracts/asset/PtOnLedgerAsset.kt b/perftestflows/src/main/kotlin/net/corda/ptflows/contracts/asset/PtOnLedgerAsset.kt new file mode 100644 index 0000000000..ac0365ff3a --- /dev/null +++ b/perftestflows/src/main/kotlin/net/corda/ptflows/contracts/asset/PtOnLedgerAsset.kt @@ -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(val party: AbstractParty, val amount: Amount) + +/** + * 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> : Contract { + companion object { + val log = loggerFor>() + + /** + * 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 , T: Any> generateSpend(tx: TransactionBuilder, + amount: Amount, + to: AbstractParty, + acceptableStates: List>, + payChangeTo: AbstractParty, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData): Pair> { + 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 , T: Any> generateSpend(tx: TransactionBuilder, + payments: List>, + acceptableStates: List>, + payChangeTo: AbstractParty, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData): Pair> { + // 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>() + 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 , T : Any> gatherCoins(acceptableCoins: Collection>, + amount: Amount): Pair>, Amount> { + require(amount.quantity > 0) { "Cannot gather zero coins" } + val gathered = arrayListOf>() + 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 , T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount>, + assetStates: List>, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData, + generateExitCommand: (Amount>) -> CommandData): Set { + 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 , T: Any> generateIssue(tx: TransactionBuilder, + transactionState: TransactionState, + issueCommand: CommandData): Set { + 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>): Collection> + + /** + * 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>, + assetStates: List>): Set { + return generateExit( + tx, + amountIssued, + assetStates, + deriveState = { state, amount, owner -> deriveState(state, amount, owner) }, + generateMoveCommand = { -> generateMoveCommand() }, + generateExitCommand = { amount -> generateExitCommand(amount) } + ) + } + + abstract fun generateExitCommand(amount: Amount>): 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, amount: Amount>, owner: AbstractParty): TransactionState +} diff --git a/perftestflows/src/main/kotlin/net/corda/ptflows/schemas/PtCashSchemaV1.kt b/perftestflows/src/main/kotlin/net/corda/ptflows/schemas/PtCashSchemaV1.kt new file mode 100644 index 0000000000..bf4f46c805 --- /dev/null +++ b/perftestflows/src/main/kotlin/net/corda/ptflows/schemas/PtCashSchemaV1.kt @@ -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() +} diff --git a/perftestflows/src/main/kotlin/net/corda/ptflows/schemas/PtCommercialPaperSchemaV1.kt b/perftestflows/src/main/kotlin/net/corda/ptflows/schemas/PtCommercialPaperSchemaV1.kt new file mode 100644 index 0000000000..3fc657d1a1 --- /dev/null +++ b/perftestflows/src/main/kotlin/net/corda/ptflows/schemas/PtCommercialPaperSchemaV1.kt @@ -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() +} diff --git a/perftestflows/src/main/kotlin/net/corda/ptflows/utils/StateSummingUtilities.kt b/perftestflows/src/main/kotlin/net/corda/ptflows/utils/StateSummingUtilities.kt new file mode 100644 index 0000000000..66ed583626 --- /dev/null +++ b/perftestflows/src/main/kotlin/net/corda/ptflows/utils/StateSummingUtilities.kt @@ -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.sumCashBy(owner: AbstractParty): Amount> = filterIsInstance().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.sumCash(): Amount> = filterIsInstance().map { it.amount }.sumOrThrow() + +/** Sums the cash states in the list, returning null if there are none. */ +fun Iterable.sumCashOrNull(): Amount>? = filterIsInstance().map { it.amount }.sumOrNull() + +/** Sums the cash states in the list, returning zero of the given currency+issuer if there are none. */ +fun Iterable.sumCashOrZero(currency: Issued): Amount> { + return filterIsInstance().map { it.amount }.sumOrZero(currency) +} + +/** Sums the asset states in the list, returning null if there are none. */ +fun Iterable.sumFungibleOrNull() = filterIsInstance>().map { it.amount }.sumOrNull() + +/** Sums the asset states in the list, returning zero of the given token if there are none. */ +fun Iterable.sumFungibleOrZero(token: Issued) = filterIsInstance>().map { it.amount }.sumOrZero(token) + diff --git a/settings.gradle b/settings.gradle index 9bdb3b1ad1..eabb775ac6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -46,4 +46,5 @@ include 'doorman' include 'verify-enclave' include 'sgx-jvm/hsm-tool' include 'signing-server' +include 'perftestflows'