mirror of
https://github.com/corda/corda.git
synced 2024-12-19 04:57:58 +00:00
Removed IRS demo and network simulator and placed them in separate repos.
This commit is contained in:
parent
0d3e35c5f5
commit
3ea7d382a2
18
.idea/modules.xml
generated
18
.idea/modules.xml
generated
@ -12,11 +12,17 @@
|
|||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts.iml" group="contracts" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts.iml" group="contracts" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts_main.iml" group="contracts" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts_main.iml" group="contracts" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts_test.iml" group="contracts" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts_test.iml" group="contracts" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/gradle-plugins/cordformation/cordformation.iml" filepath="$PROJECT_DIR$/.idea/modules/gradle-plugins/cordformation/cordformation.iml" group="gradle-plugins/cordformation" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/gradle-plugins/cordformation/cordformation_main.iml" filepath="$PROJECT_DIR$/.idea/modules/gradle-plugins/cordformation/cordformation_main.iml" group="gradle-plugins/cordformation" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/gradle-plugins/cordformation/cordformation_test.iml" filepath="$PROJECT_DIR$/.idea/modules/gradle-plugins/cordformation/cordformation_test.iml" group="gradle-plugins/cordformation" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/core/core.iml" filepath="$PROJECT_DIR$/.idea/modules/core/core.iml" group="core" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/core/core.iml" filepath="$PROJECT_DIR$/.idea/modules/core/core.iml" group="core" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/core/core_main.iml" filepath="$PROJECT_DIR$/.idea/modules/core/core_main.iml" group="core" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/core/core_main.iml" filepath="$PROJECT_DIR$/.idea/modules/core/core_main.iml" group="core" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/core/core_test.iml" filepath="$PROJECT_DIR$/.idea/modules/core/core_test.iml" group="core" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/core/core_test.iml" filepath="$PROJECT_DIR$/.idea/modules/core/core_test.iml" group="core" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code.iml" group="docs/source/example-code" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code.iml" group="docs/source/example-code" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code.iml" group="docs/source/example-code" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_main.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_main.iml" group="docs/source/example-code" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_main.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_main.iml" group="docs/source/example-code" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_main.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_main.iml" group="docs/source/example-code" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_test.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_test.iml" group="docs/source/example-code" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_test.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_test.iml" group="docs/source/example-code" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_test.iml" filepath="$PROJECT_DIR$/.idea/modules/docs/source/example-code/docs_source_example-code_test.iml" group="docs/source/example-code" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" group="experimental" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" group="experimental" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" group="experimental" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" group="experimental" />
|
||||||
@ -24,16 +30,22 @@
|
|||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/explorer/explorer.iml" filepath="$PROJECT_DIR$/.idea/modules/explorer/explorer.iml" group="explorer" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/explorer/explorer.iml" filepath="$PROJECT_DIR$/.idea/modules/explorer/explorer.iml" group="explorer" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/explorer/explorer_main.iml" filepath="$PROJECT_DIR$/.idea/modules/explorer/explorer_main.iml" group="explorer" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/explorer/explorer_main.iml" filepath="$PROJECT_DIR$/.idea/modules/explorer/explorer_main.iml" group="explorer" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/explorer/explorer_test.iml" filepath="$PROJECT_DIR$/.idea/modules/explorer/explorer_test.iml" group="explorer" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/explorer/explorer_test.iml" filepath="$PROJECT_DIR$/.idea/modules/explorer/explorer_test.iml" group="explorer" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/gradle-plugins/gradle-plugins.iml" filepath="$PROJECT_DIR$/.idea/modules/gradle-plugins/gradle-plugins.iml" group="gradle-plugins" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/gradle-plugins/gradle-plugins_main.iml" filepath="$PROJECT_DIR$/.idea/modules/gradle-plugins/gradle-plugins_main.iml" group="gradle-plugins" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/gradle-plugins/gradle-plugins_test.iml" filepath="$PROJECT_DIR$/.idea/modules/gradle-plugins/gradle-plugins_test.iml" group="gradle-plugins" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" group="contracts/isolated" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" group="contracts/isolated" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" group="contracts/isolated" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" group="contracts/isolated" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" group="contracts/isolated" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" group="contracts/isolated" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator.iml" filepath="$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator.iml" group="network-simulator" />
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator_main.iml" filepath="$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator_main.iml" group="network-simulator" />
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator_test.iml" filepath="$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator_test.iml" group="network-simulator" />
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node.iml" group="node" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node.iml" group="node" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_integrationTest.iml" group="node" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_integrationTest.iml" group="node" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_main.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_main.iml" group="node" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_main.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_main.iml" group="node" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_test.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_test.iml" group="node" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_test.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_test.iml" group="node" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/gradle-plugins/publish-utils/publish-utils.iml" filepath="$PROJECT_DIR$/.idea/modules/gradle-plugins/publish-utils/publish-utils.iml" group="gradle-plugins/publish-utils" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/gradle-plugins/publish-utils/publish-utils_main.iml" filepath="$PROJECT_DIR$/.idea/modules/gradle-plugins/publish-utils/publish-utils_main.iml" group="gradle-plugins/publish-utils" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/gradle-plugins/publish-utils/publish-utils_test.iml" filepath="$PROJECT_DIR$/.idea/modules/gradle-plugins/publish-utils/publish-utils_test.iml" group="gradle-plugins/publish-utils" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/gradle-plugins/quasar-utils/quasar-utils.iml" filepath="$PROJECT_DIR$/.idea/modules/gradle-plugins/quasar-utils/quasar-utils.iml" group="gradle-plugins/quasar-utils" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/gradle-plugins/quasar-utils/quasar-utils_main.iml" filepath="$PROJECT_DIR$/.idea/modules/gradle-plugins/quasar-utils/quasar-utils_main.iml" group="gradle-plugins/quasar-utils" />
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/gradle-plugins/quasar-utils/quasar-utils_test.iml" filepath="$PROJECT_DIR$/.idea/modules/gradle-plugins/quasar-utils/quasar-utils_test.iml" group="gradle-plugins/quasar-utils" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping.iml" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_integrationTest.iml" group="r3prototyping" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_integrationTest.iml" group="r3prototyping" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" group="r3prototyping" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" group="r3prototyping" />
|
||||||
|
@ -110,7 +110,6 @@ dependencies {
|
|||||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||||
compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.2"
|
compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.2"
|
||||||
compile 'com.squareup.okhttp3:okhttp:3.3.1'
|
|
||||||
compile 'co.paralleluniverse:capsule:1.0.3'
|
compile 'co.paralleluniverse:capsule:1.0.3'
|
||||||
|
|
||||||
// Unit testing helpers.
|
// Unit testing helpers.
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
maven {
|
|
||||||
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'java'
|
|
||||||
apply plugin: 'kotlin'
|
|
||||||
apply plugin: 'application'
|
|
||||||
apply plugin: 'us.kirchmeier.capsule'
|
|
||||||
|
|
||||||
group 'com.r3cev.prototyping'
|
|
||||||
version '1.0-SNAPSHOT'
|
|
||||||
|
|
||||||
sourceCompatibility = 1.5
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenLocal()
|
|
||||||
mavenCentral()
|
|
||||||
jcenter()
|
|
||||||
maven {
|
|
||||||
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
|
||||||
}
|
|
||||||
maven {
|
|
||||||
url 'https://dl.bintray.com/kotlin/exposed'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationDefaultJvmArgs = ["-javaagent:${rootProject.configurations.quasar.singleFile}"]
|
|
||||||
mainClassName = 'com.r3cev.corda.netmap.NetworkMapVisualiserKt'
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compile project(":core")
|
|
||||||
compile project(":node")
|
|
||||||
compile project(":contracts")
|
|
||||||
compile rootProject
|
|
||||||
|
|
||||||
// GraphStream: For visualisation
|
|
||||||
compile "org.graphstream:gs-core:1.3"
|
|
||||||
compile "org.graphstream:gs-ui:1.3"
|
|
||||||
|
|
||||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
||||||
testCompile group: 'junit', name: 'junit', version: '4.11'
|
|
||||||
}
|
|
||||||
|
|
||||||
task capsule(type: FatCapsule) {
|
|
||||||
applicationClass 'com.r3cev.corda.netmap.NetworkExplorerKt'
|
|
||||||
reallyExecutable
|
|
||||||
capsuleManifest {
|
|
||||||
minJavaVersion = '1.8.0'
|
|
||||||
javaAgents = [rootProject.configurations.quasar.singleFile.name]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,358 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
|
||||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
|
||||||
* set forth therein.
|
|
||||||
*
|
|
||||||
* All other rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.r3cev.corda.netmap
|
|
||||||
|
|
||||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
|
||||||
import com.r3corda.core.then
|
|
||||||
import com.r3corda.core.utilities.ProgressTracker
|
|
||||||
import com.r3corda.testing.node.InMemoryMessagingNetwork
|
|
||||||
import com.r3corda.testing.node.MockNetwork
|
|
||||||
import com.r3corda.simulation.IRSSimulation
|
|
||||||
import com.r3corda.simulation.Simulation
|
|
||||||
import com.r3corda.node.services.network.NetworkMapService
|
|
||||||
import javafx.animation.*
|
|
||||||
import javafx.application.Application
|
|
||||||
import javafx.application.Platform
|
|
||||||
import javafx.beans.property.SimpleDoubleProperty
|
|
||||||
import javafx.beans.value.WritableValue
|
|
||||||
import javafx.geometry.Insets
|
|
||||||
import javafx.geometry.Pos
|
|
||||||
import javafx.scene.control.*
|
|
||||||
import javafx.scene.input.KeyCode
|
|
||||||
import javafx.scene.input.KeyCodeCombination
|
|
||||||
import javafx.scene.layout.*
|
|
||||||
import javafx.scene.paint.Color
|
|
||||||
import javafx.scene.shape.Circle
|
|
||||||
import javafx.scene.shape.Line
|
|
||||||
import javafx.scene.shape.Polygon
|
|
||||||
import javafx.stage.Stage
|
|
||||||
import javafx.util.Duration
|
|
||||||
import rx.Scheduler
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.time.format.FormatStyle
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.concurrent.schedule
|
|
||||||
import kotlin.concurrent.scheduleAtFixedRate
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
import com.r3cev.corda.netmap.VisualiserViewModel.Style
|
|
||||||
|
|
||||||
fun <T : Any> WritableValue<T>.keyValue(endValue: T, interpolator: Interpolator = Interpolator.EASE_OUT) = KeyValue(this, endValue, interpolator)
|
|
||||||
|
|
||||||
// TODO: This code is all horribly ugly. Refactor to use TornadoFX to clean it up.
|
|
||||||
|
|
||||||
class NetworkMapVisualiser : Application() {
|
|
||||||
enum class NodeType {
|
|
||||||
BANK, SERVICE
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class RunPauseButtonLabel {
|
|
||||||
RUN, PAUSE;
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return name.toLowerCase().capitalize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class RunningPausedState {
|
|
||||||
class Running(val tickTimer: TimerTask): RunningPausedState()
|
|
||||||
class Paused(): RunningPausedState()
|
|
||||||
|
|
||||||
val buttonLabel: RunPauseButtonLabel
|
|
||||||
get() {
|
|
||||||
return when (this) {
|
|
||||||
is RunningPausedState.Running -> RunPauseButtonLabel.PAUSE
|
|
||||||
is RunningPausedState.Paused -> RunPauseButtonLabel.RUN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val view = VisualiserView()
|
|
||||||
private val viewModel = VisualiserViewModel()
|
|
||||||
|
|
||||||
val timer = Timer()
|
|
||||||
val uiThread: Scheduler = Schedulers.from { Platform.runLater(it) }
|
|
||||||
|
|
||||||
override fun start(stage: Stage) {
|
|
||||||
viewModel.view = view
|
|
||||||
viewModel.presentationMode = "--presentation-mode" in parameters.raw
|
|
||||||
buildScene(stage)
|
|
||||||
viewModel.displayStyle = if ("--circle" in parameters.raw) { Style.CIRCLE } else { viewModel.displayStyle }
|
|
||||||
|
|
||||||
val simulation = viewModel.simulation
|
|
||||||
// Update the white-backgrounded label indicating what protocol step it's up to.
|
|
||||||
simulation.allProtocolSteps.observeOn(uiThread).subscribe { step: Pair<Simulation.SimulatedNode, ProgressTracker.Change> ->
|
|
||||||
val (node, change) = step
|
|
||||||
val label = viewModel.nodesToWidgets[node]!!.statusLabel
|
|
||||||
if (change is ProgressTracker.Change.Position) {
|
|
||||||
// Fade in the status label if it's our first step.
|
|
||||||
if (label.text == "") {
|
|
||||||
with(FadeTransition(Duration(150.0), label)) {
|
|
||||||
fromValue = 0.0
|
|
||||||
toValue = 1.0
|
|
||||||
play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
label.text = change.newStep.label
|
|
||||||
if (change.newStep == ProgressTracker.DONE && change.tracker == change.tracker.topLevelTracker) {
|
|
||||||
runLater(500, -1) {
|
|
||||||
// Fade out the status label.
|
|
||||||
with(FadeTransition(Duration(750.0), label)) {
|
|
||||||
fromValue = 1.0
|
|
||||||
toValue = 0.0
|
|
||||||
setOnFinished { label.text = "" }
|
|
||||||
play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (change is ProgressTracker.Change.Rendering) {
|
|
||||||
label.text = change.ofStep.label
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fire the message bullets between nodes.
|
|
||||||
simulation.network.messagingNetwork.sentMessages.observeOn(uiThread).subscribe { msg: InMemoryMessagingNetwork.MessageTransfer ->
|
|
||||||
val senderNode: MockNetwork.MockNode = simulation.network.addressToNode(msg.sender.myAddress)
|
|
||||||
val destNode: MockNetwork.MockNode = simulation.network.addressToNode(msg.recipients as SingleMessageRecipient)
|
|
||||||
|
|
||||||
if (transferIsInteresting(msg)) {
|
|
||||||
viewModel.nodesToWidgets[senderNode]!!.pulseAnim.play()
|
|
||||||
viewModel.fireBulletBetweenNodes(senderNode, destNode, "bank", "bank")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Pulse all parties in a trade when the trade completes
|
|
||||||
simulation.doneSteps.observeOn(uiThread).subscribe { nodes: Collection<Simulation.SimulatedNode> ->
|
|
||||||
nodes.forEach { viewModel.nodesToWidgets[it]!!.longPulseAnim.play() }
|
|
||||||
}
|
|
||||||
|
|
||||||
stage.setOnCloseRequest { exitProcess(0) }
|
|
||||||
//stage.isMaximized = true
|
|
||||||
stage.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun runLater(startAfter: Int, delayBetween: Int, body: () -> Unit) {
|
|
||||||
if (delayBetween != -1) {
|
|
||||||
timer.scheduleAtFixedRate(startAfter.toLong(), delayBetween.toLong()) {
|
|
||||||
Platform.runLater {
|
|
||||||
body()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
timer.schedule(startAfter.toLong()) {
|
|
||||||
Platform.runLater {
|
|
||||||
body()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildScene(stage: Stage) {
|
|
||||||
view.stage = stage
|
|
||||||
view.setup(viewModel.runningPausedState, viewModel.displayStyle, viewModel.presentationMode)
|
|
||||||
bindSidebar()
|
|
||||||
bindTopbar()
|
|
||||||
viewModel.createNodes()
|
|
||||||
|
|
||||||
// Spacebar advances simulation by one step.
|
|
||||||
stage.scene.accelerators[KeyCodeCombination(KeyCode.SPACE)] = Runnable { onNextInvoked() }
|
|
||||||
|
|
||||||
reloadStylesheet(stage)
|
|
||||||
|
|
||||||
stage.focusedProperty().addListener { value, old, new ->
|
|
||||||
if (new) {
|
|
||||||
reloadStylesheet(stage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindTopbar() {
|
|
||||||
view.resetButton.setOnAction({reset()})
|
|
||||||
view.nextButton.setOnAction {
|
|
||||||
if (!view.simulateInitialisationCheckbox.isSelected && !viewModel.simulation.networkInitialisationFinished.isDone) {
|
|
||||||
skipNetworkInitialisation()
|
|
||||||
} else {
|
|
||||||
onNextInvoked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModel.simulation.networkInitialisationFinished.then {
|
|
||||||
view.simulateInitialisationCheckbox.isVisible = false
|
|
||||||
}
|
|
||||||
view.runPauseButton.setOnAction {
|
|
||||||
val oldRunningPausedState = viewModel.runningPausedState
|
|
||||||
val newRunningPausedState = when (oldRunningPausedState) {
|
|
||||||
is NetworkMapVisualiser.RunningPausedState.Running -> {
|
|
||||||
oldRunningPausedState.tickTimer.cancel()
|
|
||||||
|
|
||||||
view.nextButton.isDisable = false
|
|
||||||
view.resetButton.isDisable = false
|
|
||||||
|
|
||||||
NetworkMapVisualiser.RunningPausedState.Paused()
|
|
||||||
}
|
|
||||||
is NetworkMapVisualiser.RunningPausedState.Paused -> {
|
|
||||||
val tickTimer = timer.scheduleAtFixedRate(viewModel.stepDuration.toMillis().toLong(), viewModel.stepDuration.toMillis().toLong()) {
|
|
||||||
Platform.runLater {
|
|
||||||
onNextInvoked()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view.nextButton.isDisable = true
|
|
||||||
view.resetButton.isDisable = true
|
|
||||||
|
|
||||||
if (!view.simulateInitialisationCheckbox.isSelected && !viewModel.simulation.networkInitialisationFinished.isDone) {
|
|
||||||
skipNetworkInitialisation()
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkMapVisualiser.RunningPausedState.Running(tickTimer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view.runPauseButton.text = newRunningPausedState.buttonLabel.toString()
|
|
||||||
viewModel.runningPausedState = newRunningPausedState
|
|
||||||
}
|
|
||||||
view.styleChoice.selectionModel.selectedItemProperty()
|
|
||||||
.addListener { ov, value, newValue -> viewModel.displayStyle = newValue }
|
|
||||||
viewModel.simulation.dateChanges.observeOn(uiThread).subscribe { view.dateLabel.text = it.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun reloadStylesheet(stage: Stage) {
|
|
||||||
stage.scene.stylesheets.clear()
|
|
||||||
stage.scene.stylesheets.add(NetworkMapVisualiser::class.java.getResource("styles.css").toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindSidebar() {
|
|
||||||
viewModel.simulation.allProtocolSteps.observeOn(uiThread).subscribe { step: Pair<Simulation.SimulatedNode, ProgressTracker.Change> ->
|
|
||||||
val (node, change) = step
|
|
||||||
|
|
||||||
if (change is ProgressTracker.Change.Position) {
|
|
||||||
val tracker = change.tracker.topLevelTracker
|
|
||||||
if (change.newStep == ProgressTracker.DONE) {
|
|
||||||
if (change.tracker == tracker) {
|
|
||||||
// Protocol done; schedule it for removal in a few seconds. We batch them up to make nicer
|
|
||||||
// animations.
|
|
||||||
println("Protocol done for ${node.info.legalIdentity.name}")
|
|
||||||
viewModel.doneTrackers += tracker
|
|
||||||
} else {
|
|
||||||
// Subprotocol is done; ignore it.
|
|
||||||
}
|
|
||||||
} else if (!viewModel.trackerBoxes.containsKey(tracker)) {
|
|
||||||
// New protocol started up; add.
|
|
||||||
val extraLabel = viewModel.simulation.extraNodeLabels[node]
|
|
||||||
val label = if (extraLabel != null) "${node.info.legalIdentity.name}: $extraLabel" else node.info.legalIdentity.name
|
|
||||||
val widget = view.buildProgressTrackerWidget(label, tracker.topLevelTracker)
|
|
||||||
bindProgressTracketWidget(tracker.topLevelTracker, widget)
|
|
||||||
println("Added: ${tracker}, ${widget}")
|
|
||||||
viewModel.trackerBoxes[tracker] = widget.vbox
|
|
||||||
view.sidebar.children += widget.vbox
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer().scheduleAtFixedRate(0, 500) {
|
|
||||||
Platform.runLater {
|
|
||||||
for (tracker in viewModel.doneTrackers) {
|
|
||||||
val pane = viewModel.trackerBoxes[tracker]!!
|
|
||||||
// Slide the other tracker widgets up and over this one.
|
|
||||||
val slideProp = SimpleDoubleProperty(0.0)
|
|
||||||
slideProp.addListener { obv -> pane.padding = Insets(0.0, 0.0, slideProp.value, 0.0) }
|
|
||||||
val timeline = Timeline(
|
|
||||||
KeyFrame(Duration(250.0),
|
|
||||||
KeyValue(pane.opacityProperty(), 0.0),
|
|
||||||
KeyValue(slideProp, -pane.height - 50.0) // Subtract the bottom padding gap.
|
|
||||||
)
|
|
||||||
)
|
|
||||||
timeline.setOnFinished {
|
|
||||||
println("Removed: ${tracker}")
|
|
||||||
val vbox = viewModel.trackerBoxes.remove(tracker)
|
|
||||||
view.sidebar.children.remove(vbox)
|
|
||||||
}
|
|
||||||
timeline.play()
|
|
||||||
}
|
|
||||||
viewModel.doneTrackers.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindProgressTracketWidget(tracker: ProgressTracker, widget: TrackerWidget) {
|
|
||||||
val allSteps: List<Pair<Int, ProgressTracker.Step>> = tracker.allSteps
|
|
||||||
tracker.changes.observeOn(uiThread).subscribe { step: ProgressTracker.Change ->
|
|
||||||
val stepHeight = widget.cursorBox.height / allSteps.size
|
|
||||||
if (step is ProgressTracker.Change.Position) {
|
|
||||||
// Figure out the index of the new step.
|
|
||||||
val curStep = allSteps.indexOfFirst { it.second == step.newStep }
|
|
||||||
// Animate the cursor to the right place.
|
|
||||||
with(TranslateTransition(Duration(350.0), widget.cursor)) {
|
|
||||||
fromY = widget.cursor.translateY
|
|
||||||
toY = (curStep * stepHeight) + 22.5
|
|
||||||
play()
|
|
||||||
}
|
|
||||||
} else if (step is ProgressTracker.Change.Structural) {
|
|
||||||
val new = view.buildProgressTrackerWidget(widget.label.text, tracker)
|
|
||||||
val prevWidget = viewModel.trackerBoxes[step.tracker] ?: throw AssertionError("No previous widget for tracker: ${step.tracker}")
|
|
||||||
val i = (prevWidget.parent as VBox).children.indexOf(viewModel.trackerBoxes[step.tracker])
|
|
||||||
(prevWidget.parent as VBox).children[i] = new.vbox
|
|
||||||
viewModel.trackerBoxes[step.tracker] = new.vbox
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var started = false
|
|
||||||
private fun startSimulation() {
|
|
||||||
if (!started) {
|
|
||||||
viewModel.simulation.start()
|
|
||||||
started = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun reset() {
|
|
||||||
viewModel.simulation.stop()
|
|
||||||
viewModel.simulation = IRSSimulation(true, false, null)
|
|
||||||
started = false
|
|
||||||
start(view.stage)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun skipNetworkInitialisation() {
|
|
||||||
startSimulation()
|
|
||||||
while (!viewModel.simulation.networkInitialisationFinished.isDone) {
|
|
||||||
iterateSimulation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onNextInvoked() {
|
|
||||||
if (started) {
|
|
||||||
iterateSimulation()
|
|
||||||
} else {
|
|
||||||
startSimulation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun iterateSimulation() {
|
|
||||||
// Loop until either we ran out of things to do, or we sent an interesting message.
|
|
||||||
while (true) {
|
|
||||||
val transfer: InMemoryMessagingNetwork.MessageTransfer = viewModel.simulation.iterate() ?: break
|
|
||||||
if (transferIsInteresting(transfer))
|
|
||||||
break
|
|
||||||
else
|
|
||||||
System.err.println("skipping boring $transfer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun transferIsInteresting(transfer: InMemoryMessagingNetwork.MessageTransfer): Boolean {
|
|
||||||
// Loopback messages are boring.
|
|
||||||
if (transfer.sender.myAddress == transfer.recipients) return false
|
|
||||||
// Network map push acknowledgements are boring.
|
|
||||||
if (NetworkMapService.PUSH_ACK_PROTOCOL_TOPIC in transfer.message.topicSession.topic) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
Application.launch(NetworkMapVisualiser::class.java, *args)
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package com.r3cev.corda.netmap
|
|
||||||
|
|
||||||
import javafx.scene.paint.Color
|
|
||||||
|
|
||||||
internal
|
|
||||||
fun colorToRgb(color: Color): String {
|
|
||||||
val builder = StringBuilder()
|
|
||||||
|
|
||||||
builder.append("rgb(")
|
|
||||||
builder.append(Math.round(color.red * 256))
|
|
||||||
builder.append(",")
|
|
||||||
builder.append(Math.round(color.green * 256))
|
|
||||||
builder.append(",")
|
|
||||||
builder.append(Math.round(color.blue * 256))
|
|
||||||
builder.append(")")
|
|
||||||
|
|
||||||
return builder.toString()
|
|
||||||
}
|
|
@ -1,304 +0,0 @@
|
|||||||
package com.r3cev.corda.netmap
|
|
||||||
|
|
||||||
import com.r3corda.core.utilities.ProgressTracker
|
|
||||||
import javafx.animation.KeyFrame
|
|
||||||
import javafx.animation.Timeline
|
|
||||||
import javafx.application.Platform
|
|
||||||
import javafx.collections.FXCollections
|
|
||||||
import javafx.event.EventHandler
|
|
||||||
import javafx.geometry.Insets
|
|
||||||
import javafx.geometry.Pos
|
|
||||||
import javafx.scene.Group
|
|
||||||
import javafx.scene.Node
|
|
||||||
import javafx.scene.Scene
|
|
||||||
import javafx.scene.control.*
|
|
||||||
import javafx.scene.image.Image
|
|
||||||
import javafx.scene.image.ImageView
|
|
||||||
import javafx.scene.input.ZoomEvent
|
|
||||||
import javafx.scene.layout.*
|
|
||||||
import javafx.scene.paint.Color
|
|
||||||
import javafx.scene.shape.Polygon
|
|
||||||
import javafx.scene.text.Font
|
|
||||||
import javafx.stage.Stage
|
|
||||||
import javafx.util.Duration
|
|
||||||
import com.r3cev.corda.netmap.VisualiserViewModel.Style
|
|
||||||
|
|
||||||
data class TrackerWidget(val vbox: VBox, val cursorBox: Pane, val label: Label, val cursor: Polygon)
|
|
||||||
|
|
||||||
internal class VisualiserView() {
|
|
||||||
lateinit var root: Pane
|
|
||||||
lateinit var stage: Stage
|
|
||||||
lateinit var splitter: SplitPane
|
|
||||||
lateinit var sidebar: VBox
|
|
||||||
lateinit var resetButton: Button
|
|
||||||
lateinit var nextButton: Button
|
|
||||||
lateinit var runPauseButton: Button
|
|
||||||
lateinit var simulateInitialisationCheckbox: CheckBox
|
|
||||||
lateinit var styleChoice: ChoiceBox<Style>
|
|
||||||
|
|
||||||
var dateLabel = Label("")
|
|
||||||
var scrollPane: ScrollPane? = null
|
|
||||||
var hideButton = Button("«").apply { styleClass += "hide-sidebar-button" }
|
|
||||||
|
|
||||||
|
|
||||||
// -23.2031,29.8406,33.0469,64.3209
|
|
||||||
val mapImage = ImageView(Image(NetworkMapVisualiser::class.java.getResourceAsStream("Europe.jpg")))
|
|
||||||
|
|
||||||
val backgroundColor: Color = mapImage.image.pixelReader.getColor(0, 0)
|
|
||||||
|
|
||||||
val stageWidth = 1024.0
|
|
||||||
val stageHeight = 768.0
|
|
||||||
var defaultZoom = 0.7
|
|
||||||
|
|
||||||
val bitmapWidth = 1900.0
|
|
||||||
val bitmapHeight = 1900.0
|
|
||||||
|
|
||||||
fun setup(runningPausedState: NetworkMapVisualiser.RunningPausedState,
|
|
||||||
displayStyle: Style,
|
|
||||||
presentationMode: Boolean) {
|
|
||||||
NetworkMapVisualiser::class.java.getResourceAsStream("SourceSansPro-Regular.otf").use {
|
|
||||||
Font.loadFont(it, 120.0)
|
|
||||||
}
|
|
||||||
if(displayStyle == Style.MAP) {
|
|
||||||
mapImage.onZoom = EventHandler<javafx.scene.input.ZoomEvent> { event ->
|
|
||||||
event.consume()
|
|
||||||
mapImage.fitWidth = mapImage.fitWidth * event.zoomFactor
|
|
||||||
mapImage.fitHeight = mapImage.fitHeight * event.zoomFactor
|
|
||||||
//repositionNodes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scaleMap(displayStyle);
|
|
||||||
root = Pane(mapImage)
|
|
||||||
root.background = Background(BackgroundFill(backgroundColor, CornerRadii.EMPTY, Insets.EMPTY))
|
|
||||||
scrollPane = buildScrollPane(backgroundColor, displayStyle)
|
|
||||||
|
|
||||||
val vbox = makeTopBar(runningPausedState, displayStyle, presentationMode)
|
|
||||||
StackPane.setAlignment(vbox, Pos.TOP_CENTER)
|
|
||||||
|
|
||||||
// Now build the sidebar
|
|
||||||
val defaultSplitterPosition = 0.3
|
|
||||||
splitter = SplitPane(makeSidebar(), scrollPane)
|
|
||||||
splitter.styleClass += "splitter"
|
|
||||||
Platform.runLater {
|
|
||||||
splitter.dividers[0].position = defaultSplitterPosition
|
|
||||||
}
|
|
||||||
VBox.setVgrow(splitter, Priority.ALWAYS)
|
|
||||||
|
|
||||||
// And the left hide button.
|
|
||||||
hideButton = makeHideButton(defaultSplitterPosition)
|
|
||||||
|
|
||||||
val screenStack = VBox(vbox, StackPane(splitter, hideButton))
|
|
||||||
screenStack.styleClass += "root-pane"
|
|
||||||
stage.scene = Scene(screenStack, backgroundColor)
|
|
||||||
stage.width = 1024.0
|
|
||||||
stage.height = 768.0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buildScrollPane(backgroundColor: Color, displayStyle: Style): ScrollPane {
|
|
||||||
when (displayStyle) {
|
|
||||||
Style.MAP -> {
|
|
||||||
mapImage.fitWidth = bitmapWidth * defaultZoom
|
|
||||||
mapImage.fitHeight = bitmapHeight * defaultZoom
|
|
||||||
mapImage.onZoom = EventHandler<ZoomEvent> { event ->
|
|
||||||
event.consume()
|
|
||||||
mapImage.fitWidth = mapImage.fitWidth * event.zoomFactor
|
|
||||||
mapImage.fitHeight = mapImage.fitHeight * event.zoomFactor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Style.CIRCLE -> {
|
|
||||||
val scaleRatio = Math.min(stageWidth / bitmapWidth, stageHeight / bitmapHeight)
|
|
||||||
mapImage.fitWidth = bitmapWidth * scaleRatio
|
|
||||||
mapImage.fitHeight = bitmapHeight * scaleRatio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ScrollPane(Group(root)).apply {
|
|
||||||
when (displayStyle) {
|
|
||||||
Style.MAP -> {
|
|
||||||
hvalue = 0.4
|
|
||||||
vvalue = 0.7
|
|
||||||
}
|
|
||||||
Style.CIRCLE -> {
|
|
||||||
hvalue = 0.0
|
|
||||||
vvalue = 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
|
|
||||||
vbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
|
|
||||||
isPannable = true
|
|
||||||
isFocusTraversable = false
|
|
||||||
style = "-fx-background-color: " + colorToRgb(backgroundColor)
|
|
||||||
styleClass += "edge-to-edge"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun makeHideButton(defaultSplitterPosition: Double): Button {
|
|
||||||
var hideButtonToggled = false
|
|
||||||
hideButton.isFocusTraversable = false
|
|
||||||
hideButton.setOnAction {
|
|
||||||
if (!hideButtonToggled) {
|
|
||||||
hideButton.translateXProperty().unbind()
|
|
||||||
Timeline(
|
|
||||||
KeyFrame(Duration.millis(500.0),
|
|
||||||
splitter.dividers[0].positionProperty().keyValue(0.0),
|
|
||||||
hideButton.translateXProperty().keyValue(0.0),
|
|
||||||
hideButton.rotateProperty().keyValue(180.0)
|
|
||||||
)
|
|
||||||
).play()
|
|
||||||
} else {
|
|
||||||
bindHideButtonPosition()
|
|
||||||
Timeline(
|
|
||||||
KeyFrame(Duration.millis(500.0),
|
|
||||||
splitter.dividers[0].positionProperty().keyValue(defaultSplitterPosition),
|
|
||||||
hideButton.rotateProperty().keyValue(0.0)
|
|
||||||
)
|
|
||||||
).play()
|
|
||||||
}
|
|
||||||
hideButtonToggled = !hideButtonToggled
|
|
||||||
}
|
|
||||||
bindHideButtonPosition()
|
|
||||||
StackPane.setAlignment(hideButton, Pos.TOP_LEFT)
|
|
||||||
return hideButton
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bindHideButtonPosition() {
|
|
||||||
hideButton.translateXProperty().unbind()
|
|
||||||
hideButton.translateXProperty().bind(splitter.dividers[0].positionProperty().multiply(splitter.widthProperty()).subtract(hideButton.widthProperty()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun scaleMap(displayStyle: Style) {
|
|
||||||
when (displayStyle) {
|
|
||||||
Style.MAP -> {
|
|
||||||
mapImage.fitWidth = bitmapWidth * defaultZoom
|
|
||||||
mapImage.fitHeight = bitmapHeight * defaultZoom
|
|
||||||
}
|
|
||||||
Style.CIRCLE -> {
|
|
||||||
val scaleRatio = Math.min(stageWidth / bitmapWidth, stageHeight / bitmapHeight)
|
|
||||||
mapImage.fitWidth = bitmapWidth * scaleRatio
|
|
||||||
mapImage.fitHeight = bitmapHeight * scaleRatio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun makeSidebar(): Node {
|
|
||||||
sidebar = VBox()
|
|
||||||
sidebar.styleClass += "sidebar"
|
|
||||||
sidebar.isFillWidth = true
|
|
||||||
val sp = ScrollPane(sidebar)
|
|
||||||
sp.isFitToWidth = true
|
|
||||||
sp.isFitToHeight = true
|
|
||||||
sp.styleClass += "sidebar"
|
|
||||||
sp.hbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
|
|
||||||
sp.vbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
|
|
||||||
sp.minWidth = 0.0
|
|
||||||
return sp
|
|
||||||
}
|
|
||||||
|
|
||||||
fun makeTopBar(runningPausedState: NetworkMapVisualiser.RunningPausedState,
|
|
||||||
displayStyle: Style,
|
|
||||||
presentationMode: Boolean): VBox {
|
|
||||||
nextButton = Button("Next").apply {
|
|
||||||
styleClass += "button"
|
|
||||||
styleClass += "next-button"
|
|
||||||
}
|
|
||||||
runPauseButton = Button(runningPausedState.buttonLabel.toString()).apply {
|
|
||||||
styleClass += "button"
|
|
||||||
styleClass += "run-button"
|
|
||||||
}
|
|
||||||
simulateInitialisationCheckbox = CheckBox("Simulate initialisation")
|
|
||||||
resetButton = Button("Reset").apply {
|
|
||||||
styleClass += "button"
|
|
||||||
styleClass += "reset-button"
|
|
||||||
}
|
|
||||||
|
|
||||||
val displayStyles = FXCollections.observableArrayList<Style>()
|
|
||||||
Style.values().forEach { displayStyles.add(it) }
|
|
||||||
|
|
||||||
styleChoice = ChoiceBox(displayStyles).apply {
|
|
||||||
styleClass += "choice"
|
|
||||||
styleClass += "style-choice"
|
|
||||||
}
|
|
||||||
styleChoice.value = displayStyle
|
|
||||||
|
|
||||||
val dropShadow = Pane().apply { styleClass += "drop-shadow-pane-horizontal"; minHeight = 8.0 }
|
|
||||||
val logoImage = ImageView(javaClass.getResource("R3 logo.png").toExternalForm())
|
|
||||||
logoImage.fitHeight = 65.0
|
|
||||||
logoImage.isPreserveRatio = true
|
|
||||||
val logoLabel = HBox(logoImage, VBox(
|
|
||||||
Label("D I S T R I B U T E D L E D G E R G R O U P").apply { styleClass += "dlg-label" },
|
|
||||||
Label("Network Simulator").apply { styleClass += "logo-label" }
|
|
||||||
))
|
|
||||||
logoLabel.spacing = 10.0
|
|
||||||
HBox.setHgrow(logoLabel, Priority.ALWAYS)
|
|
||||||
logoLabel.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_PREF_SIZE)
|
|
||||||
dateLabel = Label("").apply { styleClass += "date-label" }
|
|
||||||
|
|
||||||
// Buttons area. In presentation mode there are no controls visible and you must use the keyboard.
|
|
||||||
val hbox = if (presentationMode) {
|
|
||||||
HBox(logoLabel, dateLabel).apply { styleClass += "controls-hbox" }
|
|
||||||
} else {
|
|
||||||
HBox(logoLabel, dateLabel, simulateInitialisationCheckbox, runPauseButton, nextButton, resetButton, styleChoice).apply { styleClass += "controls-hbox" }
|
|
||||||
}
|
|
||||||
hbox.styleClass += "fat-buttons"
|
|
||||||
hbox.spacing = 20.0
|
|
||||||
hbox.alignment = Pos.CENTER_RIGHT
|
|
||||||
hbox.padding = Insets(10.0, 20.0, 10.0, 20.0)
|
|
||||||
val vbox = VBox(hbox, dropShadow)
|
|
||||||
vbox.styleClass += "controls-vbox"
|
|
||||||
vbox.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE)
|
|
||||||
vbox.setMaxSize(Region.USE_COMPUTED_SIZE, Region.USE_PREF_SIZE)
|
|
||||||
return vbox
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Extract this to a real widget.
|
|
||||||
fun buildProgressTrackerWidget(label: String, tracker: ProgressTracker): TrackerWidget {
|
|
||||||
val allSteps: List<Pair<Int, ProgressTracker.Step>> = tracker.allSteps
|
|
||||||
val stepsBox = VBox().apply {
|
|
||||||
styleClass += "progress-tracker-widget-steps"
|
|
||||||
}
|
|
||||||
for ((indent, step) in allSteps) {
|
|
||||||
val stepLabel = Label(step.label).apply { padding = Insets(0.0, 0.0, 0.0, indent * 15.0) }
|
|
||||||
stepsBox.children += StackPane(stepLabel)
|
|
||||||
}
|
|
||||||
val arrowSize = 7.0
|
|
||||||
val cursor = Polygon(-arrowSize, -arrowSize, arrowSize, 0.0, -arrowSize, arrowSize).apply {
|
|
||||||
styleClass += "progress-tracker-cursor"
|
|
||||||
}
|
|
||||||
val cursorBox = Pane(cursor).apply {
|
|
||||||
styleClass += "progress-tracker-cursor-box"
|
|
||||||
minWidth = 25.0
|
|
||||||
}
|
|
||||||
val curStep = allSteps.indexOfFirst { it.second == tracker.currentStep }
|
|
||||||
Platform.runLater {
|
|
||||||
val stepHeight = cursorBox.height / allSteps.size
|
|
||||||
cursor.translateY = (curStep * stepHeight) + 20.0
|
|
||||||
}
|
|
||||||
val vbox: VBox?
|
|
||||||
HBox.setHgrow(stepsBox, Priority.ALWAYS)
|
|
||||||
val content = HBox(cursorBox, stepsBox)
|
|
||||||
// Make the title bar
|
|
||||||
val title = Label(label).apply { styleClass += "sidebar-title-label" }
|
|
||||||
StackPane.setAlignment(title, Pos.CENTER_LEFT)
|
|
||||||
vbox = VBox(StackPane(title), content)
|
|
||||||
vbox.padding = Insets(0.0, 0.0, 25.0, 0.0)
|
|
||||||
return TrackerWidget(vbox, cursorBox, title, cursor)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the current display style. MUST only be called on the UI
|
|
||||||
* thread.
|
|
||||||
*/
|
|
||||||
fun updateDisplayStyle(displayStyle: Style) {
|
|
||||||
requireNotNull(splitter)
|
|
||||||
splitter.items.remove(scrollPane!!)
|
|
||||||
scrollPane = buildScrollPane(backgroundColor, displayStyle)
|
|
||||||
splitter.items.add(scrollPane!!)
|
|
||||||
splitter.dividers[0].position = 0.3
|
|
||||||
mapImage.isVisible = when (displayStyle) {
|
|
||||||
Style.MAP -> true
|
|
||||||
Style.CIRCLE -> false
|
|
||||||
}
|
|
||||||
// TODO: Can any current bullets be re-routed in flight?
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,222 +0,0 @@
|
|||||||
package com.r3cev.corda.netmap
|
|
||||||
|
|
||||||
import com.r3corda.core.utilities.ProgressTracker
|
|
||||||
import com.r3corda.testing.node.MockNetwork
|
|
||||||
import com.r3corda.simulation.IRSSimulation
|
|
||||||
import javafx.animation.*
|
|
||||||
import javafx.geometry.Pos
|
|
||||||
import javafx.scene.control.Label
|
|
||||||
import javafx.scene.layout.Pane
|
|
||||||
import javafx.scene.layout.StackPane
|
|
||||||
import javafx.scene.shape.Circle
|
|
||||||
import javafx.scene.shape.Line
|
|
||||||
import javafx.util.Duration
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class VisualiserViewModel {
|
|
||||||
enum class Style {
|
|
||||||
MAP, CIRCLE;
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return name.toLowerCase().capitalize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class NodeWidget(val node: MockNetwork.MockNode, val innerDot: Circle, val outerDot: Circle, val longPulseDot: Circle,
|
|
||||||
val pulseAnim: Animation, val longPulseAnim: Animation,
|
|
||||||
val nameLabel: Label, val statusLabel: Label) {
|
|
||||||
fun position(index: Int, nodeCoords: (node: MockNetwork.MockNode, index: Int) -> Pair<Double, Double>) {
|
|
||||||
val (x, y) = nodeCoords(node, index)
|
|
||||||
innerDot.centerX = x
|
|
||||||
innerDot.centerY = y
|
|
||||||
outerDot.centerX = x
|
|
||||||
outerDot.centerY = y
|
|
||||||
longPulseDot.centerX = x
|
|
||||||
longPulseDot.centerY = y
|
|
||||||
(nameLabel.parent as StackPane).relocate(x - 270.0, y - 10.0)
|
|
||||||
(statusLabel.parent as StackPane).relocate(x + 20.0, y - 10.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal lateinit var view: VisualiserView
|
|
||||||
var presentationMode: Boolean = false
|
|
||||||
var simulation = IRSSimulation(true, false, null) // Manually pumped.
|
|
||||||
|
|
||||||
val trackerBoxes = HashMap<ProgressTracker, Pane>()
|
|
||||||
val doneTrackers = ArrayList<ProgressTracker>()
|
|
||||||
val nodesToWidgets = HashMap<MockNetwork.MockNode, NodeWidget>()
|
|
||||||
|
|
||||||
var bankCount: Int = 0
|
|
||||||
var serviceCount: Int = 0
|
|
||||||
|
|
||||||
var stepDuration = Duration.millis(500.0)
|
|
||||||
var runningPausedState: NetworkMapVisualiser.RunningPausedState = NetworkMapVisualiser.RunningPausedState.Paused()
|
|
||||||
|
|
||||||
var displayStyle: Style = Style.MAP
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
view.updateDisplayStyle(value)
|
|
||||||
repositionNodes()
|
|
||||||
view.bindHideButtonPosition()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun repositionNodes() {
|
|
||||||
for ((index, bank) in simulation.banks.withIndex()) {
|
|
||||||
nodesToWidgets[bank]!!.position(index, when (displayStyle) {
|
|
||||||
Style.MAP -> { node, index -> nodeMapCoords(node) }
|
|
||||||
Style.CIRCLE -> { node, index -> nodeCircleCoords(NetworkMapVisualiser.NodeType.BANK, index) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
for ((index, serviceProvider) in (simulation.serviceProviders + simulation.regulators).withIndex()) {
|
|
||||||
nodesToWidgets[serviceProvider]!!.position(index, when (displayStyle) {
|
|
||||||
Style.MAP -> { node, index -> nodeMapCoords(node) }
|
|
||||||
Style.CIRCLE -> { node, index -> nodeCircleCoords(NetworkMapVisualiser.NodeType.SERVICE, index) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun nodeMapCoords(node: MockNetwork.MockNode): Pair<Double, Double> {
|
|
||||||
// For an image of the whole world, we use:
|
|
||||||
// return node.place.coordinate.project(mapImage.fitWidth, mapImage.fitHeight, 85.0511, -85.0511, -180.0, 180.0)
|
|
||||||
|
|
||||||
// For Europe, our bounds are: (lng,lat)
|
|
||||||
// bottom left: -23.2031,29.8406
|
|
||||||
// top right: 33.0469,64.3209
|
|
||||||
try {
|
|
||||||
return node.place.coordinate.project(view.mapImage.fitWidth, view.mapImage.fitHeight, 64.3209, 29.8406, -23.2031, 33.0469)
|
|
||||||
} catch(e: Exception) {
|
|
||||||
throw Exception("Cannot project ${node.info.legalIdentity}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun nodeCircleCoords(type: NetworkMapVisualiser.NodeType, index: Int): Pair<Double, Double> {
|
|
||||||
val stepRad: Double = when(type) {
|
|
||||||
NetworkMapVisualiser.NodeType.BANK -> 2 * Math.PI / bankCount
|
|
||||||
NetworkMapVisualiser.NodeType.SERVICE -> (2 * Math.PI / serviceCount)
|
|
||||||
}
|
|
||||||
val tangentRad: Double = stepRad * index + when(type) {
|
|
||||||
NetworkMapVisualiser.NodeType.BANK -> 0.0
|
|
||||||
NetworkMapVisualiser.NodeType.SERVICE -> Math.PI / 2
|
|
||||||
}
|
|
||||||
val radius = when (type) {
|
|
||||||
NetworkMapVisualiser.NodeType.BANK -> Math.min(view.stageWidth, view.stageHeight) / 3.5
|
|
||||||
NetworkMapVisualiser.NodeType.SERVICE -> Math.min(view.stageWidth, view.stageHeight) / 8
|
|
||||||
}
|
|
||||||
val xOffset = -220
|
|
||||||
val yOffset = -80
|
|
||||||
val circleX = view.stageWidth / 2 + xOffset
|
|
||||||
val circleY = view.stageHeight / 2 + yOffset
|
|
||||||
val x: Double = radius * Math.cos(tangentRad) + circleX;
|
|
||||||
val y: Double = radius * Math.sin(tangentRad) + circleY;
|
|
||||||
return Pair(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createNodes() {
|
|
||||||
bankCount = simulation.banks.size
|
|
||||||
serviceCount = simulation.serviceProviders.size + simulation.regulators.size
|
|
||||||
for ((index, bank) in simulation.banks.withIndex()) {
|
|
||||||
nodesToWidgets[bank] = makeNodeWidget(bank, "bank", bank.configuration.myLegalName, NetworkMapVisualiser.NodeType.BANK, index)
|
|
||||||
}
|
|
||||||
for ((index, service) in simulation.serviceProviders.withIndex()) {
|
|
||||||
nodesToWidgets[service] = makeNodeWidget(service, "network-service", service.configuration.myLegalName, NetworkMapVisualiser.NodeType.SERVICE, index)
|
|
||||||
}
|
|
||||||
for ((index, service) in simulation.regulators.withIndex()) {
|
|
||||||
nodesToWidgets[service] = makeNodeWidget(service, "regulator", service.configuration.myLegalName, NetworkMapVisualiser.NodeType.SERVICE, index + simulation.serviceProviders.size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun makeNodeWidget(forNode: MockNetwork.MockNode, type: String, label: String = "Bank of Bologna",
|
|
||||||
nodeType: NetworkMapVisualiser.NodeType, index: Int): NodeWidget {
|
|
||||||
fun emitRadarPulse(initialRadius: Double, targetRadius: Double, duration: Double): Pair<Circle, Animation> {
|
|
||||||
val pulse = Circle(initialRadius).apply {
|
|
||||||
styleClass += "node-$type"
|
|
||||||
styleClass += "node-circle-pulse"
|
|
||||||
}
|
|
||||||
val animation = Timeline(
|
|
||||||
KeyFrame(Duration.seconds(0.0),
|
|
||||||
pulse.radiusProperty().keyValue(initialRadius),
|
|
||||||
pulse.opacityProperty().keyValue(1.0)
|
|
||||||
),
|
|
||||||
KeyFrame(Duration.seconds(duration),
|
|
||||||
pulse.radiusProperty().keyValue(targetRadius),
|
|
||||||
pulse.opacityProperty().keyValue(0.0)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return Pair(pulse, animation)
|
|
||||||
}
|
|
||||||
|
|
||||||
val innerDot = Circle(10.0).apply {
|
|
||||||
styleClass += "node-$type"
|
|
||||||
styleClass += "node-circle-inner"
|
|
||||||
}
|
|
||||||
val (outerDot, pulseAnim) = emitRadarPulse(10.0, 50.0, 0.45)
|
|
||||||
val (longPulseOuterDot, longPulseAnim) = emitRadarPulse(10.0, 100.0, 1.45)
|
|
||||||
view.root.children += outerDot
|
|
||||||
view.root.children += longPulseOuterDot
|
|
||||||
view.root.children += innerDot
|
|
||||||
|
|
||||||
val nameLabel = Label(label)
|
|
||||||
val nameLabelRect = StackPane(nameLabel).apply {
|
|
||||||
styleClass += "node-label"
|
|
||||||
alignment = Pos.CENTER_RIGHT
|
|
||||||
// This magic min width depends on the longest label of all nodes we may have, which we aren't calculating.
|
|
||||||
// TODO: Dynamically adjust it depending on the longest label to display.
|
|
||||||
minWidth = 250.0
|
|
||||||
}
|
|
||||||
view.root.children += nameLabelRect
|
|
||||||
|
|
||||||
val statusLabel = Label("")
|
|
||||||
val statusLabelRect = StackPane(statusLabel).apply { styleClass += "node-status-label" }
|
|
||||||
view.root.children += statusLabelRect
|
|
||||||
|
|
||||||
val widget = NodeWidget(forNode, innerDot, outerDot, longPulseOuterDot, pulseAnim, longPulseAnim, nameLabel, statusLabel)
|
|
||||||
when (displayStyle) {
|
|
||||||
Style.CIRCLE -> widget.position(index, { node, index -> nodeCircleCoords(nodeType, index) } )
|
|
||||||
Style.MAP -> widget.position(index, { node, index -> nodeMapCoords(node) })
|
|
||||||
}
|
|
||||||
return widget
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fireBulletBetweenNodes(senderNode: MockNetwork.MockNode, destNode: MockNetwork.MockNode, startType: String, endType: String) {
|
|
||||||
val sx = nodesToWidgets[senderNode]!!.innerDot.centerX
|
|
||||||
val sy = nodesToWidgets[senderNode]!!.innerDot.centerY
|
|
||||||
val dx = nodesToWidgets[destNode]!!.innerDot.centerX
|
|
||||||
val dy = nodesToWidgets[destNode]!!.innerDot.centerY
|
|
||||||
|
|
||||||
val bullet = Circle(3.0)
|
|
||||||
bullet.styleClass += "bullet"
|
|
||||||
bullet.styleClass += "connection-$startType-to-$endType"
|
|
||||||
with(TranslateTransition(stepDuration, bullet)) {
|
|
||||||
fromX = sx
|
|
||||||
fromY = sy
|
|
||||||
toX = dx
|
|
||||||
toY = dy
|
|
||||||
setOnFinished {
|
|
||||||
// For some reason removing/adding the bullet nodes causes an annoying 1px shift in the map view, so
|
|
||||||
// to avoid visual distraction we just deliberately leak the bullet node here. Obviously this is a
|
|
||||||
// memory leak that would break long term usage.
|
|
||||||
//
|
|
||||||
// TODO: Find root cause and fix.
|
|
||||||
//
|
|
||||||
// root.children.remove(bullet)
|
|
||||||
bullet.isVisible = false
|
|
||||||
}
|
|
||||||
play()
|
|
||||||
}
|
|
||||||
|
|
||||||
val line = Line(sx, sy, dx, dy).apply { styleClass += "message-line" }
|
|
||||||
// Fade in quick, then fade out slow.
|
|
||||||
with(FadeTransition(stepDuration.divide(5.0), line)) {
|
|
||||||
fromValue = 0.0
|
|
||||||
toValue = 1.0
|
|
||||||
play()
|
|
||||||
setOnFinished {
|
|
||||||
with(FadeTransition(stepDuration.multiply(6.0), line)) { fromValue = 1.0; toValue = 0.0; play() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view.root.children.add(1, line)
|
|
||||||
view.root.children.add(bullet)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 302 KiB |
@ -1,29 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
|
|
||||||
<?import javafx.geometry.Insets?>
|
|
||||||
<?import javafx.scene.control.Button?>
|
|
||||||
<?import javafx.scene.control.ScrollPane?>
|
|
||||||
<?import javafx.scene.control.SplitPane?>
|
|
||||||
<?import javafx.scene.layout.AnchorPane?>
|
|
||||||
<?import javafx.scene.layout.HBox?>
|
|
||||||
<?import javafx.scene.layout.VBox?>
|
|
||||||
|
|
||||||
|
|
||||||
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1">
|
|
||||||
<children>
|
|
||||||
<HBox alignment="CENTER_RIGHT" prefHeight="0.0" prefWidth="600.0">
|
|
||||||
<children>
|
|
||||||
<Button fx:id="nextButton" mnemonicParsing="false" onAction="#onNextClicked" text="Next" />
|
|
||||||
</children>
|
|
||||||
<padding>
|
|
||||||
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
|
|
||||||
</padding>
|
|
||||||
</HBox>
|
|
||||||
<SplitPane dividerPositions="0.2408026755852843" prefHeight="336.0" prefWidth="600.0" VBox.vgrow="ALWAYS">
|
|
||||||
<items>
|
|
||||||
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0" />
|
|
||||||
<ScrollPane fx:id="mapView" hbarPolicy="NEVER" pannable="true" prefHeight="200.0" prefWidth="200.0" vbarPolicy="NEVER" />
|
|
||||||
</items>
|
|
||||||
</SplitPane>
|
|
||||||
</children>
|
|
||||||
</VBox>
|
|
Binary file not shown.
Before Width: | Height: | Size: 23 KiB |
@ -1,43 +0,0 @@
|
|||||||
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
|
|
||||||
|
|
||||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
||||||
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
|
||||||
|
|
||||||
-----------------------------------------------------------
|
|
||||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
||||||
-----------------------------------------------------------
|
|
||||||
|
|
||||||
PREAMBLE
|
|
||||||
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
|
|
||||||
|
|
||||||
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
|
|
||||||
|
|
||||||
DEFINITIONS
|
|
||||||
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
|
|
||||||
|
|
||||||
"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
|
|
||||||
|
|
||||||
"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
|
|
||||||
|
|
||||||
"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
|
|
||||||
|
|
||||||
"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
|
|
||||||
|
|
||||||
PERMISSION & CONDITIONS
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
|
|
||||||
|
|
||||||
1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
|
|
||||||
|
|
||||||
2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
|
|
||||||
|
|
||||||
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
|
|
||||||
|
|
||||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
|
|
||||||
|
|
||||||
5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
|
|
||||||
|
|
||||||
TERMINATION
|
|
||||||
This license becomes null and void if any of the above conditions are not met.
|
|
||||||
|
|
||||||
DISCLAIMER
|
|
||||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 342 KiB |
Binary file not shown.
Before Width: | Height: | Size: 469 KiB |
@ -1,227 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
|
||||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
|
||||||
* set forth therein.
|
|
||||||
*
|
|
||||||
* All other rights reserved.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.root-pane {
|
|
||||||
-fx-font-family: "Source Sans Pro", sans-serif;
|
|
||||||
-fx-font-size: 12pt;
|
|
||||||
}
|
|
||||||
.node-bank {
|
|
||||||
-base-fill: #9e7bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-regulator {
|
|
||||||
-base-fill: #fff2d1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-network-service {
|
|
||||||
-base-fill: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-circle-inner {
|
|
||||||
-fx-fill: -base-fill;
|
|
||||||
-fx-stroke: derive(-base-fill, -40%);
|
|
||||||
-fx-stroke-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-circle-pulse {
|
|
||||||
-fx-fill: radial-gradient(center 50% 50%, radius 50%, #ffffff00, derive(-base-fill, 50%));
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide-sidebar-button {
|
|
||||||
-fx-background-color: linear-gradient(to left, #464646, derive(#1c1c1c, 10%));
|
|
||||||
-fx-min-width: 0;
|
|
||||||
-fx-text-fill: #ffffffaa;
|
|
||||||
-fx-alignment: top-right;
|
|
||||||
-fx-label-padding: 0;
|
|
||||||
-fx-padding: 0 10 0 10;
|
|
||||||
-fx-border-color: #00000066;
|
|
||||||
-fx-border-width: 1 0 1 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bullet {
|
|
||||||
-fx-fill: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-bank-to-bank {
|
|
||||||
-fx-fill: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-line {
|
|
||||||
-fx-stroke: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-bank-to-regulator {
|
|
||||||
-fx-stroke: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-label > Label, .node-status-label > Label {
|
|
||||||
-fx-text-fill: white;
|
|
||||||
-fx-effect: dropshadow(gaussian, black, 10, 0.1, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hack around the Modena theme that makes all scroll panes grey by default */
|
|
||||||
.scroll-pane > .viewport {
|
|
||||||
-fx-background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-pane .scroll-bar {
|
|
||||||
-fx-background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flat-button {
|
|
||||||
-fx-background-color: white;
|
|
||||||
-fx-padding: 0 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flat-button:hover {
|
|
||||||
-fx-underline: true;
|
|
||||||
-fx-cursor: hand;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flat-button:focused {
|
|
||||||
-fx-font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fat-buttons Button {
|
|
||||||
-fx-padding: 10 15 10 15;
|
|
||||||
-fx-min-width: 100;
|
|
||||||
-fx-font-weight: bold;
|
|
||||||
-fx-base: whitesmoke;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fat-buttons ChoiceBox {
|
|
||||||
-fx-padding: 4 8 4 8;
|
|
||||||
-fx-min-width: 100;
|
|
||||||
-fx-font-weight: bold;
|
|
||||||
-fx-base: whitesmoke;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fat-buttons Button:default {
|
|
||||||
-fx-base: orange;
|
|
||||||
-fx-text-fill: white;
|
|
||||||
-fx-font-family: 'Source Sans Pro', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fat-buttons Button:cancel {
|
|
||||||
-fx-background-color: white;
|
|
||||||
-fx-background-insets: 1;
|
|
||||||
-fx-border-color: lightgray;
|
|
||||||
-fx-border-radius: 3;
|
|
||||||
-fx-text-fill: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fat-buttons Button:cancel:hover {
|
|
||||||
-fx-base: white;
|
|
||||||
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
|
||||||
-fx-text-fill: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** take out the focus ring */
|
|
||||||
.no-focus-button:focused {
|
|
||||||
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
|
||||||
-fx-background-insets: 0 0 -1 0, 0, 1, 2;
|
|
||||||
-fx-background-radius: 3px, 3px, 2px, 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blue-button {
|
|
||||||
-fx-base: lightblue;
|
|
||||||
-fx-text-fill: darkslategrey;
|
|
||||||
}
|
|
||||||
.blue-button:disabled {
|
|
||||||
-fx-text-fill: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.green-button {
|
|
||||||
-fx-base: #62c462;
|
|
||||||
-fx-text-fill: darkslategrey;
|
|
||||||
}
|
|
||||||
.green-button:disabled {
|
|
||||||
-fx-text-fill: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.next-button {
|
|
||||||
-fx-base: #66b2ff;
|
|
||||||
-fx-text-fill: white;
|
|
||||||
|
|
||||||
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
|
||||||
-fx-background-insets: 0 0 -1 0, 0, 1, 2;
|
|
||||||
-fx-background-radius: 3px, 3px, 2px, 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.style-choice:default {
|
|
||||||
-fx-base: #66b2ff;
|
|
||||||
-fx-text-fill: white;
|
|
||||||
|
|
||||||
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
|
||||||
-fx-background-insets: 0 0 -1 0, 0, 1, 2;
|
|
||||||
-fx-background-radius: 3px, 3px, 2px, 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls-hbox {
|
|
||||||
-fx-background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drop-shadow-pane-horizontal {
|
|
||||||
/*-fx-background-color: linear-gradient(to top, #888, #fff);*/
|
|
||||||
-fx-background-color: white;
|
|
||||||
-fx-border-color: #555;
|
|
||||||
-fx-border-width: 0 0 1 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-label {
|
|
||||||
-fx-font-size: 40;
|
|
||||||
-fx-text-fill: slategray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date-label {
|
|
||||||
-fx-font-size: 30;
|
|
||||||
}
|
|
||||||
.splitter {
|
|
||||||
-fx-padding: 0;
|
|
||||||
-fx-background-color: #464646;
|
|
||||||
}
|
|
||||||
.splitter > .split-pane-divider {
|
|
||||||
-fx-background-color: linear-gradient(to left, #1c1c1c, transparent);
|
|
||||||
-fx-border-color: black;
|
|
||||||
-fx-border-width: 0 1 0 0;
|
|
||||||
-fx-padding: 0 2 0 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-tracker-cursor-box {
|
|
||||||
-fx-padding: 0 15 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-tracker-cursor {
|
|
||||||
-fx-translate-x: 15.0;
|
|
||||||
-fx-fill: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
-fx-background-color: #464646;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar > VBox > StackPane {
|
|
||||||
-fx-background-color: #666666;
|
|
||||||
-fx-padding: 5px;
|
|
||||||
}
|
|
||||||
.sidebar > VBox > StackPane > Label {
|
|
||||||
-fx-text-fill: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-tracker-widget-steps {
|
|
||||||
-fx-spacing: 5;
|
|
||||||
-fx-fill-width: true;
|
|
||||||
}
|
|
||||||
.progress-tracker-widget-steps > StackPane {
|
|
||||||
-fx-background-color: #5a5a5a;
|
|
||||||
-fx-padding: 7px;
|
|
||||||
-fx-alignment: center-left;
|
|
||||||
}
|
|
||||||
.progress-tracker-widget-steps > StackPane > Label {
|
|
||||||
-fx-text-fill: white;
|
|
||||||
}
|
|
@ -6,7 +6,6 @@ include 'node'
|
|||||||
include 'client'
|
include 'client'
|
||||||
include 'experimental'
|
include 'experimental'
|
||||||
include 'test-utils'
|
include 'test-utils'
|
||||||
include 'network-simulator'
|
|
||||||
include 'explorer'
|
include 'explorer'
|
||||||
include 'gradle-plugins:quasar-utils'
|
include 'gradle-plugins:quasar-utils'
|
||||||
include 'gradle-plugins:publish-utils'
|
include 'gradle-plugins:publish-utils'
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
package com.r3corda.demos
|
|
||||||
|
|
||||||
import com.r3corda.core.serialization.SerializeAsToken
|
|
||||||
import com.r3corda.core.serialization.SerializeAsTokenContext
|
|
||||||
import com.r3corda.core.serialization.SingletonSerializationToken
|
|
||||||
import com.r3corda.node.utilities.MutableClock
|
|
||||||
import java.time.*
|
|
||||||
import javax.annotation.concurrent.ThreadSafe
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [Clock] that can have the date advanced for use in demos.
|
|
||||||
*/
|
|
||||||
@ThreadSafe
|
|
||||||
class DemoClock(private var delegateClock: Clock = Clock.systemUTC()) : MutableClock(), SerializeAsToken {
|
|
||||||
|
|
||||||
private val token = SingletonSerializationToken(this)
|
|
||||||
|
|
||||||
override fun toToken(context: SerializeAsTokenContext) = SingletonSerializationToken.registerWithContext(token, this, context)
|
|
||||||
|
|
||||||
@Synchronized fun updateDate(date: LocalDate): Boolean {
|
|
||||||
val currentDate = LocalDate.now(this)
|
|
||||||
if (currentDate.isBefore(date)) {
|
|
||||||
// It's ok to increment
|
|
||||||
delegateClock = Clock.offset(delegateClock, Duration.between(currentDate.atStartOfDay(), date.atStartOfDay()))
|
|
||||||
notifyMutationObservers()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized override fun instant(): Instant {
|
|
||||||
return delegateClock.instant()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not use this. Instead seek to use ZonedDateTime methods.
|
|
||||||
override fun withZone(zone: ZoneId): Clock {
|
|
||||||
throw UnsupportedOperationException("Tokenized clock does not support withZone()")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized override fun getZone(): ZoneId {
|
|
||||||
return delegateClock.zone
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,499 +0,0 @@
|
|||||||
package com.r3corda.demos
|
|
||||||
|
|
||||||
import com.google.common.net.HostAndPort
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import com.google.common.util.concurrent.SettableFuture
|
|
||||||
import com.r3corda.contracts.InterestRateSwap
|
|
||||||
import com.r3corda.core.crypto.Party
|
|
||||||
import com.r3corda.core.logElapsedTime
|
|
||||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
|
||||||
import com.r3corda.core.node.CordaPluginRegistry
|
|
||||||
import com.r3corda.core.node.services.ServiceInfo
|
|
||||||
import com.r3corda.core.serialization.deserialize
|
|
||||||
import com.r3corda.core.utilities.LogHelper
|
|
||||||
import com.r3corda.demos.api.InterestRateSwapAPI
|
|
||||||
import com.r3corda.demos.api.NodeInterestRates
|
|
||||||
import com.r3corda.demos.protocols.AutoOfferProtocol
|
|
||||||
import com.r3corda.demos.protocols.ExitServerProtocol
|
|
||||||
import com.r3corda.demos.protocols.UpdateBusinessDayProtocol
|
|
||||||
import com.r3corda.demos.utilities.postJson
|
|
||||||
import com.r3corda.demos.utilities.putJson
|
|
||||||
import com.r3corda.demos.utilities.uploadFile
|
|
||||||
import com.r3corda.node.internal.AbstractNode
|
|
||||||
import com.r3corda.node.internal.Node
|
|
||||||
import com.r3corda.node.services.config.ConfigHelper
|
|
||||||
import com.r3corda.node.services.config.FullNodeConfiguration
|
|
||||||
import com.r3corda.node.services.config.NodeConfiguration
|
|
||||||
import com.r3corda.node.services.messaging.NodeMessagingClient
|
|
||||||
import com.r3corda.node.services.network.NetworkMapService
|
|
||||||
import com.r3corda.node.services.transactions.SimpleNotaryService
|
|
||||||
import com.r3corda.node.services.transactions.ValidatingNotaryService
|
|
||||||
import com.r3corda.testing.node.MockNetwork
|
|
||||||
import joptsimple.OptionParser
|
|
||||||
import joptsimple.OptionSet
|
|
||||||
import org.apache.commons.io.IOUtils
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.net.URL
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.concurrent.fixedRateTimer
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
// IRS DEMO
|
|
||||||
//
|
|
||||||
// Please see docs/build/html/running-the-trading-demo.html
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Roles. There are 4 modes this demo can be run:
|
|
||||||
* - SetupNodeA/SetupNodeB: Creates and sets up the necessary directories for nodes
|
|
||||||
* - NodeA/NodeB: Starts the nodes themselves
|
|
||||||
* - Trade: Uploads an example trade
|
|
||||||
* - DateChange: Changes the demo's date
|
|
||||||
*/
|
|
||||||
enum class IRSDemoRole {
|
|
||||||
SetupNodeA,
|
|
||||||
SetupNodeB,
|
|
||||||
NodeA,
|
|
||||||
NodeB,
|
|
||||||
Trade,
|
|
||||||
Date,
|
|
||||||
Rates
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parsed command line parameters.
|
|
||||||
*/
|
|
||||||
sealed class CliParams {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Corresponds to roles 'SetupNodeA' and 'SetupNodeB'.
|
|
||||||
*/
|
|
||||||
class SetupNode(
|
|
||||||
val node: IRSDemoNode,
|
|
||||||
val dir: Path,
|
|
||||||
val defaultLegalName: String
|
|
||||||
) : CliParams()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Corresponds to roles 'NodeA' and 'NodeB'.
|
|
||||||
*/
|
|
||||||
class RunNode(
|
|
||||||
val node: IRSDemoNode,
|
|
||||||
val dir: Path,
|
|
||||||
val networkAddress: HostAndPort,
|
|
||||||
val apiAddress: HostAndPort,
|
|
||||||
val mapAddress: String,
|
|
||||||
val tradeWithIdentities: List<Path>,
|
|
||||||
val uploadRates: Boolean,
|
|
||||||
val defaultLegalName: String,
|
|
||||||
val autoSetup: Boolean, // Run Setup for both nodes automatically with default arguments
|
|
||||||
val h2Port: Int
|
|
||||||
) : CliParams()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Corresponds to role 'Trade'.
|
|
||||||
*/
|
|
||||||
class Trade(
|
|
||||||
val apiAddress: HostAndPort,
|
|
||||||
val tradeId: String
|
|
||||||
) : CliParams()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Corresponds to role 'Date'.
|
|
||||||
*/
|
|
||||||
class DateChange(
|
|
||||||
val apiAddress: HostAndPort,
|
|
||||||
val dateString: String
|
|
||||||
) : CliParams()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Corresponds to role 'Rates'.
|
|
||||||
*/
|
|
||||||
class UploadRates(
|
|
||||||
val apiAddress: HostAndPort
|
|
||||||
) : CliParams()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Corresponds to --help.
|
|
||||||
*/
|
|
||||||
object Help : CliParams()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
val defaultBaseDirectory = "./build/irs-demo"
|
|
||||||
|
|
||||||
fun legalName(node: IRSDemoNode) =
|
|
||||||
when (node) {
|
|
||||||
IRSDemoNode.NodeA -> "Bank A"
|
|
||||||
IRSDemoNode.NodeB -> "Bank B"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun nodeDirectory(options: OptionSet, node: IRSDemoNode) =
|
|
||||||
Paths.get(options.valueOf(CliParamsSpec.baseDirectoryArg), node.name.decapitalize())
|
|
||||||
|
|
||||||
private fun parseSetupNode(options: OptionSet, node: IRSDemoNode): SetupNode {
|
|
||||||
return SetupNode(
|
|
||||||
node = node,
|
|
||||||
dir = nodeDirectory(options, node),
|
|
||||||
defaultLegalName = legalName(node)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun defaultNetworkPort(node: IRSDemoNode) =
|
|
||||||
when (node) {
|
|
||||||
IRSDemoNode.NodeA -> Node.DEFAULT_PORT
|
|
||||||
IRSDemoNode.NodeB -> Node.DEFAULT_PORT + 2
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun defaultApiPort(node: IRSDemoNode) =
|
|
||||||
when (node) {
|
|
||||||
IRSDemoNode.NodeA -> Node.DEFAULT_PORT + 1
|
|
||||||
IRSDemoNode.NodeB -> Node.DEFAULT_PORT + 3
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun defaultH2Port(node: IRSDemoNode) =
|
|
||||||
when (node) {
|
|
||||||
IRSDemoNode.NodeA -> Node.DEFAULT_PORT + 4
|
|
||||||
IRSDemoNode.NodeB -> Node.DEFAULT_PORT + 5
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseRunNode(options: OptionSet, node: IRSDemoNode): RunNode {
|
|
||||||
val dir = nodeDirectory(options, node)
|
|
||||||
|
|
||||||
return RunNode(
|
|
||||||
node = node,
|
|
||||||
dir = dir,
|
|
||||||
networkAddress = HostAndPort.fromString(options.valueOf(
|
|
||||||
CliParamsSpec.networkAddressArg.defaultsTo("localhost:${defaultNetworkPort(node)}")
|
|
||||||
)),
|
|
||||||
apiAddress = HostAndPort.fromString(options.valueOf(
|
|
||||||
CliParamsSpec.apiAddressArg.defaultsTo("localhost:${defaultApiPort(node)}")
|
|
||||||
)),
|
|
||||||
mapAddress = options.valueOf(CliParamsSpec.networkMapNetAddr),
|
|
||||||
tradeWithIdentities = if (options.has(CliParamsSpec.fakeTradeWithIdentityFile)) {
|
|
||||||
options.valuesOf(CliParamsSpec.fakeTradeWithIdentityFile).map { Paths.get(it) }
|
|
||||||
} else {
|
|
||||||
listOf(nodeDirectory(options, node.other).resolve(AbstractNode.PUBLIC_IDENTITY_FILE_NAME))
|
|
||||||
},
|
|
||||||
uploadRates = node == IRSDemoNode.NodeB,
|
|
||||||
defaultLegalName = legalName(node),
|
|
||||||
autoSetup = !options.has(CliParamsSpec.baseDirectoryArg) && !options.has(CliParamsSpec.fakeTradeWithIdentityFile),
|
|
||||||
h2Port = options.valueOf(CliParamsSpec.h2PortArg.defaultsTo(defaultH2Port(node)))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseTrade(options: OptionSet): Trade {
|
|
||||||
return Trade(
|
|
||||||
apiAddress = HostAndPort.fromString(options.valueOf(
|
|
||||||
CliParamsSpec.apiAddressArg.defaultsTo("localhost:${defaultApiPort(IRSDemoNode.NodeA)}")
|
|
||||||
)),
|
|
||||||
tradeId = options.valuesOf(CliParamsSpec.nonOptions).let {
|
|
||||||
if (it.size > 0) {
|
|
||||||
it[0]
|
|
||||||
} else {
|
|
||||||
throw IllegalArgumentException("Please provide a trade ID")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseDateChange(options: OptionSet): DateChange {
|
|
||||||
return DateChange(
|
|
||||||
apiAddress = HostAndPort.fromString(options.valueOf(
|
|
||||||
CliParamsSpec.apiAddressArg.defaultsTo("localhost:${defaultApiPort(IRSDemoNode.NodeA)}")
|
|
||||||
)),
|
|
||||||
dateString = options.valuesOf(CliParamsSpec.nonOptions).let {
|
|
||||||
if (it.size > 0) {
|
|
||||||
it[0]
|
|
||||||
} else {
|
|
||||||
throw IllegalArgumentException("Please provide a date string")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseRatesUpload(options: OptionSet): UploadRates {
|
|
||||||
return UploadRates(
|
|
||||||
apiAddress = HostAndPort.fromString(options.valueOf(
|
|
||||||
CliParamsSpec.apiAddressArg.defaultsTo("localhost:${defaultApiPort(IRSDemoNode.NodeB)}")
|
|
||||||
))
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun parse(options: OptionSet): CliParams {
|
|
||||||
if (options.has(CliParamsSpec.help)) {
|
|
||||||
return Help
|
|
||||||
}
|
|
||||||
val role: IRSDemoRole = options.valueOf(CliParamsSpec.roleArg) ?: throw IllegalArgumentException("Please provide a role")
|
|
||||||
return when (role) {
|
|
||||||
IRSDemoRole.SetupNodeA -> parseSetupNode(options, IRSDemoNode.NodeA)
|
|
||||||
IRSDemoRole.SetupNodeB -> parseSetupNode(options, IRSDemoNode.NodeB)
|
|
||||||
IRSDemoRole.NodeA -> parseRunNode(options, IRSDemoNode.NodeA)
|
|
||||||
IRSDemoRole.NodeB -> parseRunNode(options, IRSDemoNode.NodeB)
|
|
||||||
IRSDemoRole.Trade -> parseTrade(options)
|
|
||||||
IRSDemoRole.Date -> parseDateChange(options)
|
|
||||||
IRSDemoRole.Rates -> parseRatesUpload(options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class IRSDemoNode {
|
|
||||||
NodeA,
|
|
||||||
NodeB;
|
|
||||||
|
|
||||||
val other: IRSDemoNode get() {
|
|
||||||
return when (this) {
|
|
||||||
NodeA -> NodeB
|
|
||||||
NodeB -> NodeA
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object CliParamsSpec {
|
|
||||||
val parser = OptionParser()
|
|
||||||
val roleArg = parser.accepts("role")
|
|
||||||
.withRequiredArg().ofType(IRSDemoRole::class.java)
|
|
||||||
val networkAddressArg =
|
|
||||||
parser.accepts("network-address", "The p2p networking address to use")
|
|
||||||
.withOptionalArg().ofType(String::class.java)
|
|
||||||
val apiAddressArg =
|
|
||||||
parser.accepts("api-address", "The address to expose the HTTP API on")
|
|
||||||
.withOptionalArg().ofType(String::class.java)
|
|
||||||
val baseDirectoryArg =
|
|
||||||
parser.accepts("base-directory", "The directory to put all files under")
|
|
||||||
.withOptionalArg().defaultsTo(CliParams.defaultBaseDirectory)
|
|
||||||
val networkMapNetAddr =
|
|
||||||
parser.accepts("network-map-address", "The address of the network map")
|
|
||||||
.withRequiredArg().defaultsTo("localhost")
|
|
||||||
val fakeTradeWithIdentityFile =
|
|
||||||
parser.accepts("fake-trade-with-identity-file", "Extra identities to be registered with the identity service")
|
|
||||||
.withOptionalArg()
|
|
||||||
val h2PortArg = parser.accepts("h2-port").withRequiredArg().ofType(Int::class.java)
|
|
||||||
val nonOptions = parser.nonOptions()
|
|
||||||
val help = parser.accepts("help", "Prints this help").forHelp()
|
|
||||||
}
|
|
||||||
|
|
||||||
class IRSDemoPluginRegistry : CordaPluginRegistry() {
|
|
||||||
override val webApis: List<Class<*>> = listOf(InterestRateSwapAPI::class.java)
|
|
||||||
override val staticServeDirs: Map<String, String> = mapOf("irsdemo" to javaClass.getResource("irswebdemo").toExternalForm())
|
|
||||||
override val requiredProtocols: Map<String, Set<String>> = mapOf(
|
|
||||||
Pair(AutoOfferProtocol.Requester::class.java.name, setOf(InterestRateSwap.State::class.java.name)),
|
|
||||||
Pair(UpdateBusinessDayProtocol.Broadcast::class.java.name, setOf(java.time.LocalDate::class.java.name)),
|
|
||||||
Pair(ExitServerProtocol.Broadcast::class.java.name, setOf(kotlin.Int::class.java.name)))
|
|
||||||
}
|
|
||||||
|
|
||||||
private class NotSetupException: Throwable {
|
|
||||||
constructor(message: String): super(message) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val log: Logger = LoggerFactory.getLogger("IRSDemo")
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
exitProcess(runIRSDemo(args))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun runIRSDemo(args: Array<String>): Int {
|
|
||||||
val cliParams = try {
|
|
||||||
CliParams.parse(CliParamsSpec.parser.parse(*args))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error(e.message)
|
|
||||||
printHelp(CliParamsSpec.parser)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suppress the Artemis MQ noise, and activate the demo logging
|
|
||||||
LogHelper.setLevel("+IRSDemo", "+api-call", "+platform.deal", "-org.apache.activemq")
|
|
||||||
|
|
||||||
return when (cliParams) {
|
|
||||||
is CliParams.SetupNode -> setup(cliParams)
|
|
||||||
is CliParams.RunNode -> runNode(cliParams)
|
|
||||||
is CliParams.Trade -> runTrade(cliParams)
|
|
||||||
is CliParams.DateChange -> runDateChange(cliParams)
|
|
||||||
is CliParams.UploadRates -> runUploadRates(cliParams)
|
|
||||||
is CliParams.Help -> {
|
|
||||||
printHelp(CliParamsSpec.parser)
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setup(params: CliParams.SetupNode): Int {
|
|
||||||
val dirFile = params.dir.toFile()
|
|
||||||
if (!dirFile.exists()) {
|
|
||||||
dirFile.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
val configFile = params.dir.resolve("config")
|
|
||||||
val config = loadConfigFile(params.dir, configFile, emptyMap(), params.defaultLegalName)
|
|
||||||
if (!Files.exists(params.dir.resolve(AbstractNode.PUBLIC_IDENTITY_FILE_NAME))) {
|
|
||||||
createIdentities(config)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun defaultNodeSetupParams(node: IRSDemoNode): CliParams.SetupNode =
|
|
||||||
CliParams.SetupNode(
|
|
||||||
node = node,
|
|
||||||
dir = Paths.get(CliParams.defaultBaseDirectory, node.name.decapitalize()),
|
|
||||||
defaultLegalName = CliParams.legalName(node)
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun runNode(cliParams: CliParams.RunNode): Int {
|
|
||||||
if (cliParams.autoSetup) {
|
|
||||||
setup(defaultNodeSetupParams(IRSDemoNode.NodeA))
|
|
||||||
setup(defaultNodeSetupParams(IRSDemoNode.NodeB))
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val networkMap = createRecipient(cliParams.mapAddress)
|
|
||||||
|
|
||||||
val node = startNode(cliParams, networkMap)
|
|
||||||
|
|
||||||
if (cliParams.uploadRates) {
|
|
||||||
runUploadRates(cliParams.apiAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
node.run()
|
|
||||||
} catch (e: NotSetupException) {
|
|
||||||
log.error(e.message)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runDateChange(cliParams: CliParams.DateChange): Int {
|
|
||||||
log.info("Changing date to " + cliParams.dateString)
|
|
||||||
val url = URL("http://${cliParams.apiAddress}/api/irs/demodate")
|
|
||||||
if (putJson(url, "\"" + cliParams.dateString + "\"")) {
|
|
||||||
log.info("Date changed")
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
log.error("Date failed to change")
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runTrade(cliParams: CliParams.Trade): Int {
|
|
||||||
log.info("Uploading tradeID " + cliParams.tradeId)
|
|
||||||
// Note: the getResourceAsStream is an ugly hack to get the jvm to search in the right location
|
|
||||||
val fileContents = IOUtils.toString(CliParams::class.java.getResourceAsStream("example-irs-trade.json"))
|
|
||||||
val tradeFile = fileContents.replace("tradeXXX", cliParams.tradeId)
|
|
||||||
val url = URL("http://${cliParams.apiAddress}/api/irs/deals")
|
|
||||||
if (postJson(url, tradeFile)) {
|
|
||||||
log.info("Trade sent")
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
log.error("Trade failed to send")
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun runUploadRates(cliParams: CliParams.UploadRates) = runUploadRates(cliParams.apiAddress).get()
|
|
||||||
|
|
||||||
private fun createRecipient(addr: String): SingleMessageRecipient {
|
|
||||||
val hostAndPort = HostAndPort.fromString(addr).withDefaultPort(Node.DEFAULT_PORT)
|
|
||||||
return NodeMessagingClient.makeNetworkMapAddress(hostAndPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startNode(params: CliParams.RunNode, networkMap: SingleMessageRecipient): Node {
|
|
||||||
val config = getNodeConfig(params)
|
|
||||||
val advertisedServices: Set<ServiceInfo>
|
|
||||||
val networkMapId =
|
|
||||||
when (params.node) {
|
|
||||||
IRSDemoNode.NodeA -> {
|
|
||||||
advertisedServices = setOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type))
|
|
||||||
null
|
|
||||||
}
|
|
||||||
IRSDemoNode.NodeB -> {
|
|
||||||
advertisedServices = setOf(ServiceInfo(NodeInterestRates.type))
|
|
||||||
networkMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val node = logElapsedTime("Node startup", log) {
|
|
||||||
Node(config, networkMapId, advertisedServices, DemoClock()).setup().start()
|
|
||||||
}
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parsePartyFromFile(path: Path) = Files.readAllBytes(path).deserialize<Party>()
|
|
||||||
|
|
||||||
private fun runUploadRates(host: HostAndPort): ListenableFuture<Int> {
|
|
||||||
// Note: the getResourceAsStream is an ugly hack to get the jvm to search in the right location
|
|
||||||
val fileContents = IOUtils.toString(CliParams::class.java.getResourceAsStream("example.rates.txt"))
|
|
||||||
var timer: Timer? = null
|
|
||||||
val result = SettableFuture.create<Int>()
|
|
||||||
timer = fixedRateTimer("upload-rates", false, 0, 5000, {
|
|
||||||
try {
|
|
||||||
val url = URL("http://${host.toString()}/upload/interest-rates")
|
|
||||||
if (uploadFile(url, fileContents)) {
|
|
||||||
timer!!.cancel()
|
|
||||||
log.info("Rates uploaded successfully")
|
|
||||||
result.set(0)
|
|
||||||
} else {
|
|
||||||
log.error("Could not upload rates. Retrying in 5 seconds. ")
|
|
||||||
result.set(1)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error("Could not upload rates due to exception. Retrying in 5 seconds")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNodeConfig(cliParams: CliParams.RunNode): FullNodeConfiguration {
|
|
||||||
if (!Files.exists(cliParams.dir)) {
|
|
||||||
throw NotSetupException("Missing config directory. Please run node setup before running the node")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Files.exists(cliParams.dir.resolve(AbstractNode.PUBLIC_IDENTITY_FILE_NAME))) {
|
|
||||||
throw NotSetupException("Missing identity file. Please run node setup before running the node")
|
|
||||||
}
|
|
||||||
|
|
||||||
val configFile = cliParams.dir.resolve("config")
|
|
||||||
val configOverrides = mapOf(
|
|
||||||
"artemisAddress" to cliParams.networkAddress.toString(),
|
|
||||||
"webAddress" to cliParams.apiAddress.toString(),
|
|
||||||
"h2port" to cliParams.h2Port.toString()
|
|
||||||
)
|
|
||||||
return loadConfigFile(cliParams.dir, configFile, configOverrides, cliParams.defaultLegalName)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadConfigFile(baseDir: Path, configFile: Path, configOverrides: Map<String, String>, defaultLegalName: String): FullNodeConfiguration {
|
|
||||||
if (!Files.exists(configFile)) {
|
|
||||||
createDefaultConfigFile(configFile, defaultLegalName)
|
|
||||||
log.warn("Default config created at $configFile.")
|
|
||||||
}
|
|
||||||
return FullNodeConfiguration(ConfigHelper.loadConfig(baseDir, configFileOverride = configFile, configOverrides = configOverrides))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createIdentities(nodeConf: NodeConfiguration) {
|
|
||||||
val mockNetwork = MockNetwork(false)
|
|
||||||
val node = MockNetwork.MockNode(nodeConf, mockNetwork, null, setOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type)), 0, null)
|
|
||||||
node.start()
|
|
||||||
node.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createDefaultConfigFile(configFile: Path, legalName: String) {
|
|
||||||
Files.write(configFile,
|
|
||||||
"""
|
|
||||||
myLegalName = $legalName
|
|
||||||
""".trimIndent().toByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun printHelp(parser: OptionParser) {
|
|
||||||
val roleList = IRSDemoRole.values().joinToString(separator = "|") { it.toString() }
|
|
||||||
println("""
|
|
||||||
Usage: irsdemo --role $roleList [<TradeName>|<DateValue>] [options]
|
|
||||||
Please refer to the documentation in docs/build/index.html for more info.
|
|
||||||
|
|
||||||
""".trimIndent())
|
|
||||||
parser.printHelpOn(System.out)
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
package com.r3corda.demos
|
|
||||||
|
|
||||||
import com.google.common.net.HostAndPort
|
|
||||||
import com.r3corda.contracts.InterestRateSwap
|
|
||||||
import com.r3corda.contracts.asset.Cash
|
|
||||||
import com.r3corda.core.contracts.*
|
|
||||||
import com.r3corda.core.logElapsedTime
|
|
||||||
import com.r3corda.core.node.services.ServiceInfo
|
|
||||||
import com.r3corda.core.utilities.Emoji
|
|
||||||
import com.r3corda.core.utilities.LogHelper
|
|
||||||
import com.r3corda.demos.api.NodeInterestRates
|
|
||||||
import com.r3corda.node.internal.Node
|
|
||||||
import com.r3corda.node.services.config.ConfigHelper
|
|
||||||
import com.r3corda.node.services.config.FullNodeConfiguration
|
|
||||||
import com.r3corda.node.services.messaging.NodeMessagingClient
|
|
||||||
import com.r3corda.protocols.RatesFixProtocol
|
|
||||||
import joptsimple.OptionParser
|
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.math.BigDecimal
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
private val log: Logger = LoggerFactory.getLogger("RatesFixDemo")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a dummy transaction that requires a rate fix within a certain range, and gets it signed by an oracle
|
|
||||||
* service.
|
|
||||||
*/
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
val parser = OptionParser()
|
|
||||||
val networkAddressArg = parser.accepts("network-address").withRequiredArg().required()
|
|
||||||
val dirArg = parser.accepts("directory").withRequiredArg().defaultsTo("rate-fix-demo-data")
|
|
||||||
val networkMapAddrArg = parser.accepts("network-map").withRequiredArg().required()
|
|
||||||
|
|
||||||
val fixOfArg = parser.accepts("fix-of").withRequiredArg().defaultsTo("ICE LIBOR 2016-03-16 1M")
|
|
||||||
val expectedRateArg = parser.accepts("expected-rate").withRequiredArg().defaultsTo("0.67")
|
|
||||||
val rateToleranceArg = parser.accepts("rate-tolerance").withRequiredArg().defaultsTo("0.1")
|
|
||||||
|
|
||||||
val options = try {
|
|
||||||
parser.parse(*args)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error(e.message)
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suppress the Artemis MQ noise, and activate the demo logging.
|
|
||||||
LogHelper.setLevel("+RatesFixDemo", "-org.apache.activemq")
|
|
||||||
|
|
||||||
val dir = Paths.get(options.valueOf(dirArg))
|
|
||||||
val networkMapAddr = NodeMessagingClient.makeNetworkMapAddress(HostAndPort.fromString(options.valueOf(networkMapAddrArg)))
|
|
||||||
|
|
||||||
val fixOf: FixOf = NodeInterestRates.parseFixOf(options.valueOf(fixOfArg))
|
|
||||||
val expectedRate = BigDecimal(options.valueOf(expectedRateArg))
|
|
||||||
val rateTolerance = BigDecimal(options.valueOf(rateToleranceArg))
|
|
||||||
|
|
||||||
// Bring up node.
|
|
||||||
val advertisedServices: Set<ServiceInfo> = emptySet()
|
|
||||||
val myNetAddr = HostAndPort.fromString(options.valueOf(networkAddressArg))
|
|
||||||
|
|
||||||
val apiAddr = HostAndPort.fromParts(myNetAddr.hostText, myNetAddr.port + 1)
|
|
||||||
|
|
||||||
val config = ConfigHelper.loadConfig(
|
|
||||||
baseDirectoryPath = dir,
|
|
||||||
allowMissingConfig = true,
|
|
||||||
configOverrides = mapOf(
|
|
||||||
"myLegalName" to "Rate fix demo node",
|
|
||||||
"basedir" to dir.normalize().toString(),
|
|
||||||
"artemisAddress" to myNetAddr.toString(),
|
|
||||||
"webAddress" to apiAddr.toString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val nodeConfiguration = FullNodeConfiguration(config)
|
|
||||||
|
|
||||||
val node = logElapsedTime("Node startup") {
|
|
||||||
Node(nodeConfiguration, networkMapAddr, advertisedServices, DemoClock()).setup().start()
|
|
||||||
}
|
|
||||||
node.networkMapRegistrationFuture.get()
|
|
||||||
val notaryNode = node.services.networkMapCache.notaryNodes[0]
|
|
||||||
val rateOracle = node.services.networkMapCache.get(InterestRateSwap.oracleType).first()
|
|
||||||
|
|
||||||
// Make a garbage transaction that includes a rate fix.
|
|
||||||
val tx = TransactionType.General.Builder(notaryNode.notaryIdentity)
|
|
||||||
tx.addOutputState(TransactionState(Cash.State(1500.DOLLARS `issued by` node.info.legalIdentity.ref(1), node.info.legalIdentity.owningKey), notaryNode.notaryIdentity))
|
|
||||||
val protocol = RatesFixProtocol(tx, rateOracle.serviceIdentities(InterestRateSwap.oracleType).first(), fixOf, expectedRate, rateTolerance)
|
|
||||||
node.services.startProtocol(protocol).get()
|
|
||||||
node.stop()
|
|
||||||
|
|
||||||
// Show the user the output.
|
|
||||||
log.info("Got rate fix\n")
|
|
||||||
print(Emoji.renderIfSupported(tx.toWireTransaction()))
|
|
||||||
println(tx.toSignedTransaction().sigs.toString())
|
|
||||||
}
|
|
@ -1,123 +0,0 @@
|
|||||||
package com.r3corda.demos.api
|
|
||||||
|
|
||||||
import com.r3corda.contracts.InterestRateSwap
|
|
||||||
import com.r3corda.core.transactions.SignedTransaction
|
|
||||||
import com.r3corda.core.failure
|
|
||||||
import com.r3corda.core.node.ServiceHub
|
|
||||||
import com.r3corda.core.node.services.linearHeadsOfType
|
|
||||||
import com.r3corda.core.success
|
|
||||||
import com.r3corda.core.utilities.loggerFor
|
|
||||||
import com.r3corda.demos.protocols.AutoOfferProtocol
|
|
||||||
import com.r3corda.demos.protocols.ExitServerProtocol
|
|
||||||
import com.r3corda.demos.protocols.UpdateBusinessDayProtocol
|
|
||||||
import org.apache.commons.io.IOUtils
|
|
||||||
import java.net.URI
|
|
||||||
import java.net.URLConnection
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import javax.ws.rs.*
|
|
||||||
import javax.ws.rs.core.*
|
|
||||||
import java.nio.channels.*
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
* 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
|
|
||||||
* simulate any associated business processing (currently fixing).
|
|
||||||
*
|
|
||||||
* TODO: replace simulated date advancement with business event based implementation
|
|
||||||
*
|
|
||||||
* PUT /api/irs/restart - (empty payload) cause the node to restart for API user emergency use in case any servers become unresponsive,
|
|
||||||
* or if the demodate or population of deals should be reset (will only work while persistence is disabled).
|
|
||||||
*/
|
|
||||||
@Path("irs")
|
|
||||||
class InterestRateSwapAPI(val services: ServiceHub) {
|
|
||||||
|
|
||||||
private val logger = loggerFor<InterestRateSwapAPI>()
|
|
||||||
|
|
||||||
private fun generateDealLink(deal: InterestRateSwap.State) = "/api/irs/deals/" + deal.common.tradeID
|
|
||||||
|
|
||||||
private fun getDealByRef(ref: String): InterestRateSwap.State? {
|
|
||||||
val states = services.vaultService.linearHeadsOfType<InterestRateSwap.State>().filterValues { it.state.data.ref == ref }
|
|
||||||
return if (states.isEmpty()) null else {
|
|
||||||
val deals = states.values.map { it.state.data }
|
|
||||||
return if (deals.isEmpty()) null else deals[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAllDeals(): Array<InterestRateSwap.State> {
|
|
||||||
val states = services.vaultService.linearHeadsOfType<InterestRateSwap.State>()
|
|
||||||
val swaps = states.values.map { it.state.data }.toTypedArray()
|
|
||||||
return swaps
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("deals")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
fun fetchDeals(): Array<InterestRateSwap.State> = getAllDeals()
|
|
||||||
|
|
||||||
@POST
|
|
||||||
@Path("deals")
|
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
|
||||||
fun storeDeal(newDeal: InterestRateSwap.State): Response {
|
|
||||||
try {
|
|
||||||
services.invokeProtocolAsync<SignedTransaction>(AutoOfferProtocol.Requester::class.java, newDeal).get()
|
|
||||||
return Response.created(URI.create(generateDealLink(newDeal))).build()
|
|
||||||
} catch (ex: Throwable) {
|
|
||||||
logger.info("Exception when creating deal: ${ex.toString()}")
|
|
||||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(ex.toString()).build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("deals/{ref}")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
fun fetchDeal(@PathParam("ref") ref: String): Response {
|
|
||||||
val deal = getDealByRef(ref)
|
|
||||||
if (deal == null) {
|
|
||||||
return Response.status(Response.Status.NOT_FOUND).build()
|
|
||||||
} else {
|
|
||||||
return Response.ok().entity(deal).build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PUT
|
|
||||||
@Path("demodate")
|
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
|
||||||
fun storeDemoDate(newDemoDate: LocalDate): Response {
|
|
||||||
val priorDemoDate = fetchDemoDate()
|
|
||||||
// Can only move date forwards
|
|
||||||
if (newDemoDate.isAfter(priorDemoDate)) {
|
|
||||||
services.invokeProtocolAsync<Unit>(UpdateBusinessDayProtocol.Broadcast::class.java, newDemoDate).get()
|
|
||||||
return Response.ok().build()
|
|
||||||
}
|
|
||||||
val msg = "demodate is already $priorDemoDate and can only be updated with a later date"
|
|
||||||
logger.info("Attempt to set demodate to $newDemoDate but $msg")
|
|
||||||
return Response.status(Response.Status.CONFLICT).entity(msg).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@GET
|
|
||||||
@Path("demodate")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
fun fetchDemoDate(): LocalDate {
|
|
||||||
return LocalDateTime.now(services.clock).toLocalDate()
|
|
||||||
}
|
|
||||||
|
|
||||||
@PUT
|
|
||||||
@Path("restart")
|
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
|
||||||
fun exitServer(): Response {
|
|
||||||
services.invokeProtocolAsync<Boolean>(ExitServerProtocol.Broadcast::class.java, 83).get()
|
|
||||||
return Response.ok().build()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,321 +0,0 @@
|
|||||||
package com.r3corda.demos.api
|
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
|
||||||
import com.r3corda.core.RetryableException
|
|
||||||
import com.r3corda.core.contracts.*
|
|
||||||
import com.r3corda.core.crypto.DigitalSignature
|
|
||||||
import com.r3corda.core.crypto.Party
|
|
||||||
import com.r3corda.core.crypto.signWithECDSA
|
|
||||||
import com.r3corda.core.math.CubicSplineInterpolator
|
|
||||||
import com.r3corda.core.math.Interpolator
|
|
||||||
import com.r3corda.core.math.InterpolatorFactory
|
|
||||||
import com.r3corda.core.node.CordaPluginRegistry
|
|
||||||
import com.r3corda.core.node.services.ServiceType
|
|
||||||
import com.r3corda.core.protocols.ProtocolLogic
|
|
||||||
import com.r3corda.core.serialization.SingletonSerializeAsToken
|
|
||||||
import com.r3corda.core.transactions.WireTransaction
|
|
||||||
import com.r3corda.core.utilities.ProgressTracker
|
|
||||||
import com.r3corda.node.services.api.AcceptsFileUpload
|
|
||||||
import com.r3corda.node.services.api.ServiceHubInternal
|
|
||||||
import com.r3corda.node.utilities.AbstractJDBCHashSet
|
|
||||||
import com.r3corda.node.utilities.FiberBox
|
|
||||||
import com.r3corda.node.utilities.JDBCHashedTable
|
|
||||||
import com.r3corda.node.utilities.localDate
|
|
||||||
import com.r3corda.protocols.RatesFixProtocol.*
|
|
||||||
import com.r3corda.protocols.TwoPartyDealProtocol
|
|
||||||
import org.jetbrains.exposed.sql.ResultRow
|
|
||||||
import org.jetbrains.exposed.sql.statements.InsertStatement
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.math.BigDecimal
|
|
||||||
import java.security.KeyPair
|
|
||||||
import java.time.Clock
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.util.*
|
|
||||||
import javax.annotation.concurrent.ThreadSafe
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
val type = ServiceType.corda.getSubType("interest_rates")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the protocol that is used with the Fixing integration tests.
|
|
||||||
*/
|
|
||||||
class Plugin : CordaPluginRegistry() {
|
|
||||||
override val requiredProtocols: Map<String, Set<String>> = mapOf(Pair(TwoPartyDealProtocol.FixingRoleDecider::class.java.name, setOf(Duration::class.java.name, StateRef::class.java.name)))
|
|
||||||
override val servicePlugins: List<Class<*>> = listOf(Service::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Service that wraps [Oracle] and handles messages/network interaction/request scrubbing.
|
|
||||||
*/
|
|
||||||
class Service(val services: ServiceHubInternal) : AcceptsFileUpload, SingletonSerializeAsToken() {
|
|
||||||
val oracle: Oracle by lazy {
|
|
||||||
val myNodeInfo = services.myInfo
|
|
||||||
val myIdentity = myNodeInfo.serviceIdentities(type).first()
|
|
||||||
val mySigningKey = services.keyManagementService.toKeyPair(myIdentity.owningKey)
|
|
||||||
Oracle(myIdentity, mySigningKey, services.clock)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Note access to the singleton oracle property is via the registered SingletonSerializeAsToken Service.
|
|
||||||
// Otherwise the Kryo serialisation of the call stack in the Quasar Fiber extends to include
|
|
||||||
// the framework Oracle and the protocol will crash.
|
|
||||||
services.registerProtocolInitiator(FixSignProtocol::class) { FixSignHandler(it, this) }
|
|
||||||
services.registerProtocolInitiator(FixQueryProtocol::class) { FixQueryHandler(it, this) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FixSignHandler(val otherParty: Party, val service: Service) : ProtocolLogic<Unit>() {
|
|
||||||
@Suspendable
|
|
||||||
override fun call() {
|
|
||||||
val request = receive<SignRequest>(otherParty).unwrap { it }
|
|
||||||
send(otherParty, service.oracle.sign(request.tx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FixQueryHandler(val otherParty: Party, val service: Service) : ProtocolLogic<Unit>() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
object RECEIVED : ProgressTracker.Step("Received fix request")
|
|
||||||
object SENDING : ProgressTracker.Step("Sending fix response")
|
|
||||||
}
|
|
||||||
|
|
||||||
override val progressTracker = ProgressTracker(RECEIVED, SENDING)
|
|
||||||
|
|
||||||
init {
|
|
||||||
progressTracker.currentStep = RECEIVED
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
override fun call(): Unit {
|
|
||||||
val request = receive<QueryRequest>(otherParty).unwrap { it }
|
|
||||||
val answers = service.oracle.query(request.queries, request.deadline)
|
|
||||||
progressTracker.currentStep = SENDING
|
|
||||||
send(otherParty, answers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// File upload support
|
|
||||||
override val dataTypePrefix = "interest-rates"
|
|
||||||
override val acceptableFileExtensions = listOf(".rates", ".txt")
|
|
||||||
|
|
||||||
override fun upload(data: InputStream): String {
|
|
||||||
val fixes = parseFile(data.bufferedReader().readText())
|
|
||||||
// TODO: Save the uploaded fixes to the storage service and reload on construction.
|
|
||||||
|
|
||||||
// This assignment is thread safe because knownFixes is volatile and the oracle code always snapshots
|
|
||||||
// the pointer to the stack before working with the map.
|
|
||||||
oracle.knownFixes = fixes
|
|
||||||
|
|
||||||
return "Accepted ${fixes.size} new interest rate fixes"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
class Oracle(val identity: Party, private val signingKey: KeyPair, val clock: Clock) {
|
|
||||||
|
|
||||||
private object Table : JDBCHashedTable("demo_interest_rate_fixes") {
|
|
||||||
val name = varchar("index_name", length = 255)
|
|
||||||
val forDay = localDate("for_day")
|
|
||||||
val ofTenor = varchar("of_tenor", length = 16)
|
|
||||||
val value = decimal("value", scale = 20, precision = 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
private class InnerState {
|
|
||||||
val fixes = object : AbstractJDBCHashSet<Fix, Table>(Table) {
|
|
||||||
override fun elementFromRow(row: ResultRow): Fix {
|
|
||||||
return Fix(FixOf(row[table.name], row[table.forDay], Tenor(row[table.ofTenor])), row[table.value])
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addElementToInsert(insert: InsertStatement, entry: Fix, finalizables: MutableList<() -> Unit>) {
|
|
||||||
insert[table.name] = entry.of.name
|
|
||||||
insert[table.forDay] = entry.of.forDay
|
|
||||||
insert[table.ofTenor] = entry.of.ofTenor.name
|
|
||||||
insert[table.value] = entry.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var container: FixContainer = FixContainer(fixes)
|
|
||||||
}
|
|
||||||
private val mutex = FiberBox(InnerState())
|
|
||||||
|
|
||||||
var knownFixes: FixContainer
|
|
||||||
set(value) {
|
|
||||||
require(value.size > 0)
|
|
||||||
mutex.write {
|
|
||||||
fixes.clear()
|
|
||||||
fixes.addAll(value.fixes)
|
|
||||||
container = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
get() = mutex.read { container }
|
|
||||||
|
|
||||||
// Make this the last bit of initialisation logic so fully constructed when entered into instances map
|
|
||||||
init {
|
|
||||||
require(signingKey.public == identity.owningKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method will now wait until the given deadline if the fix for the given [FixOf] is not immediately
|
|
||||||
* available. To implement this, [readWithDeadline] will loop if the deadline is not reached and we throw
|
|
||||||
* [UnknownFix] as it implements [RetryableException] which has special meaning to this function.
|
|
||||||
*/
|
|
||||||
@Suspendable
|
|
||||||
fun query(queries: List<FixOf>, deadline: Instant): List<Fix> {
|
|
||||||
require(queries.isNotEmpty())
|
|
||||||
return mutex.readWithDeadline(clock, deadline) {
|
|
||||||
val answers: List<Fix?> = queries.map { container[it] }
|
|
||||||
val firstNull = answers.indexOf(null)
|
|
||||||
if (firstNull != -1) {
|
|
||||||
throw UnknownFix(queries[firstNull])
|
|
||||||
} else {
|
|
||||||
answers.filterNotNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sign(wtx: WireTransaction): DigitalSignature.LegallyIdentifiable {
|
|
||||||
// Extract the fix commands marked as being signable by us.
|
|
||||||
val fixes: List<Fix> = wtx.commands.
|
|
||||||
filter { identity.owningKey in it.signers && it.value is Fix }.
|
|
||||||
map { it.value as Fix }
|
|
||||||
|
|
||||||
// Reject this signing attempt if there are no commands of the right kind.
|
|
||||||
if (fixes.isEmpty())
|
|
||||||
throw IllegalArgumentException()
|
|
||||||
|
|
||||||
// For each fix, verify that the data is correct.
|
|
||||||
val knownFixes = knownFixes // Snapshot
|
|
||||||
for (fix in fixes) {
|
|
||||||
val known = knownFixes[fix.of]
|
|
||||||
if (known == null || known != fix)
|
|
||||||
throw UnknownFix(fix.of)
|
|
||||||
}
|
|
||||||
|
|
||||||
// It all checks out, so we can return a signature.
|
|
||||||
//
|
|
||||||
// Note that we will happily sign an invalid transaction: we don't bother trying to validate the whole
|
|
||||||
// thing. This is so that later on we can start using tear-offs.
|
|
||||||
return signingKey.signWithECDSA(wtx.serialized, identity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: can we split into two? Fix not available (retryable/transient) and unknown (permanent)
|
|
||||||
class UnknownFix(val fix: FixOf) : RetryableException("Unknown fix: $fix")
|
|
||||||
|
|
||||||
/** 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 (fix in fixes) {
|
|
||||||
val fixOf = fix.of
|
|
||||||
val rates = tempContainer.getOrPut(fixOf.name to fixOf.forDay) { HashMap<Tenor, BigDecimal>() }
|
|
||||||
rates[fixOf.ofTenor] = fix.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: the calendar data needs to be specified for every fix type in the input string
|
|
||||||
val calendar = BusinessCalendar.getInstance("London", "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.put(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 { parseFix(it) }.
|
|
||||||
toSet()
|
|
||||||
return FixContainer(fixes)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Parses a string of the form "LIBOR 16-March-2016 1M = 0.678" into a [Fix] */
|
|
||||||
fun parseFix(s: String): Fix {
|
|
||||||
val (key, value) = s.split('=').map(String::trim)
|
|
||||||
val of = parseFixOf(key)
|
|
||||||
val rate = BigDecimal(value)
|
|
||||||
return Fix(of, rate)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Parses a string of the form "LIBOR 16-March-2016 1M" into a [FixOf] */
|
|
||||||
fun parseFixOf(key: String): FixOf {
|
|
||||||
val words = key.split(' ')
|
|
||||||
val tenorString = words.last()
|
|
||||||
val date = words.dropLast(1).last()
|
|
||||||
val name = words.dropLast(2).joinToString(" ")
|
|
||||||
return FixOf(name, LocalDate.parse(date), Tenor(tenorString))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
package com.r3corda.demos.protocols
|
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
|
||||||
import com.r3corda.core.contracts.DealState
|
|
||||||
import com.r3corda.core.crypto.Party
|
|
||||||
import com.r3corda.core.node.CordaPluginRegistry
|
|
||||||
import com.r3corda.core.protocols.ProtocolLogic
|
|
||||||
import com.r3corda.core.serialization.SingletonSerializeAsToken
|
|
||||||
import com.r3corda.core.transactions.SignedTransaction
|
|
||||||
import com.r3corda.core.utilities.ProgressTracker
|
|
||||||
import com.r3corda.node.services.api.ServiceHubInternal
|
|
||||||
import com.r3corda.protocols.TwoPartyDealProtocol
|
|
||||||
import com.r3corda.protocols.TwoPartyDealProtocol.Acceptor
|
|
||||||
import com.r3corda.protocols.TwoPartyDealProtocol.AutoOffer
|
|
||||||
import com.r3corda.protocols.TwoPartyDealProtocol.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 protocol would have to reach out to external systems (or users) to verify the deals.
|
|
||||||
*/
|
|
||||||
object AutoOfferProtocol {
|
|
||||||
|
|
||||||
class Plugin : CordaPluginRegistry() {
|
|
||||||
override val servicePlugins: List<Class<*>> = listOf(Service::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Service(services: ServiceHubInternal) : SingletonSerializeAsToken() {
|
|
||||||
|
|
||||||
object DEALING : ProgressTracker.Step("Starting the deal protocol") {
|
|
||||||
override fun childProgressTracker(): ProgressTracker = TwoPartyDealProtocol.Secondary.tracker()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
services.registerProtocolInitiator(Instigator::class) { Acceptor(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Requester(val dealToBeOffered: DealState) : ProtocolLogic<SignedTransaction>() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
object RECEIVED : ProgressTracker.Step("Received API call")
|
|
||||||
object DEALING : ProgressTracker.Step("Starting the deal protocol") {
|
|
||||||
override fun childProgressTracker(): ProgressTracker = TwoPartyDealProtocol.Primary.tracker()
|
|
||||||
}
|
|
||||||
|
|
||||||
// We vend a progress tracker that already knows there's going to be a TwoPartyTradingProtocol 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.notaryNodes.isNotEmpty()) { "No notary nodes registered" }
|
|
||||||
val notary = serviceHub.networkMapCache.notaryNodes.first().notaryIdentity
|
|
||||||
// need to pick which ever party is not us
|
|
||||||
val otherParty = notUs(dealToBeOffered.parties).single()
|
|
||||||
progressTracker.currentStep = DEALING
|
|
||||||
val myKey = serviceHub.legalIdentityKey
|
|
||||||
val instigator = Instigator(
|
|
||||||
otherParty,
|
|
||||||
AutoOffer(notary, dealToBeOffered),
|
|
||||||
myKey,
|
|
||||||
progressTracker.getChildProgressTracker(DEALING)!!
|
|
||||||
)
|
|
||||||
val stx = subProtocol(instigator)
|
|
||||||
return stx
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun notUs(parties: List<Party>): List<Party> {
|
|
||||||
val notUsParties: MutableList<Party> = arrayListOf()
|
|
||||||
for (party in parties) {
|
|
||||||
if (serviceHub.myInfo.legalIdentity != party) {
|
|
||||||
notUsParties.add(party)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return notUsParties
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
package com.r3corda.demos.protocols
|
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
|
||||||
import co.paralleluniverse.strands.Strand
|
|
||||||
import com.r3corda.core.crypto.Party
|
|
||||||
import com.r3corda.core.node.CordaPluginRegistry
|
|
||||||
import com.r3corda.core.node.NodeInfo
|
|
||||||
import com.r3corda.core.protocols.ProtocolLogic
|
|
||||||
import com.r3corda.node.services.api.ServiceHubInternal
|
|
||||||
import com.r3corda.testing.node.MockNetworkMapCache
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
object ExitServerProtocol {
|
|
||||||
|
|
||||||
// Will only be enabled if you install the Handler
|
|
||||||
@Volatile private var enabled = false
|
|
||||||
|
|
||||||
// This is not really a HandshakeMessage but needs to be so that the send uses the default session ID. This will
|
|
||||||
// resolve itself when the protocol session stuff is done.
|
|
||||||
data class ExitMessage(val exitCode: Int)
|
|
||||||
|
|
||||||
class Plugin: CordaPluginRegistry() {
|
|
||||||
override val servicePlugins: List<Class<*>> = listOf(Service::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
class Service(services: ServiceHubInternal) {
|
|
||||||
init {
|
|
||||||
services.registerProtocolInitiator(Broadcast::class, ::ExitServerHandler)
|
|
||||||
enabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class ExitServerHandler(val otherParty: Party) : ProtocolLogic<Unit>() {
|
|
||||||
override fun call() {
|
|
||||||
// Just to validate we got the message
|
|
||||||
if (enabled) {
|
|
||||||
val message = receive<ExitMessage>(otherParty).unwrap { it }
|
|
||||||
System.exit(message.exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This takes a Java Integer rather than Kotlin Int as that is what we end up with in the calling map and currently
|
|
||||||
* we do not support coercing numeric types in the reflective search for matching constructors.
|
|
||||||
*/
|
|
||||||
class Broadcast(val exitCode: Int) : ProtocolLogic<Boolean>() {
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
override fun call(): Boolean {
|
|
||||||
if (enabled) {
|
|
||||||
for (recipient in serviceHub.networkMapCache.partyNodes) {
|
|
||||||
doNextRecipient(recipient)
|
|
||||||
}
|
|
||||||
// Sleep a little in case any async message delivery to other nodes needs to happen
|
|
||||||
Strand.sleep(1, TimeUnit.SECONDS)
|
|
||||||
System.exit(exitCode)
|
|
||||||
}
|
|
||||||
return enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
private fun doNextRecipient(recipient: NodeInfo) {
|
|
||||||
if (recipient.address is MockNetworkMapCache.MockAddress) {
|
|
||||||
// Ignore
|
|
||||||
} else {
|
|
||||||
send(recipient.legalIdentity, ExitMessage(exitCode))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
package com.r3corda.demos.protocols
|
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
|
||||||
import com.r3corda.core.crypto.Party
|
|
||||||
import com.r3corda.core.node.CordaPluginRegistry
|
|
||||||
import com.r3corda.core.node.NodeInfo
|
|
||||||
import com.r3corda.core.protocols.ProtocolLogic
|
|
||||||
import com.r3corda.core.utilities.ProgressTracker
|
|
||||||
import com.r3corda.demos.DemoClock
|
|
||||||
import com.r3corda.node.services.api.ServiceHubInternal
|
|
||||||
import com.r3corda.testing.node.MockNetworkMapCache
|
|
||||||
import java.time.LocalDate
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a less temporary, demo-oriented way of initiating processing of temporal events.
|
|
||||||
*/
|
|
||||||
object UpdateBusinessDayProtocol {
|
|
||||||
|
|
||||||
// This is not really a HandshakeMessage but needs to be so that the send uses the default session ID. This will
|
|
||||||
// resolve itself when the protocol session stuff is done.
|
|
||||||
data class UpdateBusinessDayMessage(val date: LocalDate)
|
|
||||||
|
|
||||||
class Plugin: CordaPluginRegistry() {
|
|
||||||
override val servicePlugins: List<Class<*>> = listOf(Service::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
class Service(services: ServiceHubInternal) {
|
|
||||||
init {
|
|
||||||
services.registerProtocolInitiator(Broadcast::class, ::UpdateBusinessDayHandler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class UpdateBusinessDayHandler(val otherParty: Party) : ProtocolLogic<Unit>() {
|
|
||||||
override fun call() {
|
|
||||||
val message = receive<UpdateBusinessDayMessage>(otherParty).unwrap { it }
|
|
||||||
(serviceHub.clock as DemoClock).updateDate(message.date)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Broadcast(val date: LocalDate,
|
|
||||||
override val progressTracker: ProgressTracker = Broadcast.tracker()) : ProtocolLogic<Unit>() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
object NOTIFYING : ProgressTracker.Step("Notifying peers")
|
|
||||||
|
|
||||||
fun tracker() = ProgressTracker(NOTIFYING)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
override fun call(): Unit {
|
|
||||||
progressTracker.currentStep = NOTIFYING
|
|
||||||
for (recipient in serviceHub.networkMapCache.partyNodes) {
|
|
||||||
doNextRecipient(recipient)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suspendable
|
|
||||||
private fun doNextRecipient(recipient: NodeInfo) {
|
|
||||||
if (recipient.address is MockNetworkMapCache.MockAddress) {
|
|
||||||
// Ignore
|
|
||||||
} else {
|
|
||||||
send(recipient.legalIdentity, UpdateBusinessDayMessage(date))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
package com.r3corda.demos.utilities
|
|
||||||
|
|
||||||
import okhttp3.*
|
|
||||||
import java.net.URL
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A small set of utilities for making HttpCalls, aimed at demos.
|
|
||||||
*/
|
|
||||||
private val client by lazy {
|
|
||||||
OkHttpClient.Builder()
|
|
||||||
.connectTimeout(5, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(60, TimeUnit.SECONDS).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun putJson(url: URL, data: String) : Boolean {
|
|
||||||
val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data)
|
|
||||||
return makeRequest(Request.Builder().url(url).put(body).build())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun postJson(url: URL, data: String) : Boolean {
|
|
||||||
val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data)
|
|
||||||
return makeRequest(Request.Builder().url(url).post(body).build())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun uploadFile(url: URL, file: String) : Boolean {
|
|
||||||
val body = MultipartBody.Builder()
|
|
||||||
.setType(MultipartBody.FORM)
|
|
||||||
.addFormDataPart("rates", "example.rates.txt", RequestBody.create(MediaType.parse("text/plain"), file))
|
|
||||||
.build()
|
|
||||||
return makeRequest(Request.Builder().url(url).post(body).build())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun makeRequest(request: Request): Boolean {
|
|
||||||
val response = client.newCall(request).execute()
|
|
||||||
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
println("Could not fulfill HTTP request. Status Code: ${response.code()}. Message: ${response.body()}")
|
|
||||||
}
|
|
||||||
return response.isSuccessful
|
|
||||||
}
|
|
@ -1,133 +0,0 @@
|
|||||||
package com.r3corda.simulation
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
|
||||||
import com.google.common.util.concurrent.FutureCallback
|
|
||||||
import com.google.common.util.concurrent.Futures
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import com.google.common.util.concurrent.SettableFuture
|
|
||||||
import com.r3corda.contracts.InterestRateSwap
|
|
||||||
import com.r3corda.core.RunOnCallerThread
|
|
||||||
import com.r3corda.core.contracts.StateAndRef
|
|
||||||
import com.r3corda.core.contracts.UniqueIdentifier
|
|
||||||
import com.r3corda.core.flatMap
|
|
||||||
import com.r3corda.core.map
|
|
||||||
import com.r3corda.core.node.services.linearHeadsOfType
|
|
||||||
import com.r3corda.core.protocols.ProtocolStateMachine
|
|
||||||
import com.r3corda.core.success
|
|
||||||
import com.r3corda.core.transactions.SignedTransaction
|
|
||||||
import com.r3corda.protocols.TwoPartyDealProtocol.Acceptor
|
|
||||||
import com.r3corda.protocols.TwoPartyDealProtocol.AutoOffer
|
|
||||||
import com.r3corda.protocols.TwoPartyDealProtocol.Instigator
|
|
||||||
import com.r3corda.testing.initiateSingleShotProtocol
|
|
||||||
import com.r3corda.testing.node.InMemoryMessagingNetwork
|
|
||||||
import com.r3corda.testing.node.MockIdentityService
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simulation in which banks execute interest rate swaps with each other, including the fixing events.
|
|
||||||
*/
|
|
||||||
class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, latencyInjector: InMemoryMessagingNetwork.LatencyCalculator?) : Simulation(networkSendManuallyPumped, runAsync, latencyInjector) {
|
|
||||||
val om = com.r3corda.node.utilities.JsonSupport.createDefaultMapper(MockIdentityService(network.identities))
|
|
||||||
|
|
||||||
init {
|
|
||||||
currentDateAndTime = LocalDate.of(2016, 3, 8).atStartOfDay()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val executeOnNextIteration = Collections.synchronizedList(LinkedList<() -> Unit>())
|
|
||||||
|
|
||||||
override fun startMainSimulation(): ListenableFuture<Unit> {
|
|
||||||
val future = SettableFuture.create<Unit>()
|
|
||||||
|
|
||||||
startIRSDealBetween(0, 1).success {
|
|
||||||
// Next iteration is a pause.
|
|
||||||
executeOnNextIteration.add {}
|
|
||||||
executeOnNextIteration.add {
|
|
||||||
// Keep fixing until there's no more left to do.
|
|
||||||
val initialFixFuture = doNextFixing(0, 1)
|
|
||||||
|
|
||||||
Futures.addCallback(initialFixFuture, object : FutureCallback<Unit> {
|
|
||||||
override fun onFailure(t: Throwable) {
|
|
||||||
future.setException(t) // Propagate the error.
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSuccess(result: Unit?) {
|
|
||||||
// Pause for an iteration.
|
|
||||||
executeOnNextIteration.add {}
|
|
||||||
executeOnNextIteration.add {
|
|
||||||
val f = doNextFixing(0, 1)
|
|
||||||
if (f != null) {
|
|
||||||
Futures.addCallback(f, this, RunOnCallerThread)
|
|
||||||
} else {
|
|
||||||
// All done!
|
|
||||||
future.set(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, RunOnCallerThread)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return future
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doNextFixing(i: Int, j: Int): ListenableFuture<Unit>? {
|
|
||||||
println("Doing a fixing between $i and $j")
|
|
||||||
val node1: SimulatedNode = banks[i]
|
|
||||||
val node2: SimulatedNode = banks[j]
|
|
||||||
|
|
||||||
val swaps: Map<UniqueIdentifier, StateAndRef<InterestRateSwap.State>> = node1.services.vaultService.linearHeadsOfType<InterestRateSwap.State>()
|
|
||||||
val theDealRef: StateAndRef<InterestRateSwap.State> = swaps.values.single()
|
|
||||||
|
|
||||||
// Do we have any more days left in this deal's lifetime? If not, return.
|
|
||||||
val nextFixingDate = theDealRef.state.data.calculation.nextFixingDate() ?: return null
|
|
||||||
extraNodeLabels[node1] = "Fixing event on $nextFixingDate"
|
|
||||||
extraNodeLabels[node2] = "Fixing event on $nextFixingDate"
|
|
||||||
|
|
||||||
// Complete the future when the state has been consumed on both nodes
|
|
||||||
val futA = node1.services.vaultService.whenConsumed(theDealRef.ref)
|
|
||||||
val futB = node2.services.vaultService.whenConsumed(theDealRef.ref)
|
|
||||||
|
|
||||||
showProgressFor(listOf(node1, node2))
|
|
||||||
showConsensusFor(listOf(node1, node2, regulators[0]))
|
|
||||||
|
|
||||||
// For some reason the first fix is always before the effective date.
|
|
||||||
if (nextFixingDate > currentDateAndTime.toLocalDate())
|
|
||||||
currentDateAndTime = nextFixingDate.atTime(15, 0)
|
|
||||||
|
|
||||||
return Futures.allAsList(futA, futB).map { Unit }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startIRSDealBetween(i: Int, j: Int): ListenableFuture<SignedTransaction> {
|
|
||||||
val node1: SimulatedNode = banks[i]
|
|
||||||
val node2: SimulatedNode = banks[j]
|
|
||||||
|
|
||||||
extraNodeLabels[node1] = "Setting up deal"
|
|
||||||
extraNodeLabels[node2] = "Setting up deal"
|
|
||||||
|
|
||||||
// We load the IRS afresh each time because the leg parts of the structure aren't data classes so they don't
|
|
||||||
// have the convenient copy() method that'd let us make small adjustments. Instead they're partly mutable.
|
|
||||||
// TODO: We should revisit this in post-Excalibur cleanup and fix, e.g. by introducing an interface.
|
|
||||||
val irs = om.readValue<InterestRateSwap.State>(javaClass.getResource("trade.json"))
|
|
||||||
irs.fixedLeg.fixedRatePayer = node1.info.legalIdentity
|
|
||||||
irs.floatingLeg.floatingRatePayer = node2.info.legalIdentity
|
|
||||||
|
|
||||||
val acceptorTx = node2.initiateSingleShotProtocol(Instigator::class) { Acceptor(it) }.flatMap {
|
|
||||||
(it.psm as ProtocolStateMachine<SignedTransaction>).resultFuture
|
|
||||||
}
|
|
||||||
|
|
||||||
showProgressFor(listOf(node1, node2))
|
|
||||||
showConsensusFor(listOf(node1, node2, regulators[0]))
|
|
||||||
|
|
||||||
val instigator = Instigator(node2.info.legalIdentity, AutoOffer(notary.info.notaryIdentity, irs), node1.keyPair!!)
|
|
||||||
val instigatorTx = node1.services.startProtocol(instigator)
|
|
||||||
|
|
||||||
return Futures.allAsList(instigatorTx, acceptorTx).flatMap { instigatorTx }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun iterate(): InMemoryMessagingNetwork.MessageTransfer? {
|
|
||||||
if (executeOnNextIteration.isNotEmpty())
|
|
||||||
executeOnNextIteration.removeAt(0)()
|
|
||||||
return super.iterate()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,338 +0,0 @@
|
|||||||
package com.r3corda.simulation
|
|
||||||
|
|
||||||
import com.google.common.util.concurrent.Futures
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import com.r3corda.core.crypto.generateKeyPair
|
|
||||||
import com.r3corda.core.flatMap
|
|
||||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
|
||||||
import com.r3corda.core.node.CityDatabase
|
|
||||||
import com.r3corda.core.node.PhysicalLocation
|
|
||||||
import com.r3corda.core.node.services.ServiceInfo
|
|
||||||
import com.r3corda.core.node.services.containsType
|
|
||||||
import com.r3corda.core.protocols.ProtocolLogic
|
|
||||||
import com.r3corda.core.then
|
|
||||||
import com.r3corda.core.utilities.ProgressTracker
|
|
||||||
import com.r3corda.demos.api.NodeInterestRates
|
|
||||||
import com.r3corda.node.services.config.NodeConfiguration
|
|
||||||
import com.r3corda.node.services.network.NetworkMapService
|
|
||||||
import com.r3corda.node.services.transactions.SimpleNotaryService
|
|
||||||
import com.r3corda.node.utilities.AddOrRemove
|
|
||||||
import com.r3corda.node.utilities.databaseTransaction
|
|
||||||
import com.r3corda.testing.node.*
|
|
||||||
import rx.Observable
|
|
||||||
import rx.subjects.PublishSubject
|
|
||||||
import java.nio.file.Path
|
|
||||||
import java.security.KeyPair
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.ZoneOffset
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for network simulations that are based on the unit test / mock environment.
|
|
||||||
*
|
|
||||||
* Sets up some nodes that can run protocols between each other, and exposes their progress trackers. Provides banks
|
|
||||||
* in a few cities around the world.
|
|
||||||
*/
|
|
||||||
abstract class Simulation(val networkSendManuallyPumped: Boolean,
|
|
||||||
runAsync: Boolean,
|
|
||||||
latencyInjector: InMemoryMessagingNetwork.LatencyCalculator?) {
|
|
||||||
init {
|
|
||||||
if (!runAsync && latencyInjector != null)
|
|
||||||
throw IllegalArgumentException("The latency injector is only useful when using manual pumping.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val bankLocations = listOf("London", "Frankfurt", "Rome")
|
|
||||||
|
|
||||||
// This puts together a mock network of SimulatedNodes.
|
|
||||||
|
|
||||||
open class SimulatedNode(config: NodeConfiguration, mockNet: MockNetwork, networkMapAddress: SingleMessageRecipient?,
|
|
||||||
advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?) : MockNetwork.MockNode(config, mockNet, networkMapAddress, advertisedServices, id, keyPair) {
|
|
||||||
override fun findMyLocation(): PhysicalLocation? = CityDatabase[configuration.nearestCity]
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class BankFactory : MockNetwork.Factory {
|
|
||||||
var counter = 0
|
|
||||||
|
|
||||||
override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?,
|
|
||||||
advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
|
||||||
val letter = 'A' + counter
|
|
||||||
val city = bankLocations[counter++ % bankLocations.size]
|
|
||||||
|
|
||||||
// TODO: create a base class that provides a default implementation
|
|
||||||
val cfg = object : NodeConfiguration {
|
|
||||||
override val basedir: Path = config.basedir
|
|
||||||
// TODO: Set this back to "Bank of $city" after video day.
|
|
||||||
override val myLegalName: String = "Bank $letter"
|
|
||||||
override val nearestCity: String = city
|
|
||||||
override val emailAddress: String = ""
|
|
||||||
override val devMode: Boolean = true
|
|
||||||
override val exportJMXto: String = ""
|
|
||||||
override val keyStorePassword: String = "dummy"
|
|
||||||
override val trustStorePassword: String = "trustpass"
|
|
||||||
override val dataSourceProperties = makeTestDataSourceProperties()
|
|
||||||
}
|
|
||||||
return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createAll(): List<SimulatedNode> {
|
|
||||||
return bankLocations.map {
|
|
||||||
network.createNode(networkMap.info.address, start = false, nodeFactory = this, keyPair = generateKeyPair()) as SimulatedNode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val bankFactory = BankFactory()
|
|
||||||
|
|
||||||
object NetworkMapNodeFactory : MockNetwork.Factory {
|
|
||||||
override fun create(config: NodeConfiguration, network: MockNetwork,
|
|
||||||
networkMapAddr: SingleMessageRecipient?, advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
|
||||||
require(advertisedServices.containsType(NetworkMapService.type))
|
|
||||||
|
|
||||||
// TODO: create a base class that provides a default implementation
|
|
||||||
val cfg = object : NodeConfiguration {
|
|
||||||
override val basedir: Path = config.basedir
|
|
||||||
override val myLegalName: String = "Network coordination center"
|
|
||||||
override val nearestCity: String = "Amsterdam"
|
|
||||||
override val emailAddress: String = ""
|
|
||||||
override val devMode: Boolean = true
|
|
||||||
override val exportJMXto: String = ""
|
|
||||||
override val keyStorePassword: String = "dummy"
|
|
||||||
override val trustStorePassword: String = "trustpass"
|
|
||||||
override val dataSourceProperties = makeTestDataSourceProperties()
|
|
||||||
}
|
|
||||||
|
|
||||||
return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object NotaryNodeFactory : MockNetwork.Factory {
|
|
||||||
override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?,
|
|
||||||
advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
|
||||||
require(advertisedServices.containsType(SimpleNotaryService.type))
|
|
||||||
|
|
||||||
// TODO: create a base class that provides a default implementation
|
|
||||||
val cfg = object : NodeConfiguration {
|
|
||||||
override val basedir: Path = config.basedir
|
|
||||||
override val myLegalName: String = "Notary Service"
|
|
||||||
override val nearestCity: String = "Zurich"
|
|
||||||
override val emailAddress: String = ""
|
|
||||||
override val devMode: Boolean = true
|
|
||||||
override val exportJMXto: String = ""
|
|
||||||
override val keyStorePassword: String = "dummy"
|
|
||||||
override val trustStorePassword: String = "trustpass"
|
|
||||||
override val dataSourceProperties = makeTestDataSourceProperties()
|
|
||||||
}
|
|
||||||
return SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object RatesOracleFactory : MockNetwork.Factory {
|
|
||||||
override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?,
|
|
||||||
advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
|
||||||
require(advertisedServices.containsType(NodeInterestRates.type))
|
|
||||||
|
|
||||||
// TODO: create a base class that provides a default implementation
|
|
||||||
val cfg = object : NodeConfiguration {
|
|
||||||
override val basedir: Path = config.basedir
|
|
||||||
override val myLegalName: String = "Rates Service Provider"
|
|
||||||
override val nearestCity: String = "Madrid"
|
|
||||||
override val emailAddress: String = ""
|
|
||||||
override val devMode: Boolean = true
|
|
||||||
override val exportJMXto: String = ""
|
|
||||||
override val keyStorePassword: String = "dummy"
|
|
||||||
override val trustStorePassword: String = "trustpass"
|
|
||||||
override val dataSourceProperties = makeTestDataSourceProperties()
|
|
||||||
}
|
|
||||||
|
|
||||||
return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair) {
|
|
||||||
override fun start(): MockNetwork.MockNode {
|
|
||||||
super.start()
|
|
||||||
javaClass.getResourceAsStream("example.rates.txt").use {
|
|
||||||
databaseTransaction(database) {
|
|
||||||
findService<NodeInterestRates.Service>().upload(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object RegulatorFactory : MockNetwork.Factory {
|
|
||||||
override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?,
|
|
||||||
advertisedServices: Set<ServiceInfo>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
|
|
||||||
|
|
||||||
// TODO: create a base class that provides a default implementation
|
|
||||||
val cfg = object : NodeConfiguration {
|
|
||||||
override val basedir: Path = config.basedir
|
|
||||||
override val myLegalName: String = "Regulator A"
|
|
||||||
override val nearestCity: String = "Paris"
|
|
||||||
override val emailAddress: String = ""
|
|
||||||
override val devMode: Boolean = true
|
|
||||||
override val exportJMXto: String = ""
|
|
||||||
override val keyStorePassword: String = "dummy"
|
|
||||||
override val trustStorePassword: String = "trustpass"
|
|
||||||
override val dataSourceProperties = makeTestDataSourceProperties()
|
|
||||||
}
|
|
||||||
|
|
||||||
val n = object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, keyPair) {
|
|
||||||
// TODO: Regulatory nodes don't actually exist properly, this is a last minute demo request.
|
|
||||||
// So we just fire a message at a node that doesn't know how to handle it, and it'll ignore it.
|
|
||||||
// But that's fine for visualisation purposes.
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val network = MockNetwork(networkSendManuallyPumped, runAsync)
|
|
||||||
// This one must come first.
|
|
||||||
val networkMap: SimulatedNode
|
|
||||||
= network.createNode(null, nodeFactory = NetworkMapNodeFactory, advertisedServices = ServiceInfo(NetworkMapService.type)) as SimulatedNode
|
|
||||||
val notary: SimulatedNode
|
|
||||||
= network.createNode(networkMap.info.address, nodeFactory = NotaryNodeFactory, advertisedServices = ServiceInfo(SimpleNotaryService.type)) as SimulatedNode
|
|
||||||
val regulators: List<SimulatedNode> = listOf(network.createNode(networkMap.info.address, start = false, nodeFactory = RegulatorFactory) as SimulatedNode)
|
|
||||||
val ratesOracle: SimulatedNode
|
|
||||||
= network.createNode(networkMap.info.address, start = false, nodeFactory = RatesOracleFactory, advertisedServices = ServiceInfo(NodeInterestRates.type)) as SimulatedNode
|
|
||||||
|
|
||||||
// All nodes must be in one of these two lists for the purposes of the visualiser tool.
|
|
||||||
val serviceProviders: List<SimulatedNode> = listOf(notary, ratesOracle, networkMap)
|
|
||||||
val banks: List<SimulatedNode> = bankFactory.createAll()
|
|
||||||
|
|
||||||
val clocks = (serviceProviders + regulators + banks).map { it.services.clock as TestClock }
|
|
||||||
|
|
||||||
// These are used from the network visualiser tool.
|
|
||||||
private val _allProtocolSteps = PublishSubject.create<Pair<SimulatedNode, ProgressTracker.Change>>()
|
|
||||||
private val _doneSteps = PublishSubject.create<Collection<SimulatedNode>>()
|
|
||||||
@Suppress("unused") val allProtocolSteps: Observable<Pair<SimulatedNode, ProgressTracker.Change>> = _allProtocolSteps
|
|
||||||
@Suppress("unused") val doneSteps: Observable<Collection<SimulatedNode>> = _doneSteps
|
|
||||||
|
|
||||||
private var pumpCursor = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current simulated date. By default this never changes. If you want it to change, you should do so from
|
|
||||||
* within your overridden [iterate] call. Changes in the current day surface in the [dateChanges] observable.
|
|
||||||
*/
|
|
||||||
var currentDateAndTime: LocalDateTime = LocalDate.now().atStartOfDay()
|
|
||||||
protected set(value) {
|
|
||||||
field = value
|
|
||||||
_dateChanges.onNext(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _dateChanges = PublishSubject.create<LocalDateTime>()
|
|
||||||
val dateChanges: Observable<LocalDateTime> get() = _dateChanges
|
|
||||||
|
|
||||||
init {
|
|
||||||
// Advance node clocks when current time is changed
|
|
||||||
dateChanges.subscribe {
|
|
||||||
clocks.setTo(currentDateAndTime.toInstant(ZoneOffset.UTC))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A place for simulations to stash human meaningful text about what the node is "thinking", which might appear
|
|
||||||
* in the UI somewhere.
|
|
||||||
*/
|
|
||||||
val extraNodeLabels = Collections.synchronizedMap(HashMap<SimulatedNode, String>())
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterates the simulation by one step.
|
|
||||||
*
|
|
||||||
* The default implementation circles around the nodes, pumping until one of them handles a message. The next call
|
|
||||||
* will carry on from where this one stopped. In an environment where you want to take actions between anything
|
|
||||||
* interesting happening, or control the precise speed at which things operate (beyond the latency injector), this
|
|
||||||
* is a useful way to do things.
|
|
||||||
*
|
|
||||||
* @return the message that was processed, or null if no node accepted a message in this round.
|
|
||||||
*/
|
|
||||||
open fun iterate(): InMemoryMessagingNetwork.MessageTransfer? {
|
|
||||||
|
|
||||||
if (networkSendManuallyPumped) {
|
|
||||||
network.messagingNetwork.pumpSend(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep going until one of the nodes has something to do, or we have checked every node.
|
|
||||||
val endpoints = network.messagingNetwork.endpoints
|
|
||||||
var countDown = endpoints.size
|
|
||||||
while (countDown > 0) {
|
|
||||||
val handledMessage = endpoints[pumpCursor].pumpReceive(false)
|
|
||||||
if (handledMessage != null)
|
|
||||||
return handledMessage
|
|
||||||
// If this node had nothing to do, advance the cursor with wraparound and try again.
|
|
||||||
pumpCursor = (pumpCursor + 1) % endpoints.size
|
|
||||||
countDown--
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun showProgressFor(nodes: List<SimulatedNode>) {
|
|
||||||
nodes.forEach { node ->
|
|
||||||
node.smm.changes.filter { it.addOrRemove == AddOrRemove.ADD }.first().subscribe {
|
|
||||||
linkProtocolProgress(node, it.logic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun linkProtocolProgress(node: SimulatedNode, protocol: ProtocolLogic<*>) {
|
|
||||||
val pt = protocol.progressTracker ?: return
|
|
||||||
pt.changes.subscribe { change: ProgressTracker.Change ->
|
|
||||||
// Runs on node thread.
|
|
||||||
_allProtocolSteps.onNext(Pair(node, change))
|
|
||||||
}
|
|
||||||
// This isn't technically a "change" but it helps with UIs to send this notification of the first step.
|
|
||||||
_allProtocolSteps.onNext(Pair(node, ProgressTracker.Change.Position(pt, pt.steps[1])))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected fun showConsensusFor(nodes: List<SimulatedNode>) {
|
|
||||||
val node = nodes.first()
|
|
||||||
node.smm.changes.filter { it.addOrRemove == com.r3corda.node.utilities.AddOrRemove.ADD }.first().subscribe {
|
|
||||||
linkConsensus(nodes, it.logic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun linkConsensus(nodes: Collection<SimulatedNode>, protocol: ProtocolLogic<*>) {
|
|
||||||
protocol.progressTracker?.changes?.subscribe { change: ProgressTracker.Change ->
|
|
||||||
// Runs on node thread.
|
|
||||||
if (protocol.progressTracker!!.currentStep == ProgressTracker.DONE) {
|
|
||||||
_doneSteps.onNext(nodes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val networkInitialisationFinished: ListenableFuture<*> =
|
|
||||||
Futures.allAsList(network.nodes.map { it.networkMapRegistrationFuture })
|
|
||||||
|
|
||||||
fun start(): ListenableFuture<Unit> {
|
|
||||||
network.startNodes()
|
|
||||||
// Wait for all the nodes to have finished registering with the network map service.
|
|
||||||
return networkInitialisationFinished.flatMap { startMainSimulation() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sub-classes should override this to trigger whatever they want to simulate. This method will be invoked once the
|
|
||||||
* network bringup has been simulated.
|
|
||||||
*/
|
|
||||||
protected open fun startMainSimulation(): ListenableFuture<Unit> {
|
|
||||||
return Futures.immediateFuture(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stop() {
|
|
||||||
network.stopNodes()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a function that returns a future, iterates that function with arguments like (0, 1), (1, 2), (2, 3) etc
|
|
||||||
* each time the returned future completes.
|
|
||||||
*/
|
|
||||||
fun startTradingCircle(tradeBetween: (indexA: Int, indexB: Int) -> ListenableFuture<*>) {
|
|
||||||
fun next(i: Int, j: Int) {
|
|
||||||
tradeBetween(i, j).then {
|
|
||||||
val ni = (i + 1) % banks.size
|
|
||||||
val nj = (j + 1) % banks.size
|
|
||||||
next(ni, nj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next(0, 1)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
package com.r3corda.simulation
|
|
||||||
|
|
||||||
import com.google.common.util.concurrent.Futures
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import com.r3corda.contracts.CommercialPaper
|
|
||||||
import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER
|
|
||||||
import com.r3corda.contracts.testing.fillWithSomeTestCash
|
|
||||||
import com.r3corda.core.contracts.DOLLARS
|
|
||||||
import com.r3corda.core.contracts.OwnableState
|
|
||||||
import com.r3corda.core.contracts.`issued by`
|
|
||||||
import com.r3corda.core.days
|
|
||||||
import com.r3corda.core.flatMap
|
|
||||||
import com.r3corda.core.node.recordTransactions
|
|
||||||
import com.r3corda.core.protocols.ProtocolStateMachine
|
|
||||||
import com.r3corda.core.seconds
|
|
||||||
import com.r3corda.core.transactions.SignedTransaction
|
|
||||||
import com.r3corda.protocols.TwoPartyTradeProtocol.Buyer
|
|
||||||
import com.r3corda.protocols.TwoPartyTradeProtocol.Seller
|
|
||||||
import com.r3corda.testing.initiateSingleShotProtocol
|
|
||||||
import com.r3corda.testing.node.InMemoryMessagingNetwork
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simulates a never ending series of trades that go pair-wise through the banks (e.g. A and B trade with each other,
|
|
||||||
* then B and C trade with each other, then C and A etc).
|
|
||||||
*/
|
|
||||||
class TradeSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwork.LatencyCalculator?) : Simulation(false, runAsync, latencyInjector) {
|
|
||||||
override fun startMainSimulation(): ListenableFuture<Unit> {
|
|
||||||
startTradingCircle { i, j -> tradeBetween(i, j) }
|
|
||||||
return Futures.immediateFailedFuture(UnsupportedOperationException("This future never completes"))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun tradeBetween(buyerBankIndex: Int, sellerBankIndex: Int): ListenableFuture<MutableList<SignedTransaction>> {
|
|
||||||
val buyer = banks[buyerBankIndex]
|
|
||||||
val seller = banks[sellerBankIndex]
|
|
||||||
|
|
||||||
buyer.services.fillWithSomeTestCash(1500.DOLLARS, notary.info.notaryIdentity)
|
|
||||||
|
|
||||||
val issuance = run {
|
|
||||||
val tx = CommercialPaper().generateIssue(seller.info.legalIdentity.ref(1, 2, 3), 1100.DOLLARS `issued by` DUMMY_CASH_ISSUER,
|
|
||||||
Instant.now() + 10.days, notary.info.notaryIdentity)
|
|
||||||
tx.setTime(Instant.now(), 30.seconds)
|
|
||||||
val notaryKey = notary.services.notaryIdentityKey
|
|
||||||
val sellerKey = seller.services.legalIdentityKey
|
|
||||||
tx.signWith(notaryKey)
|
|
||||||
tx.signWith(sellerKey)
|
|
||||||
tx.toSignedTransaction(true)
|
|
||||||
}
|
|
||||||
seller.services.recordTransactions(issuance)
|
|
||||||
|
|
||||||
val amount = 1000.DOLLARS
|
|
||||||
|
|
||||||
val buyerFuture = buyer.initiateSingleShotProtocol(Seller::class) {
|
|
||||||
Buyer(it, notary.info.notaryIdentity, amount, CommercialPaper.State::class.java)
|
|
||||||
}.flatMap { (it.psm as ProtocolStateMachine<SignedTransaction>).resultFuture }
|
|
||||||
|
|
||||||
val sellerKey = seller.services.legalIdentityKey
|
|
||||||
val sellerProtocol = Seller(
|
|
||||||
buyer.info.legalIdentity,
|
|
||||||
notary.info,
|
|
||||||
issuance.tx.outRef<OwnableState>(0),
|
|
||||||
amount,
|
|
||||||
sellerKey)
|
|
||||||
|
|
||||||
showConsensusFor(listOf(buyer, seller, notary))
|
|
||||||
showProgressFor(listOf(buyer, seller))
|
|
||||||
|
|
||||||
val sellerFuture = seller.services.startProtocol(sellerProtocol)
|
|
||||||
|
|
||||||
return Futures.successfulAsList(buyerFuture, sellerFuture)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
{
|
|
||||||
"fixedLeg": {
|
|
||||||
"fixedRatePayer": "Bank A",
|
|
||||||
"notional": {
|
|
||||||
"quantity": 2500000000,
|
|
||||||
"token": "EUR"
|
|
||||||
},
|
|
||||||
"paymentFrequency": "SemiAnnual",
|
|
||||||
"effectiveDate": "2016-03-11",
|
|
||||||
"effectiveDateAdjustment": null,
|
|
||||||
"terminationDate": "2026-03-11",
|
|
||||||
"terminationDateAdjustment": null,
|
|
||||||
"fixedRate": {
|
|
||||||
"ratioUnit": {
|
|
||||||
"value": "0.01676"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dayCountBasisDay": "D30",
|
|
||||||
"dayCountBasisYear": "Y360",
|
|
||||||
"rollConvention": "ModifiedFollowing",
|
|
||||||
"dayInMonth": 10,
|
|
||||||
"paymentRule": "InArrears",
|
|
||||||
"paymentDelay": 0,
|
|
||||||
"paymentCalendar": "London",
|
|
||||||
"interestPeriodAdjustment": "Adjusted"
|
|
||||||
},
|
|
||||||
"floatingLeg": {
|
|
||||||
"floatingRatePayer": "Bank B",
|
|
||||||
"notional": {
|
|
||||||
"quantity": 2500000000,
|
|
||||||
"token": "EUR"
|
|
||||||
},
|
|
||||||
"paymentFrequency": "Quarterly",
|
|
||||||
"effectiveDate": "2016-03-11",
|
|
||||||
"effectiveDateAdjustment": null,
|
|
||||||
"terminationDate": "2026-03-11",
|
|
||||||
"terminationDateAdjustment": null,
|
|
||||||
"dayCountBasisDay": "D30",
|
|
||||||
"dayCountBasisYear": "Y360",
|
|
||||||
"rollConvention": "ModifiedFollowing",
|
|
||||||
"fixingRollConvention": "ModifiedFollowing",
|
|
||||||
"dayInMonth": 10,
|
|
||||||
"resetDayInMonth": 10,
|
|
||||||
"paymentRule": "InArrears",
|
|
||||||
"paymentDelay": 0,
|
|
||||||
"paymentCalendar": [ "London" ],
|
|
||||||
"interestPeriodAdjustment": "Adjusted",
|
|
||||||
"fixingPeriodOffset": 2,
|
|
||||||
"resetRule": "InAdvance",
|
|
||||||
"fixingsPerPayment": "Quarterly",
|
|
||||||
"fixingCalendar": [ "NewYork" ],
|
|
||||||
"index": "ICE LIBOR",
|
|
||||||
"indexSource": "Rates Service Provider",
|
|
||||||
"indexTenor": {
|
|
||||||
"name": "3M"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"calculation": {
|
|
||||||
"expression": "( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
|
|
||||||
"floatingLegPaymentSchedule": {
|
|
||||||
},
|
|
||||||
"fixedLegPaymentSchedule": {
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"common": {
|
|
||||||
"baseCurrency": "EUR",
|
|
||||||
"eligibleCurrency": "EUR",
|
|
||||||
"eligibleCreditSupport": "Cash in an Eligible Currency",
|
|
||||||
"independentAmounts": {
|
|
||||||
"quantity": 0,
|
|
||||||
"token": "EUR"
|
|
||||||
},
|
|
||||||
"threshold": {
|
|
||||||
"quantity": 0,
|
|
||||||
"token": "EUR"
|
|
||||||
},
|
|
||||||
"minimumTransferAmount": {
|
|
||||||
"quantity": 25000000,
|
|
||||||
"token": "EUR"
|
|
||||||
},
|
|
||||||
"rounding": {
|
|
||||||
"quantity": 1000000,
|
|
||||||
"token": "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": {
|
|
||||||
"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",
|
|
||||||
"tradeID": "tradeXXX",
|
|
||||||
"hashLegalDocs": "put hash here"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,228 +0,0 @@
|
|||||||
# Some pretend noddy rate fixes, for the interest rate oracles.
|
|
||||||
|
|
||||||
ICE LIBOR 2016-03-16 1M = 0.678
|
|
||||||
ICE LIBOR 2016-03-16 2M = 0.655
|
|
||||||
EURIBOR 2016-03-15 1M = 0.123
|
|
||||||
EURIBOR 2016-03-15 2M = 0.111
|
|
||||||
|
|
||||||
# Previous fixings
|
|
||||||
ICE LIBOR 2016-03-07 3M = 0.0063516
|
|
||||||
ICE LIBOR 2016-03-07 3M = 0.0063516
|
|
||||||
ICE LIBOR 2016-03-08 3M = 0.0063517
|
|
||||||
ICE LIBOR 2016-03-09 3M = 0.0063518
|
|
||||||
ICE LIBOR 2016-03-10 3M = 0.0063519
|
|
||||||
ICE LIBOR 2016-06-06 3M = 0.0063520
|
|
||||||
ICE LIBOR 2016-06-07 3M = 0.0063521
|
|
||||||
ICE LIBOR 2016-06-08 3M = 0.0063522
|
|
||||||
ICE LIBOR 2016-06-09 3M = 0.0063523
|
|
||||||
ICE LIBOR 2016-06-10 3M = 0.0063524
|
|
||||||
ICE LIBOR 2016-09-06 3M = 0.0063525
|
|
||||||
ICE LIBOR 2016-09-07 3M = 0.0063526
|
|
||||||
ICE LIBOR 2016-09-08 3M = 0.0063527
|
|
||||||
ICE LIBOR 2016-09-09 3M = 0.0063528
|
|
||||||
ICE LIBOR 2016-09-10 3M = 0.0063529
|
|
||||||
ICE LIBOR 2016-12-06 3M = 0.0063530
|
|
||||||
ICE LIBOR 2016-12-07 3M = 0.0063531
|
|
||||||
ICE LIBOR 2016-12-08 3M = 0.0063532
|
|
||||||
ICE LIBOR 2016-12-09 3M = 0.0063533
|
|
||||||
ICE LIBOR 2016-12-10 3M = 0.0063534
|
|
||||||
ICE LIBOR 2017-03-06 3M = 0.0063535
|
|
||||||
ICE LIBOR 2017-03-07 3M = 0.0063536
|
|
||||||
ICE LIBOR 2017-03-08 3M = 0.0063537
|
|
||||||
ICE LIBOR 2017-03-09 3M = 0.0063538
|
|
||||||
ICE LIBOR 2017-03-10 3M = 0.0063539
|
|
||||||
ICE LIBOR 2017-06-06 3M = 0.0063540
|
|
||||||
ICE LIBOR 2017-06-07 3M = 0.0063541
|
|
||||||
ICE LIBOR 2017-06-08 3M = 0.0063542
|
|
||||||
ICE LIBOR 2017-06-09 3M = 0.0063543
|
|
||||||
ICE LIBOR 2017-06-10 3M = 0.0063544
|
|
||||||
ICE LIBOR 2017-09-06 3M = 0.0063545
|
|
||||||
ICE LIBOR 2017-09-07 3M = 0.0063546
|
|
||||||
ICE LIBOR 2017-09-08 3M = 0.0063547
|
|
||||||
ICE LIBOR 2017-09-09 3M = 0.0063548
|
|
||||||
ICE LIBOR 2017-09-10 3M = 0.0063549
|
|
||||||
ICE LIBOR 2017-12-06 3M = 0.0063550
|
|
||||||
ICE LIBOR 2017-12-07 3M = 0.0063551
|
|
||||||
ICE LIBOR 2017-12-08 3M = 0.0063552
|
|
||||||
ICE LIBOR 2017-12-09 3M = 0.0063553
|
|
||||||
ICE LIBOR 2017-12-10 3M = 0.0063554
|
|
||||||
ICE LIBOR 2018-03-06 3M = 0.0063555
|
|
||||||
ICE LIBOR 2018-03-07 3M = 0.0063556
|
|
||||||
ICE LIBOR 2018-03-08 3M = 0.0063557
|
|
||||||
ICE LIBOR 2018-03-09 3M = 0.0063558
|
|
||||||
ICE LIBOR 2018-03-10 3M = 0.0063559
|
|
||||||
ICE LIBOR 2018-06-06 3M = 0.0063560
|
|
||||||
ICE LIBOR 2018-06-07 3M = 0.0063561
|
|
||||||
ICE LIBOR 2018-06-08 3M = 0.0063562
|
|
||||||
ICE LIBOR 2018-06-09 3M = 0.0063563
|
|
||||||
ICE LIBOR 2018-06-10 3M = 0.0063564
|
|
||||||
ICE LIBOR 2018-09-06 3M = 0.0063565
|
|
||||||
ICE LIBOR 2018-09-07 3M = 0.0063566
|
|
||||||
ICE LIBOR 2018-09-08 3M = 0.0063567
|
|
||||||
ICE LIBOR 2018-09-09 3M = 0.0063568
|
|
||||||
ICE LIBOR 2018-09-10 3M = 0.0063569
|
|
||||||
ICE LIBOR 2018-12-06 3M = 0.0063570
|
|
||||||
ICE LIBOR 2018-12-07 3M = 0.0063571
|
|
||||||
ICE LIBOR 2018-12-08 3M = 0.0063572
|
|
||||||
ICE LIBOR 2018-12-09 3M = 0.0063573
|
|
||||||
ICE LIBOR 2018-12-10 3M = 0.0063574
|
|
||||||
ICE LIBOR 2019-03-06 3M = 0.0063575
|
|
||||||
ICE LIBOR 2019-03-07 3M = 0.0063576
|
|
||||||
ICE LIBOR 2019-03-08 3M = 0.0063577
|
|
||||||
ICE LIBOR 2019-03-09 3M = 0.0063578
|
|
||||||
ICE LIBOR 2019-03-10 3M = 0.0063579
|
|
||||||
ICE LIBOR 2019-06-06 3M = 0.0063580
|
|
||||||
ICE LIBOR 2019-06-07 3M = 0.0063581
|
|
||||||
ICE LIBOR 2019-06-08 3M = 0.0063582
|
|
||||||
ICE LIBOR 2019-06-09 3M = 0.0063583
|
|
||||||
ICE LIBOR 2019-06-10 3M = 0.0063584
|
|
||||||
ICE LIBOR 2019-09-06 3M = 0.0063585
|
|
||||||
ICE LIBOR 2019-09-07 3M = 0.0063586
|
|
||||||
ICE LIBOR 2019-09-08 3M = 0.0063587
|
|
||||||
ICE LIBOR 2019-09-09 3M = 0.0063588
|
|
||||||
ICE LIBOR 2019-09-10 3M = 0.0063589
|
|
||||||
ICE LIBOR 2019-12-06 3M = 0.0063590
|
|
||||||
ICE LIBOR 2019-12-07 3M = 0.0063591
|
|
||||||
ICE LIBOR 2019-12-08 3M = 0.0063592
|
|
||||||
ICE LIBOR 2019-12-09 3M = 0.0063593
|
|
||||||
ICE LIBOR 2019-12-10 3M = 0.0063594
|
|
||||||
ICE LIBOR 2020-03-06 3M = 0.0063595
|
|
||||||
ICE LIBOR 2020-03-07 3M = 0.0063596
|
|
||||||
ICE LIBOR 2020-03-08 3M = 0.0063597
|
|
||||||
ICE LIBOR 2020-03-09 3M = 0.0063598
|
|
||||||
ICE LIBOR 2020-03-10 3M = 0.0063599
|
|
||||||
ICE LIBOR 2020-06-06 3M = 0.0063600
|
|
||||||
ICE LIBOR 2020-06-07 3M = 0.0063601
|
|
||||||
ICE LIBOR 2020-06-08 3M = 0.0063602
|
|
||||||
ICE LIBOR 2020-06-09 3M = 0.0063603
|
|
||||||
ICE LIBOR 2020-06-10 3M = 0.0063604
|
|
||||||
ICE LIBOR 2020-09-06 3M = 0.0063605
|
|
||||||
ICE LIBOR 2020-09-07 3M = 0.0063606
|
|
||||||
ICE LIBOR 2020-09-08 3M = 0.0063607
|
|
||||||
ICE LIBOR 2020-09-09 3M = 0.0063608
|
|
||||||
ICE LIBOR 2020-09-10 3M = 0.0063609
|
|
||||||
ICE LIBOR 2020-12-06 3M = 0.0063610
|
|
||||||
ICE LIBOR 2020-12-07 3M = 0.0063611
|
|
||||||
ICE LIBOR 2020-12-08 3M = 0.0063612
|
|
||||||
ICE LIBOR 2020-12-09 3M = 0.0063613
|
|
||||||
ICE LIBOR 2020-12-10 3M = 0.0063614
|
|
||||||
ICE LIBOR 2021-03-06 3M = 0.0063615
|
|
||||||
ICE LIBOR 2021-03-07 3M = 0.0063616
|
|
||||||
ICE LIBOR 2021-03-08 3M = 0.0063617
|
|
||||||
ICE LIBOR 2021-03-09 3M = 0.0063618
|
|
||||||
ICE LIBOR 2021-03-10 3M = 0.0063619
|
|
||||||
ICE LIBOR 2021-06-06 3M = 0.0063620
|
|
||||||
ICE LIBOR 2021-06-07 3M = 0.0063621
|
|
||||||
ICE LIBOR 2021-06-08 3M = 0.0063622
|
|
||||||
ICE LIBOR 2021-06-09 3M = 0.0063623
|
|
||||||
ICE LIBOR 2021-06-10 3M = 0.0063624
|
|
||||||
ICE LIBOR 2021-09-06 3M = 0.0063625
|
|
||||||
ICE LIBOR 2021-09-07 3M = 0.0063626
|
|
||||||
ICE LIBOR 2021-09-08 3M = 0.0063627
|
|
||||||
ICE LIBOR 2021-09-09 3M = 0.0063628
|
|
||||||
ICE LIBOR 2021-09-10 3M = 0.0063629
|
|
||||||
ICE LIBOR 2021-12-06 3M = 0.0063630
|
|
||||||
ICE LIBOR 2021-12-07 3M = 0.0063631
|
|
||||||
ICE LIBOR 2021-12-08 3M = 0.0063632
|
|
||||||
ICE LIBOR 2021-12-09 3M = 0.0063633
|
|
||||||
ICE LIBOR 2021-12-10 3M = 0.0063634
|
|
||||||
ICE LIBOR 2022-03-06 3M = 0.0063635
|
|
||||||
ICE LIBOR 2022-03-07 3M = 0.0063636
|
|
||||||
ICE LIBOR 2022-03-08 3M = 0.0063637
|
|
||||||
ICE LIBOR 2022-03-09 3M = 0.0063638
|
|
||||||
ICE LIBOR 2022-03-10 3M = 0.0063639
|
|
||||||
ICE LIBOR 2022-06-06 3M = 0.0063640
|
|
||||||
ICE LIBOR 2022-06-07 3M = 0.0063641
|
|
||||||
ICE LIBOR 2022-06-08 3M = 0.0063642
|
|
||||||
ICE LIBOR 2022-06-09 3M = 0.0063643
|
|
||||||
ICE LIBOR 2022-06-10 3M = 0.0063644
|
|
||||||
ICE LIBOR 2022-09-06 3M = 0.0063645
|
|
||||||
ICE LIBOR 2022-09-07 3M = 0.0063646
|
|
||||||
ICE LIBOR 2022-09-08 3M = 0.0063647
|
|
||||||
ICE LIBOR 2022-09-09 3M = 0.0063648
|
|
||||||
ICE LIBOR 2022-09-10 3M = 0.0063649
|
|
||||||
ICE LIBOR 2022-12-06 3M = 0.0063650
|
|
||||||
ICE LIBOR 2022-12-07 3M = 0.0063651
|
|
||||||
ICE LIBOR 2022-12-08 3M = 0.0063652
|
|
||||||
ICE LIBOR 2022-12-09 3M = 0.0063653
|
|
||||||
ICE LIBOR 2022-12-10 3M = 0.0063654
|
|
||||||
ICE LIBOR 2023-03-06 3M = 0.0063655
|
|
||||||
ICE LIBOR 2023-03-07 3M = 0.0063656
|
|
||||||
ICE LIBOR 2023-03-08 3M = 0.0063657
|
|
||||||
ICE LIBOR 2023-03-09 3M = 0.0063658
|
|
||||||
ICE LIBOR 2023-03-10 3M = 0.0063659
|
|
||||||
ICE LIBOR 2023-06-06 3M = 0.0063660
|
|
||||||
ICE LIBOR 2023-06-07 3M = 0.0063661
|
|
||||||
ICE LIBOR 2023-06-08 3M = 0.0063662
|
|
||||||
ICE LIBOR 2023-06-09 3M = 0.0063663
|
|
||||||
ICE LIBOR 2023-06-10 3M = 0.0063664
|
|
||||||
ICE LIBOR 2023-09-06 3M = 0.0063665
|
|
||||||
ICE LIBOR 2023-09-07 3M = 0.0063666
|
|
||||||
ICE LIBOR 2023-09-08 3M = 0.0063667
|
|
||||||
ICE LIBOR 2023-09-09 3M = 0.0063668
|
|
||||||
ICE LIBOR 2023-09-10 3M = 0.0063669
|
|
||||||
ICE LIBOR 2023-12-06 3M = 0.0063670
|
|
||||||
ICE LIBOR 2023-12-07 3M = 0.0063671
|
|
||||||
ICE LIBOR 2023-12-08 3M = 0.0063672
|
|
||||||
ICE LIBOR 2023-12-09 3M = 0.0063673
|
|
||||||
ICE LIBOR 2023-12-10 3M = 0.0063674
|
|
||||||
ICE LIBOR 2024-03-06 3M = 0.0063675
|
|
||||||
ICE LIBOR 2024-03-07 3M = 0.0063676
|
|
||||||
ICE LIBOR 2024-03-08 3M = 0.0063677
|
|
||||||
ICE LIBOR 2024-03-09 3M = 0.0063678
|
|
||||||
ICE LIBOR 2024-03-10 3M = 0.0063679
|
|
||||||
ICE LIBOR 2024-06-06 3M = 0.0063680
|
|
||||||
ICE LIBOR 2024-06-07 3M = 0.0063681
|
|
||||||
ICE LIBOR 2024-06-08 3M = 0.0063682
|
|
||||||
ICE LIBOR 2024-06-09 3M = 0.0063683
|
|
||||||
ICE LIBOR 2024-06-10 3M = 0.0063684
|
|
||||||
ICE LIBOR 2024-09-06 3M = 0.0063685
|
|
||||||
ICE LIBOR 2024-09-07 3M = 0.0063686
|
|
||||||
ICE LIBOR 2024-09-08 3M = 0.0063687
|
|
||||||
ICE LIBOR 2024-09-09 3M = 0.0063688
|
|
||||||
ICE LIBOR 2024-09-10 3M = 0.0063689
|
|
||||||
ICE LIBOR 2024-12-06 3M = 0.0063690
|
|
||||||
ICE LIBOR 2024-12-07 3M = 0.0063691
|
|
||||||
ICE LIBOR 2024-12-08 3M = 0.0063692
|
|
||||||
ICE LIBOR 2024-12-09 3M = 0.0063693
|
|
||||||
ICE LIBOR 2024-12-10 3M = 0.0063694
|
|
||||||
ICE LIBOR 2025-03-06 3M = 0.0063695
|
|
||||||
ICE LIBOR 2025-03-07 3M = 0.0063696
|
|
||||||
ICE LIBOR 2025-03-08 3M = 0.0063697
|
|
||||||
ICE LIBOR 2025-03-09 3M = 0.0063698
|
|
||||||
ICE LIBOR 2025-03-10 3M = 0.0063699
|
|
||||||
ICE LIBOR 2025-06-06 3M = 0.0063700
|
|
||||||
ICE LIBOR 2025-06-07 3M = 0.0063701
|
|
||||||
ICE LIBOR 2025-06-08 3M = 0.0063702
|
|
||||||
ICE LIBOR 2025-06-09 3M = 0.0063703
|
|
||||||
ICE LIBOR 2025-06-10 3M = 0.0063704
|
|
||||||
ICE LIBOR 2025-09-06 3M = 0.0063705
|
|
||||||
ICE LIBOR 2025-09-07 3M = 0.0063706
|
|
||||||
ICE LIBOR 2025-09-08 3M = 0.0063707
|
|
||||||
ICE LIBOR 2025-09-09 3M = 0.0063708
|
|
||||||
ICE LIBOR 2025-09-10 3M = 0.0063709
|
|
||||||
ICE LIBOR 2025-12-06 3M = 0.0063710
|
|
||||||
ICE LIBOR 2025-12-07 3M = 0.0063711
|
|
||||||
ICE LIBOR 2025-12-08 3M = 0.0063712
|
|
||||||
ICE LIBOR 2025-12-09 3M = 0.0063713
|
|
||||||
ICE LIBOR 2025-12-10 3M = 0.0063714
|
|
||||||
ICE LIBOR 2026-03-06 3M = 0.0063715
|
|
||||||
ICE LIBOR 2026-03-07 3M = 0.0063716
|
|
||||||
ICE LIBOR 2026-03-08 3M = 0.0063717
|
|
||||||
ICE LIBOR 2026-03-09 3M = 0.0063718
|
|
||||||
ICE LIBOR 2026-03-10 3M = 0.0063719
|
|
||||||
ICE LIBOR 2026-06-06 3M = 0.0063720
|
|
||||||
ICE LIBOR 2026-06-07 3M = 0.0063721
|
|
||||||
ICE LIBOR 2026-06-08 3M = 0.0063722
|
|
||||||
ICE LIBOR 2026-06-09 3M = 0.0063723
|
|
||||||
ICE LIBOR 2026-06-10 3M = 0.0063724
|
|
||||||
ICE LIBOR 2026-09-06 3M = 0.0063725
|
|
||||||
ICE LIBOR 2026-09-07 3M = 0.0063726
|
|
||||||
ICE LIBOR 2026-09-08 3M = 0.0063727
|
|
||||||
ICE LIBOR 2026-09-09 3M = 0.0063728
|
|
||||||
ICE LIBOR 2026-09-10 3M = 0.0063729
|
|
||||||
ICE LIBOR 2026-12-06 3M = 0.0063730
|
|
||||||
ICE LIBOR 2026-12-07 3M = 0.0063731
|
|
||||||
ICE LIBOR 2026-12-08 3M = 0.0063732
|
|
||||||
ICE LIBOR 2026-12-09 3M = 0.0063733
|
|
||||||
ICE LIBOR 2026-12-10 3M = 0.0063734
|
|
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"directory": "js/bower_components"
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
bower_components
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "www",
|
|
||||||
"description": "",
|
|
||||||
"main": "",
|
|
||||||
"license": "MIT",
|
|
||||||
"homepage": "",
|
|
||||||
"private": true,
|
|
||||||
"ignore": [
|
|
||||||
"**/.*",
|
|
||||||
"node_modules",
|
|
||||||
"bower_components",
|
|
||||||
"test",
|
|
||||||
"tests"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"angular": "^1.5.6",
|
|
||||||
"jquery": "^3.0.0",
|
|
||||||
"angular-route": "^1.5.7",
|
|
||||||
"lodash": "^4.13.1",
|
|
||||||
"angular-fcsa-number": "^1.5.3",
|
|
||||||
"jquery.maskedinput": "^1.4.1",
|
|
||||||
"requirejs": "^2.2.0",
|
|
||||||
"semantic": "semantic-ui#^2.2.2"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
#fixedleg tbody tr:nth-child(odd) {
|
|
||||||
background-color: #EEFAEE;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createfixedleg, #fixedleg tbody tr:nth-child(even), #fixedleg thead th {
|
|
||||||
background-color: #D0FAD0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#floatingleg tbody tr:nth-child(odd) {
|
|
||||||
background-color: #FAEEEE;
|
|
||||||
}
|
|
||||||
|
|
||||||
#createfloatingleg, #floatingleg tbody tr:nth-child(even), #floatingleg thead th {
|
|
||||||
background-color: #FAD0D0;
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<!-- Standard Meta -->
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
||||||
|
|
||||||
<!-- Site Properties -->
|
|
||||||
<title>IRS Demo Viewer</title>
|
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="http://r3cev.com/favicon.ico" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="js/bower_components/semantic/dist/semantic.css" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="css/main.css" />
|
|
||||||
|
|
||||||
<script data-main="js/require-config" src="js/bower_components/requirejs/require.js"></script>
|
|
||||||
</head>
|
|
||||||
<body ng-controller="HomeController">
|
|
||||||
<div class="ui attached inverted menu">
|
|
||||||
<span class="header item"><a href="#/">IRS Web Demo</a></span>
|
|
||||||
<span class="item"><a href="#/">Recent Deals</a></span>
|
|
||||||
<span class="item"><a href="#/create-deal">Create Deal</a></span>
|
|
||||||
</div>
|
|
||||||
<div class="ui container">
|
|
||||||
<div id="loading" class="ui modal">
|
|
||||||
<div class="ui text loader">Loading</div>
|
|
||||||
</div>
|
|
||||||
<div ng-view></div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,79 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
define(['viewmodel/FixedRate'], (fixedRateViewModel) => {
|
|
||||||
let calculationModel = {
|
|
||||||
expression: "( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
|
|
||||||
floatingLegPaymentSchedule: {
|
|
||||||
|
|
||||||
},
|
|
||||||
fixedLegPaymentSchedule: {
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let indexLookup = {
|
|
||||||
"GBP": "ICE LIBOR",
|
|
||||||
"USD": "ICE LIBOR",
|
|
||||||
"EUR": "EURIBOR"
|
|
||||||
};
|
|
||||||
|
|
||||||
let calendarLookup = {
|
|
||||||
"GBP": "London",
|
|
||||||
"USD": "NewYork",
|
|
||||||
"EUR": "London"
|
|
||||||
};
|
|
||||||
|
|
||||||
let Deal = function(dealViewModel) {
|
|
||||||
let now = new Date();
|
|
||||||
let tradeId = `T${now.getUTCFullYear()}-${now.getUTCMonth()}-${now.getUTCDate()}.${now.getUTCHours()}:${now.getUTCMinutes()}:${now.getUTCSeconds()}:${now.getUTCMilliseconds()}`;
|
|
||||||
|
|
||||||
this.toJson = () => {
|
|
||||||
let fixedLeg = {};
|
|
||||||
let floatingLeg = {};
|
|
||||||
let 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.token = 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.token = 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;
|
|
||||||
|
|
||||||
let json = {
|
|
||||||
fixedLeg: fixedLeg,
|
|
||||||
floatingLeg: floatingLeg,
|
|
||||||
calculation: calculationModel,
|
|
||||||
common: common
|
|
||||||
};
|
|
||||||
|
|
||||||
return json;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return Deal;
|
|
||||||
});
|
|
@ -1,24 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
function formatDateForNode(date) {
|
|
||||||
// Produces yyyy-dd-mm. JS is missing proper date formatting libs
|
|
||||||
let day = ("0" + (date.getDate())).slice(-2);
|
|
||||||
let month = ("0" + (date.getMonth() + 1)).slice(-2);
|
|
||||||
return `${date.getFullYear()}-${month}-${day}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDateForAngular(dateStr) {
|
|
||||||
let parts = dateStr.split("-");
|
|
||||||
return new Date(parts[0], parts[1], parts[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
define([
|
|
||||||
'angular',
|
|
||||||
'angularRoute',
|
|
||||||
'jquery',
|
|
||||||
'fcsaNumber',
|
|
||||||
'semantic'
|
|
||||||
], (angular, angularRoute, $, fcsaNumber, semantic) => {
|
|
||||||
angular.module('irsViewer', ['ngRoute', 'fcsa-number']);
|
|
||||||
requirejs(['routes']);
|
|
||||||
});
|
|
@ -1,36 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
define([
|
|
||||||
'angular',
|
|
||||||
'maskedInput',
|
|
||||||
'utils/semantic',
|
|
||||||
'utils/dayCountBasisLookup',
|
|
||||||
'services/NodeApi',
|
|
||||||
'Deal'
|
|
||||||
], (angular, maskedInput, semantic, dayCountBasisLookup, nodeApi, Deal) => {
|
|
||||||
angular.module('irsViewer').controller('CreateDealController', function CreateDealController($http, $scope, $location, nodeService) {
|
|
||||||
semantic.init($scope, nodeService.isLoading);
|
|
||||||
|
|
||||||
$scope.dayCountBasisLookup = dayCountBasisLookup;
|
|
||||||
$scope.deal = nodeService.newDeal();
|
|
||||||
$scope.createDeal = () => {
|
|
||||||
nodeService.createDeal(new Deal($scope.deal))
|
|
||||||
.then((tradeId) => $location.path('#/deal/' + tradeId), (resp) => {
|
|
||||||
$scope.formError = resp.data;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
$('input.percent').mask("9.999999%", {placeholder: "", autoclear: false});
|
|
||||||
$('#swapirscolumns').click(() => {
|
|
||||||
let first = $('#irscolumns .irscolumn:eq( 0 )');
|
|
||||||
let last = $('#irscolumns .irscolumn:eq( 1 )');
|
|
||||||
first.before(last);
|
|
||||||
|
|
||||||
let swapPayers = () => {
|
|
||||||
let tmp = $scope.deal.floatingLeg.floatingRatePayer;
|
|
||||||
$scope.deal.floatingLeg.floatingRatePayer = $scope.deal.fixedLeg.fixedRatePayer;
|
|
||||||
$scope.deal.fixedLeg.fixedRatePayer = tmp;
|
|
||||||
};
|
|
||||||
$scope.$apply(swapPayers);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,9 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
define(['angular', 'utils/semantic', 'services/NodeApi'], (angular, semantic, nodeApi) => {
|
|
||||||
angular.module('irsViewer').controller('DealController', function DealController($http, $scope, $routeParams, nodeService) {
|
|
||||||
semantic.init($scope, nodeService.isLoading);
|
|
||||||
|
|
||||||
nodeService.getDeal($routeParams.dealId).then((deal) => $scope.deal = deal);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,23 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
define(['angular', 'utils/semantic', 'services/NodeApi'], (angular, semantic, nodeApi) => {
|
|
||||||
angular.module('irsViewer').controller('HomeController', function HomeController($http, $scope, nodeService) {
|
|
||||||
semantic.addLoadingModal($scope, nodeService.isLoading);
|
|
||||||
|
|
||||||
let handleHttpFail = (resp) => {
|
|
||||||
$scope.httpError = resp.data
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.infoMsg = "";
|
|
||||||
$scope.errorText = "";
|
|
||||||
$scope.date = { "year": "...", "month": "...", "day": "..." };
|
|
||||||
$scope.updateDate = (type) => {
|
|
||||||
nodeService.updateDate(type).then((newDate) => {
|
|
||||||
$scope.date = newDate
|
|
||||||
}, handleHttpFail);
|
|
||||||
};
|
|
||||||
|
|
||||||
nodeService.getDate().then((date) => $scope.date = date);
|
|
||||||
nodeService.getDeals().then((deals) => $scope.deals = deals);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,28 +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/dist/jquery',
|
|
||||||
semantic: 'bower_components/semantic/dist/semantic',
|
|
||||||
lodash: 'bower_components/lodash/lodash',
|
|
||||||
maskedInput: 'bower_components/jquery.maskedinput/dist/jquery.maskedinput'
|
|
||||||
},
|
|
||||||
shim: {
|
|
||||||
'angular' : {'exports' : 'angular'},
|
|
||||||
'angularRoute': ['angular'],
|
|
||||||
'fcsaNumber': ['angular'],
|
|
||||||
'semantic': ['jquery'],
|
|
||||||
'maskedInput': ['jquery']
|
|
||||||
},
|
|
||||||
priority: [
|
|
||||||
"angular"
|
|
||||||
],
|
|
||||||
baseUrl: 'js',
|
|
||||||
});
|
|
||||||
|
|
||||||
require(['angular', 'app'], (angular, app) => {
|
|
||||||
|
|
||||||
});
|
|
@ -1,33 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
define([
|
|
||||||
'angular',
|
|
||||||
'controllers/Home',
|
|
||||||
'controllers/Deal',
|
|
||||||
'controllers/CreateDeal'
|
|
||||||
], (angular) => {
|
|
||||||
angular.module('irsViewer').config(($routeProvider, $locationProvider) => {
|
|
||||||
$routeProvider
|
|
||||||
.when('/', {
|
|
||||||
controller: 'HomeController',
|
|
||||||
templateUrl: 'view/home.html'
|
|
||||||
})
|
|
||||||
.when('/deal/:dealId', {
|
|
||||||
controller: 'DealController',
|
|
||||||
templateUrl: 'view/deal.html'
|
|
||||||
})
|
|
||||||
.when('/party/:partyId', {
|
|
||||||
templateUrl: 'view/party.html'
|
|
||||||
})
|
|
||||||
.when('/create-deal', {
|
|
||||||
controller: 'CreateDealController',
|
|
||||||
templateUrl: 'view/create-deal.html'
|
|
||||||
})
|
|
||||||
.otherwise({redirectTo: '/'});
|
|
||||||
});
|
|
||||||
|
|
||||||
angular.element().ready(function() {
|
|
||||||
// bootstrap the app manually
|
|
||||||
angular.bootstrap(document, ['irsViewer']);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,99 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => {
|
|
||||||
angular.module('irsViewer').factory('nodeService', ($http) => {
|
|
||||||
return new (function() {
|
|
||||||
let date = new Date(2016, 0, 1, 0, 0, 0);
|
|
||||||
let curLoading = {};
|
|
||||||
|
|
||||||
let load = (type, promise) => {
|
|
||||||
curLoading[type] = true;
|
|
||||||
return promise.then((arg) => {
|
|
||||||
curLoading[type] = false;
|
|
||||||
return arg;
|
|
||||||
}, (arg) => {
|
|
||||||
curLoading[type] = false;
|
|
||||||
throw arg;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let changeDateOnNode = (newDate) => {
|
|
||||||
const dateStr = formatDateForNode(newDate);
|
|
||||||
let endpoint = '/api/irs/demodate';
|
|
||||||
return load('date', $http.put(endpoint, "\"" + dateStr + "\"")).then((resp) => {
|
|
||||||
date = newDate;
|
|
||||||
return this.getDateModel(date);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getDate = () => {
|
|
||||||
return load('date', $http.get('/api/irs/demodate')).then((resp) => {
|
|
||||||
const parts = resp.data.split("-");
|
|
||||||
date = new Date(parts[0], parts[1] - 1, parts[2]); // JS uses 0 based months
|
|
||||||
return this.getDateModel(date);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.updateDate = (type) => {
|
|
||||||
let 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 = () => {
|
|
||||||
return load('deals', $http.get('/api/irs/deals')).then((resp) => {
|
|
||||||
return resp.data.reverse();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getDeal = (dealId) => {
|
|
||||||
return load('deal' + dealId, $http.get('/api/irs/deals/' + dealId)).then((resp) => {
|
|
||||||
// Do some data modification to simplify the model
|
|
||||||
let deal = resp.data;
|
|
||||||
deal.fixedLeg.fixedRate.value = (deal.fixedLeg.fixedRate.ratioUnit.value * 100).toString().slice(0, 6);
|
|
||||||
return deal;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.getDateModel = (date) => {
|
|
||||||
return {
|
|
||||||
"year": date.getFullYear(),
|
|
||||||
"month": date.getMonth() + 1, // JS uses 0 based months
|
|
||||||
"day": date.getDate()
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
this.isLoading = () => {
|
|
||||||
return _.reduce(Object.keys(curLoading), (last, key) => {
|
|
||||||
return (last || curLoading[key]);
|
|
||||||
}, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.newDeal = () => {
|
|
||||||
return dealViewModel;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.createDeal = (deal) => {
|
|
||||||
return load('create-deal', $http.post('/api/irs/deals', deal.toJson()))
|
|
||||||
.then((resp) => {
|
|
||||||
return deal.tradeId;
|
|
||||||
}, (resp) => {
|
|
||||||
throw resp;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,34 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
define([], () => {
|
|
||||||
return {
|
|
||||||
"30/360": {
|
|
||||||
"day": "D30",
|
|
||||||
"year": "Y360"
|
|
||||||
},
|
|
||||||
"30E/360": {
|
|
||||||
"day": "D30E",
|
|
||||||
"year": "Y360"
|
|
||||||
},
|
|
||||||
"ACT/360": {
|
|
||||||
"day": "DActual",
|
|
||||||
"year": "Y360"
|
|
||||||
},
|
|
||||||
"ACT/365 Fixed": {
|
|
||||||
"day": "DActual",
|
|
||||||
"year": "Y365F"
|
|
||||||
},
|
|
||||||
"ACT/365 L": {
|
|
||||||
"day": "DActual",
|
|
||||||
"year": "Y365L"
|
|
||||||
},
|
|
||||||
"ACT/ACT ISDA": {
|
|
||||||
"day": "DActual",
|
|
||||||
"year": "YISDA"
|
|
||||||
},
|
|
||||||
"ACT/ACT ICMA": {
|
|
||||||
"day": "DActual",
|
|
||||||
"year": "YICMA"
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
@ -1,22 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
define(['jquery', 'semantic'], ($, semantic) => {
|
|
||||||
return {
|
|
||||||
init: function($scope, loadingFunc) {
|
|
||||||
$('.ui.accordion').accordion();
|
|
||||||
$('.ui.dropdown').dropdown();
|
|
||||||
$('.ui.sticky').sticky();
|
|
||||||
|
|
||||||
this.addLoadingModal($scope, loadingFunc);
|
|
||||||
},
|
|
||||||
addLoadingModal: ($scope, loadingFunc) => {
|
|
||||||
$scope.$watch(loadingFunc, (newVal) => {
|
|
||||||
if(newVal === true) {
|
|
||||||
$('#loading').modal('setting', 'closable', false).modal('show');
|
|
||||||
} else {
|
|
||||||
$('#loading').modal('hide');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
@ -1,38 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
define([], () => {
|
|
||||||
return {
|
|
||||||
baseCurrency: "USD",
|
|
||||||
effectiveDate: new Date(2016, 2, 11),
|
|
||||||
terminationDate: new Date(2026, 2, 11),
|
|
||||||
eligibleCreditSupport: "Cash in an Eligible Currency",
|
|
||||||
independentAmounts: {
|
|
||||||
quantity: 0
|
|
||||||
},
|
|
||||||
threshold: {
|
|
||||||
quantity: 0
|
|
||||||
},
|
|
||||||
minimumTransferAmount: {
|
|
||||||
quantity: 25000000
|
|
||||||
},
|
|
||||||
rounding: {
|
|
||||||
quantity: 1000000
|
|
||||||
},
|
|
||||||
valuationDateDescription: "Every Local Business Day",
|
|
||||||
notificationTime: "2:00pm London",
|
|
||||||
resolutionTime: "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given",
|
|
||||||
interestRate: {
|
|
||||||
oracle: "Rates Service Provider",
|
|
||||||
tenor: {
|
|
||||||
name: "6M"
|
|
||||||
},
|
|
||||||
ratioUnit: null,
|
|
||||||
name: "EONIA"
|
|
||||||
},
|
|
||||||
addressForTransfers: "",
|
|
||||||
exposure: {},
|
|
||||||
localBusinessDay: [ "London" , "NewYork" ],
|
|
||||||
dailyInterestAmount: "(CashAmount * InterestRate ) / (fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360",
|
|
||||||
hashLegalDocs: "put hash here"
|
|
||||||
};
|
|
||||||
});
|
|
@ -1,9 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
define(['viewmodel/FixedLeg', 'viewmodel/FloatingLeg', 'viewmodel/Common'], (fixedLeg, floatingLeg, common) => {
|
|
||||||
return {
|
|
||||||
fixedLeg: fixedLeg,
|
|
||||||
floatingLeg: floatingLeg,
|
|
||||||
common: common
|
|
||||||
};
|
|
||||||
});
|
|
@ -1,20 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
define(['utils/dayCountBasisLookup'], (dayCountBasisLookup) => {
|
|
||||||
return {
|
|
||||||
fixedRatePayer: "Bank A",
|
|
||||||
notional: {
|
|
||||||
quantity: 2500000000
|
|
||||||
},
|
|
||||||
paymentFrequency: "SemiAnnual",
|
|
||||||
effectiveDateAdjustment: null,
|
|
||||||
terminationDateAdjustment: null,
|
|
||||||
fixedRate: "1.676",
|
|
||||||
dayCountBasis: dayCountBasisLookup["ACT/360"],
|
|
||||||
rollConvention: "ModifiedFollowing",
|
|
||||||
dayInMonth: 10,
|
|
||||||
paymentRule: "InArrears",
|
|
||||||
paymentDelay: "0",
|
|
||||||
interestPeriodAdjustment: "Adjusted"
|
|
||||||
};
|
|
||||||
});
|
|
@ -1,9 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
define([], () => {
|
|
||||||
return {
|
|
||||||
ratioUnit: {
|
|
||||||
value: 0.01 // %
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
@ -1,28 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
define(['utils/dayCountBasisLookup'], (dayCountBasisLookup) => {
|
|
||||||
return {
|
|
||||||
floatingRatePayer: "Bank B",
|
|
||||||
notional: {
|
|
||||||
quantity: 2500000000
|
|
||||||
},
|
|
||||||
paymentFrequency: "Quarterly",
|
|
||||||
effectiveDateAdjustment: null,
|
|
||||||
terminationDateAdjustment: null,
|
|
||||||
dayCountBasis: dayCountBasisLookup["ACT/360"],
|
|
||||||
rollConvention: "ModifiedFollowing",
|
|
||||||
fixingRollConvention: "ModifiedFollowing",
|
|
||||||
dayInMonth: 10,
|
|
||||||
resetDayInMonth: 10,
|
|
||||||
paymentRule: "InArrears",
|
|
||||||
paymentDelay: "0",
|
|
||||||
interestPeriodAdjustment: "Adjusted",
|
|
||||||
fixingPeriodOffset: 2,
|
|
||||||
resetRule: "InAdvance",
|
|
||||||
fixingsPerPayment: "Quarterly",
|
|
||||||
indexSource: "Rates Service Provider",
|
|
||||||
indexTenor: {
|
|
||||||
name: "3M"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
@ -1,186 +0,0 @@
|
|||||||
<div class="ui container">
|
|
||||||
<div class="ui negative message" id="form-error" ng-show="formError">{{formError}}</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.quantity" 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="Preceding">Preceding</option>
|
|
||||||
<option value="ModifiedFollowing">Modified following</option>
|
|
||||||
<option value="ModifiedFollowingBimonthly">Modified following bimonthly</option>
|
|
||||||
<option value="EndOfMonth">End of month</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.quantity" 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="Preceding">Preceding</option>
|
|
||||||
<option value="ModifiedFollowing">Modified following</option>
|
|
||||||
<option value="ModifiedFollowingBimonthly">Modified following bimonthly</option>
|
|
||||||
<option value="EndOfMonth">End of month</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="Preceding">Preceding</option>
|
|
||||||
<option value="ModifiedFollowing">Modified following</option>
|
|
||||||
<option value="ModifiedFollowingBimonthly">Modified following bimonthly</option>
|
|
||||||
<option value="EndOfMonth">End of month</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Day In Month</label>
|
|
||||||
<input type="number" name="dayInMonth" min="1" max="31" ng-model="deal.floatingLeg.dayInMonth"/>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Reset Day In Month</label>
|
|
||||||
<input type="number" name="resetDayInMonth" min="1" max="31" ng-model="deal.floatingLeg.resetDayInMonth"/>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Payment Delay</label>
|
|
||||||
<select class="ui selection" ng-model="deal.floatingLeg.paymentDelay">
|
|
||||||
<option value="0">T+00D</option>
|
|
||||||
<option value="1">T+01D</option>
|
|
||||||
<option value="2">T+02D</option>
|
|
||||||
<option value="3">T+03D</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Interest Period Adjustment</label>
|
|
||||||
<select class="ui selection" ng-model="deal.floatingLeg.interestPeriodAdjustment">
|
|
||||||
<option value="Adjusted">Adjusted</option>
|
|
||||||
<option value="Unadjusted">Unadjusted</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Fixing Period Offset</label>
|
|
||||||
<input type="number" min="0" name="fixingPeriodOffset" ng-model="deal.floatingLeg.fixingPeriodOffset"/>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Reset Rule</label>
|
|
||||||
<select class="ui selection" ng-model="deal.floatingLeg.resetRule">
|
|
||||||
<option value="InAdvance">In Advance</option>
|
|
||||||
<option value="InArrears">In Arrears</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Fixings Per Payment</label>
|
|
||||||
<select class="ui selection" ng-model="deal.floatingLeg.fixingsPerPayment">
|
|
||||||
<option value="Annual">Annual</option>
|
|
||||||
<option value="Quarterly" selected="selected">Quarterly</option>
|
|
||||||
<option value="SemiAnnual">Semi Annual</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="sixteen wide tablet eight wide computer column">
|
|
||||||
<input type="submit" class="ui submit primary button fluid"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
@ -1,206 +0,0 @@
|
|||||||
<div class="ui container">
|
|
||||||
<div class="ui hidden divider"></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.parties">
|
|
||||||
{{party}}<span ng-show="!$last">,</span>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="center aligned">
|
|
||||||
<td>Trade ID</td>
|
|
||||||
<td>{{deal.ref}}</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.quantity | number}} {{deal.fixedLeg.notional.token}}</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">
|
|
||||||
<table class="ui celled small table">
|
|
||||||
<tbody>
|
|
||||||
<tr class="center aligned" ng-repeat="date in deal.fixedLeg.paymentCalendar.holidayDates">
|
|
||||||
<td>{{date}}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</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.quantity | number}} {{deal.floatingLeg.notional.token}}</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">
|
|
||||||
<table class="ui celled small table">
|
|
||||||
<tbody>
|
|
||||||
<tr class="center aligned" ng-repeat="date in deal.floatingLeg.paymentCalendar.holidayDates">
|
|
||||||
<td>{{date}}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</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">
|
|
||||||
<table class="ui celled small table">
|
|
||||||
<tbody>
|
|
||||||
<tr class="center aligned" ng-repeat="date in deal.floatingLeg.fixingCalendar.holidayDates">
|
|
||||||
<td>{{date}}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,57 +0,0 @@
|
|||||||
<div class="ui container">
|
|
||||||
<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">
|
|
||||||
<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">
|
|
||||||
<td><a href="#/deal/{{deal.ref}}">{{deal.ref}}</a></td>
|
|
||||||
<td class="single line">{{deal.fixedLeg.fixedRatePayer}}</td>
|
|
||||||
<td class="single line">{{deal.fixedLeg.notional.quantity | number}} {{deal.fixedLeg.notional.token}}</td>
|
|
||||||
<td class="single line">{{deal.floatingLeg.floatingRatePayer}}</td>
|
|
||||||
<td class="single line">{{deal.floatingLeg.notional.quantity | number}} {{deal.floatingLeg.notional.token}}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,22 +0,0 @@
|
|||||||
package com.r3corda.core.testing
|
|
||||||
|
|
||||||
import com.google.common.base.Throwables
|
|
||||||
import com.r3corda.core.utilities.LogHelper
|
|
||||||
import com.r3corda.simulation.IRSSimulation
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class IRSSimulationTest {
|
|
||||||
// TODO: These tests should be a lot more complete.
|
|
||||||
|
|
||||||
@Test fun `runs to completion`() {
|
|
||||||
LogHelper.setLevel("+messages")
|
|
||||||
val sim = IRSSimulation(false, false, null)
|
|
||||||
val future = sim.start()
|
|
||||||
while (!future.isDone) sim.iterate()
|
|
||||||
try {
|
|
||||||
future.get()
|
|
||||||
} catch(e: Throwable) {
|
|
||||||
throw Throwables.getRootCause(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,167 +0,0 @@
|
|||||||
package com.r3corda.core.testing
|
|
||||||
|
|
||||||
import com.r3corda.contracts.asset.CASH
|
|
||||||
import com.r3corda.contracts.asset.Cash
|
|
||||||
import com.r3corda.contracts.asset.`issued by`
|
|
||||||
import com.r3corda.contracts.asset.`owned by`
|
|
||||||
import com.r3corda.core.bd
|
|
||||||
import com.r3corda.core.contracts.DOLLARS
|
|
||||||
import com.r3corda.core.contracts.Fix
|
|
||||||
import com.r3corda.core.contracts.TransactionType
|
|
||||||
import com.r3corda.core.contracts.`with notary`
|
|
||||||
import com.r3corda.core.crypto.Party
|
|
||||||
import com.r3corda.core.crypto.generateKeyPair
|
|
||||||
import com.r3corda.core.node.services.ServiceInfo
|
|
||||||
import com.r3corda.core.utilities.DUMMY_NOTARY
|
|
||||||
import com.r3corda.core.utilities.LogHelper
|
|
||||||
import com.r3corda.testing.node.MockNetwork
|
|
||||||
import com.r3corda.demos.api.NodeInterestRates
|
|
||||||
import com.r3corda.node.services.transactions.PersistentUniquenessProvider
|
|
||||||
import com.r3corda.node.utilities.configureDatabase
|
|
||||||
import com.r3corda.node.utilities.databaseTransaction
|
|
||||||
import com.r3corda.protocols.RatesFixProtocol
|
|
||||||
import com.r3corda.testing.ALICE_PUBKEY
|
|
||||||
import com.r3corda.testing.MEGA_CORP
|
|
||||||
import com.r3corda.testing.MEGA_CORP_KEY
|
|
||||||
import com.r3corda.testing.node.makeTestDataSourceProperties
|
|
||||||
import org.jetbrains.exposed.sql.Database
|
|
||||||
import org.junit.*
|
|
||||||
import java.io.Closeable
|
|
||||||
import java.time.Clock
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertFailsWith
|
|
||||||
|
|
||||||
class NodeInterestRatesTest {
|
|
||||||
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())
|
|
||||||
|
|
||||||
val DUMMY_CASH_ISSUER_KEY = generateKeyPair()
|
|
||||||
val DUMMY_CASH_ISSUER = Party("Cash issuer", DUMMY_CASH_ISSUER_KEY.public)
|
|
||||||
|
|
||||||
val clock = Clock.systemUTC()
|
|
||||||
lateinit var oracle: NodeInterestRates.Oracle
|
|
||||||
lateinit var dataSource: Closeable
|
|
||||||
lateinit var database: Database
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setUp() {
|
|
||||||
val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties())
|
|
||||||
dataSource=dataSourceAndDatabase.first
|
|
||||||
database=dataSourceAndDatabase.second
|
|
||||||
databaseTransaction(database) {
|
|
||||||
oracle = NodeInterestRates.Oracle(MEGA_CORP, MEGA_CORP_KEY, clock).apply { knownFixes = TEST_DATA }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
dataSource.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `query successfully`() {
|
|
||||||
databaseTransaction(database) {
|
|
||||||
val q = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
|
||||||
val res = oracle.query(listOf(q), clock.instant())
|
|
||||||
assertEquals(1, res.size)
|
|
||||||
assertEquals("0.678".bd, res[0].value)
|
|
||||||
assertEquals(q, res[0].of)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `query with one success and one missing`() {
|
|
||||||
databaseTransaction(database) {
|
|
||||||
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), clock.instant()) }
|
|
||||||
assertEquals(e.fix, q2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `query successfully with interpolated rate`() {
|
|
||||||
databaseTransaction(database) {
|
|
||||||
val q = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 5M")
|
|
||||||
val res = oracle.query(listOf(q), clock.instant())
|
|
||||||
assertEquals(1, res.size)
|
|
||||||
Assert.assertEquals(0.7316228, res[0].value.toDouble(), 0.0000001)
|
|
||||||
assertEquals(q, res[0].of)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `rate missing and unable to interpolate`() {
|
|
||||||
databaseTransaction(database) {
|
|
||||||
val q = NodeInterestRates.parseFixOf("EURIBOR 2016-03-15 3M")
|
|
||||||
assertFailsWith<NodeInterestRates.UnknownFix> { oracle.query(listOf(q), clock.instant()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `empty query`() {
|
|
||||||
databaseTransaction(database) {
|
|
||||||
assertFailsWith<IllegalArgumentException> { oracle.query(emptyList(), clock.instant()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `refuse to sign with no relevant commands`() {
|
|
||||||
databaseTransaction(database) {
|
|
||||||
val tx = makeTX()
|
|
||||||
assertFailsWith<IllegalArgumentException> { oracle.sign(tx.toWireTransaction()) }
|
|
||||||
tx.addCommand(Cash.Commands.Move(), ALICE_PUBKEY)
|
|
||||||
assertFailsWith<IllegalArgumentException> { oracle.sign(tx.toWireTransaction()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `sign successfully`() {
|
|
||||||
databaseTransaction(database) {
|
|
||||||
val tx = makeTX()
|
|
||||||
val fix = oracle.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")), clock.instant()).first()
|
|
||||||
tx.addCommand(fix, oracle.identity.owningKey)
|
|
||||||
|
|
||||||
// Sign successfully.
|
|
||||||
val signature = oracle.sign(tx.toWireTransaction())
|
|
||||||
tx.checkAndAddSignature(signature)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test fun `do not sign with unknown fix`() {
|
|
||||||
databaseTransaction(database) {
|
|
||||||
val tx = makeTX()
|
|
||||||
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
|
||||||
val badFix = Fix(fixOf, "0.6789".bd)
|
|
||||||
tx.addCommand(badFix, oracle.identity.owningKey)
|
|
||||||
|
|
||||||
val e1 = assertFailsWith<NodeInterestRates.UnknownFix> { oracle.sign(tx.toWireTransaction()) }
|
|
||||||
assertEquals(fixOf, e1.fix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun network() {
|
|
||||||
val net = MockNetwork()
|
|
||||||
val n1 = net.createNotaryNode()
|
|
||||||
val n2 = net.createNode(n1.info.address, advertisedServices = ServiceInfo(NodeInterestRates.type))
|
|
||||||
databaseTransaction(n2.database) {
|
|
||||||
n2.findService<NodeInterestRates.Service>().oracle.knownFixes = TEST_DATA
|
|
||||||
}
|
|
||||||
val tx = TransactionType.General.Builder(null)
|
|
||||||
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
|
||||||
val protocol = RatesFixProtocol(tx, n2.info.serviceIdentities(NodeInterestRates.type).first(), fixOf, "0.675".bd, "0.1".bd)
|
|
||||||
LogHelper.setLevel("rates")
|
|
||||||
net.runNetwork()
|
|
||||||
val future = n1.services.startProtocol(protocol)
|
|
||||||
|
|
||||||
net.runNetwork()
|
|
||||||
future.get()
|
|
||||||
|
|
||||||
// We should now have a valid signature over our tx from the oracle.
|
|
||||||
val fix = tx.toSignedTransaction(true).tx.commands.map { it.value as Fix }.first()
|
|
||||||
assertEquals(fixOf, fix.of)
|
|
||||||
assertEquals("0.678".bd, fix.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun makeTX() = TransactionType.General.Builder(DUMMY_NOTARY).withItems(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY)
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user