mirror of
https://github.com/corda/corda.git
synced 2025-01-26 22:29:28 +00:00
Bank of Corda demo - Issuer of Cash
Resolve BankOfCorda through NMS in protocol Fixes following Integration testing. Register custom RPC Kryo classes. Protocol -> Flow renaming Bank of Corda demo - Issuer of Cash Resolve BankOfCorda through NMS in protocol Fixes following Integration testing. Protocol -> Flow renaming Addressed all comments in PR review. Removed bank lines. Updated minor inconsistency in README.md All protocol references changed to flow. changed protocol -> flow in TODO comment. changed startProtocolPermission -> startFlowPermission in README.md Added transaction id to IssuerFlow Success response. Removed explicit call to record Cash Move transaction (as already recorded in subflow) Removed quasar dependency. Addressed comment in PR. Updated to use CompositeKey. Added arguments to pass in Currency and Amount. Updated run configurations to pass in Currency and Amount values Added additional parameter to IssuerFlow request: issueToPartyReference Added Vault updates verification in RPC Integration test. Fixed RPC Integration test (Vault assertions) Updated run-time dependencies in line with other demos. Applied changes following PR review (exception handling, party resolution handling, docs) Updated gradle client run configs with new parameters. Main driver app now uses standard out for display (was using logger info() but nothing was being displayed because of restrictive config) Fixed formatting display problems. Updated Web Api code to use new CordaRPCOps interface (and new partyFromName() exposed method) Removed unused import.
This commit is contained in:
parent
70dcab6361
commit
453f7cd223
15
.idea/runConfigurations/Bank_of_Corda_Demo__Run_Issuer.xml
generated
Normal file
15
.idea/runConfigurations/Bank_of_Corda_Demo__Run_Issuer.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Bank of Corda Demo: Run Issuer" 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.bank.BankOfCordaDriverKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role ISSUER" />
|
||||
<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="bank-of-corda-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Bank_of_Corda_Demo__Run_RPC_Cash_Issue.xml
generated
Normal file
15
.idea/runConfigurations/Bank_of_Corda_Demo__Run_RPC_Cash_Issue.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Bank of Corda Demo: Run RPC Cash Issue" 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.bank.BankOfCordaDriverKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role ISSUE_CASH_RPC --quantity 12345 --currency GBP" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="bank-of-corda-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Bank_of_Corda_Demo__Run_Web_Cash_Issue.xml
generated
Normal file
15
.idea/runConfigurations/Bank_of_Corda_Demo__Run_Web_Cash_Issue.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Bank of Corda Demo: Run Web Cash Issue" 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.bank.BankOfCordaDriverKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role ISSUE_CASH_WEB --quantity 67890 --currency EUR" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="bank-of-corda-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
@ -13,6 +13,8 @@ so far. We have:
|
||||
5. The SIMM valuation demo, a large demo which shows two nodes agreeing on a portfolio and valuing the initial margin
|
||||
using the Standard Initial Margin Model.
|
||||
6. The distributed notary demo, which demonstrates a single node getting multiple transactions notarised by a distributed (Raft-based) notary.
|
||||
7. The Bank of Corda demo, which demonstrates a node acting as an issuer of assets (the Bank of Corda) and remote client
|
||||
applications requesting issuance (via RPC, HTTP) of some cash on behalf of a node called Big Corporation.
|
||||
|
||||
.. note:: If any demos don't work please jump on our mailing list and let us know.
|
||||
|
||||
@ -156,6 +158,58 @@ by using the H2 web console:
|
||||
- The committed states are stored in the ``NOTARY_COMMITTED_STATES`` table. Note that the raw data is not human-readable,
|
||||
but we're only interested in the row count for this demo.
|
||||
|
||||
Bank Of Corda demo
|
||||
------------------
|
||||
|
||||
This demo brings up three nodes: a notary, a node acting as the Bank of Corda that accepts requests for issuance of some asset
|
||||
and a node acting as Big Corporation which requests issuance of an asset (cash in this example).
|
||||
Upon receipt of a request the Bank of Corda node self-issues the asset and then transfers ownership to the requester
|
||||
after successful notarisation and recording of the issue transaction on the ledger.
|
||||
|
||||
.. note:: The Bank of Corda is somewhat like the "Bitcoin faucet", that used to dispense free bitcoins to developers for
|
||||
testing and experimentation purposes.
|
||||
|
||||
To run from the command line (recommended for Mac/UNIX users!):
|
||||
|
||||
1. Run ``./gradlew samples:bank-of-corda-demo:deployNodes`` to create a set of configs and installs under ``samples/bank-of-corda-demo/build/nodes``
|
||||
2. Run ``./samples/bank-of-corda-demo/build/nodes/runnodes`` to open up three new terminal tabs/windows with the three nodes.
|
||||
|
||||
.. note:: to verify the Bank of Corda node is alive and running navigate to the following URL
|
||||
http://localhost:10005/api/bank/date
|
||||
|
||||
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.
|
||||
|
||||
Or you can run them from inside IntelliJ as follows:
|
||||
|
||||
1. Open the Corda project in IntelliJ and run the "Install" configuration
|
||||
2. Open the Corda samples project in IntelliJ and run the "Bank Of Corda Demo: Run Issuer" configuration
|
||||
3. Run "Bank Of Corda Demo: Run RPC Cash Issue" - requests issuance of some cash on behalf of Big Corporation via RPC
|
||||
4. Run "Bank Of Corda Demo: Run Web Cash Issue" - requests issuance of some cash on behalf of Big Corporation via HTTP
|
||||
|
||||
In the "Bank Of Corda Demo: Run Issuer" window you should see the following information lines displayed:
|
||||
|
||||
- Awaiting issuance request
|
||||
- Self issuing asset
|
||||
- Transferring asset to issuance requester
|
||||
- Confirming asset issuance to requester
|
||||
|
||||
In the the client issue request window you should see the following printed:
|
||||
|
||||
- Successfully processed Cash Issue request
|
||||
|
||||
Launch the Explorer application to visualize the issuance and transfer of cash on each node:
|
||||
|
||||
``./gradlew tools:explorer:run``
|
||||
|
||||
And use the following logon details:
|
||||
|
||||
- for the Bank of Corda node specify localhost, port 10004, username user1, password test
|
||||
- for the Big Corporation node specify localhost, port 10006, username user1, password test
|
||||
|
||||
See https://docs.corda.net/node-explorer.html for further details on usage.
|
||||
|
||||
SIMM and Portfolio Demo - aka the Initial Margin Agreement Demo
|
||||
---------------------------------------------------------------
|
||||
|
||||
|
@ -7,4 +7,5 @@ Please refer to `README.md` in the individual project folders. There are the fo
|
||||
* **trader-demo** A simple driver for exercising the two party trading flow. In this scenario, a buyer wants to purchase some commercial paper by swapping his cash for commercial paper. The seller learns that the buyer exists, and sends them a message to kick off the trade. The seller, having obtained his CP, then quits and the buyer goes back to waiting. The buyer will sell as much CP as he can! **We recommend starting with this demo.**
|
||||
* **Network-visualiser** A tool that uses a simulation to visualise the interaction and messages between nodes on the Corda network. Currently only works for the IRS demo.
|
||||
* **simm-valudation-demo** A demo showing two nodes reaching agreement on the valuation of a derivatives portfolio.
|
||||
* **raft-notary-demo** A simple demonstration of a node getting multiple transactions notarised by a distributed (Raft-based) notary.
|
||||
* **raft-notary-demo** A simple demonstration of a node getting multiple transactions notarised by a distributed (Raft-based) notary.
|
||||
* **bank-of-corda-demo** A demo showing a node acting as an issuer of fungible assets (initially Cash)
|
57
samples/bank-of-corda-demo/README.md
Normal file
57
samples/bank-of-corda-demo/README.md
Normal file
@ -0,0 +1,57 @@
|
||||
# Bank of Corda demo
|
||||
Please see docs/build/html/running-the-demos.html
|
||||
|
||||
This program simulates the role of an asset issuing authority (eg. central bank for cash) by accepting requests
|
||||
from third parties to issue some quantity of an asset and transfer that ownership to the requester.
|
||||
The issuing authority accepts requests via the [IssuerFlow] flow, self-issues the asset and transfers
|
||||
ownership to the issue requester. Notarisation and signing form part of the flow.
|
||||
|
||||
The requesting party can be a CorDapp (running locally or remotely to the Bank of Corda node), a remote RPC client or
|
||||
a Web Client.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You will need to have [JDK 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)
|
||||
installed and available on your path.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Launch the Bank of Corda node (and associated Notary) by running:
|
||||
[BankOfCordaDriver] --role ISSUER
|
||||
(to validate your Node is running you can try navigating to this sample link: http://localhost:10005/api/bank/date)
|
||||
|
||||
Each of the following commands will launch a separate Node called Big Corporation which will become the owner
|
||||
of some Cash following an issue request:
|
||||
|
||||
2. Run the Bank of Corda Client driver (to simulate a web issue requester) by running:
|
||||
[BankOfCordaDriver] --role ISSUE_CASH_WEB
|
||||
This demonstrates a remote application acting on behalf of the Big Corporation and communicating directly with the
|
||||
Bank of Corda node via HTTP to request issuance of some cash.
|
||||
|
||||
3. Run the Bank of Corda Client driver (to simulate an RPC issue requester) by running:
|
||||
[BankOfCordaDriver] --role ISSUE_CASH_RPC
|
||||
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]
|
||||
2. Integration testing via RPC and HTTP uses the [Driver] DSL to launch standalone node instances
|
||||
|
||||
Security
|
||||
The RPC API requires a client to pass in user credentials:
|
||||
client.start("user1","test")
|
||||
which are validated on the Bank of Corda node against those configured at node startup:
|
||||
User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||
startNode("BankOfCorda", rpcUsers = listOf(user))
|
||||
|
||||
Notary
|
||||
We are using a [SimpleNotaryService] in this example, but could easily switch to a [ValidatingNotaryService]
|
||||
|
||||
## Future
|
||||
|
||||
The Bank of Corda node will become an integral part of other Corda samples that require initial issuance of some asset.
|
||||
|
||||
## Further Reading
|
||||
|
||||
Tutorials and developer docs for Cordapps and Corda are [here](https://docs.corda.net/).
|
148
samples/bank-of-corda-demo/build.gradle
Normal file
148
samples/bank-of-corda-demo/build.gradle
Normal file
@ -0,0 +1,148 @@
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
resources {
|
||||
srcDir "../../config/dev"
|
||||
}
|
||||
}
|
||||
test {
|
||||
resources {
|
||||
srcDir "../../config/test"
|
||||
}
|
||||
}
|
||||
integrationTest {
|
||||
kotlin {
|
||||
compileClasspath += main.output + test.output
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/integration-test/kotlin')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
testCompile group: 'junit', name: 'junit', version: '4.11'
|
||||
|
||||
// Corda integration dependencies
|
||||
runtime project(path: ":node", configuration: 'runtimeArtifacts')
|
||||
compile project(':core')
|
||||
compile project(':client')
|
||||
compile project(':node')
|
||||
compile project(':finance')
|
||||
compile project(':test-utils')
|
||||
|
||||
// Javax is required for webapis
|
||||
compile "org.glassfish.jersey.core:jersey-server:${jersey_version}"
|
||||
}
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: [':install', 'build']) {
|
||||
directory "./build/nodes"
|
||||
// This name "Notary" is hard-coded into BankOfCordaClientApi so if you change it here, change it there too.
|
||||
// In this demo the node that runs a standalone notary also acts as the network map server.
|
||||
networkMap "Notary"
|
||||
node {
|
||||
name "Notary"
|
||||
dirName "notary"
|
||||
nearestCity "London"
|
||||
advertisedServices = ["corda.notary.validating"]
|
||||
artemisPort 10002
|
||||
webPort 10003
|
||||
cordapps = []
|
||||
}
|
||||
node {
|
||||
name "BankOfCorda"
|
||||
dirName "node-bank-of-corda"
|
||||
nearestCity "London"
|
||||
advertisedServices = []
|
||||
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 ] }
|
||||
// ]
|
||||
}
|
||||
node {
|
||||
name "BigCorporation"
|
||||
dirName "node-big-corp"
|
||||
nearestCity "New York"
|
||||
advertisedServices = []
|
||||
artemisPort 10006
|
||||
webPort 10007
|
||||
cordapps = []
|
||||
}
|
||||
}
|
||||
|
||||
task integrationTest(type: Test, dependsOn: []) {
|
||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
downloadJavadoc = true // defaults to false
|
||||
downloadSources = true
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
jarAndSources(MavenPublication) {
|
||||
from components.java
|
||||
artifactId 'bankofcorda'
|
||||
|
||||
artifact sourceJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task runIssuer(type: JavaExec) {
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
main = 'net.corda.bank.BankOfCordaDriverKt'
|
||||
args '--role'
|
||||
args 'ISSUER'
|
||||
}
|
||||
|
||||
task runRPCCashIssue(type: JavaExec) {
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
main = 'net.corda.bank.BankOfCordaDriverKt'
|
||||
args '--role'
|
||||
args 'ISSUE_CASH_RPC'
|
||||
args '--quantity'
|
||||
args 20000
|
||||
args '--currency'
|
||||
args 'USD'
|
||||
}
|
||||
|
||||
task runWebCashIssue(type: JavaExec) {
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
main = 'net.corda.bank.BankOfCordaDriverKt'
|
||||
args '--role'
|
||||
args 'ISSUE_CASH_WEB'
|
||||
args '--quantity'
|
||||
args 30000
|
||||
args '--currency'
|
||||
args 'GBP'
|
||||
}
|
2
samples/bank-of-corda-demo/gradle.properties
Normal file
2
samples/bank-of-corda-demo/gradle.properties
Normal file
@ -0,0 +1,2 @@
|
||||
name = BankOfCorda
|
||||
kotlin.incremental=false
|
@ -0,0 +1,20 @@
|
||||
package net.corda.bank
|
||||
|
||||
import net.corda.bank.api.BankOfCordaClientApi
|
||||
import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.testing.getHostAndPort
|
||||
import org.junit.Test
|
||||
|
||||
class BankOfCordaHttpAPITest {
|
||||
@Test fun `test issuer flow via Http`() {
|
||||
driver(dsl = {
|
||||
val nodeBankOfCorda = startNode("BankOfCorda", setOf(ServiceInfo(SimpleNotaryService.type))).get()
|
||||
val nodeBankOfCordaApiAddr = nodeBankOfCorda.config.getHostAndPort("webAddress")
|
||||
startNode("BigCorporation").get()
|
||||
assert(BankOfCordaClientApi(nodeBankOfCordaApiAddr).requestWebIssue(IssueRequestParams(1000, "USD", "BigCorporation", "1", "BankOfCorda")))
|
||||
}, isDebug = true)
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
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.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 org.junit.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class BankOfCordaRPCClientTest {
|
||||
|
||||
@Test fun `test issuer flow via RPC`() {
|
||||
driver(dsl = {
|
||||
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuanceRequester>()))
|
||||
val nodeBankOfCorda = startNode("BankOfCorda", setOf(ServiceInfo(SimpleNotaryService.type)), arrayListOf(user)).get()
|
||||
val nodeBankOfCordaApiAddr = nodeBankOfCorda.config.getHostAndPort("artemisAddress")
|
||||
val bankOfCordaParty = nodeBankOfCorda.nodeInfo.legalIdentity
|
||||
val nodeBigCorporation = startNode("BigCorporation", rpcUsers = arrayListOf(user)).get()
|
||||
val bigCorporationParty = nodeBigCorporation.nodeInfo.legalIdentity
|
||||
|
||||
// Bank of Corda RPC Client
|
||||
val bocClient = CordaRPCClient(nodeBankOfCordaApiAddr, configureTestSSL())
|
||||
bocClient.start("user1","test")
|
||||
val bocProxy = bocClient.proxy()
|
||||
|
||||
// Big Corporation RPC Client
|
||||
val bigCorpClient = CordaRPCClient(nodeBankOfCordaApiAddr, configureTestSSL())
|
||||
bigCorpClient.start("user1","test")
|
||||
val bigCorpProxy = bigCorpClient.proxy()
|
||||
|
||||
// Register for Bank of Corda Vault updates
|
||||
val vaultUpdatesBoc = bocProxy.vaultAndUpdates().second
|
||||
|
||||
// Register for Big Corporation Vault updates
|
||||
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()
|
||||
assertTrue { result is SignedTransaction }
|
||||
|
||||
// Check Bank of Corda Vault Updates
|
||||
vaultUpdatesBoc.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { update ->
|
||||
require(update.consumed.size == 0) { update.consumed.size }
|
||||
require(update.produced.size == 1) { update.produced.size }
|
||||
},
|
||||
// MOVE
|
||||
expect { update ->
|
||||
require(update.consumed.size == 1) { update.consumed.size }
|
||||
require(update.produced.size == 0) { update.produced.size }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Check Big Corporation Vault Updates
|
||||
vaultUpdatesBigCorp.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { update ->
|
||||
require(update.consumed.size == 0) { update.consumed.size }
|
||||
require(update.produced.size == 1) { update.produced.size }
|
||||
},
|
||||
// MOVE
|
||||
expect { update ->
|
||||
require(update.consumed.size == 1) { update.consumed.size }
|
||||
require(update.produced.size == 0) { update.produced.size }
|
||||
}
|
||||
)
|
||||
}
|
||||
}, isDebug = true)
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package net.corda.bank
|
||||
|
||||
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.core.node.services.ServiceInfo
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
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 kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* This entry point allows for command line running of the Bank of Corda functions on nodes started by BankOfCordaDriver.kt.
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
BankOfCordaDriver().main(args)
|
||||
}
|
||||
|
||||
private class BankOfCordaDriver {
|
||||
enum class Role {
|
||||
ISSUE_CASH_RPC,
|
||||
ISSUE_CASH_WEB,
|
||||
ISSUER
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val parser = OptionParser()
|
||||
val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).describedAs("[ISSUER|ISSUE_CASH_RPC|ISSUE_CASH_WEB]")
|
||||
val quantity = parser.accepts("quantity").withOptionalArg().ofType(Long::class.java)
|
||||
val currency = parser.accepts("currency").withOptionalArg().ofType(String::class.java).describedAs("[GBP|USD|CHF|EUR]")
|
||||
val options = try {
|
||||
parser.parse(*args)
|
||||
} catch (e: Exception) {
|
||||
println(e.message)
|
||||
printHelp(parser)
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
// What happens next depends on the role.
|
||||
// The ISSUER will launch a Bank of Corda node
|
||||
// The ISSUE_CASH will request some Cash from the ISSUER on behalf of Big Corporation node
|
||||
val role = options.valueOf(roleArg)!!
|
||||
if (role == Role.ISSUER) {
|
||||
driver(dsl = {
|
||||
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
startNode("BankOfCorda", rpcUsers = listOf(user))
|
||||
startNode("BigCorporation")
|
||||
waitForAllNodesToFinish()
|
||||
}, isDebug = true)
|
||||
}
|
||||
else {
|
||||
try {
|
||||
val requestParams = IssueRequestParams(options.valueOf(quantity), options.valueOf(currency), "BigCorporation", "1", "BankOfCorda")
|
||||
when (role) {
|
||||
Role.ISSUE_CASH_RPC -> {
|
||||
println("Requesting Cash via RPC ...")
|
||||
val result = BankOfCordaClientApi(HostAndPort.fromString("localhost:10004")).requestRPCIssue(requestParams)
|
||||
if (result is SignedTransaction)
|
||||
println("Success!! You transaction receipt is ${result.tx.id}")
|
||||
}
|
||||
Role.ISSUE_CASH_WEB -> {
|
||||
println("Requesting Cash via Web ...")
|
||||
val result = BankOfCordaClientApi(HostAndPort.fromString("localhost:10005")).requestWebIssue(requestParams)
|
||||
if (result)
|
||||
println("Successfully processed Cash Issue request")
|
||||
}
|
||||
Role.ISSUER -> {}
|
||||
}
|
||||
}
|
||||
catch (e: Exception) {
|
||||
printHelp(parser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun printHelp(parser: OptionParser) {
|
||||
println("""
|
||||
Usage: bank-of-corda --role ISSUER
|
||||
bank-of-corda --role (ISSUE_CASH_RPC|ISSUE_CASH_WEB) --quantity <quantity> --currency <currency>
|
||||
|
||||
Please refer to the documentation in docs/build/index.html for more info.
|
||||
|
||||
""".trimIndent())
|
||||
parser.printHelpOn(System.out)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,48 @@
|
||||
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.node.services.messaging.CordaRPCClient
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.currency
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.node.services.config.configureTestSSL
|
||||
import net.corda.testing.http.HttpApi
|
||||
|
||||
/**
|
||||
* Interface for communicating with Bank of Corda node
|
||||
*/
|
||||
class BankOfCordaClientApi(val hostAndPort: HostAndPort) {
|
||||
private val apiRoot = "api/bank"
|
||||
/**
|
||||
* HTTP API
|
||||
*/
|
||||
// TODO: security controls required
|
||||
fun requestWebIssue(params: IssueRequestParams): Boolean {
|
||||
val api = HttpApi.fromHostAndPort(hostAndPort, apiRoot)
|
||||
return api.postJson("issue-asset-request", params)
|
||||
}
|
||||
/**
|
||||
* RPC API
|
||||
*/
|
||||
fun requestRPCIssue(params: IssueRequestParams): SignedTransaction {
|
||||
val client = CordaRPCClient(hostAndPort, configureTestSSL())
|
||||
// TODO: privileged security controls required
|
||||
client.start("user1","test")
|
||||
val proxy = client.proxy()
|
||||
|
||||
// Resolve parties via RPC
|
||||
val issueToParty = proxy.partyFromName(params.issueToPartyName)
|
||||
?: throw Exception("Unable to locate ${params.issueToPartyName} in Network Map Service")
|
||||
val issuerBankParty = proxy.partyFromName(params.issuerBankName)
|
||||
?: throw Exception("Unable to locate ${params.issuerBankName} in Network Map Service")
|
||||
|
||||
val amount = Amount(params.amount, currency(params.currency))
|
||||
val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte())
|
||||
|
||||
return proxy.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty).returnValue.toBlocking().first()
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package net.corda.bank.api
|
||||
|
||||
import net.corda.bank.flow.IssuerFlow.IssuanceRequester
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.currency
|
||||
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.loggerFor
|
||||
import java.time.LocalDateTime
|
||||
import javax.ws.rs.*
|
||||
import javax.ws.rs.core.MediaType
|
||||
import javax.ws.rs.core.Response
|
||||
|
||||
// API is accessible from /api/bank. All paths specified below are relative to it.
|
||||
@Path("bank")
|
||||
class BankOfCordaWebApi(val rpc: CordaRPCOps) {
|
||||
data class IssueRequestParams(val amount: Long, val currency: String,
|
||||
val issueToPartyName: String, val issueToPartyRefAsString: String,
|
||||
val issuerBankName: String)
|
||||
private companion object {
|
||||
val logger = loggerFor<BankOfCordaWebApi>()
|
||||
}
|
||||
@GET
|
||||
@Path("date")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun getCurrentDate(): Any {
|
||||
return mapOf("date" to LocalDateTime.now().toLocalDate())
|
||||
}
|
||||
/**
|
||||
* Request asset issuance
|
||||
*/
|
||||
@POST
|
||||
@Path("issue-asset-request")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
fun issueAssetRequest(params: IssueRequestParams): Response {
|
||||
// Resolve parties via RPC
|
||||
val issueToParty = rpc.partyFromName(params.issueToPartyName)
|
||||
?: throw Exception("Unable to locate ${params.issueToPartyName} in Network Map Service")
|
||||
val issuerBankParty = rpc.partyFromName(params.issuerBankName)
|
||||
?: throw Exception("Unable to locate ${params.issuerBankName} in Network Map Service")
|
||||
|
||||
val amount = Amount(params.amount, currency(params.currency))
|
||||
val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte())
|
||||
|
||||
// invoke client side of Issuer Flow: IssuanceRequester
|
||||
// The line below blocks and waits for the future to resolve.
|
||||
val result = rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty).returnValue.toBlocking().first()
|
||||
if (result is SignedTransaction) {
|
||||
logger.info("Issue request completed successfully: ${params}")
|
||||
return Response.status(Response.Status.CREATED).build()
|
||||
} else {
|
||||
return Response.status(Response.Status.BAD_REQUEST).build()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
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
|
@ -0,0 +1,109 @@
|
||||
package net.corda.bank.flow
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.PluginServiceHub
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.flows.CashFlowResult
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This flow enables a client to request issuance of some [FungibleAsset] from a
|
||||
* server acting as an issuer (see [Issued]) of FungibleAssets.
|
||||
*
|
||||
* It is not intended for production usage, but rather for experimentation and testing purposes where it may be
|
||||
* useful for creation of fake assets.
|
||||
*/
|
||||
object IssuerFlow {
|
||||
data class IssuanceRequestState(val amount: Amount<Currency>, val issueToParty: Party, val issuerPartyRef: OpaqueBytes)
|
||||
|
||||
/**
|
||||
* IssuanceRequester should be used by a client to ask a remote note to issue some [FungibleAsset] with the given details.
|
||||
* Returns the transaction created by the Issuer to move the cash to the Requester.
|
||||
*/
|
||||
class IssuanceRequester(val amount: Amount<Currency>, val issueToParty: Party, val issueToPartyRef: OpaqueBytes,
|
||||
val issuerBankParty: Party): FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef)
|
||||
return sendAndReceive<SignedTransaction>(issuerBankParty, issueRequest).unwrap { it }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Issuer refers to a Node acting as a Bank Issuer of [FungibleAsset], and processes requests from a [IssuanceRequester] client.
|
||||
* 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()
|
||||
companion object {
|
||||
object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request")
|
||||
object ISSUING : ProgressTracker.Step("Self issuing asset")
|
||||
object TRANSFERRING : ProgressTracker.Step("Transferring asset to issuance requester")
|
||||
object SENDING_CONFIRM : ProgressTracker.Step("Confirming asset issuance to requester")
|
||||
fun tracker() = ProgressTracker(AWAITING_REQUEST, ISSUING, TRANSFERRING, SENDING_CONFIRM)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = AWAITING_REQUEST
|
||||
val issueRequest = receive<IssuanceRequestState>(otherParty).unwrap { it }
|
||||
// validate request inputs (for example, lets restrict the types of currency that can be issued)
|
||||
require(listOf<Currency>(USD, GBP, EUR, CHF).contains(issueRequest.amount.token)) {
|
||||
logger.error("Currency must be one of USD, GBP, EUR, CHF")
|
||||
}
|
||||
// TODO: parse request to determine Asset to issue
|
||||
val txn = issueCashTo(issueRequest.amount, issueRequest.issueToParty, issueRequest.issuerPartyRef)
|
||||
progressTracker.currentStep = SENDING_CONFIRM
|
||||
send(otherParty, txn)
|
||||
return txn
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun issueCashTo(amount: Amount<Currency>,
|
||||
issueTo: Party, issuerPartyRef: OpaqueBytes): SignedTransaction {
|
||||
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)
|
||||
}
|
||||
// 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)}]")
|
||||
}
|
||||
|
||||
class Service(services: PluginServiceHub) {
|
||||
init {
|
||||
services.registerFlowInitiator(IssuanceRequester::class) {
|
||||
Issuer(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package net.corda.bank.plugin
|
||||
|
||||
import net.corda.bank.api.BankOfCordaWebApi
|
||||
import net.corda.bank.flow.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 BankOfCordaPlugin : CordaPluginRegistry() {
|
||||
// A list of classes that expose web APIs.
|
||||
override val webApis = listOf(Function(::BankOfCordaWebApi))
|
||||
// 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))
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
# Register a ServiceLoader service extending from net.corda.node.CordaPluginRegistry
|
||||
net.corda.bank.plugin.BankOfCordaPlugin
|
@ -0,0 +1,74 @@
|
||||
package net.corda.bank.flow
|
||||
|
||||
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.core.contracts.Amount
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.contracts.currency
|
||||
import net.corda.core.flows.FlowStateMachine
|
||||
import net.corda.core.map
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.corda.testing.MEGA_CORP_KEY
|
||||
import net.corda.testing.initiateSingleShotFlow
|
||||
import net.corda.testing.ledger
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class IssuerFlowTest {
|
||||
lateinit var net: MockNetwork
|
||||
lateinit var notaryNode: MockNetwork.MockNode
|
||||
lateinit var bankOfCordaNode: MockNetwork.MockNode
|
||||
lateinit var bankClientNode: MockNetwork.MockNode
|
||||
|
||||
@Test
|
||||
fun `test issuer flow`() {
|
||||
net = MockNetwork(false, true)
|
||||
ledger {
|
||||
notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
bankOfCordaNode = net.createPartyNode(notaryNode.info.address, BOC_ISSUER_PARTY.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 (issuer, issuerResult) = runIssuerAndIssueRequester(1000000.DOLLARS, issueToPartyAndRef)
|
||||
assertEquals(issuerResult.get(), issuer.get().resultFuture.get())
|
||||
|
||||
// try to issue an amount of a restricted currency
|
||||
assertFailsWith<Exception> {
|
||||
runIssuerAndIssueRequester(Amount(100000L, currency("BRL")), issueToPartyAndRef).issueRequestResult.get()
|
||||
}
|
||||
|
||||
bankOfCordaNode.stop()
|
||||
bankClientNode.stop()
|
||||
|
||||
bankOfCordaNode.manuallyCloseDB()
|
||||
bankClientNode.manuallyCloseDB()
|
||||
}
|
||||
}
|
||||
|
||||
private fun runIssuerAndIssueRequester(amount: Amount<Currency>, issueToPartyAndRef: PartyAndReference) : RunResult {
|
||||
val issuerFuture = bankOfCordaNode.initiateSingleShotFlow(IssuerFlow.IssuanceRequester::class) {
|
||||
otherParty -> IssuerFlow.Issuer(issueToPartyAndRef.party)
|
||||
}.map { it.fsm }
|
||||
|
||||
val issueRequest = IssuanceRequester(amount, issueToPartyAndRef.party, issueToPartyAndRef.reference, bankOfCordaNode.info.legalIdentity)
|
||||
val issueRequestResultFuture = bankClientNode.smm.add(issueRequest).resultFuture
|
||||
|
||||
return RunResult(issuerFuture, issueRequestResultFuture)
|
||||
}
|
||||
|
||||
private data class RunResult(
|
||||
val issuer: ListenableFuture<FlowStateMachine<*>>,
|
||||
val issueRequestResult: ListenableFuture<SignedTransaction>
|
||||
)
|
||||
}
|
@ -17,4 +17,5 @@ include 'samples:trader-demo'
|
||||
include 'samples:irs-demo'
|
||||
include 'samples:network-visualiser'
|
||||
include 'samples:simm-valuation-demo'
|
||||
include 'samples:raft-notary-demo'
|
||||
include 'samples:raft-notary-demo'
|
||||
include 'samples:bank-of-corda-demo'
|
Loading…
x
Reference in New Issue
Block a user