mirror of
https://github.com/corda/corda.git
synced 2025-06-01 23:20:54 +00:00
Merged in irsdemotest (pull request #136)
Added integration build step and created two integration tests (IRS Demo and Trader Demo tests)
This commit is contained in:
commit
93ea1db17c
1
.idea/modules.xml
generated
1
.idea/modules.xml
generated
@ -21,6 +21,7 @@
|
|||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_main.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_main.iml" group="node" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_main.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_main.iml" group="node" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_test.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_test.iml" group="node" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_test.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_test.iml" group="node" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping.iml" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_integrationTest.iml" group="r3prototyping" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" group="r3prototyping" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" group="r3prototyping" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" group="r3prototyping" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" group="r3prototyping" />
|
||||||
</modules>
|
</modules>
|
||||||
|
29
build.gradle
29
build.gradle
@ -44,10 +44,23 @@ repositories {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
integrationTest {
|
||||||
|
kotlin {
|
||||||
|
compileClasspath += main.output + test.output
|
||||||
|
runtimeClasspath += main.output + test.output
|
||||||
|
srcDir file('src/integration-test/kotlin')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//noinspection GroovyAssignabilityCheck
|
//noinspection GroovyAssignabilityCheck
|
||||||
configurations {
|
configurations {
|
||||||
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
|
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
|
||||||
runtime.exclude module: 'isolated'
|
runtime.exclude module: 'isolated'
|
||||||
|
|
||||||
|
integrationTestCompile.extendsFrom testCompile
|
||||||
|
integrationTestRuntime.extendsFrom testRuntime
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is required for quasar. I think.
|
// This is required for quasar. I think.
|
||||||
@ -70,6 +83,10 @@ dependencies {
|
|||||||
testCompile 'junit:junit:4.12'
|
testCompile 'junit:junit:4.12'
|
||||||
testCompile 'org.assertj:assertj-core:3.4.1'
|
testCompile 'org.assertj:assertj-core:3.4.1'
|
||||||
testCompile 'com.pholser:junit-quickcheck-core:0.6'
|
testCompile 'com.pholser:junit-quickcheck-core:0.6'
|
||||||
|
|
||||||
|
// Integration test helpers
|
||||||
|
integrationTestCompile 'junit:junit:4.12'
|
||||||
|
integrationTestCompile 'org.assertj:assertj-core:3.4.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Package up the demo programs.
|
// Package up the demo programs.
|
||||||
@ -99,8 +116,7 @@ task getTraderDemo(type: CreateStartScripts) {
|
|||||||
|
|
||||||
// Force windows script classpath to wildcard path to avoid the 'Command Line Is Too Long' issues
|
// Force windows script classpath to wildcard path to avoid the 'Command Line Is Too Long' issues
|
||||||
// with generated scripts. Include Jolokia .war explicitly as this isn't picked up by wildcard
|
// with generated scripts. Include Jolokia .war explicitly as this isn't picked up by wildcard
|
||||||
tasks.withType(CreateStartScripts)
|
tasks.withType(CreateStartScripts) {
|
||||||
{
|
|
||||||
doLast {
|
doLast {
|
||||||
windowsScript.text = windowsScript
|
windowsScript.text = windowsScript
|
||||||
.readLines()
|
.readLines()
|
||||||
@ -109,6 +125,15 @@ tasks.withType(CreateStartScripts)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
task integrationTest(type: Test) {
|
||||||
|
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||||
|
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(Test) {
|
||||||
|
reports.html.destination = file("${reporting.baseDir}/${name}")
|
||||||
|
}
|
||||||
|
|
||||||
quasarScan.dependsOn('classes', 'core:classes', 'contracts:classes', 'node:classes')
|
quasarScan.dependsOn('classes', 'core:classes', 'contracts:classes', 'node:classes')
|
||||||
|
|
||||||
applicationDistribution.into("bin") {
|
applicationDistribution.into("bin") {
|
||||||
|
@ -4,7 +4,7 @@ package com.r3corda.core.utilities
|
|||||||
* A simple wrapper class that contains icons and support for printing them only when we're connected to a terminal.
|
* A simple wrapper class that contains icons and support for printing them only when we're connected to a terminal.
|
||||||
*/
|
*/
|
||||||
object Emoji {
|
object Emoji {
|
||||||
val hasEmojiTerminal by lazy { System.getenv("TERM") != null && System.getenv("LANG").contains("UTF-8") }
|
val hasEmojiTerminal by lazy { System.getenv("TERM") != null && (System.getenv("LANG")?.contains("UTF-8") == true) }
|
||||||
|
|
||||||
const val CODE_DIAMOND = "\ud83d\udd37"
|
const val CODE_DIAMOND = "\ud83d\udd37"
|
||||||
const val CODE_BAG_OF_CASH = "\ud83d\udcb0"
|
const val CODE_BAG_OF_CASH = "\ud83d\udcb0"
|
||||||
|
@ -11,6 +11,7 @@ import javax.ws.rs.GET
|
|||||||
import javax.ws.rs.Path
|
import javax.ws.rs.Path
|
||||||
import javax.ws.rs.Produces
|
import javax.ws.rs.Produces
|
||||||
import javax.ws.rs.core.MediaType
|
import javax.ws.rs.core.MediaType
|
||||||
|
import javax.ws.rs.core.Response
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Top level interface to external interaction with the distributed ledger.
|
* Top level interface to external interaction with the distributed ledger.
|
||||||
@ -30,6 +31,14 @@ interface APIServer {
|
|||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
fun serverTime(): LocalDateTime
|
fun serverTime(): LocalDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report whether this node is started up or not
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("status")
|
||||||
|
@Produces(MediaType.TEXT_PLAIN)
|
||||||
|
fun status(): Response
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query your "local" states (containing only outputs involving you) and return the hashes & indexes associated with them
|
* Query your "local" states (containing only outputs involving you) and return the hashes & indexes associated with them
|
||||||
* to probably be later inflated by fetchLedgerTransactions() or fetchStates() although because immutable you can cache them
|
* to probably be later inflated by fetchLedgerTransactions() or fetchStates() although because immutable you can cache them
|
||||||
|
@ -10,6 +10,7 @@ import com.r3corda.core.serialization.SerializedBytes
|
|||||||
import com.r3corda.node.api.*
|
import com.r3corda.node.api.*
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import javax.ws.rs.core.Response
|
||||||
import kotlin.reflect.KParameter
|
import kotlin.reflect.KParameter
|
||||||
import kotlin.reflect.jvm.javaType
|
import kotlin.reflect.jvm.javaType
|
||||||
|
|
||||||
@ -17,6 +18,14 @@ class APIServerImpl(val node: AbstractNode) : APIServer {
|
|||||||
|
|
||||||
override fun serverTime(): LocalDateTime = LocalDateTime.now(node.services.clock)
|
override fun serverTime(): LocalDateTime = LocalDateTime.now(node.services.clock)
|
||||||
|
|
||||||
|
override fun status(): Response {
|
||||||
|
return if (node.started) {
|
||||||
|
Response.ok("started").build()
|
||||||
|
} else {
|
||||||
|
Response.status(Response.Status.SERVICE_UNAVAILABLE).entity("not started").build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun queryStates(query: StatesQuery): List<StateRef> {
|
override fun queryStates(query: StatesQuery): List<StateRef> {
|
||||||
// We're going to hard code two options here for now and assume that all LinearStates are deals
|
// We're going to hard code two options here for now and assume that all LinearStates are deals
|
||||||
// Would like to maybe move to a model where we take something like a JEXL string, although don't want to develop
|
// Would like to maybe move to a model where we take something like a JEXL string, although don't want to develop
|
||||||
|
@ -25,6 +25,7 @@ import org.glassfish.jersey.server.ServerProperties
|
|||||||
import org.glassfish.jersey.servlet.ServletContainer
|
import org.glassfish.jersey.servlet.ServletContainer
|
||||||
import java.io.RandomAccessFile
|
import java.io.RandomAccessFile
|
||||||
import java.lang.management.ManagementFactory
|
import java.lang.management.ManagementFactory
|
||||||
|
import java.net.InetSocketAddress
|
||||||
import java.nio.channels.FileLock
|
import java.nio.channels.FileLock
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
@ -52,7 +53,7 @@ class ConfigurationException(message: String) : Exception(message)
|
|||||||
* Listed clientAPI classes are assumed to have to take a single APIServer constructor parameter
|
* Listed clientAPI classes are assumed to have to take a single APIServer constructor parameter
|
||||||
* @param clock The clock used within the node and by all protocols etc
|
* @param clock The clock used within the node and by all protocols etc
|
||||||
*/
|
*/
|
||||||
class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration,
|
class Node(dir: Path, val p2pAddr: HostAndPort, val webServerAddr: HostAndPort, configuration: NodeConfiguration,
|
||||||
networkMapAddress: NodeInfo?, advertisedServices: Set<ServiceType>,
|
networkMapAddress: NodeInfo?, advertisedServices: Set<ServiceType>,
|
||||||
clock: Clock = NodeClock(),
|
clock: Clock = NodeClock(),
|
||||||
val clientAPIs: List<Class<*>> = listOf()) : AbstractNode(dir, configuration, networkMapAddress, advertisedServices, clock) {
|
val clientAPIs: List<Class<*>> = listOf()) : AbstractNode(dir, configuration, networkMapAddress, advertisedServices, clock) {
|
||||||
@ -80,9 +81,7 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
|
|||||||
|
|
||||||
private fun initWebServer(): Server {
|
private fun initWebServer(): Server {
|
||||||
// Note that the web server handlers will all run concurrently, and not on the node thread.
|
// Note that the web server handlers will all run concurrently, and not on the node thread.
|
||||||
|
val server = Server(InetSocketAddress(webServerAddr.hostText, webServerAddr.port))
|
||||||
val port = p2pAddr.port + 1 // TODO: Move this into the node config file.
|
|
||||||
val server = Server(port)
|
|
||||||
|
|
||||||
val handlerCollection = HandlerCollection()
|
val handlerCollection = HandlerCollection()
|
||||||
|
|
||||||
|
@ -35,6 +35,12 @@ class DataUploadServlet : HttpServlet() {
|
|||||||
val upload = ServletFileUpload()
|
val upload = ServletFileUpload()
|
||||||
val iterator = upload.getItemIterator(req)
|
val iterator = upload.getItemIterator(req)
|
||||||
val messages = ArrayList<String>()
|
val messages = ArrayList<String>()
|
||||||
|
|
||||||
|
if (!iterator.hasNext()) {
|
||||||
|
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Got an upload request with no files")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val item = iterator.next()
|
val item = iterator.next()
|
||||||
if (item.name != null && !acceptor.acceptableFileExtensions.any { item.name.endsWith(it) }) {
|
if (item.name != null && !acceptor.acceptableFileExtensions.any { item.name.endsWith(it) }) {
|
||||||
|
@ -0,0 +1,94 @@
|
|||||||
|
package com.r3corda.core.testing
|
||||||
|
|
||||||
|
import com.google.common.net.HostAndPort
|
||||||
|
import com.r3corda.core.testing.utilities.*
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
class IRSDemoTest {
|
||||||
|
@Test fun `runs IRS demo`() {
|
||||||
|
val nodeAddrA = freeLocalHostAndPort()
|
||||||
|
val apiAddrA = freeLocalHostAndPort()
|
||||||
|
val apiAddrB = freeLocalHostAndPort()
|
||||||
|
|
||||||
|
val baseDirectory = Paths.get("./build/integration-test/${TestTimestamp.timestamp}/irs-demo")
|
||||||
|
var procA: Process? = null
|
||||||
|
var procB: Process? = null
|
||||||
|
try {
|
||||||
|
setupNode(baseDirectory, "NodeA")
|
||||||
|
setupNode(baseDirectory, "NodeB")
|
||||||
|
procA = startNode(
|
||||||
|
baseDirectory = baseDirectory,
|
||||||
|
nodeType = "NodeA",
|
||||||
|
nodeAddr = nodeAddrA,
|
||||||
|
networkMapAddr = apiAddrA,
|
||||||
|
apiAddr = apiAddrA
|
||||||
|
)
|
||||||
|
procB = startNode(
|
||||||
|
baseDirectory = baseDirectory,
|
||||||
|
nodeType = "NodeB",
|
||||||
|
nodeAddr = freeLocalHostAndPort(),
|
||||||
|
networkMapAddr = nodeAddrA,
|
||||||
|
apiAddr = apiAddrB
|
||||||
|
)
|
||||||
|
runTrade(apiAddrA)
|
||||||
|
runDateChange(apiAddrA)
|
||||||
|
} finally {
|
||||||
|
stopNode(procA)
|
||||||
|
stopNode(procB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupNode(baseDirectory: Path, nodeType: String) {
|
||||||
|
println("Running setup for $nodeType")
|
||||||
|
val args = listOf("--role", "Setup" + nodeType, "--base-directory", baseDirectory.toString())
|
||||||
|
val proc = spawn("com.r3corda.demos.IRSDemoKt", args, "IRSDemoSetup$nodeType")
|
||||||
|
assertExitOrKill(proc)
|
||||||
|
assertEquals(proc.exitValue(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startNode(baseDirectory: Path,
|
||||||
|
nodeType: String,
|
||||||
|
nodeAddr: HostAndPort,
|
||||||
|
networkMapAddr: HostAndPort,
|
||||||
|
apiAddr: HostAndPort): Process {
|
||||||
|
println("Running node $nodeType")
|
||||||
|
println("Node addr: $nodeAddr")
|
||||||
|
println("Network map addr: $networkMapAddr")
|
||||||
|
println("API addr: $apiAddr")
|
||||||
|
val args = listOf(
|
||||||
|
"--role", nodeType,
|
||||||
|
"--base-directory", baseDirectory.toString(),
|
||||||
|
"--network-address", nodeAddr.toString(),
|
||||||
|
"--network-map-address", networkMapAddr.toString(),
|
||||||
|
"--api-address", apiAddr.toString())
|
||||||
|
val proc = spawn("com.r3corda.demos.IRSDemoKt", args, "IRSDemo$nodeType")
|
||||||
|
NodeApi.ensureNodeStartsOrKill(proc, apiAddr)
|
||||||
|
return proc
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runTrade(nodeAddr: HostAndPort) {
|
||||||
|
println("Running trade")
|
||||||
|
val args = listOf("--role", "Trade", "trade1", "--api-address", nodeAddr.toString())
|
||||||
|
val proc = spawn("com.r3corda.demos.IRSDemoKt", args, "IRSDemoTrade")
|
||||||
|
assertExitOrKill(proc)
|
||||||
|
assertEquals(proc.exitValue(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runDateChange(nodeAddr: HostAndPort) {
|
||||||
|
println("Running date change")
|
||||||
|
val args = listOf("--role", "Date", "2017-01-02", "--api-address", nodeAddr.toString())
|
||||||
|
val proc = spawn("com.r3corda.demos.IRSDemoKt", args, "IRSDemoDate")
|
||||||
|
assertExitOrKill(proc)
|
||||||
|
assertEquals(proc.exitValue(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopNode(nodeProc: Process?) {
|
||||||
|
if (nodeProc != null) {
|
||||||
|
println("Stopping node")
|
||||||
|
assertAliveAndKill(nodeProc)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
package com.r3corda.core.testing
|
||||||
|
|
||||||
|
import com.google.common.net.HostAndPort
|
||||||
|
import com.r3corda.core.testing.utilities.NodeApi
|
||||||
|
import com.r3corda.core.testing.utilities.TestTimestamp
|
||||||
|
import com.r3corda.core.testing.utilities.assertExitOrKill
|
||||||
|
import com.r3corda.core.testing.utilities.spawn
|
||||||
|
import org.junit.Test
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class TraderDemoTest {
|
||||||
|
@Test fun `runs trader demo`() {
|
||||||
|
val buyerAddr = freeLocalHostAndPort()
|
||||||
|
val buyerApiAddr = freeLocalHostAndPort()
|
||||||
|
val directory = "./build/integration-test/${TestTimestamp.timestamp}/trader-demo"
|
||||||
|
var nodeProc: Process? = null
|
||||||
|
try {
|
||||||
|
nodeProc = runBuyer(directory, buyerAddr, buyerApiAddr)
|
||||||
|
runSeller(directory, buyerAddr)
|
||||||
|
} finally {
|
||||||
|
nodeProc?.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun runBuyer(baseDirectory: String, buyerAddr: HostAndPort, buyerApiAddr: HostAndPort): Process {
|
||||||
|
println("Running Buyer")
|
||||||
|
val args = listOf(
|
||||||
|
"--role", "BUYER",
|
||||||
|
"--network-address", buyerAddr.toString(),
|
||||||
|
"--api-address", buyerApiAddr.toString(),
|
||||||
|
"--base-directory", baseDirectory
|
||||||
|
)
|
||||||
|
val proc = spawn("com.r3corda.demos.TraderDemoKt", args, "TradeDemoBuyer")
|
||||||
|
NodeApi.ensureNodeStartsOrKill(proc, buyerApiAddr)
|
||||||
|
return proc
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runSeller(baseDirectory: String, buyerAddr: HostAndPort) {
|
||||||
|
println("Running Seller")
|
||||||
|
val sellerAddr = freeLocalHostAndPort()
|
||||||
|
val sellerApiAddr = freeLocalHostAndPort()
|
||||||
|
val args = listOf(
|
||||||
|
"--role", "SELLER",
|
||||||
|
"--network-address", sellerAddr.toString(),
|
||||||
|
"--api-address", sellerApiAddr.toString(),
|
||||||
|
"--other-network-address", buyerAddr.toString(),
|
||||||
|
"--base-directory", baseDirectory
|
||||||
|
)
|
||||||
|
val proc = spawn("com.r3corda.demos.TraderDemoKt", args, "TradeDemoSeller")
|
||||||
|
assertExitOrKill(proc);
|
||||||
|
assertEquals(proc.exitValue(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package com.r3corda.core.testing.utilities
|
||||||
|
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
fun spawn(className: String, args: List<String>, appName: String): Process {
|
||||||
|
val separator = System.getProperty("file.separator")
|
||||||
|
val classpath = System.getProperty("java.class.path")
|
||||||
|
val path = System.getProperty("java.home") + separator + "bin" + separator + "java"
|
||||||
|
val javaArgs = listOf(path, "-Dname=$appName", "-javaagent:lib/quasar.jar", "-cp", classpath, className)
|
||||||
|
val builder = ProcessBuilder(javaArgs + args)
|
||||||
|
builder.redirectError(Paths.get("error.$className.log").toFile())
|
||||||
|
builder.inheritIO()
|
||||||
|
val process = builder.start();
|
||||||
|
return process
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertExitOrKill(proc: Process) {
|
||||||
|
try {
|
||||||
|
assertEquals(proc.waitFor(2, TimeUnit.MINUTES), true)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
proc.destroyForcibly()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertAliveAndKill(proc: Process) {
|
||||||
|
try {
|
||||||
|
assertEquals(proc.isAlive, true)
|
||||||
|
} finally {
|
||||||
|
proc.destroyForcibly()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package com.r3corda.core.testing.utilities
|
||||||
|
|
||||||
|
import com.google.common.net.HostAndPort
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.net.ConnectException
|
||||||
|
import java.net.SocketException
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class NodeApi {
|
||||||
|
class NodeDidNotStartException(message: String): Exception(message)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val NODE_WAIT_RETRY_COUNT: Int = 50
|
||||||
|
val NODE_WAIT_RETRY_DELAY_MS: Long = 200
|
||||||
|
|
||||||
|
fun ensureNodeStartsOrKill(proc: Process, nodeWebserverAddr: HostAndPort) {
|
||||||
|
try {
|
||||||
|
assertEquals(proc.isAlive, true)
|
||||||
|
waitForNodeStartup(nodeWebserverAddr)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
println("Forcibly killing node process")
|
||||||
|
proc.destroyForcibly()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun waitForNodeStartup(nodeWebserverAddr: HostAndPort) {
|
||||||
|
val url = URL("http://${nodeWebserverAddr.toString()}/api/status")
|
||||||
|
var retries = 0
|
||||||
|
var respCode: Int
|
||||||
|
do {
|
||||||
|
retries++
|
||||||
|
val err = try {
|
||||||
|
val conn = url.openConnection() as HttpURLConnection
|
||||||
|
conn.requestMethod = "GET"
|
||||||
|
respCode = conn.responseCode
|
||||||
|
InputStreamReader(conn.inputStream).readLines().joinToString { it }
|
||||||
|
} catch(e: ConnectException) {
|
||||||
|
// This is to be expected while it loads up
|
||||||
|
respCode = 404
|
||||||
|
"Node hasn't started"
|
||||||
|
} catch(e: SocketException) {
|
||||||
|
respCode = -1
|
||||||
|
"Could not connect: ${e.toString()}"
|
||||||
|
} catch (e: IOException) {
|
||||||
|
respCode = -1
|
||||||
|
"IOException: ${e.toString()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retries > NODE_WAIT_RETRY_COUNT) {
|
||||||
|
throw NodeDidNotStartException("The node did not start: " + err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.sleep(NODE_WAIT_RETRY_DELAY_MS)
|
||||||
|
} while (respCode != 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package com.r3corda.core.testing.utilities
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [timestamp] holds a formatted (UTC) timestamp that's set the first time it is queried. This is used to
|
||||||
|
* provide a uniform timestamp for tests
|
||||||
|
*/
|
||||||
|
class TestTimestamp {
|
||||||
|
companion object {
|
||||||
|
val timestamp: String = {
|
||||||
|
val tz = TimeZone.getTimeZone("UTC")
|
||||||
|
val df = SimpleDateFormat("yyyy-MM-dd-HH:mm:ss")
|
||||||
|
df.timeZone = tz
|
||||||
|
df.format(Date())
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
@ -4,13 +4,13 @@ import com.google.common.net.HostAndPort
|
|||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.logElapsedTime
|
import com.r3corda.core.logElapsedTime
|
||||||
|
import com.r3corda.core.messaging.SingleMessageRecipient
|
||||||
import com.r3corda.node.internal.Node
|
import com.r3corda.node.internal.Node
|
||||||
import com.r3corda.node.services.config.NodeConfiguration
|
import com.r3corda.node.services.config.NodeConfiguration
|
||||||
import com.r3corda.node.services.config.NodeConfigurationFromConfig
|
import com.r3corda.node.services.config.NodeConfigurationFromConfig
|
||||||
import com.r3corda.core.node.NodeInfo
|
import com.r3corda.core.node.NodeInfo
|
||||||
import com.r3corda.node.services.network.NetworkMapService
|
import com.r3corda.node.services.network.NetworkMapService
|
||||||
import com.r3corda.node.services.clientapi.NodeInterestRates
|
import com.r3corda.node.services.clientapi.NodeInterestRates
|
||||||
import com.r3corda.node.services.transactions.NotaryService
|
|
||||||
import com.r3corda.core.node.services.ServiceType
|
import com.r3corda.core.node.services.ServiceType
|
||||||
import com.r3corda.node.services.messaging.ArtemisMessagingService
|
import com.r3corda.node.services.messaging.ArtemisMessagingService
|
||||||
import com.r3corda.core.serialization.deserialize
|
import com.r3corda.core.serialization.deserialize
|
||||||
@ -21,11 +21,9 @@ import com.r3corda.demos.protocols.ExitServerProtocol
|
|||||||
import com.r3corda.demos.protocols.UpdateBusinessDayProtocol
|
import com.r3corda.demos.protocols.UpdateBusinessDayProtocol
|
||||||
import com.r3corda.node.internal.AbstractNode
|
import com.r3corda.node.internal.AbstractNode
|
||||||
import com.r3corda.node.internal.testing.MockNetwork
|
import com.r3corda.node.internal.testing.MockNetwork
|
||||||
import com.r3corda.node.services.network.InMemoryMessagingNetwork
|
|
||||||
import com.r3corda.node.services.transactions.SimpleNotaryService
|
import com.r3corda.node.services.transactions.SimpleNotaryService
|
||||||
import joptsimple.OptionParser
|
import joptsimple.OptionParser
|
||||||
import joptsimple.OptionSet
|
import joptsimple.OptionSet
|
||||||
import joptsimple.OptionSpec
|
|
||||||
import java.io.DataOutputStream
|
import java.io.DataOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
@ -37,16 +35,19 @@ import java.util.*
|
|||||||
import kotlin.concurrent.fixedRateTimer
|
import kotlin.concurrent.fixedRateTimer
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
import java.io.FileNotFoundException
|
import java.net.SocketTimeoutException
|
||||||
|
|
||||||
// IRS DEMO
|
// IRS DEMO
|
||||||
//
|
//
|
||||||
// Please see docs/build/html/running-the-trading-demo.html
|
// Please see docs/build/html/running-the-trading-demo.html
|
||||||
//
|
|
||||||
// TODO: TBD
|
|
||||||
//
|
|
||||||
// The different roles in the scenario this program can adopt are:
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roles. There are 4 modes this demo can be run:
|
||||||
|
* - SetupNodeA/SetupNodeB: Creates and sets up the necessary directories for nodes
|
||||||
|
* - NodeA/NodeB: Starts the nodes themselves
|
||||||
|
* - Trade: Uploads an example trade
|
||||||
|
* - DateChange: Changes the demo's date
|
||||||
|
*/
|
||||||
enum class IRSDemoRole {
|
enum class IRSDemoRole {
|
||||||
SetupNodeA,
|
SetupNodeA,
|
||||||
SetupNodeB,
|
SetupNodeB,
|
||||||
@ -56,27 +57,185 @@ enum class IRSDemoRole {
|
|||||||
Date
|
Date
|
||||||
}
|
}
|
||||||
|
|
||||||
private class NodeParams() {
|
/**
|
||||||
var id: Int = -1
|
* Parsed command line parameters.
|
||||||
var dir : Path = Paths.get("")
|
*/
|
||||||
var address : String = ""
|
sealed class CliParams {
|
||||||
var mapAddress: String = ""
|
|
||||||
var identityFile: Path = Paths.get("")
|
/**
|
||||||
var tradeWithAddrs: List<String> = listOf()
|
* Corresponds to roles 'SetupNodeA' and 'SetupNodeB'
|
||||||
var tradeWithIdentities: List<Path> = listOf()
|
*/
|
||||||
var uploadRates: Boolean = false
|
class SetupNode(
|
||||||
var defaultLegalName: String = ""
|
val node: IRSDemoNode,
|
||||||
|
val dir: Path,
|
||||||
|
val defaultLegalName: String
|
||||||
|
) : CliParams()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Corresponds to roles 'NodeA' and 'NodeB'
|
||||||
|
*/
|
||||||
|
class RunNode(
|
||||||
|
val node: IRSDemoNode,
|
||||||
|
val dir: Path,
|
||||||
|
val networkAddress : HostAndPort,
|
||||||
|
val apiAddress: HostAndPort,
|
||||||
|
val mapAddress: String,
|
||||||
|
val identityFile: Path,
|
||||||
|
val tradeWithAddrs: List<String>,
|
||||||
|
val tradeWithIdentities: List<Path>,
|
||||||
|
val uploadRates: Boolean,
|
||||||
|
val defaultLegalName: String,
|
||||||
|
val autoSetup: Boolean // Run Setup for both nodes automatically with default arguments
|
||||||
|
) : CliParams()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Corresponds to role 'Trade'
|
||||||
|
*/
|
||||||
|
class Trade(
|
||||||
|
val apiAddress: HostAndPort,
|
||||||
|
val tradeId: String
|
||||||
|
) : CliParams()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Corresponds to role 'Date'
|
||||||
|
*/
|
||||||
|
class DateChange(
|
||||||
|
val apiAddress: HostAndPort,
|
||||||
|
val dateString: String
|
||||||
|
) : CliParams()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val defaultBaseDirectory = "./build/irs-demo"
|
||||||
|
|
||||||
|
fun legalName(node: IRSDemoNode) =
|
||||||
|
when (node) {
|
||||||
|
IRSDemoNode.NodeA -> "Bank A"
|
||||||
|
IRSDemoNode.NodeB -> "Bank B"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nodeDirectory(options: OptionSet, node: IRSDemoNode) =
|
||||||
|
Paths.get(options.valueOf(CliParamsSpec.baseDirectoryArg), node.name.decapitalize())
|
||||||
|
|
||||||
|
private fun parseSetupNode(options: OptionSet, node: IRSDemoNode): SetupNode {
|
||||||
|
return SetupNode(
|
||||||
|
node = node,
|
||||||
|
dir = nodeDirectory(options, node),
|
||||||
|
defaultLegalName = legalName(node)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun defaultNetworkPort(node: IRSDemoNode) =
|
||||||
|
when (node) {
|
||||||
|
IRSDemoNode.NodeA -> Node.DEFAULT_PORT
|
||||||
|
IRSDemoNode.NodeB -> Node.DEFAULT_PORT + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun defaultApiPort(node: IRSDemoNode) =
|
||||||
|
when (node) {
|
||||||
|
IRSDemoNode.NodeA -> Node.DEFAULT_PORT + 1
|
||||||
|
IRSDemoNode.NodeB -> Node.DEFAULT_PORT + 3
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseRunNode(options: OptionSet, node: IRSDemoNode): RunNode {
|
||||||
|
val dir = nodeDirectory(options, node)
|
||||||
|
|
||||||
|
return RunNode(
|
||||||
|
node = node,
|
||||||
|
dir = dir,
|
||||||
|
networkAddress = HostAndPort.fromString(options.valueOf(
|
||||||
|
CliParamsSpec.networkAddressArg.defaultsTo("localhost:${defaultNetworkPort(node)}")
|
||||||
|
)),
|
||||||
|
apiAddress = HostAndPort.fromString(options.valueOf(
|
||||||
|
CliParamsSpec.apiAddressArg.defaultsTo("localhost:${defaultApiPort(node)}")
|
||||||
|
)),
|
||||||
|
mapAddress = options.valueOf(CliParamsSpec.networkMapNetAddr),
|
||||||
|
identityFile = if (options.has(CliParamsSpec.networkMapIdentityFile)) {
|
||||||
|
Paths.get(options.valueOf(CliParamsSpec.networkMapIdentityFile))
|
||||||
|
} else {
|
||||||
|
dir.resolve(AbstractNode.PUBLIC_IDENTITY_FILE_NAME)
|
||||||
|
},
|
||||||
|
tradeWithAddrs = if (options.has(CliParamsSpec.fakeTradeWithAddr)) {
|
||||||
|
options.valuesOf(CliParamsSpec.fakeTradeWithAddr)
|
||||||
|
} else {
|
||||||
|
listOf("localhost:${defaultNetworkPort(node.other)}")
|
||||||
|
},
|
||||||
|
tradeWithIdentities = if (options.has(CliParamsSpec.fakeTradeWithIdentityFile)) {
|
||||||
|
options.valuesOf(CliParamsSpec.fakeTradeWithIdentityFile).map { Paths.get(it) }
|
||||||
|
} else {
|
||||||
|
listOf(nodeDirectory(options, node.other).resolve(AbstractNode.PUBLIC_IDENTITY_FILE_NAME))
|
||||||
|
},
|
||||||
|
uploadRates = node == IRSDemoNode.NodeB,
|
||||||
|
defaultLegalName = legalName(node),
|
||||||
|
autoSetup = !options.has(CliParamsSpec.baseDirectoryArg) && !options.has(CliParamsSpec.fakeTradeWithIdentityFile)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseTrade(options: OptionSet): Trade {
|
||||||
|
return Trade(
|
||||||
|
apiAddress = HostAndPort.fromString(options.valueOf(
|
||||||
|
CliParamsSpec.apiAddressArg.defaultsTo("localhost:${defaultApiPort(IRSDemoNode.NodeA)}")
|
||||||
|
)),
|
||||||
|
tradeId = options.valuesOf(CliParamsSpec.nonOptions).let {
|
||||||
|
if (it.size > 0) {
|
||||||
|
it[0]
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("Please provide a trade ID")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseDateChange(options: OptionSet): DateChange {
|
||||||
|
return DateChange(
|
||||||
|
apiAddress = HostAndPort.fromString(options.valueOf(CliParamsSpec.apiAddressArg)),
|
||||||
|
dateString = options.valuesOf(CliParamsSpec.nonOptions).let {
|
||||||
|
if (it.size > 0) {
|
||||||
|
it[0]
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("Please provide a date string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parse(options: OptionSet): CliParams {
|
||||||
|
val role = options.valueOf(CliParamsSpec.roleArg)!!
|
||||||
|
return when (role) {
|
||||||
|
IRSDemoRole.SetupNodeA -> parseSetupNode(options, IRSDemoNode.NodeA)
|
||||||
|
IRSDemoRole.SetupNodeB -> parseSetupNode(options, IRSDemoNode.NodeB)
|
||||||
|
IRSDemoRole.NodeA -> parseRunNode(options, IRSDemoNode.NodeA)
|
||||||
|
IRSDemoRole.NodeB -> parseRunNode(options, IRSDemoNode.NodeB)
|
||||||
|
IRSDemoRole.Trade -> parseTrade(options)
|
||||||
|
IRSDemoRole.Date -> parseDateChange(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DemoArgs() {
|
enum class IRSDemoNode {
|
||||||
lateinit var roleArg: OptionSpec<IRSDemoRole>
|
NodeA,
|
||||||
lateinit var networkAddressArg: OptionSpec<String>
|
NodeB;
|
||||||
lateinit var dirArg: OptionSpec<String>
|
|
||||||
lateinit var networkMapIdentityFile: OptionSpec<String>
|
val other: IRSDemoNode get() {
|
||||||
lateinit var networkMapNetAddr: OptionSpec<String>
|
return when (this) {
|
||||||
lateinit var fakeTradeWithAddr: OptionSpec<String>
|
NodeA -> NodeB
|
||||||
lateinit var fakeTradeWithIdentityFile: OptionSpec<String>
|
NodeB -> NodeA
|
||||||
lateinit var nonOptions: OptionSpec<String>
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object CliParamsSpec {
|
||||||
|
val parser = OptionParser()
|
||||||
|
val roleArg = parser.accepts("role").withRequiredArg().ofType(IRSDemoRole::class.java)
|
||||||
|
val networkAddressArg = parser.accepts("network-address").withOptionalArg().ofType(String::class.java)
|
||||||
|
val apiAddressArg = parser.accepts("api-address").withOptionalArg().ofType(String::class.java)
|
||||||
|
val baseDirectoryArg = parser.accepts("base-directory").withOptionalArg().defaultsTo(CliParams.defaultBaseDirectory)
|
||||||
|
val networkMapIdentityFile = parser.accepts("network-map-identity-file").withOptionalArg()
|
||||||
|
val networkMapNetAddr = parser.accepts("network-map-address").withRequiredArg().defaultsTo("localhost")
|
||||||
|
val fakeTradeWithAddr = parser.accepts("fake-trade-with-address").withOptionalArg()
|
||||||
|
val fakeTradeWithIdentityFile = parser.accepts("fake-trade-with-identity-file").withOptionalArg()
|
||||||
|
val nonOptions = parser.nonOptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
private class NotSetupException: Throwable {
|
private class NotSetupException: Throwable {
|
||||||
@ -84,180 +243,169 @@ private class NotSetupException: Throwable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
val parser = OptionParser()
|
exitProcess(runIRSDemo(args))
|
||||||
val demoArgs = setupArgs(parser)
|
}
|
||||||
val options = try {
|
|
||||||
parser.parse(*args)
|
fun runIRSDemo(args: Array<String>): Int {
|
||||||
|
val cliParams = try {
|
||||||
|
CliParams.parse(CliParamsSpec.parser.parse(*args))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println(e.message)
|
println(e)
|
||||||
printHelp()
|
printHelp()
|
||||||
exitProcess(1)
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suppress the Artemis MQ noise, and activate the demo logging.
|
// Suppress the Artemis MQ noise, and activate the demo logging.
|
||||||
BriefLogFormatter.initVerbose("+demo.irsdemo", "+api-call", "+platform.deal", "-org.apache.activemq")
|
BriefLogFormatter.initVerbose("+demo.irsdemo", "+api-call", "+platform.deal", "-org.apache.activemq")
|
||||||
|
|
||||||
val role = options.valueOf(demoArgs.roleArg)!!
|
return when (cliParams) {
|
||||||
if(role == IRSDemoRole.SetupNodeA) {
|
is CliParams.SetupNode -> setup(cliParams)
|
||||||
val nodeParams = configureNodeParams(IRSDemoRole.NodeA, demoArgs, options)
|
is CliParams.RunNode -> runNode(cliParams)
|
||||||
setup(nodeParams)
|
is CliParams.Trade -> runTrade(cliParams)
|
||||||
} else if(role == IRSDemoRole.SetupNodeB) {
|
is CliParams.DateChange -> runDateChange(cliParams)
|
||||||
val nodeParams = configureNodeParams(IRSDemoRole.NodeB, demoArgs, options)
|
|
||||||
setup(nodeParams)
|
|
||||||
} else if(role == IRSDemoRole.Trade) {
|
|
||||||
val tradeIdArgs = options.valuesOf(demoArgs.nonOptions)
|
|
||||||
if (tradeIdArgs.size > 0) {
|
|
||||||
val tradeId = tradeIdArgs[0]
|
|
||||||
val host = if (options.has(demoArgs.networkAddressArg)) {
|
|
||||||
options.valueOf(demoArgs.networkAddressArg)
|
|
||||||
} else {
|
|
||||||
"http://localhost:" + (Node.DEFAULT_PORT + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runTrade(tradeId, host)) {
|
|
||||||
exitProcess(0)
|
|
||||||
} else {
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
println("Please provide a trade ID")
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
} else if(role == IRSDemoRole.Date) {
|
|
||||||
val dateStrArgs = options.valuesOf(demoArgs.nonOptions)
|
|
||||||
if (dateStrArgs.size > 0) {
|
|
||||||
val dateStr = dateStrArgs[0]
|
|
||||||
val host = if (options.has(demoArgs.networkAddressArg)) {
|
|
||||||
options.valueOf(demoArgs.networkAddressArg)
|
|
||||||
} else {
|
|
||||||
"http://localhost:" + (Node.DEFAULT_PORT + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
runDateChange(dateStr, host)
|
|
||||||
} else {
|
|
||||||
println("Please provide a date")
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If these directory and identity file arguments aren't specified then we can assume a default setup and
|
|
||||||
// create everything that is needed without needing to run setup.
|
|
||||||
if(!options.has(demoArgs.dirArg) && !options.has(demoArgs.fakeTradeWithIdentityFile)) {
|
|
||||||
createNodeConfig(createNodeAParams());
|
|
||||||
createNodeConfig(createNodeBParams());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
runNode(configureNodeParams(role, demoArgs, options))
|
|
||||||
} catch (e: NotSetupException) {
|
|
||||||
println(e.message)
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupArgs(parser: OptionParser): DemoArgs {
|
private fun setup(params: CliParams.SetupNode): Int {
|
||||||
val args = DemoArgs()
|
val dirFile = params.dir.toFile()
|
||||||
|
if (!dirFile.exists()) {
|
||||||
|
dirFile.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
args.roleArg = parser.accepts("role").withRequiredArg().ofType(IRSDemoRole::class.java).required()
|
val configFile = params.dir.resolve("config").toFile()
|
||||||
args.networkAddressArg = parser.accepts("network-address").withOptionalArg()
|
val config = loadConfigFile(configFile, params.defaultLegalName)
|
||||||
args.dirArg = parser.accepts("directory").withOptionalArg()
|
if (!Files.exists(params.dir.resolve(AbstractNode.PUBLIC_IDENTITY_FILE_NAME))) {
|
||||||
args.networkMapIdentityFile = parser.accepts("network-map-identity-file").withOptionalArg()
|
createIdentities(params, config)
|
||||||
args.networkMapNetAddr = parser.accepts("network-map-address").withRequiredArg().defaultsTo("localhost")
|
}
|
||||||
// Use these to list one or more peers (again, will be superseded by discovery implementation)
|
return 0
|
||||||
args.fakeTradeWithAddr = parser.accepts("fake-trade-with-address").withOptionalArg()
|
|
||||||
args.fakeTradeWithIdentityFile = parser.accepts("fake-trade-with-identity-file").withOptionalArg()
|
|
||||||
args.nonOptions = parser.nonOptions().ofType(String::class.java)
|
|
||||||
|
|
||||||
return args
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setup(params: NodeParams) {
|
private fun defaultNodeSetupParams(node: IRSDemoNode): CliParams.SetupNode =
|
||||||
createNodeConfig(params)
|
CliParams.SetupNode(
|
||||||
}
|
node = node,
|
||||||
|
dir = Paths.get(CliParams.defaultBaseDirectory, node.name.decapitalize()),
|
||||||
|
defaultLegalName = CliParams.legalName(node)
|
||||||
|
)
|
||||||
|
|
||||||
private fun runDateChange(date: String, host: String) : Boolean {
|
private fun runNode(cliParams: CliParams.RunNode): Int {
|
||||||
val url = URL(host + "/api/irs/demodate")
|
if (cliParams.autoSetup) {
|
||||||
if(putJson(url, "\"" + date + "\"")) {
|
setup(defaultNodeSetupParams(IRSDemoNode.NodeA))
|
||||||
println("Date changed")
|
setup(defaultNodeSetupParams(IRSDemoNode.NodeB))
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
println("Date failed to change")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runTrade(tradeId: String, host: String) : Boolean {
|
|
||||||
println("Uploading tradeID " + tradeId)
|
|
||||||
val fileContents = IOUtils.toString(NodeParams::class.java.getResourceAsStream("example-irs-trade.json"))
|
|
||||||
val tradeFile = fileContents.replace("tradeXXX", tradeId)
|
|
||||||
val url = URL(host + "/api/irs/deals")
|
|
||||||
if(postJson(url, tradeFile)) {
|
|
||||||
println("Trade sent")
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
println("Trade failed to send")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun configureNodeParams(role: IRSDemoRole, args: DemoArgs, options: OptionSet): NodeParams {
|
|
||||||
val nodeParams = when (role) {
|
|
||||||
IRSDemoRole.NodeA -> createNodeAParams()
|
|
||||||
IRSDemoRole.NodeB -> createNodeBParams()
|
|
||||||
else -> {
|
|
||||||
throw IllegalArgumentException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeParams.mapAddress = options.valueOf(args.networkMapNetAddr)
|
|
||||||
if (options.has(args.dirArg)) {
|
|
||||||
nodeParams.dir = Paths.get(options.valueOf(args.dirArg))
|
|
||||||
}
|
|
||||||
if (options.has(args.networkAddressArg)) {
|
|
||||||
nodeParams.address = options.valueOf(args.networkAddressArg)
|
|
||||||
}
|
|
||||||
nodeParams.identityFile = if (options.has(args.networkMapIdentityFile)) {
|
|
||||||
Paths.get(options.valueOf(args.networkMapIdentityFile))
|
|
||||||
} else {
|
|
||||||
nodeParams.dir.resolve(AbstractNode.PUBLIC_IDENTITY_FILE_NAME)
|
|
||||||
}
|
|
||||||
if (options.has(args.fakeTradeWithIdentityFile)) {
|
|
||||||
nodeParams.tradeWithIdentities = options.valuesOf(args.fakeTradeWithIdentityFile).map { Paths.get(it) }
|
|
||||||
}
|
|
||||||
if (options.has(args.fakeTradeWithAddr)) {
|
|
||||||
nodeParams.tradeWithAddrs = options.valuesOf(args.fakeTradeWithAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodeParams
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runNode(nodeParams : NodeParams) : Unit {
|
|
||||||
val node = startNode(nodeParams)
|
|
||||||
// Register handlers for the demo
|
|
||||||
AutoOfferProtocol.Handler.register(node)
|
|
||||||
UpdateBusinessDayProtocol.Handler.register(node)
|
|
||||||
ExitServerProtocol.Handler.register(node)
|
|
||||||
|
|
||||||
if(nodeParams.uploadRates) {
|
|
||||||
runUploadRates()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) Thread.sleep(Long.MAX_VALUE)
|
val networkMap = createRecipient(cliParams.mapAddress)
|
||||||
} catch(e: InterruptedException) {
|
val destinations = cliParams.tradeWithAddrs.map({
|
||||||
node.stop()
|
createRecipient(it)
|
||||||
|
})
|
||||||
|
|
||||||
|
val node = startNode(cliParams, networkMap, destinations)
|
||||||
|
// Register handlers for the demo
|
||||||
|
AutoOfferProtocol.Handler.register(node)
|
||||||
|
UpdateBusinessDayProtocol.Handler.register(node)
|
||||||
|
ExitServerProtocol.Handler.register(node)
|
||||||
|
|
||||||
|
if (cliParams.uploadRates) {
|
||||||
|
runUploadRates(cliParams.apiAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) Thread.sleep(Long.MAX_VALUE)
|
||||||
|
} catch(e: InterruptedException) {
|
||||||
|
node.stop()
|
||||||
|
}
|
||||||
|
} catch (e: NotSetupException) {
|
||||||
|
println(e.message)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runDateChange(cliParams: CliParams.DateChange): Int {
|
||||||
|
println("Changing date to " + cliParams.dateString)
|
||||||
|
val url = URL("http://${cliParams.apiAddress}/api/irs/demodate")
|
||||||
|
if (putJson(url, "\"" + cliParams.dateString + "\"")) {
|
||||||
|
println("Date changed")
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
println("Date failed to change")
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun runUploadRates() {
|
private fun runTrade(cliParams: CliParams.Trade): Int {
|
||||||
val fileContents = IOUtils.toString(NodeParams::class.java.getResource("example.rates.txt"))
|
println("Uploading tradeID " + cliParams.tradeId)
|
||||||
|
// Note: the getResourceAsStream is an ugly hack to get the jvm to search in the right location
|
||||||
|
val fileContents = IOUtils.toString(CliParams::class.java.getResourceAsStream("example-irs-trade.json"))
|
||||||
|
val tradeFile = fileContents.replace("tradeXXX", cliParams.tradeId)
|
||||||
|
val url = URL("http://${cliParams.apiAddress}/api/irs/deals")
|
||||||
|
if (postJson(url, tradeFile)) {
|
||||||
|
println("Trade sent")
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
println("Trade failed to send")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRecipient(addr: String) : SingleMessageRecipient {
|
||||||
|
val hostAndPort = HostAndPort.fromString(addr).withDefaultPort(Node.DEFAULT_PORT)
|
||||||
|
return ArtemisMessagingService.makeRecipient(hostAndPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startNode(params: CliParams.RunNode, networkMap: SingleMessageRecipient, recipients: List<SingleMessageRecipient>) : Node {
|
||||||
|
val config = getNodeConfig(params)
|
||||||
|
val advertisedServices: Set<ServiceType>
|
||||||
|
val networkMapId =
|
||||||
|
when (params.node) {
|
||||||
|
IRSDemoNode.NodeA -> {
|
||||||
|
advertisedServices = setOf(NetworkMapService.Type, SimpleNotaryService.Type)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
IRSDemoNode.NodeB -> {
|
||||||
|
advertisedServices = setOf(NodeInterestRates.Type)
|
||||||
|
nodeInfo(networkMap, params.identityFile, setOf(NetworkMapService.Type, SimpleNotaryService.Type))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val node = logElapsedTime("Node startup") {
|
||||||
|
Node(params.dir, params.networkAddress, params.apiAddress, config, networkMapId, advertisedServices, DemoClock(),
|
||||||
|
listOf(InterestRateSwapAPI::class.java)).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This should all be replaced by the identity service being updated
|
||||||
|
// as the network map changes.
|
||||||
|
if (params.tradeWithAddrs.size != params.tradeWithIdentities.size) {
|
||||||
|
throw IllegalArgumentException("Different number of peer addresses (${params.tradeWithAddrs.size}) and identities (${params.tradeWithIdentities.size})")
|
||||||
|
}
|
||||||
|
for ((recipient, identityFile) in recipients.zip(params.tradeWithIdentities)) {
|
||||||
|
val peerId = nodeInfo(recipient, identityFile)
|
||||||
|
node.services.identityService.registerIdentity(peerId.identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nodeInfo(recipient: SingleMessageRecipient, identityFile: Path, advertisedServices: Set<ServiceType> = emptySet()): NodeInfo {
|
||||||
|
try {
|
||||||
|
val path = identityFile
|
||||||
|
val party = Files.readAllBytes(path).deserialize<Party>()
|
||||||
|
return NodeInfo(recipient, party, advertisedServices)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Could not find identify file $identityFile.")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runUploadRates(host: HostAndPort) {
|
||||||
|
// Note: the getResourceAsStream is an ugly hack to get the jvm to search in the right location
|
||||||
|
val fileContents = IOUtils.toString(CliParams::class.java.getResourceAsStream("example.rates.txt"))
|
||||||
var timer : Timer? = null
|
var timer : Timer? = null
|
||||||
timer = fixedRateTimer("upload-rates", false, 0, 5000, {
|
timer = fixedRateTimer("upload-rates", false, 0, 5000, {
|
||||||
try {
|
try {
|
||||||
val url = URL("http://localhost:31341/upload/interest-rates")
|
val url = URL("http://${host.toString()}/upload/interest-rates")
|
||||||
if(uploadFile(url, fileContents)) {
|
if (uploadFile(url, fileContents)) {
|
||||||
timer!!.cancel()
|
timer!!.cancel()
|
||||||
println("Rates uploaded successfully")
|
println("Rates uploaded successfully")
|
||||||
} else {
|
} else {
|
||||||
@ -276,22 +424,28 @@ private fun sendJson(url: URL, data: String, method: String) : Boolean {
|
|||||||
connection.useCaches = false
|
connection.useCaches = false
|
||||||
connection.requestMethod = method
|
connection.requestMethod = method
|
||||||
connection.connectTimeout = 5000
|
connection.connectTimeout = 5000
|
||||||
connection.readTimeout = 5000
|
connection.readTimeout = 60000
|
||||||
connection.setRequestProperty("Connection", "Keep-Alive")
|
connection.setRequestProperty("Connection", "Keep-Alive")
|
||||||
connection.setRequestProperty("Cache-Control", "no-cache")
|
connection.setRequestProperty("Cache-Control", "no-cache")
|
||||||
connection.setRequestProperty("Content-Type", "application/json")
|
connection.setRequestProperty("Content-Type", "application/json")
|
||||||
connection.setRequestProperty("Content-Length", data.length.toString())
|
connection.setRequestProperty("Content-Length", data.length.toString())
|
||||||
val outStream = DataOutputStream(connection.outputStream)
|
|
||||||
outStream.writeBytes(data)
|
|
||||||
outStream.close()
|
|
||||||
|
|
||||||
return when(connection.responseCode) {
|
try {
|
||||||
200 -> true
|
val outStream = DataOutputStream(connection.outputStream)
|
||||||
201 -> true
|
outStream.writeBytes(data)
|
||||||
else -> {
|
outStream.close()
|
||||||
println("Failed to " + method + " data. Status Code: " + connection.responseCode + ". Mesage: " + connection.responseMessage)
|
|
||||||
false
|
return when (connection.responseCode) {
|
||||||
|
200 -> true
|
||||||
|
201 -> true
|
||||||
|
else -> {
|
||||||
|
println("Failed to " + method + " data. Status Code: " + connection.responseCode + ". Message: " + connection.responseMessage)
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch(e: SocketTimeoutException) {
|
||||||
|
println("Server took too long to respond")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,14 +469,14 @@ private fun uploadFile(url: URL, file: String) : Boolean {
|
|||||||
connection.useCaches = false
|
connection.useCaches = false
|
||||||
connection.requestMethod = "POST"
|
connection.requestMethod = "POST"
|
||||||
connection.connectTimeout = 5000
|
connection.connectTimeout = 5000
|
||||||
connection.readTimeout = 5000
|
connection.readTimeout = 60000
|
||||||
connection.setRequestProperty("Connection", "Keep-Alive")
|
connection.setRequestProperty("Connection", "Keep-Alive")
|
||||||
connection.setRequestProperty("Cache-Control", "no-cache")
|
connection.setRequestProperty("Cache-Control", "no-cache")
|
||||||
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary)
|
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary)
|
||||||
|
|
||||||
val request = DataOutputStream(connection.outputStream)
|
val request = DataOutputStream(connection.outputStream)
|
||||||
request.writeBytes(hyphens + boundary + clrf)
|
request.writeBytes(hyphens + boundary + clrf)
|
||||||
request.writeBytes("Content-Disposition: form-data; name=\"rates\" filename=\"example.rates.txt\"" + clrf)
|
request.writeBytes("Content-Disposition: form-data; name=\"rates\" filename=\"example.rates.txt\"$clrf")
|
||||||
request.writeBytes(clrf)
|
request.writeBytes(clrf)
|
||||||
request.writeBytes(file)
|
request.writeBytes(file)
|
||||||
request.writeBytes(clrf)
|
request.writeBytes(clrf)
|
||||||
@ -331,122 +485,22 @@ private fun uploadFile(url: URL, file: String) : Boolean {
|
|||||||
if (connection.responseCode == 200) {
|
if (connection.responseCode == 200) {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
println("Could not upload file. Status Code: " + connection + ". Mesage: " + connection.responseMessage)
|
println("Could not upload file. Status Code: " + connection + ". Message: " + connection.responseMessage)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNodeAParams() : NodeParams {
|
private fun getNodeConfig(cliParams: CliParams.RunNode): NodeConfiguration {
|
||||||
val params = NodeParams()
|
if (!Files.exists(cliParams.dir)) {
|
||||||
params.id = 0
|
|
||||||
params.dir = Paths.get("nodeA")
|
|
||||||
params.address = "localhost"
|
|
||||||
params.tradeWithAddrs = listOf("localhost:31340")
|
|
||||||
params.tradeWithIdentities = listOf(getRoleDir(IRSDemoRole.NodeB).resolve(AbstractNode.PUBLIC_IDENTITY_FILE_NAME))
|
|
||||||
params.defaultLegalName = "Bank A"
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createNodeBParams() : NodeParams {
|
|
||||||
val params = NodeParams()
|
|
||||||
params.id = 1
|
|
||||||
params.dir = Paths.get("nodeB")
|
|
||||||
params.address = "localhost:31340"
|
|
||||||
params.tradeWithAddrs = listOf("localhost")
|
|
||||||
params.tradeWithIdentities = listOf(getRoleDir(IRSDemoRole.NodeA).resolve(AbstractNode.PUBLIC_IDENTITY_FILE_NAME))
|
|
||||||
params.defaultLegalName = "Bank B"
|
|
||||||
params.uploadRates = true
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createNodeConfig(params: NodeParams) : NodeConfiguration {
|
|
||||||
if (!Files.exists(params.dir)) {
|
|
||||||
Files.createDirectory(params.dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
val configFile = params.dir.resolve("config").toFile()
|
|
||||||
val config = loadConfigFile(configFile, params.defaultLegalName)
|
|
||||||
if(!Files.exists(params.dir.resolve(AbstractNode.PUBLIC_IDENTITY_FILE_NAME))) {
|
|
||||||
createIdentities(params, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNodeConfig(params: NodeParams): NodeConfiguration {
|
|
||||||
if(!Files.exists(params.dir)) {
|
|
||||||
throw NotSetupException("Missing config directory. Please run node setup before running the node")
|
throw NotSetupException("Missing config directory. Please run node setup before running the node")
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!Files.exists(params.dir.resolve(AbstractNode.PUBLIC_IDENTITY_FILE_NAME))) {
|
if (!Files.exists(cliParams.dir.resolve(AbstractNode.PUBLIC_IDENTITY_FILE_NAME))) {
|
||||||
throw NotSetupException("Missing identity file. Please run node setup before running the node")
|
throw NotSetupException("Missing identity file. Please run node setup before running the node")
|
||||||
}
|
}
|
||||||
|
|
||||||
val configFile = params.dir.resolve("config").toFile()
|
val configFile = cliParams.dir.resolve("config").toFile()
|
||||||
return loadConfigFile(configFile, params.defaultLegalName)
|
return loadConfigFile(configFile, cliParams.defaultLegalName)
|
||||||
}
|
|
||||||
|
|
||||||
private fun startNode(params : NodeParams) : Node {
|
|
||||||
val config = getNodeConfig(params)
|
|
||||||
val advertisedServices: Set<ServiceType>
|
|
||||||
val myNetAddr = HostAndPort.fromString(params.address).withDefaultPort(Node.DEFAULT_PORT)
|
|
||||||
val networkMapId = if (params.mapAddress.equals(params.address)) {
|
|
||||||
// This node provides network map and notary services
|
|
||||||
advertisedServices = setOf(NetworkMapService.Type, SimpleNotaryService.Type)
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
advertisedServices = setOf(NodeInterestRates.Type)
|
|
||||||
nodeInfo(params.mapAddress, params.identityFile, setOf(NetworkMapService.Type, SimpleNotaryService.Type))
|
|
||||||
}
|
|
||||||
|
|
||||||
val node = logElapsedTime("Node startup") { Node(params.dir, myNetAddr, config, networkMapId,
|
|
||||||
advertisedServices, DemoClock(),
|
|
||||||
listOf(InterestRateSwapAPI::class.java)).setup().start() }
|
|
||||||
|
|
||||||
// TODO: This should all be replaced by the identity service being updated
|
|
||||||
// as the network map changes.
|
|
||||||
if (params.tradeWithAddrs.size != params.tradeWithIdentities.size) {
|
|
||||||
throw IllegalArgumentException("Different number of peer addresses (${params.tradeWithAddrs.size}) and identities (${params.tradeWithIdentities.size})")
|
|
||||||
}
|
|
||||||
for ((hostAndPortString, identityFile) in params.tradeWithAddrs.zip(params.tradeWithIdentities)) {
|
|
||||||
val peerId = nodeInfo(hostAndPortString, identityFile)
|
|
||||||
node.services.identityService.registerIdentity(peerId.identity)
|
|
||||||
}
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRoleDir(role: IRSDemoRole) : Path {
|
|
||||||
when(role) {
|
|
||||||
IRSDemoRole.NodeA -> return Paths.get("nodeA")
|
|
||||||
IRSDemoRole.NodeB -> return Paths.get("nodeB")
|
|
||||||
else -> {
|
|
||||||
throw IllegalArgumentException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun nodeInfo(hostAndPortString: String, identityFile: Path, advertisedServices: Set<ServiceType> = emptySet()): NodeInfo {
|
|
||||||
try {
|
|
||||||
val addr = HostAndPort.fromString(hostAndPortString).withDefaultPort(Node.DEFAULT_PORT)
|
|
||||||
val path = identityFile
|
|
||||||
val party = Files.readAllBytes(path).deserialize<Party>()
|
|
||||||
return NodeInfo(ArtemisMessagingService.makeRecipient(addr), party, advertisedServices)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("Could not find identify file $identityFile.")
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun nodeInfo(handle: InMemoryMessagingNetwork.Handle, identityFile: Path, advertisedServices: Set<ServiceType> = emptySet()): NodeInfo {
|
|
||||||
try {
|
|
||||||
val path = identityFile
|
|
||||||
val party = Files.readAllBytes(path).deserialize<Party>()
|
|
||||||
return NodeInfo(handle, party, advertisedServices)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("Could not find identify file $identityFile.")
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadConfigFile(configFile: File, defaultLegalName: String): NodeConfiguration {
|
private fun loadConfigFile(configFile: File, defaultLegalName: String): NodeConfiguration {
|
||||||
@ -459,9 +513,9 @@ private fun loadConfigFile(configFile: File, defaultLegalName: String): NodeConf
|
|||||||
return NodeConfigurationFromConfig(config)
|
return NodeConfigurationFromConfig(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createIdentities(params: NodeParams, nodeConf: NodeConfiguration) {
|
private fun createIdentities(params: CliParams.SetupNode, nodeConf: NodeConfiguration) {
|
||||||
val mockNetwork = MockNetwork(false)
|
val mockNetwork = MockNetwork(false)
|
||||||
val node = MockNetwork.MockNode(params.dir, nodeConf, mockNetwork, null, setOf(NetworkMapService.Type, SimpleNotaryService.Type), params.id, null)
|
val node = MockNetwork.MockNode(params.dir, nodeConf, mockNetwork, null, setOf(NetworkMapService.Type, SimpleNotaryService.Type), 0, null)
|
||||||
node.start()
|
node.start()
|
||||||
node.stop()
|
node.stop()
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.r3corda.demos
|
package com.r3corda.demos
|
||||||
|
|
||||||
|
import com.google.common.net.HostAndPort
|
||||||
import com.r3corda.contracts.cash.Cash
|
import com.r3corda.contracts.cash.Cash
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
@ -71,7 +72,9 @@ fun main(args: Array<String>) {
|
|||||||
override val nearestCity: String = "Atlantis"
|
override val nearestCity: String = "Atlantis"
|
||||||
}
|
}
|
||||||
|
|
||||||
val node = logElapsedTime("Node startup") { Node(dir, myNetAddr, config, networkMapAddress,
|
val apiAddr = HostAndPort.fromParts(myNetAddr.hostText, myNetAddr.port + 1)
|
||||||
|
|
||||||
|
val node = logElapsedTime("Node startup") { Node(dir, myNetAddr, apiAddr, config, networkMapAddress,
|
||||||
advertisedServices, DemoClock(),
|
advertisedServices, DemoClock(),
|
||||||
listOf(InterestRateSwapAPI::class.java)).setup().start() }
|
listOf(InterestRateSwapAPI::class.java)).setup().start() }
|
||||||
|
|
||||||
@ -89,4 +92,4 @@ fun main(args: Array<String>) {
|
|||||||
println()
|
println()
|
||||||
print(Emoji.renderIfSupported(tx.toWireTransaction()))
|
print(Emoji.renderIfSupported(tx.toWireTransaction()))
|
||||||
println(tx.toSignedTransaction().sigs)
|
println(tx.toSignedTransaction().sigs)
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,6 @@ import com.r3corda.protocols.NotaryProtocol
|
|||||||
import com.r3corda.protocols.TwoPartyTradeProtocol
|
import com.r3corda.protocols.TwoPartyTradeProtocol
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
import joptsimple.OptionParser
|
import joptsimple.OptionParser
|
||||||
import joptsimple.OptionSet
|
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
@ -65,16 +64,30 @@ enum class Role {
|
|||||||
|
|
||||||
// And this is the directory under the current working directory where each node will create its own server directory,
|
// And this is the directory under the current working directory where each node will create its own server directory,
|
||||||
// which holds things like checkpoints, keys, databases, message logs etc.
|
// which holds things like checkpoints, keys, databases, message logs etc.
|
||||||
val DIRNAME = "trader-demo"
|
val DEFAULT_BASE_DIRECTORY = "./build/trader-demo"
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
|
exitProcess(runTraderDemo(args))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runTraderDemo(args: Array<String>): Int {
|
||||||
|
val cashIssuerKey = generateKeyPair()
|
||||||
val parser = OptionParser()
|
val parser = OptionParser()
|
||||||
|
|
||||||
val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required()
|
val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required()
|
||||||
val myNetworkAddress = parser.accepts("network-address").withRequiredArg().defaultsTo("localhost")
|
val myNetworkAddress = parser.accepts("network-address").withRequiredArg().defaultsTo("localhost")
|
||||||
val theirNetworkAddress = parser.accepts("other-network-address").withRequiredArg().defaultsTo("localhost")
|
val theirNetworkAddress = parser.accepts("other-network-address").withRequiredArg().defaultsTo("localhost")
|
||||||
|
val apiNetworkAddress = parser.accepts("api-address").withRequiredArg().defaultsTo("localhost")
|
||||||
|
val baseDirectoryArg = parser.accepts("base-directory").withRequiredArg().defaultsTo(DEFAULT_BASE_DIRECTORY)
|
||||||
|
|
||||||
|
val options = try {
|
||||||
|
parser.parse(*args)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println(e.message)
|
||||||
|
println("Please refer to the documentation in docs/build/index.html to learn how to run the demo.")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
val options = parseOptions(args, parser)
|
|
||||||
val role = options.valueOf(roleArg)!!
|
val role = options.valueOf(roleArg)!!
|
||||||
|
|
||||||
val myNetAddr = HostAndPort.fromString(options.valueOf(myNetworkAddress)).withDefaultPort(
|
val myNetAddr = HostAndPort.fromString(options.valueOf(myNetworkAddress)).withDefaultPort(
|
||||||
@ -89,6 +102,9 @@ fun main(args: Array<String>) {
|
|||||||
Role.SELLER -> 31337
|
Role.SELLER -> 31337
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
val apiNetAddr = HostAndPort.fromString(options.valueOf(apiNetworkAddress)).withDefaultPort(myNetAddr.port + 1)
|
||||||
|
|
||||||
|
val baseDirectory = options.valueOf(baseDirectoryArg)!!
|
||||||
|
|
||||||
// Suppress the Artemis MQ noise, and activate the demo logging.
|
// Suppress the Artemis MQ noise, and activate the demo logging.
|
||||||
//
|
//
|
||||||
@ -96,7 +112,8 @@ fun main(args: Array<String>) {
|
|||||||
// for protocols will change in future.
|
// for protocols will change in future.
|
||||||
BriefLogFormatter.initVerbose("+demo.buyer", "+demo.seller", "-org.apache.activemq")
|
BriefLogFormatter.initVerbose("+demo.buyer", "+demo.seller", "-org.apache.activemq")
|
||||||
|
|
||||||
val directory = Paths.get(DIRNAME, role.name.toLowerCase())
|
val directory = Paths.get(baseDirectory, role.name.toLowerCase())
|
||||||
|
println("Using base demo directory $directory")
|
||||||
|
|
||||||
// Override the default config file (which you can find in the file "reference.conf") to give each node a name.
|
// Override the default config file (which you can find in the file "reference.conf") to give each node a name.
|
||||||
val config = run {
|
val config = run {
|
||||||
@ -123,7 +140,7 @@ fun main(args: Array<String>) {
|
|||||||
// be a single shared map service (this is analagous to the DNS seeds in Bitcoin).
|
// be a single shared map service (this is analagous to the DNS seeds in Bitcoin).
|
||||||
//
|
//
|
||||||
// TODO: AbstractNode should write out the full NodeInfo object and we should just load it here.
|
// TODO: AbstractNode should write out the full NodeInfo object and we should just load it here.
|
||||||
val path = Paths.get(DIRNAME, Role.BUYER.name.toLowerCase(), "identity-public")
|
val path = Paths.get(baseDirectory, Role.BUYER.name.toLowerCase(), "identity-public")
|
||||||
val party = Files.readAllBytes(path).deserialize<Party>()
|
val party = Files.readAllBytes(path).deserialize<Party>()
|
||||||
advertisedServices = emptySet()
|
advertisedServices = emptySet()
|
||||||
cashIssuer = party
|
cashIssuer = party
|
||||||
@ -132,7 +149,7 @@ fun main(args: Array<String>) {
|
|||||||
|
|
||||||
// And now construct then start the node object. It takes a little while.
|
// And now construct then start the node object. It takes a little while.
|
||||||
val node = logElapsedTime("Node startup") {
|
val node = logElapsedTime("Node startup") {
|
||||||
Node(directory, myNetAddr, config, networkMapId, advertisedServices).setup().start()
|
Node(directory, myNetAddr, apiNetAddr, config, networkMapId, advertisedServices).setup().start()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Replace with a separate trusted cash issuer
|
// TODO: Replace with a separate trusted cash issuer
|
||||||
@ -146,21 +163,17 @@ fun main(args: Array<String>) {
|
|||||||
if (role == Role.BUYER) {
|
if (role == Role.BUYER) {
|
||||||
runBuyer(node, amount)
|
runBuyer(node, amount)
|
||||||
} else {
|
} else {
|
||||||
runSeller(myNetAddr, node, theirNetAddr, amount)
|
val recipient = ArtemisMessagingService.makeRecipient(theirNetAddr)
|
||||||
|
runSeller(myNetAddr, node, recipient, amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseOptions(args: Array<String>, parser: OptionParser): OptionSet {
|
private fun runSeller(myNetAddr: HostAndPort,
|
||||||
try {
|
node: Node,
|
||||||
return parser.parse(*args)
|
recipient: SingleMessageRecipient,
|
||||||
} catch (e: Exception) {
|
amount: Amount<Issued<Currency>>) {
|
||||||
println(e.message)
|
|
||||||
println("Please refer to the documentation in docs/build/index.html to learn how to run the demo.")
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun runSeller(myNetAddr: HostAndPort, node: Node, theirNetAddr: HostAndPort, amount: Amount<Issued<Currency>>) {
|
|
||||||
// The seller will sell some commercial paper to the buyer, who will pay with (self issued) cash.
|
// 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
|
// The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an
|
||||||
@ -179,15 +192,14 @@ fun runSeller(myNetAddr: HostAndPort, node: Node, theirNetAddr: HostAndPort, amo
|
|||||||
it.second.get()
|
it.second.get()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val otherSide = ArtemisMessagingService.makeRecipient(theirNetAddr)
|
val seller = TraderDemoProtocolSeller(myNetAddr, recipient, amount)
|
||||||
val seller = TraderDemoProtocolSeller(myNetAddr, otherSide, amount)
|
|
||||||
node.smm.add("demo.seller", seller).get()
|
node.smm.add("demo.seller", seller).get()
|
||||||
}
|
}
|
||||||
|
|
||||||
node.stop()
|
node.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun runBuyer(node: Node, amount: Amount<Issued<Currency>>) {
|
private fun runBuyer(node: Node, amount: Amount<Issued<Currency>>) {
|
||||||
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
|
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
|
||||||
// For demo purposes just extract attachment jars when saved to disk, so the user can explore them.
|
// For demo purposes just extract attachment jars when saved to disk, so the user can explore them.
|
||||||
val attachmentsPath = (node.storage.attachments as NodeAttachmentService).let {
|
val attachmentsPath = (node.storage.attachments as NodeAttachmentService).let {
|
||||||
@ -211,7 +223,7 @@ fun runBuyer(node: Node, amount: Amount<Issued<Currency>>) {
|
|||||||
|
|
||||||
val DEMO_TOPIC = "initiate.demo.trade"
|
val DEMO_TOPIC = "initiate.demo.trade"
|
||||||
|
|
||||||
class TraderDemoProtocolBuyer(private val attachmentsPath: Path,
|
private class TraderDemoProtocolBuyer(private val attachmentsPath: Path,
|
||||||
val notary: Party,
|
val notary: Party,
|
||||||
val amount: Amount<Issued<Currency>>) : ProtocolLogic<Unit>() {
|
val amount: Amount<Issued<Currency>>) : ProtocolLogic<Unit>() {
|
||||||
companion object {
|
companion object {
|
||||||
@ -237,16 +249,16 @@ class TraderDemoProtocolBuyer(private val attachmentsPath: Path,
|
|||||||
// As the seller initiates the two-party trade protocol, here, we will be the buyer.
|
// As the seller initiates the two-party trade protocol, here, we will be the buyer.
|
||||||
try {
|
try {
|
||||||
progressTracker.currentStep = WAITING_FOR_SELLER_TO_CONNECT
|
progressTracker.currentStep = WAITING_FOR_SELLER_TO_CONNECT
|
||||||
val hostname = receive<HostAndPort>(DEMO_TOPIC, 0).validate { it.withDefaultPort(Node.DEFAULT_PORT) }
|
val origin = receive<HostAndPort>(DEMO_TOPIC, 0).validate { it.withDefaultPort(Node.DEFAULT_PORT) }
|
||||||
val newPartnerAddr = ArtemisMessagingService.makeRecipient(hostname)
|
val recipient = ArtemisMessagingService.makeRecipient(origin as HostAndPort)
|
||||||
|
|
||||||
// The session ID disambiguates the test trade.
|
// The session ID disambiguates the test trade.
|
||||||
val sessionID = random63BitValue()
|
val sessionID = random63BitValue()
|
||||||
progressTracker.currentStep = STARTING_BUY
|
progressTracker.currentStep = STARTING_BUY
|
||||||
send(DEMO_TOPIC, newPartnerAddr, 0, sessionID)
|
send(DEMO_TOPIC, recipient, 0, sessionID)
|
||||||
|
|
||||||
val notary = serviceHub.networkMapCache.notaryNodes[0]
|
val notary = serviceHub.networkMapCache.notaryNodes[0]
|
||||||
val buyer = TwoPartyTradeProtocol.Buyer(newPartnerAddr, notary.identity, amount,
|
val buyer = TwoPartyTradeProtocol.Buyer(recipient, notary.identity, amount,
|
||||||
CommercialPaper.State::class.java, sessionID)
|
CommercialPaper.State::class.java, sessionID)
|
||||||
|
|
||||||
// This invokes the trading protocol and out pops our finished transaction.
|
// This invokes the trading protocol and out pops our finished transaction.
|
||||||
@ -289,7 +301,7 @@ ${Emoji.renderIfSupported(cpIssuance)}""")
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TraderDemoProtocolSeller(val myAddress: HostAndPort,
|
private class TraderDemoProtocolSeller(val myAddress: HostAndPort,
|
||||||
val otherSide: SingleMessageRecipient,
|
val otherSide: SingleMessageRecipient,
|
||||||
val amount: Amount<Issued<Currency>>,
|
val amount: Amount<Issued<Currency>>,
|
||||||
override val progressTracker: ProgressTracker = TraderDemoProtocolSeller.tracker()) : ProtocolLogic<Unit>() {
|
override val progressTracker: ProgressTracker = TraderDemoProtocolSeller.tracker()) : ProtocolLogic<Unit>() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user