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:
Stefano Franz 2022-03-27 13:09:46 +02:00
commit c4b7066c83
329 changed files with 4 additions and 115796 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
These certificates are used for development mode only (and are copies of those contained within the TraderDemo jar file)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
someStringValue=hello world
someIntValue=1
nested: {
value: a string
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
src/main/resources/static/js/bower_components

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
corda.user=user
corda.password=password

View File

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

View File

@ -1,3 +0,0 @@
{
"directory": "js/bower_components"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +0,0 @@
'use strict';
define([], () => {
return {
ratioUnit: {
value: 0.01 // %
}
};
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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