Remove IssuerFlow

* Remove IssuerFlow as it is dangerous and its presence in the finance module risks accidental use in non-test code. As written it will issue arbitary amounts of currency on request from any node on the network, with no validation barring that the currency type is valid.
* Unify interface to CashIssueFlow to match the previous IssuerFlow
This commit is contained in:
Ross Nicoll 2017-08-11 14:03:23 +01:00
parent 3888635055
commit 89476904fc
29 changed files with 130 additions and 363 deletions

View File

@ -111,8 +111,9 @@ class NodeMonitorModelTest : DriverBasedTest() {
val anonymous = false val anonymous = false
rpc.startFlow(::CashIssueFlow, rpc.startFlow(::CashIssueFlow,
Amount(100, USD), Amount(100, USD),
OpaqueBytes(ByteArray(1, { 1 })),
aliceNode.legalIdentity, aliceNode.legalIdentity,
rpc.nodeIdentity().legalIdentity,
OpaqueBytes(ByteArray(1, { 1 })),
notaryNode.notaryIdentity, notaryNode.notaryIdentity,
anonymous anonymous
) )
@ -136,7 +137,8 @@ class NodeMonitorModelTest : DriverBasedTest() {
@Test @Test
fun `cash issue and move`() { fun `cash issue and move`() {
val anonymous = false val anonymous = false
rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow() rpc.startFlow(::CashIssueFlow, 100.DOLLARS, aliceNode.legalIdentity, rpc.nodeIdentity().legalIdentity, OpaqueBytes.of(1),
notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow()
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity, anonymous).returnValue.getOrThrow() rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity, anonymous).returnValue.getOrThrow()
var issueSmId: StateMachineRunId? = null var issueSmId: StateMachineRunId? = null

View File

@ -77,9 +77,10 @@ class CordaRPCClientTest : NodeBasedTest() {
login(rpcUser.username, rpcUser.password) login(rpcUser.username, rpcUser.password)
println("Creating proxy") println("Creating proxy")
println("Starting flow") println("Starting flow")
val flowHandle = connection!!.proxy.startTrackedFlow( val flowHandle = connection!!.proxy.startTrackedFlow(::CashIssueFlow,
::CashIssueFlow, 20.DOLLARS, node.info.legalIdentity,
20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity, node.info.legalIdentity) node.info.legalIdentity, OpaqueBytes.of(0), node.info.legalIdentity, true
)
println("Started flow, waiting on result") println("Started flow, waiting on result")
flowHandle.progress.subscribe { flowHandle.progress.subscribe {
println("PROGRESS $it") println("PROGRESS $it")
@ -113,8 +114,8 @@ class CordaRPCClientTest : NodeBasedTest() {
assertTrue(startCash.isEmpty(), "Should not start with any cash") assertTrue(startCash.isEmpty(), "Should not start with any cash")
val flowHandle = proxy.startFlow(::CashIssueFlow, val flowHandle = proxy.startFlow(::CashIssueFlow,
123.DOLLARS, OpaqueBytes.of(0), 123.DOLLARS, node.info.legalIdentity,
node.info.legalIdentity, node.info.legalIdentity node.info.legalIdentity, OpaqueBytes.of(0), node.info.legalIdentity, true
) )
println("Started issuing cash, waiting on result") println("Started issuing cash, waiting on result")
flowHandle.returnValue.get() flowHandle.returnValue.get()
@ -140,10 +141,11 @@ class CordaRPCClientTest : NodeBasedTest() {
} }
} }
val nodeIdentity = node.info.legalIdentity val nodeIdentity = node.info.legalIdentity
node.services.startFlow(CashIssueFlow(2000.DOLLARS, OpaqueBytes.of(0), nodeIdentity, nodeIdentity), FlowInitiator.Shell).resultFuture.getOrThrow() node.services.startFlow(CashIssueFlow(2000.DOLLARS, nodeIdentity, nodeIdentity, OpaqueBytes.of(0), nodeIdentity, true), FlowInitiator.Shell).resultFuture.getOrThrow()
proxy.startFlow(::CashIssueFlow, proxy.startFlow(::CashIssueFlow,
123.DOLLARS, OpaqueBytes.of(0), 123.DOLLARS, nodeIdentity,
nodeIdentity, nodeIdentity nodeIdentity, OpaqueBytes.of(0), nodeIdentity,
true
).returnValue.getOrThrow() ).returnValue.getOrThrow()
proxy.startFlowDynamic(CashIssueFlow::class.java, proxy.startFlowDynamic(CashIssueFlow::class.java,
1000.DOLLARS, OpaqueBytes.of(0), 1000.DOLLARS, OpaqueBytes.of(0),

View File

@ -421,6 +421,29 @@ inline fun <T : Any, A, B, C, D, reified R : FlowLogic<T>> CordaRPCOps.startTrac
arg3: D arg3: D
): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3) ): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3)
@Suppress("unused")
inline fun <T : Any, A, B, C, D, E, reified R : FlowLogic<T>> CordaRPCOps.startTrackedFlow(
@Suppress("unused_parameter")
flowConstructor: (A, B, C, D, E) -> R,
arg0: A,
arg1: B,
arg2: C,
arg3: D,
arg4: E
): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4)
@Suppress("unused")
inline fun <T : Any, A, B, C, D, E, F, reified R : FlowLogic<T>> CordaRPCOps.startTrackedFlow(
@Suppress("unused_parameter")
flowConstructor: (A, B, C, D, E, F) -> R,
arg0: A,
arg1: B,
arg2: C,
arg3: D,
arg4: E,
arg5: F
): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4, arg5)
/** /**
* The Data feed contains a snapshot of the requested data and an [Observable] of future updates. * The Data feed contains a snapshot of the requested data and an [Observable] of future updates.
*/ */

View File

@ -176,7 +176,7 @@ class ContractUpgradeFlowTest {
fun `upgrade Cash to v2`() { fun `upgrade Cash to v2`() {
// Create some cash. // Create some cash.
val anonymous = false val anonymous = false
val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary, anonymous)).resultFuture val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), a.info.legalIdentity, a.info.legalIdentity, OpaqueBytes.of(1), notary, anonymous)).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
val stx = result.getOrThrow().stx val stx = result.getOrThrow().stx
val stateAndRef = stx.tx.outRef<Cash.State>(0) val stateAndRef = stx.tx.outRef<Cash.State>(0)

View File

@ -42,6 +42,9 @@ UNRELEASED
* Currency-related API in ``net.corda.core.contracts.ContractsDSL`` has moved to ```net.corda.finance.CurrencyUtils`. * Currency-related API in ``net.corda.core.contracts.ContractsDSL`` has moved to ```net.corda.finance.CurrencyUtils`.
* Remove `IssuerFlow` as it allowed nodes to request arbitrary amounts of cash to be issued from any remote node. Use
`CashIssueFlow` instead.
Milestone 14 Milestone 14
------------ ------------

View File

@ -67,9 +67,11 @@ class IntegrationTestingTutorial {
thread { thread {
futures.push(aliceProxy.startFlow(::CashIssueFlow, futures.push(aliceProxy.startFlow(::CashIssueFlow,
i.DOLLARS, i.DOLLARS,
issueRef,
bob.nodeInfo.legalIdentity, bob.nodeInfo.legalIdentity,
notary.nodeInfo.notaryIdentity alice.nodeInfo.legalIdentity,
issueRef,
notary.nodeInfo.notaryIdentity,
true
).returnValue) ).returnValue)
} }
}.forEach(Thread::join) // Ensure the stack of futures is populated. }.forEach(Thread::join) // Ensure the stack of futures is populated.

View File

@ -128,7 +128,7 @@ fun generateTransactions(proxy: CordaRPCOps) {
proxy.startFlow(::CashPaymentFlow, Amount(quantity, USD), me) proxy.startFlow(::CashPaymentFlow, Amount(quantity, USD), me)
} else { } else {
val quantity = Math.abs(random.nextLong() % 1000) val quantity = Math.abs(random.nextLong() % 1000)
proxy.startFlow(::CashIssueFlow, Amount(quantity, USD), issueRef, me, notary) proxy.startFlow(::CashIssueFlow, Amount(quantity, USD), me, me, issueRef, notary, true)
ownedQuantity += quantity ownedQuantity += quantity
} }
} }

View File

@ -132,8 +132,9 @@ object TopupIssuerFlow {
val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity
// invoke Cash subflow to issue Asset // invoke Cash subflow to issue Asset
progressTracker.currentStep = ISSUING progressTracker.currentStep = ISSUING
val issuer = serviceHub.myInfo.legalIdentity
val issueRecipient = serviceHub.myInfo.legalIdentity val issueRecipient = serviceHub.myInfo.legalIdentity
val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, issueRecipient, notaryParty, anonymous = false) val issueCashFlow = CashIssueFlow(amount, issueRecipient, issuer, issuerPartyRef, notaryParty, anonymous = false)
val issueTx = subFlow(issueCashFlow) val issueTx = subFlow(issueCashFlow)
// NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger) // NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger)
// short-circuit when issuing to self // short-circuit when issuing to self

View File

@ -65,10 +65,11 @@ class CustomVaultQueryTest {
private fun issueCashForCurrency(amountToIssue: Amount<Currency>) { private fun issueCashForCurrency(amountToIssue: Amount<Currency>) {
// Use NodeA as issuer and create some dollars // Use NodeA as issuer and create some dollars
val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(amountToIssue, val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(amountToIssue,
OpaqueBytes.of(0x01),
nodeA.info.legalIdentity, nodeA.info.legalIdentity,
nodeA.info.legalIdentity,
OpaqueBytes.of(0x01),
notaryNode.info.notaryIdentity, notaryNode.info.notaryIdentity,
false)) anonymous = false))
// Wait for the flow to stop and print // Wait for the flow to stop and print
flowHandle1.resultFuture.getOrThrow() flowHandle1.resultFuture.getOrThrow()
} }

View File

@ -45,8 +45,9 @@ class FxTransactionBuildTutorialTest {
fun `Run ForeignExchangeFlow to completion`() { fun `Run ForeignExchangeFlow to completion`() {
// Use NodeA as issuer and create some dollars // Use NodeA as issuer and create some dollars
val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(DOLLARS(1000), val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(DOLLARS(1000),
OpaqueBytes.of(0x01),
nodeA.info.legalIdentity, nodeA.info.legalIdentity,
nodeA.info.legalIdentity,
OpaqueBytes.of(0x01),
notaryNode.info.notaryIdentity, notaryNode.info.notaryIdentity,
false)) false))
// Wait for the flow to stop and print // Wait for the flow to stop and print
@ -55,8 +56,9 @@ class FxTransactionBuildTutorialTest {
// Using NodeB as Issuer create some pounds. // Using NodeB as Issuer create some pounds.
val flowHandle2 = nodeB.services.startFlow(CashIssueFlow(POUNDS(1000), val flowHandle2 = nodeB.services.startFlow(CashIssueFlow(POUNDS(1000),
OpaqueBytes.of(0x01),
nodeB.info.legalIdentity, nodeB.info.legalIdentity,
nodeB.info.legalIdentity,
OpaqueBytes.of(0x01),
notaryNode.info.notaryIdentity, notaryNode.info.notaryIdentity,
false)) false))
// Wait for flow to come to an end and print // Wait for flow to come to an end and print

View File

@ -8,6 +8,8 @@ Unreleased
* Merged handling of well known and confidential identities in the identity service. * Merged handling of well known and confidential identities in the identity service.
* Remove `IssuerFlow` as it allowed nodes to request arbitrary amounts of cash to be issued from any remote node.
Milestone 14 Milestone 14
------------ ------------

View File

@ -22,7 +22,7 @@ sealed class CashFlowCommand {
val recipient: Party, val recipient: Party,
val notary: Party, val notary: Party,
val anonymous: Boolean) : CashFlowCommand() { val anonymous: Boolean) : CashFlowCommand() {
override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashIssueFlow, amount, issueRef, recipient, notary, anonymous) override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashIssueFlow, amount, recipient, proxy.nodeIdentity().legalIdentity, issueRef, notary, anonymous)
} }
/** /**

View File

@ -18,39 +18,43 @@ import java.util.*
* Initiates a flow that produces cash issuance transaction. * Initiates a flow that produces cash issuance transaction.
* *
* @param amount the amount of currency to issue. * @param amount the amount of currency to issue.
* @param issueRef a reference to put on the issued currency. * @param issuerBankPartyRef a reference to put on the issued currency.
* @param recipient the party who should own the currency after it is issued. * @param issueTo the party who should own the currency after it is issued.
* @param notary the notary to set on the output states. * @param notary the notary to set on the output states.
*/ */
@StartableByRPC @StartableByRPC
class CashIssueFlow(val amount: Amount<Currency>, class CashIssueFlow(val amount: Amount<Currency>,
val issueRef: OpaqueBytes, val issueTo: Party,
val recipient: Party, val issuerBankParty
: Party,
val issuerBankPartyRef: OpaqueBytes,
val notary: Party, val notary: Party,
val anonymous: Boolean, val anonymous: Boolean,
progressTracker: ProgressTracker) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) { progressTracker: ProgressTracker) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
constructor(amount: Amount<Currency>, constructor(amount: Amount<Currency>,
issueRef: OpaqueBytes, issuerBankPartyRef: OpaqueBytes,
recipient: Party, issuerBankParty: Party,
notary: Party) : this(amount, issueRef, recipient, notary, true, tracker()) issueTo: Party,
notary: Party) : this(amount, issueTo, issuerBankParty, issuerBankPartyRef, notary, true, tracker())
constructor(amount: Amount<Currency>, constructor(amount: Amount<Currency>,
issueRef: OpaqueBytes, issueTo: Party,
recipient: Party, issuerBankParty: Party,
issuerBankPartyRef: OpaqueBytes,
notary: Party, notary: Party,
anonymous: Boolean) : this(amount, issueRef, recipient, notary, anonymous, tracker()) anonymous: Boolean) : this(amount, issueTo, issuerBankParty, issuerBankPartyRef, notary, anonymous, tracker())
@Suspendable @Suspendable
override fun call(): AbstractCashFlow.Result { override fun call(): AbstractCashFlow.Result {
progressTracker.currentStep = GENERATING_ID progressTracker.currentStep = GENERATING_ID
val txIdentities = if (anonymous) { val txIdentities = if (anonymous) {
subFlow(TransactionKeyFlow(recipient)) subFlow(TransactionKeyFlow(issueTo))
} else { } else {
emptyMap<Party, AnonymousParty>() emptyMap<Party, AnonymousParty>()
} }
val anonymousRecipient = txIdentities[recipient] ?: recipient val anonymousRecipient = txIdentities[issueTo] ?: issueTo
progressTracker.currentStep = GENERATING_TX progressTracker.currentStep = GENERATING_TX
val builder: TransactionBuilder = TransactionBuilder(notary) val builder: TransactionBuilder = TransactionBuilder(notary)
val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) val issuer = issuerBankParty.ref(issuerBankPartyRef)
val signers = Cash().generateIssue(builder, amount.issuedBy(issuer), anonymousRecipient, notary) val signers = Cash().generateIssue(builder, amount.issuedBy(issuer), anonymousRecipient, notary)
progressTracker.currentStep = SIGNING_TX progressTracker.currentStep = SIGNING_TX
val tx = serviceHub.signInitialTransaction(builder, signers) val tx = serviceHub.signInitialTransaction(builder, signers)

View File

@ -1,123 +0,0 @@
package net.corda.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.Amount
import net.corda.core.contracts.FungibleAsset
import net.corda.core.contracts.Issued
import net.corda.core.flows.*
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.unwrap
import net.corda.finance.CHF
import net.corda.finance.EUR
import net.corda.finance.GBP
import net.corda.finance.USD
import java.util.*
/**
* This flow enables a client to request issuance of some [FungibleAsset] from a
* server acting as an issuer (see [Issued]) of FungibleAssets.
*
* It is not intended for production usage, but rather for experimentation and testing purposes where it may be
* useful for creation of fake assets.
*/
object IssuerFlow {
@CordaSerializable
data class IssuanceRequestState(val amount: Amount<Currency>,
val issueToParty: Party,
val issuerPartyRef: OpaqueBytes,
val notaryParty: Party,
val anonymous: Boolean)
/**
* IssuanceRequester should be used by a client to ask a remote node to issue some [FungibleAsset] with the given details.
* Returns the transaction created by the Issuer to move the cash to the Requester.
*
* @param anonymous true if the issued asset should be sent to a new confidential identity, false to send it to the
* well known identity (generally this is only used in testing).
*/
@InitiatingFlow
@StartableByRPC
class IssuanceRequester(val amount: Amount<Currency>,
val issueToParty: Party,
val issueToPartyRef: OpaqueBytes,
val issuerBankParty: Party,
val notaryParty: Party,
val anonymous: Boolean) : FlowLogic<AbstractCashFlow.Result>() {
@Suspendable
@Throws(CashException::class)
override fun call(): AbstractCashFlow.Result {
val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef, notaryParty, anonymous)
return sendAndReceive<AbstractCashFlow.Result>(issuerBankParty, issueRequest).unwrap { res ->
val tx = res.stx.tx
val expectedAmount = Amount(amount.quantity, Issued(issuerBankParty.ref(issueToPartyRef), amount.token))
val cashOutputs = tx.filterOutputs<Cash.State> { state -> state.owner == res.recipient }
require(cashOutputs.size == 1) { "Require a single cash output paying ${res.recipient}, found ${tx.outputs}" }
require(cashOutputs.single().amount == expectedAmount) { "Require payment of $expectedAmount"}
res
}
}
}
/**
* Issuer refers to a Node acting as a Bank Issuer of [FungibleAsset], and processes requests from a [IssuanceRequester] client.
* Returns the generated transaction representing the transfer of the [Issued] [FungibleAsset] to the issue requester.
*/
@InitiatedBy(IssuanceRequester::class)
class Issuer(val otherParty: Party) : FlowLogic<SignedTransaction>() {
companion object {
object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request")
object ISSUING : ProgressTracker.Step("Self issuing asset")
object TRANSFERRING : ProgressTracker.Step("Transferring asset to issuance requester")
object SENDING_CONFIRM : ProgressTracker.Step("Confirming asset issuance to requester")
fun tracker() = ProgressTracker(AWAITING_REQUEST, ISSUING, TRANSFERRING, SENDING_CONFIRM)
private val VALID_CURRENCIES = listOf(USD, GBP, EUR, CHF)
}
override val progressTracker: ProgressTracker = tracker()
@Suspendable
@Throws(CashException::class)
override fun call(): SignedTransaction {
progressTracker.currentStep = AWAITING_REQUEST
val issueRequest = receive<IssuanceRequestState>(otherParty).unwrap {
// validate request inputs (for example, lets restrict the types of currency that can be issued)
if (it.amount.token !in VALID_CURRENCIES) throw FlowException("Currency must be one of $VALID_CURRENCIES")
it
}
// TODO: parse request to determine Asset to issue
val txn = issueCashTo(issueRequest.amount, issueRequest.issueToParty, issueRequest.issuerPartyRef, issueRequest.notaryParty, issueRequest.anonymous)
progressTracker.currentStep = SENDING_CONFIRM
send(otherParty, txn)
return txn.stx
}
@Suspendable
private fun issueCashTo(amount: Amount<Currency>,
issueTo: Party,
issuerPartyRef: OpaqueBytes,
notaryParty: Party,
anonymous: Boolean): AbstractCashFlow.Result {
// invoke Cash subflow to issue Asset
progressTracker.currentStep = ISSUING
val issueRecipient = serviceHub.myInfo.legalIdentity
val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, issueRecipient, notaryParty, anonymous)
val issueTx = subFlow(issueCashFlow)
// NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger)
// short-circuit when issuing to self
if (issueTo == serviceHub.myInfo.legalIdentity)
return issueTx
// now invoke Cash subflow to Move issued assetType to issue requester
progressTracker.currentStep = TRANSFERRING
val moveCashFlow = CashPaymentFlow(amount, issueTo, anonymous)
val moveTx = subFlow(moveCashFlow)
// NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger)
return moveTx
}
}
}

View File

@ -33,9 +33,11 @@ class CashExitFlowTests {
bankOfCorda = bankOfCordaNode.info.legalIdentity bankOfCorda = bankOfCordaNode.info.legalIdentity
mockNet.runNetwork() mockNet.runNetwork()
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance,
bankOfCorda, bankOfCorda,
notary)).resultFuture bankOfCorda, ref,
notary,
anonymous = true)).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
future.getOrThrow() future.getOrThrow()
} }

View File

@ -42,9 +42,12 @@ class CashIssueFlowTests {
fun `issue some cash`() { fun `issue some cash`() {
val expected = 500.DOLLARS val expected = 500.DOLLARS
val ref = OpaqueBytes.of(0x01) val ref = OpaqueBytes.of(0x01)
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected,
bankOfCorda, bankOfCorda,
notary)).resultFuture bankOfCorda,
ref,
notary,
anonymous = true)).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
val issueTx = future.getOrThrow().stx val issueTx = future.getOrThrow().stx
val output = issueTx.tx.outputsOfType<Cash.State>().single() val output = issueTx.tx.outputsOfType<Cash.State>().single()
@ -54,9 +57,13 @@ class CashIssueFlowTests {
@Test @Test
fun `issue zero cash`() { fun `issue zero cash`() {
val expected = 0.DOLLARS val expected = 0.DOLLARS
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, OpaqueBytes.of(0x01), val ref = OpaqueBytes.of(0x01)
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected,
bankOfCorda, bankOfCorda,
notary)).resultFuture bankOfCorda,
ref,
notary,
anonymous = true)).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
assertFailsWith<IllegalArgumentException> { assertFailsWith<IllegalArgumentException> {
future.getOrThrow() future.getOrThrow()

View File

@ -37,9 +37,12 @@ class CashPaymentFlowTests {
notary = notaryNode.info.notaryIdentity notary = notaryNode.info.notaryIdentity
bankOfCorda = bankOfCordaNode.info.legalIdentity bankOfCorda = bankOfCordaNode.info.legalIdentity
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance,
bankOfCorda, bankOfCorda,
notary)).resultFuture bankOfCorda,
ref,
notary,
true)).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
future.getOrThrow() future.getOrThrow()
} }

View File

@ -1,167 +0,0 @@
package net.corda.flows
import net.corda.contracts.asset.Cash
import net.corda.core.concurrent.CordaFuture
import net.corda.core.contracts.Amount
import net.corda.core.flows.FlowException
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.transactions.SignedTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS
import net.corda.flows.IssuerFlow.IssuanceRequester
import net.corda.testing.contracts.calculateRandomlySizedAmounts
import net.corda.testing.expect
import net.corda.testing.expectEvents
import net.corda.testing.node.MockNetwork
import net.corda.testing.node.MockNetwork.MockNode
import net.corda.testing.sequence
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.util.*
import kotlin.test.assertFailsWith
@RunWith(Parameterized::class)
class IssuerFlowTest(val anonymous: Boolean) {
companion object {
@Parameterized.Parameters
@JvmStatic
fun data(): Collection<Array<Boolean>> {
return listOf(arrayOf(false), arrayOf(true))
}
}
lateinit var mockNet: MockNetwork
lateinit var notaryNode: MockNode
lateinit var bankOfCordaNode: MockNode
lateinit var bankClientNode: MockNode
@Before
fun start() {
mockNet = MockNetwork(threadPerNode = true)
val basketOfNodes = mockNet.createSomeNodes(2)
bankOfCordaNode = basketOfNodes.partyNodes[0]
bankClientNode = basketOfNodes.partyNodes[1]
notaryNode = basketOfNodes.notaryNode
}
@After
fun cleanUp() {
mockNet.stopNodes()
}
@Test
fun `test issuer flow`() {
val notary = notaryNode.services.myInfo.notaryIdentity
val (vaultUpdatesBoc, vaultUpdatesBankClient) = bankOfCordaNode.database.transaction {
// Register for vault updates
val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy<Cash.State>(criteria)
val (_, vaultUpdatesBankClient) = bankClientNode.services.vaultQueryService.trackBy<Cash.State>(criteria)
// using default IssueTo Party Reference
val issuerResult = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS,
bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary)
issuerResult.get()
Pair(vaultUpdatesBoc, vaultUpdatesBankClient)
}
// Check Bank of Corda Vault Updates
vaultUpdatesBoc.expectEvents {
sequence(
// ISSUE
expect { update ->
require(update.consumed.isEmpty()) { "Expected 0 consumed states, actual: $update" }
require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" }
val issued = update.produced.single().state.data
require(issued.owner.owningKey in bankOfCordaNode.services.keyManagementService.keys)
},
// MOVE
expect { update ->
require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" }
require(update.produced.isEmpty()) { "Expected 0 produced states, actual: $update" }
}
)
}
// Check Bank Client Vault Updates
vaultUpdatesBankClient.expectEvents {
// MOVE
expect { (consumed, produced) ->
require(consumed.isEmpty()) { consumed.size }
require(produced.size == 1) { produced.size }
val paidState = produced.single().state.data
require(paidState.owner.owningKey in bankClientNode.services.keyManagementService.keys)
}
}
}
@Test
fun `test issuer flow rejects restricted`() {
val notary = notaryNode.services.myInfo.notaryIdentity
// try to issue an amount of a restricted currency
assertFailsWith<FlowException> {
runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(100000L, Currency.getInstance("BRL")),
bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary).getOrThrow()
}
}
@Test
fun `test issue flow to self`() {
val notary = notaryNode.services.myInfo.notaryIdentity
val vaultUpdatesBoc = bankOfCordaNode.database.transaction {
val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy<Cash.State>(criteria)
// using default IssueTo Party Reference
runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS,
bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123), notary).getOrThrow()
vaultUpdatesBoc
}
// Check Bank of Corda Vault Updates
vaultUpdatesBoc.expectEvents {
sequence(
// ISSUE
expect { update ->
require(update.consumed.isEmpty()) { "Expected 0 consumed states, actual: $update" }
require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" }
}
)
}
}
@Test
fun `test concurrent issuer flow`() {
val notary = notaryNode.services.myInfo.notaryIdentity
// this test exercises the Cashflow issue and move subflows to ensure consistent spending of issued states
val amount = 10000.DOLLARS
val amounts = calculateRandomlySizedAmounts(10000.DOLLARS, 10, 10, Random())
val handles = amounts.map { pennies ->
runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(pennies, amount.token),
bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary)
}
handles.forEach {
require(it.get().stx is SignedTransaction)
}
}
private fun runIssuerAndIssueRequester(issuerNode: MockNode,
issueToNode: MockNode,
amount: Amount<Currency>,
issueToParty: Party,
ref: OpaqueBytes,
notaryParty: Party): CordaFuture<AbstractCashFlow.Result> {
val issueToPartyAndRef = issueToParty.ref(ref)
val issueRequest = IssuanceRequester(amount, issueToParty, issueToPartyAndRef.reference, issuerNode.info.legalIdentity, notaryParty,
anonymous)
return issueToNode.services.startFlow(issueRequest).resultFuture
}
}

View File

@ -29,7 +29,6 @@ import net.corda.core.utilities.toNonEmptySet
import net.corda.flows.CashExitFlow import net.corda.flows.CashExitFlow
import net.corda.flows.CashIssueFlow import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow import net.corda.flows.CashPaymentFlow
import net.corda.flows.IssuerFlow
import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.ContractUpgradeHandler
import net.corda.node.services.NotaryChangeHandler import net.corda.node.services.NotaryChangeHandler
import net.corda.node.services.NotifyTransactionHandler import net.corda.node.services.NotifyTransactionHandler
@ -210,9 +209,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
findRPCFlows(scanResult) findRPCFlows(scanResult)
} }
// TODO Remove this once the cash stuff is in its own CorDapp
registerInitiatedFlow(IssuerFlow.Issuer::class.java)
runOnStop += network::stop runOnStop += network::stop
_networkMapRegistrationFuture.captureLater(registerWithNetworkMapIfConfigured()) _networkMapRegistrationFuture.captureLater(registerWithNetworkMapIfConfigured())
smm.start() smm.start()

View File

@ -95,7 +95,7 @@ class CordaRPCOpsImplTest {
// Tell the monitoring service node to issue some cash // Tell the monitoring service node to issue some cash
val anonymous = false val anonymous = false
val recipient = aliceNode.info.legalIdentity val recipient = aliceNode.info.legalIdentity
val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, recipient, notaryNode.info.notaryIdentity, anonymous) val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), recipient, rpc.nodeIdentity().legalIdentity, ref, notaryNode.info.notaryIdentity, anonymous)
mockNet.runNetwork() mockNet.runNetwork()
var issueSmId: StateMachineRunId? = null var issueSmId: StateMachineRunId? = null
@ -133,8 +133,9 @@ class CordaRPCOpsImplTest {
val anonymous = false val anonymous = false
val result = rpc.startFlow(::CashIssueFlow, val result = rpc.startFlow(::CashIssueFlow,
100.DOLLARS, 100.DOLLARS,
OpaqueBytes(ByteArray(1, { 1 })),
aliceNode.info.legalIdentity, aliceNode.info.legalIdentity,
rpc.nodeIdentity().legalIdentity,
OpaqueBytes(ByteArray(1, { 1 })),
notaryNode.info.notaryIdentity, notaryNode.info.notaryIdentity,
false false
) )
@ -213,8 +214,9 @@ class CordaRPCOpsImplTest {
assertThatExceptionOfType(PermissionException::class.java).isThrownBy { assertThatExceptionOfType(PermissionException::class.java).isThrownBy {
rpc.startFlow(::CashIssueFlow, rpc.startFlow(::CashIssueFlow,
Amount(100, USD), Amount(100, USD),
OpaqueBytes(ByteArray(1, { 1 })),
aliceNode.info.legalIdentity, aliceNode.info.legalIdentity,
rpc.nodeIdentity().legalIdentity,
OpaqueBytes(ByteArray(1, { 1 })),
notaryNode.info.notaryIdentity, notaryNode.info.notaryIdentity,
false false
) )

View File

@ -330,8 +330,9 @@ class FlowFrameworkTests {
assertEquals(notary1.info.notaryIdentity, notary2.info.notaryIdentity) assertEquals(notary1.info.notaryIdentity, notary2.info.notaryIdentity)
node1.services.startFlow(CashIssueFlow( node1.services.startFlow(CashIssueFlow(
2000.DOLLARS, 2000.DOLLARS,
OpaqueBytes.of(0x01),
node1.info.legalIdentity, node1.info.legalIdentity,
node1.info.legalIdentity,
OpaqueBytes.of(0x01),
notary1.info.notaryIdentity, notary1.info.notaryIdentity,
anonymous = false)) anonymous = false))
// We pay a couple of times, the notary picking should go round robin // We pay a couple of times, the notary picking should go round robin

View File

@ -68,7 +68,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
'password' : "test", 'password' : "test",
'permissions': ["StartFlow.net.corda.flows.CashPaymentFlow", 'permissions': ["StartFlow.net.corda.flows.CashPaymentFlow",
"StartFlow.net.corda.flows.CashExitFlow", "StartFlow.net.corda.flows.CashExitFlow",
"StartFlow.net.corda.flows.IssuerFlow\$IssuanceRequester"]] "StartFlow.net.corda.flows.CashIssueFlow"]]
] ]
} }
node { node {

View File

@ -1,14 +1,14 @@
package net.corda.bank package net.corda.bank
import net.corda.contracts.asset.Cash import net.corda.contracts.asset.Cash
import net.corda.finance.DOLLARS import net.corda.core.internal.concurrent.transpose
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.internal.concurrent.transpose
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.finance.DOLLARS
import net.corda.flows.CashIssueFlow
import net.corda.node.services.startFlowPermission import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.nodeapi.User import net.corda.nodeapi.User
@ -20,7 +20,7 @@ class BankOfCordaRPCClientTest {
@Test @Test
fun `issuer flow via RPC`() { fun `issuer flow via RPC`() {
driver(dsl = { driver(dsl = {
val bocManager = User("bocManager", "password1", permissions = setOf(startFlowPermission<IssuanceRequester>())) val bocManager = User("bocManager", "password1", permissions = setOf(startFlowPermission<CashIssueFlow>()))
val bigCorpCFO = User("bigCorpCFO", "password2", permissions = emptySet()) val bigCorpCFO = User("bigCorpCFO", "password2", permissions = emptySet())
val (nodeBankOfCorda, nodeBigCorporation) = listOf( val (nodeBankOfCorda, nodeBigCorporation) = listOf(
startNode(BOC.name, setOf(ServiceInfo(SimpleNotaryService.type)), listOf(bocManager)), startNode(BOC.name, setOf(ServiceInfo(SimpleNotaryService.type)), listOf(bocManager)),
@ -45,11 +45,11 @@ class BankOfCordaRPCClientTest {
// Kick-off actual Issuer Flow // Kick-off actual Issuer Flow
val anonymous = true val anonymous = true
bocProxy.startFlow( bocProxy.startFlow(
::IssuanceRequester, ::CashIssueFlow,
1000.DOLLARS, 1000.DOLLARS,
nodeBigCorporation.nodeInfo.legalIdentity, nodeBigCorporation.nodeInfo.legalIdentity,
BIG_CORP_PARTY_REF,
nodeBankOfCorda.nodeInfo.legalIdentity, nodeBankOfCorda.nodeInfo.legalIdentity,
BIG_CORP_PARTY_REF,
nodeBankOfCorda.nodeInfo.notaryIdentity, nodeBankOfCorda.nodeInfo.notaryIdentity,
anonymous).returnValue.getOrThrow() anonymous).returnValue.getOrThrow()

View File

@ -8,8 +8,8 @@ import net.corda.core.node.services.ServiceType
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.flows.CashExitFlow import net.corda.flows.CashExitFlow
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow import net.corda.flows.CashPaymentFlow
import net.corda.flows.IssuerFlow
import net.corda.node.services.startFlowPermission import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.nodeapi.User import net.corda.nodeapi.User
@ -68,7 +68,7 @@ private class BankOfCordaDriver {
"test", "test",
permissions = setOf( permissions = setOf(
startFlowPermission<CashPaymentFlow>(), startFlowPermission<CashPaymentFlow>(),
startFlowPermission<IssuerFlow.IssuanceRequester>(), startFlowPermission<CashIssueFlow>(),
startFlowPermission<CashExitFlow>())) startFlowPermission<CashExitFlow>()))
val bigCorpUser = User(BIGCORP_USERNAME, "test", permissions = setOf(startFlowPermission<CashPaymentFlow>())) val bigCorpUser = User(BIGCORP_USERNAME, "test", permissions = setOf(startFlowPermission<CashPaymentFlow>()))
startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type)))

View File

@ -8,7 +8,7 @@ import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.flows.CashIssueFlow
import net.corda.testing.http.HttpApi import net.corda.testing.http.HttpApi
import java.util.* import java.util.*
@ -46,9 +46,9 @@ class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) {
?: throw IllegalStateException("Unable to locate notary node in network map cache") ?: throw IllegalStateException("Unable to locate notary node in network map cache")
val amount = Amount(params.amount, Currency.getInstance(params.currency)) val amount = Amount(params.amount, Currency.getInstance(params.currency))
val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) val issuerBankPartyRef = OpaqueBytes.of(params.issuerBankPartyRef.toByte())
return rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, notaryNode.notaryIdentity, params.anonymous) return rpc.startFlow(::CashIssueFlow, amount, issueToParty, issuerBankParty, issuerBankPartyRef, notaryNode.notaryIdentity, params.anonymous)
.returnValue.getOrThrow().stx .returnValue.getOrThrow().stx
} }
} }

View File

@ -6,7 +6,7 @@ import net.corda.core.messaging.startFlow
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.flows.CashIssueFlow
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.* import java.util.*
@ -18,7 +18,7 @@ import javax.ws.rs.core.Response
@Path("bank") @Path("bank")
class BankOfCordaWebApi(val rpc: CordaRPCOps) { class BankOfCordaWebApi(val rpc: CordaRPCOps) {
data class IssueRequestParams(val amount: Long, val currency: String, data class IssueRequestParams(val amount: Long, val currency: String,
val issueToPartyName: X500Name, val issueToPartyRefAsString: String, val issueToPartyName: X500Name, val issuerBankPartyRef: String,
val issuerBankName: X500Name, val issuerBankName: X500Name,
val notaryName: X500Name, val notaryName: X500Name,
val anonymous: Boolean) val anonymous: Boolean)
@ -52,13 +52,13 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) {
?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate $notaryParty in network map service").build() ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate $notaryParty in network map service").build()
val amount = Amount(params.amount, Currency.getInstance(params.currency)) val amount = Amount(params.amount, Currency.getInstance(params.currency))
val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) val issuerBankPartyRef = OpaqueBytes.of(params.issuerBankPartyRef.toByte())
val anonymous = params.anonymous val anonymous = params.anonymous
// invoke client side of Issuer Flow: IssuanceRequester // invoke client side of Issuer Flow: IssuanceRequester
// The line below blocks and waits for the future to resolve. // The line below blocks and waits for the future to resolve.
return try { return try {
rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow() rpc.startFlow(::CashIssueFlow, amount, issueToParty, issuerBankParty, issuerBankPartyRef, notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow()
logger.info("Issue request completed successfully: $params") logger.info("Issue request completed successfully: $params")
Response.status(Response.Status.CREATED).build() Response.status(Response.Status.CREATED).build()
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -54,7 +54,8 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) {
val anonymous = false val anonymous = false
// issue random amounts of currency up to the requested amount, in parallel // issue random amounts of currency up to the requested amount, in parallel
val resultFutures = amounts.map { pennies -> val resultFutures = amounts.map { pennies ->
rpc.startFlow(::CashIssueFlow, amount.copy(quantity = pennies), OpaqueBytes.of(1), buyer, notaryNode.notaryIdentity, anonymous).returnValue rpc.startFlow(::CashIssueFlow, amount.copy(quantity = pennies), buyer, rpc.nodeIdentity().legalIdentity,
OpaqueBytes.of(1), notaryNode.notaryIdentity, anonymous).returnValue
} }
resultFutures.transpose().getOrThrow() resultFutures.transpose().getOrThrow()

View File

@ -2,15 +2,16 @@ package net.corda.traderdemo
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.testing.DUMMY_BANK_A import net.corda.flows.CashIssueFlow
import net.corda.testing.DUMMY_BANK_B
import net.corda.testing.DUMMY_NOTARY
import net.corda.flows.IssuerFlow
import net.corda.node.services.startFlowPermission import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.nodeapi.User import net.corda.nodeapi.User
import net.corda.testing.BOC import net.corda.testing.BOC
import net.corda.testing.DUMMY_BANK_A
import net.corda.testing.DUMMY_BANK_B
import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.driver.driver import net.corda.testing.driver.driver
import net.corda.traderdemo.flow.CommercialPaperIssueFlow
import net.corda.traderdemo.flow.SellerFlow import net.corda.traderdemo.flow.SellerFlow
/** /**
@ -19,11 +20,13 @@ import net.corda.traderdemo.flow.SellerFlow
*/ */
fun main(args: Array<String>) { fun main(args: Array<String>) {
val permissions = setOf( val permissions = setOf(
startFlowPermission<IssuerFlow.IssuanceRequester>(), startFlowPermission<CashIssueFlow>(),
startFlowPermission<SellerFlow>()) startFlowPermission<SellerFlow>())
val demoUser = listOf(User("demo", "demo", permissions)) val demoUser = listOf(User("demo", "demo", permissions))
driver(driverDirectory = "build" / "trader-demo-nodes", isDebug = true) { driver(driverDirectory = "build" / "trader-demo-nodes", isDebug = true) {
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>())) val user = User("user1", "test", permissions = setOf(startFlowPermission<CashIssueFlow>(),
startFlowPermission<CommercialPaperIssueFlow>(),
startFlowPermission<SellerFlow>()))
startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type)))
startNode(DUMMY_BANK_A.name, rpcUsers = demoUser) startNode(DUMMY_BANK_A.name, rpcUsers = demoUser)
startNode(DUMMY_BANK_B.name, rpcUsers = demoUser) startNode(DUMMY_BANK_B.name, rpcUsers = demoUser)

View File

@ -26,8 +26,8 @@ import net.corda.core.messaging.FlowHandle
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.explorer.formatters.PartyNameFormatter import net.corda.explorer.formatters.PartyNameFormatter
import net.corda.explorer.model.CashTransaction import net.corda.explorer.model.CashTransaction
import net.corda.explorer.model.IssuerModel import net.corda.explorer.model.IssuerModel