mirror of
https://github.com/corda/corda.git
synced 2024-12-20 05:28:21 +00:00
Bank of Corda integration with Trader Demo and Explorer
Initial conversion of Explorer to use IssuerFlow (from BankOfCorda) Initial conversion of TraderDemo to use IssuerFlow (from BankOfCorda) Updated TraderDemo to use IssuerFlow (from BankOfCorda) Fixed TraderDemo integration text (added new BankOfCorda node) Updated Explorer with changes IssuerRequest params Explorer now correctly displaying transaction id upon Issue. Moved IssuerFlow into finance package so can be reused across multiple demos (TraderDemo) and applications (eg Explorer) Refactored BankOfCorda demo to use Finance package and TestUtil constants Updated TraderDemo to use IssuerFlow Updated Explorer to use finance package IssuerFlow. Advertised BankOfCorda as Issuer for usage by Explorer. Explorer no longer depends on BankOfCorda demo since IssuerFlow promoted to Finance module Added IssuerFlow to AbstractNode whitelist. Explicit declarations of IssuerFlow no longer required. Added plugin registration of IssuerFlow at bootstrap. Revert whitelisting of IssuerFlow (plugin configured) Refactored to use constant BOC definition. Added gradle RPC security config. Updated documentation Fixed incorrect references. Renamed Issuer banks. Added new permission set (for Issuer nodes) Added node nearestCity info Added new Issuer Event Generator for Issuer nodes only Associated currency with issuer using ServiceType naming structure. Added argument flag (-S) to trigger event generator simulation node. Fixed problem with issuers not resolving from network map. Updated perms on Issuer rpc proxy nodes. Fixed minor in cash generateExit identified by Explorer. Changes applied in prep for AWG demo. Added IntelliJ run-configurations for launching Explorer demo nodes (with and without simulation) Updated documentation (and added additional gradle task to launch Explorer nodes in simulation mode). Fix following rebase. Addressed review items from PR. Updated TraderDemo readme. Updated TraderDemo gradle file to launch Bank of Corda node. Updated JRE properties. Updated IssuerModel to incorporate correct JFX Observable handling. Fixed bug with Exit command not displaying any currency. Added TODO's for revisiting correct Exception handling strategy. Optimization for when issuing cash to self. Minor updates following PR review. Remove old refs to Royal Mint and Federal Reserve
This commit is contained in:
parent
924fb479e4
commit
eac2cb1cc6
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,20 +46,44 @@ 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``.
|
||||
|
||||
.. 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.
|
||||
Username and password can be configured via the ``rpcUsers`` field in node's configuration file.
|
||||
|
||||
.. image:: resources/explorer/login.png
|
||||
:scale: 50 %
|
||||
@ -57,12 +102,19 @@ Cash
|
||||
|
||||
.. image:: resources/explorer/vault.png
|
||||
|
||||
New cash transaction
|
||||
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)
|
||||
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,13 +75,17 @@ 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
|
||||
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)
|
||||
@ -81,6 +93,9 @@ object IssuerFlow {
|
||||
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))
|
||||
@ -97,6 +112,13 @@ object IssuerFlow {
|
||||
// NOTE: CashFlowResult.Success should always return a signedTransaction
|
||||
throw Exception("Missing CashFlow transaction [${(resultMove)}]")
|
||||
}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
class Service(services: PluginServiceHub) {
|
||||
init {
|
@ -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,26 +104,44 @@ 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)}")
|
||||
}
|
||||
|
||||
val parser = OptionParser("S")
|
||||
val options = parser.parse(*args)
|
||||
if (options.has("S")) {
|
||||
println("Running simulation mode ...")
|
||||
|
||||
// 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)
|
||||
@ -129,32 +151,71 @@ fun main(args: Array<String>) {
|
||||
bobClient.start(user.username, user.password)
|
||||
val bobRPC = bobClient.proxy()
|
||||
|
||||
val issuerClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(issuerNode.nodeInfo.address), FullNodeConfiguration(issuerNode.config))
|
||||
issuerClient.start(user.username, user.password)
|
||||
val bocRPC = issuerClient.proxy()
|
||||
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, issuerNode.nodeInfo.legalIdentity),
|
||||
notary = notaryNode.nodeInfo.notaryIdentity
|
||||
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())
|
||||
}
|
||||
eventGenerator.bankOfCordaCommandGenerator.map { command ->
|
||||
bocRPC.startFlow(::CashFlow, command)
|
||||
// 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())
|
||||
}
|
||||
|
||||
aliceClient.close()
|
||||
bobClient.close()
|
||||
issuerClient.close()
|
||||
issuerClientGBP.close()
|
||||
issuerClientUSD.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 {
|
||||
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