mirror of
https://github.com/corda/corda.git
synced 2024-12-19 04:57:58 +00:00
Merge pull request #49 from corda/boc-demo-integration
Bank of Corda integration with Trader Demo and Explorer
This commit is contained in:
commit
1bcabc8d41
15
.idea/runConfigurations/Explorer___demo_nodes.xml
generated
Normal file
15
.idea/runConfigurations/Explorer___demo_nodes.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Explorer - demo nodes" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.explorer.MainKt" />
|
||||
<option name="VM_PARAMETERS" value="" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" value="1.8" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="explorer_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Explorer___demo_nodes__simulation_.xml
generated
Normal file
15
.idea/runConfigurations/Explorer___demo_nodes__simulation_.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Explorer - demo nodes (simulation)" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.explorer.MainKt" />
|
||||
<option name="VM_PARAMETERS" value="" />
|
||||
<option name="PROGRAM_PARAMETERS" value="-S" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" value="1.8" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="explorer_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
@ -6,6 +6,7 @@ import net.corda.core.crypto.Party
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.flows.CashCommand
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned
|
||||
@ -13,15 +14,15 @@ import net.corda.flows.CashCommand
|
||||
*/
|
||||
class EventGenerator(
|
||||
val parties: List<Party>,
|
||||
val notary: Party
|
||||
val notary: Party,
|
||||
val currencies: List<Currency> = listOf(USD, GBP, CHF),
|
||||
val issuers: List<Party> = parties
|
||||
) {
|
||||
|
||||
private var vault = listOf<StateAndRef<Cash.State>>()
|
||||
|
||||
val issuerGenerator =
|
||||
Generator.pickOne(parties).combine(Generator.intRange(0, 1)) { party, ref -> party.ref(ref.toByte()) }
|
||||
Generator.pickOne(issuers).combine(Generator.intRange(0, 1)) { party, ref -> party.ref(ref.toByte()) }
|
||||
|
||||
val currencies = setOf(USD, GBP, CHF).toList() // + Currency.getAvailableCurrencies().toList().subList(0, 3).toSet()).toList()
|
||||
val currencyGenerator = Generator.pickOne(currencies)
|
||||
|
||||
val issuedGenerator = issuerGenerator.combine(currencyGenerator) { issuer, currency -> Issued(issuer, currency) }
|
||||
@ -93,8 +94,11 @@ class EventGenerator(
|
||||
1.0 to moveCashGenerator
|
||||
)
|
||||
|
||||
val bankOfCordaCommandGenerator = Generator.frequency(
|
||||
0.6 to issueCashGenerator,
|
||||
val bankOfCordaExitGenerator = Generator.frequency(
|
||||
0.4 to exitCashGenerator
|
||||
)
|
||||
|
||||
val bankOfCordaIssueGenerator = Generator.frequency(
|
||||
0.6 to issueCashGenerator
|
||||
)
|
||||
}
|
@ -13,13 +13,7 @@ sealed class ServiceType(val id: String) {
|
||||
// * IDs can only contain alphanumeric, full stop and underscore ASCII characters
|
||||
require(id.matches(Regex("[a-z][a-zA-Z0-9._]+")))
|
||||
}
|
||||
|
||||
private class ServiceTypeImpl(baseId: String, subTypeId: String) : ServiceType("$baseId.$subTypeId") {
|
||||
init {
|
||||
// only allowed one level of subtype
|
||||
require(subTypeId.matches(Regex("[a-z]\\w+")))
|
||||
}
|
||||
}
|
||||
private class ServiceTypeImpl(baseId: String, subTypeId: String) : ServiceType("$baseId.$subTypeId")
|
||||
|
||||
private class ServiceTypeDirect(id: String) : ServiceType(id)
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
Node Explorer
|
||||
=============
|
||||
|
||||
The node explorer provide views to the node's vault and transaction data using Corda's RPC framework.
|
||||
The user can execute cash transaction commands to issue and move cash to other party on the network or exit cash using the user interface.
|
||||
The node explorer provides views into a node's vault and transaction data using Corda's RPC framework.
|
||||
The user can execute cash transaction commands to issue and move cash to other parties on the network or exit cash (eg. remove from the ledger)
|
||||
|
||||
Running the UI
|
||||
--------------
|
||||
@ -17,6 +17,27 @@ Running the UI
|
||||
|
||||
Running demo nodes
|
||||
------------------
|
||||
|
||||
A demonstration Corda network topology is configured with 5 nodes playing the following roles:
|
||||
|
||||
1. Notary
|
||||
2. Issuer nodes, representing two fictional central banks (UK Bank Plc issuer of GBP and USA Bank Corp issuer of USD)
|
||||
3. Participant nodes, representing two users (Alice and Bob)
|
||||
|
||||
When connected to an *Issuer* node, a user can execute cash transaction commands to issue and move cash to itself or other
|
||||
parties on the network or to exit cash (for itself only).
|
||||
|
||||
When connected to a *Participant* node a user can only execute cash transaction commands to move cash to other parties on the network.
|
||||
|
||||
The Demo Nodes can be started in one of two modes:
|
||||
|
||||
1. Normal
|
||||
|
||||
Fresh clean environment empty of transactions.
|
||||
Firstly, launch an Explorer instance to login to one of the Issuer nodes and issue some cash to the other participants (Bob and Alice).
|
||||
Then launch another Explorer instance to login to a participant node and start making payments (eg. move cash).
|
||||
You will only be able to exit (eg. redeem from the ledger) cash as an issuer node.
|
||||
|
||||
**Windows**::
|
||||
|
||||
gradlew.bat tools:explorer:runDemoNodes
|
||||
@ -25,21 +46,45 @@ Running demo nodes
|
||||
|
||||
./gradlew tools:explorer:runDemoNodes
|
||||
|
||||
.. note:: 3 Corda nodes will be created on the following port on localhost by default.
|
||||
2. Simulation
|
||||
|
||||
In this mode Nodes will automatically commence executing commands as part of a random generation process.
|
||||
Issuer nodes will randomly issue, move and exit cash.
|
||||
Participant nodes will randomly generate spends (eg. move cash to other nodes, including issuers)
|
||||
|
||||
**Windows**::
|
||||
|
||||
gradlew.bat tools:explorer:runSimulationNodes
|
||||
|
||||
**Other**::
|
||||
|
||||
./gradlew tools:explorer:runSimulationNodes
|
||||
|
||||
|
||||
.. note:: 5 Corda nodes will be created on the following port on localhost by default.
|
||||
|
||||
* Notary -> 20002
|
||||
* Alice -> 20004
|
||||
* Bob -> 20006
|
||||
* UK Bank Plc -> 20008 (*Issuer node*)
|
||||
* USA Bank Corp -> 20010 (*Issuer node*)
|
||||
|
||||
Explorer login credentials to the Issuer nodes are defaulted to ``manager`` and ``test``.
|
||||
Explorer login credentials to the Participants nodes are defaulted to ``user1`` and ``test``.
|
||||
Please note you are not allowed to connect to the notary.
|
||||
|
||||
.. note:: Alternatively, you may start the demo nodes from within IntelliJ using either of the run configurations
|
||||
``Explorer - demo nodes`` or ``Explorer - demo nodes (simulation)``
|
||||
|
||||
.. note:: Use the Explorer in conjunction with the Trader Demo and Bank of Corda samples to use other *Issuer* nodes.
|
||||
|
||||
Interface
|
||||
---------
|
||||
Login
|
||||
User can login to any Corda node using the explorer. Alternatively, ``gradlew explorer:runDemoNodes`` can be used to start up demo nodes for testing.
|
||||
Corda node address, username and password are required for login, the address is defaulted to localhost:0 if leave blank.
|
||||
Username and password can be configured via the ``rpcUsers`` field in node's configuration file; for demo nodes, it is defaulted to ``user1`` and ``test``.
|
||||
Username and password can be configured via the ``rpcUsers`` field in node's configuration file.
|
||||
|
||||
.. note:: If you are connecting to the demo nodes, only Alice and Bob (20004, 20006) are accessible using user1 credential, you won't be able to connect to the notary.
|
||||
|
||||
.. image:: resources/explorer/login.png
|
||||
:scale: 50 %
|
||||
:align: center
|
||||
@ -57,12 +102,19 @@ Cash
|
||||
|
||||
.. image:: resources/explorer/vault.png
|
||||
|
||||
New cash transaction
|
||||
This is where you can create new cash transactions.
|
||||
The user can choose from three transaction types (issue, pay and exit) and any party visible on the network.
|
||||
New Transactions
|
||||
This is where you can create new cash transactions.
|
||||
The user can choose from three transaction types (issue, pay and exit) and any party visible on the network.
|
||||
|
||||
General nodes can only execute pay commands to any other party on the network.
|
||||
|
||||
.. image:: resources/explorer/newTransactionCash.png
|
||||
|
||||
Issuer Nodes
|
||||
Issuer nodes can execute issue (to itself or to any other party), pay and exit transactions.
|
||||
The result of the transaction will be visible in the transaction screen when executed.
|
||||
|
||||
.. image:: resources/explorer/newTransaction.png
|
||||
.. image:: resources/explorer/newTransactionIssuer.png
|
||||
|
||||
Transactions
|
||||
The transaction view contains all transactions handled by the node in a table view. It shows basic information on the table e.g. Transaction ID,
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 704 KiB After Width: | Height: | Size: 571 KiB |
BIN
docs/source/resources/explorer/newTransactionCash.png
Normal file
BIN
docs/source/resources/explorer/newTransactionCash.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 267 KiB |
BIN
docs/source/resources/explorer/newTransactionIssuer.png
Normal file
BIN
docs/source/resources/explorer/newTransactionIssuer.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 261 KiB |
Binary file not shown.
Before Width: | Height: | Size: 309 KiB After Width: | Height: | Size: 371 KiB |
@ -31,15 +31,15 @@ develop the demos themselves. For more details about running via the command lin
|
||||
Trader demo
|
||||
-----------
|
||||
|
||||
This demo brings up three nodes: Bank A, Bank B and a notary/network map node that they both use. Bank A will
|
||||
be the buyer, and self-issues some cash in order to acquire commercial paper from Bank B, the seller.
|
||||
This demo brings up four nodes: Bank A, Bank B, Bank Of Corda and a notary/network map node that they both use. Bank A will
|
||||
be the buyer, and requests some cash from the Bank of Corda in order to acquire commercial paper from Bank B, the seller.
|
||||
|
||||
To run from the command line:
|
||||
|
||||
1. Run ``./gradlew samples:trader-demo:deployNodes`` to create a set of configs and installs under ``samples/trader-demo/build/nodes``
|
||||
2. Run ``./samples/trader-demo/build/nodes/runnodes`` (or ``runnodes.bat`` on Windows) to open up three new terminals with the three nodes.
|
||||
3. Run ``./gradlew samples:trader-demo:runBuyer`` to set up the buyer node with some self-issued cash. This step
|
||||
is not expected to print much.
|
||||
2. Run ``./samples/trader-demo/build/nodes/runnodes`` (or ``runnodes.bat`` on Windows) to open up four new terminals with the four nodes.
|
||||
3. Run ``./gradlew samples:trader-demo:runBuyer`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node.
|
||||
This step will display progress information related to the cash issuance process (in the bank of corda node log output).
|
||||
4. Run ``./gradlew samples:trader-demo:runSeller`` to trigger the transaction. You can see both sides of the
|
||||
trade print their progress and final transaction state in the bank node tabs/windows.
|
||||
|
||||
@ -177,6 +177,11 @@ To run from the command line (recommended for Mac/UNIX users!):
|
||||
.. note:: to verify the Bank of Corda node is alive and running navigate to the following URL
|
||||
http://localhost:10005/api/bank/date
|
||||
|
||||
.. note:: the Bank of Corda node explicitly advertises with a node service type as follows:
|
||||
``advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer"))))``
|
||||
This allows for 3rd party applications to perform actions based on Node Type.
|
||||
For example, the Explorer tool only allows nodes of this type to issue and exit cash.
|
||||
|
||||
3. Run ``./gradlew samples:bank-of-corda-demo:runRPCCashIssue`` in another terminal window to trigger a cash issuance request
|
||||
4. Run ``./gradlew samples:bank-of-corda-demo:runWebCashIssue`` in another terminal window to trigger another cash issuance request
|
||||
Now look at the other windows to see the output of the demo.
|
||||
|
@ -50,7 +50,7 @@ abstract class AbstractConserveAmount<S : FungibleAsset<T>, C : CommandData, T :
|
||||
deriveState: (TransactionState<S>, Amount<Issued<T>>, CompositeKey) -> TransactionState<S>,
|
||||
generateMoveCommand: () -> CommandData,
|
||||
generateExitCommand: (Amount<Issued<T>>) -> CommandData): CompositeKey {
|
||||
val owner = assetStates.map { it.state.data.owner }.toSet().single()
|
||||
val owner = assetStates.map { it.state.data.owner }.toSet().singleOrNull() ?: throw InsufficientBalanceException(amountIssued)
|
||||
val currency = amountIssued.token.product
|
||||
val amount = Amount(amountIssued.quantity, currency)
|
||||
var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token }
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.bank.flow
|
||||
package net.corda.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.ThreadBox
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
@ -34,7 +35,14 @@ object IssuerFlow {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef)
|
||||
return sendAndReceive<SignedTransaction>(issuerBankParty, issueRequest).unwrap { it }
|
||||
try {
|
||||
return sendAndReceive<SignedTransaction>(issuerBankParty, issueRequest).unwrap { it }
|
||||
// catch and report exception before throwing back to caller
|
||||
} catch (e: Exception) {
|
||||
logger.error("IssuanceRequesterException: request failed: [${issueRequest}]")
|
||||
// TODO: awaiting exception handling strategy (what action should be taken here?)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,7 +51,7 @@ object IssuerFlow {
|
||||
* Returns the generated transaction representing the transfer of the [Issued] [FungibleAsset] to the issue requester.
|
||||
*/
|
||||
class Issuer(val otherParty: Party): FlowLogic<SignedTransaction>() {
|
||||
override val progressTracker: ProgressTracker = Issuer.tracker()
|
||||
override val progressTracker: ProgressTracker = tracker()
|
||||
companion object {
|
||||
object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request")
|
||||
object ISSUING : ProgressTracker.Step("Self issuing asset")
|
||||
@ -67,35 +75,49 @@ object IssuerFlow {
|
||||
return txn
|
||||
}
|
||||
|
||||
// TODO: resolve race conditions caused by the 2 separate Cashflow commands (Issue and Pay) not reusing the same
|
||||
// state references (thus causing Notarisation double spend exceptions).
|
||||
@Suspendable
|
||||
private fun issueCashTo(amount: Amount<Currency>,
|
||||
issueTo: Party, issuerPartyRef: OpaqueBytes): SignedTransaction {
|
||||
// TODO: pass notary in as request parameter
|
||||
val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity
|
||||
// invoke Cash subflow to issue Asset
|
||||
progressTracker.currentStep = ISSUING
|
||||
val bankOfCordaParty = serviceHub.myInfo.legalIdentity
|
||||
val issueCashFlow = CashFlow(CashCommand.IssueCash(amount, issuerPartyRef, bankOfCordaParty, notaryParty))
|
||||
val resultIssue = subFlow(issueCashFlow)
|
||||
// NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger)
|
||||
if (resultIssue is CashFlowResult.Failed) {
|
||||
logger.error("Problem issuing cash: ${resultIssue.message}")
|
||||
throw Exception(resultIssue.message)
|
||||
try {
|
||||
val issueCashFlow = CashFlow(CashCommand.IssueCash(amount, issuerPartyRef, bankOfCordaParty, notaryParty))
|
||||
val resultIssue = subFlow(issueCashFlow)
|
||||
// NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger)
|
||||
if (resultIssue is CashFlowResult.Failed) {
|
||||
logger.error("Problem issuing cash: ${resultIssue.message}")
|
||||
throw Exception(resultIssue.message)
|
||||
}
|
||||
// short-circuit when issuing to self
|
||||
if (issueTo.equals(serviceHub.myInfo.legalIdentity))
|
||||
return (resultIssue as CashFlowResult.Success).transaction!!
|
||||
// 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 resultMove = subFlow(moveCashFlow)
|
||||
// NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger)
|
||||
if (resultMove is CashFlowResult.Failed) {
|
||||
logger.error("Problem transferring cash: ${resultMove.message}")
|
||||
throw Exception(resultMove.message)
|
||||
}
|
||||
val txn = (resultMove as CashFlowResult.Success).transaction
|
||||
txn?.let {
|
||||
return txn
|
||||
}
|
||||
// NOTE: CashFlowResult.Success should always return a signedTransaction
|
||||
throw Exception("Missing CashFlow transaction [${(resultMove)}]")
|
||||
}
|
||||
// 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 resultMove = subFlow(moveCashFlow)
|
||||
// NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger)
|
||||
if (resultMove is CashFlowResult.Failed) {
|
||||
logger.error("Problem transferring cash: ${resultMove.message}")
|
||||
throw Exception(resultMove.message)
|
||||
// catch and report exception before throwing back to caller flow
|
||||
catch (e: Exception) {
|
||||
logger.error("Issuer Exception: failed for amount ${amount} issuedTo ${issueTo} with issuerPartyRef ${issuerPartyRef}")
|
||||
// TODO: awaiting exception handling strategy (what action should be taken here?)
|
||||
throw e
|
||||
}
|
||||
val txn = (resultMove as CashFlowResult.Success).transaction
|
||||
txn?.let {
|
||||
return txn
|
||||
}
|
||||
// NOTE: CashFlowResult.Success should always return a signedTransaction
|
||||
throw Exception("Missing CashFlow transaction [${(resultMove)}]")
|
||||
}
|
||||
|
||||
class Service(services: PluginServiceHub) {
|
@ -539,6 +539,25 @@ class CashTests {
|
||||
assertFailsWith<InsufficientBalanceException> { makeExit(1000.DOLLARS, MEGA_CORP, 1) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Try exiting for an owner with no states
|
||||
*/
|
||||
@Test
|
||||
fun generateOwnerWithNoStatesExit() {
|
||||
assertFailsWith<InsufficientBalanceException> { makeExit(100.POUNDS, CHARLIE, 1) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Try exiting when vault is empty
|
||||
*/
|
||||
@Test
|
||||
fun generateExitWithEmptyVault() {
|
||||
assertFailsWith<InsufficientBalanceException> {
|
||||
val tx = TransactionType.General.Builder(DUMMY_NOTARY)
|
||||
Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateSimpleDirectSpend() {
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
package net.corda.bank.flow
|
||||
package net.corda.flows
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import net.corda.bank.api.BOC_ISSUER_PARTY
|
||||
import net.corda.bank.api.BOC_KEY
|
||||
import net.corda.bank.flow.IssuerFlow.IssuanceRequester
|
||||
import net.corda.testing.BOC
|
||||
import net.corda.testing.BOC_KEY
|
||||
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
@ -35,11 +35,11 @@ class IssuerFlowTest {
|
||||
net = MockNetwork(false, true)
|
||||
ledger {
|
||||
notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
bankOfCordaNode = net.createPartyNode(notaryNode.info.address, BOC_ISSUER_PARTY.name, BOC_KEY)
|
||||
bankOfCordaNode = net.createPartyNode(notaryNode.info.address, BOC.name, BOC_KEY)
|
||||
bankClientNode = net.createPartyNode(notaryNode.info.address, MEGA_CORP.name, MEGA_CORP_KEY)
|
||||
|
||||
// using default IssueTo Party Reference
|
||||
val issueToPartyAndRef = MEGA_CORP.ref(OpaqueBytes.of(123))
|
||||
val issueToPartyAndRef = MEGA_CORP.ref(OpaqueBytes.Companion.of(123))
|
||||
val (issuer, issuerResult) = runIssuerAndIssueRequester(1000000.DOLLARS, issueToPartyAndRef)
|
||||
assertEquals(issuerResult.get(), issuer.get().resultFuture.get())
|
||||
|
||||
@ -64,7 +64,7 @@ class IssuerFlowTest {
|
||||
val issueRequest = IssuanceRequester(amount, issueToPartyAndRef.party, issueToPartyAndRef.reference, bankOfCordaNode.info.legalIdentity)
|
||||
val issueRequestResultFuture = bankClientNode.smm.add(issueRequest).resultFuture
|
||||
|
||||
return RunResult(issuerFuture, issueRequestResultFuture)
|
||||
return IssuerFlowTest.RunResult(issuerFuture, issueRequestResultFuture)
|
||||
}
|
||||
|
||||
private data class RunResult(
|
@ -6,6 +6,7 @@ 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.crypto.Party
|
||||
import net.corda.core.crypto.X509Utilities
|
||||
import net.corda.core.flows.FlowLogic
|
||||
@ -16,14 +17,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.api.APIServer
|
||||
import net.corda.node.services.api.*
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
|
@ -35,7 +35,8 @@ Similar to 3 above, but using RPC as the remote communications mechanism.
|
||||
## Developer notes
|
||||
|
||||
Testing of the Bank of Corda application is demonstrated at two levels:
|
||||
1. Unit testing the flow uses the [LedgerDSL] and [MockServices]
|
||||
1. Unit testing the flow uses the [LedgerDSL] and [MockServices]. Please see [IssuerFlowTest]
|
||||
The IssuerFlow is one of several reusable flows defined in the finance package.
|
||||
2. Integration testing via RPC and HTTP uses the [Driver] DSL to launch standalone node instances
|
||||
|
||||
Security
|
||||
@ -48,9 +49,9 @@ which are validated on the Bank of Corda node against those configured at node s
|
||||
Notary
|
||||
We are using a [SimpleNotaryService] in this example, but could easily switch to a [ValidatingNotaryService]
|
||||
|
||||
## Future
|
||||
## Integration with other Demos and Tools
|
||||
|
||||
The Bank of Corda node will become an integral part of other Corda samples that require initial issuance of some asset.
|
||||
The Bank of Corda issuer node concept has been integrated into the Explorer tool (simulation nodes) and Trader Demo.
|
||||
|
||||
## Further Reading
|
||||
|
||||
|
@ -78,10 +78,11 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||
artemisPort 10004
|
||||
webPort 10005
|
||||
cordapps = []
|
||||
// TODO: task needs to parse this item when generating node.conf
|
||||
// rpcUsers : [
|
||||
// { user=user1, password=test, permissions=[ StartFlow.net.corda.bank.flow.IssuerFlow$IssuanceRequester ] }
|
||||
// ]
|
||||
rpcUsers = [
|
||||
['user' : "user1",
|
||||
'password' : "test",
|
||||
'permissions' : ["StartFlow.net.corda.flows.CashFlow"]]
|
||||
]
|
||||
}
|
||||
node {
|
||||
name "BigCorporation"
|
||||
|
@ -1,21 +1,17 @@
|
||||
package net.corda.bank
|
||||
|
||||
import net.corda.bank.api.BOC_ISSUER_PARTY_REF
|
||||
import net.corda.bank.flow.IssuerFlow.IssuanceRequester
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.config.configureTestSSL
|
||||
import net.corda.node.services.messaging.CordaRPCClient
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.testing.expect
|
||||
import net.corda.testing.expectEvents
|
||||
import net.corda.testing.getHostAndPort
|
||||
import net.corda.testing.sequence
|
||||
import net.corda.testing.*
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@ -47,7 +43,7 @@ class BankOfCordaRPCClientTest {
|
||||
val vaultUpdatesBigCorp = bigCorpProxy.vaultAndUpdates().second
|
||||
|
||||
// Kick-off actual Issuer Flow
|
||||
val result = bocProxy.startFlow(::IssuanceRequester, 1000.DOLLARS, bigCorporationParty, BOC_ISSUER_PARTY_REF, bankOfCordaParty).returnValue.toBlocking().first()
|
||||
val result = bocProxy.startFlow(::IssuanceRequester, 1000.DOLLARS, bigCorporationParty, BOC_PARTY_REF, bankOfCordaParty).returnValue.toBlocking().first()
|
||||
assertTrue { result is SignedTransaction }
|
||||
|
||||
// Check Bank of Corda Vault Updates
|
||||
|
@ -4,9 +4,11 @@ import com.google.common.net.HostAndPort
|
||||
import joptsimple.OptionParser
|
||||
import net.corda.bank.api.BankOfCordaClientApi
|
||||
import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams
|
||||
import net.corda.bank.flow.IssuerFlow
|
||||
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.node.driver.driver
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.startFlowPermission
|
||||
@ -46,10 +48,10 @@ private class BankOfCordaDriver {
|
||||
val role = options.valueOf(roleArg)!!
|
||||
if (role == Role.ISSUER) {
|
||||
driver(dsl = {
|
||||
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||
val user = User("user1", "test", permissions = setOf(startFlowPermission<CashFlow>(), startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
startNode("BankOfCorda", rpcUsers = listOf(user))
|
||||
startNode("BigCorporation")
|
||||
startNode("BankOfCorda", rpcUsers = listOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer"))))
|
||||
startNode("BigCorporation", rpcUsers = listOf(user))
|
||||
waitForAllNodesToFinish()
|
||||
}, isDebug = true)
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ package net.corda.bank.api
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams
|
||||
import net.corda.bank.flow.IssuerFlow.IssuanceRequester
|
||||
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||
import net.corda.node.services.messaging.CordaRPCClient
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.currency
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.corda.bank.api
|
||||
|
||||
import net.corda.bank.flow.IssuerFlow.IssuanceRequester
|
||||
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.currency
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
|
@ -1,21 +0,0 @@
|
||||
package net.corda.bank.api
|
||||
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.composite
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
|
||||
val defaultRef = OpaqueBytes.of(1)
|
||||
|
||||
/*
|
||||
* Bank Of Corda (BOC_ISSUER_PARTY)
|
||||
*/
|
||||
val BOC_KEY: KeyPair by lazy { generateKeyPair() }
|
||||
val BOC_PUBKEY: CompositeKey get() = BOC_KEY.public.composite
|
||||
val BOC_ISSUER_PARTY: Party get() = Party("BankOfCorda", BOC_PUBKEY)
|
||||
val BOC_ISSUER_PARTY_AND_REF = BOC_ISSUER_PARTY.ref(defaultRef)
|
||||
val BOC_ISSUER_PARTY_REF = BOC_ISSUER_PARTY_AND_REF.reference
|
@ -1,7 +1,7 @@
|
||||
package net.corda.bank.plugin
|
||||
|
||||
import net.corda.bank.api.BankOfCordaWebApi
|
||||
import net.corda.bank.flow.IssuerFlow
|
||||
import net.corda.flows.IssuerFlow
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Trader Demo
|
||||
|
||||
This code demonstrates two nodes exchanging cash for a commercial paper.
|
||||
This code demonstrates four nodes, a notary, an issuer of cash (Bank of Corda), and two parties trading with each other, exchanging cash for a commercial paper.
|
||||
|
||||
Please see the either the [online documentation](https://docs.corda.net/running-the-demos.html#trader-demo) for more info on the attachment demo, or the [included offline version](../../docs/build/html/running-the-demos.html#trader-demo).
|
||||
|
@ -50,6 +50,9 @@ dependencies {
|
||||
compile project(':finance')
|
||||
compile project(':test-utils')
|
||||
|
||||
// Corda Plugins: dependent flows and services
|
||||
compile project(':samples:bank-of-corda-demo')
|
||||
|
||||
// Javax is required for webapis
|
||||
compile "org.glassfish.jersey.core:jersey-server:${jersey_version}"
|
||||
|
||||
@ -95,6 +98,15 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||
webPort 10007
|
||||
cordapps = []
|
||||
}
|
||||
node {
|
||||
name "BankOfCorda"
|
||||
dirName "bankofcorda"
|
||||
nearestCity "London"
|
||||
advertisedServices = []
|
||||
artemisPort 10008
|
||||
webPort 10009
|
||||
cordapps = []
|
||||
}
|
||||
}
|
||||
|
||||
task integrationTest(type: Test, dependsOn: []) {
|
||||
|
@ -2,7 +2,10 @@ package net.corda.traderdemo
|
||||
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.flows.IssuerFlow
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.testing.getHostAndPort
|
||||
import org.junit.Test
|
||||
@ -14,6 +17,8 @@ class TraderDemoTest {
|
||||
val nodeA = startNode("Bank A").getOrThrow()
|
||||
val nodeAApiAddr = nodeA.config.getHostAndPort("webAddress")
|
||||
val nodeBApiAddr = startNode("Bank B").getOrThrow().config.getHostAndPort("webAddress")
|
||||
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||
startNode("BankOfCorda", rpcUsers = listOf(user)).getOrThrow()
|
||||
|
||||
assert(TraderDemoClientApi(nodeAApiAddr).runBuyer())
|
||||
assert(TraderDemoClientApi(nodeBApiAddr).runSeller(counterparty = nodeA.nodeInfo.legalIdentity.name))
|
||||
|
@ -1,8 +1,12 @@
|
||||
package net.corda.traderdemo
|
||||
|
||||
import net.corda.flows.IssuerFlow
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.testing.BOC
|
||||
|
||||
/**
|
||||
* This file is exclusively for being able to run your nodes through an IDE (as opposed to running deployNodes)
|
||||
@ -10,9 +14,11 @@ import net.corda.node.services.transactions.SimpleNotaryService
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
driver(dsl = {
|
||||
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
startNode("Bank A")
|
||||
startNode("Bank B")
|
||||
startNode(BOC.name, rpcUsers = listOf(user))
|
||||
waitForAllNodesToFinish()
|
||||
}, isDebug = true)
|
||||
}
|
||||
|
@ -5,11 +5,11 @@ import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.Emoji
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.flows.CashFlowResult
|
||||
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||
import net.corda.testing.BOC
|
||||
import net.corda.traderdemo.flow.SellerFlow
|
||||
import java.util.*
|
||||
import javax.ws.rs.*
|
||||
@ -27,26 +27,22 @@ class TraderDemoApi(val rpc: CordaRPCOps) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Self issue some cash.
|
||||
* TODO: At some point this demo should be extended to have a central bank node.
|
||||
* Uses a central bank node (Bank of Corda) to request issuance of some cash.
|
||||
*/
|
||||
@PUT
|
||||
@Path("create-test-cash")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
fun createTestCash(params: TestCashParams): Response {
|
||||
val notary = rpc.networkMapUpdates().first.first { it.legalIdentity.name == params.notary }
|
||||
val bankOfCordaParty = rpc.partyFromName(BOC.name)
|
||||
?: throw Exception("Unable to locate ${BOC.name} in Network Map Service")
|
||||
val me = rpc.nodeIdentity()
|
||||
val amounts = calculateRandomlySizedAmounts(params.amount.DOLLARS, 3, 10, Random())
|
||||
// TODO: revert back to multiple issue request amounts (3,10) when soft locking implemented
|
||||
val amounts = calculateRandomlySizedAmounts(params.amount.DOLLARS, 1, 1, Random())
|
||||
val handles = amounts.map {
|
||||
rpc.startFlow(::CashFlow, CashCommand.IssueCash(
|
||||
amount = params.amount.DOLLARS,
|
||||
issueRef = OpaqueBytes.of(1),
|
||||
recipient = me.legalIdentity,
|
||||
notary = notary.notaryIdentity
|
||||
))
|
||||
rpc.startFlow(::IssuanceRequester, params.amount.DOLLARS, me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty)
|
||||
}
|
||||
handles.forEach {
|
||||
require(it.returnValue.toBlocking().first() is CashFlowResult.Success)
|
||||
require(it.returnValue.toBlocking().first() is SignedTransaction)
|
||||
}
|
||||
return Response.status(Response.Status.CREATED).build()
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package net.corda.traderdemo.plugin
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.flows.IssuerFlow
|
||||
import net.corda.traderdemo.api.TraderDemoApi
|
||||
import net.corda.traderdemo.flow.BuyerFlow
|
||||
import net.corda.traderdemo.flow.SellerFlow
|
||||
|
@ -10,6 +10,7 @@ import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
@ -67,6 +68,11 @@ val CHARLIE: Party get() = Party("Charlie", CHARLIE_PUBKEY)
|
||||
val MEGA_CORP: Party get() = Party("MegaCorp", MEGA_CORP_PUBKEY)
|
||||
val MINI_CORP: Party get() = Party("MiniCorp", MINI_CORP_PUBKEY)
|
||||
|
||||
val BOC_KEY: KeyPair by lazy { generateKeyPair() }
|
||||
val BOC_PUBKEY: CompositeKey get() = BOC_KEY.public.composite
|
||||
val BOC: Party get() = Party("BankOfCorda", BOC_PUBKEY)
|
||||
val BOC_PARTY_REF = BOC.ref(OpaqueBytes.of(1)).reference
|
||||
|
||||
val ALL_TEST_KEYS: List<KeyPair> get() = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, DUMMY_NOTARY_KEY)
|
||||
|
||||
val MOCK_IDENTITY_SERVICE: MockIdentityService get() = MockIdentityService(listOf(MEGA_CORP, MINI_CORP, DUMMY_NOTARY))
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Node Explorer
|
||||
|
||||
The node explorer provide views of the node's vault and transaction data using Corda's RPC framework.
|
||||
The node explorer provides views of the node's vault and transaction data using Corda's RPC framework.
|
||||
The user can execute cash transaction commands to issue and move cash to other parties on the network or exit cash using the user interface.
|
||||
|
||||
## Running the UI
|
||||
@ -16,6 +16,14 @@ The user can execute cash transaction commands to issue and move cash to other p
|
||||
|
||||
## Running Demo Nodes
|
||||
|
||||
A demonstration Corda network topology is configured with 5 nodes playing the following roles:
|
||||
1. Notary
|
||||
2. Issuer nodes (representing two fictional central banks - UK Bank Plc issuer of GBP and USA Bank Corp issuer of USD)
|
||||
3. Participant nodes (representing two users - Alice and Bob)
|
||||
|
||||
The Issuer nodes have the ability to issue, move and exit cash amounts.
|
||||
The Participant nodes are only able to spend cash (eg. move cash).
|
||||
|
||||
**Windows:**
|
||||
|
||||
gradlew.bat tools:explorer:runDemoNodes
|
||||
@ -29,8 +37,11 @@ The user can execute cash transaction commands to issue and move cash to other p
|
||||
Notary -> 20002
|
||||
Alice -> 20004
|
||||
Bob -> 20006
|
||||
Bank of Corda -> 20008
|
||||
|
||||
UK Bank Plc -> 20008
|
||||
USA Bank Corp -> 20010
|
||||
|
||||
Explorer login credentials to the Issuer nodes are 'manager'/'test'.
|
||||
Explorer login credentials to the Participants nodes are 'user1'/'test'.
|
||||
|
||||
## TODOs:
|
||||
- Shows more useful information in the dashboard.
|
||||
|
@ -81,3 +81,9 @@ task(runDemoNodes, dependsOn: 'classes', type: JavaExec) {
|
||||
main = 'net.corda.explorer.MainKt'
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
}
|
||||
|
||||
task(runSimulationNodes, dependsOn: 'classes', type: JavaExec) {
|
||||
main = 'net.corda.explorer.MainKt'
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
args '-S'
|
||||
}
|
@ -8,9 +8,12 @@ import javafx.scene.control.ButtonType
|
||||
import javafx.scene.image.Image
|
||||
import javafx.stage.Stage
|
||||
import jfxtras.resources.JFXtrasFontRoboto
|
||||
import joptsimple.OptionParser
|
||||
import net.corda.client.mock.EventGenerator
|
||||
import net.corda.client.model.Models
|
||||
import net.corda.client.model.observableValue
|
||||
import net.corda.core.contracts.GBP
|
||||
import net.corda.core.contracts.USD
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.ServiceType
|
||||
@ -19,6 +22,7 @@ import net.corda.explorer.model.SettingsModel
|
||||
import net.corda.explorer.views.*
|
||||
import net.corda.explorer.views.cordapps.cash.CashViewer
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||
import net.corda.node.driver.PortAllocation
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.User
|
||||
@ -100,61 +104,118 @@ class Main : App(MainView::class) {
|
||||
}
|
||||
|
||||
/**
|
||||
* This main method will starts 3 nodes (Notary, Alice and Bob) locally for UI testing, they will be on localhost:20002, 20004, 20006 respectively.
|
||||
* This main method will starts 5 nodes (Notary, Alice, Bob, UK Bank and USA Bank) locally for UI testing, they will be on localhost:20002, 20004, 20006, 20008, 20010 respectively.
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
val portAllocation = PortAllocation.Incremental(20000)
|
||||
driver(portAllocation = portAllocation) {
|
||||
val user = User("user1", "test", permissions = setOf(startFlowPermission<CashFlow>()))
|
||||
val manager = User("manager", "test", permissions = setOf(startFlowPermission<CashFlow>(), startFlowPermission<IssuanceRequester>()))
|
||||
// TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo.
|
||||
val notary = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)), customOverrides = mapOf("nearestCity" to "Zurich"))
|
||||
val alice = startNode("Alice", rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "Paris"))
|
||||
val bob = startNode("Bob", rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "Frankfurt"))
|
||||
val issuer = startNode("Royal Mint", rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "London"))
|
||||
val notary = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)),
|
||||
customOverrides = mapOf("nearestCity" to "Zurich"))
|
||||
val alice = startNode("Alice", rpcUsers = arrayListOf(user),
|
||||
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))),
|
||||
customOverrides = mapOf("nearestCity" to "Milan"))
|
||||
val bob = startNode("Bob", rpcUsers = arrayListOf(user),
|
||||
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))),
|
||||
customOverrides = mapOf("nearestCity" to "Madrid"))
|
||||
val issuerGBP = startNode("UK Bank Plc", rpcUsers = arrayListOf(manager),
|
||||
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.GBP"))),
|
||||
customOverrides = mapOf("nearestCity" to "London"))
|
||||
val issuerUSD = startNode("USA Bank Corp", rpcUsers = arrayListOf(manager),
|
||||
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.USD"))),
|
||||
customOverrides = mapOf("nearestCity" to "New York"))
|
||||
|
||||
val notaryNode = notary.get()
|
||||
val aliceNode = alice.get()
|
||||
val bobNode = bob.get()
|
||||
val issuerNode = issuer.get()
|
||||
val issuerNodeGBP = issuerGBP.get()
|
||||
val issuerNodeUSD = issuerUSD.get()
|
||||
|
||||
arrayOf(notaryNode, aliceNode, bobNode, issuerNode).forEach {
|
||||
arrayOf(notaryNode, aliceNode, bobNode, issuerNodeGBP, issuerNodeUSD).forEach {
|
||||
println("${it.nodeInfo.legalIdentity} started on ${ArtemisMessagingComponent.toHostAndPort(it.nodeInfo.address)}")
|
||||
}
|
||||
// Register with alice to use alice's RPC proxy to create random events.
|
||||
val aliceClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(aliceNode.nodeInfo.address), FullNodeConfiguration(aliceNode.config))
|
||||
aliceClient.start(user.username, user.password)
|
||||
val aliceRPC = aliceClient.proxy()
|
||||
|
||||
val bobClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(bobNode.nodeInfo.address), FullNodeConfiguration(bobNode.config))
|
||||
bobClient.start(user.username, user.password)
|
||||
val bobRPC = bobClient.proxy()
|
||||
val parser = OptionParser("S")
|
||||
val options = parser.parse(*args)
|
||||
if (options.has("S")) {
|
||||
println("Running simulation mode ...")
|
||||
|
||||
val issuerClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(issuerNode.nodeInfo.address), FullNodeConfiguration(issuerNode.config))
|
||||
issuerClient.start(user.username, user.password)
|
||||
val bocRPC = issuerClient.proxy()
|
||||
// Register with alice to use alice's RPC proxy to create random events.
|
||||
val aliceClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(aliceNode.nodeInfo.address), FullNodeConfiguration(aliceNode.config))
|
||||
aliceClient.start(user.username, user.password)
|
||||
val aliceRPC = aliceClient.proxy()
|
||||
|
||||
val eventGenerator = EventGenerator(
|
||||
parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity, issuerNode.nodeInfo.legalIdentity),
|
||||
notary = notaryNode.nodeInfo.notaryIdentity
|
||||
)
|
||||
val bobClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(bobNode.nodeInfo.address), FullNodeConfiguration(bobNode.config))
|
||||
bobClient.start(user.username, user.password)
|
||||
val bobRPC = bobClient.proxy()
|
||||
|
||||
for (i in 0..1000) {
|
||||
Thread.sleep(500)
|
||||
listOf(aliceRPC, bobRPC).forEach {
|
||||
eventGenerator.clientCommandGenerator.map { command ->
|
||||
it.startFlow(::CashFlow, command)
|
||||
val issuerClientGBP = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(issuerNodeGBP.nodeInfo.address), FullNodeConfiguration(issuerNodeGBP.config))
|
||||
issuerClientGBP.start(manager.username, manager.password)
|
||||
val issuerRPCGBP = issuerClientGBP.proxy()
|
||||
|
||||
val issuerClientUSD = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(issuerNodeGBP.nodeInfo.address), FullNodeConfiguration(issuerNodeUSD.config))
|
||||
issuerClientUSD.start(manager.username, manager.password)
|
||||
val issuerRPCUSD = issuerClientUSD.proxy()
|
||||
|
||||
val eventGenerator = EventGenerator(
|
||||
parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity),
|
||||
notary = notaryNode.nodeInfo.notaryIdentity,
|
||||
issuers = listOf(issuerNodeGBP.nodeInfo.legalIdentity,issuerNodeUSD.nodeInfo.legalIdentity)
|
||||
)
|
||||
val issuerGBPEventGenerator = EventGenerator(
|
||||
parties = listOf(issuerNodeGBP.nodeInfo.legalIdentity, aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity),
|
||||
notary = notaryNode.nodeInfo.notaryIdentity,
|
||||
currencies = listOf(GBP)
|
||||
)
|
||||
val issuerUSDEventGenerator = EventGenerator(
|
||||
parties = listOf(issuerNodeUSD.nodeInfo.legalIdentity, aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity),
|
||||
notary = notaryNode.nodeInfo.notaryIdentity,
|
||||
currencies = listOf(USD)
|
||||
)
|
||||
|
||||
for (i in 0..1000) {
|
||||
Thread.sleep(500)
|
||||
// Party pay requests
|
||||
listOf(aliceRPC, bobRPC).forEach {
|
||||
eventGenerator.clientCommandGenerator.map { command ->
|
||||
it.startFlow(::CashFlow, command)
|
||||
Unit
|
||||
}.generate(SplittableRandom())
|
||||
}
|
||||
// Exit requests
|
||||
issuerGBPEventGenerator.bankOfCordaExitGenerator.map { command ->
|
||||
issuerRPCGBP.startFlow(::CashFlow, command)
|
||||
Unit
|
||||
}.generate(SplittableRandom())
|
||||
issuerUSDEventGenerator.bankOfCordaExitGenerator.map { command ->
|
||||
issuerRPCUSD.startFlow(::CashFlow, command)
|
||||
Unit
|
||||
}.generate(SplittableRandom())
|
||||
// Issuer requests
|
||||
issuerGBPEventGenerator.bankOfCordaIssueGenerator.map { command ->
|
||||
issuerRPCGBP.startFlow(::IssuanceRequester,
|
||||
command.amount,
|
||||
command.recipient,
|
||||
command.issueRef,
|
||||
issuerNodeGBP.nodeInfo.legalIdentity)
|
||||
Unit
|
||||
}.generate(SplittableRandom())
|
||||
issuerUSDEventGenerator.bankOfCordaIssueGenerator.map { command ->
|
||||
issuerRPCUSD.startFlow(::IssuanceRequester,
|
||||
command.amount,
|
||||
command.recipient,
|
||||
command.issueRef,
|
||||
issuerNodeUSD.nodeInfo.legalIdentity)
|
||||
Unit
|
||||
}.generate(SplittableRandom())
|
||||
}
|
||||
eventGenerator.bankOfCordaCommandGenerator.map { command ->
|
||||
bocRPC.startFlow(::CashFlow, command)
|
||||
Unit
|
||||
}.generate(SplittableRandom())
|
||||
aliceClient.close()
|
||||
bobClient.close()
|
||||
issuerClientGBP.close()
|
||||
issuerClientUSD.close()
|
||||
}
|
||||
|
||||
aliceClient.close()
|
||||
bobClient.close()
|
||||
issuerClient.close()
|
||||
waitForAllNodesToFinish()
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package net.corda.explorer.model
|
||||
|
||||
import javafx.collections.ObservableList
|
||||
import net.corda.client.fxutils.ChosenList
|
||||
import net.corda.client.fxutils.map
|
||||
import net.corda.client.model.NetworkIdentityModel
|
||||
import net.corda.client.model.observableList
|
||||
import net.corda.client.model.observableValue
|
||||
import net.corda.core.contracts.currency
|
||||
import net.corda.core.node.NodeInfo
|
||||
import tornadofx.observable
|
||||
|
||||
val ISSUER_SERVICE_TYPE = Regex("corda.issuer.(USD|GBP|CHF)")
|
||||
|
||||
class IssuerModel {
|
||||
private val networkIdentities by observableList(NetworkIdentityModel::networkIdentities)
|
||||
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
|
||||
private val supportedCurrencies by observableList(ReportingCurrencyModel::supportedCurrencies)
|
||||
|
||||
val issuers: ObservableList<NodeInfo> = networkIdentities.filtered { it.advertisedServices.any { it.info.type.id.matches(ISSUER_SERVICE_TYPE) } }
|
||||
|
||||
val currencyTypes = ChosenList(myIdentity.map {
|
||||
it?.issuerCurrency()?.let { (listOf(it)).observable() } ?: supportedCurrencies
|
||||
})
|
||||
|
||||
val transactionTypes = ChosenList(myIdentity.map {
|
||||
if (it?.isIssuerNode() ?: false)
|
||||
CashTransaction.values().asList().observable()
|
||||
else
|
||||
listOf(CashTransaction.Pay).observable()
|
||||
})
|
||||
|
||||
private fun NodeInfo.isIssuerNode() = advertisedServices.any { it.info.type.id.matches(ISSUER_SERVICE_TYPE) }
|
||||
|
||||
private fun NodeInfo.issuerCurrency() = if (isIssuerNode()) {
|
||||
val issuer = advertisedServices.first { it.info.type.id.matches(ISSUER_SERVICE_TYPE) }
|
||||
currency(issuer.info.type.id.substringAfterLast("."))
|
||||
} else
|
||||
null
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package net.corda.explorer.plugin
|
||||
|
||||
import net.corda.flows.IssuerFlow
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import java.util.function.Function
|
||||
|
||||
class ExplorerPlugin : CordaPluginRegistry() {
|
||||
// A list of flow that are required for this cordapp
|
||||
override val requiredFlows: Map<String, Set<String>> =
|
||||
mapOf(IssuerFlow.IssuanceRequester::class.java.name to setOf(Amount::class.java.name, Party::class.java.name, OpaqueBytes::class.java.name, Party::class.java.name)
|
||||
)
|
||||
override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service))
|
||||
}
|
@ -11,10 +11,7 @@ import javafx.scene.layout.Priority
|
||||
import javafx.scene.text.TextAlignment
|
||||
import javafx.util.StringConverter
|
||||
import net.corda.client.model.Models
|
||||
import tornadofx.View
|
||||
import tornadofx.gridpane
|
||||
import tornadofx.label
|
||||
import tornadofx.textfield
|
||||
import tornadofx.*
|
||||
|
||||
/**
|
||||
* Helper method to reduce boiler plate code
|
||||
@ -80,7 +77,7 @@ fun EventTarget.copyableLabel(value: ObservableValue<String>? = null, op: (TextF
|
||||
styleClass.add("copyable-label")
|
||||
}
|
||||
|
||||
inline fun <reified M : Any> View.getModel(): M = Models.get(M::class, this.javaClass.kotlin)
|
||||
inline fun <reified M : Any> UIComponent.getModel(): M = Models.get(M::class, this.javaClass.kotlin)
|
||||
|
||||
// Cartesian product of 2 collections.
|
||||
fun <A, B> Collection<A>.cross(other: Collection<B>) = this.flatMap { a -> other.map { b -> a to b } }
|
||||
|
@ -6,26 +6,33 @@ import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.scene.control.*
|
||||
import javafx.stage.Window
|
||||
import net.corda.client.fxutils.ChosenList
|
||||
import net.corda.client.fxutils.isNotNull
|
||||
import net.corda.client.fxutils.map
|
||||
import net.corda.client.fxutils.unique
|
||||
import net.corda.client.model.*
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.contracts.withoutIssuer
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.explorer.model.CashTransaction
|
||||
import net.corda.explorer.model.IssuerModel
|
||||
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.CashFlowResult
|
||||
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||
import org.controlsfx.dialog.ExceptionDialog
|
||||
import tornadofx.Fragment
|
||||
import tornadofx.booleanBinding
|
||||
import tornadofx.observable
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
|
||||
@ -51,11 +58,24 @@ class NewTransaction : Fragment() {
|
||||
private val issueRef = SimpleObjectProperty<Byte>()
|
||||
// Inject data
|
||||
private val parties by observableList(NetworkIdentityModel::parties)
|
||||
private val issuers by observableList(IssuerModel::issuers)
|
||||
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
|
||||
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
|
||||
private val notaries by observableList(NetworkIdentityModel::notaries)
|
||||
private val cash by observableList(ContractStateModel::cash)
|
||||
private val executeButton = ButtonType("Execute", ButtonBar.ButtonData.APPLY)
|
||||
private val currencyTypes by observableList(IssuerModel::currencyTypes)
|
||||
private val supportedCurrencies by observableList(ReportingCurrencyModel::supportedCurrencies)
|
||||
private val transactionTypes by observableList(IssuerModel::transactionTypes)
|
||||
|
||||
private val currencyItems = ChosenList(transactionTypeCB.valueProperty().map {
|
||||
when(it){
|
||||
CashTransaction.Pay -> supportedCurrencies
|
||||
CashTransaction.Issue,
|
||||
CashTransaction.Exit -> currencyTypes
|
||||
else -> FXCollections.emptyObservableList()
|
||||
}
|
||||
})
|
||||
|
||||
fun show(window: Window): Unit {
|
||||
dialog(window).showAndWait().ifPresent {
|
||||
@ -67,14 +87,29 @@ class NewTransaction : Fragment() {
|
||||
}
|
||||
dialog.show()
|
||||
runAsync {
|
||||
rpcProxy.value!!.startFlow(::CashFlow, it).returnValue.toBlocking().first()
|
||||
if (it is CashCommand.IssueCash) {
|
||||
myIdentity.value?.let { myIdentity ->
|
||||
rpcProxy.value!!.startFlow(::IssuanceRequester,
|
||||
it.amount,
|
||||
it.recipient,
|
||||
it.issueRef,
|
||||
myIdentity.legalIdentity).returnValue.toBlocking().first()
|
||||
}
|
||||
}
|
||||
else {
|
||||
rpcProxy.value!!.startFlow(::CashFlow, it).returnValue.toBlocking().first()
|
||||
}
|
||||
}.ui {
|
||||
dialog.contentText = when (it) {
|
||||
is SignedTransaction -> {
|
||||
dialog.alertType = Alert.AlertType.INFORMATION
|
||||
"Cash Issued \nTransaction ID : ${it.id} \nMessage"
|
||||
}
|
||||
is CashFlowResult.Success -> {
|
||||
dialog.alertType = Alert.AlertType.INFORMATION
|
||||
"Transaction Started \nTransaction ID : ${it.transaction?.id} \nMessage : ${it.message}"
|
||||
}
|
||||
is CashFlowResult.Failed -> {
|
||||
else -> {
|
||||
dialog.alertType = Alert.AlertType.ERROR
|
||||
it.toString()
|
||||
}
|
||||
@ -92,15 +127,15 @@ class NewTransaction : Fragment() {
|
||||
dialogPane = root
|
||||
initOwner(window)
|
||||
setResultConverter {
|
||||
val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
|
||||
val defaultRef = OpaqueBytes.of(1)
|
||||
val issueRef = if (issueRef.value != null) OpaqueBytes.of(issueRef.value) else defaultRef
|
||||
when (it) {
|
||||
executeButton -> when (transactionTypeCB.value) {
|
||||
CashTransaction.Issue -> {
|
||||
val issueRef = if (issueRef.value != null) OpaqueBytes(ByteArray(1, { issueRef.value })) else defaultRef
|
||||
CashCommand.IssueCash(Amount(amount.value, currencyChoiceBox.value), issueRef, partyBChoiceBox.value.legalIdentity, notaries.first().notaryIdentity)
|
||||
}
|
||||
CashTransaction.Pay -> CashCommand.PayCash(Amount(amount.value, Issued(PartyAndReference(issuerChoiceBox.value, defaultRef), currencyChoiceBox.value)), partyBChoiceBox.value.legalIdentity)
|
||||
CashTransaction.Exit -> CashCommand.ExitCash(Amount(amount.value, currencyChoiceBox.value), defaultRef)
|
||||
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)
|
||||
else -> null
|
||||
}
|
||||
else -> null
|
||||
@ -115,7 +150,7 @@ class NewTransaction : Fragment() {
|
||||
root.disableProperty().bind(enableProperty.not())
|
||||
|
||||
// Transaction Types Choice Box
|
||||
transactionTypeCB.items = CashTransaction.values().asList().observable()
|
||||
transactionTypeCB.items = transactionTypes
|
||||
|
||||
// Party A textfield always display my identity name, not editable.
|
||||
partyATextField.isEditable = false
|
||||
@ -133,7 +168,7 @@ class NewTransaction : Fragment() {
|
||||
// Issuer
|
||||
issuerLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
issuerChoiceBox.apply {
|
||||
items = cash.map { it.token.issuer.party }.unique().sorted()
|
||||
items = issuers.map { it.legalIdentity }.unique().sorted()
|
||||
converter = stringConverter { it.name }
|
||||
visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Pay })
|
||||
}
|
||||
@ -143,16 +178,16 @@ class NewTransaction : Fragment() {
|
||||
isEditable = false
|
||||
}
|
||||
// Issue Reference
|
||||
issueRefLabel.visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Issue })
|
||||
issueRefLabel.visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Issue || it == CashTransaction.Exit })
|
||||
|
||||
issueRefTextField.apply {
|
||||
textFormatter = byteFormatter().apply { issueRef.bind(this.valueProperty()) }
|
||||
visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Issue })
|
||||
visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Issue || it == CashTransaction.Exit })
|
||||
}
|
||||
// Currency
|
||||
currencyLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
// TODO : Create a currency model to store these values
|
||||
currencyChoiceBox.items = FXCollections.observableList(setOf(USD, GBP, CHF).toList())
|
||||
currencyChoiceBox.items = currencyItems
|
||||
currencyChoiceBox.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
val issuer = Bindings.createObjectBinding({ if (issuerChoiceBox.isVisible) issuerChoiceBox.value else myIdentity.value?.legalIdentity }, arrayOf(myIdentity, issuerChoiceBox.visibleProperty(), issuerChoiceBox.valueProperty()))
|
||||
availableAmount.visibleProperty().bind(
|
||||
|
@ -0,0 +1,2 @@
|
||||
# Register a ServiceLoader service extending from net.corda.node.CordaPluginRegistry
|
||||
net.corda.explorer.plugin.ExplorerPlugin
|
@ -0,0 +1,23 @@
|
||||
package net.corda.explorer.model
|
||||
|
||||
import net.corda.core.contracts.USD
|
||||
import net.corda.core.contracts.currency
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class IssuerModelTest {
|
||||
|
||||
@Test
|
||||
fun `test issuer regex`() {
|
||||
val regex = Regex("corda.issuer.(USD|GBP|CHF)")
|
||||
kotlin.test.assertTrue("corda.issuer.USD".matches(regex))
|
||||
kotlin.test.assertTrue("corda.issuer.GBP".matches(regex))
|
||||
|
||||
kotlin.test.assertFalse("corda.issuer.USD.GBP".matches(regex))
|
||||
kotlin.test.assertFalse("corda.issuer.EUR".matches(regex))
|
||||
kotlin.test.assertFalse("corda.issuer".matches(regex))
|
||||
|
||||
kotlin.test.assertEquals(USD, currency("corda.issuer.USD".substringAfterLast(".")))
|
||||
assertFailsWith(IllegalArgumentException::class, { currency("corda.issuer.DOLLAR".substringBeforeLast(".")) })
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user