mirror of
https://github.com/corda/corda.git
synced 2024-12-29 09:18:58 +00:00
Split CashFlow into three flows
Split CashFlow into independent CashIssueFlow, CashExitFlow and CashPaymentFlow, so that users can be given access to one but not the other(s). Signed-off-by: Ross Nicoll <ross.nicoll@r3.com>
This commit is contained in:
parent
521994ce23
commit
7dc6f47b3d
@ -8,8 +8,8 @@ import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.random63BitValue
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.config.configureTestSSL
|
||||
@ -23,7 +23,10 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class CordaRPCClientTest : NodeBasedTest() {
|
||||
private val rpcUser = User("user1", "test", permissions = setOf(startFlowPermission<CashFlow>()))
|
||||
private val rpcUser = User("user1", "test", permissions = setOf(
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>()
|
||||
))
|
||||
private lateinit var node: Node
|
||||
private lateinit var client: CordaRPCClient
|
||||
|
||||
@ -60,8 +63,8 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
val proxy = client.proxy()
|
||||
println("Starting flow")
|
||||
val flowHandle = proxy.startFlow(
|
||||
::CashFlow,
|
||||
CashCommand.IssueCash(20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity, node.info.legalIdentity))
|
||||
::CashIssueFlow,
|
||||
20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity, node.info.legalIdentity)
|
||||
println("Started flow, waiting on result")
|
||||
flowHandle.progress.subscribe {
|
||||
println("PROGRESS $it")
|
||||
@ -73,10 +76,10 @@ class CordaRPCClientTest : NodeBasedTest() {
|
||||
fun `FlowException thrown by flow`() {
|
||||
client.start(rpcUser.username, rpcUser.password)
|
||||
val proxy = client.proxy()
|
||||
val handle = proxy.startFlow(::CashFlow, CashCommand.PayCash(
|
||||
amount = 100.DOLLARS.issuedBy(node.info.legalIdentity.ref(1)),
|
||||
recipient = node.info.legalIdentity
|
||||
))
|
||||
val handle = proxy.startFlow(::CashPaymentFlow,
|
||||
100.DOLLARS.issuedBy(node.info.legalIdentity.ref(1)),
|
||||
node.info.legalIdentity
|
||||
)
|
||||
// TODO Restrict this to CashException once RPC serialisation has been fixed
|
||||
assertThatExceptionOfType(FlowException::class.java).isThrownBy {
|
||||
handle.returnValue.getOrThrow()
|
||||
|
@ -19,7 +19,6 @@ import net.corda.core.node.services.StateMachineTransactionMapping
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.node.driver.DriverBasedTest
|
||||
import net.corda.node.driver.driver
|
||||
@ -94,7 +93,7 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
|
||||
@Test
|
||||
fun `cash issue works end to end`() {
|
||||
rpc.startFlow(::CashFlow, CashCommand.IssueCash(
|
||||
rpc.startFlow(::CashFlow, CashFlow.Command.IssueCash(
|
||||
amount = Amount(100, USD),
|
||||
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||
recipient = aliceNode.legalIdentity,
|
||||
@ -119,14 +118,14 @@ class NodeMonitorModelTest : DriverBasedTest() {
|
||||
|
||||
@Test
|
||||
fun `cash issue and move`() {
|
||||
rpc.startFlow(::CashFlow, CashCommand.IssueCash(
|
||||
rpc.startFlow(::CashFlow, CashFlow.Command.IssueCash(
|
||||
amount = Amount(100, USD),
|
||||
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||
recipient = aliceNode.legalIdentity,
|
||||
notary = notaryNode.notaryIdentity
|
||||
)).returnValue.getOrThrow()
|
||||
|
||||
rpc.startFlow(::CashFlow, CashCommand.PayCash(
|
||||
rpc.startFlow(::CashFlow, CashFlow.Command.PayCash(
|
||||
amount = Amount(100, Issued(PartyAndReference(aliceNode.legalIdentity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
|
||||
recipient = aliceNode.legalIdentity
|
||||
))
|
||||
|
@ -5,7 +5,7 @@ import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@ -64,7 +64,7 @@ class EventGenerator(
|
||||
|
||||
val issueCashGenerator =
|
||||
amountGenerator.combine(partyGenerator, issueRefGenerator) { amount, to, issueRef ->
|
||||
CashCommand.IssueCash(
|
||||
CashFlow.Command.IssueCash(
|
||||
amount,
|
||||
issueRef,
|
||||
to,
|
||||
@ -76,7 +76,7 @@ class EventGenerator(
|
||||
amountIssuedGenerator.combine(
|
||||
partyGenerator
|
||||
) { amountIssued, recipient ->
|
||||
CashCommand.PayCash(
|
||||
CashFlow.Command.PayCash(
|
||||
amount = amountIssued,
|
||||
recipient = recipient
|
||||
)
|
||||
@ -84,7 +84,7 @@ class EventGenerator(
|
||||
|
||||
val exitCashGenerator =
|
||||
amountIssuedGenerator.map {
|
||||
CashCommand.ExitCash(
|
||||
CashFlow.Command.ExitCash(
|
||||
it.withoutIssuer(),
|
||||
it.token.issuer.reference
|
||||
)
|
||||
|
@ -9,7 +9,7 @@ import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.User
|
||||
@ -56,7 +56,7 @@ class IntegrationTestingTutorial {
|
||||
val issueRef = OpaqueBytes.of(0)
|
||||
for (i in 1 .. 10) {
|
||||
thread {
|
||||
aliceProxy.startFlow(::CashFlow, CashCommand.IssueCash(
|
||||
aliceProxy.startFlow(::CashFlow, CashFlow.Command.IssueCash(
|
||||
amount = i.DOLLARS,
|
||||
issueRef = issueRef,
|
||||
recipient = bob.nodeInfo.legalIdentity,
|
||||
@ -82,7 +82,7 @@ class IntegrationTestingTutorial {
|
||||
|
||||
// START 5
|
||||
for (i in 1 .. 10) {
|
||||
val flowHandle = bobProxy.startFlow(::CashFlow, CashCommand.PayCash(
|
||||
val flowHandle = bobProxy.startFlow(::CashFlow, CashFlow.Command.PayCash(
|
||||
amount = i.DOLLARS.issuedBy(alice.nodeInfo.legalIdentity.ref(issueRef)),
|
||||
recipient = alice.nodeInfo.legalIdentity
|
||||
))
|
||||
@ -102,4 +102,4 @@ class IntegrationTestingTutorial {
|
||||
}
|
||||
}
|
||||
}
|
||||
// END 5
|
||||
// END 5
|
||||
|
@ -12,8 +12,9 @@ import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.flows.CashExitFlow
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.startFlowPermission
|
||||
@ -41,7 +42,9 @@ fun main(args: Array<String>) {
|
||||
val printOrVisualise = PrintOrVisualise.valueOf(args[0])
|
||||
|
||||
val baseDirectory = Paths.get("build/rpc-api-tutorial")
|
||||
val user = User("user", "password", permissions = setOf(startFlowPermission<CashFlow>()))
|
||||
val user = User("user", "password", permissions = setOf(startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>(),
|
||||
startFlowPermission<CashExitFlow>()))
|
||||
|
||||
driver(driverDirectory = baseDirectory) {
|
||||
startNode("Notary", advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type)))
|
||||
@ -114,14 +117,14 @@ fun generateTransactions(proxy: CordaRPCOps) {
|
||||
val n = random.nextDouble()
|
||||
if (ownedQuantity > 10000 && n > 0.8) {
|
||||
val quantity = Math.abs(random.nextLong()) % 2000
|
||||
proxy.startFlow(::CashFlow, CashCommand.ExitCash(Amount(quantity, USD), issueRef))
|
||||
proxy.startFlow(::CashExitFlow, Amount(quantity, USD), issueRef)
|
||||
ownedQuantity -= quantity
|
||||
} else if (ownedQuantity > 1000 && n < 0.7) {
|
||||
val quantity = Math.abs(random.nextLong() % Math.min(ownedQuantity, 2000))
|
||||
proxy.startFlow(::CashFlow, CashCommand.PayCash(Amount(quantity, Issued(meAndRef, USD)), me))
|
||||
proxy.startFlow(::CashPaymentFlow, Amount(quantity, Issued(meAndRef, USD)), me)
|
||||
} else {
|
||||
val quantity = Math.abs(random.nextLong() % 1000)
|
||||
proxy.startFlow(::CashFlow, CashCommand.IssueCash(Amount(quantity, USD), issueRef, me, notary))
|
||||
proxy.startFlow(::CashIssueFlow, Amount(quantity, USD), issueRef, me, notary)
|
||||
ownedQuantity += quantity
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.corda.docs
|
||||
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
@ -8,9 +7,8 @@ import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.core.node.ServiceEntry
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
@ -51,19 +49,19 @@ class FxTransactionBuildTutorialTest {
|
||||
@Test
|
||||
fun `Run ForeignExchangeFlow to completion`() {
|
||||
// Use NodeA as issuer and create some dollars
|
||||
val flowHandle1 = nodeA.services.startFlow(CashFlow(CashCommand.IssueCash(DOLLARS(1000),
|
||||
val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(DOLLARS(1000),
|
||||
OpaqueBytes.of(0x01),
|
||||
nodeA.info.legalIdentity,
|
||||
notaryNode.info.notaryIdentity)))
|
||||
notaryNode.info.notaryIdentity))
|
||||
// Wait for the flow to stop and print
|
||||
flowHandle1.resultFuture.getOrThrow()
|
||||
printBalances()
|
||||
|
||||
// Using NodeB as Issuer create some pounds.
|
||||
val flowHandle2 = nodeB.services.startFlow(CashFlow(CashCommand.IssueCash(POUNDS(1000),
|
||||
val flowHandle2 = nodeB.services.startFlow(CashIssueFlow(POUNDS(1000),
|
||||
OpaqueBytes.of(0x01),
|
||||
nodeB.info.legalIdentity,
|
||||
notaryNode.info.notaryIdentity)))
|
||||
notaryNode.info.notaryIdentity))
|
||||
// Wait for flow to come to an end and print
|
||||
flowHandle2.resultFuture.getOrThrow()
|
||||
printBalances()
|
||||
@ -107,4 +105,4 @@ class FxTransactionBuildTutorialTest {
|
||||
println("BalanceB\n" + nodeB.services.vaultService.cashBalances)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -259,8 +259,8 @@ Launch the Explorer application to visualize the issuance and transfer of cash f
|
||||
|
||||
Using the following login details:
|
||||
|
||||
- For the Bank of Corda node: localhost / port 10004 / username user1 / password test
|
||||
- For the Big Corporation node: localhost / port 10006 / username user1 / password test
|
||||
- For the Bank of Corda node: localhost / port 10004 / username bankUser / password test
|
||||
- For the Big Corporation node: localhost / port 10006 / username bigCorpUser / password test
|
||||
|
||||
See https://docs.corda.net/node-explorer.html for further details on usage.
|
||||
|
||||
|
32
finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt
Normal file
32
finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt
Normal file
@ -0,0 +1,32 @@
|
||||
package net.corda.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
|
||||
/**
|
||||
* Initiates a flow that produces an Issue/Move or Exit Cash transaction.
|
||||
*/
|
||||
abstract class AbstractCashFlow(override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() {
|
||||
companion object {
|
||||
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_TX, SIGNING_TX, FINALISING_TX)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
internal fun finaliseTx(participants: Set<Party>, tx: SignedTransaction, message: String) {
|
||||
try {
|
||||
subFlow(FinalityFlow(tx, participants))
|
||||
} catch (e: NotaryException) {
|
||||
throw CashException(message, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CashException(message: String, cause: Throwable) : FlowException(message, cause)
|
67
finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt
Normal file
67
finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt
Normal file
@ -0,0 +1,67 @@
|
||||
package net.corda.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
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.
|
||||
*/
|
||||
class CashExitFlow(val amount: Amount<Currency>, val issueRef: OpaqueBytes, progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) {
|
||||
constructor(amount: Amount<Currency>, issueRef: OpaqueBytes) : this(amount, issueRef, tracker())
|
||||
|
||||
companion object {
|
||||
fun tracker() = ProgressTracker(GENERATING_TX, SIGNING_TX, FINALISING_TX)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Throws(CashException::class)
|
||||
override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = GENERATING_TX
|
||||
val builder: TransactionBuilder = TransactionType.General.Builder(null)
|
||||
val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef)
|
||||
try {
|
||||
Cash().generateExit(
|
||||
builder,
|
||||
amount.issuedBy(issuer),
|
||||
serviceHub.vaultService.currentVault.statesOfType<Cash.State>().filter { it.state.data.owner == issuer.party.owningKey })
|
||||
} catch (e: InsufficientBalanceException) {
|
||||
throw CashException("Exiting more cash than exists", e)
|
||||
}
|
||||
progressTracker.currentStep = SIGNING_TX
|
||||
val myKey = serviceHub.legalIdentityKey
|
||||
builder.signWith(myKey)
|
||||
|
||||
// Work out who the owners of the burnt states were
|
||||
val inputStatesNullable = serviceHub.vaultService.statesForRefs(builder.inputStates())
|
||||
val inputStates = inputStatesNullable.values.filterNotNull().map { it.data }
|
||||
if (inputStatesNullable.size != inputStates.size) {
|
||||
val unresolvedStateRefs = inputStatesNullable.filter { it.value == null }.map { it.key }
|
||||
throw IllegalStateException("Failed to resolve input StateRefs: $unresolvedStateRefs")
|
||||
}
|
||||
|
||||
// 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
|
||||
.filterIsInstance<Cash.State>()
|
||||
.map { serviceHub.identityService.partyFromKey(it.owner) }
|
||||
.filterNotNull()
|
||||
.toSet()
|
||||
|
||||
// Commit the transaction
|
||||
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
|
||||
progressTracker.currentStep = FINALISING_TX
|
||||
finaliseTx(participants, tx, "Unable to notarise exit")
|
||||
return tx
|
||||
}
|
||||
}
|
@ -1,19 +1,13 @@
|
||||
package net.corda.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.AnonymousParty
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import java.security.KeyPair
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@ -21,8 +15,8 @@ import java.util.*
|
||||
*
|
||||
* @param command Indicates what Cash transaction to create with what parameters.
|
||||
*/
|
||||
class CashFlow(val command: CashCommand, override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() {
|
||||
constructor(command: CashCommand) : this(command, tracker())
|
||||
class CashFlow(val command: CashFlow.Command, override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() {
|
||||
constructor(command: CashFlow.Command) : this(command, tracker())
|
||||
|
||||
companion object {
|
||||
object ISSUING : ProgressTracker.Step("Issuing cash")
|
||||
@ -36,133 +30,38 @@ class CashFlow(val command: CashCommand, override val progressTracker: ProgressT
|
||||
@Throws(CashException::class)
|
||||
override fun call(): SignedTransaction {
|
||||
return when (command) {
|
||||
is CashCommand.IssueCash -> issueCash(command)
|
||||
is CashCommand.PayCash -> initiatePayment(command)
|
||||
is CashCommand.ExitCash -> exitCash(command)
|
||||
is CashFlow.Command.IssueCash -> subFlow(CashIssueFlow(command.amount, command.issueRef, command.recipient, command.notary))
|
||||
is CashFlow.Command.PayCash -> subFlow(CashPaymentFlow(command.amount, command.recipient))
|
||||
is CashFlow.Command.ExitCash -> subFlow(CashExitFlow(command.amount, command.issueRef))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check with the recipient if they want to accept the cash.
|
||||
@Suspendable
|
||||
private fun initiatePayment(req: CashCommand.PayCash): SignedTransaction {
|
||||
progressTracker.currentStep = PAYING
|
||||
val builder: TransactionBuilder = TransactionType.General.Builder(null)
|
||||
// TODO: Have some way of restricting this to states the caller controls
|
||||
val (spendTX, keysForSigning) = try {
|
||||
serviceHub.vaultService.generateSpend(
|
||||
builder,
|
||||
req.amount.withoutIssuer(),
|
||||
req.recipient.owningKey,
|
||||
setOf(req.amount.token.issuer.party))
|
||||
} catch (e: InsufficientBalanceException) {
|
||||
throw CashException("Insufficent cash for spend", e)
|
||||
}
|
||||
/**
|
||||
* A command to initiate the Cash flow with.
|
||||
*/
|
||||
sealed class Command {
|
||||
/**
|
||||
* A command to initiate the Cash flow with.
|
||||
*/
|
||||
class IssueCash(val amount: Amount<Currency>,
|
||||
val issueRef: OpaqueBytes,
|
||||
val recipient: Party,
|
||||
val notary: Party) : CashFlow.Command()
|
||||
|
||||
keysForSigning.keys.forEach {
|
||||
val key = serviceHub.keyManagementService.keys[it] ?: throw IllegalStateException("Could not find signing key for ${it.toStringShort()}")
|
||||
builder.signWith(KeyPair(it, key))
|
||||
}
|
||||
/**
|
||||
* Pay cash to someone else.
|
||||
*
|
||||
* @param amount the amount of currency to issue on to the ledger.
|
||||
* @param recipient the party to issue the cash to.
|
||||
*/
|
||||
class PayCash(val amount: Amount<Issued<Currency>>, val recipient: Party) : CashFlow.Command()
|
||||
|
||||
val tx = spendTX.toSignedTransaction(checkSufficientSignatures = false)
|
||||
finaliseTx(setOf(req.recipient), tx, "Unable to notarise spend")
|
||||
return tx
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun exitCash(req: CashCommand.ExitCash): SignedTransaction {
|
||||
progressTracker.currentStep = EXITING
|
||||
val builder: TransactionBuilder = TransactionType.General.Builder(null)
|
||||
val issuer = serviceHub.myInfo.legalIdentity.ref(req.issueRef)
|
||||
try {
|
||||
Cash().generateExit(
|
||||
builder,
|
||||
req.amount.issuedBy(issuer),
|
||||
serviceHub.vaultService.currentVault.statesOfType<Cash.State>().filter { it.state.data.owner == issuer.party.owningKey })
|
||||
} catch (e: InsufficientBalanceException) {
|
||||
throw CashException("Exiting more cash than exists", e)
|
||||
}
|
||||
val myKey = serviceHub.legalIdentityKey
|
||||
builder.signWith(myKey)
|
||||
|
||||
// Work out who the owners of the burnt states were
|
||||
val inputStatesNullable = serviceHub.vaultService.statesForRefs(builder.inputStates())
|
||||
val inputStates = inputStatesNullable.values.filterNotNull().map { it.data }
|
||||
if (inputStatesNullable.size != inputStates.size) {
|
||||
val unresolvedStateRefs = inputStatesNullable.filter { it.value == null }.map { it.key }
|
||||
throw IllegalStateException("Failed to resolve input StateRefs: $unresolvedStateRefs")
|
||||
}
|
||||
|
||||
// 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
|
||||
.filterIsInstance<Cash.State>()
|
||||
.map { serviceHub.identityService.partyFromKey(it.owner) }
|
||||
.filterNotNull()
|
||||
.toSet()
|
||||
|
||||
// Commit the transaction
|
||||
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
|
||||
finaliseTx(participants, tx, "Unable to notarise exit")
|
||||
return tx
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun finaliseTx(participants: Set<Party>, tx: SignedTransaction, message: String) {
|
||||
try {
|
||||
subFlow(FinalityFlow(tx, participants))
|
||||
} catch (e: NotaryException) {
|
||||
throw CashException(message, e)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO This doesn't throw any exception so it might be worth splitting the three cash commands into separate flows
|
||||
@Suspendable
|
||||
private fun issueCash(req: CashCommand.IssueCash): SignedTransaction {
|
||||
progressTracker.currentStep = ISSUING
|
||||
val builder: TransactionBuilder = TransactionType.General.Builder(notary = null)
|
||||
val issuer = serviceHub.myInfo.legalIdentity.ref(req.issueRef)
|
||||
Cash().generateIssue(builder, req.amount.issuedBy(issuer), req.recipient.owningKey, req.notary)
|
||||
val myKey = serviceHub.legalIdentityKey
|
||||
builder.signWith(myKey)
|
||||
val tx = builder.toSignedTransaction()
|
||||
subFlow(FinalityFlow(tx))
|
||||
return tx
|
||||
/**
|
||||
* Exit cash from the ledger.
|
||||
*
|
||||
* @param amount the amount of currency to exit from the ledger.
|
||||
* @param issueRef the reference previously specified on the issuance.
|
||||
*/
|
||||
class ExitCash(val amount: Amount<Currency>, val issueRef: OpaqueBytes) : CashFlow.Command()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A command to initiate the Cash flow with.
|
||||
*/
|
||||
sealed class CashCommand {
|
||||
/**
|
||||
* Issue cash state objects.
|
||||
*
|
||||
* @param amount the amount of currency to issue on to the ledger.
|
||||
* @param issueRef the reference to specify on the issuance, used to differentiate pools of cash. Convention is
|
||||
* to use the single byte "0x01" as a default.
|
||||
* @param recipient the party to issue the cash to.
|
||||
* @param notary the notary to use for this transaction.
|
||||
*/
|
||||
class IssueCash(val amount: Amount<Currency>,
|
||||
val issueRef: OpaqueBytes,
|
||||
val recipient: Party,
|
||||
val notary: Party) : CashCommand()
|
||||
|
||||
/**
|
||||
* Pay cash to someone else.
|
||||
*
|
||||
* @param amount the amount of currency to issue on to the ledger.
|
||||
* @param recipient the party to issue the cash to.
|
||||
*/
|
||||
class PayCash(val amount: Amount<Issued<Currency>>, val recipient: Party) : CashCommand()
|
||||
|
||||
/**
|
||||
* Exit cash from the ledger.
|
||||
*
|
||||
* @param amount the amount of currency to exit from the ledger.
|
||||
* @param issueRef the reference previously specified on the issuance.
|
||||
*/
|
||||
class ExitCash(val amount: Amount<Currency>, val issueRef: OpaqueBytes) : CashCommand()
|
||||
}
|
||||
|
||||
class CashException(message: String, cause: Throwable) : FlowException(message, cause)
|
||||
|
46
finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt
Normal file
46
finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt
Normal file
@ -0,0 +1,46 @@
|
||||
package net.corda.flows
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.contracts.TransactionType
|
||||
import net.corda.core.contracts.issuedBy
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
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 notary the notary to set on the output states.
|
||||
*/
|
||||
class CashIssueFlow(val amount: Amount<Currency>,
|
||||
val issueRef: OpaqueBytes,
|
||||
val recipient: Party,
|
||||
val notary: Party,
|
||||
progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) {
|
||||
constructor(amount: Amount<Currency>,
|
||||
issueRef: OpaqueBytes,
|
||||
recipient: Party,
|
||||
notary: Party) : this(amount, issueRef, recipient, notary, tracker())
|
||||
|
||||
override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = GENERATING_TX
|
||||
val builder: TransactionBuilder = TransactionType.General.Builder(notary = null)
|
||||
val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef)
|
||||
Cash().generateIssue(builder, amount.issuedBy(issuer), recipient.owningKey, notary)
|
||||
progressTracker.currentStep = SIGNING_TX
|
||||
val myKey = serviceHub.legalIdentityKey
|
||||
builder.signWith(myKey)
|
||||
val tx = builder.toSignedTransaction()
|
||||
progressTracker.currentStep = FINALISING_TX
|
||||
subFlow(FinalityFlow(tx))
|
||||
return tx
|
||||
}
|
||||
}
|
49
finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt
Normal file
49
finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt
Normal file
@ -0,0 +1,49 @@
|
||||
package net.corda.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import java.security.KeyPair
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Initiates a flow that produces an cash move transaction.
|
||||
*
|
||||
* @param amount the amount of a currency to pay to the recipient.
|
||||
* @param recipient the party to pay the currency to.
|
||||
*/
|
||||
open class CashPaymentFlow(val amount: Amount<Issued<Currency>>, val recipient: Party, progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) {
|
||||
constructor(amount: Amount<Issued<Currency>>, recipient: Party) : this(amount, recipient, tracker())
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = GENERATING_TX
|
||||
val builder: TransactionBuilder = TransactionType.General.Builder(null)
|
||||
// TODO: Have some way of restricting this to states the caller controls
|
||||
val (spendTX, keysForSigning) = try {
|
||||
serviceHub.vaultService.generateSpend(
|
||||
builder,
|
||||
amount.withoutIssuer(),
|
||||
recipient.owningKey,
|
||||
setOf(amount.token.issuer.party))
|
||||
} catch (e: InsufficientBalanceException) {
|
||||
throw CashException("Insufficent cash for spend", e)
|
||||
}
|
||||
|
||||
progressTracker.currentStep = SIGNING_TX
|
||||
keysForSigning.keys.forEach {
|
||||
val key = serviceHub.keyManagementService.keys[it] ?: throw IllegalStateException("Could not find signing key for ${it.toStringShort()}")
|
||||
builder.signWith(KeyPair(it, key))
|
||||
}
|
||||
|
||||
progressTracker.currentStep = FINALISING_TX
|
||||
val tx = spendTX.toSignedTransaction(checkSufficientSignatures = false)
|
||||
finaliseTx(setOf(recipient), tx, "Unable to notarise spend")
|
||||
return tx
|
||||
}
|
||||
}
|
@ -79,7 +79,7 @@ object IssuerFlow {
|
||||
// invoke Cash subflow to issue Asset
|
||||
progressTracker.currentStep = ISSUING
|
||||
val bankOfCordaParty = serviceHub.myInfo.legalIdentity
|
||||
val issueCashFlow = CashFlow(CashCommand.IssueCash(amount, issuerPartyRef, bankOfCordaParty, notaryParty))
|
||||
val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, bankOfCordaParty, notaryParty)
|
||||
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
|
||||
@ -87,7 +87,7 @@ object IssuerFlow {
|
||||
return issueTx
|
||||
// now invoke Cash subflow to Move issued assetType to issue requester
|
||||
progressTracker.currentStep = TRANSFERRING
|
||||
val moveCashFlow = CashFlow(CashCommand.PayCash(amount.issuedBy(bankOfCordaParty.ref(issuerPartyRef)), issueTo))
|
||||
val moveCashFlow = CashPaymentFlow(amount.issuedBy(bankOfCordaParty.ref(issuerPartyRef)), issueTo)
|
||||
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
|
||||
|
@ -11,8 +11,9 @@ import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.node.driver.DriverBasedTest
|
||||
import net.corda.node.driver.NodeHandle
|
||||
import net.corda.node.driver.driver
|
||||
@ -35,7 +36,10 @@ class DistributedServiceTests : DriverBasedTest() {
|
||||
override fun setup() = driver {
|
||||
// Start Alice and 3 notaries in a RAFT cluster
|
||||
val clusterSize = 3
|
||||
val testUser = User("test", "test", permissions = setOf(startFlowPermission<CashFlow>()))
|
||||
val testUser = User("test", "test", permissions = setOf(
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>())
|
||||
)
|
||||
val aliceFuture = startNode("Alice", rpcUsers = listOf(testUser))
|
||||
val notariesFuture = startNotaryCluster(
|
||||
"Notary",
|
||||
@ -135,15 +139,15 @@ class DistributedServiceTests : DriverBasedTest() {
|
||||
|
||||
private fun issueCash(amount: Amount<Currency>) {
|
||||
val issueHandle = aliceProxy.startFlow(
|
||||
::CashFlow,
|
||||
CashCommand.IssueCash(amount, OpaqueBytes.of(0), alice.nodeInfo.legalIdentity, raftNotaryIdentity))
|
||||
::CashIssueFlow,
|
||||
amount, OpaqueBytes.of(0), alice.nodeInfo.legalIdentity, raftNotaryIdentity)
|
||||
issueHandle.returnValue.getOrThrow()
|
||||
}
|
||||
|
||||
private fun paySelf(amount: Amount<Currency>) {
|
||||
val payHandle = aliceProxy.startFlow(
|
||||
::CashFlow,
|
||||
CashCommand.PayCash(amount.issuedBy(alice.nodeInfo.legalIdentity.ref(0)), alice.nodeInfo.legalIdentity))
|
||||
::CashPaymentFlow,
|
||||
amount.issuedBy(alice.nodeInfo.legalIdentity.ref(0)), alice.nodeInfo.legalIdentity)
|
||||
payHandle.returnValue.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.*
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.X509Utilities
|
||||
import net.corda.core.flows.FlowLogic
|
||||
@ -16,14 +18,12 @@ import net.corda.core.messaging.SingleMessageRecipient
|
||||
import net.corda.core.node.*
|
||||
import net.corda.core.node.services.*
|
||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.flows.FinalityFlow
|
||||
import net.corda.flows.sendRequest
|
||||
import net.corda.flows.*
|
||||
import net.corda.node.services.api.*
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||
@ -51,7 +51,6 @@ import net.corda.node.utilities.databaseTransaction
|
||||
import org.apache.activemq.artemis.utils.ReusableLatch
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.slf4j.Logger
|
||||
import java.io.File
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
@ -82,11 +81,12 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
val PUBLIC_IDENTITY_FILE_NAME = "identity-public"
|
||||
|
||||
val defaultFlowWhiteList: Map<Class<out FlowLogic<*>>, Set<Class<*>>> = mapOf(
|
||||
CashFlow::class.java to setOf(
|
||||
CashCommand.IssueCash::class.java,
|
||||
CashCommand.PayCash::class.java,
|
||||
CashCommand.ExitCash::class.java
|
||||
),
|
||||
CashFlow::class.java to setOf(CashFlow.Command.IssueCash::class.java,
|
||||
CashFlow.Command.PayCash::class.java,
|
||||
CashFlow.Command.ExitCash::class.java),
|
||||
CashExitFlow::class.java to setOf(Amount::class.java, PartyAndReference::class.java),
|
||||
CashIssueFlow::class.java to setOf(Amount::class.java, OpaqueBytes::class.java, Party::class.java),
|
||||
CashPaymentFlow::class.java to setOf(Amount::class.java, Party::class.java),
|
||||
FinalityFlow::class.java to emptySet()
|
||||
)
|
||||
}
|
||||
|
@ -9,8 +9,8 @@ import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.node.internal.CordaRPCOpsImpl
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.messaging.CURRENT_RPC_USER
|
||||
@ -48,7 +48,10 @@ class CordaRPCOpsImplTest {
|
||||
aliceNode = network.createNode(networkMapAddress = networkMap.info.address)
|
||||
notaryNode = network.createNode(advertisedServices = ServiceInfo(SimpleNotaryService.type), networkMapAddress = networkMap.info.address)
|
||||
rpc = CordaRPCOpsImpl(aliceNode.services, aliceNode.smm, aliceNode.database)
|
||||
CURRENT_RPC_USER.set(User("user", "pwd", permissions = setOf(startFlowPermission<CashFlow>())))
|
||||
CURRENT_RPC_USER.set(User("user", "pwd", permissions = setOf(
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>()
|
||||
)))
|
||||
|
||||
databaseTransaction(aliceNode.database) {
|
||||
stateMachineUpdates = rpc.stateMachinesAndUpdates().second
|
||||
@ -69,8 +72,7 @@ class CordaRPCOpsImplTest {
|
||||
|
||||
// Tell the monitoring service node to issue some cash
|
||||
val recipient = aliceNode.info.legalIdentity
|
||||
val outEvent = CashCommand.IssueCash(Amount(quantity, GBP), ref, recipient, notaryNode.info.notaryIdentity)
|
||||
rpc.startFlow(::CashFlow, outEvent)
|
||||
rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, recipient, notaryNode.info.notaryIdentity)
|
||||
network.runNetwork()
|
||||
|
||||
val expectedState = Cash.State(Amount(quantity,
|
||||
@ -107,19 +109,19 @@ class CordaRPCOpsImplTest {
|
||||
@Test
|
||||
fun `issue and move`() {
|
||||
|
||||
rpc.startFlow(::CashFlow, CashCommand.IssueCash(
|
||||
amount = Amount(100, USD),
|
||||
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||
recipient = aliceNode.info.legalIdentity,
|
||||
notary = notaryNode.info.notaryIdentity
|
||||
))
|
||||
rpc.startFlow(::CashIssueFlow,
|
||||
Amount(100, USD),
|
||||
OpaqueBytes(ByteArray(1, { 1 })),
|
||||
aliceNode.info.legalIdentity,
|
||||
notaryNode.info.notaryIdentity
|
||||
)
|
||||
|
||||
network.runNetwork()
|
||||
|
||||
rpc.startFlow(::CashFlow, CashCommand.PayCash(
|
||||
amount = Amount(100, Issued(PartyAndReference(aliceNode.info.legalIdentity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
|
||||
recipient = aliceNode.info.legalIdentity
|
||||
))
|
||||
rpc.startFlow(::CashPaymentFlow,
|
||||
Amount(100, Issued(PartyAndReference(aliceNode.info.legalIdentity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
|
||||
aliceNode.info.legalIdentity
|
||||
)
|
||||
|
||||
network.runNetwork()
|
||||
|
||||
@ -188,12 +190,12 @@ class CordaRPCOpsImplTest {
|
||||
fun `cash command by user not permissioned for cash`() {
|
||||
CURRENT_RPC_USER.set(User("user", "pwd", permissions = emptySet()))
|
||||
assertThatExceptionOfType(PermissionException::class.java).isThrownBy {
|
||||
rpc.startFlow(::CashFlow, CashCommand.IssueCash(
|
||||
amount = Amount(100, USD),
|
||||
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||
recipient = aliceNode.info.legalIdentity,
|
||||
notary = notaryNode.info.notaryIdentity
|
||||
))
|
||||
rpc.startFlow(::CashIssueFlow,
|
||||
Amount(100, USD),
|
||||
OpaqueBytes(ByteArray(1, { 1 })),
|
||||
aliceNode.info.legalIdentity,
|
||||
notaryNode.info.notaryIdentity
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,8 +24,8 @@ import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.flows.FinalityFlow
|
||||
import net.corda.flows.NotaryFlow
|
||||
import net.corda.node.services.persistence.checkpoints
|
||||
@ -320,16 +320,16 @@ class StateMachineManagerTests {
|
||||
@Test
|
||||
fun `different notaries are picked when addressing shared notary identity`() {
|
||||
assertEquals(notary1.info.notaryIdentity, notary2.info.notaryIdentity)
|
||||
node1.services.startFlow(CashFlow(CashCommand.IssueCash(
|
||||
node1.services.startFlow(CashIssueFlow(
|
||||
DOLLARS(2000),
|
||||
OpaqueBytes.of(0x01),
|
||||
node1.info.legalIdentity,
|
||||
notary1.info.notaryIdentity)))
|
||||
notary1.info.notaryIdentity))
|
||||
// We pay a couple of times, the notary picking should go round robin
|
||||
for (i in 1 .. 3) {
|
||||
node1.services.startFlow(CashFlow(CashCommand.PayCash(
|
||||
node1.services.startFlow(CashPaymentFlow(
|
||||
DOLLARS(500).issuedBy(node1.info.legalIdentity.ref(0x01)),
|
||||
node2.info.legalIdentity)))
|
||||
node2.info.legalIdentity))
|
||||
net.runNetwork()
|
||||
}
|
||||
val endpoint = net.messagingNetwork.endpoint(notary1.net.myAddress as InMemoryMessagingNetwork.PeerHandle)!!
|
||||
|
@ -41,9 +41,9 @@ Testing of the Bank of Corda application is demonstrated at two levels:
|
||||
|
||||
Security
|
||||
The RPC API requires a client to pass in user credentials:
|
||||
client.start("user1","test")
|
||||
client.start("bankUser","test")
|
||||
which are validated on the Bank of Corda node against those configured at node startup:
|
||||
User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||
User("bankUser", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||
startNode("BankOfCorda", rpcUsers = listOf(user))
|
||||
|
||||
Notary
|
||||
|
@ -77,9 +77,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||
webPort 10005
|
||||
cordapps = []
|
||||
rpcUsers = [
|
||||
['user' : "user1",
|
||||
['user' : "bankUser",
|
||||
'password' : "test",
|
||||
'permissions' : ["StartFlow.net.corda.flows.CashFlow",
|
||||
'permissions' : ["StartFlow.net.corda.flows.CashPaymentFlow",
|
||||
"StartFlow.net.corda.flows.IssuerFlow\$IssuanceRequester"]]
|
||||
]
|
||||
}
|
||||
@ -91,9 +91,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||
webPort 10007
|
||||
cordapps = []
|
||||
rpcUsers = [
|
||||
['user' : "user1",
|
||||
['user' : "bigCorpUser",
|
||||
'password' : "test",
|
||||
'permissions' : ["StartFlow.net.corda.flows.CashFlow"]]
|
||||
'permissions' : ["StartFlow.net.corda.flows.CashPaymentFlow"]]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import net.corda.flows.IssuerFlow
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.ServiceType
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.startFlowPermission
|
||||
@ -51,8 +51,8 @@ private class BankOfCordaDriver {
|
||||
val role = options.valueOf(roleArg)!!
|
||||
if (role == Role.ISSUER) {
|
||||
driver(dsl = {
|
||||
val bankUser = User(BANK_USERNAME, "test", permissions = setOf(startFlowPermission<CashFlow>(), startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||
val bigCorpUser = User(BIGCORP_USERNAME, "test", permissions = setOf(startFlowPermission<CashFlow>()))
|
||||
val bankUser = User(BANK_USERNAME, "test", permissions = setOf(startFlowPermission<CashPaymentFlow>(), startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||
val bigCorpUser = User(BIGCORP_USERNAME, "test", permissions = setOf(startFlowPermission<CashPaymentFlow>()))
|
||||
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
val bankOfCorda = startNode("BankOfCorda", rpcUsers = listOf(bankUser), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.USD"))))
|
||||
startNode("BigCorporation", rpcUsers = listOf(bigCorpUser))
|
||||
|
@ -32,7 +32,7 @@ class BankOfCordaClientApi(val hostAndPort: HostAndPort) {
|
||||
fun requestRPCIssue(params: IssueRequestParams): SignedTransaction {
|
||||
val client = CordaRPCClient(hostAndPort, configureTestSSL())
|
||||
// TODO: privileged security controls required
|
||||
client.start("user1", "test")
|
||||
client.start("bankUser", "test")
|
||||
val proxy = client.proxy()
|
||||
|
||||
// Resolve parties via RPC
|
||||
|
@ -28,7 +28,6 @@ import net.corda.explorer.model.ReportingCurrencyModel
|
||||
import net.corda.explorer.views.bigDecimalFormatter
|
||||
import net.corda.explorer.views.byteFormatter
|
||||
import net.corda.explorer.views.stringConverter
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||
import org.controlsfx.dialog.ExceptionDialog
|
||||
@ -88,7 +87,7 @@ class NewTransaction : Fragment() {
|
||||
}
|
||||
dialog.show()
|
||||
runAsync {
|
||||
val handle = if (it is CashCommand.IssueCash) {
|
||||
val handle = if (it is CashFlow.Command.IssueCash) {
|
||||
myIdentity.value?.let { myIdentity ->
|
||||
rpcProxy.value!!.startFlow(::IssuanceRequester,
|
||||
it.amount,
|
||||
@ -111,9 +110,9 @@ class NewTransaction : Fragment() {
|
||||
Alert.AlertType.ERROR to response.message
|
||||
} else {
|
||||
val type = when (command) {
|
||||
is CashCommand.IssueCash -> "Cash Issued"
|
||||
is CashCommand.ExitCash -> "Cash Exited"
|
||||
is CashCommand.PayCash -> "Cash Paid"
|
||||
is CashFlow.Command.IssueCash -> "Cash Issued"
|
||||
is CashFlow.Command.ExitCash -> "Cash Exited"
|
||||
is CashFlow.Command.PayCash -> "Cash Paid"
|
||||
}
|
||||
Alert.AlertType.INFORMATION to "$type \nTransaction ID : ${(response as SignedTransaction).id}"
|
||||
}
|
||||
@ -128,7 +127,7 @@ class NewTransaction : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun dialog(window: Window) = Dialog<CashCommand>().apply {
|
||||
private fun dialog(window: Window) = Dialog<CashFlow.Command>().apply {
|
||||
dialogPane = root
|
||||
initOwner(window)
|
||||
setResultConverter {
|
||||
@ -137,10 +136,10 @@ class NewTransaction : Fragment() {
|
||||
when (it) {
|
||||
executeButton -> when (transactionTypeCB.value) {
|
||||
CashTransaction.Issue -> {
|
||||
CashCommand.IssueCash(Amount(amount.value, currencyChoiceBox.value), issueRef, partyBChoiceBox.value.legalIdentity, notaries.first().notaryIdentity)
|
||||
CashFlow.Command.IssueCash(Amount(amount.value, currencyChoiceBox.value), issueRef, partyBChoiceBox.value.legalIdentity, notaries.first().notaryIdentity)
|
||||
}
|
||||
CashTransaction.Pay -> CashCommand.PayCash(Amount(amount.value, Issued(PartyAndReference(issuerChoiceBox.value, issueRef), currencyChoiceBox.value)), partyBChoiceBox.value.legalIdentity)
|
||||
CashTransaction.Exit -> CashCommand.ExitCash(Amount(amount.value, currencyChoiceBox.value), issueRef)
|
||||
CashTransaction.Pay -> CashFlow.Command.PayCash(Amount(amount.value, Issued(PartyAndReference(issuerChoiceBox.value, issueRef), currencyChoiceBox.value)), partyBChoiceBox.value.legalIdentity)
|
||||
CashTransaction.Exit -> CashFlow.Command.ExitCash(Amount(amount.value, currencyChoiceBox.value), issueRef)
|
||||
else -> null
|
||||
}
|
||||
else -> null
|
||||
|
@ -12,7 +12,8 @@ import net.corda.core.flows.FlowException
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.flows.CashException
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.loadtest.LoadTest
|
||||
import net.corda.loadtest.NodeHandle
|
||||
@ -28,18 +29,18 @@ private val log = LoggerFactory.getLogger("CrossCash")
|
||||
*/
|
||||
|
||||
data class CrossCashCommand(
|
||||
val command: CashCommand,
|
||||
val command: CashFlow.Command,
|
||||
val node: NodeHandle
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return when (command) {
|
||||
is CashCommand.IssueCash -> {
|
||||
is CashFlow.Command.IssueCash -> {
|
||||
"ISSUE ${node.info.legalIdentity} -> ${command.recipient} : ${command.amount}"
|
||||
}
|
||||
is CashCommand.PayCash -> {
|
||||
is CashFlow.Command.PayCash -> {
|
||||
"MOVE ${node.info.legalIdentity} -> ${command.recipient} : ${command.amount}"
|
||||
}
|
||||
is CashCommand.ExitCash -> {
|
||||
is CashFlow.Command.ExitCash -> {
|
||||
"EXIT ${node.info.legalIdentity} : ${command.amount}"
|
||||
}
|
||||
}
|
||||
@ -145,7 +146,7 @@ val crossCashTest = LoadTest<CrossCashCommand, CrossCashState>(
|
||||
|
||||
interpret = { state, command ->
|
||||
when (command.command) {
|
||||
is CashCommand.IssueCash -> {
|
||||
is CashFlow.Command.IssueCash -> {
|
||||
val newDiffQueues = state.copyQueues()
|
||||
val originators = newDiffQueues.getOrPut(command.command.recipient, { HashMap() })
|
||||
val issuer = command.node.info.legalIdentity
|
||||
@ -155,7 +156,7 @@ val crossCashTest = LoadTest<CrossCashCommand, CrossCashState>(
|
||||
queue.add(Pair(issuer, quantity))
|
||||
CrossCashState(state.nodeVaults, newDiffQueues)
|
||||
}
|
||||
is CashCommand.PayCash -> {
|
||||
is CashFlow.Command.PayCash -> {
|
||||
val newNodeVaults = state.copyVaults()
|
||||
val newDiffQueues = state.copyQueues()
|
||||
val recipientOriginators = newDiffQueues.getOrPut(command.command.recipient, { HashMap() })
|
||||
@ -182,7 +183,7 @@ val crossCashTest = LoadTest<CrossCashCommand, CrossCashState>(
|
||||
recipientQueue.add(Pair(issuer, quantity))
|
||||
CrossCashState(newNodeVaults, newDiffQueues)
|
||||
}
|
||||
is CashCommand.ExitCash -> {
|
||||
is CashFlow.Command.ExitCash -> {
|
||||
val newNodeVaults = state.copyVaults()
|
||||
val issuer = command.node.info.legalIdentity
|
||||
val quantity = command.command.amount.quantity
|
||||
|
@ -8,7 +8,7 @@ import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.crypto.AnonymousParty
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import java.util.*
|
||||
|
||||
fun generateIssue(
|
||||
@ -16,12 +16,12 @@ fun generateIssue(
|
||||
currency: Currency,
|
||||
notary: Party,
|
||||
possibleRecipients: List<Party>
|
||||
): Generator<CashCommand.IssueCash> {
|
||||
): Generator<CashFlow.Command.IssueCash> {
|
||||
return generateAmount(0, max, Generator.pure(currency)).combine(
|
||||
Generator.pure(OpaqueBytes.of(0)),
|
||||
Generator.pickOne(possibleRecipients)
|
||||
) { amount, ref, recipient ->
|
||||
CashCommand.IssueCash(amount, ref, recipient, notary)
|
||||
CashFlow.Command.IssueCash(amount, ref, recipient, notary)
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,19 +30,19 @@ fun generateMove(
|
||||
currency: Currency,
|
||||
issuer: AnonymousParty,
|
||||
possibleRecipients: List<Party>
|
||||
): Generator<CashCommand.PayCash> {
|
||||
): Generator<CashFlow.Command.PayCash> {
|
||||
return generateAmount(1, max, Generator.pure(Issued(PartyAndReference(issuer, OpaqueBytes.of(0)), currency))).combine(
|
||||
Generator.pickOne(possibleRecipients)
|
||||
) { amount, recipient ->
|
||||
CashCommand.PayCash(amount, recipient)
|
||||
CashFlow.Command.PayCash(amount, recipient)
|
||||
}
|
||||
}
|
||||
|
||||
fun generateExit(
|
||||
max: Long,
|
||||
currency: Currency
|
||||
): Generator<CashCommand.ExitCash> {
|
||||
): Generator<CashFlow.Command.ExitCash> {
|
||||
return generateAmount(1, max, Generator.pure(currency)).map { amount ->
|
||||
CashCommand.ExitCash(amount, OpaqueBytes.of(0))
|
||||
CashFlow.Command.ExitCash(amount, OpaqueBytes.of(0))
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,8 @@ import net.corda.core.crypto.Party
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.flows.CashException
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.loadtest.LoadTest
|
||||
import net.corda.loadtest.NodeHandle
|
||||
@ -22,7 +23,7 @@ private val log = LoggerFactory.getLogger("SelfIssue")
|
||||
|
||||
// DOCS START 1
|
||||
data class SelfIssueCommand(
|
||||
val command: CashCommand.IssueCash,
|
||||
val command: CashFlow.Command.IssueCash,
|
||||
val node: NodeHandle
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user