diff --git a/.gitignore b/.gitignore
index 295c86e572..b7ebccf366 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ tags
/experimental/build
/docs/build/doctrees
/test-utils/build
+/client/build
# gradle's buildSrc build/
/buildSrc/build/
diff --git a/.idea/modules.xml b/.idea/modules.xml
index e284ae5b60..df2780c199 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -5,6 +5,9 @@
+
+
+
diff --git a/client/build.gradle b/client/build.gradle
new file mode 100644
index 0000000000..5fe9e7006b
--- /dev/null
+++ b/client/build.gradle
@@ -0,0 +1,53 @@
+apply plugin: 'kotlin'
+apply plugin: QuasarPlugin
+
+repositories {
+ mavenLocal()
+ mavenCentral()
+ maven {
+ url 'http://oss.sonatype.org/content/repositories/snapshots'
+ }
+ jcenter()
+ maven {
+ url 'https://dl.bintray.com/kotlin/exposed'
+ }
+}
+
+
+//noinspection GroovyAssignabilityCheck
+configurations {
+
+ // we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
+ runtime.exclude module: 'isolated'
+}
+
+sourceSets {
+ test {
+ resources {
+ srcDir "../config/test"
+ }
+ }
+}
+
+// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
+// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
+
+dependencies {
+ compile project(':node')
+
+ // Log4J: logging framework (with SLF4J bindings)
+ 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"
+
+ // ReactFX: Functional reactive UI programming.
+ compile 'org.reactfx:reactfx:2.0-M5'
+ compile 'org.fxmisc.easybind:easybind:1.0.3'
+
+ // Unit testing helpers.
+ testCompile 'junit:junit:4.12'
+ testCompile "org.assertj:assertj-core:${assertj_version}"
+}
+
+quasarScan.dependsOn('classes', ':core:classes', ':contracts:classes')
diff --git a/client/src/main/kotlin/com/r3corda/client/WalletMonitorClient.kt b/client/src/main/kotlin/com/r3corda/client/WalletMonitorClient.kt
new file mode 100644
index 0000000000..bc48f8ed06
--- /dev/null
+++ b/client/src/main/kotlin/com/r3corda/client/WalletMonitorClient.kt
@@ -0,0 +1,61 @@
+package com.r3corda.client
+
+import com.google.common.util.concurrent.ListenableFuture
+import com.google.common.util.concurrent.SettableFuture
+import com.r3corda.core.contracts.ClientToServiceCommand
+import com.r3corda.core.messaging.MessagingService
+import com.r3corda.core.node.NodeInfo
+import com.r3corda.core.random63BitValue
+import com.r3corda.core.serialization.deserialize
+import com.r3corda.core.serialization.serialize
+import com.r3corda.node.services.monitor.*
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import rx.Observable
+import rx.Observer
+
+/**
+ * Worked example of a client which communicates with the wallet monitor service.
+ */
+
+private val log: Logger = LoggerFactory.getLogger("WalletMonitorClient")
+
+class WalletMonitorClient(
+ val net: MessagingService,
+ val node: NodeInfo,
+ val outEvents: Observable,
+ val inEvents: Observer
+) {
+ private val sessionID = random63BitValue()
+
+ fun register(): ListenableFuture {
+
+ val future = SettableFuture.create()
+ log.info("Registering with ID $sessionID. I am ${net.myAddress}")
+ net.addMessageHandler(WalletMonitorService.REGISTER_TOPIC, sessionID) { msg, reg ->
+ val resp = msg.data.deserialize()
+ net.removeMessageHandler(reg)
+ future.set(resp.success)
+ }
+ net.addMessageHandler(WalletMonitorService.STATE_TOPIC, sessionID) { msg, req ->
+ // TODO
+ }
+
+ net.addMessageHandler(WalletMonitorService.IN_EVENT_TOPIC, sessionID) { msg, reg ->
+ val event = msg.data.deserialize()
+ inEvents.onNext(event)
+ }
+
+ val req = RegisterRequest(net.myAddress, sessionID)
+ val registerMessage = net.createMessage(WalletMonitorService.REGISTER_TOPIC, 0, req.serialize().bits)
+ net.send(registerMessage, node.address)
+
+ outEvents.subscribe { event ->
+ val envelope = ClientToServiceCommandMessage(sessionID, net.myAddress, event)
+ val message = net.createMessage(WalletMonitorService.OUT_EVENT_TOPIC, 0, envelope.serialize().bits)
+ net.send(message, node.address)
+ }
+
+ return future
+ }
+}
diff --git a/client/src/main/kotlin/com/r3corda/client/testing/Expect.kt b/client/src/main/kotlin/com/r3corda/client/testing/Expect.kt
new file mode 100644
index 0000000000..f3d1113dc8
--- /dev/null
+++ b/client/src/main/kotlin/com/r3corda/client/testing/Expect.kt
@@ -0,0 +1,212 @@
+package com.r3corda.client.testing
+
+import co.paralleluniverse.strands.SettableFuture
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import rx.Observable
+
+/**
+ * This file defines a simple DSL for testing non-deterministic sequence of events arriving on an [Observable].
+ *
+ * [sequence] is used to impose ordering invariants on the stream, whereas [parallel] allows events to arrive in any order.
+ *
+ * The only restriction on [parallel] is that we should be able to discriminate which branch to take based on the
+ * arrived event's type. If this is ambiguous the first matching piece of DSL will be run.
+
+ * [sequence]s and [parallel]s can be nested arbitrarily
+ *
+ * Example usage:
+ *
+ * val stream: Observable = (..)
+ * stream.expectEvents(
+ * sequence(
+ * expect { event: SomeEvent.A -> require(event.isOk()) },
+ * parallel(
+ * expect { event.SomeEvent.B -> },
+ * expect { event.SomeEvent.C -> }
+ * )
+ * )
+ * )
+ *
+ * The above will test our expectation that the stream should first emit an A, and then a B and C in unspecified order.
+ */
+
+private val log: Logger = LoggerFactory.getLogger("Expect")
+
+/**
+ * Expect an event of type [T] and run [expectClosure] on it
+ */
+inline fun expect(noinline expectClosure: (T) -> Unit) = expect(T::class.java, expectClosure)
+fun expect(klass: Class, expectClosure: (T) -> Unit): ExpectCompose {
+ return ExpectCompose.Single(Expect(klass, expectClosure))
+}
+
+/**
+ * Tests that events arrive in the specified order.
+ *
+ * @param expectations The pieces of DSL that should run sequentially when events arrive.
+ */
+fun sequence(vararg expectations: ExpectCompose): ExpectCompose = ExpectCompose.Sequential(listOf(*expectations))
+
+/**
+ * Tests that events arrive in unspecified order.
+ *
+ * @param expectations The pieces of DSL all of which should run but in an unspecified order depending on what sequence events arrive.
+ */
+fun parallel(vararg expectations: ExpectCompose): ExpectCompose = ExpectCompose.Parallel(listOf(*expectations))
+
+/**
+ * Tests that N events of the same type arrive
+ *
+ * @param number The number of events expected.
+ * @param expectation The piece of DSL to run on each event, with the index of the event passed in.
+ */
+inline fun repeat(number: Int, expectation: (Int) -> ExpectCompose) = sequence(*Array(number) { expectation(it) })
+
+/**
+ * Run the specified DSL against the event stream.
+ *
+ * @param isStrict If false non-matched events are disregarded (so the DSL will only check a subset of events).
+ * @param expectCompose The DSL we expect to match against the stream of events.
+ */
+fun Observable.expectEvents(isStrict: Boolean = true, expectCompose: () -> ExpectCompose) {
+ val finishFuture = SettableFuture()
+ val stateLock = object {}
+ var state = ExpectComposeState.fromExpectCompose(expectCompose())
+ subscribe { event ->
+ synchronized(stateLock) {
+ if (state is ExpectComposeState.Finished) {
+ log.warn("Got event $event, but was expecting no further events")
+ return@subscribe
+ }
+ val next = state.nextState(event)
+ log.info("$event :: ${state.getExpectedEvents()} -> ${next?.second?.getExpectedEvents()}")
+ if (next == null) {
+ val expectedStates = state.getExpectedEvents()
+ val message = "Got $event, expected one of $expectedStates"
+ if (isStrict) {
+ finishFuture.setException(Exception(message))
+ state = ExpectComposeState.Finished()
+ } else {
+ log.warn("$message, discarding event as isStrict=false")
+ }
+ } else {
+ state = next.second
+ try {
+ next.first()
+ } catch (exception: Exception) {
+ finishFuture.setException(exception)
+ }
+ if (state is ExpectComposeState.Finished) {
+ finishFuture.set(Unit)
+ }
+ }
+ }
+ }
+ finishFuture.get()
+}
+
+sealed class ExpectCompose {
+ internal class Single(val expect: Expect) : ExpectCompose()
+ internal class Sequential(val sequence: List>) : ExpectCompose()
+ internal class Parallel(val parallel: List>) : ExpectCompose()
+}
+
+internal data class Expect(
+ val clazz: Class,
+ val expectClosure: (T) -> Unit
+)
+
+private sealed class ExpectComposeState {
+
+ abstract fun nextState(event: E): Pair<() -> Unit, ExpectComposeState>?
+ abstract fun getExpectedEvents(): List>
+
+ class Finished : ExpectComposeState() {
+ override fun nextState(event: E) = null
+ override fun getExpectedEvents(): List> = listOf()
+ }
+ class Single(val single: ExpectCompose.Single) : ExpectComposeState() {
+ override fun nextState(event: E): Pair<() -> Unit, ExpectComposeState>? =
+ if (single.expect.clazz.isAssignableFrom(event.javaClass)) {
+ @Suppress("UNCHECKED_CAST")
+ Pair({ single.expect.expectClosure(event) }, Finished())
+ } else {
+ null
+ }
+ override fun getExpectedEvents() = listOf(single.expect.clazz)
+ }
+
+ class Sequential(
+ val sequential: ExpectCompose.Sequential,
+ val index: Int,
+ val state: ExpectComposeState
+ ) : ExpectComposeState() {
+ override fun nextState(event: E): Pair<() -> Unit, ExpectComposeState>? {
+ val next = state.nextState(event)
+ return if (next == null) {
+ null
+ } else if (next.second is Finished) {
+ if (index == sequential.sequence.size - 1) {
+ Pair(next.first, Finished())
+ } else {
+ val nextState = fromExpectCompose(sequential.sequence[index + 1])
+ if (nextState is Finished) {
+ Pair(next.first, Finished())
+ } else {
+ Pair(next.first, Sequential(sequential, index + 1, nextState))
+ }
+ }
+ } else {
+ Pair(next.first, Sequential(sequential, index, next.second))
+ }
+ }
+
+ override fun getExpectedEvents() = state.getExpectedEvents()
+ }
+
+ class Parallel(
+ val parallel: ExpectCompose.Parallel,
+ val states: List>
+ ) : ExpectComposeState() {
+ override fun nextState(event: E): Pair<() -> Unit, ExpectComposeState>? {
+ states.forEachIndexed { stateIndex, state ->
+ val next = state.nextState(event)
+ if (next != null) {
+ val nextStates = states.mapIndexed { i, expectComposeState ->
+ if (i == stateIndex) next.second else expectComposeState
+ }
+ if (nextStates.all { it is Finished }) {
+ return Pair(next.first, Finished())
+ } else {
+ return Pair(next.first, Parallel(parallel, nextStates))
+ }
+ }
+ }
+ return null
+ }
+ override fun getExpectedEvents() = states.flatMap { it.getExpectedEvents() }
+ }
+
+ companion object {
+ fun fromExpectCompose(expectCompose: ExpectCompose): ExpectComposeState {
+ return when (expectCompose) {
+ is ExpectCompose.Single -> Single(expectCompose)
+ is ExpectCompose.Sequential -> {
+ if (expectCompose.sequence.size > 0) {
+ Sequential(expectCompose, 0, fromExpectCompose(expectCompose.sequence[0]))
+ } else {
+ Finished()
+ }
+ }
+ is ExpectCompose.Parallel -> {
+ if (expectCompose.parallel.size > 0) {
+ Parallel(expectCompose, expectCompose.parallel.map { fromExpectCompose(it) })
+ } else {
+ Finished()
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/client/src/test/kotlin/com/r3corda/client/WalletMonitorClientTests.kt b/client/src/test/kotlin/com/r3corda/client/WalletMonitorClientTests.kt
new file mode 100644
index 0000000000..9626185e28
--- /dev/null
+++ b/client/src/test/kotlin/com/r3corda/client/WalletMonitorClientTests.kt
@@ -0,0 +1,259 @@
+package com.r3corda.client
+
+import co.paralleluniverse.strands.SettableFuture
+import com.r3corda.client.testing.*
+import com.r3corda.core.contracts.*
+import com.r3corda.core.serialization.OpaqueBytes
+import com.r3corda.node.driver.driver
+import com.r3corda.node.driver.startClient
+import com.r3corda.node.services.monitor.ServiceToClientEvent
+import com.r3corda.node.services.monitor.TransactionBuildResult
+import com.r3corda.node.services.transactions.SimpleNotaryService
+import com.r3corda.node.utilities.AddOrRemove
+import org.junit.Test
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import rx.subjects.PublishSubject
+import kotlin.test.fail
+
+val log: Logger = LoggerFactory.getLogger(WalletMonitorServiceTests::class.java)
+
+class WalletMonitorServiceTests {
+ @Test
+ fun cashIssueWorksEndToEnd() {
+
+ driver {
+ val aliceNodeFuture = startNode("Alice")
+ val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(SimpleNotaryService.Type))
+
+ val aliceNode = aliceNodeFuture.get()
+ val notaryNode = notaryNodeFuture.get()
+ val client = startClient(aliceNode).get()
+
+ log.info("Alice is ${aliceNode.identity}")
+ log.info("Notary is ${notaryNode.identity}")
+
+ val aliceInStream = PublishSubject.create()
+ val aliceOutStream = PublishSubject.create()
+
+ val aliceMonitorClient = WalletMonitorClient(client, aliceNode, aliceOutStream, aliceInStream)
+ require(aliceMonitorClient.register().get())
+
+ aliceOutStream.onNext(ClientToServiceCommand.IssueCash(
+ amount = Amount(100, USD),
+ issueRef = OpaqueBytes(ByteArray(1, { 1 })),
+ recipient = aliceNode.identity,
+ notary = notaryNode.identity
+ ))
+
+ val buildFuture = SettableFuture()
+ val eventFuture = SettableFuture()
+ aliceInStream.subscribe {
+ if (it is ServiceToClientEvent.OutputState)
+ eventFuture.set(it)
+ else if (it is ServiceToClientEvent.TransactionBuild)
+ buildFuture.set(it)
+ else
+ log.warn("Unexpected event $it")
+ }
+
+ val buildEvent = buildFuture.get()
+ val state = buildEvent.state
+ if (state is TransactionBuildResult.Failed) {
+ fail(state.message)
+ }
+
+ val outputEvent = eventFuture.get()
+ require(outputEvent.consumed.size == 0)
+ require(outputEvent.produced.size == 1)
+ }
+ }
+
+ @Test
+ fun issueAndMoveWorks() {
+ driver {
+ val aliceNodeFuture = startNode("Alice")
+ val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(SimpleNotaryService.Type))
+
+ val aliceNode = aliceNodeFuture.get()
+ val notaryNode = notaryNodeFuture.get()
+ val client = startClient(aliceNode).get()
+
+ log.info("Alice is ${aliceNode.identity}")
+ log.info("Notary is ${notaryNode.identity}")
+
+ val aliceInStream = PublishSubject.create()
+ val aliceOutStream = PublishSubject.create()
+
+ val aliceMonitorClient = WalletMonitorClient(client, aliceNode, aliceOutStream, aliceInStream)
+ require(aliceMonitorClient.register().get())
+
+ aliceOutStream.onNext(ClientToServiceCommand.IssueCash(
+ amount = Amount(100, USD),
+ issueRef = OpaqueBytes(ByteArray(1, { 1 })),
+ recipient = aliceNode.identity,
+ notary = notaryNode.identity
+ ))
+
+ aliceOutStream.onNext(ClientToServiceCommand.PayCash(
+ amount = Amount(100, Issued(PartyAndReference(aliceNode.identity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
+ recipient = aliceNode.identity
+ ))
+
+ aliceInStream.expectEvents {
+ sequence(
+ // ISSUE
+ parallel(
+ sequence(
+ expect { add: ServiceToClientEvent.StateMachine ->
+ require(add.addOrRemove == AddOrRemove.ADD)
+ },
+ expect { remove: ServiceToClientEvent.StateMachine ->
+ require(remove.addOrRemove == AddOrRemove.REMOVE)
+ }
+ ),
+ expect { tx: ServiceToClientEvent.Transaction ->
+ require(tx.transaction.tx.inputs.isEmpty())
+ require(tx.transaction.tx.outputs.size == 1)
+ val signaturePubKeys = tx.transaction.sigs.map { it.by }.toSet()
+ // Only Alice signed
+ require(signaturePubKeys.size == 1)
+ require(signaturePubKeys.contains(aliceNode.identity.owningKey))
+ },
+ expect { build: ServiceToClientEvent.TransactionBuild ->
+ val state = build.state
+ when (state) {
+ is TransactionBuildResult.ProtocolStarted -> {
+ }
+ is TransactionBuildResult.Failed -> fail(state.message)
+ }
+ },
+ expect { output: ServiceToClientEvent.OutputState ->
+ require(output.consumed.size == 0)
+ require(output.produced.size == 1)
+ }
+ ),
+
+ // MOVE
+ parallel(
+ sequence(
+ expect { add: ServiceToClientEvent.StateMachine ->
+ require(add.addOrRemove == AddOrRemove.ADD)
+ },
+ expect { add: ServiceToClientEvent.StateMachine ->
+ require(add.addOrRemove == AddOrRemove.REMOVE)
+ }
+ ),
+ expect { tx: ServiceToClientEvent.Transaction ->
+ require(tx.transaction.tx.inputs.size == 1)
+ require(tx.transaction.tx.outputs.size == 1)
+ val signaturePubKeys = tx.transaction.sigs.map { it.by }.toSet()
+ // Alice and Notary signed
+ require(signaturePubKeys.size == 2)
+ require(signaturePubKeys.contains(aliceNode.identity.owningKey))
+ require(signaturePubKeys.contains(notaryNode.identity.owningKey))
+ },
+ sequence(
+ expect { build: ServiceToClientEvent.TransactionBuild ->
+ val state = build.state
+ when (state) {
+ is TransactionBuildResult.ProtocolStarted -> {
+ log.info("${state.message}")
+ }
+ is TransactionBuildResult.Failed -> fail(state.message)
+ }
+ },
+ repeat(7) {
+ expect { build: ServiceToClientEvent.Progress -> }
+ }
+ ),
+ expect { output: ServiceToClientEvent.OutputState ->
+ require(output.consumed.size == 1)
+ require(output.produced.size == 1)
+ }
+ )
+ )
+ }
+ }
+ }
+
+ @Test
+ fun movingCashOfDifferentIssueRefsFails() {
+ driver {
+ val aliceNodeFuture = startNode("Alice")
+ val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(SimpleNotaryService.Type))
+
+ val aliceNode = aliceNodeFuture.get()
+ val notaryNode = notaryNodeFuture.get()
+ val client = startClient(aliceNode).get()
+
+ log.info("Alice is ${aliceNode.identity}")
+ log.info("Notary is ${notaryNode.identity}")
+
+ val aliceInStream = PublishSubject.create()
+ val aliceOutStream = PublishSubject.create()
+
+ val aliceMonitorClient = WalletMonitorClient(client, aliceNode, aliceOutStream, aliceInStream)
+ require(aliceMonitorClient.register().get())
+
+ aliceOutStream.onNext(ClientToServiceCommand.IssueCash(
+ amount = Amount(100, USD),
+ issueRef = OpaqueBytes(ByteArray(1, { 1 })),
+ recipient = aliceNode.identity,
+ notary = notaryNode.identity
+ ))
+
+ aliceOutStream.onNext(ClientToServiceCommand.IssueCash(
+ amount = Amount(100, USD),
+ issueRef = OpaqueBytes(ByteArray(1, { 2 })),
+ recipient = aliceNode.identity,
+ notary = notaryNode.identity
+ ))
+
+ aliceOutStream.onNext(ClientToServiceCommand.PayCash(
+ amount = Amount(200, Issued(PartyAndReference(aliceNode.identity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
+ recipient = aliceNode.identity
+ ))
+
+ aliceInStream.expectEvents {
+ sequence(
+ // ISSUE 1
+ parallel(
+ sequence(
+ expect { add: ServiceToClientEvent.StateMachine ->
+ require(add.addOrRemove == AddOrRemove.ADD)
+ },
+ expect { remove: ServiceToClientEvent.StateMachine ->
+ require(remove.addOrRemove == AddOrRemove.REMOVE)
+ }
+ ),
+ expect { tx: ServiceToClientEvent.Transaction -> },
+ expect { build: ServiceToClientEvent.TransactionBuild -> },
+ expect { output: ServiceToClientEvent.OutputState -> }
+ ),
+
+ // ISSUE 2
+ parallel(
+ sequence(
+ expect { add: ServiceToClientEvent.StateMachine ->
+ require(add.addOrRemove == AddOrRemove.ADD)
+ },
+ expect { remove: ServiceToClientEvent.StateMachine ->
+ require(remove.addOrRemove == AddOrRemove.REMOVE)
+ }
+ ),
+ expect { tx: ServiceToClientEvent.Transaction -> },
+ expect { build: ServiceToClientEvent.TransactionBuild -> },
+ expect { output: ServiceToClientEvent.OutputState -> }
+ ),
+
+ // MOVE, should fail
+ expect { build: ServiceToClientEvent.TransactionBuild ->
+ val state = build.state
+ require(state is TransactionBuildResult.Failed)
+ }
+ )
+ }
+ }
+ }
+}
diff --git a/node/src/main/kotlin/com/r3corda/node/services/monitor/Events.kt b/node/src/main/kotlin/com/r3corda/node/services/monitor/Events.kt
index 748af3f9c4..01a38b0dc8 100644
--- a/node/src/main/kotlin/com/r3corda/node/services/monitor/Events.kt
+++ b/node/src/main/kotlin/com/r3corda/node/services/monitor/Events.kt
@@ -1,10 +1,7 @@
package com.r3corda.node.services.monitor
import com.r3corda.core.contracts.*
-import com.r3corda.core.crypto.Party
-import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.node.utilities.AddOrRemove
-import java.security.PublicKey
import java.time.Instant
import java.util.*
@@ -12,25 +9,34 @@ import java.util.*
* Events triggered by changes in the node, and sent to monitoring client(s).
*/
sealed class ServiceToClientEvent(val time: Instant) {
- class Transaction(time: Instant, val transaction: SignedTransaction) : ServiceToClientEvent(time)
- class OutputState(time: Instant, val consumed: Set, val produced: Set>) : ServiceToClientEvent(time)
- class StateMachine(time: Instant, val fiberId: Long, val label: String, val addOrRemove: AddOrRemove) : ServiceToClientEvent(time)
- class Progress(time: Instant, val fiberId: Long, val message: String) : ServiceToClientEvent(time)
- class TransactionBuild(time: Instant, val id: UUID, val state: TransactionBuildResult) : ServiceToClientEvent(time)
+ class Transaction(time: Instant, val transaction: SignedTransaction) : ServiceToClientEvent(time) {
+ override fun toString() = "Transaction(${transaction.tx.commands})"
+ }
+ class OutputState(
+ time: Instant,
+ val consumed: Set,
+ val produced: Set>
+ ) : ServiceToClientEvent(time) {
+ override fun toString() = "OutputState(consumed=$consumed, produced=${produced.map { it.state.data.javaClass.simpleName } })"
+ }
+ class StateMachine(
+ time: Instant,
+ val fiberId: Long,
+ val label: String,
+ val addOrRemove: AddOrRemove
+ ) : ServiceToClientEvent(time) {
+ override fun toString() = "StateMachine(${addOrRemove.name})"
+ }
+ class Progress(time: Instant, val fiberId: Long, val message: String) : ServiceToClientEvent(time) {
+ override fun toString() = "Progress($message)"
+ }
+ class TransactionBuild(time: Instant, val id: UUID, val state: TransactionBuildResult) : ServiceToClientEvent(time) {
+ override fun toString() = "TransactionBuild($state)"
+ }
+
}
sealed class TransactionBuildResult {
- /**
- * State indicating the action undertaken has been completed (it was not complex enough to require a
- * state machine starting).
- *
- * @param transaction the transaction created as a result.
- */
- // TODO: We should have a consistent "Transaction your request triggered has been built" event, rather than these
- // once-off results from a request. Unclear if that means all requests need to trigger a protocol state machine,
- // so the client sees a consistent process, or if some other solution can be found.
- class Complete(val transaction: SignedTransaction, val message: String?) : TransactionBuildResult()
-
/**
* State indicating that a protocol is managing this request, and that the client should track protocol state machine
* updates for further information. The monitor will separately receive notification of the state machine having been
@@ -39,11 +45,15 @@ sealed class TransactionBuildResult {
*
* @param transaction the transaction created as a result, in the case where the protocol has completed.
*/
- class ProtocolStarted(val fiberId: Long, val transaction: SignedTransaction?, val message: String?) : TransactionBuildResult()
+ class ProtocolStarted(val fiberId: Long, val transaction: SignedTransaction?, val message: String?) : TransactionBuildResult() {
+ override fun toString() = "Started($message)"
+ }
/**
* State indicating the action undertaken failed, either directly (it is not something which requires a
* state machine), or before a state machine was started.
*/
- class Failed(val message: String?) : TransactionBuildResult()
+ class Failed(val message: String?) : TransactionBuildResult() {
+ override fun toString() = "Failed($message)"
+ }
}
diff --git a/node/src/main/kotlin/com/r3corda/node/services/monitor/WalletMonitorService.kt b/node/src/main/kotlin/com/r3corda/node/services/monitor/WalletMonitorService.kt
index 8c7690f0cb..ebefa7b7bf 100644
--- a/node/src/main/kotlin/com/r3corda/node/services/monitor/WalletMonitorService.kt
+++ b/node/src/main/kotlin/com/r3corda/node/services/monitor/WalletMonitorService.kt
@@ -201,7 +201,7 @@ class WalletMonitorService(net: MessagingService, val smm: StateMachineManager,
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
private fun issueCash(req: ClientToServiceCommand.IssueCash): TransactionBuildResult {
- val builder: TransactionBuilder = TransactionType.General.Builder(notary = req.notary)
+ val builder: TransactionBuilder = TransactionType.General.Builder(notary = null)
val issuer = PartyAndReference(services.storageService.myLegalIdentity, req.issueRef)
Cash().generateIssue(builder, req.amount.issuedBy(issuer), req.recipient.owningKey, req.notary)
builder.signWith(services.storageService.myLegalIdentityKey)
diff --git a/settings.gradle b/settings.gradle
index ee2860a7e5..43a8b99cde 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -3,6 +3,7 @@ include 'contracts'
include 'contracts:isolated'
include 'core'
include 'node'
+include 'client'
include 'experimental'
include 'test-utils'