Merge pull request #49 from corda/boc-demo-integration

Bank of Corda integration with Trader Demo and Explorer
This commit is contained in:
josecoll 2016-12-22 16:34:55 +00:00 committed by GitHub
commit 1bcabc8d41
39 changed files with 497 additions and 175 deletions

View 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>

View 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>

View File

@ -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
)
}

View File

@ -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)

View File

@ -1,8 +1,8 @@
Node Explorer
=============
The node explorer provide views to the node's vault and transaction data using Corda's RPC framework.
The user can execute cash transaction commands to issue and move cash to other party on the network or exit cash using the user interface.
The node explorer provides views into a node's vault and transaction data using Corda's RPC framework.
The user can execute cash transaction commands to issue and move cash to other parties on the network or exit cash (eg. remove from the ledger)
Running the UI
--------------
@ -17,6 +17,27 @@ Running the UI
Running demo nodes
------------------
A demonstration Corda network topology is configured with 5 nodes playing the following roles:
1. Notary
2. Issuer nodes, representing two fictional central banks (UK Bank Plc issuer of GBP and USA Bank Corp issuer of USD)
3. Participant nodes, representing two users (Alice and Bob)
When connected to an *Issuer* node, a user can execute cash transaction commands to issue and move cash to itself or other
parties on the network or to exit cash (for itself only).
When connected to a *Participant* node a user can only execute cash transaction commands to move cash to other parties on the network.
The Demo Nodes can be started in one of two modes:
1. Normal
Fresh clean environment empty of transactions.
Firstly, launch an Explorer instance to login to one of the Issuer nodes and issue some cash to the other participants (Bob and Alice).
Then launch another Explorer instance to login to a participant node and start making payments (eg. move cash).
You will only be able to exit (eg. redeem from the ledger) cash as an issuer node.
**Windows**::
gradlew.bat tools:explorer:runDemoNodes
@ -25,21 +46,45 @@ Running demo nodes
./gradlew tools:explorer:runDemoNodes
.. note:: 3 Corda nodes will be created on the following port on localhost by default.
2. Simulation
In this mode Nodes will automatically commence executing commands as part of a random generation process.
Issuer nodes will randomly issue, move and exit cash.
Participant nodes will randomly generate spends (eg. move cash to other nodes, including issuers)
**Windows**::
gradlew.bat tools:explorer:runSimulationNodes
**Other**::
./gradlew tools:explorer:runSimulationNodes
.. note:: 5 Corda nodes will be created on the following port on localhost by default.
* Notary -> 20002
* Alice -> 20004
* Bob -> 20006
* UK Bank Plc -> 20008 (*Issuer node*)
* USA Bank Corp -> 20010 (*Issuer node*)
Explorer login credentials to the Issuer nodes are defaulted to ``manager`` and ``test``.
Explorer login credentials to the Participants nodes are defaulted to ``user1`` and ``test``.
Please note you are not allowed to connect to the notary.
.. note:: Alternatively, you may start the demo nodes from within IntelliJ using either of the run configurations
``Explorer - demo nodes`` or ``Explorer - demo nodes (simulation)``
.. note:: Use the Explorer in conjunction with the Trader Demo and Bank of Corda samples to use other *Issuer* nodes.
Interface
---------
Login
User can login to any Corda node using the explorer. Alternatively, ``gradlew explorer:runDemoNodes`` can be used to start up demo nodes for testing.
Corda node address, username and password are required for login, the address is defaulted to localhost:0 if leave blank.
Username and password can be configured via the ``rpcUsers`` field in node's configuration file; for demo nodes, it is defaulted to ``user1`` and ``test``.
Username and password can be configured via the ``rpcUsers`` field in node's configuration file.
.. note:: If you are connecting to the demo nodes, only Alice and Bob (20004, 20006) are accessible using user1 credential, you won't be able to connect to the notary.
.. image:: resources/explorer/login.png
:scale: 50 %
:align: center
@ -57,12 +102,19 @@ Cash
.. image:: resources/explorer/vault.png
New cash transaction
This is where you can create new cash transactions.
The user can choose from three transaction types (issue, pay and exit) and any party visible on the network.
New Transactions
This is where you can create new cash transactions.
The user can choose from three transaction types (issue, pay and exit) and any party visible on the network.
General nodes can only execute pay commands to any other party on the network.
.. image:: resources/explorer/newTransactionCash.png
Issuer Nodes
Issuer nodes can execute issue (to itself or to any other party), pay and exit transactions.
The result of the transaction will be visible in the transaction screen when executed.
.. image:: resources/explorer/newTransaction.png
.. image:: resources/explorer/newTransactionIssuer.png
Transactions
The transaction view contains all transactions handled by the node in a table view. It shows basic information on the table e.g. Transaction ID,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 KiB

After

Width:  |  Height:  |  Size: 571 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

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

View File

@ -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.

View File

@ -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 }

View File

@ -1,6 +1,7 @@
package net.corda.bank.flow
package net.corda.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.ThreadBox
import net.corda.core.contracts.*
import net.corda.core.crypto.Party
import net.corda.core.crypto.SecureHash
@ -34,7 +35,14 @@ object IssuerFlow {
@Suspendable
override fun call(): SignedTransaction {
val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef)
return sendAndReceive<SignedTransaction>(issuerBankParty, issueRequest).unwrap { it }
try {
return sendAndReceive<SignedTransaction>(issuerBankParty, issueRequest).unwrap { it }
// catch and report exception before throwing back to caller
} catch (e: Exception) {
logger.error("IssuanceRequesterException: request failed: [${issueRequest}]")
// TODO: awaiting exception handling strategy (what action should be taken here?)
throw e
}
}
}
@ -43,7 +51,7 @@ object IssuerFlow {
* Returns the generated transaction representing the transfer of the [Issued] [FungibleAsset] to the issue requester.
*/
class Issuer(val otherParty: Party): FlowLogic<SignedTransaction>() {
override val progressTracker: ProgressTracker = Issuer.tracker()
override val progressTracker: ProgressTracker = tracker()
companion object {
object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request")
object ISSUING : ProgressTracker.Step("Self issuing asset")
@ -67,35 +75,49 @@ object IssuerFlow {
return txn
}
// TODO: resolve race conditions caused by the 2 separate Cashflow commands (Issue and Pay) not reusing the same
// state references (thus causing Notarisation double spend exceptions).
@Suspendable
private fun issueCashTo(amount: Amount<Currency>,
issueTo: Party, issuerPartyRef: OpaqueBytes): SignedTransaction {
// TODO: pass notary in as request parameter
val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity
// invoke Cash subflow to issue Asset
progressTracker.currentStep = ISSUING
val bankOfCordaParty = serviceHub.myInfo.legalIdentity
val issueCashFlow = CashFlow(CashCommand.IssueCash(amount, issuerPartyRef, bankOfCordaParty, notaryParty))
val resultIssue = subFlow(issueCashFlow)
// NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger)
if (resultIssue is CashFlowResult.Failed) {
logger.error("Problem issuing cash: ${resultIssue.message}")
throw Exception(resultIssue.message)
try {
val issueCashFlow = CashFlow(CashCommand.IssueCash(amount, issuerPartyRef, bankOfCordaParty, notaryParty))
val resultIssue = subFlow(issueCashFlow)
// NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger)
if (resultIssue is CashFlowResult.Failed) {
logger.error("Problem issuing cash: ${resultIssue.message}")
throw Exception(resultIssue.message)
}
// short-circuit when issuing to self
if (issueTo.equals(serviceHub.myInfo.legalIdentity))
return (resultIssue as CashFlowResult.Success).transaction!!
// now invoke Cash subflow to Move issued assetType to issue requester
progressTracker.currentStep = TRANSFERRING
val moveCashFlow = CashFlow(CashCommand.PayCash(amount.issuedBy(bankOfCordaParty.ref(issuerPartyRef)), issueTo))
val resultMove = subFlow(moveCashFlow)
// NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger)
if (resultMove is CashFlowResult.Failed) {
logger.error("Problem transferring cash: ${resultMove.message}")
throw Exception(resultMove.message)
}
val txn = (resultMove as CashFlowResult.Success).transaction
txn?.let {
return txn
}
// NOTE: CashFlowResult.Success should always return a signedTransaction
throw Exception("Missing CashFlow transaction [${(resultMove)}]")
}
// now invoke Cash subflow to Move issued assetType to issue requester
progressTracker.currentStep = TRANSFERRING
val moveCashFlow = CashFlow(CashCommand.PayCash(amount.issuedBy(bankOfCordaParty.ref(issuerPartyRef)), issueTo))
val resultMove = subFlow(moveCashFlow)
// NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger)
if (resultMove is CashFlowResult.Failed) {
logger.error("Problem transferring cash: ${resultMove.message}")
throw Exception(resultMove.message)
// catch and report exception before throwing back to caller flow
catch (e: Exception) {
logger.error("Issuer Exception: failed for amount ${amount} issuedTo ${issueTo} with issuerPartyRef ${issuerPartyRef}")
// TODO: awaiting exception handling strategy (what action should be taken here?)
throw e
}
val txn = (resultMove as CashFlowResult.Success).transaction
txn?.let {
return txn
}
// NOTE: CashFlowResult.Success should always return a signedTransaction
throw Exception("Missing CashFlow transaction [${(resultMove)}]")
}
class Service(services: PluginServiceHub) {

View File

@ -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() {

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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).

View File

@ -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: []) {

View File

@ -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))

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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

View File

@ -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))

View File

@ -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.

View File

@ -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'
}

View File

@ -8,9 +8,12 @@ import javafx.scene.control.ButtonType
import javafx.scene.image.Image
import javafx.stage.Stage
import jfxtras.resources.JFXtrasFontRoboto
import joptsimple.OptionParser
import net.corda.client.mock.EventGenerator
import net.corda.client.model.Models
import net.corda.client.model.observableValue
import net.corda.core.contracts.GBP
import net.corda.core.contracts.USD
import net.corda.core.messaging.startFlow
import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.ServiceType
@ -19,6 +22,7 @@ import net.corda.explorer.model.SettingsModel
import net.corda.explorer.views.*
import net.corda.explorer.views.cordapps.cash.CashViewer
import net.corda.flows.CashFlow
import net.corda.flows.IssuerFlow.IssuanceRequester
import net.corda.node.driver.PortAllocation
import net.corda.node.driver.driver
import net.corda.node.services.User
@ -100,61 +104,118 @@ class Main : App(MainView::class) {
}
/**
* This main method will starts 3 nodes (Notary, Alice and Bob) locally for UI testing, they will be on localhost:20002, 20004, 20006 respectively.
* This main method will starts 5 nodes (Notary, Alice, Bob, UK Bank and USA Bank) locally for UI testing, they will be on localhost:20002, 20004, 20006, 20008, 20010 respectively.
*/
fun main(args: Array<String>) {
val portAllocation = PortAllocation.Incremental(20000)
driver(portAllocation = portAllocation) {
val user = User("user1", "test", permissions = setOf(startFlowPermission<CashFlow>()))
val manager = User("manager", "test", permissions = setOf(startFlowPermission<CashFlow>(), startFlowPermission<IssuanceRequester>()))
// TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo.
val notary = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)), customOverrides = mapOf("nearestCity" to "Zurich"))
val alice = startNode("Alice", rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "Paris"))
val bob = startNode("Bob", rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "Frankfurt"))
val issuer = startNode("Royal Mint", rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "London"))
val notary = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)),
customOverrides = mapOf("nearestCity" to "Zurich"))
val alice = startNode("Alice", rpcUsers = arrayListOf(user),
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))),
customOverrides = mapOf("nearestCity" to "Milan"))
val bob = startNode("Bob", rpcUsers = arrayListOf(user),
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))),
customOverrides = mapOf("nearestCity" to "Madrid"))
val issuerGBP = startNode("UK Bank Plc", rpcUsers = arrayListOf(manager),
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.GBP"))),
customOverrides = mapOf("nearestCity" to "London"))
val issuerUSD = startNode("USA Bank Corp", rpcUsers = arrayListOf(manager),
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.USD"))),
customOverrides = mapOf("nearestCity" to "New York"))
val notaryNode = notary.get()
val aliceNode = alice.get()
val bobNode = bob.get()
val issuerNode = issuer.get()
val issuerNodeGBP = issuerGBP.get()
val issuerNodeUSD = issuerUSD.get()
arrayOf(notaryNode, aliceNode, bobNode, issuerNode).forEach {
arrayOf(notaryNode, aliceNode, bobNode, issuerNodeGBP, issuerNodeUSD).forEach {
println("${it.nodeInfo.legalIdentity} started on ${ArtemisMessagingComponent.toHostAndPort(it.nodeInfo.address)}")
}
// Register with alice to use alice's RPC proxy to create random events.
val aliceClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(aliceNode.nodeInfo.address), FullNodeConfiguration(aliceNode.config))
aliceClient.start(user.username, user.password)
val aliceRPC = aliceClient.proxy()
val bobClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(bobNode.nodeInfo.address), FullNodeConfiguration(bobNode.config))
bobClient.start(user.username, user.password)
val bobRPC = bobClient.proxy()
val parser = OptionParser("S")
val options = parser.parse(*args)
if (options.has("S")) {
println("Running simulation mode ...")
val issuerClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(issuerNode.nodeInfo.address), FullNodeConfiguration(issuerNode.config))
issuerClient.start(user.username, user.password)
val bocRPC = issuerClient.proxy()
// Register with alice to use alice's RPC proxy to create random events.
val aliceClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(aliceNode.nodeInfo.address), FullNodeConfiguration(aliceNode.config))
aliceClient.start(user.username, user.password)
val aliceRPC = aliceClient.proxy()
val eventGenerator = EventGenerator(
parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity, issuerNode.nodeInfo.legalIdentity),
notary = notaryNode.nodeInfo.notaryIdentity
)
val bobClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(bobNode.nodeInfo.address), FullNodeConfiguration(bobNode.config))
bobClient.start(user.username, user.password)
val bobRPC = bobClient.proxy()
for (i in 0..1000) {
Thread.sleep(500)
listOf(aliceRPC, bobRPC).forEach {
eventGenerator.clientCommandGenerator.map { command ->
it.startFlow(::CashFlow, command)
val issuerClientGBP = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(issuerNodeGBP.nodeInfo.address), FullNodeConfiguration(issuerNodeGBP.config))
issuerClientGBP.start(manager.username, manager.password)
val issuerRPCGBP = issuerClientGBP.proxy()
val issuerClientUSD = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(issuerNodeGBP.nodeInfo.address), FullNodeConfiguration(issuerNodeUSD.config))
issuerClientUSD.start(manager.username, manager.password)
val issuerRPCUSD = issuerClientUSD.proxy()
val eventGenerator = EventGenerator(
parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity),
notary = notaryNode.nodeInfo.notaryIdentity,
issuers = listOf(issuerNodeGBP.nodeInfo.legalIdentity,issuerNodeUSD.nodeInfo.legalIdentity)
)
val issuerGBPEventGenerator = EventGenerator(
parties = listOf(issuerNodeGBP.nodeInfo.legalIdentity, aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity),
notary = notaryNode.nodeInfo.notaryIdentity,
currencies = listOf(GBP)
)
val issuerUSDEventGenerator = EventGenerator(
parties = listOf(issuerNodeUSD.nodeInfo.legalIdentity, aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity),
notary = notaryNode.nodeInfo.notaryIdentity,
currencies = listOf(USD)
)
for (i in 0..1000) {
Thread.sleep(500)
// Party pay requests
listOf(aliceRPC, bobRPC).forEach {
eventGenerator.clientCommandGenerator.map { command ->
it.startFlow(::CashFlow, command)
Unit
}.generate(SplittableRandom())
}
// Exit requests
issuerGBPEventGenerator.bankOfCordaExitGenerator.map { command ->
issuerRPCGBP.startFlow(::CashFlow, command)
Unit
}.generate(SplittableRandom())
issuerUSDEventGenerator.bankOfCordaExitGenerator.map { command ->
issuerRPCUSD.startFlow(::CashFlow, command)
Unit
}.generate(SplittableRandom())
// Issuer requests
issuerGBPEventGenerator.bankOfCordaIssueGenerator.map { command ->
issuerRPCGBP.startFlow(::IssuanceRequester,
command.amount,
command.recipient,
command.issueRef,
issuerNodeGBP.nodeInfo.legalIdentity)
Unit
}.generate(SplittableRandom())
issuerUSDEventGenerator.bankOfCordaIssueGenerator.map { command ->
issuerRPCUSD.startFlow(::IssuanceRequester,
command.amount,
command.recipient,
command.issueRef,
issuerNodeUSD.nodeInfo.legalIdentity)
Unit
}.generate(SplittableRandom())
}
eventGenerator.bankOfCordaCommandGenerator.map { command ->
bocRPC.startFlow(::CashFlow, command)
Unit
}.generate(SplittableRandom())
aliceClient.close()
bobClient.close()
issuerClientGBP.close()
issuerClientUSD.close()
}
aliceClient.close()
bobClient.close()
issuerClient.close()
waitForAllNodesToFinish()
}
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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 } }

View File

@ -6,26 +6,33 @@ import javafx.beans.property.SimpleObjectProperty
import javafx.collections.FXCollections
import javafx.scene.control.*
import javafx.stage.Window
import net.corda.client.fxutils.ChosenList
import net.corda.client.fxutils.isNotNull
import net.corda.client.fxutils.map
import net.corda.client.fxutils.unique
import net.corda.client.model.*
import net.corda.core.contracts.*
import net.corda.core.contracts.Amount
import net.corda.core.contracts.Issued
import net.corda.core.contracts.PartyAndReference
import net.corda.core.contracts.withoutIssuer
import net.corda.core.crypto.Party
import net.corda.core.messaging.startFlow
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.explorer.model.CashTransaction
import net.corda.explorer.model.IssuerModel
import net.corda.explorer.model.ReportingCurrencyModel
import net.corda.explorer.views.bigDecimalFormatter
import net.corda.explorer.views.byteFormatter
import net.corda.explorer.views.stringConverter
import net.corda.flows.CashCommand
import net.corda.flows.CashFlow
import net.corda.flows.CashFlowResult
import net.corda.flows.IssuerFlow.IssuanceRequester
import org.controlsfx.dialog.ExceptionDialog
import tornadofx.Fragment
import tornadofx.booleanBinding
import tornadofx.observable
import java.math.BigDecimal
import java.util.*
@ -51,11 +58,24 @@ class NewTransaction : Fragment() {
private val issueRef = SimpleObjectProperty<Byte>()
// Inject data
private val parties by observableList(NetworkIdentityModel::parties)
private val issuers by observableList(IssuerModel::issuers)
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
private val notaries by observableList(NetworkIdentityModel::notaries)
private val cash by observableList(ContractStateModel::cash)
private val executeButton = ButtonType("Execute", ButtonBar.ButtonData.APPLY)
private val currencyTypes by observableList(IssuerModel::currencyTypes)
private val supportedCurrencies by observableList(ReportingCurrencyModel::supportedCurrencies)
private val transactionTypes by observableList(IssuerModel::transactionTypes)
private val currencyItems = ChosenList(transactionTypeCB.valueProperty().map {
when(it){
CashTransaction.Pay -> supportedCurrencies
CashTransaction.Issue,
CashTransaction.Exit -> currencyTypes
else -> FXCollections.emptyObservableList()
}
})
fun show(window: Window): Unit {
dialog(window).showAndWait().ifPresent {
@ -67,14 +87,29 @@ class NewTransaction : Fragment() {
}
dialog.show()
runAsync {
rpcProxy.value!!.startFlow(::CashFlow, it).returnValue.toBlocking().first()
if (it is CashCommand.IssueCash) {
myIdentity.value?.let { myIdentity ->
rpcProxy.value!!.startFlow(::IssuanceRequester,
it.amount,
it.recipient,
it.issueRef,
myIdentity.legalIdentity).returnValue.toBlocking().first()
}
}
else {
rpcProxy.value!!.startFlow(::CashFlow, it).returnValue.toBlocking().first()
}
}.ui {
dialog.contentText = when (it) {
is SignedTransaction -> {
dialog.alertType = Alert.AlertType.INFORMATION
"Cash Issued \nTransaction ID : ${it.id} \nMessage"
}
is CashFlowResult.Success -> {
dialog.alertType = Alert.AlertType.INFORMATION
"Transaction Started \nTransaction ID : ${it.transaction?.id} \nMessage : ${it.message}"
}
is CashFlowResult.Failed -> {
else -> {
dialog.alertType = Alert.AlertType.ERROR
it.toString()
}
@ -92,15 +127,15 @@ class NewTransaction : Fragment() {
dialogPane = root
initOwner(window)
setResultConverter {
val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
val defaultRef = OpaqueBytes.of(1)
val issueRef = if (issueRef.value != null) OpaqueBytes.of(issueRef.value) else defaultRef
when (it) {
executeButton -> when (transactionTypeCB.value) {
CashTransaction.Issue -> {
val issueRef = if (issueRef.value != null) OpaqueBytes(ByteArray(1, { issueRef.value })) else defaultRef
CashCommand.IssueCash(Amount(amount.value, currencyChoiceBox.value), issueRef, partyBChoiceBox.value.legalIdentity, notaries.first().notaryIdentity)
}
CashTransaction.Pay -> CashCommand.PayCash(Amount(amount.value, Issued(PartyAndReference(issuerChoiceBox.value, defaultRef), currencyChoiceBox.value)), partyBChoiceBox.value.legalIdentity)
CashTransaction.Exit -> CashCommand.ExitCash(Amount(amount.value, currencyChoiceBox.value), defaultRef)
CashTransaction.Pay -> CashCommand.PayCash(Amount(amount.value, Issued(PartyAndReference(issuerChoiceBox.value, issueRef), currencyChoiceBox.value)), partyBChoiceBox.value.legalIdentity)
CashTransaction.Exit -> CashCommand.ExitCash(Amount(amount.value, currencyChoiceBox.value), issueRef)
else -> null
}
else -> null
@ -115,7 +150,7 @@ class NewTransaction : Fragment() {
root.disableProperty().bind(enableProperty.not())
// Transaction Types Choice Box
transactionTypeCB.items = CashTransaction.values().asList().observable()
transactionTypeCB.items = transactionTypes
// Party A textfield always display my identity name, not editable.
partyATextField.isEditable = false
@ -133,7 +168,7 @@ class NewTransaction : Fragment() {
// Issuer
issuerLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
issuerChoiceBox.apply {
items = cash.map { it.token.issuer.party }.unique().sorted()
items = issuers.map { it.legalIdentity }.unique().sorted()
converter = stringConverter { it.name }
visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Pay })
}
@ -143,16 +178,16 @@ class NewTransaction : Fragment() {
isEditable = false
}
// Issue Reference
issueRefLabel.visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Issue })
issueRefLabel.visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Issue || it == CashTransaction.Exit })
issueRefTextField.apply {
textFormatter = byteFormatter().apply { issueRef.bind(this.valueProperty()) }
visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Issue })
visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Issue || it == CashTransaction.Exit })
}
// Currency
currencyLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
// TODO : Create a currency model to store these values
currencyChoiceBox.items = FXCollections.observableList(setOf(USD, GBP, CHF).toList())
currencyChoiceBox.items = currencyItems
currencyChoiceBox.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
val issuer = Bindings.createObjectBinding({ if (issuerChoiceBox.isVisible) issuerChoiceBox.value else myIdentity.value?.legalIdentity }, arrayOf(myIdentity, issuerChoiceBox.visibleProperty(), issuerChoiceBox.valueProperty()))
availableAmount.visibleProperty().bind(

View File

@ -0,0 +1,2 @@
# Register a ServiceLoader service extending from net.corda.node.CordaPluginRegistry
net.corda.explorer.plugin.ExplorerPlugin

View File

@ -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(".")) })
}
}