mirror of
https://github.com/corda/corda.git
synced 2024-12-20 05:28:21 +00:00
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:
parent
3888635055
commit
89476904fc
@ -111,8 +111,9 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
val anonymous = false
|
||||
rpc.startFlow(::CashIssueFlow,
|
||||
Amount(100, USD),
|
||||
OpaqueBytes(ByteArray(1, { 1 })),
|
||||
aliceNode.legalIdentity,
|
||||
rpc.nodeIdentity().legalIdentity,
|
||||
OpaqueBytes(ByteArray(1, { 1 })),
|
||||
notaryNode.notaryIdentity,
|
||||
anonymous
|
||||
)
|
||||
@ -136,7 +137,8 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
@Test
|
||||
fun `cash issue and move`() {
|
||||
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()
|
||||
|
||||
var issueSmId: StateMachineRunId? = null
|
||||
|
@ -77,9 +77,10 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
login(rpcUser.username, rpcUser.password)
|
||||
println("Creating proxy")
|
||||
println("Starting flow")
|
||||
val flowHandle = connection!!.proxy.startTrackedFlow(
|
||||
::CashIssueFlow,
|
||||
20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity, node.info.legalIdentity)
|
||||
val flowHandle = connection!!.proxy.startTrackedFlow(::CashIssueFlow,
|
||||
20.DOLLARS, node.info.legalIdentity,
|
||||
node.info.legalIdentity, OpaqueBytes.of(0), node.info.legalIdentity, true
|
||||
)
|
||||
println("Started flow, waiting on result")
|
||||
flowHandle.progress.subscribe {
|
||||
println("PROGRESS $it")
|
||||
@ -113,8 +114,8 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
assertTrue(startCash.isEmpty(), "Should not start with any cash")
|
||||
|
||||
val flowHandle = proxy.startFlow(::CashIssueFlow,
|
||||
123.DOLLARS, OpaqueBytes.of(0),
|
||||
node.info.legalIdentity, node.info.legalIdentity
|
||||
123.DOLLARS, node.info.legalIdentity,
|
||||
node.info.legalIdentity, OpaqueBytes.of(0), node.info.legalIdentity, true
|
||||
)
|
||||
println("Started issuing cash, waiting on result")
|
||||
flowHandle.returnValue.get()
|
||||
@ -140,10 +141,11 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
}
|
||||
}
|
||||
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,
|
||||
123.DOLLARS, OpaqueBytes.of(0),
|
||||
nodeIdentity, nodeIdentity
|
||||
123.DOLLARS, nodeIdentity,
|
||||
nodeIdentity, OpaqueBytes.of(0), nodeIdentity,
|
||||
true
|
||||
).returnValue.getOrThrow()
|
||||
proxy.startFlowDynamic(CashIssueFlow::class.java,
|
||||
1000.DOLLARS, OpaqueBytes.of(0),
|
||||
|
@ -421,6 +421,29 @@ inline fun <T : Any, A, B, C, D, reified R : FlowLogic<T>> CordaRPCOps.startTrac
|
||||
arg3: D
|
||||
): 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.
|
||||
*/
|
||||
|
@ -176,7 +176,7 @@ class ContractUpgradeFlowTest {
|
||||
fun `upgrade Cash to v2`() {
|
||||
// Create some cash.
|
||||
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()
|
||||
val stx = result.getOrThrow().stx
|
||||
val stateAndRef = stx.tx.outRef<Cash.State>(0)
|
||||
|
@ -42,6 +42,9 @@ UNRELEASED
|
||||
|
||||
* 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
|
||||
------------
|
||||
|
||||
|
@ -67,9 +67,11 @@ class IntegrationTestingTutorial {
|
||||
thread {
|
||||
futures.push(aliceProxy.startFlow(::CashIssueFlow,
|
||||
i.DOLLARS,
|
||||
issueRef,
|
||||
bob.nodeInfo.legalIdentity,
|
||||
notary.nodeInfo.notaryIdentity
|
||||
alice.nodeInfo.legalIdentity,
|
||||
issueRef,
|
||||
notary.nodeInfo.notaryIdentity,
|
||||
true
|
||||
).returnValue)
|
||||
}
|
||||
}.forEach(Thread::join) // Ensure the stack of futures is populated.
|
||||
|
@ -128,7 +128,7 @@ fun generateTransactions(proxy: CordaRPCOps) {
|
||||
proxy.startFlow(::CashPaymentFlow, Amount(quantity, USD), me)
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -132,8 +132,9 @@ object TopupIssuerFlow {
|
||||
val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity
|
||||
// invoke Cash subflow to issue Asset
|
||||
progressTracker.currentStep = ISSUING
|
||||
val issuer = 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)
|
||||
// NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger)
|
||||
// short-circuit when issuing to self
|
||||
|
@ -65,10 +65,11 @@ class CustomVaultQueryTest {
|
||||
private fun issueCashForCurrency(amountToIssue: Amount<Currency>) {
|
||||
// Use NodeA as issuer and create some dollars
|
||||
val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(amountToIssue,
|
||||
OpaqueBytes.of(0x01),
|
||||
nodeA.info.legalIdentity,
|
||||
nodeA.info.legalIdentity,
|
||||
OpaqueBytes.of(0x01),
|
||||
notaryNode.info.notaryIdentity,
|
||||
false))
|
||||
anonymous = false))
|
||||
// Wait for the flow to stop and print
|
||||
flowHandle1.resultFuture.getOrThrow()
|
||||
}
|
||||
|
@ -45,8 +45,9 @@ class FxTransactionBuildTutorialTest {
|
||||
fun `Run ForeignExchangeFlow to completion`() {
|
||||
// Use NodeA as issuer and create some dollars
|
||||
val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(DOLLARS(1000),
|
||||
OpaqueBytes.of(0x01),
|
||||
nodeA.info.legalIdentity,
|
||||
nodeA.info.legalIdentity,
|
||||
OpaqueBytes.of(0x01),
|
||||
notaryNode.info.notaryIdentity,
|
||||
false))
|
||||
// Wait for the flow to stop and print
|
||||
@ -55,8 +56,9 @@ class FxTransactionBuildTutorialTest {
|
||||
|
||||
// Using NodeB as Issuer create some pounds.
|
||||
val flowHandle2 = nodeB.services.startFlow(CashIssueFlow(POUNDS(1000),
|
||||
OpaqueBytes.of(0x01),
|
||||
nodeB.info.legalIdentity,
|
||||
nodeB.info.legalIdentity,
|
||||
OpaqueBytes.of(0x01),
|
||||
notaryNode.info.notaryIdentity,
|
||||
false))
|
||||
// Wait for flow to come to an end and print
|
||||
|
@ -8,6 +8,8 @@ Unreleased
|
||||
|
||||
* 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
|
||||
------------
|
||||
|
||||
|
@ -22,7 +22,7 @@ sealed class CashFlowCommand {
|
||||
val recipient: Party,
|
||||
val notary: Party,
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -18,39 +18,43 @@ import java.util.*
|
||||
* Initiates a flow that produces cash issuance transaction.
|
||||
*
|
||||
* @param amount the amount of currency to issue.
|
||||
* @param issueRef a reference to put on the issued currency.
|
||||
* @param recipient the party who should own the currency after it is issued.
|
||||
* @param issuerBankPartyRef a reference to put on the issued currency.
|
||||
* @param issueTo the party who should own the currency after it is issued.
|
||||
* @param notary the notary to set on the output states.
|
||||
*/
|
||||
@StartableByRPC
|
||||
class CashIssueFlow(val amount: Amount<Currency>,
|
||||
val issueRef: OpaqueBytes,
|
||||
val recipient: Party,
|
||||
val issueTo: Party,
|
||||
val issuerBankParty
|
||||
: Party,
|
||||
val issuerBankPartyRef: OpaqueBytes,
|
||||
val notary: Party,
|
||||
val anonymous: Boolean,
|
||||
progressTracker: ProgressTracker) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
|
||||
constructor(amount: Amount<Currency>,
|
||||
issueRef: OpaqueBytes,
|
||||
recipient: Party,
|
||||
notary: Party) : this(amount, issueRef, recipient, notary, true, tracker())
|
||||
issuerBankPartyRef: OpaqueBytes,
|
||||
issuerBankParty: Party,
|
||||
issueTo: Party,
|
||||
notary: Party) : this(amount, issueTo, issuerBankParty, issuerBankPartyRef, notary, true, tracker())
|
||||
constructor(amount: Amount<Currency>,
|
||||
issueRef: OpaqueBytes,
|
||||
recipient: Party,
|
||||
issueTo: Party,
|
||||
issuerBankParty: Party,
|
||||
issuerBankPartyRef: OpaqueBytes,
|
||||
notary: Party,
|
||||
anonymous: Boolean) : this(amount, issueRef, recipient, notary, anonymous, tracker())
|
||||
anonymous: Boolean) : this(amount, issueTo, issuerBankParty, issuerBankPartyRef, notary, anonymous, tracker())
|
||||
|
||||
@Suspendable
|
||||
override fun call(): AbstractCashFlow.Result {
|
||||
progressTracker.currentStep = GENERATING_ID
|
||||
val txIdentities = if (anonymous) {
|
||||
subFlow(TransactionKeyFlow(recipient))
|
||||
subFlow(TransactionKeyFlow(issueTo))
|
||||
} else {
|
||||
emptyMap<Party, AnonymousParty>()
|
||||
}
|
||||
val anonymousRecipient = txIdentities[recipient] ?: recipient
|
||||
val anonymousRecipient = txIdentities[issueTo] ?: issueTo
|
||||
progressTracker.currentStep = GENERATING_TX
|
||||
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)
|
||||
progressTracker.currentStep = SIGNING_TX
|
||||
val tx = serviceHub.signInitialTransaction(builder, signers)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -33,9 +33,11 @@ class CashExitFlowTests {
|
||||
bankOfCorda = bankOfCordaNode.info.legalIdentity
|
||||
|
||||
mockNet.runNetwork()
|
||||
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref,
|
||||
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance,
|
||||
bankOfCorda,
|
||||
notary)).resultFuture
|
||||
bankOfCorda, ref,
|
||||
notary,
|
||||
anonymous = true)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
future.getOrThrow()
|
||||
}
|
||||
|
@ -42,9 +42,12 @@ class CashIssueFlowTests {
|
||||
fun `issue some cash`() {
|
||||
val expected = 500.DOLLARS
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref,
|
||||
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected,
|
||||
bankOfCorda,
|
||||
notary)).resultFuture
|
||||
bankOfCorda,
|
||||
ref,
|
||||
notary,
|
||||
anonymous = true)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
val issueTx = future.getOrThrow().stx
|
||||
val output = issueTx.tx.outputsOfType<Cash.State>().single()
|
||||
@ -54,9 +57,13 @@ class CashIssueFlowTests {
|
||||
@Test
|
||||
fun `issue zero cash`() {
|
||||
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,
|
||||
notary)).resultFuture
|
||||
bankOfCorda,
|
||||
ref,
|
||||
notary,
|
||||
anonymous = true)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
future.getOrThrow()
|
||||
|
@ -37,9 +37,12 @@ class CashPaymentFlowTests {
|
||||
notary = notaryNode.info.notaryIdentity
|
||||
bankOfCorda = bankOfCordaNode.info.legalIdentity
|
||||
|
||||
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref,
|
||||
val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance,
|
||||
bankOfCorda,
|
||||
notary)).resultFuture
|
||||
bankOfCorda,
|
||||
ref,
|
||||
notary,
|
||||
true)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
future.getOrThrow()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -29,7 +29,6 @@ import net.corda.core.utilities.toNonEmptySet
|
||||
import net.corda.flows.CashExitFlow
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.flows.IssuerFlow
|
||||
import net.corda.node.services.ContractUpgradeHandler
|
||||
import net.corda.node.services.NotaryChangeHandler
|
||||
import net.corda.node.services.NotifyTransactionHandler
|
||||
@ -210,9 +209,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
findRPCFlows(scanResult)
|
||||
}
|
||||
|
||||
// TODO Remove this once the cash stuff is in its own CorDapp
|
||||
registerInitiatedFlow(IssuerFlow.Issuer::class.java)
|
||||
|
||||
runOnStop += network::stop
|
||||
_networkMapRegistrationFuture.captureLater(registerWithNetworkMapIfConfigured())
|
||||
smm.start()
|
||||
|
@ -95,7 +95,7 @@ class CordaRPCOpsImplTest {
|
||||
// Tell the monitoring service node to issue some cash
|
||||
val anonymous = false
|
||||
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()
|
||||
|
||||
var issueSmId: StateMachineRunId? = null
|
||||
@ -133,8 +133,9 @@ class CordaRPCOpsImplTest {
|
||||
val anonymous = false
|
||||
val result = rpc.startFlow(::CashIssueFlow,
|
||||
100.DOLLARS,
|
||||
OpaqueBytes(ByteArray(1, { 1 })),
|
||||
aliceNode.info.legalIdentity,
|
||||
rpc.nodeIdentity().legalIdentity,
|
||||
OpaqueBytes(ByteArray(1, { 1 })),
|
||||
notaryNode.info.notaryIdentity,
|
||||
false
|
||||
)
|
||||
@ -213,8 +214,9 @@ class CordaRPCOpsImplTest {
|
||||
assertThatExceptionOfType(PermissionException::class.java).isThrownBy {
|
||||
rpc.startFlow(::CashIssueFlow,
|
||||
Amount(100, USD),
|
||||
OpaqueBytes(ByteArray(1, { 1 })),
|
||||
aliceNode.info.legalIdentity,
|
||||
rpc.nodeIdentity().legalIdentity,
|
||||
OpaqueBytes(ByteArray(1, { 1 })),
|
||||
notaryNode.info.notaryIdentity,
|
||||
false
|
||||
)
|
||||
|
@ -330,8 +330,9 @@ class FlowFrameworkTests {
|
||||
assertEquals(notary1.info.notaryIdentity, notary2.info.notaryIdentity)
|
||||
node1.services.startFlow(CashIssueFlow(
|
||||
2000.DOLLARS,
|
||||
OpaqueBytes.of(0x01),
|
||||
node1.info.legalIdentity,
|
||||
node1.info.legalIdentity,
|
||||
OpaqueBytes.of(0x01),
|
||||
notary1.info.notaryIdentity,
|
||||
anonymous = false))
|
||||
// We pay a couple of times, the notary picking should go round robin
|
||||
|
@ -68,7 +68,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
'password' : "test",
|
||||
'permissions': ["StartFlow.net.corda.flows.CashPaymentFlow",
|
||||
"StartFlow.net.corda.flows.CashExitFlow",
|
||||
"StartFlow.net.corda.flows.IssuerFlow\$IssuanceRequester"]]
|
||||
"StartFlow.net.corda.flows.CashIssueFlow"]]
|
||||
]
|
||||
}
|
||||
node {
|
||||
|
@ -1,14 +1,14 @@
|
||||
package net.corda.bank
|
||||
|
||||
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.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
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.transactions.SimpleNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
@ -20,7 +20,7 @@ class BankOfCordaRPCClientTest {
|
||||
@Test
|
||||
fun `issuer flow via RPC`() {
|
||||
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 (nodeBankOfCorda, nodeBigCorporation) = listOf(
|
||||
startNode(BOC.name, setOf(ServiceInfo(SimpleNotaryService.type)), listOf(bocManager)),
|
||||
@ -45,11 +45,11 @@ class BankOfCordaRPCClientTest {
|
||||
// Kick-off actual Issuer Flow
|
||||
val anonymous = true
|
||||
bocProxy.startFlow(
|
||||
::IssuanceRequester,
|
||||
::CashIssueFlow,
|
||||
1000.DOLLARS,
|
||||
nodeBigCorporation.nodeInfo.legalIdentity,
|
||||
BIG_CORP_PARTY_REF,
|
||||
nodeBankOfCorda.nodeInfo.legalIdentity,
|
||||
BIG_CORP_PARTY_REF,
|
||||
nodeBankOfCorda.nodeInfo.notaryIdentity,
|
||||
anonymous).returnValue.getOrThrow()
|
||||
|
||||
|
@ -8,8 +8,8 @@ import net.corda.core.node.services.ServiceType
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.flows.CashExitFlow
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.flows.IssuerFlow
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
@ -68,7 +68,7 @@ private class BankOfCordaDriver {
|
||||
"test",
|
||||
permissions = setOf(
|
||||
startFlowPermission<CashPaymentFlow>(),
|
||||
startFlowPermission<IssuerFlow.IssuanceRequester>(),
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashExitFlow>()))
|
||||
val bigCorpUser = User(BIGCORP_USERNAME, "test", permissions = setOf(startFlowPermission<CashPaymentFlow>()))
|
||||
startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
|
@ -8,7 +8,7 @@ import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.testing.http.HttpApi
|
||||
import java.util.*
|
||||
|
||||
@ -46,9 +46,9 @@ class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) {
|
||||
?: throw IllegalStateException("Unable to locate notary node in network map cache")
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
@ -18,7 +18,7 @@ import javax.ws.rs.core.Response
|
||||
@Path("bank")
|
||||
class BankOfCordaWebApi(val rpc: CordaRPCOps) {
|
||||
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 notaryName: X500Name,
|
||||
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()
|
||||
|
||||
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
|
||||
|
||||
// invoke client side of Issuer Flow: IssuanceRequester
|
||||
// The line below blocks and waits for the future to resolve.
|
||||
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")
|
||||
Response.status(Response.Status.CREATED).build()
|
||||
} catch (e: Exception) {
|
||||
|
@ -54,7 +54,8 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) {
|
||||
val anonymous = false
|
||||
// issue random amounts of currency up to the requested amount, in parallel
|
||||
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()
|
||||
|
@ -2,15 +2,16 @@ package net.corda.traderdemo
|
||||
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.testing.DUMMY_BANK_A
|
||||
import net.corda.testing.DUMMY_BANK_B
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.flows.IssuerFlow
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.nodeapi.User
|
||||
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.traderdemo.flow.CommercialPaperIssueFlow
|
||||
import net.corda.traderdemo.flow.SellerFlow
|
||||
|
||||
/**
|
||||
@ -19,11 +20,13 @@ import net.corda.traderdemo.flow.SellerFlow
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
val permissions = setOf(
|
||||
startFlowPermission<IssuerFlow.IssuanceRequester>(),
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<SellerFlow>())
|
||||
val demoUser = listOf(User("demo", "demo", permissions))
|
||||
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_BANK_A.name, rpcUsers = demoUser)
|
||||
startNode(DUMMY_BANK_B.name, rpcUsers = demoUser)
|
||||
|
@ -26,8 +26,8 @@ import net.corda.core.messaging.FlowHandle
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.explorer.formatters.PartyNameFormatter
|
||||
import net.corda.explorer.model.CashTransaction
|
||||
import net.corda.explorer.model.IssuerModel
|
||||
|
Loading…
Reference in New Issue
Block a user