mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Merge branch 'cbdc/poc-encrypt-backchain-sf' of https://github.com/corda/corda into cbdc/poc-encrypt-backchain-sf
# Conflicts: # build.gradle
This commit is contained in:
commit
c4b7066c83
@ -242,7 +242,6 @@ apply plugin: 'project-report'
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'com.jfrog.artifactory'
|
||||
apply plugin: 'com.r3.testing.distributed-testing'
|
||||
|
||||
|
||||
// If the command line project option -PversionFromGit is added to the gradle invocation, we'll resolve
|
||||
|
@ -9,4 +9,4 @@ package net.corda.common.logging
|
||||
* (originally added to source control for ease of use)
|
||||
*/
|
||||
|
||||
internal const val CURRENT_MAJOR_RELEASE = "4.8.5"
|
||||
internal const val CURRENT_MAJOR_RELEASE = "4.8.5.8-CONCLAVE-SNAPSHOT"
|
@ -34,8 +34,6 @@ shadowJar {
|
||||
|
||||
enum ImageVariant {
|
||||
UBUNTU_ZULU("Dockerfile", "1.8", "zulu-openjdk8"),
|
||||
UBUNTU_ZULU_11("Dockerfile11", "11", "zulu-openjdk11"),
|
||||
AL_CORRETTO("DockerfileAL", "1.8", "amazonlinux2"),
|
||||
OFFICIAL(UBUNTU_ZULU)
|
||||
|
||||
String dockerFile
|
||||
@ -254,4 +252,4 @@ def buildDockerImageTask = tasks.register("buildDockerImage", BuildDockerImageTa
|
||||
|
||||
tasks.register("pushDockerImage", PushDockerImage) {
|
||||
from(buildDockerImageTask.get())
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
# export DOCKER_USERNAME=<username>
|
||||
# export DOCKER_PASSWORD=<password>
|
||||
|
||||
FROM corda.azurecr.io/jdk/azul/zulu-sa-jdk:11.0.3_7_LTS
|
||||
FROM adoptopenjdk/openjdk11
|
||||
|
||||
## Add packages, clean cache, create dirs, create corda user and change ownership
|
||||
RUN apt-get update && \
|
||||
@ -79,4 +79,4 @@ COPY --chown=corda:corda starting-node.conf /opt/corda/starting-node.conf
|
||||
USER "corda"
|
||||
EXPOSE ${MY_P2P_PORT} ${MY_RPC_PORT} ${MY_RPC_ADMIN_PORT}
|
||||
WORKDIR /opt/corda
|
||||
CMD ["run-corda"]
|
||||
CMD ["run-corda"]
|
||||
|
@ -1,11 +0,0 @@
|
||||
# Sample applications
|
||||
|
||||
Please refer to `README.md` in the individual project folders. There are the following demos:
|
||||
|
||||
* **attachment-demo** A simple demonstration of sending a transaction with an attachment from one node to another, and then accessing the attachment on the remote node.
|
||||
* **irs-demo** A demo showing two nodes agreeing to an interest rate swap and doing fixings using an oracle.
|
||||
* **trader-demo** A simple driver for exercising the two party trading flow. In this scenario, a buyer wants to purchase some commercial paper by swapping his cash for commercial paper. The seller learns that the buyer exists, and sends them a message to kick off the trade. The seller, having obtained his CP, then quits and the buyer goes back to waiting. The buyer will sell as much CP as he can! **We recommend starting with this demo.**
|
||||
* **simm-valuation-demo** A demo showing two nodes reaching agreement on the valuation of a derivatives portfolio.
|
||||
* **notary-demo** A simple demonstration of a node getting multiple transactions notarised by a single or distributed (Raft or BFT SMaRt) notary.
|
||||
* **bank-of-corda-demo** A demo showing a node acting as an issuer of fungible assets (initially Cash)
|
||||
* **network-verifier** A very simple CorDapp that can be used to test that communication over a Corda network works.
|
@ -1,23 +0,0 @@
|
||||
# Attachment Demo
|
||||
|
||||
This demo brings up three nodes, and sends a transaction containing an attachment from one to the other.
|
||||
|
||||
To run from the command line in Unix:
|
||||
|
||||
1. Run ``./gradlew samples:attachment-demo:deployNodes`` to create a set of configs and installs under
|
||||
``samples/attachment-demo/build/nodes``
|
||||
2. Run ``./samples/attachment-demo/build/nodes/runnodes`` to open up three new terminal tabs/windows with the three
|
||||
nodes and webserver for BankB
|
||||
3. Run ``./gradlew samples:attachment-demo:runRecipient``, which will block waiting for a trade to start
|
||||
4. Run ``./gradlew samples:attachment-demo:runSender`` in another terminal window to send the attachment. Now look at
|
||||
the other windows to see the output of the demo
|
||||
|
||||
To run from the command line in Windows:
|
||||
|
||||
1. Run ``gradlew samples:attachment-demo:deployNodes`` to create a set of configs and installs under
|
||||
``samples\attachment-demo\build\nodes``
|
||||
2. Run ``samples\attachment-demo\build\nodes\runnodes`` to open up three new terminal tabs/windows with the three nodes
|
||||
and webserver for BankB
|
||||
3. Run ``gradlew samples:attachment-demo:runRecipient``, which will block waiting for a trade to start
|
||||
4. Run ``gradlew samples:attachment-demo:runSender`` in another terminal window to send the attachment. Now look at the
|
||||
other windows to see the output of the demo
|
@ -1,147 +0,0 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.cordapp'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
|
||||
description 'Corda attachment demo'
|
||||
|
||||
cordapp {
|
||||
info {
|
||||
name "Corda Attachment Demo"
|
||||
vendor "R3"
|
||||
targetPlatformVersion corda_platform_version.toInteger()
|
||||
minimumPlatformVersion 1
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
integrationTest {
|
||||
kotlin {
|
||||
compileClasspath += main.output + test.output
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/integration-test/kotlin')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
compile "net.sf.jopt-simple:jopt-simple:$jopt_simple_version"
|
||||
compile "javax.servlet:javax.servlet-api:${servlet_version}"
|
||||
compile "javax.ws.rs:javax.ws.rs-api:2.1.1"
|
||||
cordaCompile project(':client:rpc')
|
||||
|
||||
// Cordformation needs a SLF4J implementation when executing the Network
|
||||
// Bootstrapper, but Log4J doesn't shutdown completely from within Gradle.
|
||||
// Use a much simpler SLF4J implementation here instead.
|
||||
cordaRuntime "org.slf4j:slf4j-simple:$slf4j_version"
|
||||
|
||||
// Corda integration dependencies
|
||||
cordaRuntime project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
cordaRuntime project(path: ":testing:testserver:testcapsule:", configuration: 'runtimeArtifacts')
|
||||
|
||||
cordapp project(':samples:attachment-demo:contracts')
|
||||
cordapp project(':samples:attachment-demo:workflows')
|
||||
|
||||
testCompile(project(':node-driver')) {
|
||||
// We already have a SLF4J implementation on our runtime classpath,
|
||||
// and we don't need another one.
|
||||
exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl'
|
||||
}
|
||||
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
|
||||
testImplementation "junit:junit:$junit_version"
|
||||
|
||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
|
||||
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
|
||||
|
||||
testCompile "org.assertj:assertj-core:$assertj_version"
|
||||
|
||||
integrationTestCompile project(':testing:testserver')
|
||||
}
|
||||
|
||||
task integrationTest(type: Test, dependsOn: []) {
|
||||
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
|
||||
def nodeTask = tasks.getByPath(':node:capsule:assemble')
|
||||
def webTask = tasks.getByPath(':testing:testserver:testcapsule::assemble')
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, webTask]) {
|
||||
ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': ["StartFlow.net.corda.attachmentdemo.AttachmentDemoFlow",
|
||||
"InvokeRpc.partiesFromName",
|
||||
"InvokeRpc.notaryPartyFromX500Name",
|
||||
"InvokeRpc.attachmentExists",
|
||||
"InvokeRpc.openAttachment",
|
||||
"InvokeRpc.uploadAttachment",
|
||||
"InvokeRpc.internalVerifiedTransactionsFeed",
|
||||
"InvokeRpc.startTrackedFlowDynamic",
|
||||
"InvokeRpc.nodeInfo"]]]
|
||||
|
||||
nodeDefaults {
|
||||
projectCordapp {
|
||||
deploy = false
|
||||
}
|
||||
cordapp project(':samples:attachment-demo:contracts')
|
||||
cordapp project(':samples:attachment-demo:workflows')
|
||||
runSchemaMigration = true
|
||||
}
|
||||
node {
|
||||
name "O=Notary Node,L=Zurich,C=CH"
|
||||
notary = [validating: true,
|
||||
serviceLegalName: "O=Notary Service,L=Zurich,C=CH"
|
||||
]
|
||||
p2pPort 10002
|
||||
cordapps = []
|
||||
rpcUsers = ext.rpcUsers
|
||||
rpcSettings {
|
||||
address "localhost:10003"
|
||||
adminAddress "localhost:10004"
|
||||
}
|
||||
extraConfig = ['h2Settings.address': 'localhost:10012']
|
||||
}
|
||||
node {
|
||||
name "O=Bank A,L=London,C=GB"
|
||||
p2pPort 10005
|
||||
cordapps = []
|
||||
rpcUsers = ext.rpcUsers
|
||||
rpcSettings {
|
||||
address "localhost:10006"
|
||||
adminAddress "localhost:10007"
|
||||
}
|
||||
extraConfig = ['h2Settings.address': 'localhost:10013']
|
||||
}
|
||||
node {
|
||||
name "O=Bank B,L=New York,C=US"
|
||||
p2pPort 10008
|
||||
rpcSettings {
|
||||
address "localhost:10009"
|
||||
adminAddress "localhost:10011"
|
||||
}
|
||||
webPort 10010
|
||||
cordapps = []
|
||||
rpcUsers = ext.rpcUsers
|
||||
extraConfig = ['h2Settings.address': 'localhost:10014']
|
||||
}
|
||||
}
|
||||
|
||||
task runSender(type: JavaExec, dependsOn: jar) {
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
main = 'net.corda.attachmentdemo.AttachmentDemoKt'
|
||||
args '--role'
|
||||
args 'SENDER'
|
||||
}
|
||||
|
||||
task runRecipient(type: JavaExec, dependsOn: jar) {
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
main = 'net.corda.attachmentdemo.AttachmentDemoKt'
|
||||
args '--role'
|
||||
args 'RECIPIENT'
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'net.corda.plugins.cordapp'
|
||||
|
||||
description 'Corda attachment demo - contracts'
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
cordaCompile project(':core')
|
||||
}
|
||||
|
||||
cordapp {
|
||||
targetPlatformVersion corda_platform_version.toInteger()
|
||||
minimumPlatformVersion 1
|
||||
contract {
|
||||
name "Corda Attachment Demo"
|
||||
versionId 1
|
||||
vendor "R3"
|
||||
licence "Open Source (Apache 2)"
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
baseName 'corda-attachment-demo-contracts'
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package net.corda.attachmentdemo.contracts
|
||||
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TypeOnlyCommandData
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
|
||||
class AttachmentContract : Contract {
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val state = tx.outputsOfType<State>().single()
|
||||
// we check that at least one has the matching hash, the other will be the contract
|
||||
require(tx.attachments.any { it.id == state.hash }) {"At least one attachment in transaction must match hash ${state.hash}"}
|
||||
}
|
||||
|
||||
object Command : TypeOnlyCommandData()
|
||||
|
||||
data class State(val hash: SecureHash.SHA256) : ContractState {
|
||||
override val participants: List<AbstractParty> = emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
const val ATTACHMENT_PROGRAM_ID = "net.corda.attachmentdemo.contracts.AttachmentContract"
|
@ -1,53 +0,0 @@
|
||||
package net.corda.attachmentdemo
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.Permissions.Companion.all
|
||||
import net.corda.testing.core.DUMMY_BANK_A_NAME
|
||||
import net.corda.testing.core.DUMMY_BANK_B_NAME
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.driver.internal.incrementalPortAllocation
|
||||
import net.corda.testing.node.NotarySpec
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.DummyClusterSpec
|
||||
import net.corda.testing.node.internal.findCordapp
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.CompletableFuture.supplyAsync
|
||||
|
||||
class AttachmentDemoTest {
|
||||
// run with a 10,000,000 bytes in-memory zip file. In practice, a slightly bigger file will be used (~10,002,000 bytes).
|
||||
@Test(timeout=300_000)
|
||||
fun `attachment demo using a 10MB zip file`() {
|
||||
val numOfExpectedBytes = 10_000_000
|
||||
driver(DriverParameters(
|
||||
portAllocation = incrementalPortAllocation(),
|
||||
startNodesInProcess = true,
|
||||
cordappsForAllNodes = listOf(findCordapp("net.corda.attachmentdemo.contracts"), findCordapp("net.corda.attachmentdemo.workflows")),
|
||||
notarySpecs = listOf(NotarySpec(name = DUMMY_NOTARY_NAME, cluster = DummyClusterSpec(clusterSize = 1))))
|
||||
) {
|
||||
val demoUser = listOf(User("demo", "demo", setOf(all())))
|
||||
val (nodeA, nodeB) = listOf(
|
||||
startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = demoUser, maximumHeapSize = "1g"),
|
||||
startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = demoUser, maximumHeapSize = "1g")
|
||||
).map { it.getOrThrow() }
|
||||
val webserverHandle = startWebserver(nodeB).getOrThrow()
|
||||
|
||||
val senderThread = supplyAsync {
|
||||
CordaRPCClient(nodeA.rpcAddress).start(demoUser[0].username, demoUser[0].password).use {
|
||||
sender(it.proxy, numOfExpectedBytes)
|
||||
}
|
||||
}
|
||||
|
||||
val recipientThread = supplyAsync {
|
||||
CordaRPCClient(nodeB.rpcAddress).start(demoUser[0].username, demoUser[0].password).use {
|
||||
recipient(it.proxy, webserverHandle.listenAddress.port)
|
||||
}
|
||||
}
|
||||
|
||||
senderThread.getOrThrow()
|
||||
recipientThread.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,136 +0,0 @@
|
||||
package net.corda.attachmentdemo
|
||||
|
||||
import joptsimple.OptionParser
|
||||
import net.corda.attachmentdemo.contracts.AttachmentContract
|
||||
import net.corda.attachmentdemo.workflows.AttachmentDemoFlow
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.internal.InputStreamAndHash
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startTrackedFlow
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import java.io.InputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.jar.JarInputStream
|
||||
import javax.servlet.http.HttpServletResponse.SC_OK
|
||||
import javax.ws.rs.core.HttpHeaders.CONTENT_DISPOSITION
|
||||
import javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
internal enum class Role {
|
||||
SENDER,
|
||||
RECIPIENT
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val parser = OptionParser()
|
||||
|
||||
val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required()
|
||||
val options = try {
|
||||
parser.parse(*args)
|
||||
} catch (e: Exception) {
|
||||
println(e.message)
|
||||
printHelp(parser)
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
val role = options.valueOf(roleArg)!!
|
||||
when (role) {
|
||||
Role.SENDER -> {
|
||||
val host = NetworkHostAndPort("localhost", 10006)
|
||||
println("Connecting to sender node ($host)")
|
||||
CordaRPCClient(host).start("demo", "demo").use {
|
||||
sender(it.proxy)
|
||||
}
|
||||
}
|
||||
Role.RECIPIENT -> {
|
||||
val host = NetworkHostAndPort("localhost", 10009)
|
||||
println("Connecting to the recipient node ($host)")
|
||||
CordaRPCClient(host).start("demo", "demo").use {
|
||||
recipient(it.proxy, 10010)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** An in memory test zip attachment of at least numOfClearBytes size, will be used. */
|
||||
// DOCSTART 2
|
||||
fun sender(rpc: CordaRPCOps, numOfClearBytes: Int = 1024) { // default size 1K.
|
||||
val (inputStream, hash) = InputStreamAndHash.createInMemoryTestZip(numOfClearBytes, 0)
|
||||
sender(rpc, inputStream, hash)
|
||||
}
|
||||
|
||||
private fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.SHA256) {
|
||||
// Get the identity key of the other side (the recipient).
|
||||
val notaryParty = rpc.notaryPartyFromX500Name(CordaX500Name.parse("O=Notary Service,L=Zurich,C=CH")) ?: throw IllegalArgumentException("Couldn't find notary party")
|
||||
val bankBParty = rpc.partiesFromName("Bank B", false).firstOrNull() ?: throw IllegalArgumentException("Couldn't find Bank B party")
|
||||
// Make sure we have the file in storage
|
||||
if (!rpc.attachmentExists(hash)) {
|
||||
inputStream.use {
|
||||
val id = rpc.uploadAttachment(it)
|
||||
require(hash == id) { "Id was '$id' instead of '$hash'" }
|
||||
}
|
||||
require(rpc.attachmentExists(hash)) { "Attachment matching hash: $hash does not exist" }
|
||||
}
|
||||
|
||||
val flowHandle = rpc.startTrackedFlow(::AttachmentDemoFlow, bankBParty, notaryParty, hash)
|
||||
flowHandle.progress.subscribe(::println)
|
||||
val stx = flowHandle.returnValue.getOrThrow()
|
||||
println("Sent ${stx.id}")
|
||||
}
|
||||
// DOCEND 2
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
// DOCSTART 1
|
||||
fun recipient(rpc: CordaRPCOps, webPort: Int) {
|
||||
println("Waiting to receive transaction ...")
|
||||
val stx = rpc.internalVerifiedTransactionsFeed().updates.toBlocking().first()
|
||||
val wtx = stx.tx
|
||||
if (wtx.attachments.isNotEmpty()) {
|
||||
if (wtx.outputs.isNotEmpty()) {
|
||||
val state = wtx.outputsOfType<AttachmentContract.State>().single()
|
||||
require(rpc.attachmentExists(state.hash)) { "attachment matching hash: ${state.hash} does not exist" }
|
||||
|
||||
// Download the attachment via the Web endpoint.
|
||||
val connection = URL("http://localhost:$webPort/attachments/${state.hash}").openConnection() as HttpURLConnection
|
||||
try {
|
||||
require(connection.responseCode == SC_OK) { "HTTP status code was ${connection.responseCode}" }
|
||||
require(connection.contentType == APPLICATION_OCTET_STREAM) { "Content-Type header was ${connection.contentType}" }
|
||||
require(connection.getHeaderField(CONTENT_DISPOSITION) == "attachment; filename=\"${state.hash}.zip\"") {
|
||||
"Content-Disposition header was ${connection.getHeaderField(CONTENT_DISPOSITION)}"
|
||||
}
|
||||
|
||||
// Write out the entries inside this jar.
|
||||
println("Attachment JAR contains these entries:")
|
||||
JarInputStream(connection.inputStream).use {
|
||||
while (true) {
|
||||
val e = it.nextJarEntry ?: break
|
||||
println("Entry> ${e.name}")
|
||||
it.closeEntry()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
println("File received - we're happy!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(wtx)}")
|
||||
} else {
|
||||
println("Error: no output state found in ${wtx.id}")
|
||||
}
|
||||
} else {
|
||||
println("Error: no attachments found in ${wtx.id}")
|
||||
}
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
private fun printHelp(parser: OptionParser) {
|
||||
println("""
|
||||
Usage: attachment-demo --role [RECIPIENT|SENDER] [options]
|
||||
Please refer to the documentation in docs/build/index.html for more info.
|
||||
|
||||
""".trimIndent())
|
||||
parser.printHelpOn(System.out)
|
||||
}
|
Binary file not shown.
@ -1,3 +0,0 @@
|
||||
org.slf4j.simpleLogger.defaultLogLevel=info
|
||||
org.slf4j.simpleLogger.showDateTime=true
|
||||
org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z
|
@ -1,36 +0,0 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.cordapp'
|
||||
|
||||
description 'Corda attachment demo - workflows'
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
|
||||
// Corda integration dependencies
|
||||
cordaCompile project(':core')
|
||||
cordapp project(':samples:attachment-demo:contracts')
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
downloadJavadoc = true // defaults to false
|
||||
downloadSources = true
|
||||
}
|
||||
}
|
||||
|
||||
cordapp {
|
||||
targetPlatformVersion corda_platform_version.toInteger()
|
||||
minimumPlatformVersion 1
|
||||
workflow {
|
||||
name "Corda Attachment Demo"
|
||||
versionId 1
|
||||
vendor "R3"
|
||||
licence "Open Source (Apache 2)"
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
baseName 'corda-attachment-demo-workflows'
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package net.corda.attachmentdemo.workflows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.attachmentdemo.contracts.ATTACHMENT_PROGRAM_ID
|
||||
import net.corda.attachmentdemo.contracts.AttachmentContract
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.StatesToRecord
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class AttachmentDemoFlow(private val otherSide: Party,
|
||||
private val notary: Party,
|
||||
private val attachId: SecureHash.SHA256) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
object SIGNING : ProgressTracker.Step("Signing transaction")
|
||||
|
||||
override val progressTracker: ProgressTracker = ProgressTracker(SIGNING)
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
// Create a trivial transaction with an output that describes the attachment, and the attachment itself
|
||||
val ptx = TransactionBuilder(notary)
|
||||
.addOutputState(AttachmentContract.State(attachId), ATTACHMENT_PROGRAM_ID)
|
||||
.addCommand(AttachmentContract.Command, ourIdentity.owningKey)
|
||||
.addAttachment(attachId)
|
||||
|
||||
progressTracker.currentStep = SIGNING
|
||||
|
||||
val stx = serviceHub.signInitialTransaction(ptx)
|
||||
|
||||
// Send the transaction to the other recipient
|
||||
return subFlow(FinalityFlow(stx, initiateFlow(otherSide)))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(AttachmentDemoFlow::class)
|
||||
class StoreAttachmentFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// As a non-participant to the transaction we need to record all states
|
||||
subFlow(ReceiveFinalityFlow(otherSide, statesToRecord = StatesToRecord.ALL_VISIBLE))
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
@StartableByService
|
||||
class NoProgressTrackerShellDemo : FlowLogic<String>() {
|
||||
@Suspendable
|
||||
override fun call(): String {
|
||||
return "You Called me!"
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
These certificates are used for development mode only (and are copies of those contained within the TraderDemo jar file)
|
Binary file not shown.
Binary file not shown.
@ -1,53 +0,0 @@
|
||||
Bank Of Corda demo
|
||||
------------------
|
||||
|
||||
This demo brings up three nodes: a notary, a node acting as the Bank of Corda that accepts requests for issuance of
|
||||
some asset and a node acting as Big Corporation which requests issuance of an asset (cash in this example).
|
||||
|
||||
Upon receipt of a request the Bank of Corda node self-issues the asset and then transfers ownership to the requester
|
||||
after successful notarisation and recording of the issue transaction on the ledger.
|
||||
|
||||
.. note:: The Bank of Corda is somewhat like a "Bitcoin faucet" that dispenses free bitcoins to developers for
|
||||
testing and experimentation purposes.
|
||||
|
||||
To run from the command line in Unix:
|
||||
|
||||
1. Run ``./gradlew samples:bank-of-corda-demo:deployNodes`` to create a set of configs and installs under
|
||||
``samples/bank-of-corda-demo/build/nodes``
|
||||
2. Run ``./samples/bank-of-corda-demo/build/nodes/runnodes`` to open up three new terminal tabs/windows with the three
|
||||
nodes
|
||||
3. Run ``./gradlew samples:bank-of-corda-demo:runRPCCashIssue`` to trigger a cash issuance request
|
||||
4. Run ``./gradlew samples:bank-of-corda-demo:runWebCashIssue`` to trigger another cash issuance request.
|
||||
Now look at your terminal tab/window to see the output of the demo
|
||||
|
||||
To run from the command line in Windows:
|
||||
|
||||
1. Run ``gradlew samples:bank-of-corda-demo:deployNodes`` to create a set of configs and installs under
|
||||
``samples\bank-of-corda-demo\build\nodes``
|
||||
2. Run ``samples\bank-of-corda-demo\build\nodes\runnodes`` to open up three new terminal tabs/windows with the three
|
||||
nodes
|
||||
3. Run ``gradlew samples:bank-of-corda-demo:runRPCCashIssue`` to trigger a cash issuance request
|
||||
4. Run ``gradlew samples:bank-of-corda-demo:runWebCashIssue`` to trigger another cash issuance request.
|
||||
Now look at the your terminal tab/window to see the output of the demo
|
||||
|
||||
To verify that the Bank of Corda node is alive and running, navigate to the following URL:
|
||||
http://localhost:10007/api/bank/date
|
||||
|
||||
In the window you run the command you should see (in case of Web, RPC is similar):
|
||||
|
||||
- Requesting Cash via Web ...
|
||||
- Successfully processed Cash Issue request
|
||||
|
||||
If you want to see flow activity enter in node's shell ``flow watch``. It will display all state machines running
|
||||
currently on the node.
|
||||
|
||||
Launch the Explorer application to visualize the issuance and transfer of cash for each node:
|
||||
|
||||
``./gradlew tools:explorer:run`` (on Unix) or ``gradlew tools:explorer:run`` (on Windows)
|
||||
|
||||
Using the following login details:
|
||||
|
||||
- For the Bank of Corda node: localhost / port 10006 / username bankUser / password test
|
||||
- For the Big Corporation node: localhost / port 10009 / username bigCorpUser / password test
|
||||
|
||||
See https://docs.corda.net/node-explorer.html for further details on usage.
|
@ -1,153 +0,0 @@
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.cordapp'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
|
||||
// The bank of corda CorDapp depends upon Cash CorDapp features
|
||||
cordapp project(':finance:contracts')
|
||||
cordapp project(':finance:workflows')
|
||||
|
||||
// Cordformation needs a SLF4J implementation when executing the Network
|
||||
// Bootstrapper, but Log4J doesn't shutdown completely from within Gradle.
|
||||
// Use a much simpler SLF4J implementation here instead.
|
||||
cordaRuntime "org.slf4j:slf4j-simple:$slf4j_version"
|
||||
|
||||
// Corda integration dependencies
|
||||
cordaRuntime project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
cordaRuntime project(path: ":testing:testserver:testcapsule:", configuration: 'runtimeArtifacts')
|
||||
cordaCompile project(':core')
|
||||
cordaCompile project(':client:jfx')
|
||||
cordaCompile project(':client:rpc')
|
||||
cordaCompile(project(':testing:testserver')) {
|
||||
exclude group: "org.apache.logging.log4j"
|
||||
}
|
||||
cordaCompile (project(':node-driver')) {
|
||||
exclude group: "org.apache.logging.log4j"
|
||||
}
|
||||
|
||||
// Javax is required for webapis
|
||||
compile "org.glassfish.jersey.core:jersey-server:${jersey_version}"
|
||||
|
||||
// Test dependencies
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
|
||||
testImplementation "junit:junit:$junit_version"
|
||||
|
||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
|
||||
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
|
||||
}
|
||||
|
||||
def nodeTask = tasks.getByPath(':node:capsule:assemble')
|
||||
def webTask = tasks.getByPath(':testing:testserver:testcapsule::assemble')
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, webTask]) {
|
||||
nodeDefaults {
|
||||
cordapp project(':finance:workflows')
|
||||
cordapp project(':finance:contracts')
|
||||
runSchemaMigration = true
|
||||
}
|
||||
node {
|
||||
name "O=Notary Node,L=Zurich,C=CH"
|
||||
notary = [validating: true,
|
||||
serviceLegalName: "O=Notary Service,L=Zurich,C=CH"
|
||||
]
|
||||
p2pPort 10002
|
||||
rpcSettings {
|
||||
address "localhost:10003"
|
||||
adminAddress "localhost:10004"
|
||||
}
|
||||
extraConfig = [h2Settings: [address: "localhost:10016"]]
|
||||
}
|
||||
node {
|
||||
name "O=BankOfCorda,L=London,C=GB"
|
||||
p2pPort 10005
|
||||
rpcSettings {
|
||||
address "localhost:10006"
|
||||
adminAddress "localhost:10015"
|
||||
}
|
||||
webPort 10007
|
||||
rpcUsers = [[user: "bankUser", password: "test", permissions: ["ALL"]]]
|
||||
extraConfig = [
|
||||
h2Settings: [address: "localhost:10017"]
|
||||
]
|
||||
cordapp(project(':finance:workflows')) {
|
||||
config "issuableCurrencies = [ USD ]"
|
||||
}
|
||||
}
|
||||
node {
|
||||
name "O=BigCorporation,L=New York,C=US"
|
||||
p2pPort 10008
|
||||
rpcSettings {
|
||||
address "localhost:10009"
|
||||
adminAddress "localhost:10011"
|
||||
}
|
||||
webPort 10010
|
||||
rpcUsers = [[user: "bigCorpUser", password: "test", permissions: ["ALL"]]]
|
||||
extraConfig = [
|
||||
h2Settings: [address: "localhost:10018"]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
downloadJavadoc = true // defaults to false
|
||||
downloadSources = true
|
||||
}
|
||||
}
|
||||
|
||||
task runRPCCashIssue(type: JavaExec) {
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
main = 'net.corda.bank.IssueCash'
|
||||
args '--role'
|
||||
args 'ISSUE_CASH_RPC'
|
||||
args '--quantity'
|
||||
args 20000
|
||||
args '--currency'
|
||||
args 'USD'
|
||||
if (JavaVersion.current() == JavaVersion.VERSION_11) {
|
||||
jvmArgs '--add-opens'
|
||||
jvmArgs 'java.base/java.time=ALL-UNNAMED'
|
||||
jvmArgs '--add-opens'
|
||||
jvmArgs 'java.base/java.io=ALL-UNNAMED'
|
||||
}
|
||||
}
|
||||
|
||||
task runWebCashIssue(type: JavaExec) {
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
main = 'net.corda.bank.IssueCash'
|
||||
args '--role'
|
||||
args 'ISSUE_CASH_WEB'
|
||||
args '--quantity'
|
||||
args 30000
|
||||
args '--currency'
|
||||
args 'GBP'
|
||||
if (JavaVersion.current() == JavaVersion.VERSION_11) {
|
||||
jvmArgs '--add-opens'
|
||||
jvmArgs 'java.base/java.time=ALL-UNNAMED'
|
||||
jvmArgs '--add-opens'
|
||||
jvmArgs 'java.base/java.io=ALL-UNNAMED'
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes(
|
||||
'Automatic-Module-Name': 'net.corda.samples.demos.bankofcorda'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
cordapp {
|
||||
targetPlatformVersion corda_platform_version.toInteger()
|
||||
minimumPlatformVersion 1
|
||||
info {
|
||||
name "Bank of Corda Demo"
|
||||
version "1"
|
||||
vendor "R3"
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
package net.corda.bank
|
||||
|
||||
import joptsimple.OptionParser
|
||||
import net.corda.bank.api.BankOfCordaClientApi
|
||||
import net.corda.bank.api.BankOfCordaWebApi
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.testing.core.BOC_NAME
|
||||
import java.util.*
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
object IssueCash {
|
||||
private val NOTARY_NAME = CordaX500Name(organisation = "Notary Service", locality = "Zurich", country = "CH")
|
||||
private val BIGCORP_NAME = CordaX500Name(organisation = "BigCorporation", locality = "New York", country = "US")
|
||||
private const val BOC_RPC_PORT = 10006
|
||||
private const val BOC_WEB_PORT = 10007
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
val parser = OptionParser()
|
||||
val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).describedAs("[ISSUER|ISSUE_CASH_RPC|ISSUE_CASH_WEB]")
|
||||
val quantity = parser.accepts("quantity").withOptionalArg().ofType(Long::class.java)
|
||||
val currency = parser.accepts("currency").withOptionalArg().ofType(String::class.java).describedAs("[GBP|USD|CHF|EUR]")
|
||||
val options = try {
|
||||
parser.parse(*args)
|
||||
} catch (e: Exception) {
|
||||
println(e.message)
|
||||
printHelp(parser)
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
val role = options.valueOf(roleArg)!!
|
||||
val amount = Amount(options.valueOf(quantity), Currency.getInstance(options.valueOf(currency)))
|
||||
when (role) {
|
||||
Role.ISSUE_CASH_RPC -> {
|
||||
println("Requesting Cash via RPC ...")
|
||||
val result = requestRpcIssue(amount)
|
||||
println("Success!! Your transaction receipt is ${result.tx.id}")
|
||||
}
|
||||
Role.ISSUE_CASH_WEB -> {
|
||||
println("Requesting Cash via Web ...")
|
||||
requestWebIssue(amount)
|
||||
println("Successfully processed Cash Issue request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestRpcIssue(amount: Amount<Currency>): SignedTransaction {
|
||||
return BankOfCordaClientApi.requestRPCIssue(NetworkHostAndPort("localhost", BOC_RPC_PORT), createParams(amount, NOTARY_NAME))
|
||||
}
|
||||
|
||||
private fun requestWebIssue(amount: Amount<Currency>) {
|
||||
BankOfCordaClientApi.requestWebIssue(NetworkHostAndPort("localhost", BOC_WEB_PORT), createParams(amount, NOTARY_NAME))
|
||||
}
|
||||
|
||||
private fun createParams(amount: Amount<Currency>, notaryName: CordaX500Name): BankOfCordaWebApi.IssueRequestParams {
|
||||
return BankOfCordaWebApi.IssueRequestParams(amount, BIGCORP_NAME, "1", BOC_NAME, notaryName)
|
||||
}
|
||||
|
||||
private fun printHelp(parser: OptionParser) {
|
||||
println("""
|
||||
Usage: bank-of-corda --role ISSUER
|
||||
bank-of-corda --role (ISSUE_CASH_RPC|ISSUE_CASH_WEB) --quantity <quantity> --currency <currency>
|
||||
Please refer to the documentation in docs/build/index.html for more info.
|
||||
""".trimIndent())
|
||||
parser.printHelpOn(System.out)
|
||||
}
|
||||
|
||||
enum class Role {
|
||||
ISSUE_CASH_RPC,
|
||||
ISSUE_CASH_WEB,
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
package net.corda.bank.api
|
||||
|
||||
import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.client.rpc.GracefulReconnect
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||
import net.corda.testing.http.HttpApi
|
||||
|
||||
/**
|
||||
* Interface for communicating with Bank of Corda node
|
||||
*/
|
||||
object BankOfCordaClientApi {
|
||||
const val BOC_RPC_USER = "bankUser"
|
||||
const val BOC_RPC_PWD = "test"
|
||||
|
||||
private val logger = loggerFor<BankOfCordaClientApi>()
|
||||
|
||||
/**
|
||||
* HTTP API
|
||||
*/
|
||||
// TODO: security controls required
|
||||
fun requestWebIssue(webAddress: NetworkHostAndPort, params: IssueRequestParams) {
|
||||
val api = HttpApi.fromHostAndPort(webAddress, "api/bank")
|
||||
api.postJson("issue-asset-request", params)
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC API
|
||||
*
|
||||
* @return a payment transaction (following successful issuance of cash to self).
|
||||
*/
|
||||
fun requestRPCIssue(rpcAddress: NetworkHostAndPort, params: IssueRequestParams): SignedTransaction {
|
||||
return requestRPCIssueHA(listOf(rpcAddress), params)
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC API
|
||||
*
|
||||
* @return a cash issue transaction.
|
||||
*/
|
||||
fun requestRPCIssueHA(availableRpcServers: List<NetworkHostAndPort>, params: IssueRequestParams): SignedTransaction {
|
||||
// TODO: privileged security controls required
|
||||
CordaRPCClient(availableRpcServers)
|
||||
.start(BOC_RPC_USER, BOC_RPC_PWD, gracefulReconnect = GracefulReconnect()).use { rpc->
|
||||
rpc.proxy.waitUntilNetworkReady().getOrThrow()
|
||||
|
||||
// Resolve parties via RPC
|
||||
val issueToParty = rpc.proxy.wellKnownPartyFromX500Name(params.issueToPartyName)
|
||||
?: throw IllegalStateException("Unable to locate ${params.issueToPartyName} in Network Map Service")
|
||||
val notaryLegalIdentity = rpc.proxy.notaryIdentities().firstOrNull { it.name == params.notaryName }
|
||||
?: throw IllegalStateException("Couldn't locate notary ${params.notaryName} in NetworkMapCache")
|
||||
|
||||
val anonymous = true
|
||||
val issuerBankPartyRef = OpaqueBytes.of(params.issuerBankPartyRef.toByte())
|
||||
|
||||
logger.info("${rpc.proxy.nodeInfo()} issuing ${params.amount} to transfer to $issueToParty ...")
|
||||
return rpc.proxy.startFlow(
|
||||
::CashIssueAndPaymentFlow,
|
||||
params.amount,
|
||||
issuerBankPartyRef,
|
||||
issueToParty,
|
||||
anonymous,
|
||||
notaryLegalIdentity
|
||||
)
|
||||
.returnValue.getOrThrow().stx
|
||||
}
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package net.corda.bank.api
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
import javax.ws.rs.*
|
||||
import javax.ws.rs.core.MediaType
|
||||
import javax.ws.rs.core.Response
|
||||
|
||||
// API is accessible from /api/bank. All paths specified below are relative to it.
|
||||
@Path("bank")
|
||||
class BankOfCordaWebApi(private val rpc: CordaRPCOps) {
|
||||
data class IssueRequestParams(
|
||||
val amount: Amount<Currency>,
|
||||
val issueToPartyName: CordaX500Name,
|
||||
val issuerBankPartyRef: String,
|
||||
val issuerBankName: CordaX500Name,
|
||||
val notaryName: CordaX500Name
|
||||
)
|
||||
|
||||
private companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("date")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun getCurrentDate(): Any {
|
||||
return mapOf("date" to LocalDateTime.now().toLocalDate())
|
||||
}
|
||||
|
||||
/**
|
||||
* Request asset issuance
|
||||
*/
|
||||
@POST
|
||||
@Path("issue-asset-request")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
fun issueAssetRequest(params: IssueRequestParams): Response {
|
||||
// Resolve parties via RPC
|
||||
val issueToParty = rpc.wellKnownPartyFromX500Name(params.issueToPartyName)
|
||||
?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.issueToPartyName} in identity service").build()
|
||||
rpc.wellKnownPartyFromX500Name(params.issuerBankName) ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.issuerBankName} in identity service").build()
|
||||
val notaryParty = rpc.notaryIdentities().firstOrNull { it.name == params.notaryName }
|
||||
?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate notary ${params.notaryName} in network map").build()
|
||||
|
||||
val anonymous = true
|
||||
val issuerBankPartyRef = OpaqueBytes.of(params.issuerBankPartyRef.toByte())
|
||||
|
||||
// invoke client side of Issuer Flow: IssuanceRequester
|
||||
// The line below blocks and waits for the future to resolve.
|
||||
return try {
|
||||
rpc.startFlow(::CashIssueAndPaymentFlow, params.amount, issuerBankPartyRef, issueToParty, anonymous, notaryParty).returnValue.getOrThrow()
|
||||
logger.info("Issue and payment request completed successfully: $params")
|
||||
Response.status(Response.Status.CREATED).build()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Issue and payment request failed", e)
|
||||
Response.status(Response.Status.FORBIDDEN).build()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package net.corda.bank.plugin
|
||||
|
||||
import net.corda.bank.api.BankOfCordaWebApi
|
||||
import net.corda.webserver.services.WebServerPluginRegistry
|
||||
import java.util.function.Function
|
||||
|
||||
class BankOfCordaPlugin : WebServerPluginRegistry {
|
||||
// A list of classes that expose web APIs.
|
||||
override val webApis = listOf(Function(::BankOfCordaWebApi))
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
# Register a ServiceLoader service extending from net.corda.webserver.services.WebServerPluginRegistry
|
||||
net.corda.bank.plugin.BankOfCordaPlugin
|
@ -1,23 +0,0 @@
|
||||
# Cordapp Configuration Sample
|
||||
|
||||
This sample shows a simple example of how to use per-cordapp configuration. It includes;
|
||||
|
||||
* A configuration file
|
||||
* Gradle build file to show how to install your Cordapp configuration
|
||||
* A flow that consumes the Cordapp configuration
|
||||
|
||||
To run from the command line in Unix:
|
||||
|
||||
1. Run ``./gradlew samples:cordapp-configuration:deployNodes`` to create a set of configs and installs under
|
||||
``samples/cordapp-configuration/build/nodes``
|
||||
2. Run ``./samples/cordapp-configuration/build/nodes/runnodes`` to open up three new terminals with the three nodes
|
||||
3. At the shell prompt for Bank A or Bank B run ``start net.corda.configsample.GetStringConfigFlow configKey: someStringValue``.
|
||||
This will start the flow and read the `someStringValue` CorDapp config.
|
||||
|
||||
To run from the command line in Windows:
|
||||
|
||||
1. Run ``gradlew samples:cordapp-configuration:deployNodes`` to create a set of configs and installs under
|
||||
``samples\cordapp-configuration\build\nodes``
|
||||
2. Run ``samples\cordapp-configuration\build\nodes\runnodes`` to open up three new terminals with the three nodes
|
||||
3. At the shell prompt for Bank A or Bank B run ``start net.corda.configsample.GetStringConfigFlow configKey: someStringValue``.
|
||||
This will start the flow and read the `someStringValue` CorDapp config.
|
@ -1,68 +0,0 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
|
||||
dependencies {
|
||||
runtimeOnly project(':node-api')
|
||||
// Cordformation needs a SLF4J implementation when executing the Network
|
||||
// Bootstrapper, but Log4J doesn't shutdown completely from within Gradle.
|
||||
// Use a much simpler SLF4J implementation here instead.
|
||||
cordaRuntime "org.slf4j:slf4j-simple:$slf4j_version"
|
||||
|
||||
// Corda integration dependencies
|
||||
runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
|
||||
cordapp project(':samples:cordapp-configuration:workflows')
|
||||
}
|
||||
|
||||
def nodeTask = tasks.getByPath(':node:capsule:assemble')
|
||||
def webTask = tasks.getByPath(':testing:testserver:testcapsule::assemble')
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, webTask]) {
|
||||
directory file("$buildDir/nodes")
|
||||
nodeDefaults {
|
||||
projectCordapp {
|
||||
deploy = false // TODO This is a bug, project cordapp should be disabled if no cordapp plugin is applied.
|
||||
}
|
||||
rpcUsers = [['username': "default", 'password': "default", 'permissions': [ 'ALL' ]]]
|
||||
cordapp project(':samples:cordapp-configuration:workflows')
|
||||
runSchemaMigration = true
|
||||
}
|
||||
node {
|
||||
name "O=Notary Node,L=Zurich,C=CH"
|
||||
notary = [validating : true,
|
||||
serviceLegalName: "O=Notary Service,L=Zurich,C=CH"
|
||||
]
|
||||
p2pPort 10002
|
||||
rpcSettings {
|
||||
port 10003
|
||||
adminPort 10004
|
||||
}
|
||||
extraConfig = ['h2Settings.address' : 'localhost:10005']
|
||||
}
|
||||
node {
|
||||
name "O=Bank A,L=London,C=GB"
|
||||
p2pPort 10006
|
||||
// This configures the default cordapp for this node
|
||||
cordapp (project(':samples:cordapp-configuration:workflows')) {
|
||||
config "someStringValue=test"
|
||||
}
|
||||
rpcSettings {
|
||||
port 10007
|
||||
adminPort 10008
|
||||
}
|
||||
extraConfig = ['h2Settings.address' : 'localhost:10009']
|
||||
}
|
||||
node {
|
||||
name "O=Bank B,L=New York,C=US"
|
||||
p2pPort 10010
|
||||
// This configures the default cordapp for this node
|
||||
cordapp (project(':samples:cordapp-configuration:workflows')){
|
||||
config project.file("src/config.conf")
|
||||
}
|
||||
rpcSettings {
|
||||
port 10011
|
||||
adminPort 10012
|
||||
}
|
||||
extraConfig = ['h2Settings.address' : 'localhost:10013']
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info" shutdownHook="disable">
|
||||
|
||||
<!-- Configure Log4J2 for the Network Bootstrapper. -->
|
||||
|
||||
<Properties>
|
||||
<Property name="log-path">build/logs</Property>
|
||||
<Property name="log-name">cordapp-${hostName}</Property>
|
||||
<Property name="archive">${log-path}/archive</Property>
|
||||
</Properties>
|
||||
|
||||
<ThresholdFilter level="trace"/>
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console-Appender" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%-5level] %date{HH:mm:ss,SSSZ} [%t] %c{2}.%method - %msg%n"/>
|
||||
</Console>
|
||||
|
||||
<!-- Required for printBasicInfo -->
|
||||
<Console name="Console-Appender-Println" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%msg%n" />
|
||||
</Console>
|
||||
|
||||
<!-- Will generate up to 10 log files for a given day. During every rollover it will delete
|
||||
those that are older than 60 days, but keep the most recent 10 GB -->
|
||||
<RollingFile name="RollingFile-Appender"
|
||||
fileName="${log-path}/${log-name}.log"
|
||||
filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz">
|
||||
|
||||
<PatternLayout pattern="[%-5level] %date{ISO8601}{UTC}Z [%t] %c{2} - %msg%n"/>
|
||||
|
||||
<Policies>
|
||||
<TimeBasedTriggeringPolicy/>
|
||||
<SizeBasedTriggeringPolicy size="10MB"/>
|
||||
</Policies>
|
||||
|
||||
<DefaultRolloverStrategy min="1" max="10">
|
||||
<Delete basePath="${archive}" maxDepth="1">
|
||||
<IfFileName glob="${log-name}*.log.gz"/>
|
||||
<IfLastModified age="60d">
|
||||
<IfAny>
|
||||
<IfAccumulatedFileSize exceeds="10 GB"/>
|
||||
</IfAny>
|
||||
</IfLastModified>
|
||||
</Delete>
|
||||
</DefaultRolloverStrategy>
|
||||
|
||||
</RollingFile>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="trace">
|
||||
<AppenderRef ref="Console-Appender" level="info"/>
|
||||
<AppenderRef ref="RollingFile-Appender" level="debug"/>
|
||||
</Root>
|
||||
<Logger name="BasicInfo" additivity="false">
|
||||
<AppenderRef ref="Console-Appender-Println"/>
|
||||
<AppenderRef ref="RollingFile-Appender" />
|
||||
</Logger>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
@ -1,17 +0,0 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'net.corda.plugins.cordapp'
|
||||
|
||||
dependencies {
|
||||
cordaCompile project(':core')
|
||||
}
|
||||
|
||||
cordapp {
|
||||
targetPlatformVersion corda_platform_version.toInteger()
|
||||
minimumPlatformVersion 1
|
||||
workflow {
|
||||
name "Cordapp Configuration Sample"
|
||||
versionId 1
|
||||
vendor "R3"
|
||||
licence "Open Source (Apache 2)"
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
someStringValue=hello world
|
||||
someIntValue=1
|
||||
nested: {
|
||||
value: a string
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package net.corda.configsample
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
|
||||
// DOCSTART 1
|
||||
@StartableByRPC
|
||||
class GetStringConfigFlow(private val configKey: String) : FlowLogic<String>() {
|
||||
object READING : ProgressTracker.Step("Reading config")
|
||||
override val progressTracker = ProgressTracker(READING)
|
||||
|
||||
@Suspendable
|
||||
override fun call(): String {
|
||||
progressTracker.currentStep = READING
|
||||
val config = serviceHub.getAppContext().config
|
||||
return config.getString(configKey)
|
||||
}
|
||||
}
|
||||
// DOCEND 1
|
@ -1,62 +0,0 @@
|
||||
# IRS Demo
|
||||
|
||||
This demo brings up three nodes: Bank A, Bank B and a node that simultaneously runs a notary, a network map and an
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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/cordapp/build``
|
||||
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. On Linux, run ``./web/build/webapps/runwebapps.sh`` to open three more terminals for associated webservers. On macOS,
|
||||
use the following command instead: ``osascript ./web/build/webapps/runwebapps.scpt``
|
||||
|
||||
To run from the command line in Windows:
|
||||
|
||||
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: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.bat`` to open up several 3 terminals for each nodes
|
||||
5. Run ``web\build\webapps\runwebapps.bat`` 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/ 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 it.
|
||||
|
||||
*Note:* The IRS web UI currently has a bug when changing the clock time where it may show no numbers or apply fixings
|
||||
inconsistently. The issues will be addressed in a future release. Meanwhile, you can take a look at a simpler oracle
|
||||
example here: https://github.com/corda/oracle-example.
|
||||
|
||||
## Running the system test
|
||||
|
||||
The system test utilizes Docker. The amount of RAM required to run the IRS system test is around 2.5GB, so it is important
|
||||
to make sure the appropriate system resources are allocated (On MacOS/Windows this may require explicit changes to your Docker configuration).
|
||||
|
||||
### Gradle
|
||||
|
||||
The system test is designed to exercise the entire stack, including Corda nodes and the web frontend. It uses [Docker](https://www.docker.com),
|
||||
[docker-compose](https://docs.docker.com/compose/), and
|
||||
[PhantomJS](http://phantomjs.org/). Docker and docker-compose need to be installed and configured to on the system path
|
||||
(which happens by default). The PhantomJs binary has to be put in a known location and needs execution permissions enabled
|
||||
(``chmod a+x phantomjs`` on *nix) and the full path to the binary needs to be available as system property named ``phantomjs.binary.path`` or
|
||||
an environment variable named ``PHANTOMJS_BINARY_PATH``.
|
||||
|
||||
To start the test, run ``:samples:irs-demo:systemTest``.
|
||||
|
||||
### Other
|
||||
|
||||
In order to run the test by other means than the Gradle task - two more environment variables are expected -
|
||||
``CORDAPP_DOCKER_COMPOSE`` and ``WEB_DOCKER_COMPOSE`` which should specify the full path for the docker-compose files for the IRS cordapp
|
||||
and web frontend respectively. Those can be obtained by running the ``:samples:irs-demo:cordapp:prepareDockerNodes`` and
|
||||
``web:generateDockerCompose`` Gradle tasks. The ``:samples:irs-demo:systemTest`` Gradle task simply executes these two tasks and sets up the
|
||||
correct environment variables.
|
||||
|
@ -1,102 +0,0 @@
|
||||
plugins {
|
||||
id "org.springframework.boot" version "1.5.21.RELEASE"
|
||||
id 'io.spring.dependency-management' version '1.0.9.RELEASE' apply false
|
||||
}
|
||||
|
||||
// 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"
|
||||
ext['selenium.version'] = "$selenium_version"
|
||||
ext['jackson.version'] = "$jackson_version"
|
||||
ext['dropwizard-metrics.version'] = "$metrics_version"
|
||||
ext['mockito.version'] = "$mockito_version"
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'application'
|
||||
|
||||
mainClassName = 'net.corda.irs.IRSDemo'
|
||||
|
||||
sourceSets {
|
||||
slowIntegrationTest {
|
||||
kotlin {
|
||||
compileClasspath += main.output + test.output
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/integration-test/kotlin')
|
||||
}
|
||||
}
|
||||
systemTest {
|
||||
kotlin {
|
||||
compileClasspath += main.output + test.output
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/system-test/kotlin')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
slowIntegrationTestCompile.extendsFrom testCompile
|
||||
slowIntegrationTestRuntimeOnly.extendsFrom testRuntimeOnly
|
||||
demoArtifacts.extendsFrom testRuntimeClasspath
|
||||
systemTestCompile.extendsFrom testCompile
|
||||
}
|
||||
|
||||
evaluationDependsOn("cordapp")
|
||||
evaluationDependsOn("web")
|
||||
|
||||
dependencies {
|
||||
compile "commons-io:commons-io:$commons_io_version"
|
||||
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')
|
||||
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
|
||||
testImplementation "junit:junit:$junit_version"
|
||||
|
||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
|
||||
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
|
||||
|
||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||
|
||||
slowIntegrationTestCompile project(path: ":samples:irs-demo:web", configuration: "demoArtifacts")
|
||||
testCompile "com.palantir.docker.compose:docker-compose-rule-junit4:$docker_compose_rule_version"
|
||||
testCompile "org.seleniumhq.selenium:selenium-java:$selenium_version"
|
||||
// testCompile "com.github.detro:ghostdriver:$ghostdriver_version"
|
||||
}
|
||||
|
||||
bootRepackage {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
task slowIntegrationTest(type: Test, dependsOn: []) {
|
||||
testClassesDirs = sourceSets.slowIntegrationTest.output.classesDirs
|
||||
classpath = sourceSets.slowIntegrationTest.runtimeClasspath
|
||||
}
|
||||
|
||||
task systemTest(type: Test, dependsOn: ["cordapp:prepareDockerNodes", "web:generateDockerCompose"]) {
|
||||
testClassesDirs = sourceSets.systemTest.output.classesDirs
|
||||
classpath = sourceSets.systemTest.runtimeClasspath
|
||||
|
||||
systemProperty "CORDAPP_DOCKER_COMPOSE", tasks.getByPath("cordapp:prepareDockerNodes").dockerComposePath.toString()
|
||||
systemProperty "WEB_DOCKER_COMPOSE", tasks.getByPath("web:generateDockerCompose").dockerComposePath.toString()
|
||||
|
||||
def phantomJsPath = System.getProperty("phantomjs.binary.path") ?: System.getenv("PHANTOMJS_BINARY_PATH")
|
||||
if (phantomJsPath != null) {
|
||||
systemProperty "phantomjs.binary.path", phantomJsPath
|
||||
}
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
downloadJavadoc = true // defaults to false
|
||||
downloadSources = true
|
||||
}
|
||||
}
|
@ -1,175 +0,0 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
apply plugin: 'net.corda.plugins.cordapp'
|
||||
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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cordapp {
|
||||
info {
|
||||
name "Corda IRS Demo"
|
||||
vendor "R3"
|
||||
targetPlatformVersion corda_platform_version.toInteger()
|
||||
minimumPlatformVersion 1
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
cordapp project(':finance:contracts')
|
||||
cordapp project(':finance:workflows')
|
||||
|
||||
// Corda integration dependencies
|
||||
cordaRuntime project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
cordaRuntime "org.slf4j:slf4j-simple:$slf4j_version"
|
||||
|
||||
cordapp project(':samples:irs-demo:cordapp:contracts-irs')
|
||||
cordapp project(':samples:irs-demo:cordapp:workflows-irs')
|
||||
}
|
||||
|
||||
def rpcUsersList = [
|
||||
['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",
|
||||
"InvokeRpc.vaultQueryBy",
|
||||
"InvokeRpc.networkMapSnapshot",
|
||||
"InvokeRpc.currentNodeTime",
|
||||
"InvokeRpc.wellKnownPartyFromX500Name"
|
||||
]]
|
||||
]
|
||||
|
||||
def nodeTask = tasks.getByPath(':node:capsule:assemble')
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask]) {
|
||||
nodeDefaults{
|
||||
projectCordapp {
|
||||
deploy = false
|
||||
}
|
||||
cordapp project(':samples:irs-demo:cordapp:contracts-irs')
|
||||
cordapp project(':samples:irs-demo:cordapp:workflows-irs')
|
||||
runSchemaMigration = true
|
||||
}
|
||||
node {
|
||||
name "O=Notary Node,L=Zurich,C=CH"
|
||||
notary = [validating : true,
|
||||
serviceLegalName: "O=Notary Service,L=Zurich,C=CH"
|
||||
]
|
||||
p2pPort 10002
|
||||
rpcSettings {
|
||||
address("localhost:10003")
|
||||
adminAddress("localhost:10023")
|
||||
}
|
||||
cordapps = ["${project(":finance").group}:contracts:$corda_release_version", "${project(":finance").group}:workflows:$corda_release_version"]
|
||||
rpcUsers = rpcUsersList
|
||||
useTestClock true
|
||||
extraConfig = ['h2Settings.address' : 'localhost:10024']
|
||||
}
|
||||
node {
|
||||
name "O=Bank A,L=London,C=GB"
|
||||
p2pPort 10005
|
||||
rpcSettings {
|
||||
address("localhost:10006")
|
||||
adminAddress("localhost:10026")
|
||||
}
|
||||
cordapps = ["${project(":finance").group}:contracts:$corda_release_version", "${project(":finance").group}:workflows:$corda_release_version"]
|
||||
rpcUsers = rpcUsersList
|
||||
useTestClock true
|
||||
extraConfig = ['h2Settings.address' : 'localhost:10027']
|
||||
}
|
||||
node {
|
||||
name "O=Bank B,L=New York,C=US"
|
||||
p2pPort 10008
|
||||
rpcSettings {
|
||||
address("localhost:10009")
|
||||
adminAddress("localhost:10029")
|
||||
}
|
||||
cordapps = ["${project.group}:contracts:$corda_release_version", "${project.group}:workflows:$corda_release_version"]
|
||||
rpcUsers = rpcUsersList
|
||||
useTestClock true
|
||||
extraConfig = ['h2Settings.address' : 'localhost:10030']
|
||||
}
|
||||
node {
|
||||
name "O=Regulator,L=Moscow,C=RU"
|
||||
p2pPort 10011
|
||||
rpcSettings {
|
||||
address("localhost:10012")
|
||||
adminAddress("localhost:10032")
|
||||
}
|
||||
cordapps = ["${project.group}:contracts:$corda_release_version", "${project.group}:workflows:$corda_release_version"]
|
||||
cordapps = ["${project(":finance").group}:contracts:$corda_release_version", "${project(":finance").group}:workflows:$corda_release_version"]
|
||||
rpcUsers = rpcUsersList
|
||||
useTestClock true
|
||||
extraConfig = ['h2Settings.address' : 'localhost:10033']
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
task prepareDockerNodes(type: net.corda.plugins.Dockerform, dependsOn: ['jar', nodeTask]) {
|
||||
nodeDefaults{
|
||||
cordapp project(':samples:irs-demo:cordapp:contracts-irs')
|
||||
cordapp project(':samples:irs-demo:cordapp:workflows-irs')
|
||||
}
|
||||
node {
|
||||
name "O=Notary Service,L=Zurich,C=CH"
|
||||
notary = [validating : true,
|
||||
serviceLegalName: "O=Notary Service,L=Zurich,C=CH"
|
||||
]
|
||||
cordapps = ["${project(":finance").group}:contracts:$corda_release_version", "${project(":finance").group}:workflows:$corda_release_version"]
|
||||
rpcUsers = rpcUsersList
|
||||
useTestClock true
|
||||
}
|
||||
node {
|
||||
name "O=Bank A,L=London,C=GB"
|
||||
cordapps = ["${project(":finance").group}:contracts:$corda_release_version", "${project(":finance").group}:workflows:$corda_release_version"]
|
||||
rpcUsers = rpcUsersList
|
||||
useTestClock true
|
||||
}
|
||||
node {
|
||||
name "O=Bank B,L=New York,C=US"
|
||||
cordapps = ["${project(":finance").group}:contracts:$corda_release_version", "${project(":finance").group}:workflows:$corda_release_version"]
|
||||
rpcUsers = rpcUsersList
|
||||
useTestClock true
|
||||
}
|
||||
node {
|
||||
name "O=Regulator,L=Moscow,C=RU"
|
||||
cordapps = ["${project.group}:contracts:$corda_release_version", "${project.group}:workflows:$corda_release_version"]
|
||||
rpcUsers = rpcUsersList
|
||||
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).configureEach { 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
|
||||
downloadSources = true
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'net.corda.plugins.cordapp'
|
||||
|
||||
dependencies {
|
||||
// The irs demo CorDapp depends upon Cash CorDapp features
|
||||
cordaCompile project(':core')
|
||||
cordaRuntime project(':node-api')
|
||||
cordapp project(':finance:contracts')
|
||||
|
||||
// Apache JEXL: An embeddable expression evaluation library.
|
||||
compile "org.apache.commons:commons-jexl3:3.1"
|
||||
|
||||
compile "com.fasterxml.jackson.core:jackson-annotations:${jackson_version}"
|
||||
|
||||
testCompile project(':node-driver')
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
|
||||
testImplementation "junit:junit:$junit_version"
|
||||
|
||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
|
||||
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
|
||||
}
|
||||
|
||||
cordapp {
|
||||
targetPlatformVersion corda_platform_version.toInteger()
|
||||
minimumPlatformVersion 1
|
||||
contract {
|
||||
name "Corda IRS Demo"
|
||||
versionId 1
|
||||
vendor "R3"
|
||||
licence "Open Source (Apache 2)"
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
baseName 'corda-irs-demo-contracts'
|
||||
}
|
@ -1,747 +0,0 @@
|
||||
package net.corda.irs.contract
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.finance.contracts.*
|
||||
import net.corda.irs.utilities.suggestInterestRateAnnouncementTimeWindow
|
||||
import org.apache.commons.jexl3.JexlBuilder
|
||||
import org.apache.commons.jexl3.MapContext
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
|
||||
const val IRS_PROGRAM_ID = "net.corda.irs.contract.InterestRateSwap"
|
||||
|
||||
// This is a placeholder for some types that we haven't identified exactly what they are just yet for things still in discussion
|
||||
@CordaSerializable
|
||||
open class UnknownType {
|
||||
override fun equals(other: Any?): Boolean = other is UnknownType
|
||||
override fun hashCode() = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Event superclass - everything happens on a date.
|
||||
*/
|
||||
open class Event(val date: LocalDate) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Event) return false
|
||||
|
||||
if (date != other.date) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode() = Objects.hash(date)
|
||||
}
|
||||
|
||||
/**
|
||||
* Top level PaymentEvent class - represents an obligation to pay an amount on a given date, which may be either in the past or the future.
|
||||
*/
|
||||
abstract class PaymentEvent(date: LocalDate) : Event(date) {
|
||||
abstract fun calculate(): Amount<Currency>
|
||||
}
|
||||
|
||||
/**
|
||||
* A [RatePaymentEvent] represents a dated obligation of payment.
|
||||
* It is a specialisation / modification of a basic cash flow event (to be written) that has some additional assistance
|
||||
* functions for interest rate swap legs of the fixed and floating nature.
|
||||
* For the fixed leg, the rate is already known at creation and therefore the flows can be pre-determined.
|
||||
* For the floating leg, the rate refers to a reference rate which is to be "fixed" at a point in the future.
|
||||
*/
|
||||
abstract class RatePaymentEvent(date: LocalDate,
|
||||
val accrualStartDate: LocalDate,
|
||||
val accrualEndDate: LocalDate,
|
||||
val dayCountBasisDay: DayCountBasisDay,
|
||||
val dayCountBasisYear: DayCountBasisYear,
|
||||
val notional: Amount<Currency>,
|
||||
val rate: Rate) : PaymentEvent(date) {
|
||||
companion object {
|
||||
const val CSVHeader = "AccrualStartDate,AccrualEndDate,DayCountFactor,Days,Date,Ccy,Notional,Rate,Flow"
|
||||
}
|
||||
|
||||
override fun calculate(): Amount<Currency> = flow
|
||||
|
||||
abstract val flow: Amount<Currency>
|
||||
|
||||
val days: Int get() = BusinessCalendar.calculateDaysBetween(accrualStartDate, accrualEndDate, dayCountBasisYear, dayCountBasisDay)
|
||||
|
||||
// TODO : Fix below (use daycount convention for division, not hardcoded 360 etc)
|
||||
val dayCountFactor: BigDecimal get() = (BigDecimal(days).divide(BigDecimal(360.0), 8, RoundingMode.HALF_UP)).setScale(4, RoundingMode.HALF_UP)
|
||||
|
||||
open fun asCSV() = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.token},$notional,$rate,$flow"
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is RatePaymentEvent) return false
|
||||
|
||||
if (accrualStartDate != other.accrualStartDate) return false
|
||||
if (accrualEndDate != other.accrualEndDate) return false
|
||||
if (dayCountBasisDay != other.dayCountBasisDay) return false
|
||||
if (dayCountBasisYear != other.dayCountBasisYear) return false
|
||||
if (notional != other.notional) return false
|
||||
if (rate != other.rate) return false
|
||||
// if (flow != other.flow) return false // Flow is derived
|
||||
|
||||
return super.equals(other)
|
||||
}
|
||||
|
||||
override fun hashCode() = super.hashCode() + 31 * Objects.hash(accrualStartDate, accrualEndDate, dayCountBasisDay,
|
||||
dayCountBasisYear, notional, rate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic class for the Fixed Rate Payments on the fixed leg - see [RatePaymentEvent].
|
||||
* Assumes that the rate is valid.
|
||||
*/
|
||||
@CordaSerializable
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class FixedRatePaymentEvent(date: LocalDate,
|
||||
accrualStartDate: LocalDate,
|
||||
accrualEndDate: LocalDate,
|
||||
dayCountBasisDay: DayCountBasisDay,
|
||||
dayCountBasisYear: DayCountBasisYear,
|
||||
notional: Amount<Currency>,
|
||||
rate: Rate) :
|
||||
RatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay, dayCountBasisYear, notional, rate) {
|
||||
companion object {
|
||||
val CSVHeader = RatePaymentEvent.CSVHeader
|
||||
}
|
||||
|
||||
override val flow: Amount<Currency> get() = Amount(dayCountFactor.times(BigDecimal(notional.quantity)).times(rate.ratioUnit!!.value).toLong(), notional.token)
|
||||
|
||||
override fun toString(): String =
|
||||
"FixedRatePaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate : $flow"
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic class for the Floating Rate Payments on the floating leg - see [RatePaymentEvent].
|
||||
* If the rate is null returns a zero payment. // TODO: Is this the desired behaviour?
|
||||
*/
|
||||
@CordaSerializable
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class FloatingRatePaymentEvent(date: LocalDate,
|
||||
accrualStartDate: LocalDate,
|
||||
accrualEndDate: LocalDate,
|
||||
dayCountBasisDay: DayCountBasisDay,
|
||||
dayCountBasisYear: DayCountBasisYear,
|
||||
val fixingDate: LocalDate,
|
||||
notional: Amount<Currency>,
|
||||
rate: Rate) : RatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay, dayCountBasisYear, notional, rate) {
|
||||
|
||||
companion object {
|
||||
val CSVHeader = RatePaymentEvent.CSVHeader + ",FixingDate"
|
||||
}
|
||||
|
||||
override val flow: Amount<Currency>
|
||||
get() {
|
||||
// TODO: Should an uncalculated amount return a zero ? null ? etc.
|
||||
val v = rate.ratioUnit?.value ?: return Amount(0, notional.token)
|
||||
return Amount(dayCountFactor.times(BigDecimal(notional.quantity)).times(v).toLong(), notional.token)
|
||||
}
|
||||
|
||||
override fun toString(): String = "FloatingPaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate (fix on $fixingDate): $flow"
|
||||
|
||||
override fun asCSV(): String = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.token},$notional,$fixingDate,$rate,$flow"
|
||||
|
||||
/**
|
||||
* Used for making immutables.
|
||||
*/
|
||||
fun withNewRate(newRate: Rate): FloatingRatePaymentEvent =
|
||||
FloatingRatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay,
|
||||
dayCountBasisYear, fixingDate, notional, newRate)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
other as FloatingRatePaymentEvent
|
||||
if (fixingDate != other.fixingDate) return false
|
||||
return super.equals(other)
|
||||
}
|
||||
|
||||
override fun hashCode() = super.hashCode() + 31 * Objects.hash(fixingDate)
|
||||
|
||||
// Can't autogenerate as not a data class :-(
|
||||
fun copy(date: LocalDate = this.date,
|
||||
accrualStartDate: LocalDate = this.accrualStartDate,
|
||||
accrualEndDate: LocalDate = this.accrualEndDate,
|
||||
dayCountBasisDay: DayCountBasisDay = this.dayCountBasisDay,
|
||||
dayCountBasisYear: DayCountBasisYear = this.dayCountBasisYear,
|
||||
fixingDate: LocalDate = this.fixingDate,
|
||||
notional: Amount<Currency> = this.notional,
|
||||
rate: Rate = this.rate) = FloatingRatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay, dayCountBasisYear, fixingDate, notional, rate)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The Interest Rate Swap class. For a quick overview of what an IRS is, see here - http://www.pimco.co.uk/EN/Education/Pages/InterestRateSwapsBasics1-08.aspx (no endorsement).
|
||||
* This contract has 4 significant data classes within it, the "Common", "Calculation", "FixedLeg" and "FloatingLeg".
|
||||
* It also has 4 commands, "Agree", "Fix", "Pay" and "Mature".
|
||||
* Currently, we are not interested (excuse pun) in valuing the swap, calculating the PVs, DFs and all that good stuff (soon though).
|
||||
* This is just a representation of a vanilla Fixed vs Floating (same currency) IRS in the R3 prototype model.
|
||||
*/
|
||||
class InterestRateSwap : Contract {
|
||||
/**
|
||||
* This Common area contains all the information that is not leg specific.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Common(
|
||||
val baseCurrency: Currency,
|
||||
val eligibleCurrency: Currency,
|
||||
val eligibleCreditSupport: String,
|
||||
val independentAmounts: Amount<Currency>,
|
||||
val threshold: Amount<Currency>,
|
||||
val minimumTransferAmount: Amount<Currency>,
|
||||
val rounding: Amount<Currency>,
|
||||
val valuationDateDescription: String, // This describes (in english) how regularly the swap is to be valued, e.g. "every local working day"
|
||||
val notificationTime: String,
|
||||
val resolutionTime: String,
|
||||
val interestRate: ReferenceRate,
|
||||
val addressForTransfers: String,
|
||||
val exposure: UnknownType,
|
||||
val localBusinessDay: BusinessCalendar,
|
||||
val dailyInterestAmount: Expression,
|
||||
val tradeID: String,
|
||||
val hashLegalDocs: String
|
||||
)
|
||||
|
||||
/**
|
||||
* The Calculation data class is "mutable" through out the life of the swap, as in, it's the only thing that contains
|
||||
* data that will changed from state to state (Recall that the design insists that everything is immutable, so we actually
|
||||
* copy / update for each transition).
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Calculation(
|
||||
val expression: Expression,
|
||||
val floatingLegPaymentSchedule: Map<LocalDate, FloatingRatePaymentEvent>,
|
||||
val fixedLegPaymentSchedule: Map<LocalDate, FixedRatePaymentEvent>
|
||||
) {
|
||||
/**
|
||||
* Gets the date of the next fixing.
|
||||
* @return LocalDate or null if no more fixings.
|
||||
*/
|
||||
fun nextFixingDate(): LocalDate? {
|
||||
return floatingLegPaymentSchedule.filter { it.value.rate is ReferenceRate }.// TODO - a better way to determine what fixings remain to be fixed
|
||||
minBy { it.value.fixingDate.toEpochDay() }?.value?.fixingDate
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fixing for that date.
|
||||
*/
|
||||
fun getFixing(date: LocalDate): FloatingRatePaymentEvent =
|
||||
floatingLegPaymentSchedule.values.single { it.fixingDate == date }
|
||||
|
||||
/**
|
||||
* Returns a copy after modifying (applying) the fixing for that date.
|
||||
*/
|
||||
fun applyFixing(date: LocalDate, newRate: FixedRate): Calculation {
|
||||
val paymentEvent = getFixing(date)
|
||||
val newFloatingLPS = floatingLegPaymentSchedule + (paymentEvent.date to paymentEvent.withNewRate(newRate))
|
||||
return Calculation(expression = expression,
|
||||
floatingLegPaymentSchedule = newFloatingLPS,
|
||||
fixedLegPaymentSchedule = fixedLegPaymentSchedule)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CommonLeg(
|
||||
val notional: Amount<Currency>,
|
||||
val paymentFrequency: Frequency,
|
||||
val effectiveDate: LocalDate,
|
||||
val effectiveDateAdjustment: DateRollConvention?,
|
||||
val terminationDate: LocalDate,
|
||||
val terminationDateAdjustment: DateRollConvention?,
|
||||
val dayCountBasisDay: DayCountBasisDay,
|
||||
val dayCountBasisYear: DayCountBasisYear,
|
||||
val dayInMonth: Int,
|
||||
val paymentRule: PaymentRule,
|
||||
val paymentDelay: Int,
|
||||
val paymentCalendar: BusinessCalendar,
|
||||
val interestPeriodAdjustment: AccrualAdjustment
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "Notional=$notional,PaymentFrequency=$paymentFrequency,EffectiveDate=$effectiveDate,EffectiveDateAdjustment:$effectiveDateAdjustment,TerminatationDate=$terminationDate," +
|
||||
"TerminationDateAdjustment=$terminationDateAdjustment,DayCountBasis=$dayCountBasisDay/$dayCountBasisYear,DayInMonth=$dayInMonth," +
|
||||
"PaymentRule=$paymentRule,PaymentDelay=$paymentDelay,PaymentCalendar=$paymentCalendar,InterestPeriodAdjustment=$interestPeriodAdjustment"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as CommonLeg
|
||||
|
||||
if (notional != other.notional) return false
|
||||
if (paymentFrequency != other.paymentFrequency) return false
|
||||
if (effectiveDate != other.effectiveDate) return false
|
||||
if (effectiveDateAdjustment != other.effectiveDateAdjustment) return false
|
||||
if (terminationDate != other.terminationDate) return false
|
||||
if (terminationDateAdjustment != other.terminationDateAdjustment) return false
|
||||
if (dayCountBasisDay != other.dayCountBasisDay) return false
|
||||
if (dayCountBasisYear != other.dayCountBasisYear) return false
|
||||
if (dayInMonth != other.dayInMonth) return false
|
||||
if (paymentRule != other.paymentRule) return false
|
||||
if (paymentDelay != other.paymentDelay) return false
|
||||
if (paymentCalendar != other.paymentCalendar) return false
|
||||
if (interestPeriodAdjustment != other.interestPeriodAdjustment) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode() = super.hashCode() + 31 * Objects.hash(notional, paymentFrequency, effectiveDate,
|
||||
effectiveDateAdjustment, terminationDate, effectiveDateAdjustment, terminationDate, terminationDateAdjustment,
|
||||
dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
open class FixedLeg(
|
||||
var fixedRatePayer: AbstractParty,
|
||||
notional: Amount<Currency>,
|
||||
paymentFrequency: Frequency,
|
||||
effectiveDate: LocalDate,
|
||||
effectiveDateAdjustment: DateRollConvention?,
|
||||
terminationDate: LocalDate,
|
||||
terminationDateAdjustment: DateRollConvention?,
|
||||
dayCountBasisDay: DayCountBasisDay,
|
||||
dayCountBasisYear: DayCountBasisYear,
|
||||
dayInMonth: Int,
|
||||
paymentRule: PaymentRule,
|
||||
paymentDelay: Int,
|
||||
paymentCalendar: BusinessCalendar,
|
||||
interestPeriodAdjustment: AccrualAdjustment,
|
||||
var fixedRate: FixedRate,
|
||||
var rollConvention: DateRollConvention // TODO - best way of implementing - still awaiting some clarity
|
||||
) : CommonLeg
|
||||
(notional, paymentFrequency, effectiveDate, effectiveDateAdjustment, terminationDate, terminationDateAdjustment,
|
||||
dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment) {
|
||||
override fun toString(): String = "FixedLeg(Payer=$fixedRatePayer," + super.toString() + ",fixedRate=$fixedRate," +
|
||||
"rollConvention=$rollConvention"
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
other as FixedLeg
|
||||
|
||||
if (fixedRatePayer != other.fixedRatePayer) return false
|
||||
if (fixedRate != other.fixedRate) return false
|
||||
if (rollConvention != other.rollConvention) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode() = super.hashCode() + 31 * Objects.hash(fixedRatePayer, fixedRate, rollConvention)
|
||||
|
||||
// Can't autogenerate as not a data class :-(
|
||||
fun copy(fixedRatePayer: AbstractParty = this.fixedRatePayer,
|
||||
notional: Amount<Currency> = this.notional,
|
||||
paymentFrequency: Frequency = this.paymentFrequency,
|
||||
effectiveDate: LocalDate = this.effectiveDate,
|
||||
effectiveDateAdjustment: DateRollConvention? = this.effectiveDateAdjustment,
|
||||
terminationDate: LocalDate = this.terminationDate,
|
||||
terminationDateAdjustment: DateRollConvention? = this.terminationDateAdjustment,
|
||||
dayCountBasisDay: DayCountBasisDay = this.dayCountBasisDay,
|
||||
dayCountBasisYear: DayCountBasisYear = this.dayCountBasisYear,
|
||||
dayInMonth: Int = this.dayInMonth,
|
||||
paymentRule: PaymentRule = this.paymentRule,
|
||||
paymentDelay: Int = this.paymentDelay,
|
||||
paymentCalendar: BusinessCalendar = this.paymentCalendar,
|
||||
interestPeriodAdjustment: AccrualAdjustment = this.interestPeriodAdjustment,
|
||||
fixedRate: FixedRate = this.fixedRate) = FixedLeg(
|
||||
fixedRatePayer, notional, paymentFrequency, effectiveDate, effectiveDateAdjustment, terminationDate,
|
||||
terminationDateAdjustment, dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay,
|
||||
paymentCalendar, interestPeriodAdjustment, fixedRate, rollConvention)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
open class FloatingLeg(
|
||||
var floatingRatePayer: AbstractParty,
|
||||
notional: Amount<Currency>,
|
||||
paymentFrequency: Frequency,
|
||||
effectiveDate: LocalDate,
|
||||
effectiveDateAdjustment: DateRollConvention?,
|
||||
terminationDate: LocalDate,
|
||||
terminationDateAdjustment: DateRollConvention?,
|
||||
dayCountBasisDay: DayCountBasisDay,
|
||||
dayCountBasisYear: DayCountBasisYear,
|
||||
dayInMonth: Int,
|
||||
paymentRule: PaymentRule,
|
||||
paymentDelay: Int,
|
||||
paymentCalendar: BusinessCalendar,
|
||||
interestPeriodAdjustment: AccrualAdjustment,
|
||||
var rollConvention: DateRollConvention,
|
||||
var fixingRollConvention: DateRollConvention,
|
||||
var resetDayInMonth: Int,
|
||||
var fixingPeriodOffset: Int,
|
||||
var resetRule: PaymentRule,
|
||||
var fixingsPerPayment: Frequency,
|
||||
var fixingCalendar: BusinessCalendar,
|
||||
var index: String,
|
||||
var indexSource: String,
|
||||
var indexTenor: Tenor
|
||||
) : CommonLeg(notional, paymentFrequency, effectiveDate, effectiveDateAdjustment, terminationDate, terminationDateAdjustment,
|
||||
dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment) {
|
||||
override fun toString(): String = "FloatingLeg(Payer=$floatingRatePayer," + super.toString() +
|
||||
"rollConvention=$rollConvention,FixingRollConvention=$fixingRollConvention,ResetDayInMonth=$resetDayInMonth" +
|
||||
"FixingPeriondOffset=$fixingPeriodOffset,ResetRule=$resetRule,FixingsPerPayment=$fixingsPerPayment,FixingCalendar=$fixingCalendar," +
|
||||
"Index=$index,IndexSource=$indexSource,IndexTenor=$indexTenor"
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
other as FloatingLeg
|
||||
|
||||
if (floatingRatePayer != other.floatingRatePayer) return false
|
||||
if (rollConvention != other.rollConvention) return false
|
||||
if (fixingRollConvention != other.fixingRollConvention) return false
|
||||
if (resetDayInMonth != other.resetDayInMonth) return false
|
||||
if (fixingPeriodOffset != other.fixingPeriodOffset) return false
|
||||
if (resetRule != other.resetRule) return false
|
||||
if (fixingsPerPayment != other.fixingsPerPayment) return false
|
||||
if (fixingCalendar != other.fixingCalendar) return false
|
||||
if (index != other.index) return false
|
||||
if (indexSource != other.indexSource) return false
|
||||
if (indexTenor != other.indexTenor) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode() = super.hashCode() + 31 * Objects.hash(floatingRatePayer, rollConvention,
|
||||
fixingRollConvention, resetDayInMonth, fixingPeriodOffset, resetRule, fixingsPerPayment, fixingCalendar,
|
||||
index, indexSource, indexTenor)
|
||||
|
||||
|
||||
fun copy(floatingRatePayer: AbstractParty = this.floatingRatePayer,
|
||||
notional: Amount<Currency> = this.notional,
|
||||
paymentFrequency: Frequency = this.paymentFrequency,
|
||||
effectiveDate: LocalDate = this.effectiveDate,
|
||||
effectiveDateAdjustment: DateRollConvention? = this.effectiveDateAdjustment,
|
||||
terminationDate: LocalDate = this.terminationDate,
|
||||
terminationDateAdjustment: DateRollConvention? = this.terminationDateAdjustment,
|
||||
dayCountBasisDay: DayCountBasisDay = this.dayCountBasisDay,
|
||||
dayCountBasisYear: DayCountBasisYear = this.dayCountBasisYear,
|
||||
dayInMonth: Int = this.dayInMonth,
|
||||
paymentRule: PaymentRule = this.paymentRule,
|
||||
paymentDelay: Int = this.paymentDelay,
|
||||
paymentCalendar: BusinessCalendar = this.paymentCalendar,
|
||||
interestPeriodAdjustment: AccrualAdjustment = this.interestPeriodAdjustment,
|
||||
rollConvention: DateRollConvention = this.rollConvention,
|
||||
fixingRollConvention: DateRollConvention = this.fixingRollConvention,
|
||||
resetDayInMonth: Int = this.resetDayInMonth,
|
||||
fixingPeriod: Int = this.fixingPeriodOffset,
|
||||
resetRule: PaymentRule = this.resetRule,
|
||||
fixingsPerPayment: Frequency = this.fixingsPerPayment,
|
||||
fixingCalendar: BusinessCalendar = this.fixingCalendar,
|
||||
index: String = this.index,
|
||||
indexSource: String = this.indexSource,
|
||||
indexTenor: Tenor = this.indexTenor
|
||||
) = FloatingLeg(floatingRatePayer, notional, paymentFrequency, effectiveDate, effectiveDateAdjustment,
|
||||
terminationDate, terminationDateAdjustment, dayCountBasisDay, dayCountBasisYear, dayInMonth,
|
||||
paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment, rollConvention,
|
||||
fixingRollConvention, resetDayInMonth, fixingPeriod, resetRule, fixingsPerPayment,
|
||||
fixingCalendar, index, indexSource, indexTenor)
|
||||
}
|
||||
|
||||
// These functions may make more sense to use for basket types, but for now let's leave them here
|
||||
private fun checkLegDates(legs: List<CommonLeg>) {
|
||||
requireThat {
|
||||
"Effective date is before termination date" using legs.all { it.effectiveDate < it.terminationDate }
|
||||
"Effective dates are in alignment" using legs.all { it.effectiveDate == legs[0].effectiveDate }
|
||||
"Termination dates are in alignment" using legs.all { it.terminationDate == legs[0].terminationDate }
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkLegAmounts(legs: List<CommonLeg>) {
|
||||
requireThat {
|
||||
"The notional is non zero" using legs.any { it.notional.quantity > (0).toLong() }
|
||||
"The notional for all legs must be the same" using legs.all { it.notional == legs[0].notional }
|
||||
}
|
||||
for (leg: CommonLeg in legs) {
|
||||
if (leg is FixedLeg) {
|
||||
requireThat {
|
||||
// TODO: Confirm: would someone really enter a swap with a negative fixed rate?
|
||||
"Fixed leg rate must be positive" using leg.fixedRate.isPositive()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two schedules of Floating Leg Payments, returns the difference (i.e. omissions in either leg or changes to the values).
|
||||
*/
|
||||
private fun getFloatingLegPaymentsDifferences(payments1: Map<LocalDate, Event>, payments2: Map<LocalDate, Event>): List<Pair<LocalDate, Pair<FloatingRatePaymentEvent, FloatingRatePaymentEvent>>> {
|
||||
val diff1 = payments1.filter { payments1[it.key] != payments2[it.key] }
|
||||
val diff2 = payments2.filter { payments1[it.key] != payments2[it.key] }
|
||||
return (diff1.keys + diff2.keys).map {
|
||||
it to Pair(diff1[it] as FloatingRatePaymentEvent, diff2[it] as FloatingRatePaymentEvent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyAgreeCommand(inputs: List<State>, outputs: List<State>) {
|
||||
val irs = outputs.single()
|
||||
requireThat {
|
||||
"There are no in states for an agreement" using inputs.isEmpty()
|
||||
"There are events in the fix schedule" using (irs.calculation.fixedLegPaymentSchedule.isNotEmpty())
|
||||
"There are events in the float schedule" using (irs.calculation.floatingLegPaymentSchedule.isNotEmpty())
|
||||
"All notionals must be non zero" using (irs.fixedLeg.notional.quantity > 0 && irs.floatingLeg.notional.quantity > 0)
|
||||
"The fixed leg rate must be positive" using (irs.fixedLeg.fixedRate.isPositive())
|
||||
"The currency of the notionals must be the same" using (irs.fixedLeg.notional.token == irs.floatingLeg.notional.token)
|
||||
"All leg notionals must be the same" using (irs.fixedLeg.notional == irs.floatingLeg.notional)
|
||||
"The effective date is before the termination date for the fixed leg" using (irs.fixedLeg.effectiveDate < irs.fixedLeg.terminationDate)
|
||||
"The effective date is before the termination date for the floating leg" using (irs.floatingLeg.effectiveDate < irs.floatingLeg.terminationDate)
|
||||
"The effective dates are aligned" using (irs.floatingLeg.effectiveDate == irs.fixedLeg.effectiveDate)
|
||||
"The termination dates are aligned" using (irs.floatingLeg.terminationDate == irs.fixedLeg.terminationDate)
|
||||
"The fixing period date offset cannot be negative" using (irs.floatingLeg.fixingPeriodOffset >= 0)
|
||||
|
||||
// TODO: further tests
|
||||
}
|
||||
checkLegAmounts(listOf(irs.fixedLeg, irs.floatingLeg))
|
||||
checkLegDates(listOf(irs.fixedLeg, irs.floatingLeg))
|
||||
}
|
||||
|
||||
private fun verifyFixCommand(inputs: List<State>, outputs: List<State>, command: CommandWithParties<Commands.Refix>) {
|
||||
val irs = outputs.single()
|
||||
val prevIrs = inputs.single()
|
||||
val paymentDifferences = getFloatingLegPaymentsDifferences(prevIrs.calculation.floatingLegPaymentSchedule, irs.calculation.floatingLegPaymentSchedule)
|
||||
|
||||
// Having both of these tests are "redundant" as far as verify() goes, however, by performing both
|
||||
// we can relay more information back to the user in the case of failure.
|
||||
requireThat {
|
||||
"There is at least one difference in the IRS floating leg payment schedules" using !paymentDifferences.isEmpty()
|
||||
"There is only one change in the IRS floating leg payment schedule" using (paymentDifferences.size == 1)
|
||||
}
|
||||
|
||||
val (oldFloatingRatePaymentEvent, newFixedRatePaymentEvent) = paymentDifferences.single().second // Ignore the date of the changed rate (we checked that earlier).
|
||||
val fixValue = command.value.fix
|
||||
// Need to check that everything is the same apart from the new fixed rate entry.
|
||||
requireThat {
|
||||
"The fixed leg parties are constant" using (irs.fixedLeg.fixedRatePayer == prevIrs.fixedLeg.fixedRatePayer) // Although superseded by the below test, this is included for a regression issue
|
||||
"The fixed leg is constant" using (irs.fixedLeg == prevIrs.fixedLeg)
|
||||
"The floating leg is constant" using (irs.floatingLeg == prevIrs.floatingLeg)
|
||||
"The common values are constant" using (irs.common == prevIrs.common)
|
||||
"The fixed leg payment schedule is constant" using (irs.calculation.fixedLegPaymentSchedule == prevIrs.calculation.fixedLegPaymentSchedule)
|
||||
"The expression is unchanged" using (irs.calculation.expression == prevIrs.calculation.expression)
|
||||
"There is only one changed payment in the floating leg" using (paymentDifferences.size == 1)
|
||||
"There changed payment is a floating payment" using (oldFloatingRatePaymentEvent.rate is ReferenceRate)
|
||||
"The new payment is a fixed payment" using (newFixedRatePaymentEvent.rate is FixedRate)
|
||||
"The changed payments dates are aligned" using (oldFloatingRatePaymentEvent.date == newFixedRatePaymentEvent.date)
|
||||
"The new payment has the correct rate" using (newFixedRatePaymentEvent.rate.ratioUnit!!.value == fixValue.value)
|
||||
"The fixing is for the next required date" using (prevIrs.calculation.nextFixingDate() == fixValue.of.forDay)
|
||||
"The fix payment has the same currency as the notional" using (newFixedRatePaymentEvent.flow.token == irs.floatingLeg.notional.token)
|
||||
// "The fixing is not in the future " by (fixCommand) // The oracle should not have signed this .
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyPayCommand() {
|
||||
requireThat {
|
||||
"Payments not supported / verifiable yet" using false
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyMatureCommand(inputs: List<State>, outputs: List<State>) {
|
||||
val irs = inputs.single()
|
||||
requireThat {
|
||||
"No more fixings to be applied" using (irs.calculation.nextFixingDate() == null)
|
||||
"The irs is fully consumed and there is no id matched output state" using outputs.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
requireNotNull(tx.timeWindow) { "must be have a time-window)" }
|
||||
val groups: List<LedgerTransaction.InOutGroup<State, UniqueIdentifier>> = tx.groupStates { state -> state.linearId }
|
||||
var atLeastOneCommandProcessed = false
|
||||
for ((inputs, outputs, _) in groups) {
|
||||
val agreeCommand = tx.commands.select<Commands.Agree>().firstOrNull()
|
||||
if (agreeCommand != null) {
|
||||
verifyAgreeCommand(inputs, outputs)
|
||||
atLeastOneCommandProcessed = true
|
||||
}
|
||||
val fixCommand = tx.commands.select<Commands.Refix>().firstOrNull()
|
||||
if (fixCommand != null) {
|
||||
verifyFixCommand(inputs, outputs, fixCommand)
|
||||
atLeastOneCommandProcessed = true
|
||||
}
|
||||
val payCommand = tx.commands.select<Commands.Pay>().firstOrNull()
|
||||
if (payCommand != null) {
|
||||
verifyPayCommand()
|
||||
atLeastOneCommandProcessed = true
|
||||
}
|
||||
val matureCommand = tx.commands.select<Commands.Mature>().firstOrNull()
|
||||
if (matureCommand != null) {
|
||||
verifyMatureCommand(inputs, outputs)
|
||||
atLeastOneCommandProcessed = true
|
||||
}
|
||||
}
|
||||
require(atLeastOneCommandProcessed) { "At least one command needs to present" }
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
data class Refix(val fix: Fix) : Commands // Receive interest rate from oracle, Both sides agree
|
||||
class Pay : TypeOnlyCommandData(), Commands // Not implemented just yet
|
||||
class Agree : TypeOnlyCommandData(), Commands // Both sides agree to trade
|
||||
class Mature : TypeOnlyCommandData(), Commands // Trade has matured; no more actions. Cleanup. // TODO: Do we need this?
|
||||
}
|
||||
|
||||
/**
|
||||
* The state class contains the 4 major data classes.
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class State(
|
||||
val fixedLeg: FixedLeg,
|
||||
val floatingLeg: FloatingLeg,
|
||||
val calculation: Calculation,
|
||||
val common: Common,
|
||||
override val oracle: Party,
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier(common.tradeID)
|
||||
) : FixableDealState, SchedulableState {
|
||||
val ref: String get() = linearId.externalId ?: ""
|
||||
|
||||
override val participants: List<AbstractParty>
|
||||
get() = listOf(fixedLeg.fixedRatePayer, floatingLeg.floatingRatePayer)
|
||||
|
||||
// DOCSTART 1
|
||||
override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? {
|
||||
val nextFixingOf = nextFixingOf() ?: return null
|
||||
|
||||
// This is perhaps not how we should determine the time point in the business day, but instead expect the schedule to detail some of these aspects
|
||||
val instant = suggestInterestRateAnnouncementTimeWindow(index = nextFixingOf.name, source = floatingLeg.indexSource, date = nextFixingOf.forDay).fromTime!!
|
||||
return ScheduledActivity(flowLogicRefFactory.create("net.corda.irs.flows.FixingFlow\$FixingRoleDecider", thisStateRef), instant)
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
override fun generateAgreement(notary: Party): TransactionBuilder {
|
||||
return InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, oracle, notary)
|
||||
}
|
||||
|
||||
override fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) {
|
||||
InterestRateSwap().generateFix(ptx, StateAndRef(TransactionState(this, IRS_PROGRAM_ID, oldState.state.notary, constraint = AlwaysAcceptAttachmentConstraint), oldState.ref), fix)
|
||||
}
|
||||
|
||||
override fun nextFixingOf(): FixOf? {
|
||||
val date = calculation.nextFixingDate()
|
||||
return if (date == null) null else {
|
||||
val fixingEvent = calculation.getFixing(date)
|
||||
val oracleRate = fixingEvent.rate as ReferenceRate
|
||||
FixOf(oracleRate.name, date, oracleRate.tenor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For evaluating arbitrary java on the platform.
|
||||
*/
|
||||
|
||||
fun evaluateCalculation(businessDate: LocalDate, expression: Expression = calculation.expression): Any {
|
||||
// TODO: Jexl is purely for prototyping. It may be replaced
|
||||
// TODO: Whatever we do use must be secure and sandboxed
|
||||
val jexl = JexlBuilder().create()
|
||||
val expr = jexl.createExpression(expression.expr)
|
||||
val jc = MapContext()
|
||||
jc.set("fixedLeg", fixedLeg)
|
||||
jc.set("floatingLeg", floatingLeg)
|
||||
jc.set("calculation", calculation)
|
||||
jc.set("common", common)
|
||||
jc.set("currentBusinessDate", businessDate)
|
||||
return expr.evaluate(jc)
|
||||
}
|
||||
|
||||
/**
|
||||
* Just makes printing it out a bit better for those who don't have 80000 column wide monitors.
|
||||
*/
|
||||
fun prettyPrint() = toString().replace(",", "\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* This generates the agreement state and also the schedules from the initial data.
|
||||
* Note: The day count, interest rate calculation etc are not finished yet, but they are demonstrable.
|
||||
*/
|
||||
fun generateAgreement(floatingLeg: FloatingLeg, fixedLeg: FixedLeg, calculation: Calculation,
|
||||
common: Common, oracle: Party, notary: Party): TransactionBuilder {
|
||||
val fixedLegPaymentSchedule = LinkedHashMap<LocalDate, FixedRatePaymentEvent>()
|
||||
var dates = BusinessCalendar.createGenericSchedule(
|
||||
fixedLeg.effectiveDate,
|
||||
fixedLeg.paymentFrequency,
|
||||
fixedLeg.paymentCalendar,
|
||||
fixedLeg.rollConvention,
|
||||
endDate = fixedLeg.terminationDate)
|
||||
var periodStartDate = fixedLeg.effectiveDate
|
||||
|
||||
// Create a schedule for the fixed payments
|
||||
for (periodEndDate in dates) {
|
||||
val paymentDate = BusinessCalendar.getOffsetDate(periodEndDate, Frequency.Daily, fixedLeg.paymentDelay)
|
||||
val paymentEvent = FixedRatePaymentEvent(
|
||||
paymentDate,
|
||||
periodStartDate,
|
||||
periodEndDate,
|
||||
fixedLeg.dayCountBasisDay,
|
||||
fixedLeg.dayCountBasisYear,
|
||||
fixedLeg.notional,
|
||||
fixedLeg.fixedRate
|
||||
)
|
||||
fixedLegPaymentSchedule[paymentDate] = paymentEvent
|
||||
periodStartDate = periodEndDate
|
||||
}
|
||||
|
||||
dates = BusinessCalendar.createGenericSchedule(floatingLeg.effectiveDate,
|
||||
floatingLeg.fixingsPerPayment,
|
||||
floatingLeg.fixingCalendar,
|
||||
floatingLeg.rollConvention,
|
||||
endDate = floatingLeg.terminationDate)
|
||||
|
||||
val floatingLegPaymentSchedule: MutableMap<LocalDate, FloatingRatePaymentEvent> = LinkedHashMap()
|
||||
periodStartDate = floatingLeg.effectiveDate
|
||||
|
||||
// Now create a schedule for the floating and fixes.
|
||||
for (periodEndDate in dates) {
|
||||
val paymentDate = BusinessCalendar.getOffsetDate(periodEndDate, Frequency.Daily, floatingLeg.paymentDelay)
|
||||
val paymentEvent = FloatingRatePaymentEvent(
|
||||
paymentDate,
|
||||
periodStartDate,
|
||||
periodEndDate,
|
||||
floatingLeg.dayCountBasisDay,
|
||||
floatingLeg.dayCountBasisYear,
|
||||
calcFixingDate(periodStartDate, floatingLeg.fixingPeriodOffset, floatingLeg.fixingCalendar),
|
||||
floatingLeg.notional,
|
||||
ReferenceRate(floatingLeg.indexSource, floatingLeg.indexTenor, floatingLeg.index)
|
||||
)
|
||||
|
||||
floatingLegPaymentSchedule[paymentDate] = paymentEvent
|
||||
periodStartDate = periodEndDate
|
||||
}
|
||||
|
||||
val newCalculation = Calculation(calculation.expression, floatingLegPaymentSchedule, fixedLegPaymentSchedule)
|
||||
|
||||
// Put all the above into a new State object.
|
||||
val state = State(fixedLeg, floatingLeg, newCalculation, common, oracle)
|
||||
return TransactionBuilder(notary)
|
||||
.addCommand(Command(Commands.Agree(), listOf(state.floatingLeg.floatingRatePayer.owningKey, state.fixedLeg.fixedRatePayer.owningKey)))
|
||||
.addOutputState(TransactionState(state, IRS_PROGRAM_ID, notary, null, AlwaysAcceptAttachmentConstraint))
|
||||
}
|
||||
|
||||
private fun calcFixingDate(date: LocalDate, fixingPeriodOffset: Int, calendar: BusinessCalendar): LocalDate {
|
||||
return when (fixingPeriodOffset) {
|
||||
0 -> date
|
||||
else -> calendar.moveBusinessDays(date, DateRollDirection.BACKWARD, fixingPeriodOffset)
|
||||
}
|
||||
}
|
||||
|
||||
fun generateFix(tx: TransactionBuilder, irs: StateAndRef<State>, fixing: Fix) {
|
||||
tx.addInputState(irs)
|
||||
val fixedRate = FixedRate(RatioUnit(fixing.value))
|
||||
tx.addOutputState(
|
||||
irs.state.data.copy(calculation = irs.state.data.calculation.applyFixing(fixing.of.forDay, fixedRate)),
|
||||
irs.state.contract,
|
||||
irs.state.notary,
|
||||
constraint = AlwaysAcceptAttachmentConstraint
|
||||
)
|
||||
tx.addCommand(Commands.Refix(fixing), listOf(irs.state.data.floatingLeg.floatingRatePayer.owningKey, irs.state.data.fixedLeg.fixedRatePayer.owningKey))
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package net.corda.irs.contract
|
||||
|
||||
fun InterestRateSwap.State.exportIRSToCSV(): String =
|
||||
"Fixed Leg\n" + FixedRatePaymentEvent.CSVHeader + "\n" +
|
||||
this.calculation.fixedLegPaymentSchedule.toSortedMap().values.joinToString("\n") { it.asCSV() } + "\n" +
|
||||
"Floating Leg\n" + FloatingRatePaymentEvent.CSVHeader + "\n" +
|
||||
this.calculation.floatingLegPaymentSchedule.toSortedMap().values.joinToString("\n") { it.asCSV() } + "\n"
|
@ -1,96 +0,0 @@
|
||||
package net.corda.irs.contract
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.finance.contracts.Tenor
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
|
||||
// Things in here will move to the general utils class when we've hammered out various discussions regarding amounts, dates, oracle etc.
|
||||
|
||||
/**
|
||||
* A utility class to prevent the various mixups between percentages, decimals, bips etc.
|
||||
*/
|
||||
@CordaSerializable
|
||||
open class RatioUnit(val value: BigDecimal) { // TODO: Discuss this type
|
||||
override fun equals(other: Any?) = (other as? RatioUnit)?.value == value
|
||||
override fun hashCode() = value.hashCode()
|
||||
override fun toString() = value.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* A class to represent a percentage in an unambiguous way.
|
||||
*/
|
||||
open class PercentageRatioUnit(val percentageAsString: String) : RatioUnit(BigDecimal(percentageAsString).divide(BigDecimal("100"))) {
|
||||
override fun toString() = value.times(BigDecimal(100)).toString() + "%"
|
||||
}
|
||||
|
||||
/**
|
||||
* For the convenience of writing "5".percent
|
||||
* Note that we do not currently allow 10.percent (ie no quotes) as this might get a little confusing if 0.1.percent was
|
||||
* written. Additionally, there is a possibility of creating a precision error in the implicit conversion.
|
||||
*/
|
||||
val String.percent: PercentageRatioUnit get() = PercentageRatioUnit(this)
|
||||
|
||||
/**
|
||||
* Parent of the Rate family. Used to denote fixed rates, floating rates, reference rates etc.
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@CordaSerializable
|
||||
open class Rate(val ratioUnit: RatioUnit? = null) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as Rate
|
||||
|
||||
if (ratioUnit != other.ratioUnit) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the hash code of the ratioUnit or zero if the ratioUnit is null, as is the case for floating rate fixings
|
||||
* that have not yet happened. Yet-to-be fixed floating rates need to be equal such that schedules can be tested
|
||||
* for equality.
|
||||
*/
|
||||
override fun hashCode() = ratioUnit?.hashCode() ?: 0
|
||||
|
||||
override fun toString() = ratioUnit.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* A very basic subclass to represent a fixed rate.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) {
|
||||
@JsonIgnore
|
||||
fun isPositive(): Boolean = ratioUnit!!.value > BigDecimal("0.0")
|
||||
|
||||
override fun equals(other: Any?) = other?.javaClass == javaClass && super.equals(other)
|
||||
}
|
||||
|
||||
/**
|
||||
* The parent class of the Floating rate classes.
|
||||
*/
|
||||
@CordaSerializable
|
||||
open class FloatingRate : Rate(null)
|
||||
|
||||
/**
|
||||
* So a reference rate is a rate that takes its value from a source at a given date
|
||||
* e.g. LIBOR 6M as of 17 March 2016. Hence it requires a source (name) and a value date in the getAsOf(..) method.
|
||||
*/
|
||||
class ReferenceRate(val oracle: String, val tenor: Tenor, val name: String) : FloatingRate() {
|
||||
override fun toString(): String = "$name - $tenor"
|
||||
}
|
||||
|
||||
// TODO: For further discussion.
|
||||
operator fun Amount<Currency>.times(other: RatioUnit): Amount<Currency> = Amount((BigDecimal(this.quantity).multiply(other.value)).longValueExact(), this.token)
|
||||
//operator fun Amount<Currency>.times(other: FixedRate): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency)
|
||||
//fun Amount<Currency>.times(other: InterestRateSwap.RatioUnit): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency)
|
||||
|
||||
operator fun kotlin.Int.times(other: FixedRate): Int = BigDecimal(this).multiply(other.ratioUnit!!.value).intValueExact()
|
||||
operator fun Int.times(other: Rate): Int = BigDecimal(this).multiply(other.ratioUnit!!.value).intValueExact()
|
||||
operator fun Int.times(other: RatioUnit): Int = BigDecimal(this).multiply(other.value).intValueExact()
|
@ -1,24 +0,0 @@
|
||||
package net.corda.irs.utilities
|
||||
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.utilities.hours
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
* This whole file exists as short cuts to get demos working. In reality we'd have static data and/or rules engine
|
||||
* defining things like this. It currently resides in the core module because it needs to be visible to the IRS
|
||||
* contract.
|
||||
*/
|
||||
// We at some future point may implement more than just this constant announcement window and thus use the params.
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun suggestInterestRateAnnouncementTimeWindow(index: String, source: String, date: LocalDate): TimeWindow {
|
||||
// TODO: we would ordinarily convert clock to same time zone as the index/source would announce in
|
||||
// and suggest an announcement time for the interest rate
|
||||
// Here we apply a blanket announcement time of 11:45 London irrespective of source or index
|
||||
val time = LocalTime.of(11, 45)
|
||||
val zoneId = ZoneId.of("Europe/London")
|
||||
return TimeWindow.fromStartAndDuration(ZonedDateTime.of(date, time, zoneId).toInstant(), 24.hours)
|
||||
}
|
@ -1,732 +0,0 @@
|
||||
package net.corda.irs.contract
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.EUR
|
||||
import net.corda.finance.contracts.*
|
||||
import net.corda.finance.workflows.utils.loadTestCalendar
|
||||
import net.corda.testing.common.internal.addNotary
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.dsl.*
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.ledger
|
||||
import net.corda.testing.node.transaction
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
private val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
|
||||
private val DUMMY_PARTY = Party(CordaX500Name("Dummy", "Madrid", "ES"), generateKeyPair().public)
|
||||
private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
|
||||
private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||
private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
|
||||
private val ORACLE_PUBKEY = TestIdentity(CordaX500Name("Oracle", "London", "GB")).publicKey
|
||||
private val DUMMY_NOTARY get() = dummyNotary.party
|
||||
private val MEGA_CORP get() = megaCorp.party
|
||||
private val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
|
||||
private val MINI_CORP get() = miniCorp.party
|
||||
fun createDummyIRS(irsSelect: Int): InterestRateSwap.State {
|
||||
return when (irsSelect) {
|
||||
1 -> {
|
||||
|
||||
val fixedLeg = InterestRateSwap.FixedLeg(
|
||||
fixedRatePayer = MEGA_CORP,
|
||||
notional = 15900000.DOLLARS,
|
||||
paymentFrequency = Frequency.SemiAnnual,
|
||||
effectiveDate = LocalDate.of(2016, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2026, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
fixedRate = FixedRate(PercentageRatioUnit("1.677")),
|
||||
dayCountBasisDay = DayCountBasisDay.D30,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 3,
|
||||
paymentCalendar = loadTestCalendar("London") + loadTestCalendar("NewYork"),
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted
|
||||
)
|
||||
|
||||
val floatingLeg = InterestRateSwap.FloatingLeg(
|
||||
floatingRatePayer = MINI_CORP,
|
||||
notional = 15900000.DOLLARS,
|
||||
paymentFrequency = Frequency.Quarterly,
|
||||
effectiveDate = LocalDate.of(2016, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2026, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
dayCountBasisDay = DayCountBasisDay.D30,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
fixingRollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
resetDayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 3,
|
||||
paymentCalendar = loadTestCalendar("London") + loadTestCalendar("NewYork"),
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted,
|
||||
fixingPeriodOffset = 2,
|
||||
resetRule = PaymentRule.InAdvance,
|
||||
fixingsPerPayment = Frequency.Quarterly,
|
||||
fixingCalendar = loadTestCalendar("London"),
|
||||
index = "LIBOR",
|
||||
indexSource = "TEL3750",
|
||||
indexTenor = Tenor("3M")
|
||||
)
|
||||
|
||||
val calculation = InterestRateSwap.Calculation(
|
||||
|
||||
// TODO: this seems to fail quite dramatically
|
||||
//expression = "fixedLeg.notional * fixedLeg.fixedRate",
|
||||
|
||||
// TODO: How I want it to look
|
||||
//expression = "( fixedLeg.notional * (fixedLeg.fixedRate)) - (floatingLeg.notional * (rateSchedule.get(context.getDate('currentDate'))))",
|
||||
|
||||
// How it's ended up looking, which I think is now broken but it's a WIP.
|
||||
expression = Expression("( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -" +
|
||||
"(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))"),
|
||||
|
||||
floatingLegPaymentSchedule = mutableMapOf(),
|
||||
fixedLegPaymentSchedule = mutableMapOf()
|
||||
)
|
||||
|
||||
val common = InterestRateSwap.Common(
|
||||
baseCurrency = EUR,
|
||||
eligibleCurrency = EUR,
|
||||
eligibleCreditSupport = "Cash in an Eligible Currency",
|
||||
independentAmounts = Amount(0, EUR),
|
||||
threshold = Amount(0, EUR),
|
||||
minimumTransferAmount = Amount(250000 * 100, EUR),
|
||||
rounding = Amount(10000 * 100, EUR),
|
||||
valuationDateDescription = "Every Local Business Day",
|
||||
notificationTime = "2:00pm London",
|
||||
resolutionTime = "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
||||
interestRate = ReferenceRate("T3270", Tenor("6M"), "EONIA"),
|
||||
addressForTransfers = "",
|
||||
exposure = UnknownType(),
|
||||
localBusinessDay = loadTestCalendar("London"),
|
||||
tradeID = "trade1",
|
||||
hashLegalDocs = "put hash here",
|
||||
dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360")
|
||||
)
|
||||
|
||||
InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, oracle = DUMMY_PARTY)
|
||||
}
|
||||
2 -> {
|
||||
// 10y swap, we pay 1.3% fixed 30/360 semi, rec 3m usd libor act/360 Q on 25m notional (mod foll/adj on both sides)
|
||||
// I did a mock up start date 10/03/2015 – 10/03/2025 so you have 5 cashflows on float side that have been preset the rest are unknown
|
||||
|
||||
val fixedLeg = InterestRateSwap.FixedLeg(
|
||||
fixedRatePayer = MEGA_CORP,
|
||||
notional = 25000000.DOLLARS,
|
||||
paymentFrequency = Frequency.SemiAnnual,
|
||||
effectiveDate = LocalDate.of(2015, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2025, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
fixedRate = FixedRate(PercentageRatioUnit("1.3")),
|
||||
dayCountBasisDay = DayCountBasisDay.D30,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 0,
|
||||
paymentCalendar = BusinessCalendar.EMPTY,
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted
|
||||
)
|
||||
|
||||
val floatingLeg = InterestRateSwap.FloatingLeg(
|
||||
floatingRatePayer = MINI_CORP,
|
||||
notional = 25000000.DOLLARS,
|
||||
paymentFrequency = Frequency.Quarterly,
|
||||
effectiveDate = LocalDate.of(2015, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2025, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
dayCountBasisDay = DayCountBasisDay.DActual,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
fixingRollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
resetDayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 0,
|
||||
paymentCalendar = BusinessCalendar.EMPTY,
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted,
|
||||
fixingPeriodOffset = 2,
|
||||
resetRule = PaymentRule.InAdvance,
|
||||
fixingsPerPayment = Frequency.Quarterly,
|
||||
fixingCalendar = BusinessCalendar.EMPTY,
|
||||
index = "USD LIBOR",
|
||||
indexSource = "TEL3750",
|
||||
indexTenor = Tenor("3M")
|
||||
)
|
||||
|
||||
val calculation = InterestRateSwap.Calculation(
|
||||
|
||||
// TODO: this seems to fail quite dramatically
|
||||
//expression = "fixedLeg.notional * fixedLeg.fixedRate",
|
||||
|
||||
// TODO: How I want it to look
|
||||
//expression = "( fixedLeg.notional * (fixedLeg.fixedRate)) - (floatingLeg.notional * (rateSchedule.get(context.getDate('currentDate'))))",
|
||||
|
||||
// How it's ended up looking, which I think is now broken but it's a WIP.
|
||||
expression = Expression("( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -" +
|
||||
"(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))"),
|
||||
|
||||
floatingLegPaymentSchedule = mutableMapOf(),
|
||||
fixedLegPaymentSchedule = mutableMapOf()
|
||||
)
|
||||
|
||||
val common = InterestRateSwap.Common(
|
||||
baseCurrency = EUR,
|
||||
eligibleCurrency = EUR,
|
||||
eligibleCreditSupport = "Cash in an Eligible Currency",
|
||||
independentAmounts = Amount(0, EUR),
|
||||
threshold = Amount(0, EUR),
|
||||
minimumTransferAmount = Amount(250000 * 100, EUR),
|
||||
rounding = Amount(10000 * 100, EUR),
|
||||
valuationDateDescription = "Every Local Business Day",
|
||||
notificationTime = "2:00pm London",
|
||||
resolutionTime = "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
||||
interestRate = ReferenceRate("T3270", Tenor("6M"), "EONIA"),
|
||||
addressForTransfers = "",
|
||||
exposure = UnknownType(),
|
||||
localBusinessDay = loadTestCalendar("London"),
|
||||
tradeID = "trade2",
|
||||
hashLegalDocs = "put hash here",
|
||||
dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360")
|
||||
)
|
||||
|
||||
return InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, oracle = DUMMY_PARTY)
|
||||
}
|
||||
else -> TODO("IRS number $irsSelect not defined")
|
||||
}
|
||||
}
|
||||
|
||||
class IRSTests {
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
private val cordappPackages = listOf("net.corda.irs.contract")
|
||||
private val networkParameters = testNetworkParameters().addNotary(dummyNotary.party)
|
||||
private val megaCorpServices = MockServices(cordappPackages, megaCorp, mock(), networkParameters, megaCorp.keyPair)
|
||||
private val miniCorpServices = MockServices(cordappPackages, miniCorp, mock(), networkParameters, miniCorp.keyPair)
|
||||
private val notaryServices = MockServices(cordappPackages, dummyNotary, mock(), networkParameters, dummyNotary.keyPair)
|
||||
private val ledgerServices = MockServices(
|
||||
emptyList(),
|
||||
megaCorp,
|
||||
mock<IdentityService>().also {
|
||||
doReturn(megaCorp.party).whenever(it).partyFromKey(megaCorp.publicKey)
|
||||
doReturn(null).whenever(it).partyFromKey(ORACLE_PUBKEY)
|
||||
},
|
||||
networkParameters
|
||||
)
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun ok() {
|
||||
trade().verifies()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `ok with groups`() {
|
||||
tradegroups().verifies()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an IRS txn - we'll need it for a few things.
|
||||
*/
|
||||
private fun generateIRSTxn(irsSelect: Int): SignedTransaction {
|
||||
val dummyIRS = createDummyIRS(irsSelect)
|
||||
return run {
|
||||
val gtx = InterestRateSwap().generateAgreement(
|
||||
fixedLeg = dummyIRS.fixedLeg,
|
||||
floatingLeg = dummyIRS.floatingLeg,
|
||||
calculation = dummyIRS.calculation,
|
||||
common = dummyIRS.common,
|
||||
oracle = DUMMY_PARTY,
|
||||
notary = DUMMY_NOTARY).apply {
|
||||
setTimeWindow(TEST_TX_TIME, 30.seconds)
|
||||
}
|
||||
val ptx1 = megaCorpServices.signInitialTransaction(gtx)
|
||||
val ptx2 = miniCorpServices.addSignature(ptx1)
|
||||
notaryServices.addSignature(ptx2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Just make sure it's sane.
|
||||
*/
|
||||
@Test(timeout=300_000)
|
||||
fun pprintIRS() {
|
||||
val irs = singleIRS()
|
||||
println(irs.prettyPrint())
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility so I don't have to keep typing this.
|
||||
*/
|
||||
fun singleIRS(irsSelector: Int = 1): InterestRateSwap.State {
|
||||
return generateIRSTxn(irsSelector).tx.outputsOfType<InterestRateSwap.State>().single()
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the generate. No explicit exception as if something goes wrong, we'll find out anyway.
|
||||
*/
|
||||
@Test(timeout=300_000)
|
||||
fun generateIRS() {
|
||||
// Tests aren't allowed to return things
|
||||
generateIRSTxn(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing a simple IRS, add a few fixings and then display as CSV.
|
||||
*/
|
||||
@Test(timeout=300_000)
|
||||
fun `IRS Export test`() {
|
||||
// No transactions etc required - we're just checking simple maths and export functionallity
|
||||
val irs = singleIRS(2)
|
||||
|
||||
var newCalculation = irs.calculation
|
||||
|
||||
val fixings = mapOf(LocalDate.of(2015, 3, 6) to "0.6",
|
||||
LocalDate.of(2015, 6, 8) to "0.75",
|
||||
LocalDate.of(2015, 9, 8) to "0.8",
|
||||
LocalDate.of(2015, 12, 8) to "0.55",
|
||||
LocalDate.of(2016, 3, 8) to "0.644")
|
||||
|
||||
for ((key, value) in fixings) {
|
||||
newCalculation = newCalculation.applyFixing(key, FixedRate(PercentageRatioUnit(value)))
|
||||
}
|
||||
|
||||
val newIRS = InterestRateSwap.State(irs.fixedLeg, irs.floatingLeg, newCalculation, irs.common, DUMMY_PARTY)
|
||||
println(newIRS.exportIRSToCSV())
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure it has a schedule and the schedule has some unfixed rates.
|
||||
*/
|
||||
@Test(timeout=300_000)
|
||||
fun `next fixing date`() {
|
||||
val irs = singleIRS(1)
|
||||
println(irs.calculation.nextFixingDate())
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through all the fix dates and add something.
|
||||
*/
|
||||
@Test(timeout=300_000)
|
||||
fun generateIRSandFixSome() {
|
||||
val services = MockServices(listOf("net.corda.irs.contract"), MEGA_CORP.name,
|
||||
mock<IdentityService>().also {
|
||||
listOf(MEGA_CORP, MINI_CORP).forEach { party ->
|
||||
doReturn(party).whenever(it).partyFromKey(party.owningKey)
|
||||
}
|
||||
},
|
||||
networkParameters = ledgerServices.networkParameters)
|
||||
var previousTXN = generateIRSTxn(1)
|
||||
previousTXN.toLedgerTransaction(services).verify()
|
||||
services.recordTransactions(previousTXN)
|
||||
fun currentIRS() = previousTXN.tx.outputsOfType<InterestRateSwap.State>().single()
|
||||
|
||||
while (true) {
|
||||
val nextFix: FixOf = currentIRS().nextFixingOf() ?: break
|
||||
val fixTX: SignedTransaction = run {
|
||||
val tx = TransactionBuilder(DUMMY_NOTARY)
|
||||
val fixing = Fix(nextFix, "0.052".percent.value)
|
||||
InterestRateSwap().generateFix(tx, previousTXN.tx.outRef(0), fixing)
|
||||
tx.setTimeWindow(TEST_TX_TIME, 30.seconds)
|
||||
val ptx1 = megaCorpServices.signInitialTransaction(tx)
|
||||
val ptx2 = miniCorpServices.addSignature(ptx1)
|
||||
notaryServices.addSignature(ptx2)
|
||||
}
|
||||
fixTX.toLedgerTransaction(services).verify()
|
||||
services.recordTransactions(fixTX)
|
||||
previousTXN = fixTX
|
||||
}
|
||||
}
|
||||
|
||||
// Move these later as they aren't IRS specific.
|
||||
@Test(timeout=300_000)
|
||||
fun `test some rate objects 100 * FixedRate(5%)`() {
|
||||
val r1 = FixedRate(PercentageRatioUnit("5"))
|
||||
assertEquals(5, 100 * r1)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `expression calculation testing`() {
|
||||
val dummyIRS = singleIRS()
|
||||
val stuffToPrint: ArrayList<String> = arrayListOf(
|
||||
"fixedLeg.notional.quantity",
|
||||
"fixedLeg.fixedRate.ratioUnit",
|
||||
"fixedLeg.fixedRate.ratioUnit.value",
|
||||
"floatingLeg.notional.quantity",
|
||||
"fixedLeg.fixedRate",
|
||||
"currentBusinessDate",
|
||||
"calculation.floatingLegPaymentSchedule.get(currentBusinessDate)",
|
||||
"fixedLeg.notional.token.currencyCode",
|
||||
"fixedLeg.notional.quantity * 10",
|
||||
"fixedLeg.notional.quantity * fixedLeg.fixedRate.ratioUnit.value",
|
||||
"(fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360 ",
|
||||
"(fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value))"
|
||||
// "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate"
|
||||
// "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value",
|
||||
//"( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
|
||||
// "( fixedLeg.notional * fixedLeg.fixedRate )"
|
||||
)
|
||||
|
||||
for (i in stuffToPrint) {
|
||||
println(i)
|
||||
val z = dummyIRS.evaluateCalculation(LocalDate.of(2016, 9, 15), Expression(i))
|
||||
println(z.javaClass)
|
||||
println(z)
|
||||
println("-----------")
|
||||
}
|
||||
// This does not throw an exception in the test itself; it evaluates the above and they will throw if they do not pass.
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a typical transactional history for an IRS.
|
||||
*/
|
||||
fun trade(): LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter> {
|
||||
|
||||
val ld = LocalDate.of(2016, 3, 8)
|
||||
val bd = BigDecimal("0.0063518")
|
||||
return ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction("Agreement") {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
output(IRS_PROGRAM_ID, "irs post agreement", singleIRS())
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
transaction("Fix") {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
input("irs post agreement")
|
||||
val postAgreement = "irs post agreement".output<InterestRateSwap.State>()
|
||||
output(IRS_PROGRAM_ID, "irs post first fixing",
|
||||
postAgreement.copy(
|
||||
postAgreement.fixedLeg,
|
||||
postAgreement.floatingLeg,
|
||||
postAgreement.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
|
||||
postAgreement.common))
|
||||
command(ORACLE_PUBKEY,
|
||||
InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun transaction(script: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail) = run {
|
||||
ledgerServices.transaction(DUMMY_NOTARY, script)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `ensure failure occurs when there are inbound states for an agreement command`() {
|
||||
val irs = singleIRS()
|
||||
transaction {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
input(IRS_PROGRAM_ID, irs)
|
||||
output(IRS_PROGRAM_ID, "irs post agreement", irs)
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "There are no in states for an agreement"
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `ensure failure occurs when no events in fix schedule`() {
|
||||
val irs = singleIRS()
|
||||
val emptySchedule = mutableMapOf<LocalDate, FixedRatePaymentEvent>()
|
||||
transaction {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
output(IRS_PROGRAM_ID, irs.copy(calculation = irs.calculation.copy(fixedLegPaymentSchedule = emptySchedule)))
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "There are events in the fix schedule"
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `ensure failure occurs when no events in floating schedule`() {
|
||||
val irs = singleIRS()
|
||||
val emptySchedule = mutableMapOf<LocalDate, FloatingRatePaymentEvent>()
|
||||
transaction {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
output(IRS_PROGRAM_ID, irs.copy(calculation = irs.calculation.copy(floatingLegPaymentSchedule = emptySchedule)))
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "There are events in the float schedule"
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `ensure notionals are non zero`() {
|
||||
val irs = singleIRS()
|
||||
transaction {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
output(IRS_PROGRAM_ID, irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(quantity = 0))))
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "All notionals must be non zero"
|
||||
}
|
||||
|
||||
transaction {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
output(IRS_PROGRAM_ID, irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(quantity = 0))))
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "All notionals must be non zero"
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `ensure positive rate on fixed leg`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(fixedRate = FixedRate(PercentageRatioUnit("-0.1"))))
|
||||
transaction {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
output(IRS_PROGRAM_ID, modifiedIRS)
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "The fixed leg rate must be positive"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This will be modified once we adapt the IRS to be cross currency.
|
||||
*/
|
||||
@Test(timeout=300_000)
|
||||
fun `ensure same currency notionals`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.fixedLeg.notional.quantity, Currency.getInstance("JPY"))))
|
||||
transaction {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
output(IRS_PROGRAM_ID, modifiedIRS)
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "The currency of the notionals must be the same"
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `ensure notional amounts are equal`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.floatingLeg.notional.quantity + 1, irs.floatingLeg.notional.token)))
|
||||
transaction {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
output(IRS_PROGRAM_ID, modifiedIRS)
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "All leg notionals must be the same"
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `ensure trade date and termination date checks are done pt1`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS1 = irs.copy(fixedLeg = irs.fixedLeg.copy(terminationDate = irs.fixedLeg.effectiveDate.minusDays(1)))
|
||||
transaction {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
output(IRS_PROGRAM_ID, modifiedIRS1)
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "The effective date is before the termination date for the fixed leg"
|
||||
}
|
||||
|
||||
val modifiedIRS2 = irs.copy(floatingLeg = irs.floatingLeg.copy(terminationDate = irs.floatingLeg.effectiveDate.minusDays(1)))
|
||||
transaction {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
output(IRS_PROGRAM_ID, modifiedIRS2)
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "The effective date is before the termination date for the floating leg"
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `ensure trade date and termination date checks are done pt2`() {
|
||||
val irs = singleIRS()
|
||||
|
||||
val modifiedIRS3 = irs.copy(floatingLeg = irs.floatingLeg.copy(terminationDate = irs.fixedLeg.terminationDate.minusDays(1)))
|
||||
transaction {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
output(IRS_PROGRAM_ID, modifiedIRS3)
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "The termination dates are aligned"
|
||||
}
|
||||
|
||||
val modifiedIRS4 = irs.copy(floatingLeg = irs.floatingLeg.copy(effectiveDate = irs.fixedLeg.effectiveDate.minusDays(1)))
|
||||
transaction {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
output(IRS_PROGRAM_ID, modifiedIRS4)
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this `fails with` "The effective dates are aligned"
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `various fixing tests`() {
|
||||
val ld = LocalDate.of(2016, 3, 8)
|
||||
val bd = BigDecimal("0.0063518")
|
||||
|
||||
transaction {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
output(IRS_PROGRAM_ID, "irs post agreement", singleIRS())
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
val oldIRS = singleIRS(1)
|
||||
val newIRS = oldIRS.copy(oldIRS.fixedLeg,
|
||||
oldIRS.floatingLeg,
|
||||
oldIRS.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
|
||||
oldIRS.common)
|
||||
|
||||
transaction {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
input(IRS_PROGRAM_ID, oldIRS)
|
||||
|
||||
// Templated tweak for reference. A corrent fixing applied should be ok
|
||||
tweak {
|
||||
command(ORACLE_PUBKEY,
|
||||
InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
output(IRS_PROGRAM_ID, newIRS)
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
// This test makes sure that verify confirms the fixing was applied and there is a difference in the old and new
|
||||
tweak {
|
||||
command(ORACLE_PUBKEY, InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
output(IRS_PROGRAM_ID, oldIRS)
|
||||
this `fails with` "There is at least one difference in the IRS floating leg payment schedules"
|
||||
}
|
||||
|
||||
// This tests tries to sneak in a change to another fixing (which may or may not be the latest one)
|
||||
tweak {
|
||||
command(ORACLE_PUBKEY, InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
|
||||
val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.toList()[1]
|
||||
val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey]
|
||||
val modifiedFirstResetValue = firstResetValue!!.copy(notional = Amount(firstResetValue.notional.quantity, Currency.getInstance("JPY")))
|
||||
output(IRS_PROGRAM_ID,
|
||||
newIRS.copy(
|
||||
newIRS.fixedLeg,
|
||||
newIRS.floatingLeg,
|
||||
newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus(
|
||||
Pair(firstResetKey, modifiedFirstResetValue))),
|
||||
newIRS.common))
|
||||
this `fails with` "There is only one change in the IRS floating leg payment schedule"
|
||||
}
|
||||
|
||||
// This tests modifies the payment currency for the fixing
|
||||
tweak {
|
||||
command(ORACLE_PUBKEY, InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
|
||||
val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key }
|
||||
val modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.quantity, Currency.getInstance("JPY")))
|
||||
output(IRS_PROGRAM_ID,
|
||||
newIRS.copy(
|
||||
newIRS.fixedLeg,
|
||||
newIRS.floatingLeg,
|
||||
newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus(
|
||||
Pair(latestReset.key, modifiedLatestResetValue))),
|
||||
newIRS.common))
|
||||
this `fails with` "The fix payment has the same currency as the notional"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns an example of transactions that are grouped by TradeId and then a fixing applied.
|
||||
* It's important to make the tradeID different for two reasons, the hashes will be the same and all sorts of confusion will
|
||||
* result and the grouping won't work either.
|
||||
* In reality, the only fields that should be in common will be the next fixing date and the reference rate.
|
||||
*/
|
||||
fun tradegroups(): LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter> {
|
||||
val ld1 = LocalDate.of(2016, 3, 8)
|
||||
val bd1 = BigDecimal("0.0063518")
|
||||
|
||||
val irs = singleIRS()
|
||||
return ledgerServices.ledger(DUMMY_NOTARY) {
|
||||
transaction("Agreement") {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
output(IRS_PROGRAM_ID, "irs post agreement1",
|
||||
irs.copy(
|
||||
irs.fixedLeg,
|
||||
irs.floatingLeg,
|
||||
irs.calculation,
|
||||
irs.common.copy(tradeID = "t1")))
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
transaction("Agreement") {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
output(IRS_PROGRAM_ID, "irs post agreement2",
|
||||
irs.copy(
|
||||
linearId = UniqueIdentifier("t2"),
|
||||
fixedLeg = irs.fixedLeg,
|
||||
floatingLeg = irs.floatingLeg,
|
||||
calculation = irs.calculation,
|
||||
common = irs.common.copy(tradeID = "t2")))
|
||||
command(MEGA_CORP_PUBKEY, InterestRateSwap.Commands.Agree())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
transaction("Fix") {
|
||||
attachments(IRS_PROGRAM_ID)
|
||||
input("irs post agreement1")
|
||||
input("irs post agreement2")
|
||||
val postAgreement1 = "irs post agreement1".output<InterestRateSwap.State>()
|
||||
output(IRS_PROGRAM_ID, "irs post first fixing1",
|
||||
postAgreement1.copy(
|
||||
postAgreement1.fixedLeg,
|
||||
postAgreement1.floatingLeg,
|
||||
postAgreement1.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
|
||||
postAgreement1.common.copy(tradeID = "t1")))
|
||||
val postAgreement2 = "irs post agreement2".output<InterestRateSwap.State>()
|
||||
output(IRS_PROGRAM_ID, "irs post first fixing2",
|
||||
postAgreement2.copy(
|
||||
postAgreement2.fixedLeg,
|
||||
postAgreement2.floatingLeg,
|
||||
postAgreement2.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
|
||||
postAgreement2.common.copy(tradeID = "t2")))
|
||||
command(ORACLE_PUBKEY,
|
||||
InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1)))
|
||||
timeWindow(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
org.slf4j.simpleLogger.defaultLogLevel=info
|
||||
org.slf4j.simpleLogger.showDateTime=true
|
||||
org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z
|
@ -1,66 +0,0 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.cordapp'
|
||||
|
||||
configurations {
|
||||
demoArtifacts.extendsFrom testRuntimeClasspath
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The irs demo CorDapp depends upon Cash CorDapp features
|
||||
cordapp project(':finance:contracts')
|
||||
cordapp project(':finance:workflows')
|
||||
|
||||
// Corda integration dependencies
|
||||
cordaCompile project(':core')
|
||||
|
||||
|
||||
compile("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version")
|
||||
|
||||
// only included to control the `DemoClock` as part of the demo application
|
||||
// normally `:node` should not be depended on in any CorDapps
|
||||
compileOnly project(':node')
|
||||
|
||||
// Cordapp dependencies
|
||||
// Specify your cordapp's dependencies below, including dependent cordapps
|
||||
compile "commons-io:commons-io:$commons_io_version"
|
||||
|
||||
testCompile project(':node-driver')
|
||||
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"
|
||||
testImplementation "junit:junit:$junit_version"
|
||||
|
||||
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${junit_vintage_version}"
|
||||
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
|
||||
testRuntimeOnly "org.junit.platform:junit-platform-launcher:${junit_platform_version}"
|
||||
|
||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||
|
||||
cordapp project(':samples:irs-demo:cordapp:contracts-irs')
|
||||
}
|
||||
|
||||
cordapp {
|
||||
targetPlatformVersion corda_platform_version.toInteger()
|
||||
minimumPlatformVersion 1
|
||||
workflow {
|
||||
name "Corda IRS Demo"
|
||||
versionId 1
|
||||
vendor "R3"
|
||||
licence "Open Source (Apache 2)"
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
baseName 'corda-irs-demo-workflows'
|
||||
}
|
||||
|
||||
task testJar(type: Jar) {
|
||||
classifier "tests"
|
||||
from sourceSets.main.output
|
||||
from sourceSets.test.output
|
||||
}
|
||||
|
||||
artifacts {
|
||||
demoArtifacts testJar
|
||||
}
|
@ -1,283 +0,0 @@
|
||||
package net.corda.irs.api
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.node.AppServiceHub
|
||||
import net.corda.core.node.services.CordaService
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.finance.contracts.BusinessCalendar
|
||||
import net.corda.finance.contracts.Fix
|
||||
import net.corda.finance.contracts.FixOf
|
||||
import net.corda.finance.contracts.Tenor
|
||||
import net.corda.finance.workflows.utils.loadTestCalendar
|
||||
import net.corda.irs.flows.RatesFixFlow
|
||||
import net.corda.irs.math.CubicSplineInterpolator
|
||||
import net.corda.irs.math.Interpolator
|
||||
import net.corda.irs.math.InterpolatorFactory
|
||||
import org.apache.commons.io.IOUtils
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import kotlin.collections.HashSet
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.set
|
||||
|
||||
/**
|
||||
* An interest rates service is an oracle that signs transactions which contain embedded assertions about an interest
|
||||
* rate fix (e.g. LIBOR, EURIBOR ...).
|
||||
*
|
||||
* The oracle has two functions. It can be queried for a fix for the given day. And it can sign a transaction that
|
||||
* includes a fix that it finds acceptable. So to use it you would query the oracle, incorporate its answer into the
|
||||
* transaction you are building, and then (after possibly extra steps) hand the final transaction back to the oracle
|
||||
* for signing.
|
||||
*/
|
||||
object NodeInterestRates {
|
||||
// DOCSTART 2
|
||||
@InitiatedBy(RatesFixFlow.FixSignFlow::class)
|
||||
class FixSignHandler(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val request = otherPartySession.receive<RatesFixFlow.SignRequest>().unwrap { it }
|
||||
val oracle = serviceHub.cordaService(Oracle::class.java)
|
||||
otherPartySession.send(oracle.sign(request.ftx))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(RatesFixFlow.FixQueryFlow::class)
|
||||
class FixQueryHandler(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
object RECEIVED : ProgressTracker.Step("Received fix request")
|
||||
object SENDING : ProgressTracker.Step("Sending fix response")
|
||||
|
||||
override val progressTracker = ProgressTracker(RECEIVED, SENDING)
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val request = otherPartySession.receive<RatesFixFlow.QueryRequest>().unwrap { it }
|
||||
progressTracker.currentStep = RECEIVED
|
||||
val oracle = serviceHub.cordaService(Oracle::class.java)
|
||||
val answers = oracle.query(request.queries)
|
||||
progressTracker.currentStep = SENDING
|
||||
otherPartySession.send(answers)
|
||||
}
|
||||
}
|
||||
// DOCEND 2
|
||||
|
||||
/**
|
||||
* An implementation of an interest rate fix oracle which is given data in a simple string format.
|
||||
*
|
||||
* The oracle will try to interpolate the missing value of a tenor for the given fix name and date.
|
||||
*/
|
||||
@ThreadSafe
|
||||
// DOCSTART 3
|
||||
@CordaService
|
||||
class Oracle(private val services: AppServiceHub) : SingletonSerializeAsToken() {
|
||||
private val mutex = ThreadBox(InnerState())
|
||||
|
||||
init {
|
||||
// Set some default fixes to the Oracle, so we can smoothly run the IRS Demo without uploading fixes.
|
||||
// This is required to avoid a situation where the runnodes version of the demo isn't in a good state
|
||||
// upon startup.
|
||||
addDefaultFixes()
|
||||
}
|
||||
// DOCEND 3
|
||||
|
||||
private class InnerState {
|
||||
// TODO Update this to use a database once we have an database API
|
||||
val fixes = HashSet<Fix>()
|
||||
var container: FixContainer = FixContainer(fixes)
|
||||
}
|
||||
|
||||
var knownFixes: FixContainer
|
||||
set(value) {
|
||||
require(value.size > 0)
|
||||
mutex.locked {
|
||||
fixes.clear()
|
||||
fixes.addAll(value.fixes)
|
||||
container = value
|
||||
}
|
||||
}
|
||||
get() = mutex.locked { container }
|
||||
|
||||
@Suspendable
|
||||
fun query(queries: List<FixOf>): List<Fix> {
|
||||
require(queries.isNotEmpty())
|
||||
return mutex.locked {
|
||||
val answers: List<Fix?> = queries.map { container[it] }
|
||||
val firstNull = answers.indexOf(null)
|
||||
if (firstNull != -1) {
|
||||
throw UnknownFix(queries[firstNull])
|
||||
} else {
|
||||
answers.filterNotNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO There is security problem with that. What if transaction contains several commands of the same type, but
|
||||
// Oracle gets signing request for only some of them with a valid partial tree? We sign over a whole transaction.
|
||||
// It will be fixed by adding partial signatures later.
|
||||
// DOCSTART 1
|
||||
fun sign(ftx: FilteredTransaction): TransactionSignature {
|
||||
ftx.verify()
|
||||
// Performing validation of obtained filtered components.
|
||||
fun commandValidator(elem: Command<*>): Boolean {
|
||||
require(services.myInfo.legalIdentities.first().owningKey in elem.signers && elem.value is Fix) {
|
||||
"Oracle received unknown command (not in signers or not Fix)."
|
||||
}
|
||||
val fix = elem.value as Fix
|
||||
val known = knownFixes[fix.of]
|
||||
if (known == null || known != fix)
|
||||
throw UnknownFix(fix.of)
|
||||
return true
|
||||
}
|
||||
|
||||
fun check(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> commandValidator(elem)
|
||||
else -> throw IllegalArgumentException("Oracle received data of different type than expected.")
|
||||
}
|
||||
}
|
||||
|
||||
require(ftx.checkWithFun(::check))
|
||||
ftx.checkCommandVisibility(services.myInfo.legalIdentities.first().owningKey)
|
||||
// It all checks out, so we can return a signature.
|
||||
//
|
||||
// Note that we will happily sign an invalid transaction, as we are only being presented with a filtered
|
||||
// version so we can't resolve or check it ourselves. However, that doesn't matter much, as if we sign
|
||||
// an invalid transaction the signature is worthless.
|
||||
return services.createSignature(ftx, services.myInfo.legalIdentities.first().owningKey)
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
fun uploadFixes(s: String) {
|
||||
knownFixes = parseFile(s)
|
||||
}
|
||||
|
||||
private fun addDefaultFixes() {
|
||||
knownFixes = parseFile(IOUtils.toString(this::class.java.classLoader.getResourceAsStream("net/corda/irs/simulation/example.rates.txt"), Charsets.UTF_8.name()))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: can we split into two? Fix not available (retryable/transient) and unknown (permanent)
|
||||
class UnknownFix(val fix: FixOf) : FlowException("Unknown fix: $fix")
|
||||
|
||||
// Upload the raw fix data via RPC. In a real system the oracle data would be taken from a database.
|
||||
@StartableByRPC
|
||||
class UploadFixesFlow(val s: String) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() = serviceHub.cordaService(Oracle::class.java).uploadFixes(s)
|
||||
}
|
||||
|
||||
/** Fix container, for every fix name & date pair stores a tenor to interest rate map - [InterpolatingRateMap] */
|
||||
class FixContainer(val fixes: Set<Fix>, val factory: InterpolatorFactory = CubicSplineInterpolator) {
|
||||
private val container = buildContainer(fixes)
|
||||
val size: Int get() = fixes.size
|
||||
|
||||
operator fun get(fixOf: FixOf): Fix? {
|
||||
val rates = container[fixOf.name to fixOf.forDay]
|
||||
val fixValue = rates?.getRate(fixOf.ofTenor) ?: return null
|
||||
return Fix(fixOf, fixValue)
|
||||
}
|
||||
|
||||
private fun buildContainer(fixes: Set<Fix>): Map<Pair<String, LocalDate>, InterpolatingRateMap> {
|
||||
val tempContainer = HashMap<Pair<String, LocalDate>, HashMap<Tenor, BigDecimal>>()
|
||||
for ((fixOf, value) in fixes) {
|
||||
val rates = tempContainer.getOrPut(fixOf.name to fixOf.forDay) { HashMap() }
|
||||
rates[fixOf.ofTenor] = value
|
||||
}
|
||||
|
||||
// TODO: the calendar data needs to be specified for every fix type in the input string
|
||||
val calendar = loadTestCalendar("London") + loadTestCalendar("NewYork")
|
||||
|
||||
return tempContainer.mapValues { InterpolatingRateMap(it.key.second, it.value, calendar, factory) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a mapping between tenors and interest rates.
|
||||
* Interpolates missing values using the provided interpolation mechanism.
|
||||
*/
|
||||
class InterpolatingRateMap(val date: LocalDate,
|
||||
inputRates: Map<Tenor, BigDecimal>,
|
||||
val calendar: BusinessCalendar,
|
||||
val factory: InterpolatorFactory) {
|
||||
|
||||
/** Snapshot of the input */
|
||||
private val rates = HashMap(inputRates)
|
||||
|
||||
/** Number of rates excluding the interpolated ones */
|
||||
val size = inputRates.size
|
||||
|
||||
private val interpolator: Interpolator? by lazy {
|
||||
// Need to convert tenors to doubles for interpolation
|
||||
val numericMap = rates.mapKeys { daysToMaturity(it.key) }.toSortedMap()
|
||||
val keys = numericMap.keys.map { it.toDouble() }.toDoubleArray()
|
||||
val values = numericMap.values.map { it.toDouble() }.toDoubleArray()
|
||||
|
||||
try {
|
||||
factory.create(keys, values)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null // Not enough data points for interpolation
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the interest rate for a given [Tenor],
|
||||
* or _null_ if the rate is not found and cannot be interpolated.
|
||||
*/
|
||||
fun getRate(tenor: Tenor): BigDecimal? {
|
||||
return rates.getOrElse(tenor) {
|
||||
val rate = interpolate(tenor)
|
||||
if (rate != null) rates[tenor] = rate
|
||||
return rate
|
||||
}
|
||||
}
|
||||
|
||||
private fun daysToMaturity(tenor: Tenor) = tenor.daysToMaturity(date, calendar)
|
||||
|
||||
private fun interpolate(tenor: Tenor): BigDecimal? {
|
||||
val key = daysToMaturity(tenor).toDouble()
|
||||
val value = interpolator?.interpolate(key) ?: return null
|
||||
return BigDecimal(value)
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses lines containing fixes */
|
||||
fun parseFile(s: String): FixContainer {
|
||||
val fixes = s.lines().
|
||||
map(String::trim).
|
||||
// Filter out comment and empty lines.
|
||||
filterNot { it.startsWith("#") || it.isBlank() }.
|
||||
map(this::parseFix).
|
||||
toSet()
|
||||
return FixContainer(fixes)
|
||||
}
|
||||
|
||||
/** Parses a string of the form "LIBOR 16-March-2016 1M = 0.678" into a [Fix] */
|
||||
private fun parseFix(s: String): Fix {
|
||||
try {
|
||||
val (key, value) = s.split('=').map(String::trim)
|
||||
val of = parseFixOf(key)
|
||||
val rate = BigDecimal(value)
|
||||
return Fix(of, rate)
|
||||
} catch (e: Exception) {
|
||||
throw IllegalArgumentException("Unable to parse fix $s: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses a string of the form "LIBOR 16-March-2016 1M" into a [FixOf] */
|
||||
fun parseFixOf(key: String): FixOf {
|
||||
val words = key.split(' ')
|
||||
val tenorString = words.last()
|
||||
val date = words.dropLast(1).last()
|
||||
val name = words.dropLast(2).joinToString(" ")
|
||||
return FixOf(name, LocalDate.parse(date), Tenor(tenorString))
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
package net.corda.irs.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.excludeHostNode
|
||||
import net.corda.core.identity.groupAbstractPartyByWellKnownParty
|
||||
import net.corda.core.node.StatesToRecord
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.finance.contracts.DealState
|
||||
import net.corda.finance.flows.TwoPartyDealFlow
|
||||
import net.corda.finance.flows.TwoPartyDealFlow.Acceptor
|
||||
import net.corda.finance.flows.TwoPartyDealFlow.AutoOffer
|
||||
import net.corda.finance.flows.TwoPartyDealFlow.Instigator
|
||||
|
||||
/**
|
||||
* This whole class is really part of a demo just to initiate the agreement of a deal with a simple
|
||||
* API call from a single party without bi-directional access to the database of offers etc.
|
||||
*
|
||||
* In the "real world", we'd probably have the offers sitting in the platform prior to the agreement step
|
||||
* or the flow would have to reach out to external systems (or users) to verify the deals.
|
||||
*/
|
||||
object AutoOfferFlow {
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class Requester(val dealToBeOffered: DealState) : FlowLogic<SignedTransaction>() {
|
||||
companion object {
|
||||
object RECEIVED : ProgressTracker.Step("Received API call")
|
||||
object DEALING : ProgressTracker.Step("Starting the deal flow") {
|
||||
override fun childProgressTracker(): ProgressTracker = TwoPartyDealFlow.Primary.tracker()
|
||||
}
|
||||
|
||||
// We vend a progress tracker that already knows there's going to be a TwoPartyTradingFlow involved at some
|
||||
// point: by setting up the tracker in advance, the user can see what's coming in more detail, instead of being
|
||||
// surprised when it appears as a new set of tasks below the current one.
|
||||
fun tracker() = ProgressTracker(RECEIVED, DEALING)
|
||||
}
|
||||
|
||||
override val progressTracker = tracker()
|
||||
|
||||
init {
|
||||
progressTracker.currentStep = RECEIVED
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
require(serviceHub.networkMapCache.notaryIdentities.isNotEmpty()) { "No notary nodes registered" }
|
||||
val notary = serviceHub.networkMapCache.notaryIdentities.first() // TODO We should pass the notary as a parameter to the flow, not leave it to random choice.
|
||||
// need to pick which ever party is not us
|
||||
val otherParty = excludeHostNode(serviceHub, groupAbstractPartyByWellKnownParty(serviceHub, dealToBeOffered.participants)).keys.single()
|
||||
progressTracker.currentStep = DEALING
|
||||
val session = initiateFlow(otherParty)
|
||||
val instigator = Instigator(
|
||||
session,
|
||||
AutoOffer(notary, dealToBeOffered),
|
||||
progressTracker.getChildProgressTracker(DEALING)!!
|
||||
)
|
||||
return subFlow(instigator)
|
||||
}
|
||||
}
|
||||
|
||||
// DOCSTART 1
|
||||
@InitiatedBy(Requester::class)
|
||||
class AutoOfferAcceptor(otherSideSession: FlowSession) : Acceptor(otherSideSession) {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val finalTx = super.call()
|
||||
// Our transaction is now committed to the ledger, so report it to our regulator. We use a custom flow
|
||||
// that wraps SendTransactionFlow to allow the receiver to customise how ReceiveTransactionFlow is run,
|
||||
// and because in a real life app you'd probably have more complex logic here e.g. describing why the report
|
||||
// was filed, checking that the reportee is a regulated entity and not some random node from the wrong
|
||||
// country and so on.
|
||||
val regulator = serviceHub.identityService.partiesFromName("Regulator", true).single()
|
||||
subFlow(ReportToRegulatorFlow(regulator, finalTx))
|
||||
return finalTx
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
class ReportToRegulatorFlow(private val regulator: Party, private val finalTx: SignedTransaction) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val session = initiateFlow(regulator)
|
||||
subFlow(SendTransactionFlow(session, finalTx))
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(ReportToRegulatorFlow::class)
|
||||
class ReceiveRegulatoryReportFlow(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// Start the matching side of SendTransactionFlow above, but tell it to record all visible states even
|
||||
// though they (as far as the node can tell) are nothing to do with us.
|
||||
subFlow(ReceiveTransactionFlow(otherSideSession, true, StatesToRecord.ALL_VISIBLE))
|
||||
}
|
||||
}
|
||||
// DOCEND 1
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
package net.corda.irs.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.finance.contracts.Fix
|
||||
import net.corda.finance.contracts.FixableDealState
|
||||
import net.corda.finance.flows.TwoPartyDealFlow
|
||||
import java.math.BigDecimal
|
||||
import java.security.PublicKey
|
||||
|
||||
object FixingFlow {
|
||||
/**
|
||||
* One side of the fixing flow for an interest rate swap, but could easily be generalised further.
|
||||
*
|
||||
* Do not infer too much from the name of the class. This is just to indicate that it is the "side"
|
||||
* of the flow that is run by the party with the fixed leg of swap deal, which is the basis for deciding
|
||||
* who does what in the flow.
|
||||
*/
|
||||
@InitiatedBy(FixingRoleDecider::class)
|
||||
class Fixer(override val otherSideSession: FlowSession) : TwoPartyDealFlow.Secondary<FixingSession>() {
|
||||
|
||||
private lateinit var txState: TransactionState<*>
|
||||
private lateinit var deal: FixableDealState
|
||||
|
||||
override fun validateHandshake(handshake: TwoPartyDealFlow.Handshake<FixingSession>): TwoPartyDealFlow.Handshake<FixingSession> {
|
||||
logger.trace { "Got fixing request for: ${handshake.payload}" }
|
||||
|
||||
txState = serviceHub.loadState(handshake.payload.ref)
|
||||
deal = txState.data as FixableDealState
|
||||
|
||||
// validate the party that initiated is the one on the deal and that the recipient corresponds with it.
|
||||
// TODO: this is in no way secure and will be replaced by general session initiation logic in the future
|
||||
// Also check we are one of the parties
|
||||
require(deal.participants.count { it.owningKey == ourIdentity.owningKey } == 1)
|
||||
|
||||
return handshake
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun assembleSharedTX(handshake: TwoPartyDealFlow.Handshake<FixingSession>): Triple<TransactionBuilder, List<PublicKey>, List<TransactionSignature>> {
|
||||
val fixOf = deal.nextFixingOf()!!
|
||||
|
||||
// TODO Do we need/want to substitute in new public keys for the Parties?
|
||||
val myOldParty = deal.participants.single { it.owningKey == ourIdentity.owningKey }
|
||||
|
||||
val newDeal = deal
|
||||
|
||||
val ptx = TransactionBuilder(txState.notary)
|
||||
|
||||
// DOCSTART 1
|
||||
val addFixing = object : RatesFixFlow(ptx, handshake.payload.oracle, fixOf, BigDecimal.ZERO, BigDecimal.ONE) {
|
||||
@Suspendable
|
||||
override fun beforeSigning(fix: Fix) {
|
||||
newDeal.generateFix(ptx, StateAndRef(txState, handshake.payload.ref), fix)
|
||||
|
||||
// We set the transaction's time-window: it may be that none of the contracts need this!
|
||||
// But it can't hurt to have one.
|
||||
ptx.setTimeWindow(serviceHub.clock.instant(), 30.seconds)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun filtering(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
// Only expose Fix commands in which the oracle is on the list of requested signers
|
||||
// to the oracle node, to avoid leaking privacy
|
||||
is Command<*> -> handshake.payload.oracle.owningKey in elem.signers && elem.value is Fix
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
val sig = subFlow(addFixing)
|
||||
// DOCEND 1
|
||||
return Triple(ptx, arrayListOf(myOldParty.owningKey), listOf(sig))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One side of the fixing flow for an interest rate swap, but could easily be generalised further.
|
||||
*
|
||||
* As per the [Fixer], do not infer too much from this class name in terms of business roles. This
|
||||
* is just the "side" of the flow run by the party with the floating leg as a way of deciding who
|
||||
* does what in the flow.
|
||||
*/
|
||||
class Floater(override val otherSideSession: FlowSession,
|
||||
override val payload: FixingSession,
|
||||
override val progressTracker: ProgressTracker = TwoPartyDealFlow.Primary.tracker()) : TwoPartyDealFlow.Primary() {
|
||||
private val dealToFix: StateAndRef<FixableDealState> by transient {
|
||||
val state: TransactionState<FixableDealState> = uncheckedCast(serviceHub.loadState(payload.ref))
|
||||
StateAndRef(state, payload.ref)
|
||||
}
|
||||
|
||||
override val notaryParty: Party get() = dealToFix.state.notary
|
||||
|
||||
@Suspendable
|
||||
override fun checkProposal(stx: SignedTransaction) = requireThat {
|
||||
// Add some constraints here.
|
||||
}
|
||||
}
|
||||
|
||||
/** Used to set up the session between [Floater] and [Fixer] */
|
||||
@CordaSerializable
|
||||
data class FixingSession(val ref: StateRef, val oracle: Party)
|
||||
|
||||
/**
|
||||
* This flow looks at the deal and decides whether to be the Fixer or Floater role in agreeing a fixing.
|
||||
*
|
||||
* It is kicked off as an activity on both participant nodes by the scheduler when it's time for a fixing. If the
|
||||
* Fixer role is chosen, then that will be initiated by the [FixingSession] message sent from the other party.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
@SchedulableFlow
|
||||
class FixingRoleDecider(val ref: StateRef, override val progressTracker: ProgressTracker) : FlowLogic<Unit>() {
|
||||
@Suppress("unused") // Used via reflection.
|
||||
constructor(ref: StateRef) : this(ref, tracker())
|
||||
|
||||
companion object {
|
||||
class LOADING : ProgressTracker.Step("Loading state to decide fixing role")
|
||||
|
||||
fun tracker() = ProgressTracker(LOADING())
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
progressTracker.nextStep()
|
||||
val dealToFix = serviceHub.loadState(ref)
|
||||
val fixableDeal = (dealToFix.data as FixableDealState)
|
||||
val parties = fixableDeal.participants.sortedBy { it.owningKey.toBase58String() }
|
||||
val myKey = ourIdentity.owningKey
|
||||
if (parties[0].owningKey == myKey) {
|
||||
val fixing = FixingSession(ref, fixableDeal.oracle)
|
||||
val counterparty = serviceHub.identityService.wellKnownPartyFromAnonymous(parties[1]) ?: throw IllegalStateException("Cannot resolve floater party")
|
||||
// Start the Floater which will then kick-off the Fixer
|
||||
val session = initiateFlow(counterparty)
|
||||
subFlow(Floater(session, fixing))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
package net.corda.irs.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.finance.contracts.Fix
|
||||
import net.corda.finance.contracts.FixOf
|
||||
import net.corda.irs.flows.RatesFixFlow.FixOutOfRange
|
||||
import java.math.BigDecimal
|
||||
import java.util.function.Predicate
|
||||
|
||||
// This code is unit tested in NodeInterestRates.kt
|
||||
|
||||
/**
|
||||
* This flow queries the given oracle for an interest rate fix, and if it is within the given tolerance embeds the
|
||||
* fix in the transaction and then proceeds to get the oracle to sign it. Although the [call] method combines the query
|
||||
* and signing step, you can run the steps individually by constructing this object and then using the public methods
|
||||
* for each step.
|
||||
*
|
||||
* @throws FixOutOfRange if the returned fix was further away from the expected rate by the given amount.
|
||||
*/
|
||||
open class RatesFixFlow(protected val tx: TransactionBuilder,
|
||||
protected val oracle: Party,
|
||||
protected val fixOf: FixOf,
|
||||
protected val expectedRate: BigDecimal,
|
||||
protected val rateTolerance: BigDecimal,
|
||||
override val progressTracker: ProgressTracker = RatesFixFlow.tracker(fixOf.name)) : FlowLogic<TransactionSignature>() {
|
||||
|
||||
companion object {
|
||||
class QUERYING(val name: String) : ProgressTracker.Step("Querying oracle for $name interest rate")
|
||||
object WORKING : ProgressTracker.Step("Working with data returned by oracle")
|
||||
object SIGNING : ProgressTracker.Step("Requesting confirmation signature from interest rate oracle")
|
||||
|
||||
fun tracker(fixName: String) = ProgressTracker(QUERYING(fixName), WORKING, SIGNING)
|
||||
}
|
||||
|
||||
class FixOutOfRange(@Suppress("unused") val byAmount: BigDecimal) : FlowException("Fix out of range by $byAmount")
|
||||
|
||||
@CordaSerializable
|
||||
data class QueryRequest(val queries: List<FixOf>)
|
||||
|
||||
@CordaSerializable
|
||||
data class SignRequest(val ftx: FilteredTransaction)
|
||||
|
||||
// DOCSTART 2
|
||||
@Suspendable
|
||||
override fun call(): TransactionSignature {
|
||||
progressTracker.currentStep = progressTracker.steps[1]
|
||||
val fix = subFlow(FixQueryFlow(fixOf, oracle))
|
||||
progressTracker.currentStep = WORKING
|
||||
checkFixIsNearExpected(fix)
|
||||
tx.addCommand(fix, oracle.owningKey)
|
||||
beforeSigning(fix)
|
||||
progressTracker.currentStep = SIGNING
|
||||
val mtx = tx.toWireTransaction(serviceHub).buildFilteredTransaction(Predicate { filtering(it) })
|
||||
return subFlow(FixSignFlow(tx, oracle, mtx))
|
||||
}
|
||||
// DOCEND 2
|
||||
|
||||
/**
|
||||
* You can override this to perform any additional work needed after the fix is added to the transaction but
|
||||
* before it's sent back to the oracle for signing (for example, adding output states that depend on the fix).
|
||||
*/
|
||||
@Suspendable
|
||||
protected open fun beforeSigning(fix: Fix) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtering functions over transaction, used to build partial transaction with partial Merkle tree presented to oracle.
|
||||
* When overriding be careful when making the sub-class an anonymous or inner class (object declarations in Kotlin),
|
||||
* because that kind of classes can access variables from the enclosing scope and cause serialization problems when
|
||||
* checkpointed.
|
||||
*/
|
||||
@Suspendable
|
||||
protected open fun filtering(elem: Any): Boolean = false
|
||||
|
||||
private fun checkFixIsNearExpected(fix: Fix) {
|
||||
val delta = (fix.value - expectedRate).abs()
|
||||
if (delta > rateTolerance) {
|
||||
// TODO: Kick to a user confirmation / ui flow if it's out of bounds instead of raising an exception.
|
||||
throw FixOutOfRange(delta)
|
||||
}
|
||||
}
|
||||
|
||||
// DOCSTART 1
|
||||
@InitiatingFlow
|
||||
class FixQueryFlow(val fixOf: FixOf, val oracle: Party) : FlowLogic<Fix>() {
|
||||
@Suspendable
|
||||
override fun call(): Fix {
|
||||
val oracleSession = initiateFlow(oracle)
|
||||
// TODO: add deadline to receive
|
||||
val resp = oracleSession.sendAndReceive<List<Fix>>(QueryRequest(listOf(fixOf)))
|
||||
|
||||
return resp.unwrap {
|
||||
val fix = it.first()
|
||||
// Check the returned fix is for what we asked for.
|
||||
check(fix.of == fixOf)
|
||||
fix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
class FixSignFlow(val tx: TransactionBuilder, val oracle: Party,
|
||||
val partialMerkleTx: FilteredTransaction) : FlowLogic<TransactionSignature>() {
|
||||
@Suspendable
|
||||
override fun call(): TransactionSignature {
|
||||
val oracleSession = initiateFlow(oracle)
|
||||
val resp = oracleSession.sendAndReceive<TransactionSignature>(SignRequest(partialMerkleTx))
|
||||
return resp.unwrap { sig ->
|
||||
check(oracleSession.counterparty.owningKey.isFulfilledBy(listOf(sig.by)))
|
||||
tx.toWireTransaction(serviceHub).checkSignature(sig)
|
||||
sig
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND 1
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package net.corda.irs.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.utilities.DemoClock
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* This is a less temporary, demo-oriented way of initiating processing of temporal events.
|
||||
*/
|
||||
object UpdateBusinessDayFlow {
|
||||
|
||||
@CordaSerializable
|
||||
data class UpdateBusinessDayMessage(val date: LocalDate)
|
||||
|
||||
@InitiatedBy(Broadcast::class)
|
||||
private class UpdateBusinessDayHandler(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val message = otherPartySession.receive<UpdateBusinessDayMessage>().unwrap { it }
|
||||
(serviceHub.clock as DemoClock).updateDate(message.date)
|
||||
otherPartySession.send(true) // Let's Broadcast know we've updated the clock
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class Broadcast(val date: LocalDate, override val progressTracker: ProgressTracker) : FlowLogic<Unit>() {
|
||||
constructor(date: LocalDate) : this(date, tracker())
|
||||
|
||||
companion object {
|
||||
object NOTIFYING : ProgressTracker.Step("Notifying peers")
|
||||
|
||||
fun tracker() = ProgressTracker(NOTIFYING)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
progressTracker.currentStep = NOTIFYING
|
||||
for (recipient in getRecipients()) {
|
||||
doNextRecipient(recipient)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns recipients ordered by legal name, with notary nodes taking priority over party nodes.
|
||||
* Ordering is required so that we avoid situations where on clock update a party starts a scheduled flow, but
|
||||
* the notary or counterparty still use the old clock, so the time-window on the transaction does not validate.
|
||||
*/
|
||||
private fun getRecipients(): Iterable<Party> {
|
||||
val notaryParties = serviceHub.networkMapCache.notaryIdentities
|
||||
val peerParties = serviceHub.networkMapCache.allNodes.filter {
|
||||
it.legalIdentities.all { !serviceHub.networkMapCache.isNotary(it) }
|
||||
}.map { it.legalIdentities[0] }.sortedBy { it.name.toString() }
|
||||
return notaryParties + peerParties
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun doNextRecipient(recipient: Party) {
|
||||
initiateFlow(recipient).sendAndReceive<Boolean>(UpdateBusinessDayMessage(date))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
@file:JvmName("FinanceJSONSupport")
|
||||
|
||||
package net.corda.irs.flows.plugin
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import net.corda.finance.contracts.BusinessCalendar
|
||||
import net.corda.finance.contracts.Expression
|
||||
import net.corda.finance.workflows.utils.TEST_CALENDAR_NAMES
|
||||
import net.corda.finance.workflows.utils.loadTestCalendar
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
|
||||
fun registerFinanceJSONMappers(objectMapper: ObjectMapper) {
|
||||
val financeModule = SimpleModule("finance").apply {
|
||||
addSerializer(BusinessCalendar::class.java, CalendarSerializer)
|
||||
addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
|
||||
addSerializer(Expression::class.java, ExpressionSerializer)
|
||||
addDeserializer(Expression::class.java, ExpressionDeserializer)
|
||||
}
|
||||
objectMapper.registerModule(financeModule)
|
||||
}
|
||||
|
||||
data class BusinessCalendarWrapper(val holidayDates: SortedSet<LocalDate>) {
|
||||
fun toCalendar() = BusinessCalendar(holidayDates)
|
||||
}
|
||||
|
||||
object CalendarSerializer : JsonSerializer<BusinessCalendar>() {
|
||||
override fun serialize(obj: BusinessCalendar, generator: JsonGenerator, context: SerializerProvider) {
|
||||
val calendarName = TEST_CALENDAR_NAMES.find { loadTestCalendar(it) == obj }
|
||||
if (calendarName != null) {
|
||||
generator.writeString(calendarName)
|
||||
} else {
|
||||
generator.writeObject(BusinessCalendarWrapper(obj.holidayDates))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object CalendarDeserializer : JsonDeserializer<BusinessCalendar>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar {
|
||||
return try {
|
||||
try {
|
||||
StringArrayDeserializer.instance.deserialize(parser, context).fold(BusinessCalendar.EMPTY) { acc, name -> acc + loadTestCalendar(name) }
|
||||
} catch (e: Exception) {
|
||||
parser.readValueAs(BusinessCalendarWrapper::class.java).toCalendar()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException(parser, "Invalid calendar(s) ${parser.text}: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ExpressionSerializer : JsonSerializer<Expression>() {
|
||||
override fun serialize(expr: Expression, generator: JsonGenerator, provider: SerializerProvider) = generator.writeString(expr.expr)
|
||||
}
|
||||
|
||||
object ExpressionDeserializer : JsonDeserializer<Expression>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): Expression = Expression(parser.text)
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
package net.corda.irs.math
|
||||
|
||||
import java.util.*
|
||||
|
||||
interface Interpolator {
|
||||
fun interpolate(x: Double): Double
|
||||
}
|
||||
|
||||
interface InterpolatorFactory {
|
||||
fun create(xs: DoubleArray, ys: DoubleArray): Interpolator
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates values between the given data points using straight lines.
|
||||
*/
|
||||
class LinearInterpolator(private val xs: DoubleArray, private val ys: DoubleArray) : Interpolator {
|
||||
init {
|
||||
require(xs.size == ys.size) { "x and y dimensions should match: ${xs.size} != ${ys.size}" }
|
||||
require(xs.size >= 2) { "At least 2 data points are required for linear interpolation, received: ${xs.size}" }
|
||||
}
|
||||
|
||||
companion object Factory : InterpolatorFactory {
|
||||
override fun create(xs: DoubleArray, ys: DoubleArray) = LinearInterpolator(xs, ys)
|
||||
}
|
||||
|
||||
override fun interpolate(x: Double): Double {
|
||||
val x0 = xs.first()
|
||||
if (x0 == x) return x0
|
||||
|
||||
require(x > x0) { "Can't interpolate below $x0" }
|
||||
|
||||
for (i in 1 until xs.size) {
|
||||
if (xs[i] == x) return xs[i]
|
||||
else if (xs[i] > x) return interpolateBetween(x, xs[i - 1], xs[i], ys[i - 1], ys[i])
|
||||
}
|
||||
throw IllegalArgumentException("Can't interpolate above ${xs.last()}")
|
||||
}
|
||||
|
||||
private fun interpolateBetween(x: Double, x1: Double, x2: Double, y1: Double, y2: Double): Double {
|
||||
// N.B. The classic y1 + (y2 - y1) * (x - x1) / (x2 - x1) is numerically unstable!!
|
||||
val deltaX = (x - x1) / (x2 - x1)
|
||||
return y1 * (1.0 - deltaX) + y2 * deltaX
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates values between the given data points using a [SplineFunction].
|
||||
*
|
||||
* Implementation uses the Natural Cubic Spline algorithm as described in
|
||||
* R. L. Burden and J. D. Faires (2011), *Numerical Analysis*. 9th ed. Boston, MA: Brooks/Cole, Cengage Learning. p149-150.
|
||||
*/
|
||||
class CubicSplineInterpolator(private val xs: DoubleArray, private val ys: DoubleArray) : Interpolator {
|
||||
init {
|
||||
require(xs.size == ys.size) { "x and y dimensions should match: ${xs.size} != ${ys.size}" }
|
||||
require(xs.size >= 3) { "At least 3 data points are required for cubic interpolation, received: ${xs.size}" }
|
||||
}
|
||||
|
||||
companion object Factory : InterpolatorFactory {
|
||||
override fun create(xs: DoubleArray, ys: DoubleArray) = CubicSplineInterpolator(xs, ys)
|
||||
}
|
||||
|
||||
private val splineFunction by lazy { computeSplineFunction() }
|
||||
|
||||
override fun interpolate(x: Double): Double {
|
||||
require(x >= xs.first() && x <= xs.last()) { "Can't interpolate below ${xs.first()} or above ${xs.last()}" }
|
||||
return splineFunction.getValue(x)
|
||||
}
|
||||
|
||||
private fun computeSplineFunction(): SplineFunction {
|
||||
val n = xs.size - 1
|
||||
|
||||
// Coefficients of polynomial
|
||||
val b = DoubleArray(n) // linear
|
||||
val c = DoubleArray(n + 1) // quadratic
|
||||
val d = DoubleArray(n) // cubic
|
||||
|
||||
// Helpers
|
||||
val h = DoubleArray(n)
|
||||
val g = DoubleArray(n)
|
||||
|
||||
for (i in 0 until n)
|
||||
h[i] = xs[i + 1] - xs[i]
|
||||
for (i in 1 until n)
|
||||
g[i] = 3 / h[i] * (ys[i + 1] - ys[i]) - 3 / h[i - 1] * (ys[i] - ys[i - 1])
|
||||
|
||||
// Solve tridiagonal linear system (using Crout Factorization)
|
||||
val m = DoubleArray(n)
|
||||
val z = DoubleArray(n)
|
||||
for (i in 1 until n) {
|
||||
val l = 2 * (xs[i + 1] - xs[i - 1]) - h[i - 1] * m[i - 1]
|
||||
m[i] = h[i] / l
|
||||
z[i] = (g[i] - h[i - 1] * z[i - 1]) / l
|
||||
}
|
||||
for (j in n - 1 downTo 0) {
|
||||
c[j] = z[j] - m[j] * c[j + 1]
|
||||
b[j] = (ys[j + 1] - ys[j]) / h[j] - h[j] * (c[j + 1] + 2.0 * c[j]) / 3.0
|
||||
d[j] = (c[j + 1] - c[j]) / (3.0 * h[j])
|
||||
}
|
||||
|
||||
val segmentMap = TreeMap<Double, Polynomial>()
|
||||
for (i in 0 until n) {
|
||||
val coefficients = doubleArrayOf(ys[i], b[i], c[i], d[i])
|
||||
segmentMap[xs[i]] = Polynomial(coefficients)
|
||||
}
|
||||
return SplineFunction(segmentMap)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a polynomial function of arbitrary degree.
|
||||
* @param coefficients polynomial coefficients in the order of degree (constant first, followed by higher degree term coefficients).
|
||||
*/
|
||||
class Polynomial(private val coefficients: DoubleArray) {
|
||||
private val reversedCoefficients = coefficients.reversed()
|
||||
|
||||
fun getValue(x: Double) = reversedCoefficients.fold(0.0, { result, c -> result * x + c })
|
||||
}
|
||||
|
||||
/**
|
||||
* A *spline* is function piecewise-defined by polynomial functions.
|
||||
* Points at which polynomial pieces connect are known as *knots*.
|
||||
*
|
||||
* @param segmentMap a mapping between a knot and the polynomial that covers the subsequent interval.
|
||||
*/
|
||||
class SplineFunction(private val segmentMap: TreeMap<Double, Polynomial>) {
|
||||
fun getValue(x: Double): Double {
|
||||
val (knot, polynomial) = segmentMap.floorEntry(x)
|
||||
return polynomial.getValue(x - knot)
|
||||
}
|
||||
}
|
@ -1,228 +0,0 @@
|
||||
# Some pretend noddy rate fixes, for the interest rate oracles.
|
||||
|
||||
ICE LIBOR 2016-03-16 1M = 0.678
|
||||
ICE LIBOR 2016-03-16 2M = 0.655
|
||||
EURIBOR 2016-03-15 1M = 0.123
|
||||
EURIBOR 2016-03-15 2M = 0.111
|
||||
|
||||
# Previous fixings
|
||||
ICE LIBOR 2016-03-07 3M = 0.0063516
|
||||
ICE LIBOR 2016-03-07 3M = 0.0063516
|
||||
ICE LIBOR 2016-03-08 3M = 0.0063517
|
||||
ICE LIBOR 2016-03-09 3M = 0.0063518
|
||||
ICE LIBOR 2016-03-10 3M = 0.0063519
|
||||
ICE LIBOR 2016-06-06 3M = 0.0063520
|
||||
ICE LIBOR 2016-06-07 3M = 0.0063521
|
||||
ICE LIBOR 2016-06-08 3M = 0.0063522
|
||||
ICE LIBOR 2016-06-09 3M = 0.0063523
|
||||
ICE LIBOR 2016-06-10 3M = 0.0063524
|
||||
ICE LIBOR 2016-09-06 3M = 0.0063525
|
||||
ICE LIBOR 2016-09-07 3M = 0.0063526
|
||||
ICE LIBOR 2016-09-08 3M = 0.0063527
|
||||
ICE LIBOR 2016-09-09 3M = 0.0063528
|
||||
ICE LIBOR 2016-09-10 3M = 0.0063529
|
||||
ICE LIBOR 2016-12-06 3M = 0.0063530
|
||||
ICE LIBOR 2016-12-07 3M = 0.0063531
|
||||
ICE LIBOR 2016-12-08 3M = 0.0063532
|
||||
ICE LIBOR 2016-12-09 3M = 0.0063533
|
||||
ICE LIBOR 2016-12-10 3M = 0.0063534
|
||||
ICE LIBOR 2017-03-06 3M = 0.0063535
|
||||
ICE LIBOR 2017-03-07 3M = 0.0063536
|
||||
ICE LIBOR 2017-03-08 3M = 0.0063537
|
||||
ICE LIBOR 2017-03-09 3M = 0.0063538
|
||||
ICE LIBOR 2017-03-10 3M = 0.0063539
|
||||
ICE LIBOR 2017-06-06 3M = 0.0063540
|
||||
ICE LIBOR 2017-06-07 3M = 0.0063541
|
||||
ICE LIBOR 2017-06-08 3M = 0.0063542
|
||||
ICE LIBOR 2017-06-09 3M = 0.0063543
|
||||
ICE LIBOR 2017-06-10 3M = 0.0063544
|
||||
ICE LIBOR 2017-09-06 3M = 0.0063545
|
||||
ICE LIBOR 2017-09-07 3M = 0.0063546
|
||||
ICE LIBOR 2017-09-08 3M = 0.0063547
|
||||
ICE LIBOR 2017-09-09 3M = 0.0063548
|
||||
ICE LIBOR 2017-09-10 3M = 0.0063549
|
||||
ICE LIBOR 2017-12-06 3M = 0.0063550
|
||||
ICE LIBOR 2017-12-07 3M = 0.0063551
|
||||
ICE LIBOR 2017-12-08 3M = 0.0063552
|
||||
ICE LIBOR 2017-12-09 3M = 0.0063553
|
||||
ICE LIBOR 2017-12-10 3M = 0.0063554
|
||||
ICE LIBOR 2018-03-06 3M = 0.0063555
|
||||
ICE LIBOR 2018-03-07 3M = 0.0063556
|
||||
ICE LIBOR 2018-03-08 3M = 0.0063557
|
||||
ICE LIBOR 2018-03-09 3M = 0.0063558
|
||||
ICE LIBOR 2018-03-10 3M = 0.0063559
|
||||
ICE LIBOR 2018-06-06 3M = 0.0063560
|
||||
ICE LIBOR 2018-06-07 3M = 0.0063561
|
||||
ICE LIBOR 2018-06-08 3M = 0.0063562
|
||||
ICE LIBOR 2018-06-09 3M = 0.0063563
|
||||
ICE LIBOR 2018-06-10 3M = 0.0063564
|
||||
ICE LIBOR 2018-09-06 3M = 0.0063565
|
||||
ICE LIBOR 2018-09-07 3M = 0.0063566
|
||||
ICE LIBOR 2018-09-08 3M = 0.0063567
|
||||
ICE LIBOR 2018-09-09 3M = 0.0063568
|
||||
ICE LIBOR 2018-09-10 3M = 0.0063569
|
||||
ICE LIBOR 2018-12-06 3M = 0.0063570
|
||||
ICE LIBOR 2018-12-07 3M = 0.0063571
|
||||
ICE LIBOR 2018-12-08 3M = 0.0063572
|
||||
ICE LIBOR 2018-12-09 3M = 0.0063573
|
||||
ICE LIBOR 2018-12-10 3M = 0.0063574
|
||||
ICE LIBOR 2019-03-06 3M = 0.0063575
|
||||
ICE LIBOR 2019-03-07 3M = 0.0063576
|
||||
ICE LIBOR 2019-03-08 3M = 0.0063577
|
||||
ICE LIBOR 2019-03-09 3M = 0.0063578
|
||||
ICE LIBOR 2019-03-10 3M = 0.0063579
|
||||
ICE LIBOR 2019-06-06 3M = 0.0063580
|
||||
ICE LIBOR 2019-06-07 3M = 0.0063581
|
||||
ICE LIBOR 2019-06-08 3M = 0.0063582
|
||||
ICE LIBOR 2019-06-09 3M = 0.0063583
|
||||
ICE LIBOR 2019-06-10 3M = 0.0063584
|
||||
ICE LIBOR 2019-09-06 3M = 0.0063585
|
||||
ICE LIBOR 2019-09-07 3M = 0.0063586
|
||||
ICE LIBOR 2019-09-08 3M = 0.0063587
|
||||
ICE LIBOR 2019-09-09 3M = 0.0063588
|
||||
ICE LIBOR 2019-09-10 3M = 0.0063589
|
||||
ICE LIBOR 2019-12-06 3M = 0.0063590
|
||||
ICE LIBOR 2019-12-07 3M = 0.0063591
|
||||
ICE LIBOR 2019-12-08 3M = 0.0063592
|
||||
ICE LIBOR 2019-12-09 3M = 0.0063593
|
||||
ICE LIBOR 2019-12-10 3M = 0.0063594
|
||||
ICE LIBOR 2020-03-06 3M = 0.0063595
|
||||
ICE LIBOR 2020-03-07 3M = 0.0063596
|
||||
ICE LIBOR 2020-03-08 3M = 0.0063597
|
||||
ICE LIBOR 2020-03-09 3M = 0.0063598
|
||||
ICE LIBOR 2020-03-10 3M = 0.0063599
|
||||
ICE LIBOR 2020-06-06 3M = 0.0063600
|
||||
ICE LIBOR 2020-06-07 3M = 0.0063601
|
||||
ICE LIBOR 2020-06-08 3M = 0.0063602
|
||||
ICE LIBOR 2020-06-09 3M = 0.0063603
|
||||
ICE LIBOR 2020-06-10 3M = 0.0063604
|
||||
ICE LIBOR 2020-09-06 3M = 0.0063605
|
||||
ICE LIBOR 2020-09-07 3M = 0.0063606
|
||||
ICE LIBOR 2020-09-08 3M = 0.0063607
|
||||
ICE LIBOR 2020-09-09 3M = 0.0063608
|
||||
ICE LIBOR 2020-09-10 3M = 0.0063609
|
||||
ICE LIBOR 2020-12-06 3M = 0.0063610
|
||||
ICE LIBOR 2020-12-07 3M = 0.0063611
|
||||
ICE LIBOR 2020-12-08 3M = 0.0063612
|
||||
ICE LIBOR 2020-12-09 3M = 0.0063613
|
||||
ICE LIBOR 2020-12-10 3M = 0.0063614
|
||||
ICE LIBOR 2021-03-06 3M = 0.0063615
|
||||
ICE LIBOR 2021-03-07 3M = 0.0063616
|
||||
ICE LIBOR 2021-03-08 3M = 0.0063617
|
||||
ICE LIBOR 2021-03-09 3M = 0.0063618
|
||||
ICE LIBOR 2021-03-10 3M = 0.0063619
|
||||
ICE LIBOR 2021-06-06 3M = 0.0063620
|
||||
ICE LIBOR 2021-06-07 3M = 0.0063621
|
||||
ICE LIBOR 2021-06-08 3M = 0.0063622
|
||||
ICE LIBOR 2021-06-09 3M = 0.0063623
|
||||
ICE LIBOR 2021-06-10 3M = 0.0063624
|
||||
ICE LIBOR 2021-09-06 3M = 0.0063625
|
||||
ICE LIBOR 2021-09-07 3M = 0.0063626
|
||||
ICE LIBOR 2021-09-08 3M = 0.0063627
|
||||
ICE LIBOR 2021-09-09 3M = 0.0063628
|
||||
ICE LIBOR 2021-09-10 3M = 0.0063629
|
||||
ICE LIBOR 2021-12-06 3M = 0.0063630
|
||||
ICE LIBOR 2021-12-07 3M = 0.0063631
|
||||
ICE LIBOR 2021-12-08 3M = 0.0063632
|
||||
ICE LIBOR 2021-12-09 3M = 0.0063633
|
||||
ICE LIBOR 2021-12-10 3M = 0.0063634
|
||||
ICE LIBOR 2022-03-06 3M = 0.0063635
|
||||
ICE LIBOR 2022-03-07 3M = 0.0063636
|
||||
ICE LIBOR 2022-03-08 3M = 0.0063637
|
||||
ICE LIBOR 2022-03-09 3M = 0.0063638
|
||||
ICE LIBOR 2022-03-10 3M = 0.0063639
|
||||
ICE LIBOR 2022-06-06 3M = 0.0063640
|
||||
ICE LIBOR 2022-06-07 3M = 0.0063641
|
||||
ICE LIBOR 2022-06-08 3M = 0.0063642
|
||||
ICE LIBOR 2022-06-09 3M = 0.0063643
|
||||
ICE LIBOR 2022-06-10 3M = 0.0063644
|
||||
ICE LIBOR 2022-09-06 3M = 0.0063645
|
||||
ICE LIBOR 2022-09-07 3M = 0.0063646
|
||||
ICE LIBOR 2022-09-08 3M = 0.0063647
|
||||
ICE LIBOR 2022-09-09 3M = 0.0063648
|
||||
ICE LIBOR 2022-09-10 3M = 0.0063649
|
||||
ICE LIBOR 2022-12-06 3M = 0.0063650
|
||||
ICE LIBOR 2022-12-07 3M = 0.0063651
|
||||
ICE LIBOR 2022-12-08 3M = 0.0063652
|
||||
ICE LIBOR 2022-12-09 3M = 0.0063653
|
||||
ICE LIBOR 2022-12-10 3M = 0.0063654
|
||||
ICE LIBOR 2023-03-06 3M = 0.0063655
|
||||
ICE LIBOR 2023-03-07 3M = 0.0063656
|
||||
ICE LIBOR 2023-03-08 3M = 0.0063657
|
||||
ICE LIBOR 2023-03-09 3M = 0.0063658
|
||||
ICE LIBOR 2023-03-10 3M = 0.0063659
|
||||
ICE LIBOR 2023-06-06 3M = 0.0063660
|
||||
ICE LIBOR 2023-06-07 3M = 0.0063661
|
||||
ICE LIBOR 2023-06-08 3M = 0.0063662
|
||||
ICE LIBOR 2023-06-09 3M = 0.0063663
|
||||
ICE LIBOR 2023-06-10 3M = 0.0063664
|
||||
ICE LIBOR 2023-09-06 3M = 0.0063665
|
||||
ICE LIBOR 2023-09-07 3M = 0.0063666
|
||||
ICE LIBOR 2023-09-08 3M = 0.0063667
|
||||
ICE LIBOR 2023-09-09 3M = 0.0063668
|
||||
ICE LIBOR 2023-09-10 3M = 0.0063669
|
||||
ICE LIBOR 2023-12-06 3M = 0.0063670
|
||||
ICE LIBOR 2023-12-07 3M = 0.0063671
|
||||
ICE LIBOR 2023-12-08 3M = 0.0063672
|
||||
ICE LIBOR 2023-12-09 3M = 0.0063673
|
||||
ICE LIBOR 2023-12-10 3M = 0.0063674
|
||||
ICE LIBOR 2024-03-06 3M = 0.0063675
|
||||
ICE LIBOR 2024-03-07 3M = 0.0063676
|
||||
ICE LIBOR 2024-03-08 3M = 0.0063677
|
||||
ICE LIBOR 2024-03-09 3M = 0.0063678
|
||||
ICE LIBOR 2024-03-10 3M = 0.0063679
|
||||
ICE LIBOR 2024-06-06 3M = 0.0063680
|
||||
ICE LIBOR 2024-06-07 3M = 0.0063681
|
||||
ICE LIBOR 2024-06-08 3M = 0.0063682
|
||||
ICE LIBOR 2024-06-09 3M = 0.0063683
|
||||
ICE LIBOR 2024-06-10 3M = 0.0063684
|
||||
ICE LIBOR 2024-09-06 3M = 0.0063685
|
||||
ICE LIBOR 2024-09-07 3M = 0.0063686
|
||||
ICE LIBOR 2024-09-08 3M = 0.0063687
|
||||
ICE LIBOR 2024-09-09 3M = 0.0063688
|
||||
ICE LIBOR 2024-09-10 3M = 0.0063689
|
||||
ICE LIBOR 2024-12-06 3M = 0.0063690
|
||||
ICE LIBOR 2024-12-07 3M = 0.0063691
|
||||
ICE LIBOR 2024-12-08 3M = 0.0063692
|
||||
ICE LIBOR 2024-12-09 3M = 0.0063693
|
||||
ICE LIBOR 2024-12-10 3M = 0.0063694
|
||||
ICE LIBOR 2025-03-06 3M = 0.0063695
|
||||
ICE LIBOR 2025-03-07 3M = 0.0063696
|
||||
ICE LIBOR 2025-03-08 3M = 0.0063697
|
||||
ICE LIBOR 2025-03-09 3M = 0.0063698
|
||||
ICE LIBOR 2025-03-10 3M = 0.0063699
|
||||
ICE LIBOR 2025-06-06 3M = 0.0063700
|
||||
ICE LIBOR 2025-06-07 3M = 0.0063701
|
||||
ICE LIBOR 2025-06-08 3M = 0.0063702
|
||||
ICE LIBOR 2025-06-09 3M = 0.0063703
|
||||
ICE LIBOR 2025-06-10 3M = 0.0063704
|
||||
ICE LIBOR 2025-09-06 3M = 0.0063705
|
||||
ICE LIBOR 2025-09-07 3M = 0.0063706
|
||||
ICE LIBOR 2025-09-08 3M = 0.0063707
|
||||
ICE LIBOR 2025-09-09 3M = 0.0063708
|
||||
ICE LIBOR 2025-09-10 3M = 0.0063709
|
||||
ICE LIBOR 2025-12-06 3M = 0.0063710
|
||||
ICE LIBOR 2025-12-07 3M = 0.0063711
|
||||
ICE LIBOR 2025-12-08 3M = 0.0063712
|
||||
ICE LIBOR 2025-12-09 3M = 0.0063713
|
||||
ICE LIBOR 2025-12-10 3M = 0.0063714
|
||||
ICE LIBOR 2026-03-06 3M = 0.0063715
|
||||
ICE LIBOR 2026-03-07 3M = 0.0063716
|
||||
ICE LIBOR 2026-03-08 3M = 0.0063717
|
||||
ICE LIBOR 2026-03-09 3M = 0.0063718
|
||||
ICE LIBOR 2026-03-10 3M = 0.0063719
|
||||
ICE LIBOR 2026-06-06 3M = 0.0063720
|
||||
ICE LIBOR 2026-06-07 3M = 0.0063721
|
||||
ICE LIBOR 2026-06-08 3M = 0.0063722
|
||||
ICE LIBOR 2026-06-09 3M = 0.0063723
|
||||
ICE LIBOR 2026-06-10 3M = 0.0063724
|
||||
ICE LIBOR 2026-09-06 3M = 0.0063725
|
||||
ICE LIBOR 2026-09-07 3M = 0.0063726
|
||||
ICE LIBOR 2026-09-08 3M = 0.0063727
|
||||
ICE LIBOR 2026-09-09 3M = 0.0063728
|
||||
ICE LIBOR 2026-09-10 3M = 0.0063729
|
||||
ICE LIBOR 2026-12-06 3M = 0.0063730
|
||||
ICE LIBOR 2026-12-07 3M = 0.0063731
|
||||
ICE LIBOR 2026-12-08 3M = 0.0063732
|
||||
ICE LIBOR 2026-12-09 3M = 0.0063733
|
||||
ICE LIBOR 2026-12-10 3M = 0.0063734
|
@ -1,27 +0,0 @@
|
||||
package net.corda.irs
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.core.DUMMY_BANK_A_NAME
|
||||
import net.corda.testing.core.DUMMY_BANK_B_NAME
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
|
||||
/**
|
||||
* This file is exclusively for being able to run your nodes through an IDE (as opposed to running deployNodes)
|
||||
* Do not use in a production environment.
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
driver(DriverParameters(useTestClock = true, waitForAllNodesToFinish = true)) {
|
||||
val (nodeA, nodeB) = listOf(
|
||||
startNode(providedName = DUMMY_BANK_A_NAME),
|
||||
startNode(providedName = DUMMY_BANK_B_NAME),
|
||||
startNode(providedName = CordaX500Name("Regulator", "Moscow", "RU"))
|
||||
).map { it.getOrThrow() }
|
||||
val controller = defaultNotaryNode.getOrThrow()
|
||||
|
||||
startWebserver(controller)
|
||||
startWebserver(nodeA)
|
||||
startWebserver(nodeB)
|
||||
}
|
||||
}
|
@ -1,214 +0,0 @@
|
||||
package net.corda.irs.api
|
||||
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.Fix
|
||||
import net.corda.finance.contracts.asset.CASH
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.contracts.asset.Move
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.configureDatabase
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.createMockCordaService
|
||||
import net.corda.testing.node.makeTestIdentityService
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.util.function.Predicate
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
|
||||
class NodeInterestRatesTest {
|
||||
private companion object {
|
||||
val alice = TestIdentity(ALICE_NAME, 70)
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
val MEGA_CORP_KEY = generateKeyPair()
|
||||
val ALICE get() = alice.party
|
||||
val ALICE_PUBKEY get() = alice.publicKey
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
private val TEST_DATA = NodeInterestRates.parseFile("""
|
||||
LIBOR 2016-03-16 1M = 0.678
|
||||
LIBOR 2016-03-16 2M = 0.685
|
||||
LIBOR 2016-03-16 1Y = 0.890
|
||||
LIBOR 2016-03-16 2Y = 0.962
|
||||
EURIBOR 2016-03-15 1M = 0.123
|
||||
EURIBOR 2016-03-15 2M = 0.111
|
||||
""".trimIndent())
|
||||
private val dummyCashIssuer = TestIdentity(CordaX500Name("Cash issuer", "London", "GB"))
|
||||
private val services = MockServices(listOf("net.corda.finance.contracts.asset"), dummyCashIssuer, makeTestIdentityService(), MEGA_CORP_KEY)
|
||||
// This is safe because MockServices only ever have a single identity
|
||||
private val identity = services.myInfo.singleIdentity()
|
||||
|
||||
private lateinit var oracle: NodeInterestRates.Oracle
|
||||
private lateinit var database: CordaPersistence
|
||||
|
||||
private fun fixCmdFilter(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> identity.owningKey in elem.signers && elem.value is Fix
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterCmds(elem: Any): Boolean = elem is Command<*>
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null })
|
||||
database.transaction {
|
||||
oracle = createMockCordaService(services, NodeInterestRates::Oracle)
|
||||
oracle.knownFixes = TEST_DATA
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `query successfully`() {
|
||||
database.transaction {
|
||||
val q = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
val res = oracle.query(listOf(q))
|
||||
assertEquals(1, res.size)
|
||||
assertEquals(BigDecimal("0.678"), res[0].value)
|
||||
assertEquals(q, res[0].of)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `query with one success and one missing`() {
|
||||
database.transaction {
|
||||
val q1 = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
val q2 = NodeInterestRates.parseFixOf("LIBOR 2016-03-15 1M")
|
||||
val e = assertFailsWith<NodeInterestRates.UnknownFix> { oracle.query(listOf(q1, q2)) }
|
||||
assertEquals(e.fix, q2)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `query successfully with interpolated rate`() {
|
||||
database.transaction {
|
||||
val q = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 5M")
|
||||
val res = oracle.query(listOf(q))
|
||||
assertEquals(1, res.size)
|
||||
assertEquals(0.7316228, res[0].value.toDouble(), 0.0000001)
|
||||
assertEquals(q, res[0].of)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `rate missing and unable to interpolate`() {
|
||||
database.transaction {
|
||||
val q = NodeInterestRates.parseFixOf("EURIBOR 2016-03-15 3M")
|
||||
assertFailsWith<NodeInterestRates.UnknownFix> { oracle.query(listOf(q)) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `empty query`() {
|
||||
database.transaction {
|
||||
assertFailsWith<IllegalArgumentException> { oracle.query(emptyList()) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `refuse to sign with no relevant commands`() {
|
||||
database.transaction {
|
||||
val tx = makeFullTx()
|
||||
val wtx1 = tx.toWireTransaction(services)
|
||||
fun filterAllOutputs(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is TransactionState<ContractState> -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val ftx1 = wtx1.buildFilteredTransaction(Predicate(::filterAllOutputs))
|
||||
assertFailsWith<IllegalArgumentException> { oracle.sign(ftx1) }
|
||||
tx.addCommand(Move(), ALICE_PUBKEY)
|
||||
val wtx2 = tx.toWireTransaction(services)
|
||||
val ftx2 = wtx2.buildFilteredTransaction(Predicate { x -> filterCmds(x) })
|
||||
assertFalse(wtx1.id == wtx2.id)
|
||||
assertFailsWith<IllegalArgumentException> { oracle.sign(ftx2) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `sign successfully`() {
|
||||
database.transaction {
|
||||
val tx = makePartialTX()
|
||||
val fix = oracle.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M"))).first()
|
||||
tx.addCommand(fix, identity.owningKey)
|
||||
// Sign successfully.
|
||||
val wtx = tx.toWireTransaction(services)
|
||||
val ftx = wtx.buildFilteredTransaction(Predicate { fixCmdFilter(it) })
|
||||
val signature = oracle.sign(ftx)
|
||||
wtx.checkSignature(signature)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `do not sign with unknown fix`() {
|
||||
database.transaction {
|
||||
val tx = makePartialTX()
|
||||
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
val badFix = Fix(fixOf, BigDecimal("0.6789"))
|
||||
tx.addCommand(badFix, identity.owningKey)
|
||||
val wtx = tx.toWireTransaction(services)
|
||||
val ftx = wtx.buildFilteredTransaction(Predicate { fixCmdFilter(it) })
|
||||
val e1 = assertFailsWith<NodeInterestRates.UnknownFix> { oracle.sign(ftx) }
|
||||
assertEquals(fixOf, e1.fix)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `do not sign too many leaves`() {
|
||||
database.transaction {
|
||||
val tx = makePartialTX()
|
||||
val fix = oracle.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M"))).first()
|
||||
fun filtering(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> identity.owningKey in elem.signers && elem.value is Fix
|
||||
is TransactionState<ContractState> -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
tx.addCommand(fix, identity.owningKey)
|
||||
val wtx = tx.toWireTransaction(services)
|
||||
val ftx = wtx.buildFilteredTransaction(Predicate(::filtering))
|
||||
assertFailsWith<IllegalArgumentException> { oracle.sign(ftx) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `empty partial transaction to sign`() {
|
||||
val tx = makeFullTx()
|
||||
val wtx = tx.toWireTransaction(services)
|
||||
val ftx = wtx.buildFilteredTransaction(Predicate { false })
|
||||
assertFailsWith<IllegalArgumentException> { oracle.sign(ftx) } // It throws failed requirement (as it is empty there is no command to check and sign).
|
||||
}
|
||||
|
||||
private fun makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems(
|
||||
TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy ALICE, Cash.PROGRAM_ID, DUMMY_NOTARY))
|
||||
|
||||
private fun makeFullTx() = makePartialTX().withItems(dummyCommand())
|
||||
}
|
||||
|
||||
|
@ -1,154 +0,0 @@
|
||||
package net.corda.irs.api
|
||||
|
||||
import com.google.common.collect.testing.Helpers.assertContains
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.flows.UnexpectedFlowEndException
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.Fix
|
||||
import net.corda.finance.contracts.FixOf
|
||||
import net.corda.finance.contracts.asset.CASH
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.irs.flows.RatesFixFlow
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.LogHelper
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNodeParameters
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class OracleNodeTearOffTests {
|
||||
private val TEST_DATA = NodeInterestRates.parseFile("""
|
||||
LIBOR 2016-03-16 1M = 0.678
|
||||
LIBOR 2016-03-16 2M = 0.685
|
||||
LIBOR 2016-03-16 1Y = 0.890
|
||||
LIBOR 2016-03-16 2Y = 0.962
|
||||
EURIBOR 2016-03-15 1M = 0.123
|
||||
EURIBOR 2016-03-15 2M = 0.111
|
||||
""".trimIndent())
|
||||
|
||||
private val dummyCashIssuer = TestIdentity(CordaX500Name("Cash issuer", "London", "GB"))
|
||||
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
val alice = TestIdentity(ALICE_NAME, 70)
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var aliceNode: StartedMockNode
|
||||
private lateinit var oracleNode: StartedMockNode
|
||||
private val oracle get() = oracleNode.services.myInfo.singleIdentity()
|
||||
|
||||
@Before
|
||||
// DOCSTART 1
|
||||
fun setUp() {
|
||||
mockNet = @Suppress("DEPRECATION") MockNetwork(cordappPackages = listOf("net.corda.finance.contracts", "net.corda.irs"))
|
||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
oracleNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)).apply {
|
||||
transaction {
|
||||
services.cordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
// DOCSTART 2
|
||||
@Test(timeout=300_000)
|
||||
fun `verify that the oracle signs the transaction if the interest rate within allowed limit`() {
|
||||
// Create a partial transaction
|
||||
val tx = TransactionBuilder(DUMMY_NOTARY)
|
||||
.withItems(TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy alice.party, Cash.PROGRAM_ID, DUMMY_NOTARY))
|
||||
// Specify the rate we wish to get verified by the oracle
|
||||
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
|
||||
// Create a new flow for the fix
|
||||
val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1"))
|
||||
// Run the mock network and wait for a result
|
||||
mockNet.runNetwork()
|
||||
val future = aliceNode.startFlow(flow)
|
||||
mockNet.runNetwork()
|
||||
future.getOrThrow()
|
||||
|
||||
// We should now have a valid rate on our tx from the oracle.
|
||||
val fix = tx.toWireTransaction(aliceNode.services).commands.map { it }.first()
|
||||
assertEquals(fixOf, (fix.value as Fix).of)
|
||||
// Check that the response contains the valid rate, which is within the supplied tolerance
|
||||
assertEquals(BigDecimal("0.678"), (fix.value as Fix).value)
|
||||
// Check that the transaction has been signed by the oracle
|
||||
assertContains(fix.signers, oracle.owningKey)
|
||||
}
|
||||
// DOCEND 2
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `verify that the oracle rejects the transaction if the interest rate is outside the allowed limit`() {
|
||||
val tx = makePartialTX()
|
||||
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.695"), BigDecimal("0.01"))
|
||||
LogHelper.setLevel("rates")
|
||||
|
||||
mockNet.runNetwork()
|
||||
val future = aliceNode.startFlow(flow)
|
||||
mockNet.runNetwork()
|
||||
assertThatThrownBy{
|
||||
future.getOrThrow()
|
||||
}.isInstanceOf(RatesFixFlow.FixOutOfRange::class.java).hasMessage("Fix out of range by 0.017")
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `verify that the oracle rejects the transaction if there is a privacy leak`() {
|
||||
val tx = makePartialTX()
|
||||
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
val flow = OverFilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1"))
|
||||
LogHelper.setLevel("rates")
|
||||
|
||||
mockNet.runNetwork()
|
||||
val future = aliceNode.startFlow(flow)
|
||||
mockNet.runNetwork()
|
||||
//The oracle
|
||||
assertThatThrownBy{
|
||||
future.getOrThrow()
|
||||
}.isInstanceOf(UnexpectedFlowEndException::class.java)
|
||||
}
|
||||
|
||||
// Creates a version of [RatesFixFlow] that makes the command
|
||||
class FilteredRatesFlow(tx: TransactionBuilder,
|
||||
oracle: Party,
|
||||
fixOf: FixOf,
|
||||
expectedRate: BigDecimal,
|
||||
rateTolerance: BigDecimal,
|
||||
progressTracker: ProgressTracker = tracker(fixOf.name))
|
||||
: RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) {
|
||||
override fun filtering(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is Command<*> -> oracle.owningKey in elem.signers && elem.value is Fix
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a version of [RatesFixFlow] that makes the command
|
||||
class OverFilteredRatesFlow(tx: TransactionBuilder,
|
||||
oracle: Party,
|
||||
fixOf: FixOf,
|
||||
expectedRate: BigDecimal,
|
||||
rateTolerance: BigDecimal,
|
||||
progressTracker: ProgressTracker = tracker(fixOf.name))
|
||||
: RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) {
|
||||
override fun filtering(elem: Any): Boolean = true
|
||||
}
|
||||
|
||||
private fun makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems(
|
||||
TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy alice.party, Cash.PROGRAM_ID, DUMMY_NOTARY))
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
package net.corda.irs.math
|
||||
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class InterpolatorsTest {
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `linear interpolator throws when key to interpolate is outside the data set`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0, 4.0, 5.0)
|
||||
val interpolator = LinearInterpolator(xs, ys = xs)
|
||||
assertFailsWith<IllegalArgumentException> { interpolator.interpolate(0.0) }
|
||||
assertFailsWith<IllegalArgumentException> { interpolator.interpolate(6.0) }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `linear interpolator throws when data set is less than 2 points`() {
|
||||
val xs = doubleArrayOf(1.0)
|
||||
assertFailsWith<IllegalArgumentException> { LinearInterpolator(xs, ys = xs) }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `linear interpolator returns existing value when key is in data set`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0, 4.0, 5.0)
|
||||
val interpolatedValue = LinearInterpolator(xs, ys = xs).interpolate(2.0)
|
||||
assertEquals(2.0, interpolatedValue)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `linear interpolator interpolates missing values correctly`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0)
|
||||
val toInterpolate = doubleArrayOf(1.5, 2.5, 2.8, 3.3, 3.7, 4.3, 4.7)
|
||||
|
||||
val interpolator = LinearInterpolator(xs, xs)
|
||||
val actual = toInterpolate.map { interpolator.interpolate(it) }.toDoubleArray()
|
||||
Assert.assertArrayEquals(toInterpolate, actual, 0.01)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `cubic interpolator throws when key to interpolate is outside the data set`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0, 4.0, 5.0)
|
||||
val interpolator = CubicSplineInterpolator(xs, ys = xs)
|
||||
assertFailsWith<IllegalArgumentException> { interpolator.interpolate(0.0) }
|
||||
assertFailsWith<IllegalArgumentException> { interpolator.interpolate(6.0) }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `cubic interpolator throws when data set is less than 3 points`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0)
|
||||
assertFailsWith<IllegalArgumentException> { CubicSplineInterpolator(xs, ys = xs) }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `cubic interpolator returns existing value when key is in data set`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0, 4.0, 5.0)
|
||||
val interpolatedValue = CubicSplineInterpolator(xs, ys = xs).interpolate(2.0)
|
||||
assertEquals(2.0, interpolatedValue)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `cubic interpolator interpolates missing values correctly`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0)
|
||||
val ys = doubleArrayOf(2.0, 4.0, 5.0, 11.0, 10.0)
|
||||
val toInterpolate = doubleArrayOf(1.5, 2.5, 2.8, 3.3, 3.7, 4.3, 4.7)
|
||||
// Expected values generated using R's splinefun (package net.corda.stats v3.2.4), "natural" method
|
||||
val expected = doubleArrayOf(3.28, 4.03, 4.37, 6.7, 9.46, 11.5, 10.91)
|
||||
|
||||
val interpolator = CubicSplineInterpolator(xs, ys)
|
||||
val actual = toInterpolate.map { interpolator.interpolate(it) }.toDoubleArray()
|
||||
Assert.assertArrayEquals(expected, actual, 0.01)
|
||||
}
|
||||
}
|
@ -1,178 +0,0 @@
|
||||
package net.corda.irs
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.TreeNode
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import net.corda.client.jackson.JacksonSupport
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
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.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.irs.flows.plugin.registerFinanceJSONMappers
|
||||
import net.corda.irs.contract.InterestRateSwap
|
||||
import net.corda.irs.web.IrsDemoWebApplication
|
||||
import net.corda.test.spring.springDriver
|
||||
import net.corda.testing.core.DUMMY_BANK_A_NAME
|
||||
import net.corda.testing.core.DUMMY_BANK_B_NAME
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.http.HttpApi
|
||||
import net.corda.testing.node.NotarySpec
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.FINANCE_CORDAPPS
|
||||
import net.corda.testing.node.internal.cordappWithPackages
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import java.time.Duration
|
||||
import java.time.LocalDate
|
||||
import org.junit.Ignore
|
||||
|
||||
@Ignore
|
||||
class IRSDemoTest {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
private val rpcUsers = listOf(User("user", "password", setOf("ALL")))
|
||||
private val currentDate: LocalDate = LocalDate.now()
|
||||
private val futureDate: LocalDate = currentDate.plusMonths(6)
|
||||
private val maxWaitTime: Duration = 150.seconds
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `runs IRS demo`() {
|
||||
springDriver(DriverParameters(
|
||||
useTestClock = true,
|
||||
notarySpecs = listOf(NotarySpec(DUMMY_NOTARY_NAME, rpcUsers = rpcUsers)),
|
||||
cordappsForAllNodes = FINANCE_CORDAPPS + cordappWithPackages("net.corda.irs")
|
||||
)) {
|
||||
val (nodeA, nodeB, controller) = listOf(
|
||||
startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = rpcUsers),
|
||||
startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = rpcUsers),
|
||||
startNode(providedName = CordaX500Name("Regulator", "Moscow", "RU")),
|
||||
defaultNotaryNode
|
||||
).map { it.getOrThrow() }
|
||||
|
||||
log.info("All nodes started")
|
||||
|
||||
val (controllerAddr, nodeAAddr, nodeBAddr) = listOf(controller, nodeA, nodeB).map {
|
||||
startSpringBootWebapp(IrsDemoWebApplication::class.java, it, "/api/irs/demodate")
|
||||
}.map { it.getOrThrow().listenAddress }
|
||||
|
||||
log.info("All webservers started")
|
||||
|
||||
val (controllerApi, nodeAApi, nodeBApi) = listOf(controller, nodeA, nodeB).zip(listOf(controllerAddr, nodeAAddr, nodeBAddr)).map {
|
||||
val mapper = JacksonSupport.createDefaultMapper(it.first.rpc)
|
||||
registerFinanceJSONMappers(mapper)
|
||||
registerIRSModule(mapper)
|
||||
HttpApi.fromHostAndPort(it.second, "api/irs", mapper = mapper)
|
||||
}
|
||||
val nextFixingDates = getFixingDateObservable(nodeA.rpcAddress)
|
||||
val numADeals = getTradeCount(nodeAApi)
|
||||
val numBDeals = getTradeCount(nodeBApi)
|
||||
|
||||
runUploadRates(controllerApi)
|
||||
runTrade(nodeAApi, controller.nodeInfo.singleIdentity())
|
||||
|
||||
assertThat(getTradeCount(nodeAApi)).isEqualTo(numADeals + 1)
|
||||
assertThat(getTradeCount(nodeBApi)).isEqualTo(numBDeals + 1)
|
||||
assertThat(getFloatingLegFixCount(nodeAApi) == 0)
|
||||
|
||||
// Wait until the initial trade and all scheduled fixings up to the current date have finished
|
||||
nextFixingDates.firstWithTimeout(maxWaitTime) { it == null || it >= currentDate }
|
||||
runDateChange(nodeBApi)
|
||||
nextFixingDates.firstWithTimeout(maxWaitTime) { it == null || it >= futureDate }
|
||||
|
||||
assertThat(getFloatingLegFixCount(nodeAApi) > 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFloatingLegFixCount(nodeApi: HttpApi): Int {
|
||||
return getTrades(nodeApi)[0].calculation.floatingLegPaymentSchedule.count { it.value.rate.ratioUnit != null }
|
||||
}
|
||||
|
||||
private fun getFixingDateObservable(address: NetworkHostAndPort): Observable<LocalDate?> {
|
||||
val client = CordaRPCClient(address)
|
||||
val proxy = client.start("user", "password").proxy
|
||||
val vaultUpdates = proxy.vaultTrackBy<InterestRateSwap.State>().updates
|
||||
|
||||
return vaultUpdates.map { update ->
|
||||
val irsStates = update.produced.map { it.state.data }
|
||||
irsStates.mapNotNull { it.calculation.nextFixingDate() }.max()
|
||||
}.cache()
|
||||
}
|
||||
|
||||
private fun runDateChange(nodeApi: HttpApi) {
|
||||
log.info("Running date change against ${nodeApi.root}")
|
||||
nodeApi.putJson("demodate", "\"$futureDate\"")
|
||||
}
|
||||
|
||||
private fun runTrade(nodeApi: HttpApi, oracle: Party) {
|
||||
log.info("Running trade against ${nodeApi.root}")
|
||||
val fileContents = loadResourceFile("net/corda/irs/web/simulation/example-irs-trade.json")
|
||||
val tradeFile = fileContents.replace("tradeXXX", "trade1").replace("oracleXXX", oracle.name.toString())
|
||||
nodeApi.postJson("deals", tradeFile)
|
||||
}
|
||||
|
||||
private fun runUploadRates(nodeApi: HttpApi) {
|
||||
log.info("Running upload rates against ${nodeApi.root}")
|
||||
val fileContents = loadResourceFile("net/corda/irs/simulation/example.rates.txt")
|
||||
nodeApi.postPlain("fixes", fileContents)
|
||||
}
|
||||
|
||||
private fun loadResourceFile(filename: String): String {
|
||||
return IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream(filename), Charsets.UTF_8.name())
|
||||
}
|
||||
|
||||
private fun getTradeCount(nodeApi: HttpApi): Int {
|
||||
log.info("Getting trade count from ${nodeApi.root}")
|
||||
val deals = nodeApi.getJson<Array<*>>("deals")
|
||||
return deals.size
|
||||
}
|
||||
|
||||
private fun getTrades(nodeApi: HttpApi): Array<InterestRateSwap.State> {
|
||||
log.info("Getting trades from ${nodeApi.root}")
|
||||
return nodeApi.getJson("deals")
|
||||
}
|
||||
|
||||
private fun <T> Observable<T>.firstWithTimeout(timeout: Duration, pred: (T) -> Boolean) {
|
||||
first(pred).toFuture().getOrThrow(timeout)
|
||||
}
|
||||
|
||||
private fun registerIRSModule(mapper: ObjectMapper) {
|
||||
val module = SimpleModule("finance").apply {
|
||||
addDeserializer(InterestRateSwap.State::class.java, InterestRateSwapStateDeserializer(mapper))
|
||||
}
|
||||
mapper.registerModule(module)
|
||||
}
|
||||
|
||||
class InterestRateSwapStateDeserializer(private val mapper: ObjectMapper) : JsonDeserializer<InterestRateSwap.State>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): InterestRateSwap.State {
|
||||
return try {
|
||||
val node = parser.readValueAsTree<TreeNode>()
|
||||
val fixedLeg: InterestRateSwap.FixedLeg = mapper.readValue(node.get("fixedLeg").toString())
|
||||
val floatingLeg: InterestRateSwap.FloatingLeg = mapper.readValue(node.get("floatingLeg").toString())
|
||||
val calculation: InterestRateSwap.Calculation = mapper.readValue(node.get("calculation").toString())
|
||||
val common: InterestRateSwap.Common = mapper.readValue(node.get("common").toString())
|
||||
val linearId: UniqueIdentifier = mapper.readValue(node.get("linearId").toString())
|
||||
val oracle: Party = mapper.readValue(node.get("oracle").toString())
|
||||
InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common, linearId = linearId, oracle = oracle)
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException(parser, "Invalid interest rate swap state(s) ${parser.text}: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package net.corda.test.spring
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.internal.concurrent.map
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.WebserverHandle
|
||||
import net.corda.testing.driver.internal.NodeHandleInternal
|
||||
import net.corda.testing.node.internal.*
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.net.ConnectException
|
||||
import java.net.URL
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
fun <A> springDriver(
|
||||
defaultParameters: DriverParameters = DriverParameters(),
|
||||
dsl: SpringBootDriverDSL.() -> A
|
||||
): A {
|
||||
return genericDriver(
|
||||
defaultParameters = defaultParameters,
|
||||
driverDslWrapper = { driverDSL: DriverDSLImpl -> SpringBootDriverDSL(driverDSL) },
|
||||
coerce = { it }, dsl = dsl
|
||||
)
|
||||
}
|
||||
|
||||
data class SpringBootDriverDSL(private val driverDSL: DriverDSLImpl) : InternalDriverDSL by driverDSL {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
val debugPort = if (driverDSL.isDebug) driverDSL.debugPortAllocation.nextPort() else null
|
||||
val process = startApplication(handle, debugPort, clazz)
|
||||
driverDSL.shutdownManager.registerProcessShutdown(process)
|
||||
val webReadyFuture = addressMustBeBoundFuture(driverDSL.executorService, (handle as NodeHandleInternal).webAddress, process)
|
||||
return webReadyFuture.map { queryWebserver(handle, process, checkUrl) }
|
||||
}
|
||||
|
||||
private fun queryWebserver(handle: NodeHandle, process: Process, checkUrl: String): WebserverHandle {
|
||||
val protocol = if ((handle as NodeHandleInternal).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(handle: NodeHandle, debugPort: Int?, clazz: Class<*>): Process {
|
||||
return ProcessUtilities.startJavaProcess(
|
||||
className = clazz.canonicalName, // cannot directly get class for this, so just use string
|
||||
jdwpPort = debugPort,
|
||||
extraJvmArguments = listOf(
|
||||
"-Dname=node-${handle.p2pAddress}-webserver",
|
||||
"-Djava.io.tmpdir=${System.getProperty("java.io.tmpdir")}"
|
||||
// Inherit from parent process
|
||||
),
|
||||
workingDirectory = handle.baseDirectory,
|
||||
arguments = listOf(
|
||||
"--base-directory", handle.baseDirectory.toString(),
|
||||
"--server.port=${(handle as NodeHandleInternal).webAddress.port}",
|
||||
"--corda.host=${handle.rpcAddress}",
|
||||
"--corda.user=${handle.rpcUsers.first().username}",
|
||||
"--corda.password=${handle.rpcUsers.first().password}"
|
||||
),
|
||||
maximumHeapSize = "256M"
|
||||
)
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
package net.corda.irs
|
||||
|
||||
import com.palantir.docker.compose.DockerComposeRule
|
||||
import com.palantir.docker.compose.configuration.DockerComposeFiles
|
||||
import com.palantir.docker.compose.connection.waiting.HealthChecks
|
||||
import org.junit.ClassRule
|
||||
import org.junit.Test
|
||||
import org.openqa.selenium.By
|
||||
import org.openqa.selenium.WebElement
|
||||
import org.openqa.selenium.phantomjs.PhantomJSDriver
|
||||
import org.openqa.selenium.support.ui.WebDriverWait
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class IRSDemoDockerTest {
|
||||
companion object {
|
||||
|
||||
private fun ensureProperty(property: String) {
|
||||
if (System.getProperty(property) == null) {
|
||||
throw IllegalStateException("System property $property not set. Please refer to README file for proper setup instructions.")
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
ensureProperty("CORDAPP_DOCKER_COMPOSE")
|
||||
ensureProperty("WEB_DOCKER_COMPOSE")
|
||||
ensureProperty("phantomjs.binary.path")
|
||||
}
|
||||
|
||||
@ClassRule
|
||||
@JvmField
|
||||
var docker: DockerComposeRule = DockerComposeRule.builder()
|
||||
.files(DockerComposeFiles.from(
|
||||
System.getProperty("CORDAPP_DOCKER_COMPOSE"),
|
||||
System.getProperty("WEB_DOCKER_COMPOSE")))
|
||||
.waitingForService("web-a", HealthChecks.toRespondOverHttp(8080, { port -> port.inFormat("http://\$HOST:\$EXTERNAL_PORT") }))
|
||||
.build()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `runs IRS demo selenium phantomjs`() {
|
||||
|
||||
val driver = PhantomJSDriver()
|
||||
|
||||
val webAPort = docker.containers().container("web-a").port(8080)
|
||||
|
||||
driver.get("http://${webAPort.ip}:${webAPort.externalPort}")
|
||||
|
||||
//no deals on fresh interface
|
||||
val dealRows = driver.findElementsByCssSelector("table#deal-list tbody tr")
|
||||
assertTrue(dealRows.isEmpty())
|
||||
|
||||
// Click Angular link and wait for form to appear
|
||||
val findElementByLinkText = driver.findElementByLinkText("Create Deal")
|
||||
findElementByLinkText.click()
|
||||
|
||||
val driverWait = WebDriverWait(driver, 120)
|
||||
|
||||
val form = driverWait.until<WebElement>({
|
||||
it?.findElement(By.cssSelector("form"))
|
||||
})
|
||||
|
||||
form.submit()
|
||||
|
||||
//Wait for deals to appear in a rows table
|
||||
val dealsList = driverWait.until<WebElement>({
|
||||
it?.findElement(By.cssSelector("table#deal-list tbody tr"))
|
||||
})
|
||||
|
||||
assertNotNull(dealsList)
|
||||
}
|
||||
}
|
1
samples/irs-demo/web/.gitignore
vendored
1
samples/irs-demo/web/.gitignore
vendored
@ -1 +0,0 @@
|
||||
src/main/resources/static/js/bower_components
|
@ -1,173 +0,0 @@
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
import org.yaml.snakeyaml.DumperOptions
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "org.yaml:snakeyaml:1.24"
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'com.craigburke.client-dependencies' version '1.4.0'
|
||||
id 'io.spring.dependency-management'
|
||||
id 'org.springframework.boot'
|
||||
}
|
||||
|
||||
group = "${parent.group}.irs-demo"
|
||||
|
||||
dependencyManagement {
|
||||
dependencies {
|
||||
dependency "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||
dependency "org.apache.logging.log4j:log4j-core:$log4j_version"
|
||||
dependency "org.apache.logging.log4j:log4j-api:$log4j_version"
|
||||
}
|
||||
}
|
||||
|
||||
clientDependencies {
|
||||
registry 'realBower', type:'bower', url:'https://registry.bower.io'
|
||||
realBower {
|
||||
"angular"("1.5.8")
|
||||
"jquery"("^3.0.0")
|
||||
"angular-route"("1.5.8")
|
||||
"lodash"("^4.13.1")
|
||||
"angular-fcsa-number"("^1.5.3")
|
||||
"jquery.maskedinput"("^1.4.1")
|
||||
"requirejs"("^2.2.0")
|
||||
"semantic-ui"("^2.2.2", into: "semantic")
|
||||
}
|
||||
|
||||
// put the JS dependencies into src directory so it can easily be referenced
|
||||
// from HTML files in webapp frontend, useful for testing/development
|
||||
// Note that this dir is added to .gitignore
|
||||
installDir = 'src/main/resources/static/js/bower_components'
|
||||
}
|
||||
|
||||
// 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
|
||||
ext['jackson.version'] = jackson_version
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlin-spring'
|
||||
apply plugin: 'eclipse'
|
||||
apply plugin: 'project-report'
|
||||
apply plugin: 'application'
|
||||
|
||||
configurations {
|
||||
demoArtifacts.extendsFrom testRuntime
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile('org.springframework.boot:spring-boot-starter-web') {
|
||||
exclude module: "spring-boot-starter-logging"
|
||||
exclude module: "logback-classic"
|
||||
}
|
||||
compile('org.springframework.boot:spring-boot-starter-log4j2')
|
||||
runtimeOnly("org.apache.logging.log4j:log4j-web:$log4j_version")
|
||||
compile("com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version")
|
||||
compile project(":client:rpc")
|
||||
compile project(":client:jackson")
|
||||
compile project(":finance:workflows")
|
||||
// TODO In the future remove -irs bit from the directory name. Currently it clashes with :finance:workflows (same for contracts).
|
||||
compile project(":samples:irs-demo:cordapp:workflows-irs")
|
||||
|
||||
testCompile project(":test-utils")
|
||||
testCompile project(path: ":samples:irs-demo:cordapp:workflows-irs", configuration: "demoArtifacts")
|
||||
|
||||
// JOpt: for command line flags.
|
||||
compile "net.sf.jopt-simple:jopt-simple:$jopt_simple_version"
|
||||
|
||||
testCompile('org.springframework.boot:spring-boot-starter-test') {
|
||||
exclude module: "spring-boot-starter-logging"
|
||||
exclude module: "logback-classic"
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
from sourceSets.main.output
|
||||
dependsOn clientInstall
|
||||
archiveClassifier = 'thin'
|
||||
}
|
||||
|
||||
def docker_dir = file("$project.buildDir/docker")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
task demoJar(type: Jar) {
|
||||
classifier "test"
|
||||
from sourceSets.test.output
|
||||
}
|
||||
|
||||
artifacts {
|
||||
demoArtifacts demoJar
|
||||
}
|
||||
|
||||
task createDockerfile(type: com.bmuschko.gradle.docker.tasks.image.Dockerfile, dependsOn: [bootRepackage]) {
|
||||
destFile = file("$docker_dir/Dockerfile")
|
||||
|
||||
from 'azul/zulu-openjdk-alpine:8u152'
|
||||
copyFile jar.archiveName, "/opt/irs/web/"
|
||||
workingDir "/opt/irs/web/"
|
||||
defaultCommand "sh", "-c", "java -Dcorda.host=\$CORDA_HOST -jar ${jar.archiveName}"
|
||||
}
|
||||
|
||||
task prepareDockerDir(type: Copy, dependsOn: [bootRepackage, createDockerfile]) {
|
||||
from jar
|
||||
into docker_dir
|
||||
}
|
||||
|
||||
task generateDockerCompose(dependsOn: [prepareDockerDir]) {
|
||||
|
||||
def outFile = new File(project.buildDir, "docker-compose.yml")
|
||||
|
||||
ext['dockerComposePath'] = outFile
|
||||
|
||||
doLast {
|
||||
def dockerComposeObject = [
|
||||
"version": "3",
|
||||
"services": [
|
||||
"web-a": [
|
||||
"build": "$docker_dir".toString(),
|
||||
"environment": [
|
||||
"CORDA_HOST": "bank-a:10003"
|
||||
],
|
||||
"ports": ["8080"]
|
||||
],
|
||||
"web-b": [
|
||||
"build": "$docker_dir".toString(),
|
||||
"environment": [
|
||||
"CORDA_HOST": "bank-b:10003"
|
||||
],
|
||||
"ports": ["8080"]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
def options = new org.yaml.snakeyaml.DumperOptions()
|
||||
options.indent = 2
|
||||
options.defaultFlowStyle = DumperOptions.FlowStyle.BLOCK
|
||||
|
||||
def dockerComposeContent = new org.yaml.snakeyaml.Yaml(options).dump(dockerComposeObject)
|
||||
|
||||
Files.write(outFile.toPath(), dockerComposeContent.getBytes(StandardCharsets.UTF_8))
|
||||
}
|
||||
|
||||
outputs.file(outFile)
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
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.client.rpc.RPCException
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.irs.flows.plugin.registerFinanceJSONMappers
|
||||
import org.slf4j.LoggerFactory
|
||||
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 {
|
||||
log.info("Connecting to Corda on $cordaHost using username $cordaUser and password $cordaPassword")
|
||||
// TODO remove this when CordaRPC gets proper connection retry, please
|
||||
var maxRetries = 100
|
||||
do {
|
||||
try {
|
||||
return CordaRPCClient(NetworkHostAndPort.parse(cordaHost)).start(cordaUser, cordaPassword).proxy
|
||||
} catch (ex: RPCException) {
|
||||
if (maxRetries-- > 0) {
|
||||
Thread.sleep(1000)
|
||||
} else {
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
} while (true)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun objectMapper(@Autowired cordaRPCOps: CordaRPCOps): ObjectMapper {
|
||||
val mapper = JacksonSupport.createDefaultMapper(cordaRPCOps)
|
||||
registerFinanceJSONMappers(mapper)
|
||||
return mapper
|
||||
}
|
||||
|
||||
// running as standalone java app
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
SpringApplication.run(IrsDemoWebApplication::class.java, *args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,55 +0,0 @@
|
||||
package net.corda.irs.web.api
|
||||
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.irs.api.NodeInterestRates
|
||||
import net.corda.irs.flows.UpdateBusinessDayFlow
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
|
||||
/**
|
||||
* GET /api/irs/demodate - return the current date as viewed by the system in YYYY-MM-DD format.
|
||||
* PUT /api/irs/demodate - put date in format YYYY-MM-DD to advance the current date as viewed by the system and
|
||||
* POST /api/irs/fixes - store the fixing data as a text file
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/irs")
|
||||
class InterestRatesSwapDemoAPI {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
@Autowired
|
||||
lateinit var rpc: CordaRPCOps
|
||||
|
||||
@PutMapping("demodate")
|
||||
fun storeDemoDate(@RequestBody newDemoDate: LocalDate): ResponseEntity<Any?> {
|
||||
val priorDemoDate = fetchDemoDate()
|
||||
// Can only move date forwards
|
||||
if (newDemoDate.isAfter(priorDemoDate)) {
|
||||
rpc.startFlow(UpdateBusinessDayFlow::Broadcast, newDemoDate).returnValue.getOrThrow()
|
||||
return ResponseEntity.ok().build()
|
||||
}
|
||||
val msg = "demodate is already $priorDemoDate and can only be updated with a later date"
|
||||
logger.error("Attempt to set demodate to $newDemoDate but $msg")
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT).body(msg)
|
||||
}
|
||||
|
||||
@GetMapping("demodate")
|
||||
fun fetchDemoDate(): LocalDate {
|
||||
return LocalDateTime.ofInstant(rpc.currentNodeTime(), ZoneId.systemDefault()).toLocalDate()
|
||||
}
|
||||
|
||||
@PostMapping("fixes")
|
||||
fun storeFixes(@RequestBody file: String): ResponseEntity<Any?> {
|
||||
rpc.startFlow(NodeInterestRates::UploadFixesFlow, file).returnValue.getOrThrow()
|
||||
return ResponseEntity.ok().build()
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
package net.corda.irs.web.api
|
||||
|
||||
import net.corda.core.contracts.filterStatesOfType
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.messaging.vaultQueryBy
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.irs.contract.InterestRateSwap
|
||||
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 net.corda.irs.flows.AutoOfferFlow
|
||||
|
||||
/**
|
||||
* This provides a simplified API, currently for demonstration use only.
|
||||
*
|
||||
* It provides several JSON REST calls as follows:
|
||||
*
|
||||
* GET /api/irs/deals - returns an array of all deals tracked by the wallet of this node.
|
||||
* GET /api/irs/deals/{ref} - return the deal referenced by the externally provided refence that was previously uploaded.
|
||||
* POST /api/irs/deals - Payload is a JSON formatted [InterestRateSwap.State] create a new deal (includes an externally provided reference for use above).
|
||||
*
|
||||
* TODO: where we currently refer to singular external deal reference, of course this could easily be multiple identifiers e.g. CUSIP, ISIN.
|
||||
*
|
||||
* simulate any associated business processing (currently fixing).
|
||||
*
|
||||
* TODO: replace simulated date advancement with business event based implementation
|
||||
*/
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/irs")
|
||||
class InterestRateSwapAPI {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
private fun generateDealLink(deal: InterestRateSwap.State) = "/api/irs/deals/" + deal.common.tradeID
|
||||
|
||||
private fun getDealByRef(ref: String): InterestRateSwap.State? {
|
||||
val vault = rpc.vaultQueryBy<InterestRateSwap.State>().states
|
||||
val states = vault.filterStatesOfType<InterestRateSwap.State>().filter { it.state.data.linearId.externalId == ref }
|
||||
return if (states.isEmpty()) null else {
|
||||
val deals = states.map { it.state.data }
|
||||
return if (deals.isEmpty()) null else deals[0]
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired
|
||||
lateinit var rpc: CordaRPCOps
|
||||
|
||||
private fun getAllDeals(): Array<InterestRateSwap.State> {
|
||||
val vault = rpc.vaultQueryBy<InterestRateSwap.State>().states
|
||||
val states = vault.filterStatesOfType<InterestRateSwap.State>()
|
||||
return states.map { it.state.data }.toTypedArray()
|
||||
}
|
||||
|
||||
@GetMapping("/deals")
|
||||
fun fetchDeals(): Array<InterestRateSwap.State> = getAllDeals()
|
||||
|
||||
@PostMapping("/deals")
|
||||
fun storeDeal(@RequestBody newDeal: InterestRateSwap.State): ResponseEntity<Any?> {
|
||||
return try {
|
||||
rpc.startFlow(AutoOfferFlow::Requester, newDeal).returnValue.getOrThrow()
|
||||
ResponseEntity.created(URI.create(generateDealLink(newDeal))).build()
|
||||
} catch (ex: Exception) {
|
||||
logger.info("Exception when creating deal: $ex", ex)
|
||||
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/deals/{ref:.+}")
|
||||
fun fetchDeal(@PathVariable ref: String?): ResponseEntity<Any?> {
|
||||
val deal = getDealByRef(ref!!)
|
||||
return if (deal == null) {
|
||||
ResponseEntity.notFound().build()
|
||||
} else {
|
||||
ResponseEntity.ok(deal)
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/deals/networksnapshot")
|
||||
fun fetchDeal() = rpc.networkMapSnapshot().toString()
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
corda.host=localhost:10006
|
||||
server.port=10007
|
@ -1,2 +0,0 @@
|
||||
corda.host=localhost:10009
|
||||
server.port=10010
|
@ -1,2 +0,0 @@
|
||||
corda.host=localhost:10003
|
||||
server.port=10004
|
@ -1,2 +0,0 @@
|
||||
corda.user=user
|
||||
corda.password=password
|
@ -1,60 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info">
|
||||
|
||||
<Properties>
|
||||
<Property name="log-path">logs</Property>
|
||||
<Property name="log-name">node-${hostName}</Property>
|
||||
<Property name="archive">${log-path}/archive</Property>
|
||||
</Properties>
|
||||
|
||||
<ThresholdFilter level="trace"/>
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console-Appender" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%-5level] %date{HH:mm:ss,SSSZ} [%t] %c{2}.%method - %msg%n"/>
|
||||
</Console>
|
||||
|
||||
<!-- Required for printBasicInfo -->
|
||||
<Console name="Console-Appender-Println" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%msg%n" />
|
||||
</Console>
|
||||
|
||||
<!-- Will generate up to 10 log files for a given day. During every rollover it will delete
|
||||
those that are older than 60 days, but keep the most recent 10 GB -->
|
||||
<RollingFile name="RollingFile-Appender"
|
||||
fileName="${log-path}/${log-name}.log"
|
||||
filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz">
|
||||
|
||||
<PatternLayout pattern="[%-5level] %date{ISO8601}{UTC}Z [%t] %c{2} - %msg%n"/>
|
||||
|
||||
<Policies>
|
||||
<TimeBasedTriggeringPolicy/>
|
||||
<SizeBasedTriggeringPolicy size="10MB"/>
|
||||
</Policies>
|
||||
|
||||
<DefaultRolloverStrategy min="1" max="10">
|
||||
<Delete basePath="${archive}" maxDepth="1">
|
||||
<IfFileName glob="${log-name}*.log.gz"/>
|
||||
<IfLastModified age="60d">
|
||||
<IfAny>
|
||||
<IfAccumulatedFileSize exceeds="10 GB"/>
|
||||
</IfAny>
|
||||
</IfLastModified>
|
||||
</Delete>
|
||||
</DefaultRolloverStrategy>
|
||||
|
||||
</RollingFile>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="trace">
|
||||
<AppenderRef ref="Console-Appender" level="info"/>
|
||||
<AppenderRef ref="RollingFile-Appender" level="debug"/>
|
||||
</Root>
|
||||
<Logger name="BasicInfo" additivity="false">
|
||||
<AppenderRef ref="Console-Appender-Println"/>
|
||||
<AppenderRef ref="RollingFile-Appender" />
|
||||
</Logger>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"directory": "js/bower_components"
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "www",
|
||||
"description": "",
|
||||
"main": "",
|
||||
"license": "MIT",
|
||||
"homepage": "",
|
||||
"private": true,
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests"
|
||||
],
|
||||
"dependencies": {
|
||||
"angular": "1.5.8",
|
||||
"jquery": "^3.0.0",
|
||||
"angular-route": "1.5.8",
|
||||
"lodash": "^4.13.1",
|
||||
"angular-fcsa-number": "^1.5.3",
|
||||
"jquery.maskedinput": "^1.4.1",
|
||||
"requirejs": "^2.2.0",
|
||||
"semantic": "semantic-ui#^2.2.2"
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
#fixedleg tbody tr:nth-child(odd) {
|
||||
background-color: #EEFAEE;
|
||||
}
|
||||
|
||||
#createfixedleg, #fixedleg tbody tr:nth-child(even), #fixedleg thead th {
|
||||
background-color: #D0FAD0;
|
||||
}
|
||||
|
||||
#floatingleg tbody tr:nth-child(odd) {
|
||||
background-color: #FAEEEE;
|
||||
}
|
||||
|
||||
#createfloatingleg, #floatingleg tbody tr:nth-child(even), #floatingleg thead th {
|
||||
background-color: #FAD0D0;
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- Standard Meta -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
|
||||
<!-- Site Properties -->
|
||||
<title>IRS Demo Viewer</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="http://r3cev.com/favicon.ico" />
|
||||
<link rel="stylesheet" type="text/css" href="js/bower_components/semantic/semantic.css" />
|
||||
<link rel="stylesheet" type="text/css" href="css/main.css" />
|
||||
|
||||
<script data-main="js/require-config" src="js/bower_components/requirejs/require.js"></script>
|
||||
</head>
|
||||
<body ng-controller="HomeController">
|
||||
<div class="ui attached inverted menu">
|
||||
<span class="header item"><a href="#/">IRS Web Demo</a></span>
|
||||
<span class="item"><a href="#/">Recent Deals</a></span>
|
||||
<span class="item"><a href="#/create-deal">Create Deal</a></span>
|
||||
</div>
|
||||
<div class="ui container">
|
||||
<div id="loading" class="ui modal">
|
||||
<div class="ui text loader">Loading</div>
|
||||
</div>
|
||||
<div ng-view></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,76 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
define(['viewmodel/FixedRate'], function (fixedRateViewModel) {
|
||||
var calculationModel = {
|
||||
expression: "( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
|
||||
floatingLegPaymentSchedule: {},
|
||||
fixedLegPaymentSchedule: {}
|
||||
};
|
||||
|
||||
var indexLookup = {
|
||||
"GBP": "ICE LIBOR",
|
||||
"USD": "ICE LIBOR",
|
||||
"EUR": "EURIBOR"
|
||||
};
|
||||
|
||||
var calendarLookup = {
|
||||
"GBP": "London",
|
||||
"USD": "NewYork",
|
||||
"EUR": "London"
|
||||
};
|
||||
|
||||
var Deal = function Deal(dealViewModel) {
|
||||
var now = new Date();
|
||||
var tradeId = "T" + now.getUTCFullYear() + "-" + now.getUTCMonth() + "-" + now.getUTCDate() + "." + now.getUTCHours() + ":" + now.getUTCMinutes() + ":" + now.getUTCSeconds() + ":" + now.getUTCMilliseconds();
|
||||
|
||||
this.toJson = function () {
|
||||
var fixedLeg = {};
|
||||
var floatingLeg = {};
|
||||
var common = {};
|
||||
_.assign(fixedLeg, dealViewModel.fixedLeg);
|
||||
_.assign(floatingLeg, dealViewModel.floatingLeg);
|
||||
_.assign(common, dealViewModel.common);
|
||||
_.assign(fixedLeg.fixedRate, fixedRateViewModel);
|
||||
|
||||
fixedLeg.fixedRate = Number(fixedLeg.fixedRate) / 100;
|
||||
fixedLeg.notional = fixedLeg.notional + ' ' + common.baseCurrency;
|
||||
fixedLeg.effectiveDate = formatDateForNode(common.effectiveDate);
|
||||
fixedLeg.terminationDate = formatDateForNode(common.terminationDate);
|
||||
fixedLeg.fixedRate = { ratioUnit: { value: fixedLeg.fixedRate } };
|
||||
fixedLeg.dayCountBasisDay = fixedLeg.dayCountBasis.day;
|
||||
fixedLeg.dayCountBasisYear = fixedLeg.dayCountBasis.year;
|
||||
fixedLeg.paymentCalendar = calendarLookup[common.baseCurrency];
|
||||
delete fixedLeg.dayCountBasis;
|
||||
|
||||
floatingLeg.notional = floatingLeg.notional + ' ' + common.baseCurrency;
|
||||
floatingLeg.effectiveDate = formatDateForNode(common.effectiveDate);
|
||||
floatingLeg.terminationDate = formatDateForNode(common.terminationDate);
|
||||
floatingLeg.dayCountBasisDay = floatingLeg.dayCountBasis.day;
|
||||
floatingLeg.dayCountBasisYear = floatingLeg.dayCountBasis.year;
|
||||
floatingLeg.index = indexLookup[common.baseCurrency];
|
||||
floatingLeg.fixingCalendar = [calendarLookup[common.baseCurrency]];
|
||||
floatingLeg.paymentCalendar = [calendarLookup[common.baseCurrency]];
|
||||
delete floatingLeg.dayCountBasis;
|
||||
|
||||
common.tradeID = tradeId;
|
||||
common.eligibleCurrency = common.baseCurrency;
|
||||
common.independentAmounts.token = common.baseCurrency;
|
||||
common.threshold.token = common.baseCurrency;
|
||||
common.minimumTransferAmount.token = common.baseCurrency;
|
||||
common.rounding.token = common.baseCurrency;
|
||||
delete common.effectiveDate;
|
||||
delete common.terminationDate;
|
||||
|
||||
var json = {
|
||||
fixedLeg: fixedLeg,
|
||||
floatingLeg: floatingLeg,
|
||||
calculation: calculationModel,
|
||||
common: common,
|
||||
oracle: dealViewModel.oracle
|
||||
};
|
||||
|
||||
return json;
|
||||
};
|
||||
};
|
||||
return Deal;
|
||||
});
|
@ -1,18 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
function formatDateForNode(date) {
|
||||
// Produces yyyy-dd-mm. JS is missing proper date formatting libs
|
||||
var day = ("0" + date.getDate()).slice(-2);
|
||||
var month = ("0" + (date.getMonth() + 1)).slice(-2);
|
||||
return date.getFullYear() + "-" + month + "-" + day;
|
||||
}
|
||||
|
||||
function formatDateForAngular(dateStr) {
|
||||
var parts = dateStr.split("-");
|
||||
return new Date(parts[0], parts[1], parts[2]);
|
||||
}
|
||||
|
||||
define(['angular', 'angularRoute', 'jquery', 'fcsaNumber', 'semantic'], function (angular, angularRoute, $, fcsaNumber, semantic) {
|
||||
angular.module('irsViewer', ['ngRoute', 'fcsa-number']);
|
||||
requirejs(['routes']);
|
||||
});
|
@ -1,31 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
define(['angular', 'maskedInput', 'utils/semantic', 'utils/dayCountBasisLookup', 'services/NodeApi', 'Deal', 'services/HttpErrorHandler'], function (angular, maskedInput, semantic, dayCountBasisLookup, nodeApi, Deal) {
|
||||
angular.module('irsViewer').controller('CreateDealController', function CreateDealController($http, $scope, $location, nodeService, httpErrorHandler) {
|
||||
semantic.init($scope, nodeService.isLoading);
|
||||
var handleHttpFail = httpErrorHandler.createErrorHandler($scope);
|
||||
|
||||
$scope.dayCountBasisLookup = dayCountBasisLookup;
|
||||
$scope.deal = nodeService.newDeal();
|
||||
$scope.createDeal = function () {
|
||||
nodeService.createDeal(new Deal($scope.deal)).then(function (tradeId) {
|
||||
return $location.path('#/deal/' + tradeId);
|
||||
}, function (resp) {
|
||||
$scope.formError = resp.data;
|
||||
}, handleHttpFail);
|
||||
};
|
||||
$('input.percent').mask("9.999999", { placeholder: "", autoclear: false });
|
||||
$('#swapirscolumns').click(function () {
|
||||
var first = $('#irscolumns .irscolumn:eq( 0 )');
|
||||
var last = $('#irscolumns .irscolumn:eq( 1 )');
|
||||
first.before(last);
|
||||
|
||||
var swapPayers = function swapPayers() {
|
||||
var tmp = $scope.deal.floatingLeg.floatingRatePayer;
|
||||
$scope.deal.floatingLeg.floatingRatePayer = $scope.deal.fixedLeg.fixedRatePayer;
|
||||
$scope.deal.fixedLeg.fixedRatePayer = tmp;
|
||||
};
|
||||
$scope.$apply(swapPayers);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,21 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
define(['angular', 'utils/semantic', 'services/NodeApi', 'services/HttpErrorHandler'], function (angular, semantic) {
|
||||
angular.module('irsViewer').controller('DealController', function DealController($http, $scope, $routeParams, nodeService, httpErrorHandler) {
|
||||
semantic.init($scope, nodeService.isLoading);
|
||||
var handleHttpFail = httpErrorHandler.createErrorHandler($scope);
|
||||
var decorateDeal = function decorateDeal(deal) {
|
||||
var paymentSchedule = deal.calculation.floatingLegPaymentSchedule;
|
||||
Object.keys(paymentSchedule).map(function (key, index) {
|
||||
var sign = paymentSchedule[key].rate.positive ? 1 : -1;
|
||||
paymentSchedule[key].ratePercent = paymentSchedule[key].rate.ratioUnit ? (paymentSchedule[key].rate.ratioUnit.value * 100 * sign).toFixed(5) + "%" : "";
|
||||
});
|
||||
|
||||
return deal;
|
||||
};
|
||||
|
||||
nodeService.getDeal($routeParams.dealId).then(function (deal) {
|
||||
return $scope.deal = decorateDeal(deal);
|
||||
}, handleHttpFail);
|
||||
});
|
||||
});
|
@ -1,36 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
define(['angular', 'utils/semantic', 'services/NodeApi', 'services/HttpErrorHandler'], function (angular, semantic) {
|
||||
angular.module('irsViewer').controller('HomeController', function HomeController($http, $scope, nodeService, httpErrorHandler) {
|
||||
semantic.addLoadingModal($scope, nodeService.isLoading);
|
||||
|
||||
var handleHttpFail = httpErrorHandler.createErrorHandler($scope);
|
||||
|
||||
$scope.infoMsg = "";
|
||||
$scope.errorText = "";
|
||||
$scope.date = { "year": "...", "month": "...", "day": "..." };
|
||||
$scope.updateDate = function (type) {
|
||||
nodeService.updateDate(type).then(function (newDate) {
|
||||
$scope.date = newDate;
|
||||
}, handleHttpFail);
|
||||
};
|
||||
/* Extract the common name from an X500 name */
|
||||
$scope.renderX500Name = function (x500Name) {
|
||||
var name = x500Name;
|
||||
x500Name.split(',').forEach(function (element) {
|
||||
var keyValue = element.split('=');
|
||||
if (keyValue[0].toUpperCase() == 'CN') {
|
||||
name = keyValue[1];
|
||||
}
|
||||
});
|
||||
return name;
|
||||
};
|
||||
|
||||
nodeService.getDate().then(function (date) {
|
||||
return $scope.date = date;
|
||||
}, handleHttpFail);
|
||||
nodeService.getDeals().then(function (deals) {
|
||||
return $scope.deals = deals;
|
||||
}, handleHttpFail);
|
||||
});
|
||||
});
|
@ -1,24 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
require.config({
|
||||
paths: {
|
||||
angular: 'bower_components/angular/angular',
|
||||
angularRoute: 'bower_components/angular-route/angular-route',
|
||||
fcsaNumber: 'bower_components/angular-fcsa-number/src/fcsaNumber',
|
||||
jquery: 'bower_components/jquery/jquery',
|
||||
semantic: 'bower_components/semantic/semantic',
|
||||
lodash: 'bower_components/lodash/lodash',
|
||||
maskedInput: 'bower_components/jquery.maskedinput/jquery.maskedinput'
|
||||
},
|
||||
shim: {
|
||||
'angular': { 'exports': 'angular' },
|
||||
'angularRoute': ['angular'],
|
||||
'fcsaNumber': ['angular'],
|
||||
'semantic': ['jquery'],
|
||||
'maskedInput': ['jquery']
|
||||
},
|
||||
priority: ["angular"],
|
||||
baseUrl: 'js'
|
||||
});
|
||||
|
||||
require(['angular', 'app'], function (angular, app) {});
|
@ -1,23 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
define(['angular', 'controllers/Home', 'controllers/Deal', 'controllers/CreateDeal'], function (angular) {
|
||||
angular.module('irsViewer').config(function ($routeProvider, $locationProvider) {
|
||||
$routeProvider.when('/', {
|
||||
controller: 'HomeController',
|
||||
templateUrl: 'view/home.html'
|
||||
}).when('/deal/:dealId', {
|
||||
controller: 'DealController',
|
||||
templateUrl: 'view/deal.html'
|
||||
}).when('/party/:partyId', {
|
||||
templateUrl: 'view/party.html'
|
||||
}).when('/create-deal', {
|
||||
controller: 'CreateDealController',
|
||||
templateUrl: 'view/create-deal.html'
|
||||
}).otherwise({ redirectTo: '/' });
|
||||
});
|
||||
|
||||
angular.element().ready(function () {
|
||||
// bootstrap the app manually
|
||||
angular.bootstrap(document, ['irsViewer']);
|
||||
});
|
||||
});
|
@ -1,17 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
define(['angular', 'lodash', 'viewmodel/Deal'], function (angular, _) {
|
||||
angular.module('irsViewer').factory('httpErrorHandler', function () {
|
||||
return {
|
||||
createErrorHandler: function createErrorHandler(scope) {
|
||||
return function (resp) {
|
||||
if (resp.status == -1) {
|
||||
scope.httpError = "Could not connect to node web server";
|
||||
} else {
|
||||
scope.httpError = resp.data;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
@ -1,104 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
define(['angular', 'lodash', 'viewmodel/Deal'], function (angular, _, dealViewModel) {
|
||||
angular.module('irsViewer').factory('nodeService', function ($http) {
|
||||
return new function () {
|
||||
var _this = this;
|
||||
|
||||
var date = new Date(2016, 0, 1, 0, 0, 0);
|
||||
var curLoading = {};
|
||||
var serverAddr = ''; // Leave empty to target the same host this page is served from
|
||||
|
||||
var load = function load(type, promise) {
|
||||
curLoading[type] = true;
|
||||
return promise.then(function (arg) {
|
||||
curLoading[type] = false;
|
||||
return arg;
|
||||
}, function (arg) {
|
||||
curLoading[type] = false;
|
||||
throw arg;
|
||||
});
|
||||
};
|
||||
|
||||
var endpoint = function endpoint(target) {
|
||||
return serverAddr + target;
|
||||
};
|
||||
|
||||
var changeDateOnNode = function changeDateOnNode(newDate) {
|
||||
var dateStr = formatDateForNode(newDate);
|
||||
return load('date', $http.put(endpoint('/api/irs/demodate'), "\"" + dateStr + "\"")).then(function (resp) {
|
||||
date = newDate;
|
||||
return _this.getDateModel(date);
|
||||
});
|
||||
};
|
||||
|
||||
this.getDate = function () {
|
||||
return load('date', $http.get(endpoint('/api/irs/demodate'))).then(function (resp) {
|
||||
var dateParts = resp.data.split("-");
|
||||
date = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); // JS uses 0 based months
|
||||
return _this.getDateModel(date);
|
||||
});
|
||||
};
|
||||
|
||||
this.updateDate = function (type) {
|
||||
var newDate = date;
|
||||
switch (type) {
|
||||
case "year":
|
||||
newDate.setFullYear(date.getFullYear() + 1);
|
||||
break;
|
||||
|
||||
case "month":
|
||||
newDate.setMonth(date.getMonth() + 1);
|
||||
break;
|
||||
|
||||
case "day":
|
||||
newDate.setDate(date.getDate() + 1);
|
||||
break;
|
||||
}
|
||||
|
||||
return changeDateOnNode(newDate);
|
||||
};
|
||||
|
||||
this.getDeals = function () {
|
||||
return load('deals', $http.get(endpoint('/api/irs/deals'))).then(function (resp) {
|
||||
return resp.data.reverse();
|
||||
});
|
||||
};
|
||||
|
||||
this.getDeal = function (dealId) {
|
||||
return load('deal' + dealId, $http.get(endpoint('/api/irs/deals/' + dealId))).then(function (resp) {
|
||||
// Do some data modification to simplify the model
|
||||
var deal = resp.data;
|
||||
deal.fixedLeg.fixedRate.value = (deal.fixedLeg.fixedRate.ratioUnit.value * 100).toString().slice(0, 6);
|
||||
return deal;
|
||||
});
|
||||
};
|
||||
|
||||
this.getDateModel = function (date) {
|
||||
return {
|
||||
"year": date.getFullYear(),
|
||||
"month": date.getMonth() + 1, // JS uses 0 based months
|
||||
"day": date.getDate()
|
||||
};
|
||||
};
|
||||
|
||||
this.isLoading = function () {
|
||||
return _.reduce(Object.keys(curLoading), function (last, key) {
|
||||
return last || curLoading[key];
|
||||
}, false);
|
||||
};
|
||||
|
||||
this.newDeal = function () {
|
||||
return dealViewModel;
|
||||
};
|
||||
|
||||
this.createDeal = function (deal) {
|
||||
return load('create-deal', $http.post(endpoint('/api/irs/deals'), deal.toJson())).then(function (resp) {
|
||||
return deal.tradeId;
|
||||
}, function (resp) {
|
||||
throw resp;
|
||||
});
|
||||
};
|
||||
}();
|
||||
});
|
||||
});
|
@ -1,34 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
define([], function () {
|
||||
return {
|
||||
"30/360": {
|
||||
"day": "D30",
|
||||
"year": "Y360"
|
||||
},
|
||||
"30E/360": {
|
||||
"day": "D30E",
|
||||
"year": "Y360"
|
||||
},
|
||||
"ACT/360": {
|
||||
"day": "DActual",
|
||||
"year": "Y360"
|
||||
},
|
||||
"ACT/365 Fixed": {
|
||||
"day": "DActual",
|
||||
"year": "Y365F"
|
||||
},
|
||||
"ACT/365 L": {
|
||||
"day": "DActual",
|
||||
"year": "Y365L"
|
||||
},
|
||||
"ACT/ACT ISDA": {
|
||||
"day": "DActual",
|
||||
"year": "YISDA"
|
||||
},
|
||||
"ACT/ACT ICMA": {
|
||||
"day": "DActual",
|
||||
"year": "YICMA"
|
||||
}
|
||||
};
|
||||
});
|
@ -1,22 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
define(['jquery', 'semantic'], function ($, semantic) {
|
||||
return {
|
||||
init: function init($scope, loadingFunc) {
|
||||
$('.ui.accordion').accordion();
|
||||
$('.ui.dropdown').dropdown();
|
||||
$('.ui.sticky').sticky();
|
||||
|
||||
this.addLoadingModal($scope, loadingFunc);
|
||||
},
|
||||
addLoadingModal: function addLoadingModal($scope, loadingFunc) {
|
||||
$scope.$watch(loadingFunc, function (newVal) {
|
||||
if (newVal === true) {
|
||||
$('#loading').modal('setting', 'closable', false).modal('show');
|
||||
} else {
|
||||
$('#loading').modal('hide');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
@ -1,38 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
define([], function () {
|
||||
return {
|
||||
baseCurrency: "USD",
|
||||
effectiveDate: new Date(2016, 2, 11),
|
||||
terminationDate: new Date(2026, 2, 11),
|
||||
eligibleCreditSupport: "Cash in an Eligible Currency",
|
||||
independentAmounts: {
|
||||
quantity: 0
|
||||
},
|
||||
threshold: {
|
||||
quantity: 0
|
||||
},
|
||||
minimumTransferAmount: {
|
||||
quantity: 25000000
|
||||
},
|
||||
rounding: {
|
||||
quantity: 1000000
|
||||
},
|
||||
valuationDateDescription: "Every Local Business Day",
|
||||
notificationTime: "2:00pm London",
|
||||
resolutionTime: "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given",
|
||||
interestRate: {
|
||||
oracle: "Rates Service Provider",
|
||||
tenor: {
|
||||
name: "6M"
|
||||
},
|
||||
ratioUnit: null,
|
||||
name: "EONIA"
|
||||
},
|
||||
addressForTransfers: "",
|
||||
exposure: {},
|
||||
localBusinessDay: ["London", "NewYork"],
|
||||
dailyInterestAmount: "(CashAmount * InterestRate ) / (fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360",
|
||||
hashLegalDocs: "put hash here"
|
||||
};
|
||||
});
|
@ -1,10 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
define(['viewmodel/FixedLeg', 'viewmodel/FloatingLeg', 'viewmodel/Common'], function (fixedLeg, floatingLeg, common) {
|
||||
return {
|
||||
fixedLeg: fixedLeg,
|
||||
floatingLeg: floatingLeg,
|
||||
common: common,
|
||||
oracle: "O=Notary Service,L=Zurich,C=CH"
|
||||
};
|
||||
});
|
@ -1,18 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
define(['utils/dayCountBasisLookup'], function (dayCountBasisLookup) {
|
||||
return {
|
||||
fixedRatePayer: "O=Bank A,L=London,C=GB",
|
||||
notional: 2500000000,
|
||||
paymentFrequency: "SemiAnnual",
|
||||
effectiveDateAdjustment: null,
|
||||
terminationDateAdjustment: null,
|
||||
fixedRate: "1.676",
|
||||
dayCountBasis: dayCountBasisLookup["ACT/360"],
|
||||
rollConvention: "ModifiedFollowing",
|
||||
dayInMonth: 10,
|
||||
paymentRule: "InArrears",
|
||||
paymentDelay: "0",
|
||||
interestPeriodAdjustment: "Adjusted"
|
||||
};
|
||||
});
|
@ -1,9 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
define([], () => {
|
||||
return {
|
||||
ratioUnit: {
|
||||
value: 0.01 // %
|
||||
}
|
||||
};
|
||||
});
|
@ -1,26 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
define(['utils/dayCountBasisLookup'], function (dayCountBasisLookup) {
|
||||
return {
|
||||
floatingRatePayer: "O=Bank B,L=New York,C=US",
|
||||
notional: 2500000000,
|
||||
paymentFrequency: "Quarterly",
|
||||
effectiveDateAdjustment: null,
|
||||
terminationDateAdjustment: null,
|
||||
dayCountBasis: dayCountBasisLookup["ACT/360"],
|
||||
rollConvention: "ModifiedFollowing",
|
||||
fixingRollConvention: "ModifiedFollowing",
|
||||
dayInMonth: 10,
|
||||
resetDayInMonth: 10,
|
||||
paymentRule: "InArrears",
|
||||
paymentDelay: "0",
|
||||
interestPeriodAdjustment: "Adjusted",
|
||||
fixingPeriodOffset: 2,
|
||||
resetRule: "InAdvance",
|
||||
fixingsPerPayment: "Quarterly",
|
||||
indexSource: "Rates Service Provider",
|
||||
indexTenor: {
|
||||
name: "3M"
|
||||
}
|
||||
};
|
||||
});
|
@ -1,185 +0,0 @@
|
||||
<div class="ui container">
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui negative message" id="form-error" ng-show="formError">{{formError}}</div>
|
||||
<div class="ui negative message" id="http-error" ng-show="httpError">{{httpError}}</div>
|
||||
<h3 class="ui horizontal divider header">
|
||||
<i class="list icon"></i>
|
||||
New Deal
|
||||
</h3>
|
||||
<form id="deal-form" class="ui form" ng-submit="createDeal()">
|
||||
<div id="irscolumns" class="ui centered grid">
|
||||
<div class="sixteen wide tablet eight wide computer column">
|
||||
<div class="field">
|
||||
<label>Base Currency</label>
|
||||
<select class="ui fluid" name="token" ng-model="deal.common.baseCurrency">
|
||||
<option value="EUR">EUR</option>
|
||||
<option value="USD">USD</option>
|
||||
<option value="GBP">GBP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Effective Date</label>
|
||||
<input type="date" name="effectiveDate" ng-model="deal.common.effectiveDate"/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Termination Date</label>
|
||||
<input type="date" name="terminationDate" ng-model="deal.common.terminationDate"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sixteen wide column">
|
||||
<button type="button" id="swapirscolumns" class="ui icon button fluid">
|
||||
<i class="exchange icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="eight wide column irscolumn" id="createfixedleg">
|
||||
<h3>Fixed Leg</h3>
|
||||
<div class="field">
|
||||
<label>Fixed Rate Payer</label>
|
||||
<input type="text" name="fixedRatePayer" ng-model="deal.fixedLeg.fixedRatePayer"/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Notional</label>
|
||||
<input type="text" name="quantity" ng-model="deal.fixedLeg.notional" fcsa-number/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Fixed Rate</label>
|
||||
<input type="text" name="value" class="percent" ng-model="deal.fixedLeg.fixedRate"/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Payment Frequency</label>
|
||||
<select class="ui selection" ng-model="deal.fixedLeg.paymentFrequency">
|
||||
<option value="Annual">Annual</option>
|
||||
<option value="SemiAnnual">Semi Annual</option>
|
||||
<option value="Quarterly">Quarterly</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Day Count Basis</label>
|
||||
<select class="ui selection"
|
||||
ng-model="deal.fixedLeg.dayCountBasis"
|
||||
ng-options="key for (key, value) in dayCountBasisLookup">
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Roll Convention</label>
|
||||
<select class="ui selection" ng-model="deal.fixedLeg.rollConvention">
|
||||
<option value="Following">Following</option>
|
||||
<option value="Previous">Previous</option>
|
||||
<option value="ModifiedFollowing">Modified following</option>
|
||||
<option value="ModifiedPrevious">Modified previous</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Day in Month</label>
|
||||
<input type="number" name="dayInMonth" min="1" max="31" ng-model="deal.fixedLeg.dayInMonth"/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Payment Delay</label>
|
||||
<select class="ui selection" ng-model="deal.fixedLeg.paymentDelay">
|
||||
<option value="0">T+00D</option>
|
||||
<option value="1">T+01D</option>
|
||||
<option value="2" selected="selected">T+02D</option>
|
||||
<option value="3">T+03D</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Interest Period Adjustment</label>
|
||||
<select class="ui selection" ng-model="deal.fixedLeg.interestPeriodAdjustment">
|
||||
<option value="Adjusted">Adjusted</option>
|
||||
<option value="Unadjusted">Unadjusted</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="eight wide column irscolumn" id="createfloatingleg">
|
||||
<h3>Floating Leg</h3>
|
||||
<div class="field">
|
||||
<label>Floating Rate Payer</label>
|
||||
<input type="text" name="floatingRatePayer" ng-model="deal.floatingLeg.floatingRatePayer"/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Notional</label>
|
||||
<input type="text" name="quantity" ng-model="deal.floatingLeg.notional" fcsa-number/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Payment Frequency</label>
|
||||
<select class="ui selection" ng-model="deal.floatingLeg.paymentFrequency">
|
||||
<option value="Annual">Annual</option>
|
||||
<option value="Quarterly">Quarterly</option>
|
||||
<option value="SemiAnnual">Semi Annual</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Day Count Basis</label>
|
||||
<select class="ui selection"
|
||||
ng-model="deal.floatingLeg.dayCountBasis"
|
||||
ng-options="key for (key, value) in dayCountBasisLookup">
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Roll Convention</label>
|
||||
<select class="ui selection" ng-model="deal.floatingLeg.rollConvention">
|
||||
<option value="Following">Following</option>
|
||||
<option value="Previous">Previous</option>
|
||||
<option value="ModifiedFollowing">Modified following</option>
|
||||
<option value="ModifiedPrevious">Modified previous</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Fixing Roll Convention</label>
|
||||
<select class="ui selection" ng-model="deal.floatingLeg.fixingRollConvention">
|
||||
<option value="Following">Following</option>
|
||||
<option value="Previous">Previous</option>
|
||||
<option value="ModifiedFollowing">Modified following</option>
|
||||
<option value="ModifiedPrevious">Modified previous</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Day In Month</label>
|
||||
<input type="number" name="dayInMonth" min="1" max="31" ng-model="deal.floatingLeg.dayInMonth"/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Reset Day In Month</label>
|
||||
<input type="number" name="resetDayInMonth" min="1" max="31" ng-model="deal.floatingLeg.resetDayInMonth"/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Payment Delay</label>
|
||||
<select class="ui selection" ng-model="deal.floatingLeg.paymentDelay">
|
||||
<option value="0">T+00D</option>
|
||||
<option value="1">T+01D</option>
|
||||
<option value="2">T+02D</option>
|
||||
<option value="3">T+03D</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Interest Period Adjustment</label>
|
||||
<select class="ui selection" ng-model="deal.floatingLeg.interestPeriodAdjustment">
|
||||
<option value="Adjusted">Adjusted</option>
|
||||
<option value="Unadjusted">Unadjusted</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Fixing Period Offset</label>
|
||||
<input type="number" min="0" name="fixingPeriodOffset" ng-model="deal.floatingLeg.fixingPeriodOffset"/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Reset Rule</label>
|
||||
<select class="ui selection" ng-model="deal.floatingLeg.resetRule">
|
||||
<option value="InAdvance">In Advance</option>
|
||||
<option value="InArrears">In Arrears</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Fixings Per Payment</label>
|
||||
<select class="ui selection" ng-model="deal.floatingLeg.fixingsPerPayment">
|
||||
<option value="Annual">Annual</option>
|
||||
<option value="Quarterly" selected="selected">Quarterly</option>
|
||||
<option value="SemiAnnual">Semi Annual</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sixteen wide tablet eight wide computer column">
|
||||
<input type="submit" class="ui submit primary button fluid"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
@ -1,213 +0,0 @@
|
||||
<div class="ui container">
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui negative message" id="http-error" ng-show="httpError">{{httpError}}</div>
|
||||
<div class="ui grid">
|
||||
<div class="sixteen wide column" id="common">
|
||||
<table class="ui striped table">
|
||||
<thead>
|
||||
<tr class="center aligned">
|
||||
<th colspan="2">Common Information</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="center aligned">
|
||||
<td>Parties</td>
|
||||
<td>
|
||||
<span ng-repeat="party in deal.participants">
|
||||
{{party}}<span ng-show="!$last">,</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Trade ID</td>
|
||||
<td>{{deal.common.tradeID}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Valuation Date</td>
|
||||
<td>{{deal.common.valuationDateDescription}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Legal Document Hash</td>
|
||||
<td>{{deal.common.hashLegalDocs}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Interest Rates</td>
|
||||
<td>
|
||||
{{deal.common.interestRate.name}} with tenor
|
||||
{{deal.common.interestRate.tenor.name}} via oracle
|
||||
{{deal.common.interestRate.oracle}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="eight wide column" id="fixedleg">
|
||||
<table class="ui striped table">
|
||||
<thead>
|
||||
<tr class="center aligned">
|
||||
<th colspan="2">Fixed Leg</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="center aligned">
|
||||
<td>Payer</td>
|
||||
<td>{{deal.fixedLeg.fixedRatePayer}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Notional</td>
|
||||
<td>{{deal.fixedLeg.notional}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Payment Frequency</td>
|
||||
<td>{{deal.fixedLeg.paymentFrequency}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Effective From</td>
|
||||
<td>{{deal.fixedLeg.effectiveDate}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Fixed Rate</td>
|
||||
<td>
|
||||
<span ng-show="!deal.fixedLeg.fixedRate.positive">-</span>
|
||||
{{deal.fixedLeg.fixedRate.value}}%
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Terminates</td>
|
||||
<td>{{deal.fixedLeg.terminationDate}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Payment Rule</td>
|
||||
<td>{{deal.fixedLeg.paymentRule}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Payment Calendars</td>
|
||||
<td>
|
||||
<span ng-repeat="calendar in deal.fixedLeg.paymentCalendar.calendars">
|
||||
<span>{{calendar}}</span><span ng-show="!$last">,</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td colspan="2">
|
||||
<div class="ui accordion">
|
||||
<div class="title">
|
||||
<i class="dropdown icon"></i>
|
||||
Holiday Dates
|
||||
</div>
|
||||
<div class="content">
|
||||
<span>{{deal.fixedLeg.paymentCalendar}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="eight wide column" id="floatingleg">
|
||||
<table class="ui striped table">
|
||||
<thead>
|
||||
<tr class="center aligned">
|
||||
<th colspan="2">Floating Leg</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="center aligned">
|
||||
<td>Payer</td>
|
||||
<td>{{deal.floatingLeg.floatingRatePayer}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Notional</td>
|
||||
<td>{{deal.floatingLeg.notional}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Payment Frequency</td>
|
||||
<td>{{deal.floatingLeg.paymentFrequency}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Effective From</td>
|
||||
<td>{{deal.floatingLeg.effectiveDate}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Index</td>
|
||||
<td>{{deal.floatingLeg.index}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Terminates</td>
|
||||
<td>{{deal.floatingLeg.terminationDate}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Payment Rule</td>
|
||||
<td>{{deal.floatingLeg.paymentRule}}</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Payment Calendars</td>
|
||||
<td>
|
||||
<span ng-repeat="calendar in deal.floatingLeg.paymentCalendar.calendars">
|
||||
<span>{{calendar}}</span><span ng-show="!$last">,</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td colspan="2">
|
||||
<div class="ui accordion">
|
||||
<div class="title">
|
||||
<i class="dropdown icon"></i>
|
||||
Holiday Dates
|
||||
</div>
|
||||
<div class="content">
|
||||
<span>
|
||||
{{deal.floatingLeg.paymentCalendar}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td>Fixing Calendars</td>
|
||||
<td>
|
||||
<span ng-repeat="calendar in deal.floatingLeg.fixingCalendar.calendars">
|
||||
<span>{{calendar}}</span><span ng-show="!$last">,</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td colspan="2">
|
||||
<div class="ui accordion">
|
||||
<div class="title">
|
||||
<i class="dropdown icon"></i>
|
||||
Holiday Dates
|
||||
</div>
|
||||
<div class="content">
|
||||
<span>
|
||||
{{deal.floatingLeg.fixingCalendar}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="center aligned">
|
||||
<td colspan="2">
|
||||
<div class="ui accordion">
|
||||
<div class="title">
|
||||
<i class="dropdown icon"></i>
|
||||
Fixings
|
||||
</div>
|
||||
<div class="content">
|
||||
<table class="ui celled small table">
|
||||
<tbody>
|
||||
<tr class="center aligned" ng-repeat="fixing in deal.calculation.floatingLegPaymentSchedule">
|
||||
<td>{{fixing.fixingDate[0]}}-{{fixing.fixingDate[1]}}-{{fixing.fixingDate[2]}}</td>
|
||||
<td>{{fixing.ratePercent}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,58 +0,0 @@
|
||||
<div class="ui container">
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui negative message" id="http-error" ng-show="httpError">{{httpError}}</div>
|
||||
<div class="ui info message" id="info-message" ng-show="infoMsg">{{infoMsg}}</div>
|
||||
<div class="ui active dimmer" ng-show="isLoading()">
|
||||
<div class="ui text loader">Loading</div>
|
||||
</div>
|
||||
<h3 class="ui horizontal divider header">
|
||||
<i class="options icon"></i>
|
||||
Controls
|
||||
</h3>
|
||||
<div class="ui card centered">
|
||||
<div class="content" style="width:110%">
|
||||
<div class="header">Run fixings</div>
|
||||
<div class="description">
|
||||
<div class="ui left labeled button">
|
||||
<span class="ui basic label">{{date.year}}</span>
|
||||
<button class="ui icon button" ng-click="updateDate('year')"><i class="plus icon"></i></button>
|
||||
</div>
|
||||
<div class="ui left labeled button">
|
||||
<span class="ui basic label">{{date.month}}</span>
|
||||
<button class="ui icon button" ng-click="updateDate('month')"><i class="plus icon"></i></button>
|
||||
</div>
|
||||
<div class="ui left labeled button">
|
||||
<span class="ui basic label">{{date.day}}</span>
|
||||
<button class="ui icon button" ng-click="updateDate('day')"><i class="plus icon"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui hidden divider"></div>
|
||||
<div class="ui main">
|
||||
<h3 class="ui horizontal divider header">
|
||||
<i class="browser icon"></i>
|
||||
Recent deals
|
||||
</h3>
|
||||
<table class="ui striped table" id="deal-list">
|
||||
<thead>
|
||||
<tr class="center aligned">
|
||||
<th>Trade Id</th>
|
||||
<th>Fixed Leg Payer</th>
|
||||
<th>Amount</th>
|
||||
<th>Floating Rate Payer</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="center aligned" ng-repeat="deal in deals" id="deal-{{deal.common.tradeID}}">
|
||||
<td><a href="#/deal/{{deal.common.tradeID}}">{{deal.common.tradeID}}</a></td>
|
||||
<td class="single line">{{renderX500Name(deal.fixedLeg.fixedRatePayer)}}</td>
|
||||
<td class="single line">{{deal.fixedLeg.notional}}</td>
|
||||
<td class="single line">{{renderX500Name(deal.floatingLeg.floatingRatePayer)}}</td>
|
||||
<td class="single line">{{deal.floatingLeg.notional}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
@ -1,19 +0,0 @@
|
||||
package net.corda.irs.web
|
||||
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.mock.mockito.MockBean
|
||||
import org.springframework.test.context.junit4.SpringRunner
|
||||
|
||||
@RunWith(SpringRunner::class)
|
||||
@SpringBootTest(properties = ["corda.host=localhost:12345", "corda.user=user", "corda.password=password"])
|
||||
class IrsDemoWebApplicationTests {
|
||||
@MockBean
|
||||
lateinit var rpc: CordaRPCOps
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun contextLoads() {
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
@file:JvmName("IRSDemo")
|
||||
|
||||
package net.corda.irs.web.demo
|
||||
|
||||
import joptsimple.OptionParser
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
enum class Role {
|
||||
UploadRates,
|
||||
Trade,
|
||||
Date
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val parser = OptionParser()
|
||||
val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required()
|
||||
val valueArg = parser.nonOptions()
|
||||
val options = try {
|
||||
parser.parse(*args)
|
||||
} catch (e: Exception) {
|
||||
println(e.message)
|
||||
printHelp(parser)
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
// What happens next depends on the role. The buyer sits around waiting for a trade to start. The seller role
|
||||
// will contact the buyer and actually make something happen.
|
||||
val role = options.valueOf(roleArg)!!
|
||||
val value = options.valueOf(valueArg)
|
||||
when (role) {
|
||||
Role.UploadRates -> IRSDemoClientApi(NetworkHostAndPort("localhost", 10004)).runUploadRates()
|
||||
Role.Trade -> IRSDemoClientApi(NetworkHostAndPort("localhost", 10007)).runTrade(value, CordaX500Name.parse("O=Notary Service,L=Zurich,C=CH"))
|
||||
Role.Date -> IRSDemoClientApi(NetworkHostAndPort("localhost", 10010)).runDateChange(value)
|
||||
}
|
||||
}
|
||||
|
||||
fun printHelp(parser: OptionParser) {
|
||||
println("""
|
||||
Usage: irs-demo --role [UploadRates|Trade|Date] [trade ID|yyy-mm-dd]
|
||||
Please refer to the documentation in docs/build/index.html for more info.
|
||||
|
||||
""".trimIndent())
|
||||
parser.printHelpOn(System.out)
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user