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.crypto.Party
import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.transactions.TransactionBuilder import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.node.services.monitor.ServiceToClientEvent
import java.time.Instant import java.time.Instant
/** /**
@ -34,7 +33,7 @@ class EventGenerator(
val partyGenerator = Generator.oneOf(parties) val partyGenerator = Generator.oneOf(parties)
val cashStateGenerator = amountIssuedGenerator.combine(publicKeyGenerator) { amount, from -> val cashStateGenerator = amountIssuedGenerator.combine(publicKeyGenerator) { amount, from ->
val builder = TransactionBuilder() val builder = TransactionBuilder(notary = notary)
builder.addOutputState(Cash.State(amount, from)) builder.addOutputState(Cash.State(amount, from))
builder.addCommand(Command(Cash.Commands.Issue(), amount.token.issuer.party.owningKey)) builder.addCommand(Command(Cash.Commands.Issue(), amount.token.issuer.party.owningKey))
builder.toWireTransaction().outRef<Cash.State>(0) 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 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) } 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( val clientToServiceCommandGenerator = Generator.frequency(
0.4 to issueCashGenerator, 0.4 to issueCashGenerator,
0.5 to moveCashGenerator, 0.5 to moveCashGenerator,

View File

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

View File

@ -1,40 +1,27 @@
package com.r3corda.client.model package com.r3corda.client.model
import com.r3corda.client.fxutils.foldToObservableList import com.r3corda.client.fxutils.*
import com.r3corda.client.fxutils.getObservableValue
import com.r3corda.core.contracts.ContractState import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.contracts.StateRef import com.r3corda.core.contracts.StateRef
import com.r3corda.client.fxutils.recordInSequence import com.r3corda.client.fxutils.recordInSequence
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.protocols.StateMachineRunId import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.transactions.SignedTransaction import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.node.services.monitor.ServiceToClientEvent import com.r3corda.node.services.messaging.StateMachineUpdate
import com.r3corda.node.services.monitor.TransactionBuildResult
import com.r3corda.node.utilities.AddOrRemove
import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import javafx.collections.ObservableList import javafx.collections.ObservableList
import javafx.collections.ObservableMap import javafx.collections.ObservableMap
import org.fxmisc.easybind.EasyBind import org.fxmisc.easybind.EasyBind
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import rx.Observable import rx.Observable
import java.time.Instant
import java.util.UUID
import kotlin.reflect.KProperty1
interface GatheredTransactionData { data class GatheredTransactionData(
val stateMachineRunId: ObservableValue<StateMachineRunId?> val transaction: PartiallyResolvedTransaction,
val uuid: ObservableValue<UUID?> val stateMachines: ObservableList<out StateMachineData>
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>
}
/** /**
* [PartiallyResolvedTransaction] holds a [SignedTransaction] that has zero or more inputs resolved. The intent is * [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( data class ProtocolStatus(
val status: String val status: String
) )
sealed class StateMachineStatus(val stateMachineName: String) { sealed class StateMachineStatus(val stateMachineName: String) {
class Added(stateMachineName: String): StateMachineStatus(stateMachineName) class Added(stateMachineName: String): StateMachineStatus(stateMachineName)
class Removed(stateMachineName: String): StateMachineStatus(stateMachineName) class Removed(stateMachineName: String): StateMachineStatus(stateMachineName)
override fun toString(): String = "${javaClass.simpleName}($stateMachineName)" override fun toString(): String = "${javaClass.simpleName}($stateMachineName)"
} }
data class GatheredTransactionDataWritable( data class StateMachineData(
override val stateMachineRunId: SimpleObjectProperty<StateMachineRunId?> = SimpleObjectProperty(null), val id: StateMachineRunId,
override val uuid: SimpleObjectProperty<UUID?> = SimpleObjectProperty(null), val protocolStatus: ObservableValue<ProtocolStatus?>,
override val stateMachineStatus: SimpleObjectProperty<StateMachineStatus?> = SimpleObjectProperty(null), val stateMachineStatus: ObservableValue<StateMachineStatus>
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)
/** /**
* 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 { 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)
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))
}
}
}
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)
}
/** /**
* Aggregation of updates to transactions. We use the observable list as the only container and do linear search for * We JOIN the transaction list with state machines
* 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> = val gatheredTransactionDataList: ObservableList<out GatheredTransactionData> =
serviceToClient.foldToObservableList<ServiceToClientEvent, GatheredTransactionDataWritable, ObservableMap<SecureHash, SignedTransaction>>( partiallyResolvedTransactions.leftOuterJoin(
initialAccumulator = FXCollections.observableHashMap<SecureHash, SignedTransaction>(), smTxMappingList,
folderFun = { serviceToClientEvent, transactions, transactionStates -> PartiallyResolvedTransaction::id,
val _unit = when (serviceToClientEvent) { StateMachineTransactionMapping::transactionId
is ServiceToClientEvent.Transaction -> { ) { transaction, mappings ->
transactions.set(serviceToClientEvent.transaction.id, serviceToClientEvent.transaction) GatheredTransactionData(
val somewhatResolvedTransaction = PartiallyResolvedTransaction.fromSignedTransaction( transaction,
serviceToClientEvent.transaction, mappings.map { mapping ->
transactions stateMachineDataMap.getObservableValue(mapping.stateMachineRunId)
) }.flatten().filterNotNull()
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
}
)
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)
}
}
}
} }

View File

@ -8,6 +8,7 @@ import org.reactfx.EventSink
import org.reactfx.EventStream import org.reactfx.EventStream
import rx.Observable import rx.Observable
import rx.Observer import rx.Observer
import rx.subjects.Subject
import java.util.* import java.util.*
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KProperty 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>) = inline fun <reified M : Any, T> observer(noinline observerProperty: (M) -> Observer<T>) =
TrackedDelegate.ObserverDelegate(M::class, observerProperty) 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>) = inline fun <reified M : Any, T> eventStream(noinline streamProperty: (M) -> EventStream<T>) =
TrackedDelegate.EventStreamDelegate(M::class, streamProperty) TrackedDelegate.EventStreamDelegate(M::class, streamProperty)
@ -118,14 +122,19 @@ object Models {
sealed class TrackedDelegate<M : Any>(val klass: KClass<M>) { sealed class TrackedDelegate<M : Any>(val klass: KClass<M>) {
init { Models.initModel(klass) } 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> { 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> { 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) { 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 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.contracts.ClientToServiceCommand
import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.NodeInfo
import com.r3corda.node.services.monitor.ServiceToClientEvent import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.node.services.monitor.StateSnapshotMessage 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.Observable
import rx.Observer
import rx.subjects.PublishSubject 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 { 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>() private val clientToServiceSource = PublishSubject.create<ClientToServiceCommand>()
val clientToService: Observer<ClientToServiceCommand> = clientToServiceSource val clientToService: PublishSubject<ClientToServiceCommand> = clientToServiceSource
private val serviceToClientSource = PublishSubject.create<ServiceToClientEvent>()
val serviceToClient: Observable<ServiceToClientEvent> = serviceToClientSource
private val snapshotSource = PublishSubject.create<StateSnapshotMessage>()
val snapshot: Observable<StateSnapshotMessage> = snapshotSource
/** /**
* 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 messagingService The messaging to use for communication.
* @param monitorNodeInfo the [Node] to connect to. * @param monitorNodeInfo the [Node] to connect to.
* TODO provide an unsubscribe mechanism * TODO provide an unsubscribe mechanism
*/ */
fun register(messagingService: MessagingService, monitorNodeInfo: NodeInfo) { fun register(vaultMonitorNodeInfo: NodeInfo, certificatesPath: Path) {
val monitorClient = NodeMonitorClient(
messagingService, val client = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(vaultMonitorNodeInfo.address), certificatesPath)
monitorNodeInfo, client.start()
clientToServiceSource, val proxy = client.proxy()
serviceToClientSource,
snapshotSource val (stateMachines, stateMachineUpdates) = proxy.stateMachinesAndUpdates()
) // Extract the protocol tracking stream
require(monitorClient.register().get()) // 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 package com.r3corda.explorer
import com.r3corda.client.NodeMonitorClient
import com.r3corda.client.mock.EventGenerator 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.Models
import com.r3corda.client.model.NodeMonitorModel 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.contracts.ClientToServiceCommand
import com.r3corda.core.node.services.ServiceInfo import com.r3corda.core.node.services.ServiceInfo
import com.r3corda.explorer.model.IdentityModel import com.r3corda.explorer.model.IdentityModel
import com.r3corda.node.driver.PortAllocation import com.r3corda.node.driver.PortAllocation
import com.r3corda.node.driver.driver import com.r3corda.node.driver.driver
import com.r3corda.node.driver.startClient import com.r3corda.node.driver.startClient
import com.r3corda.node.services.monitor.ServiceToClientEvent
import com.r3corda.node.services.transactions.SimpleNotaryService import com.r3corda.node.services.transactions.SimpleNotaryService
import javafx.stage.Stage import javafx.stage.Stage
import rx.Observer import rx.subjects.Subject
import rx.subjects.PublishSubject
import tornadofx.App import tornadofx.App
import java.util.* import java.util.*
class Main : App() { class Main : App() {
override val primaryView = MainWindow::class 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) { override fun start(stage: Stage) {
@ -42,35 +37,26 @@ class Main : App() {
driver(portAllocation = portAllocation) { driver(portAllocation = portAllocation) {
val aliceNodeFuture = startNode("Alice") val aliceNodeFuture = startNode("Alice")
val bobNodeFuture = startNode("Bob")
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.Type))) val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.Type)))
val aliceNode = aliceNodeFuture.get() val aliceNode = aliceNodeFuture.get()
val bobNode = bobNodeFuture.get()
val notaryNode = notaryNodeFuture.get() val notaryNode = notaryNodeFuture.get()
val aliceClient = startClient(aliceNode).get() val aliceClient = startClient(aliceNode).get()
Models.get<IdentityModel>(Main::class).myIdentity.set(aliceNode.identity) Models.get<IdentityModel>(Main::class).myIdentity.set(aliceNode.identity)
Models.get<NodeMonitorModel>(Main::class).register(aliceClient, aliceNode) Models.get<NodeMonitorModel>(Main::class).register(aliceNode, aliceClient.config.certificatesPath)
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())
for (i in 0 .. 10000) { for (i in 0 .. 10000) {
Thread.sleep(500) Thread.sleep(500)
val eventGenerator = EventGenerator( val eventGenerator = EventGenerator(
parties = listOf(aliceNode.identity, bobNode.identity), parties = listOf(aliceNode.identity),
notary = notaryNode.identity notary = notaryNode.identity
) )
eventGenerator.clientToServiceCommandGenerator.combine(Generator.oneOf(listOf(aliceOutStream, bobOutStream))) { eventGenerator.clientToServiceCommandGenerator.map { command ->
command, stream -> stream.onNext(command) aliceOutStream.onNext(command)
}.generate(Random()) }.generate(Random())
} }
@ -80,4 +66,3 @@ class Main : App() {
}).start() }).start()
} }
} }

View File

@ -16,7 +16,6 @@ import com.r3corda.explorer.model.IdentityModel
import com.r3corda.explorer.model.ReportingCurrencyModel import com.r3corda.explorer.model.ReportingCurrencyModel
import com.r3corda.explorer.sign import com.r3corda.explorer.sign
import com.r3corda.explorer.ui.* import com.r3corda.explorer.ui.*
import com.r3corda.node.services.monitor.ServiceToClientEvent
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.beans.value.ObservableValue import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections import javafx.collections.FXCollections
@ -31,7 +30,6 @@ import javafx.scene.layout.VBox
import javafx.scene.paint.Color import javafx.scene.paint.Color
import tornadofx.View import tornadofx.View
import java.security.PublicKey import java.security.PublicKey
import java.time.Instant
import java.util.* import java.util.*
class TransactionViewer: View() { class TransactionViewer: View() {
@ -74,11 +72,6 @@ class TransactionViewer: View() {
private val signaturesTitledPane: TitledPane by fxid() private val signaturesTitledPane: TitledPane by fxid()
private val signaturesList: ListView<PublicKey> 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() private val matchingTransactionsLabel: Label by fxid()
// Inject data // Inject data
@ -93,18 +86,13 @@ class TransactionViewer: View() {
* have the data. * have the data.
*/ */
data class ViewerNode( data class ViewerNode(
val transactionId: ObservableValue<SecureHash?>, val transaction: PartiallyResolvedTransaction,
val transactionId: SecureHash,
val stateMachineRunId: ObservableValue<StateMachineRunId?>, val stateMachineRunId: ObservableValue<StateMachineRunId?>,
val clientUuid: ObservableValue<UUID?>, val stateMachineStatus: ObservableValue<out StateMachineStatus?>,
val originator: ObservableValue<String>, val protocolStatus: ObservableValue<out ProtocolStatus?>,
val transactionStatus: ObservableValue<TransactionCreateStatus?>, val commandTypes: Collection<Class<CommandData>>,
val stateMachineStatus: ObservableValue<StateMachineStatus?>, val totalValueEquiv: ObservableValue<AmountDiff<Currency>?>
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>
) )
/** /**
@ -119,42 +107,26 @@ class TransactionViewer: View() {
* We map the gathered data about transactions almost one-to-one to the nodes. * We map the gathered data about transactions almost one-to-one to the nodes.
*/ */
private val viewerNodes = gatheredTransactionDataList.map { 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( ViewerNode(
transactionId = it.transaction.map { it?.id }, transaction = it.transaction,
stateMachineRunId = it.stateMachineRunId, transactionId = it.transaction.id,
clientUuid = it.uuid, stateMachineRunId = stateMachine.map { it?.id },
/** protocolStatus = stateMachineProperty { it.protocolStatus },
* We can't really do any better based on uuid, we need to store explicit data for this TODO stateMachineStatus = stateMachineProperty { it.stateMachineStatus },
*/ commandTypes = it.transaction.transaction.tx.commands.map { it.value.javaClass },
originator = it.uuid.map { uuid -> totalValueEquiv = {
if (uuid == null) { val resolvedInputs = it.transaction.inputs.sequence().map { resolution ->
"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) { when (resolution) {
is PartiallyResolvedTransaction.InputResolution.Unresolved -> null is PartiallyResolvedTransaction.InputResolution.Unresolved -> null
is PartiallyResolvedTransaction.InputResolution.Resolved -> resolution.stateAndRef is PartiallyResolvedTransaction.InputResolution.Resolved -> resolution.stateAndRef
} }
}.foldObservable(listOf()) { inputs: List<StateAndRef<ContractState>>?, state: StateAndRef<ContractState>? -> }.fold(listOf()) { inputs: List<StateAndRef<ContractState>>?, state: StateAndRef<ContractState>? ->
if (inputs != null && state != null) { if (inputs != null && state != null) {
inputs + state inputs + state
} else { } else {
@ -165,13 +137,10 @@ class TransactionViewer: View() {
::calculateTotalEquiv.lift( ::calculateTotalEquiv.lift(
myIdentity, myIdentity,
reportingExchange, reportingExchange,
resolvedInputs, resolvedInputs.lift(),
transaction.transaction.tx.outputs.lift() it.transaction.transaction.tx.outputs.lift()
) )
} }()
},
transaction = it.transaction,
allEvents = it.allEvents
) )
} }
@ -179,9 +148,9 @@ class TransactionViewer: View() {
* The detail panes are only filled out if a transaction is selected * The detail panes are only filled out if a transaction is selected
*/ */
private val selectedViewerNode = transactionViewTable.singleRowSelection() private val selectedViewerNode = transactionViewTable.singleRowSelection()
private val selectedTransaction = selectedViewerNode.bindOut { private val selectedTransaction = selectedViewerNode.map {
when (it) { when (it) {
is SingleRowSelection.None -> null.lift() is SingleRowSelection.None -> null
is SingleRowSelection.Selected -> it.node.transaction 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. * We only display the detail panes if there is a node selected.
*/ */
private val allNodesShown = FXCollections.observableArrayList<Node>( private val allNodesShown = FXCollections.observableArrayList<Node>(
transactionViewTable, transactionViewTable,
contractStatesTitledPane, contractStatesTitledPane,
signaturesTitledPane, signaturesTitledPane
lowLevelEventsTitledPane
) )
private val onlyTransactionsTableShown = FXCollections.observableArrayList<Node>( private val onlyTransactionsTableShown = FXCollections.observableArrayList<Node>(
transactionViewTable transactionViewTable
@ -326,11 +287,9 @@ class TransactionViewer: View() {
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / transactionViewTable.columns.size).toInt() 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 ?: ""}" } } transactionViewStateMachineId.setCellValueFactory { it.value.stateMachineRunId.map { "${it?.uuid ?: ""}" } }
transactionViewClientUuid.setCellValueFactory { it.value.clientUuid.map { "${it ?: ""}" } }
transactionViewProtocolStatus.setCellValueFactory { it.value.protocolStatus.map { "${it ?: ""}" } } transactionViewProtocolStatus.setCellValueFactory { it.value.protocolStatus.map { "${it ?: ""}" } }
transactionViewTransactionStatus.setCellValueFactory { it.value.transactionStatus }
transactionViewTransactionStatus.setCustomCellFactory { transactionViewTransactionStatus.setCustomCellFactory {
val label = Label() val label = Label()
val backgroundFill = when (it) { val backgroundFill = when (it) {
@ -342,7 +301,7 @@ class TransactionViewer: View() {
label.text = "$it" label.text = "$it"
label label
} }
transactionViewStateMachineStatus.setCellValueFactory { it.value.stateMachineStatus } transactionViewStateMachineStatus.setCellValueFactory { it.value.stateMachineStatus.map { it } }
transactionViewStateMachineStatus.setCustomCellFactory { transactionViewStateMachineStatus.setCustomCellFactory {
val label = Label() val label = Label()
val backgroundFill = when (it) { val backgroundFill = when (it) {
@ -356,7 +315,7 @@ class TransactionViewer: View() {
} }
transactionViewCommandTypes.setCellValueFactory { 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.setCellValueFactory<ViewerNode, AmountDiff<Currency>> { it.value.totalValueEquiv }
transactionViewTotalValueEquiv.cellFactory = object : Formatter<AmountDiff<Currency>> { transactionViewTotalValueEquiv.cellFactory = object : Formatter<AmountDiff<Currency>> {
@ -394,14 +353,6 @@ class TransactionViewer: View() {
override fun format(value: PublicKey) = value.toStringShort() override fun format(value: PublicKey) = value.toStringShort()
}.toListCellFactory() }.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 { matchingTransactionsLabel.textProperty().bind(Bindings.size(viewerNodes).map {
"$it matching transaction${if (it == 1) "" else "s"}" "$it matching transaction${if (it == 1) "" else "s"}"
}) })

View File

@ -40,7 +40,7 @@
</ImageView> </ImageView>
</children> </children>
</StackPane> </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> <items>
<TableView fx:id="transactionViewTable" prefHeight="200.0" prefWidth="200.0"> <TableView fx:id="transactionViewTable" prefHeight="200.0" prefWidth="200.0">
<columns> <columns>
@ -121,21 +121,11 @@
</SplitPane> </SplitPane>
</content> </content>
</TitledPane> </TitledPane>
<TitledPane fx:id="signaturesTitledPane" animated="false" text="Required signatures"> <TitledPane fx:id="signaturesTitledPane" animated="false" text="Signatures">
<content> <content>
<ListView fx:id="signaturesList" /> <ListView fx:id="signaturesList" />
</content> </content>
</TitledPane> </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> </items>
</SplitPane> </SplitPane>
<HBox> <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.identity.InMemoryIdentityService
import com.r3corda.node.services.keys.PersistentKeyManagementService import com.r3corda.node.services.keys.PersistentKeyManagementService
import com.r3corda.node.services.messaging.CordaRPCOps 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.InMemoryNetworkMapCache
import com.r3corda.node.services.network.NetworkMapService import com.r3corda.node.services.network.NetworkMapService
import com.r3corda.node.services.network.NetworkMapService.Companion.REGISTER_PROTOCOL_TOPIC 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 vault: VaultService
lateinit var keyManagement: KeyManagementService lateinit var keyManagement: KeyManagementService
var inNodeNetworkMapService: NetworkMapService? = null var inNodeNetworkMapService: NetworkMapService? = null
var inNodeMonitorService: NodeMonitorService? = null
var inNodeNotaryService: NotaryService? = null var inNodeNotaryService: NotaryService? = null
var uniquenessProvider: UniquenessProvider? = null var uniquenessProvider: UniquenessProvider? = null
lateinit var identity: IdentityService 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() buildAdvertisedServices()
// TODO: this model might change but for now it provides some de-coupling // 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. // TODO: sort out ordering of open & protected modifiers of functions in this class.
protected open fun makeVaultService(): VaultService = NodeVaultService(services) protected open fun makeVaultService(): VaultService = NodeVaultService(services)
protected open fun makeMonitorService(): NodeMonitorService = NodeMonitorService(services, smm)
open fun stop() { open fun stop() {
// TODO: We need a good way of handling "nice to have" shutdown events, especially those that deal with the // 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() // 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 package com.r3corda.node.internal
import com.r3corda.core.contracts.ContractState import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.StateAndRef 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.node.services.Vault
import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.node.services.api.ServiceHubInternal import com.r3corda.node.services.api.ServiceHubInternal
import com.r3corda.node.services.messaging.CordaRPCOps import com.r3corda.node.services.messaging.CordaRPCOps
import com.r3corda.node.services.messaging.StateMachineInfo import com.r3corda.node.services.messaging.StateMachineInfo
import com.r3corda.node.services.messaging.StateMachineUpdate 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.services.statemachine.StateMachineManager
import com.r3corda.node.utilities.databaseTransaction import com.r3corda.node.utilities.databaseTransaction
import com.r3corda.protocols.BroadcastTransactionProtocol
import com.r3corda.protocols.FinalityProtocol
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import rx.Observable import rx.Observable
import java.security.KeyPair
/** /**
* Server side implementations of RPCs available to MQ based client tools. Execution takes place on the server * 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. * thread (i.e. serially). Arguments are serialised and deserialised automatically.
*/ */
class ServerRPCOps( class ServerRPCOps(
val services: ServiceHubInternal, val services: ServiceHub,
val stateMachineManager: StateMachineManager, val smm: StateMachineManager,
val database: Database val database: Database
) : CordaRPCOps { ) : CordaRPCOps {
override val protocolVersion: Int = 0 override val protocolVersion: Int = 0
@ -31,11 +40,100 @@ class ServerRPCOps(
} }
override fun verifiedTransactions() = services.storageService.validatedTransactions.track() override fun verifiedTransactions() = services.storageService.validatedTransactions.track()
override fun stateMachinesAndUpdates(): Pair<List<StateMachineInfo>, Observable<StateMachineUpdate>> { override fun stateMachinesAndUpdates(): Pair<List<StateMachineInfo>, Observable<StateMachineUpdate>> {
val (allStateMachines, changes) = stateMachineManager.track() val (allStateMachines, changes) = smm.track()
return Pair( return Pair(
allStateMachines.map { StateMachineInfo.fromProtocolStateMachineImpl(it) }, allStateMachines.map { StateMachineInfo.fromProtocolStateMachineImpl(it) },
changes.map { StateMachineUpdate.fromStateMachineChange(it) } changes.map { StateMachineUpdate.fromStateMachineChange(it) }
) )
} }
override fun stateMachineRecordedTransactionMapping() = services.storageService.stateMachineRecordedTransactionMapping.track() 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 package com.r3corda.node.services.messaging
import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.contracts.ContractState import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef 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.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId import com.r3corda.core.protocols.StateMachineRunId
@ -28,9 +28,9 @@ data class StateMachineInfo(
} }
} }
sealed class StateMachineUpdate { sealed class StateMachineUpdate(val id: StateMachineRunId) {
class Added(val stateMachineInfo: StateMachineInfo) : StateMachineUpdate() class Added(val stateMachineInfo: StateMachineInfo) : StateMachineUpdate(stateMachineInfo.id)
class Removed(val stateMachineRunId: StateMachineRunId) : StateMachineUpdate() class Removed(id: StateMachineRunId) : StateMachineUpdate(id)
companion object { companion object {
fun fromStateMachineChange(change: StateMachineManager.Change): StateMachineUpdate { 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 * 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. * client apps and are implemented by the node in the [ServerRPCOps] class.
@ -73,6 +95,15 @@ interface CordaRPCOps : RPCOps {
*/ */
@RPCReturnsObservables @RPCReturnsObservables
fun verifiedTransactions(): Pair<List<SignedTransaction>, Observable<SignedTransaction>> fun verifiedTransactions(): Pair<List<SignedTransaction>, Observable<SignedTransaction>>
/**
* Returns a pair of state machine id - recorded transaction hash pairs
*/
@RPCReturnsObservables @RPCReturnsObservables
fun stateMachineRecordedTransactionMapping(): Pair<List<StateMachineTransactionMapping>, Observable<StateMachineTransactionMapping>> 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.Serializer
import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output 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.ErrorOr
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party 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.serialization.*
import com.r3corda.core.transactions.SignedTransaction import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.WireTransaction import com.r3corda.core.transactions.WireTransaction
import de.javakaffee.kryoserializers.ArraysAsListSerializer import de.javakaffee.kryoserializers.ArraysAsListSerializer
import de.javakaffee.kryoserializers.guava.* 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.apache.activemq.artemis.api.core.client.ClientMessage
import org.objenesis.strategy.StdInstantiatorStrategy import org.objenesis.strategy.StdInstantiatorStrategy
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -118,7 +128,41 @@ private class RPCKryo(private val observableSerializer: Serializer<Observable<An
register(Notification::class.java) register(Notification::class.java)
register(Notification.Kind::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. // Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway.
register(IllegalArgumentException::class.java) register(IllegalArgumentException::class.java)

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