Refactor explorer and friends to use RPC, remove NodeMonitor*

This commit is contained in:
Andras Slemmer 2016-09-29 17:19:44 +01:00
parent 415de1ce1f
commit 5af0e97444
20 changed files with 776 additions and 1324 deletions

View File

@ -1,252 +0,0 @@
package com.r3corda.client
import com.r3corda.core.contracts.*
import com.r3corda.core.node.services.ServiceInfo
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 com.r3corda.testing.*
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(NodeMonitorClientTests::class.java)
class NodeMonitorClientTests {
@Test
fun cashIssueWorksEndToEnd() {
driver {
val aliceNodeFuture = startNode("Alice")
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(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<ServiceToClientEvent>()
val aliceOutStream = PublishSubject.create<ClientToServiceCommand>()
val aliceMonitorClient = NodeMonitorClient(client, aliceNode, aliceOutStream, aliceInStream, PublishSubject.create())
require(aliceMonitorClient.register().get())
aliceOutStream.onNext(ClientToServiceCommand.IssueCash(
amount = Amount(100, USD),
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
recipient = aliceNode.identity,
notary = notaryNode.identity
))
aliceInStream.expectEvents(isStrict = false) {
parallel(
expect { build: ServiceToClientEvent.TransactionBuild ->
val state = build.state
if (state is TransactionBuildResult.Failed) {
fail(state.message)
}
},
expect { output: ServiceToClientEvent.OutputState ->
require(output.consumed.size == 0)
require(output.produced.size == 1)
}
)
}
}
}
@Test
fun issueAndMoveWorks() {
driver {
val aliceNodeFuture = startNode("Alice")
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(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<ServiceToClientEvent>()
val aliceOutStream = PublishSubject.create<ClientToServiceCommand>()
val aliceMonitorClient = NodeMonitorClient(client, aliceNode, aliceOutStream, aliceInStream, PublishSubject.create())
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)
}
},
replicate(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(ServiceInfo(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<ServiceToClientEvent>()
val aliceOutStream = PublishSubject.create<ClientToServiceCommand>()
val aliceMonitorClient = NodeMonitorClient(client, aliceNode, aliceOutStream, aliceInStream, PublishSubject.create())
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)
}
)
}
}
}
}

View File

@ -0,0 +1,200 @@
package com.r3corda.client
import com.google.common.util.concurrent.SettableFuture
import com.r3corda.client.model.NodeMonitorModel
import com.r3corda.client.model.ProgressTrackingEvent
import com.r3corda.core.bufferUntilSubscribed
import com.r3corda.core.contracts.*
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.node.driver.driver
import com.r3corda.node.driver.startClient
import com.r3corda.node.services.messaging.NodeMessagingClient
import com.r3corda.node.services.messaging.StateMachineUpdate
import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.testing.*
import org.junit.*
import rx.Observable
import rx.Observer
import kotlin.concurrent.thread
class NodeMonitorModelTest {
lateinit var aliceNode: NodeInfo
lateinit var notaryNode: NodeInfo
lateinit var aliceClient: NodeMessagingClient
val driverStarted = SettableFuture.create<Unit>()
val stopDriver = SettableFuture.create<Unit>()
val driverStopped = SettableFuture.create<Unit>()
lateinit var stateMachineTransactionMapping: Observable<StateMachineTransactionMapping>
lateinit var stateMachineUpdates: Observable<StateMachineUpdate>
lateinit var progressTracking: Observable<ProgressTrackingEvent>
lateinit var transactions: Observable<SignedTransaction>
lateinit var vaultUpdates: Observable<Vault.Update>
lateinit var clientToService: Observer<ClientToServiceCommand>
@Before
fun start() {
thread {
driver {
val aliceNodeFuture = startNode("Alice")
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(SimpleNotaryService.Type))
aliceNode = aliceNodeFuture.get()
notaryNode = notaryNodeFuture.get()
aliceClient = startClient(aliceNode).get()
val monitor = NodeMonitorModel()
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
stateMachineUpdates = monitor.stateMachineUpdates.bufferUntilSubscribed()
progressTracking = monitor.progressTracking.bufferUntilSubscribed()
transactions = monitor.transactions.bufferUntilSubscribed()
vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed()
clientToService = monitor.clientToService
monitor.register(aliceNode, aliceClient.config.certificatesPath)
driverStarted.set(Unit)
stopDriver.get()
}
driverStopped.set(Unit)
}
driverStarted.get()
}
@After
fun stop() {
stopDriver.set(Unit)
driverStopped.get()
}
@Test
fun cashIssueWorksEndToEnd() {
clientToService.onNext(ClientToServiceCommand.IssueCash(
amount = Amount(100, USD),
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
recipient = aliceNode.identity,
notary = notaryNode.identity
))
vaultUpdates.expectEvents(isStrict = false) {
sequence(
// SNAPSHOT
expect { output: Vault.Update ->
require(output.consumed.size == 0) { output.consumed.size }
require(output.produced.size == 0) { output.produced.size }
},
// ISSUE
expect { output: Vault.Update ->
require(output.consumed.size == 0) { output.consumed.size }
require(output.produced.size == 1) { output.produced.size }
}
)
}
}
@Test
fun issueAndMoveWorks() {
clientToService.onNext(ClientToServiceCommand.IssueCash(
amount = Amount(100, USD),
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
recipient = aliceNode.identity,
notary = notaryNode.identity
))
clientToService.onNext(ClientToServiceCommand.PayCash(
amount = Amount(100, Issued(PartyAndReference(aliceNode.identity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
recipient = aliceNode.identity
))
var issueSmId: StateMachineRunId? = null
var moveSmId: StateMachineRunId? = null
var issueTx: SignedTransaction? = null
var moveTx: SignedTransaction? = null
stateMachineUpdates.expectEvents {
sequence(
// ISSUE
expect { add: StateMachineUpdate.Added ->
issueSmId = add.id
},
expect { remove: StateMachineUpdate.Removed ->
require(remove.id == issueSmId)
},
// MOVE
expect { add: StateMachineUpdate.Added ->
moveSmId = add.id
},
expect { remove: StateMachineUpdate.Removed ->
require(remove.id == moveSmId)
}
)
}
transactions.expectEvents {
sequence(
// ISSUE
expect { tx ->
require(tx.tx.inputs.isEmpty())
require(tx.tx.outputs.size == 1)
val signaturePubKeys = tx.sigs.map { it.by }.toSet()
// Only Alice signed
require(signaturePubKeys.size == 1)
require(signaturePubKeys.contains(aliceNode.identity.owningKey))
issueTx = tx
},
// MOVE
expect { tx ->
require(tx.tx.inputs.size == 1)
require(tx.tx.outputs.size == 1)
val signaturePubKeys = tx.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))
moveTx = tx
}
)
}
vaultUpdates.expectEvents {
sequence(
// SNAPSHOT
expect { output: Vault.Update ->
require(output.consumed.size == 0) { output.consumed.size }
require(output.produced.size == 0) { output.produced.size }
},
// ISSUE
expect { update ->
require(update.consumed.size == 0) { update.consumed.size }
require(update.produced.size == 1) { update.produced.size }
},
// MOVE
expect { update ->
require(update.consumed.size == 1) { update.consumed.size }
require(update.produced.size == 1) { update.produced.size }
}
)
}
stateMachineTransactionMapping.expectEvents {
sequence(
// ISSUE
expect { mapping ->
require(mapping.stateMachineRunId == issueSmId)
require(mapping.transactionId == issueTx!!.id)
},
// MOVE
expect { mapping ->
require(mapping.stateMachineRunId == moveSmId)
require(mapping.transactionId == moveTx!!.id)
}
)
}
}
}

View File

@ -1,63 +0,0 @@
package com.r3corda.client
import com.google.common.util.concurrent.ListenableFuture
import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.map
import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.messaging.createMessage
import com.r3corda.core.messaging.onNext
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.core.success
import com.r3corda.core.utilities.loggerFor
import com.r3corda.node.services.monitor.*
import com.r3corda.node.services.monitor.NodeMonitorService.Companion.IN_EVENT_TOPIC
import com.r3corda.node.services.monitor.NodeMonitorService.Companion.OUT_EVENT_TOPIC
import com.r3corda.node.services.monitor.NodeMonitorService.Companion.REGISTER_TOPIC
import com.r3corda.node.services.monitor.NodeMonitorService.Companion.STATE_TOPIC
import rx.Observable
import rx.Observer
/**
* Worked example of a client which communicates with the wallet monitor service.
*/
class NodeMonitorClient(
val net: MessagingService,
val node: NodeInfo,
val outEvents: Observable<ClientToServiceCommand>,
val inEvents: Observer<ServiceToClientEvent>,
val snapshot: Observer<StateSnapshotMessage>
) {
companion object {
private val log = loggerFor<NodeMonitorClient>()
}
fun register(): ListenableFuture<Boolean> {
val sessionID = random63BitValue()
log.info("Registering with ID $sessionID. I am ${net.myAddress}")
val future = net.onNext<RegisterResponse>(REGISTER_TOPIC, sessionID).map { it.success }
net.onNext<StateSnapshotMessage>(STATE_TOPIC, sessionID).success { snapshot.onNext(it) }
net.addMessageHandler(IN_EVENT_TOPIC, sessionID) { msg, reg ->
val event = msg.data.deserialize<ServiceToClientEvent>()
inEvents.onNext(event)
}
val req = RegisterRequest(net.myAddress, sessionID)
val registerMessage = net.createMessage(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(OUT_EVENT_TOPIC, 0, envelope.serialize().bits)
net.send(message, node.address)
}
return future
}
}

View File

@ -5,7 +5,6 @@ import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.node.services.monitor.ServiceToClientEvent
import java.time.Instant
/**
@ -34,7 +33,7 @@ class EventGenerator(
val partyGenerator = Generator.oneOf(parties)
val cashStateGenerator = amountIssuedGenerator.combine(publicKeyGenerator) { amount, from ->
val builder = TransactionBuilder()
val builder = TransactionBuilder(notary = notary)
builder.addOutputState(Cash.State(amount, from))
builder.addCommand(Command(Cash.Commands.Issue(), amount.token.issuer.party.owningKey))
builder.toWireTransaction().outRef<Cash.State>(0)
@ -60,10 +59,6 @@ class EventGenerator(
}
)
val outputStateGenerator = consumedGenerator.combine(producedGenerator) { consumed, produced ->
ServiceToClientEvent.OutputState(Instant.now(), consumed, produced)
}
val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
val amountGenerator = Generator.intRange(0, 10000).combine(currencyGenerator) { quantity, currency -> Amount(quantity.toLong(), currency) }
@ -96,10 +91,6 @@ class EventGenerator(
)
}
val serviceToClientEventGenerator = Generator.frequency<ServiceToClientEvent>(
1.0 to outputStateGenerator
)
val clientToServiceCommandGenerator = Generator.frequency(
0.4 to issueCashGenerator,
0.5 to moveCashGenerator,

View File

@ -6,52 +6,33 @@ import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.contracts.StateRef
import com.r3corda.node.services.monitor.ServiceToClientEvent
import com.r3corda.node.services.monitor.StateSnapshotMessage
import com.r3corda.core.node.services.Vault
import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.removeIf
import rx.Observable
sealed class StatesModification<out T : ContractState>{
class Diff<out T : ContractState>(
val added: Collection<StateAndRef<T>>,
val removed: Collection<StateRef>
) : StatesModification<T>()
class Reset<out T : ContractState>(val states: Collection<StateAndRef<T>>) : StatesModification<T>()
}
data class Diff<out T : ContractState>(
val added: Collection<StateAndRef<T>>,
val removed: Collection<StateRef>
)
/**
* This model exposes the list of owned contract states.
*/
class ContractStateModel {
private val serviceToClient: Observable<ServiceToClientEvent> by observable(NodeMonitorModel::serviceToClient)
private val snapshot: Observable<StateSnapshotMessage> by observable(NodeMonitorModel::snapshot)
private val outputStates = serviceToClient.ofType(ServiceToClientEvent.OutputState::class.java)
private val vaultUpdates: Observable<Vault.Update> by observable(NodeMonitorModel::vaultUpdates)
val contractStatesDiff: Observable<StatesModification.Diff<ContractState>> =
outputStates.map { StatesModification.Diff(it.produced, it.consumed) }
// We filter the diff first rather than the complete contract state list.
val cashStatesModification: Observable<StatesModification<Cash.State>> = Observable.merge(
arrayOf(
contractStatesDiff.map {
StatesModification.Diff(it.added.filterCashStateAndRefs(), it.removed)
},
snapshot.map {
StatesModification.Reset(it.contractStates.filterCashStateAndRefs())
}
)
)
val contractStatesDiff: Observable<Diff<ContractState>> = vaultUpdates.map {
Diff(it.produced, it.consumed)
}
val cashStatesDiff: Observable<Diff<Cash.State>> = contractStatesDiff.map {
// We can't filter removed hashes here as we don't have type info
Diff(it.added.filterCashStateAndRefs(), it.removed)
}
val cashStates: ObservableList<StateAndRef<Cash.State>> =
cashStatesModification.foldToObservableList(Unit) { statesDiff, _accumulator, observableList ->
when (statesDiff) {
is StatesModification.Diff -> {
observableList.removeIf { it.ref in statesDiff.removed }
observableList.addAll(statesDiff.added)
}
is StatesModification.Reset -> {
observableList.setAll(statesDiff.states)
}
}
cashStatesDiff.foldToObservableList(Unit) { statesDiff, _accumulator, observableList ->
observableList.removeIf { it.ref in statesDiff.removed }
observableList.addAll(statesDiff.added)
}

View File

@ -1,40 +1,27 @@
package com.r3corda.client.model
import com.r3corda.client.fxutils.foldToObservableList
import com.r3corda.client.fxutils.getObservableValue
import com.r3corda.client.fxutils.*
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.contracts.StateRef
import com.r3corda.client.fxutils.recordInSequence
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.node.services.monitor.ServiceToClientEvent
import com.r3corda.node.services.monitor.TransactionBuildResult
import com.r3corda.node.utilities.AddOrRemove
import com.r3corda.node.services.messaging.StateMachineUpdate
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.collections.ObservableMap
import org.fxmisc.easybind.EasyBind
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory
import rx.Observable
import java.time.Instant
import java.util.UUID
import kotlin.reflect.KProperty1
interface GatheredTransactionData {
val stateMachineRunId: ObservableValue<StateMachineRunId?>
val uuid: ObservableValue<UUID?>
val protocolStatus: ObservableValue<ProtocolStatus?>
val stateMachineStatus: ObservableValue<StateMachineStatus?>
val transaction: ObservableValue<PartiallyResolvedTransaction?>
val status: ObservableValue<TransactionCreateStatus?>
val lastUpdate: ObservableValue<Instant>
val allEvents: ObservableList<out ServiceToClientEvent>
}
data class GatheredTransactionData(
val transaction: PartiallyResolvedTransaction,
val stateMachines: ObservableList<out StateMachineData>
)
/**
* [PartiallyResolvedTransaction] holds a [SignedTransaction] that has zero or more inputs resolved. The intent is
@ -79,248 +66,71 @@ sealed class TransactionCreateStatus(val message: String?) {
data class ProtocolStatus(
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 GatheredTransactionDataWritable(
override val stateMachineRunId: SimpleObjectProperty<StateMachineRunId?> = SimpleObjectProperty(null),
override val uuid: SimpleObjectProperty<UUID?> = SimpleObjectProperty(null),
override val stateMachineStatus: SimpleObjectProperty<StateMachineStatus?> = SimpleObjectProperty(null),
override val protocolStatus: SimpleObjectProperty<ProtocolStatus?> = SimpleObjectProperty(null),
override val transaction: SimpleObjectProperty<PartiallyResolvedTransaction?> = SimpleObjectProperty(null),
override val status: SimpleObjectProperty<TransactionCreateStatus?> = SimpleObjectProperty(null),
override val lastUpdate: SimpleObjectProperty<Instant>,
override val allEvents: ObservableList<ServiceToClientEvent> = FXCollections.observableArrayList()
) : GatheredTransactionData
private val log = LoggerFactory.getLogger(GatheredTransactionDataModel::class.java)
data class StateMachineData(
val id: StateMachineRunId,
val protocolStatus: ObservableValue<ProtocolStatus?>,
val stateMachineStatus: ObservableValue<StateMachineStatus>
)
/**
* This model provides an observable list of states relating to the creation of a transaction not yet on ledger.
* This model provides an observable list of transactions and what state machines/protocols recorded them
*/
class GatheredTransactionDataModel {
private val serviceToClient: Observable<ServiceToClientEvent> by observable(NodeMonitorModel::serviceToClient)
private val transactions: Observable<SignedTransaction> by observable(NodeMonitorModel::transactions)
private val stateMachineUpdates: Observable<StateMachineUpdate> by observable(NodeMonitorModel::stateMachineUpdates)
private val progressTracking: Observable<ProgressTrackingEvent> by observable(NodeMonitorModel::progressTracking)
private val stateMachineTransactionMapping: Observable<StateMachineTransactionMapping> by observable(NodeMonitorModel::stateMachineTransactionMapping)
/**
* Aggregation of updates to transactions. We use the observable list as the only container and do linear search for
* matching transactions because we have three keys(fiber ID, UUID, tx id) and this way it's easier to avoid syncing issues.
*
* The Fiber ID is used to identify events that relate to the same transaction server-side, whereas the UUID is
* generated on the UI and is used to identify events with the UI action that triggered them. Currently a UUID is
* generated for each outgoing [ClientToServiceCommand].
*
* TODO: Make this more efficient by maintaining and syncing two maps (for the two keys) in the accumulator
* (Note that a transaction may be mapped by one or both)
* TODO: Expose a writable stream to combine [serviceToClient] with to allow recording of transactions made locally(UUID)
*/
val gatheredTransactionDataList: ObservableList<out GatheredTransactionData> =
serviceToClient.foldToObservableList<ServiceToClientEvent, GatheredTransactionDataWritable, ObservableMap<SecureHash, SignedTransaction>>(
initialAccumulator = FXCollections.observableHashMap<SecureHash, SignedTransaction>(),
folderFun = { serviceToClientEvent, transactions, transactionStates ->
val _unit = when (serviceToClientEvent) {
is ServiceToClientEvent.Transaction -> {
transactions.set(serviceToClientEvent.transaction.id, serviceToClientEvent.transaction)
val somewhatResolvedTransaction = PartiallyResolvedTransaction.fromSignedTransaction(
serviceToClientEvent.transaction,
transactions
)
newTransactionIdTransactionStateOrModify(transactionStates, serviceToClientEvent,
transaction = somewhatResolvedTransaction,
tweak = {}
)
}
is ServiceToClientEvent.OutputState -> {
}
is ServiceToClientEvent.StateMachine -> {
newFiberIdTransactionStateOrModify(transactionStates, serviceToClientEvent,
stateMachineRunId = serviceToClientEvent.id,
tweak = {
stateMachineStatus.set(when (serviceToClientEvent.addOrRemove) {
AddOrRemove.ADD -> StateMachineStatus.Added(serviceToClientEvent.label)
AddOrRemove.REMOVE -> {
val currentStatus = stateMachineStatus.value
if (currentStatus is StateMachineStatus.Added) {
StateMachineStatus.Removed(currentStatus.stateMachineName)
} else {
StateMachineStatus.Removed(serviceToClientEvent.label)
}
}
})
}
)
}
is ServiceToClientEvent.Progress -> {
newFiberIdTransactionStateOrModify(transactionStates, serviceToClientEvent,
stateMachineRunId = serviceToClientEvent.id,
tweak = {
protocolStatus.set(ProtocolStatus(serviceToClientEvent.message))
}
)
}
is ServiceToClientEvent.TransactionBuild -> {
val state = serviceToClientEvent.state
when (state) {
is TransactionBuildResult.ProtocolStarted -> {
state.transaction?.let {
transactions.set(it.id, it)
}
}
}
newUuidTransactionStateOrModify(transactionStates, serviceToClientEvent,
uuid = serviceToClientEvent.id,
stateMachineRunId = when (state) {
is TransactionBuildResult.ProtocolStarted -> state.id
is TransactionBuildResult.Failed -> null
},
transactionId = when (state) {
is TransactionBuildResult.ProtocolStarted -> state.transaction?.id
is TransactionBuildResult.Failed -> null
},
tweak = {
return@newUuidTransactionStateOrModify when (state) {
is TransactionBuildResult.ProtocolStarted -> {
state.transaction?.let {
transaction.set(PartiallyResolvedTransaction.fromSignedTransaction(it, transactions))
}
status.set(TransactionCreateStatus.Started(state.message))
}
is TransactionBuildResult.Failed -> {
status.set(TransactionCreateStatus.Failed(state.message))
}
}
}
)
}
}
transactions
val collectedTransactions = transactions.recordInSequence()
val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
val stateMachineStatus: ObservableMap<StateMachineRunId, out ObservableValue<StateMachineStatus>> =
stateMachineUpdates.foldToObservableMap(Unit) { update, _unit, map: ObservableMap<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>> ->
when (update) {
is StateMachineUpdate.Added -> {
val added: SimpleObjectProperty<StateMachineStatus> =
SimpleObjectProperty(StateMachineStatus.Added(update.stateMachineInfo.protocolLogicClassName))
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))
}
)
companion object {
private fun newTransactionIdTransactionStateOrModify(
transactionStates: ObservableList<GatheredTransactionDataWritable>,
event: ServiceToClientEvent,
transaction: PartiallyResolvedTransaction,
tweak: GatheredTransactionDataWritable.() -> Unit
) {
val index = transactionStates.indexOfFirst { transaction.id == it.transaction.value?.id }
val state = if (index < 0) {
val newState = GatheredTransactionDataWritable(
transaction = SimpleObjectProperty(transaction),
lastUpdate = SimpleObjectProperty(event.time)
)
tweak(newState)
transactionStates.add(newState)
newState
} else {
val existingState = transactionStates[index]
existingState.lastUpdate.set(event.time)
tweak(existingState)
existingState
}
state.allEvents.add(event)
}
private fun newFiberIdTransactionStateOrModify(
transactionStates: ObservableList<GatheredTransactionDataWritable>,
event: ServiceToClientEvent,
stateMachineRunId: StateMachineRunId,
tweak: GatheredTransactionDataWritable.() -> Unit
) {
val index = transactionStates.indexOfFirst { it.stateMachineRunId.value == stateMachineRunId }
val state = if (index < 0) {
val newState = GatheredTransactionDataWritable(
stateMachineRunId = SimpleObjectProperty(stateMachineRunId),
lastUpdate = SimpleObjectProperty(event.time)
)
tweak(newState)
transactionStates.add(newState)
newState
} else {
val existingState = transactionStates[index]
existingState.lastUpdate.set(event.time)
tweak(existingState)
existingState
}
state.allEvents.add(event)
}
private fun newUuidTransactionStateOrModify(
transactionStates: ObservableList<GatheredTransactionDataWritable>,
event: ServiceToClientEvent,
uuid: UUID,
stateMachineRunId: StateMachineRunId?,
transactionId: SecureHash?,
tweak: GatheredTransactionDataWritable.() -> Unit
) {
val matchingStates = transactionStates.filtered {
it.uuid.value == uuid ||
(stateMachineRunId != null && it.stateMachineRunId.value == stateMachineRunId) ||
(transactionId != null && it.transaction.value?.transaction?.id == transactionId)
}
val mergedState = mergeGatheredData(matchingStates)
for (i in 0 .. matchingStates.size - 1) {
transactionStates.removeAt(matchingStates.getSourceIndex(i))
}
val state = if (mergedState == null) {
val newState = GatheredTransactionDataWritable(
uuid = SimpleObjectProperty(uuid),
stateMachineRunId = SimpleObjectProperty(stateMachineRunId),
lastUpdate = SimpleObjectProperty(event.time)
)
transactionStates.add(newState)
newState
} else {
mergedState.lastUpdate.set(event.time)
mergedState
}
tweak(state)
state.allEvents.add(event)
}
private fun mergeGatheredData(
gatheredDataList: List<GatheredTransactionDataWritable>
): GatheredTransactionDataWritable? {
var gathered: GatheredTransactionDataWritable? = null
// Modify the last one if we can
gatheredDataList.asReversed().forEach {
val localGathered = gathered
if (localGathered == null) {
gathered = it
} else {
mergeField(it, localGathered, GatheredTransactionDataWritable::stateMachineRunId)
mergeField(it, localGathered, GatheredTransactionDataWritable::uuid)
mergeField(it, localGathered, GatheredTransactionDataWritable::stateMachineStatus)
mergeField(it, localGathered, GatheredTransactionDataWritable::protocolStatus)
mergeField(it, localGathered, GatheredTransactionDataWritable::transaction)
mergeField(it, localGathered, GatheredTransactionDataWritable::status)
localGathered.allEvents.addAll(it.allEvents)
}
}
return gathered
}
private fun <A> mergeField(
from: GatheredTransactionDataWritable,
to: GatheredTransactionDataWritable,
field: KProperty1<GatheredTransactionDataWritable, SimpleObjectProperty<A?>>) {
val fromValue = field(from).value
if (fromValue != null) {
val toField = field(to)
val toValue = toField.value
if (toValue != null && fromValue != toValue) {
log.warn("Conflicting data for field ${field.name}: $fromValue vs $toValue")
}
toField.set(fromValue)
}
}
val stateMachineDataList: ObservableList<StateMachineData> =
LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
StateMachineData(id, progress.map { it?.let { ProtocolStatus(it.message) } }, status)
}.getObservableValues()
val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
val partiallyResolvedTransactions = collectedTransactions.map {
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
}
/**
* We JOIN the transaction list with state machines
*/
val gatheredTransactionDataList: ObservableList<out GatheredTransactionData> =
partiallyResolvedTransactions.leftOuterJoin(
smTxMappingList,
PartiallyResolvedTransaction::id,
StateMachineTransactionMapping::transactionId
) { transaction, mappings ->
GatheredTransactionData(
transaction,
mappings.map { mapping ->
stateMachineDataMap.getObservableValue(mapping.stateMachineRunId)
}.flatten().filterNotNull()
)
}
}

View File

@ -8,6 +8,7 @@ import org.reactfx.EventSink
import org.reactfx.EventStream
import rx.Observable
import rx.Observer
import rx.subjects.Subject
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
@ -72,6 +73,9 @@ inline fun <reified M : Any, T> observable(noinline observableProperty: (M) -> O
inline fun <reified M : Any, T> observer(noinline observerProperty: (M) -> Observer<T>) =
TrackedDelegate.ObserverDelegate(M::class, observerProperty)
inline fun <reified M : Any, T> subject(noinline subjectProperty: (M) -> Subject<T, T>) =
TrackedDelegate.SubjectDelegate(M::class, subjectProperty)
inline fun <reified M : Any, T> eventStream(noinline streamProperty: (M) -> EventStream<T>) =
TrackedDelegate.EventStreamDelegate(M::class, streamProperty)
@ -118,14 +122,19 @@ object Models {
sealed class TrackedDelegate<M : Any>(val klass: KClass<M>) {
init { Models.initModel(klass) }
class ObservableDelegate<M : Any, T> (klass: KClass<M>, val eventStreamProperty: (M) -> Observable<T>) : TrackedDelegate<M>(klass) {
class ObservableDelegate<M : Any, T> (klass: KClass<M>, val observableProperty: (M) -> Observable<T>) : TrackedDelegate<M>(klass) {
operator fun getValue(thisRef: Any, property: KProperty<*>): Observable<T> {
return eventStreamProperty(Models.get(klass, thisRef.javaClass.kotlin))
return observableProperty(Models.get(klass, thisRef.javaClass.kotlin))
}
}
class ObserverDelegate<M : Any, T> (klass: KClass<M>, val eventStreamProperty: (M) -> Observer<T>) : TrackedDelegate<M>(klass) {
class ObserverDelegate<M : Any, T> (klass: KClass<M>, val observerProperty: (M) -> Observer<T>) : TrackedDelegate<M>(klass) {
operator fun getValue(thisRef: Any, property: KProperty<*>): Observer<T> {
return eventStreamProperty(Models.get(klass, thisRef.javaClass.kotlin))
return observerProperty(Models.get(klass, thisRef.javaClass.kotlin))
}
}
class SubjectDelegate<M : Any, T> (klass: KClass<M>, val subjectProperty: (M) -> Subject<T, T>) : TrackedDelegate<M>(klass) {
operator fun getValue(thisRef: Any, property: KProperty<*>): Subject<T, T> {
return subjectProperty(Models.get(klass, thisRef.javaClass.kotlin))
}
}
class EventStreamDelegate<M : Any, T> (klass: KClass<M>, val eventStreamProperty: (M) -> org.reactfx.EventStream<T>) : TrackedDelegate<M>(klass) {

View File

@ -1,42 +1,97 @@
package com.r3corda.client.model
import com.r3corda.client.NodeMonitorClient
import com.r3corda.client.CordaRPCClient
import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.node.NodeInfo
import com.r3corda.node.services.monitor.ServiceToClientEvent
import com.r3corda.node.services.monitor.StateSnapshotMessage
import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.node.services.messaging.ArtemisMessagingComponent
import com.r3corda.node.services.messaging.StateMachineInfo
import com.r3corda.node.services.messaging.StateMachineUpdate
import rx.Observable
import rx.Observer
import rx.subjects.PublishSubject
import java.nio.file.Path
data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val message: String) {
companion object {
fun createStreamFromStateMachineInfo(stateMachine: StateMachineInfo): Observable<ProgressTrackingEvent>? {
return stateMachine.progressTrackerStepAndUpdates?.let { pair ->
val (current, future) = pair
future.map { ProgressTrackingEvent(stateMachine.id, it) }.startWith(ProgressTrackingEvent(stateMachine.id, current))
}
}
}
}
/**
* This model exposes raw event streams to and from the [NodeMonitorService] through a [NodeMonitorClient]
* This model exposes raw event streams to and from the node.
*/
class NodeMonitorModel {
private val stateMachineUpdatesSubject = PublishSubject.create<StateMachineUpdate>()
private val vaultUpdatesSubject = PublishSubject.create<Vault.Update>()
private val transactionsSubject = PublishSubject.create<SignedTransaction>()
private val stateMachineTransactionMappingSubject = PublishSubject.create<StateMachineTransactionMapping>()
private val progressTrackingSubject = PublishSubject.create<ProgressTrackingEvent>()
val stateMachineUpdates: Observable<StateMachineUpdate> = stateMachineUpdatesSubject
val vaultUpdates: Observable<Vault.Update> = vaultUpdatesSubject
val transactions: Observable<SignedTransaction> = transactionsSubject
val stateMachineTransactionMapping: Observable<StateMachineTransactionMapping> = stateMachineTransactionMappingSubject
val progressTracking: Observable<ProgressTrackingEvent> = progressTrackingSubject
private val clientToServiceSource = PublishSubject.create<ClientToServiceCommand>()
val clientToService: Observer<ClientToServiceCommand> = clientToServiceSource
private val serviceToClientSource = PublishSubject.create<ServiceToClientEvent>()
val serviceToClient: Observable<ServiceToClientEvent> = serviceToClientSource
private val snapshotSource = PublishSubject.create<StateSnapshotMessage>()
val snapshot: Observable<StateSnapshotMessage> = snapshotSource
val clientToService: PublishSubject<ClientToServiceCommand> = clientToServiceSource
/**
* Register for updates to/from a given wallet.
* Register for updates to/from a given vault.
* @param messagingService The messaging to use for communication.
* @param monitorNodeInfo the [Node] to connect to.
* TODO provide an unsubscribe mechanism
*/
fun register(messagingService: MessagingService, monitorNodeInfo: NodeInfo) {
val monitorClient = NodeMonitorClient(
messagingService,
monitorNodeInfo,
clientToServiceSource,
serviceToClientSource,
snapshotSource
)
require(monitorClient.register().get())
fun register(vaultMonitorNodeInfo: NodeInfo, certificatesPath: Path) {
val client = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(vaultMonitorNodeInfo.address), certificatesPath)
client.start()
val proxy = client.proxy()
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesAndUpdates()
// Extract the protocol tracking stream
// TODO is there a nicer way of doing this? Stream of streams in general results in code like this...
val currentProgressTrackerUpdates = stateMachines.mapNotNull { stateMachine ->
ProgressTrackingEvent.createStreamFromStateMachineInfo(stateMachine)
}
val futureProgressTrackerUpdates = stateMachineUpdatesSubject.map { stateMachineUpdate ->
if (stateMachineUpdate is StateMachineUpdate.Added) {
ProgressTrackingEvent.createStreamFromStateMachineInfo(stateMachineUpdate.stateMachineInfo) ?: Observable.empty<ProgressTrackingEvent>()
} else {
Observable.empty<ProgressTrackingEvent>()
}
}
futureProgressTrackerUpdates.startWith(currentProgressTrackerUpdates).flatMap { it }.subscribe(progressTrackingSubject)
// Now the state machines
val currentStateMachines = stateMachines.map { StateMachineUpdate.Added(it) }
stateMachineUpdates.startWith(currentStateMachines).subscribe(stateMachineUpdatesSubject)
// Vault updates
val (vault, vaultUpdates) = proxy.vaultAndUpdates()
val initialVaultUpdate = Vault.Update(setOf(), vault.toSet())
vaultUpdates.startWith(initialVaultUpdate).subscribe(vaultUpdatesSubject)
// Transactions
val (transactions, newTransactions) = proxy.verifiedTransactions()
newTransactions.startWith(transactions).subscribe(transactionsSubject)
// SM -> TX mapping
val (smTxMappings, futureSmTxMappings) = proxy.stateMachineRecordedTransactionMapping()
futureSmTxMappings.startWith(smTxMappings).subscribe(stateMachineTransactionMappingSubject)
// Client -> Service
clientToServiceSource.subscribe {
proxy.executeCommand(it)
}
}
}

View File

@ -1,29 +1,24 @@
package com.r3corda.explorer
import com.r3corda.client.NodeMonitorClient
import com.r3corda.client.mock.EventGenerator
import com.r3corda.client.mock.Generator
import com.r3corda.client.mock.oneOf
import com.r3corda.client.model.Models
import com.r3corda.client.model.NodeMonitorModel
import com.r3corda.client.model.observer
import com.r3corda.client.model.subject
import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.node.services.ServiceInfo
import com.r3corda.explorer.model.IdentityModel
import com.r3corda.node.driver.PortAllocation
import com.r3corda.node.driver.driver
import com.r3corda.node.driver.startClient
import com.r3corda.node.services.monitor.ServiceToClientEvent
import com.r3corda.node.services.transactions.SimpleNotaryService
import javafx.stage.Stage
import rx.Observer
import rx.subjects.PublishSubject
import rx.subjects.Subject
import tornadofx.App
import java.util.*
class Main : App() {
override val primaryView = MainWindow::class
val aliceOutStream: Observer<ClientToServiceCommand> by observer(NodeMonitorModel::clientToService)
val aliceOutStream: Subject<ClientToServiceCommand, ClientToServiceCommand> by subject(NodeMonitorModel::clientToService)
override fun start(stage: Stage) {
@ -42,35 +37,26 @@ class Main : App() {
driver(portAllocation = portAllocation) {
val aliceNodeFuture = startNode("Alice")
val bobNodeFuture = startNode("Bob")
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.Type)))
val aliceNode = aliceNodeFuture.get()
val bobNode = bobNodeFuture.get()
val notaryNode = notaryNodeFuture.get()
val aliceClient = startClient(aliceNode).get()
Models.get<IdentityModel>(Main::class).myIdentity.set(aliceNode.identity)
Models.get<NodeMonitorModel>(Main::class).register(aliceClient, aliceNode)
val bobInStream = PublishSubject.create<ServiceToClientEvent>()
val bobOutStream = PublishSubject.create<ClientToServiceCommand>()
val bobClient = startClient(bobNode).get()
val bobMonitorClient = NodeMonitorClient(bobClient, bobNode, bobOutStream, bobInStream, PublishSubject.create())
assert(bobMonitorClient.register().get())
Models.get<NodeMonitorModel>(Main::class).register(aliceNode, aliceClient.config.certificatesPath)
for (i in 0 .. 10000) {
Thread.sleep(500)
val eventGenerator = EventGenerator(
parties = listOf(aliceNode.identity, bobNode.identity),
parties = listOf(aliceNode.identity),
notary = notaryNode.identity
)
eventGenerator.clientToServiceCommandGenerator.combine(Generator.oneOf(listOf(aliceOutStream, bobOutStream))) {
command, stream -> stream.onNext(command)
eventGenerator.clientToServiceCommandGenerator.map { command ->
aliceOutStream.onNext(command)
}.generate(Random())
}
@ -80,4 +66,3 @@ class Main : App() {
}).start()
}
}

View File

@ -16,7 +16,6 @@ import com.r3corda.explorer.model.IdentityModel
import com.r3corda.explorer.model.ReportingCurrencyModel
import com.r3corda.explorer.sign
import com.r3corda.explorer.ui.*
import com.r3corda.node.services.monitor.ServiceToClientEvent
import javafx.beans.binding.Bindings
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
@ -31,7 +30,6 @@ import javafx.scene.layout.VBox
import javafx.scene.paint.Color
import tornadofx.View
import java.security.PublicKey
import java.time.Instant
import java.util.*
class TransactionViewer: View() {
@ -74,11 +72,6 @@ class TransactionViewer: View() {
private val signaturesTitledPane: TitledPane by fxid()
private val signaturesList: ListView<PublicKey> by fxid()
private val lowLevelEventsTitledPane: TitledPane by fxid()
private val lowLevelEventsTable: TableView<ServiceToClientEvent> by fxid()
private val lowLevelEventsTimestamp: TableColumn<ServiceToClientEvent, Instant> by fxid()
private val lowLevelEventsEvent: TableColumn<ServiceToClientEvent, ServiceToClientEvent> by fxid()
private val matchingTransactionsLabel: Label by fxid()
// Inject data
@ -93,18 +86,13 @@ class TransactionViewer: View() {
* have the data.
*/
data class ViewerNode(
val transactionId: ObservableValue<SecureHash?>,
val transaction: PartiallyResolvedTransaction,
val transactionId: SecureHash,
val stateMachineRunId: ObservableValue<StateMachineRunId?>,
val clientUuid: ObservableValue<UUID?>,
val originator: ObservableValue<String>,
val transactionStatus: ObservableValue<TransactionCreateStatus?>,
val stateMachineStatus: ObservableValue<StateMachineStatus?>,
val protocolStatus: ObservableValue<ProtocolStatus?>,
val statusUpdated: ObservableValue<Instant>,
val commandTypes: ObservableValue<Collection<Class<CommandData>>>,
val totalValueEquiv: ObservableValue<AmountDiff<Currency>?>,
val transaction: ObservableValue<PartiallyResolvedTransaction?>,
val allEvents: ObservableList<out ServiceToClientEvent>
val stateMachineStatus: ObservableValue<out StateMachineStatus?>,
val protocolStatus: ObservableValue<out ProtocolStatus?>,
val commandTypes: Collection<Class<CommandData>>,
val totalValueEquiv: ObservableValue<AmountDiff<Currency>?>
)
/**
@ -119,59 +107,40 @@ class TransactionViewer: View() {
* We map the gathered data about transactions almost one-to-one to the nodes.
*/
private val viewerNodes = gatheredTransactionDataList.map {
// TODO in theory there may be several associated state machines, we should at least give a warning if there are
// several, currently we just throw others away
val stateMachine = it.stateMachines.first()
fun <A> stateMachineProperty(property: (StateMachineData) -> ObservableValue<out A?>): ObservableValue<out A?> {
return stateMachine.map { it?.let(property) }.bindOut { it ?: null.lift() }
}
ViewerNode(
transactionId = it.transaction.map { it?.id },
stateMachineRunId = it.stateMachineRunId,
clientUuid = it.uuid,
/**
* We can't really do any better based on uuid, we need to store explicit data for this TODO
*/
originator = it.uuid.map { uuid ->
if (uuid == null) {
"Someone"
} else {
"Us"
}
},
transactionStatus = it.status,
protocolStatus = it.protocolStatus,
stateMachineStatus = it.stateMachineStatus,
statusUpdated = it.lastUpdate,
commandTypes = it.transaction.map {
val commands = mutableSetOf<Class<CommandData>>()
it?.transaction?.tx?.commands?.forEach {
commands.add(it.value.javaClass)
}
commands
},
totalValueEquiv = it.transaction.bind { transaction ->
if (transaction == null) {
null.lift<AmountDiff<Currency>?>()
} else {
val resolvedInputs = transaction.inputs.sequence().map { resolution ->
when (resolution) {
is PartiallyResolvedTransaction.InputResolution.Unresolved -> null
is PartiallyResolvedTransaction.InputResolution.Resolved -> resolution.stateAndRef
}
}.foldObservable(listOf()) { inputs: List<StateAndRef<ContractState>>?, state: StateAndRef<ContractState>? ->
if (inputs != null && state != null) {
inputs + state
} else {
null
}
}
::calculateTotalEquiv.lift(
myIdentity,
reportingExchange,
resolvedInputs,
transaction.transaction.tx.outputs.lift()
)
}
},
transaction = it.transaction,
allEvents = it.allEvents
transactionId = it.transaction.id,
stateMachineRunId = stateMachine.map { it?.id },
protocolStatus = stateMachineProperty { it.protocolStatus },
stateMachineStatus = stateMachineProperty { it.stateMachineStatus },
commandTypes = it.transaction.transaction.tx.commands.map { it.value.javaClass },
totalValueEquiv = {
val resolvedInputs = it.transaction.inputs.sequence().map { resolution ->
when (resolution) {
is PartiallyResolvedTransaction.InputResolution.Unresolved -> null
is PartiallyResolvedTransaction.InputResolution.Resolved -> resolution.stateAndRef
}
}.fold(listOf()) { inputs: List<StateAndRef<ContractState>>?, state: StateAndRef<ContractState>? ->
if (inputs != null && state != null) {
inputs + state
} else {
null
}
}
::calculateTotalEquiv.lift(
myIdentity,
reportingExchange,
resolvedInputs.lift(),
it.transaction.transaction.tx.outputs.lift()
)
}()
)
}
@ -179,9 +148,9 @@ class TransactionViewer: View() {
* The detail panes are only filled out if a transaction is selected
*/
private val selectedViewerNode = transactionViewTable.singleRowSelection()
private val selectedTransaction = selectedViewerNode.bindOut {
private val selectedTransaction = selectedViewerNode.map {
when (it) {
is SingleRowSelection.None -> null.lift()
is SingleRowSelection.None -> null
is SingleRowSelection.Selected -> it.node.transaction
}
}
@ -215,21 +184,13 @@ class TransactionViewer: View() {
}
})
private val lowLevelEvents = ChosenList(selectedViewerNode.map {
when (it) {
is SingleRowSelection.None -> FXCollections.emptyObservableList<ServiceToClientEvent>()
is SingleRowSelection.Selected -> it.node.allEvents
}
})
/**
* We only display the detail panes if there is a node selected.
*/
private val allNodesShown = FXCollections.observableArrayList<Node>(
transactionViewTable,
contractStatesTitledPane,
signaturesTitledPane,
lowLevelEventsTitledPane
signaturesTitledPane
)
private val onlyTransactionsTableShown = FXCollections.observableArrayList<Node>(
transactionViewTable
@ -326,11 +287,9 @@ class TransactionViewer: View() {
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / transactionViewTable.columns.size).toInt()
}
transactionViewTransactionId.setCellValueFactory { it.value.transactionId.map { "${it ?: ""}" } }
transactionViewTransactionId.setCellValueFactory { "${it.value.transactionId}".lift() }
transactionViewStateMachineId.setCellValueFactory { it.value.stateMachineRunId.map { "${it?.uuid ?: ""}" } }
transactionViewClientUuid.setCellValueFactory { it.value.clientUuid.map { "${it ?: ""}" } }
transactionViewProtocolStatus.setCellValueFactory { it.value.protocolStatus.map { "${it ?: ""}" } }
transactionViewTransactionStatus.setCellValueFactory { it.value.transactionStatus }
transactionViewTransactionStatus.setCustomCellFactory {
val label = Label()
val backgroundFill = when (it) {
@ -342,7 +301,7 @@ class TransactionViewer: View() {
label.text = "$it"
label
}
transactionViewStateMachineStatus.setCellValueFactory { it.value.stateMachineStatus }
transactionViewStateMachineStatus.setCellValueFactory { it.value.stateMachineStatus.map { it } }
transactionViewStateMachineStatus.setCustomCellFactory {
val label = Label()
val backgroundFill = when (it) {
@ -356,7 +315,7 @@ class TransactionViewer: View() {
}
transactionViewCommandTypes.setCellValueFactory {
it.value.commandTypes.map { it.map { it.simpleName }.joinToString(",") }
it.value.commandTypes.map { it.simpleName }.joinToString(",").lift()
}
transactionViewTotalValueEquiv.setCellValueFactory<ViewerNode, AmountDiff<Currency>> { it.value.totalValueEquiv }
transactionViewTotalValueEquiv.cellFactory = object : Formatter<AmountDiff<Currency>> {
@ -394,14 +353,6 @@ class TransactionViewer: View() {
override fun format(value: PublicKey) = value.toStringShort()
}.toListCellFactory()
// Low level events
Bindings.bindContent(lowLevelEventsTable.items, lowLevelEvents)
lowLevelEventsTimestamp.setCellValueFactory { it.value.time.lift() }
lowLevelEventsEvent.setCellValueFactory { it.value.lift() }
lowLevelEventsTable.setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column ->
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / lowLevelEventsTable.columns.size).toInt()
}
matchingTransactionsLabel.textProperty().bind(Bindings.size(viewerNodes).map {
"$it matching transaction${if (it == 1) "" else "s"}"
})

View File

@ -40,7 +40,7 @@
</ImageView>
</children>
</StackPane>
<SplitPane fx:id="topSplitPane" dividerPositions="0.3, 0.6, 0.7" orientation="VERTICAL" prefHeight="562.0" prefWidth="1087.0" VBox.vgrow="ALWAYS">
<SplitPane fx:id="topSplitPane" dividerPositions="0.3, 0.6" orientation="VERTICAL" prefHeight="562.0" prefWidth="1087.0" VBox.vgrow="ALWAYS">
<items>
<TableView fx:id="transactionViewTable" prefHeight="200.0" prefWidth="200.0">
<columns>
@ -121,21 +121,11 @@
</SplitPane>
</content>
</TitledPane>
<TitledPane fx:id="signaturesTitledPane" animated="false" text="Required signatures">
<TitledPane fx:id="signaturesTitledPane" animated="false" text="Signatures">
<content>
<ListView fx:id="signaturesList" />
</content>
</TitledPane>
<TitledPane fx:id="lowLevelEventsTitledPane" animated="false" text="Low level events">
<content>
<TableView fx:id="lowLevelEventsTable">
<columns>
<TableColumn fx:id="lowLevelEventsTimestamp" prefWidth="102.0" text="Timestamp" />
<TableColumn fx:id="lowLevelEventsEvent" prefWidth="138.0" text="Event" />
</columns>
</TableView>
</content>
</TitledPane>
</items>
</SplitPane>
<HBox>

View File

@ -33,7 +33,6 @@ import com.r3corda.node.services.events.ScheduledActivityObserver
import com.r3corda.node.services.identity.InMemoryIdentityService
import com.r3corda.node.services.keys.PersistentKeyManagementService
import com.r3corda.node.services.messaging.CordaRPCOps
import com.r3corda.node.services.monitor.NodeMonitorService
import com.r3corda.node.services.network.InMemoryNetworkMapCache
import com.r3corda.node.services.network.NetworkMapService
import com.r3corda.node.services.network.NetworkMapService.Companion.REGISTER_PROTOCOL_TOPIC
@ -140,7 +139,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val networkMap
lateinit var vault: VaultService
lateinit var keyManagement: KeyManagementService
var inNodeNetworkMapService: NetworkMapService? = null
var inNodeMonitorService: NodeMonitorService? = null
var inNodeNotaryService: NotaryService? = null
var uniquenessProvider: UniquenessProvider? = null
lateinit var identity: IdentityService
@ -231,7 +229,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val networkMap
}
}
inNodeMonitorService = makeMonitorService() // Note this HAS to be after smm is set
buildAdvertisedServices()
// TODO: this model might change but for now it provides some de-coupling
@ -410,8 +407,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val networkMap
// TODO: sort out ordering of open & protected modifiers of functions in this class.
protected open fun makeVaultService(): VaultService = NodeVaultService(services)
protected open fun makeMonitorService(): NodeMonitorService = NodeMonitorService(services, smm)
open fun stop() {
// TODO: We need a good way of handling "nice to have" shutdown events, especially those that deal with the
// network, including unsubscribing from updates from remote services. Possibly some sort of parameter to stop()

View File

@ -1,24 +1,33 @@
package com.r3corda.node.internal
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef
import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.node.ServiceHub
import com.r3corda.core.node.services.Vault
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.node.services.api.ServiceHubInternal
import com.r3corda.node.services.messaging.CordaRPCOps
import com.r3corda.node.services.messaging.StateMachineInfo
import com.r3corda.node.services.messaging.StateMachineUpdate
import com.r3corda.node.services.messaging.TransactionBuildResult
import com.r3corda.node.services.statemachine.StateMachineManager
import com.r3corda.node.utilities.databaseTransaction
import com.r3corda.protocols.BroadcastTransactionProtocol
import com.r3corda.protocols.FinalityProtocol
import org.jetbrains.exposed.sql.Database
import rx.Observable
import java.security.KeyPair
/**
* Server side implementations of RPCs available to MQ based client tools. Execution takes place on the server
* thread (i.e. serially). Arguments are serialised and deserialised automatically.
*/
class ServerRPCOps(
val services: ServiceHubInternal,
val stateMachineManager: StateMachineManager,
val services: ServiceHub,
val smm: StateMachineManager,
val database: Database
) : CordaRPCOps {
override val protocolVersion: Int = 0
@ -31,11 +40,100 @@ class ServerRPCOps(
}
override fun verifiedTransactions() = services.storageService.validatedTransactions.track()
override fun stateMachinesAndUpdates(): Pair<List<StateMachineInfo>, Observable<StateMachineUpdate>> {
val (allStateMachines, changes) = stateMachineManager.track()
val (allStateMachines, changes) = smm.track()
return Pair(
allStateMachines.map { StateMachineInfo.fromProtocolStateMachineImpl(it) },
changes.map { StateMachineUpdate.fromStateMachineChange(it) }
)
}
override fun stateMachineRecordedTransactionMapping() = services.storageService.stateMachineRecordedTransactionMapping.track()
override fun executeCommand(command: ClientToServiceCommand): TransactionBuildResult {
return databaseTransaction(database) {
when (command) {
is ClientToServiceCommand.IssueCash -> issueCash(command)
is ClientToServiceCommand.PayCash -> initiatePayment(command)
is ClientToServiceCommand.ExitCash -> exitCash(command)
}
}
}
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
private fun initiatePayment(req: ClientToServiceCommand.PayCash): TransactionBuildResult {
val builder: TransactionBuilder = TransactionType.General.Builder(null)
// TODO: Have some way of restricting this to states the caller controls
try {
Cash().generateSpend(builder, req.amount.withoutIssuer(), req.recipient.owningKey,
// TODO: Move cash state filtering by issuer down to the contract itself
services.vaultService.currentVault.statesOfType<Cash.State>().filter { it.state.data.amount.token == req.amount.token },
setOf(req.amount.token.issuer.party))
.forEach {
val key = services.keyManagementService.keys[it] ?: throw IllegalStateException("Could not find signing key for ${it.toStringShort()}")
builder.signWith(KeyPair(it, key))
}
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
val protocol = FinalityProtocol(tx, setOf(req), setOf(req.recipient))
return TransactionBuildResult.ProtocolStarted(
smm.add(BroadcastTransactionProtocol.TOPIC, protocol).id,
tx,
"Cash payment transaction generated"
)
} catch(ex: InsufficientBalanceException) {
return TransactionBuildResult.Failed(ex.message ?: "Insufficient balance")
}
}
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
private fun exitCash(req: ClientToServiceCommand.ExitCash): TransactionBuildResult {
val builder: TransactionBuilder = TransactionType.General.Builder(null)
try {
val issuer = PartyAndReference(services.storageService.myLegalIdentity, req.issueRef)
Cash().generateExit(builder, req.amount.issuedBy(issuer),
services.vaultService.currentVault.statesOfType<Cash.State>().filter { it.state.data.owner == issuer.party.owningKey })
builder.signWith(services.storageService.myLegalIdentityKey)
// Work out who the owners of the burnt states were
val inputStatesNullable = services.vaultService.statesForRefs(builder.inputStates())
val inputStates = inputStatesNullable.values.filterNotNull().map { it.data }
if (inputStatesNullable.size != inputStates.size) {
val unresolvedStateRefs = inputStatesNullable.filter { it.value == null }.map { it.key }
throw InputStateRefResolveFailed(unresolvedStateRefs)
}
// TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them
// count as a reason to fail?
val participants: Set<Party> = inputStates.filterIsInstance<Cash.State>().map { services.identityService.partyFromKey(it.owner) }.filterNotNull().toSet()
// Commit the transaction
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
val protocol = FinalityProtocol(tx, setOf(req), participants)
return TransactionBuildResult.ProtocolStarted(
smm.add(BroadcastTransactionProtocol.TOPIC, protocol).id,
tx,
"Cash destruction transaction generated"
)
} catch (ex: InsufficientBalanceException) {
return TransactionBuildResult.Failed(ex.message ?: "Insufficient balance")
}
}
// 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 = 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)
val tx = builder.toSignedTransaction(checkSufficientSignatures = true)
// Issuance transactions do not need to be notarised, so we can skip directly to broadcasting it
val protocol = BroadcastTransactionProtocol(tx, setOf(req), setOf(req.recipient))
return TransactionBuildResult.ProtocolStarted(
smm.add(BroadcastTransactionProtocol.TOPIC, protocol).id,
tx,
"Cash issuance completed"
)
}
class InputStateRefResolveFailed(stateRefs: List<StateRef>) :
Exception("Failed to resolve input StateRefs $stateRefs")
}

View File

@ -1,8 +1,8 @@
package com.r3corda.node.services.messaging
import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId
@ -28,9 +28,9 @@ data class StateMachineInfo(
}
}
sealed class StateMachineUpdate {
class Added(val stateMachineInfo: StateMachineInfo) : StateMachineUpdate()
class Removed(val stateMachineRunId: StateMachineRunId) : StateMachineUpdate()
sealed class StateMachineUpdate(val id: StateMachineRunId) {
class Added(val stateMachineInfo: StateMachineInfo) : StateMachineUpdate(stateMachineInfo.id)
class Removed(id: StateMachineRunId) : StateMachineUpdate(id)
companion object {
fun fromStateMachineChange(change: StateMachineManager.Change): StateMachineUpdate {
@ -51,6 +51,28 @@ sealed class StateMachineUpdate {
}
}
sealed class 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
* added, as it would any other state machine. This response is used solely to enable the monitor to identify
* the state machine (and its progress) as associated with the request.
*
* @param transaction the transaction created as a result, in the case where the protocol has completed.
*/
class ProtocolStarted(val id: StateMachineRunId, 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() {
override fun toString() = "Failed($message)"
}
}
/**
* RPC operations that the node exposes to clients using the Java client library. These can be called from
* client apps and are implemented by the node in the [ServerRPCOps] class.
@ -73,6 +95,15 @@ interface CordaRPCOps : RPCOps {
*/
@RPCReturnsObservables
fun verifiedTransactions(): Pair<List<SignedTransaction>, Observable<SignedTransaction>>
/**
* Returns a pair of state machine id - recorded transaction hash pairs
*/
@RPCReturnsObservables
fun stateMachineRecordedTransactionMapping(): Pair<List<StateMachineTransactionMapping>, Observable<StateMachineTransactionMapping>>
/**
* Executes the given command, possibly triggering cash creation etc.
*/
fun executeCommand(command: ClientToServiceCommand): TransactionBuildResult
}

View File

@ -5,13 +5,23 @@ import com.esotericsoftware.kryo.Registration
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.serializers.DefaultSerializers
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.ErrorOr
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.serialization.*
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.WireTransaction
import de.javakaffee.kryoserializers.ArraysAsListSerializer
import de.javakaffee.kryoserializers.guava.*
import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.apache.activemq.artemis.api.core.client.ClientMessage
import org.objenesis.strategy.StdInstantiatorStrategy
import org.slf4j.LoggerFactory
@ -118,7 +128,41 @@ private class RPCKryo(private val observableSerializer: Serializer<Observable<An
register(Notification::class.java)
register(Notification.Kind::class.java)
register(kotlin.Pair::class.java)
register(ArrayList::class.java)
register(listOf<Any>().javaClass) // EmptyList
register(IllegalStateException::class.java)
register(Pair::class.java)
register(StateMachineUpdate.Added::class.java)
register(StateMachineUpdate.Removed::class.java)
register(StateMachineInfo::class.java)
register(DigitalSignature.WithKey::class.java)
register(DigitalSignature.LegallyIdentifiable::class.java)
register(ByteArray::class.java)
register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer)
register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer)
register(Vault::class.java)
register(Vault.Update::class.java)
register(StateMachineRunId::class.java)
register(StateMachineTransactionMapping::class.java)
register(UUID::class.java)
register(LinkedHashSet::class.java)
register(StateAndRef::class.java)
register(setOf<Unit>().javaClass) // EmptySet
register(StateRef::class.java)
register(SecureHash.SHA256::class.java)
register(TransactionState::class.java)
register(Cash.State::class.java)
register(Amount::class.java)
register(Issued::class.java)
register(PartyAndReference::class.java)
register(OpaqueBytes::class.java)
register(Currency::class.java)
register(Cash::class.java)
register(Cash.Clauses.ConserveAmount::class.java)
register(listOf(Unit).javaClass) // SingletonList
register(setOf(Unit).javaClass) // SingletonSet
register(TransactionBuildResult.ProtocolStarted::class.java)
register(TransactionBuildResult.Failed::class.java)
// Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway.
register(IllegalArgumentException::class.java)
@ -139,4 +183,4 @@ private class RPCKryo(private val observableSerializer: Serializer<Observable<An
}
}
fun createRPCKryo(observableSerializer: Serializer<Observable<Any>>? = null): Kryo = RPCKryo(observableSerializer)
fun createRPCKryo(observableSerializer: Serializer<Observable<Any>>? = null): Kryo = RPCKryo(observableSerializer)

View File

@ -1,60 +0,0 @@
package com.r3corda.node.services.monitor
import com.r3corda.core.contracts.*
import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.node.utilities.AddOrRemove
import java.time.Instant
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) {
override fun toString() = "Transaction(${transaction.tx.commands})"
}
class OutputState(
time: Instant,
val consumed: Set<StateRef>,
val produced: Set<StateAndRef<ContractState>>
) : ServiceToClientEvent(time) {
override fun toString() = "OutputState(consumed=$consumed, produced=${produced.map { it.state.data.javaClass.simpleName } })"
}
class StateMachine(
time: Instant,
val id: StateMachineRunId,
val label: String,
val addOrRemove: AddOrRemove
) : ServiceToClientEvent(time) {
override fun toString() = "StateMachine($label, ${addOrRemove.name})"
}
class Progress(time: Instant, val id: StateMachineRunId, 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 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
* added, as it would any other state machine. This response is used solely to enable the monitor to identify
* the state machine (and its progress) as associated with the request.
*
* @param transaction the transaction created as a result, in the case where the protocol has completed.
*/
class ProtocolStarted(val id: StateMachineRunId, 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() {
override fun toString() = "Failed($message)"
}
}

View File

@ -1,20 +0,0 @@
package com.r3corda.node.services.monitor
import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.messaging.SingleMessageRecipient
import com.r3corda.protocols.DirectRequestMessage
data class RegisterRequest(override val replyToRecipient: SingleMessageRecipient,
override val sessionID: Long) : DirectRequestMessage
data class RegisterResponse(val success: Boolean)
// TODO: This should have a shared secret the monitor was sent in the registration response, for security
data class DeregisterRequest(override val replyToRecipient: SingleMessageRecipient,
override val sessionID: Long) : DirectRequestMessage
data class DeregisterResponse(val success: Boolean)
data class StateSnapshotMessage(val contractStates: Collection<StateAndRef<ContractState>>, val protocolStates: Collection<String>)
data class ClientToServiceCommandMessage(override val sessionID: Long, override val replyToRecipient: SingleMessageRecipient, val command: ClientToServiceCommand) : DirectRequestMessage

View File

@ -1,233 +0,0 @@
package com.r3corda.node.services.monitor
import co.paralleluniverse.common.util.VisibleForTesting
import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.messaging.MessageRecipients
import com.r3corda.core.messaging.createMessage
import com.r3corda.core.node.services.DEFAULT_SESSION_ID
import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.serialization.serialize
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.utilities.loggerFor
import com.r3corda.node.services.api.AbstractNodeService
import com.r3corda.node.services.api.ServiceHubInternal
import com.r3corda.node.services.statemachine.StateMachineManager
import com.r3corda.node.utilities.AddOrRemove
import com.r3corda.protocols.BroadcastTransactionProtocol
import com.r3corda.protocols.FinalityProtocol
import java.security.KeyPair
import java.time.Instant
import java.util.*
import javax.annotation.concurrent.ThreadSafe
/**
* Service which allows external clients to monitor the node's vault and state machine manager, as well as trigger
* actions within the node. The service also sends requests for user input back to clients, for example to enter
* additional information while a protocol runs, or confirm an action.
*
* This is intended to enable a range of tools from end user UI to ops tools which monitor health across a number of nodes.
*/
// TODO: Implement authorization controls+
// TODO: Replace this entirely with a publish/subscribe based solution on a to-be-written service (likely JMS or similar),
// rather than implement authentication and publish/subscribe ourselves.
// TODO: Clients need to be able to indicate whether they support interactivity (no point in sending requests for input
// to a monitoring tool)
@ThreadSafe
class NodeMonitorService(services: ServiceHubInternal, val smm: StateMachineManager) : AbstractNodeService(services) {
companion object {
val REGISTER_TOPIC = "platform.monitor.register"
val DEREGISTER_TOPIC = "platform.monitor.deregister"
val STATE_TOPIC = "platform.monitor.state_snapshot"
val IN_EVENT_TOPIC = "platform.monitor.in"
val OUT_EVENT_TOPIC = "platform.monitor.out"
val logger = loggerFor<NodeMonitorService>()
}
val listeners: MutableSet<RegisteredListener> = HashSet()
data class RegisteredListener(val recipients: MessageRecipients, val sessionID: Long)
init {
addMessageHandler(REGISTER_TOPIC) { req: RegisterRequest -> processRegisterRequest(req) }
addMessageHandler(DEREGISTER_TOPIC) { req: DeregisterRequest -> processDeregisterRequest(req) }
addMessageHandler(OUT_EVENT_TOPIC) { req: ClientToServiceCommandMessage -> processEventRequest(req) }
// Notify listeners on state changes
services.storageService.validatedTransactions.updates.subscribe { tx -> notifyTransaction(tx) }
services.vaultService.updates.subscribe { update -> notifyVaultUpdate(update) }
smm.changes.subscribe { change ->
val id: StateMachineRunId = change.id
val logic: ProtocolLogic<*> = change.logic
val progressTracker = logic.progressTracker
notifyEvent(ServiceToClientEvent.StateMachine(Instant.now(), id, logic.javaClass.name, change.addOrRemove))
if (progressTracker != null) {
when (change.addOrRemove) {
AddOrRemove.ADD -> progressTracker.changes.subscribe { progress ->
notifyEvent(ServiceToClientEvent.Progress(Instant.now(), id, progress.toString()))
}
AddOrRemove.REMOVE -> {
// Nothing to do
}
}
}
}
}
@VisibleForTesting
internal fun notifyVaultUpdate(update: Vault.Update)
= notifyEvent(ServiceToClientEvent.OutputState(Instant.now(), update.consumed, update.produced))
@VisibleForTesting
internal fun notifyTransaction(transaction: SignedTransaction)
= notifyEvent(ServiceToClientEvent.Transaction(Instant.now(), transaction))
private fun processEventRequest(reqMessage: ClientToServiceCommandMessage) {
val req = reqMessage.command
val result: TransactionBuildResult? =
try {
when (req) {
is ClientToServiceCommand.IssueCash -> issueCash(req)
is ClientToServiceCommand.PayCash -> initiatePayment(req)
is ClientToServiceCommand.ExitCash -> exitCash(req)
else -> throw IllegalArgumentException("Unknown request type ${req.javaClass.name}")
}
} catch(ex: Exception) {
logger.warn("Exception while processing message of type ${req.javaClass.simpleName}", ex)
TransactionBuildResult.Failed(ex.message)
}
// Send back any result from the event. Not all events (especially TransactionInput) produce a
// result.
if (result != null) {
val event = ServiceToClientEvent.TransactionBuild(Instant.now(), req.id, result)
val respMessage = net.createMessage(IN_EVENT_TOPIC, reqMessage.sessionID,
event.serialize().bits)
net.send(respMessage, reqMessage.getReplyTo(services.networkMapCache))
}
}
/**
* Process a request from a monitor to remove them from the subscribers.
*/
fun processDeregisterRequest(req: DeregisterRequest) {
val message = try {
// TODO: Session ID should be managed by the messaging layer, so it handles ensuring that the
// request comes from the same endpoint that registered at the start.
listeners.remove(RegisteredListener(req.replyToRecipient, req.sessionID))
net.createMessage(DEREGISTER_TOPIC, req.sessionID, DeregisterResponse(true).serialize().bits)
} catch (ex: IllegalStateException) {
net.createMessage(DEREGISTER_TOPIC, req.sessionID, DeregisterResponse(false).serialize().bits)
}
net.send(message, req.replyToRecipient)
}
/**
* Process a request from a monitor to add them to the subscribers. This includes hooks to authenticate the request,
* but currently all requests pass (and there's no access control on vaults, so it has no actual meaning).
*/
fun processRegisterRequest(req: RegisterRequest) {
try {
listeners.add(RegisteredListener(req.replyToRecipient, req.sessionID))
val stateMessage = StateSnapshotMessage(services.vaultService.currentVault.states.toList(),
smm.allStateMachines.map { it.javaClass.name })
net.send(net.createMessage(STATE_TOPIC, DEFAULT_SESSION_ID, stateMessage.serialize().bits), req.replyToRecipient)
val message = net.createMessage(REGISTER_TOPIC, req.sessionID, RegisterResponse(true).serialize().bits)
net.send(message, req.replyToRecipient)
} catch (ex: IllegalStateException) {
val message = net.createMessage(REGISTER_TOPIC, req.sessionID, RegisterResponse(false).serialize().bits)
net.send(message, req.replyToRecipient)
}
}
private fun notifyEvent(event: ServiceToClientEvent) = listeners.forEach { monitor ->
net.send(net.createMessage(IN_EVENT_TOPIC, monitor.sessionID, event.serialize().bits), monitor.recipients)
}
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
private fun initiatePayment(req: ClientToServiceCommand.PayCash): TransactionBuildResult {
val builder: TransactionBuilder = TransactionType.General.Builder(null)
// TODO: Have some way of restricting this to states the caller controls
try {
Cash().generateSpend(builder, req.amount.withoutIssuer(), req.recipient.owningKey,
// TODO: Move cash state filtering by issuer down to the contract itself
services.vaultService.currentVault.statesOfType<Cash.State>().filter { it.state.data.amount.token == req.amount.token },
setOf(req.amount.token.issuer.party))
.forEach {
val key = services.keyManagementService.keys[it] ?: throw IllegalStateException("Could not find signing key for ${it.toStringShort()}")
builder.signWith(KeyPair(it, key))
}
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
val protocol = FinalityProtocol(tx, setOf(req), setOf(req.recipient))
return TransactionBuildResult.ProtocolStarted(
smm.add("broadcast", protocol).id,
tx,
"Cash payment transaction generated"
)
} catch(ex: InsufficientBalanceException) {
return TransactionBuildResult.Failed(ex.message ?: "Insufficient balance")
}
}
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
private fun exitCash(req: ClientToServiceCommand.ExitCash): TransactionBuildResult {
val builder: TransactionBuilder = TransactionType.General.Builder(null)
try {
val issuer = PartyAndReference(services.storageService.myLegalIdentity, req.issueRef)
Cash().generateExit(builder, req.amount.issuedBy(issuer),
services.vaultService.currentVault.statesOfType<Cash.State>().filter { it.state.data.owner == issuer.party.owningKey })
builder.signWith(services.storageService.myLegalIdentityKey)
// Work out who the owners of the burnt states were
val inputStatesNullable = services.vaultService.statesForRefs(builder.inputStates())
val inputStates = inputStatesNullable.values.filterNotNull().map { it.data }
if (inputStatesNullable.size != inputStates.size) {
val unresolvedStateRefs = inputStatesNullable.filter { it.value == null }.map { it.key }
throw InputStateRefResolveFailed(unresolvedStateRefs)
}
// TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them
// count as a reason to fail?
val participants: Set<Party> = inputStates.filterIsInstance<Cash.State>().map { services.identityService.partyFromKey(it.owner) }.filterNotNull().toSet()
// Commit the transaction
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
val protocol = FinalityProtocol(tx, setOf(req), participants)
return TransactionBuildResult.ProtocolStarted(
smm.add("broadcast", protocol).id,
tx,
"Cash destruction transaction generated"
)
} catch (ex: InsufficientBalanceException) {
return TransactionBuildResult.Failed(ex.message ?: "Insufficient balance")
}
}
// 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 = 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)
val tx = builder.toSignedTransaction(checkSufficientSignatures = true)
// Issuance transactions do not need to be notarised, so we can skip directly to broadcasting it
val protocol = BroadcastTransactionProtocol(tx, setOf(req), setOf(req.recipient))
return TransactionBuildResult.ProtocolStarted(
smm.add("broadcast", protocol).id,
tx,
"Cash issuance completed"
)
}
class InputStateRefResolveFailed(stateRefs: List<StateRef>) :
Exception("Failed to resolve input StateRefs $stateRefs")
}

View File

@ -0,0 +1,174 @@
package com.r3corda.node
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.*
import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.utilities.DUMMY_NOTARY
import com.r3corda.node.internal.ServerRPCOps
import com.r3corda.node.services.messaging.StateMachineUpdate
import com.r3corda.node.services.network.NetworkMapService
import com.r3corda.node.services.transactions.ValidatingNotaryService
import com.r3corda.testing.expect
import com.r3corda.testing.expectEvents
import com.r3corda.testing.node.MockNetwork
import com.r3corda.testing.node.MockNetwork.MockNode
import com.r3corda.testing.sequence
import org.junit.Before
import org.junit.Test
import rx.Observable
import kotlin.test.assertEquals
import kotlin.test.assertFalse
/**
* Unit tests for the node monitoring service.
*/
class ServerRPCTest {
lateinit var network: MockNetwork
lateinit var aliceNode: MockNode
lateinit var notaryNode: MockNode
lateinit var rpc: ServerRPCOps
lateinit var stateMachineUpdates: Observable<StateMachineUpdate>
lateinit var transactions: Observable<SignedTransaction>
lateinit var vaultUpdates: Observable<Vault.Update>
@Before
fun setup() {
network = MockNetwork()
val networkMap = network.createNode(advertisedServices = NetworkMapService.Type)
aliceNode = network.createNode(networkMapAddress = networkMap.info.address)
notaryNode = network.createNode(advertisedServices = ValidatingNotaryService.Type, networkMapAddress = networkMap.info.address)
rpc = ServerRPCOps(aliceNode.services, aliceNode.smm, aliceNode.database)
stateMachineUpdates = rpc.stateMachinesAndUpdates().second
transactions = rpc.verifiedTransactions().second
vaultUpdates = rpc.vaultAndUpdates().second
}
@Test
fun `cash issue accepted`() {
val quantity = 1000L
val ref = OpaqueBytes(ByteArray(1) {1})
// Check the monitoring service wallet is empty
assertFalse(aliceNode.services.vaultService.currentVault.states.iterator().hasNext())
// Tell the monitoring service node to issue some cash
val recipient = aliceNode.services.storageService.myLegalIdentity
val outEvent = ClientToServiceCommand.IssueCash(Amount(quantity, GBP), ref, recipient, DUMMY_NOTARY)
rpc.executeCommand(outEvent)
network.runNetwork()
val expectedState = Cash.State(Amount(quantity,
Issued(aliceNode.services.storageService.myLegalIdentity.ref(ref), GBP)),
recipient.owningKey)
var issueSmId: StateMachineRunId? = null
stateMachineUpdates.expectEvents {
sequence(
// ISSUE
expect { add: StateMachineUpdate.Added ->
issueSmId = add.id
},
expect { remove: StateMachineUpdate.Removed ->
require(remove.id == issueSmId)
}
)
}
transactions.expectEvents {
expect { tx ->
assertEquals(expectedState, tx.tx.outputs.single().data)
}
}
vaultUpdates.expectEvents {
expect { update ->
val actual = update.produced.single().state.data
assertEquals(expectedState, actual)
}
}
}
@Test
fun issueAndMoveWorks() {
rpc.executeCommand(ClientToServiceCommand.IssueCash(
amount = Amount(100, USD),
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
recipient = aliceNode.services.storageService.myLegalIdentity,
notary = notaryNode.services.storageService.myLegalIdentity
))
network.runNetwork()
rpc.executeCommand(ClientToServiceCommand.PayCash(
amount = Amount(100, Issued(PartyAndReference(aliceNode.services.storageService.myLegalIdentity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
recipient = aliceNode.services.storageService.myLegalIdentity
))
network.runNetwork()
var issueSmId: StateMachineRunId? = null
var moveSmId: StateMachineRunId? = null
stateMachineUpdates.expectEvents {
sequence(
// ISSUE
expect { add: StateMachineUpdate.Added ->
issueSmId = add.id
},
expect { remove: StateMachineUpdate.Removed ->
require(remove.id == issueSmId)
},
// MOVE
expect { add: StateMachineUpdate.Added ->
moveSmId = add.id
},
expect { remove: StateMachineUpdate.Removed ->
require(remove.id == moveSmId)
}
)
}
transactions.expectEvents {
sequence(
// ISSUE
expect { tx ->
require(tx.tx.inputs.isEmpty())
require(tx.tx.outputs.size == 1)
val signaturePubKeys = tx.sigs.map { it.by }.toSet()
// Only Alice signed
require(signaturePubKeys.size == 1)
require(signaturePubKeys.contains(aliceNode.services.storageService.myLegalIdentity.owningKey))
},
// MOVE
expect { tx ->
require(tx.tx.inputs.size == 1)
require(tx.tx.outputs.size == 1)
val signaturePubKeys = tx.sigs.map { it.by }.toSet()
// Alice and Notary signed
require(signaturePubKeys.size == 2)
require(signaturePubKeys.contains(aliceNode.services.storageService.myLegalIdentity.owningKey))
require(signaturePubKeys.contains(notaryNode.services.storageService.myLegalIdentity.owningKey))
}
)
}
vaultUpdates.expectEvents {
sequence(
// ISSUE
expect { update ->
require(update.consumed.size == 0) { update.consumed.size }
require(update.produced.size == 1) { update.produced.size }
},
// MOVE
expect { update ->
require(update.consumed.size == 1) { update.consumed.size }
require(update.produced.size == 1) { update.produced.size }
}
)
}
}
}

View File

@ -1,234 +0,0 @@
package com.r3corda.node.services
import com.google.common.util.concurrent.ListenableFuture
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.newSecureRandom
import com.r3corda.core.messaging.createMessage
import com.r3corda.core.node.services.DEFAULT_SESSION_ID
import com.r3corda.core.node.services.Vault
import com.r3corda.core.random63BitValue
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.serialization.deserialize
import com.r3corda.core.serialization.serialize
import com.r3corda.core.utilities.DUMMY_NOTARY
import com.r3corda.core.utilities.DUMMY_PUBKEY_1
import com.r3corda.node.services.monitor.*
import com.r3corda.node.services.monitor.NodeMonitorService.Companion.IN_EVENT_TOPIC
import com.r3corda.node.services.monitor.NodeMonitorService.Companion.REGISTER_TOPIC
import com.r3corda.node.utilities.AddOrRemove
import com.r3corda.testing.expect
import com.r3corda.testing.expectEvents
import com.r3corda.testing.node.MockNetwork
import com.r3corda.testing.node.MockNetwork.MockNode
import com.r3corda.testing.parallel
import com.r3corda.testing.sequence
import org.junit.Before
import org.junit.Test
import rx.subjects.ReplaySubject
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
/**
* Unit tests for the node monitoring service.
*/
class NodeMonitorServiceTests {
lateinit var network: MockNetwork
@Before
fun setup() {
network = MockNetwork()
}
/**
* Authenticate the register node with the monitor service node.
*/
private fun authenticate(monitorServiceNode: MockNode, registerNode: MockNode): Long {
network.runNetwork()
val sessionId = random63BitValue()
val authenticatePsm = register(registerNode, monitorServiceNode, sessionId)
network.runNetwork()
authenticatePsm.get(1, TimeUnit.SECONDS)
return sessionId
}
/**
* Test a very simple case of trying to register against the service.
*/
@Test
fun `success with network`() {
val (monitorServiceNode, registerNode) = network.createTwoNodes()
network.runNetwork()
val authenticatePsm = register(registerNode, monitorServiceNode, random63BitValue())
network.runNetwork()
val result = authenticatePsm.get(1, TimeUnit.SECONDS)
assertTrue(result.success)
}
/**
* Test that having registered, changes are relayed correctly.
*/
@Test
fun `event received`() {
val (monitorServiceNode, registerNode) = network.createTwoNodes()
val sessionID = authenticate(monitorServiceNode, registerNode)
var receivePsm = receiveWalletUpdate(registerNode, sessionID)
var expected = Vault.Update(emptySet(), emptySet())
monitorServiceNode.inNodeMonitorService!!.notifyVaultUpdate(expected)
network.runNetwork()
var actual = receivePsm.get(1, TimeUnit.SECONDS)
assertEquals(expected.consumed, actual.consumed)
assertEquals(expected.produced, actual.produced)
// Check that states are passed through correctly
receivePsm = receiveWalletUpdate(registerNode, sessionID)
val consumed = setOf(StateRef(SecureHash.randomSHA256(), 0))
val producedState = TransactionState(DummyContract.SingleOwnerState(newSecureRandom().nextInt(), DUMMY_PUBKEY_1), DUMMY_NOTARY)
val produced = setOf(StateAndRef(producedState, StateRef(SecureHash.randomSHA256(), 0)))
expected = Vault.Update(consumed, produced)
monitorServiceNode.inNodeMonitorService!!.notifyVaultUpdate(expected)
network.runNetwork()
actual = receivePsm.get(1, TimeUnit.SECONDS)
assertEquals(expected.produced, actual.produced)
assertEquals(expected.consumed, actual.consumed)
}
@Test
fun `cash issue accepted`() {
val (monitorServiceNode, registerNode) = network.createTwoNodes()
val sessionID = authenticate(monitorServiceNode, registerNode)
val quantity = 1000L
val events = ReplaySubject.create<ServiceToClientEvent>()
val ref = OpaqueBytes(ByteArray(1) {1})
registerNode.net.addMessageHandler(IN_EVENT_TOPIC, sessionID) { msg, reg ->
events.onNext(msg.data.deserialize<ServiceToClientEvent>())
}
// Check the monitoring service wallet is empty
assertFalse(monitorServiceNode.services.vaultService.currentVault.states.iterator().hasNext())
// Tell the monitoring service node to issue some cash
val recipient = monitorServiceNode.services.storageService.myLegalIdentity
val outEvent = ClientToServiceCommand.IssueCash(Amount(quantity, GBP), ref, recipient, DUMMY_NOTARY)
val message = registerNode.net.createMessage(NodeMonitorService.OUT_EVENT_TOPIC, DEFAULT_SESSION_ID,
ClientToServiceCommandMessage(sessionID, registerNode.net.myAddress, outEvent).serialize().bits)
registerNode.net.send(message, monitorServiceNode.net.myAddress)
network.runNetwork()
val expectedState = Cash.State(Amount(quantity,
Issued(monitorServiceNode.services.storageService.myLegalIdentity.ref(ref), GBP)),
recipient.owningKey)
// Check we've received a response
events.expectEvents {
parallel(
sequence(
expect { event: ServiceToClientEvent.StateMachine ->
require(event.addOrRemove == AddOrRemove.ADD)
},
expect { event: ServiceToClientEvent.StateMachine ->
require(event.addOrRemove == AddOrRemove.REMOVE)
}
),
expect { event: ServiceToClientEvent.Transaction -> },
expect { event: ServiceToClientEvent.TransactionBuild ->
// Check the returned event is correct
val tx = (event.state as TransactionBuildResult.ProtocolStarted).transaction
assertNotNull(tx)
assertEquals(expectedState, tx!!.tx.outputs.single().data)
},
expect { event: ServiceToClientEvent.OutputState ->
// Check the generated state is correct
val actual = event.produced.single().state.data
assertEquals(expectedState, actual)
}
)
}
}
@Test
fun `cash move accepted`() {
val (monitorServiceNode, registerNode) = network.createTwoNodes()
val sessionID = authenticate(monitorServiceNode, registerNode)
val quantity = 1000L
val events = ReplaySubject.create<ServiceToClientEvent>()
registerNode.net.addMessageHandler(IN_EVENT_TOPIC, sessionID) { msg, reg ->
events.onNext(msg.data.deserialize<ServiceToClientEvent>())
}
val recipient = monitorServiceNode.services.storageService.myLegalIdentity
// Tell the monitoring service node to issue some cash so we can spend it later
val issueCommand = ClientToServiceCommand.IssueCash(Amount(quantity, GBP), OpaqueBytes.of(0), recipient, recipient)
val issueMessage = registerNode.net.createMessage(NodeMonitorService.OUT_EVENT_TOPIC, DEFAULT_SESSION_ID,
ClientToServiceCommandMessage(sessionID, registerNode.net.myAddress, issueCommand).serialize().bits)
registerNode.net.send(issueMessage, monitorServiceNode.net.myAddress)
val payCommand = ClientToServiceCommand.PayCash(Amount(quantity, Issued(recipient.ref(0), GBP)), recipient)
val payMessage = registerNode.net.createMessage(NodeMonitorService.OUT_EVENT_TOPIC, DEFAULT_SESSION_ID,
ClientToServiceCommandMessage(sessionID, registerNode.net.myAddress, payCommand).serialize().bits)
registerNode.net.send(payMessage, monitorServiceNode.net.myAddress)
network.runNetwork()
events.expectEvents(isStrict = false) {
sequence(
// ISSUE
parallel(
sequence(
expect { event: ServiceToClientEvent.StateMachine ->
require(event.addOrRemove == AddOrRemove.ADD)
},
expect { event: ServiceToClientEvent.StateMachine ->
require(event.addOrRemove == AddOrRemove.REMOVE)
}
),
expect { event: ServiceToClientEvent.Transaction -> },
expect { event: ServiceToClientEvent.TransactionBuild -> },
expect { event: ServiceToClientEvent.OutputState -> }
),
// MOVE
parallel(
sequence(
expect { event: ServiceToClientEvent.StateMachine ->
require(event.addOrRemove == AddOrRemove.ADD)
},
expect { event: ServiceToClientEvent.StateMachine ->
require(event.addOrRemove == AddOrRemove.REMOVE)
}
),
expect { event: ServiceToClientEvent.Transaction ->
require(event.transaction.sigs.size == 1)
event.transaction.sigs.map { it.by }.containsAll(
listOf(
monitorServiceNode.services.storageService.myLegalIdentity.owningKey
)
)
},
expect { event: ServiceToClientEvent.TransactionBuild ->
require(event.state is TransactionBuildResult.ProtocolStarted)
},
expect { event: ServiceToClientEvent.OutputState ->
require(event.consumed.size == 1)
require(event.produced.size == 1)
}
)
)
}
}
private fun register(registerNode: MockNode, monitorServiceNode: MockNode, sessionId: Long): ListenableFuture<RegisterResponse> {
val req = RegisterRequest(registerNode.services.networkService.myAddress, sessionId)
return registerNode.sendAndReceive<RegisterResponse>(REGISTER_TOPIC, monitorServiceNode, req)
}
private fun receiveWalletUpdate(registerNode: MockNode, sessionId: Long): ListenableFuture<ServiceToClientEvent.OutputState> {
return registerNode.receive<ServiceToClientEvent.OutputState>(IN_EVENT_TOPIC, sessionId)
}
}