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:
Maksymilian Pawlak 2017-10-25 16:40:21 +01:00 committed by GitHub
parent ba75146446
commit 44a7d872d8
1035 changed files with 623 additions and 254 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ tags
.gradle
local.properties
.gradletasknamecache
# General build files
**/build/*

8
.idea/compiler.xml generated
View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
# Register a ServiceLoader service extending from net.corda.webserver.services.WebServerPluginRegistry
net.corda.irs.plugin.IRSPlugin

View File

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

View File

@ -1,2 +0,0 @@
net.corda.irs.plugin.IRSPlugin
net.corda.irs.plugin.IRSDemoPlugin

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
corda.host=localhost:10006
server.port=10007

View File

@ -0,0 +1,2 @@
corda.host=localhost:10009
server.port=10010

View File

@ -0,0 +1,2 @@
corda.host=localhost:10003
server.port=10004

View File

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