Cash flows and unit tests

This commit is contained in:
Christian Sailer 2017-10-10 17:00:36 +01:00
parent e0b684b3ea
commit 7ab94650a6
9 changed files with 619 additions and 0 deletions

View File

@ -0,0 +1,53 @@
package net.corda.ptflows.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 AbstractPtCashFlow<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 PtCashException(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 PtCashException(message: String, cause: Throwable) : FlowException(message, cause)

View File

@ -0,0 +1,40 @@
package net.corda.ptflows.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StartableByRPC
import net.corda.core.serialization.CordaSerializable
import net.corda.ptflows.CHF
import net.corda.ptflows.EUR
import net.corda.ptflows.GBP
import net.corda.ptflows.USD
import java.util.*
/**
* Flow to obtain cash cordapp app configuration.
*/
@StartableByRPC
class PtCashConfigDataFlow : FlowLogic<PtCashConfiguration>() {
companion object {
private val supportedCurrencies = listOf(USD, GBP, CHF, EUR)
}
@Suspendable
override fun call(): PtCashConfiguration {
val issuableCurrencies = supportedCurrencies.mapNotNull {
try {
// Currently it uses checkFlowPermission to determine the list of issuable currency as a temporary hack.
// TODO: get the config from proper configuration source.
checkFlowPermission("corda.issuer.$it", emptyMap())
it
} catch (e: FlowException) {
null
}
}
return PtCashConfiguration(issuableCurrencies, supportedCurrencies)
}
}
@CordaSerializable
data class PtCashConfiguration(val issuableCurrencies: List<Currency>, val supportedCurrencies: List<Currency>)

View File

@ -0,0 +1,82 @@
package net.corda.ptflows.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 net.corda.ptflows.contracts.asset.PtCash
import net.corda.ptflows.contracts.asset.PtCashSelection
import net.corda.ptflows.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 PtCashExitFlow(private val amount: Amount<Currency>,
private val issuerRef: OpaqueBytes,
progressTracker: ProgressTracker) : AbstractPtCashFlow<AbstractPtCashFlow.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(PtCashException::class)
override fun call(): AbstractPtCashFlow.Result {
progressTracker.currentStep = GENERATING_TX
val builder = TransactionBuilder(notary = null)
val issuer = ourIdentity.ref(issuerRef)
val exitStates = PtCashSelection
.getInstance { serviceHub.jdbcSession().metaData }
.unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference))
val signers = try {
PtCash().generateExit(
builder,
amount.issuedBy(issuer),
exitStates)
} catch (e: InsufficientBalanceException) {
throw PtCashException("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.vaultQueryService.queryBy<PtCash.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)
}

View File

@ -0,0 +1,49 @@
package net.corda.ptflows.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 PtCashIssueAndPaymentFlow(val amount: Amount<Currency>,
val issueRef: OpaqueBytes,
val recipient: Party,
val anonymous: Boolean,
val notary: Party,
progressTracker: ProgressTracker) : AbstractPtCashFlow<AbstractPtCashFlow.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(PtCashIssueFlow(amount, issueRef, notary))
return subFlow(PtCashPaymentFlow(amount, recipient, anonymous))
}
@CordaSerializable
class IssueAndPaymentRequest(amount: Amount<Currency>,
val issueRef: OpaqueBytes,
val recipient: Party,
val notary: Party,
val anonymous: Boolean) : AbstractRequest(amount)
}

View File

@ -0,0 +1,52 @@
package net.corda.ptflows.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 net.corda.ptflows.contracts.asset.PtCash
import net.corda.ptflows.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 PtCashIssueFlow(private val amount: Amount<Currency>,
private val issuerBankPartyRef: OpaqueBytes,
private val notary: Party,
progressTracker: ProgressTracker) : AbstractPtCashFlow<AbstractPtCashFlow.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(): AbstractPtCashFlow.Result {
progressTracker.currentStep = GENERATING_TX
val builder = TransactionBuilder(notary)
val issuer = ourIdentity.ref(issuerBankPartyRef)
val signers = PtCash().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)
}

View File

@ -0,0 +1,74 @@
package net.corda.ptflows.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 net.corda.ptflows.contracts.asset.PtCash
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 PtCashPaymentFlow(
val amount: Amount<Currency>,
val recipient: Party,
val anonymous: Boolean,
progressTracker: ProgressTracker,
val issuerConstraint: Set<Party> = emptySet()) : AbstractPtCashFlow<AbstractPtCashFlow.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(): AbstractPtCashFlow.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 {
PtCash.generateSpend(serviceHub,
builder,
amount,
ourIdentityAndCert,
anonymousRecipient,
issuerConstraint)
} catch (e: InsufficientBalanceException) {
throw PtCashException("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)
}

View File

@ -0,0 +1,79 @@
package net.corda.ptflows.contracts.flows
import net.corda.core.identity.Party
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.ptflows.flows.PtCashException
import net.corda.ptflows.flows.PtCashExitFlow
import net.corda.ptflows.flows.PtCashIssueFlow
import net.corda.ptflows.DOLLARS
import net.corda.ptflows.`issued by`
import net.corda.ptflows.contracts.asset.PtCash
import net.corda.node.internal.StartedNode
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 net.corda.testing.setCordappPackages
import net.corda.testing.unsetCordappPackages
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() {
setCordappPackages("net.corda.ptflows.contracts.asset")
mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin())
val nodes = mockNet.createSomeNodes(1)
notaryNode = nodes.notaryNode
bankOfCordaNode = nodes.partyNodes[0]
bankOfCorda = bankOfCordaNode.info.chooseIdentity()
mockNet.runNetwork()
notary = bankOfCordaNode.services.getDefaultNotary()
val future = bankOfCordaNode.services.startFlow(PtCashIssueFlow(initialBalance, ref, notary)).resultFuture
mockNet.runNetwork()
future.getOrThrow()
}
@After
fun cleanUp() {
mockNet.stopNodes()
unsetCordappPackages()
}
@Test
fun `exit some cash`() {
val exitAmount = 500.DOLLARS
val future = bankOfCordaNode.services.startFlow(PtCashExitFlow(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<PtCash.State>().single()
assertEquals(expected, output.amount)
}
@Test
fun `exit zero cash`() {
val expected = 0.DOLLARS
val future = bankOfCordaNode.services.startFlow(PtCashExitFlow(expected, ref)).resultFuture
mockNet.runNetwork()
assertFailsWith<PtCashException> {
future.getOrThrow()
}
}
}

View File

@ -0,0 +1,69 @@
package net.corda.ptflows.contracts.flows
import net.corda.core.identity.Party
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.ptflows.DOLLARS
import net.corda.ptflows.`issued by`
import net.corda.ptflows.contracts.asset.PtCash
import net.corda.node.internal.StartedNode
import net.corda.ptflows.flows.PtCashIssueFlow
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 net.corda.testing.setCordappPackages
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() {
setCordappPackages("net.corda.ptflows.contracts.asset")
mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin())
val nodes = mockNet.createSomeNodes(1)
notaryNode = nodes.notaryNode
bankOfCordaNode = nodes.partyNodes[0]
bankOfCorda = bankOfCordaNode.info.chooseIdentity()
mockNet.runNetwork()
notary = bankOfCordaNode.services.getDefaultNotary()
}
@After
fun cleanUp() {
mockNet.stopNodes()
}
@Test
fun `issue some cash`() {
val expected = 500.DOLLARS
val ref = OpaqueBytes.of(0x01)
val future = bankOfCordaNode.services.startFlow(PtCashIssueFlow(expected, ref, notary)).resultFuture
mockNet.runNetwork()
val issueTx = future.getOrThrow().stx
val output = issueTx.tx.outputsOfType<PtCash.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(PtCashIssueFlow(expected, ref, notary)).resultFuture
mockNet.runNetwork()
assertFailsWith<IllegalArgumentException> {
future.getOrThrow()
}
}
}

View File

@ -0,0 +1,121 @@
package net.corda.ptflows.contracts.flows
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.ptflows.DOLLARS
import net.corda.ptflows.`issued by`
import net.corda.ptflows.contracts.asset.PtCash
import net.corda.ptflows.flows.PtCashException
import net.corda.ptflows.flows.PtCashIssueFlow
import net.corda.ptflows.flows.PtCashPaymentFlow
import net.corda.node.internal.StartedNode
import net.corda.testing.chooseIdentity
import net.corda.testing.expect
import net.corda.testing.expectEvents
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 net.corda.testing.setCordappPackages
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() {
setCordappPackages("net.corda.ptflows.contracts.asset")
mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin())
val nodes = mockNet.createSomeNodes(1)
notaryNode = nodes.notaryNode
bankOfCordaNode = nodes.partyNodes[0]
bankOfCorda = bankOfCordaNode.info.chooseIdentity()
notary = notaryNode.services.getDefaultNotary()
val future = bankOfCordaNode.services.startFlow(PtCashIssueFlow(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.vaultQueryService.trackBy<PtCash.State>(criteria)
val (_, vaultUpdatesBankClient) = notaryNode.services.vaultQueryService.trackBy<PtCash.State>(criteria)
val future = bankOfCordaNode.services.startFlow(PtCashPaymentFlow(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(PtCashPaymentFlow(expected,
payTo)).resultFuture
mockNet.runNetwork()
assertFailsWith<PtCashException> {
future.getOrThrow()
}
}
@Test
fun `pay zero cash`() {
val payTo = notaryNode.info.chooseIdentity()
val expected = 0.DOLLARS
val future = bankOfCordaNode.services.startFlow(PtCashPaymentFlow(expected,
payTo)).resultFuture
mockNet.runNetwork()
assertFailsWith<IllegalArgumentException> {
future.getOrThrow()
}
}
}