mirror of
https://github.com/corda/corda.git
synced 2025-02-18 16:40:55 +00:00
Merge remote-tracking branch 'remotes/origin/master' into colljos-vault-code-clean-up-refactor
# Conflicts: # core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt # node/src/test/kotlin/com/r3corda/node/services/NodeSchedulerServiceTest.kt Fixed failing CommercialPaper test (caused by re-use of same database transaction context for vault across two different transaction participants)
This commit is contained in:
commit
7d080c39df
15
.idea/modules.xml
generated
15
.idea/modules.xml
generated
@ -12,6 +12,9 @@
|
||||
<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_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_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" />
|
||||
@ -24,16 +27,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_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/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_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/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_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_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_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" />
|
||||
|
22
build.gradle
22
build.gradle
@ -110,8 +110,6 @@ dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.2"
|
||||
compile 'com.squareup.okhttp3:okhttp:3.3.1'
|
||||
compile 'co.paralleluniverse:capsule:1.0.3'
|
||||
|
||||
// Unit testing helpers.
|
||||
testCompile 'junit:junit:4.12'
|
||||
@ -133,22 +131,6 @@ task getAttachmentDemo(type: CreateStartScripts) {
|
||||
classpath = jar.outputs.files + project.configurations.runtime
|
||||
}
|
||||
|
||||
task getRateFixDemo(type: CreateStartScripts) {
|
||||
mainClassName = "com.r3corda.demos.RateFixDemoKt"
|
||||
applicationName = "get-rate-fix"
|
||||
defaultJvmOpts = ["-javaagent:${configurations.quasar.singleFile}"]
|
||||
outputDir = new File(project.buildDir, 'scripts')
|
||||
classpath = jar.outputs.files + project.configurations.runtime
|
||||
}
|
||||
|
||||
task getIRSDemo(type: CreateStartScripts) {
|
||||
mainClassName = "com.r3corda.demos.IRSDemoKt"
|
||||
applicationName = "irsdemo"
|
||||
defaultJvmOpts = ["-javaagent:${configurations.quasar.singleFile}"]
|
||||
outputDir = new File(project.buildDir, 'scripts')
|
||||
classpath = jar.outputs.files + project.configurations.runtime
|
||||
}
|
||||
|
||||
task getTraderDemo(type: CreateStartScripts) {
|
||||
mainClassName = "com.r3corda.demos.TraderDemoKt"
|
||||
applicationName = "trader-demo"
|
||||
@ -203,8 +185,6 @@ quasarScan.dependsOn('classes', 'core:classes', 'contracts:classes', 'node:class
|
||||
|
||||
applicationDistribution.into("bin") {
|
||||
from(getAttachmentDemo)
|
||||
from(getRateFixDemo)
|
||||
from(getIRSDemo)
|
||||
from(getTraderDemo)
|
||||
fileMode = 0755
|
||||
}
|
||||
@ -212,7 +192,7 @@ applicationDistribution.into("bin") {
|
||||
task buildCordaJAR(type: FatCapsule, dependsOn: ['quasarScan', 'buildCertSigningRequestUtilityJAR']) {
|
||||
applicationClass 'com.r3corda.node.MainKt'
|
||||
archiveName 'corda.jar'
|
||||
applicationSource = files(project.tasks.findByName('jar'), 'build/classes/main/CordaCaplet.class')
|
||||
applicationSource = files(project.tasks.findByName('jar'), 'node/build/classes/main/CordaCaplet.class')
|
||||
|
||||
capsuleManifest {
|
||||
appClassPath = ["jolokia-agent-war-${project.ext.jolokia_version}.war"]
|
||||
|
@ -0,0 +1,62 @@
|
||||
package com.r3corda.client
|
||||
|
||||
import com.r3corda.core.random63BitValue
|
||||
import com.r3corda.node.driver.driver
|
||||
import com.r3corda.node.services.config.configureTestSSL
|
||||
import com.r3corda.node.services.messaging.ArtemisMessagingComponent.Companion.toHostAndPort
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class CordaRPCClientTest {
|
||||
|
||||
private val validUsername = "user1"
|
||||
private val validPassword = "test"
|
||||
private val stopDriver = CountDownLatch(1)
|
||||
private var driverThread: Thread? = null
|
||||
private lateinit var client: CordaRPCClient
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
val driverStarted = CountDownLatch(1)
|
||||
driverThread = thread {
|
||||
driver {
|
||||
val driverInfo = startNode().get()
|
||||
client = CordaRPCClient(toHostAndPort(driverInfo.nodeInfo.address), configureTestSSL())
|
||||
driverStarted.countDown()
|
||||
stopDriver.await()
|
||||
}
|
||||
}
|
||||
driverStarted.await()
|
||||
}
|
||||
|
||||
@After
|
||||
fun stop() {
|
||||
stopDriver.countDown()
|
||||
driverThread?.join()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `log in with valid username and password`() {
|
||||
client.start(validUsername, validPassword)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `log in with unknown user`() {
|
||||
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
|
||||
client.start(random63BitValue().toString(), validPassword)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `log in with incorrect password`() {
|
||||
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
|
||||
client.start(validUsername, random63BitValue().toString())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package com.r3corda.client
|
||||
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import com.r3corda.client.model.NodeMonitorModel
|
||||
import com.r3corda.client.model.ProgressTrackingEvent
|
||||
import com.r3corda.core.bufferUntilSubscribed
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.services.NetworkMapCache
|
||||
import com.r3corda.core.node.services.ServiceInfo
|
||||
import com.r3corda.core.node.services.StateMachineTransactionMapping
|
||||
import com.r3corda.core.node.services.Vault
|
||||
@ -13,43 +13,47 @@ import com.r3corda.core.protocols.StateMachineRunId
|
||||
import com.r3corda.core.serialization.OpaqueBytes
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.node.driver.driver
|
||||
import com.r3corda.node.driver.startClient
|
||||
import com.r3corda.node.services.messaging.NodeMessagingClient
|
||||
import com.r3corda.node.services.config.configureTestSSL
|
||||
import com.r3corda.node.services.messaging.StateMachineUpdate
|
||||
import com.r3corda.node.services.transactions.SimpleNotaryService
|
||||
import com.r3corda.testing.*
|
||||
import org.junit.*
|
||||
import com.r3corda.testing.expect
|
||||
import com.r3corda.testing.expectEvents
|
||||
import com.r3corda.testing.sequence
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import rx.Observer
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class NodeMonitorModelTest {
|
||||
|
||||
lateinit var aliceNode: NodeInfo
|
||||
lateinit var notaryNode: NodeInfo
|
||||
lateinit var aliceClient: NodeMessagingClient
|
||||
val driverStarted = SettableFuture.create<Unit>()
|
||||
val stopDriver = SettableFuture.create<Unit>()
|
||||
val driverStopped = SettableFuture.create<Unit>()
|
||||
val stopDriver = CountDownLatch(1)
|
||||
var driverThread: Thread? = null
|
||||
|
||||
lateinit var stateMachineTransactionMapping: Observable<StateMachineTransactionMapping>
|
||||
lateinit var stateMachineUpdates: Observable<StateMachineUpdate>
|
||||
lateinit var progressTracking: Observable<ProgressTrackingEvent>
|
||||
lateinit var transactions: Observable<SignedTransaction>
|
||||
lateinit var vaultUpdates: Observable<Vault.Update>
|
||||
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
|
||||
lateinit var clientToService: Observer<ClientToServiceCommand>
|
||||
lateinit var newNode: (String) -> NodeInfo
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
thread {
|
||||
val driverStarted = CountDownLatch(1)
|
||||
driverThread = thread {
|
||||
driver {
|
||||
val aliceNodeFuture = startNode("Alice")
|
||||
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
|
||||
aliceNode = aliceNodeFuture.get()
|
||||
notaryNode = notaryNodeFuture.get()
|
||||
aliceClient = startClient(aliceNode).get()
|
||||
|
||||
aliceNode = aliceNodeFuture.get().nodeInfo
|
||||
notaryNode = notaryNodeFuture.get().nodeInfo
|
||||
newNode = { nodeName -> startNode(nodeName).get().nodeInfo }
|
||||
val monitor = NodeMonitorModel()
|
||||
|
||||
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
|
||||
@ -57,21 +61,41 @@ class NodeMonitorModelTest {
|
||||
progressTracking = monitor.progressTracking.bufferUntilSubscribed()
|
||||
transactions = monitor.transactions.bufferUntilSubscribed()
|
||||
vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed()
|
||||
networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
|
||||
clientToService = monitor.clientToService
|
||||
|
||||
monitor.register(aliceNode, aliceClient.config.certificatesPath)
|
||||
driverStarted.set(Unit)
|
||||
stopDriver.get()
|
||||
monitor.register(aliceNode, configureTestSSL(), "user1", "test")
|
||||
driverStarted.countDown()
|
||||
stopDriver.await()
|
||||
}
|
||||
driverStopped.set(Unit)
|
||||
}
|
||||
driverStarted.get()
|
||||
driverStarted.await()
|
||||
}
|
||||
|
||||
@After
|
||||
fun stop() {
|
||||
stopDriver.set(Unit)
|
||||
driverStopped.get()
|
||||
stopDriver.countDown()
|
||||
driverThread?.join()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNetworkMapUpdate() {
|
||||
newNode("Bob")
|
||||
newNode("Charlie")
|
||||
networkMapUpdates.expectEvents(isStrict = false) {
|
||||
sequence(
|
||||
// TODO : Add test for remove when driver DSL support individual node shutdown.
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Alice") { output.node.legalIdentity.name }
|
||||
},
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Bob") { output.node.legalIdentity.name }
|
||||
},
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Charlie") { output.node.legalIdentity.name }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -24,19 +24,12 @@ import kotlin.concurrent.thread
|
||||
* useful tasks. See the documentation for [proxy] or review the docsite to learn more about how this API works.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class CordaRPCClient(val host: HostAndPort, certificatesPath: Path) : Closeable, ArtemisMessagingComponent(sslConfig(certificatesPath)) {
|
||||
class CordaRPCClient(val host: HostAndPort, override val config: NodeSSLConfiguration) : Closeable, ArtemisMessagingComponent() {
|
||||
companion object {
|
||||
private val rpcLog = LoggerFactory.getLogger("com.r3corda.rpc")
|
||||
|
||||
private fun sslConfig(certificatesPath: Path): NodeSSLConfiguration = object : NodeSSLConfiguration {
|
||||
override val certificatesPath: Path = certificatesPath
|
||||
override val keyStorePassword = "cordacadevpass"
|
||||
override val trustStorePassword = "trustpass"
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Certificate handling for clients needs more work.
|
||||
|
||||
private inner class State {
|
||||
var running = false
|
||||
lateinit var sessionFactory: ClientSessionFactory
|
||||
@ -57,7 +50,7 @@ class CordaRPCClient(val host: HostAndPort, certificatesPath: Path) : Closeable,
|
||||
|
||||
/** Opens the connection to the server and registers a JVM shutdown hook to cleanly disconnect. */
|
||||
@Throws(ActiveMQNotConnectedException::class)
|
||||
fun start() {
|
||||
fun start(username: String, password: String) {
|
||||
state.locked {
|
||||
check(!running)
|
||||
checkStorePasswords() // Check the password.
|
||||
@ -66,7 +59,7 @@ class CordaRPCClient(val host: HostAndPort, certificatesPath: Path) : Closeable,
|
||||
sessionFactory = serverLocator.createSessionFactory()
|
||||
// We use our initial connection ID as the queue namespace.
|
||||
myID = sessionFactory.connection.id as Int and 0x000000FFFFFF
|
||||
session = sessionFactory.createSession()
|
||||
session = sessionFactory.createSession(username, password, false, true, true, serverLocator.isPreAcknowledge, serverLocator.ackBatchSize)
|
||||
session.start()
|
||||
clientImpl = CordaRPCClientImpl(session, state.lock, myAddressPrefix)
|
||||
running = true
|
||||
|
@ -0,0 +1,24 @@
|
||||
package com.r3corda.client.model
|
||||
|
||||
import com.r3corda.client.fxutils.foldToObservableList
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.services.NetworkMapCache
|
||||
import javafx.collections.ObservableList
|
||||
import kotlinx.support.jdk8.collections.removeIf
|
||||
import rx.Observable
|
||||
|
||||
class NetworkIdentityModel {
|
||||
private val networkIdentityObservable: Observable<NetworkMapCache.MapChange> by observable(NodeMonitorModel::networkMap)
|
||||
|
||||
val networkIdentities: ObservableList<NodeInfo> =
|
||||
networkIdentityObservable.foldToObservableList(Unit) { update, _accumulator, observableList ->
|
||||
observableList.removeIf {
|
||||
when (update.type) {
|
||||
NetworkMapCache.MapChangeType.Removed -> it == update.node
|
||||
NetworkMapCache.MapChangeType.Modified -> it == update.prevNodeInfo
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
observableList.addAll(update.node)
|
||||
}
|
||||
}
|
@ -3,16 +3,19 @@ package com.r3corda.client.model
|
||||
import com.r3corda.client.CordaRPCClient
|
||||
import com.r3corda.core.contracts.ClientToServiceCommand
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.services.NetworkMapCache
|
||||
import com.r3corda.core.node.services.StateMachineTransactionMapping
|
||||
import com.r3corda.core.node.services.Vault
|
||||
import com.r3corda.core.protocols.StateMachineRunId
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.node.services.messaging.ArtemisMessagingComponent
|
||||
import com.r3corda.node.services.config.NodeSSLConfiguration
|
||||
import com.r3corda.node.services.messaging.ArtemisMessagingComponent.Companion.toHostAndPort
|
||||
import com.r3corda.node.services.messaging.CordaRPCOps
|
||||
import com.r3corda.node.services.messaging.StateMachineInfo
|
||||
import com.r3corda.node.services.messaging.StateMachineUpdate
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.nio.file.Path
|
||||
|
||||
data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val message: String) {
|
||||
companion object {
|
||||
@ -35,26 +38,27 @@ class NodeMonitorModel {
|
||||
private val transactionsSubject = PublishSubject.create<SignedTransaction>()
|
||||
private val stateMachineTransactionMappingSubject = PublishSubject.create<StateMachineTransactionMapping>()
|
||||
private val progressTrackingSubject = PublishSubject.create<ProgressTrackingEvent>()
|
||||
private val networkMapSubject = PublishSubject.create<NetworkMapCache.MapChange>()
|
||||
|
||||
val stateMachineUpdates: Observable<StateMachineUpdate> = stateMachineUpdatesSubject
|
||||
val vaultUpdates: Observable<Vault.Update> = vaultUpdatesSubject
|
||||
val transactions: Observable<SignedTransaction> = transactionsSubject
|
||||
val stateMachineTransactionMapping: Observable<StateMachineTransactionMapping> = stateMachineTransactionMappingSubject
|
||||
val progressTracking: Observable<ProgressTrackingEvent> = progressTrackingSubject
|
||||
val networkMap: Observable<NetworkMapCache.MapChange> = networkMapSubject
|
||||
|
||||
private val clientToServiceSource = PublishSubject.create<ClientToServiceCommand>()
|
||||
val clientToService: PublishSubject<ClientToServiceCommand> = clientToServiceSource
|
||||
|
||||
val proxyObservable = SimpleObjectProperty<CordaRPCOps?>()
|
||||
|
||||
/**
|
||||
* Register for updates to/from a given vault.
|
||||
* @param messagingService The messaging to use for communication.
|
||||
* @param monitorNodeInfo the [Node] to connect to.
|
||||
* TODO provide an unsubscribe mechanism
|
||||
*/
|
||||
fun register(vaultMonitorNodeInfo: NodeInfo, certificatesPath: Path) {
|
||||
|
||||
val client = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(vaultMonitorNodeInfo.address), certificatesPath)
|
||||
client.start()
|
||||
fun register(vaultMonitorNodeInfo: NodeInfo, sslConfig: NodeSSLConfiguration, username: String, password: String) {
|
||||
val client = CordaRPCClient(toHostAndPort(vaultMonitorNodeInfo.address), sslConfig)
|
||||
client.start(username, password)
|
||||
val proxy = client.proxy()
|
||||
|
||||
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesAndUpdates()
|
||||
@ -89,9 +93,15 @@ class NodeMonitorModel {
|
||||
val (smTxMappings, futureSmTxMappings) = proxy.stateMachineRecordedTransactionMapping()
|
||||
futureSmTxMappings.startWith(smTxMappings).subscribe(stateMachineTransactionMappingSubject)
|
||||
|
||||
// Parties on network
|
||||
val (parties, futurePartyUpdate) = proxy.networkMapUpdates()
|
||||
futurePartyUpdate.startWith(parties.map { NetworkMapCache.MapChange(it, null, NetworkMapCache.MapChangeType.Added) }).subscribe(networkMapSubject)
|
||||
|
||||
// Client -> Service
|
||||
clientToServiceSource.subscribe {
|
||||
proxy.executeCommand(it)
|
||||
}
|
||||
|
||||
proxyObservable.set(proxy)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,801 +0,0 @@
|
||||
package com.r3corda.contracts
|
||||
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.node.services.ServiceType
|
||||
import com.r3corda.core.protocols.ProtocolLogicRefFactory
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import com.r3corda.core.utilities.suggestInterestRateAnnouncementTimeWindow
|
||||
import com.r3corda.protocols.TwoPartyDealProtocol
|
||||
import org.apache.commons.jexl3.JexlBuilder
|
||||
import org.apache.commons.jexl3.MapContext
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.security.PublicKey
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
|
||||
val IRS_PROGRAM_ID = InterestRateSwap()
|
||||
|
||||
// This is a placeholder for some types that we haven't identified exactly what they are just yet for things still in discussion
|
||||
open class UnknownType() {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return (other is UnknownType)
|
||||
}
|
||||
|
||||
override fun hashCode() = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Event superclass - everything happens on a date.
|
||||
*/
|
||||
open class Event(val date: LocalDate) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Event) return false
|
||||
|
||||
if (date != other.date) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode() = Objects.hash(date)
|
||||
}
|
||||
|
||||
/**
|
||||
* Top level PaymentEvent class - represents an obligation to pay an amount on a given date, which may be either in the past or the future.
|
||||
*/
|
||||
abstract class PaymentEvent(date: LocalDate) : Event(date) {
|
||||
abstract fun calculate(): Amount<Currency>
|
||||
}
|
||||
|
||||
/**
|
||||
* A [RatePaymentEvent] represents a dated obligation of payment.
|
||||
* It is a specialisation / modification of a basic cash flow event (to be written) that has some additional assistance
|
||||
* functions for interest rate swap legs of the fixed and floating nature.
|
||||
* For the fixed leg, the rate is already known at creation and therefore the flows can be pre-determined.
|
||||
* For the floating leg, the rate refers to a reference rate which is to be "fixed" at a point in the future.
|
||||
*/
|
||||
abstract class RatePaymentEvent(date: LocalDate,
|
||||
val accrualStartDate: LocalDate,
|
||||
val accrualEndDate: LocalDate,
|
||||
val dayCountBasisDay: DayCountBasisDay,
|
||||
val dayCountBasisYear: DayCountBasisYear,
|
||||
val notional: Amount<Currency>,
|
||||
val rate: Rate) : PaymentEvent(date) {
|
||||
companion object {
|
||||
val CSVHeader = "AccrualStartDate,AccrualEndDate,DayCountFactor,Days,Date,Ccy,Notional,Rate,Flow"
|
||||
}
|
||||
|
||||
override fun calculate(): Amount<Currency> = flow
|
||||
|
||||
abstract val flow: Amount<Currency>
|
||||
|
||||
val days: Int get() = calculateDaysBetween(accrualStartDate, accrualEndDate, dayCountBasisYear, dayCountBasisDay)
|
||||
|
||||
// TODO : Fix below (use daycount convention for division, not hardcoded 360 etc)
|
||||
val dayCountFactor: BigDecimal get() = (BigDecimal(days).divide(BigDecimal(360.0), 8, RoundingMode.HALF_UP)).setScale(4, RoundingMode.HALF_UP)
|
||||
|
||||
open fun asCSV() = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.token},$notional,$rate,$flow"
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is RatePaymentEvent) return false
|
||||
|
||||
if (accrualStartDate != other.accrualStartDate) return false
|
||||
if (accrualEndDate != other.accrualEndDate) return false
|
||||
if (dayCountBasisDay != other.dayCountBasisDay) return false
|
||||
if (dayCountBasisYear != other.dayCountBasisYear) return false
|
||||
if (notional != other.notional) return false
|
||||
if (rate != other.rate) return false
|
||||
// if (flow != other.flow) return false // Flow is derived
|
||||
|
||||
return super.equals(other)
|
||||
}
|
||||
|
||||
override fun hashCode() = super.hashCode() + 31 * Objects.hash(accrualStartDate, accrualEndDate, dayCountBasisDay,
|
||||
dayCountBasisYear, notional, rate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic class for the Fixed Rate Payments on the fixed leg - see [RatePaymentEvent].
|
||||
* Assumes that the rate is valid.
|
||||
*/
|
||||
class FixedRatePaymentEvent(date: LocalDate,
|
||||
accrualStartDate: LocalDate,
|
||||
accrualEndDate: LocalDate,
|
||||
dayCountBasisDay: DayCountBasisDay,
|
||||
dayCountBasisYear: DayCountBasisYear,
|
||||
notional: Amount<Currency>,
|
||||
rate: Rate) :
|
||||
RatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay, dayCountBasisYear, notional, rate) {
|
||||
companion object {
|
||||
val CSVHeader = RatePaymentEvent.CSVHeader
|
||||
}
|
||||
|
||||
override val flow: Amount<Currency> get() = Amount(dayCountFactor.times(BigDecimal(notional.quantity)).times(rate.ratioUnit!!.value).toLong(), notional.token)
|
||||
|
||||
override fun toString(): String =
|
||||
"FixedRatePaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate : $flow"
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic class for the Floating Rate Payments on the floating leg - see [RatePaymentEvent].
|
||||
* If the rate is null returns a zero payment. // TODO: Is this the desired behaviour?
|
||||
*/
|
||||
class FloatingRatePaymentEvent(date: LocalDate,
|
||||
accrualStartDate: LocalDate,
|
||||
accrualEndDate: LocalDate,
|
||||
dayCountBasisDay: DayCountBasisDay,
|
||||
dayCountBasisYear: DayCountBasisYear,
|
||||
val fixingDate: LocalDate,
|
||||
notional: Amount<Currency>,
|
||||
rate: Rate) : RatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay, dayCountBasisYear, notional, rate) {
|
||||
|
||||
companion object {
|
||||
val CSVHeader = RatePaymentEvent.CSVHeader + ",FixingDate"
|
||||
}
|
||||
|
||||
override val flow: Amount<Currency> get() {
|
||||
// TODO: Should an uncalculated amount return a zero ? null ? etc.
|
||||
val v = rate.ratioUnit?.value ?: return Amount(0, notional.token)
|
||||
return Amount(dayCountFactor.times(BigDecimal(notional.quantity)).times(v).toLong(), notional.token)
|
||||
}
|
||||
|
||||
override fun toString(): String = "FloatingPaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate (fix on $fixingDate): $flow"
|
||||
|
||||
override fun asCSV(): String = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.token},$notional,$fixingDate,$rate,$flow"
|
||||
|
||||
/**
|
||||
* Used for making immutables.
|
||||
*/
|
||||
fun withNewRate(newRate: Rate): FloatingRatePaymentEvent =
|
||||
FloatingRatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay,
|
||||
dayCountBasisYear, fixingDate, notional, newRate)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
other as FloatingRatePaymentEvent
|
||||
if (fixingDate != other.fixingDate) return false
|
||||
return super.equals(other)
|
||||
}
|
||||
|
||||
override fun hashCode() = super.hashCode() + 31 * Objects.hash(fixingDate)
|
||||
|
||||
// Can't autogenerate as not a data class :-(
|
||||
fun copy(date: LocalDate = this.date,
|
||||
accrualStartDate: LocalDate = this.accrualStartDate,
|
||||
accrualEndDate: LocalDate = this.accrualEndDate,
|
||||
dayCountBasisDay: DayCountBasisDay = this.dayCountBasisDay,
|
||||
dayCountBasisYear: DayCountBasisYear = this.dayCountBasisYear,
|
||||
fixingDate: LocalDate = this.fixingDate,
|
||||
notional: Amount<Currency> = this.notional,
|
||||
rate: Rate = this.rate) = FloatingRatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay, dayCountBasisYear, fixingDate, notional, rate)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The Interest Rate Swap class. For a quick overview of what an IRS is, see here - http://www.pimco.co.uk/EN/Education/Pages/InterestRateSwapsBasics1-08.aspx (no endorsement).
|
||||
* This contract has 4 significant data classes within it, the "Common", "Calculation", "FixedLeg" and "FloatingLeg".
|
||||
* It also has 4 commands, "Agree", "Fix", "Pay" and "Mature".
|
||||
* Currently, we are not interested (excuse pun) in valuing the swap, calculating the PVs, DFs and all that good stuff (soon though).
|
||||
* This is just a representation of a vanilla Fixed vs Floating (same currency) IRS in the R3 prototype model.
|
||||
*/
|
||||
class InterestRateSwap() : Contract {
|
||||
override val legalContractReference = SecureHash.sha256("is_this_the_text_of_the_contract ? TBD")
|
||||
|
||||
companion object {
|
||||
val oracleType = ServiceType.corda.getSubType("interest_rates")
|
||||
}
|
||||
|
||||
/**
|
||||
* This Common area contains all the information that is not leg specific.
|
||||
*/
|
||||
data class Common(
|
||||
val baseCurrency: Currency,
|
||||
val eligibleCurrency: Currency,
|
||||
val eligibleCreditSupport: String,
|
||||
val independentAmounts: Amount<Currency>,
|
||||
val threshold: Amount<Currency>,
|
||||
val minimumTransferAmount: Amount<Currency>,
|
||||
val rounding: Amount<Currency>,
|
||||
val valuationDateDescription: String, // This describes (in english) how regularly the swap is to be valued, e.g. "every local working day"
|
||||
val notificationTime: String,
|
||||
val resolutionTime: String,
|
||||
val interestRate: ReferenceRate,
|
||||
val addressForTransfers: String,
|
||||
val exposure: UnknownType,
|
||||
val localBusinessDay: BusinessCalendar,
|
||||
val dailyInterestAmount: Expression,
|
||||
val tradeID: String,
|
||||
val hashLegalDocs: String
|
||||
)
|
||||
|
||||
/**
|
||||
* The Calculation data class is "mutable" through out the life of the swap, as in, it's the only thing that contains
|
||||
* data that will changed from state to state (Recall that the design insists that everything is immutable, so we actually
|
||||
* copy / update for each transition).
|
||||
*/
|
||||
data class Calculation(
|
||||
val expression: Expression,
|
||||
val floatingLegPaymentSchedule: Map<LocalDate, FloatingRatePaymentEvent>,
|
||||
val fixedLegPaymentSchedule: Map<LocalDate, FixedRatePaymentEvent>
|
||||
) {
|
||||
/**
|
||||
* Gets the date of the next fixing.
|
||||
* @return LocalDate or null if no more fixings.
|
||||
*/
|
||||
fun nextFixingDate(): LocalDate? {
|
||||
return floatingLegPaymentSchedule.
|
||||
filter { it.value.rate is ReferenceRate }.// TODO - a better way to determine what fixings remain to be fixed
|
||||
minBy { it.value.fixingDate.toEpochDay() }?.value?.fixingDate
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fixing for that date.
|
||||
*/
|
||||
fun getFixing(date: LocalDate): FloatingRatePaymentEvent =
|
||||
floatingLegPaymentSchedule.values.single { it.fixingDate == date }
|
||||
|
||||
/**
|
||||
* Returns a copy after modifying (applying) the fixing for that date.
|
||||
*/
|
||||
fun applyFixing(date: LocalDate, newRate: FixedRate): Calculation {
|
||||
val paymentEvent = getFixing(date)
|
||||
val newFloatingLPS = floatingLegPaymentSchedule + (paymentEvent.date to paymentEvent.withNewRate(newRate))
|
||||
return Calculation(expression = expression,
|
||||
floatingLegPaymentSchedule = newFloatingLPS,
|
||||
fixedLegPaymentSchedule = fixedLegPaymentSchedule)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CommonLeg(
|
||||
val notional: Amount<Currency>,
|
||||
val paymentFrequency: Frequency,
|
||||
val effectiveDate: LocalDate,
|
||||
val effectiveDateAdjustment: DateRollConvention?,
|
||||
val terminationDate: LocalDate,
|
||||
val terminationDateAdjustment: DateRollConvention?,
|
||||
val dayCountBasisDay: DayCountBasisDay,
|
||||
val dayCountBasisYear: DayCountBasisYear,
|
||||
val dayInMonth: Int,
|
||||
val paymentRule: PaymentRule,
|
||||
val paymentDelay: Int,
|
||||
val paymentCalendar: BusinessCalendar,
|
||||
val interestPeriodAdjustment: AccrualAdjustment
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "Notional=$notional,PaymentFrequency=$paymentFrequency,EffectiveDate=$effectiveDate,EffectiveDateAdjustment:$effectiveDateAdjustment,TerminatationDate=$terminationDate," +
|
||||
"TerminationDateAdjustment=$terminationDateAdjustment,DayCountBasis=$dayCountBasisDay/$dayCountBasisYear,DayInMonth=$dayInMonth," +
|
||||
"PaymentRule=$paymentRule,PaymentDelay=$paymentDelay,PaymentCalendar=$paymentCalendar,InterestPeriodAdjustment=$interestPeriodAdjustment"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as CommonLeg
|
||||
|
||||
if (notional != other.notional) return false
|
||||
if (paymentFrequency != other.paymentFrequency) return false
|
||||
if (effectiveDate != other.effectiveDate) return false
|
||||
if (effectiveDateAdjustment != other.effectiveDateAdjustment) return false
|
||||
if (terminationDate != other.terminationDate) return false
|
||||
if (terminationDateAdjustment != other.terminationDateAdjustment) return false
|
||||
if (dayCountBasisDay != other.dayCountBasisDay) return false
|
||||
if (dayCountBasisYear != other.dayCountBasisYear) return false
|
||||
if (dayInMonth != other.dayInMonth) return false
|
||||
if (paymentRule != other.paymentRule) return false
|
||||
if (paymentDelay != other.paymentDelay) return false
|
||||
if (paymentCalendar != other.paymentCalendar) return false
|
||||
if (interestPeriodAdjustment != other.interestPeriodAdjustment) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode() = super.hashCode() + 31 * Objects.hash(notional, paymentFrequency, effectiveDate,
|
||||
effectiveDateAdjustment, terminationDate, effectiveDateAdjustment, terminationDate, terminationDateAdjustment,
|
||||
dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment)
|
||||
}
|
||||
|
||||
open class FixedLeg(
|
||||
var fixedRatePayer: Party,
|
||||
notional: Amount<Currency>,
|
||||
paymentFrequency: Frequency,
|
||||
effectiveDate: LocalDate,
|
||||
effectiveDateAdjustment: DateRollConvention?,
|
||||
terminationDate: LocalDate,
|
||||
terminationDateAdjustment: DateRollConvention?,
|
||||
dayCountBasisDay: DayCountBasisDay,
|
||||
dayCountBasisYear: DayCountBasisYear,
|
||||
dayInMonth: Int,
|
||||
paymentRule: PaymentRule,
|
||||
paymentDelay: Int,
|
||||
paymentCalendar: BusinessCalendar,
|
||||
interestPeriodAdjustment: AccrualAdjustment,
|
||||
var fixedRate: FixedRate,
|
||||
var rollConvention: DateRollConvention // TODO - best way of implementing - still awaiting some clarity
|
||||
) : CommonLeg
|
||||
(notional, paymentFrequency, effectiveDate, effectiveDateAdjustment, terminationDate, terminationDateAdjustment,
|
||||
dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment) {
|
||||
override fun toString(): String = "FixedLeg(Payer=$fixedRatePayer," + super.toString() + ",fixedRate=$fixedRate," +
|
||||
"rollConvention=$rollConvention"
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
other as FixedLeg
|
||||
|
||||
if (fixedRatePayer != other.fixedRatePayer) return false
|
||||
if (fixedRate != other.fixedRate) return false
|
||||
if (rollConvention != other.rollConvention) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode() = super.hashCode() + 31 * Objects.hash(fixedRatePayer, fixedRate, rollConvention)
|
||||
|
||||
// Can't autogenerate as not a data class :-(
|
||||
fun copy(fixedRatePayer: Party = this.fixedRatePayer,
|
||||
notional: Amount<Currency> = this.notional,
|
||||
paymentFrequency: Frequency = this.paymentFrequency,
|
||||
effectiveDate: LocalDate = this.effectiveDate,
|
||||
effectiveDateAdjustment: DateRollConvention? = this.effectiveDateAdjustment,
|
||||
terminationDate: LocalDate = this.terminationDate,
|
||||
terminationDateAdjustment: DateRollConvention? = this.terminationDateAdjustment,
|
||||
dayCountBasisDay: DayCountBasisDay = this.dayCountBasisDay,
|
||||
dayCountBasisYear: DayCountBasisYear = this.dayCountBasisYear,
|
||||
dayInMonth: Int = this.dayInMonth,
|
||||
paymentRule: PaymentRule = this.paymentRule,
|
||||
paymentDelay: Int = this.paymentDelay,
|
||||
paymentCalendar: BusinessCalendar = this.paymentCalendar,
|
||||
interestPeriodAdjustment: AccrualAdjustment = this.interestPeriodAdjustment,
|
||||
fixedRate: FixedRate = this.fixedRate) = FixedLeg(
|
||||
fixedRatePayer, notional, paymentFrequency, effectiveDate, effectiveDateAdjustment, terminationDate,
|
||||
terminationDateAdjustment, dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay,
|
||||
paymentCalendar, interestPeriodAdjustment, fixedRate, rollConvention)
|
||||
|
||||
}
|
||||
|
||||
open class FloatingLeg(
|
||||
var floatingRatePayer: Party,
|
||||
notional: Amount<Currency>,
|
||||
paymentFrequency: Frequency,
|
||||
effectiveDate: LocalDate,
|
||||
effectiveDateAdjustment: DateRollConvention?,
|
||||
terminationDate: LocalDate,
|
||||
terminationDateAdjustment: DateRollConvention?,
|
||||
dayCountBasisDay: DayCountBasisDay,
|
||||
dayCountBasisYear: DayCountBasisYear,
|
||||
dayInMonth: Int,
|
||||
paymentRule: PaymentRule,
|
||||
paymentDelay: Int,
|
||||
paymentCalendar: BusinessCalendar,
|
||||
interestPeriodAdjustment: AccrualAdjustment,
|
||||
var rollConvention: DateRollConvention,
|
||||
var fixingRollConvention: DateRollConvention,
|
||||
var resetDayInMonth: Int,
|
||||
var fixingPeriodOffset: Int,
|
||||
var resetRule: PaymentRule,
|
||||
var fixingsPerPayment: Frequency,
|
||||
var fixingCalendar: BusinessCalendar,
|
||||
var index: String,
|
||||
var indexSource: String,
|
||||
var indexTenor: Tenor
|
||||
) : CommonLeg(notional, paymentFrequency, effectiveDate, effectiveDateAdjustment, terminationDate, terminationDateAdjustment,
|
||||
dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment) {
|
||||
override fun toString(): String = "FloatingLeg(Payer=$floatingRatePayer," + super.toString() +
|
||||
"rollConvention=$rollConvention,FixingRollConvention=$fixingRollConvention,ResetDayInMonth=$resetDayInMonth" +
|
||||
"FixingPeriondOffset=$fixingPeriodOffset,ResetRule=$resetRule,FixingsPerPayment=$fixingsPerPayment,FixingCalendar=$fixingCalendar," +
|
||||
"Index=$index,IndexSource=$indexSource,IndexTenor=$indexTenor"
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
other as FloatingLeg
|
||||
|
||||
if (floatingRatePayer != other.floatingRatePayer) return false
|
||||
if (rollConvention != other.rollConvention) return false
|
||||
if (fixingRollConvention != other.fixingRollConvention) return false
|
||||
if (resetDayInMonth != other.resetDayInMonth) return false
|
||||
if (fixingPeriodOffset != other.fixingPeriodOffset) return false
|
||||
if (resetRule != other.resetRule) return false
|
||||
if (fixingsPerPayment != other.fixingsPerPayment) return false
|
||||
if (fixingCalendar != other.fixingCalendar) return false
|
||||
if (index != other.index) return false
|
||||
if (indexSource != other.indexSource) return false
|
||||
if (indexTenor != other.indexTenor) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode() = super.hashCode() + 31 * Objects.hash(floatingRatePayer, rollConvention,
|
||||
fixingRollConvention, resetDayInMonth, fixingPeriodOffset, resetRule, fixingsPerPayment, fixingCalendar,
|
||||
index, indexSource, indexTenor)
|
||||
|
||||
|
||||
fun copy(floatingRatePayer: Party = this.floatingRatePayer,
|
||||
notional: Amount<Currency> = this.notional,
|
||||
paymentFrequency: Frequency = this.paymentFrequency,
|
||||
effectiveDate: LocalDate = this.effectiveDate,
|
||||
effectiveDateAdjustment: DateRollConvention? = this.effectiveDateAdjustment,
|
||||
terminationDate: LocalDate = this.terminationDate,
|
||||
terminationDateAdjustment: DateRollConvention? = this.terminationDateAdjustment,
|
||||
dayCountBasisDay: DayCountBasisDay = this.dayCountBasisDay,
|
||||
dayCountBasisYear: DayCountBasisYear = this.dayCountBasisYear,
|
||||
dayInMonth: Int = this.dayInMonth,
|
||||
paymentRule: PaymentRule = this.paymentRule,
|
||||
paymentDelay: Int = this.paymentDelay,
|
||||
paymentCalendar: BusinessCalendar = this.paymentCalendar,
|
||||
interestPeriodAdjustment: AccrualAdjustment = this.interestPeriodAdjustment,
|
||||
rollConvention: DateRollConvention = this.rollConvention,
|
||||
fixingRollConvention: DateRollConvention = this.fixingRollConvention,
|
||||
resetDayInMonth: Int = this.resetDayInMonth,
|
||||
fixingPeriod: Int = this.fixingPeriodOffset,
|
||||
resetRule: PaymentRule = this.resetRule,
|
||||
fixingsPerPayment: Frequency = this.fixingsPerPayment,
|
||||
fixingCalendar: BusinessCalendar = this.fixingCalendar,
|
||||
index: String = this.index,
|
||||
indexSource: String = this.indexSource,
|
||||
indexTenor: Tenor = this.indexTenor
|
||||
) = FloatingLeg(floatingRatePayer, notional, paymentFrequency, effectiveDate, effectiveDateAdjustment,
|
||||
terminationDate, terminationDateAdjustment, dayCountBasisDay, dayCountBasisYear, dayInMonth,
|
||||
paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment, rollConvention,
|
||||
fixingRollConvention, resetDayInMonth, fixingPeriod, resetRule, fixingsPerPayment,
|
||||
fixingCalendar, index, indexSource, indexTenor)
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract) = verifyClause(tx, AllComposition(Clauses.Timestamped(), Clauses.Group()), tx.commands.select<Commands>())
|
||||
|
||||
interface Clauses {
|
||||
/**
|
||||
* Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides
|
||||
* helper functions for the clauses.
|
||||
*/
|
||||
abstract class AbstractIRSClause : Clause<State, Commands, UniqueIdentifier>() {
|
||||
// These functions may make more sense to use for basket types, but for now let's leave them here
|
||||
fun checkLegDates(legs: List<CommonLeg>) {
|
||||
requireThat {
|
||||
"Effective date is before termination date" by legs.all { it.effectiveDate < it.terminationDate }
|
||||
"Effective dates are in alignment" by legs.all { it.effectiveDate == legs[0].effectiveDate }
|
||||
"Termination dates are in alignment" by legs.all { it.terminationDate == legs[0].terminationDate }
|
||||
}
|
||||
}
|
||||
|
||||
fun checkLegAmounts(legs: List<CommonLeg>) {
|
||||
requireThat {
|
||||
"The notional is non zero" by legs.any { it.notional.quantity > (0).toLong() }
|
||||
"The notional for all legs must be the same" by legs.all { it.notional == legs[0].notional }
|
||||
}
|
||||
for (leg: CommonLeg in legs) {
|
||||
if (leg is FixedLeg) {
|
||||
requireThat {
|
||||
// TODO: Confirm: would someone really enter a swap with a negative fixed rate?
|
||||
"Fixed leg rate must be positive" by leg.fixedRate.isPositive()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: After business rules discussion, add further checks to the schedules and rates
|
||||
fun checkSchedules(@Suppress("UNUSED_PARAMETER") legs: List<CommonLeg>): Boolean = true
|
||||
|
||||
fun checkRates(@Suppress("UNUSED_PARAMETER") legs: List<CommonLeg>): Boolean = true
|
||||
|
||||
/**
|
||||
* Compares two schedules of Floating Leg Payments, returns the difference (i.e. omissions in either leg or changes to the values).
|
||||
*/
|
||||
fun getFloatingLegPaymentsDifferences(payments1: Map<LocalDate, Event>, payments2: Map<LocalDate, Event>): List<Pair<LocalDate, Pair<FloatingRatePaymentEvent, FloatingRatePaymentEvent>>> {
|
||||
val diff1 = payments1.filter { payments1[it.key] != payments2[it.key] }
|
||||
val diff2 = payments2.filter { payments1[it.key] != payments2[it.key] }
|
||||
return (diff1.keys + diff2.keys).map {
|
||||
it to Pair(diff1[it] as FloatingRatePaymentEvent, diff2[it] as FloatingRatePaymentEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Group : GroupClauseVerifier<State, Commands, UniqueIdentifier>(AnyComposition(Agree(), Fix(), Pay(), Mature())) {
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, UniqueIdentifier>>
|
||||
// Group by Trade ID for in / out states
|
||||
= tx.groupStates() { state -> state.linearId }
|
||||
}
|
||||
|
||||
class Timestamped : Clause<ContractState, Commands, Unit>() {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Unit?): Set<Commands> {
|
||||
require(tx.timestamp?.midpoint != null) { "must be timestamped" }
|
||||
// We return an empty set because we don't process any commands
|
||||
return emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
class Agree : AbstractIRSClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Agree::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Agree>()
|
||||
val irs = outputs.filterIsInstance<State>().single()
|
||||
requireThat {
|
||||
"There are no in states for an agreement" by inputs.isEmpty()
|
||||
"There are events in the fix schedule" by (irs.calculation.fixedLegPaymentSchedule.size > 0)
|
||||
"There are events in the float schedule" by (irs.calculation.floatingLegPaymentSchedule.size > 0)
|
||||
"All notionals must be non zero" by (irs.fixedLeg.notional.quantity > 0 && irs.floatingLeg.notional.quantity > 0)
|
||||
"The fixed leg rate must be positive" by (irs.fixedLeg.fixedRate.isPositive())
|
||||
"The currency of the notionals must be the same" by (irs.fixedLeg.notional.token == irs.floatingLeg.notional.token)
|
||||
"All leg notionals must be the same" by (irs.fixedLeg.notional == irs.floatingLeg.notional)
|
||||
|
||||
"The effective date is before the termination date for the fixed leg" by (irs.fixedLeg.effectiveDate < irs.fixedLeg.terminationDate)
|
||||
"The effective date is before the termination date for the floating leg" by (irs.floatingLeg.effectiveDate < irs.floatingLeg.terminationDate)
|
||||
"The effective dates are aligned" by (irs.floatingLeg.effectiveDate == irs.fixedLeg.effectiveDate)
|
||||
"The termination dates are aligned" by (irs.floatingLeg.terminationDate == irs.fixedLeg.terminationDate)
|
||||
"The rates are valid" by checkRates(listOf(irs.fixedLeg, irs.floatingLeg))
|
||||
"The schedules are valid" by checkSchedules(listOf(irs.fixedLeg, irs.floatingLeg))
|
||||
"The fixing period date offset cannot be negative" by (irs.floatingLeg.fixingPeriodOffset >= 0)
|
||||
|
||||
// TODO: further tests
|
||||
}
|
||||
checkLegAmounts(listOf(irs.fixedLeg, irs.floatingLeg))
|
||||
checkLegDates(listOf(irs.fixedLeg, irs.floatingLeg))
|
||||
|
||||
return setOf(command.value)
|
||||
}
|
||||
}
|
||||
|
||||
class Fix : AbstractIRSClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Refix::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Refix>()
|
||||
val irs = outputs.filterIsInstance<State>().single()
|
||||
val prevIrs = inputs.filterIsInstance<State>().single()
|
||||
val paymentDifferences = getFloatingLegPaymentsDifferences(prevIrs.calculation.floatingLegPaymentSchedule, irs.calculation.floatingLegPaymentSchedule)
|
||||
|
||||
// Having both of these tests are "redundant" as far as verify() goes, however, by performing both
|
||||
// we can relay more information back to the user in the case of failure.
|
||||
requireThat {
|
||||
"There is at least one difference in the IRS floating leg payment schedules" by !paymentDifferences.isEmpty()
|
||||
"There is only one change in the IRS floating leg payment schedule" by (paymentDifferences.size == 1)
|
||||
}
|
||||
|
||||
val changedRates = paymentDifferences.single().second // Ignore the date of the changed rate (we checked that earlier).
|
||||
val (oldFloatingRatePaymentEvent, newFixedRatePaymentEvent) = changedRates
|
||||
val fixValue = command.value.fix
|
||||
// Need to check that everything is the same apart from the new fixed rate entry.
|
||||
requireThat {
|
||||
"The fixed leg parties are constant" by (irs.fixedLeg.fixedRatePayer == prevIrs.fixedLeg.fixedRatePayer) // Although superseded by the below test, this is included for a regression issue
|
||||
"The fixed leg is constant" by (irs.fixedLeg == prevIrs.fixedLeg)
|
||||
"The floating leg is constant" by (irs.floatingLeg == prevIrs.floatingLeg)
|
||||
"The common values are constant" by (irs.common == prevIrs.common)
|
||||
"The fixed leg payment schedule is constant" by (irs.calculation.fixedLegPaymentSchedule == prevIrs.calculation.fixedLegPaymentSchedule)
|
||||
"The expression is unchanged" by (irs.calculation.expression == prevIrs.calculation.expression)
|
||||
"There is only one changed payment in the floating leg" by (paymentDifferences.size == 1)
|
||||
"There changed payment is a floating payment" by (oldFloatingRatePaymentEvent.rate is ReferenceRate)
|
||||
"The new payment is a fixed payment" by (newFixedRatePaymentEvent.rate is FixedRate)
|
||||
"The changed payments dates are aligned" by (oldFloatingRatePaymentEvent.date == newFixedRatePaymentEvent.date)
|
||||
"The new payment has the correct rate" by (newFixedRatePaymentEvent.rate.ratioUnit!!.value == fixValue.value)
|
||||
"The fixing is for the next required date" by (prevIrs.calculation.nextFixingDate() == fixValue.of.forDay)
|
||||
"The fix payment has the same currency as the notional" by (newFixedRatePaymentEvent.flow.token == irs.floatingLeg.notional.token)
|
||||
// "The fixing is not in the future " by (fixCommand) // The oracle should not have signed this .
|
||||
}
|
||||
|
||||
return setOf(command.value)
|
||||
}
|
||||
}
|
||||
|
||||
class Pay : AbstractIRSClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Pay::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Pay>()
|
||||
requireThat {
|
||||
"Payments not supported / verifiable yet" by false
|
||||
}
|
||||
return setOf(command.value)
|
||||
}
|
||||
}
|
||||
|
||||
class Mature : AbstractIRSClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Mature::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Mature>()
|
||||
val irs = inputs.filterIsInstance<State>().single()
|
||||
requireThat {
|
||||
"No more fixings to be applied" by (irs.calculation.nextFixingDate() == null)
|
||||
"The irs is fully consumed and there is no id matched output state" by outputs.isEmpty()
|
||||
}
|
||||
|
||||
return setOf(command.value)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
data class Refix(val fix: Fix) : Commands // Receive interest rate from oracle, Both sides agree
|
||||
class Pay : TypeOnlyCommandData(), Commands // Not implemented just yet
|
||||
class Agree : TypeOnlyCommandData(), Commands // Both sides agree to trade
|
||||
class Mature : TypeOnlyCommandData(), Commands // Trade has matured; no more actions. Cleanup. // TODO: Do we need this?
|
||||
}
|
||||
|
||||
/**
|
||||
* The state class contains the 4 major data classes.
|
||||
*/
|
||||
data class State(
|
||||
val fixedLeg: FixedLeg,
|
||||
val floatingLeg: FloatingLeg,
|
||||
val calculation: Calculation,
|
||||
val common: Common,
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier(common.tradeID)
|
||||
) : FixableDealState, SchedulableState {
|
||||
|
||||
override val contract = IRS_PROGRAM_ID
|
||||
|
||||
override val oracleType: ServiceType
|
||||
get() = InterestRateSwap.oracleType
|
||||
|
||||
override val ref = common.tradeID
|
||||
|
||||
override val participants: List<PublicKey>
|
||||
get() = parties.map { it.owningKey }
|
||||
|
||||
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean {
|
||||
return (fixedLeg.fixedRatePayer.owningKey in ourKeys) || (floatingLeg.floatingRatePayer.owningKey in ourKeys)
|
||||
}
|
||||
|
||||
override val parties: List<Party>
|
||||
get() = listOf(fixedLeg.fixedRatePayer, floatingLeg.floatingRatePayer)
|
||||
|
||||
override fun nextScheduledActivity(thisStateRef: StateRef, protocolLogicRefFactory: ProtocolLogicRefFactory): ScheduledActivity? {
|
||||
val nextFixingOf = nextFixingOf() ?: return null
|
||||
|
||||
// This is perhaps not how we should determine the time point in the business day, but instead expect the schedule to detail some of these aspects
|
||||
val instant = suggestInterestRateAnnouncementTimeWindow(index = nextFixingOf.name, source = floatingLeg.indexSource, date = nextFixingOf.forDay).start
|
||||
return ScheduledActivity(protocolLogicRefFactory.create(TwoPartyDealProtocol.FixingRoleDecider::class.java, thisStateRef), instant)
|
||||
}
|
||||
|
||||
override fun generateAgreement(notary: Party): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, notary)
|
||||
|
||||
override fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) {
|
||||
InterestRateSwap().generateFix(ptx, StateAndRef(TransactionState(this, oldState.state.notary), oldState.ref), fix)
|
||||
}
|
||||
|
||||
override fun nextFixingOf(): FixOf? {
|
||||
val date = calculation.nextFixingDate()
|
||||
return if (date == null) null else {
|
||||
val fixingEvent = calculation.getFixing(date)
|
||||
val oracleRate = fixingEvent.rate as ReferenceRate
|
||||
FixOf(oracleRate.name, date, oracleRate.tenor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For evaluating arbitrary java on the platform.
|
||||
*/
|
||||
|
||||
fun evaluateCalculation(businessDate: LocalDate, expression: Expression = calculation.expression): Any {
|
||||
// TODO: Jexl is purely for prototyping. It may be replaced
|
||||
// TODO: Whatever we do use must be secure and sandboxed
|
||||
val jexl = JexlBuilder().create()
|
||||
val expr = jexl.createExpression(expression.expr)
|
||||
val jc = MapContext()
|
||||
jc.set("fixedLeg", fixedLeg)
|
||||
jc.set("floatingLeg", floatingLeg)
|
||||
jc.set("calculation", calculation)
|
||||
jc.set("common", common)
|
||||
jc.set("currentBusinessDate", businessDate)
|
||||
return expr.evaluate(jc)
|
||||
}
|
||||
|
||||
/**
|
||||
* Just makes printing it out a bit better for those who don't have 80000 column wide monitors.
|
||||
*/
|
||||
fun prettyPrint() = toString().replace(",", "\n")
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This generates the agreement state and also the schedules from the initial data.
|
||||
* Note: The day count, interest rate calculation etc are not finished yet, but they are demonstrable.
|
||||
*/
|
||||
fun generateAgreement(floatingLeg: FloatingLeg, fixedLeg: FixedLeg, calculation: Calculation,
|
||||
common: Common, notary: Party): TransactionBuilder {
|
||||
|
||||
val fixedLegPaymentSchedule = HashMap<LocalDate, FixedRatePaymentEvent>()
|
||||
var dates = BusinessCalendar.createGenericSchedule(fixedLeg.effectiveDate, fixedLeg.paymentFrequency, fixedLeg.paymentCalendar, fixedLeg.rollConvention, endDate = fixedLeg.terminationDate)
|
||||
var periodStartDate = fixedLeg.effectiveDate
|
||||
|
||||
// Create a schedule for the fixed payments
|
||||
for (periodEndDate in dates) {
|
||||
val paymentDate = BusinessCalendar.getOffsetDate(periodEndDate, Frequency.Daily, fixedLeg.paymentDelay)
|
||||
val paymentEvent = FixedRatePaymentEvent(
|
||||
paymentDate,
|
||||
periodStartDate,
|
||||
periodEndDate,
|
||||
fixedLeg.dayCountBasisDay,
|
||||
fixedLeg.dayCountBasisYear,
|
||||
fixedLeg.notional,
|
||||
fixedLeg.fixedRate
|
||||
)
|
||||
fixedLegPaymentSchedule[paymentDate] = paymentEvent
|
||||
periodStartDate = periodEndDate
|
||||
}
|
||||
|
||||
dates = BusinessCalendar.createGenericSchedule(floatingLeg.effectiveDate,
|
||||
floatingLeg.fixingsPerPayment,
|
||||
floatingLeg.fixingCalendar,
|
||||
floatingLeg.rollConvention,
|
||||
endDate = floatingLeg.terminationDate)
|
||||
|
||||
val floatingLegPaymentSchedule: MutableMap<LocalDate, FloatingRatePaymentEvent> = HashMap()
|
||||
periodStartDate = floatingLeg.effectiveDate
|
||||
|
||||
// Now create a schedule for the floating and fixes.
|
||||
for (periodEndDate in dates) {
|
||||
val paymentDate = BusinessCalendar.getOffsetDate(periodEndDate, Frequency.Daily, floatingLeg.paymentDelay)
|
||||
val paymentEvent = FloatingRatePaymentEvent(
|
||||
paymentDate,
|
||||
periodStartDate,
|
||||
periodEndDate,
|
||||
floatingLeg.dayCountBasisDay,
|
||||
floatingLeg.dayCountBasisYear,
|
||||
calcFixingDate(periodStartDate, floatingLeg.fixingPeriodOffset, floatingLeg.fixingCalendar),
|
||||
floatingLeg.notional,
|
||||
ReferenceRate(floatingLeg.indexSource, floatingLeg.indexTenor, floatingLeg.index)
|
||||
)
|
||||
|
||||
floatingLegPaymentSchedule[paymentDate] = paymentEvent
|
||||
periodStartDate = periodEndDate
|
||||
}
|
||||
|
||||
val newCalculation = Calculation(calculation.expression, floatingLegPaymentSchedule, fixedLegPaymentSchedule)
|
||||
|
||||
// Put all the above into a new State object.
|
||||
val state = State(fixedLeg, floatingLeg, newCalculation, common)
|
||||
return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Agree(), listOf(state.floatingLeg.floatingRatePayer.owningKey, state.fixedLeg.fixedRatePayer.owningKey)))
|
||||
}
|
||||
|
||||
private fun calcFixingDate(date: LocalDate, fixingPeriodOffset: Int, calendar: BusinessCalendar): LocalDate {
|
||||
return when (fixingPeriodOffset) {
|
||||
0 -> date
|
||||
else -> calendar.moveBusinessDays(date, DateRollDirection.BACKWARD, fixingPeriodOffset)
|
||||
}
|
||||
}
|
||||
|
||||
fun generateFix(tx: TransactionBuilder, irs: StateAndRef<State>, fixing: Fix) {
|
||||
tx.addInputState(irs)
|
||||
val fixedRate = FixedRate(RatioUnit(fixing.value))
|
||||
tx.addOutputState(
|
||||
irs.state.data.copy(calculation = irs.state.data.calculation.applyFixing(fixing.of.forDay, fixedRate)),
|
||||
irs.state.notary
|
||||
)
|
||||
tx.addCommand(Commands.Refix(fixing), listOf(irs.state.data.floatingLeg.floatingRatePayer.owningKey, irs.state.data.fixedLeg.fixedRatePayer.owningKey))
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package com.r3corda.contracts
|
||||
|
||||
fun InterestRateSwap.State.exportIRSToCSV(): String =
|
||||
"Fixed Leg\n" + FixedRatePaymentEvent.CSVHeader + "\n" +
|
||||
this.calculation.fixedLegPaymentSchedule.toSortedMap().values.map { it.asCSV() }.joinToString("\n") + "\n" +
|
||||
"Floating Leg\n" + FloatingRatePaymentEvent.CSVHeader + "\n" +
|
||||
this.calculation.floatingLegPaymentSchedule.toSortedMap().values.map { it.asCSV() }.joinToString("\n") + "\n"
|
@ -1,88 +0,0 @@
|
||||
package com.r3corda.contracts
|
||||
|
||||
import com.r3corda.core.contracts.Amount
|
||||
import com.r3corda.core.contracts.Tenor
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
|
||||
|
||||
// Things in here will move to the general utils class when we've hammered out various discussions regarding amounts, dates, oracle etc.
|
||||
|
||||
/**
|
||||
* A utility class to prevent the various mixups between percentages, decimals, bips etc.
|
||||
*/
|
||||
open class RatioUnit(val value: BigDecimal) { // TODO: Discuss this type
|
||||
override fun equals(other: Any?) = (other as? RatioUnit)?.value == value
|
||||
override fun hashCode() = value.hashCode()
|
||||
override fun toString() = value.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* A class to reprecent a percentage in an unambiguous way.
|
||||
*/
|
||||
open class PercentageRatioUnit(percentageAsString: String) : RatioUnit(BigDecimal(percentageAsString).divide(BigDecimal("100"))) {
|
||||
override fun toString() = value.times(BigDecimal(100)).toString() + "%"
|
||||
}
|
||||
|
||||
/**
|
||||
* For the convenience of writing "5".percent
|
||||
* Note that we do not currently allow 10.percent (ie no quotes) as this might get a little confusing if 0.1.percent was
|
||||
* written. Additionally, there is a possibility of creating a precision error in the implicit conversion.
|
||||
*/
|
||||
val String.percent: PercentageRatioUnit get() = PercentageRatioUnit(this)
|
||||
|
||||
/**
|
||||
* Parent of the Rate family. Used to denote fixed rates, floating rates, reference rates etc.
|
||||
*/
|
||||
open class Rate(val ratioUnit: RatioUnit? = null) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as Rate
|
||||
|
||||
if (ratioUnit != other.ratioUnit) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the hash code of the ratioUnit or zero if the ratioUnit is null, as is the case for floating rate fixings
|
||||
* that have not yet happened. Yet-to-be fixed floating rates need to be equal such that schedules can be tested
|
||||
* for equality.
|
||||
*/
|
||||
override fun hashCode() = ratioUnit?.hashCode() ?: 0
|
||||
override fun toString() = ratioUnit.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* A very basic subclass to represent a fixed rate.
|
||||
*/
|
||||
class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) {
|
||||
fun isPositive(): Boolean = ratioUnit!!.value > BigDecimal("0.0")
|
||||
|
||||
override fun equals(other: Any?) = other?.javaClass == javaClass && super.equals(other)
|
||||
override fun hashCode() = super.hashCode()
|
||||
}
|
||||
|
||||
/**
|
||||
* The parent class of the Floating rate classes.
|
||||
*/
|
||||
open class FloatingRate : Rate(null)
|
||||
|
||||
/**
|
||||
* So a reference rate is a rate that takes its value from a source at a given date
|
||||
* e.g. LIBOR 6M as of 17 March 2016. Hence it requires a source (name) and a value date in the getAsOf(..) method.
|
||||
*/
|
||||
class ReferenceRate(val oracle: String, val tenor: Tenor, val name: String) : FloatingRate() {
|
||||
override fun toString(): String = "$name - $tenor"
|
||||
}
|
||||
|
||||
// TODO: For further discussion.
|
||||
operator fun Amount<Currency>.times(other: RatioUnit): Amount<Currency> = Amount((BigDecimal(this.quantity).multiply(other.value)).longValueExact(), this.token)
|
||||
//operator fun Amount<Currency>.times(other: FixedRate): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency)
|
||||
//fun Amount<Currency>.times(other: InterestRateSwap.RatioUnit): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency)
|
||||
|
||||
operator fun kotlin.Int.times(other: FixedRate): Int = BigDecimal(this).multiply(other.ratioUnit!!.value).intValueExact()
|
||||
operator fun Int.times(other: Rate): Int = BigDecimal(this).multiply(other.ratioUnit!!.value).intValueExact()
|
||||
operator fun Int.times(other: RatioUnit): Int = BigDecimal(this).multiply(other.value).intValueExact()
|
@ -1,718 +0,0 @@
|
||||
package com.r3corda.contracts
|
||||
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.node.recordTransactions
|
||||
import com.r3corda.core.seconds
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.utilities.DUMMY_NOTARY
|
||||
import com.r3corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import com.r3corda.core.utilities.TEST_TX_TIME
|
||||
import com.r3corda.testing.*
|
||||
import com.r3corda.testing.node.MockServices
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
|
||||
fun createDummyIRS(irsSelect: Int): InterestRateSwap.State {
|
||||
return when (irsSelect) {
|
||||
1 -> {
|
||||
|
||||
val fixedLeg = InterestRateSwap.FixedLeg(
|
||||
fixedRatePayer = MEGA_CORP,
|
||||
notional = 15900000.DOLLARS,
|
||||
paymentFrequency = Frequency.SemiAnnual,
|
||||
effectiveDate = LocalDate.of(2016, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2026, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
fixedRate = FixedRate(PercentageRatioUnit("1.677")),
|
||||
dayCountBasisDay = DayCountBasisDay.D30,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 3,
|
||||
paymentCalendar = BusinessCalendar.getInstance("London", "NewYork"),
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted
|
||||
)
|
||||
|
||||
val floatingLeg = InterestRateSwap.FloatingLeg(
|
||||
floatingRatePayer = MINI_CORP,
|
||||
notional = 15900000.DOLLARS,
|
||||
paymentFrequency = Frequency.Quarterly,
|
||||
effectiveDate = LocalDate.of(2016, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2026, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
dayCountBasisDay = DayCountBasisDay.D30,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
fixingRollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
resetDayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 3,
|
||||
paymentCalendar = BusinessCalendar.getInstance("London", "NewYork"),
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted,
|
||||
fixingPeriodOffset = 2,
|
||||
resetRule = PaymentRule.InAdvance,
|
||||
fixingsPerPayment = Frequency.Quarterly,
|
||||
fixingCalendar = BusinessCalendar.getInstance("London"),
|
||||
index = "LIBOR",
|
||||
indexSource = "TEL3750",
|
||||
indexTenor = Tenor("3M")
|
||||
)
|
||||
|
||||
val calculation = InterestRateSwap.Calculation (
|
||||
|
||||
// TODO: this seems to fail quite dramatically
|
||||
//expression = "fixedLeg.notional * fixedLeg.fixedRate",
|
||||
|
||||
// TODO: How I want it to look
|
||||
//expression = "( fixedLeg.notional * (fixedLeg.fixedRate)) - (floatingLeg.notional * (rateSchedule.get(context.getDate('currentDate'))))",
|
||||
|
||||
// How it's ended up looking, which I think is now broken but it's a WIP.
|
||||
expression = Expression("( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -" +
|
||||
"(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))"),
|
||||
|
||||
floatingLegPaymentSchedule = HashMap(),
|
||||
fixedLegPaymentSchedule = HashMap()
|
||||
)
|
||||
|
||||
val EUR = currency("EUR")
|
||||
|
||||
val common = InterestRateSwap.Common(
|
||||
baseCurrency = EUR,
|
||||
eligibleCurrency = EUR,
|
||||
eligibleCreditSupport = "Cash in an Eligible Currency",
|
||||
independentAmounts = Amount(0, EUR),
|
||||
threshold = Amount(0, EUR),
|
||||
minimumTransferAmount = Amount(250000 * 100, EUR),
|
||||
rounding = Amount(10000 * 100, EUR),
|
||||
valuationDateDescription = "Every Local Business Day",
|
||||
notificationTime = "2:00pm London",
|
||||
resolutionTime = "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
||||
interestRate = ReferenceRate("T3270", Tenor("6M"), "EONIA"),
|
||||
addressForTransfers = "",
|
||||
exposure = UnknownType(),
|
||||
localBusinessDay = BusinessCalendar.getInstance("London"),
|
||||
tradeID = "trade1",
|
||||
hashLegalDocs = "put hash here",
|
||||
dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360")
|
||||
)
|
||||
|
||||
InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common)
|
||||
}
|
||||
2 -> {
|
||||
// 10y swap, we pay 1.3% fixed 30/360 semi, rec 3m usd libor act/360 Q on 25m notional (mod foll/adj on both sides)
|
||||
// I did a mock up start date 10/03/2015 – 10/03/2025 so you have 5 cashflows on float side that have been preset the rest are unknown
|
||||
|
||||
val fixedLeg = InterestRateSwap.FixedLeg(
|
||||
fixedRatePayer = MEGA_CORP,
|
||||
notional = 25000000.DOLLARS,
|
||||
paymentFrequency = Frequency.SemiAnnual,
|
||||
effectiveDate = LocalDate.of(2015, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2025, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
fixedRate = FixedRate(PercentageRatioUnit("1.3")),
|
||||
dayCountBasisDay = DayCountBasisDay.D30,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 0,
|
||||
paymentCalendar = BusinessCalendar.getInstance(),
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted
|
||||
)
|
||||
|
||||
val floatingLeg = InterestRateSwap.FloatingLeg(
|
||||
floatingRatePayer = MINI_CORP,
|
||||
notional = 25000000.DOLLARS,
|
||||
paymentFrequency = Frequency.Quarterly,
|
||||
effectiveDate = LocalDate.of(2015, 3, 10),
|
||||
effectiveDateAdjustment = null,
|
||||
terminationDate = LocalDate.of(2025, 3, 10),
|
||||
terminationDateAdjustment = null,
|
||||
dayCountBasisDay = DayCountBasisDay.DActual,
|
||||
dayCountBasisYear = DayCountBasisYear.Y360,
|
||||
rollConvention = DateRollConvention.ModifiedFollowing,
|
||||
fixingRollConvention = DateRollConvention.ModifiedFollowing,
|
||||
dayInMonth = 10,
|
||||
resetDayInMonth = 10,
|
||||
paymentRule = PaymentRule.InArrears,
|
||||
paymentDelay = 0,
|
||||
paymentCalendar = BusinessCalendar.getInstance(),
|
||||
interestPeriodAdjustment = AccrualAdjustment.Adjusted,
|
||||
fixingPeriodOffset = 2,
|
||||
resetRule = PaymentRule.InAdvance,
|
||||
fixingsPerPayment = Frequency.Quarterly,
|
||||
fixingCalendar = BusinessCalendar.getInstance(),
|
||||
index = "USD LIBOR",
|
||||
indexSource = "TEL3750",
|
||||
indexTenor = Tenor("3M")
|
||||
)
|
||||
|
||||
val calculation = InterestRateSwap.Calculation (
|
||||
|
||||
// TODO: this seems to fail quite dramatically
|
||||
//expression = "fixedLeg.notional * fixedLeg.fixedRate",
|
||||
|
||||
// TODO: How I want it to look
|
||||
//expression = "( fixedLeg.notional * (fixedLeg.fixedRate)) - (floatingLeg.notional * (rateSchedule.get(context.getDate('currentDate'))))",
|
||||
|
||||
// How it's ended up looking, which I think is now broken but it's a WIP.
|
||||
expression = Expression("( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -" +
|
||||
"(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))"),
|
||||
|
||||
floatingLegPaymentSchedule = HashMap(),
|
||||
fixedLegPaymentSchedule = HashMap()
|
||||
)
|
||||
|
||||
val EUR = currency("EUR")
|
||||
|
||||
val common = InterestRateSwap.Common(
|
||||
baseCurrency = EUR,
|
||||
eligibleCurrency = EUR,
|
||||
eligibleCreditSupport = "Cash in an Eligible Currency",
|
||||
independentAmounts = Amount(0, EUR),
|
||||
threshold = Amount(0, EUR),
|
||||
minimumTransferAmount = Amount(250000 * 100, EUR),
|
||||
rounding = Amount(10000 * 100, EUR),
|
||||
valuationDateDescription = "Every Local Business Day",
|
||||
notificationTime = "2:00pm London",
|
||||
resolutionTime = "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
||||
interestRate = ReferenceRate("T3270", Tenor("6M"), "EONIA"),
|
||||
addressForTransfers = "",
|
||||
exposure = UnknownType(),
|
||||
localBusinessDay = BusinessCalendar.getInstance("London"),
|
||||
tradeID = "trade2",
|
||||
hashLegalDocs = "put hash here",
|
||||
dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360")
|
||||
)
|
||||
|
||||
return InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common)
|
||||
|
||||
}
|
||||
else -> TODO("IRS number $irsSelect not defined")
|
||||
}
|
||||
}
|
||||
|
||||
class IRSTests {
|
||||
@Test
|
||||
fun ok() {
|
||||
trade().verifies()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ok with groups`() {
|
||||
tradegroups().verifies()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an IRS txn - we'll need it for a few things.
|
||||
*/
|
||||
fun generateIRSTxn(irsSelect: Int): SignedTransaction {
|
||||
val dummyIRS = createDummyIRS(irsSelect)
|
||||
val genTX: SignedTransaction = run {
|
||||
val gtx = InterestRateSwap().generateAgreement(
|
||||
fixedLeg = dummyIRS.fixedLeg,
|
||||
floatingLeg = dummyIRS.floatingLeg,
|
||||
calculation = dummyIRS.calculation,
|
||||
common = dummyIRS.common,
|
||||
notary = DUMMY_NOTARY).apply {
|
||||
setTime(TEST_TX_TIME, 30.seconds)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
signWith(MINI_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
gtx.toSignedTransaction()
|
||||
}
|
||||
return genTX
|
||||
}
|
||||
|
||||
/**
|
||||
* Just make sure it's sane.
|
||||
*/
|
||||
@Test
|
||||
fun pprintIRS() {
|
||||
val irs = singleIRS()
|
||||
println(irs.prettyPrint())
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility so I don't have to keep typing this.
|
||||
*/
|
||||
fun singleIRS(irsSelector: Int = 1): InterestRateSwap.State {
|
||||
return generateIRSTxn(irsSelector).tx.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the generate. No explicit exception as if something goes wrong, we'll find out anyway.
|
||||
*/
|
||||
@Test
|
||||
fun generateIRS() {
|
||||
// Tests aren't allowed to return things
|
||||
generateIRSTxn(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing a simple IRS, add a few fixings and then display as CSV.
|
||||
*/
|
||||
@Test
|
||||
fun `IRS Export test`() {
|
||||
// No transactions etc required - we're just checking simple maths and export functionallity
|
||||
val irs = singleIRS(2)
|
||||
|
||||
var newCalculation = irs.calculation
|
||||
|
||||
val fixings = mapOf(LocalDate.of(2015, 3, 6) to "0.6",
|
||||
LocalDate.of(2015, 6, 8) to "0.75",
|
||||
LocalDate.of(2015, 9, 8) to "0.8",
|
||||
LocalDate.of(2015, 12, 8) to "0.55",
|
||||
LocalDate.of(2016, 3, 8) to "0.644")
|
||||
|
||||
for ((key, value) in fixings) {
|
||||
newCalculation = newCalculation.applyFixing(key, FixedRate(PercentageRatioUnit(value)))
|
||||
}
|
||||
|
||||
val newIRS = InterestRateSwap.State(irs.fixedLeg, irs.floatingLeg, newCalculation, irs.common)
|
||||
println(newIRS.exportIRSToCSV())
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure it has a schedule and the schedule has some unfixed rates.
|
||||
*/
|
||||
@Test
|
||||
fun `next fixing date`() {
|
||||
val irs = singleIRS(1)
|
||||
println(irs.calculation.nextFixingDate())
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through all the fix dates and add something.
|
||||
*/
|
||||
@Test
|
||||
fun generateIRSandFixSome() {
|
||||
val services = MockServices()
|
||||
var previousTXN = generateIRSTxn(1)
|
||||
previousTXN.toLedgerTransaction(services).verify()
|
||||
services.recordTransactions(previousTXN)
|
||||
fun currentIRS() = previousTXN.tx.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
|
||||
|
||||
while (true) {
|
||||
val nextFix: FixOf = currentIRS().nextFixingOf() ?: break
|
||||
val fixTX: SignedTransaction = run {
|
||||
val tx = TransactionType.General.Builder(DUMMY_NOTARY)
|
||||
val fixing = Fix(nextFix, "0.052".percent.value)
|
||||
InterestRateSwap().generateFix(tx, previousTXN.tx.outRef(0), fixing)
|
||||
with(tx) {
|
||||
setTime(TEST_TX_TIME, 30.seconds)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
signWith(MINI_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
tx.toSignedTransaction()
|
||||
}
|
||||
fixTX.toLedgerTransaction(services).verify()
|
||||
services.recordTransactions(fixTX)
|
||||
previousTXN = fixTX
|
||||
}
|
||||
}
|
||||
|
||||
// Move these later as they aren't IRS specific.
|
||||
@Test
|
||||
fun `test some rate objects 100 * FixedRate(5%)`() {
|
||||
val r1 = FixedRate(PercentageRatioUnit("5"))
|
||||
assert(100 * r1 == 5)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `expression calculation testing`() {
|
||||
val dummyIRS = singleIRS()
|
||||
val stuffToPrint: ArrayList<String> = arrayListOf(
|
||||
"fixedLeg.notional.quantity",
|
||||
"fixedLeg.fixedRate.ratioUnit",
|
||||
"fixedLeg.fixedRate.ratioUnit.value",
|
||||
"floatingLeg.notional.quantity",
|
||||
"fixedLeg.fixedRate",
|
||||
"currentBusinessDate",
|
||||
"calculation.floatingLegPaymentSchedule.get(currentBusinessDate)",
|
||||
"fixedLeg.notional.token.currencyCode",
|
||||
"fixedLeg.notional.quantity * 10",
|
||||
"fixedLeg.notional.quantity * fixedLeg.fixedRate.ratioUnit.value",
|
||||
"(fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360 ",
|
||||
"(fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value))"
|
||||
// "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate"
|
||||
// "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value",
|
||||
//"( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
|
||||
// "( fixedLeg.notional * fixedLeg.fixedRate )"
|
||||
)
|
||||
|
||||
for (i in stuffToPrint) {
|
||||
println(i)
|
||||
val z = dummyIRS.evaluateCalculation(LocalDate.of(2016, 9, 15), Expression(i))
|
||||
println(z.javaClass)
|
||||
println(z)
|
||||
println("-----------")
|
||||
}
|
||||
// This does not throw an exception in the test itself; it evaluates the above and they will throw if they do not pass.
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates a typical transactional history for an IRS.
|
||||
*/
|
||||
fun trade(): LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter> {
|
||||
|
||||
val ld = LocalDate.of(2016, 3, 8)
|
||||
val bd = BigDecimal("0.0063518")
|
||||
|
||||
return ledger {
|
||||
transaction("Agreement") {
|
||||
output("irs post agreement") { singleIRS() }
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
transaction("Fix") {
|
||||
input("irs post agreement")
|
||||
val postAgreement = "irs post agreement".output<InterestRateSwap.State>()
|
||||
output("irs post first fixing") {
|
||||
postAgreement.copy(
|
||||
postAgreement.fixedLeg,
|
||||
postAgreement.floatingLeg,
|
||||
postAgreement.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
|
||||
postAgreement.common
|
||||
)
|
||||
}
|
||||
command(ORACLE_PUBKEY) {
|
||||
InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd))
|
||||
}
|
||||
timestamp(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure failure occurs when there are inbound states for an agreement command`() {
|
||||
val irs = singleIRS()
|
||||
transaction {
|
||||
input() { irs }
|
||||
output("irs post agreement") { irs }
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "There are no in states for an agreement"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure failure occurs when no events in fix schedule`() {
|
||||
val irs = singleIRS()
|
||||
val emptySchedule = HashMap<LocalDate, FixedRatePaymentEvent>()
|
||||
transaction {
|
||||
output() {
|
||||
irs.copy(calculation = irs.calculation.copy(fixedLegPaymentSchedule = emptySchedule))
|
||||
}
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "There are events in the fix schedule"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure failure occurs when no events in floating schedule`() {
|
||||
val irs = singleIRS()
|
||||
val emptySchedule = HashMap<LocalDate, FloatingRatePaymentEvent>()
|
||||
transaction {
|
||||
output() {
|
||||
irs.copy(calculation = irs.calculation.copy(floatingLegPaymentSchedule = emptySchedule))
|
||||
}
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "There are events in the float schedule"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure notionals are non zero`() {
|
||||
val irs = singleIRS()
|
||||
transaction {
|
||||
output() {
|
||||
irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(quantity = 0)))
|
||||
}
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "All notionals must be non zero"
|
||||
}
|
||||
|
||||
transaction {
|
||||
output() {
|
||||
irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(quantity = 0)))
|
||||
}
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "All notionals must be non zero"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure positive rate on fixed leg`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(fixedRate = FixedRate(PercentageRatioUnit("-0.1"))))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS
|
||||
}
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "The fixed leg rate must be positive"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This will be modified once we adapt the IRS to be cross currency.
|
||||
*/
|
||||
@Test
|
||||
fun `ensure same currency notionals`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.fixedLeg.notional.quantity, Currency.getInstance("JPY"))))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS
|
||||
}
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "The currency of the notionals must be the same"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure notional amounts are equal`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.floatingLeg.notional.quantity + 1, irs.floatingLeg.notional.token)))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS
|
||||
}
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "All leg notionals must be the same"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure trade date and termination date checks are done pt1`() {
|
||||
val irs = singleIRS()
|
||||
val modifiedIRS1 = irs.copy(fixedLeg = irs.fixedLeg.copy(terminationDate = irs.fixedLeg.effectiveDate.minusDays(1)))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS1
|
||||
}
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "The effective date is before the termination date for the fixed leg"
|
||||
}
|
||||
|
||||
val modifiedIRS2 = irs.copy(floatingLeg = irs.floatingLeg.copy(terminationDate = irs.floatingLeg.effectiveDate.minusDays(1)))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS2
|
||||
}
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "The effective date is before the termination date for the floating leg"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure trade date and termination date checks are done pt2`() {
|
||||
val irs = singleIRS()
|
||||
|
||||
val modifiedIRS3 = irs.copy(floatingLeg = irs.floatingLeg.copy(terminationDate = irs.fixedLeg.terminationDate.minusDays(1)))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS3
|
||||
}
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "The termination dates are aligned"
|
||||
}
|
||||
|
||||
|
||||
val modifiedIRS4 = irs.copy(floatingLeg = irs.floatingLeg.copy(effectiveDate = irs.fixedLeg.effectiveDate.minusDays(1)))
|
||||
transaction {
|
||||
output() {
|
||||
modifiedIRS4
|
||||
}
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "The effective dates are aligned"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `various fixing tests`() {
|
||||
val ld = LocalDate.of(2016, 3, 8)
|
||||
val bd = BigDecimal("0.0063518")
|
||||
|
||||
transaction {
|
||||
output("irs post agreement") { singleIRS() }
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
val oldIRS = singleIRS(1)
|
||||
val newIRS = oldIRS.copy(oldIRS.fixedLeg,
|
||||
oldIRS.floatingLeg,
|
||||
oldIRS.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
|
||||
oldIRS.common)
|
||||
|
||||
transaction {
|
||||
input() {
|
||||
oldIRS
|
||||
|
||||
}
|
||||
|
||||
// Templated tweak for reference. A corrent fixing applied should be ok
|
||||
tweak {
|
||||
command(ORACLE_PUBKEY) {
|
||||
InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd))
|
||||
}
|
||||
timestamp(TEST_TX_TIME)
|
||||
output() { newIRS }
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
// This test makes sure that verify confirms the fixing was applied and there is a difference in the old and new
|
||||
tweak {
|
||||
command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) }
|
||||
timestamp(TEST_TX_TIME)
|
||||
output() { oldIRS }
|
||||
this `fails with` "There is at least one difference in the IRS floating leg payment schedules"
|
||||
}
|
||||
|
||||
// This tests tries to sneak in a change to another fixing (which may or may not be the latest one)
|
||||
tweak {
|
||||
command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) }
|
||||
timestamp(TEST_TX_TIME)
|
||||
|
||||
val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.first()
|
||||
val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey]
|
||||
val modifiedFirstResetValue = firstResetValue!!.copy(notional = Amount(firstResetValue.notional.quantity, Currency.getInstance("JPY")))
|
||||
|
||||
output() {
|
||||
newIRS.copy(
|
||||
newIRS.fixedLeg,
|
||||
newIRS.floatingLeg,
|
||||
newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus(
|
||||
Pair(firstResetKey, modifiedFirstResetValue))),
|
||||
newIRS.common
|
||||
)
|
||||
}
|
||||
this `fails with` "There is only one change in the IRS floating leg payment schedule"
|
||||
}
|
||||
|
||||
// This tests modifies the payment currency for the fixing
|
||||
tweak {
|
||||
command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) }
|
||||
timestamp(TEST_TX_TIME)
|
||||
|
||||
val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key }
|
||||
val modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.quantity, Currency.getInstance("JPY")))
|
||||
|
||||
output() {
|
||||
newIRS.copy(
|
||||
newIRS.fixedLeg,
|
||||
newIRS.floatingLeg,
|
||||
newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus(
|
||||
Pair(latestReset.key, modifiedLatestResetValue))),
|
||||
newIRS.common
|
||||
)
|
||||
}
|
||||
this `fails with` "The fix payment has the same currency as the notional"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This returns an example of transactions that are grouped by TradeId and then a fixing applied.
|
||||
* It's important to make the tradeID different for two reasons, the hashes will be the same and all sorts of confusion will
|
||||
* result and the grouping won't work either.
|
||||
* In reality, the only fields that should be in common will be the next fixing date and the reference rate.
|
||||
*/
|
||||
fun tradegroups(): LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter> {
|
||||
val ld1 = LocalDate.of(2016, 3, 8)
|
||||
val bd1 = BigDecimal("0.0063518")
|
||||
|
||||
val irs = singleIRS()
|
||||
|
||||
return ledger {
|
||||
transaction("Agreement") {
|
||||
output("irs post agreement1") {
|
||||
irs.copy(
|
||||
irs.fixedLeg,
|
||||
irs.floatingLeg,
|
||||
irs.calculation,
|
||||
irs.common.copy(tradeID = "t1")
|
||||
)
|
||||
}
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
transaction("Agreement") {
|
||||
output("irs post agreement2") {
|
||||
irs.copy(
|
||||
linearId = UniqueIdentifier("t2"),
|
||||
fixedLeg = irs.fixedLeg,
|
||||
floatingLeg = irs.floatingLeg,
|
||||
calculation = irs.calculation,
|
||||
common = irs.common.copy(tradeID = "t2")
|
||||
)
|
||||
}
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
|
||||
transaction("Fix") {
|
||||
input("irs post agreement1")
|
||||
input("irs post agreement2")
|
||||
val postAgreement1 = "irs post agreement1".output<InterestRateSwap.State>()
|
||||
output("irs post first fixing1") {
|
||||
postAgreement1.copy(
|
||||
postAgreement1.fixedLeg,
|
||||
postAgreement1.floatingLeg,
|
||||
postAgreement1.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
|
||||
postAgreement1.common.copy(tradeID = "t1")
|
||||
)
|
||||
}
|
||||
val postAgreement2 = "irs post agreement2".output<InterestRateSwap.State>()
|
||||
output("irs post first fixing2") {
|
||||
postAgreement2.copy(
|
||||
postAgreement2.fixedLeg,
|
||||
postAgreement2.floatingLeg,
|
||||
postAgreement2.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
|
||||
postAgreement2.common.copy(tradeID = "t2")
|
||||
)
|
||||
}
|
||||
|
||||
command(ORACLE_PUBKEY) {
|
||||
InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1))
|
||||
}
|
||||
timestamp(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ import java.io.BufferedInputStream
|
||||
import java.io.InputStream
|
||||
import java.math.BigDecimal
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.LinkOption
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.time.temporal.Temporal
|
||||
@ -89,6 +90,7 @@ inline fun <T> SettableFuture<T>.catch(block: () -> T) {
|
||||
}
|
||||
|
||||
fun <R> Path.use(block: (InputStream) -> R): R = Files.newInputStream(this).use(block)
|
||||
fun Path.exists(vararg options: LinkOption): Boolean = Files.exists(this, *options)
|
||||
|
||||
// Simple infix function to add back null safety that the JDK lacks: timeA until timeB
|
||||
infix fun Temporal.until(endExclusive: Temporal) = Duration.between(this, endExclusive)
|
||||
@ -290,7 +292,7 @@ fun <T, I: Comparable<I>> Iterable<T>.isOrderedAndUnique(extractId: T.() -> I):
|
||||
if (lastLast == null) {
|
||||
true
|
||||
} else {
|
||||
lastLast.compareTo(extractId(it)) < 0
|
||||
lastLast < extractId(it)
|
||||
}
|
||||
}
|
||||
}
|
@ -20,4 +20,4 @@ data class NodeInfo(val address: SingleMessageRecipient,
|
||||
val physicalLocation: PhysicalLocation? = null) {
|
||||
val notaryIdentity: Party get() = advertisedServices.single { it.info.type.isNotary() }.identity
|
||||
fun serviceIdentities(type: ServiceType): List<Party> = advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity }
|
||||
}
|
||||
}
|
@ -8,8 +8,8 @@ import com.r3corda.core.messaging.MessagingService
|
||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.security.PublicKey
|
||||
import rx.Observable
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* A network map contains lists of nodes on the network along with information about their identity keys, services
|
||||
@ -23,7 +23,7 @@ interface NetworkMapCache {
|
||||
}
|
||||
|
||||
enum class MapChangeType { Added, Removed, Modified }
|
||||
data class MapChange(val node: NodeInfo, val prevNodeInfo: NodeInfo?, val type: MapChangeType )
|
||||
data class MapChange(val node: NodeInfo, val prevNodeInfo: NodeInfo?, val type: MapChangeType)
|
||||
|
||||
/** A list of nodes that advertise a network map service */
|
||||
val networkMapNodes: List<NodeInfo>
|
||||
@ -43,6 +43,12 @@ interface NetworkMapCache {
|
||||
*/
|
||||
val regulators: List<NodeInfo>
|
||||
|
||||
/**
|
||||
* Atomically get the current party nodes and a stream of updates. Note that the Observable buffers updates until the
|
||||
* first subscriber is registered so as to avoid racing with early updates.
|
||||
*/
|
||||
fun track(): Pair<List<NodeInfo>, Observable<MapChange>>
|
||||
|
||||
/**
|
||||
* Get a copy of all nodes in the map.
|
||||
*/
|
||||
|
@ -27,7 +27,7 @@ import kotlin.reflect.primaryConstructor
|
||||
*/
|
||||
class ProtocolLogicRefFactory(private val protocolWhitelist: Map<String, Set<String>>) : SingletonSerializeAsToken() {
|
||||
|
||||
constructor() : this(mapOf(Pair(TwoPartyDealProtocol.FixingRoleDecider::class.java.name, setOf(StateRef::class.java.name, Duration::class.java.name))))
|
||||
constructor() : this(mapOf())
|
||||
|
||||
// Pending real dependence on AppContext for class loading etc
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
|
@ -12,6 +12,7 @@ data class StateMachineRunId private constructor(val uuid: UUID) {
|
||||
|
||||
companion object {
|
||||
fun createRandom(): StateMachineRunId = StateMachineRunId(UUID.randomUUID())
|
||||
fun wrap(uuid: UUID): StateMachineRunId = StateMachineRunId(uuid)
|
||||
}
|
||||
|
||||
override fun toString(): String = "[$uuid]"
|
||||
|
@ -1,19 +0,0 @@
|
||||
package com.r3corda.core.utilities
|
||||
|
||||
import java.time.*
|
||||
|
||||
/**
|
||||
* This whole file exists as short cuts to get demos working. In reality we'd have static data and/or rules engine
|
||||
* defining things like this. It currently resides in the core module because it needs to be visible to the IRS
|
||||
* contract.
|
||||
*/
|
||||
// We at some future point may implement more than just this constant announcement window and thus use the params.
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun suggestInterestRateAnnouncementTimeWindow(index: String, source: String, date: LocalDate): TimeWindow {
|
||||
// TODO: we would ordinarily convert clock to same time zone as the index/source would announce in
|
||||
// and suggest an announcement time for the interest rate
|
||||
// Here we apply a blanket announcement time of 11:45 London irrespective of source or index
|
||||
val time = LocalTime.of(11, 45)
|
||||
val zoneId = ZoneId.of("Europe/London")
|
||||
return TimeWindow(ZonedDateTime.of(date, time, zoneId).toInstant(), Duration.ofHours(24))
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
package com.r3corda.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.r3corda.core.contracts.Fix
|
||||
import com.r3corda.core.contracts.FixOf
|
||||
import com.r3corda.core.crypto.DigitalSignature
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import com.r3corda.core.transactions.WireTransaction
|
||||
import com.r3corda.core.utilities.ProgressTracker
|
||||
import com.r3corda.core.utilities.suggestInterestRateAnnouncementTimeWindow
|
||||
import com.r3corda.protocols.RatesFixProtocol.FixOutOfRange
|
||||
import java.math.BigDecimal
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
// This code is unit tested in NodeInterestRates.kt
|
||||
|
||||
/**
|
||||
* This protocol queries the given oracle for an interest rate fix, and if it is within the given tolerance embeds the
|
||||
* fix in the transaction and then proceeds to get the oracle to sign it. Although the [call] method combines the query
|
||||
* and signing step, you can run the steps individually by constructing this object and then using the public methods
|
||||
* for each step.
|
||||
*
|
||||
* @throws FixOutOfRange if the returned fix was further away from the expected rate by the given amount.
|
||||
*/
|
||||
open class RatesFixProtocol(protected val tx: TransactionBuilder,
|
||||
private val oracle: Party,
|
||||
private val fixOf: FixOf,
|
||||
private val expectedRate: BigDecimal,
|
||||
private val rateTolerance: BigDecimal,
|
||||
override val progressTracker: ProgressTracker = RatesFixProtocol.tracker(fixOf.name)) : ProtocolLogic<Unit>() {
|
||||
|
||||
companion object {
|
||||
class QUERYING(val name: String) : ProgressTracker.Step("Querying oracle for $name interest rate")
|
||||
object WORKING : ProgressTracker.Step("Working with data returned by oracle")
|
||||
object SIGNING : ProgressTracker.Step("Requesting confirmation signature from interest rate oracle")
|
||||
|
||||
fun tracker(fixName: String) = ProgressTracker(QUERYING(fixName), WORKING, SIGNING)
|
||||
}
|
||||
|
||||
class FixOutOfRange(@Suppress("unused") val byAmount: BigDecimal) : Exception("Fix out of range by $byAmount")
|
||||
|
||||
data class QueryRequest(val queries: List<FixOf>, val deadline: Instant)
|
||||
data class SignRequest(val tx: WireTransaction)
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
progressTracker.currentStep = progressTracker.steps[1]
|
||||
val fix = subProtocol(FixQueryProtocol(fixOf, oracle))
|
||||
progressTracker.currentStep = WORKING
|
||||
checkFixIsNearExpected(fix)
|
||||
tx.addCommand(fix, oracle.owningKey)
|
||||
beforeSigning(fix)
|
||||
progressTracker.currentStep = SIGNING
|
||||
val signature = subProtocol(FixSignProtocol(tx, oracle))
|
||||
tx.addSignatureUnchecked(signature)
|
||||
}
|
||||
|
||||
/**
|
||||
* You can override this to perform any additional work needed after the fix is added to the transaction but
|
||||
* before it's sent back to the oracle for signing (for example, adding output states that depend on the fix).
|
||||
*/
|
||||
@Suspendable
|
||||
protected open fun beforeSigning(fix: Fix) {
|
||||
}
|
||||
|
||||
private fun checkFixIsNearExpected(fix: Fix) {
|
||||
val delta = (fix.value - expectedRate).abs()
|
||||
if (delta > rateTolerance) {
|
||||
// TODO: Kick to a user confirmation / ui flow if it's out of bounds instead of raising an exception.
|
||||
throw FixOutOfRange(delta)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FixQueryProtocol(val fixOf: FixOf, val oracle: Party) : ProtocolLogic<Fix>() {
|
||||
@Suspendable
|
||||
override fun call(): Fix {
|
||||
val deadline = suggestInterestRateAnnouncementTimeWindow(fixOf.name, oracle.name, fixOf.forDay).end
|
||||
// TODO: add deadline to receive
|
||||
val resp = sendAndReceive<ArrayList<Fix>>(oracle, QueryRequest(listOf(fixOf), deadline))
|
||||
|
||||
return resp.unwrap {
|
||||
val fix = it.first()
|
||||
// Check the returned fix is for what we asked for.
|
||||
check(fix.of == fixOf)
|
||||
fix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FixSignProtocol(val tx: TransactionBuilder, val oracle: Party) : ProtocolLogic<DigitalSignature.LegallyIdentifiable>() {
|
||||
@Suspendable
|
||||
override fun call(): DigitalSignature.LegallyIdentifiable {
|
||||
val wtx = tx.toWireTransaction()
|
||||
val resp = sendAndReceive<DigitalSignature.LegallyIdentifiable>(oracle, SignRequest(wtx))
|
||||
|
||||
return resp.unwrap { sig ->
|
||||
check(sig.signer == oracle)
|
||||
tx.checkSignature(sig)
|
||||
sig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -306,126 +306,4 @@ object TwoPartyDealProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One side of the fixing protocol for an interest rate swap, but could easily be generalised further.
|
||||
*
|
||||
* Do not infer too much from the name of the class. This is just to indicate that it is the "side"
|
||||
* of the protocol that is run by the party with the fixed leg of swap deal, which is the basis for deciding
|
||||
* who does what in the protocol.
|
||||
*/
|
||||
class Fixer(override val otherParty: Party,
|
||||
override val progressTracker: ProgressTracker = Secondary.tracker()) : Secondary<FixingSession>() {
|
||||
|
||||
private lateinit var txState: TransactionState<*>
|
||||
private lateinit var deal: FixableDealState
|
||||
|
||||
override fun validateHandshake(handshake: Handshake<FixingSession>): Handshake<FixingSession> {
|
||||
logger.trace { "Got fixing request for: ${handshake.payload}" }
|
||||
|
||||
txState = serviceHub.loadState(handshake.payload.ref)
|
||||
deal = txState.data as FixableDealState
|
||||
|
||||
// validate the party that initiated is the one on the deal and that the recipient corresponds with it.
|
||||
// TODO: this is in no way secure and will be replaced by general session initiation logic in the future
|
||||
val myName = serviceHub.myInfo.legalIdentity.name
|
||||
// Also check we are one of the parties
|
||||
deal.parties.filter { it.name == myName }.single()
|
||||
|
||||
return handshake
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun assembleSharedTX(handshake: Handshake<FixingSession>): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val fixOf = deal.nextFixingOf()!!
|
||||
|
||||
// TODO Do we need/want to substitute in new public keys for the Parties?
|
||||
val myName = serviceHub.myInfo.legalIdentity.name
|
||||
val myOldParty = deal.parties.single { it.name == myName }
|
||||
|
||||
val newDeal = deal
|
||||
|
||||
val ptx = TransactionType.General.Builder(txState.notary)
|
||||
|
||||
val oracle = serviceHub.networkMapCache.get(handshake.payload.oracleType).first()
|
||||
|
||||
val addFixing = object : RatesFixProtocol(ptx, oracle.serviceIdentities(handshake.payload.oracleType).first(), fixOf, BigDecimal.ZERO, BigDecimal.ONE) {
|
||||
@Suspendable
|
||||
override fun beforeSigning(fix: Fix) {
|
||||
newDeal.generateFix(ptx, StateAndRef(txState, handshake.payload.ref), fix)
|
||||
|
||||
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
|
||||
// to have one.
|
||||
ptx.setTime(serviceHub.clock.instant(), 30.seconds)
|
||||
}
|
||||
}
|
||||
subProtocol(addFixing)
|
||||
return Pair(ptx, arrayListOf(myOldParty.owningKey))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One side of the fixing protocol for an interest rate swap, but could easily be generalised furher.
|
||||
*
|
||||
* As per the [Fixer], do not infer too much from this class name in terms of business roles. This
|
||||
* is just the "side" of the protocol run by the party with the floating leg as a way of deciding who
|
||||
* does what in the protocol.
|
||||
*/
|
||||
class Floater(override val otherParty: Party,
|
||||
override val payload: FixingSession,
|
||||
override val progressTracker: ProgressTracker = Primary.tracker()) : Primary() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal val dealToFix: StateAndRef<FixableDealState> by TransientProperty {
|
||||
val state = serviceHub.loadState(payload.ref) as TransactionState<FixableDealState>
|
||||
StateAndRef(state, payload.ref)
|
||||
}
|
||||
|
||||
override val myKeyPair: KeyPair get() {
|
||||
val myName = serviceHub.myInfo.legalIdentity.name
|
||||
val publicKey = dealToFix.state.data.parties.filter { it.name == myName }.single().owningKey
|
||||
return serviceHub.keyManagementService.toKeyPair(publicKey)
|
||||
}
|
||||
|
||||
override val notaryNode: NodeInfo get() =
|
||||
serviceHub.networkMapCache.notaryNodes.filter { it.notaryIdentity == dealToFix.state.notary }.single()
|
||||
}
|
||||
|
||||
|
||||
/** Used to set up the session between [Floater] and [Fixer] */
|
||||
data class FixingSession(val ref: StateRef, val oracleType: ServiceType)
|
||||
|
||||
/**
|
||||
* This protocol looks at the deal and decides whether to be the Fixer or Floater role in agreeing a fixing.
|
||||
*
|
||||
* It is kicked off as an activity on both participant nodes by the scheduler when it's time for a fixing. If the
|
||||
* Fixer role is chosen, then that will be initiated by the [FixingSession] message sent from the other party and
|
||||
* handled by the [FixingSessionInitiationHandler].
|
||||
*
|
||||
* TODO: Replace [FixingSession] and [FixingSessionInitiationHandler] with generic session initiation logic once it exists.
|
||||
*/
|
||||
class FixingRoleDecider(val ref: StateRef,
|
||||
override val progressTracker: ProgressTracker = tracker()) : ProtocolLogic<Unit>() {
|
||||
|
||||
companion object {
|
||||
class LOADING() : ProgressTracker.Step("Loading state to decide fixing role")
|
||||
|
||||
fun tracker() = ProgressTracker(LOADING())
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Unit {
|
||||
progressTracker.nextStep()
|
||||
val dealToFix = serviceHub.loadState(ref)
|
||||
// TODO: this is not the eventual mechanism for identifying the parties
|
||||
val fixableDeal = (dealToFix.data as FixableDealState)
|
||||
val sortedParties = fixableDeal.parties.sortedBy { it.name }
|
||||
if (sortedParties[0].name == serviceHub.myInfo.legalIdentity.name) {
|
||||
val fixing = FixingSession(ref, fixableDeal.oracleType)
|
||||
// Start the Floater which will then kick-off the Fixer
|
||||
subProtocol(Floater(sortedParties[1], fixing))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
24
core/src/test/kotlin/com/r3corda/core/UtilsTest.kt
Normal file
24
core/src/test/kotlin/com/r3corda/core/UtilsTest.kt
Normal file
@ -0,0 +1,24 @@
|
||||
package com.r3corda.core
|
||||
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class UtilsTest {
|
||||
fun `ordered and unique basic`() {
|
||||
val basic = listOf(1, 2, 3, 5, 8)
|
||||
assertTrue(basic.isOrderedAndUnique { this })
|
||||
|
||||
val negative = listOf(-1, 2, 5)
|
||||
assertTrue(negative.isOrderedAndUnique { this })
|
||||
}
|
||||
|
||||
fun `ordered and unique duplicate`() {
|
||||
val duplicated = listOf(1, 2, 2, 3, 5, 8)
|
||||
assertFalse(duplicated.isOrderedAndUnique { this })
|
||||
}
|
||||
|
||||
fun `ordered and unique out of sequence`() {
|
||||
val mixed = listOf(3, 1, 2, 8, 5)
|
||||
assertFalse(mixed.isOrderedAndUnique { this })
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package com.r3corda.docs
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.r3corda.client.CordaRPCClient
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.node.services.config.NodeSSLConfiguration
|
||||
import org.graphstream.graph.Edge
|
||||
import org.graphstream.graph.Node
|
||||
import org.graphstream.graph.implementations.SingleGraph
|
||||
@ -26,12 +27,18 @@ fun main(args: Array<String>) {
|
||||
}
|
||||
val nodeAddress = HostAndPort.fromString(args[0])
|
||||
val printOrVisualise = PrintOrVisualise.valueOf(args[1])
|
||||
val certificatesPath = Paths.get("build/trader-demo/buyer/certificates")
|
||||
val sslConfig = object : NodeSSLConfiguration {
|
||||
override val certificatesPath = Paths.get("build/trader-demo/buyer/certificates")
|
||||
override val keyStorePassword = "cordacadevpass"
|
||||
override val trustStorePassword = "trustpass"
|
||||
}
|
||||
// END 1
|
||||
|
||||
// START 2
|
||||
val client = CordaRPCClient(nodeAddress, certificatesPath)
|
||||
client.start()
|
||||
val username = System.console().readLine("Enter username: ")
|
||||
val password = String(System.console().readPassword("Enter password: "))
|
||||
val client = CordaRPCClient(nodeAddress, sslConfig)
|
||||
client.start(username, password)
|
||||
val proxy = client.proxy()
|
||||
// END 2
|
||||
|
||||
@ -65,7 +72,7 @@ fun main(args: Array<String>) {
|
||||
futureTransactions.subscribe { transaction ->
|
||||
graph.addNode<Node>("${transaction.id}")
|
||||
transaction.tx.inputs.forEach { ref ->
|
||||
graph.addEdge<Edge>("${ref}", "${ref.txhash}", "${transaction.id}")
|
||||
graph.addEdge<Edge>("$ref", "${ref.txhash}", "${transaction.id}")
|
||||
}
|
||||
}
|
||||
graph.display()
|
||||
|
@ -16,7 +16,7 @@ we also need to access the certificates of the node, we will access the node's `
|
||||
:start-after: START 1
|
||||
:end-before: END 1
|
||||
|
||||
Now we can connect to the node itself:
|
||||
Now we can connect to the node itself using a valid RPC login. By default the user `user1` is available with password `test`.
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/com/r3corda/docs/ClientRpcTutorial.kt
|
||||
:language: kotlin
|
||||
|
@ -1,24 +1,18 @@
|
||||
package com.r3corda.explorer
|
||||
|
||||
import com.r3corda.client.mock.EventGenerator
|
||||
import com.r3corda.client.model.Models
|
||||
import com.r3corda.client.model.NodeMonitorModel
|
||||
import com.r3corda.client.model.subject
|
||||
import com.r3corda.core.contracts.ClientToServiceCommand
|
||||
import com.r3corda.core.node.services.ServiceInfo
|
||||
import com.r3corda.explorer.model.IdentityModel
|
||||
import com.r3corda.node.driver.PortAllocation
|
||||
import com.r3corda.node.driver.driver
|
||||
import com.r3corda.node.driver.startClient
|
||||
import com.r3corda.node.services.config.configureTestSSL
|
||||
import com.r3corda.node.services.transactions.SimpleNotaryService
|
||||
import javafx.stage.Stage
|
||||
import rx.subjects.Subject
|
||||
import tornadofx.App
|
||||
import java.util.*
|
||||
|
||||
class Main : App() {
|
||||
override val primaryView = MainWindow::class
|
||||
val aliceOutStream: Subject<ClientToServiceCommand, ClientToServiceCommand> by subject(NodeMonitorModel::clientToService)
|
||||
|
||||
override fun start(stage: Stage) {
|
||||
|
||||
@ -32,22 +26,22 @@ class Main : App() {
|
||||
// start the driver on another thread
|
||||
// TODO Change this to connecting to an actual node (specified on cli/in a config) once we're happy with the code
|
||||
Thread({
|
||||
|
||||
val portAllocation = PortAllocation.Incremental(20000)
|
||||
driver(portAllocation = portAllocation) {
|
||||
|
||||
val aliceNodeFuture = startNode("Alice")
|
||||
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
|
||||
val aliceNode = aliceNodeFuture.get()
|
||||
val notaryNode = notaryNodeFuture.get()
|
||||
|
||||
val aliceClient = startClient(aliceNode).get()
|
||||
val aliceNode = aliceNodeFuture.get().nodeInfo
|
||||
val notaryNode = notaryNodeFuture.get().nodeInfo
|
||||
|
||||
Models.get<IdentityModel>(Main::class).notary.set(notaryNode.notaryIdentity)
|
||||
Models.get<IdentityModel>(Main::class).myIdentity.set(aliceNode.legalIdentity)
|
||||
Models.get<NodeMonitorModel>(Main::class).register(aliceNode, aliceClient.config.certificatesPath)
|
||||
Models.get<NodeMonitorModel>(Main::class).register(aliceNode, configureTestSSL(), "user1", "test")
|
||||
|
||||
for (i in 0 .. 10000) {
|
||||
startNode("Bob").get()
|
||||
|
||||
/* for (i in 0 .. 10000) {
|
||||
Thread.sleep(500)
|
||||
|
||||
val eventGenerator = EventGenerator(
|
||||
@ -58,11 +52,9 @@ class Main : App() {
|
||||
eventGenerator.clientToServiceCommandGenerator.map { command ->
|
||||
aliceOutStream.onNext(command)
|
||||
}.generate(Random())
|
||||
}
|
||||
|
||||
}*/
|
||||
waitForAllNodesToFinish()
|
||||
}
|
||||
|
||||
}).start()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package com.r3corda.explorer.components
|
||||
|
||||
import javafx.scene.control.Alert
|
||||
import javafx.scene.control.Label
|
||||
import javafx.scene.control.TextArea
|
||||
import javafx.scene.layout.GridPane
|
||||
import javafx.scene.layout.Priority
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
class ExceptionDialog(ex: Throwable) : Alert(AlertType.ERROR) {
|
||||
|
||||
private fun Throwable.toExceptionText(): String {
|
||||
return StringWriter().use {
|
||||
PrintWriter(it).use {
|
||||
this.printStackTrace(it)
|
||||
}
|
||||
it.toString()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// Create expandable Exception.
|
||||
val label = Label("The exception stacktrace was:")
|
||||
contentText = ex.message
|
||||
|
||||
val textArea = TextArea(ex.toExceptionText())
|
||||
textArea.isEditable = false
|
||||
textArea.isWrapText = true
|
||||
|
||||
textArea.maxWidth = Double.MAX_VALUE
|
||||
textArea.maxHeight = Double.MAX_VALUE
|
||||
GridPane.setVgrow(textArea, Priority.ALWAYS)
|
||||
GridPane.setHgrow(textArea, Priority.ALWAYS)
|
||||
|
||||
val expContent = GridPane()
|
||||
expContent.maxWidth = Double.MAX_VALUE
|
||||
expContent.add(label, 0, 0)
|
||||
expContent.add(textArea, 0, 1)
|
||||
|
||||
// Set expandable Exception into the dialog pane.
|
||||
dialogPane.expandableContent = expContent
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ package com.r3corda.explorer.model
|
||||
import com.r3corda.core.crypto.Party
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
|
||||
|
||||
class IdentityModel {
|
||||
val myIdentity = SimpleObjectProperty<Party>()
|
||||
}
|
||||
val myIdentity = SimpleObjectProperty<Party?>()
|
||||
val notary = SimpleObjectProperty<Party?>()
|
||||
}
|
@ -5,7 +5,8 @@ import javafx.beans.property.SimpleObjectProperty
|
||||
enum class SelectedView {
|
||||
Home,
|
||||
Cash,
|
||||
Transaction
|
||||
Transaction,
|
||||
NewTransaction
|
||||
}
|
||||
|
||||
class TopLevelModel {
|
||||
|
@ -0,0 +1,7 @@
|
||||
package com.r3corda.explorer.model
|
||||
|
||||
enum class CashTransaction(val partyNameA: String, val partyNameB: String?) {
|
||||
Issue("Issuer Bank", "Receiver Bank"),
|
||||
Pay("Payer", "Payee"),
|
||||
Exit("Issuer Bank", null);
|
||||
}
|
@ -33,6 +33,7 @@ class Header : View() {
|
||||
SelectedView.Home -> "Home"
|
||||
SelectedView.Cash -> "Cash"
|
||||
SelectedView.Transaction -> "Transactions"
|
||||
SelectedView.NewTransaction -> "New Transaction"
|
||||
null -> "Home"
|
||||
}
|
||||
})
|
||||
@ -42,6 +43,7 @@ class Header : View() {
|
||||
SelectedView.Home -> homeImage
|
||||
SelectedView.Cash -> cashImage
|
||||
SelectedView.Transaction -> transactionImage
|
||||
SelectedView.NewTransaction -> cashImage
|
||||
null -> homeImage
|
||||
}
|
||||
})
|
||||
@ -60,4 +62,4 @@ class Header : View() {
|
||||
sectionIcon.fitWidthProperty().bind(secionLabelHeightNonZero)
|
||||
sectionIcon.fitHeightProperty().bind(sectionIcon.fitWidthProperty())
|
||||
}
|
||||
}
|
||||
}
|
@ -18,11 +18,9 @@ import javafx.scene.control.Label
|
||||
import javafx.scene.control.TitledPane
|
||||
import javafx.scene.input.MouseButton
|
||||
import javafx.scene.layout.TilePane
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import tornadofx.View
|
||||
import java.util.*
|
||||
|
||||
|
||||
class Home : View() {
|
||||
override val root: TilePane by fxml()
|
||||
|
||||
@ -32,6 +30,8 @@ class Home : View() {
|
||||
private val ourTransactionsPane: TitledPane by fxid()
|
||||
private val ourTransactionsLabel: Label by fxid()
|
||||
|
||||
private val newTransaction: TitledPane by fxid()
|
||||
|
||||
private val selectedView: WritableValue<SelectedView> by writableValue(TopLevelModel::selectedView)
|
||||
private val cashStates: ObservableList<StateAndRef<Cash.State>> by observableList(ContractStateModel::cashStates)
|
||||
private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData>
|
||||
@ -63,6 +63,11 @@ class Home : View() {
|
||||
selectedView.value = SelectedView.Transaction
|
||||
}
|
||||
}
|
||||
newTransaction.setOnMouseClicked { clickEvent ->
|
||||
if (clickEvent.button == MouseButton.PRIMARY) {
|
||||
selectedView.value = SelectedView.NewTransaction
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import com.r3corda.client.fxutils.map
|
||||
import com.r3corda.client.model.NetworkIdentityModel
|
||||
import com.r3corda.client.model.NodeMonitorModel
|
||||
import com.r3corda.client.model.observableList
|
||||
import com.r3corda.client.model.observableValue
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.serialization.OpaqueBytes
|
||||
import com.r3corda.explorer.components.ExceptionDialog
|
||||
import com.r3corda.explorer.model.CashTransaction
|
||||
import com.r3corda.explorer.model.IdentityModel
|
||||
import com.r3corda.node.services.messaging.CordaRPCOps
|
||||
import com.r3corda.node.services.messaging.TransactionBuildResult
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.binding.BooleanBinding
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.control.*
|
||||
import javafx.util.StringConverter
|
||||
import javafx.util.converter.BigDecimalStringConverter
|
||||
import tornadofx.View
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class NewTransaction : View() {
|
||||
override val root: Parent by fxml()
|
||||
|
||||
private val partyATextField: TextField by fxid()
|
||||
private val partyBChoiceBox: ChoiceBox<NodeInfo> by fxid()
|
||||
private val partyALabel: Label by fxid()
|
||||
private val partyBLabel: Label by fxid()
|
||||
private val amountLabel: Label by fxid()
|
||||
|
||||
private val executeButton: Button by fxid()
|
||||
|
||||
private val transactionTypeCB: ChoiceBox<CashTransaction> by fxid()
|
||||
private val amount: TextField by fxid()
|
||||
private val currency: ChoiceBox<Currency> by fxid()
|
||||
|
||||
private val networkIdentities: ObservableList<NodeInfo> by observableList(NetworkIdentityModel::networkIdentities)
|
||||
|
||||
private val rpcProxy: ObservableValue<CordaRPCOps?> by observableValue(NodeMonitorModel::proxyObservable)
|
||||
private val myIdentity: ObservableValue<Party?> by observableValue(IdentityModel::myIdentity)
|
||||
private val notary: ObservableValue<Party?> by observableValue(IdentityModel::notary)
|
||||
|
||||
private val issueRefLabel: Label by fxid()
|
||||
private val issueRefTextField: TextField by fxid()
|
||||
|
||||
private fun ObservableValue<*>.isNotNull(): BooleanBinding {
|
||||
return Bindings.createBooleanBinding({ this.value != null }, arrayOf(this))
|
||||
}
|
||||
|
||||
fun resetScreen() {
|
||||
partyBChoiceBox.valueProperty().set(null)
|
||||
transactionTypeCB.valueProperty().set(null)
|
||||
currency.valueProperty().set(null)
|
||||
amount.clear()
|
||||
}
|
||||
|
||||
init {
|
||||
// Disable everything when not connected to node.
|
||||
val enableProperty = myIdentity.isNotNull().and(notary.isNotNull()).and(rpcProxy.isNotNull())
|
||||
root.disableProperty().bind(enableProperty.not())
|
||||
transactionTypeCB.items = FXCollections.observableArrayList(CashTransaction.values().asList())
|
||||
|
||||
// Party A textfield always display my identity name, not editable.
|
||||
partyATextField.isEditable = false
|
||||
partyATextField.textProperty().bind(myIdentity.map { it?.name ?: "" })
|
||||
partyALabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA?.let { "$it : " } })
|
||||
partyATextField.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA }.isNotNull())
|
||||
|
||||
partyBLabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB?.let { "$it : " } })
|
||||
partyBChoiceBox.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB }.isNotNull())
|
||||
partyBChoiceBox.items = networkIdentities
|
||||
|
||||
partyBChoiceBox.converter = object : StringConverter<NodeInfo?>() {
|
||||
override fun toString(node: NodeInfo?): String {
|
||||
return node?.legalIdentity?.name ?: ""
|
||||
}
|
||||
|
||||
override fun fromString(string: String?): NodeInfo {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
// BigDecimal text Formatter, restricting text box input to decimal values.
|
||||
val textFormatter = Pattern.compile("-?((\\d*)|(\\d+\\.\\d*))").run {
|
||||
TextFormatter<BigDecimal>(BigDecimalStringConverter(), null) { change ->
|
||||
val newText = change.controlNewText
|
||||
if (matcher(newText).matches()) change else null
|
||||
}
|
||||
}
|
||||
amount.textFormatter = textFormatter
|
||||
|
||||
// Hide currency and amount fields when transaction type is not specified.
|
||||
// TODO : Create a currency model to store these values
|
||||
currency.items = FXCollections.observableList(setOf(USD, GBP, CHF).toList())
|
||||
currency.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
amount.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
amountLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
issueRefLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
issueRefTextField.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
|
||||
// Validate inputs.
|
||||
val formValidCondition = arrayOf(
|
||||
myIdentity.isNotNull(),
|
||||
transactionTypeCB.valueProperty().isNotNull,
|
||||
partyBChoiceBox.visibleProperty().not().or(partyBChoiceBox.valueProperty().isNotNull),
|
||||
textFormatter.valueProperty().isNotNull,
|
||||
textFormatter.valueProperty().isNotEqualTo(BigDecimal.ZERO),
|
||||
currency.valueProperty().isNotNull
|
||||
).reduce(BooleanBinding::and)
|
||||
|
||||
// Enable execute button when form is valid.
|
||||
executeButton.disableProperty().bind(formValidCondition.not())
|
||||
executeButton.setOnAction { event ->
|
||||
// Null checks to ensure these observable values are set, execute button should be disabled if any of these value are null, this extra checks are for precaution and getting non-nullable values without using !!.
|
||||
myIdentity.value?.let { myIdentity ->
|
||||
notary.value?.let { notary ->
|
||||
rpcProxy.value?.let { rpcProxy ->
|
||||
Triple(myIdentity, notary, rpcProxy)
|
||||
}
|
||||
}
|
||||
}?.let {
|
||||
val (myIdentity, notary, rpcProxy) = it
|
||||
transactionTypeCB.value?.let {
|
||||
val issueRef = OpaqueBytes(if (issueRefTextField.text.trim().isNotBlank()) issueRefTextField.text.toByteArray() else ByteArray(1, { 1 }))
|
||||
val command = when (it) {
|
||||
CashTransaction.Issue -> ClientToServiceCommand.IssueCash(Amount(textFormatter.value, currency.value), issueRef, partyBChoiceBox.value.legalIdentity, notary)
|
||||
CashTransaction.Pay -> ClientToServiceCommand.PayCash(Amount(textFormatter.value, Issued(PartyAndReference(myIdentity, issueRef), currency.value)), partyBChoiceBox.value.legalIdentity)
|
||||
CashTransaction.Exit -> ClientToServiceCommand.ExitCash(Amount(textFormatter.value, currency.value), issueRef)
|
||||
}
|
||||
|
||||
val dialog = Alert(Alert.AlertType.INFORMATION).apply {
|
||||
headerText = null
|
||||
contentText = "Transaction Started."
|
||||
dialogPane.isDisable = true
|
||||
initOwner((event.target as Node).scene.window)
|
||||
}
|
||||
dialog.show()
|
||||
runAsync {
|
||||
rpcProxy.executeCommand(command)
|
||||
}.ui {
|
||||
dialog.contentText = when (it) {
|
||||
is TransactionBuildResult.ProtocolStarted -> {
|
||||
dialog.alertType = Alert.AlertType.INFORMATION
|
||||
dialog.setOnCloseRequest { resetScreen() }
|
||||
"Transaction Started \nTransaction ID : ${it.transaction?.id} \nMessage : ${it.message}"
|
||||
}
|
||||
is TransactionBuildResult.Failed -> {
|
||||
dialog.alertType = Alert.AlertType.ERROR
|
||||
it.toString()
|
||||
}
|
||||
}
|
||||
dialog.dialogPane.isDisable = false
|
||||
dialog.dialogPane.scene.window.sizeToScene()
|
||||
}.setOnFailed {
|
||||
dialog.close()
|
||||
ExceptionDialog(it.source.exception).apply {
|
||||
initOwner((event.target as Node).scene.window)
|
||||
showAndWait()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove focus from textfield when click on the blank area.
|
||||
root.setOnMouseClicked { e -> root.requestFocus() }
|
||||
}
|
||||
}
|
@ -20,17 +20,20 @@ class TopLevel : View() {
|
||||
private val home: Home by inject()
|
||||
private val cash: CashViewer by inject()
|
||||
private val transaction: TransactionViewer by inject()
|
||||
private val newTransaction: NewTransaction by inject()
|
||||
|
||||
// Note: this is weirdly very important, as it forces the initialisation of Views. Therefore this is the entry
|
||||
// point to the top level observable/stream wiring! Any events sent before this init may be lost!
|
||||
private val homeRoot = home.root
|
||||
private val cashRoot = cash.root
|
||||
private val transactionRoot = transaction.root
|
||||
private val newTransactionRoot = newTransaction.root
|
||||
|
||||
private fun getView(selection: SelectedView) = when (selection) {
|
||||
SelectedView.Home -> homeRoot
|
||||
SelectedView.Cash -> cashRoot
|
||||
SelectedView.Transaction -> transactionRoot
|
||||
SelectedView.NewTransaction -> newTransactionRoot
|
||||
}
|
||||
val selectedView: ObjectProperty<SelectedView> by objectProperty(TopLevelModel::selectedView)
|
||||
|
||||
@ -46,4 +49,4 @@ class TopLevel : View() {
|
||||
|
||||
root.children.add(0, header.root)
|
||||
}
|
||||
}
|
||||
}
|
@ -79,7 +79,7 @@ class TransactionViewer: View() {
|
||||
by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
|
||||
private val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>>
|
||||
by observableValue(ReportingCurrencyModel::reportingExchange)
|
||||
private val myIdentity: ObservableValue<Party> by observableValue(IdentityModel::myIdentity)
|
||||
private val myIdentity: ObservableValue<Party?> by observableValue(IdentityModel::myIdentity)
|
||||
|
||||
/**
|
||||
* This is what holds data for a single transaction node. Note how a lot of these are nullable as we often simply don't
|
||||
@ -363,7 +363,7 @@ class TransactionViewer: View() {
|
||||
* We calculate the total value by subtracting relevant input states and adding relevant output states, as long as they're cash
|
||||
*/
|
||||
private fun calculateTotalEquiv(
|
||||
identity: Party,
|
||||
identity: Party?,
|
||||
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
|
||||
inputs: List<StateAndRef<ContractState>>?,
|
||||
outputs: List<TransactionState<ContractState>>): AmountDiff<Currency>? {
|
||||
@ -372,7 +372,7 @@ private fun calculateTotalEquiv(
|
||||
}
|
||||
var sum = 0L
|
||||
val (reportingCurrency, exchange) = reportingCurrencyExchange
|
||||
val publicKey = identity.owningKey
|
||||
val publicKey = identity?.owningKey
|
||||
inputs.forEach {
|
||||
val contractState = it.state.data
|
||||
if (contractState is Cash.State && publicKey == contractState.owner) {
|
||||
|
@ -1,30 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.TitledPane?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.TilePane?>
|
||||
|
||||
<TilePane prefHeight="425.0" prefWidth="425.0" tileAlignment="TOP_LEFT" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<TitledPane id="tile_cash" fx:id="ourCashPane" alignment="CENTER" collapsible="false" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our cash">
|
||||
<content>
|
||||
<Label fx:id="ourCashLabel" text="USD 186.7m" textAlignment="CENTER" wrapText="true" />
|
||||
</content>
|
||||
<Label fx:id="ourCashLabel" text="USD 186.7m" textAlignment="CENTER" wrapText="true"/>
|
||||
</TitledPane>
|
||||
<TitledPane id="tile_debtors" fx:id="ourDebtorsPane" alignment="CENTER" collapsible="false" layoutX="232.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our debtors">
|
||||
<content>
|
||||
<Label text="USD 71.3m" textAlignment="CENTER" wrapText="true" />
|
||||
</content>
|
||||
<Label text="USD 71.3m" textAlignment="CENTER" wrapText="true"/>
|
||||
</TitledPane>
|
||||
<TitledPane id="tile_creditors" fx:id="ourCreditorsPane" alignment="CENTER" collapsible="false" layoutX="312.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our creditors">
|
||||
<content>
|
||||
<Label text="USD (29.4m)" textAlignment="CENTER" wrapText="true" />
|
||||
</content>
|
||||
<Label text="USD (29.4m)" textAlignment="CENTER" wrapText="true"/>
|
||||
</TitledPane>
|
||||
<TitledPane id="tile_tx" fx:id="ourTransactionsPane" alignment="CENTER" collapsible="false" layoutX="392.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our transactions">
|
||||
<content>
|
||||
<Label fx:id="ourTransactionsLabel" text="In flight: 1,315" textAlignment="CENTER" wrapText="true" />
|
||||
</content>
|
||||
<Label fx:id="ourTransactionsLabel" text="In flight: 1,315" textAlignment="CENTER" wrapText="true"/>
|
||||
</TitledPane>
|
||||
</children>
|
||||
<TitledPane id="tile_new_tx" fx:id="newTransaction" alignment="CENTER" collapsible="false" layoutX="472.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="New Transaction">
|
||||
</TitledPane>
|
||||
</TilePane>
|
||||
|
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<GridPane hgap="10" vgap="10" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<!-- Row 1 -->
|
||||
<Label text="Transaction Type : " GridPane.halignment="RIGHT"/>
|
||||
<ChoiceBox fx:id="transactionTypeCB" maxWidth="Infinity" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.hgrow="ALWAYS"/>
|
||||
|
||||
<!-- Row 2 -->
|
||||
<Label fx:id="partyALabel" GridPane.halignment="RIGHT" GridPane.rowIndex="1"/>
|
||||
<TextField fx:id="partyATextField" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="1"/>
|
||||
|
||||
<!-- Row 3 -->
|
||||
<Label fx:id="partyBLabel" GridPane.halignment="RIGHT" GridPane.rowIndex="2"/>
|
||||
<ChoiceBox fx:id="partyBChoiceBox" maxWidth="Infinity" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.fillWidth="true" GridPane.hgrow="ALWAYS" GridPane.rowIndex="2"/>
|
||||
|
||||
<!-- Row 4 -->
|
||||
<Label fx:id="amountLabel" text="Amount : " GridPane.halignment="RIGHT" GridPane.rowIndex="3"/>
|
||||
<ChoiceBox fx:id="currency" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
|
||||
<TextField fx:id="amount" maxWidth="Infinity" GridPane.columnIndex="2" GridPane.hgrow="ALWAYS" GridPane.rowIndex="3"/>
|
||||
|
||||
<!-- Row 5 -->
|
||||
<Label fx:id="issueRefLabel" text="Issue Reference : " GridPane.halignment="RIGHT" GridPane.rowIndex="4"/>
|
||||
<TextField fx:id="issueRefTextField" GridPane.columnIndex="1" GridPane.rowIndex="4" GridPane.columnSpan="2"/>
|
||||
|
||||
<!-- Row 6 -->
|
||||
<Button fx:id="executeButton" text="Execute" GridPane.columnIndex="2" GridPane.halignment="RIGHT" GridPane.rowIndex="5"/>
|
||||
|
||||
<Pane fx:id="mainPane" prefHeight="0.0" prefWidth="0.0"/>
|
||||
|
||||
<padding>
|
||||
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
|
||||
</padding>
|
||||
</GridPane>
|
@ -4,7 +4,5 @@
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<VBox fx:id="topLevel" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<BorderPane fx:id="selectionBorderPane" />
|
||||
</children>
|
||||
<BorderPane fx:id="selectionBorderPane"/>
|
||||
</VBox>
|
||||
|
@ -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;
|
||||
}
|
@ -134,6 +134,9 @@ dependencies {
|
||||
compile "org.hibernate:hibernate-core:5.2.2.Final"
|
||||
compile "org.hibernate:hibernate-java8:5.2.2.Final"
|
||||
|
||||
// Capsule is a library for building independently executable fat JARs.
|
||||
compile 'co.paralleluniverse:capsule:1.0.3'
|
||||
|
||||
// Integration test helpers
|
||||
integrationTestCompile 'junit:junit:4.12'
|
||||
|
||||
|
@ -11,14 +11,8 @@ import org.junit.Test
|
||||
|
||||
class DriverTests {
|
||||
companion object {
|
||||
fun nodeMustBeUp(networkMapCache: NetworkMapCache, nodeInfo: NodeInfo, nodeName: String) {
|
||||
fun nodeMustBeUp(nodeInfo: NodeInfo, nodeName: String) {
|
||||
val hostAndPort = ArtemisMessagingComponent.toHostAndPort(nodeInfo.address)
|
||||
// Check that the node is registered in the network map
|
||||
poll("network map cache for $nodeName") {
|
||||
networkMapCache.get().firstOrNull {
|
||||
it.legalIdentity.name == nodeName
|
||||
}
|
||||
}
|
||||
// Check that the port is bound
|
||||
addressMustBeBound(hostAndPort)
|
||||
}
|
||||
@ -36,31 +30,31 @@ class DriverTests {
|
||||
val notary = startNode("TestNotary", setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
val regulator = startNode("Regulator", setOf(ServiceInfo(RegulatorService.type)))
|
||||
|
||||
nodeMustBeUp(networkMapCache, notary.get(), "TestNotary")
|
||||
nodeMustBeUp(networkMapCache, regulator.get(), "Regulator")
|
||||
nodeMustBeUp(notary.get().nodeInfo, "TestNotary")
|
||||
nodeMustBeUp(regulator.get().nodeInfo, "Regulator")
|
||||
Pair(notary.get(), regulator.get())
|
||||
}
|
||||
nodeMustBeDown(notary)
|
||||
nodeMustBeDown(regulator)
|
||||
nodeMustBeDown(notary.nodeInfo)
|
||||
nodeMustBeDown(regulator.nodeInfo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun startingNodeWithNoServicesWorks() {
|
||||
val noService = driver {
|
||||
val noService = startNode("NoService")
|
||||
nodeMustBeUp(networkMapCache, noService.get(), "NoService")
|
||||
nodeMustBeUp(noService.get().nodeInfo, "NoService")
|
||||
noService.get()
|
||||
}
|
||||
nodeMustBeDown(noService)
|
||||
nodeMustBeDown(noService.nodeInfo)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun randomFreePortAllocationWorks() {
|
||||
val nodeInfo = driver(portAllocation = PortAllocation.RandomFree()) {
|
||||
val nodeInfo = startNode("NoService")
|
||||
nodeMustBeUp(networkMapCache, nodeInfo.get(), "NoService")
|
||||
nodeMustBeUp(nodeInfo.get().nodeInfo, "NoService")
|
||||
nodeInfo.get()
|
||||
}
|
||||
nodeMustBeDown(nodeInfo)
|
||||
nodeMustBeDown(nodeInfo.nodeInfo)
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import com.r3corda.core.contracts.*
|
||||
import com.r3corda.node.api.StatesQuery
|
||||
import com.r3corda.core.crypto.DigitalSignature
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.serialization.SerializedBytes
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.transactions.WireTransaction
|
||||
@ -41,6 +42,16 @@ interface APIServer {
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
fun status(): Response
|
||||
|
||||
/**
|
||||
* Report this node's configuration and identities.
|
||||
* Currently tunnels the NodeInfo as an encoding of the Kryo serialised form.
|
||||
* TODO this functionality should be available via the RPC
|
||||
*/
|
||||
@GET
|
||||
@Path("info")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun info(): NodeInfo
|
||||
|
||||
/**
|
||||
* Query your "local" states (containing only outputs involving you) and return the hashes & indexes associated with them
|
||||
* to probably be later inflated by fetchLedgerTransactions() or fetchStates() although because immutable you can cache them
|
||||
|
@ -1,5 +1,7 @@
|
||||
package com.r3corda.node.driver
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.r3corda.core.ThreadBox
|
||||
import com.r3corda.core.crypto.Party
|
||||
@ -7,6 +9,7 @@ import com.r3corda.core.crypto.generateKeyPair
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.services.NetworkMapCache
|
||||
import com.r3corda.core.node.services.ServiceInfo
|
||||
import com.r3corda.node.serialization.NodeClock
|
||||
import com.r3corda.node.services.config.ConfigHelper
|
||||
import com.r3corda.node.services.config.FullNodeConfiguration
|
||||
import com.r3corda.node.services.messaging.ArtemisMessagingComponent
|
||||
@ -14,22 +17,24 @@ import com.r3corda.node.services.messaging.ArtemisMessagingServer
|
||||
import com.r3corda.node.services.messaging.NodeMessagingClient
|
||||
import com.r3corda.node.services.network.InMemoryNetworkMapCache
|
||||
import com.r3corda.node.services.network.NetworkMapService
|
||||
import com.r3corda.node.utilities.AffinityExecutor
|
||||
import com.r3corda.node.utilities.JsonSupport
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
import java.net.*
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.Clock
|
||||
import java.util.*
|
||||
import java.util.concurrent.*
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
/**
|
||||
* This file defines a small "Driver" DSL for starting up nodes.
|
||||
* This file defines a small "Driver" DSL for starting up nodes that is only intended for development, demos and tests.
|
||||
*
|
||||
* The process the driver is run in behaves as an Artemis client and starts up other processes. Namely it first
|
||||
* bootstraps a network map service to allow the specified nodes to connect to, then starts up the actual nodes.
|
||||
@ -54,38 +59,18 @@ interface DriverDSLExposedInterface {
|
||||
* @param advertisedServices The set of services to be advertised by the node. Defaults to empty set.
|
||||
* @return The [NodeInfo] of the started up node retrieved from the network map service.
|
||||
*/
|
||||
fun startNode(providedName: String? = null, advertisedServices: Set<ServiceInfo> = setOf()): Future<NodeInfo>
|
||||
fun startNode(providedName: String? = null, advertisedServices: Set<ServiceInfo> = setOf()): Future<NodeInfoAndConfig>
|
||||
|
||||
/**
|
||||
* Starts an [NodeMessagingClient].
|
||||
*
|
||||
* @param providedName name of the client, which will be used for creating its directory.
|
||||
* @param serverAddress the artemis server to connect to, for example a [Node].
|
||||
*/
|
||||
fun startClient(providedName: String, serverAddress: HostAndPort): Future<NodeMessagingClient>
|
||||
|
||||
/**
|
||||
* Starts a local [ArtemisMessagingServer] of which there may only be one.
|
||||
*/
|
||||
fun startLocalServer(): Future<ArtemisMessagingServer>
|
||||
fun waitForAllNodesToFinish()
|
||||
val networkMapCache: NetworkMapCache
|
||||
}
|
||||
|
||||
fun DriverDSLExposedInterface.startClient(localServer: ArtemisMessagingServer) =
|
||||
startClient("driver-local-server-client", localServer.myHostPort)
|
||||
|
||||
fun DriverDSLExposedInterface.startClient(remoteNodeInfo: NodeInfo, providedName: String? = null) =
|
||||
startClient(
|
||||
providedName = providedName ?: "${remoteNodeInfo.legalIdentity.name}-client",
|
||||
serverAddress = ArtemisMessagingComponent.toHostAndPort(remoteNodeInfo.address)
|
||||
)
|
||||
|
||||
interface DriverDSLInternalInterface : DriverDSLExposedInterface {
|
||||
fun start()
|
||||
fun shutdown()
|
||||
}
|
||||
|
||||
data class NodeInfoAndConfig(val nodeInfo: NodeInfo, val config: Config)
|
||||
|
||||
sealed class PortAllocation {
|
||||
abstract fun nextPort(): Int
|
||||
fun nextHostAndPort(): HostAndPort = HostAndPort.fromParts("localhost", nextPort())
|
||||
@ -122,6 +107,7 @@ sealed class PortAllocation {
|
||||
* and may be specified in [DriverDSL.startNode].
|
||||
* @param portAllocation The port allocation strategy to use for the messaging and the web server addresses. Defaults to incremental.
|
||||
* @param debugPortAllocation The port allocation strategy to use for jvm debugging. Defaults to incremental.
|
||||
* @param useTestClock If true the test clock will be used in Node.
|
||||
* @param isDebug Indicates whether the spawned nodes should start in jdwt debug mode.
|
||||
* @param dsl The dsl itself.
|
||||
* @return The value returned in the [dsl] closure.
|
||||
@ -130,6 +116,7 @@ fun <A> driver(
|
||||
baseDirectory: String = "build/${getTimestampAsDirectoryName()}",
|
||||
portAllocation: PortAllocation = PortAllocation.Incremental(10000),
|
||||
debugPortAllocation: PortAllocation = PortAllocation.Incremental(5005),
|
||||
useTestClock: Boolean = false,
|
||||
isDebug: Boolean = false,
|
||||
dsl: DriverDSLExposedInterface.() -> A
|
||||
) = genericDriver(
|
||||
@ -137,6 +124,7 @@ fun <A> driver(
|
||||
portAllocation = portAllocation,
|
||||
debugPortAllocation = debugPortAllocation,
|
||||
baseDirectory = baseDirectory,
|
||||
useTestClock = useTestClock,
|
||||
isDebug = isDebug
|
||||
),
|
||||
coerce = { it },
|
||||
@ -216,17 +204,15 @@ fun <A> poll(pollName: String, pollIntervalMs: Long = 500, warnCount: Int = 120,
|
||||
return result
|
||||
}
|
||||
|
||||
class DriverDSL(
|
||||
open class DriverDSL(
|
||||
val portAllocation: PortAllocation,
|
||||
val debugPortAllocation: PortAllocation,
|
||||
val baseDirectory: String,
|
||||
val useTestClock: Boolean,
|
||||
val isDebug: Boolean
|
||||
) : DriverDSLInternalInterface {
|
||||
override val networkMapCache = InMemoryNetworkMapCache()
|
||||
private val networkMapName = "NetworkMapService"
|
||||
private val networkMapAddress = portAllocation.nextHostAndPort()
|
||||
private var networkMapNodeInfo: NodeInfo? = null
|
||||
private val identity = generateKeyPair()
|
||||
|
||||
class State {
|
||||
val registeredProcesses = LinkedList<Process>()
|
||||
@ -284,7 +270,26 @@ class DriverDSL(
|
||||
addressMustNotBeBound(networkMapAddress)
|
||||
}
|
||||
|
||||
override fun startNode(providedName: String?, advertisedServices: Set<ServiceInfo>): Future<NodeInfo> {
|
||||
private fun queryNodeInfo(webAddress: HostAndPort): NodeInfo? {
|
||||
val url = URL("http://${webAddress.toString()}/api/info")
|
||||
try {
|
||||
val conn = url.openConnection() as HttpURLConnection
|
||||
conn.requestMethod = "GET"
|
||||
if (conn.responseCode != 200) {
|
||||
return null
|
||||
}
|
||||
// For now the NodeInfo is tunneled in its Kryo format over the Node's Web interface.
|
||||
val om = ObjectMapper()
|
||||
val module = SimpleModule("NodeInfo")
|
||||
module.addDeserializer(NodeInfo::class.java, JsonSupport.NodeInfoDeserializer)
|
||||
om.registerModule(module)
|
||||
return om.readValue(conn.inputStream, NodeInfo::class.java)
|
||||
} catch(e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
override fun startNode(providedName: String?, advertisedServices: Set<ServiceInfo>): Future<NodeInfoAndConfig> {
|
||||
val messagingAddress = portAllocation.nextHostAndPort()
|
||||
val apiAddress = portAllocation.nextHostAndPort()
|
||||
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
|
||||
@ -301,94 +306,19 @@ class DriverDSL(
|
||||
"artemisAddress" to messagingAddress.toString(),
|
||||
"webAddress" to apiAddress.toString(),
|
||||
"extraAdvertisedServiceIds" to advertisedServices.joinToString(","),
|
||||
"networkMapAddress" to networkMapAddress.toString()
|
||||
"networkMapAddress" to networkMapAddress.toString(),
|
||||
"useTestClock" to useTestClock
|
||||
)
|
||||
)
|
||||
|
||||
return Executors.newSingleThreadExecutor().submit(Callable<NodeInfo> {
|
||||
return Executors.newSingleThreadExecutor().submit(Callable<NodeInfoAndConfig> {
|
||||
registerProcess(DriverDSL.startNode(config, quasarJarPath, debugPort))
|
||||
poll("network map cache for $name") {
|
||||
networkMapCache.partyNodes.forEach {
|
||||
if (it.legalIdentity.name == name) {
|
||||
return@poll it
|
||||
}
|
||||
}
|
||||
null
|
||||
}
|
||||
NodeInfoAndConfig(queryNodeInfo(apiAddress)!!, config)
|
||||
})
|
||||
}
|
||||
|
||||
override fun startClient(
|
||||
providedName: String,
|
||||
serverAddress: HostAndPort
|
||||
): Future<NodeMessagingClient> {
|
||||
|
||||
val nodeConfiguration = FullNodeConfiguration(
|
||||
ConfigHelper.loadConfig(
|
||||
baseDirectoryPath = Paths.get(baseDirectory, providedName),
|
||||
allowMissingConfig = true,
|
||||
configOverrides = mapOf(
|
||||
"myLegalName" to providedName
|
||||
)
|
||||
)
|
||||
)
|
||||
val client = NodeMessagingClient(nodeConfiguration,
|
||||
serverHostPort = serverAddress,
|
||||
myIdentity = identity.public,
|
||||
executor = AffinityExecutor.ServiceAffinityExecutor(providedName, 1),
|
||||
persistentInbox = false // Do not create a permanent queue for our transient UI identity
|
||||
)
|
||||
|
||||
return Executors.newSingleThreadExecutor().submit(Callable<NodeMessagingClient> {
|
||||
client.configureWithDevSSLCertificate()
|
||||
client.start(null)
|
||||
thread { client.run() }
|
||||
state.locked {
|
||||
clients.add(client)
|
||||
}
|
||||
client
|
||||
})
|
||||
}
|
||||
|
||||
override fun startLocalServer(): Future<ArtemisMessagingServer> {
|
||||
val name = "driver-local-server"
|
||||
val config = FullNodeConfiguration(
|
||||
ConfigHelper.loadConfig(
|
||||
baseDirectoryPath = Paths.get(baseDirectory, name),
|
||||
allowMissingConfig = true,
|
||||
configOverrides = mapOf(
|
||||
"myLegalName" to name
|
||||
)
|
||||
)
|
||||
)
|
||||
val server = ArtemisMessagingServer(config,
|
||||
portAllocation.nextHostAndPort(),
|
||||
networkMapCache
|
||||
)
|
||||
return Executors.newSingleThreadExecutor().submit(Callable<ArtemisMessagingServer> {
|
||||
server.configureWithDevSSLCertificate()
|
||||
server.start()
|
||||
state.locked {
|
||||
localServer = server
|
||||
}
|
||||
server
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
override fun start() {
|
||||
startNetworkMapService()
|
||||
val networkMapClient = startClient("driver-$networkMapName-client", networkMapAddress).get()
|
||||
val networkMapAddr = NodeMessagingClient.makeNetworkMapAddress(networkMapAddress)
|
||||
networkMapCache.addMapService(networkMapClient, networkMapAddr, true)
|
||||
networkMapNodeInfo = poll("network map cache for $networkMapName") {
|
||||
networkMapCache.partyNodes.forEach {
|
||||
if (it.legalIdentity.name == networkMapName) {
|
||||
return@poll it
|
||||
}
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun startNetworkMapService() {
|
||||
@ -396,7 +326,6 @@ class DriverDSL(
|
||||
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
|
||||
|
||||
val nodeDirectory = "$baseDirectory/$networkMapName"
|
||||
|
||||
val config = ConfigHelper.loadConfig(
|
||||
baseDirectoryPath = Paths.get(nodeDirectory),
|
||||
allowMissingConfig = true,
|
||||
@ -405,7 +334,8 @@ class DriverDSL(
|
||||
"basedir" to Paths.get(nodeDirectory).normalize().toString(),
|
||||
"artemisAddress" to networkMapAddress.toString(),
|
||||
"webAddress" to apiAddress.toString(),
|
||||
"extraAdvertisedServiceIds" to ""
|
||||
"extraAdvertisedServiceIds" to "",
|
||||
"useTestClock" to useTestClock
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -24,6 +24,8 @@ class APIServerImpl(val node: AbstractNode) : APIServer {
|
||||
}
|
||||
}
|
||||
|
||||
override fun info() = node.services.myInfo
|
||||
|
||||
override fun queryStates(query: StatesQuery): List<StateRef> {
|
||||
// We're going to hard code two options here for now and assume that all LinearStates are deals
|
||||
// Would like to maybe move to a model where we take something like a JEXL string, although don't want to develop
|
||||
|
@ -153,7 +153,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
lateinit var net: MessagingServiceInternal
|
||||
lateinit var netMapCache: NetworkMapCache
|
||||
lateinit var api: APIServer
|
||||
lateinit var scheduler: SchedulerService
|
||||
lateinit var scheduler: NodeSchedulerService
|
||||
lateinit var protocolLogicFactory: ProtocolLogicRefFactory
|
||||
lateinit var schemas: SchemaService
|
||||
val customServices: ArrayList<Any> = ArrayList()
|
||||
@ -209,9 +209,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
// the identity key. But the infrastructure to make that easy isn't here yet.
|
||||
keyManagement = makeKeyManagementService()
|
||||
api = APIServerImpl(this@AbstractNode)
|
||||
scheduler = NodeSchedulerService(database, services)
|
||||
|
||||
protocolLogicFactory = initialiseProtocolLogicFactory()
|
||||
scheduler = NodeSchedulerService(database, services, protocolLogicFactory)
|
||||
|
||||
val tokenizableServices = mutableListOf(storage, net, vault, keyManagement, identity, platformClock, scheduler)
|
||||
|
||||
@ -257,6 +256,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
runOnStop += Runnable { net.stop() }
|
||||
_networkMapRegistrationFuture.setFuture(registerWithNetworkMap())
|
||||
smm.start()
|
||||
scheduler.start()
|
||||
}
|
||||
started = true
|
||||
return this
|
||||
@ -434,15 +434,11 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
|
||||
protected abstract fun makeMessagingService(): MessagingServiceInternal
|
||||
|
||||
protected abstract fun startMessagingService(cordaRPCOps: CordaRPCOps?)
|
||||
|
||||
protected open fun initialiseCheckpointService(dir: Path): CheckpointStorage {
|
||||
return DBCheckpointStorage()
|
||||
}
|
||||
protected abstract fun startMessagingService(cordaRPCOps: CordaRPCOps)
|
||||
|
||||
protected open fun initialiseStorageService(dir: Path): Pair<TxWritableStorageService, CheckpointStorage> {
|
||||
val attachments = makeAttachmentStorage(dir)
|
||||
val checkpointStorage = initialiseCheckpointService(dir)
|
||||
val checkpointStorage = DBCheckpointStorage()
|
||||
val transactionStorage = DBTransactionStorage()
|
||||
_servicesThatAcceptUploads += attachments
|
||||
// Populate the partyKeys set.
|
||||
@ -451,7 +447,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
// Ensure all required keys exist.
|
||||
obtainKeyPair(configuration.basedir, service.type.id + "-private-key", service.type.id + "-public", service.type.id)
|
||||
}
|
||||
val stateMachineTransactionMappingStorage = InMemoryStateMachineRecordedTransactionMappingStorage()
|
||||
val stateMachineTransactionMappingStorage = DBTransactionMappingStorage()
|
||||
return Pair(
|
||||
constructStorageService(attachments, transactionStorage, stateMachineTransactionMappingStorage),
|
||||
checkpointStorage
|
||||
|
@ -4,6 +4,7 @@ import com.codahale.metrics.JmxReporter
|
||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
||||
import com.r3corda.core.node.ServiceHub
|
||||
import com.r3corda.core.node.services.ServiceInfo
|
||||
import com.r3corda.core.then
|
||||
import com.r3corda.core.utilities.loggerFor
|
||||
import com.r3corda.node.serialization.NodeClock
|
||||
import com.r3corda.node.services.api.MessagingServiceInternal
|
||||
@ -119,11 +120,10 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
||||
}
|
||||
val legalIdentity = obtainLegalIdentity()
|
||||
val myIdentityOrNullIfNetworkMapService = if (networkMapService != null) legalIdentity.owningKey else null
|
||||
return NodeMessagingClient(configuration, serverAddr, myIdentityOrNullIfNetworkMapService, serverThread,
|
||||
persistenceTx = { body: () -> Unit -> databaseTransaction(database) { body() } })
|
||||
return NodeMessagingClient(configuration, serverAddr, myIdentityOrNullIfNetworkMapService, serverThread, database)
|
||||
}
|
||||
|
||||
override fun startMessagingService(cordaRPCOps: CordaRPCOps?) {
|
||||
override fun startMessagingService(cordaRPCOps: CordaRPCOps) {
|
||||
// Start up the embedded MQ server
|
||||
messageBroker?.apply {
|
||||
runOnStop += Runnable { messageBroker?.stop() }
|
||||
@ -268,23 +268,25 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
||||
override fun start(): Node {
|
||||
alreadyRunningNodeCheck()
|
||||
super.start()
|
||||
webServer = initWebServer()
|
||||
// Begin exporting our own metrics via JMX.
|
||||
JmxReporter.
|
||||
forRegistry(services.monitoringService.metrics).
|
||||
inDomain("com.r3cev.corda").
|
||||
createsObjectNamesWith { type, domain, name ->
|
||||
// Make the JMX hierarchy a bit better organised.
|
||||
val category = name.substringBefore('.')
|
||||
val subName = name.substringAfter('.', "")
|
||||
if (subName == "")
|
||||
ObjectName("$domain:name=$category")
|
||||
else
|
||||
ObjectName("$domain:type=$category,name=$subName")
|
||||
}.
|
||||
build().
|
||||
start()
|
||||
|
||||
// Only start the service API requests once the network map registration is complete
|
||||
networkMapRegistrationFuture.then {
|
||||
webServer = initWebServer()
|
||||
// Begin exporting our own metrics via JMX.
|
||||
JmxReporter.
|
||||
forRegistry(services.monitoringService.metrics).
|
||||
inDomain("com.r3cev.corda").
|
||||
createsObjectNamesWith { type, domain, name ->
|
||||
// Make the JMX hierarchy a bit better organised.
|
||||
val category = name.substringBefore('.')
|
||||
val subName = name.substringAfter('.', "")
|
||||
if (subName == "")
|
||||
ObjectName("$domain:name=$category")
|
||||
else
|
||||
ObjectName("$domain:type=$category,name=$subName")
|
||||
}.
|
||||
build().
|
||||
start()
|
||||
}
|
||||
shutdownThread = thread(start = false) {
|
||||
stop()
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ import com.r3corda.core.contracts.InsufficientBalanceException
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.toStringShort
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.ServiceHub
|
||||
import com.r3corda.core.node.services.NetworkMapCache
|
||||
import com.r3corda.core.node.services.StateMachineTransactionMapping
|
||||
import com.r3corda.core.node.services.Vault
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
@ -33,6 +35,10 @@ class ServerRPCOps(
|
||||
) : CordaRPCOps {
|
||||
override val protocolVersion: Int = 0
|
||||
|
||||
override fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>> {
|
||||
return services.networkMapCache.track()
|
||||
}
|
||||
|
||||
override fun vaultAndUpdates(): Pair<List<StateAndRef<ContractState>>, Observable<Vault.Update>> {
|
||||
return databaseTransaction(database) {
|
||||
val (vault, updates) = services.vaultService.track()
|
||||
@ -146,5 +152,4 @@ class ServerRPCOps(
|
||||
|
||||
class InputStateRefResolveFailed(stateRefs: List<StateRef>) :
|
||||
Exception("Failed to resolve input StateRefs $stateRefs")
|
||||
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package com.r3corda.node.services.clientapi
|
||||
|
||||
import com.r3corda.core.node.CordaPluginRegistry
|
||||
import com.r3corda.core.serialization.SingletonSerializeAsToken
|
||||
import com.r3corda.node.services.api.ServiceHubInternal
|
||||
import com.r3corda.protocols.TwoPartyDealProtocol.Fixer
|
||||
import com.r3corda.protocols.TwoPartyDealProtocol.Floater
|
||||
|
||||
/**
|
||||
* This is a temporary handler required for establishing random sessionIDs for the [Fixer] and [Floater] as part of
|
||||
* running scheduled fixings for the [InterestRateSwap] contract.
|
||||
*
|
||||
* TODO: This will be replaced with the symmetric session work
|
||||
*/
|
||||
object FixingSessionInitiation {
|
||||
class Plugin: CordaPluginRegistry() {
|
||||
override val servicePlugins: List<Class<*>> = listOf(Service::class.java)
|
||||
}
|
||||
|
||||
class Service(services: ServiceHubInternal) : SingletonSerializeAsToken() {
|
||||
init {
|
||||
services.registerProtocolInitiator(Floater::class) { Fixer(it) }
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package com.r3corda.node.services.config
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.r3corda.core.crypto.X509Utilities
|
||||
import com.r3corda.core.exists
|
||||
import com.r3corda.core.utilities.loggerFor
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
@ -89,14 +90,24 @@ fun Config.getProperties(path: String): Properties {
|
||||
*/
|
||||
fun NodeSSLConfiguration.configureWithDevSSLCertificate() {
|
||||
Files.createDirectories(certificatesPath)
|
||||
if (!Files.exists(trustStorePath)) {
|
||||
if (!trustStorePath.exists()) {
|
||||
Files.copy(javaClass.classLoader.getResourceAsStream("com/r3corda/node/internal/certificates/cordatruststore.jks"),
|
||||
trustStorePath)
|
||||
}
|
||||
if (!Files.exists(keyStorePath)) {
|
||||
if (!keyStorePath.exists()) {
|
||||
val caKeyStore = X509Utilities.loadKeyStore(
|
||||
javaClass.classLoader.getResourceAsStream("com/r3corda/node/internal/certificates/cordadevcakeys.jks"),
|
||||
"cordacadevpass")
|
||||
X509Utilities.createKeystoreForSSL(keyStorePath, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Move this to CoreTestUtils.kt once we can pry this from the explorer
|
||||
fun configureTestSSL(): NodeSSLConfiguration = object : NodeSSLConfiguration {
|
||||
override val certificatesPath = Files.createTempDirectory("certs")
|
||||
override val keyStorePassword: String get() = "cordacadevpass"
|
||||
override val trustStorePassword: String get() = "trustpass"
|
||||
init {
|
||||
configureWithDevSSLCertificate()
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,13 @@ import com.r3corda.core.div
|
||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
||||
import com.r3corda.core.node.services.ServiceInfo
|
||||
import com.r3corda.node.internal.Node
|
||||
import com.r3corda.node.serialization.NodeClock
|
||||
import com.r3corda.node.services.messaging.NodeMessagingClient
|
||||
import com.r3corda.node.services.network.NetworkMapService
|
||||
import com.r3corda.node.utilities.TestClock
|
||||
import com.typesafe.config.Config
|
||||
import java.nio.file.Path
|
||||
import java.time.Clock
|
||||
import java.util.*
|
||||
|
||||
interface NodeSSLConfiguration {
|
||||
@ -46,8 +49,12 @@ class FullNodeConfiguration(config: Config) : NodeConfiguration {
|
||||
val webAddress: HostAndPort by config
|
||||
val messagingServerAddress: HostAndPort? by config.getOrElse { null }
|
||||
val extraAdvertisedServiceIds: String by config
|
||||
val useTestClock: Boolean by config.getOrElse { false }
|
||||
|
||||
fun createNode(): Node {
|
||||
// This is a sanity feature do not remove.
|
||||
require(!useTestClock || devMode) { "Cannot use test clock outside of dev mode" }
|
||||
|
||||
val advertisedServices = mutableSetOf<ServiceInfo>()
|
||||
if (!extraAdvertisedServiceIds.isNullOrEmpty()) {
|
||||
for (serviceId in extraAdvertisedServiceIds.split(",")) {
|
||||
@ -56,7 +63,7 @@ class FullNodeConfiguration(config: Config) : NodeConfiguration {
|
||||
}
|
||||
if (networkMapAddress == null) advertisedServices.add(ServiceInfo(NetworkMapService.type))
|
||||
val networkMapMessageAddress: SingleMessageRecipient? = if (networkMapAddress == null) null else NodeMessagingClient.makeNetworkMapAddress(networkMapAddress!!)
|
||||
return Node(this, networkMapMessageAddress, advertisedServices)
|
||||
return Node(this, networkMapMessageAddress, advertisedServices, if(useTestClock == true) TestClock() else NodeClock())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,21 +1,26 @@
|
||||
package com.r3corda.node.services.events
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import com.r3corda.core.ThreadBox
|
||||
import com.r3corda.core.contracts.SchedulableState
|
||||
import com.r3corda.core.contracts.ScheduledActivity
|
||||
import com.r3corda.core.contracts.ScheduledStateRef
|
||||
import com.r3corda.core.contracts.StateRef
|
||||
import com.r3corda.core.node.services.SchedulerService
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
import com.r3corda.core.protocols.ProtocolLogicRefFactory
|
||||
import com.r3corda.core.serialization.SingletonSerializeAsToken
|
||||
import com.r3corda.core.utilities.ProgressTracker
|
||||
import com.r3corda.core.utilities.loggerFor
|
||||
import com.r3corda.core.utilities.trace
|
||||
import com.r3corda.node.services.api.ServiceHubInternal
|
||||
import com.r3corda.node.utilities.awaitWithDeadline
|
||||
import com.r3corda.node.utilities.databaseTransaction
|
||||
import com.r3corda.node.utilities.*
|
||||
import kotlinx.support.jdk8.collections.compute
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.statements.InsertStatement
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
@ -27,8 +32,6 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
* This will observe transactions as they are stored and schedule and unschedule activities based on the States consumed
|
||||
* or produced.
|
||||
*
|
||||
* TODO: Needs extensive support from persistence and protocol frameworks to be truly reliable and atomic.
|
||||
*
|
||||
* Currently does not provide any system state other than the ContractState so the expectation is that a transaction
|
||||
* is the outcome of the activity in order to schedule another activity. Once we have implemented more persistence
|
||||
* in the nodes, maybe we can consider multiple activities and whether the activities have been completed or not,
|
||||
@ -42,30 +45,56 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
@ThreadSafe
|
||||
class NodeSchedulerService(private val database: Database,
|
||||
private val services: ServiceHubInternal,
|
||||
private val protocolLogicRefFactory: ProtocolLogicRefFactory = ProtocolLogicRefFactory(),
|
||||
private val protocolLogicRefFactory: ProtocolLogicRefFactory,
|
||||
private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor())
|
||||
: SchedulerService, SingletonSerializeAsToken() {
|
||||
|
||||
private val log = loggerFor<NodeSchedulerService>()
|
||||
|
||||
private object Table : JDBCHashedTable("${NODE_DATABASE_PREFIX}scheduled_states") {
|
||||
val output = stateRef("transaction_id", "output_index")
|
||||
val scheduledAt = instant("scheduled_at")
|
||||
}
|
||||
|
||||
// Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're
|
||||
// inside mutex.locked {} code block. So we can't forget to take the lock unless we accidentally leak a reference
|
||||
// to somewhere.
|
||||
private class InnerState {
|
||||
// TODO: This has no persistence, and we don't consider initialising from non-empty map if we add persistence.
|
||||
// If we were to rebuild the vault at start up by replaying transactions and re-calculating, then
|
||||
// persistence here would be unnecessary.
|
||||
var scheduledStates = HashMap<StateRef, ScheduledStateRef>()
|
||||
var scheduledStates = object : AbstractJDBCHashMap<StateRef, ScheduledStateRef, Table>(Table, loadOnInit = true) {
|
||||
override fun keyFromRow(row: ResultRow): StateRef = StateRef(row[table.output.txId], row[table.output.index])
|
||||
|
||||
override fun valueFromRow(row: ResultRow): ScheduledStateRef {
|
||||
return ScheduledStateRef(StateRef(row[table.output.txId], row[table.output.index]), row[table.scheduledAt])
|
||||
}
|
||||
|
||||
override fun addKeyToInsert(insert: InsertStatement, entry: Map.Entry<StateRef, ScheduledStateRef>, finalizables: MutableList<() -> Unit>) {
|
||||
insert[table.output.txId] = entry.key.txhash
|
||||
insert[table.output.index] = entry.key.index
|
||||
}
|
||||
|
||||
override fun addValueToInsert(insert: InsertStatement, entry: Map.Entry<StateRef, ScheduledStateRef>, finalizables: MutableList<() -> Unit>) {
|
||||
insert[table.scheduledAt] = entry.value.scheduledAt
|
||||
}
|
||||
|
||||
}
|
||||
var earliestState: ScheduledStateRef? = null
|
||||
var rescheduled: SettableFuture<Boolean>? = null
|
||||
|
||||
internal fun recomputeEarliest() {
|
||||
earliestState = scheduledStates.map { it.value }.sortedBy { it.scheduledAt }.firstOrNull()
|
||||
earliestState = scheduledStates.values.sortedBy { it.scheduledAt }.firstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
private val mutex = ThreadBox(InnerState())
|
||||
|
||||
// We need the [StateMachineManager] to be constructed before this is called in case it schedules a protocol.
|
||||
fun start() {
|
||||
mutex.locked {
|
||||
recomputeEarliest()
|
||||
rescheduleWakeUp()
|
||||
}
|
||||
}
|
||||
|
||||
override fun scheduleStateActivity(action: ScheduledStateRef) {
|
||||
log.trace { "Schedule $action" }
|
||||
mutex.locked {
|
||||
@ -100,7 +129,7 @@ class NodeSchedulerService(private val database: Database,
|
||||
* without the [Future] being cancelled then we run the scheduled action. Finally we remove that action from the
|
||||
* scheduled actions and recompute the next scheduled action.
|
||||
*/
|
||||
private fun rescheduleWakeUp() {
|
||||
internal fun rescheduleWakeUp() {
|
||||
// Note, we already have the mutex but we need the scope again here
|
||||
val (scheduledState, ourRescheduledFuture) = mutex.alreadyLocked {
|
||||
rescheduled?.cancel(false)
|
||||
@ -123,60 +152,72 @@ class NodeSchedulerService(private val database: Database,
|
||||
}
|
||||
|
||||
private fun onTimeReached(scheduledState: ScheduledStateRef) {
|
||||
try {
|
||||
databaseTransaction(database) {
|
||||
runScheduledActionForState(scheduledState)
|
||||
services.startProtocol(RunScheduled(scheduledState, this@NodeSchedulerService))
|
||||
}
|
||||
|
||||
class RunScheduled(val scheduledState: ScheduledStateRef, val scheduler: NodeSchedulerService) : ProtocolLogic<Unit>() {
|
||||
companion object {
|
||||
object RUNNING : ProgressTracker.Step("Running scheduled...")
|
||||
|
||||
fun tracker() = ProgressTracker(RUNNING)
|
||||
}
|
||||
|
||||
override val progressTracker = tracker()
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Unit {
|
||||
progressTracker.currentStep = RUNNING
|
||||
|
||||
// Ensure we are still scheduled.
|
||||
val scheduledLogic: ProtocolLogic<*>? = getScheduledLogic()
|
||||
if(scheduledLogic != null) {
|
||||
subProtocol(scheduledLogic)
|
||||
}
|
||||
} finally {
|
||||
// Unschedule once complete (or checkpointed)
|
||||
mutex.locked {
|
||||
}
|
||||
|
||||
private fun getScheduledaActivity(): ScheduledActivity? {
|
||||
val txState = serviceHub.loadState(scheduledState.ref)
|
||||
val state = txState.data as SchedulableState
|
||||
return try {
|
||||
// This can throw as running contract code.
|
||||
state.nextScheduledActivity(scheduledState.ref, scheduler.protocolLogicRefFactory)
|
||||
} catch(e: Exception) {
|
||||
logger.error("Attempt to run scheduled state $scheduledState resulted in error.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getScheduledLogic(): ProtocolLogic<*>? {
|
||||
val scheduledActivity = getScheduledaActivity()
|
||||
var scheduledLogic: ProtocolLogic<*>? = null
|
||||
scheduler.mutex.locked {
|
||||
// need to remove us from those scheduled, but only if we are still next
|
||||
scheduledStates.compute(scheduledState.ref) { ref, value ->
|
||||
if (value === scheduledState) null else value
|
||||
if (value === scheduledState) {
|
||||
if (scheduledActivity == null) {
|
||||
logger.info("Scheduled state $scheduledState has rescheduled to never.")
|
||||
null
|
||||
} else if (scheduledActivity.scheduledAt.isAfter(serviceHub.clock.instant())) {
|
||||
logger.info("Scheduled state $scheduledState has rescheduled to ${scheduledActivity.scheduledAt}.")
|
||||
ScheduledStateRef(scheduledState.ref, scheduledActivity.scheduledAt)
|
||||
} else {
|
||||
// TODO: ProtocolLogicRefFactory needs to sort out the class loader etc
|
||||
val logic = scheduler.protocolLogicRefFactory.toProtocolLogic(scheduledActivity.logicRef)
|
||||
logger.trace { "Scheduler starting ProtocolLogic $logic" }
|
||||
// ProtocolLogic will be checkpointed by the time this returns.
|
||||
//scheduler.services.startProtocolAndForget(logic)
|
||||
scheduledLogic = logic
|
||||
null
|
||||
}
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
// and schedule the next one
|
||||
recomputeEarliest()
|
||||
rescheduleWakeUp()
|
||||
scheduler.rescheduleWakeUp()
|
||||
}
|
||||
return scheduledLogic
|
||||
}
|
||||
}
|
||||
|
||||
private fun runScheduledActionForState(scheduledState: ScheduledStateRef) {
|
||||
val txState = services.loadState(scheduledState.ref)
|
||||
// It's OK to return if it's null as there's nothing scheduled
|
||||
// TODO: implement sandboxing as necessary
|
||||
val scheduledActivity = sandbox {
|
||||
val state = txState.data as SchedulableState
|
||||
state.nextScheduledActivity(scheduledState.ref, protocolLogicRefFactory)
|
||||
} ?: return
|
||||
|
||||
if (scheduledActivity.scheduledAt.isAfter(services.clock.instant())) {
|
||||
// I suppose it might turn out that the action is no longer due (a bug, maybe), so we need to defend against that and re-schedule
|
||||
// TODO: warn etc
|
||||
mutex.locked {
|
||||
// Replace with updated instant
|
||||
scheduledStates.compute(scheduledState.ref) { ref, value ->
|
||||
if (value === scheduledState) ScheduledStateRef(scheduledState.ref, scheduledActivity.scheduledAt) else value
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
* TODO: align with protocol invocation via API... make it the same code
|
||||
* TODO: Persistence and durability issues:
|
||||
* a) Need to consider potential to run activity twice if restart between here and removing from map if we add persistence
|
||||
* b) But if remove from map first, there's potential to run zero times if restart
|
||||
* c) Address by switch to 3rd party scheduler? Only benefit of this impl. is support for DemoClock or other MutableClocks (e.g. for testing)
|
||||
* TODO: ProtocolLogicRefFactory needs to sort out the class loader etc
|
||||
*/
|
||||
val logic = protocolLogicRefFactory.toProtocolLogic(scheduledActivity.logicRef)
|
||||
log.trace { "Firing ProtocolLogic $logic" }
|
||||
// TODO: ProtocolLogic should be checkpointed by the time this returns
|
||||
services.startProtocol(logic)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Does nothing right now, but beware we are calling dynamically loaded code in the contract inside here.
|
||||
private inline fun <T : Any> sandbox(code: () -> T?): T? {
|
||||
return code()
|
||||
}
|
||||
}
|
@ -13,18 +13,10 @@ import com.r3corda.node.services.api.ServiceHubInternal
|
||||
*/
|
||||
class ScheduledActivityObserver(val services: ServiceHubInternal) {
|
||||
init {
|
||||
// TODO: Need to consider failure scenarios. This needs to run if the TX is successfully recorded
|
||||
services.vaultService.updates.subscribe { update ->
|
||||
update.consumed.forEach { services.schedulerService.unscheduleStateActivity(it) }
|
||||
update.produced.forEach { scheduleStateActivity(it, services.protocolLogicRefFactory) }
|
||||
}
|
||||
|
||||
// In the short term, to get restart-able IRS demo, re-initialise from vault state
|
||||
// TODO: there's a race condition here. We need to move persistence into the scheduler but that is a bigger
|
||||
// change so I want to revisit as a distinct branch/PR.
|
||||
for (state in services.vaultService.currentVault.statesOfType<SchedulableState>()) {
|
||||
scheduleStateActivity(state, services.protocolLogicRefFactory)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleStateActivity(produced: StateAndRef<ContractState>, protocolLogicRefFactory: ProtocolLogicRefFactory) {
|
||||
|
@ -15,16 +15,15 @@ import org.apache.activemq.artemis.api.core.TransportConfiguration
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyStore
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* The base class for Artemis services that defines shared data structures and transport configuration
|
||||
*
|
||||
* @param certificatePath A place where Artemis can stash its message journal and other files.
|
||||
* @param config The config object is used to pass in the passwords for the certificate KeyStore and TrustStore
|
||||
*/
|
||||
abstract class ArtemisMessagingComponent(val config: NodeSSLConfiguration) : SingletonSerializeAsToken() {
|
||||
abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
|
||||
|
||||
companion object {
|
||||
init {
|
||||
@ -36,7 +35,7 @@ abstract class ArtemisMessagingComponent(val config: NodeSSLConfiguration) : Sin
|
||||
const val RPC_REQUESTS_QUEUE = "rpc.requests"
|
||||
|
||||
@JvmStatic
|
||||
protected val NETWORK_MAP_ADDRESS = SimpleString(PEERS_PREFIX +"networkmap")
|
||||
protected val NETWORK_MAP_ADDRESS = SimpleString("${PEERS_PREFIX}networkmap")
|
||||
|
||||
/**
|
||||
* Assuming the passed in target address is actually an ArtemisAddress will extract the host and port of the node. This should
|
||||
@ -70,7 +69,7 @@ abstract class ArtemisMessagingComponent(val config: NodeSSLConfiguration) : Sin
|
||||
}
|
||||
|
||||
protected data class NetworkMapAddress(override val hostAndPort: HostAndPort) : SingleMessageRecipient, ArtemisAddress {
|
||||
override val queueName: SimpleString = NETWORK_MAP_ADDRESS
|
||||
override val queueName: SimpleString get() = NETWORK_MAP_ADDRESS
|
||||
}
|
||||
|
||||
/**
|
||||
@ -80,12 +79,12 @@ abstract class ArtemisMessagingComponent(val config: NodeSSLConfiguration) : Sin
|
||||
*/
|
||||
data class NodeAddress(val identity: PublicKey, override val hostAndPort: HostAndPort) : SingleMessageRecipient, ArtemisAddress {
|
||||
override val queueName: SimpleString by lazy { SimpleString(PEERS_PREFIX+identity.toBase58String()) }
|
||||
|
||||
override fun toString(): String {
|
||||
return "NodeAddress(identity = $queueName, $hostAndPort"
|
||||
}
|
||||
override fun toString(): String = "${javaClass.simpleName}(identity = $queueName, $hostAndPort)"
|
||||
}
|
||||
|
||||
/** The config object is used to pass in the passwords for the certificate KeyStore and TrustStore */
|
||||
abstract val config: NodeSSLConfiguration
|
||||
|
||||
protected fun parseKeyFromQueueName(name: String): PublicKey {
|
||||
require(name.startsWith(PEERS_PREFIX))
|
||||
return parsePublicKeyBase58(name.substring(PEERS_PREFIX.length))
|
||||
@ -119,39 +118,46 @@ abstract class ArtemisMessagingComponent(val config: NodeSSLConfiguration) : Sin
|
||||
}
|
||||
}
|
||||
|
||||
protected fun tcpTransport(direction: ConnectionDirection, host: String, port: Int) =
|
||||
TransportConfiguration(
|
||||
when (direction) {
|
||||
ConnectionDirection.INBOUND -> NettyAcceptorFactory::class.java.name
|
||||
ConnectionDirection.OUTBOUND -> NettyConnectorFactory::class.java.name
|
||||
},
|
||||
mapOf(
|
||||
// Basic TCP target details
|
||||
TransportConstants.HOST_PROP_NAME to host,
|
||||
TransportConstants.PORT_PROP_NAME to port.toInt(),
|
||||
protected fun tcpTransport(direction: ConnectionDirection, host: String, port: Int): TransportConfiguration {
|
||||
config.keyStorePath.expectedOnDefaultFileSystem()
|
||||
config.trustStorePath.expectedOnDefaultFileSystem()
|
||||
return TransportConfiguration(
|
||||
when (direction) {
|
||||
ConnectionDirection.INBOUND -> NettyAcceptorFactory::class.java.name
|
||||
ConnectionDirection.OUTBOUND -> NettyConnectorFactory::class.java.name
|
||||
},
|
||||
mapOf(
|
||||
// Basic TCP target details
|
||||
TransportConstants.HOST_PROP_NAME to host,
|
||||
TransportConstants.PORT_PROP_NAME to port.toInt(),
|
||||
|
||||
// Turn on AMQP support, which needs the protocol jar on the classpath.
|
||||
// Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop
|
||||
// It does not use AMQP messages for its own messages e.g. topology and heartbeats
|
||||
// TODO further investigate how to ensure we use a well defined wire level protocol for Node to Node communications
|
||||
TransportConstants.PROTOCOLS_PROP_NAME to "CORE,AMQP",
|
||||
// Turn on AMQP support, which needs the protocol jar on the classpath.
|
||||
// Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop
|
||||
// It does not use AMQP messages for its own messages e.g. topology and heartbeats
|
||||
// TODO further investigate how to ensure we use a well defined wire level protocol for Node to Node communications
|
||||
TransportConstants.PROTOCOLS_PROP_NAME to "CORE,AMQP",
|
||||
|
||||
// Enable TLS transport layer with client certs and restrict to at least SHA256 in handshake
|
||||
// and AES encryption
|
||||
TransportConstants.SSL_ENABLED_PROP_NAME to true,
|
||||
TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS",
|
||||
TransportConstants.KEYSTORE_PATH_PROP_NAME to config.keyStorePath,
|
||||
TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to config.keyStorePassword, // TODO proper management of keystores and password
|
||||
TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to "JKS",
|
||||
TransportConstants.TRUSTSTORE_PATH_PROP_NAME to config.trustStorePath,
|
||||
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to config.trustStorePassword,
|
||||
TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","),
|
||||
TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to "TLSv1.2",
|
||||
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true
|
||||
)
|
||||
)
|
||||
// Enable TLS transport layer with client certs and restrict to at least SHA256 in handshake
|
||||
// and AES encryption
|
||||
TransportConstants.SSL_ENABLED_PROP_NAME to true,
|
||||
TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS",
|
||||
TransportConstants.KEYSTORE_PATH_PROP_NAME to config.keyStorePath,
|
||||
TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to config.keyStorePassword, // TODO proper management of keystores and password
|
||||
TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to "JKS",
|
||||
TransportConstants.TRUSTSTORE_PATH_PROP_NAME to config.trustStorePath,
|
||||
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to config.trustStorePassword,
|
||||
TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","),
|
||||
TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to "TLSv1.2",
|
||||
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun configureWithDevSSLCertificate() {
|
||||
config.configureWithDevSSLCertificate()
|
||||
}
|
||||
|
||||
protected fun Path.expectedOnDefaultFileSystem() {
|
||||
require(fileSystem == FileSystems.getDefault()) { "Artemis only uses the default file system" }
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,15 @@ package com.r3corda.node.services.messaging
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.r3corda.core.ThreadBox
|
||||
import com.r3corda.core.crypto.AddressFormatException
|
||||
import com.r3corda.core.crypto.newSecureRandom
|
||||
import com.r3corda.core.div
|
||||
import com.r3corda.core.exists
|
||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
||||
import com.r3corda.core.node.services.NetworkMapCache
|
||||
import com.r3corda.core.use
|
||||
import com.r3corda.core.utilities.loggerFor
|
||||
import com.r3corda.node.services.config.NodeConfiguration
|
||||
import com.r3corda.node.services.messaging.ArtemisMessagingServer.NodeLoginModule.Companion.NODE_ROLE_NAME
|
||||
import com.r3corda.node.services.messaging.ArtemisMessagingServer.NodeLoginModule.Companion.RPC_ROLE_NAME
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.core.config.BridgeConfiguration
|
||||
import org.apache.activemq.artemis.core.config.Configuration
|
||||
@ -17,11 +21,25 @@ import org.apache.activemq.artemis.core.security.Role
|
||||
import org.apache.activemq.artemis.core.server.ActiveMQServer
|
||||
import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl
|
||||
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.InVMLoginModule
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal
|
||||
import rx.Subscription
|
||||
import java.math.BigInteger
|
||||
import java.io.IOException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.Principal
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import javax.security.auth.Subject
|
||||
import javax.security.auth.callback.CallbackHandler
|
||||
import javax.security.auth.callback.NameCallback
|
||||
import javax.security.auth.callback.PasswordCallback
|
||||
import javax.security.auth.callback.UnsupportedCallbackException
|
||||
import javax.security.auth.login.AppConfigurationEntry
|
||||
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag.REQUIRED
|
||||
import javax.security.auth.login.FailedLoginException
|
||||
import javax.security.auth.login.LoginException
|
||||
import javax.security.auth.spi.LoginModule
|
||||
|
||||
// TODO: Verify that nobody can connect to us and fiddle with our config over the socket due to the secman.
|
||||
// TODO: Implement a discovery engine that can trigger builds of new connections when another node registers? (later)
|
||||
@ -37,9 +55,9 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
* a fully connected network, trusted network or on localhost.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class ArtemisMessagingServer(config: NodeConfiguration,
|
||||
class ArtemisMessagingServer(override val config: NodeConfiguration,
|
||||
val myHostPort: HostAndPort,
|
||||
val networkMapCache: NetworkMapCache) : ArtemisMessagingComponent(config) {
|
||||
val networkMapCache: NetworkMapCache) : ArtemisMessagingComponent() {
|
||||
companion object {
|
||||
val log = loggerFor<ArtemisMessagingServer>()
|
||||
}
|
||||
@ -52,6 +70,10 @@ class ArtemisMessagingServer(config: NodeConfiguration,
|
||||
private lateinit var activeMQServer: ActiveMQServer
|
||||
private var networkChangeHandle: Subscription? = null
|
||||
|
||||
init {
|
||||
config.basedir.expectedOnDefaultFileSystem()
|
||||
}
|
||||
|
||||
fun start() = mutex.locked {
|
||||
if (!running) {
|
||||
configureAndStartServer()
|
||||
@ -116,12 +138,7 @@ class ArtemisMessagingServer(config: NodeConfiguration,
|
||||
}
|
||||
|
||||
private fun configureAndStartServer() {
|
||||
val config = createArtemisConfig(config.certificatesPath, myHostPort).apply {
|
||||
securityRoles = mapOf(
|
||||
"#" to setOf(Role("internal", true, true, true, true, true, true, true))
|
||||
)
|
||||
}
|
||||
|
||||
val config = createArtemisConfig()
|
||||
val securityManager = createArtemisSecurityManager()
|
||||
|
||||
activeMQServer = ActiveMQServerImpl(config, securityManager).apply {
|
||||
@ -157,28 +174,61 @@ class ArtemisMessagingServer(config: NodeConfiguration,
|
||||
activeMQServer.start()
|
||||
}
|
||||
|
||||
private fun createArtemisConfig(directory: Path, hp: HostAndPort): Configuration {
|
||||
val config = ConfigurationImpl()
|
||||
setConfigDirectories(config, directory)
|
||||
config.acceptorConfigurations = setOf(
|
||||
tcpTransport(ConnectionDirection.INBOUND, "0.0.0.0", hp.port)
|
||||
private fun createArtemisConfig(): Configuration = ConfigurationImpl().apply {
|
||||
val artemisDir = config.basedir / "artemis"
|
||||
bindingsDirectory = (artemisDir / "bindings").toString()
|
||||
journalDirectory = (artemisDir / "journal").toString()
|
||||
largeMessagesDirectory = (artemisDir / "largemessages").toString()
|
||||
acceptorConfigurations = setOf(
|
||||
tcpTransport(ConnectionDirection.INBOUND, "0.0.0.0", myHostPort.port)
|
||||
)
|
||||
// Enable built in message deduplication. Note we still have to do our own as the delayed commits
|
||||
// and our own definition of commit mean that the built in deduplication cannot remove all duplicates.
|
||||
config.idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess
|
||||
config.isPersistIDCache = true
|
||||
return config
|
||||
idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess
|
||||
isPersistIDCache = true
|
||||
isPopulateValidatedUser = true
|
||||
setupUserRoles()
|
||||
}
|
||||
|
||||
// This gives nodes full access and RPC clients only enough to do RPC
|
||||
private fun ConfigurationImpl.setupUserRoles() {
|
||||
// TODO COR-307
|
||||
val nodeRole = Role(NODE_ROLE_NAME, true, true, true, true, true, true, true, true)
|
||||
val clientRpcRole = restrictedRole(RPC_ROLE_NAME, consume = true, createNonDurableQueue = true, deleteNonDurableQueue = true)
|
||||
securityRoles = mapOf(
|
||||
"#" to setOf(nodeRole),
|
||||
"clients.*.rpc.responses.*" to setOf(nodeRole, clientRpcRole),
|
||||
"clients.*.rpc.observations.*" to setOf(nodeRole, clientRpcRole),
|
||||
RPC_REQUESTS_QUEUE to setOf(nodeRole, restrictedRole(RPC_ROLE_NAME, send = true))
|
||||
)
|
||||
}
|
||||
|
||||
private fun restrictedRole(name: String, send: Boolean = false, consume: Boolean = false, createDurableQueue: Boolean = false,
|
||||
deleteDurableQueue: Boolean = false, createNonDurableQueue: Boolean = false,
|
||||
deleteNonDurableQueue: Boolean = false, manage: Boolean = false, browse: Boolean = false): Role {
|
||||
return Role(name, send, consume, createDurableQueue, deleteDurableQueue, createNonDurableQueue,
|
||||
deleteNonDurableQueue, manage, browse)
|
||||
}
|
||||
|
||||
private fun createArtemisSecurityManager(): ActiveMQJAASSecurityManager {
|
||||
// TODO: set up proper security configuration https://r3-cev.atlassian.net/browse/COR-307
|
||||
val securityConfig = SecurityConfiguration().apply {
|
||||
addUser("internal", BigInteger(128, newSecureRandom()).toString(16))
|
||||
addRole("internal", "internal")
|
||||
defaultUser = "internal"
|
||||
val rpcUsersFile = config.basedir / "rpc-users.properties"
|
||||
if (!rpcUsersFile.exists()) {
|
||||
val users = Properties()
|
||||
users["user1"] = "test"
|
||||
Files.newOutputStream(rpcUsersFile).use {
|
||||
users.store(it, null)
|
||||
}
|
||||
}
|
||||
|
||||
return ActiveMQJAASSecurityManager(InVMLoginModule::class.java.name, securityConfig)
|
||||
val securityConfig = object : SecurityConfiguration() {
|
||||
// Override to make it work with our login module
|
||||
override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> {
|
||||
val options = mapOf(NodeLoginModule.FILE_KEY to rpcUsersFile)
|
||||
return arrayOf(AppConfigurationEntry(name, REQUIRED, options))
|
||||
}
|
||||
}
|
||||
|
||||
return ActiveMQJAASSecurityManager(NodeLoginModule::class.java.name, securityConfig)
|
||||
}
|
||||
|
||||
private fun connectorExists(hostAndPort: HostAndPort) = hostAndPort.toString() in activeMQServer.configuration.connectorConfigurations
|
||||
@ -194,12 +244,11 @@ class ArtemisMessagingServer(config: NodeConfiguration,
|
||||
|
||||
private fun bridgeExists(name: SimpleString) = activeMQServer.clusterManager.bridges.containsKey(name.toString())
|
||||
|
||||
private fun deployBridge(hostAndPort: HostAndPort, name: SimpleString) {
|
||||
private fun deployBridge(hostAndPort: HostAndPort, name: String) {
|
||||
activeMQServer.deployBridge(BridgeConfiguration().apply {
|
||||
val nameStr = name.toString()
|
||||
setName(nameStr)
|
||||
queueName = nameStr
|
||||
forwardingAddress = nameStr
|
||||
setName(name)
|
||||
queueName = name
|
||||
forwardingAddress = name
|
||||
staticConnectors = listOf(hostAndPort.toString())
|
||||
confirmationWindowSize = 100000 // a guess
|
||||
isUseDuplicateDetection = true // Enable the bridges automatic deduplication logic
|
||||
@ -218,7 +267,7 @@ class ArtemisMessagingServer(config: NodeConfiguration,
|
||||
if (!connectorExists(hostAndPort))
|
||||
addConnector(hostAndPort)
|
||||
if (!bridgeExists(name))
|
||||
deployBridge(hostAndPort, name)
|
||||
deployBridge(hostAndPort, name.toString())
|
||||
}
|
||||
|
||||
private fun maybeDestroyBridge(name: SimpleString) {
|
||||
@ -227,11 +276,81 @@ class ArtemisMessagingServer(config: NodeConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
private fun setConfigDirectories(config: Configuration, dir: Path) {
|
||||
config.apply {
|
||||
bindingsDirectory = dir.resolve("bindings").toString()
|
||||
journalDirectory = dir.resolve("journal").toString()
|
||||
largeMessagesDirectory = dir.resolve("largemessages").toString()
|
||||
|
||||
class NodeLoginModule : LoginModule {
|
||||
|
||||
companion object {
|
||||
const val FILE_KEY = "rpc-users-file"
|
||||
const val NODE_ROLE_NAME = "NodeRole"
|
||||
const val RPC_ROLE_NAME = "RpcRole"
|
||||
}
|
||||
|
||||
private val users = Properties()
|
||||
private var loginSucceeded: Boolean = false
|
||||
private lateinit var subject: Subject
|
||||
private lateinit var callbackHandler: CallbackHandler
|
||||
private lateinit var principals: List<Principal>
|
||||
|
||||
override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) {
|
||||
this.subject = subject
|
||||
this.callbackHandler = callbackHandler
|
||||
val rpcUsersFile = options[FILE_KEY] as Path
|
||||
if (rpcUsersFile.exists()) {
|
||||
rpcUsersFile.use {
|
||||
users.load(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun login(): Boolean {
|
||||
val nameCallback = NameCallback("Username: ")
|
||||
val passwordCallback = PasswordCallback("Password: ", false)
|
||||
|
||||
try {
|
||||
callbackHandler.handle(arrayOf(nameCallback, passwordCallback))
|
||||
} catch (e: IOException) {
|
||||
throw LoginException(e.message)
|
||||
} catch (e: UnsupportedCallbackException) {
|
||||
throw LoginException("${e.message} not available to obtain information from user")
|
||||
}
|
||||
|
||||
val username = nameCallback.name ?: throw FailedLoginException("User name is null")
|
||||
val receivedPassword = passwordCallback.password ?: throw FailedLoginException("Password is null")
|
||||
val password = if (username == "Node") "Node" else users[username] ?: throw FailedLoginException("User does not exist")
|
||||
if (password != String(receivedPassword)) {
|
||||
throw FailedLoginException("Password does not match")
|
||||
}
|
||||
|
||||
principals = listOf(
|
||||
UserPrincipal(username),
|
||||
RolePrincipal(if (username == "Node") NODE_ROLE_NAME else RPC_ROLE_NAME))
|
||||
|
||||
loginSucceeded = true
|
||||
return loginSucceeded
|
||||
}
|
||||
|
||||
override fun commit(): Boolean {
|
||||
val result = loginSucceeded
|
||||
if (result) {
|
||||
subject.principals.addAll(principals)
|
||||
}
|
||||
clear()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun abort(): Boolean {
|
||||
clear()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun logout(): Boolean {
|
||||
subject.principals.removeAll(principals)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun clear() {
|
||||
loginSucceeded = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package com.r3corda.node.services.messaging
|
||||
import com.r3corda.core.contracts.ClientToServiceCommand
|
||||
import com.r3corda.core.contracts.ContractState
|
||||
import com.r3corda.core.contracts.StateAndRef
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.services.NetworkMapCache
|
||||
import com.r3corda.core.node.services.StateMachineTransactionMapping
|
||||
import com.r3corda.core.node.services.Vault
|
||||
import com.r3corda.core.protocols.StateMachineRunId
|
||||
@ -103,9 +105,15 @@ interface CordaRPCOps : RPCOps {
|
||||
@RPCReturnsObservables
|
||||
fun stateMachineRecordedTransactionMapping(): Pair<List<StateMachineTransactionMapping>, Observable<StateMachineTransactionMapping>>
|
||||
|
||||
/**
|
||||
* Returns all parties currently visible on the network with their advertised services and an observable of future updates to the network.
|
||||
*/
|
||||
@RPCReturnsObservables
|
||||
fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>>
|
||||
|
||||
/**
|
||||
* Executes the given command, possibly triggering cash creation etc.
|
||||
* TODO: The signature of this is weird because it's the remains of an old service call, we should have a call for each command instead.
|
||||
*/
|
||||
fun executeCommand(command: ClientToServiceCommand): TransactionBuildResult
|
||||
}
|
||||
}
|
@ -11,11 +11,13 @@ import com.r3corda.node.services.api.MessagingServiceInternal
|
||||
import com.r3corda.node.services.config.NodeConfiguration
|
||||
import com.r3corda.node.utilities.*
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
|
||||
import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
|
||||
import org.apache.activemq.artemis.api.core.Message.HDR_VALIDATED_USER
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.client.*
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.statements.InsertStatement
|
||||
import java.nio.file.FileSystems
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
@ -49,12 +51,11 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
* in this class.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class NodeMessagingClient(config: NodeConfiguration,
|
||||
class NodeMessagingClient(override val config: NodeConfiguration,
|
||||
val serverHostPort: HostAndPort,
|
||||
val myIdentity: PublicKey?,
|
||||
val executor: AffinityExecutor,
|
||||
val persistentInbox: Boolean = true,
|
||||
val persistenceTx: (() -> Unit) -> Unit = { it() }) : ArtemisMessagingComponent(config), MessagingServiceInternal {
|
||||
val database: Database) : ArtemisMessagingComponent(), MessagingServiceInternal {
|
||||
companion object {
|
||||
val log = loggerFor<NodeMessagingClient>()
|
||||
|
||||
@ -86,8 +87,7 @@ class NodeMessagingClient(config: NodeConfiguration,
|
||||
var rpcConsumer: ClientConsumer? = null
|
||||
var rpcNotificationConsumer: ClientConsumer? = null
|
||||
|
||||
// TODO: This is not robust and needs to be replaced by more intelligently using the message queue server.
|
||||
var undeliveredMessages = listOf<Message>()
|
||||
var pendingRedelivery = JDBCHashSet<Message>("pending_messages",loadOnInit = true)
|
||||
}
|
||||
|
||||
/** A registration to handle messages of different types */
|
||||
@ -106,23 +106,16 @@ class NodeMessagingClient(config: NodeConfiguration,
|
||||
val uuid = uuidString("message_id")
|
||||
}
|
||||
|
||||
private val processedMessages: MutableSet<UUID> = Collections.synchronizedSet(if (persistentInbox) {
|
||||
private val processedMessages: MutableSet<UUID> = Collections.synchronizedSet(
|
||||
object : AbstractJDBCHashSet<UUID, Table>(Table, loadOnInit = true) {
|
||||
override fun elementFromRow(row: ResultRow): UUID = row[table.uuid]
|
||||
|
||||
override fun addElementToInsert(insert: InsertStatement, entry: UUID, finalizables: MutableList<() -> Unit>) {
|
||||
insert[table.uuid] = entry
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HashSet<UUID>()
|
||||
})
|
||||
})
|
||||
|
||||
init {
|
||||
require(config.basedir.fileSystem == FileSystems.getDefault()) { "Artemis only uses the default file system" }
|
||||
}
|
||||
|
||||
fun start(rpcOps: CordaRPCOps? = null) {
|
||||
fun start(rpcOps: CordaRPCOps) {
|
||||
state.locked {
|
||||
check(!started) { "start can't be called twice" }
|
||||
started = true
|
||||
@ -135,7 +128,7 @@ class NodeMessagingClient(config: NodeConfiguration,
|
||||
|
||||
// Create a session. Note that the acknowledgement of messages is not flushed to
|
||||
// the Artermis journal until the default buffer size of 1MB is acknowledged.
|
||||
val session = clientFactory!!.createSession(true, true, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE)
|
||||
val session = clientFactory!!.createSession("Node", "Node", false, true, true, locator.isPreAcknowledge, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE)
|
||||
this.session = session
|
||||
session.start()
|
||||
|
||||
@ -146,7 +139,7 @@ class NodeMessagingClient(config: NodeConfiguration,
|
||||
val queueName = toQueueName(myAddress)
|
||||
val query = session.queueQuery(queueName)
|
||||
if (!query.isExists) {
|
||||
session.createQueue(queueName, queueName, persistentInbox)
|
||||
session.createQueue(queueName, queueName, true)
|
||||
}
|
||||
knownQueues.add(queueName)
|
||||
p2pConsumer = session.createConsumer(queueName)
|
||||
@ -154,13 +147,11 @@ class NodeMessagingClient(config: NodeConfiguration,
|
||||
// Create an RPC queue and consumer: this will service locally connected clients only (not via a
|
||||
// bridge) and those clients must have authenticated. We could use a single consumer for everything
|
||||
// and perhaps we should, but these queues are not worth persisting.
|
||||
if (rpcOps != null) {
|
||||
session.createTemporaryQueue(RPC_REQUESTS_QUEUE, RPC_REQUESTS_QUEUE)
|
||||
session.createTemporaryQueue("activemq.notifications", "rpc.qremovals", "_AMQ_NotifType = 1")
|
||||
rpcConsumer = session.createConsumer(RPC_REQUESTS_QUEUE)
|
||||
rpcNotificationConsumer = session.createConsumer("rpc.qremovals")
|
||||
dispatcher = createRPCDispatcher(state, rpcOps)
|
||||
}
|
||||
session.createTemporaryQueue(RPC_REQUESTS_QUEUE, RPC_REQUESTS_QUEUE)
|
||||
session.createTemporaryQueue("activemq.notifications", "rpc.qremovals", "_AMQ_NotifType = 1")
|
||||
rpcConsumer = session.createConsumer(RPC_REQUESTS_QUEUE)
|
||||
rpcNotificationConsumer = session.createConsumer("rpc.qremovals")
|
||||
dispatcher = createRPCDispatcher(state, rpcOps)
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,8 +218,9 @@ class NodeMessagingClient(config: NodeConfiguration,
|
||||
val topic = message.getStringProperty(TOPIC_PROPERTY)
|
||||
val sessionID = message.getLongProperty(SESSION_ID_PROPERTY)
|
||||
// Use the magic deduplication property built into Artemis as our message identity too
|
||||
val uuid = UUID.fromString(message.getStringProperty(org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID))
|
||||
log.info("received message from: ${message.address} topic: $topic sessionID: $sessionID uuid: $uuid")
|
||||
val uuid = UUID.fromString(message.getStringProperty(HDR_DUPLICATE_DETECTION_ID))
|
||||
val user = message.getStringProperty(HDR_VALIDATED_USER)
|
||||
log.info("Received message from: ${message.address} user: $user topic: $topic sessionID: $sessionID uuid: $uuid")
|
||||
|
||||
val body = ByteArray(message.bodySize).apply { message.bodyBuffer.readBytes(this) }
|
||||
|
||||
@ -259,10 +251,10 @@ class NodeMessagingClient(config: NodeConfiguration,
|
||||
// without causing log spam.
|
||||
log.warn("Received message for ${msg.topicSession} that doesn't have any registered handlers yet")
|
||||
|
||||
// This is a hack; transient messages held in memory isn't crash resistant.
|
||||
// TODO: Use Artemis API more effectively so we don't pop messages off a queue that we aren't ready to use.
|
||||
state.locked {
|
||||
undeliveredMessages += msg
|
||||
databaseTransaction(database) {
|
||||
pendingRedelivery.add(msg)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -277,7 +269,7 @@ class NodeMessagingClient(config: NodeConfiguration,
|
||||
// Note that handlers may re-enter this class. We aren't holding any locks and methods like
|
||||
// start/run/stop have re-entrancy assertions at the top, so it is OK.
|
||||
executor.fetchFrom {
|
||||
persistenceTx {
|
||||
databaseTransaction(database) {
|
||||
callHandlers(msg, deliverTo)
|
||||
}
|
||||
}
|
||||
@ -346,7 +338,7 @@ class NodeMessagingClient(config: NodeConfiguration,
|
||||
putLongProperty(SESSION_ID_PROPERTY, sessionID)
|
||||
writeBodyBufferBytes(message.data)
|
||||
// Use the magic deduplication property built into Artemis as our message identity too
|
||||
putStringProperty(org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
|
||||
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
|
||||
}
|
||||
|
||||
if (knownQueues.add(queueName)) {
|
||||
@ -376,9 +368,12 @@ class NodeMessagingClient(config: NodeConfiguration,
|
||||
val handler = Handler(topicSession, callback)
|
||||
handlers.add(handler)
|
||||
val messagesToRedeliver = state.locked {
|
||||
val messagesToRedeliver = undeliveredMessages
|
||||
undeliveredMessages = listOf()
|
||||
messagesToRedeliver
|
||||
val pending = ArrayList<Message>()
|
||||
databaseTransaction(database) {
|
||||
pending.addAll(pendingRedelivery)
|
||||
pendingRedelivery.clear()
|
||||
}
|
||||
pending
|
||||
}
|
||||
messagesToRedeliver.forEach { deliver(it) }
|
||||
return handler
|
||||
@ -391,8 +386,8 @@ class NodeMessagingClient(config: NodeConfiguration,
|
||||
override fun createMessage(topicSession: TopicSession, data: ByteArray, uuid: UUID): Message {
|
||||
// TODO: We could write an object that proxies directly to an underlying MQ message here and avoid copying.
|
||||
return object : Message {
|
||||
override val topicSession: TopicSession get() = topicSession
|
||||
override val data: ByteArray get() = data
|
||||
override val topicSession: TopicSession = topicSession
|
||||
override val data: ByteArray = data
|
||||
override val debugTimestamp: Instant = Instant.now()
|
||||
override fun serialise(): ByteArray = this.serialise()
|
||||
override val uniqueMessageId: UUID = uuid
|
||||
@ -408,7 +403,7 @@ class NodeMessagingClient(config: NodeConfiguration,
|
||||
val msg = session!!.createMessage(false).apply {
|
||||
writeBodyBufferBytes(bits.bits)
|
||||
// Use the magic deduplication property built into Artemis as our message identity too
|
||||
putStringProperty(org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
|
||||
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
|
||||
}
|
||||
producer!!.send(toAddress, msg)
|
||||
}
|
||||
|
@ -5,13 +5,17 @@ import com.esotericsoftware.kryo.Registration
|
||||
import com.esotericsoftware.kryo.Serializer
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.serializers.DefaultSerializers
|
||||
import com.esotericsoftware.kryo.serializers.JavaSerializer
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.core.ErrorOr
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.DigitalSignature
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.*
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.PhysicalLocation
|
||||
import com.r3corda.core.node.ServiceEntry
|
||||
import com.r3corda.core.node.services.NetworkMapCache
|
||||
import com.r3corda.core.node.services.ServiceInfo
|
||||
import com.r3corda.core.node.services.StateMachineTransactionMapping
|
||||
import com.r3corda.core.node.services.Vault
|
||||
import com.r3corda.core.protocols.StateMachineRunId
|
||||
@ -163,17 +167,42 @@ private class RPCKryo(private val observableSerializer: Serializer<Observable<An
|
||||
register(setOf(Unit).javaClass) // SingletonSet
|
||||
register(TransactionBuildResult.ProtocolStarted::class.java)
|
||||
register(TransactionBuildResult.Failed::class.java)
|
||||
|
||||
register(ServiceEntry::class.java)
|
||||
register(NodeInfo::class.java)
|
||||
register(PhysicalLocation::class.java)
|
||||
register(NetworkMapCache.MapChange::class.java)
|
||||
register(NetworkMapCache.MapChangeType::class.java)
|
||||
register(ArtemisMessagingComponent.NodeAddress::class.java,
|
||||
read = { kryo, input -> ArtemisMessagingComponent.NodeAddress(parsePublicKeyBase58(kryo.readObject(input, String::class.java)), kryo.readObject(input, HostAndPort::class.java)) },
|
||||
write = { kryo, output, nodeAddress ->
|
||||
kryo.writeObject(output, nodeAddress.identity.toBase58String())
|
||||
kryo.writeObject(output, nodeAddress.hostAndPort)
|
||||
}
|
||||
)
|
||||
register(HostAndPort::class.java)
|
||||
register(ServiceInfo::class.java, read = { kryo, input -> ServiceInfo.parse(input.readString()) }, write = Kryo::writeObject)
|
||||
// Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway.
|
||||
register(IllegalArgumentException::class.java)
|
||||
// Kryo couldn't serialize Collections.unmodifiableCollection in Throwable correctly, causing null pointer exception when try to access the deserialize object.
|
||||
register(NoSuchElementException::class.java, JavaSerializer())
|
||||
register(RPCException::class.java)
|
||||
register(Array<StackTraceElement>::class.java, object : Serializer<Array<StackTraceElement>>() {
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<Array<StackTraceElement>>): Array<StackTraceElement> = emptyArray()
|
||||
override fun write(kryo: Kryo, output: Output, `object`: Array<StackTraceElement>) {}
|
||||
})
|
||||
register(Array<StackTraceElement>::class.java, read = { kryo, input -> emptyArray() }, write = { kryo, output, o -> })
|
||||
register(Collections.unmodifiableList(emptyList<String>()).javaClass)
|
||||
}
|
||||
|
||||
// Helper method, attempt to reduce boiler plate code
|
||||
private fun <T> register(type: Class<T>, read: (Kryo, Input) -> T, write: (Kryo, Output, T) -> Unit) {
|
||||
register(type, object : Serializer<T>() {
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<T>?): T {
|
||||
return read(kryo, input)
|
||||
}
|
||||
|
||||
override fun write(kryo: Kryo, output: Output, o: T) {
|
||||
write(kryo, output, o)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val observableRegistration: Registration? = if (observableSerializer != null) register(Observable::class.java, observableSerializer) else null
|
||||
|
||||
override fun getRegistration(type: Class<*>): Registration {
|
||||
|
@ -3,6 +3,7 @@ package com.r3corda.node.services.network
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import com.r3corda.core.bufferUntilSubscribed
|
||||
import com.r3corda.core.contracts.Contract
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.map
|
||||
@ -23,6 +24,7 @@ import com.r3corda.node.services.network.NetworkMapService.Companion.FETCH_PROTO
|
||||
import com.r3corda.node.services.network.NetworkMapService.Companion.SUBSCRIPTION_PROTOCOL_TOPIC
|
||||
import com.r3corda.node.services.network.NetworkMapService.FetchMapResponse
|
||||
import com.r3corda.node.services.network.NetworkMapService.SubscribeResponse
|
||||
import com.r3corda.node.services.transactions.SimpleNotaryService
|
||||
import com.r3corda.node.utilities.AddOrRemove
|
||||
import com.r3corda.protocols.sendRequest
|
||||
import rx.Observable
|
||||
@ -54,6 +56,18 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
|
||||
private var registeredForPush = false
|
||||
protected var registeredNodes = Collections.synchronizedMap(HashMap<Party, NodeInfo>())
|
||||
|
||||
override fun track(): Pair<List<NodeInfo>, Observable<MapChange>> {
|
||||
synchronized(_changed) {
|
||||
fun NodeInfo.isCordaService(): Boolean {
|
||||
return advertisedServices.any { it.info.type in setOf(SimpleNotaryService.type, NetworkMapService.type) }
|
||||
}
|
||||
|
||||
val currentParties = partyNodes.filter { !it.isCordaService() }
|
||||
val changes = changed.filter { !it.node.isCordaService() }
|
||||
return Pair(currentParties, changes.bufferUntilSubscribed())
|
||||
}
|
||||
}
|
||||
|
||||
override fun get() = registeredNodes.map { it.value }
|
||||
override fun get(serviceType: ServiceType) = registeredNodes.filterValues { it.advertisedServices.any { it.info.type.isSubTypeOf(serviceType) } }.map { it.value }
|
||||
override fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = get(type).firstOrNull()
|
||||
@ -96,17 +110,22 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
|
||||
}
|
||||
|
||||
override fun addNode(node: NodeInfo) {
|
||||
val oldValue = registeredNodes.put(node.legalIdentity, node)
|
||||
if (oldValue == null) {
|
||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Added))
|
||||
} else if(oldValue != node) {
|
||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Modified))
|
||||
synchronized(_changed) {
|
||||
val oldValue = registeredNodes.put(node.legalIdentity, node)
|
||||
if (oldValue == null) {
|
||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Added))
|
||||
} else if (oldValue != node) {
|
||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Modified))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun removeNode(node: NodeInfo) {
|
||||
val oldValue = registeredNodes.remove(node.legalIdentity)
|
||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Removed))
|
||||
synchronized(_changed) {
|
||||
val oldValue = registeredNodes.remove(node.legalIdentity)
|
||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Removed))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,65 @@
|
||||
package com.r3corda.node.services.persistence
|
||||
|
||||
import com.r3corda.core.ThreadBox
|
||||
import com.r3corda.core.bufferUntilSubscribed
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.node.services.StateMachineRecordedTransactionMappingStorage
|
||||
import com.r3corda.core.node.services.StateMachineTransactionMapping
|
||||
import com.r3corda.core.protocols.StateMachineRunId
|
||||
import com.r3corda.node.utilities.*
|
||||
import org.jetbrains.exposed.sql.ResultRow
|
||||
import org.jetbrains.exposed.sql.statements.InsertStatement
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* Database storage of a txhash -> state machine id mapping.
|
||||
*
|
||||
* Mappings are added as transactions are persisted by [ServiceHub.recordTransaction], and never deleted. Used in the
|
||||
* RPC API to correlate transaction creation with protocols.
|
||||
*
|
||||
*/
|
||||
@ThreadSafe
|
||||
class DBTransactionMappingStorage : StateMachineRecordedTransactionMappingStorage {
|
||||
|
||||
private object Table : JDBCHashedTable("${NODE_DATABASE_PREFIX}transaction_mappings") {
|
||||
val txId = secureHash("tx_id")
|
||||
val stateMachineRunId = uuidString("state_machine_run_id")
|
||||
}
|
||||
|
||||
private class TransactionMappingsMap : AbstractJDBCHashMap<SecureHash, StateMachineRunId, Table>(Table, loadOnInit = false) {
|
||||
override fun keyFromRow(row: ResultRow): SecureHash = row[table.txId]
|
||||
|
||||
override fun valueFromRow(row: ResultRow): StateMachineRunId = StateMachineRunId.wrap(row[table.stateMachineRunId])
|
||||
|
||||
override fun addKeyToInsert(insert: InsertStatement, entry: Map.Entry<SecureHash, StateMachineRunId>, finalizables: MutableList<() -> Unit>) {
|
||||
insert[table.txId] = entry.key
|
||||
}
|
||||
|
||||
override fun addValueToInsert(insert: InsertStatement, entry: Map.Entry<SecureHash, StateMachineRunId>, finalizables: MutableList<() -> Unit>) {
|
||||
insert[table.stateMachineRunId] = entry.value.uuid
|
||||
}
|
||||
}
|
||||
|
||||
private val mutex = ThreadBox(object {
|
||||
val stateMachineTransactionMap = TransactionMappingsMap()
|
||||
val updates = PublishSubject.create<StateMachineTransactionMapping>()
|
||||
})
|
||||
|
||||
override fun addMapping(stateMachineRunId: StateMachineRunId, transactionId: SecureHash) {
|
||||
mutex.locked {
|
||||
stateMachineTransactionMap[transactionId] = stateMachineRunId
|
||||
updates.onNext(StateMachineTransactionMapping(stateMachineRunId, transactionId))
|
||||
}
|
||||
}
|
||||
|
||||
override fun track(): Pair<List<StateMachineTransactionMapping>, Observable<StateMachineTransactionMapping>> {
|
||||
mutex.locked {
|
||||
return Pair(
|
||||
stateMachineTransactionMap.map { StateMachineTransactionMapping(it.value, it.key) },
|
||||
updates.bufferUntilSubscribed()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
package com.r3corda.node.services.persistence
|
||||
|
||||
import com.r3corda.core.serialization.SerializedBytes
|
||||
import com.r3corda.core.serialization.deserialize
|
||||
import com.r3corda.core.serialization.serialize
|
||||
import com.r3corda.core.utilities.loggerFor
|
||||
import com.r3corda.core.utilities.trace
|
||||
import com.r3corda.node.services.api.Checkpoint
|
||||
import com.r3corda.node.services.api.CheckpointStorage
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.*
|
||||
import java.util.Collections.synchronizedMap
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
|
||||
/**
|
||||
* File-based checkpoint storage, storing checkpoints per file.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class PerFileCheckpointStorage(val storeDir: Path) : CheckpointStorage {
|
||||
|
||||
companion object {
|
||||
private val logger = loggerFor<PerFileCheckpointStorage>()
|
||||
private val fileExtension = ".checkpoint"
|
||||
}
|
||||
|
||||
private val checkpointFiles = synchronizedMap(IdentityHashMap<Checkpoint, Path>())
|
||||
|
||||
init {
|
||||
logger.trace { "Initialising per file checkpoint storage on $storeDir" }
|
||||
Files.createDirectories(storeDir)
|
||||
Files.list(storeDir)
|
||||
.filter { it.toString().toLowerCase().endsWith(fileExtension) }
|
||||
.forEach {
|
||||
val checkpoint = Files.readAllBytes(it).deserialize<Checkpoint>()
|
||||
checkpointFiles[checkpoint] = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun addCheckpoint(checkpoint: Checkpoint) {
|
||||
val fileName = "${checkpoint.id.toString().toLowerCase()}$fileExtension"
|
||||
val checkpointFile = storeDir.resolve(fileName)
|
||||
atomicWrite(checkpointFile, checkpoint.serialize())
|
||||
logger.trace { "Stored $checkpoint to $checkpointFile" }
|
||||
checkpointFiles[checkpoint] = checkpointFile
|
||||
}
|
||||
|
||||
private fun atomicWrite(checkpointFile: Path, serialisedCheckpoint: SerializedBytes<Checkpoint>) {
|
||||
val tempCheckpointFile = checkpointFile.parent.resolve("${checkpointFile.fileName}.tmp")
|
||||
serialisedCheckpoint.writeToFile(tempCheckpointFile)
|
||||
Files.move(tempCheckpointFile, checkpointFile, StandardCopyOption.ATOMIC_MOVE)
|
||||
}
|
||||
|
||||
override fun removeCheckpoint(checkpoint: Checkpoint) {
|
||||
val checkpointFile = checkpointFiles.remove(checkpoint)
|
||||
require(checkpointFile != null) { "Trying to removing unknown checkpoint: $checkpoint" }
|
||||
Files.delete(checkpointFile)
|
||||
logger.trace { "Removed $checkpoint ($checkpointFile)" }
|
||||
}
|
||||
|
||||
override fun forEach(block: (Checkpoint)->Boolean) {
|
||||
synchronized(checkpointFiles) {
|
||||
for(checkpoint in checkpointFiles.keys) {
|
||||
if (!block(checkpoint)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
package com.r3corda.node.services.persistence
|
||||
|
||||
import com.r3corda.core.ThreadBox
|
||||
import com.r3corda.core.bufferUntilSubscribed
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.node.services.TransactionStorage
|
||||
import com.r3corda.core.serialization.deserialize
|
||||
import com.r3corda.core.serialization.serialize
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.utilities.loggerFor
|
||||
import com.r3corda.core.utilities.trace
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
|
||||
/**
|
||||
* File-based transaction storage, storing transactions per file.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class PerFileTransactionStorage(val storeDir: Path) : TransactionStorage {
|
||||
companion object {
|
||||
private val logger = loggerFor<PerFileCheckpointStorage>()
|
||||
private val fileExtension = ".transaction"
|
||||
}
|
||||
|
||||
private val mutex = ThreadBox(object {
|
||||
val transactionsMap = HashMap<SecureHash, SignedTransaction>()
|
||||
val updatesPublisher = PublishSubject.create<SignedTransaction>()
|
||||
|
||||
fun notify(transaction: SignedTransaction) = updatesPublisher.onNext(transaction)
|
||||
})
|
||||
|
||||
override val updates: Observable<SignedTransaction>
|
||||
get() = mutex.content.updatesPublisher
|
||||
|
||||
init {
|
||||
logger.trace { "Initialising per file transaction storage on $storeDir" }
|
||||
Files.createDirectories(storeDir)
|
||||
mutex.locked {
|
||||
Files.list(storeDir)
|
||||
.filter { it.toString().toLowerCase().endsWith(fileExtension) }
|
||||
.map { Files.readAllBytes(it).deserialize<SignedTransaction>() }
|
||||
.forEach { transactionsMap[it.id] = it }
|
||||
}
|
||||
}
|
||||
|
||||
override fun addTransaction(transaction: SignedTransaction) {
|
||||
val transactionFile = storeDir.resolve("${transaction.id.toString().toLowerCase()}$fileExtension")
|
||||
transaction.serialize().writeToFile(transactionFile)
|
||||
mutex.locked {
|
||||
transactionsMap[transaction.id] = transaction
|
||||
notify(transaction)
|
||||
}
|
||||
logger.trace { "Stored $transaction to $transactionFile" }
|
||||
}
|
||||
|
||||
override fun getTransaction(id: SecureHash): SignedTransaction? = mutex.locked { transactionsMap[id] }
|
||||
|
||||
val transactions: Iterable<SignedTransaction> get() = mutex.locked { transactionsMap.values.toList() }
|
||||
|
||||
override fun track(): Pair<List<SignedTransaction>, Observable<SignedTransaction>> {
|
||||
return mutex.locked {
|
||||
Pair(transactionsMap.values.toList(), updates.bufferUntilSubscribed())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -240,7 +240,7 @@ class ProtocolStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
actionOnSuspend(ioRequest)
|
||||
} catch (t: Throwable) {
|
||||
// Do not throw exception again - Quasar completely bins it.
|
||||
logger.warn("Captured exception which was swallowed by Quasar", t)
|
||||
logger.warn("Captured exception which was swallowed by Quasar for $logic at ${fiber.stackTrace.toList().joinToString("\n")}", t)
|
||||
// TODO When error handling is introduced, look into whether we should be deleting the checkpoint and
|
||||
// completing the Future
|
||||
processException(t)
|
||||
|
@ -14,6 +14,7 @@ import java.security.PublicKey
|
||||
import java.sql.Connection
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.*
|
||||
|
||||
@ -147,6 +148,8 @@ fun Table.secureHash(name: String) = this.registerColumn<SecureHash>(name, Secur
|
||||
fun Table.party(nameColumnName: String, keyColumnName: String) = PartyColumns(this.varchar(nameColumnName, length = 255), this.publicKey(keyColumnName))
|
||||
fun Table.uuidString(name: String) = this.registerColumn<UUID>(name, UUIDStringColumnType)
|
||||
fun Table.localDate(name: String) = this.registerColumn<LocalDate>(name, LocalDateColumnType)
|
||||
fun Table.localDateTime(name: String) = this.registerColumn<LocalDateTime>(name, LocalDateTimeColumnType)
|
||||
fun Table.instant(name: String) = this.registerColumn<Instant>(name, InstantColumnType)
|
||||
fun Table.stateRef(txIdColumnName: String, indexColumnName: String) = StateRefColumns(this.secureHash(txIdColumnName), this.integer(indexColumnName))
|
||||
|
||||
/**
|
||||
@ -210,4 +213,68 @@ object LocalDateColumnType : ColumnType() {
|
||||
override fun notNullValueToDB(value: Any): Any = if (value is LocalDate) {
|
||||
java.sql.Date(value.atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli())
|
||||
} else value
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* [ColumnType] for marshalling to/from database on behalf of [java.time.LocalDateTime].
|
||||
*/
|
||||
object LocalDateTimeColumnType : ColumnType() {
|
||||
private val sqlType = DateColumnType(time = true).sqlType()
|
||||
override fun sqlType(): String = sqlType
|
||||
|
||||
override fun nonNullValueToString(value: Any): String {
|
||||
if (value is String) return value
|
||||
|
||||
val localDateTime = when (value) {
|
||||
is LocalDateTime -> value
|
||||
is java.sql.Date -> value.toLocalDate().atStartOfDay()
|
||||
is java.sql.Timestamp -> value.toLocalDateTime()
|
||||
else -> error("Unexpected value: $value")
|
||||
}
|
||||
return "'$localDateTime'"
|
||||
}
|
||||
|
||||
override fun valueFromDB(value: Any): Any = when (value) {
|
||||
is java.sql.Date -> value.toLocalDate().atStartOfDay()
|
||||
is java.sql.Timestamp -> value.toLocalDateTime()
|
||||
is Long -> LocalDateTime.from(Instant.ofEpochMilli(value))
|
||||
else -> value
|
||||
}
|
||||
|
||||
override fun notNullValueToDB(value: Any): Any = if (value is LocalDateTime) {
|
||||
java.sql.Timestamp(value.toInstant(ZoneOffset.UTC).toEpochMilli())
|
||||
} else value
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* [ColumnType] for marshalling to/from database on behalf of [java.time.Instant].
|
||||
*/
|
||||
object InstantColumnType : ColumnType() {
|
||||
private val sqlType = DateColumnType(time = true).sqlType()
|
||||
override fun sqlType(): String = sqlType
|
||||
|
||||
override fun nonNullValueToString(value: Any): String {
|
||||
if (value is String) return value
|
||||
|
||||
val localDateTime = when (value) {
|
||||
is Instant -> value
|
||||
is java.sql.Date -> value.toLocalDate().atStartOfDay().toInstant(ZoneOffset.UTC)
|
||||
is java.sql.Timestamp -> value.toLocalDateTime().toInstant(ZoneOffset.UTC)
|
||||
else -> error("Unexpected value: $value")
|
||||
}
|
||||
return "'$localDateTime'"
|
||||
}
|
||||
|
||||
override fun valueFromDB(value: Any): Any = when (value) {
|
||||
is java.sql.Date -> value.toLocalDate().atStartOfDay().toInstant(ZoneOffset.UTC)
|
||||
is java.sql.Timestamp -> value.toLocalDateTime().toInstant(ZoneOffset.UTC)
|
||||
is Long -> LocalDateTime.from(Instant.ofEpochMilli(value)).toInstant(ZoneOffset.UTC)
|
||||
else -> value
|
||||
}
|
||||
|
||||
override fun notNullValueToDB(value: Any): Any = if (value is Instant) {
|
||||
java.sql.Timestamp(value.toEpochMilli())
|
||||
} else value
|
||||
}
|
@ -11,7 +11,10 @@ import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import com.r3corda.core.contracts.BusinessCalendar
|
||||
import com.r3corda.core.crypto.*
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.services.IdentityService
|
||||
import com.r3corda.core.serialization.deserialize
|
||||
import com.r3corda.core.serialization.serialize
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
@ -54,6 +57,11 @@ object JsonSupport {
|
||||
cordaModule.addSerializer(PublicKeyTree::class.java, PublicKeyTreeSerializer)
|
||||
cordaModule.addDeserializer(PublicKeyTree::class.java, PublicKeyTreeDeserializer)
|
||||
|
||||
// For NodeInfo
|
||||
// TODO this tunnels the Kryo representation as a Base58 encoded string. Replace when RPC supports this.
|
||||
cordaModule.addSerializer(NodeInfo::class.java, NodeInfoSerializer)
|
||||
cordaModule.addDeserializer(NodeInfo::class.java, NodeInfoDeserializer)
|
||||
|
||||
mapper.registerModule(timeModule)
|
||||
mapper.registerModule(cordaModule)
|
||||
mapper.registerModule(KotlinModule())
|
||||
@ -102,6 +110,25 @@ object JsonSupport {
|
||||
}
|
||||
}
|
||||
|
||||
object NodeInfoSerializer : JsonSerializer<NodeInfo>() {
|
||||
override fun serialize(value: NodeInfo, gen: JsonGenerator, serializers: SerializerProvider) {
|
||||
gen.writeString(Base58.encode(value.serialize().bits))
|
||||
}
|
||||
}
|
||||
|
||||
object NodeInfoDeserializer : JsonDeserializer<NodeInfo>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext): NodeInfo {
|
||||
if (parser.currentToken == JsonToken.FIELD_NAME) {
|
||||
parser.nextToken()
|
||||
}
|
||||
try {
|
||||
return Base58.decode(parser.text).deserialize<NodeInfo>()
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException(parser, "Invalid NodeInfo ${parser.text}: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object SecureHashSerializer : JsonSerializer<SecureHash>() {
|
||||
override fun serialize(obj: SecureHash, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
generator.writeString(obj.toString())
|
||||
|
@ -1,9 +1,8 @@
|
||||
package com.r3corda.demos
|
||||
package com.r3corda.node.utilities
|
||||
|
||||
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
|
||||
|
||||
@ -11,7 +10,7 @@ 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 {
|
||||
class TestClock(private var delegateClock: Clock = Clock.systemUTC()) : MutableClock(), SerializeAsToken {
|
||||
|
||||
private val token = SingletonSerializationToken(this)
|
||||
|
||||
@ -41,4 +40,4 @@ class DemoClock(private var delegateClock: Clock = Clock.systemUTC()) : MutableC
|
||||
return delegateClock.zone
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
# Register a ServiceLoader service extending from com.r3corda.node.CordaPluginRegistry
|
||||
com.r3corda.node.services.clientapi.FixingSessionInitiation$Plugin
|
||||
com.r3corda.node.services.NotaryChange$Plugin
|
||||
com.r3corda.node.services.persistence.DataVending$Plugin
|
@ -13,4 +13,5 @@ dataSourceProperties = {
|
||||
devMode = true
|
||||
certificateSigningService = "https://cordaci-netperm.corda.r3cev.com"
|
||||
useHTTPS = false
|
||||
h2port = 0
|
||||
h2port = 0
|
||||
useTestClock = false
|
@ -155,16 +155,14 @@ class TwoPartyTradeProtocolTests {
|
||||
bobNode.pumpReceive()
|
||||
|
||||
// OK, now Bob has sent the partial transaction back to Alice and is waiting for Alice's signature.
|
||||
assertThat(bobNode.checkpointStorage.checkpoints()).hasSize(1)
|
||||
databaseTransaction(bobNode.database) {
|
||||
assertThat(bobNode.checkpointStorage.checkpoints()).hasSize(1)
|
||||
}
|
||||
|
||||
val storage = bobNode.storage.validatedTransactions
|
||||
val bobTransactionsBeforeCrash = if (storage is PerFileTransactionStorage) {
|
||||
storage.transactions
|
||||
} else if (storage is DBTransactionStorage) {
|
||||
databaseTransaction(bobNode.database) {
|
||||
storage.transactions
|
||||
}
|
||||
} else throw IllegalArgumentException("Unknown storage implementation")
|
||||
val bobTransactionsBeforeCrash = databaseTransaction(bobNode.database) {
|
||||
(storage as DBTransactionStorage).transactions
|
||||
}
|
||||
assertThat(bobTransactionsBeforeCrash).isNotEmpty()
|
||||
|
||||
// .. and let's imagine that Bob's computer has a power cut. He now has nothing now beyond what was on disk.
|
||||
@ -350,7 +348,11 @@ class TwoPartyTradeProtocolTests {
|
||||
net.runNetwork() // Clear network map registration messages
|
||||
|
||||
val aliceTxStream = aliceNode.storage.validatedTransactions.track().second
|
||||
val aliceTxMappings = aliceNode.storage.stateMachineRecordedTransactionMapping.track().second
|
||||
// TODO: Had to put this temp val here to avoid compiler crash. Put back inside [databaseTransaction] if the compiler stops crashing.
|
||||
val aliceMappingsStorage = aliceNode.storage.stateMachineRecordedTransactionMapping
|
||||
val aliceTxMappings = databaseTransaction(aliceNode.database) {
|
||||
aliceMappingsStorage.track().second
|
||||
}
|
||||
val aliceSmId = runBuyerAndSeller("alice's paper".outputStateAndRef()).sellerId
|
||||
|
||||
net.runNetwork()
|
||||
|
@ -1,22 +1,37 @@
|
||||
package com.r3corda.node.services
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.r3corda.core.contracts.ClientToServiceCommand
|
||||
import com.r3corda.core.contracts.ContractState
|
||||
import com.r3corda.core.contracts.StateAndRef
|
||||
import com.r3corda.core.crypto.generateKeyPair
|
||||
import com.r3corda.core.messaging.Message
|
||||
import com.r3corda.core.messaging.createMessage
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.services.DEFAULT_SESSION_ID
|
||||
import com.r3corda.core.node.services.NetworkMapCache
|
||||
import com.r3corda.core.node.services.StateMachineTransactionMapping
|
||||
import com.r3corda.core.node.services.Vault
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.utilities.LogHelper
|
||||
import com.r3corda.node.services.config.NodeConfiguration
|
||||
import com.r3corda.node.services.messaging.ArtemisMessagingServer
|
||||
import com.r3corda.node.services.messaging.NodeMessagingClient
|
||||
import com.r3corda.node.services.messaging.*
|
||||
import com.r3corda.node.services.network.InMemoryNetworkMapCache
|
||||
import com.r3corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import com.r3corda.node.utilities.AffinityExecutor
|
||||
import com.r3corda.node.utilities.configureDatabase
|
||||
import com.r3corda.node.utilities.databaseTransaction
|
||||
import com.r3corda.testing.freeLocalHostAndPort
|
||||
import com.r3corda.testing.node.makeTestDataSourceProperties
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import rx.Observable
|
||||
import java.io.Closeable
|
||||
import java.net.ServerSocket
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
@ -33,12 +48,44 @@ class ArtemisMessagingTests {
|
||||
val identity = generateKeyPair()
|
||||
|
||||
lateinit var config: NodeConfiguration
|
||||
lateinit var dataSource: Closeable
|
||||
lateinit var database: Database
|
||||
|
||||
var messagingClient: NodeMessagingClient? = null
|
||||
var messagingServer: ArtemisMessagingServer? = null
|
||||
|
||||
val networkMapCache = InMemoryNetworkMapCache()
|
||||
|
||||
val rpcOps = object : CordaRPCOps {
|
||||
override val protocolVersion: Int
|
||||
get() = throw UnsupportedOperationException()
|
||||
|
||||
override fun stateMachinesAndUpdates(): Pair<List<StateMachineInfo>, Observable<StateMachineUpdate>> {
|
||||
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun vaultAndUpdates(): Pair<List<StateAndRef<ContractState>>, Observable<Vault.Update>> {
|
||||
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun verifiedTransactions(): Pair<List<SignedTransaction>, Observable<SignedTransaction>> {
|
||||
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun stateMachineRecordedTransactionMapping(): Pair<List<StateMachineTransactionMapping>, Observable<StateMachineTransactionMapping>> {
|
||||
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>> {
|
||||
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun executeCommand(command: ClientToServiceCommand): TransactionBuildResult {
|
||||
throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
// TODO: create a base class that provides a default implementation
|
||||
@ -52,12 +99,18 @@ class ArtemisMessagingTests {
|
||||
override val keyStorePassword: String = "testpass"
|
||||
override val trustStorePassword: String = "trustpass"
|
||||
}
|
||||
LogHelper.setLevel(PersistentUniquenessProvider::class)
|
||||
val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties())
|
||||
dataSource = dataSourceAndDatabase.first
|
||||
database = dataSourceAndDatabase.second
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
messagingClient?.stop()
|
||||
messagingServer?.stop()
|
||||
dataSource.close()
|
||||
LogHelper.reset(PersistentUniquenessProvider::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -73,7 +126,7 @@ class ArtemisMessagingTests {
|
||||
val remoteServerAddress = freeLocalHostAndPort()
|
||||
|
||||
createMessagingServer(remoteServerAddress).start()
|
||||
createMessagingClient(server = remoteServerAddress).start()
|
||||
createMessagingClient(server = remoteServerAddress).start(rpcOps)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -84,14 +137,14 @@ class ArtemisMessagingTests {
|
||||
createMessagingServer(serverAddress).start()
|
||||
|
||||
messagingClient = createMessagingClient(server = invalidServerAddress)
|
||||
assertThatThrownBy { messagingClient!!.start() }
|
||||
assertThatThrownBy { messagingClient!!.start(rpcOps) }
|
||||
messagingClient = null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `client should connect to local server`() {
|
||||
createMessagingServer().start()
|
||||
createMessagingClient().start()
|
||||
createMessagingClient().start(rpcOps)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -101,7 +154,7 @@ class ArtemisMessagingTests {
|
||||
createMessagingServer().start()
|
||||
|
||||
val messagingClient = createMessagingClient()
|
||||
messagingClient.start()
|
||||
messagingClient.start(rpcOps)
|
||||
thread { messagingClient.run() }
|
||||
|
||||
messagingClient.addMessageHandler(topic) { message, r ->
|
||||
@ -117,9 +170,11 @@ class ArtemisMessagingTests {
|
||||
}
|
||||
|
||||
private fun createMessagingClient(server: HostAndPort = hostAndPort): NodeMessagingClient {
|
||||
return NodeMessagingClient(config, server, identity.public, AffinityExecutor.ServiceAffinityExecutor("ArtemisMessagingTests", 1), false).apply {
|
||||
configureWithDevSSLCertificate()
|
||||
messagingClient = this
|
||||
return databaseTransaction(database) {
|
||||
NodeMessagingClient(config, server, identity.public, AffinityExecutor.ServiceAffinityExecutor("ArtemisMessagingTests", 1), database).apply {
|
||||
configureWithDevSSLCertificate()
|
||||
messagingClient = this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ import com.r3corda.core.serialization.SingletonSerializeAsToken
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.utilities.DUMMY_NOTARY
|
||||
import com.r3corda.node.services.events.NodeSchedulerService
|
||||
import com.r3corda.node.services.persistence.PerFileCheckpointStorage
|
||||
import com.r3corda.node.services.persistence.DBCheckpointStorage
|
||||
import com.r3corda.node.services.statemachine.StateMachineManager
|
||||
import com.r3corda.node.services.vault.NodeVaultService
|
||||
import com.r3corda.node.utilities.AddOrRemove
|
||||
@ -72,6 +72,14 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
val testReference: NodeSchedulerServiceTest
|
||||
}
|
||||
|
||||
init {
|
||||
val kms = MockKeyManagementService(ALICE_KEY)
|
||||
val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging(false, InMemoryMessagingNetwork.Handle(0, "None"), AffinityExecutor.ServiceAffinityExecutor("test", 1), persistenceTx = { it() })
|
||||
services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference {
|
||||
override val testReference = this@NodeSchedulerServiceTest
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
countDown = CountDownLatch(1)
|
||||
@ -249,7 +257,9 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
scheduleTX(time, 3)
|
||||
|
||||
backgroundExecutor.execute { schedulerGatedExecutor.waitAndRun() }
|
||||
scheduler.unscheduleStateActivity(scheduledRef1!!.ref)
|
||||
databaseTransaction(database) {
|
||||
scheduler.unscheduleStateActivity(scheduledRef1!!.ref)
|
||||
}
|
||||
testClock.advanceBy(1.days)
|
||||
countDown.await()
|
||||
assertThat(calls).isEqualTo(3)
|
||||
@ -265,7 +275,9 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
backgroundExecutor.execute { schedulerGatedExecutor.waitAndRun() }
|
||||
assertThat(calls).isEqualTo(0)
|
||||
|
||||
scheduler.unscheduleStateActivity(scheduledRef1!!.ref)
|
||||
databaseTransaction(database) {
|
||||
scheduler.unscheduleStateActivity(scheduledRef1!!.ref)
|
||||
}
|
||||
testClock.advanceBy(1.days)
|
||||
assertThat(calls).isEqualTo(0)
|
||||
backgroundExecutor.shutdown()
|
||||
@ -287,10 +299,9 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
}.toSignedTransaction()
|
||||
val txHash = usefulTX.id
|
||||
|
||||
services.recordTransactions(usefulTX)
|
||||
scheduledRef = ScheduledStateRef(StateRef(txHash, 0), state.instant)
|
||||
scheduler.scheduleStateActivity(scheduledRef!!)
|
||||
}
|
||||
services.recordTransactions(usefulTX)
|
||||
scheduledRef = ScheduledStateRef(StateRef(txHash, 0), state.instant)
|
||||
scheduler.scheduleStateActivity(scheduledRef!!)
|
||||
}
|
||||
return scheduledRef
|
||||
}
|
||||
|
@ -1,99 +0,0 @@
|
||||
package com.r3corda.node.services.persistence
|
||||
|
||||
import com.google.common.jimfs.Configuration.unix
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import com.google.common.primitives.Ints
|
||||
import com.r3corda.core.serialization.SerializedBytes
|
||||
import com.r3corda.node.services.api.Checkpoint
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
class PerFileCheckpointStorageTests {
|
||||
|
||||
val fileSystem: FileSystem = Jimfs.newFileSystem(unix())
|
||||
val storeDir: Path = fileSystem.getPath("store")
|
||||
lateinit var checkpointStorage: PerFileCheckpointStorage
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
newCheckpointStorage()
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
fileSystem.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `add new checkpoint`() {
|
||||
val checkpoint = newCheckpoint()
|
||||
checkpointStorage.addCheckpoint(checkpoint)
|
||||
assertThat(checkpointStorage.checkpoints()).containsExactly(checkpoint)
|
||||
newCheckpointStorage()
|
||||
assertThat(checkpointStorage.checkpoints()).containsExactly(checkpoint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remove checkpoint`() {
|
||||
val checkpoint = newCheckpoint()
|
||||
checkpointStorage.addCheckpoint(checkpoint)
|
||||
checkpointStorage.removeCheckpoint(checkpoint)
|
||||
assertThat(checkpointStorage.checkpoints()).isEmpty()
|
||||
newCheckpointStorage()
|
||||
assertThat(checkpointStorage.checkpoints()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remove unknown checkpoint`() {
|
||||
val checkpoint = newCheckpoint()
|
||||
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
||||
checkpointStorage.removeCheckpoint(checkpoint)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `add two checkpoints then remove first one`() {
|
||||
val firstCheckpoint = newCheckpoint()
|
||||
checkpointStorage.addCheckpoint(firstCheckpoint)
|
||||
val secondCheckpoint = newCheckpoint()
|
||||
checkpointStorage.addCheckpoint(secondCheckpoint)
|
||||
checkpointStorage.removeCheckpoint(firstCheckpoint)
|
||||
assertThat(checkpointStorage.checkpoints()).containsExactly(secondCheckpoint)
|
||||
newCheckpointStorage()
|
||||
assertThat(checkpointStorage.checkpoints()).containsExactly(secondCheckpoint)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `add checkpoint and then remove after 'restart'`() {
|
||||
val originalCheckpoint = newCheckpoint()
|
||||
checkpointStorage.addCheckpoint(originalCheckpoint)
|
||||
newCheckpointStorage()
|
||||
val reconstructedCheckpoint = checkpointStorage.checkpoints().single()
|
||||
assertThat(reconstructedCheckpoint).isEqualTo(originalCheckpoint).isNotSameAs(originalCheckpoint)
|
||||
checkpointStorage.removeCheckpoint(reconstructedCheckpoint)
|
||||
assertThat(checkpointStorage.checkpoints()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non-checkpoint files are ignored`() {
|
||||
val checkpoint = newCheckpoint()
|
||||
checkpointStorage.addCheckpoint(checkpoint)
|
||||
Files.write(storeDir.resolve("random-non-checkpoint-file"), "this is not a checkpoint!!".toByteArray())
|
||||
newCheckpointStorage()
|
||||
assertThat(checkpointStorage.checkpoints()).containsExactly(checkpoint)
|
||||
}
|
||||
|
||||
private fun newCheckpointStorage() {
|
||||
checkpointStorage = PerFileCheckpointStorage(storeDir)
|
||||
}
|
||||
|
||||
private var checkpointCount = 1
|
||||
private fun newCheckpoint() = Checkpoint(SerializedBytes(Ints.toByteArray(checkpointCount++)))
|
||||
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
package com.r3corda.node.services.persistence
|
||||
|
||||
import com.google.common.jimfs.Configuration.unix
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import com.google.common.primitives.Ints
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import com.r3corda.core.crypto.DigitalSignature
|
||||
import com.r3corda.core.crypto.NullPublicKey
|
||||
import com.r3corda.core.serialization.SerializedBytes
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class PerFileTransactionStorageTests {
|
||||
|
||||
val fileSystem: FileSystem = Jimfs.newFileSystem(unix())
|
||||
val storeDir: Path = fileSystem.getPath("store")
|
||||
lateinit var transactionStorage: PerFileTransactionStorage
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
newTransactionStorage()
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
fileSystem.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty store`() {
|
||||
assertThat(transactionStorage.getTransaction(newTransaction().id)).isNull()
|
||||
assertThat(transactionStorage.transactions).isEmpty()
|
||||
newTransactionStorage()
|
||||
assertThat(transactionStorage.transactions).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `one transaction`() {
|
||||
val transaction = newTransaction()
|
||||
transactionStorage.addTransaction(transaction)
|
||||
assertTransactionIsRetrievable(transaction)
|
||||
assertThat(transactionStorage.transactions).containsExactly(transaction)
|
||||
newTransactionStorage()
|
||||
assertTransactionIsRetrievable(transaction)
|
||||
assertThat(transactionStorage.transactions).containsExactly(transaction)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `two transactions across restart`() {
|
||||
val firstTransaction = newTransaction()
|
||||
val secondTransaction = newTransaction()
|
||||
transactionStorage.addTransaction(firstTransaction)
|
||||
newTransactionStorage()
|
||||
transactionStorage.addTransaction(secondTransaction)
|
||||
assertTransactionIsRetrievable(firstTransaction)
|
||||
assertTransactionIsRetrievable(secondTransaction)
|
||||
assertThat(transactionStorage.transactions).containsOnly(firstTransaction, secondTransaction)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non-transaction files are ignored`() {
|
||||
val transactions = newTransaction()
|
||||
transactionStorage.addTransaction(transactions)
|
||||
Files.write(storeDir.resolve("random-non-tx-file"), "this is not a transaction!!".toByteArray())
|
||||
newTransactionStorage()
|
||||
assertThat(transactionStorage.transactions).containsExactly(transactions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updates are fired`() {
|
||||
val future = SettableFuture.create<SignedTransaction>()
|
||||
transactionStorage.updates.subscribe { tx -> future.set(tx) }
|
||||
val expected = newTransaction()
|
||||
transactionStorage.addTransaction(expected)
|
||||
val actual = future.get(1, TimeUnit.SECONDS)
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
private fun newTransactionStorage() {
|
||||
transactionStorage = PerFileTransactionStorage(storeDir)
|
||||
}
|
||||
|
||||
private fun assertTransactionIsRetrievable(transaction: SignedTransaction) {
|
||||
assertThat(transactionStorage.getTransaction(transaction.id)).isEqualTo(transaction)
|
||||
}
|
||||
|
||||
private var txCount = 0
|
||||
private fun newTransaction() = SignedTransaction(
|
||||
SerializedBytes(Ints.toByteArray(++txCount)),
|
||||
listOf(DigitalSignature.WithKey(NullPublicKey, ByteArray(1))))
|
||||
|
||||
}
|
@ -10,6 +10,7 @@ import com.r3corda.core.random63BitValue
|
||||
import com.r3corda.core.serialization.deserialize
|
||||
import com.r3corda.node.services.persistence.checkpoints
|
||||
import com.r3corda.node.services.statemachine.StateMachineManager.*
|
||||
import com.r3corda.node.utilities.databaseTransaction
|
||||
import com.r3corda.testing.initiateSingleShotProtocol
|
||||
import com.r3corda.testing.node.InMemoryMessagingNetwork
|
||||
import com.r3corda.testing.node.InMemoryMessagingNetwork.MessageTransfer
|
||||
@ -73,6 +74,7 @@ class StateMachineManagerTests {
|
||||
|
||||
// We push through just enough messages to get only the payload sent
|
||||
node2.pumpReceive()
|
||||
node2.disableDBCloseOnStop()
|
||||
node2.stop()
|
||||
net.runNetwork()
|
||||
val restoredProtocol = node2.restartAndGetRestoredProtocol<ReceiveThenSuspendProtocol>(node1)
|
||||
@ -95,6 +97,7 @@ class StateMachineManagerTests {
|
||||
val protocol = NoOpProtocol()
|
||||
node3.smm.add(protocol)
|
||||
assertEquals(false, protocol.protocolStarted) // Not started yet as no network activity has been allowed yet
|
||||
node3.disableDBCloseOnStop()
|
||||
node3.stop()
|
||||
|
||||
node3 = net.createNode(node1.info.address, forcedID = node3.id)
|
||||
@ -103,6 +106,7 @@ class StateMachineManagerTests {
|
||||
net.runNetwork() // Allow network map messages to flow
|
||||
node3.smm.executor.flush()
|
||||
assertEquals(true, restoredProtocol.protocolStarted) // Now we should have run the protocol and hopefully cleared the init checkpoint
|
||||
node3.disableDBCloseOnStop()
|
||||
node3.stop()
|
||||
|
||||
// Now it is completed the protocol should leave no Checkpoint.
|
||||
@ -119,6 +123,7 @@ class StateMachineManagerTests {
|
||||
node2.smm.add(ReceiveThenSuspendProtocol(node1.info.legalIdentity)) // Prepare checkpointed receive protocol
|
||||
// Make sure the add() has finished initial processing.
|
||||
node2.smm.executor.flush()
|
||||
node2.disableDBCloseOnStop()
|
||||
node2.stop() // kill receiver
|
||||
val restoredProtocol = node2.restartAndGetRestoredProtocol<ReceiveThenSuspendProtocol>(node1)
|
||||
assertThat(restoredProtocol.receivedPayloads[0]).isEqualTo(payload)
|
||||
@ -138,16 +143,22 @@ class StateMachineManagerTests {
|
||||
|
||||
// Kick off first send and receive
|
||||
node2.smm.add(PingPongProtocol(node3.info.legalIdentity, payload))
|
||||
assertEquals(1, node2.checkpointStorage.checkpoints().size)
|
||||
databaseTransaction(node2.database) {
|
||||
assertEquals(1, node2.checkpointStorage.checkpoints().size)
|
||||
}
|
||||
// Make sure the add() has finished initial processing.
|
||||
node2.smm.executor.flush()
|
||||
node2.disableDBCloseOnStop()
|
||||
// Restart node and thus reload the checkpoint and resend the message with same UUID
|
||||
node2.stop()
|
||||
databaseTransaction(node2.database) {
|
||||
assertEquals(1, node2.checkpointStorage.checkpoints().size) // confirm checkpoint
|
||||
}
|
||||
val node2b = net.createNode(node1.info.address, node2.id, advertisedServices = *node2.advertisedServices.toTypedArray())
|
||||
node2.manuallyCloseDB()
|
||||
val (firstAgain, fut1) = node2b.getSingleProtocol<PingPongProtocol>()
|
||||
// Run the network which will also fire up the second protocol. First message should get deduped. So message data stays in sync.
|
||||
net.runNetwork()
|
||||
assertEquals(1, node2.checkpointStorage.checkpoints().size)
|
||||
node2b.smm.executor.flush()
|
||||
fut1.get()
|
||||
|
||||
@ -156,8 +167,12 @@ class StateMachineManagerTests {
|
||||
assertEquals(4, receivedCount, "Protocol should have exchanged 4 unique messages")// Two messages each way
|
||||
// can't give a precise value as every addMessageHandler re-runs the undelivered messages
|
||||
assertTrue(sentCount > receivedCount, "Node restart should have retransmitted messages")
|
||||
assertEquals(0, node2b.checkpointStorage.checkpoints().size, "Checkpoints left after restored protocol should have ended")
|
||||
assertEquals(0, node3.checkpointStorage.checkpoints().size, "Checkpoints left after restored protocol should have ended")
|
||||
databaseTransaction(node2b.database) {
|
||||
assertEquals(0, node2b.checkpointStorage.checkpoints().size, "Checkpoints left after restored protocol should have ended")
|
||||
}
|
||||
databaseTransaction(node3.database) {
|
||||
assertEquals(0, node3.checkpointStorage.checkpoints().size, "Checkpoints left after restored protocol should have ended")
|
||||
}
|
||||
assertEquals(payload2, firstAgain.receivedPayload, "Received payload does not match the first value on Node 3")
|
||||
assertEquals(payload2 + 1, firstAgain.receivedPayload2, "Received payload does not match the expected second value on Node 3")
|
||||
assertEquals(payload, secondProtocol.get().receivedPayload, "Received payload does not match the (restarted) first value on Node 2")
|
||||
@ -253,8 +268,10 @@ class StateMachineManagerTests {
|
||||
|
||||
private inline fun <reified P : ProtocolLogic<*>> MockNode.restartAndGetRestoredProtocol(
|
||||
networkMapNode: MockNode? = null): P {
|
||||
disableDBCloseOnStop() //Handover DB to new node copy
|
||||
stop()
|
||||
val newNode = mockNet.createNode(networkMapNode?.info?.address, id, advertisedServices = *advertisedServices.toTypedArray())
|
||||
manuallyCloseDB()
|
||||
mockNet.runNetwork() // allow NetworkMapService messages to stabilise and thus start the state machine
|
||||
return newNode.getSingleProtocol<P>().first
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ include 'node'
|
||||
include 'client'
|
||||
include 'experimental'
|
||||
include 'test-utils'
|
||||
include 'network-simulator'
|
||||
include 'explorer'
|
||||
include 'gradle-plugins:quasar-utils'
|
||||
include 'gradle-plugins:publish-utils'
|
||||
|
@ -1,97 +0,0 @@
|
||||
package com.r3corda.core.testing
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.r3corda.testing.*
|
||||
import kotlin.test.assertEquals
|
||||
import org.junit.Test
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
class IRSDemoTest: IntegrationTestCategory {
|
||||
@Test fun `runs IRS demo`() {
|
||||
val hostAndPorts = getFreeLocalPorts("localhost", 4)
|
||||
|
||||
val nodeAddrA = hostAndPorts[0]
|
||||
val apiAddrA = hostAndPorts[1]
|
||||
val apiAddrB = hostAndPorts[2]
|
||||
|
||||
val baseDirectory = Paths.get("./build/integration-test/${TestTimestamp.timestamp}/irs-demo")
|
||||
var procA: Process? = null
|
||||
var procB: Process? = null
|
||||
try {
|
||||
setupNode(baseDirectory, "NodeA")
|
||||
setupNode(baseDirectory, "NodeB")
|
||||
procA = startNode(
|
||||
baseDirectory = baseDirectory,
|
||||
nodeType = "NodeA",
|
||||
nodeAddr = nodeAddrA,
|
||||
networkMapAddr = apiAddrA,
|
||||
apiAddr = apiAddrA
|
||||
)
|
||||
procB = startNode(
|
||||
baseDirectory = baseDirectory,
|
||||
nodeType = "NodeB",
|
||||
nodeAddr = hostAndPorts[3],
|
||||
networkMapAddr = nodeAddrA,
|
||||
apiAddr = apiAddrB
|
||||
)
|
||||
runTrade(apiAddrA)
|
||||
runDateChange(apiAddrA)
|
||||
} finally {
|
||||
stopNode(procA)
|
||||
stopNode(procB)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupNode(baseDirectory: Path, nodeType: String) {
|
||||
println("Running setup for $nodeType")
|
||||
val args = listOf("--role", "Setup" + nodeType, "--base-directory", baseDirectory.toString())
|
||||
val proc = spawn("com.r3corda.demos.IRSDemoKt", args, "IRSDemoSetup$nodeType")
|
||||
assertExitOrKill(proc)
|
||||
assertEquals(proc.exitValue(), 0)
|
||||
}
|
||||
|
||||
private fun startNode(baseDirectory: Path,
|
||||
nodeType: String,
|
||||
nodeAddr: HostAndPort,
|
||||
networkMapAddr: HostAndPort,
|
||||
apiAddr: HostAndPort): Process {
|
||||
println("Running node $nodeType")
|
||||
println("Node addr: $nodeAddr")
|
||||
println("Network map addr: $networkMapAddr")
|
||||
println("API addr: $apiAddr")
|
||||
val args = listOf(
|
||||
"--role", nodeType,
|
||||
"--base-directory", baseDirectory.toString(),
|
||||
"--network-address", nodeAddr.toString(),
|
||||
"--network-map-address", networkMapAddr.toString(),
|
||||
"--api-address", apiAddr.toString(),
|
||||
"--h2-port", "0")
|
||||
val proc = spawn("com.r3corda.demos.IRSDemoKt", args, "IRSDemo$nodeType")
|
||||
NodeApi.ensureNodeStartsOrKill(proc, apiAddr)
|
||||
return proc
|
||||
}
|
||||
|
||||
private fun runTrade(nodeAddr: HostAndPort) {
|
||||
println("Running trade")
|
||||
val args = listOf("--role", "Trade", "trade1", "--api-address", nodeAddr.toString())
|
||||
val proc = spawn("com.r3corda.demos.IRSDemoKt", args, "IRSDemoTrade")
|
||||
assertExitOrKill(proc)
|
||||
assertEquals(proc.exitValue(), 0)
|
||||
}
|
||||
|
||||
private fun runDateChange(nodeAddr: HostAndPort) {
|
||||
println("Running date change")
|
||||
val args = listOf("--role", "Date", "2017-01-02", "--api-address", nodeAddr.toString())
|
||||
val proc = spawn("com.r3corda.demos.IRSDemoKt", args, "IRSDemoDate")
|
||||
assertExitOrKill(proc)
|
||||
assertEquals(proc.exitValue(), 0)
|
||||
}
|
||||
|
||||
private fun stopNode(nodeProc: Process?) {
|
||||
if (nodeProc != null) {
|
||||
println("Stopping node")
|
||||
assertAliveAndKill(nodeProc)
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user