mirror of
https://github.com/corda/corda.git
synced 2025-06-01 15:10:54 +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.serialization.OpaqueBytes
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.flows.CashCommand
|
import net.corda.flows.CashCommand
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned
|
* [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(
|
class EventGenerator(
|
||||||
val parties: List<Party>,
|
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>>()
|
private var vault = listOf<StateAndRef<Cash.State>>()
|
||||||
|
|
||||||
val issuerGenerator =
|
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 currencyGenerator = Generator.pickOne(currencies)
|
||||||
|
|
||||||
val issuedGenerator = issuerGenerator.combine(currencyGenerator) { issuer, currency -> Issued(issuer, currency) }
|
val issuedGenerator = issuerGenerator.combine(currencyGenerator) { issuer, currency -> Issued(issuer, currency) }
|
||||||
@ -93,8 +94,11 @@ class EventGenerator(
|
|||||||
1.0 to moveCashGenerator
|
1.0 to moveCashGenerator
|
||||||
)
|
)
|
||||||
|
|
||||||
val bankOfCordaCommandGenerator = Generator.frequency(
|
val bankOfCordaExitGenerator = Generator.frequency(
|
||||||
0.6 to issueCashGenerator,
|
|
||||||
0.4 to exitCashGenerator
|
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
|
// * IDs can only contain alphanumeric, full stop and underscore ASCII characters
|
||||||
require(id.matches(Regex("[a-z][a-zA-Z0-9._]+")))
|
require(id.matches(Regex("[a-z][a-zA-Z0-9._]+")))
|
||||||
}
|
}
|
||||||
|
private class ServiceTypeImpl(baseId: String, subTypeId: String) : ServiceType("$baseId.$subTypeId")
|
||||||
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 ServiceTypeDirect(id: String) : ServiceType(id)
|
private class ServiceTypeDirect(id: String) : ServiceType(id)
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
Node Explorer
|
Node Explorer
|
||||||
=============
|
=============
|
||||||
|
|
||||||
The node explorer provide views to the node's vault and transaction data using Corda's RPC framework.
|
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 party on the network or exit cash using the user interface.
|
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
|
Running the UI
|
||||||
--------------
|
--------------
|
||||||
@ -17,6 +17,27 @@ Running the UI
|
|||||||
|
|
||||||
Running demo nodes
|
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**::
|
**Windows**::
|
||||||
|
|
||||||
gradlew.bat tools:explorer:runDemoNodes
|
gradlew.bat tools:explorer:runDemoNodes
|
||||||
@ -25,21 +46,45 @@ Running demo nodes
|
|||||||
|
|
||||||
./gradlew tools:explorer:runDemoNodes
|
./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
|
* Notary -> 20002
|
||||||
* Alice -> 20004
|
* Alice -> 20004
|
||||||
* Bob -> 20006
|
* 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
|
Interface
|
||||||
---------
|
---------
|
||||||
Login
|
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.
|
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.
|
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
|
.. image:: resources/explorer/login.png
|
||||||
:scale: 50 %
|
:scale: 50 %
|
||||||
:align: center
|
:align: center
|
||||||
@ -57,12 +102,19 @@ Cash
|
|||||||
|
|
||||||
.. image:: resources/explorer/vault.png
|
.. image:: resources/explorer/vault.png
|
||||||
|
|
||||||
New cash transaction
|
New Transactions
|
||||||
This is where you can create new cash 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.
|
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.
|
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
|
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,
|
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
|
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
|
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 self-issues some cash in order to acquire commercial paper from Bank B, the seller.
|
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:
|
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``
|
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.
|
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 set up the buyer node with some self-issued cash. This step
|
3. Run ``./gradlew samples:trader-demo:runBuyer`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node.
|
||||||
is not expected to print much.
|
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
|
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.
|
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
|
.. note:: to verify the Bank of Corda node is alive and running navigate to the following URL
|
||||||
http://localhost:10005/api/bank/date
|
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
|
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
|
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.
|
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>,
|
deriveState: (TransactionState<S>, Amount<Issued<T>>, CompositeKey) -> TransactionState<S>,
|
||||||
generateMoveCommand: () -> CommandData,
|
generateMoveCommand: () -> CommandData,
|
||||||
generateExitCommand: (Amount<Issued<T>>) -> CommandData): CompositeKey {
|
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 currency = amountIssued.token.product
|
||||||
val amount = Amount(amountIssued.quantity, currency)
|
val amount = Amount(amountIssued.quantity, currency)
|
||||||
var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token }
|
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 co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.ThreadBox
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
@ -34,7 +35,14 @@ object IssuerFlow {
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): SignedTransaction {
|
override fun call(): SignedTransaction {
|
||||||
val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef)
|
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.
|
* Returns the generated transaction representing the transfer of the [Issued] [FungibleAsset] to the issue requester.
|
||||||
*/
|
*/
|
||||||
class Issuer(val otherParty: Party): FlowLogic<SignedTransaction>() {
|
class Issuer(val otherParty: Party): FlowLogic<SignedTransaction>() {
|
||||||
override val progressTracker: ProgressTracker = Issuer.tracker()
|
override val progressTracker: ProgressTracker = tracker()
|
||||||
companion object {
|
companion object {
|
||||||
object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request")
|
object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request")
|
||||||
object ISSUING : ProgressTracker.Step("Self issuing asset")
|
object ISSUING : ProgressTracker.Step("Self issuing asset")
|
||||||
@ -67,35 +75,49 @@ object IssuerFlow {
|
|||||||
return txn
|
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
|
@Suspendable
|
||||||
private fun issueCashTo(amount: Amount<Currency>,
|
private fun issueCashTo(amount: Amount<Currency>,
|
||||||
issueTo: Party, issuerPartyRef: OpaqueBytes): SignedTransaction {
|
issueTo: Party, issuerPartyRef: OpaqueBytes): SignedTransaction {
|
||||||
|
// TODO: pass notary in as request parameter
|
||||||
val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity
|
val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity
|
||||||
// invoke Cash subflow to issue Asset
|
// invoke Cash subflow to issue Asset
|
||||||
progressTracker.currentStep = ISSUING
|
progressTracker.currentStep = ISSUING
|
||||||
val bankOfCordaParty = serviceHub.myInfo.legalIdentity
|
val bankOfCordaParty = serviceHub.myInfo.legalIdentity
|
||||||
val issueCashFlow = CashFlow(CashCommand.IssueCash(amount, issuerPartyRef, bankOfCordaParty, notaryParty))
|
try {
|
||||||
val resultIssue = subFlow(issueCashFlow)
|
val issueCashFlow = CashFlow(CashCommand.IssueCash(amount, issuerPartyRef, bankOfCordaParty, notaryParty))
|
||||||
// NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger)
|
val resultIssue = subFlow(issueCashFlow)
|
||||||
if (resultIssue is CashFlowResult.Failed) {
|
// NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger)
|
||||||
logger.error("Problem issuing cash: ${resultIssue.message}")
|
if (resultIssue is CashFlowResult.Failed) {
|
||||||
throw Exception(resultIssue.message)
|
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
|
// catch and report exception before throwing back to caller flow
|
||||||
progressTracker.currentStep = TRANSFERRING
|
catch (e: Exception) {
|
||||||
val moveCashFlow = CashFlow(CashCommand.PayCash(amount.issuedBy(bankOfCordaParty.ref(issuerPartyRef)), issueTo))
|
logger.error("Issuer Exception: failed for amount ${amount} issuedTo ${issueTo} with issuerPartyRef ${issuerPartyRef}")
|
||||||
val resultMove = subFlow(moveCashFlow)
|
// TODO: awaiting exception handling strategy (what action should be taken here?)
|
||||||
// NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger)
|
throw e
|
||||||
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)}]")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Service(services: PluginServiceHub) {
|
class Service(services: PluginServiceHub) {
|
@ -539,6 +539,25 @@ class CashTests {
|
|||||||
assertFailsWith<InsufficientBalanceException> { makeExit(1000.DOLLARS, MEGA_CORP, 1) }
|
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
|
@Test
|
||||||
fun generateSimpleDirectSpend() {
|
fun generateSimpleDirectSpend() {
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package net.corda.bank.flow
|
package net.corda.flows
|
||||||
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import net.corda.bank.api.BOC_ISSUER_PARTY
|
import net.corda.testing.BOC
|
||||||
import net.corda.bank.api.BOC_KEY
|
import net.corda.testing.BOC_KEY
|
||||||
import net.corda.bank.flow.IssuerFlow.IssuanceRequester
|
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.contracts.DOLLARS
|
import net.corda.core.contracts.DOLLARS
|
||||||
import net.corda.core.contracts.PartyAndReference
|
import net.corda.core.contracts.PartyAndReference
|
||||||
@ -35,11 +35,11 @@ class IssuerFlowTest {
|
|||||||
net = MockNetwork(false, true)
|
net = MockNetwork(false, true)
|
||||||
ledger {
|
ledger {
|
||||||
notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
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)
|
bankClientNode = net.createPartyNode(notaryNode.info.address, MEGA_CORP.name, MEGA_CORP_KEY)
|
||||||
|
|
||||||
// using default IssueTo Party Reference
|
// 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)
|
val (issuer, issuerResult) = runIssuerAndIssueRequester(1000000.DOLLARS, issueToPartyAndRef)
|
||||||
assertEquals(issuerResult.get(), issuer.get().resultFuture.get())
|
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 issueRequest = IssuanceRequester(amount, issueToPartyAndRef.party, issueToPartyAndRef.reference, bankOfCordaNode.info.legalIdentity)
|
||||||
val issueRequestResultFuture = bankClientNode.smm.add(issueRequest).resultFuture
|
val issueRequestResultFuture = bankClientNode.smm.add(issueRequest).resultFuture
|
||||||
|
|
||||||
return RunResult(issuerFuture, issueRequestResultFuture)
|
return IssuerFlowTest.RunResult(issuerFuture, issueRequestResultFuture)
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class RunResult(
|
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.MoreExecutors
|
||||||
import com.google.common.util.concurrent.SettableFuture
|
import com.google.common.util.concurrent.SettableFuture
|
||||||
import net.corda.core.*
|
import net.corda.core.*
|
||||||
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.crypto.X509Utilities
|
import net.corda.core.crypto.X509Utilities
|
||||||
import net.corda.core.flows.FlowLogic
|
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.*
|
||||||
import net.corda.core.node.services.*
|
import net.corda.core.node.services.*
|
||||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
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.SingletonSerializeAsToken
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.flows.CashCommand
|
import net.corda.flows.*
|
||||||
import net.corda.flows.CashFlow
|
|
||||||
import net.corda.flows.FinalityFlow
|
|
||||||
import net.corda.flows.sendRequest
|
|
||||||
import net.corda.node.api.APIServer
|
import net.corda.node.api.APIServer
|
||||||
import net.corda.node.services.api.*
|
import net.corda.node.services.api.*
|
||||||
import net.corda.node.services.config.NodeConfiguration
|
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
|
## Developer notes
|
||||||
|
|
||||||
Testing of the Bank of Corda application is demonstrated at two levels:
|
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
|
2. Integration testing via RPC and HTTP uses the [Driver] DSL to launch standalone node instances
|
||||||
|
|
||||||
Security
|
Security
|
||||||
@ -48,9 +49,9 @@ which are validated on the Bank of Corda node against those configured at node s
|
|||||||
Notary
|
Notary
|
||||||
We are using a [SimpleNotaryService] in this example, but could easily switch to a [ValidatingNotaryService]
|
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
|
## Further Reading
|
||||||
|
|
||||||
|
@ -78,10 +78,11 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
|||||||
artemisPort 10004
|
artemisPort 10004
|
||||||
webPort 10005
|
webPort 10005
|
||||||
cordapps = []
|
cordapps = []
|
||||||
// TODO: task needs to parse this item when generating node.conf
|
rpcUsers = [
|
||||||
// rpcUsers : [
|
['user' : "user1",
|
||||||
// { user=user1, password=test, permissions=[ StartFlow.net.corda.bank.flow.IssuerFlow$IssuanceRequester ] }
|
'password' : "test",
|
||||||
// ]
|
'permissions' : ["StartFlow.net.corda.flows.CashFlow"]]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
node {
|
node {
|
||||||
name "BigCorporation"
|
name "BigCorporation"
|
||||||
|
@ -1,21 +1,17 @@
|
|||||||
package net.corda.bank
|
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.contracts.DOLLARS
|
||||||
import net.corda.core.messaging.startFlow
|
import net.corda.core.messaging.startFlow
|
||||||
import net.corda.core.node.services.ServiceInfo
|
import net.corda.core.node.services.ServiceInfo
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||||
import net.corda.node.driver.driver
|
import net.corda.node.driver.driver
|
||||||
import net.corda.node.services.User
|
import net.corda.node.services.User
|
||||||
import net.corda.node.services.config.configureTestSSL
|
import net.corda.node.services.config.configureTestSSL
|
||||||
import net.corda.node.services.messaging.CordaRPCClient
|
import net.corda.node.services.messaging.CordaRPCClient
|
||||||
import net.corda.node.services.startFlowPermission
|
import net.corda.node.services.startFlowPermission
|
||||||
import net.corda.node.services.transactions.SimpleNotaryService
|
import net.corda.node.services.transactions.SimpleNotaryService
|
||||||
import net.corda.testing.expect
|
import net.corda.testing.*
|
||||||
import net.corda.testing.expectEvents
|
|
||||||
import net.corda.testing.getHostAndPort
|
|
||||||
import net.corda.testing.sequence
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
@ -47,7 +43,7 @@ class BankOfCordaRPCClientTest {
|
|||||||
val vaultUpdatesBigCorp = bigCorpProxy.vaultAndUpdates().second
|
val vaultUpdatesBigCorp = bigCorpProxy.vaultAndUpdates().second
|
||||||
|
|
||||||
// Kick-off actual Issuer Flow
|
// 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 }
|
assertTrue { result is SignedTransaction }
|
||||||
|
|
||||||
// Check Bank of Corda Vault Updates
|
// Check Bank of Corda Vault Updates
|
||||||
|
@ -4,9 +4,11 @@ import com.google.common.net.HostAndPort
|
|||||||
import joptsimple.OptionParser
|
import joptsimple.OptionParser
|
||||||
import net.corda.bank.api.BankOfCordaClientApi
|
import net.corda.bank.api.BankOfCordaClientApi
|
||||||
import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams
|
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.ServiceInfo
|
||||||
|
import net.corda.core.node.services.ServiceType
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import net.corda.flows.CashFlow
|
||||||
import net.corda.node.driver.driver
|
import net.corda.node.driver.driver
|
||||||
import net.corda.node.services.User
|
import net.corda.node.services.User
|
||||||
import net.corda.node.services.startFlowPermission
|
import net.corda.node.services.startFlowPermission
|
||||||
@ -46,10 +48,10 @@ private class BankOfCordaDriver {
|
|||||||
val role = options.valueOf(roleArg)!!
|
val role = options.valueOf(roleArg)!!
|
||||||
if (role == Role.ISSUER) {
|
if (role == Role.ISSUER) {
|
||||||
driver(dsl = {
|
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("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||||
startNode("BankOfCorda", rpcUsers = listOf(user))
|
startNode("BankOfCorda", rpcUsers = listOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer"))))
|
||||||
startNode("BigCorporation")
|
startNode("BigCorporation", rpcUsers = listOf(user))
|
||||||
waitForAllNodesToFinish()
|
waitForAllNodesToFinish()
|
||||||
}, isDebug = true)
|
}, isDebug = true)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ package net.corda.bank.api
|
|||||||
|
|
||||||
import com.google.common.net.HostAndPort
|
import com.google.common.net.HostAndPort
|
||||||
import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams
|
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.node.services.messaging.CordaRPCClient
|
||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.contracts.currency
|
import net.corda.core.contracts.currency
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package net.corda.bank.api
|
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.Amount
|
||||||
import net.corda.core.contracts.currency
|
import net.corda.core.contracts.currency
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
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
|
package net.corda.bank.plugin
|
||||||
|
|
||||||
import net.corda.bank.api.BankOfCordaWebApi
|
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.contracts.Amount
|
||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.node.CordaPluginRegistry
|
import net.corda.core.node.CordaPluginRegistry
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Trader Demo
|
# 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).
|
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(':finance')
|
||||||
compile project(':test-utils')
|
compile project(':test-utils')
|
||||||
|
|
||||||
|
// Corda Plugins: dependent flows and services
|
||||||
|
compile project(':samples:bank-of-corda-demo')
|
||||||
|
|
||||||
// Javax is required for webapis
|
// Javax is required for webapis
|
||||||
compile "org.glassfish.jersey.core:jersey-server:${jersey_version}"
|
compile "org.glassfish.jersey.core:jersey-server:${jersey_version}"
|
||||||
|
|
||||||
@ -95,6 +98,15 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
|||||||
webPort 10007
|
webPort 10007
|
||||||
cordapps = []
|
cordapps = []
|
||||||
}
|
}
|
||||||
|
node {
|
||||||
|
name "BankOfCorda"
|
||||||
|
dirName "bankofcorda"
|
||||||
|
nearestCity "London"
|
||||||
|
advertisedServices = []
|
||||||
|
artemisPort 10008
|
||||||
|
webPort 10009
|
||||||
|
cordapps = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task integrationTest(type: Test, dependsOn: []) {
|
task integrationTest(type: Test, dependsOn: []) {
|
||||||
|
@ -2,7 +2,10 @@ package net.corda.traderdemo
|
|||||||
|
|
||||||
import net.corda.core.getOrThrow
|
import net.corda.core.getOrThrow
|
||||||
import net.corda.core.node.services.ServiceInfo
|
import net.corda.core.node.services.ServiceInfo
|
||||||
|
import net.corda.flows.IssuerFlow
|
||||||
import net.corda.node.driver.driver
|
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.node.services.transactions.SimpleNotaryService
|
||||||
import net.corda.testing.getHostAndPort
|
import net.corda.testing.getHostAndPort
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -14,6 +17,8 @@ class TraderDemoTest {
|
|||||||
val nodeA = startNode("Bank A").getOrThrow()
|
val nodeA = startNode("Bank A").getOrThrow()
|
||||||
val nodeAApiAddr = nodeA.config.getHostAndPort("webAddress")
|
val nodeAApiAddr = nodeA.config.getHostAndPort("webAddress")
|
||||||
val nodeBApiAddr = startNode("Bank B").getOrThrow().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(nodeAApiAddr).runBuyer())
|
||||||
assert(TraderDemoClientApi(nodeBApiAddr).runSeller(counterparty = nodeA.nodeInfo.legalIdentity.name))
|
assert(TraderDemoClientApi(nodeBApiAddr).runSeller(counterparty = nodeA.nodeInfo.legalIdentity.name))
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
package net.corda.traderdemo
|
package net.corda.traderdemo
|
||||||
|
|
||||||
|
import net.corda.flows.IssuerFlow
|
||||||
import net.corda.core.node.services.ServiceInfo
|
import net.corda.core.node.services.ServiceInfo
|
||||||
import net.corda.node.driver.driver
|
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.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)
|
* 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>) {
|
fun main(args: Array<String>) {
|
||||||
driver(dsl = {
|
driver(dsl = {
|
||||||
|
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||||
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
|
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||||
startNode("Bank A")
|
startNode("Bank A")
|
||||||
startNode("Bank B")
|
startNode("Bank B")
|
||||||
|
startNode(BOC.name, rpcUsers = listOf(user))
|
||||||
waitForAllNodesToFinish()
|
waitForAllNodesToFinish()
|
||||||
}, isDebug = true)
|
}, isDebug = true)
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,11 @@ import net.corda.core.contracts.DOLLARS
|
|||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
import net.corda.core.messaging.startFlow
|
import net.corda.core.messaging.startFlow
|
||||||
import net.corda.core.serialization.OpaqueBytes
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.Emoji
|
import net.corda.core.utilities.Emoji
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import net.corda.flows.CashCommand
|
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||||
import net.corda.flows.CashFlow
|
import net.corda.testing.BOC
|
||||||
import net.corda.flows.CashFlowResult
|
|
||||||
import net.corda.traderdemo.flow.SellerFlow
|
import net.corda.traderdemo.flow.SellerFlow
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.ws.rs.*
|
import javax.ws.rs.*
|
||||||
@ -27,26 +27,22 @@ class TraderDemoApi(val rpc: CordaRPCOps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Self issue some cash.
|
* Uses a central bank node (Bank of Corda) to request issuance of some cash.
|
||||||
* TODO: At some point this demo should be extended to have a central bank node.
|
|
||||||
*/
|
*/
|
||||||
@PUT
|
@PUT
|
||||||
@Path("create-test-cash")
|
@Path("create-test-cash")
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
fun createTestCash(params: TestCashParams): Response {
|
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 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 {
|
val handles = amounts.map {
|
||||||
rpc.startFlow(::CashFlow, CashCommand.IssueCash(
|
rpc.startFlow(::IssuanceRequester, params.amount.DOLLARS, me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty)
|
||||||
amount = params.amount.DOLLARS,
|
|
||||||
issueRef = OpaqueBytes.of(1),
|
|
||||||
recipient = me.legalIdentity,
|
|
||||||
notary = notary.notaryIdentity
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
handles.forEach {
|
handles.forEach {
|
||||||
require(it.returnValue.toBlocking().first() is CashFlowResult.Success)
|
require(it.returnValue.toBlocking().first() is SignedTransaction)
|
||||||
}
|
}
|
||||||
return Response.status(Response.Status.CREATED).build()
|
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.contracts.Amount
|
||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.node.CordaPluginRegistry
|
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.api.TraderDemoApi
|
||||||
import net.corda.traderdemo.flow.BuyerFlow
|
import net.corda.traderdemo.flow.BuyerFlow
|
||||||
import net.corda.traderdemo.flow.SellerFlow
|
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.crypto.*
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.utilities.DUMMY_NOTARY
|
import net.corda.core.utilities.DUMMY_NOTARY
|
||||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
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 MEGA_CORP: Party get() = Party("MegaCorp", MEGA_CORP_PUBKEY)
|
||||||
val MINI_CORP: Party get() = Party("MiniCorp", MINI_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 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))
|
val MOCK_IDENTITY_SERVICE: MockIdentityService get() = MockIdentityService(listOf(MEGA_CORP, MINI_CORP, DUMMY_NOTARY))
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Node Explorer
|
# 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.
|
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
|
## 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
|
## 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:**
|
**Windows:**
|
||||||
|
|
||||||
gradlew.bat tools:explorer:runDemoNodes
|
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
|
Notary -> 20002
|
||||||
Alice -> 20004
|
Alice -> 20004
|
||||||
Bob -> 20006
|
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:
|
## TODOs:
|
||||||
- Shows more useful information in the dashboard.
|
- Shows more useful information in the dashboard.
|
||||||
|
@ -81,3 +81,9 @@ task(runDemoNodes, dependsOn: 'classes', type: JavaExec) {
|
|||||||
main = 'net.corda.explorer.MainKt'
|
main = 'net.corda.explorer.MainKt'
|
||||||
classpath = sourceSets.main.runtimeClasspath
|
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.scene.image.Image
|
||||||
import javafx.stage.Stage
|
import javafx.stage.Stage
|
||||||
import jfxtras.resources.JFXtrasFontRoboto
|
import jfxtras.resources.JFXtrasFontRoboto
|
||||||
|
import joptsimple.OptionParser
|
||||||
import net.corda.client.mock.EventGenerator
|
import net.corda.client.mock.EventGenerator
|
||||||
import net.corda.client.model.Models
|
import net.corda.client.model.Models
|
||||||
import net.corda.client.model.observableValue
|
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.messaging.startFlow
|
||||||
import net.corda.core.node.services.ServiceInfo
|
import net.corda.core.node.services.ServiceInfo
|
||||||
import net.corda.core.node.services.ServiceType
|
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.*
|
||||||
import net.corda.explorer.views.cordapps.cash.CashViewer
|
import net.corda.explorer.views.cordapps.cash.CashViewer
|
||||||
import net.corda.flows.CashFlow
|
import net.corda.flows.CashFlow
|
||||||
|
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||||
import net.corda.node.driver.PortAllocation
|
import net.corda.node.driver.PortAllocation
|
||||||
import net.corda.node.driver.driver
|
import net.corda.node.driver.driver
|
||||||
import net.corda.node.services.User
|
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>) {
|
fun main(args: Array<String>) {
|
||||||
val portAllocation = PortAllocation.Incremental(20000)
|
val portAllocation = PortAllocation.Incremental(20000)
|
||||||
driver(portAllocation = portAllocation) {
|
driver(portAllocation = portAllocation) {
|
||||||
val user = User("user1", "test", permissions = setOf(startFlowPermission<CashFlow>()))
|
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.
|
// 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 notary = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)),
|
||||||
val alice = startNode("Alice", rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "Paris"))
|
customOverrides = mapOf("nearestCity" to "Zurich"))
|
||||||
val bob = startNode("Bob", rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "Frankfurt"))
|
val alice = startNode("Alice", rpcUsers = arrayListOf(user),
|
||||||
val issuer = startNode("Royal Mint", rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "London"))
|
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 notaryNode = notary.get()
|
||||||
val aliceNode = alice.get()
|
val aliceNode = alice.get()
|
||||||
val bobNode = bob.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)}")
|
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))
|
val parser = OptionParser("S")
|
||||||
bobClient.start(user.username, user.password)
|
val options = parser.parse(*args)
|
||||||
val bobRPC = bobClient.proxy()
|
if (options.has("S")) {
|
||||||
|
println("Running simulation mode ...")
|
||||||
|
|
||||||
val issuerClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(issuerNode.nodeInfo.address), FullNodeConfiguration(issuerNode.config))
|
// Register with alice to use alice's RPC proxy to create random events.
|
||||||
issuerClient.start(user.username, user.password)
|
val aliceClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(aliceNode.nodeInfo.address), FullNodeConfiguration(aliceNode.config))
|
||||||
val bocRPC = issuerClient.proxy()
|
aliceClient.start(user.username, user.password)
|
||||||
|
val aliceRPC = aliceClient.proxy()
|
||||||
|
|
||||||
val eventGenerator = EventGenerator(
|
val bobClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(bobNode.nodeInfo.address), FullNodeConfiguration(bobNode.config))
|
||||||
parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity, issuerNode.nodeInfo.legalIdentity),
|
bobClient.start(user.username, user.password)
|
||||||
notary = notaryNode.nodeInfo.notaryIdentity
|
val bobRPC = bobClient.proxy()
|
||||||
)
|
|
||||||
|
|
||||||
for (i in 0..1000) {
|
val issuerClientGBP = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(issuerNodeGBP.nodeInfo.address), FullNodeConfiguration(issuerNodeGBP.config))
|
||||||
Thread.sleep(500)
|
issuerClientGBP.start(manager.username, manager.password)
|
||||||
listOf(aliceRPC, bobRPC).forEach {
|
val issuerRPCGBP = issuerClientGBP.proxy()
|
||||||
eventGenerator.clientCommandGenerator.map { command ->
|
|
||||||
it.startFlow(::CashFlow, command)
|
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
|
Unit
|
||||||
}.generate(SplittableRandom())
|
}.generate(SplittableRandom())
|
||||||
}
|
}
|
||||||
eventGenerator.bankOfCordaCommandGenerator.map { command ->
|
aliceClient.close()
|
||||||
bocRPC.startFlow(::CashFlow, command)
|
bobClient.close()
|
||||||
Unit
|
issuerClientGBP.close()
|
||||||
}.generate(SplittableRandom())
|
issuerClientUSD.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
aliceClient.close()
|
|
||||||
bobClient.close()
|
|
||||||
issuerClient.close()
|
|
||||||
waitForAllNodesToFinish()
|
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.scene.text.TextAlignment
|
||||||
import javafx.util.StringConverter
|
import javafx.util.StringConverter
|
||||||
import net.corda.client.model.Models
|
import net.corda.client.model.Models
|
||||||
import tornadofx.View
|
import tornadofx.*
|
||||||
import tornadofx.gridpane
|
|
||||||
import tornadofx.label
|
|
||||||
import tornadofx.textfield
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to reduce boiler plate code
|
* Helper method to reduce boiler plate code
|
||||||
@ -80,7 +77,7 @@ fun EventTarget.copyableLabel(value: ObservableValue<String>? = null, op: (TextF
|
|||||||
styleClass.add("copyable-label")
|
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.
|
// Cartesian product of 2 collections.
|
||||||
fun <A, B> Collection<A>.cross(other: Collection<B>) = this.flatMap { a -> other.map { b -> a to b } }
|
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.collections.FXCollections
|
||||||
import javafx.scene.control.*
|
import javafx.scene.control.*
|
||||||
import javafx.stage.Window
|
import javafx.stage.Window
|
||||||
|
import net.corda.client.fxutils.ChosenList
|
||||||
import net.corda.client.fxutils.isNotNull
|
import net.corda.client.fxutils.isNotNull
|
||||||
import net.corda.client.fxutils.map
|
import net.corda.client.fxutils.map
|
||||||
import net.corda.client.fxutils.unique
|
import net.corda.client.fxutils.unique
|
||||||
import net.corda.client.model.*
|
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.crypto.Party
|
||||||
import net.corda.core.messaging.startFlow
|
import net.corda.core.messaging.startFlow
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.serialization.OpaqueBytes
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.explorer.model.CashTransaction
|
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.bigDecimalFormatter
|
||||||
import net.corda.explorer.views.byteFormatter
|
import net.corda.explorer.views.byteFormatter
|
||||||
import net.corda.explorer.views.stringConverter
|
import net.corda.explorer.views.stringConverter
|
||||||
import net.corda.flows.CashCommand
|
import net.corda.flows.CashCommand
|
||||||
import net.corda.flows.CashFlow
|
import net.corda.flows.CashFlow
|
||||||
import net.corda.flows.CashFlowResult
|
import net.corda.flows.CashFlowResult
|
||||||
|
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||||
import org.controlsfx.dialog.ExceptionDialog
|
import org.controlsfx.dialog.ExceptionDialog
|
||||||
import tornadofx.Fragment
|
import tornadofx.Fragment
|
||||||
import tornadofx.booleanBinding
|
import tornadofx.booleanBinding
|
||||||
import tornadofx.observable
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -51,11 +58,24 @@ class NewTransaction : Fragment() {
|
|||||||
private val issueRef = SimpleObjectProperty<Byte>()
|
private val issueRef = SimpleObjectProperty<Byte>()
|
||||||
// Inject data
|
// Inject data
|
||||||
private val parties by observableList(NetworkIdentityModel::parties)
|
private val parties by observableList(NetworkIdentityModel::parties)
|
||||||
|
private val issuers by observableList(IssuerModel::issuers)
|
||||||
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
|
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
|
||||||
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
|
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
|
||||||
private val notaries by observableList(NetworkIdentityModel::notaries)
|
private val notaries by observableList(NetworkIdentityModel::notaries)
|
||||||
private val cash by observableList(ContractStateModel::cash)
|
private val cash by observableList(ContractStateModel::cash)
|
||||||
private val executeButton = ButtonType("Execute", ButtonBar.ButtonData.APPLY)
|
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 {
|
fun show(window: Window): Unit {
|
||||||
dialog(window).showAndWait().ifPresent {
|
dialog(window).showAndWait().ifPresent {
|
||||||
@ -67,14 +87,29 @@ class NewTransaction : Fragment() {
|
|||||||
}
|
}
|
||||||
dialog.show()
|
dialog.show()
|
||||||
runAsync {
|
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 {
|
}.ui {
|
||||||
dialog.contentText = when (it) {
|
dialog.contentText = when (it) {
|
||||||
|
is SignedTransaction -> {
|
||||||
|
dialog.alertType = Alert.AlertType.INFORMATION
|
||||||
|
"Cash Issued \nTransaction ID : ${it.id} \nMessage"
|
||||||
|
}
|
||||||
is CashFlowResult.Success -> {
|
is CashFlowResult.Success -> {
|
||||||
dialog.alertType = Alert.AlertType.INFORMATION
|
dialog.alertType = Alert.AlertType.INFORMATION
|
||||||
"Transaction Started \nTransaction ID : ${it.transaction?.id} \nMessage : ${it.message}"
|
"Transaction Started \nTransaction ID : ${it.transaction?.id} \nMessage : ${it.message}"
|
||||||
}
|
}
|
||||||
is CashFlowResult.Failed -> {
|
else -> {
|
||||||
dialog.alertType = Alert.AlertType.ERROR
|
dialog.alertType = Alert.AlertType.ERROR
|
||||||
it.toString()
|
it.toString()
|
||||||
}
|
}
|
||||||
@ -92,15 +127,15 @@ class NewTransaction : Fragment() {
|
|||||||
dialogPane = root
|
dialogPane = root
|
||||||
initOwner(window)
|
initOwner(window)
|
||||||
setResultConverter {
|
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) {
|
when (it) {
|
||||||
executeButton -> when (transactionTypeCB.value) {
|
executeButton -> when (transactionTypeCB.value) {
|
||||||
CashTransaction.Issue -> {
|
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)
|
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.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), defaultRef)
|
CashTransaction.Exit -> CashCommand.ExitCash(Amount(amount.value, currencyChoiceBox.value), issueRef)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
@ -115,7 +150,7 @@ class NewTransaction : Fragment() {
|
|||||||
root.disableProperty().bind(enableProperty.not())
|
root.disableProperty().bind(enableProperty.not())
|
||||||
|
|
||||||
// Transaction Types Choice Box
|
// Transaction Types Choice Box
|
||||||
transactionTypeCB.items = CashTransaction.values().asList().observable()
|
transactionTypeCB.items = transactionTypes
|
||||||
|
|
||||||
// Party A textfield always display my identity name, not editable.
|
// Party A textfield always display my identity name, not editable.
|
||||||
partyATextField.isEditable = false
|
partyATextField.isEditable = false
|
||||||
@ -133,7 +168,7 @@ class NewTransaction : Fragment() {
|
|||||||
// Issuer
|
// Issuer
|
||||||
issuerLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
issuerLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||||
issuerChoiceBox.apply {
|
issuerChoiceBox.apply {
|
||||||
items = cash.map { it.token.issuer.party }.unique().sorted()
|
items = issuers.map { it.legalIdentity }.unique().sorted()
|
||||||
converter = stringConverter { it.name }
|
converter = stringConverter { it.name }
|
||||||
visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Pay })
|
visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Pay })
|
||||||
}
|
}
|
||||||
@ -143,16 +178,16 @@ class NewTransaction : Fragment() {
|
|||||||
isEditable = false
|
isEditable = false
|
||||||
}
|
}
|
||||||
// Issue Reference
|
// 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 {
|
issueRefTextField.apply {
|
||||||
textFormatter = byteFormatter().apply { issueRef.bind(this.valueProperty()) }
|
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
|
// Currency
|
||||||
currencyLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
currencyLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||||
// TODO : Create a currency model to store these values
|
// 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)
|
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()))
|
val issuer = Bindings.createObjectBinding({ if (issuerChoiceBox.isVisible) issuerChoiceBox.value else myIdentity.value?.legalIdentity }, arrayOf(myIdentity, issuerChoiceBox.visibleProperty(), issuerChoiceBox.valueProperty()))
|
||||||
availableAmount.visibleProperty().bind(
|
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…
x
Reference in New Issue
Block a user