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" /> <extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.traderdemo.TraderDemoKt" /> <option name="MAIN_CLASS_NAME" value="net.corda.traderdemo.TraderDemoKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " /> <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="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" /> <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" /> <option name="ALTERNATIVE_JRE_PATH" />

View File

@ -3,7 +3,7 @@
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" /> <extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.traderdemo.TraderDemoKt" /> <option name="MAIN_CLASS_NAME" value="net.corda.traderdemo.TraderDemoKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " /> <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="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" /> <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" /> <option name="ALTERNATIVE_JRE_PATH" />

View File

@ -67,6 +67,11 @@ dependencies {
} }
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) { 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" directory "./build/nodes"
// This name "Notary" is hard-coded into TraderDemoClientApi so if you change it here, change it there too. // 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. // 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 artemisPort 10004
webPort 10005 webPort 10005
cordapps = [] cordapps = []
rpcUsers = ext.rpcUsers
} }
node { node {
name "Bank B" name "Bank B"
@ -94,6 +100,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
artemisPort 10006 artemisPort 10006
webPort 10007 webPort 10007
cordapps = [] cordapps = []
rpcUsers = ext.rpcUsers
} }
node { node {
name "BankOfCorda" name "BankOfCorda"

View File

@ -13,16 +13,22 @@ import org.junit.Test
class TraderDemoTest { class TraderDemoTest {
@Test fun `runs trader demo`() { @Test fun `runs trader demo`() {
driver(isDebug = true) { 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 user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
val (nodeA, nodeB) = Futures.allAsList( val (nodeA, nodeB) = Futures.allAsList(
startNode("Bank A"), startNode("Bank A", rpcUsers = demoUser),
startNode("Bank B"), startNode("Bank B", rpcUsers = demoUser),
startNode("BankOfCorda", rpcUsers = listOf(user)), startNode("BankOfCorda", rpcUsers = listOf(user)),
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type))) startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
).getOrThrow() ).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(nodeARpc).runBuyer())
assert(TraderDemoClientApi(nodeB.configuration.webAddress).runSeller(counterparty = nodeA.nodeInfo.legalIdentity.name)) 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.startFlowPermission
import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.testing.BOC 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) * 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. * Do not use in a production environment.
*/ */
fun main(args: Array<String>) { 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>())) val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type))) startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
startNode("Bank A") startNode("Bank A", rpcUsers = demoUser)
startNode("Bank B") startNode("Bank B", rpcUsers = demoUser)
startNode(BOC.name, rpcUsers = listOf(user)) startNode(BOC.name, rpcUsers = listOf(user))
waitForAllNodesToFinish() waitForAllNodesToFinish()
}, isDebug = true) }
} }

View File

@ -3,8 +3,13 @@ package net.corda.traderdemo
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import joptsimple.OptionParser import joptsimple.OptionParser
import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.DOLLARS
import net.corda.core.div
import net.corda.core.utilities.loggerFor 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 org.slf4j.Logger
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.system.exitProcess import kotlin.system.exitProcess
/** /**
@ -26,6 +31,7 @@ private class TraderDemo {
fun main(args: Array<String>) { fun main(args: Array<String>) {
val parser = OptionParser() val parser = OptionParser()
val certsPath = parser.accepts("certificates").withRequiredArg()
val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required() val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required()
val options = try { val options = try {
@ -40,9 +46,15 @@ private class TraderDemo {
// will contact the buyer and actually make something happen. // will contact the buyer and actually make something happen.
val role = options.valueOf(roleArg)!! val role = options.valueOf(roleArg)!!
if (role == Role.BUYER) { 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 { } 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()) """.trimIndent())
parser.printHelpOn(System.out) 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 package net.corda.traderdemo
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import net.corda.contracts.testing.calculateRandomlySizedAmounts
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.DOLLARS 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.testing.http.HttpApi
import net.corda.traderdemo.flow.SellerFlow
import java.util.* import java.util.*
import kotlin.test.assertEquals
/** /**
* Interface for communicating with nodes running the trader demo. * Interface for communicating with nodes running the trader demo.
*/ */
class TraderDemoClientApi(hostAndPort: HostAndPort) { class TraderDemoClientApi(val rpc: CordaRPCOps) {
private val api = HttpApi.fromHostAndPort(hostAndPort, apiRoot) private companion object {
val logger = loggerFor<TraderDemoClientApi>()
}
fun runBuyer(amount: Amount<Currency> = 30000.0.DOLLARS, notary: String = "Notary"): Boolean { 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 { 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 { // The line below blocks and waits for the future to resolve.
private val apiRoot = "api/traderdemo" 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.node.CordaPluginRegistry
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.flows.IssuerFlow import net.corda.flows.IssuerFlow
import net.corda.traderdemo.api.TraderDemoApi
import net.corda.traderdemo.flow.BuyerFlow import net.corda.traderdemo.flow.BuyerFlow
import net.corda.traderdemo.flow.SellerFlow import net.corda.traderdemo.flow.SellerFlow
import java.util.function.Function import java.util.function.Function
class TraderDemoPlugin : CordaPluginRegistry() { 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 // A list of Flows that are required for this cordapp
override val requiredFlows: Map<String, Set<String>> = mapOf( override val requiredFlows: Map<String, Set<String>> = mapOf(
SellerFlow::class.java.name to setOf(Party::class.java.name, Amount::class.java.name) SellerFlow::class.java.name to setOf(Party::class.java.name, Amount::class.java.name)