mirror of
https://github.com/corda/corda.git
synced 2024-12-19 21:17:58 +00:00
IRS Demo - split IRS Demo into two separate applications to showcase … (#1638)
* IRS Demo - split IRS Demo into two separate applications to showcase the separation and usage of RPC client
This commit is contained in:
parent
ba75146446
commit
44a7d872d8
1
.gitignore
vendored
1
.gitignore
vendored
@ -11,6 +11,7 @@ tags
|
||||
|
||||
.gradle
|
||||
local.properties
|
||||
.gradletasknamecache
|
||||
|
||||
# General build files
|
||||
**/build/*
|
||||
|
8
.idea/compiler.xml
generated
8
.idea/compiler.xml
generated
@ -2,8 +2,6 @@
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="1.8">
|
||||
<module name="api-scanner_main" target="1.8" />
|
||||
<module name="api-scanner_test" target="1.8" />
|
||||
<module name="attachment-demo_integrationTest" target="1.8" />
|
||||
<module name="attachment-demo_main" target="1.8" />
|
||||
<module name="attachment-demo_test" target="1.8" />
|
||||
@ -21,6 +19,7 @@
|
||||
<module name="corda-webserver_integrationTest" target="1.8" />
|
||||
<module name="corda-webserver_main" target="1.8" />
|
||||
<module name="corda-webserver_test" target="1.8" />
|
||||
<module name="cordapp_integrationTest" target="1.8" />
|
||||
<module name="cordapp_main" target="1.8" />
|
||||
<module name="cordapp_test" target="1.8" />
|
||||
<module name="cordform-common_main" target="1.8" />
|
||||
@ -43,11 +42,8 @@
|
||||
<module name="explorer-capsule_test" target="1.6" />
|
||||
<module name="explorer_main" target="1.8" />
|
||||
<module name="explorer_test" target="1.8" />
|
||||
<module name="finance_integrationTest" target="1.8" />
|
||||
<module name="finance_main" target="1.8" />
|
||||
<module name="finance_test" target="1.8" />
|
||||
<module name="gradle-plugins-cordform-common_main" target="1.8" />
|
||||
<module name="gradle-plugins-cordform-common_test" target="1.8" />
|
||||
<module name="graphs_main" target="1.8" />
|
||||
<module name="graphs_test" target="1.8" />
|
||||
<module name="irs-demo_integrationTest" target="1.8" />
|
||||
@ -122,6 +118,8 @@
|
||||
<module name="verifier_integrationTest" target="1.8" />
|
||||
<module name="verifier_main" target="1.8" />
|
||||
<module name="verifier_test" target="1.8" />
|
||||
<module name="web_main" target="1.8" />
|
||||
<module name="web_test" target="1.8" />
|
||||
<module name="webcapsule_main" target="1.6" />
|
||||
<module name="webcapsule_test" target="1.6" />
|
||||
<module name="webserver-webcapsule_main" target="1.6" />
|
||||
|
@ -4,25 +4,28 @@ This demo brings up three nodes: Bank A, Bank B and a node that simultaneously r
|
||||
interest rates oracle. The two banks agree on an interest rate swap, and then do regular fixings of the deal as the
|
||||
time on a simulated clock passes.
|
||||
|
||||
To run from the command line in Unix:
|
||||
Functionality is split into two parts - CordApp which provides actual distributed ledger backend and Spring Boot
|
||||
webapp which provides REST API and web frontend. Application communicate using Corda RPC protocol.
|
||||
|
||||
1. Run ``./gradlew samples:irs-demo:deployNodes`` to install configs and a command line tool under
|
||||
To run from the command line in Unix:
|
||||
1. Run ``./gradlew samples:irs-demo:cordapp:deployNodes`` to install configs and a command line tool under
|
||||
``samples/irs-demo/build``
|
||||
2. Run ``./gradlew samples:irs-demo:installDist``
|
||||
3. Move to the ``samples/irs-demo/build`` directory
|
||||
4. Run ``./nodes/runnodes`` to open up three new terminals with the three nodes (you may have to install xterm)
|
||||
2. Run ``./gradlew samples:irs-demo:web:deployWebapps`` to install configs and tools for running webservers
|
||||
3. Move to the ``samples/irs-demo/`` directory
|
||||
4. Run ``./cordapp/build/nodes/runnodes`` to open up three new terminals with the three nodes (you may have to install xterm)
|
||||
5. Run ``./web/build/webapps/runwebapps`` to open three more terminals for associated webserver
|
||||
|
||||
To run from the command line in Windows:
|
||||
|
||||
1. Run ``gradlew.bat samples:irs-demo:deployNodes`` to install configs and a command line tool under
|
||||
1. Run ``gradlew.bat samples:irs-demo:cordapp:deployNodes`` to install configs and a command line tool under
|
||||
``samples\irs-demo\build``
|
||||
2. Run ``gradlew.bat samples:irs-demo:installDist``
|
||||
3. Run ``cd samples\irs-demo\build`` to change current working directory
|
||||
4. Run ``nodes\runnodes`` to open up several 6 terminals, 2 for each node. First terminal is a web-server associated
|
||||
with every node and second one is Corda interactive shell for the node
|
||||
2. Run ``gradlew.bat samples:irs-demo:web:deployWebapps`` to install configs and tools for running webservers
|
||||
3. Run ``cd samples\irs-demo`` to change current working directory
|
||||
4. Run ``cordapp\build\nodes\runnodes`` to open up several 3 terminals for each nodes
|
||||
5. Run ``web\build\webapps\webapps`` to open up several 3 terminals for each nodes' webservers
|
||||
|
||||
This demo also has a web app. To use this, run nodes and then navigate to http://localhost:10007/web/irsdemo and
|
||||
http://localhost:10010/web/irsdemo to see each node's view of the ledger.
|
||||
This demo also has a web app. To use this, run nodes and then navigate to http://localhost:10007/ and
|
||||
http://localhost:10010/ to see each node's view of the ledger.
|
||||
|
||||
To use the web app, click the "Create Deal" button, fill in the form, then click the "Submit" button. You can then use
|
||||
the time controls at the top left of the home page to run the fixings. Click any individual trade in the blotter to
|
||||
|
@ -1,6 +1,25 @@
|
||||
buildscript {
|
||||
ext {
|
||||
springBootVersion = '1.5.7.RELEASE'
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
|
||||
}
|
||||
}
|
||||
|
||||
// Spring Boot plugin adds a numerous hardcoded dependencies in the version much lower then Corda expects
|
||||
// causing the problems in runtime. Those can be changed by manipulating above properties
|
||||
// See https://github.com/spring-gradle-plugins/dependency-management-plugin/blob/master/README.md#changing-the-value-of-a-version-property
|
||||
ext['artemis.version'] = "$artemis_version"
|
||||
ext['hibernate.version'] = "$hibernate_version"
|
||||
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'org.springframework.boot'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'net.corda.plugins.cordapp'
|
||||
@ -23,59 +42,25 @@ sourceSets {
|
||||
configurations {
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
demoArtifacts.extendsFrom testRuntime
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
|
||||
|
||||
// The irs demo CorDapp depends upon Cash CorDapp features
|
||||
cordapp project(':finance')
|
||||
|
||||
// Corda integration dependencies
|
||||
cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts')
|
||||
cordaCompile project(':core')
|
||||
cordaCompile project(':webserver')
|
||||
|
||||
// Javax is required for webapis
|
||||
compile "org.glassfish.jersey.core:jersey-server:${jersey_version}"
|
||||
|
||||
// Cordapp dependencies
|
||||
// Specify your cordapp's dependencies below, including dependent cordapps
|
||||
compile "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||
compile group: 'commons-io', name: 'commons-io', version: '2.5'
|
||||
compile project(path: ":samples:irs-demo:cordapp", configuration: "demoArtifacts")
|
||||
compile project(":samples:irs-demo:web")
|
||||
compile('org.springframework.boot:spring-boot-starter-web') {
|
||||
exclude module: "spring-boot-starter-logging"
|
||||
exclude module: "logback-classic"
|
||||
}
|
||||
|
||||
testCompile project(':node-driver')
|
||||
testCompile "junit:junit:$junit_version"
|
||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||
}
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
directory "./build/nodes"
|
||||
node {
|
||||
name "O=Notary Service,L=Zurich,C=CH"
|
||||
notary = [validating : true]
|
||||
p2pPort 10002
|
||||
rpcPort 10003
|
||||
webPort 10004
|
||||
cordapps = ["$project.group:finance:$corda_release_version"]
|
||||
useTestClock true
|
||||
}
|
||||
node {
|
||||
name "O=Bank A,L=London,C=GB"
|
||||
p2pPort 10005
|
||||
rpcPort 10006
|
||||
webPort 10007
|
||||
cordapps = ["$project.group:finance:$corda_release_version"]
|
||||
useTestClock true
|
||||
}
|
||||
node {
|
||||
name "O=Bank B,L=New York,C=US"
|
||||
p2pPort 10008
|
||||
rpcPort 10009
|
||||
webPort 10010
|
||||
cordapps = ["$project.group:finance:$corda_release_version"]
|
||||
useTestClock true
|
||||
}
|
||||
bootRepackage {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
task integrationTest(type: Test, dependsOn: []) {
|
||||
@ -83,41 +68,9 @@ task integrationTest(type: Test, dependsOn: []) {
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
|
||||
// This fixes the "line too long" error when running this demo with windows CLI
|
||||
// TODO: Automatically apply to all projects via a plugin
|
||||
tasks.withType(CreateStartScripts).each { task ->
|
||||
task.doLast {
|
||||
String text = task.windowsScript.text
|
||||
// Replaces the per file classpath (which are all jars in "lib") with a wildcard on lib
|
||||
text = text.replaceFirst(/(set CLASSPATH=%APP_HOME%\\lib\\).*/, { "${it[1]}*" })
|
||||
task.windowsScript.write text
|
||||
}
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
downloadJavadoc = true // defaults to false
|
||||
downloadSources = true
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
jarAndSources(MavenPublication) {
|
||||
from components.java
|
||||
artifactId 'irsdemo'
|
||||
|
||||
artifact sourceJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
from sourceSets.test.output
|
||||
manifest {
|
||||
attributes(
|
||||
'Automatic-Module-Name': 'net.corda.samples.demos.irs'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
115
samples/irs-demo/cordapp/build.gradle
Normal file
115
samples/irs-demo/cordapp/build.gradle
Normal file
@ -0,0 +1,115 @@
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
apply plugin: 'net.corda.plugins.cordapp'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'application'
|
||||
|
||||
mainClassName = 'net.corda.irs.IRSDemo'
|
||||
|
||||
sourceSets {
|
||||
integrationTest {
|
||||
kotlin {
|
||||
compileClasspath += main.output + test.output
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/integration-test/kotlin')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
demoArtifacts.extendsFrom integrationTestRuntime
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The irs demo CorDapp depends upon Cash CorDapp features
|
||||
cordapp project(':finance')
|
||||
|
||||
// Corda integration dependencies
|
||||
cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
cordaCompile project(':core')
|
||||
|
||||
// Cordapp dependencies
|
||||
// Specify your cordapp's dependencies below, including dependent cordapps
|
||||
compile group: 'commons-io', name: 'commons-io', version: '2.5'
|
||||
|
||||
testCompile project(':node-driver')
|
||||
testCompile "junit:junit:$junit_version"
|
||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||
}
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
|
||||
ext.rpcUsers = [
|
||||
['username' : "user",
|
||||
'password' : "password",
|
||||
'permissions' : [
|
||||
"StartFlow.net.corda.irs.flows.AutoOfferFlow\$Requester",
|
||||
"StartFlow.net.corda.irs.flows.UpdateBusinessDayFlow\$Broadcast",
|
||||
"StartFlow.net.corda.irs.api.NodeInterestRates\$UploadFixesFlow"
|
||||
]]
|
||||
]
|
||||
|
||||
directory "./build/nodes"
|
||||
node {
|
||||
name "O=Notary Service,L=Zurich,C=CH"
|
||||
notary = [validating : true]
|
||||
p2pPort 10002
|
||||
rpcPort 10003
|
||||
cordapps = ["net.corda:finance:$corda_release_version"]
|
||||
rpcUsers = ext.rpcUsers
|
||||
useTestClock true
|
||||
}
|
||||
node {
|
||||
name "O=Bank A,L=London,C=GB"
|
||||
p2pPort 10005
|
||||
rpcPort 10006
|
||||
cordapps = ["net.corda:finance:$corda_release_version"]
|
||||
rpcUsers = ext.rpcUsers
|
||||
useTestClock true
|
||||
}
|
||||
node {
|
||||
name "O=Bank B,L=New York,C=US"
|
||||
p2pPort 10008
|
||||
rpcPort 10009
|
||||
cordapps = ["net.corda:finance:$corda_release_version"]
|
||||
rpcUsers = ext.rpcUsers
|
||||
useTestClock true
|
||||
}
|
||||
}
|
||||
|
||||
task integrationTest(type: Test, dependsOn: []) {
|
||||
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
|
||||
// This fixes the "line too long" error when running this demo with windows CLI
|
||||
// TODO: Automatically apply to all projects via a plugin
|
||||
tasks.withType(CreateStartScripts).each { task ->
|
||||
task.doLast {
|
||||
String text = task.windowsScript.text
|
||||
// Replaces the per file classpath (which are all jars in "lib") with a wildcard on lib
|
||||
text = text.replaceFirst(/(set CLASSPATH=%APP_HOME%\\lib\\).*/, { "${it[1]}*" })
|
||||
task.windowsScript.write text
|
||||
}
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
downloadJavadoc = true // defaults to false
|
||||
downloadSources = true
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
from sourceSets.test.output
|
||||
}
|
||||
|
||||
artifacts {
|
||||
demoArtifacts jar
|
||||
}
|
@ -13,23 +13,25 @@ import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.vaultTrackBy
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.finance.plugin.registerFinanceJSONMappers
|
||||
import net.corda.irs.contract.InterestRateSwap
|
||||
import net.corda.irs.utilities.uploadFile
|
||||
import net.corda.irs.web.IrsDemoWebApplication
|
||||
import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.test.spring.springDriver
|
||||
import net.corda.testing.DUMMY_BANK_A
|
||||
import net.corda.testing.DUMMY_BANK_B
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.IntegrationTestCategory
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.http.HttpApi
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import java.net.URL
|
||||
import java.time.Duration
|
||||
import java.time.LocalDate
|
||||
|
||||
@ -39,31 +41,34 @@ class IRSDemoTest : IntegrationTestCategory {
|
||||
val log = loggerFor<IRSDemoTest>()
|
||||
}
|
||||
|
||||
private val rpcUser = User("user", "password", setOf("ALL"))
|
||||
private val currentDate: LocalDate = LocalDate.now()
|
||||
private val futureDate: LocalDate = currentDate.plusMonths(6)
|
||||
private val maxWaitTime: Duration = 60.seconds
|
||||
val rpcUsers = listOf(User("user", "password",
|
||||
setOf("StartFlow.net.corda.irs.flows.AutoOfferFlow\$Requester",
|
||||
"StartFlow.net.corda.irs.flows.UpdateBusinessDayFlow\$Broadcast",
|
||||
"StartFlow.net.corda.irs.api.NodeInterestRates\$UploadFixesFlow")))
|
||||
|
||||
val currentDate: LocalDate = LocalDate.now()
|
||||
val futureDate: LocalDate = currentDate.plusMonths(6)
|
||||
val maxWaitTime: Duration = 60.seconds
|
||||
|
||||
@Test
|
||||
fun `runs IRS demo`() {
|
||||
driver(useTestClock = true, isDebug = true) {
|
||||
springDriver(useTestClock = true, isDebug = true, extraCordappPackagesToScan = listOf("net.corda.irs")) {
|
||||
val (controller, nodeA, nodeB) = listOf(
|
||||
startNotaryNode(DUMMY_NOTARY.name, validating = false),
|
||||
startNode(providedName = DUMMY_BANK_A.name, rpcUsers = listOf(rpcUser)),
|
||||
startNode(providedName = DUMMY_BANK_B.name))
|
||||
.map { it.getOrThrow() }
|
||||
startNotaryNode(DUMMY_NOTARY.name, validating = true, rpcUsers = rpcUsers),
|
||||
startNode(providedName = DUMMY_BANK_A.name, rpcUsers = rpcUsers),
|
||||
startNode(providedName = DUMMY_BANK_B.name, rpcUsers = rpcUsers)).map { it.getOrThrow() }
|
||||
|
||||
log.info("All nodes started")
|
||||
|
||||
val (controllerAddr, nodeAAddr, nodeBAddr) = listOf(
|
||||
startWebserver(controller),
|
||||
startWebserver(nodeA),
|
||||
startWebserver(nodeB))
|
||||
.map { it.getOrThrow().listenAddress }
|
||||
val controllerAddrFuture = startSpringBootWebapp(IrsDemoWebApplication::class.java, controller, "/api/irs/demodate")
|
||||
val nodeAAddrFuture = startSpringBootWebapp(IrsDemoWebApplication::class.java, nodeA, "/api/irs/demodate")
|
||||
val nodeBAddrFuture = startSpringBootWebapp(IrsDemoWebApplication::class.java, nodeB, "/api/irs/demodate")
|
||||
val (controllerAddr, nodeAAddr, nodeBAddr) =
|
||||
listOf(controllerAddrFuture, nodeAAddrFuture, nodeBAddrFuture).map { it.getOrThrow().listenAddress }
|
||||
|
||||
log.info("All webservers started")
|
||||
|
||||
val (_, nodeAApi, nodeBApi) = listOf(controller, nodeA, nodeB).zip(listOf(controllerAddr, nodeAAddr, nodeBAddr)).map {
|
||||
val (controllerApi, nodeAApi, nodeBApi) = listOf(controller, nodeA, nodeB).zip(listOf(controllerAddr, nodeAAddr, nodeBAddr)).map {
|
||||
val mapper = net.corda.client.jackson.JacksonSupport.createDefaultMapper(it.first.rpc)
|
||||
registerFinanceJSONMappers(mapper)
|
||||
registerIRSModule(mapper)
|
||||
@ -73,7 +78,7 @@ class IRSDemoTest : IntegrationTestCategory {
|
||||
val numADeals = getTradeCount(nodeAApi)
|
||||
val numBDeals = getTradeCount(nodeBApi)
|
||||
|
||||
runUploadRates(controllerAddr)
|
||||
runUploadRates(controllerApi)
|
||||
runTrade(nodeAApi, controller.nodeInfo.chooseIdentity())
|
||||
|
||||
assertThat(getTradeCount(nodeAApi)).isEqualTo(numADeals + 1)
|
||||
@ -89,9 +94,7 @@ class IRSDemoTest : IntegrationTestCategory {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFloatingLegFixCount(nodeApi: HttpApi): Int {
|
||||
return getTrades(nodeApi)[0].calculation.floatingLegPaymentSchedule.count { it.value.rate.ratioUnit != null }
|
||||
}
|
||||
fun getFloatingLegFixCount(nodeApi: HttpApi) = getTrades(nodeApi)[0].calculation.floatingLegPaymentSchedule.count { it.value.rate.ratioUnit != null }
|
||||
|
||||
private fun getFixingDateObservable(config: FullNodeConfiguration): Observable<LocalDate?> {
|
||||
val client = CordaRPCClient(config.rpcAddress!!)
|
||||
@ -111,16 +114,15 @@ class IRSDemoTest : IntegrationTestCategory {
|
||||
|
||||
private fun runTrade(nodeApi: HttpApi, oracle: Party) {
|
||||
log.info("Running trade against ${nodeApi.root}")
|
||||
val fileContents = loadResourceFile("net/corda/irs/simulation/example-irs-trade.json")
|
||||
val fileContents = loadResourceFile("net/corda/irs/web/simulation/example-irs-trade.json")
|
||||
val tradeFile = fileContents.replace("tradeXXX", "trade1").replace("oracleXXX", oracle.name.toString())
|
||||
assertThat(nodeApi.postJson("deals", tradeFile)).isTrue()
|
||||
}
|
||||
|
||||
private fun runUploadRates(host: NetworkHostAndPort) {
|
||||
log.info("Running upload rates against $host")
|
||||
private fun runUploadRates(nodeApi: HttpApi) {
|
||||
log.info("Running upload rates against ${nodeApi.root}")
|
||||
val fileContents = loadResourceFile("net/corda/irs/simulation/example.rates.txt")
|
||||
val url = URL("http://$host/api/irs/fixes")
|
||||
assertThat(uploadFile(url, fileContents)).isTrue()
|
||||
assertThat(nodeApi.postPlain("fixes", fileContents)).isTrue()
|
||||
}
|
||||
|
||||
private fun loadResourceFile(filename: String): String {
|
||||
|
@ -0,0 +1,124 @@
|
||||
package net.corda.test.spring
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.internal.concurrent.flatMap
|
||||
import net.corda.core.internal.concurrent.fork
|
||||
import net.corda.core.internal.concurrent.map
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.testing.driver.*
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.net.ConnectException
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
interface SpringDriverExposedDSLInterface : DriverDSLExposedInterface {
|
||||
|
||||
/**
|
||||
* Starts a Spring Boot application, passes the RPC connection data as parameters the process.
|
||||
* Returns future which will complete after (and if) the server passes healthcheck.
|
||||
* @param clazz Class with main method which is expected to run Spring application
|
||||
* @param handle Corda Node handle this webapp is expected to connect to
|
||||
* @param checkUrl URL path to use for server readiness check - uses [okhttp3.Response.isSuccessful] as qualifier
|
||||
*
|
||||
* TODO: Rather then expecting a given clazz to contain main method which start Spring app our own simple class can do this
|
||||
*/
|
||||
fun startSpringBootWebapp(clazz: Class<*>, handle: NodeHandle, checkUrl: String): CordaFuture<WebserverHandle>
|
||||
}
|
||||
|
||||
interface SpringDriverInternalDSLInterface : DriverDSLInternalInterface, SpringDriverExposedDSLInterface
|
||||
|
||||
fun <A> springDriver(
|
||||
defaultParameters: DriverParameters = DriverParameters(),
|
||||
isDebug: Boolean = defaultParameters.isDebug,
|
||||
driverDirectory: Path = defaultParameters.driverDirectory,
|
||||
portAllocation: PortAllocation = defaultParameters.portAllocation,
|
||||
debugPortAllocation: PortAllocation = defaultParameters.debugPortAllocation,
|
||||
systemProperties: Map<String, String> = defaultParameters.systemProperties,
|
||||
useTestClock: Boolean = defaultParameters.useTestClock,
|
||||
initialiseSerialization: Boolean = defaultParameters.initialiseSerialization,
|
||||
startNodesInProcess: Boolean = defaultParameters.startNodesInProcess,
|
||||
extraCordappPackagesToScan: List<String> = defaultParameters.extraCordappPackagesToScan,
|
||||
dsl: SpringDriverExposedDSLInterface.() -> A
|
||||
) = genericDriver(
|
||||
defaultParameters = defaultParameters,
|
||||
isDebug = isDebug,
|
||||
driverDirectory = driverDirectory,
|
||||
portAllocation = portAllocation,
|
||||
debugPortAllocation = debugPortAllocation,
|
||||
systemProperties = systemProperties,
|
||||
useTestClock = useTestClock,
|
||||
initialiseSerialization = initialiseSerialization,
|
||||
startNodesInProcess = startNodesInProcess,
|
||||
extraCordappPackagesToScan = extraCordappPackagesToScan,
|
||||
driverDslWrapper = { driverDSL:DriverDSL -> SpringBootDriverDSL(driverDSL) },
|
||||
coerce = { it },
|
||||
dsl = dsl
|
||||
)
|
||||
|
||||
data class SpringBootDriverDSL(
|
||||
val driverDSL: DriverDSL
|
||||
) : DriverDSLInternalInterface by driverDSL, SpringDriverInternalDSLInterface {
|
||||
|
||||
val log = loggerFor<SpringBootDriverDSL>()
|
||||
|
||||
override fun startSpringBootWebapp(clazz: Class<*>, handle: NodeHandle, checkUrl: String): CordaFuture<WebserverHandle> {
|
||||
val debugPort = if (driverDSL.isDebug) driverDSL.debugPortAllocation.nextPort() else null
|
||||
val processFuture = startApplication(driverDSL.executorService, handle, debugPort, clazz)
|
||||
driverDSL.registerProcess(processFuture)
|
||||
return processFuture.map { queryWebserver(handle, it, checkUrl) }
|
||||
}
|
||||
|
||||
private fun queryWebserver(handle: NodeHandle, process: Process, checkUrl: String): WebserverHandle {
|
||||
val protocol = if (handle.configuration.useHTTPS) "https://" else "http://"
|
||||
val url = URL(URL("$protocol${handle.webAddress}"), checkUrl)
|
||||
val client = OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).readTimeout(10, TimeUnit.SECONDS).build()
|
||||
|
||||
var maxRetries = 30
|
||||
|
||||
while (process.isAlive && maxRetries > 0) try {
|
||||
val response = client.newCall(Request.Builder().url(url).build()).execute()
|
||||
response.use {
|
||||
if (response.isSuccessful) {
|
||||
return WebserverHandle(handle.webAddress, process)
|
||||
}
|
||||
}
|
||||
|
||||
TimeUnit.SECONDS.sleep(2)
|
||||
maxRetries--
|
||||
} catch (e: ConnectException) {
|
||||
log.debug("Retrying webserver info at ${handle.webAddress}")
|
||||
}
|
||||
|
||||
throw IllegalStateException("Webserver at ${handle.webAddress} has died or was not reachable at URL ${url}")
|
||||
}
|
||||
|
||||
private fun startApplication(executorService: ExecutorService, handle: NodeHandle, debugPort: Int?, clazz: Class<*>): CordaFuture<Process> {
|
||||
return executorService.fork {
|
||||
val className = clazz.canonicalName
|
||||
ProcessUtilities.startJavaProcessImpl(
|
||||
className = className, // cannot directly get class for this, so just use string
|
||||
jdwpPort = debugPort,
|
||||
extraJvmArguments = listOf(
|
||||
"-Dname=node-${handle.configuration.p2pAddress}-webserver",
|
||||
"-Djava.io.tmpdir=${System.getProperty("java.io.tmpdir")}"
|
||||
// Inherit from parent process
|
||||
),
|
||||
classpath = ProcessUtilities.defaultClassPath,
|
||||
workingDirectory = handle.configuration.baseDirectory,
|
||||
errorLogPath = Paths.get("error.$className.log"),
|
||||
arguments = listOf(
|
||||
"--base-directory", handle.configuration.baseDirectory.toString(),
|
||||
"--server.port=${handle.webAddress.port}",
|
||||
"--corda.host=${handle.configuration.rpcAddress}",
|
||||
"--corda.user=${handle.configuration.rpcUsers.first().username}",
|
||||
"--corda.password=${handle.configuration.rpcUsers.first().password}"
|
||||
),
|
||||
maximumHeapSize = null
|
||||
)
|
||||
}.flatMap { process -> addressMustBeBoundFuture(driverDSL.executorService, handle.webAddress, process).map { process } }
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package net.corda.irs.plugin
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import net.corda.finance.plugin.registerFinanceJSONMappers
|
||||
import net.corda.irs.api.InterestRateSwapAPI
|
||||
import net.corda.webserver.services.WebServerPluginRegistry
|
||||
import java.util.function.Function
|
||||
|
||||
class IRSPlugin : WebServerPluginRegistry {
|
||||
override val webApis = listOf(Function(::InterestRateSwapAPI))
|
||||
override val staticServeDirs: Map<String, String> = mapOf(
|
||||
"irsdemo" to javaClass.classLoader.getResource("irsweb").toExternalForm()
|
||||
)
|
||||
|
||||
override fun customizeJSONSerialization(om: ObjectMapper): Unit = registerFinanceJSONMappers(om)
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package net.corda.irs.utilities
|
||||
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import java.net.URL
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* A small set of utilities for making HttpCalls, aimed at demos.
|
||||
*/
|
||||
private val client by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS).build()
|
||||
}
|
||||
|
||||
fun putJson(url: URL, data: String): Boolean {
|
||||
val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data)
|
||||
return makeRequest(Request.Builder().url(url).put(body).build())
|
||||
}
|
||||
|
||||
fun postJson(url: URL, data: String): Boolean {
|
||||
val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data)
|
||||
return makeRequest(Request.Builder().url(url).post(body).build())
|
||||
}
|
||||
|
||||
fun uploadFile(url: URL, file: String): Boolean {
|
||||
val body = RequestBody.create(MediaType.parse("text/plain; charset=utf-8"), file)
|
||||
return makeRequest(Request.Builder().url(url).post(body).build())
|
||||
}
|
||||
|
||||
private fun makeRequest(request: Request): Boolean {
|
||||
val response = client.newCall(request).execute()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
println("Could not fulfill HTTP request. Status Code: ${response.code()}. Message: ${response.body().string()}")
|
||||
}
|
||||
response.close()
|
||||
return response.isSuccessful
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
# Register a ServiceLoader service extending from net.corda.webserver.services.WebServerPluginRegistry
|
||||
net.corda.irs.plugin.IRSPlugin
|
@ -1,9 +0,0 @@
|
||||
package net.corda.irs.plugin
|
||||
|
||||
import net.corda.irs.api.InterestRatesSwapDemoAPI
|
||||
import net.corda.webserver.services.WebServerPluginRegistry
|
||||
import java.util.function.Function
|
||||
|
||||
class IRSDemoPlugin : WebServerPluginRegistry {
|
||||
override val webApis = listOf(Function(::InterestRatesSwapDemoAPI))
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
net.corda.irs.plugin.IRSPlugin
|
||||
net.corda.irs.plugin.IRSDemoPlugin
|
55
samples/irs-demo/web/build.gradle
Normal file
55
samples/irs-demo/web/build.gradle
Normal file
@ -0,0 +1,55 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
|
||||
classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlin_version}")
|
||||
}
|
||||
}
|
||||
|
||||
// Spring Boot plugin adds a numerous hardcoded dependencies in the version much lower then Corda expects
|
||||
// causing the problems in runtime. Those can be changed by manipulating above properties
|
||||
// See https://github.com/spring-gradle-plugins/dependency-management-plugin/blob/master/README.md#changing-the-value-of-a-version-property
|
||||
ext['artemis.version'] = "$artemis_version"
|
||||
ext['hibernate.version'] = "$hibernate_version"
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlin-spring'
|
||||
apply plugin: 'eclipse'
|
||||
apply plugin: 'org.springframework.boot'
|
||||
apply plugin: 'project-report'
|
||||
apply plugin: 'application'
|
||||
|
||||
dependencies {
|
||||
compile('org.springframework.boot:spring-boot-starter-web') {
|
||||
exclude module: "spring-boot-starter-logging"
|
||||
exclude module: "logback-classic"
|
||||
}
|
||||
compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.8.9")
|
||||
compile project(":client:rpc")
|
||||
compile project(":client:jackson")
|
||||
compile project(":test-utils")
|
||||
compile project(path: ":samples:irs-demo:cordapp", configuration: "demoArtifacts")
|
||||
testCompile('org.springframework.boot:spring-boot-starter-test') {
|
||||
exclude module: "spring-boot-starter-logging"
|
||||
exclude module: "logback-classic"
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
from sourceSets.test.output
|
||||
}
|
||||
|
||||
task deployWebapps(type: Copy, dependsOn: ['jar', 'bootRepackage']) {
|
||||
ext.webappDir = file("build/webapps")
|
||||
|
||||
from(jar.outputs)
|
||||
from("src/test/resources/scripts/") {
|
||||
filter { it
|
||||
.replace('#JAR_PATH#', jar.archiveName)
|
||||
.replace('#DIR#', ext.webappDir.getAbsolutePath())
|
||||
}
|
||||
}
|
||||
into ext.webappDir
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package net.corda.irs.web
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import net.corda.client.jackson.JacksonSupport
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.finance.plugin.registerFinanceJSONMappers
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.boot.SpringApplication
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.context.annotation.Bean
|
||||
|
||||
/**
|
||||
* Simple and sample SpringBoot web application which communicates with Corda node using RPC.
|
||||
* [CordaRPCOps] instance can be managed simply as plain Spring bean.
|
||||
* If support for (de)serializatin of Corda classes is required, [ObjectMapper] can be configured using helper
|
||||
* functions, see [objectMapper]
|
||||
*/
|
||||
@SpringBootApplication
|
||||
class IrsDemoWebApplication {
|
||||
@Value("\${corda.host}")
|
||||
lateinit var cordaHost:String
|
||||
|
||||
@Value("\${corda.user}")
|
||||
lateinit var cordaUser:String
|
||||
|
||||
@Value("\${corda.password}")
|
||||
lateinit var cordaPassword:String
|
||||
|
||||
|
||||
@Bean
|
||||
fun rpcClient(): CordaRPCOps {
|
||||
return CordaRPCClient(NetworkHostAndPort.parse(cordaHost)).start(cordaUser, cordaPassword).proxy
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun objectMapper(@Autowired cordaRPCOps: CordaRPCOps): ObjectMapper {
|
||||
val mapper = JacksonSupport.createDefaultMapper(cordaRPCOps)
|
||||
registerFinanceJSONMappers(mapper)
|
||||
return mapper
|
||||
}
|
||||
|
||||
// running as standalone java app
|
||||
companion object {
|
||||
@JvmStatic fun main(args: Array<String>) {
|
||||
SpringApplication.run(IrsDemoWebApplication::class.java, *args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package net.corda.irs.api
|
||||
package net.corda.irs.web.api
|
||||
|
||||
import net.corda.core.contracts.filterStatesOfType
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
@ -7,11 +7,12 @@ import net.corda.core.messaging.vaultQueryBy
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.irs.contract.InterestRateSwap
|
||||
import net.corda.irs.flows.AutoOfferFlow
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import java.net.URI
|
||||
import javax.ws.rs.*
|
||||
import javax.ws.rs.core.MediaType
|
||||
import javax.ws.rs.core.Response
|
||||
import net.corda.irs.flows.AutoOfferFlow
|
||||
|
||||
/**
|
||||
* This provides a simplified API, currently for demonstration use only.
|
||||
@ -28,8 +29,10 @@ import javax.ws.rs.core.Response
|
||||
*
|
||||
* TODO: replace simulated date advancement with business event based implementation
|
||||
*/
|
||||
@Path("irs")
|
||||
class InterestRateSwapAPI(val rpc: CordaRPCOps) {
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/irs")
|
||||
class InterestRateSwapAPI {
|
||||
|
||||
private val logger = loggerFor<InterestRateSwapAPI>()
|
||||
|
||||
@ -44,6 +47,10 @@ class InterestRateSwapAPI(val rpc: CordaRPCOps) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Autowired
|
||||
lateinit var rpc: CordaRPCOps
|
||||
|
||||
private fun getAllDeals(): Array<InterestRateSwap.State> {
|
||||
val vault = rpc.vaultQueryBy<InterestRateSwap.State>().states
|
||||
val states = vault.filterStatesOfType<InterestRateSwap.State>()
|
||||
@ -51,33 +58,30 @@ class InterestRateSwapAPI(val rpc: CordaRPCOps) {
|
||||
return swaps
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("deals")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@GetMapping("/deals")
|
||||
fun fetchDeals(): Array<InterestRateSwap.State> = getAllDeals()
|
||||
|
||||
@POST
|
||||
@Path("deals")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
fun storeDeal(newDeal: InterestRateSwap.State): Response {
|
||||
@PostMapping("/deals")
|
||||
fun storeDeal(@RequestBody newDeal: InterestRateSwap.State): ResponseEntity<Any?> {
|
||||
return try {
|
||||
rpc.startFlow(AutoOfferFlow::Requester, newDeal).returnValue.getOrThrow()
|
||||
Response.created(URI.create(generateDealLink(newDeal))).build()
|
||||
ResponseEntity.created(URI.create(generateDealLink(newDeal))).build()
|
||||
} catch (ex: Throwable) {
|
||||
logger.info("Exception when creating deal: $ex")
|
||||
Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(ex.toString()).build()
|
||||
logger.info("Exception when creating deal: $ex", ex)
|
||||
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("deals/{ref}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun fetchDeal(@PathParam("ref") ref: String): Response {
|
||||
val deal = getDealByRef(ref)
|
||||
if (deal == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build()
|
||||
@GetMapping("/deals/{ref:.+}")
|
||||
fun fetchDeal(@PathVariable ref: String?): ResponseEntity<Any?> {
|
||||
val deal = getDealByRef(ref!!)
|
||||
return if (deal == null) {
|
||||
ResponseEntity.notFound().build()
|
||||
} else {
|
||||
return Response.ok().entity(deal).build()
|
||||
ResponseEntity.ok(deal)
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/deals/networksnapshot")
|
||||
fun fetchDeal() = rpc.networkMapSnapshot().toString()
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
corda.host=localhost:10006
|
||||
server.port=10007
|
@ -0,0 +1,2 @@
|
||||
corda.host=localhost:10009
|
||||
server.port=10010
|
@ -0,0 +1,2 @@
|
||||
corda.host=localhost:10003
|
||||
server.port=10004
|
@ -0,0 +1,2 @@
|
||||
corda.user=user
|
||||
corda.password=password
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user