Trader demo now uses RPC to node rather than via a HTTP server.

This commit is contained in:
Clinton Alexander
2017-01-17 11:41:20 +00:00
parent 429fbb3b97
commit f512bd2e71
5 changed files with 84 additions and 95 deletions

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

@ -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("nodea", 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("nodeb", 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)