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:
Jose Coll 2016-11-18 17:51:12 +00:00 committed by josecoll
parent 70dcab6361
commit 453f7cd223
19 changed files with 839 additions and 2 deletions

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

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

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

View File

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

View File

@ -8,3 +8,4 @@ Please refer to `README.md` in the individual project folders. There are the fo
* **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.
* **bank-of-corda-demo** A demo showing a node acting as an issuer of fungible assets (initially Cash)

View 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/).

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

View File

@ -0,0 +1,2 @@
name = BankOfCorda
kotlin.incremental=false

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,3 +18,4 @@ include 'samples:irs-demo'
include 'samples:network-visualiser'
include 'samples:simm-valuation-demo'
include 'samples:raft-notary-demo'
include 'samples:bank-of-corda-demo'