diff --git a/.idea/runConfigurations/Attachment_Demo__Run_Recipient.xml b/.idea/runConfigurations/Attachment_Demo__Run_Recipient.xml
index c67e0a97b0..7ea10c3f86 100644
--- a/.idea/runConfigurations/Attachment_Demo__Run_Recipient.xml
+++ b/.idea/runConfigurations/Attachment_Demo__Run_Recipient.xml
@@ -3,7 +3,7 @@
-
+
@@ -12,4 +12,4 @@
-
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Attachment_Demo__Run_Sender.xml b/.idea/runConfigurations/Attachment_Demo__Run_Sender.xml
index 38f58ca2ff..db6567b2e1 100644
--- a/.idea/runConfigurations/Attachment_Demo__Run_Sender.xml
+++ b/.idea/runConfigurations/Attachment_Demo__Run_Sender.xml
@@ -3,7 +3,7 @@
-
+
@@ -12,4 +12,4 @@
-
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Explorer___demo_nodes.xml b/.idea/runConfigurations/Explorer___demo_nodes.xml
new file mode 100644
index 0000000000..42dcfcb487
--- /dev/null
+++ b/.idea/runConfigurations/Explorer___demo_nodes.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Explorer___demo_nodes__simulation_.xml b/.idea/runConfigurations/Explorer___demo_nodes__simulation_.xml
new file mode 100644
index 0000000000..6671745713
--- /dev/null
+++ b/.idea/runConfigurations/Explorer___demo_nodes__simulation_.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Notary_Demo__Run_Nodes.xml b/.idea/runConfigurations/Notary_Demo__Run_Nodes.xml
deleted file mode 100644
index 6dc64077b3..0000000000
--- a/.idea/runConfigurations/Notary_Demo__Run_Nodes.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/Notary_Demo__Run_Notarisation.xml b/.idea/runConfigurations/Notary_Demo__Run_Notarisation.xml
deleted file mode 100644
index 6971e14cb7..0000000000
--- a/.idea/runConfigurations/Notary_Demo__Run_Notarisation.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations/Raft_Notary_Demo__Run_Nodes.xml b/.idea/runConfigurations/Raft_Notary_Demo__Run_Nodes.xml
new file mode 100644
index 0000000000..a94708b378
--- /dev/null
+++ b/.idea/runConfigurations/Raft_Notary_Demo__Run_Nodes.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Raft_Notary_Demo__Run_Notarisation.xml b/.idea/runConfigurations/Raft_Notary_Demo__Run_Notarisation.xml
new file mode 100644
index 0000000000..0cf5567128
--- /dev/null
+++ b/.idea/runConfigurations/Raft_Notary_Demo__Run_Notarisation.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Trader_Demo__Run_Buyer.xml b/.idea/runConfigurations/Trader_Demo__Run_Buyer.xml
index 166781bed1..21a140d525 100644
--- a/.idea/runConfigurations/Trader_Demo__Run_Buyer.xml
+++ b/.idea/runConfigurations/Trader_Demo__Run_Buyer.xml
@@ -3,7 +3,7 @@
-
+
@@ -12,4 +12,4 @@
-
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Trader_Demo__Run_Seller.xml b/.idea/runConfigurations/Trader_Demo__Run_Seller.xml
index 3412483e66..530b17f0db 100644
--- a/.idea/runConfigurations/Trader_Demo__Run_Seller.xml
+++ b/.idea/runConfigurations/Trader_Demo__Run_Seller.xml
@@ -3,7 +3,7 @@
-
+
@@ -12,4 +12,4 @@
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 08d4770edf..128f04d9ff 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,13 @@ Read our full and planned feature list [here](https://docs.corda.net/inthebox.ht
Firstly, read the [Getting started](https://docs.corda.net/getting-set-up.html) documentation.
-Watching the following webinars will give you a great introduction to Corda:
+Next, use the following guides to set up your dev environment:
+
+* If you are on **Windows** [use this getting started guide](https://www.corda.net/wp-content/uploads/2017/01/Corda-Windows-Quick-start-guide-1.pdf) which also explains through how to run the sample apps.
+
+* Alternatively if you are on **Mac/Linux**, [watch this brief Webinar](https://vimeo.com/200167665) which walks through getting Corda, installing it, building it, running nodes and opening projects in IntelliJ.
+
+After the above, watching the following webinars will give you a great introduction to Corda:
### Webinar 1 – [Introduction to Corda](https://vimeo.com/192757743/c2ec39c1e1)
@@ -72,3 +78,11 @@ Please read [here](./CONTRIBUTING.md).
## License
[Apache 2.0](./LICENCE)
+
+## Acknowledgements
+
+![YourKit](https://www.yourkit.com/images/yklogo.png)
+
+YourKit supports open source projects with its full-featured Java Profiler.
+
+YourKit, LLC is the creator of [YourKit Java Profiler](https://www.yourkit.com/java/profiler/) and [YourKit .NET Profiler](https://www.yourkit.com/.net/profiler/), innovative and intelligent tools for profiling Java and .NET applications.
diff --git a/build.gradle b/build.gradle
index b619f77eee..8eaf09b7ad 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,19 +4,32 @@ buildscript {
file("publish.properties").withInputStream { props.load(it) }
// Our version: bump this on release.
- ext.corda_version = "0.7-SNAPSHOT"
+ ext.corda_version = "0.8-SNAPSHOT"
ext.gradle_plugins_version = props.getProperty("gradlePluginsVersion")
+
+ // Dependency versions. Can run 'gradle dependencyUpdates' to find new versions of things.
+ //
+ // TODO: Sort this alphabetically.
ext.kotlin_version = '1.0.5-2'
- ext.quasar_version = '0.7.6'
+ ext.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved.
ext.asm_version = '0.5.3'
- ext.artemis_version = '1.4.0'
- ext.jackson_version = '2.8.0.rc2'
+ ext.artemis_version = '1.5.1'
+ ext.jackson_version = '2.8.5'
ext.jetty_version = '9.3.9.v20160517'
- ext.jersey_version = '2.23.1'
- ext.jolokia_version = '2.0.0-M1'
- ext.assertj_version = '3.5.1'
- ext.log4j_version = '2.6.2'
- ext.bouncycastle_version = '1.54'
+ ext.jersey_version = '2.25'
+ ext.jolokia_version = '2.0.0-M3'
+ ext.assertj_version = '3.6.1'
+ ext.log4j_version = '2.7'
+ ext.bouncycastle_version = '1.56'
+ ext.guava_version = '19.0'
+ ext.quickcheck_version = '0.7'
+ ext.okhttp_version = '3.5.0'
+ ext.typesafe_config_version = '1.3.1'
+ ext.junit_version = '4.12'
+ ext.jopt_simple_version = '5.0.2'
+ ext.jansi_version = '1.14'
+ ext.hibernate_version = '5.2.6.Final'
+ ext.dokka_version = '0.9.13'
repositories {
mavenLocal()
@@ -33,9 +46,8 @@ buildscript {
classpath "net.corda.plugins:publish-utils:$gradle_plugins_version"
classpath "net.corda.plugins:quasar-utils:$gradle_plugins_version"
classpath "net.corda.plugins:cordformation:$gradle_plugins_version"
-
- // Can run 'gradle dependencyUpdates' to find new versions of things.
- classpath 'com.github.ben-manes:gradle-versions-plugin:0.12.0'
+ classpath 'com.github.ben-manes:gradle-versions-plugin:0.13.0'
+ classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
}
}
@@ -51,6 +63,7 @@ apply plugin: 'com.github.ben-manes.versions'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'net.corda.plugins.cordformation'
+apply plugin: 'org.jetbrains.dokka'
// We need the following three lines even though they're inside an allprojects {} block below because otherwise
// IntelliJ gets confused when importing the project and ends up erasing and recreating the .idea directory, along
@@ -95,7 +108,7 @@ repositories {
// Required for building out the fat JAR.
dependencies {
compile project(':node')
- compile "com.google.guava:guava:19.0"
+ compile "com.google.guava:guava:$guava_version"
runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts')
}
@@ -129,7 +142,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
networkMap "Controller"
node {
name "Controller"
- dirName "controller"
nearestCity "London"
advertisedServices = ["corda.notary.validating"]
artemisPort 10002
@@ -138,7 +150,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
}
node {
name "Bank A"
- dirName "nodea"
nearestCity "London"
advertisedServices = []
artemisPort 10004
@@ -147,7 +158,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
}
node {
name "Bank B"
- dirName "nodeb"
nearestCity "New York"
advertisedServices = []
artemisPort 10006
@@ -177,4 +187,23 @@ bintrayConfig {
name = 'R3'
email = 'dev@corda.net'
}
-}
\ No newline at end of file
+}
+
+// API docs
+
+dokka {
+ moduleName = 'corda'
+ outputDirectory = 'docs/build/html/api/kotlin'
+ processConfigurations = ['compile']
+ sourceDirs = files('core/src/main/kotlin', 'client/src/main/kotlin', 'node/src/main/kotlin', 'finance/src/main/kotlin')
+}
+
+task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
+ moduleName = 'corda'
+ outputFormat = "javadoc"
+ outputDirectory = 'docs/build/html/api/javadoc'
+ processConfigurations = ['compile']
+ sourceDirs = files('core/src/main/kotlin', 'client/src/main/kotlin', 'node/src/main/kotlin', 'finance/src/main/kotlin')
+}
+
+task apidocs(dependsOn: ['dokka', 'dokkaJavadoc'])
\ No newline at end of file
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 225d2b829e..fb34bb0911 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -5,5 +5,6 @@ repositories {
}
dependencies {
- compile "com.google.guava:guava:19.0"
+ // Cannot use ext.guava_version here :(
+ compile "com.google.guava:guava:20.0"
}
diff --git a/client/build.gradle b/client/build.gradle
index 1748bef18d..4878aede15 100644
--- a/client/build.gradle
+++ b/client/build.gradle
@@ -51,7 +51,7 @@ dependencies {
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
compile "org.apache.logging.log4j:log4j-core:${log4j_version}"
- compile "com.google.guava:guava:19.0"
+ compile "com.google.guava:guava:$guava_version"
// ReactFX: Functional reactive UI programming.
compile 'org.reactfx:reactfx:2.0-M5'
@@ -61,16 +61,16 @@ dependencies {
compile "org.apache.activemq:artemis-core-client:${artemis_version}"
// Unit testing helpers.
- testCompile 'junit:junit:4.12'
+ testCompile "junit:junit:$junit_version"
testCompile "org.assertj:assertj-core:${assertj_version}"
testCompile project(':test-utils')
// Integration test helpers
- integrationTestCompile 'junit:junit:4.12'
+ integrationTestCompile "junit:junit:$junit_version"
}
task integrationTest(type: Test) {
testClassesDir = sourceSets.integrationTest.output.classesDir
classpath = sourceSets.integrationTest.runtimeClasspath
-}
\ No newline at end of file
+}
diff --git a/client/src/integration-test/kotlin/net/corda/client/CordaRPCClientTest.kt b/client/src/integration-test/kotlin/net/corda/client/CordaRPCClientTest.kt
index fd5f01ebc4..db5092e396 100644
--- a/client/src/integration-test/kotlin/net/corda/client/CordaRPCClientTest.kt
+++ b/client/src/integration-test/kotlin/net/corda/client/CordaRPCClientTest.kt
@@ -8,48 +8,26 @@ import net.corda.core.random63BitValue
import net.corda.core.serialization.OpaqueBytes
import net.corda.flows.CashCommand
import net.corda.flows.CashFlow
-import net.corda.node.driver.NodeInfoAndConfig
+import net.corda.node.driver.DriverBasedTest
+import net.corda.node.driver.NodeHandle
import net.corda.node.driver.driver
import net.corda.node.services.User
-import net.corda.node.services.config.configureTestSSL
-import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.toHostAndPort
import net.corda.node.services.messaging.CordaRPCClient
import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.ValidatingNotaryService
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 {
+class CordaRPCClientTest : DriverBasedTest() {
private val rpcUser = User("user1", "test", permissions = setOf(startFlowPermission()))
- private val stopDriver = CountDownLatch(1)
- private var driverThread: Thread? = null
+ private lateinit var node: NodeHandle
private lateinit var client: CordaRPCClient
- private lateinit var driverInfo: NodeInfoAndConfig
- @Before
- fun start() {
- val driverStarted = CountDownLatch(1)
- driverThread = thread {
- driver(isDebug = true) {
- driverInfo = startNode(rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow()
- client = CordaRPCClient(toHostAndPort(driverInfo.nodeInfo.address), configureTestSSL())
- driverStarted.countDown()
- stopDriver.await()
- }
- }
- driverStarted.await()
- }
-
- @After
- fun stop() {
- stopDriver.countDown()
- driverThread?.join()
+ override fun setup() = driver(isDebug = true) {
+ node = startNode(rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow()
+ client = node.rpcClientToNode()
+ runTest()
}
@Test
@@ -78,7 +56,9 @@ class CordaRPCClientTest {
println("Creating proxy")
val proxy = client.proxy()
println("Starting flow")
- val flowHandle = proxy.startFlow(::CashFlow, CashCommand.IssueCash(20.DOLLARS, OpaqueBytes.of(0), driverInfo.nodeInfo.legalIdentity, driverInfo.nodeInfo.legalIdentity))
+ val flowHandle = proxy.startFlow(
+ ::CashFlow,
+ CashCommand.IssueCash(20.DOLLARS, OpaqueBytes.of(0), node.nodeInfo.legalIdentity, node.nodeInfo.legalIdentity))
println("Started flow, waiting on result")
flowHandle.progress.subscribe {
println("PROGRESS $it")
diff --git a/client/src/integration-test/kotlin/net/corda/client/NodeMonitorModelTest.kt b/client/src/integration-test/kotlin/net/corda/client/NodeMonitorModelTest.kt
index 2f85a897f1..4847dc943d 100644
--- a/client/src/integration-test/kotlin/net/corda/client/NodeMonitorModelTest.kt
+++ b/client/src/integration-test/kotlin/net/corda/client/NodeMonitorModelTest.kt
@@ -9,7 +9,9 @@ import net.corda.core.contracts.PartyAndReference
import net.corda.core.contracts.USD
import net.corda.core.flows.StateMachineRunId
import net.corda.core.getOrThrow
+import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineUpdate
+import net.corda.core.messaging.startFlow
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.ServiceInfo
@@ -19,6 +21,7 @@ import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.flows.CashCommand
import net.corda.flows.CashFlow
+import net.corda.node.driver.DriverBasedTest
import net.corda.node.driver.driver
import net.corda.node.services.User
import net.corda.node.services.config.configureTestSSL
@@ -29,63 +32,42 @@ import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.testing.expect
import net.corda.testing.expectEvents
import net.corda.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 {
+class NodeMonitorModelTest : DriverBasedTest() {
lateinit var aliceNode: NodeInfo
lateinit var notaryNode: NodeInfo
- val stopDriver = CountDownLatch(1)
- var driverThread: Thread? = null
+ lateinit var rpc: CordaRPCOps
lateinit var stateMachineTransactionMapping: Observable
lateinit var stateMachineUpdates: Observable
lateinit var progressTracking: Observable
lateinit var transactions: Observable
lateinit var vaultUpdates: Observable
lateinit var networkMapUpdates: Observable
- lateinit var clientToService: Observer
lateinit var newNode: (String) -> NodeInfo
- @Before
- fun start() {
- val driverStarted = CountDownLatch(1)
- driverThread = thread {
- driver {
- val cashUser = User("user1", "test", permissions = setOf(startFlowPermission()))
- val aliceNodeFuture = startNode("Alice", rpcUsers = listOf(cashUser))
- val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
+ override fun setup() = driver {
+ val cashUser = User("user1", "test", permissions = setOf(startFlowPermission()))
+ val aliceNodeFuture = startNode("Alice", rpcUsers = listOf(cashUser))
+ val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
- aliceNode = aliceNodeFuture.getOrThrow().nodeInfo
- notaryNode = notaryNodeFuture.getOrThrow().nodeInfo
- newNode = { nodeName -> startNode(nodeName).getOrThrow().nodeInfo }
- val monitor = NodeMonitorModel()
+ aliceNode = aliceNodeFuture.getOrThrow().nodeInfo
+ notaryNode = notaryNodeFuture.getOrThrow().nodeInfo
+ newNode = { nodeName -> startNode(nodeName).getOrThrow().nodeInfo }
+ val monitor = NodeMonitorModel()
- stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
- stateMachineUpdates = monitor.stateMachineUpdates.bufferUntilSubscribed()
- progressTracking = monitor.progressTracking.bufferUntilSubscribed()
- transactions = monitor.transactions.bufferUntilSubscribed()
- vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed()
- networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
- clientToService = monitor.clientToService
+ stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
+ stateMachineUpdates = monitor.stateMachineUpdates.bufferUntilSubscribed()
+ progressTracking = monitor.progressTracking.bufferUntilSubscribed()
+ transactions = monitor.transactions.bufferUntilSubscribed()
+ vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed()
+ networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
- monitor.register(ArtemisMessagingComponent.toHostAndPort(aliceNode.address), configureTestSSL(), cashUser.username, cashUser.password)
- driverStarted.countDown()
- stopDriver.await()
- }
- }
- driverStarted.await()
- }
-
- @After
- fun stop() {
- stopDriver.countDown()
- driverThread?.join()
+ monitor.register(ArtemisMessagingComponent.toHostAndPort(aliceNode.address), configureTestSSL(), cashUser.username, cashUser.password)
+ rpc = monitor.proxyObservable.value!!
+ runTest()
}
@Test
@@ -112,7 +94,7 @@ class NodeMonitorModelTest {
@Test
fun `cash issue works end to end`() {
- clientToService.onNext(CashCommand.IssueCash(
+ rpc.startFlow(::CashFlow, CashCommand.IssueCash(
amount = Amount(100, USD),
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
recipient = aliceNode.legalIdentity,
@@ -137,14 +119,14 @@ class NodeMonitorModelTest {
@Test
fun `cash issue and move`() {
- clientToService.onNext(CashCommand.IssueCash(
+ rpc.startFlow(::CashFlow, CashCommand.IssueCash(
amount = Amount(100, USD),
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
recipient = aliceNode.legalIdentity,
notary = notaryNode.notaryIdentity
))
- clientToService.onNext(CashCommand.PayCash(
+ rpc.startFlow(::CashFlow, CashCommand.PayCash(
amount = Amount(100, Issued(PartyAndReference(aliceNode.legalIdentity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
recipient = aliceNode.legalIdentity
))
diff --git a/client/src/main/kotlin/net/corda/client/fxutils/ObservableFold.kt b/client/src/main/kotlin/net/corda/client/fxutils/ObservableFold.kt
index 6e77fcdc4e..d9c34b11dd 100644
--- a/client/src/main/kotlin/net/corda/client/fxutils/ObservableFold.kt
+++ b/client/src/main/kotlin/net/corda/client/fxutils/ObservableFold.kt
@@ -7,6 +7,7 @@ import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.collections.ObservableMap
import rx.Observable
+import java.util.concurrent.TimeUnit
/**
* Simple utilities for converting an [rx.Observable] into a javafx [ObservableValue]/[ObservableList]
@@ -29,76 +30,46 @@ fun Observable.foldToObservableValue(initial: B, folderFun: (A, B) ->
}
/**
- * [foldToObservableList] takes an [rx.Observable] stream and creates an [ObservableList] out of it, while maintaining
- * an accumulator.
- * @param initialAccumulator The initial value of the accumulator.
+ * [fold] takes an [rx.Observable] stream and applies fold function on it, and collects all elements using the accumulator.
+ * @param accumulator The accumulator for accumulating elements.
* @param folderFun The transformation function to be called on the observable list when a new element is emitted on
* the stream, which should modify the list as needed.
*/
-fun Observable.foldToObservableList(
- initialAccumulator: C, folderFun: (A, C, ObservableList) -> C
-): ObservableList {
- val result = FXCollections.observableArrayList()
+fun Observable.fold(accumulator: R, folderFun: (R, T) -> Unit): R {
/**
- * This capture is fine, as [Platform.runLater] runs closures in order
+ * This capture is fine, as [Platform.runLater] runs closures in order.
+ * The buffer is to avoid flooding FX thread with runnable.
*/
- var currentAccumulator = initialAccumulator
- subscribe {
- Platform.runLater {
- currentAccumulator = folderFun(it, currentAccumulator, result)
+ buffer(1, TimeUnit.SECONDS).subscribe {
+ if (it.isNotEmpty()) {
+ Platform.runLater {
+ it.fold(accumulator) { list, item ->
+ folderFun.invoke(list, item)
+ list
+ }
+ }
}
}
- return result
+ return accumulator
}
/**
* [recordInSequence] records incoming events on the [rx.Observable] in sequence.
*/
fun Observable.recordInSequence(): ObservableList {
- return foldToObservableList(Unit) { newElement, _unit, list ->
+ return fold(FXCollections.observableArrayList()) { list, newElement ->
list.add(newElement)
}
}
-/**
- * [foldToObservableMap] takes an [rx.Observable] stream and creates an [ObservableMap] out of it, while maintaining
- * an accumulator.
- * @param initialAccumulator The initial value of the accumulator.
- * @param folderFun The transformation function to be called on the observable map when a new element is emitted on
- * the stream, which should modify the map as needed.
- */
-fun Observable.foldToObservableMap(
- initialAccumulator: C, folderFun: (A, C, ObservableMap) -> C
-): ObservableMap {
- val result = FXCollections.observableHashMap()
- /**
- * This capture is fine, as [Platform.runLater] runs closures in order
- */
- var currentAccumulator = initialAccumulator
- subscribe {
- Platform.runLater {
- currentAccumulator = folderFun(it, currentAccumulator, result)
- }
- }
- return result
-}
-
/**
* This variant simply associates each event with its key.
* @param toKey Function retrieving the key to associate with.
* @param merge The function to be called if there is an existing element at the key.
*/
-fun Observable.recordAsAssociation(
- toKey: (A) -> K,
- merge: (K, oldValue: A, newValue: A) -> A = { _key, _oldValue, newValue -> newValue }
-): ObservableMap {
- return foldToObservableMap(Unit) { newElement, _unit, map ->
- val key = toKey(newElement)
- val oldValue = map.get(key)
- if (oldValue != null) {
- map.set(key, merge(key, oldValue, newElement))
- } else {
- map.set(key, newElement)
- }
+fun Observable.recordAsAssociation(toKey: (A) -> K, merge: (K, oldValue: A, newValue: A) -> A = { _key, _oldValue, newValue -> newValue }): ObservableMap {
+ return fold(FXCollections.observableHashMap()) { map, item ->
+ val key = toKey(item)
+ map[key] = map[key]?.let { merge(key, it, item) } ?: item
}
}
diff --git a/client/src/main/kotlin/net/corda/client/mock/EventGenerator.kt b/client/src/main/kotlin/net/corda/client/mock/EventGenerator.kt
index 224efaa53c..487bde0c56 100644
--- a/client/src/main/kotlin/net/corda/client/mock/EventGenerator.kt
+++ b/client/src/main/kotlin/net/corda/client/mock/EventGenerator.kt
@@ -6,6 +6,7 @@ import net.corda.core.crypto.Party
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.TransactionBuilder
import net.corda.flows.CashCommand
+import java.util.*
/**
* [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned
@@ -13,15 +14,15 @@ import net.corda.flows.CashCommand
*/
class EventGenerator(
val parties: List,
- val notary: Party
+ val notary: Party,
+ val currencies: List = listOf(USD, GBP, CHF),
+ val issuers: List = parties
) {
-
private var vault = listOf>()
val issuerGenerator =
- Generator.pickOne(parties).combine(Generator.intRange(0, 1)) { party, ref -> party.ref(ref.toByte()) }
+ Generator.pickOne(issuers).combine(Generator.intRange(0, 1)) { party, ref -> party.ref(ref.toByte()) }
- val currencies = setOf(USD, GBP, CHF).toList() // + Currency.getAvailableCurrencies().toList().subList(0, 3).toSet()).toList()
val currencyGenerator = Generator.pickOne(currencies)
val issuedGenerator = issuerGenerator.combine(currencyGenerator) { issuer, currency -> Issued(issuer, currency) }
@@ -93,8 +94,11 @@ class EventGenerator(
1.0 to moveCashGenerator
)
- val bankOfCordaCommandGenerator = Generator.frequency(
- 0.6 to issueCashGenerator,
+ val bankOfCordaExitGenerator = Generator.frequency(
0.4 to exitCashGenerator
)
+
+ val bankOfCordaIssueGenerator = Generator.frequency(
+ 0.6 to issueCashGenerator
+ )
}
\ No newline at end of file
diff --git a/client/src/main/kotlin/net/corda/client/model/ContractStateModel.kt b/client/src/main/kotlin/net/corda/client/model/ContractStateModel.kt
index 8eb3c6bb5d..33ec550322 100644
--- a/client/src/main/kotlin/net/corda/client/model/ContractStateModel.kt
+++ b/client/src/main/kotlin/net/corda/client/model/ContractStateModel.kt
@@ -1,19 +1,19 @@
package net.corda.client.model
+import javafx.collections.FXCollections
import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.removeIf
-import net.corda.client.fxutils.foldToObservableList
+import net.corda.client.fxutils.fold
import net.corda.client.fxutils.map
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef
-import net.corda.core.contracts.StateRef
import net.corda.core.node.services.Vault
import rx.Observable
data class Diff(
val added: Collection>,
- val removed: Collection
+ val removed: Collection>
)
/**
@@ -26,14 +26,12 @@ class ContractStateModel {
Diff(it.produced, it.consumed)
}
private val cashStatesDiff: Observable> = contractStatesDiff.map {
- // We can't filter removed hashes here as we don't have type info
- Diff(it.added.filterCashStateAndRefs(), it.removed)
+ Diff(it.added.filterCashStateAndRefs(), it.removed.filterCashStateAndRefs())
+ }
+ val cashStates: ObservableList> = cashStatesDiff.fold(FXCollections.observableArrayList()) { list, statesDiff ->
+ list.removeIf { it in statesDiff.removed }
+ list.addAll(statesDiff.added)
}
- val cashStates: ObservableList> =
- cashStatesDiff.foldToObservableList(Unit) { statesDiff, _accumulator, observableList ->
- observableList.removeIf { it.ref in statesDiff.removed }
- observableList.addAll(statesDiff.added)
- }
val cash = cashStates.map { it.state.data.amount }
diff --git a/client/src/main/kotlin/net/corda/client/model/GatheredTransactionDataModel.kt b/client/src/main/kotlin/net/corda/client/model/GatheredTransactionDataModel.kt
deleted file mode 100644
index 69a466b39b..0000000000
--- a/client/src/main/kotlin/net/corda/client/model/GatheredTransactionDataModel.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-package net.corda.client.model
-
-import javafx.beans.property.SimpleObjectProperty
-import javafx.beans.value.ObservableValue
-import javafx.collections.ObservableList
-import javafx.collections.ObservableMap
-import net.corda.client.fxutils.*
-import net.corda.core.contracts.ContractState
-import net.corda.core.contracts.StateAndRef
-import net.corda.core.contracts.StateRef
-import net.corda.core.crypto.SecureHash
-import net.corda.core.flows.StateMachineRunId
-import net.corda.core.messaging.StateMachineUpdate
-import net.corda.core.transactions.SignedTransaction
-import org.fxmisc.easybind.EasyBind
-
-data class GatheredTransactionData(
- val transaction: PartiallyResolvedTransaction,
- val stateMachines: ObservableList
-)
-
-/**
- * [PartiallyResolvedTransaction] holds a [SignedTransaction] that has zero or more inputs resolved. The intent is
- * to prepare clients for cases where an input can only be resolved in the future/cannot be resolved at all (for example
- * because of permissioning)
- */
-data class PartiallyResolvedTransaction(
- val transaction: SignedTransaction,
- val inputs: List>) {
- val id = transaction.id
-
- sealed class InputResolution(val stateRef: StateRef) {
- class Unresolved(stateRef: StateRef) : InputResolution(stateRef)
- class Resolved(val stateAndRef: StateAndRef) : InputResolution(stateAndRef.ref)
- }
-
- companion object {
- fun fromSignedTransaction(
- transaction: SignedTransaction,
- transactions: ObservableMap
- ) = PartiallyResolvedTransaction(
- transaction = transaction,
- inputs = transaction.tx.inputs.map { stateRef ->
- EasyBind.map(transactions.getObservableValue(stateRef.txhash)) {
- if (it == null) {
- InputResolution.Unresolved(stateRef)
- } else {
- InputResolution.Resolved(it.tx.outRef(stateRef.index))
- }
- }
- }
- )
- }
-}
-
-sealed class TransactionCreateStatus(val message: String?) {
- class Started(message: String?) : TransactionCreateStatus(message)
- class Failed(message: String?) : TransactionCreateStatus(message)
-
- override fun toString(): String = message ?: javaClass.simpleName
-}
-
-data class FlowStatus(
- val status: String
-)
-
-sealed class StateMachineStatus(val stateMachineName: String) {
- class Added(stateMachineName: String) : StateMachineStatus(stateMachineName)
- class Removed(stateMachineName: String) : StateMachineStatus(stateMachineName)
-
- override fun toString(): String = "${javaClass.simpleName}($stateMachineName)"
-}
-
-data class StateMachineData(
- val id: StateMachineRunId,
- val flowStatus: ObservableValue,
- val stateMachineStatus: ObservableValue
-)
-
-/**
- * This model provides an observable list of transactions and what state machines/flows recorded them
- */
-class GatheredTransactionDataModel {
- private val transactions by observable(NodeMonitorModel::transactions)
- private val stateMachineUpdates by observable(NodeMonitorModel::stateMachineUpdates)
- private val progressTracking by observable(NodeMonitorModel::progressTracking)
- private val stateMachineTransactionMapping by observable(NodeMonitorModel::stateMachineTransactionMapping)
-
- private val collectedTransactions = transactions.recordInSequence()
- private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
- private val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
- private val stateMachineStatus = stateMachineUpdates.foldToObservableMap(Unit) { update, _unit, map: ObservableMap> ->
- when (update) {
- is StateMachineUpdate.Added -> {
- val added: SimpleObjectProperty =
- SimpleObjectProperty(StateMachineStatus.Added(update.stateMachineInfo.flowLogicClassName))
- map[update.id] = added
- }
- is StateMachineUpdate.Removed -> {
- val added = map[update.id]
- added ?: throw Exception("State machine removed with unknown id ${update.id}")
- added.set(StateMachineStatus.Removed(added.value.stateMachineName))
- }
- }
- }
- private val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
- StateMachineData(id, progress.map { it?.let { FlowStatus(it.message) } }, status)
- }.getObservableValues()
- // TODO : Create a new screen for state machines.
- private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
- private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
- val partiallyResolvedTransactions = collectedTransactions.map {
- PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
- }
-}
diff --git a/client/src/main/kotlin/net/corda/client/model/NetworkIdentityModel.kt b/client/src/main/kotlin/net/corda/client/model/NetworkIdentityModel.kt
index c1673238a4..2b5cea50bb 100644
--- a/client/src/main/kotlin/net/corda/client/model/NetworkIdentityModel.kt
+++ b/client/src/main/kotlin/net/corda/client/model/NetworkIdentityModel.kt
@@ -1,11 +1,12 @@
package net.corda.client.model
import javafx.beans.value.ObservableValue
+import javafx.collections.FXCollections
import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.removeIf
import net.corda.client.fxutils.firstOrDefault
import net.corda.client.fxutils.firstOrNullObservable
-import net.corda.client.fxutils.foldToObservableList
+import net.corda.client.fxutils.fold
import net.corda.client.fxutils.map
import net.corda.core.crypto.CompositeKey
import net.corda.core.node.NodeInfo
@@ -17,15 +18,15 @@ class NetworkIdentityModel {
private val networkIdentityObservable by observable(NodeMonitorModel::networkMap)
val networkIdentities: ObservableList =
- networkIdentityObservable.foldToObservableList(Unit) { update, _accumulator, observableList ->
- observableList.removeIf {
+ networkIdentityObservable.fold(FXCollections.observableArrayList()) { list, update ->
+ list.removeIf {
when (update) {
is MapChange.Removed -> it == update.node
is MapChange.Modified -> it == update.previousNode
else -> false
}
}
- observableList.addAll(update.node)
+ list.addAll(update.node)
}
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
diff --git a/client/src/main/kotlin/net/corda/client/model/NodeMonitorModel.kt b/client/src/main/kotlin/net/corda/client/model/NodeMonitorModel.kt
index afcb7ebcad..df383eb4a0 100644
--- a/client/src/main/kotlin/net/corda/client/model/NodeMonitorModel.kt
+++ b/client/src/main/kotlin/net/corda/client/model/NodeMonitorModel.kt
@@ -6,14 +6,11 @@ import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineInfo
import net.corda.core.messaging.StateMachineUpdate
-import net.corda.core.messaging.startFlow
import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.node.services.StateMachineTransactionMapping
import net.corda.core.node.services.Vault
import net.corda.core.transactions.SignedTransaction
-import net.corda.flows.CashCommand
-import net.corda.flows.CashFlow
-import net.corda.node.services.config.NodeSSLConfiguration
+import net.corda.node.services.config.SSLConfiguration
import net.corda.node.services.messaging.CordaRPCClient
import rx.Observable
import rx.subjects.PublishSubject
@@ -48,16 +45,13 @@ class NodeMonitorModel {
val progressTracking: Observable = progressTrackingSubject
val networkMap: Observable = networkMapSubject
- private val clientToServiceSource = PublishSubject.create()
- val clientToService: PublishSubject = clientToServiceSource
-
val proxyObservable = SimpleObjectProperty()
/**
* Register for updates to/from a given vault.
* TODO provide an unsubscribe mechanism
*/
- fun register(nodeHostAndPort: HostAndPort, sslConfig: NodeSSLConfiguration, username: String, password: String) {
+ fun register(nodeHostAndPort: HostAndPort, sslConfig: SSLConfiguration, username: String, password: String) {
val client = CordaRPCClient(nodeHostAndPort, sslConfig)
client.start(username, password)
val proxy = client.proxy()
@@ -98,10 +92,6 @@ class NodeMonitorModel {
val (parties, futurePartyUpdate) = proxy.networkMapUpdates()
futurePartyUpdate.startWith(parties.map { MapChange.Added(it) }).subscribe(networkMapSubject)
- // Client -> Service
- clientToServiceSource.subscribe {
- proxy.startFlow(::CashFlow, it)
- }
proxyObservable.set(proxy)
}
}
\ No newline at end of file
diff --git a/client/src/main/kotlin/net/corda/client/model/TransactionDataModel.kt b/client/src/main/kotlin/net/corda/client/model/TransactionDataModel.kt
new file mode 100644
index 0000000000..3a6c9eddd7
--- /dev/null
+++ b/client/src/main/kotlin/net/corda/client/model/TransactionDataModel.kt
@@ -0,0 +1,116 @@
+package net.corda.client.model
+
+import javafx.beans.property.SimpleObjectProperty
+import javafx.beans.value.ObservableValue
+import javafx.collections.FXCollections
+import javafx.collections.ObservableList
+import javafx.collections.ObservableMap
+import net.corda.client.fxutils.*
+import net.corda.core.contracts.ContractState
+import net.corda.core.contracts.StateAndRef
+import net.corda.core.contracts.StateRef
+import net.corda.core.crypto.SecureHash
+import net.corda.core.flows.StateMachineRunId
+import net.corda.core.messaging.StateMachineUpdate
+import net.corda.core.transactions.SignedTransaction
+import org.fxmisc.easybind.EasyBind
+
+data class GatheredTransactionData(
+ val transaction: PartiallyResolvedTransaction,
+ val stateMachines: ObservableList
+)
+
+/**
+ * [PartiallyResolvedTransaction] holds a [SignedTransaction] that has zero or more inputs resolved. The intent is
+ * to prepare clients for cases where an input can only be resolved in the future/cannot be resolved at all (for example
+ * because of permissioning)
+ */
+data class PartiallyResolvedTransaction(
+ val transaction: SignedTransaction,
+ val inputs: List>) {
+ val id = transaction.id
+
+ sealed class InputResolution(val stateRef: StateRef) {
+ class Unresolved(stateRef: StateRef) : InputResolution(stateRef)
+ class Resolved(val stateAndRef: StateAndRef) : InputResolution(stateAndRef.ref)
+ }
+
+ companion object {
+ fun fromSignedTransaction(
+ transaction: SignedTransaction,
+ transactions: ObservableMap
+ ) = PartiallyResolvedTransaction(
+ transaction = transaction,
+ inputs = transaction.tx.inputs.map { stateRef ->
+ EasyBind.map(transactions.getObservableValue(stateRef.txhash)) {
+ if (it == null) {
+ InputResolution.Unresolved(stateRef)
+ } else {
+ InputResolution.Resolved(it.tx.outRef(stateRef.index))
+ }
+ }
+ }
+ )
+ }
+}
+
+sealed class TransactionCreateStatus(val message: String?) {
+ class Started(message: String?) : TransactionCreateStatus(message)
+ class Failed(message: String?) : TransactionCreateStatus(message)
+
+ override fun toString(): String = message ?: javaClass.simpleName
+}
+
+data class FlowStatus(
+ val status: String
+)
+
+sealed class StateMachineStatus(val stateMachineName: String) {
+ class Added(stateMachineName: String) : StateMachineStatus(stateMachineName)
+ class Removed(stateMachineName: String) : StateMachineStatus(stateMachineName)
+
+ override fun toString(): String = "${javaClass.simpleName}($stateMachineName)"
+}
+
+data class StateMachineData(
+ val id: StateMachineRunId,
+ val flowStatus: ObservableValue,
+ val stateMachineStatus: ObservableValue
+)
+
+/**
+ * This model provides an observable list of transactions and what state machines/flows recorded them
+ */
+class TransactionDataModel {
+ private val transactions by observable(NodeMonitorModel::transactions)
+ private val stateMachineUpdates by observable(NodeMonitorModel::stateMachineUpdates)
+ private val progressTracking by observable(NodeMonitorModel::progressTracking)
+ private val stateMachineTransactionMapping by observable(NodeMonitorModel::stateMachineTransactionMapping)
+
+ private val collectedTransactions = transactions.recordInSequence()
+ private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
+ private val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
+ private val stateMachineStatus = stateMachineUpdates.fold(FXCollections.observableHashMap>()) { map, update ->
+ when (update) {
+ is StateMachineUpdate.Added -> {
+ val added: SimpleObjectProperty =
+ SimpleObjectProperty(StateMachineStatus.Added(update.stateMachineInfo.flowLogicClassName))
+ map[update.id] = added
+ }
+ is StateMachineUpdate.Removed -> {
+ val added = map[update.id]
+ added ?: throw Exception("State machine removed with unknown id ${update.id}")
+ added.set(StateMachineStatus.Removed(added.value.stateMachineName))
+ }
+ }
+ }
+ private val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
+ StateMachineData(id, progress.map { it?.let { FlowStatus(it.message) } }, status)
+ }.getObservableValues()
+ // TODO : Create a new screen for state machines.
+ private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
+ private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
+ val partiallyResolvedTransactions = collectedTransactions.map {
+ PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
+ }
+}
diff --git a/config/dev/generalnodea.conf b/config/dev/generalnodea.conf
index cf34c2b870..b55b3e1a9b 100644
--- a/config/dev/generalnodea.conf
+++ b/config/dev/generalnodea.conf
@@ -1,4 +1,3 @@
-basedir : "./nodea"
myLegalName : "Bank A"
nearestCity : "London"
keyStorePassword : "cordacadevpass"
@@ -6,5 +5,8 @@ trustStorePassword : "trustpass"
artemisAddress : "localhost:31337"
webAddress : "localhost:31339"
extraAdvertisedServiceIds: "corda.interest_rates"
-networkMapAddress : "localhost:12345"
+networkMapService : {
+ address : "localhost:12345"
+ legalName : "Network Map Service"
+}
useHTTPS : false
diff --git a/config/dev/generalnodeb.conf b/config/dev/generalnodeb.conf
index 56815ad1e8..20e773388b 100644
--- a/config/dev/generalnodeb.conf
+++ b/config/dev/generalnodeb.conf
@@ -1,4 +1,3 @@
-basedir : "./nodeb"
myLegalName : "Bank B"
nearestCity : "London"
keyStorePassword : "cordacadevpass"
@@ -6,5 +5,8 @@ trustStorePassword : "trustpass"
artemisAddress : "localhost:31338"
webAddress : "localhost:31340"
extraAdvertisedServiceIds: "corda.interest_rates"
-networkMapAddress : "localhost:12345"
+networkMapService : {
+ address : "localhost:12345"
+ legalName : "Network Map Service"
+}
useHTTPS : false
diff --git a/config/dev/nameservernode.conf b/config/dev/nameservernode.conf
index 7a91eda14f..51dd1df842 100644
--- a/config/dev/nameservernode.conf
+++ b/config/dev/nameservernode.conf
@@ -1,4 +1,3 @@
-basedir : "./nameserver"
myLegalName : "Notary Service"
nearestCity : "London"
keyStorePassword : "cordacadevpass"
diff --git a/core/build.gradle b/core/build.gradle
index 2265ea4402..2afd7e2634 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -31,11 +31,11 @@ sourceSets {
}
dependencies {
- testCompile 'junit:junit:4.12'
+ testCompile "junit:junit:$junit_version"
testCompile "commons-fileupload:commons-fileupload:1.3.2"
// Guava: Google test library (collections test suite)
- testCompile "com.google.guava:guava-testlib:19.0"
+ testCompile "com.google.guava:guava-testlib:$guava_version"
// Bring in the MockNode infrastructure for writing protocol unit tests.
testCompile project(":node")
@@ -43,7 +43,7 @@ dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
- compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.2"
+ compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.3"
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// Thread safety annotations
@@ -56,18 +56,18 @@ dependencies {
// AssertJ: for fluent assertions for testing
testCompile "org.assertj:assertj-core:${assertj_version}"
- compile 'com.pholser:junit-quickcheck-core:0.6'
- compile 'com.pholser:junit-quickcheck-generators:0.6'
+ compile "com.pholser:junit-quickcheck-core:$quickcheck_version"
+ compile "com.pholser:junit-quickcheck-generators:$quickcheck_version"
// Guava: Google utilities library.
- compile "com.google.guava:guava:19.0"
+ compile "com.google.guava:guava:$guava_version"
// RxJava: observable streams of events.
- compile "io.reactivex:rxjava:1.1.6"
+ compile "io.reactivex:rxjava:1.2.4"
// Kryo: object graph serialization.
compile "com.esotericsoftware:kryo:4.0.0"
- compile "de.javakaffee:kryo-serializers:0.38"
+ compile "de.javakaffee:kryo-serializers:0.41"
// Apache JEXL: An embeddable expression evaluation library.
// This may be temporary until we experiment with other ways to do on-the-fly contract specialisation via an API.
@@ -88,4 +88,4 @@ dependencies {
// RS API: Response type and codes for ApiUtils.
compile "javax.ws.rs:javax.ws.rs-api:2.0"
-}
\ No newline at end of file
+}
diff --git a/core/src/main/kotlin/net/corda/core/Utils.kt b/core/src/main/kotlin/net/corda/core/Utils.kt
index fea248b305..d5ca5f4312 100644
--- a/core/src/main/kotlin/net/corda/core/Utils.kt
+++ b/core/src/main/kotlin/net/corda/core/Utils.kt
@@ -1,3 +1,4 @@
+// TODO Move out the Kotlin specific stuff into a separate file
@file:JvmName("Utils")
package net.corda.core
@@ -26,10 +27,7 @@ import java.nio.file.*
import java.nio.file.attribute.FileAttribute
import java.time.Duration
import java.time.temporal.Temporal
-import java.util.concurrent.CompletableFuture
-import java.util.concurrent.ExecutionException
-import java.util.concurrent.Executor
-import java.util.concurrent.Future
+import java.util.concurrent.*
import java.util.concurrent.locks.ReentrantLock
import java.util.function.BiConsumer
import java.util.stream.Stream
@@ -67,9 +65,9 @@ infix fun Long.checkedAdd(b: Long) = Math.addExact(this, b)
fun random63BitValue(): Long = Math.abs(newSecureRandom().nextLong())
/** Same as [Future.get] but with a more descriptive name, and doesn't throw [ExecutionException], instead throwing its cause */
-fun Future.getOrThrow(): T {
- try {
- return get()
+fun Future.getOrThrow(timeout: Duration? = null): T {
+ return try {
+ if (timeout == null) get() else get(timeout.toNanos(), TimeUnit.NANOSECONDS)
} catch (e: ExecutionException) {
throw e.cause!!
}
@@ -204,19 +202,31 @@ fun List.randomOrNull(predicate: (T) -> Boolean) = filter(predicate).rand
// An alias that can sometimes make code clearer to read.
val RunOnCallerThread: Executor = MoreExecutors.directExecutor()
+inline fun elapsedTime(block: () -> Unit): Duration {
+ val start = System.nanoTime()
+ block()
+ val end = System.nanoTime()
+ return Duration.ofNanos(end-start)
+}
+
// TODO: Add inline back when a new Kotlin version is released and check if the java.lang.VerifyError
// returns in the IRSSimulationTest. If not, commit the inline back.
fun logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T {
- val now = System.currentTimeMillis()
- val r = body()
- val elapsed = System.currentTimeMillis() - now
- if (logger != null)
- logger.info("$label took $elapsed msec")
- else
- println("$label took $elapsed msec")
- return r
+ // Use nanoTime as it's monotonic.
+ val now = System.nanoTime()
+ try {
+ return body()
+ } finally {
+ val elapsed = Duration.ofNanos(System.nanoTime() - now).toMillis()
+ if (logger != null)
+ logger.info("$label took $elapsed msec")
+ else
+ println("$label took $elapsed msec")
+ }
}
+fun Logger.logElapsedTime(label: String, body: () -> T): T = logElapsedTime(label, this, body)
+
/**
* A threadbox is a simple utility that makes it harder to forget to take a lock before accessing some shared state.
* Simply define a private class to hold the data that must be grouped under the same lock, and then pass the only
@@ -271,19 +281,17 @@ class TransientProperty(private val initializer: () -> T) {
/**
* Given a path to a zip file, extracts it to the given directory.
*/
-fun extractZipFile(zipPath: Path, toPath: Path) {
- val normalisedToPath = toPath.normalize()
- normalisedToPath.createDirectories()
+fun extractZipFile(zipFile: Path, toDirectory: Path) {
+ val normalisedDirectory = toDirectory.normalize().createDirectories()
- zipPath.read {
+ zipFile.read {
val zip = ZipInputStream(BufferedInputStream(it))
while (true) {
val e = zip.nextEntry ?: break
- val outPath = normalisedToPath / e.name
+ val outPath = (normalisedDirectory / e.name).normalize()
- // Security checks: we should reject a zip that contains tricksy paths that try to escape toPath.
- if (!outPath.normalize().startsWith(normalisedToPath))
- throw IllegalStateException("ZIP contained a path that resolved incorrectly: ${e.name}")
+ // Security checks: we should reject a zip that contains tricksy paths that try to escape toDirectory.
+ check(outPath.startsWith(normalisedDirectory)) { "ZIP contained a path that resolved incorrectly: ${e.name}" }
if (e.isDirectory) {
outPath.createDirectories()
@@ -355,8 +363,7 @@ data class ErrorOr private constructor(val value: A?, val error: Throwabl
}
/**
- * Returns an observable that buffers events until subscribed.
- *
+ * Returns an Observable that buffers events until subscribed.
* @see UnicastSubject
*/
fun Observable.bufferUntilSubscribed(): Observable {
@@ -375,5 +382,18 @@ fun Observer.tee(vararg teeTo: Observer): Observer {
return subject
}
-/** Allows summing big decimals that are in iterable collections */
+/**
+ * Returns a [ListenableFuture] bound to the *first* item emitted by this Observable. The future will complete with a
+ * NoSuchElementException if no items are emitted or any other error thrown by the Observable.
+ */
+fun Observable.toFuture(): ListenableFuture {
+ val future = SettableFuture.create()
+ first().subscribe(
+ { future.set(it) },
+ { future.setException(it) }
+ )
+ return future
+}
+
+/** Return the sum of an Iterable of [BigDecimal]s. */
fun Iterable.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + b }
diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt
index ce2dbea0b5..922d222403 100644
--- a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt
+++ b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt
@@ -119,7 +119,7 @@ inline fun verifyMoveCommand(inputs: List()
val keysThatSigned = command.signers.toSet()
requireThat {
- "the owning keys are the same as the signing keys" by keysThatSigned.containsAll(owningPubKeys)
+ "the owning keys are a subset of the signing keys" by keysThatSigned.containsAll(owningPubKeys)
}
return command.value
}
diff --git a/core/src/main/kotlin/net/corda/core/contracts/FinanceTypes.kt b/core/src/main/kotlin/net/corda/core/contracts/FinanceTypes.kt
index 0b66c5e1b6..9266f366dc 100644
--- a/core/src/main/kotlin/net/corda/core/contracts/FinanceTypes.kt
+++ b/core/src/main/kotlin/net/corda/core/contracts/FinanceTypes.kt
@@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.google.common.annotations.VisibleForTesting
import java.math.BigDecimal
+import java.math.BigInteger
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.format.DateTimeFormatter
@@ -34,6 +35,19 @@ import java.util.*
* @param T the type of the token, for example [Currency].
*/
data class Amount(val quantity: Long, val token: T) : Comparable> {
+ companion object {
+ /**
+ * Build an amount from a decimal representation. For example, with an input of "12.34" GBP,
+ * returns an amount with a quantity of "1234".
+ *
+ * @see Amount.toDecimal
+ */
+ fun fromDecimal(quantity: BigDecimal, currency: Currency) : Amount {
+ val longQuantity = quantity.movePointRight(currency.defaultFractionDigits).toLong()
+ return Amount(longQuantity, currency)
+ }
+ }
+
init {
// Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain
// contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance.
@@ -41,7 +55,12 @@ data class Amount(val quantity: Long, val token: T) : Comparable> {
require(quantity >= 0) { "Negative amounts are not allowed: $quantity" }
}
- constructor(amount: BigDecimal, currency: T) : this(amount.toLong(), currency)
+ /**
+ * Construct the amount using the given decimal value as quantity. Any fractional part
+ * is discarded. To convert and use the fractional part, see [fromDecimal].
+ */
+ constructor(quantity: BigDecimal, token: T) : this(quantity.toLong(), token)
+ constructor(quantity: BigInteger, token: T) : this(quantity.toLong(), token)
operator fun plus(other: Amount): Amount {
checkCurrency(other)
@@ -70,6 +89,14 @@ data class Amount(val quantity: Long, val token: T) : Comparable> {
}
}
+/**
+ * Convert a currency [Amount] to a decimal representation. For example, with an amount with a quantity
+ * of "1234" GBP, returns "12.34".
+ *
+ * @see Amount.Companion.fromDecimal
+ */
+fun Amount.toDecimal() : BigDecimal = BigDecimal(quantity).movePointLeft(token.defaultFractionDigits)
+
fun Iterable>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
fun Iterable>.sumOrThrow() = reduce { left, right -> left + right }
fun Iterable>.sumOrZero(currency: T) = if (iterator().hasNext()) sumOrThrow() else Amount(0, currency)
diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt
index 1434087756..3aa1298d62 100644
--- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt
+++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt
@@ -114,42 +114,41 @@ interface ContractState {
* list should just contain the owner.
*/
val participants: List
-
- /**
- * All contract states may be _encumbered_ by up to one other state.
- *
- * The encumbrance state, if present, forces additional controls over the encumbered state, since the platform checks
- * that the encumbrance state is present as an input in the same transaction that consumes the encumbered state, and
- * the contract code and rules of the encumbrance state will also be verified during the execution of the transaction.
- * For example, a cash contract state could be encumbered with a time-lock contract state; the cash state is then only
- * processable in a transaction that verifies that the time specified in the encumbrance time-lock has passed.
- *
- * The encumbered state refers to another by index, and the referred encumbrance state
- * is an output state in a particular position on the same transaction that created the encumbered state. An alternative
- * implementation would be encumber by reference to a StateRef., which would allow the specification of encumbrance
- * by a state created in a prior transaction.
- *
- * Note that an encumbered state that is being consumed must have its encumbrance consumed in the same transaction,
- * otherwise the transaction is not valid.
- */
- val encumbrance: Int? get() = null
}
/**
* A wrapper for [ContractState] containing additional platform-level state information.
* This is the definitive state that is stored on the ledger and used in transaction outputs.
*/
-data class TransactionState(
+data class TransactionState @JvmOverloads constructor(
/** The custom contract state */
val data: T,
/** Identity of the notary that ensures the state is not used as an input to a transaction more than once */
- val notary: Party) {
+ val notary: Party,
+ /**
+ * All contract states may be _encumbered_ by up to one other state.
+ *
+ * The encumbrance state, if present, forces additional controls over the encumbered state, since the platform checks
+ * that the encumbrance state is present as an input in the same transaction that consumes the encumbered state, and
+ * the contract code and rules of the encumbrance state will also be verified during the execution of the transaction.
+ * For example, a cash contract state could be encumbered with a time-lock contract state; the cash state is then only
+ * processable in a transaction that verifies that the time specified in the encumbrance time-lock has passed.
+ *
+ * The encumbered state refers to another by index, and the referred encumbrance state
+ * is an output state in a particular position on the same transaction that created the encumbered state. An alternative
+ * implementation would be encumbering by reference to a [StateRef], which would allow the specification of encumbrance
+ * by a state created in a prior transaction.
+ *
+ * Note that an encumbered state that is being consumed must have its encumbrance consumed in the same transaction,
+ * otherwise the transaction is not valid.
+ */
+ val encumbrance: Int? = null) {
/**
* Copies the underlying state, replacing the notary field with the new value.
* To replace the notary, we need an approval (signature) from _all_ participants of the [ContractState].
*/
- fun withNotary(newNotary: Party) = TransactionState(this.data, newNotary)
+ fun withNotary(newNotary: Party) = TransactionState(this.data, newNotary, encumbrance)
}
/** Wraps the [ContractState] in a [TransactionState] object */
diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt
index 39d8d1830a..3570bc35e1 100644
--- a/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt
+++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt
@@ -19,6 +19,8 @@ sealed class TransactionType {
*/
fun verify(tx: LedgerTransaction) {
require(tx.notary != null || tx.timestamp == null) { "Transactions with timestamps must be notarised." }
+ val duplicates = detectDuplicateInputs(tx)
+ if (duplicates.isNotEmpty()) throw TransactionVerificationException.DuplicateInputStates(tx, duplicates)
val missing = verifySigners(tx)
if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx, missing.toList())
verifyTransaction(tx)
@@ -35,6 +37,19 @@ sealed class TransactionType {
return missing
}
+ /** Check that the inputs are unique. */
+ private fun detectDuplicateInputs(tx: LedgerTransaction): Set {
+ var seenInputs = emptySet()
+ var duplicates = emptySet()
+ tx.inputs.forEach { state ->
+ if (seenInputs.contains(state.ref)) {
+ duplicates += state.ref
+ }
+ seenInputs += state.ref
+ }
+ return duplicates
+ }
+
/**
* Return the list of public keys that that require signatures for the transaction type.
* Note: the notary key is checked separately for all transactions and need not be included.
@@ -74,14 +89,14 @@ sealed class TransactionType {
private fun verifyEncumbrances(tx: LedgerTransaction) {
// Validate that all encumbrances exist within the set of input states.
- val encumberedInputs = tx.inputs.filter { it.state.data.encumbrance != null }
+ val encumberedInputs = tx.inputs.filter { it.state.encumbrance != null }
encumberedInputs.forEach { encumberedInput ->
val encumbranceStateExists = tx.inputs.any {
- it.ref.txhash == encumberedInput.ref.txhash && it.ref.index == encumberedInput.state.data.encumbrance
+ it.ref.txhash == encumberedInput.ref.txhash && it.ref.index == encumberedInput.state.encumbrance
}
if (!encumbranceStateExists) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
- tx, encumberedInput.state.data.encumbrance!!,
+ tx, encumberedInput.state.encumbrance!!,
TransactionVerificationException.Direction.INPUT
)
}
@@ -90,7 +105,7 @@ sealed class TransactionType {
// Check that, in the outputs, an encumbered state does not refer to itself as the encumbrance,
// and that the number of outputs can contain the encumbrance.
for ((i, output) in tx.outputs.withIndex()) {
- val encumbranceIndex = output.data.encumbrance ?: continue
+ val encumbranceIndex = output.encumbrance ?: continue
if (encumbranceIndex == i || encumbranceIndex >= tx.outputs.size) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
tx, encumbranceIndex,
diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt
index cf5765e782..060d8c49f5 100644
--- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt
+++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt
@@ -97,6 +97,9 @@ sealed class TransactionVerificationException(val tx: LedgerTransaction, cause:
class SignersMissing(tx: LedgerTransaction, val missing: List) : TransactionVerificationException(tx, null) {
override fun toString() = "Signers missing: ${missing.joinToString()}"
}
+ class DuplicateInputStates(tx: LedgerTransaction, val duplicates: Set) : TransactionVerificationException(tx, null) {
+ override fun toString() = "Duplicate inputs: ${duplicates.joinToString()}"
+ }
class InvalidNotaryChange(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
class NotaryChangeInWrongTransactionType(tx: LedgerTransaction, val outputNotary: Party) : TransactionVerificationException(tx, null) {
diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/AllComposition.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/AllComposition.kt
index 350fb397d4..5be41988e5 100644
--- a/core/src/main/kotlin/net/corda/core/contracts/clauses/AllComposition.kt
+++ b/core/src/main/kotlin/net/corda/core/contracts/clauses/AllComposition.kt
@@ -1,39 +1,10 @@
package net.corda.core.contracts.clauses
-import net.corda.core.contracts.AuthenticatedObject
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.ContractState
-import net.corda.core.contracts.TransactionForContract
-import java.util.*
/**
* Compose a number of clauses, such that all of the clauses must run for verification to pass.
*/
-// TODO: Rename to AllOf
-class AllComposition(firstClause: Clause, vararg remainingClauses: Clause) : CompositeClause() {
- override val clauses = ArrayList>()
-
- init {
- clauses.add(firstClause)
- clauses.addAll(remainingClauses)
- }
-
- override fun matchedClauses(commands: List>): List> {
- clauses.forEach { clause ->
- check(clause.matches(commands)) { "Failed to match clause ${clause}" }
- }
- return clauses
- }
-
- override fun verify(tx: TransactionForContract,
- inputs: List,
- outputs: List,
- commands: List>,
- groupingKey: K?): Set {
- return matchedClauses(commands).flatMapTo(HashSet()) { clause ->
- clause.verify(tx, inputs, outputs, commands, groupingKey)
- }
- }
-
- override fun toString() = "All: $clauses.toList()"
-}
+@Deprecated("Use AllOf")
+class AllComposition(firstClause: Clause, vararg remainingClauses: Clause) : AllOf(firstClause, *remainingClauses)
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/AllOf.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/AllOf.kt
new file mode 100644
index 0000000000..3c88b8053c
--- /dev/null
+++ b/core/src/main/kotlin/net/corda/core/contracts/clauses/AllOf.kt
@@ -0,0 +1,38 @@
+package net.corda.core.contracts.clauses
+
+import net.corda.core.contracts.AuthenticatedObject
+import net.corda.core.contracts.CommandData
+import net.corda.core.contracts.ContractState
+import net.corda.core.contracts.TransactionForContract
+import java.util.*
+
+/**
+ * Compose a number of clauses, such that all of the clauses must run for verification to pass.
+ */
+open class AllOf(firstClause: Clause, vararg remainingClauses: Clause) : CompositeClause() {
+ override val clauses = ArrayList>()
+
+ init {
+ clauses.add(firstClause)
+ clauses.addAll(remainingClauses)
+ }
+
+ override fun matchedClauses(commands: List>): List> {
+ clauses.forEach { clause ->
+ check(clause.matches(commands)) { "Failed to match clause ${clause}" }
+ }
+ return clauses
+ }
+
+ override fun verify(tx: TransactionForContract,
+ inputs: List,
+ outputs: List,
+ commands: List>,
+ groupingKey: K?): Set {
+ return matchedClauses(commands).flatMapTo(HashSet()) { clause ->
+ clause.verify(tx, inputs, outputs, commands, groupingKey)
+ }
+ }
+
+ override fun toString() = "All: $clauses.toList()"
+}
diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/AnyComposition.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/AnyComposition.kt
index ecdd77bd4b..fbad044ca3 100644
--- a/core/src/main/kotlin/net/corda/core/contracts/clauses/AnyComposition.kt
+++ b/core/src/main/kotlin/net/corda/core/contracts/clauses/AnyComposition.kt
@@ -1,25 +1,10 @@
package net.corda.core.contracts.clauses
-import net.corda.core.contracts.AuthenticatedObject
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.ContractState
-import net.corda.core.contracts.TransactionForContract
-import java.util.*
/**
* Compose a number of clauses, such that any number of the clauses can run.
*/
-// TODO: Rename to AnyOf
-class AnyComposition(vararg rawClauses: Clause) : CompositeClause() {
- override val clauses: List> = rawClauses.asList()
-
- override fun matchedClauses(commands: List>): List> = clauses.filter { it.matches(commands) }
-
- override fun verify(tx: TransactionForContract, inputs: List, outputs: List, commands: List>, groupingKey: K?): Set {
- return matchedClauses(commands).flatMapTo(HashSet()) { clause ->
- clause.verify(tx, inputs, outputs, commands, groupingKey)
- }
- }
-
- override fun toString(): String = "Or: ${clauses.toList()}"
-}
+@Deprecated("Use AnyOf instead, although note that any of requires at least one matched clause")
+class AnyComposition(vararg rawClauses: Clause) : AnyOf(*rawClauses)
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/AnyOf.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/AnyOf.kt
new file mode 100644
index 0000000000..ceb732bea2
--- /dev/null
+++ b/core/src/main/kotlin/net/corda/core/contracts/clauses/AnyOf.kt
@@ -0,0 +1,28 @@
+package net.corda.core.contracts.clauses
+
+import net.corda.core.contracts.AuthenticatedObject
+import net.corda.core.contracts.CommandData
+import net.corda.core.contracts.ContractState
+import net.corda.core.contracts.TransactionForContract
+import java.util.*
+
+/**
+ * Compose a number of clauses, such that one or more of the clauses can run.
+ */
+open class AnyOf(vararg rawClauses: Clause) : CompositeClause() {
+ override val clauses: List> = rawClauses.toList()
+
+ override fun matchedClauses(commands: List>): List> {
+ val matched = clauses.filter { it.matches(commands) }
+ require(matched.isNotEmpty()) { "At least one clause must match" }
+ return matched
+ }
+
+ override fun verify(tx: TransactionForContract, inputs: List, outputs: List, commands: List>, groupingKey: K?): Set {
+ return matchedClauses(commands).flatMapTo(HashSet()) { clause ->
+ clause.verify(tx, inputs, outputs, commands, groupingKey)
+ }
+ }
+
+ override fun toString(): String = "Any: ${clauses.toList()}"
+}
diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/Clause.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/Clause.kt
index 5c3804ca1a..00409ce2c3 100644
--- a/core/src/main/kotlin/net/corda/core/contracts/clauses/Clause.kt
+++ b/core/src/main/kotlin/net/corda/core/contracts/clauses/Clause.kt
@@ -26,7 +26,11 @@ abstract class Clause {
/**
* Determine the subclauses which will be verified as a result of verifying this clause.
+ *
+ * @throws IllegalStateException if the given commands do not result in a valid execution (for example no match
+ * with [FirstOf]).
*/
+ @Throws(IllegalStateException::class)
open fun getExecutionPath(commands: List>): List>
= listOf(this)
diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/CompositeClause.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/CompositeClause.kt
index 875544ec0c..be0e711731 100644
--- a/core/src/main/kotlin/net/corda/core/contracts/clauses/CompositeClause.kt
+++ b/core/src/main/kotlin/net/corda/core/contracts/clauses/CompositeClause.kt
@@ -14,6 +14,12 @@ abstract class CompositeClause>): List>
= matchedClauses(commands).flatMap { it.getExecutionPath(commands) }
- /** Determine which clauses are matched by the supplied commands */
+ /**
+ * Determine which clauses are matched by the supplied commands.
+ *
+ * @throws IllegalStateException if the given commands do not result in a valid execution (for example no match
+ * with [FirstOf]).
+ */
+ @Throws(IllegalStateException::class)
abstract fun matchedClauses(commands: List>): List>
}
diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/FirstComposition.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/FirstComposition.kt
index adbf1647a5..0f1e00a72b 100644
--- a/core/src/main/kotlin/net/corda/core/contracts/clauses/FirstComposition.kt
+++ b/core/src/main/kotlin/net/corda/core/contracts/clauses/FirstComposition.kt
@@ -10,7 +10,7 @@ import java.util.*
/**
* Compose a number of clauses, such that the first match is run, and it errors if none is run.
*/
-// TODO: Rename to FirstOf
+@Deprecated("Use FirstOf instead")
class FirstComposition(val firstClause: Clause, vararg remainingClauses: Clause) : CompositeClause() {
companion object {
val logger = loggerFor>()
diff --git a/core/src/main/kotlin/net/corda/core/contracts/clauses/FirstOf.kt b/core/src/main/kotlin/net/corda/core/contracts/clauses/FirstOf.kt
new file mode 100644
index 0000000000..bac08f3f4d
--- /dev/null
+++ b/core/src/main/kotlin/net/corda/core/contracts/clauses/FirstOf.kt
@@ -0,0 +1,41 @@
+package net.corda.core.contracts.clauses
+
+import net.corda.core.contracts.AuthenticatedObject
+import net.corda.core.contracts.CommandData
+import net.corda.core.contracts.ContractState
+import net.corda.core.contracts.TransactionForContract
+import net.corda.core.utilities.loggerFor
+import java.util.*
+
+/**
+ * Compose a number of clauses, such that the first match is run, and it errors if none is run.
+ */
+class FirstOf(val firstClause: Clause, vararg remainingClauses: Clause) : CompositeClause() {
+ companion object {
+ val logger = loggerFor>()
+ }
+
+ override val clauses = ArrayList>()
+
+ /**
+ * Get the single matched clause from the set this composes, based on the given commands. This is provided as
+ * helper method for internal use, rather than using the exposed [matchedClauses] function which unnecessarily
+ * wraps the clause in a list.
+ */
+ private fun matchedClause(commands: List>): Clause {
+ return clauses.firstOrNull { it.matches(commands) } ?: throw IllegalStateException("No delegate clause matched in first composition")
+ }
+
+ override fun matchedClauses(commands: List>) = listOf(matchedClause(commands))
+
+ init {
+ clauses.add(firstClause)
+ clauses.addAll(remainingClauses)
+ }
+
+ override fun verify(tx: TransactionForContract, inputs: List, outputs: List, commands: List>, groupingKey: K?): Set {
+ return matchedClause(commands).verify(tx, inputs, outputs, commands, groupingKey)
+ }
+
+ override fun toString() = "First: ${clauses.toList()}"
+}
diff --git a/core/src/main/kotlin/net/corda/core/crypto/WhitelistTrustManager.kt b/core/src/main/kotlin/net/corda/core/crypto/WhitelistTrustManager.kt
deleted file mode 100644
index 678c6b551b..0000000000
--- a/core/src/main/kotlin/net/corda/core/crypto/WhitelistTrustManager.kt
+++ /dev/null
@@ -1,185 +0,0 @@
-package net.corda.core.crypto
-
-import sun.security.util.HostnameChecker
-import java.net.InetAddress
-import java.net.Socket
-import java.net.UnknownHostException
-import java.security.KeyStore
-import java.security.Provider
-import java.security.Security
-import java.security.cert.CertificateException
-import java.security.cert.X509Certificate
-import java.util.concurrent.ConcurrentHashMap
-import javax.net.ssl.*
-
-/**
- * Call this to change the default verification algorithm and this use the WhitelistTrustManager
- * implementation. This is a work around to the fact that ArtemisMQ and probably many other libraries
- * don't correctly configure the SSLParameters with setEndpointIdentificationAlgorithm and thus don't check
- * that the certificate matches with the DNS entry requested. This exposes us to man in the middle attacks.
- * The issue has been raised with ArtemisMQ: https://issues.apache.org/jira/browse/ARTEMIS-656
- */
-fun registerWhitelistTrustManager() {
- if (Security.getProvider("WhitelistTrustManager") == null) {
- WhitelistTrustManagerProvider.register()
- }
-
- // Forcibly change the TrustManagerFactory defaultAlgorithm to be us
- // This will apply to all code using TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
- // Which includes the standard HTTPS implementation and most other SSL code
- // TrustManagerFactory.getInstance(WhitelistTrustManagerProvider.originalTrustProviderAlgorithm)) will
- // allow access to the original implementation which is normally "PKIX"
- Security.setProperty("ssl.TrustManagerFactory.algorithm", "whitelistTrustManager")
-}
-
-/**
- * Custom Security Provider that forces the TrustManagerFactory to be our custom one.
- * Also holds the identity of the original TrustManager algorithm so
- * that we can delegate most of the checking to the proper Java code. We simply add some more checks.
- *
- * The whitelist automatically includes the local server DNS name and IP address
- *
- */
-object WhitelistTrustManagerProvider : Provider("WhitelistTrustManager",
- 1.0,
- "Provider for custom trust manager that always validates certificate names") {
-
- val originalTrustProviderAlgorithm = Security.getProperty("ssl.TrustManagerFactory.algorithm")
-
- private val _whitelist = ConcurrentHashMap.newKeySet()
- val whitelist: Set get() = _whitelist.toSet() // The acceptable IP and DNS names for clients and servers.
-
- init {
- // Add ourselves to whitelist as currently we have to connect to a local ArtemisMQ broker
- val host = InetAddress.getLocalHost()
- addWhitelistEntry(host.hostName)
- }
-
- /**
- * Security provider registration function for WhitelistTrustManagerProvider
- */
- fun register() {
- Security.addProvider(WhitelistTrustManagerProvider)
-
- // Register our custom TrustManagerFactorySpi
- put("TrustManagerFactory.whitelistTrustManager", "net.corda.core.crypto.WhitelistTrustManagerSpi")
- }
-
- /**
- * Adds an extra name to the whitelist if not already present
- * If this is a new entry it will internally request a DNS lookup which may block the calling thread.
- */
- fun addWhitelistEntry(serverName: String) {
- if (!_whitelist.contains(serverName)) { // Safe as we never delete from the set
- addWhitelistEntries(listOf(serverName))
- }
- }
-
- /**
- * Adds a list of servers to the whitelist and also adds their fully resolved name/ip address after DNS lookup
- * If the server name is not an actual DNS name this is silently ignored.
- * The DNS request may block the calling thread.
- */
- fun addWhitelistEntries(serverNames: List) {
- _whitelist.addAll(serverNames)
- for (name in serverNames) {
- try {
- val addresses = InetAddress.getAllByName(name).toList()
- _whitelist.addAll(addresses.map { y -> y.canonicalHostName })
- _whitelist.addAll(addresses.map { y -> y.hostAddress })
- } catch (ex: UnknownHostException) {
- // Ignore if the server name is not resolvable e.g. for wildcard addresses, or addresses that can only be resolved externally
- }
- }
- }
-}
-
-/**
- * Registered TrustManagerFactorySpi
- */
-class WhitelistTrustManagerSpi : TrustManagerFactorySpi() {
- // Get the original implementation to delegate to (can't use Kotlin delegation on abstract classes unfortunately).
- val originalProvider = TrustManagerFactory.getInstance(WhitelistTrustManagerProvider.originalTrustProviderAlgorithm)
-
- override fun engineInit(keyStore: KeyStore?) {
- originalProvider.init(keyStore)
- }
-
- override fun engineInit(managerFactoryParameters: ManagerFactoryParameters?) {
- originalProvider.init(managerFactoryParameters)
- }
-
- override fun engineGetTrustManagers(): Array {
- val parent = originalProvider.trustManagers.first() as X509ExtendedTrustManager
- // Wrap original provider in ours and return
- return arrayOf(WhitelistTrustManager(parent))
- }
-}
-
-/**
- * Our TrustManager extension takes the standard certificate checker and first delegates all the
- * chain checking to that. If everything is well formed we then simply add a check against our whitelist
- */
-class WhitelistTrustManager(val originalProvider: X509ExtendedTrustManager) : X509ExtendedTrustManager() {
- // Use same Helper class as standard HTTPS library validator
- val checker = HostnameChecker.getInstance(HostnameChecker.TYPE_TLS)
-
- private fun checkIdentity(hostname: String?, cert: X509Certificate) {
- // Based on standard code in sun.security.ssl.X509TrustManagerImpl.checkIdentity
- // if IPv6 strip off the "[]"
- if ((hostname != null) && hostname.startsWith("[") && hostname.endsWith("]")) {
- checker.match(hostname.substring(1, hostname.length - 1), cert)
- } else {
- checker.match(hostname, cert)
- }
- }
-
- /**
- * scan whitelist and confirm the certificate matches at least one entry
- */
- private fun checkWhitelist(cert: X509Certificate) {
- for (whiteListEntry in WhitelistTrustManagerProvider.whitelist) {
- try {
- checkIdentity(whiteListEntry, cert)
- return // if we get here without throwing we had a match
- } catch(ex: CertificateException) {
- // Ignore and check the next entry until we find a match, or exhaust the whitelist
- }
- }
- throw CertificateException("Certificate not on whitelist ${cert.subjectDN}")
- }
-
- override fun checkClientTrusted(chain: Array, authType: String, socket: Socket?) {
- originalProvider.checkClientTrusted(chain, authType, socket)
- checkWhitelist(chain[0])
- }
-
- override fun checkClientTrusted(chain: Array, authType: String, engine: SSLEngine?) {
- originalProvider.checkClientTrusted(chain, authType, engine)
- checkWhitelist(chain[0])
- }
-
- override fun checkClientTrusted(chain: Array, authType: String) {
- originalProvider.checkClientTrusted(chain, authType)
- checkWhitelist(chain[0])
- }
-
- override fun checkServerTrusted(chain: Array, authType: String, socket: Socket?) {
- originalProvider.checkServerTrusted(chain, authType, socket)
- checkWhitelist(chain[0])
- }
-
- override fun checkServerTrusted(chain: Array, authType: String, engine: SSLEngine?) {
- originalProvider.checkServerTrusted(chain, authType, engine)
- checkWhitelist(chain[0])
- }
-
- override fun checkServerTrusted(chain: Array, authType: String) {
- originalProvider.checkServerTrusted(chain, authType)
- checkWhitelist(chain[0])
- }
-
- override fun getAcceptedIssuers(): Array {
- return originalProvider.acceptedIssuers
- }
-}
diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt
index 08f619797d..0f7bf0a58c 100644
--- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt
+++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt
@@ -26,21 +26,18 @@ import rx.Observable
* it to the [subFlow] method. It will return the result of that flow when it completes.
*/
abstract class FlowLogic {
-
- /** Reference to the [Fiber] instance that is the top level controller for the entire flow. */
- lateinit var fsm: FlowStateMachine<*>
-
/** This is where you should log things to. */
- val logger: Logger get() = fsm.logger
+ val logger: Logger get() = stateMachine.logger
+
+ /** Returns a wrapped [UUID] object that identifies this state machine run (i.e. subflows have the same identifier as their parents). */
+ val runId: StateMachineRunId get() = stateMachine.id
/**
* Provides access to big, heavy classes that may be reconstructed from time to time, e.g. across restarts. It is
* only available once the flow has started, which means it cannnot be accessed in the constructor. Either
* access this lazily or from inside [call].
*/
- val serviceHub: ServiceHub get() = fsm.serviceHub
-
- private var sessionFlow: FlowLogic<*> = this
+ val serviceHub: ServiceHub get() = stateMachine.serviceHub
/**
* Return the marker [Class] which [party] has used to register the counterparty flow that is to execute on the
@@ -49,35 +46,73 @@ abstract class FlowLogic {
*/
open fun getCounterpartyMarker(party: Party): Class<*> = javaClass
- // Kotlin helpers that allow the use of generic types.
- inline fun sendAndReceive(otherParty: Party, payload: Any): UntrustworthyData {
- return sendAndReceive(otherParty, payload, T::class.java)
- }
-
- // TODO: Move the receiveType param to first position for readability
+ /**
+ * Serializes and queues the given [payload] object for sending to the [otherParty]. Suspends until a response
+ * is received, which must be of the given [R] type.
+ *
+ * Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly
+ * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
+ * corrupted data in order to exploit your code.
+ *
+ * Note that this function is not just a simple send+receive pair: it is more efficient and more correct to
+ * use this when you expect to do a message swap than do use [send] and then [receive] in turn.
+ *
+ * @returns an [UntrustworthyData] wrapper around the received object.
+ */
+ inline fun sendAndReceive(otherParty: Party, payload: Any) = sendAndReceive(R::class.java, otherParty, payload)
+ /**
+ * Serializes and queues the given [payload] object for sending to the [otherParty]. Suspends until a response
+ * is received, which must be of the given [receiveType]. Remember that when receiving data from other parties the data
+ * should not be trusted until it's been thoroughly verified for consistency and that all expectations are
+ * satisfied, as a malicious peer may send you subtly corrupted data in order to exploit your code.
+ *
+ * Note that this function is not just a simple send+receive pair: it is more efficient and more correct to
+ * use this when you expect to do a message swap than do use [send] and then [receive] in turn.
+ *
+ * @returns an [UntrustworthyData] wrapper around the received object.
+ */
@Suspendable
- fun sendAndReceive(otherParty: Party, payload: Any, receiveType: Class): UntrustworthyData {
- return fsm.sendAndReceive(otherParty, payload, receiveType, sessionFlow)
- }
-
- inline fun receive(otherParty: Party): UntrustworthyData = receive(otherParty, T::class.java)
-
- // TODO: Move the receiveType param to first position for readability
-
- @Suspendable
- fun receive(otherParty: Party, receiveType: Class): UntrustworthyData {
- return fsm.receive(otherParty, receiveType, sessionFlow)
- }
-
- @Suspendable
- fun send(otherParty: Party, payload: Any) {
- fsm.send(otherParty, payload, sessionFlow)
+ open fun sendAndReceive(receiveType: Class, otherParty: Party, payload: Any): UntrustworthyData {
+ return stateMachine.sendAndReceive(receiveType, otherParty, payload, sessionFlow)
}
/**
- * Invokes the given subflow by simply passing through this [FlowLogic]s reference to the
- * [FlowStateMachine] and then calling the [call] method.
+ * Suspends until the specified [otherParty] sends us a message of type [R].
+ *
+ * Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly
+ * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
+ * corrupted data in order to exploit your code.
+ */
+ inline fun receive(otherParty: Party): UntrustworthyData = receive(R::class.java, otherParty)
+
+ /**
+ * Suspends until the specified [otherParty] sends us a message of type [receiveType].
+ *
+ * Remember that when receiving data from other parties the data should not be trusted until it's been thoroughly
+ * verified for consistency and that all expectations are satisfied, as a malicious peer may send you subtly
+ * corrupted data in order to exploit your code.
+ */
+ @Suspendable
+ open fun receive(receiveType: Class, otherParty: Party): UntrustworthyData {
+ return stateMachine.receive(receiveType, otherParty, sessionFlow)
+ }
+
+ /**
+ * Queues the given [payload] for sending to the [otherParty] and continues without suspending.
+ *
+ * Note that the other party may receive the message at some arbitrary later point or not at all: if [otherParty]
+ * is offline then message delivery will be retried until it comes back or until the message is older than the
+ * network's event horizon time.
+ */
+ @Suspendable
+ open fun send(otherParty: Party, payload: Any) = stateMachine.send(otherParty, payload, sessionFlow)
+
+ /**
+ * Invokes the given subflow. This function returns once the subflow completes successfully with the result
+ * returned by that subflows [call] method. If the subflow has a progress tracker, it is attached to the
+ * current step in this flow's progress tracker.
+ *
* @param shareParentSessions In certain situations the need arises to use the same sessions the parent flow has
* already established. However this also prevents the subflow from creating new sessions with those parties.
* For this reason the default value is false.
@@ -85,8 +120,9 @@ abstract class FlowLogic {
// TODO Rethink the default value for shareParentSessions
// TODO shareParentSessions is a bit too low-level and perhaps can be expresed in a better way
@Suspendable
- fun subFlow(subLogic: FlowLogic, shareParentSessions: Boolean = false): R {
- subLogic.fsm = fsm
+ @JvmOverloads
+ open fun subFlow(subLogic: FlowLogic, shareParentSessions: Boolean = false): R {
+ subLogic.stateMachine = stateMachine
maybeWireUpProgressTracking(subLogic)
if (shareParentSessions) {
subLogic.sessionFlow = this
@@ -97,6 +133,52 @@ abstract class FlowLogic {
return result
}
+ /**
+ * Override this to provide a [ProgressTracker]. If one is provided and stepped, the framework will do something
+ * helpful with the progress reports. If this flow is invoked as a subflow of another, then the
+ * tracker will be made a child of the current step in the parent. If it's null, this flow doesn't track
+ * progress.
+ *
+ * Note that this has to return a tracker before the flow is invoked. You can't change your mind half way
+ * through.
+ */
+ open val progressTracker: ProgressTracker? = null
+
+ /**
+ * This is where you fill out your business logic. The returned object will usually be ignored, but can be
+ * helpful if this flow is meant to be used as a subflow.
+ */
+ @Suspendable
+ abstract fun call(): T
+
+ /**
+ * Returns a pair of the current progress step, as a string, and an observable of stringified changes to the
+ * [progressTracker].
+ *
+ * @return Returns null if this flow has no progress tracker.
+ */
+ fun track(): Pair>? {
+ // TODO this is not threadsafe, needs an atomic get-step-and-subscribe
+ return progressTracker?.let {
+ Pair(it.currentStep.toString(), it.changes.map { it.toString() })
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ private var _stateMachine: FlowStateMachine<*>? = null
+ /**
+ * Internal only. Reference to the [Fiber] instance that is the top level controller for the entire flow. When
+ * inside a flow this is equivalent to [Strand.currentStrand]. This is public only because it must be accessed
+ * across module boundaries.
+ */
+ var stateMachine: FlowStateMachine<*>
+ get() = _stateMachine ?: throw IllegalStateException("This can only be done after the flow has been started.")
+ set(value) { _stateMachine = value }
+
+ // This points to the outermost flow and is changed when a subflow is invoked.
+ private var sessionFlow: FlowLogic<*> = this
+
private fun maybeWireUpProgressTracking(subLogic: FlowLogic<*>) {
val ours = progressTracker
@@ -109,27 +191,4 @@ abstract class FlowLogic {
ours.setChildProgressTracker(ours.currentStep, theirs)
}
}
-
- /**
- * Override this to provide a [ProgressTracker]. If one is provided and stepped, the framework will do something
- * helpful with the progress reports. If this flow is invoked as a sub-flow of another, then the
- * tracker will be made a child of the current step in the parent. If it's null, this flow doesn't track
- * progress.
- *
- * Note that this has to return a tracker before the flow is invoked. You can't change your mind half way
- * through.
- */
- open val progressTracker: ProgressTracker? = null
-
- /** This is where you fill out your business logic. */
- @Suspendable
- abstract fun call(): T
-
- // TODO this is not threadsafe, needs an atomic get-step-and-subscribe
- fun track(): Pair>? {
- return progressTracker?.let {
- Pair(it.currentStep.toString(), it.changes.map { it.toString() })
- }
- }
-
}
diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowStateMachine.kt b/core/src/main/kotlin/net/corda/core/flows/FlowStateMachine.kt
index f8e6d8c9b4..fa290bfab8 100644
--- a/core/src/main/kotlin/net/corda/core/flows/FlowStateMachine.kt
+++ b/core/src/main/kotlin/net/corda/core/flows/FlowStateMachine.kt
@@ -8,8 +8,11 @@ import net.corda.core.utilities.UntrustworthyData
import org.slf4j.Logger
import java.util.*
+/**
+ * A unique identifier for a single state machine run, valid across node restarts. Note that a single run always
+ * has at least one flow, but that flow may also invoke sub-flows: they all share the same run id.
+ */
data class StateMachineRunId private constructor(val uuid: UUID) {
-
companion object {
fun createRandom(): StateMachineRunId = StateMachineRunId(UUID.randomUUID())
fun wrap(uuid: UUID): StateMachineRunId = StateMachineRunId(uuid)
@@ -18,34 +21,24 @@ data class StateMachineRunId private constructor(val uuid: UUID) {
override fun toString(): String = "[$uuid]"
}
-/**
- * A FlowStateMachine instance is a suspendable fiber that delegates all actual logic to a [FlowLogic] instance.
- * For any given flow there is only one PSM, even if that flow invokes subflows.
- *
- * These classes are created by the [StateMachineManager] when a new flow is started at the topmost level. If
- * a flow invokes a sub-flow, then it will pass along the PSM to the child. The call method of the topmost
- * logic element gets to return the value that the entire state machine resolves to.
- */
+/** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */
interface FlowStateMachine {
@Suspendable
- fun sendAndReceive(otherParty: Party,
+ fun sendAndReceive(receiveType: Class,
+ otherParty: Party,
payload: Any,
- receiveType: Class,
sessionFlow: FlowLogic<*>): UntrustworthyData
@Suspendable
- fun receive(otherParty: Party, receiveType: Class, sessionFlow: FlowLogic<*>): UntrustworthyData
+ fun receive(receiveType: Class, otherParty: Party, sessionFlow: FlowLogic<*>): UntrustworthyData
@Suspendable
fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>)
val serviceHub: ServiceHub
val logger: Logger
-
- /** Unique ID for this machine run, valid across restarts */
val id: StateMachineRunId
- /** This future will complete when the call method returns. */
val resultFuture: ListenableFuture
}
-class FlowSessionException(message: String) : Exception(message)
+class FlowException(message: String) : RuntimeException(message)
diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt
index 5b5b8ba8a6..e4d43df5d9 100644
--- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt
+++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt
@@ -23,8 +23,12 @@ data class StateMachineInfo(
)
sealed class StateMachineUpdate(val id: StateMachineRunId) {
- class Added(val stateMachineInfo: StateMachineInfo) : StateMachineUpdate(stateMachineInfo.id)
- class Removed(id: StateMachineRunId) : StateMachineUpdate(id)
+ class Added(val stateMachineInfo: StateMachineInfo) : StateMachineUpdate(stateMachineInfo.id) {
+ override fun toString() = "Added($id, ${stateMachineInfo.flowLogicClassName})"
+ }
+ class Removed(id: StateMachineRunId) : StateMachineUpdate(id) {
+ override fun toString() = "Removed($id)"
+ }
}
/**
@@ -172,5 +176,6 @@ inline fun > CordaRPCOps.startFlow
data class FlowHandle(
val id: StateMachineRunId,
val progress: Observable,
+ // TODO This should be ListenableFuture
val returnValue: Observable
)
diff --git a/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt b/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt
index 0a55d7dca4..6b29086153 100644
--- a/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt
+++ b/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt
@@ -4,6 +4,7 @@ import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import net.corda.core.catch
import net.corda.core.node.services.DEFAULT_SESSION_ID
+import net.corda.core.node.services.PartyInfo
import net.corda.core.serialization.DeserializeAsKotlinObjectDef
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
@@ -79,6 +80,9 @@ interface MessagingService {
*/
fun createMessage(topicSession: TopicSession, data: ByteArray, uuid: UUID = UUID.randomUUID()): Message
+ /** Given information about either a specific node or a service returns its corresponding address */
+ fun getAddressOfParty(partyInfo: PartyInfo): MessageRecipients
+
/** Returns an address that refers to this node. */
val myAddress: SingleMessageRecipient
}
@@ -127,7 +131,7 @@ inline fun MessagingService.runOnNextMessage(topicSession: TopicSession, crossin
/**
* Returns a [ListenableFuture] of the next message payload ([Message.data]) which is received on the given topic and sessionId.
- * The payload is deserilaized to an object of type [M]. Any exceptions thrown will be captured by the future.
+ * The payload is deserialized to an object of type [M]. Any exceptions thrown will be captured by the future.
*/
fun MessagingService.onNext(topic: String, sessionId: Long): ListenableFuture {
val messageFuture = SettableFuture.create()
diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt
index 9542136c72..8ddb260ed0 100644
--- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt
+++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt
@@ -58,6 +58,7 @@ interface ServiceHub {
/**
* Will check [logicType] and [args] against a whitelist and if acceptable then construct and initiate the flow.
+ * Note that you must be on the server thread to call this method.
*
* @throws IllegalFlowLogicException or IllegalArgumentException if there are problems with the [logicType] or [args].
*/
@@ -81,7 +82,6 @@ interface ServiceHub {
* Typical use is during signing in flows and for unit test signing.
*/
val notaryIdentityKey: KeyPair get() = this.keyManagementService.toKeyPair(this.myInfo.notaryIdentity.owningKey.keys)
-
}
/**
diff --git a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt
index 4aad949d96..86c6fdbae3 100644
--- a/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt
+++ b/core/src/main/kotlin/net/corda/core/node/services/NetworkMapCache.kt
@@ -5,9 +5,11 @@ import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.contracts.Contract
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.Party
+import net.corda.core.messaging.MessageRecipients
import net.corda.core.messaging.MessagingService
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.NodeInfo
+import net.corda.core.node.ServiceEntry
import net.corda.core.randomOrNull
import rx.Observable
@@ -63,31 +65,27 @@ interface NetworkMapCache {
/** Look up the node info for a legal name. */
fun getNodeByLegalName(name: String): NodeInfo? = partyNodes.singleOrNull { it.legalIdentity.name == name }
- /** Look up the node info for a composite key. */
- fun getNodeByCompositeKey(compositeKey: CompositeKey): NodeInfo? {
+ /**
+ * In general, nodes can advertise multiple identities: a legal identity, and separate identities for each of
+ * the services it provides. In case of a distributed service – run by multiple nodes – each participant advertises
+ * the identity of the *whole group*.
+ */
+
+ /** Look up the node info for a specific peer key. */
+ fun getNodeByLegalIdentityKey(compositeKey: CompositeKey): NodeInfo? {
// Although we should never have more than one match, it is theoretically possible. Report an error if it happens.
- val candidates = partyNodes.filter {
- (it.legalIdentity.owningKey == compositeKey)
- || it.advertisedServices.any { it.identity.owningKey == compositeKey }
- }
+ val candidates = partyNodes.filter { it.legalIdentity.owningKey == compositeKey }
check(candidates.size <= 1) { "Found more than one match for key $compositeKey" }
return candidates.singleOrNull()
}
-
- /**
- * Given a [party], returns a node advertising it as an identity. If more than one node found the result
- * is chosen at random.
- *
- * In general, nodes can advertise multiple identities: a legal identity, and separate identities for each of
- * the services it provides. In case of a distributed service – run by multiple nodes – each participant advertises
- * the identity of the *whole group*. If the provided [party] is a group identity, multiple nodes advertising it
- * will be found, and this method will return a randomly chosen one. If [party] is an individual (legal) identity,
- * we currently assume that it will be advertised by one node only, which will be returned as the result.
- */
- fun getRepresentativeNode(party: Party): NodeInfo? {
- return partyNodes.randomOrNull { it.legalIdentity == party || it.advertisedServices.any { it.identity == party } }
+ /** Look up all nodes advertising the service owned by [compositeKey] */
+ fun getNodesByAdvertisedServiceIdentityKey(compositeKey: CompositeKey): List {
+ return partyNodes.filter { it.advertisedServices.any { it.identity.owningKey == compositeKey } }
}
+ /** Returns information about the party, which may be a specific node or a service */
+ fun getPartyInfo(party: Party): PartyInfo?
+
/** Gets a notary identity by the given name. */
fun getNotary(name: String): Party? {
val notaryNode = notaryNodes.randomOrNull {
diff --git a/core/src/main/kotlin/net/corda/core/node/services/PartyInfo.kt b/core/src/main/kotlin/net/corda/core/node/services/PartyInfo.kt
new file mode 100644
index 0000000000..b34086992a
--- /dev/null
+++ b/core/src/main/kotlin/net/corda/core/node/services/PartyInfo.kt
@@ -0,0 +1,18 @@
+package net.corda.core.node.services
+
+import net.corda.core.crypto.Party
+import net.corda.core.node.NodeInfo
+import net.corda.core.node.ServiceEntry
+
+/**
+ * Holds information about a [Party], which may refer to either a specific node or a service.
+ */
+sealed class PartyInfo() {
+ abstract val party: Party
+ class Node(val node: NodeInfo) : PartyInfo() {
+ override val party = node.legalIdentity
+ }
+ class Service(val service: ServiceEntry) : PartyInfo() {
+ override val party = service.identity
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/net/corda/core/node/services/ServiceType.kt b/core/src/main/kotlin/net/corda/core/node/services/ServiceType.kt
index 439ae3f5bc..66f58548d4 100644
--- a/core/src/main/kotlin/net/corda/core/node/services/ServiceType.kt
+++ b/core/src/main/kotlin/net/corda/core/node/services/ServiceType.kt
@@ -11,15 +11,9 @@ sealed class ServiceType(val id: String) {
//
// * IDs must start with a lower case letter
// * IDs can only contain alphanumeric, full stop and underscore ASCII characters
- require(id.matches(Regex("[a-z][a-zA-Z0-9._]+")))
- }
-
- private class ServiceTypeImpl(baseId: String, subTypeId: String) : ServiceType("$baseId.$subTypeId") {
- init {
- // only allowed one level of subtype
- require(subTypeId.matches(Regex("[a-z]\\w+")))
- }
+ require(id.matches(Regex("[a-z][a-zA-Z0-9._]+"))) { id }
}
+ private class ServiceTypeImpl(baseId: String, subTypeId: String) : ServiceType("$baseId.$subTypeId")
private class ServiceTypeDirect(id: String) : ServiceType(id)
diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt
index 5f5215f1ac..3de6b3f864 100644
--- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt
+++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt
@@ -1,12 +1,12 @@
package net.corda.core.node.services
import com.google.common.util.concurrent.ListenableFuture
-import com.google.common.util.concurrent.SettableFuture
import net.corda.core.contracts.*
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.Party
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.toStringShort
+import net.corda.core.toFuture
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import rx.Observable
@@ -38,7 +38,7 @@ val DEFAULT_SESSION_ID = 0L
* Active means they haven't been consumed yet (or we don't know about it).
* Relevant means they contain at least one of our pubkeys.
*/
-class Vault(val states: Iterable>) {
+class Vault(val states: List>) {
@Suppress("UNCHECKED_CAST")
inline fun statesOfType() = states.filter { it.state.data is T } as List>
@@ -50,7 +50,10 @@ class Vault(val states: Iterable>) {
* If the vault observes multiple transactions simultaneously, where some transactions consume the outputs of some of the
* other transactions observed, then the changes are observed "net" of those.
*/
- data class Update(val consumed: Set, val produced: Set>) {
+ data class Update(val consumed: Set