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:
Jose Coll 2016-10-27 12:56:08 +01:00
commit 7d080c39df
139 changed files with 1352 additions and 7864 deletions

15
.idea/modules.xml generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,8 @@ import javafx.beans.property.SimpleObjectProperty
enum class SelectedView {
Home,
Cash,
Transaction
Transaction,
NewTransaction
}
class TopLevelModel {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,4 +13,5 @@ dataSourceProperties = {
devMode = true
certificateSigningService = "https://cordaci-netperm.corda.r3cev.com"
useHTTPS = false
h2port = 0
h2port = 0
useTestClock = false

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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