mirror of
https://github.com/corda/corda.git
synced 2024-12-28 00:38:55 +00:00
Merge pull request #56 from corda/christians_perftestflows
Isolated cash and commercial paper for performance testing
This commit is contained in:
commit
8428f78821
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -86,6 +86,8 @@
|
||||
<module name="node_test" target="1.8" />
|
||||
<module name="notary-demo_main" target="1.8" />
|
||||
<module name="notary-demo_test" target="1.8" />
|
||||
<module name="perftestcordapp_main" target="1.8" />
|
||||
<module name="perftestcordapp_test" target="1.8" />
|
||||
<module name="publish-utils_main" target="1.8" />
|
||||
<module name="publish-utils_test" target="1.8" />
|
||||
<module name="quasar-hook_main" target="1.8" />
|
||||
|
49
perftestcordapp/build.gradle
Normal file
49
perftestcordapp/build.gradle
Normal file
@ -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
|
||||
}
|
@ -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 <T : Any> AMOUNT(amount: Int, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token)
|
||||
fun <T : Any> AMOUNT(amount: Double, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount), token)
|
||||
fun DOLLARS(amount: Int): Amount<Currency> = AMOUNT(amount, USD)
|
||||
fun DOLLARS(amount: Double): Amount<Currency> = AMOUNT(amount, USD)
|
||||
fun POUNDS(amount: Int): Amount<Currency> = AMOUNT(amount, GBP)
|
||||
fun SWISS_FRANCS(amount: Int): Amount<Currency> = AMOUNT(amount, CHF)
|
||||
|
||||
val Int.DOLLARS: Amount<Currency> get() = DOLLARS(this)
|
||||
val Double.DOLLARS: Amount<Currency> get() = DOLLARS(this)
|
||||
val Int.POUNDS: Amount<Currency> get() = POUNDS(this)
|
||||
val Int.SWISS_FRANCS: Amount<Currency> get() = SWISS_FRANCS(this)
|
||||
|
||||
infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
|
||||
infix fun Amount<Currency>.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
|
||||
infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
|
||||
infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit))
|
||||
|
@ -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<Issued<Currency>>,
|
||||
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<Issued<Currency>>): State = copy(faceValue = newFaceValue)
|
||||
fun withMaturityDate(newMaturityDate: Instant): State = copy(maturityDate = newMaturityDate)
|
||||
|
||||
/** Object Relational Mapping support. */
|
||||
override fun supportedSchemas(): Iterable<MappedSchema> = 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<CommercialPaper.Commands>()
|
||||
val timeWindow: TimeWindow? = tx.timeWindow
|
||||
|
||||
// Suppress compiler warning as 'key' is an unused variable when destructuring 'groups'.
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
when (command.value) {
|
||||
is Commands.Move -> {
|
||||
val input = inputs.single()
|
||||
requireThat {
|
||||
"the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers)
|
||||
"the state is propagated" using (outputs.size == 1)
|
||||
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
|
||||
// the input ignoring the owner field due to the grouping.
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.Redeem -> {
|
||||
// Redemption of the paper requires movement of on-ledger cash.
|
||||
val input = inputs.single()
|
||||
val received = tx.outputStates.sumCashBy(input.owner)
|
||||
val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must have a time-window")
|
||||
requireThat {
|
||||
"the paper must have matured" using (time >= input.maturityDate)
|
||||
"the received amount equals the face value" using (received == input.faceValue)
|
||||
"the paper must be destroyed" using outputs.isEmpty()
|
||||
"the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers)
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.Issue -> {
|
||||
val output = outputs.single()
|
||||
val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances have a time-window")
|
||||
requireThat {
|
||||
// Don't allow people to issue commercial paper under other entities identities.
|
||||
"output states are issued by a command signer" using
|
||||
(output.issuance.party.owningKey in command.signers)
|
||||
"output values sum to more than the inputs" using (output.faceValue.quantity > 0)
|
||||
"the maturity date is not in the past" using (time < output.maturityDate)
|
||||
// Don't allow an existing CP state to be replaced by this issuance.
|
||||
// TODO: Consider how to handle the case of mistaken issuances, or other need to patch.
|
||||
"output values sum to more than the inputs" using inputs.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Think about how to evolve contracts over time with new commands.
|
||||
else -> throw IllegalArgumentException("Unrecognised command")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a transaction that issues commercial paper, owned by the issuing parties key. Does not update
|
||||
* an existing transaction because you aren't able to issue multiple pieces of CP in a single transaction
|
||||
* at the moment: this restriction is not fundamental and may be lifted later.
|
||||
*/
|
||||
fun generateIssue(issuance: PartyAndReference, faceValue: Amount<Issued<Currency>>, maturityDate: Instant,
|
||||
notary: Party): TransactionBuilder {
|
||||
val state = State(issuance, issuance.party, faceValue, maturityDate)
|
||||
return TransactionBuilder(notary = notary).withItems(StateAndContract(state, CP_PROGRAM_ID), Command(Commands.Issue(), issuance.party.owningKey))
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given partial transaction with an input/output/command to reassign ownership of the paper.
|
||||
*/
|
||||
fun generateMove(tx: TransactionBuilder, paper: StateAndRef<State>, newOwner: AbstractParty) {
|
||||
tx.addInputState(paper)
|
||||
tx.addOutputState(paper.state.data.withOwner(newOwner), CP_PROGRAM_ID)
|
||||
tx.addCommand(Commands.Move(), paper.state.data.owner.owningKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Intended to be called by the issuer of some commercial paper, when an owner has notified us that they wish
|
||||
* to redeem the paper. We must therefore send enough money to the key that owns the paper to satisfy the face
|
||||
* value, and then ensure the paper is removed from the ledger.
|
||||
*
|
||||
* @throws InsufficientBalanceException if the vault doesn't contain enough money to pay the redeemer.
|
||||
*/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
@Suspendable
|
||||
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, services: ServiceHub, ourIdentity: PartyAndCertificate) {
|
||||
// Add the cash movement using the states in our vault.
|
||||
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)
|
||||
}
|
||||
}
|
@ -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<Currency, Cash.Commands, Cash.State>() {
|
||||
override fun extractCommands(commands: Collection<CommandWithParties<CommandData>>): List<CommandWithParties<Cash.Commands>>
|
||||
= commands.select<Cash.Commands>()
|
||||
|
||||
// DOCSTART 1
|
||||
/** A state representing a cash claim against some party. */
|
||||
data class State(
|
||||
override val amount: Amount<Issued<Currency>>,
|
||||
|
||||
/** There must be a MoveCommand signed by this key to claim the amount. */
|
||||
override val owner: AbstractParty
|
||||
) : FungibleAsset<Currency>, QueryableState {
|
||||
constructor(deposit: PartyAndReference, amount: Amount<Currency>, owner: AbstractParty)
|
||||
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
|
||||
|
||||
override val exitKeys = setOf(owner.owningKey, amount.token.issuer.party.owningKey)
|
||||
override val participants = listOf(owner)
|
||||
|
||||
override fun withNewOwnerAndAmount(newAmount: Amount<Issued<Currency>>, newOwner: AbstractParty): FungibleAsset<Currency>
|
||||
= copy(amount = amount.copy(newAmount.quantity), owner = newOwner)
|
||||
|
||||
override fun toString() = "${Emoji.bagOfCash}Cash($amount at ${amount.token.issuer} owned by $owner)"
|
||||
|
||||
override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner))
|
||||
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<MappedSchema> = 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<out Contract>? = null) : MoveCommand
|
||||
|
||||
/**
|
||||
* Allows new cash states to be issued into existence.
|
||||
*/
|
||||
class Issue : TypeOnlyCommandData()
|
||||
|
||||
/**
|
||||
* A command stating that money has been withdrawn from the shared ledger and is now accounted for
|
||||
* in some other way.
|
||||
*/
|
||||
data class Exit(val amount: Amount<Issued<Currency>>) : CommandData
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.
|
||||
*/
|
||||
fun generateIssue(tx: TransactionBuilder, tokenDef: Issued<Currency>, pennies: Long, owner: AbstractParty, notary: Party)
|
||||
= generateIssue(tx, Amount(pennies, tokenDef), owner, notary)
|
||||
|
||||
/**
|
||||
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
|
||||
*/
|
||||
fun generateIssue(tx: TransactionBuilder, amount: Amount<Issued<Currency>>, owner: AbstractParty, notary: Party)
|
||||
= generateIssue(tx, TransactionState(State(amount, owner), PROGRAM_ID, notary), Commands.Issue())
|
||||
|
||||
override fun deriveState(txState: TransactionState<State>, amount: Amount<Issued<Currency>>, owner: AbstractParty)
|
||||
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
|
||||
|
||||
override fun generateExitCommand(amount: Amount<Issued<Currency>>) = Commands.Exit(amount)
|
||||
override fun generateMoveCommand() = Commands.Move()
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
// Each group is a set of input/output states with distinct (reference, currency) attributes. These types
|
||||
// of cash are not fungible and must be kept separated for bookkeeping purposes.
|
||||
val groups = tx.groupStates { it: 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<Commands.Issue>().firstOrNull()
|
||||
if (issueCommand != null) {
|
||||
verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer)
|
||||
} else {
|
||||
val inputAmount = inputs.sumCashOrNull() ?: throw IllegalArgumentException("there is at least one cash input for this group")
|
||||
val outputAmount = outputs.sumCashOrZero(Issued(issuer, currency))
|
||||
|
||||
// If we want to remove cash from the ledger, that must be signed for by the issuer.
|
||||
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero.
|
||||
val exitKeys: Set<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
|
||||
val exitCommand = tx.commands.select<Commands.Exit>(parties = null, signers = exitKeys).filter { it.value.amount.token == key }.singleOrNull()
|
||||
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, Issued(issuer, currency))
|
||||
|
||||
requireThat {
|
||||
"there are no zero sized inputs" using inputs.none { it.amount.quantity == 0L }
|
||||
"for reference ${issuer.reference} at issuer ${issuer.party} the amounts balance: ${inputAmount.quantity} - ${amountExitingLedger.quantity} != ${outputAmount.quantity}" using
|
||||
(inputAmount == outputAmount + amountExitingLedger)
|
||||
}
|
||||
|
||||
verifyMoveCommand<Commands.Move>(inputs, tx.commands)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyIssueCommand(inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
tx: LedgerTransaction,
|
||||
issueCommand: CommandWithParties<Commands.Issue>,
|
||||
currency: Currency,
|
||||
issuer: PartyAndReference) {
|
||||
// If we have an issue command, perform special processing: the group is allowed to have no inputs,
|
||||
// and the output states must have a deposit reference owned by the signer.
|
||||
//
|
||||
// Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must
|
||||
// sum to more than the inputs. An issuance of zero size is not allowed.
|
||||
//
|
||||
// Note that this means literally anyone with access to the network can issue cash claims of arbitrary
|
||||
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some
|
||||
// as-yet-unwritten identity service. See ADP-22 for discussion.
|
||||
|
||||
// The grouping ensures that all outputs have the same deposit reference and currency.
|
||||
val inputAmount = inputs.sumCashOrZero(Issued(issuer, currency))
|
||||
val outputAmount = outputs.sumCash()
|
||||
val cashCommands = tx.commands.select<Commands.Issue>()
|
||||
requireThat {
|
||||
// TODO: This doesn't work with the trader demo, so use the underlying key instead
|
||||
// "output states are issued by a command signer" by (issuer.party in issueCommand.signingParties)
|
||||
"output states are issued by a command signer" using (issuer.party.owningKey in issueCommand.signers)
|
||||
"output values sum to more than the inputs" using (outputAmount > inputAmount)
|
||||
"there is only a single issue command" using (cashCommands.count() == 1)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PROGRAM_ID: ContractClassName = "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<Currency>,
|
||||
to: AbstractParty,
|
||||
onlyFromParties: Set<AbstractParty> = emptySet()) = generateSpend(services, tx, listOf(PartyAndAmount(to, amount)), services.myInfo.legalIdentitiesAndCerts.single(), onlyFromParties)
|
||||
|
||||
/**
|
||||
* Generate a transaction that moves an amount of currency to the given party.
|
||||
*
|
||||
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
|
||||
*
|
||||
* @param services The [ServiceHub] to provide access to the database session.
|
||||
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
|
||||
* to move the cash will be added on top.
|
||||
* @param amount How much currency to send.
|
||||
* @param to the recipient party.
|
||||
* @param ourIdentity well known identity to create a new confidential identity from, for sending change to.
|
||||
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
|
||||
* of given parties. This can be useful if the party you're trying to pay has expectations
|
||||
* about which type of asset claims they are willing to accept.
|
||||
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
|
||||
* the resulting transaction for it to be valid.
|
||||
* @throws InsufficientBalanceException when a cash spending transaction fails because
|
||||
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
@Suspendable
|
||||
fun generateSpend(services: ServiceHub,
|
||||
tx: TransactionBuilder,
|
||||
amount: Amount<Currency>,
|
||||
ourIdentity: PartyAndCertificate,
|
||||
to: AbstractParty,
|
||||
onlyFromParties: Set<AbstractParty> = emptySet()): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
return generateSpend(services, tx, listOf(PartyAndAmount(to, amount)), ourIdentity, onlyFromParties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a transaction that moves money of the given amounts to the recipients specified, and sends any change
|
||||
* back to sole identity of the calling node. Fails for nodes with multiple identities.
|
||||
*
|
||||
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
|
||||
*
|
||||
* @param services The [ServiceHub] to provide access to the database session.
|
||||
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
|
||||
* to move the cash will be added on top.
|
||||
* @param payments A list of amounts to pay, and the party to send the payment to.
|
||||
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
|
||||
* of given parties. This can be useful if the party you're trying to pay has expectations
|
||||
* about which type of asset claims they are willing to accept.
|
||||
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
|
||||
* the resulting transaction for it to be valid.
|
||||
* @throws InsufficientBalanceException when a cash spending transaction fails because
|
||||
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
@Suspendable
|
||||
@Deprecated("Our identity should be specified", replaceWith = ReplaceWith("generateSpend(services, tx, amount, to, ourIdentity, onlyFromParties)"))
|
||||
fun generateSpend(services: ServiceHub,
|
||||
tx: TransactionBuilder,
|
||||
payments: List<PartyAndAmount<Currency>>,
|
||||
onlyFromParties: Set<AbstractParty> = emptySet()) = generateSpend(services, tx, payments, services.myInfo.legalIdentitiesAndCerts.single(), onlyFromParties)
|
||||
|
||||
/**
|
||||
* Generate a transaction that moves money of the given amounts to the recipients specified.
|
||||
*
|
||||
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
|
||||
*
|
||||
* @param services The [ServiceHub] to provide access to the database session.
|
||||
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
|
||||
* to move the cash will be added on top.
|
||||
* @param payments A list of amounts to pay, and the party to send the payment to.
|
||||
* @param ourIdentity well known identity to create a new confidential identity from, for sending change to.
|
||||
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
|
||||
* of given parties. This can be useful if the party you're trying to pay has expectations
|
||||
* about which type of asset claims they are willing to accept.
|
||||
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
|
||||
* the resulting transaction for it to be valid.
|
||||
* @throws InsufficientBalanceException when a cash spending transaction fails because
|
||||
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
@Suspendable
|
||||
fun generateSpend(services: ServiceHub,
|
||||
tx: TransactionBuilder,
|
||||
payments: List<PartyAndAmount<Currency>>,
|
||||
ourIdentity: PartyAndCertificate,
|
||||
onlyFromParties: Set<AbstractParty> = emptySet()): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
fun deriveState(txState: TransactionState<Cash.State>, amt: Amount<Issued<Currency>>, owner: AbstractParty)
|
||||
= txState.copy(data = txState.data.copy(amount = amt, owner = owner))
|
||||
|
||||
// Retrieve unspent and unlocked cash states that meet our spending criteria.
|
||||
val totalAmount = payments.map { it.amount }.sumOrThrow()
|
||||
val cashSelection = 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<Currency>.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<Issued<Currency>>.STATE: Cash.State get() = Cash.State(this, NULL_PARTY)
|
@ -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<T : Any>(val party: AbstractParty, val amount: Amount<T>)
|
||||
|
||||
/**
|
||||
* An asset transaction may split and merge assets represented by a set of (issuer, depositRef) pairs, across multiple
|
||||
* input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour (a blend of
|
||||
* issuer+depositRef) and you couldn't merge outputs of two colours together, but you COULD put them in the same
|
||||
* transaction.
|
||||
*
|
||||
* The goal of this design is to ensure that assets can be withdrawn from the ledger easily: if you receive some asset
|
||||
* via this contract, you always know where to go in order to extract it from the R3 ledger, no matter how many hands
|
||||
* it has passed through in the intervening time.
|
||||
*
|
||||
* At the same time, other contracts that just want assets and don't care much who is currently holding it can ignore
|
||||
* the issuer/depositRefs and just examine the amount fields.
|
||||
*/
|
||||
abstract class OnLedgerAsset<T : Any, C : CommandData, S : FungibleAsset<T>> : Contract {
|
||||
companion object {
|
||||
val log = loggerFor<OnLedgerAsset<*, *, *>>()
|
||||
|
||||
/**
|
||||
* Generate a transaction that moves an amount of currency to the given pubkey.
|
||||
*
|
||||
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
|
||||
*
|
||||
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
|
||||
* to move the cash will be added on top.
|
||||
* @param amount How much currency to send.
|
||||
* @param to a key of the recipient.
|
||||
* @param acceptableStates a list of acceptable input states to use.
|
||||
* @param payChangeTo party to pay any change to; this is normally a confidential identity of the calling
|
||||
* party.
|
||||
* @param deriveState a function to derive an output state based on an input state, amount for the output
|
||||
* and public key to pay to.
|
||||
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
|
||||
* the resulting transaction for it to be valid.
|
||||
* @throws InsufficientBalanceException when a cash spending transaction fails because
|
||||
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
|
||||
*/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
@JvmStatic
|
||||
fun <S : FungibleAsset<T>, T : Any> generateSpend(tx: TransactionBuilder,
|
||||
amount: Amount<T>,
|
||||
to: AbstractParty,
|
||||
acceptableStates: List<StateAndRef<S>>,
|
||||
payChangeTo: AbstractParty,
|
||||
deriveState: (TransactionState<S>, Amount<Issued<T>>, AbstractParty) -> TransactionState<S>,
|
||||
generateMoveCommand: () -> CommandData): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
return generateSpend(tx, listOf(PartyAndAmount(to, amount)), acceptableStates, payChangeTo, deriveState, generateMoveCommand)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds to the given transaction states that move amounts of a fungible asset to the given parties, using only
|
||||
* the provided acceptable input states to find a solution (not all of them may be used in the end). A change
|
||||
* output will be generated if the state amounts don't exactly fit.
|
||||
*
|
||||
* The fungible assets must all be of the same type and the amounts must be summable i.e. amounts of the same
|
||||
* token.
|
||||
*
|
||||
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
|
||||
* to move the cash will be added on top.
|
||||
* @param amount How much currency to send.
|
||||
* @param to a key of the recipient.
|
||||
* @param acceptableStates a list of acceptable input states to use.
|
||||
* @param payChangeTo party to pay any change to; this is normally a confidential identity of the calling
|
||||
* party. We use a new confidential identity here so that the recipient is not identifiable.
|
||||
* @param deriveState a function to derive an output state based on an input state, amount for the output
|
||||
* and public key to pay to.
|
||||
* @param T A type representing a token
|
||||
* @param S A fungible asset state type
|
||||
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
|
||||
* the resulting transaction for it to be valid.
|
||||
* @throws InsufficientBalanceException when a cash spending transaction fails because
|
||||
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
|
||||
*/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
@JvmStatic
|
||||
fun <S : FungibleAsset<T>, T : Any> generateSpend(tx: TransactionBuilder,
|
||||
payments: List<PartyAndAmount<T>>,
|
||||
acceptableStates: List<StateAndRef<S>>,
|
||||
payChangeTo: AbstractParty,
|
||||
deriveState: (TransactionState<S>, Amount<Issued<T>>, AbstractParty) -> TransactionState<S>,
|
||||
generateMoveCommand: () -> CommandData): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
// Discussion
|
||||
//
|
||||
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.
|
||||
//
|
||||
// First we must select a set of asset states (which for convenience we will call 'coins' here, as in bitcoinj).
|
||||
// The input states can be considered our "vault", and may consist of different products, and with different
|
||||
// issuers and deposits.
|
||||
//
|
||||
// Coin selection is a complex problem all by itself and many different approaches can be used. It is easily
|
||||
// possible for different actors to use different algorithms and approaches that, for example, compete on
|
||||
// privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of
|
||||
// obfuscation and so on.
|
||||
//
|
||||
// Having selected input states of the correct asset, we must craft output states for the amount we're sending and
|
||||
// the "change", which goes back to us. The change is required to make the amounts balance. We may need more
|
||||
// than one change output in order to avoid merging assets from different deposits. The point of this design
|
||||
// is to ensure that ledger entries are immutable and globally identifiable.
|
||||
//
|
||||
// Finally, we add the states to the provided partial transaction.
|
||||
|
||||
// TODO: We should be prepared to produce multiple transactions spending inputs from
|
||||
// different notaries, or at least group states by notary and take the set with the
|
||||
// highest total value.
|
||||
|
||||
// TODO: Check that re-running this on the same transaction multiple times does the right thing.
|
||||
|
||||
// The notary may be associated with a locked state only.
|
||||
tx.notary = acceptableStates.firstOrNull()?.state?.notary
|
||||
|
||||
// Calculate the total amount we're sending (they must be all of a compatible token).
|
||||
val totalSendAmount = payments.map { it.amount }.sumOrThrow()
|
||||
// Select a subset of the available states we were given that sums up to >= totalSendAmount.
|
||||
val (gathered, gatheredAmount) = gatherCoins(acceptableStates, totalSendAmount)
|
||||
check(gatheredAmount >= totalSendAmount)
|
||||
val keysUsed = gathered.map { it.state.data.owner.owningKey }
|
||||
|
||||
// Now calculate the output states. This is complicated by the fact that a single payment may require
|
||||
// multiple output states, due to the need to keep states separated by issuer. We start by figuring out
|
||||
// how much we've gathered for each issuer: this map will keep track of how much we've used from each
|
||||
// as we work our way through the payments.
|
||||
val statesGroupedByIssuer = gathered.groupBy { it.state.data.amount.token }
|
||||
val remainingFromEachIssuer = statesGroupedByIssuer
|
||||
.mapValues {
|
||||
it.value.map {
|
||||
it.state.data.amount
|
||||
}.sumOrThrow()
|
||||
}.toList().toMutableList()
|
||||
val outputStates = mutableListOf<TransactionState<S>>()
|
||||
for ((party, paymentAmount) in payments) {
|
||||
var remainingToPay = paymentAmount.quantity
|
||||
while (remainingToPay > 0) {
|
||||
val (token, remainingFromCurrentIssuer) = remainingFromEachIssuer.last()
|
||||
val templateState = statesGroupedByIssuer[token]!!.first().state
|
||||
val delta = remainingFromCurrentIssuer.quantity - remainingToPay
|
||||
when {
|
||||
delta > 0 -> {
|
||||
// The states from the current issuer more than covers this payment.
|
||||
outputStates += deriveState(templateState, Amount(remainingToPay, token), party)
|
||||
remainingFromEachIssuer[0] = Pair(token, Amount(delta, token))
|
||||
remainingToPay = 0
|
||||
}
|
||||
delta == 0L -> {
|
||||
// The states from the current issuer exactly covers this payment.
|
||||
outputStates += deriveState(templateState, Amount(remainingToPay, token), party)
|
||||
remainingFromEachIssuer.removeAt(remainingFromEachIssuer.lastIndex)
|
||||
remainingToPay = 0
|
||||
}
|
||||
delta < 0 -> {
|
||||
// The states from the current issuer don't cover this payment, so we'll have to use >1 output
|
||||
// state to cover this payment.
|
||||
outputStates += deriveState(templateState, remainingFromCurrentIssuer, party)
|
||||
remainingFromEachIssuer.removeAt(remainingFromEachIssuer.lastIndex)
|
||||
remainingToPay -= remainingFromCurrentIssuer.quantity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Whatever values we have left over for each issuer must become change outputs.
|
||||
for ((token, amount) in remainingFromEachIssuer) {
|
||||
val templateState = statesGroupedByIssuer[token]!!.first().state
|
||||
outputStates += deriveState(templateState, amount, payChangeTo)
|
||||
}
|
||||
|
||||
for (state in gathered) tx.addInputState(state)
|
||||
for (state in outputStates) tx.addOutputState(state)
|
||||
|
||||
// What if we already have a move command with the right keys? Filter it out here or in platform code?
|
||||
tx.addCommand(generateMoveCommand(), keysUsed)
|
||||
|
||||
return Pair(tx, keysUsed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather assets from the given list of states, sufficient to match or exceed the given amount.
|
||||
*
|
||||
* @param acceptableCoins list of states to use as inputs.
|
||||
* @param amount the amount to gather states up to.
|
||||
* @throws InsufficientBalanceException if there isn't enough value in the states to cover the requested amount.
|
||||
*/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
private fun <S : FungibleAsset<T>, T : Any> gatherCoins(acceptableCoins: Collection<StateAndRef<S>>,
|
||||
amount: Amount<T>): Pair<ArrayList<StateAndRef<S>>, Amount<T>> {
|
||||
require(amount.quantity > 0) { "Cannot gather zero coins" }
|
||||
val gathered = arrayListOf<StateAndRef<S>>()
|
||||
var gatheredAmount = Amount(0, amount.token)
|
||||
for (c in acceptableCoins) {
|
||||
if (gatheredAmount >= amount) break
|
||||
gathered.add(c)
|
||||
gatheredAmount += Amount(c.state.data.amount.quantity, amount.token)
|
||||
}
|
||||
|
||||
if (gatheredAmount < amount) {
|
||||
log.trace { "Insufficient balance: requested $amount, available $gatheredAmount" }
|
||||
throw InsufficientBalanceException(amount - gatheredAmount)
|
||||
}
|
||||
|
||||
log.trace { "Gathered coins: requested $amount, available $gatheredAmount, change: ${gatheredAmount - amount}" }
|
||||
|
||||
return Pair(gathered, gatheredAmount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an transaction exiting fungible assets from the ledger.
|
||||
*
|
||||
* @param tx transaction builder to add states and commands to.
|
||||
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
|
||||
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
|
||||
* the responsibility of the caller to check that they do not attempt to exit funds held by others.
|
||||
* @return the public keys which must sign the transaction for it to be valid.
|
||||
*/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
@JvmStatic
|
||||
@Deprecated("Replaced with generateExit() which takes in a party to pay change to")
|
||||
fun <S : FungibleAsset<T>, T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
|
||||
assetStates: List<StateAndRef<S>>,
|
||||
deriveState: (TransactionState<S>, Amount<Issued<T>>, AbstractParty) -> TransactionState<S>,
|
||||
generateMoveCommand: () -> CommandData,
|
||||
generateExitCommand: (Amount<Issued<T>>) -> CommandData): Set<PublicKey> {
|
||||
val owner = assetStates.map { it.state.data.owner }.toSet().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 <S : FungibleAsset<T>, T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
|
||||
assetStates: List<StateAndRef<S>>,
|
||||
payChangeTo: AbstractParty,
|
||||
deriveState: (TransactionState<S>, Amount<Issued<T>>, AbstractParty) -> TransactionState<S>,
|
||||
generateMoveCommand: () -> CommandData,
|
||||
generateExitCommand: (Amount<Issued<T>>) -> CommandData): Set<PublicKey> {
|
||||
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 <S : FungibleAsset<T>, T : Any> generateIssue(tx: TransactionBuilder,
|
||||
transactionState: TransactionState<S>,
|
||||
issueCommand: CommandData): Set<PublicKey> {
|
||||
check(tx.inputStates().isEmpty())
|
||||
check(tx.outputStates().map { it.data }.filterIsInstance(transactionState.javaClass).isEmpty())
|
||||
require(transactionState.data.amount.quantity > 0)
|
||||
val at = transactionState.data.amount.token.issuer
|
||||
val commandSigner = at.party.owningKey
|
||||
tx.addOutputState(transactionState)
|
||||
tx.addCommand(issueCommand, commandSigner)
|
||||
return setOf(commandSigner)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun extractCommands(commands: Collection<CommandWithParties<CommandData>>): Collection<CommandWithParties<C>>
|
||||
|
||||
/**
|
||||
* Generate an transaction exiting assets from the ledger.
|
||||
*
|
||||
* @param tx transaction builder to add states and commands to.
|
||||
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
|
||||
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
|
||||
* the responsibility of the caller to check that they do not exit funds held by others.
|
||||
* @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<Issued<T>>,
|
||||
assetStates: List<StateAndRef<S>>): Set<PublicKey> {
|
||||
return generateExit(
|
||||
tx,
|
||||
amountIssued,
|
||||
assetStates,
|
||||
deriveState = { state, amount, owner -> deriveState(state, amount, owner) },
|
||||
generateMoveCommand = { -> generateMoveCommand() },
|
||||
generateExitCommand = { amount -> generateExitCommand(amount) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an transaction exiting assets from the ledger.
|
||||
*
|
||||
* @param tx transaction builder to add states and commands to.
|
||||
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
|
||||
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
|
||||
* the responsibility of the caller to check that they do not exit funds held by others.
|
||||
* @return the public keys which must sign the transaction for it to be valid.
|
||||
*/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
|
||||
assetStates: List<StateAndRef<S>>,
|
||||
payChangeTo: AbstractParty): Set<PublicKey> {
|
||||
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<Issued<T>>): CommandData
|
||||
abstract fun generateMoveCommand(): MoveCommand
|
||||
|
||||
/**
|
||||
* Derive a new transaction state based on the given example, with amount and owner modified. This allows concrete
|
||||
* implementations to have fields in their state which we don't know about here, and we simply leave them untouched
|
||||
* when sending out "change" from spending/exiting.
|
||||
*/
|
||||
abstract fun deriveState(txState: TransactionState<S>, amount: Amount<Issued<T>>, owner: AbstractParty): TransactionState<S>
|
||||
}
|
@ -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<AbstractCashSelection>()
|
||||
|
||||
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<AbstractCashSelection>()
|
||||
}
|
||||
|
||||
// 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<Currency>, lockId: UUID, notary: Party?,
|
||||
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : 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<Currency>,
|
||||
onlyFromIssuerParties: Set<AbstractParty> = emptySet(),
|
||||
notary: Party? = null,
|
||||
lockId: UUID,
|
||||
withIssuerRefs: Set<OpaqueBytes> = emptySet()): List<StateAndRef<Cash.State>> {
|
||||
val stateAndRefs = mutableListOf<StateAndRef<Cash.State>>()
|
||||
|
||||
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<Currency>, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>, stateAndRefs: MutableList<StateAndRef<Cash.State>>): 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<TransactionState<Cash.State>>(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
|
||||
}
|
||||
}
|
@ -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<CashSelectionH2Impl>()
|
||||
}
|
||||
|
||||
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<Currency>, lockId: UUID, notary: Party?,
|
||||
onlyFromIssuerParties: Set<AbstractParty>, withIssuerRefs: Set<OpaqueBytes>) : 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()
|
||||
}
|
||||
}
|
@ -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<out T>(override val progressTracker: ProgressTracker) : FlowLogic<T>() {
|
||||
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<Party>, 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<Currency>)
|
||||
}
|
||||
|
||||
class CashException(message: String, cause: Throwable) : FlowException(message, cause)
|
@ -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<Currency>
|
||||
|
||||
init {
|
||||
// Warning!! You are about to see a major hack!
|
||||
val baseDirectory = services.declaredField<Any>("serviceHub").value
|
||||
.let { it.javaClass.getMethod("getConfiguration").apply { isAccessible = true }.invoke(it) }
|
||||
.declaredField<Path>("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<CashConfiguration>() {
|
||||
@Suspendable
|
||||
override fun call(): CashConfiguration {
|
||||
val configHolder = serviceHub.cordaService(ConfigHolder::class.java)
|
||||
return CashConfiguration(configHolder.issuableCurrencies, supportedCurrencies)
|
||||
}
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
data class CashConfiguration(val issuableCurrencies: List<Currency>, val supportedCurrencies: List<Currency>)
|
@ -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<Currency>,
|
||||
private val issuerRef: OpaqueBytes,
|
||||
progressTracker: ProgressTracker) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
|
||||
constructor(amount: Amount<Currency>, 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<Cash.State>(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<Party> = 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<Currency>, val issueRef: OpaqueBytes) : AbstractRequest(amount)
|
||||
}
|
@ -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<Currency>,
|
||||
val issueRef: OpaqueBytes,
|
||||
val recipient: Party,
|
||||
val anonymous: Boolean,
|
||||
val notary: Party,
|
||||
progressTracker: ProgressTracker) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
|
||||
constructor(amount: Amount<Currency>,
|
||||
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<Currency>,
|
||||
val issueRef: OpaqueBytes,
|
||||
val recipient: Party,
|
||||
val notary: Party,
|
||||
val anonymous: Boolean) : AbstractRequest(amount)
|
||||
}
|
@ -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<Currency>,
|
||||
private val issuerBankPartyRef: OpaqueBytes,
|
||||
private val notary: Party,
|
||||
progressTracker: ProgressTracker) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
|
||||
constructor(amount: Amount<Currency>,
|
||||
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<Currency>, val issueRef: OpaqueBytes, val notary: Party) : AbstractRequest(amount)
|
||||
}
|
@ -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<Currency>,
|
||||
val recipient: Party,
|
||||
val anonymous: Boolean,
|
||||
progressTracker: ProgressTracker,
|
||||
val issuerConstraint: Set<Party> = emptySet()) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
|
||||
/** A straightforward constructor that constructs spends using cash states of any issuer. */
|
||||
constructor(amount: Amount<Currency>, recipient: Party) : this(amount, recipient, true, tracker())
|
||||
/** A straightforward constructor that constructs spends using cash states of any issuer. */
|
||||
constructor(amount: Amount<Currency>, 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<Party, AnonymousParty>()
|
||||
}
|
||||
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<Currency>,
|
||||
val recipient: Party,
|
||||
val anonymous: Boolean,
|
||||
val issuerConstraint: Set<Party> = emptySet()) : AbstractRequest(amount)
|
||||
}
|
@ -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<Currency>) : 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<Currency>,
|
||||
val payToIdentity: PartyAndCertificate
|
||||
)
|
||||
|
||||
open class Seller(private val otherSideSession: FlowSession,
|
||||
private val assetToSell: StateAndRef<OwnableState>,
|
||||
private val price: Amount<Currency>,
|
||||
private val myParty: PartyAndCertificate, // TODO Left because in tests it's used to pass anonymous party.
|
||||
override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
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<ContractState> = 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<Currency>,
|
||||
private val typeToBuy: Class<out OwnableState>,
|
||||
private val anonymous: Boolean) : FlowLogic<SignedTransaction>() {
|
||||
constructor(otherSideSession: FlowSession, notary: Party, acceptablePrice: Amount<Currency>, typeToBuy: Class<out OwnableState>) :
|
||||
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<StateAndRef<OwnableState>, SellerTradeInfo> {
|
||||
val assetForSale = subFlow(ReceiveStateAndRefFlow<OwnableState>(sellerSession)).single()
|
||||
return assetForSale to sellerSession.receive<SellerTradeInfo>().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<OwnableState>, 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<PublicKey>)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
@ -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()
|
||||
}
|
@ -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<ContractState>.sumCashBy(owner: AbstractParty): Amount<Issued<Currency>> = filterIsInstance<Cash.State>().filter { it.owner == owner }.map { it.amount }.sumOrThrow()
|
||||
|
||||
/**
|
||||
* Sums the cash states in the list, throwing an exception if there are none, or if any of the cash
|
||||
* states cannot be added together (i.e. are different currencies or issuers).
|
||||
*/
|
||||
fun Iterable<ContractState>.sumCash(): Amount<Issued<Currency>> = filterIsInstance<Cash.State>().map { it.amount }.sumOrThrow()
|
||||
|
||||
/** Sums the cash states in the list, returning null if there are none. */
|
||||
fun Iterable<ContractState>.sumCashOrNull(): Amount<Issued<Currency>>? = filterIsInstance<Cash.State>().map { it.amount }.sumOrNull()
|
||||
|
||||
/** Sums the cash states in the list, returning zero of the given currency+issuer if there are none. */
|
||||
fun Iterable<ContractState>.sumCashOrZero(currency: Issued<Currency>): Amount<Issued<Currency>> {
|
||||
return filterIsInstance<Cash.State>().map { it.amount }.sumOrZero(currency)
|
||||
}
|
||||
|
||||
/** Sums the asset states in the list, returning null if there are none. */
|
||||
fun <T : Any> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrNull()
|
||||
|
||||
/** Sums the asset states in the list, returning zero of the given token if there are none. */
|
||||
fun <T : Any> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrZero(token)
|
||||
|
@ -0,0 +1,2 @@
|
||||
com.r3.corda.enterprise.perftestcordapp.contracts.asset.cash.selection.CashSelectionH2Impl
|
||||
|
@ -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<CommercialPaper.State>().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<TransactionDSLInterpreter>.outputs(aliceGetsBack: Amount<Issued<Currency>>) {
|
||||
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<CommercialPaper.State>() }
|
||||
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<ContractState>
|
||||
private lateinit var bigCorpVaultService: VaultService
|
||||
|
||||
private lateinit var aliceServices: MockServices
|
||||
private lateinit var aliceVaultService: VaultService
|
||||
private lateinit var alicesVault: Vault<ContractState>
|
||||
|
||||
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<SignedTransaction, UUID> {
|
||||
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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Currency>,
|
||||
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<Cash.State> {
|
||||
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<SignedTransaction> = 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<Cash.State>(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<StateAndRef<Cash.State>>
|
||||
|
||||
@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<Cash.State>().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<Cash.State>().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<Cash.State>(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<Currency>, 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<Currency>, 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<Currency>, 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<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 1) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Try exiting with a reference mis-match.
|
||||
*/
|
||||
@Test
|
||||
fun generateInvalidReferenceExit() {
|
||||
initialiseTestSerialization()
|
||||
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 2) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Try exiting an amount greater than the maximum available.
|
||||
*/
|
||||
@Test
|
||||
fun generateInsufficientExit() {
|
||||
initialiseTestSerialization()
|
||||
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 1000.DOLLARS, MEGA_CORP, 1) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Try exiting for an owner with no states
|
||||
*/
|
||||
@Test
|
||||
fun generateOwnerWithNoStatesExit() {
|
||||
initialiseTestSerialization()
|
||||
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, CHARLIE, 1) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Try exiting when vault is empty
|
||||
*/
|
||||
@Test
|
||||
fun generateExitWithEmptyVault() {
|
||||
initialiseTestSerialization()
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
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<Cash.State> = vaultStatesUnconsumed.elementAt(0)
|
||||
val vaultState1: StateAndRef<Cash.State> = vaultStatesUnconsumed.elementAt(1)
|
||||
val vaultState2: StateAndRef<Cash.State> = 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<Cash.State>()
|
||||
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<Cash.State>()
|
||||
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<Cash.State>().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<Cash.State>().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)
|
||||
}
|
||||
}
|
@ -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<MockNode>
|
||||
private lateinit var bankOfCorda: Party
|
||||
private lateinit var notaryNode: StartedNode<MockNode>
|
||||
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<Cash.State>().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<CashException> {
|
||||
future.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
@ -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<MockNode>
|
||||
private lateinit var bankOfCorda: Party
|
||||
private lateinit var notaryNode: StartedNode<MockNode>
|
||||
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<Cash.State>().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<IllegalArgumentException> {
|
||||
future.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
@ -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<MockNode>
|
||||
private lateinit var bankOfCorda: Party
|
||||
private lateinit var notaryNode: StartedNode<MockNode>
|
||||
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<Cash.State>(criteria)
|
||||
val (_, vaultUpdatesBankClient) = notaryNode.services.vaultService.trackBy<Cash.State>(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<CashException> {
|
||||
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<IllegalArgumentException> {
|
||||
future.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Checkpoint> {
|
||||
val checkpoints = mutableListOf<Checkpoint>()
|
||||
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<Boolean> {
|
||||
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<MockNetwork.MockNode> {
|
||||
override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?,
|
||||
id: Int, notaryIdentity: Pair<ServiceInfo, KeyPair>?, 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<MockNetwork.MockNode> {
|
||||
// Create a node in the mock network ...
|
||||
return mockNet.createNode(nodeFactory = object : MockNetwork.Factory<MockNetwork.MockNode> {
|
||||
override fun create(config: NodeConfiguration,
|
||||
network: MockNetwork,
|
||||
networkMapAddr: SingleMessageRecipient?,
|
||||
id: Int, notaryIdentity: Pair<ServiceInfo, KeyPair>?,
|
||||
entropyRoot: BigInteger): MockNetwork.MockNode {
|
||||
return object : MockNetwork.MockNode(config, network, networkMapAddr, id, notaryIdentity, entropyRoot) {
|
||||
// That constructs a recording tx storage
|
||||
override fun makeTransactionStorage(): WritableTransactionStorage {
|
||||
return RecordingTransactionStorage(database, super.makeTransactionStorage())
|
||||
}
|
||||
}
|
||||
}
|
||||
}, legalName = name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check dependencies of sale asset are resolved`() {
|
||||
mockNet = MockNetwork(false, cordappPackages = cordappPackages)
|
||||
val notaryNode = mockNet.createNotaryNode()
|
||||
val aliceNode = makeNodeWithTracking(ALICE_NAME)
|
||||
val bobNode = makeNodeWithTracking(BOB_NAME)
|
||||
val bankNode = makeNodeWithTracking(BOC_NAME)
|
||||
mockNet.runNetwork()
|
||||
notaryNode.internals.ensureRegistered()
|
||||
val notary = aliceNode.services.getDefaultNotary()
|
||||
val alice = aliceNode.info.singleIdentity()
|
||||
val bob = bobNode.info.singleIdentity()
|
||||
val bank = bankNode.info.singleIdentity()
|
||||
val issuer = bank.ref(1, 2, 3)
|
||||
|
||||
ledger(aliceNode.services, initialiseSerialization = false) {
|
||||
|
||||
// Insert a prospectus type attachment into the commercial paper transaction.
|
||||
val stream = ByteArrayOutputStream()
|
||||
JarOutputStream(stream).use {
|
||||
it.putNextEntry(ZipEntry("Prospectus.txt"))
|
||||
it.write("Our commercial paper is top notch stuff".toByteArray())
|
||||
it.closeEntry()
|
||||
}
|
||||
val attachmentID = aliceNode.database.transaction {
|
||||
attachment(ByteArrayInputStream(stream.toByteArray()))
|
||||
}
|
||||
|
||||
val bobsFakeCash = bobNode.database.transaction {
|
||||
fillUpForBuyer(false, issuer, AnonymousParty(bob.owningKey), notary)
|
||||
}.second
|
||||
val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, bankNode)
|
||||
val alicesFakePaper = aliceNode.database.transaction {
|
||||
fillUpForSeller(false, issuer, alice,
|
||||
1200.DOLLARS `issued by` bank.ref(0), attachmentID, notary).second
|
||||
}
|
||||
val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
|
||||
|
||||
mockNet.runNetwork() // Clear network map registration messages
|
||||
|
||||
runBuyerAndSeller(notary, aliceNode, bobNode, "alice's paper".outputStateAndRef())
|
||||
|
||||
mockNet.runNetwork()
|
||||
|
||||
run {
|
||||
val records = (bobNode.services.validatedTransactions as RecordingTransactionStorage).records
|
||||
// Check Bobs's database accesses as Bob's cash transactions are downloaded by Alice.
|
||||
records.expectEvents(isStrict = false) {
|
||||
sequence(
|
||||
// Buyer Bob is told about Alice's commercial paper, but doesn't know it ..
|
||||
expect(TxRecord.Get(alicesFakePaper[0].id)),
|
||||
// He asks and gets the tx, validates it, sees it's a self issue with no dependencies, stores.
|
||||
expect(TxRecord.Add(alicesSignedTxns.values.first())),
|
||||
// Alice gets Bob's proposed transaction and doesn't know his two cash states. She asks, Bob answers.
|
||||
expect(TxRecord.Get(bobsFakeCash[1].id)),
|
||||
expect(TxRecord.Get(bobsFakeCash[2].id)),
|
||||
// Alice notices that Bob's cash txns depend on a third tx she also doesn't know. She asks, Bob answers.
|
||||
expect(TxRecord.Get(bobsFakeCash[0].id))
|
||||
)
|
||||
}
|
||||
|
||||
// Bob has downloaded the attachment.
|
||||
bobNode.database.transaction {
|
||||
bobNode.services.attachments.openAttachment(attachmentID)!!.openAsJAR().use {
|
||||
it.nextJarEntry
|
||||
val contents = it.reader().readText()
|
||||
assertTrue(contents.contains("Our commercial paper is top notch stuff"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// And from Alice's perspective ...
|
||||
run {
|
||||
val records = (aliceNode.services.validatedTransactions as RecordingTransactionStorage).records
|
||||
records.expectEvents(isStrict = false) {
|
||||
sequence(
|
||||
// Seller Alice sends her seller info to Bob, who wants to check the asset for sale.
|
||||
// He requests, Alice looks up in her DB to send the tx to Bob
|
||||
expect(TxRecord.Get(alicesFakePaper[0].id)),
|
||||
// Seller Alice gets a proposed tx which depends on Bob's two cash txns and her own tx.
|
||||
expect(TxRecord.Get(bobsFakeCash[1].id)),
|
||||
expect(TxRecord.Get(bobsFakeCash[2].id)),
|
||||
expect(TxRecord.Get(alicesFakePaper[0].id)),
|
||||
// Alice notices that Bob's cash txns depend on a third tx she also doesn't know.
|
||||
expect(TxRecord.Get(bobsFakeCash[0].id)),
|
||||
// Bob answers with the transactions that are now all verifiable, as Alice bottomed out.
|
||||
// Bob's transactions are valid, so she commits to the database
|
||||
expect(TxRecord.Add(bobsSignedTxns[bobsFakeCash[0].id]!!)),
|
||||
expect(TxRecord.Get(bobsFakeCash[0].id)), // Verify
|
||||
expect(TxRecord.Add(bobsSignedTxns[bobsFakeCash[2].id]!!)),
|
||||
expect(TxRecord.Get(bobsFakeCash[0].id)), // Verify
|
||||
expect(TxRecord.Add(bobsSignedTxns[bobsFakeCash[1].id]!!)),
|
||||
// Now she verifies the transaction is contract-valid (not signature valid) which means
|
||||
// looking up the states again.
|
||||
expect(TxRecord.Get(bobsFakeCash[1].id)),
|
||||
expect(TxRecord.Get(bobsFakeCash[2].id)),
|
||||
expect(TxRecord.Get(alicesFakePaper[0].id)),
|
||||
// Alice needs to look up the input states to find out which Notary they point to
|
||||
expect(TxRecord.Get(bobsFakeCash[1].id)),
|
||||
expect(TxRecord.Get(bobsFakeCash[2].id)),
|
||||
expect(TxRecord.Get(alicesFakePaper[0].id))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `track works`() {
|
||||
mockNet = MockNetwork(false, cordappPackages = cordappPackages)
|
||||
val notaryNode = mockNet.createNotaryNode()
|
||||
val aliceNode = makeNodeWithTracking(ALICE_NAME)
|
||||
val bobNode = makeNodeWithTracking(BOB_NAME)
|
||||
val bankNode = makeNodeWithTracking(BOC_NAME)
|
||||
|
||||
mockNet.runNetwork()
|
||||
notaryNode.internals.ensureRegistered()
|
||||
val notary = aliceNode.services.getDefaultNotary()
|
||||
val alice: Party = aliceNode.info.singleIdentity()
|
||||
val bank: Party = bankNode.info.singleIdentity()
|
||||
val issuer = bank.ref(1, 2, 3)
|
||||
|
||||
ledger(aliceNode.services, initialiseSerialization = false) {
|
||||
// Insert a prospectus type attachment into the commercial paper transaction.
|
||||
val stream = ByteArrayOutputStream()
|
||||
JarOutputStream(stream).use {
|
||||
it.putNextEntry(ZipEntry("Prospectus.txt"))
|
||||
it.write("Our commercial paper is top notch stuff".toByteArray())
|
||||
it.closeEntry()
|
||||
}
|
||||
val attachmentID = aliceNode.database.transaction {
|
||||
attachment(ByteArrayInputStream(stream.toByteArray()))
|
||||
}
|
||||
|
||||
val bobsKey = bobNode.services.keyManagementService.keys.single()
|
||||
val bobsFakeCash = bobNode.database.transaction {
|
||||
fillUpForBuyer(false, issuer, AnonymousParty(bobsKey), notary)
|
||||
}.second
|
||||
insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, bankNode)
|
||||
|
||||
val alicesFakePaper = aliceNode.database.transaction {
|
||||
fillUpForSeller(false, issuer, alice,
|
||||
1200.DOLLARS `issued by` bank.ref(0), attachmentID, notary).second
|
||||
}
|
||||
|
||||
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
|
||||
|
||||
mockNet.runNetwork() // Clear network map registration messages
|
||||
|
||||
val aliceTxStream = aliceNode.services.validatedTransactions.track().updates
|
||||
val aliceTxMappings = with(aliceNode) {
|
||||
database.transaction { services.stateMachineRecordedTransactionMapping.track().updates }
|
||||
}
|
||||
val aliceSmId = runBuyerAndSeller(notary, aliceNode, bobNode,
|
||||
"alice's paper".outputStateAndRef()).sellerId
|
||||
|
||||
mockNet.runNetwork()
|
||||
|
||||
// We need to declare this here, if we do it inside [expectEvents] kotlin throws an internal compiler error(!).
|
||||
val aliceTxExpectations = sequence(
|
||||
expect { tx: SignedTransaction ->
|
||||
require(tx.id == bobsFakeCash[0].id)
|
||||
},
|
||||
expect { tx: SignedTransaction ->
|
||||
require(tx.id == bobsFakeCash[2].id)
|
||||
},
|
||||
expect { tx: SignedTransaction ->
|
||||
require(tx.id == bobsFakeCash[1].id)
|
||||
}
|
||||
)
|
||||
aliceTxStream.expectEvents { aliceTxExpectations }
|
||||
val aliceMappingExpectations = sequence(
|
||||
expect<StateMachineTransactionMapping> { (stateMachineRunId, transactionId) ->
|
||||
require(stateMachineRunId == aliceSmId)
|
||||
require(transactionId == bobsFakeCash[0].id)
|
||||
},
|
||||
expect { (stateMachineRunId, transactionId) ->
|
||||
require(stateMachineRunId == aliceSmId)
|
||||
require(transactionId == bobsFakeCash[2].id)
|
||||
},
|
||||
expect { (stateMachineRunId, transactionId) ->
|
||||
require(stateMachineRunId == aliceSmId)
|
||||
require(transactionId == bobsFakeCash[1].id)
|
||||
}
|
||||
)
|
||||
aliceTxMappings.expectEvents { aliceMappingExpectations }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dependency with error on buyer side`() {
|
||||
mockNet = MockNetwork(false, cordappPackages = cordappPackages)
|
||||
ledger(MockServices(cordappPackages), initialiseSerialization = false) {
|
||||
runWithError(true, false, "at least one cash input")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dependency with error on seller side`() {
|
||||
mockNet = MockNetwork(false, cordappPackages = cordappPackages)
|
||||
ledger(MockServices(cordappPackages), initialiseSerialization = false) {
|
||||
runWithError(false, true, "Issuances have a time-window")
|
||||
}
|
||||
}
|
||||
|
||||
private data class RunResult(
|
||||
// The buyer is not created immediately, only when the seller starts running
|
||||
val buyer: CordaFuture<FlowStateMachine<*>>,
|
||||
val sellerResult: CordaFuture<SignedTransaction>,
|
||||
val sellerId: StateMachineRunId
|
||||
)
|
||||
|
||||
private fun runBuyerAndSeller(notary: Party,
|
||||
sellerNode: StartedNode<MockNetwork.MockNode>,
|
||||
buyerNode: StartedNode<MockNetwork.MockNode>,
|
||||
assetToSell: StateAndRef<OwnableState>): RunResult {
|
||||
val buyerFlows: Observable<out FlowLogic<*>> = buyerNode.internals.registerInitiatedFlow(BuyerAcceptor::class.java)
|
||||
val firstBuyerFiber = buyerFlows.toFuture().map { it.stateMachine }
|
||||
val seller = SellerInitiator(buyerNode.info.chooseIdentity(), notary, assetToSell, 1000.DOLLARS, anonymous)
|
||||
val sellerResult = sellerNode.services.startFlow(seller).resultFuture
|
||||
return RunResult(firstBuyerFiber, sellerResult, seller.stateMachine.id)
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
class SellerInitiator(private val buyer: Party,
|
||||
private val notary: Party,
|
||||
private val assetToSell: StateAndRef<OwnableState>,
|
||||
private val price: Amount<Currency>,
|
||||
private val anonymous: Boolean) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val myPartyAndCert = if (anonymous) {
|
||||
serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
|
||||
} else {
|
||||
ourIdentityAndCert
|
||||
}
|
||||
val buyerSession = initiateFlow(buyer)
|
||||
buyerSession.send(TestTx(notary, price, anonymous))
|
||||
return subFlow(Seller(
|
||||
buyerSession,
|
||||
assetToSell,
|
||||
price,
|
||||
myPartyAndCert))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(SellerInitiator::class)
|
||||
class BuyerAcceptor(private val sellerSession: FlowSession) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val (notary, price, anonymous) = sellerSession.receive<TestTx>().unwrap {
|
||||
require(serviceHub.networkMapCache.isNotary(it.notaryIdentity)) { "${it.notaryIdentity} is not a notary" }
|
||||
it
|
||||
}
|
||||
return subFlow(Buyer(sellerSession, notary, price, CommercialPaper.State::class.java, anonymous))
|
||||
}
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
data class TestTx(val notaryIdentity: Party, val price: Amount<Currency>, val anonymous: Boolean)
|
||||
|
||||
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.runWithError(
|
||||
bobError: Boolean,
|
||||
aliceError: Boolean,
|
||||
expectedMessageSubstring: String
|
||||
) {
|
||||
val notaryNode = mockNet.createNotaryNode()
|
||||
val aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
val bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
val bankNode = mockNet.createPartyNode(BOC_NAME)
|
||||
|
||||
mockNet.runNetwork()
|
||||
notaryNode.internals.ensureRegistered()
|
||||
val notary = aliceNode.services.getDefaultNotary()
|
||||
val alice = aliceNode.info.singleIdentity()
|
||||
val bob = bobNode.info.singleIdentity()
|
||||
val bank = bankNode.info.singleIdentity()
|
||||
val issuer = bank.ref(1, 2, 3)
|
||||
|
||||
val bobsBadCash = bobNode.database.transaction {
|
||||
fillUpForBuyer(bobError, issuer, bob, notary).second
|
||||
}
|
||||
val alicesFakePaper = aliceNode.database.transaction {
|
||||
fillUpForSeller(aliceError, issuer, alice,1200.DOLLARS `issued by` issuer, null, notary).second
|
||||
}
|
||||
|
||||
insertFakeTransactions(bobsBadCash, bobNode, notaryNode, bankNode)
|
||||
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
|
||||
|
||||
mockNet.runNetwork() // Clear network map registration messages
|
||||
|
||||
val (bobStateMachine, aliceResult) = runBuyerAndSeller(notary, aliceNode, bobNode, "alice's paper".outputStateAndRef())
|
||||
|
||||
mockNet.runNetwork()
|
||||
|
||||
val e = assertFailsWith<TransactionVerificationException> {
|
||||
if (bobError)
|
||||
aliceResult.getOrThrow()
|
||||
else
|
||||
bobStateMachine.getOrThrow().resultFuture.getOrThrow()
|
||||
}
|
||||
val underlyingMessage = e.rootCause.message!!
|
||||
if (expectedMessageSubstring !in underlyingMessage) {
|
||||
assertEquals(expectedMessageSubstring, underlyingMessage)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun insertFakeTransactions(
|
||||
wtxToSign: List<WireTransaction>,
|
||||
node: StartedNode<*>,
|
||||
notaryNode: StartedNode<*>,
|
||||
vararg extraSigningNodes: StartedNode<*>): Map<SecureHash, SignedTransaction> {
|
||||
|
||||
val signed = wtxToSign.map {
|
||||
val id = it.id
|
||||
val sigs = mutableListOf<TransactionSignature>()
|
||||
val nodeKey = node.info.chooseIdentity().owningKey
|
||||
sigs.add(node.services.keyManagementService.sign(SignableData(id, SignatureMetadata(1, Crypto.findSignatureScheme(nodeKey).schemeNumberID)), nodeKey))
|
||||
sigs.add(notaryNode.services.keyManagementService.sign(SignableData(id, SignatureMetadata(1,
|
||||
Crypto.findSignatureScheme(notaryNode.info.legalIdentities[1].owningKey).schemeNumberID)), notaryNode.info.legalIdentities[1].owningKey))
|
||||
extraSigningNodes.forEach { currentNode ->
|
||||
sigs.add(currentNode.services.keyManagementService.sign(
|
||||
SignableData(id, SignatureMetadata(1, Crypto.findSignatureScheme(currentNode.info.chooseIdentity().owningKey).schemeNumberID)),
|
||||
currentNode.info.chooseIdentity().owningKey)
|
||||
)
|
||||
}
|
||||
SignedTransaction(it, sigs)
|
||||
}
|
||||
return node.database.transaction {
|
||||
node.services.recordTransactions(signed)
|
||||
val validatedTransactions = node.services.validatedTransactions
|
||||
if (validatedTransactions is RecordingTransactionStorage) {
|
||||
validatedTransactions.records.clear()
|
||||
}
|
||||
signed.associateBy { it.id }
|
||||
}
|
||||
}
|
||||
|
||||
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.fillUpForBuyer(
|
||||
withError: Boolean,
|
||||
issuer: PartyAndReference,
|
||||
owner: AbstractParty,
|
||||
notary: Party): Pair<Vault<ContractState>, List<WireTransaction>> {
|
||||
val interimOwner = issuer.party
|
||||
// Bob (Buyer) has some cash he got from the Bank of Elbonia, Alice (Seller) has some commercial paper she
|
||||
// wants to sell to Bob.
|
||||
val eb1 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) {
|
||||
// Issued money to itself.
|
||||
output(Cash.PROGRAM_ID, "elbonian money 1", notary = notary) { 800.DOLLARS.CASH issuedBy issuer ownedBy interimOwner }
|
||||
output(Cash.PROGRAM_ID, "elbonian money 2", notary = notary) { 1000.DOLLARS.CASH issuedBy issuer ownedBy interimOwner }
|
||||
if (!withError) {
|
||||
command(issuer.party.owningKey) { Cash.Commands.Issue() }
|
||||
} else {
|
||||
// Put a broken command on so at least a signature is created
|
||||
command(issuer.party.owningKey) { Cash.Commands.Move() }
|
||||
}
|
||||
timeWindow(TEST_TX_TIME)
|
||||
if (withError) {
|
||||
this.fails()
|
||||
} else {
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
|
||||
// Bob gets some cash onto the ledger from BoE
|
||||
val bc1 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) {
|
||||
input("elbonian money 1")
|
||||
output(Cash.PROGRAM_ID, "bob cash 1", notary = notary) { 800.DOLLARS.CASH issuedBy issuer ownedBy owner }
|
||||
command(interimOwner.owningKey) { Cash.Commands.Move() }
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
val bc2 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) {
|
||||
input("elbonian money 2")
|
||||
output(Cash.PROGRAM_ID, "bob cash 2", notary = notary) { 300.DOLLARS.CASH issuedBy issuer ownedBy owner }
|
||||
output(Cash.PROGRAM_ID, notary = notary) { 700.DOLLARS.CASH issuedBy issuer ownedBy interimOwner } // Change output.
|
||||
command(interimOwner.owningKey) { Cash.Commands.Move() }
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
val vault = Vault<ContractState>(listOf("bob cash 1".outputStateAndRef(), "bob cash 2".outputStateAndRef()))
|
||||
return Pair(vault, listOf(eb1, bc1, bc2))
|
||||
}
|
||||
|
||||
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.fillUpForSeller(
|
||||
withError: Boolean,
|
||||
issuer: PartyAndReference,
|
||||
owner: AbstractParty,
|
||||
amount: Amount<Issued<Currency>>,
|
||||
attachmentID: SecureHash?,
|
||||
notary: Party): Pair<Vault<ContractState>, List<WireTransaction>> {
|
||||
val ap = transaction(transactionBuilder = TransactionBuilder(notary = notary)) {
|
||||
output(CommercialPaper.CP_PROGRAM_ID, "alice's paper", notary = notary) {
|
||||
CommercialPaper.State(issuer, owner, amount, TEST_TX_TIME + 7.days)
|
||||
}
|
||||
command(issuer.party.owningKey) { CommercialPaper.Commands.Issue() }
|
||||
if (!withError)
|
||||
timeWindow(time = TEST_TX_TIME)
|
||||
if (attachmentID != null)
|
||||
attachment(attachmentID)
|
||||
if (withError) {
|
||||
this.fails()
|
||||
} else {
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
|
||||
val vault = Vault<ContractState>(listOf("alice's paper".outputStateAndRef()))
|
||||
return Pair(vault, listOf(ap))
|
||||
}
|
||||
|
||||
|
||||
class RecordingTransactionStorage(val database: CordaPersistence, val delegate: WritableTransactionStorage) : WritableTransactionStorage, SingletonSerializeAsToken() {
|
||||
override fun track(): DataFeed<List<SignedTransaction>, SignedTransaction> {
|
||||
return database.transaction {
|
||||
delegate.track()
|
||||
}
|
||||
}
|
||||
|
||||
val records: MutableList<TxRecord> = Collections.synchronizedList(ArrayList<TxRecord>())
|
||||
override val updates: Observable<SignedTransaction>
|
||||
get() = delegate.updates
|
||||
|
||||
override fun addTransaction(transaction: SignedTransaction): Boolean {
|
||||
database.transaction {
|
||||
records.add(TxRecord.Add(transaction))
|
||||
delegate.addTransaction(transaction)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getTransaction(id: SecureHash): SignedTransaction? {
|
||||
return database.transaction {
|
||||
records.add(TxRecord.Get(id))
|
||||
delegate.getTransaction(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface TxRecord {
|
||||
data class Add(val transaction: SignedTransaction) : TxRecord
|
||||
data class Get(val id: SecureHash) : TxRecord
|
||||
}
|
||||
|
||||
}
|
@ -47,4 +47,5 @@ include 'doorman'
|
||||
include 'verify-enclave'
|
||||
include 'sgx-jvm/hsm-tool'
|
||||
include 'signing-server'
|
||||
include 'perftestcordapp'
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user