diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index bfeff657cb..1bd2adab38 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -86,6 +86,8 @@
+
+
diff --git a/perftestcordapp/build.gradle b/perftestcordapp/build.gradle
new file mode 100644
index 0000000000..ba8c4a72b0
--- /dev/null
+++ b/perftestcordapp/build.gradle
@@ -0,0 +1,49 @@
+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.cordapp'
+//apply plugin: 'com.jfrog.artifactory'
+
+description 'Corda performance test modules'
+
+dependencies {
+ // Note the :finance module is a CorDapp in its own right
+ // and CorDapps using :finance features should use 'cordapp' not 'compile' linkage.
+ cordaCompile project(':core')
+ cordaCompile project(':confidential-identities')
+
+ // TODO Remove this once we have app configs
+ compile "com.typesafe:config:$typesafe_config_version"
+
+ testCompile project(':test-utils')
+ testCompile project(path: ':core', configuration: 'testArtifacts')
+ testCompile "junit:junit:$junit_version"
+
+ // AssertJ: for fluent assertions for testing
+ testCompile "org.assertj:assertj-core:$assertj_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/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/Currencies.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/Currencies.kt
new file mode 100644
index 0000000000..29c8d1a5ed
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/Currencies.kt
@@ -0,0 +1,34 @@
+@file:JvmName("Currencies")
+
+package com.r3.corda.enterprise.perftestcordapp
+
+import net.corda.core.contracts.Amount
+import net.corda.core.contracts.Issued
+import net.corda.core.contracts.PartyAndReference
+import java.math.BigDecimal
+import java.util.*
+
+@JvmField val USD: Currency = Currency.getInstance("USD")
+@JvmField val GBP: Currency = Currency.getInstance("GBP")
+@JvmField val EUR: Currency = Currency.getInstance("EUR")
+@JvmField val CHF: Currency = Currency.getInstance("CHF")
+@JvmField val JPY: Currency = Currency.getInstance("JPY")
+@JvmField val RUB: Currency = Currency.getInstance("RUB")
+
+fun AMOUNT(amount: Int, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token)
+fun AMOUNT(amount: Double, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount), token)
+fun DOLLARS(amount: Int): Amount = AMOUNT(amount, USD)
+fun DOLLARS(amount: Double): Amount = AMOUNT(amount, USD)
+fun POUNDS(amount: Int): Amount = AMOUNT(amount, GBP)
+fun SWISS_FRANCS(amount: Int): Amount = AMOUNT(amount, CHF)
+
+val Int.DOLLARS: Amount get() = DOLLARS(this)
+val Double.DOLLARS: Amount get() = DOLLARS(this)
+val Int.POUNDS: Amount get() = POUNDS(this)
+val Int.SWISS_FRANCS: Amount get() = SWISS_FRANCS(this)
+
+infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
+infix fun Amount.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
+infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
+infix fun Amount.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit))
+
diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaper.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaper.kt
new file mode 100644
index 0000000000..c8ec2eb1fd
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaper.kt
@@ -0,0 +1,197 @@
+package com.r3.corda.enterprise.perftestcordapp.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 com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
+import com.r3.corda.enterprise.perftestcordapp.schemas.CommercialPaperSchemaV1
+import com.r3.corda.enterprise.perftestcordapp.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 = "com.r3.corda.enterprise.perftestcordapp.contracts.CommercialPaper"
+
+// TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance.
+class CommercialPaper : Contract {
+ companion object {
+ const val CP_PROGRAM_ID: ContractClassName = "com.r3.corda.enterprise.perftestcordapp.contracts.CommercialPaper"
+ }
+ data class State(
+ val issuance: PartyAndReference,
+ override val owner: AbstractParty,
+ val faceValue: Amount>,
+ val maturityDate: Instant
+ ) : OwnableState, QueryableState{
+ 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,
+ fun withOwner(newOwner: AbstractParty): State = copy(owner = newOwner)
+
+ fun withFaceValue(newFaceValue: Amount>): State = copy(faceValue = newFaceValue)
+ fun withMaturityDate(newMaturityDate: Instant): State = copy(maturityDate = newMaturityDate)
+
+ /** Object Relational Mapping support. */
+ override fun supportedSchemas(): Iterable = listOf(CommercialPaperSchemaV1)
+ /** Additional used schemas would be added here (eg. CommercialPaperV2, ...) */
+
+ /** Object Relational Mapping support. */
+ override fun generateMappedObject(schema: MappedSchema): PersistentState {
+ return when (schema) {
+ is CommercialPaperSchemaV1 -> CommercialPaperSchemaV1.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.
+ Cash.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/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/Cash.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/Cash.kt
new file mode 100644
index 0000000000..583207edcd
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/Cash.kt
@@ -0,0 +1,351 @@
+// So the static extension functions get put into a class with a better name than CashKt
+package com.r3.corda.enterprise.perftestcordapp.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.toBase58String
+import com.r3.corda.enterprise.perftestcordapp.schemas.CashSchemaV1
+import com.r3.corda.enterprise.perftestcordapp.utils.sumCash
+import com.r3.corda.enterprise.perftestcordapp.utils.sumCashOrNull
+import com.r3.corda.enterprise.perftestcordapp.utils.sumCashOrZero
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection
+import java.math.BigInteger
+import java.security.PublicKey
+import java.util.*
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+//
+// Cash
+//
+
+/**
+ * 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 Cash : OnLedgerAsset() {
+ 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))
+ infix fun ownedBy(owner: AbstractParty) = copy(owner = owner)
+ infix fun issuedBy(party: AbstractParty) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = amount.token.issuer.copy(party = party))))
+ infix fun issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = deposit)))
+ infix fun withDeposit(deposit: PartyAndReference): Cash.State = copy(amount = amount.copy(token = amount.token.copy(issuer = deposit)))
+
+ /** Object Relational Mapping support. */
+ override fun generateMappedObject(schema: MappedSchema): PersistentState {
+ return when (schema) {
+ is CashSchemaV1 -> CashSchemaV1.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(CashSchemaV1)
+ /** 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: Cash.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 = "com.r3.corda.enterprise.perftestcordapp.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 = AbstractCashSelection.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 OnLedgerAsset.generateSpend(tx, payments, acceptableCoins,
+ changeIdentity.party.anonymise(),
+ { state, quantity, owner -> deriveState(state, quantity, owner) },
+ { Cash().generateMoveCommand() })
+ }
+ }
+}
+
+// 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: Cash.State get() = Cash.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: Cash.State get() = Cash.State(this, NULL_PARTY)
diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/OnLedgerAsset.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/OnLedgerAsset.kt
new file mode 100644
index 0000000000..190e3c6f7f
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/OnLedgerAsset.kt
@@ -0,0 +1,374 @@
+package com.r3.corda.enterprise.perftestcordapp.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 OnLedgerAsset> : 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
+ @Deprecated("Replaced with generateExit() which takes in a party to pay change to")
+ 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().firstOrNull() ?: throw InsufficientBalanceException(amountIssued)
+ return generateExit(tx, amountIssued, assetStates, owner, deriveState, generateMoveCommand, generateExitCommand)
+ }
+
+ /**
+ * 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.
+ * @param payChangeTo party to pay any change to; this is normally a confidential identity of the calling
+ * party.
+ * @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>,
+ payChangeTo: AbstractParty,
+ deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState,
+ generateMoveCommand: () -> CommandData,
+ generateExitCommand: (Amount>) -> CommandData): Set {
+ require(assetStates.isNotEmpty()) { "List of states to exit cannot be empty." }
+ 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, payChangeTo))
+ } 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.
+ * @param payChangeTo party to pay any change to; this is normally a confidential identity of the calling
+ * party.
+ * @return the public keys which must sign the transaction for it to be valid.
+ */
+ @Throws(InsufficientBalanceException::class)
+ @Deprecated("Replaced with generateExit() which takes in a party to pay change to")
+ 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) }
+ )
+ }
+
+ /**
+ * 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>,
+ payChangeTo: AbstractParty): Set {
+ return generateExit(
+ tx,
+ amountIssued,
+ assetStates,
+ payChangeTo,
+ 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/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/AbstractCashSelection.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/AbstractCashSelection.kt
new file mode 100644
index 0000000000..8241e3c607
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/AbstractCashSelection.kt
@@ -0,0 +1,168 @@
+package com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection
+
+import co.paralleluniverse.fibers.Suspendable
+import net.corda.core.contracts.Amount
+import net.corda.core.contracts.StateAndRef
+import net.corda.core.contracts.StateRef
+import net.corda.core.contracts.TransactionState
+import net.corda.core.crypto.SecureHash
+import net.corda.core.flows.FlowLogic
+import net.corda.core.identity.AbstractParty
+import net.corda.core.identity.Party
+import net.corda.core.node.ServiceHub
+import net.corda.core.node.services.StatesNotAvailableException
+import net.corda.core.serialization.SerializationDefaults
+import net.corda.core.serialization.deserialize
+import net.corda.core.utilities.*
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
+import java.sql.*
+import java.util.*
+import java.util.concurrent.atomic.AtomicReference
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * 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
+ */
+abstract class AbstractCashSelection {
+ companion object {
+ val instance = AtomicReference()
+
+ fun getInstance(metadata: () -> java.sql.DatabaseMetaData): AbstractCashSelection {
+ return instance.get() ?: {
+ val _metadata = metadata()
+ val cashSelectionAlgos = ServiceLoader.load(AbstractCashSelection::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/${AbstractCashSelection::class.java}")
+ }.invoke()
+ }
+
+ val log = loggerFor()
+ }
+
+ // coin selection retry loop counter, sleep (msecs) and lock for selecting states
+ // TODO: make parameters configurable when we get CorDapp configuration.
+ private val MAX_RETRIES = 8
+ private val RETRY_SLEEP = 100
+ private val RETRY_CAP = 2000
+ private val spendLock: ReentrantLock = ReentrantLock()
+
+ /**
+ * 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.
+ */
+ abstract fun isCompatible(metadata: DatabaseMetaData): Boolean
+
+ /**
+ * A vendor specific query(ies) to gather Cash states that are available.
+ * @param statement The service hub to allow access to the database session
+ * @param amount The amount of currency desired (ignoring issues, but specifying the currency)
+ * @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 notary If null the notary source is ignored, if specified then only states marked
+ * with this notary are included.
+ * @param onlyFromIssuerParties Optional issuer parties to match against.
+ * @param withIssuerRefs Optional issuer references to match against.
+ * @return JDBC ResultSet with 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.
+ */
+ abstract fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?,
+ onlyFromIssuerParties: Set, withIssuerRefs: Set) : ResultSet
+
+ override abstract fun toString() : String
+
+ /**
+ * Query to gather Cash states that are available and retry if they are temporarily unavailable.
+ * @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> {
+ val stateAndRefs = mutableListOf>()
+
+ for (retryCount in 1..MAX_RETRIES) {
+ if (!attemptSpend(services, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs, stateAndRefs)) {
+ log.warn("Coin selection failed on attempt $retryCount")
+ // TODO: revisit the back off strategy for contended spending.
+ if (retryCount != MAX_RETRIES) {
+ stateAndRefs.clear()
+ val durationMillis = (minOf(RETRY_SLEEP.shl(retryCount), RETRY_CAP / 2) * (1.0 + Math.random())).toInt()
+ FlowLogic.sleep(durationMillis.millis)
+ } else {
+ log.warn("Insufficient spendable states identified for $amount")
+ }
+ } else {
+ break
+ }
+ }
+ return stateAndRefs
+ }
+
+ private fun attemptSpend(services: ServiceHub, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, stateAndRefs: MutableList>): Boolean {
+ spendLock.withLock {
+ val connection = services.jdbcSession()
+ try {
+ // we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null)
+ // the softLockReserve update will detect whether we try to lock states locked by others
+ val rs = executeQuery(connection, amount, lockId, notary, onlyFromIssuerParties, withIssuerRefs)
+ stateAndRefs.clear()
+
+ var totalPennies = 0L
+ while (rs.next()) {
+ val txHash = SecureHash.parse(rs.getString(1))
+ val index = rs.getInt(2)
+ val stateRef = StateRef(txHash, index)
+ val state = rs.getBlob(3).deserialize>(context = SerializationDefaults.STORAGE_CONTEXT)
+ val pennies = rs.getLong(4)
+ totalPennies = rs.getLong(5)
+ val rowLockId = rs.getString(6)
+ stateAndRefs.add(StateAndRef(state, stateRef))
+ log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" }
+ }
+
+ if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) {
+ // we should have a minimum number of states to satisfy our selection `amount` criteria
+ log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs")
+
+ // With the current single threaded state machine available states are guaranteed to lock.
+ // TODO However, we will have to revisit these methods in the future multi-threaded.
+ services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet())
+ return true
+ }
+ log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}")
+ // retry as more states may become available
+ } catch (e: SQLException) {
+ log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId]
+ $e.
+ """)
+ } catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine
+ log.warn(e.message)
+ // retry only if there are locked states that may become available again (or consumed with change)
+ }
+ }
+ return false
+ }
+}
\ No newline at end of file
diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionH2Impl.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionH2Impl.kt
new file mode 100644
index 0000000000..557d729abb
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/cash/selection/CashSelectionH2Impl.kt
@@ -0,0 +1,67 @@
+package com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection
+
+import net.corda.core.contracts.Amount
+import net.corda.core.identity.AbstractParty
+import net.corda.core.identity.Party
+import net.corda.core.utilities.*
+import java.sql.Connection
+import java.sql.DatabaseMetaData
+import java.sql.ResultSet
+import java.util.*
+
+class CashSelectionH2Impl : AbstractCashSelection() {
+
+ companion object {
+ const val JDBC_DRIVER_NAME = "H2 JDBC Driver"
+ val log = loggerFor()
+ }
+
+ override fun isCompatible(metadata: DatabaseMetaData): Boolean {
+ return metadata.driverName == JDBC_DRIVER_NAME
+ }
+
+ override fun toString() = "${this::class.java} for $JDBC_DRIVER_NAME"
+
+
+ // We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins:
+ // 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the
+ // running total of such an accumulator
+ // 2) H2 uses session variables to perform this accumulator function:
+ // http://www.h2database.com/html/functions.html#set
+ // 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries)
+ override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?,
+ onlyFromIssuerParties: Set, withIssuerRefs: Set) : ResultSet {
+ connection.createStatement().execute("CALL SET(@t, 0);")
+
+ val selectJoin = """
+ SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
+ FROM vault_states AS vs, contract_pt_cash_states AS ccs
+ WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
+ AND vs.state_status = 0
+ AND ccs.ccy_code = ? and @t < ?
+ AND (vs.lock_id = ? OR vs.lock_id is null)
+ """ +
+ (if (notary != null)
+ " AND vs.notary_name = ?" else "") +
+ (if (onlyFromIssuerParties.isNotEmpty())
+ " AND ccs.issuer_key IN (?)" else "") +
+ (if (withIssuerRefs.isNotEmpty())
+ " AND ccs.issuer_ref IN (?)" else "")
+
+ // Use prepared statement for protection against SQL Injection (http://www.h2database.com/html/advanced.html#sql_injection)
+ val psSelectJoin = connection.prepareStatement(selectJoin)
+ var pIndex = 0
+ psSelectJoin.setString(++pIndex, amount.token.currencyCode)
+ psSelectJoin.setLong(++pIndex, amount.quantity)
+ psSelectJoin.setString(++pIndex, lockId.toString())
+ if (notary != null)
+ psSelectJoin.setString(++pIndex, notary.name.toString())
+ if (onlyFromIssuerParties.isNotEmpty())
+ psSelectJoin.setObject(++pIndex, onlyFromIssuerParties.map { it.owningKey.toBase58String() as Any}.toTypedArray() )
+ if (withIssuerRefs.isNotEmpty())
+ psSelectJoin.setObject(++pIndex, withIssuerRefs.map { it.bytes.toHexString() as Any }.toTypedArray())
+ log.debug { psSelectJoin.toString() }
+
+ return psSelectJoin.executeQuery()
+ }
+}
\ No newline at end of file
diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/AbstractCashFlow.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/AbstractCashFlow.kt
new file mode 100644
index 0000000000..119b99d0ea
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/AbstractCashFlow.kt
@@ -0,0 +1,53 @@
+package com.r3.corda.enterprise.perftestcordapp.flows
+
+import co.paralleluniverse.fibers.Suspendable
+import net.corda.core.contracts.Amount
+import net.corda.core.flows.FinalityFlow
+import net.corda.core.flows.FlowException
+import net.corda.core.flows.FlowLogic
+import net.corda.core.flows.NotaryException
+import net.corda.core.identity.AbstractParty
+import net.corda.core.identity.Party
+import net.corda.core.serialization.CordaSerializable
+import net.corda.core.transactions.SignedTransaction
+import net.corda.core.utilities.ProgressTracker
+import java.util.*
+
+/**
+ * Initiates a flow that produces an Issue/Move or Exit Cash transaction.
+ */
+abstract class AbstractCashFlow(override val progressTracker: ProgressTracker) : FlowLogic() {
+ companion object {
+ object GENERATING_ID : ProgressTracker.Step("Generating anonymous identities")
+ object GENERATING_TX : ProgressTracker.Step("Generating transaction")
+ object SIGNING_TX : ProgressTracker.Step("Signing transaction")
+ object FINALISING_TX : ProgressTracker.Step("Finalising transaction")
+
+ fun tracker() = ProgressTracker(GENERATING_ID, GENERATING_TX, SIGNING_TX, FINALISING_TX)
+ }
+
+ @Suspendable
+ protected fun finaliseTx(tx: SignedTransaction, extraParticipants: Set, message: String): SignedTransaction {
+ try {
+ return subFlow(FinalityFlow(tx, extraParticipants))
+ } catch (e: NotaryException) {
+ throw CashException(message, e)
+ }
+ }
+
+ /**
+ * Combined signed transaction and identity lookup map, which is the resulting data from regular cash flows.
+ * Specialised flows for unit tests differ from this.
+ *
+ * @param stx the signed transaction.
+ * @param recipient the identity used for the other side of the transaction, where applicable (i.e. this is
+ * null for exit transactions). For anonymous transactions this is the confidential identity generated for the
+ * transaction, otherwise this is the well known identity.
+ */
+ @CordaSerializable
+ data class Result(val stx: SignedTransaction, val recipient: AbstractParty?)
+
+ abstract class AbstractRequest(val amount: Amount)
+}
+
+class CashException(message: String, cause: Throwable) : FlowException(message, cause)
\ No newline at end of file
diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashConfigDataFlow.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashConfigDataFlow.kt
new file mode 100644
index 0000000000..4bb1af7659
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashConfigDataFlow.kt
@@ -0,0 +1,61 @@
+package com.r3.corda.enterprise.perftestcordapp.flows
+
+import co.paralleluniverse.fibers.Suspendable
+import com.typesafe.config.ConfigFactory
+import net.corda.core.flows.FlowLogic
+import net.corda.core.flows.StartableByRPC
+import net.corda.core.internal.declaredField
+import net.corda.core.internal.div
+import net.corda.core.internal.read
+import net.corda.core.node.AppServiceHub
+import net.corda.core.node.services.CordaService
+import net.corda.core.serialization.CordaSerializable
+import net.corda.core.serialization.SingletonSerializeAsToken
+import com.r3.corda.enterprise.perftestcordapp.CHF
+import com.r3.corda.enterprise.perftestcordapp.EUR
+import com.r3.corda.enterprise.perftestcordapp.GBP
+import com.r3.corda.enterprise.perftestcordapp.USD
+import com.r3.corda.enterprise.perftestcordapp.flows.ConfigHolder.Companion.supportedCurrencies
+import java.nio.file.Path
+import java.util.*
+
+// TODO Until apps have access to their own config, we'll hack things by first getting the baseDirectory, read the node.conf
+// again to get our config and store it here for access by our flow
+@CordaService
+class ConfigHolder(services: AppServiceHub) : SingletonSerializeAsToken() {
+ companion object {
+ val supportedCurrencies = listOf(USD, GBP, CHF, EUR)
+ }
+
+ val issuableCurrencies: List
+
+ init {
+ // Warning!! You are about to see a major hack!
+ val baseDirectory = services.declaredField("serviceHub").value
+ .let { it.javaClass.getMethod("getConfiguration").apply { isAccessible = true }.invoke(it) }
+ .declaredField("baseDirectory").value
+ val config = (baseDirectory / "node.conf").read { ConfigFactory.parseReader(it.reader()) }
+ if (config.hasPath("issuableCurrencies")) {
+ issuableCurrencies = config.getStringList("issuableCurrencies").map { Currency.getInstance(it) }
+ require(supportedCurrencies.containsAll(issuableCurrencies))
+ } else {
+ issuableCurrencies = emptyList()
+ }
+ }
+}
+
+
+/**
+ * Flow to obtain cash cordapp app configuration.
+ */
+@StartableByRPC
+class CashConfigDataFlow : FlowLogic() {
+ @Suspendable
+ override fun call(): CashConfiguration {
+ val configHolder = serviceHub.cordaService(ConfigHolder::class.java)
+ return CashConfiguration(configHolder.issuableCurrencies, supportedCurrencies)
+ }
+}
+
+@CordaSerializable
+data class CashConfiguration(val issuableCurrencies: List, val supportedCurrencies: List)
diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashExitFlow.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashExitFlow.kt
new file mode 100644
index 0000000000..fad3ec7b58
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashExitFlow.kt
@@ -0,0 +1,82 @@
+package com.r3.corda.enterprise.perftestcordapp.flows
+
+import co.paralleluniverse.fibers.Suspendable
+import net.corda.core.contracts.Amount
+import net.corda.core.contracts.InsufficientBalanceException
+import net.corda.core.flows.StartableByRPC
+import net.corda.core.identity.Party
+import net.corda.core.node.services.queryBy
+import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM
+import net.corda.core.node.services.vault.PageSpecification
+import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
+import net.corda.core.serialization.CordaSerializable
+import net.corda.core.transactions.TransactionBuilder
+import net.corda.core.utilities.OpaqueBytes
+import net.corda.core.utilities.ProgressTracker
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection
+import com.r3.corda.enterprise.perftestcordapp.issuedBy
+import java.util.*
+
+/**
+ * Initiates a flow that produces an cash exit transaction.
+ *
+ * @param amount the amount of a currency to remove from the ledger.
+ * @param issuerRef the reference on the issued currency. Added to the node's legal identity to determine the
+ * issuer.
+ */
+@StartableByRPC
+class CashExitFlow(private val amount: Amount,
+ private val issuerRef: OpaqueBytes,
+ progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) {
+ constructor(amount: Amount, issueRef: OpaqueBytes) : this(amount, issueRef, tracker())
+ constructor(request: ExitRequest) : this(request.amount, request.issueRef, tracker())
+
+ companion object {
+ fun tracker() = ProgressTracker(GENERATING_TX, SIGNING_TX, FINALISING_TX)
+ }
+
+ /**
+ * @return the signed transaction, and a mapping of parties to new anonymous identities generated
+ * (for this flow this map is always empty).
+ */
+ @Suspendable
+ @Throws(CashException::class)
+ override fun call(): AbstractCashFlow.Result {
+ progressTracker.currentStep = GENERATING_TX
+ val builder = TransactionBuilder(notary = null)
+ val issuer = ourIdentity.ref(issuerRef)
+ val exitStates = AbstractCashSelection
+ .getInstance { serviceHub.jdbcSession().metaData }
+ .unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference))
+ val signers = try {
+ Cash().generateExit(
+ builder,
+ amount.issuedBy(issuer),
+ exitStates)
+ } catch (e: InsufficientBalanceException) {
+ throw CashException("Exiting more cash than exists", e)
+ }
+
+ // Work out who the owners of the burnt states were (specify page size so we don't silently drop any if > DEFAULT_PAGE_SIZE)
+ val inputStates = serviceHub.vaultService.queryBy(VaultQueryCriteria(stateRefs = builder.inputStates()),
+ PageSpecification(pageNumber = DEFAULT_PAGE_NUM, pageSize = builder.inputStates().size)).states
+
+ // TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them
+ // count as a reason to fail?
+ val participants: Set = inputStates
+ .mapNotNull { serviceHub.identityService.wellKnownPartyFromAnonymous(it.state.data.owner) }
+ .toSet()
+ // Sign transaction
+ progressTracker.currentStep = SIGNING_TX
+ val tx = serviceHub.signInitialTransaction(builder, signers)
+
+ // Commit the transaction
+ progressTracker.currentStep = FINALISING_TX
+ val notarised = finaliseTx(tx, participants, "Unable to notarise exit")
+ return Result(notarised, null)
+ }
+
+ @CordaSerializable
+ class ExitRequest(amount: Amount, val issueRef: OpaqueBytes) : AbstractRequest(amount)
+}
diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentFlow.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentFlow.kt
new file mode 100644
index 0000000000..97708a0891
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueAndPaymentFlow.kt
@@ -0,0 +1,49 @@
+package com.r3.corda.enterprise.perftestcordapp.flows
+
+import co.paralleluniverse.fibers.Suspendable
+import net.corda.core.contracts.Amount
+import net.corda.core.flows.StartableByRPC
+import net.corda.core.identity.Party
+import net.corda.core.serialization.CordaSerializable
+import net.corda.core.utilities.OpaqueBytes
+import net.corda.core.utilities.ProgressTracker
+import java.util.*
+
+/**
+ * Initiates a flow that self-issues cash (which should then be sent to recipient(s) using a payment transaction).
+ *
+ * We issue cash only to ourselves so that all KYC/AML checks on payments are enforced consistently, rather than risk
+ * checks for issuance and payments differing. Outside of test scenarios it would be extremely unusual to issue cash
+ * and immediately transfer it, so impact of this limitation is considered minimal.
+ *
+ * @param amount the amount of currency to issue.
+ * @param issuerBankPartyRef a reference to put on the issued currency.
+ * @param notary the notary to set on the output states.
+ */
+@StartableByRPC
+class CashIssueAndPaymentFlow(val amount: Amount,
+ val issueRef: OpaqueBytes,
+ val recipient: Party,
+ val anonymous: Boolean,
+ val notary: Party,
+ progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) {
+ constructor(amount: Amount,
+ issueRef: OpaqueBytes,
+ recipient: Party,
+ anonymous: Boolean,
+ notary: Party) : this(amount, issueRef, recipient, anonymous, notary, tracker())
+ constructor(request: IssueAndPaymentRequest) : this(request.amount, request.issueRef, request.recipient, request.anonymous, request.notary, tracker())
+
+ @Suspendable
+ override fun call(): Result {
+ subFlow(CashIssueFlow(amount, issueRef, notary))
+ return subFlow(CashPaymentFlow(amount, recipient, anonymous))
+ }
+
+ @CordaSerializable
+ class IssueAndPaymentRequest(amount: Amount,
+ val issueRef: OpaqueBytes,
+ val recipient: Party,
+ val notary: Party,
+ val anonymous: Boolean) : AbstractRequest(amount)
+}
\ No newline at end of file
diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueFlow.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueFlow.kt
new file mode 100644
index 0000000000..d6f71fa1db
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueFlow.kt
@@ -0,0 +1,52 @@
+package com.r3.corda.enterprise.perftestcordapp.flows
+
+import co.paralleluniverse.fibers.Suspendable
+import net.corda.core.contracts.Amount
+import net.corda.core.flows.StartableByRPC
+import net.corda.core.identity.Party
+import net.corda.core.serialization.CordaSerializable
+import net.corda.core.transactions.TransactionBuilder
+import net.corda.core.utilities.OpaqueBytes
+import net.corda.core.utilities.ProgressTracker
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
+import com.r3.corda.enterprise.perftestcordapp.issuedBy
+import java.util.*
+
+/**
+ * Initiates a flow that self-issues cash (which should then be sent to recipient(s) using a payment transaction).
+ *
+ * We issue cash only to ourselves so that all KYC/AML checks on payments are enforced consistently, rather than risk
+ * checks for issuance and payments differing. Outside of test scenarios it would be extremely unusual to issue cash
+ * and immediately transfer it, so impact of this limitation is considered minimal.
+ *
+ * @param amount the amount of currency to issue.
+ * @param issuerBankPartyRef a reference to put on the issued currency.
+ * @param notary the notary to set on the output states.
+ */
+@StartableByRPC
+class CashIssueFlow(private val amount: Amount,
+ private val issuerBankPartyRef: OpaqueBytes,
+ private val notary: Party,
+ progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) {
+ constructor(amount: Amount,
+ issuerBankPartyRef: OpaqueBytes,
+ notary: Party) : this(amount, issuerBankPartyRef, notary, tracker())
+ constructor(request: IssueRequest) : this(request.amount, request.issueRef, request.notary, tracker())
+
+ @Suspendable
+ override fun call(): AbstractCashFlow.Result {
+ progressTracker.currentStep = GENERATING_TX
+ val builder = TransactionBuilder(notary)
+ val issuer = ourIdentity.ref(issuerBankPartyRef)
+ val signers = Cash().generateIssue(builder, amount.issuedBy(issuer), ourIdentity, notary)
+ progressTracker.currentStep = SIGNING_TX
+ val tx = serviceHub.signInitialTransaction(builder, signers)
+ progressTracker.currentStep = FINALISING_TX
+ // There is no one to send the tx to as we're the only participants
+ val notarised = finaliseTx(tx, emptySet(), "Unable to notarise issue")
+ return Result(notarised, ourIdentity)
+ }
+
+ @CordaSerializable
+ class IssueRequest(amount: Amount, val issueRef: OpaqueBytes, val notary: Party) : AbstractRequest(amount)
+}
diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashPaymentFlow.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashPaymentFlow.kt
new file mode 100644
index 0000000000..b60c83ced0
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashPaymentFlow.kt
@@ -0,0 +1,74 @@
+package com.r3.corda.enterprise.perftestcordapp.flows
+
+import co.paralleluniverse.fibers.Suspendable
+import net.corda.confidential.SwapIdentitiesFlow
+import net.corda.core.contracts.Amount
+import net.corda.core.contracts.InsufficientBalanceException
+import net.corda.core.flows.StartableByRPC
+import net.corda.core.identity.AnonymousParty
+import net.corda.core.identity.Party
+import net.corda.core.serialization.CordaSerializable
+import net.corda.core.transactions.TransactionBuilder
+import net.corda.core.utilities.ProgressTracker
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
+import java.util.*
+
+/**
+ * Initiates a flow that sends cash to a recipient.
+ *
+ * @param amount the amount of a currency to pay to the recipient.
+ * @param recipient the party to pay the currency to.
+ * @param issuerConstraint if specified, the payment will be made using only cash issued by the given parties.
+ * @param anonymous whether to anonymous the recipient party. Should be true for normal usage, but may be false
+ * for testing purposes.
+ */
+@StartableByRPC
+open class CashPaymentFlow(
+ val amount: Amount,
+ val recipient: Party,
+ val anonymous: Boolean,
+ progressTracker: ProgressTracker,
+ val issuerConstraint: Set = emptySet()) : AbstractCashFlow(progressTracker) {
+ /** A straightforward constructor that constructs spends using cash states of any issuer. */
+ constructor(amount: Amount, recipient: Party) : this(amount, recipient, true, tracker())
+ /** A straightforward constructor that constructs spends using cash states of any issuer. */
+ constructor(amount: Amount, recipient: Party, anonymous: Boolean) : this(amount, recipient, anonymous, tracker())
+ constructor(request: PaymentRequest) : this(request.amount, request.recipient, request.anonymous, tracker(), request.issuerConstraint)
+
+ @Suspendable
+ override fun call(): AbstractCashFlow.Result {
+ progressTracker.currentStep = GENERATING_ID
+ val txIdentities = if (anonymous) {
+ subFlow(SwapIdentitiesFlow(recipient))
+ } else {
+ emptyMap()
+ }
+ val anonymousRecipient = txIdentities[recipient] ?: recipient
+ progressTracker.currentStep = GENERATING_TX
+ val builder = TransactionBuilder(notary = null)
+ // TODO: Have some way of restricting this to states the caller controls
+ val (spendTX, keysForSigning) = try {
+ Cash.generateSpend(serviceHub,
+ builder,
+ amount,
+ ourIdentityAndCert,
+ anonymousRecipient,
+ issuerConstraint)
+ } catch (e: InsufficientBalanceException) {
+ throw CashException("Insufficient cash for spend: ${e.message}", e)
+ }
+
+ progressTracker.currentStep = SIGNING_TX
+ val tx = serviceHub.signInitialTransaction(spendTX, keysForSigning)
+
+ progressTracker.currentStep = FINALISING_TX
+ val notarised = finaliseTx(tx, setOf(recipient), "Unable to notarise spend")
+ return Result(notarised, anonymousRecipient)
+ }
+
+ @CordaSerializable
+ class PaymentRequest(amount: Amount,
+ val recipient: Party,
+ val anonymous: Boolean,
+ val issuerConstraint: Set = emptySet()) : AbstractRequest(amount)
+}
diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlow.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlow.kt
new file mode 100644
index 0000000000..7c5c615ef2
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlow.kt
@@ -0,0 +1,242 @@
+package com.r3.corda.enterprise.perftestcordapp.flows
+
+import co.paralleluniverse.fibers.Suspendable
+import net.corda.confidential.IdentitySyncFlow
+import net.corda.core.contracts.*
+import net.corda.core.flows.*
+import net.corda.core.identity.Party
+import net.corda.core.identity.PartyAndCertificate
+import net.corda.core.serialization.CordaSerializable
+import net.corda.core.transactions.SignedTransaction
+import net.corda.core.transactions.TransactionBuilder
+import net.corda.core.utilities.ProgressTracker
+import net.corda.core.utilities.seconds
+import net.corda.core.utilities.unwrap
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
+import com.r3.corda.enterprise.perftestcordapp.utils.sumCashBy
+import java.security.PublicKey
+import java.util.*
+
+/**
+ * This asset trading flow implements a "delivery vs payment" type swap. It has two parties (B and S for buyer
+ * and seller) and the following steps:
+ *
+ * 1. S sends the [StateAndRef] pointing to what they want to sell to B, along with info about the price they require
+ * B to pay. For example this has probably been agreed on an exchange.
+ * 2. B sends to S a [SignedTransaction] that includes the state as input, B's cash as input, the state with the new
+ * owner key as output, and any change cash as output. It contains a single signature from B but isn't valid because
+ * it lacks a signature from S authorising movement of the asset.
+ * 3. S signs it and commits it to the ledger, notarising it and distributing the final signed transaction back
+ * to B.
+ *
+ * Assuming no malicious termination, they both end the flow being in possession of a valid, signed transaction
+ * that represents an atomic asset swap.
+ *
+ * Note that it's the *seller* who initiates contact with the buyer, not vice-versa as you might imagine.
+ */
+object TwoPartyTradeFlow {
+ // TODO: Common elements in multi-party transaction consensus and signing should be refactored into a superclass of this
+ // and [AbstractStateReplacementFlow].
+
+ class UnacceptablePriceException(givenPrice: Amount) : FlowException("Unacceptable price: $givenPrice")
+
+ class AssetMismatchException(val expectedTypeName: String, val typeName: String) : FlowException() {
+ override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName"
+ }
+
+ /**
+ * This object is serialised to the network and is the first flow message the seller sends to the buyer.
+ *
+ * @param payToIdentity anonymous identity of the seller, for payment to be sent to.
+ */
+ @CordaSerializable
+ data class SellerTradeInfo(
+ val price: Amount,
+ val payToIdentity: PartyAndCertificate
+ )
+
+ open class Seller(private val otherSideSession: FlowSession,
+ private val assetToSell: StateAndRef,
+ private val price: Amount,
+ private val myParty: PartyAndCertificate, // TODO Left because in tests it's used to pass anonymous party.
+ override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic() {
+
+ companion object {
+ object AWAITING_PROPOSAL : ProgressTracker.Step("Awaiting transaction proposal")
+ // DOCSTART 3
+ object VERIFYING_AND_SIGNING : ProgressTracker.Step("Verifying and signing transaction proposal") {
+ override fun childProgressTracker() = SignTransactionFlow.tracker()
+ }
+ // DOCEND 3
+
+ fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING_AND_SIGNING)
+ }
+
+ // DOCSTART 4
+ @Suspendable
+ override fun call(): SignedTransaction {
+ progressTracker.currentStep = AWAITING_PROPOSAL
+ // Make the first message we'll send to kick off the flow.
+ val hello = SellerTradeInfo(price, myParty)
+ // What we get back from the other side is a transaction that *might* be valid and acceptable to us,
+ // but we must check it out thoroughly before we sign!
+ // SendTransactionFlow allows seller to access our data to resolve the transaction.
+ subFlow(SendStateAndRefFlow(otherSideSession, listOf(assetToSell)))
+ otherSideSession.send(hello)
+
+ // Verify and sign the transaction.
+ progressTracker.currentStep = VERIFYING_AND_SIGNING
+
+ // Sync identities to ensure we know all of the identities involved in the transaction we're about to
+ // be asked to sign
+ subFlow(IdentitySyncFlow.Receive(otherSideSession))
+
+ // DOCSTART 5
+ val signTransactionFlow = object : SignTransactionFlow(otherSideSession, VERIFYING_AND_SIGNING.childProgressTracker()) {
+ override fun checkTransaction(stx: SignedTransaction) {
+ // Verify that we know who all the participants in the transaction are
+ val states: Iterable = stx.tx.inputs.map { serviceHub.loadState(it).data } + stx.tx.outputs.map { it.data }
+ states.forEach { state ->
+ state.participants.forEach { anon ->
+ require(serviceHub.identityService.wellKnownPartyFromAnonymous(anon) != null) {
+ "Transaction state $state involves unknown participant $anon"
+ }
+ }
+ }
+
+ if (stx.tx.outputStates.sumCashBy(myParty.party).withoutIssuer() != price)
+ throw FlowException("Transaction is not sending us the right amount of cash")
+ }
+ }
+
+ val txId = subFlow(signTransactionFlow).id
+ // DOCEND 5
+
+ return waitForLedgerCommit(txId)
+ }
+ // DOCEND 4
+
+ // Following comment moved here so that it doesn't appear in the docsite:
+ // There are all sorts of funny games a malicious secondary might play with it sends maybeSTX,
+ // we should fix them:
+ //
+ // - This tx may attempt to send some assets we aren't intending to sell to the secondary, if
+ // we're reusing keys! So don't reuse keys!
+ // - This tx may include output states that impose odd conditions on the movement of the cash,
+ // once we implement state pairing.
+ //
+ // but the goal of this code is not to be fully secure (yet), but rather, just to find good ways to
+ // express flow state machines on top of the messaging layer.
+ }
+
+ open class Buyer(private val sellerSession: FlowSession,
+ private val notary: Party,
+ private val acceptablePrice: Amount,
+ private val typeToBuy: Class,
+ private val anonymous: Boolean) : FlowLogic() {
+ constructor(otherSideSession: FlowSession, notary: Party, acceptablePrice: Amount, typeToBuy: Class) :
+ this(otherSideSession, notary, acceptablePrice, typeToBuy, true)
+ // DOCSTART 2
+ object RECEIVING : ProgressTracker.Step("Waiting for seller trading info")
+
+ object VERIFYING : ProgressTracker.Step("Verifying seller assets")
+ object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal")
+ object COLLECTING_SIGNATURES : ProgressTracker.Step("Collecting signatures from other parties") {
+ override fun childProgressTracker() = CollectSignaturesFlow.tracker()
+ }
+
+ object RECORDING : ProgressTracker.Step("Recording completed transaction") {
+ // TODO: Currently triggers a race condition on Team City. See https://github.com/corda/corda/issues/733.
+ // override fun childProgressTracker() = FinalityFlow.tracker()
+ }
+
+ override val progressTracker = ProgressTracker(RECEIVING, VERIFYING, SIGNING, COLLECTING_SIGNATURES, RECORDING)
+ // DOCEND 2
+
+ // DOCSTART 1
+ @Suspendable
+ override fun call(): SignedTransaction {
+ // Wait for a trade request to come in from the other party.
+ progressTracker.currentStep = RECEIVING
+ val (assetForSale, tradeRequest) = receiveAndValidateTradeRequest()
+
+ // Create the identity we'll be paying to, and send the counterparty proof we own the identity
+ val buyerAnonymousIdentity = if (anonymous)
+ serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
+ else
+ ourIdentityAndCert
+ // Put together a proposed transaction that performs the trade, and sign it.
+ progressTracker.currentStep = SIGNING
+ val (ptx, cashSigningPubKeys) = assembleSharedTX(assetForSale, tradeRequest, buyerAnonymousIdentity)
+
+ // Now sign the transaction with whatever keys we need to move the cash.
+ val partSignedTx = serviceHub.signInitialTransaction(ptx, cashSigningPubKeys)
+
+ // Sync up confidential identities in the transaction with our counterparty
+ subFlow(IdentitySyncFlow.Send(sellerSession, ptx.toWireTransaction(serviceHub)))
+
+ // Send the signed transaction to the seller, who must then sign it themselves and commit
+ // it to the ledger by sending it to the notary.
+ progressTracker.currentStep = COLLECTING_SIGNATURES
+ val sellerSignature = subFlow(CollectSignatureFlow(partSignedTx, sellerSession, sellerSession.counterparty.owningKey))
+ val twiceSignedTx = partSignedTx + sellerSignature
+
+ // Notarise and record the transaction.
+ progressTracker.currentStep = RECORDING
+ return subFlow(FinalityFlow(twiceSignedTx))
+ }
+
+ @Suspendable
+ private fun receiveAndValidateTradeRequest(): Pair, SellerTradeInfo> {
+ val assetForSale = subFlow(ReceiveStateAndRefFlow(sellerSession)).single()
+ return assetForSale to sellerSession.receive().unwrap {
+ progressTracker.currentStep = VERIFYING
+ // What is the seller trying to sell us?
+ val asset = assetForSale.state.data
+ val assetTypeName = asset.javaClass.name
+
+ // The asset must either be owned by the well known identity of the counterparty, or we must be able to
+ // prove the owner is a confidential identity of the counterparty.
+ val assetForSaleIdentity = serviceHub.identityService.wellKnownPartyFromAnonymous(asset.owner)
+ require(assetForSaleIdentity == sellerSession.counterparty)
+
+ // Register the identity we're about to send payment to. This shouldn't be the same as the asset owner
+ // identity, so that anonymity is enforced.
+ val wellKnownPayToIdentity = serviceHub.identityService.verifyAndRegisterIdentity(it.payToIdentity) ?: it.payToIdentity
+ require(wellKnownPayToIdentity.party == sellerSession.counterparty) { "Well known identity to pay to must match counterparty identity" }
+
+ if (it.price > acceptablePrice)
+ throw UnacceptablePriceException(it.price)
+ if (!typeToBuy.isInstance(asset))
+ throw AssetMismatchException(typeToBuy.name, assetTypeName)
+
+ it
+ }
+ }
+
+ @Suspendable
+ private fun assembleSharedTX(assetForSale: StateAndRef, tradeRequest: SellerTradeInfo, buyerAnonymousIdentity: PartyAndCertificate): SharedTx {
+ val ptx = TransactionBuilder(notary)
+
+ // Add input and output states for the movement of cash, by using the Cash contract to generate the states
+ val (tx, cashSigningPubKeys) = Cash.generateSpend(serviceHub, ptx, tradeRequest.price, ourIdentityAndCert, tradeRequest.payToIdentity.party)
+
+ // Add inputs/outputs/a command for the movement of the asset.
+ tx.addInputState(assetForSale)
+
+ val (command, state) = assetForSale.state.data.withNewOwner(buyerAnonymousIdentity.party)
+ tx.addOutputState(state, assetForSale.state.contract, assetForSale.state.notary)
+ tx.addCommand(command, assetForSale.state.data.owner.owningKey)
+
+ // We set the transaction's time-window: it may be that none of the contracts need this!
+ // But it can't hurt to have one.
+ val currentTime = serviceHub.clock.instant()
+ tx.setTimeWindow(currentTime, 30.seconds)
+
+ return SharedTx(tx, cashSigningPubKeys)
+ }
+ // DOCEND 1
+
+ data class SharedTx(val tx: TransactionBuilder, val cashSigningPubKeys: List)
+ }
+}
diff --git a/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CashSchemaV1.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CashSchemaV1.kt
new file mode 100644
index 0000000000..9ec6f97ddf
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CashSchemaV1.kt
@@ -0,0 +1,45 @@
+package com.r3.corda.enterprise.perftestcordapp.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 CashSchema
+
+/**
+ * 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 CashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCashState::class.java)) {
+ @Entity
+ @Table(name = "contract_pt_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/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CommercialPaperSchemaV1.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CommercialPaperSchemaV1.kt
new file mode 100644
index 0000000000..71857824f9
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/schemas/CommercialPaperSchemaV1.kt
@@ -0,0 +1,52 @@
+package com.r3.corda.enterprise.perftestcordapp.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 CommercialPaperSchema
+
+/**
+ * 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 CommercialPaperSchemaV1 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, mappedTypes = listOf(PersistentCommercialPaperState::class.java)) {
+ @Entity
+ @Table(name = "cp_pt_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/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/utils/StateSummingUtilities.kt b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/utils/StateSummingUtilities.kt
new file mode 100644
index 0000000000..9a6457b370
--- /dev/null
+++ b/perftestcordapp/src/main/kotlin/com/r3/corda/enterprise/perftestcordapp/utils/StateSummingUtilities.kt
@@ -0,0 +1,40 @@
+package com.r3.corda.enterprise.perftestcordapp.utils
+
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
+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/perftestcordapp/src/main/resources/META-INF/services/com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection b/perftestcordapp/src/main/resources/META-INF/services/com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection
new file mode 100644
index 0000000000..c9ce8a6ee9
--- /dev/null
+++ b/perftestcordapp/src/main/resources/META-INF/services/com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.AbstractCashSelection
@@ -0,0 +1,2 @@
+com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.CashSelectionH2Impl
+
diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaperTests.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaperTests.kt
new file mode 100644
index 0000000000..dade7aa141
--- /dev/null
+++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/CommercialPaperTests.kt
@@ -0,0 +1,302 @@
+package com.r3.corda.enterprise.perftestcordapp.contracts
+
+import net.corda.core.contracts.*
+import net.corda.core.identity.AnonymousParty
+import net.corda.core.identity.Party
+import net.corda.core.node.services.Vault
+import net.corda.core.node.services.VaultService
+import net.corda.core.transactions.SignedTransaction
+import net.corda.core.transactions.TransactionBuilder
+import net.corda.core.utilities.days
+import net.corda.core.utilities.seconds
+import com.r3.corda.enterprise.perftestcordapp.DOLLARS
+import com.r3.corda.enterprise.perftestcordapp.`issued by`
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.*
+import net.corda.testing.*
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.fillWithSomeTestCash
+import net.corda.testing.node.MockServices
+import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.time.Instant
+import java.util.*
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+
+// TODO: The generate functions aren't tested by these tests: add them.
+
+interface CommercialPaperTestTemplate {
+ fun getPaper(): CommercialPaper.State
+ fun getIssueCommand(notary: Party): CommandData
+ fun getRedeemCommand(notary: Party): CommandData
+ fun getMoveCommand(): CommandData
+ fun getContract(): ContractClassName
+}
+
+
+class KotlinCommercialPaperTest : CommercialPaperTestTemplate {
+ override fun getPaper(): CommercialPaper.State = CommercialPaper.State(
+ issuance = MEGA_CORP.ref(123),
+ owner = MEGA_CORP,
+ faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123),
+ maturityDate = TEST_TX_TIME + 7.days
+ )
+
+ override fun getIssueCommand(notary: Party): CommandData = CommercialPaper.Commands.Issue()
+ override fun getRedeemCommand(notary: Party): CommandData = CommercialPaper.Commands.Redeem()
+ override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move()
+ override fun getContract() = CommercialPaper.CP_PROGRAM_ID
+}
+
+class KotlinCommercialPaperLegacyTest : CommercialPaperTestTemplate {
+ override fun getPaper(): CommercialPaper.State = CommercialPaper.State(
+ issuance = MEGA_CORP.ref(123),
+ owner = MEGA_CORP,
+ faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123),
+ maturityDate = TEST_TX_TIME + 7.days
+ )
+
+ override fun getIssueCommand(notary: Party): CommandData = CommercialPaper.Commands.Issue()
+ override fun getRedeemCommand(notary: Party): CommandData = CommercialPaper.Commands.Redeem()
+ override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move()
+ override fun getContract() = CommercialPaper.CP_PROGRAM_ID
+}
+
+@RunWith(Parameterized::class)
+class CommercialPaperTestsGeneric {
+ companion object {
+ @Parameterized.Parameters @JvmStatic
+ fun data() = listOf(KotlinCommercialPaperTest(), KotlinCommercialPaperLegacyTest())
+ }
+
+ @Parameterized.Parameter
+ lateinit var thisTest: CommercialPaperTestTemplate
+
+ val issuer = MEGA_CORP.ref(123)
+
+ @Test
+ fun `trade lifecycle test`() {
+ val someProfits = 1200.DOLLARS `issued by` issuer
+ ledger {
+ unverifiedTransaction {
+ attachment(Cash.PROGRAM_ID)
+ output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE)
+ output(Cash.PROGRAM_ID, "some profits", someProfits.STATE ownedBy MEGA_CORP)
+ }
+
+ // Some CP is issued onto the ledger by MegaCorp.
+ transaction("Issuance") {
+ attachments(CP_PROGRAM_ID, CommercialPaper.CP_PROGRAM_ID)
+ output(thisTest.getContract(), "paper") { thisTest.getPaper() }
+ command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
+ timeWindow(TEST_TX_TIME)
+ this.verifies()
+ }
+
+ // The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days,
+ // that sounds a bit too good to be true!
+ transaction("Trade") {
+ attachments(Cash.PROGRAM_ID, CommercialPaper.CP_PROGRAM_ID)
+ input("paper")
+ input("alice's $900")
+ output(Cash.PROGRAM_ID, "borrowed $900") { 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP }
+ output(thisTest.getContract(), "alice's paper") { "paper".output().withOwner(ALICE) }
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ command(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() }
+ this.verifies()
+ }
+
+ // Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
+ // as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change.
+ transaction("Redemption") {
+ attachments(CP_PROGRAM_ID, CommercialPaper.CP_PROGRAM_ID)
+ input("alice's paper")
+ input("some profits")
+
+ fun TransactionDSL.outputs(aliceGetsBack: Amount>) {
+ output(Cash.PROGRAM_ID, "Alice's profit") { aliceGetsBack.STATE ownedBy ALICE }
+ output(Cash.PROGRAM_ID, "Change") { (someProfits - aliceGetsBack).STATE ownedBy MEGA_CORP }
+ }
+
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
+ command(ALICE_PUBKEY) { thisTest.getRedeemCommand(DUMMY_NOTARY) }
+
+ tweak {
+ outputs(700.DOLLARS `issued by` issuer)
+ timeWindow(TEST_TX_TIME + 8.days)
+ this `fails with` "received amount equals the face value"
+ }
+ outputs(1000.DOLLARS `issued by` issuer)
+
+
+ tweak {
+ timeWindow(TEST_TX_TIME + 2.days)
+ this `fails with` "must have matured"
+ }
+ timeWindow(TEST_TX_TIME + 8.days)
+
+ tweak {
+ output(thisTest.getContract()) { "paper".output() }
+ this `fails with` "must be destroyed"
+ }
+
+ this.verifies()
+ }
+ }
+ }
+
+ @Test
+ fun `key mismatch at issue`() {
+ transaction {
+ attachment(CP_PROGRAM_ID)
+ attachment(CP_PROGRAM_ID)
+ output(thisTest.getContract()) { thisTest.getPaper() }
+ command(MINI_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
+ timeWindow(TEST_TX_TIME)
+ this `fails with` "output states are issued by a command signer"
+ }
+ }
+
+ @Test
+ fun `face value is not zero`() {
+ transaction {
+ attachment(CP_PROGRAM_ID)
+ attachment(CP_PROGRAM_ID)
+ output(thisTest.getContract()) { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) }
+ command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
+ timeWindow(TEST_TX_TIME)
+ this `fails with` "output values sum to more than the inputs"
+ }
+ }
+
+ @Test
+ fun `maturity date not in the past`() {
+ transaction {
+ attachment(CP_PROGRAM_ID)
+ attachment(CP_PROGRAM_ID)
+ output(thisTest.getContract()) { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) }
+ command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
+ timeWindow(TEST_TX_TIME)
+ this `fails with` "maturity date is not in the past"
+ }
+ }
+
+ @Test
+ fun `issue cannot replace an existing state`() {
+ transaction {
+ attachment(CP_PROGRAM_ID)
+ attachment(CP_PROGRAM_ID)
+ input(thisTest.getContract(), thisTest.getPaper())
+ output(thisTest.getContract()) { thisTest.getPaper() }
+ command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) }
+ timeWindow(TEST_TX_TIME)
+ this `fails with` "output values sum to more than the inputs"
+ }
+ }
+
+ /**
+ * Unit test requires two separate Database instances to represent each of the two
+ * transaction participants (enforces uniqueness of vault content in lieu of partipant identity)
+ */
+
+ private lateinit var bigCorpServices: MockServices
+ private lateinit var bigCorpVault: Vault
+ private lateinit var bigCorpVaultService: VaultService
+
+ private lateinit var aliceServices: MockServices
+ private lateinit var aliceVaultService: VaultService
+ private lateinit var alicesVault: Vault
+
+ private val notaryServices = MockServices(DUMMY_NOTARY_KEY)
+ private val issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY)
+
+ private lateinit var moveTX: SignedTransaction
+
+ // @Test
+ @Ignore
+ fun `issue move and then redeem`() {
+ initialiseTestSerialization()
+ val aliceDatabaseAndServices = MockServices.makeTestDatabaseAndMockServices(keys = listOf(ALICE_KEY))
+ val databaseAlice = aliceDatabaseAndServices.first
+ aliceServices = aliceDatabaseAndServices.second
+ aliceVaultService = aliceServices.vaultService
+
+ databaseAlice.transaction {
+ alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, issuerServices, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = DUMMY_CASH_ISSUER)
+ aliceVaultService = aliceServices.vaultService
+ }
+
+ val bigCorpDatabaseAndServices = MockServices.makeTestDatabaseAndMockServices(keys = listOf(BIG_CORP_KEY))
+ val databaseBigCorp = bigCorpDatabaseAndServices.first
+ bigCorpServices = bigCorpDatabaseAndServices.second
+ bigCorpVaultService = bigCorpServices.vaultService
+
+ databaseBigCorp.transaction {
+ bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, issuerServices, atLeastThisManyStates = 1, atMostThisManyStates = 1, issuedBy = DUMMY_CASH_ISSUER)
+ bigCorpVaultService = bigCorpServices.vaultService
+ }
+
+ // Propagate the cash transactions to each side.
+ aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.validatedTransactions.getTransaction(it.ref.txhash)!! })
+ bigCorpServices.recordTransactions(alicesVault.states.map { aliceServices.validatedTransactions.getTransaction(it.ref.txhash)!! })
+
+ // BigCorpâ„¢ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
+ val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER
+ val issuance = bigCorpServices.myInfo.chooseIdentity().ref(1)
+ val issueBuilder = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY)
+ issueBuilder.setTimeWindow(TEST_TX_TIME, 30.seconds)
+ val issuePtx = bigCorpServices.signInitialTransaction(issueBuilder)
+ val issueTx = notaryServices.addSignature(issuePtx)
+
+ databaseAlice.transaction {
+ // Alice pays $9000 to BigCorp to own some of their debt.
+ moveTX = run {
+ val builder = TransactionBuilder(DUMMY_NOTARY)
+ Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public))
+ CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public))
+ val ptx = aliceServices.signInitialTransaction(builder)
+ val ptx2 = bigCorpServices.addSignature(ptx)
+ val stx = notaryServices.addSignature(ptx2)
+ stx
+ }
+ }
+
+ databaseBigCorp.transaction {
+ // Verify the txns are valid and insert into both sides.
+ listOf(issueTx, moveTX).forEach {
+ it.toLedgerTransaction(aliceServices).verify()
+ aliceServices.recordTransactions(it)
+ bigCorpServices.recordTransactions(it)
+ }
+ }
+
+ databaseBigCorp.transaction {
+ fun makeRedeemTX(time: Instant): Pair {
+ val builder = TransactionBuilder(DUMMY_NOTARY)
+ builder.setTimeWindow(time, 30.seconds)
+ CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpServices, bigCorpServices.myInfo.chooseIdentityAndCert())
+ val ptx = aliceServices.signInitialTransaction(builder)
+ val ptx2 = bigCorpServices.addSignature(ptx)
+ val stx = notaryServices.addSignature(ptx2)
+ return Pair(stx, builder.lockId)
+ }
+
+ val redeemTX = makeRedeemTX(TEST_TX_TIME + 10.days)
+ val tooEarlyRedemption = redeemTX.first
+ val tooEarlyRedemptionLockId = redeemTX.second
+ val e = assertFailsWith(TransactionVerificationException::class) {
+ tooEarlyRedemption.toLedgerTransaction(aliceServices).verify()
+ }
+ // manually release locks held by this failing transaction
+ aliceServices.vaultService.softLockRelease(tooEarlyRedemptionLockId)
+ assertTrue(e.cause!!.message!!.contains("paper must have matured"))
+
+ val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days).first
+ validRedemption.toLedgerTransaction(aliceServices).verify()
+ // soft lock not released after success either!!! (as transaction not recorded)
+ }
+ resetTestSerialization()
+ }
+}
diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashSelectionH2Test.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashSelectionH2Test.kt
new file mode 100644
index 0000000000..8929387a9e
--- /dev/null
+++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashSelectionH2Test.kt
@@ -0,0 +1,40 @@
+package com.r3.corda.enterprise.perftestcordapp.contracts.asset
+
+import net.corda.core.utilities.getOrThrow
+import com.r3.corda.enterprise.perftestcordapp.DOLLARS
+import com.r3.corda.enterprise.perftestcordapp.flows.CashException
+import com.r3.corda.enterprise.perftestcordapp.flows.CashPaymentFlow
+import net.corda.testing.chooseIdentity
+import net.corda.testing.node.MockNetwork
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.junit.Test
+
+
+class CashSelectionH2Test {
+
+ @Test
+ fun `check does not hold connection over retries`() {
+ val mockNet = MockNetwork(threadPerNode = true)
+ try {
+ val notaryNode = mockNet.createNotaryNode()
+ val bankA = mockNet.createNode(configOverrides = { existingConfig ->
+ // Tweak connections to be minimal to make this easier (1 results in a hung node during start up, so use 2 connections).
+ existingConfig.dataSourceProperties.setProperty("maximumPoolSize", "2")
+ existingConfig
+ })
+
+ mockNet.startNodes()
+
+ // Start more cash spends than we have connections. If spend leaks a connection on retry, we will run out of connections.
+ val flow1 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notaryNode.info.chooseIdentity()))
+ val flow2 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notaryNode.info.chooseIdentity()))
+ val flow3 = bankA.services.startFlow(CashPaymentFlow(amount = 100.DOLLARS, anonymous = false, recipient = notaryNode.info.chooseIdentity()))
+
+ assertThatThrownBy { flow1.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java)
+ assertThatThrownBy { flow2.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java)
+ assertThatThrownBy { flow3.resultFuture.getOrThrow() }.isInstanceOf(CashException::class.java)
+ } finally {
+ mockNet.stopNodes()
+ }
+ }
+}
\ No newline at end of file
diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashTests.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashTests.kt
new file mode 100644
index 0000000000..8a67039948
--- /dev/null
+++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/contracts/asset/CashTests.kt
@@ -0,0 +1,901 @@
+package com.r3.corda.enterprise.perftestcordapp.contracts.asset
+
+
+import com.r3.corda.enterprise.perftestcordapp.*
+import com.r3.corda.enterprise.perftestcordapp.utils.sumCash
+import com.r3.corda.enterprise.perftestcordapp.utils.sumCashBy
+import com.r3.corda.enterprise.perftestcordapp.utils.sumCashOrNull
+import com.r3.corda.enterprise.perftestcordapp.utils.sumCashOrZero
+import net.corda.core.contracts.*
+import net.corda.core.crypto.SecureHash
+import net.corda.core.crypto.generateKeyPair
+import net.corda.core.identity.AbstractParty
+import net.corda.core.identity.AnonymousParty
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.identity.Party
+import net.corda.core.node.ServiceHub
+import net.corda.core.node.services.Vault
+import net.corda.core.node.services.VaultService
+import net.corda.core.node.services.queryBy
+import net.corda.core.transactions.SignedTransaction
+import net.corda.core.transactions.TransactionBuilder
+import net.corda.core.transactions.WireTransaction
+import net.corda.core.utilities.OpaqueBytes
+import net.corda.node.services.vault.NodeVaultService
+import net.corda.node.utilities.CordaPersistence
+import net.corda.testing.*
+import net.corda.testing.contracts.DummyState
+import net.corda.testing.contracts.calculateRandomlySizedAmounts
+import net.corda.testing.node.MockServices
+import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import java.security.KeyPair
+import java.util.*
+import kotlin.test.*
+
+/**
+ * Creates a random set of between (by default) 3 and 10 cash states that add up to the given amount and adds them
+ * to the vault. This is intended for unit tests. The cash is issued by [DUMMY_CASH_ISSUER] and owned by the legal
+ * identity key from the storage service.
+ *
+ * The service hub needs to provide at least a key management service and a storage service.
+ *
+ * @param issuerServices service hub of the issuer node, which will be used to sign the transaction.
+ * @param outputNotary the notary to use for output states. The transaction is NOT signed by this notary.
+ * @return a vault object that represents the generated states (it will NOT be the full vault from the service hub!).
+ */
+fun ServiceHub.fillWithSomeTestCash(howMuch: Amount,
+ issuerServices: ServiceHub = this,
+ outputNotary: Party = DUMMY_NOTARY,
+ atLeastThisManyStates: Int = 3,
+ atMostThisManyStates: Int = 10,
+ rng: Random = Random(),
+ ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })),
+ ownedBy: AbstractParty? = null,
+ issuedBy: PartyAndReference = DUMMY_CASH_ISSUER): Vault {
+ val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng)
+
+ val myKey = ownedBy?.owningKey ?: myInfo.chooseIdentity().owningKey
+ val anonParty = AnonymousParty(myKey)
+
+ // We will allocate one state to one transaction, for simplicities sake.
+ val cash = Cash()
+ val transactions: List = amounts.map { pennies ->
+ val issuance = TransactionBuilder(null as Party?)
+ cash.generateIssue(issuance, Amount(pennies, Issued(issuedBy.copy(reference = ref), howMuch.token)), anonParty, outputNotary)
+
+ return@map issuerServices.signInitialTransaction(issuance, issuedBy.party.owningKey)
+ }
+
+ recordTransactions(transactions)
+
+ // Get all the StateRefs of all the generated transactions.
+ val states = transactions.flatMap { stx ->
+ stx.tx.outputs.indices.map { i -> stx.tx.outRef(i) }
+ }
+
+ return Vault(states)
+}
+
+
+class CashTests : TestDependencyInjectionBase() {
+ private val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
+ private val defaultIssuer = MEGA_CORP.ref(defaultRef)
+ private val inState = Cash.State(
+ amount = 1000.DOLLARS `issued by` defaultIssuer,
+ owner = AnonymousParty(ALICE_PUBKEY)
+ )
+ // Input state held by the issuer
+ private val issuerInState = inState.copy(owner = defaultIssuer.party)
+ private val outState = issuerInState.copy(owner = AnonymousParty(BOB_PUBKEY))
+
+ private fun Cash.State.editDepositRef(ref: Byte) = copy(
+ amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref))))
+ )
+
+ private lateinit var miniCorpServices: MockServices
+ private lateinit var megaCorpServices: MockServices
+ val vault: VaultService get() = miniCorpServices.vaultService
+ lateinit var database: CordaPersistence
+ private lateinit var vaultStatesUnconsumed: List>
+
+ @Before
+ fun setUp() {
+ LogHelper.setLevel(NodeVaultService::class)
+ megaCorpServices = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), MEGA_CORP_KEY)
+ val databaseAndServices = makeTestDatabaseAndMockServices(cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), keys = listOf(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY))
+ database = databaseAndServices.first
+ miniCorpServices = databaseAndServices.second
+
+ // Create some cash. Any attempt to spend >$500 will require multiple issuers to be involved.
+ database.transaction {
+ miniCorpServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
+ ownedBy = OUR_IDENTITY_1, issuedBy = MEGA_CORP.ref(1), issuerServices = megaCorpServices)
+ miniCorpServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
+ ownedBy = OUR_IDENTITY_1, issuedBy = MEGA_CORP.ref(1), issuerServices = megaCorpServices)
+ miniCorpServices.fillWithSomeTestCash(howMuch = 80.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
+ ownedBy = OUR_IDENTITY_1, issuedBy = MINI_CORP.ref(1), issuerServices = miniCorpServices)
+ miniCorpServices.fillWithSomeTestCash(howMuch = 80.SWISS_FRANCS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
+ ownedBy = OUR_IDENTITY_1, issuedBy = MINI_CORP.ref(1), issuerServices = miniCorpServices)
+ }
+ database.transaction {
+ vaultStatesUnconsumed = miniCorpServices.vaultService.queryBy().states
+ }
+ resetTestSerialization()
+ }
+
+ @After
+ fun tearDown() {
+ database.close()
+ }
+
+ @Test
+ fun trivial() {
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { inState }
+
+ tweak {
+ output(Cash.PROGRAM_ID) { outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer) }
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ this `fails with` "the amounts balance"
+ }
+ tweak {
+ output(Cash.PROGRAM_ID) { outState }
+ command(ALICE_PUBKEY) { DummyCommandData }
+ // Invalid command
+ this `fails with` "required com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash.Commands.Move command"
+ }
+ tweak {
+ output(Cash.PROGRAM_ID) { outState }
+ command(BOB_PUBKEY) { Cash.Commands.Move() }
+ this `fails with` "the owning keys are a subset of the signing keys"
+ }
+ tweak {
+ output(Cash.PROGRAM_ID) { outState }
+ output(Cash.PROGRAM_ID) { outState issuedBy MINI_CORP }
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ this `fails with` "at least one cash input"
+ }
+ // Simple reallocation works.
+ tweak {
+ output(Cash.PROGRAM_ID) { outState }
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ this.verifies()
+ }
+ }
+ }
+
+ @Test
+ fun `issue by move`() {
+ // Check we can't "move" money into existence.
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { DummyState() }
+ output(Cash.PROGRAM_ID) { outState }
+ command(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
+
+ this `fails with` "there is at least one cash input for this group"
+ }
+ }
+
+ @Test
+ fun issue() {
+ // Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
+ // institution is allowed to issue as much cash as they want.
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ output(Cash.PROGRAM_ID) { outState }
+ command(ALICE_PUBKEY) { Cash.Commands.Issue() }
+ this `fails with` "output states are issued by a command signer"
+ }
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ output(Cash.PROGRAM_ID) {
+ Cash.State(
+ amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34),
+ owner = AnonymousParty(ALICE_PUBKEY)
+ )
+ }
+ command(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
+ this.verifies()
+ }
+ }
+
+ @Test
+ fun generateIssueRaw() {
+ initialiseTestSerialization()
+ // Test generation works.
+ val tx: WireTransaction = TransactionBuilder(notary = null).apply {
+ Cash().generateIssue(this, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = AnonymousParty(ALICE_PUBKEY), notary = DUMMY_NOTARY)
+ }.toWireTransaction(miniCorpServices)
+ assertTrue(tx.inputs.isEmpty())
+ val s = tx.outputsOfType().single()
+ assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount)
+ assertEquals(MINI_CORP as AbstractParty, s.amount.token.issuer.party)
+ assertEquals(AnonymousParty(ALICE_PUBKEY), s.owner)
+ assertTrue(tx.commands[0].value is Cash.Commands.Issue)
+ assertEquals(MINI_CORP_PUBKEY, tx.commands[0].signers[0])
+ }
+
+ @Test
+ fun generateIssueFromAmount() {
+ initialiseTestSerialization()
+ // Test issuance from an issued amount
+ val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34)
+ val tx: WireTransaction = TransactionBuilder(notary = null).apply {
+ Cash().generateIssue(this, amount, owner = AnonymousParty(ALICE_PUBKEY), notary = DUMMY_NOTARY)
+ }.toWireTransaction(miniCorpServices)
+ assertTrue(tx.inputs.isEmpty())
+ assertEquals(tx.outputs[0], tx.outputs[0])
+ }
+
+ @Test
+ fun `extended issue examples`() {
+ // We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { issuerInState }
+ output(Cash.PROGRAM_ID) { inState.copy(amount = inState.amount * 2) }
+
+ // Move fails: not allowed to summon money.
+ tweak {
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ this `fails with` "the amounts balance"
+ }
+
+ // Issue works.
+ tweak {
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
+ this.verifies()
+ }
+ }
+
+ // Can't use an issue command to lower the amount.
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { inState }
+ output(Cash.PROGRAM_ID) { inState.copy(amount = inState.amount.splitEvenly(2).first()) }
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
+ this `fails with` "output values sum to more than the inputs"
+ }
+
+ // Can't have an issue command that doesn't actually issue money.
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { inState }
+ output(Cash.PROGRAM_ID) { inState }
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
+ this `fails with` "output values sum to more than the inputs"
+ }
+
+ // Can't have any other commands if we have an issue command (because the issue command overrules them)
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { inState }
+ output(Cash.PROGRAM_ID) { inState.copy(amount = inState.amount * 2) }
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
+ tweak {
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
+ this `fails with` "there is only a single issue command"
+ }
+ this.verifies()
+ }
+ }
+
+ /**
+ * Test that the issuance builder rejects building into a transaction with existing
+ * cash inputs.
+ */
+ @Test(expected = IllegalStateException::class)
+ fun `reject issuance with inputs`() {
+ initialiseTestSerialization()
+ // Issue some cash
+ var ptx = TransactionBuilder(DUMMY_NOTARY)
+
+ Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY)
+ val tx = miniCorpServices.signInitialTransaction(ptx)
+
+ // Include the previously issued cash in a new issuance command
+ ptx = TransactionBuilder(DUMMY_NOTARY)
+ ptx.addInputState(tx.tx.outRef(0))
+ Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY)
+ }
+
+ @Test
+ fun testMergeSplit() {
+ // Splitting value works.
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ tweak {
+ input(Cash.PROGRAM_ID) { inState }
+ val splits4 = inState.amount.splitEvenly(4)
+ for (i in 0..3) output(Cash.PROGRAM_ID) { inState.copy(amount = splits4[i]) }
+ this.verifies()
+ }
+ // Merging 4 inputs into 2 outputs works.
+ tweak {
+ val splits2 = inState.amount.splitEvenly(2)
+ val splits4 = inState.amount.splitEvenly(4)
+ for (i in 0..3) input(Cash.PROGRAM_ID) { inState.copy(amount = splits4[i]) }
+ for (i in 0..1) output(Cash.PROGRAM_ID) { inState.copy(amount = splits2[i]) }
+ this.verifies()
+ }
+ // Merging 2 inputs into 1 works.
+ tweak {
+ val splits2 = inState.amount.splitEvenly(2)
+ for (i in 0..1) input(Cash.PROGRAM_ID) { inState.copy(amount = splits2[i]) }
+ output(Cash.PROGRAM_ID) { inState }
+ this.verifies()
+ }
+ }
+ }
+
+ @Test
+ fun zeroSizedValues() {
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { inState }
+ input(Cash.PROGRAM_ID) { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ this `fails with` "zero sized inputs"
+ }
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { inState }
+ output(Cash.PROGRAM_ID) { inState }
+ output(Cash.PROGRAM_ID) { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) }
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ this `fails with` "zero sized outputs"
+ }
+ }
+
+ @Test
+ fun trivialMismatches() {
+ // Can't change issuer.
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { inState }
+ output(Cash.PROGRAM_ID) { outState issuedBy MINI_CORP }
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ this `fails with` "the amounts balance"
+ }
+ // Can't change deposit reference when splitting.
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ val splits2 = inState.amount.splitEvenly(2)
+ input(Cash.PROGRAM_ID) { inState }
+ for (i in 0..1) output(Cash.PROGRAM_ID) { outState.copy(amount = splits2[i]).editDepositRef(i.toByte()) }
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ this `fails with` "the amounts balance"
+ }
+ // Can't mix currencies.
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { inState }
+ output(Cash.PROGRAM_ID) { outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer) }
+ output(Cash.PROGRAM_ID) { outState.copy(amount = 200.POUNDS `issued by` defaultIssuer) }
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ this `fails with` "the amounts balance"
+ }
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { inState }
+ input(Cash.PROGRAM_ID) {
+ inState.copy(
+ amount = 150.POUNDS `issued by` defaultIssuer,
+ owner = AnonymousParty(BOB_PUBKEY)
+ )
+ }
+ output(Cash.PROGRAM_ID) { outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer) }
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ this `fails with` "the amounts balance"
+ }
+ // Can't have superfluous input states from different issuers.
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { inState }
+ input(Cash.PROGRAM_ID) { inState issuedBy MINI_CORP }
+ output(Cash.PROGRAM_ID) { outState }
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ this `fails with` "the amounts balance"
+ }
+ // Can't combine two different deposits at the same issuer.
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { inState }
+ input(Cash.PROGRAM_ID) { inState.editDepositRef(3) }
+ output(Cash.PROGRAM_ID) { outState.copy(amount = inState.amount * 2).editDepositRef(3) }
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ this `fails with` "for reference [01]"
+ }
+ }
+
+ @Test
+ fun exitLedger() {
+ // Single input/output straightforward case.
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { issuerInState }
+ output(Cash.PROGRAM_ID) { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
+
+ tweak {
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) }
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
+ this `fails with` "the amounts balance"
+ }
+
+ tweak {
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
+ this `fails with` "required com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash.Commands.Move command"
+
+ tweak {
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
+ this.verifies()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `exit ledger with multiple issuers`() {
+ // Multi-issuer case.
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { issuerInState }
+ input(Cash.PROGRAM_ID) { issuerInState.copy(owner = MINI_CORP) issuedBy MINI_CORP }
+
+ output(Cash.PROGRAM_ID) { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) issuedBy MINI_CORP }
+ output(Cash.PROGRAM_ID) { issuerInState.copy(owner = MINI_CORP, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
+
+ command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { Cash.Commands.Move() }
+
+ this `fails with` "the amounts balance"
+
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
+ this `fails with` "the amounts balance"
+
+ command(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)) }
+ this.verifies()
+ }
+ }
+
+ @Test
+ fun `exit cash not held by its issuer`() {
+ // Single input/output straightforward case.
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input(Cash.PROGRAM_ID) { inState }
+ output(Cash.PROGRAM_ID) { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+ this `fails with` "the amounts balance"
+ }
+ }
+
+ @Test
+ fun multiIssuer() {
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ // Gather 2000 dollars from two different issuers.
+ input(Cash.PROGRAM_ID) { inState }
+ input(Cash.PROGRAM_ID) { inState issuedBy MINI_CORP }
+ command(ALICE_PUBKEY) { Cash.Commands.Move() }
+
+ // Can't merge them together.
+ tweak {
+ output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY), amount = 2000.DOLLARS `issued by` defaultIssuer) }
+ this `fails with` "the amounts balance"
+ }
+ // Missing MiniCorp deposit
+ tweak {
+ output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) }
+ output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) }
+ this `fails with` "the amounts balance"
+ }
+
+ // This works.
+ output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) }
+ output(Cash.PROGRAM_ID) { inState.copy(owner = AnonymousParty(BOB_PUBKEY)) issuedBy MINI_CORP }
+ this.verifies()
+ }
+ }
+
+ @Test
+ fun multiCurrency() {
+ // Check we can do an atomic currency trade tx.
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), AnonymousParty(BOB_PUBKEY))
+ input(Cash.PROGRAM_ID) { inState ownedBy AnonymousParty(ALICE_PUBKEY) }
+ input(Cash.PROGRAM_ID) { pounds }
+ output(Cash.PROGRAM_ID) { inState ownedBy AnonymousParty(BOB_PUBKEY) }
+ output(Cash.PROGRAM_ID) { pounds ownedBy AnonymousParty(ALICE_PUBKEY) }
+ command(ALICE_PUBKEY, BOB_PUBKEY) { Cash.Commands.Move() }
+
+ this.verifies()
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ //
+ // Spend tx generation
+
+ private val OUR_KEY: KeyPair by lazy { generateKeyPair() }
+ private val OUR_IDENTITY_1: AbstractParty get() = AnonymousParty(OUR_KEY.public)
+ private val OUR_IDENTITY_AND_CERT = getTestPartyAndCertificate(CordaX500Name(organisation = "Me", locality = "London", country = "GB"), OUR_KEY.public)
+
+ private val THEIR_IDENTITY_1 = AnonymousParty(MINI_CORP_PUBKEY)
+ private val THEIR_IDENTITY_2 = AnonymousParty(CHARLIE_PUBKEY)
+
+ private fun makeCash(amount: Amount, issuer: AbstractParty, depositRef: Byte = 1) =
+ StateAndRef(
+ TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), OUR_IDENTITY_1), Cash.PROGRAM_ID, DUMMY_NOTARY),
+ StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
+ )
+
+ private val WALLET = listOf(
+ makeCash(100.DOLLARS, MEGA_CORP),
+ makeCash(400.DOLLARS, MEGA_CORP),
+ makeCash(80.DOLLARS, MINI_CORP),
+ makeCash(80.SWISS_FRANCS, MINI_CORP, 2)
+ )
+
+ /**
+ * Generate an exit transaction, removing some amount of cash from the ledger.
+ */
+ private fun makeExit(serviceHub: ServiceHub, amount: Amount, issuer: Party, depositRef: Byte = 1): WireTransaction {
+ val tx = TransactionBuilder(DUMMY_NOTARY)
+ val payChangeTo = serviceHub.keyManagementService.freshKeyAndCert(MINI_CORP_IDENTITY, false).party
+ Cash().generateExit(tx, Amount(amount.quantity, Issued(issuer.ref(depositRef), amount.token)), WALLET, payChangeTo)
+ return tx.toWireTransaction(serviceHub)
+ }
+
+ private fun makeSpend(amount: Amount, dest: AbstractParty): WireTransaction {
+ val tx = TransactionBuilder(DUMMY_NOTARY)
+ database.transaction {
+ Cash.generateSpend(miniCorpServices, tx, amount, OUR_IDENTITY_AND_CERT, dest)
+ }
+ return tx.toWireTransaction(miniCorpServices)
+ }
+
+ /**
+ * Try exiting an amount which matches a single state.
+ */
+ @Test
+ fun generateSimpleExit() {
+ initialiseTestSerialization()
+ val wtx = makeExit(miniCorpServices, 100.DOLLARS, MEGA_CORP, 1)
+ assertEquals(WALLET[0].ref, wtx.inputs[0])
+ assertEquals(0, wtx.outputs.size)
+
+ val expectedMove = Cash.Commands.Move()
+ val expectedExit = Cash.Commands.Exit(Amount(10000, Issued(MEGA_CORP.ref(1), USD)))
+
+ assertEquals(listOf(expectedMove, expectedExit), wtx.commands.map { it.value })
+ }
+
+ /**
+ * Try exiting an amount smaller than the smallest available input state, and confirm change is generated correctly.
+ */
+ @Test
+ fun generatePartialExit() {
+ initialiseTestSerialization()
+ val wtx = makeExit(miniCorpServices, 50.DOLLARS, MEGA_CORP, 1)
+ val actualInput = wtx.inputs.single()
+ // Filter the available inputs and confirm exactly one has been used
+ val expectedInputs = WALLET.filter { it.ref == actualInput }
+ assertEquals(1, expectedInputs.size)
+ val inputState = expectedInputs.single()
+ val actualChange = wtx.outputs.single().data as Cash.State
+ val expectedChangeAmount = inputState.state.data.amount.quantity - 50.DOLLARS.quantity
+ val expectedChange = WALLET[0].state.data.copy(amount = WALLET[0].state.data.amount.copy(quantity = expectedChangeAmount), owner = actualChange.owner)
+ assertEquals(expectedChange, wtx.getOutput(0))
+ }
+
+ /**
+ * Try exiting a currency we don't have.
+ */
+ @Test
+ fun generateAbsentExit() {
+ initialiseTestSerialization()
+ assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 1) }
+ }
+
+ /**
+ * Try exiting with a reference mis-match.
+ */
+ @Test
+ fun generateInvalidReferenceExit() {
+ initialiseTestSerialization()
+ assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 2) }
+ }
+
+ /**
+ * Try exiting an amount greater than the maximum available.
+ */
+ @Test
+ fun generateInsufficientExit() {
+ initialiseTestSerialization()
+ assertFailsWith { makeExit(miniCorpServices, 1000.DOLLARS, MEGA_CORP, 1) }
+ }
+
+ /**
+ * Try exiting for an owner with no states
+ */
+ @Test
+ fun generateOwnerWithNoStatesExit() {
+ initialiseTestSerialization()
+ assertFailsWith { makeExit(miniCorpServices, 100.POUNDS, CHARLIE, 1) }
+ }
+
+ /**
+ * Try exiting when vault is empty
+ */
+ @Test
+ fun generateExitWithEmptyVault() {
+ initialiseTestSerialization()
+ assertFailsWith {
+ val tx = TransactionBuilder(DUMMY_NOTARY)
+ Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList(), OUR_IDENTITY_1)
+ }
+ }
+
+ @Test
+ fun generateSimpleDirectSpend() {
+ initialiseTestSerialization()
+ val wtx =
+ database.transaction {
+ makeSpend(100.DOLLARS, THEIR_IDENTITY_1)
+ }
+ database.transaction {
+ val vaultState = vaultStatesUnconsumed.elementAt(0)
+ assertEquals(vaultState.ref, wtx.inputs[0])
+ assertEquals(vaultState.state.data.copy(owner = THEIR_IDENTITY_1), wtx.getOutput(0))
+ assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
+ }
+ }
+
+ @Test
+ fun generateSimpleSpendWithParties() {
+ initialiseTestSerialization()
+ database.transaction {
+
+ val tx = TransactionBuilder(DUMMY_NOTARY)
+ Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, OUR_IDENTITY_AND_CERT, ALICE, setOf(MINI_CORP))
+
+ assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0])
+ }
+ }
+
+ @Test
+ fun generateSimpleSpendWithChange() {
+ initialiseTestSerialization()
+ val wtx =
+ database.transaction {
+ makeSpend(10.DOLLARS, THEIR_IDENTITY_1)
+ }
+ database.transaction {
+ val vaultState = vaultStatesUnconsumed.elementAt(0)
+ val changeAmount = 90.DOLLARS `issued by` defaultIssuer
+ val likelyChangeState = wtx.outputs.map(TransactionState<*>::data).single { state ->
+ if (state is Cash.State) {
+ state.amount == changeAmount
+ } else {
+ false
+ }
+ }
+ val changeOwner = (likelyChangeState as Cash.State).owner
+ assertEquals(1, miniCorpServices.keyManagementService.filterMyKeys(setOf(changeOwner.owningKey)).toList().size)
+ assertEquals(vaultState.ref, wtx.inputs[0])
+ assertEquals(vaultState.state.data.copy(owner = THEIR_IDENTITY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data)
+ assertEquals(vaultState.state.data.copy(amount = changeAmount, owner = changeOwner), wtx.outputs[1].data)
+ assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
+ }
+ }
+
+ @Test
+ fun generateSpendWithTwoInputs() {
+ initialiseTestSerialization()
+ val wtx =
+ database.transaction {
+ makeSpend(500.DOLLARS, THEIR_IDENTITY_1)
+ }
+ database.transaction {
+ val vaultState0 = vaultStatesUnconsumed.elementAt(0)
+ val vaultState1 = vaultStatesUnconsumed.elementAt(1)
+ assertEquals(vaultState0.ref, wtx.inputs[0])
+ assertEquals(vaultState1.ref, wtx.inputs[1])
+ assertEquals(vaultState0.state.data.copy(owner = THEIR_IDENTITY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.getOutput(0))
+ assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
+ }
+ }
+
+ @Test
+ fun generateSpendMixedDeposits() {
+ initialiseTestSerialization()
+ val wtx =
+ database.transaction {
+ val wtx = makeSpend(580.DOLLARS, THEIR_IDENTITY_1)
+ assertEquals(3, wtx.inputs.size)
+ wtx
+ }
+ database.transaction {
+ val vaultState0: StateAndRef = vaultStatesUnconsumed.elementAt(0)
+ val vaultState1: StateAndRef = vaultStatesUnconsumed.elementAt(1)
+ val vaultState2: StateAndRef = vaultStatesUnconsumed.elementAt(2)
+ assertEquals(vaultState0.ref, wtx.inputs[0])
+ assertEquals(vaultState1.ref, wtx.inputs[1])
+ assertEquals(vaultState2.ref, wtx.inputs[2])
+ assertEquals(vaultState0.state.data.copy(owner = THEIR_IDENTITY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data)
+ assertEquals(vaultState2.state.data.copy(owner = THEIR_IDENTITY_1), wtx.outputs[0].data)
+ assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0])
+ }
+ }
+
+ @Test
+ fun generateSpendInsufficientBalance() {
+ initialiseTestSerialization()
+ database.transaction {
+
+ val e: InsufficientBalanceException = assertFailsWith("balance") {
+ makeSpend(1000.DOLLARS, THEIR_IDENTITY_1)
+ }
+ assertEquals((1000 - 580).DOLLARS, e.amountMissing)
+
+ assertFailsWith(InsufficientBalanceException::class) {
+ makeSpend(81.SWISS_FRANCS, THEIR_IDENTITY_1)
+ }
+ }
+ }
+
+ /**
+ * Confirm that aggregation of states is correctly modelled.
+ */
+ @Test
+ fun aggregation() {
+ val fiveThousandDollarsFromMega = Cash.State(5000.DOLLARS `issued by` MEGA_CORP.ref(2), MEGA_CORP)
+ val twoThousandDollarsFromMega = Cash.State(2000.DOLLARS `issued by` MEGA_CORP.ref(2), MINI_CORP)
+ val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` MINI_CORP.ref(3), MEGA_CORP)
+
+ // Obviously it must be possible to aggregate states with themselves
+ assertEquals(fiveThousandDollarsFromMega.amount.token, fiveThousandDollarsFromMega.amount.token)
+
+ // Owner is not considered when calculating whether it is possible to aggregate states
+ assertEquals(fiveThousandDollarsFromMega.amount.token, twoThousandDollarsFromMega.amount.token)
+
+ // States cannot be aggregated if the deposit differs
+ assertNotEquals(fiveThousandDollarsFromMega.amount.token, oneThousandDollarsFromMini.amount.token)
+ assertNotEquals(twoThousandDollarsFromMega.amount.token, oneThousandDollarsFromMini.amount.token)
+
+ // States cannot be aggregated if the currency differs
+ assertNotEquals(oneThousandDollarsFromMini.amount.token,
+ Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP).amount.token)
+
+ // States cannot be aggregated if the reference differs
+ assertNotEquals(fiveThousandDollarsFromMega.amount.token, (fiveThousandDollarsFromMega withDeposit defaultIssuer).amount.token)
+ assertNotEquals((fiveThousandDollarsFromMega withDeposit defaultIssuer).amount.token, fiveThousandDollarsFromMega.amount.token)
+ }
+
+ @Test
+ fun `summing by owner`() {
+ val states = listOf(
+ Cash.State(1000.DOLLARS `issued by` defaultIssuer, MINI_CORP),
+ Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
+ Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP)
+ )
+ assertEquals(6000.DOLLARS `issued by` defaultIssuer, states.sumCashBy(MEGA_CORP))
+ }
+
+ @Test(expected = UnsupportedOperationException::class)
+ fun `summing by owner throws`() {
+ val states = listOf(
+ Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
+ Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP)
+ )
+ states.sumCashBy(MINI_CORP)
+ }
+
+ @Test
+ fun `summing no currencies`() {
+ val states = emptyList()
+ assertEquals(0.POUNDS `issued by` defaultIssuer, states.sumCashOrZero(GBP `issued by` defaultIssuer))
+ assertNull(states.sumCashOrNull())
+ }
+
+ @Test(expected = UnsupportedOperationException::class)
+ fun `summing no currencies throws`() {
+ val states = emptyList()
+ states.sumCash()
+ }
+
+ @Test
+ fun `summing a single currency`() {
+ val states = listOf(
+ Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
+ Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
+ Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP)
+ )
+ // Test that summing everything produces the total number of dollars
+ val expected = 7000.DOLLARS `issued by` defaultIssuer
+ val actual = states.sumCash()
+ assertEquals(expected, actual)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun `summing multiple currencies`() {
+ val states = listOf(
+ Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
+ Cash.State(4000.POUNDS `issued by` defaultIssuer, MEGA_CORP)
+ )
+ // Test that summing everything fails because we're mixing units
+ states.sumCash()
+ }
+
+ // Double spend.
+ @Test
+ fun chainCashDoubleSpendFailsWith() {
+ val mockService = MockServices(listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"), MEGA_CORP_KEY)
+
+ ledger(mockService) {
+ unverifiedTransaction {
+ attachment(Cash.PROGRAM_ID)
+ output(Cash.PROGRAM_ID, "MEGA_CORP cash") {
+ Cash.State(
+ amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
+ owner = MEGA_CORP
+ )
+ }
+ }
+
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input("MEGA_CORP cash")
+ output(Cash.PROGRAM_ID, "MEGA_CORP cash 2", "MEGA_CORP cash".output().copy(owner = AnonymousParty(ALICE_PUBKEY)))
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
+ this.verifies()
+ }
+
+ tweak {
+ transaction {
+ attachment(Cash.PROGRAM_ID)
+ input("MEGA_CORP cash")
+ // We send it to another pubkey so that the transaction is not identical to the previous one
+ output(Cash.PROGRAM_ID, "MEGA_CORP cash 3", "MEGA_CORP cash".output().copy(owner = ALICE))
+ command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
+ this.verifies()
+ }
+ this.fails()
+ }
+
+ this.verifies()
+ }
+ }
+
+ @Test
+ fun multiSpend() {
+ initialiseTestSerialization()
+ val tx = TransactionBuilder(DUMMY_NOTARY)
+ database.transaction {
+ val payments = listOf(
+ PartyAndAmount(THEIR_IDENTITY_1, 400.DOLLARS),
+ PartyAndAmount(THEIR_IDENTITY_2, 150.DOLLARS)
+ )
+ Cash.generateSpend(miniCorpServices, tx, payments)
+ }
+ val wtx = tx.toWireTransaction(miniCorpServices)
+ fun out(i: Int) = wtx.getOutput(i) as Cash.State
+ assertEquals(4, wtx.outputs.size)
+ assertEquals(80.DOLLARS, out(0).amount.withoutIssuer())
+ assertEquals(320.DOLLARS, out(1).amount.withoutIssuer())
+ assertEquals(150.DOLLARS, out(2).amount.withoutIssuer())
+ assertEquals(30.DOLLARS, out(3).amount.withoutIssuer())
+ assertEquals(MINI_CORP, out(0).amount.token.issuer.party)
+ assertEquals(MEGA_CORP, out(1).amount.token.issuer.party)
+ assertEquals(MEGA_CORP, out(2).amount.token.issuer.party)
+ assertEquals(MEGA_CORP, out(3).amount.token.issuer.party)
+ }
+}
diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashExitFlowTests.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashExitFlowTests.kt
new file mode 100644
index 0000000000..fc827ac9c6
--- /dev/null
+++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashExitFlowTests.kt
@@ -0,0 +1,73 @@
+package com.r3.corda.enterprise.perftestcordapp.flows
+
+import com.r3.corda.enterprise.perftestcordapp.DOLLARS
+import com.r3.corda.enterprise.perftestcordapp.`issued by`
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
+import net.corda.core.identity.Party
+import net.corda.core.utilities.OpaqueBytes
+import net.corda.core.utilities.getOrThrow
+import net.corda.node.internal.StartedNode
+import net.corda.testing.BOC
+import net.corda.testing.chooseIdentity
+import net.corda.testing.getDefaultNotary
+import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
+import net.corda.testing.node.MockNetwork
+import net.corda.testing.node.MockNetwork.MockNode
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class CashExitFlowTests {
+ private lateinit var mockNet : MockNetwork
+ private val initialBalance = 2000.DOLLARS
+ private val ref = OpaqueBytes.of(0x01)
+ private lateinit var bankOfCordaNode: StartedNode
+ private lateinit var bankOfCorda: Party
+ private lateinit var notaryNode: StartedNode
+ private lateinit var notary: Party
+
+ @Before
+ fun start() {
+ mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"))
+ notaryNode = mockNet.createNotaryNode()
+ bankOfCordaNode = mockNet.createPartyNode(BOC.name)
+ notary = notaryNode.services.getDefaultNotary()
+ bankOfCorda = bankOfCordaNode.info.chooseIdentity()
+
+ mockNet.runNetwork()
+ notary = bankOfCordaNode.services.getDefaultNotary()
+ val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, notary)).resultFuture
+ mockNet.runNetwork()
+ future.getOrThrow()
+ }
+
+ @After
+ fun cleanUp() {
+ mockNet.stopNodes()
+ }
+
+ @Test
+ fun `exit some cash`() {
+ val exitAmount = 500.DOLLARS
+ val future = bankOfCordaNode.services.startFlow(CashExitFlow(exitAmount, ref)).resultFuture
+ mockNet.runNetwork()
+ val exitTx = future.getOrThrow().stx.tx
+ val expected = (initialBalance - exitAmount).`issued by`(bankOfCorda.ref(ref))
+ assertEquals(1, exitTx.inputs.size)
+ assertEquals(1, exitTx.outputs.size)
+ val output = exitTx.outputsOfType().single()
+ assertEquals(expected, output.amount)
+ }
+
+ @Test
+ fun `exit zero cash`() {
+ val expected = 0.DOLLARS
+ val future = bankOfCordaNode.services.startFlow(CashExitFlow(expected, ref)).resultFuture
+ mockNet.runNetwork()
+ assertFailsWith {
+ future.getOrThrow()
+ }
+ }
+}
diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueFlowTests.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueFlowTests.kt
new file mode 100644
index 0000000000..9df67f5f29
--- /dev/null
+++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashIssueFlowTests.kt
@@ -0,0 +1,65 @@
+package com.r3.corda.enterprise.perftestcordapp.flows
+
+import net.corda.core.identity.Party
+import net.corda.core.utilities.OpaqueBytes
+import net.corda.core.utilities.getOrThrow
+import com.r3.corda.enterprise.perftestcordapp.DOLLARS
+import com.r3.corda.enterprise.perftestcordapp.`issued by`
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
+import net.corda.node.internal.StartedNode
+import net.corda.testing.chooseIdentity
+import net.corda.testing.getDefaultNotary
+import net.corda.testing.BOC
+import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
+import net.corda.testing.node.MockNetwork
+import net.corda.testing.node.MockNetwork.MockNode
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class CashIssueFlowTests {
+ private lateinit var mockNet : MockNetwork
+ private lateinit var bankOfCordaNode: StartedNode
+ private lateinit var bankOfCorda: Party
+ private lateinit var notaryNode: StartedNode
+ private lateinit var notary: Party
+
+ @Before
+ fun start() {
+ mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"))
+ notaryNode = mockNet.createNotaryNode()
+ bankOfCordaNode = mockNet.createPartyNode(BOC.name)
+ bankOfCorda = bankOfCordaNode.info.chooseIdentity()
+ notary = notaryNode.services.getDefaultNotary()
+ mockNet.runNetwork()
+ }
+
+ @After
+ fun cleanUp() {
+ mockNet.stopNodes()
+ }
+
+ @Test
+ fun `issue some cash`() {
+ val expected = 500.DOLLARS
+ val ref = OpaqueBytes.of(0x01)
+ val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, notary)).resultFuture
+ mockNet.runNetwork()
+ val issueTx = future.getOrThrow().stx
+ val output = issueTx.tx.outputsOfType().single()
+ assertEquals(expected.`issued by`(bankOfCorda.ref(ref)), output.amount)
+ }
+
+ @Test
+ fun `issue zero cash`() {
+ val expected = 0.DOLLARS
+ val ref = OpaqueBytes.of(0x01)
+ val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, notary)).resultFuture
+ mockNet.runNetwork()
+ assertFailsWith {
+ future.getOrThrow()
+ }
+ }
+}
diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashPaymentFlowTests.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashPaymentFlowTests.kt
new file mode 100644
index 0000000000..58aba54e43
--- /dev/null
+++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/CashPaymentFlowTests.kt
@@ -0,0 +1,112 @@
+package com.r3.corda.enterprise.perftestcordapp.flows
+
+import com.r3.corda.enterprise.perftestcordapp.DOLLARS
+import com.r3.corda.enterprise.perftestcordapp.`issued by`
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
+import net.corda.core.identity.Party
+import net.corda.core.node.services.Vault
+import net.corda.core.node.services.trackBy
+import net.corda.core.node.services.vault.QueryCriteria
+import net.corda.core.utilities.OpaqueBytes
+import net.corda.core.utilities.getOrThrow
+import net.corda.node.internal.StartedNode
+import net.corda.testing.*
+import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
+import net.corda.testing.node.MockNetwork
+import net.corda.testing.node.MockNetwork.MockNode
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class CashPaymentFlowTests {
+ private lateinit var mockNet : MockNetwork
+ private val initialBalance = 2000.DOLLARS
+ private val ref = OpaqueBytes.of(0x01)
+ private lateinit var bankOfCordaNode: StartedNode
+ private lateinit var bankOfCorda: Party
+ private lateinit var notaryNode: StartedNode
+ private lateinit var notary: Party
+
+ @Before
+ fun start() {
+ mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin(), cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts.asset"))
+ notaryNode = mockNet.createNotaryNode()
+ bankOfCordaNode = mockNet.createPartyNode(BOC.name)
+ bankOfCorda = bankOfCordaNode.info.chooseIdentity()
+ notary = notaryNode.services.getDefaultNotary()
+ val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, notary)).resultFuture
+ mockNet.runNetwork()
+ future.getOrThrow()
+ }
+
+ @After
+ fun cleanUp() {
+ mockNet.stopNodes()
+ }
+
+ @Test
+ fun `pay some cash`() {
+ val payTo = notaryNode.info.chooseIdentity()
+ val expectedPayment = 500.DOLLARS
+ val expectedChange = 1500.DOLLARS
+
+ bankOfCordaNode.database.transaction {
+ // Register for vault updates
+ val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
+ val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultService.trackBy(criteria)
+ val (_, vaultUpdatesBankClient) = notaryNode.services.vaultService.trackBy(criteria)
+
+ val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment,
+ payTo)).resultFuture
+ mockNet.runNetwork()
+ future.getOrThrow()
+
+ // Check Bank of Corda vault updates - we take in some issued cash and split it into $500 to the notary
+ // and $1,500 back to us, so we expect to consume one state, produce one state for our own vault
+ vaultUpdatesBoc.expectEvents {
+ expect { update ->
+ require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" }
+ require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" }
+ val changeState = update.produced.single().state.data
+ assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), changeState.amount)
+ }
+ }
+
+ // Check notary node vault updates
+ vaultUpdatesBankClient.expectEvents {
+ expect { (consumed, produced) ->
+ require(consumed.isEmpty()) { consumed.size }
+ require(produced.size == 1) { produced.size }
+ val paymentState = produced.single().state.data
+ assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `pay more than we have`() {
+ val payTo = notaryNode.info.chooseIdentity()
+ val expected = 4000.DOLLARS
+ val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expected,
+ payTo)).resultFuture
+ mockNet.runNetwork()
+ assertFailsWith {
+ future.getOrThrow()
+ }
+ }
+
+ @Test
+ fun `pay zero cash`() {
+ val payTo = notaryNode.info.chooseIdentity()
+ val expected = 0.DOLLARS
+ val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expected,
+ payTo)).resultFuture
+ mockNet.runNetwork()
+ assertFailsWith {
+ future.getOrThrow()
+ }
+ }
+}
diff --git a/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlowTest.kt b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlowTest.kt
new file mode 100644
index 0000000000..ee6fc4e0e4
--- /dev/null
+++ b/perftestcordapp/src/test/kotlin/com/r3/corda/enterprise/perftestcordapp/flows/TwoPartyTradeFlowTest.kt
@@ -0,0 +1,790 @@
+package com.r3.corda.enterprise.perftestcordapp.flows
+
+// NB: Unlike the other flow tests in this package, this is not originally copied from net.corda.finance, but
+// from net.corda.node.messaging
+
+import co.paralleluniverse.fibers.Suspendable
+import net.corda.core.concurrent.CordaFuture
+import net.corda.core.contracts.*
+import net.corda.core.crypto.*
+import net.corda.core.flows.*
+import net.corda.core.identity.AbstractParty
+import net.corda.core.identity.AnonymousParty
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.identity.Party
+import net.corda.core.internal.FlowStateMachine
+import net.corda.core.internal.concurrent.map
+import net.corda.core.internal.rootCause
+import net.corda.core.messaging.DataFeed
+import net.corda.core.messaging.SingleMessageRecipient
+import net.corda.core.messaging.StateMachineTransactionMapping
+import net.corda.core.node.services.Vault
+import net.corda.core.serialization.CordaSerializable
+import net.corda.core.serialization.SingletonSerializeAsToken
+import net.corda.core.toFuture
+import net.corda.core.transactions.SignedTransaction
+import net.corda.core.transactions.TransactionBuilder
+import net.corda.core.transactions.WireTransaction
+import net.corda.core.utilities.days
+import net.corda.core.utilities.getOrThrow
+import net.corda.core.utilities.toNonEmptySet
+import net.corda.core.utilities.unwrap
+import com.r3.corda.enterprise.perftestcordapp.DOLLARS
+import com.r3.corda.enterprise.perftestcordapp.`issued by`
+import com.r3.corda.enterprise.perftestcordapp.contracts.CommercialPaper
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.CASH
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.Cash
+import com.r3.corda.enterprise.perftestcordapp.flows.TwoPartyTradeFlow.Buyer
+import com.r3.corda.enterprise.perftestcordapp.flows.TwoPartyTradeFlow.Seller
+import net.corda.node.internal.StartedNode
+import net.corda.node.services.api.WritableTransactionStorage
+import net.corda.node.services.config.NodeConfiguration
+import net.corda.node.services.persistence.DBTransactionStorage
+import net.corda.node.utilities.CordaPersistence
+import net.corda.nodeapi.internal.ServiceInfo
+import net.corda.testing.*
+import com.r3.corda.enterprise.perftestcordapp.contracts.asset.fillWithSomeTestCash
+import net.corda.node.services.api.Checkpoint
+import net.corda.node.services.api.CheckpointStorage
+import net.corda.testing.node.InMemoryMessagingNetwork
+import net.corda.testing.node.MockNetwork
+import net.corda.testing.node.MockServices
+import net.corda.testing.node.pumpReceive
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import rx.Observable
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.math.BigInteger
+import java.security.KeyPair
+import java.util.*
+import java.util.jar.JarOutputStream
+import java.util.zip.ZipEntry
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertTrue
+
+
+/**
+ * Copied from DBCheckpointStorageTests as it is required as helper for this test
+ */
+internal fun CheckpointStorage.checkpoints(): List {
+ val checkpoints = mutableListOf()
+ forEach {
+ checkpoints += it
+ true
+ }
+ return checkpoints
+}
+
+
+/**
+ * In this example, Alice wishes to sell her commercial paper to Bob in return for $1,000,000 and they wish to do
+ * it on the ledger atomically. Therefore they must work together to build a transaction.
+ *
+ * We assume that Alice and Bob already found each other via some market, and have agreed the details already.
+ */
+@RunWith(Parameterized::class)
+class TwoPartyTradeFlowTests(val anonymous: Boolean) {
+ companion object {
+ private val cordappPackages = listOf("com.r3.corda.enterprise.perftestcordapp.contracts")
+ @JvmStatic
+ @Parameterized.Parameters
+ fun data(): Collection {
+ return listOf(true, false)
+ }
+ }
+
+ private lateinit var mockNet: MockNetwork
+
+ @Before
+ fun before() {
+ LogHelper.setLevel("platform.trade", "core.contract.TransactionGroup", "recordingmap")
+ }
+
+ @After
+ fun after() {
+ mockNet.stopNodes()
+ LogHelper.reset("platform.trade", "core.contract.TransactionGroup", "recordingmap")
+ }
+
+ @Test
+ fun `trade cash for commercial paper`() {
+ // We run this in parallel threads to help catch any race conditions that may exist. The other tests
+ // we run in the unit test thread exclusively to speed things up, ensure deterministic results and
+ // allow interruption half way through.
+ mockNet = MockNetwork(false, true, cordappPackages = cordappPackages)
+ ledger(MockServices(cordappPackages), initialiseSerialization = false) {
+ val notaryNode = mockNet.createNotaryNode()
+ val aliceNode = mockNet.createPartyNode(ALICE_NAME)
+ val bobNode = mockNet.createPartyNode(BOB_NAME)
+ val bankNode = mockNet.createPartyNode(BOC_NAME)
+ val alice = aliceNode.info.singleIdentity()
+ val bank = bankNode.info.singleIdentity()
+ val notary = notaryNode.services.getDefaultNotary()
+ val cashIssuer = bank.ref(1)
+ val cpIssuer = bank.ref(1, 2, 3)
+
+ aliceNode.internals.disableDBCloseOnStop()
+ bobNode.internals.disableDBCloseOnStop()
+
+ bobNode.database.transaction {
+ bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, outputNotary = notary,
+ issuedBy = cashIssuer)
+ }
+
+ val alicesFakePaper = aliceNode.database.transaction {
+ fillUpForSeller(false, cpIssuer, alice,
+ 1200.DOLLARS `issued by` bank.ref(0), null, notary).second
+ }
+
+ insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
+
+ val (bobStateMachine, aliceResult) = runBuyerAndSeller(notary, aliceNode, bobNode,
+ "alice's paper".outputStateAndRef())
+
+ // TODO: Verify that the result was inserted into the transaction database.
+ // assertEquals(bobResult.get(), aliceNode.storage.validatedTransactions[aliceResult.get().id])
+ assertEquals(aliceResult.getOrThrow(), bobStateMachine.getOrThrow().resultFuture.getOrThrow())
+
+ aliceNode.dispose()
+ bobNode.dispose()
+
+ aliceNode.database.transaction {
+ assertThat(aliceNode.checkpointStorage.checkpoints()).isEmpty()
+ }
+ aliceNode.internals.manuallyCloseDB()
+ bobNode.database.transaction {
+ assertThat(bobNode.checkpointStorage.checkpoints()).isEmpty()
+ }
+ bobNode.internals.manuallyCloseDB()
+ }
+ }
+
+ @Test(expected = InsufficientBalanceException::class)
+ fun `trade cash for commercial paper fails using soft locking`() {
+ mockNet = MockNetwork(false, true, cordappPackages = cordappPackages)
+ ledger(MockServices(cordappPackages), initialiseSerialization = false) {
+ val notaryNode = mockNet.createNotaryNode()
+ val aliceNode = mockNet.createPartyNode(ALICE_NAME)
+ val bobNode = mockNet.createPartyNode(BOB_NAME)
+ val bankNode = mockNet.createPartyNode(BOC_NAME)
+ val alice = aliceNode.info.singleIdentity()
+ val bank = bankNode.info.singleIdentity()
+ val issuer = bank.ref(1)
+ val notary = aliceNode.services.getDefaultNotary()
+
+ aliceNode.internals.disableDBCloseOnStop()
+ bobNode.internals.disableDBCloseOnStop()
+
+ val cashStates = bobNode.database.transaction {
+ bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, notary, 3, 3,
+ issuedBy = issuer)
+ }
+
+ val alicesFakePaper = aliceNode.database.transaction {
+ fillUpForSeller(false, issuer, alice,
+ 1200.DOLLARS `issued by` bank.ref(0), null, notary).second
+ }
+
+ insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
+
+ val cashLockId = UUID.randomUUID()
+ bobNode.database.transaction {
+ // lock the cash states with an arbitrary lockId (to prevent the Buyer flow from claiming the states)
+ val refs = cashStates.states.map { it.ref }
+ if (refs.isNotEmpty()) {
+ bobNode.services.vaultService.softLockReserve(cashLockId, refs.toNonEmptySet())
+ }
+ }
+
+ val (bobStateMachine, aliceResult) = runBuyerAndSeller(notary, aliceNode, bobNode,
+ "alice's paper".outputStateAndRef())
+
+ assertEquals(aliceResult.getOrThrow(), bobStateMachine.getOrThrow().resultFuture.getOrThrow())
+
+ aliceNode.dispose()
+ bobNode.dispose()
+
+ aliceNode.database.transaction {
+ assertThat(aliceNode.checkpointStorage.checkpoints()).isEmpty()
+ }
+ aliceNode.internals.manuallyCloseDB()
+ bobNode.database.transaction {
+ assertThat(bobNode.checkpointStorage.checkpoints()).isEmpty()
+ }
+ bobNode.internals.manuallyCloseDB()
+ }
+ }
+
+ @Test
+ fun `shutdown and restore`() {
+ mockNet = MockNetwork(false, cordappPackages = cordappPackages)
+ ledger(MockServices(cordappPackages), initialiseSerialization = false) {
+ val notaryNode = mockNet.createNotaryNode()
+ val aliceNode = mockNet.createPartyNode(ALICE_NAME)
+ var bobNode = mockNet.createPartyNode(BOB_NAME)
+ val bankNode = mockNet.createPartyNode(BOC_NAME)
+ aliceNode.internals.disableDBCloseOnStop()
+ bobNode.internals.disableDBCloseOnStop()
+
+ val bobAddr = bobNode.network.myAddress as InMemoryMessagingNetwork.PeerHandle
+ mockNet.runNetwork() // Clear network map registration messages
+
+ val notary = notaryNode.services.getDefaultNotary()
+ val alice = aliceNode.info.singleIdentity()
+ val bank = bankNode.info.singleIdentity()
+ val issuer = bank.ref(1, 2, 3)
+
+ bobNode.database.transaction {
+ bobNode.services.fillWithSomeTestCash(2000.DOLLARS, bankNode.services, outputNotary = notary,
+ issuedBy = issuer)
+ }
+ val alicesFakePaper = aliceNode.database.transaction {
+ fillUpForSeller(false, issuer, alice,
+ 1200.DOLLARS `issued by` bank.ref(0), null, notary).second
+ }
+ insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
+ val aliceFuture = runBuyerAndSeller(notary, aliceNode, bobNode, "alice's paper".outputStateAndRef()).sellerResult
+
+ // Everything is on this thread so we can now step through the flow one step at a time.
+ // Seller Alice already sent a message to Buyer Bob. Pump once:
+ bobNode.pumpReceive()
+
+ // Bob sends a couple of queries for the dependencies back to Alice. Alice reponds.
+ aliceNode.pumpReceive()
+ bobNode.pumpReceive()
+ aliceNode.pumpReceive()
+ bobNode.pumpReceive()
+ aliceNode.pumpReceive()
+ bobNode.pumpReceive()
+
+ // OK, now Bob has sent the partial transaction back to Alice and is waiting for Alice's signature.
+ bobNode.database.transaction {
+ assertThat(bobNode.checkpointStorage.checkpoints()).hasSize(1)
+ }
+
+ val storage = bobNode.services.validatedTransactions
+ val bobTransactionsBeforeCrash = bobNode.database.transaction {
+ (storage as DBTransactionStorage).transactions
+ }
+ assertThat(bobTransactionsBeforeCrash).isNotEmpty
+
+ // .. and let's imagine that Bob's computer has a power cut. He now has nothing now beyond what was on disk.
+ bobNode.dispose()
+
+ // Alice doesn't know that and carries on: she wants to know about the cash transactions he's trying to use.
+ // She will wait around until Bob comes back.
+ assertThat(aliceNode.pumpReceive()).isNotNull()
+
+ // FIXME: Knowledge of confidential identities is lost on node shutdown, so Bob's node now refuses to sign the
+ // transaction because it has no idea who the parties are.
+
+ // ... bring the node back up ... the act of constructing the SMM will re-register the message handlers
+ // that Bob was waiting on before the reboot occurred.
+ bobNode = mockNet.createNode(bobAddr.id, object : MockNetwork.Factory {
+ override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?,
+ id: Int, notaryIdentity: Pair?, entropyRoot: BigInteger): MockNetwork.MockNode {
+ return MockNetwork.MockNode(config, network, networkMapAddr, bobAddr.id, notaryIdentity, entropyRoot)
+ }
+ }, BOB_NAME)
+
+ // Find the future representing the result of this state machine again.
+ val bobFuture = bobNode.smm.findStateMachines(BuyerAcceptor::class.java).single().second
+
+ // And off we go again.
+ mockNet.runNetwork()
+
+ // Bob is now finished and has the same transaction as Alice.
+ assertThat(bobFuture.getOrThrow()).isEqualTo(aliceFuture.getOrThrow())
+
+ assertThat(bobNode.smm.findStateMachines(Buyer::class.java)).isEmpty()
+ bobNode.database.transaction {
+ assertThat(bobNode.checkpointStorage.checkpoints()).isEmpty()
+ }
+ aliceNode.database.transaction {
+ assertThat(aliceNode.checkpointStorage.checkpoints()).isEmpty()
+ }
+
+ bobNode.database.transaction {
+ val restoredBobTransactions = bobTransactionsBeforeCrash.filter {
+ bobNode.services.validatedTransactions.getTransaction(it.id) != null
+ }
+ assertThat(restoredBobTransactions).containsAll(bobTransactionsBeforeCrash)
+ }
+
+ aliceNode.internals.manuallyCloseDB()
+ bobNode.internals.manuallyCloseDB()
+ }
+ }
+
+ // Creates a mock node with an overridden storage service that uses a RecordingMap, that lets us test the order
+ // of gets and puts.
+ private fun makeNodeWithTracking(name: CordaX500Name): StartedNode {
+ // Create a node in the mock network ...
+ return mockNet.createNode(nodeFactory = object : MockNetwork.Factory {
+ override fun create(config: NodeConfiguration,
+ network: MockNetwork,
+ networkMapAddr: SingleMessageRecipient?,
+ id: Int, notaryIdentity: Pair