mirror of
https://github.com/corda/corda.git
synced 2024-12-24 15:16:45 +00:00
Merge pull request #161 from corda/clint-traderdemorpc
Trader demo now uses RPC to communicate with nodes
This commit is contained in:
commit
5d40a03e60
@ -3,7 +3,7 @@
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.traderdemo.TraderDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role BUYER" />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role BUYER --certificates="build/trader-demo-nodes/Bank A/certificates"" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
|
@ -3,7 +3,7 @@
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.traderdemo.TraderDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role SELLER" />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role SELLER --certificates="build/trader-demo-nodes/Bank B/certificates"" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
|
@ -67,6 +67,11 @@ dependencies {
|
||||
}
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||
ext.rpcUsers = [['user': "demo", 'password': "demo", 'permissions': [
|
||||
'StartFlow.net.corda.flows.IssuerFlow$IssuanceRequester',
|
||||
"StartFlow.net.corda.traderdemo.flow.SellerFlow"
|
||||
]]]
|
||||
|
||||
directory "./build/nodes"
|
||||
// This name "Notary" is hard-coded into TraderDemoClientApi 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.
|
||||
@ -86,6 +91,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||
artemisPort 10004
|
||||
webPort 10005
|
||||
cordapps = []
|
||||
rpcUsers = ext.rpcUsers
|
||||
}
|
||||
node {
|
||||
name "Bank B"
|
||||
@ -94,6 +100,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||
artemisPort 10006
|
||||
webPort 10007
|
||||
cordapps = []
|
||||
rpcUsers = ext.rpcUsers
|
||||
}
|
||||
node {
|
||||
name "BankOfCorda"
|
||||
|
@ -13,16 +13,22 @@ import org.junit.Test
|
||||
class TraderDemoTest {
|
||||
@Test fun `runs trader demo`() {
|
||||
driver(isDebug = true) {
|
||||
val permissions = setOf(
|
||||
startFlowPermission<IssuerFlow.IssuanceRequester>(),
|
||||
startFlowPermission<net.corda.traderdemo.flow.SellerFlow>())
|
||||
val demoUser = listOf(User("demo", "demo", permissions))
|
||||
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||
val (nodeA, nodeB) = Futures.allAsList(
|
||||
startNode("Bank A"),
|
||||
startNode("Bank B"),
|
||||
startNode("Bank A", rpcUsers = demoUser),
|
||||
startNode("Bank B", rpcUsers = demoUser),
|
||||
startNode("BankOfCorda", rpcUsers = listOf(user)),
|
||||
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
).getOrThrow()
|
||||
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB)
|
||||
.map { it.rpcClientToNode().start(demoUser[0].username, demoUser[0].password).proxy() }
|
||||
|
||||
assert(TraderDemoClientApi(nodeA.configuration.webAddress).runBuyer())
|
||||
assert(TraderDemoClientApi(nodeB.configuration.webAddress).runSeller(counterparty = nodeA.nodeInfo.legalIdentity.name))
|
||||
assert(TraderDemoClientApi(nodeARpc).runBuyer())
|
||||
assert(TraderDemoClientApi(nodeBRpc).runSeller(counterparty = nodeA.nodeInfo.legalIdentity.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,18 +7,24 @@ import net.corda.node.services.User
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.testing.BOC
|
||||
import java.nio.file.Paths
|
||||
import net.corda.core.div
|
||||
|
||||
/**
|
||||
* This file is exclusively for being able to run your nodes through an IDE (as opposed to running deployNodes)
|
||||
* Do not use in a production environment.
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
driver(dsl = {
|
||||
val permissions = setOf(
|
||||
startFlowPermission<IssuerFlow.IssuanceRequester>(),
|
||||
startFlowPermission<net.corda.traderdemo.flow.SellerFlow>())
|
||||
val demoUser = listOf(User("demo", "demo", permissions))
|
||||
driver(driverDirectory = Paths.get("build") / "trader-demo-nodes", isDebug = true) {
|
||||
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
startNode("Bank A")
|
||||
startNode("Bank B")
|
||||
startNode("Bank A", rpcUsers = demoUser)
|
||||
startNode("Bank B", rpcUsers = demoUser)
|
||||
startNode(BOC.name, rpcUsers = listOf(user))
|
||||
waitForAllNodesToFinish()
|
||||
}, isDebug = true)
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,13 @@ package net.corda.traderdemo
|
||||
import com.google.common.net.HostAndPort
|
||||
import joptsimple.OptionParser
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.div
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.config.NodeSSLConfiguration
|
||||
import net.corda.node.services.messaging.CordaRPCClient
|
||||
import org.slf4j.Logger
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
@ -26,6 +31,7 @@ private class TraderDemo {
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val parser = OptionParser()
|
||||
val certsPath = parser.accepts("certificates").withRequiredArg()
|
||||
|
||||
val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required()
|
||||
val options = try {
|
||||
@ -40,9 +46,15 @@ private class TraderDemo {
|
||||
// will contact the buyer and actually make something happen.
|
||||
val role = options.valueOf(roleArg)!!
|
||||
if (role == Role.BUYER) {
|
||||
TraderDemoClientApi(HostAndPort.fromString("localhost:10005")).runBuyer()
|
||||
val host = HostAndPort.fromString("localhost:10004")
|
||||
CordaRPCClient(host, sslConfigFor("BankA", options.valueOf(certsPath))).use("demo", "demo") {
|
||||
TraderDemoClientApi(this).runBuyer()
|
||||
}
|
||||
} else {
|
||||
TraderDemoClientApi(HostAndPort.fromString("localhost:10007")).runSeller(1000.DOLLARS, "Bank A")
|
||||
val host = HostAndPort.fromString("localhost:10006")
|
||||
CordaRPCClient(host, sslConfigFor("BankB", options.valueOf(certsPath))).use("demo", "demo") {
|
||||
TraderDemoClientApi(this).runSeller(1000.DOLLARS, "Bank A")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,6 +66,13 @@ private class TraderDemo {
|
||||
""".trimIndent())
|
||||
parser.printHelpOn(System.out)
|
||||
}
|
||||
|
||||
// TODO: Take this out once we have a dedicated RPC port and allow SSL on it to be optional.
|
||||
private fun sslConfigFor(nodename: String, certsPath: String?): NodeSSLConfiguration {
|
||||
return object : NodeSSLConfiguration {
|
||||
override val keyStorePassword: String = "cordacadevpass"
|
||||
override val trustStorePassword: String = "trustpass"
|
||||
override val certificatesDirectory: Path = if (certsPath != null) Paths.get(certsPath) else Paths.get("build") / "nodes" / nodename / "certificates"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,26 +1,70 @@
|
||||
package net.corda.traderdemo
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import net.corda.contracts.testing.calculateRandomlySizedAmounts
|
||||
import net.corda.core.contracts.Amount
|
||||
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.IssuerFlow.IssuanceRequester
|
||||
import net.corda.node.services.messaging.CordaRPCClient
|
||||
import net.corda.testing.BOC
|
||||
import net.corda.testing.http.HttpApi
|
||||
import net.corda.traderdemo.flow.SellerFlow
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* Interface for communicating with nodes running the trader demo.
|
||||
*/
|
||||
class TraderDemoClientApi(hostAndPort: HostAndPort) {
|
||||
private val api = HttpApi.fromHostAndPort(hostAndPort, apiRoot)
|
||||
class TraderDemoClientApi(val rpc: CordaRPCOps) {
|
||||
private companion object {
|
||||
val logger = loggerFor<TraderDemoClientApi>()
|
||||
}
|
||||
|
||||
fun runBuyer(amount: Amount<Currency> = 30000.0.DOLLARS, notary: String = "Notary"): Boolean {
|
||||
return api.putJson("create-test-cash", mapOf("amount" to amount.quantity, "notary" to notary))
|
||||
val bankOfCordaParty = rpc.partyFromName(BOC.name)
|
||||
?: throw Exception("Unable to locate ${BOC.name} in Network Map Service")
|
||||
val me = rpc.nodeIdentity()
|
||||
// TODO: revert back to multiple issue request amounts (3,10) when soft locking implemented
|
||||
val amounts = calculateRandomlySizedAmounts(amount, 1, 1, Random())
|
||||
val handles = amounts.map {
|
||||
rpc.startFlow(::IssuanceRequester, amount, me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty)
|
||||
}
|
||||
|
||||
handles.forEach {
|
||||
require(it.returnValue.toBlocking().first() is SignedTransaction)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun runSeller(amount: Amount<Currency> = 1000.0.DOLLARS, counterparty: String): Boolean {
|
||||
return api.postJson("$counterparty/sell-cash", mapOf("amount" to amount.quantity))
|
||||
val otherParty = rpc.partyFromName(counterparty)
|
||||
if (otherParty != null) {
|
||||
// The seller will sell some commercial paper to the buyer, who will pay with (self issued) cash.
|
||||
//
|
||||
// The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an
|
||||
// attachment. Make sure we have the transaction prospectus attachment loaded into our store.
|
||||
//
|
||||
// This can also be done via an HTTP upload, but here we short-circuit and do it from code.
|
||||
if (!rpc.attachmentExists(SellerFlow.PROSPECTUS_HASH)) {
|
||||
javaClass.classLoader.getResourceAsStream("bank-of-london-cp.jar").use {
|
||||
val id = rpc.uploadAttachment(it)
|
||||
assertEquals(SellerFlow.PROSPECTUS_HASH, id)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val apiRoot = "api/traderdemo"
|
||||
// The line below blocks and waits for the future to resolve.
|
||||
val stx = rpc.startFlow(::SellerFlow, otherParty, amount).returnValue.toBlocking().first()
|
||||
logger.info("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(stx.tx)}")
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,77 +0,0 @@
|
||||
package net.corda.traderdemo.api
|
||||
|
||||
import net.corda.contracts.testing.calculateRandomlySizedAmounts
|
||||
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.IssuerFlow.IssuanceRequester
|
||||
import net.corda.testing.BOC
|
||||
import net.corda.traderdemo.flow.SellerFlow
|
||||
import java.util.*
|
||||
import javax.ws.rs.*
|
||||
import javax.ws.rs.core.MediaType
|
||||
import javax.ws.rs.core.Response
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
// API is accessible from /api/traderdemo. All paths specified below are relative to it.
|
||||
@Path("traderdemo")
|
||||
class TraderDemoApi(val rpc: CordaRPCOps) {
|
||||
data class TestCashParams(val amount: Int, val notary: String)
|
||||
data class SellParams(val amount: Int)
|
||||
private companion object {
|
||||
val logger = loggerFor<TraderDemoApi>()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 bankOfCordaParty = rpc.partyFromName(BOC.name)
|
||||
?: throw Exception("Unable to locate ${BOC.name} in Network Map Service")
|
||||
val me = rpc.nodeIdentity()
|
||||
// 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(::IssuanceRequester, params.amount.DOLLARS, me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty)
|
||||
}
|
||||
handles.forEach {
|
||||
require(it.returnValue.toBlocking().first() is SignedTransaction)
|
||||
}
|
||||
return Response.status(Response.Status.CREATED).build()
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{party}/sell-cash")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
fun sellCash(params: SellParams, @PathParam("party") partyName: String): Response {
|
||||
val otherParty = rpc.partyFromName(partyName)
|
||||
if (otherParty != null) {
|
||||
// The seller will sell some commercial paper to the buyer, who will pay with (self issued) cash.
|
||||
//
|
||||
// The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an
|
||||
// attachment. Make sure we have the transaction prospectus attachment loaded into our store.
|
||||
//
|
||||
// This can also be done via an HTTP upload, but here we short-circuit and do it from code.
|
||||
if (!rpc.attachmentExists(SellerFlow.PROSPECTUS_HASH)) {
|
||||
javaClass.classLoader.getResourceAsStream("bank-of-london-cp.jar").use {
|
||||
val id = rpc.uploadAttachment(it)
|
||||
assertEquals(SellerFlow.PROSPECTUS_HASH, id)
|
||||
}
|
||||
}
|
||||
|
||||
// The line below blocks and waits for the future to resolve.
|
||||
val stx = rpc.startFlow(::SellerFlow, otherParty, params.amount.DOLLARS).returnValue.toBlocking().first()
|
||||
logger.info("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(stx.tx)}")
|
||||
return Response.status(Response.Status.OK).build()
|
||||
} else {
|
||||
return Response.status(Response.Status.BAD_REQUEST).build()
|
||||
}
|
||||
}
|
||||
}
|
@ -5,14 +5,11 @@ 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
|
||||
import java.util.function.Function
|
||||
|
||||
class TraderDemoPlugin : CordaPluginRegistry() {
|
||||
// A list of classes that expose web APIs.
|
||||
override val webApis = listOf(Function(::TraderDemoApi))
|
||||
// A list of Flows that are required for this cordapp
|
||||
override val requiredFlows: Map<String, Set<String>> = mapOf(
|
||||
SellerFlow::class.java.name to setOf(Party::class.java.name, Amount::class.java.name)
|
||||
|
Loading…
Reference in New Issue
Block a user