Merge pull request #161 from corda/clint-traderdemorpc

Trader demo now uses RPC to communicate with nodes
This commit is contained in:
Clinton 2017-01-17 15:43:55 +00:00 committed by GitHub
commit 5d40a03e60
9 changed files with 105 additions and 103 deletions

View File

@ -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=&quot;build/trader-demo-nodes/Bank A/certificates&quot;" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />

View File

@ -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=&quot;build/trader-demo-nodes/Bank B/certificates&quot;" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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