diff --git a/.idea/runConfigurations/Explorer___demo_nodes__flow_triage_.xml b/.idea/runConfigurations/Explorer___demo_nodes__flow_triage_.xml
new file mode 100644
index 0000000000..dbcebe8258
--- /dev/null
+++ b/.idea/runConfigurations/Explorer___demo_nodes__flow_triage_.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt
index 946815f91c..df54451540 100644
--- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt
+++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt
@@ -4,9 +4,7 @@ import com.google.common.net.HostAndPort
import javafx.beans.property.SimpleObjectProperty
import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.CordaRPCClientConfiguration
-import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.CordaRPCOps
-import net.corda.core.messaging.StateMachineInfo
import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.node.services.StateMachineTransactionMapping
@@ -16,17 +14,6 @@ import net.corda.core.transactions.SignedTransaction
import rx.Observable
import rx.subjects.PublishSubject
-data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val message: String) {
- companion object {
- fun createStreamFromStateMachineInfo(stateMachine: StateMachineInfo): Observable? {
- 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 node.
*/
@@ -75,7 +62,9 @@ class NodeMonitorModel {
Observable.empty()
}
}
- futureProgressTrackerUpdates.startWith(currentProgressTrackerUpdates).flatMap { it }.subscribe(progressTrackingSubject)
+
+ // We need to retry, because when flow errors, we unsubscribe from progressTrackingSubject. So we end up with stream of state machine updates and no progress trackers.
+ futureProgressTrackerUpdates.startWith(currentProgressTrackerUpdates).flatMap { it }.retry().subscribe(progressTrackingSubject)
// Now the state machines
val currentStateMachines = stateMachines.map { StateMachineUpdate.Added(it) }
diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/StateMachineDataModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/StateMachineDataModel.kt
new file mode 100644
index 0000000000..300523906b
--- /dev/null
+++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/StateMachineDataModel.kt
@@ -0,0 +1,97 @@
+package net.corda.client.jfx.model
+
+import javafx.beans.property.SimpleIntegerProperty
+import javafx.beans.property.SimpleObjectProperty
+import javafx.beans.value.ObservableValue
+import javafx.collections.FXCollections
+import net.corda.client.jfx.utils.fold
+import net.corda.client.jfx.utils.map
+import net.corda.client.jfx.utils.recordAsAssociation
+import net.corda.core.ErrorOr
+import net.corda.core.flows.FlowInitiator
+import net.corda.core.flows.StateMachineRunId
+import net.corda.core.messaging.StateMachineInfo
+import net.corda.core.messaging.StateMachineUpdate
+import org.fxmisc.easybind.EasyBind
+import rx.Observable
+
+data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val message: String) {
+ companion object {
+ fun createStreamFromStateMachineInfo(stateMachine: StateMachineInfo): Observable? {
+ return stateMachine.progressTrackerStepAndUpdates?.let { (current, future) ->
+ future.map { ProgressTrackingEvent(stateMachine.id, it) }.startWith(ProgressTrackingEvent(stateMachine.id, current))
+ }
+ }
+ }
+}
+
+data class ProgressStatus(val status: String?)
+
+sealed class StateMachineStatus {
+ data class Added(val id: StateMachineRunId, val stateMachineName: String, val flowInitiator: FlowInitiator) : StateMachineStatus()
+ data class Removed(val id: StateMachineRunId, val result: ErrorOr<*>) : StateMachineStatus()
+}
+
+data class StateMachineData(
+ val id: StateMachineRunId,
+ val stateMachineName: String,
+ val flowInitiator: FlowInitiator,
+ val smmStatus: Pair, ObservableValue>
+)
+
+data class Counter(
+ var errored: SimpleIntegerProperty = SimpleIntegerProperty(0),
+ var success: SimpleIntegerProperty = SimpleIntegerProperty(0),
+ var progress: SimpleIntegerProperty = SimpleIntegerProperty(0)
+) {
+ fun addSmm() { progress.value += 1 }
+ fun removeSmm(result: ErrorOr<*>) {
+ progress.value -= 1
+ when (result.error) {
+ null -> success.value += 1
+ else -> errored.value += 1
+ }
+ }
+}
+
+class StateMachineDataModel {
+ private val stateMachineUpdates by observable(NodeMonitorModel::stateMachineUpdates)
+ private val progressTracking by observable(NodeMonitorModel::progressTracking)
+ private val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
+
+ val counter = Counter()
+
+ private val stateMachineIndexMap = HashMap()
+ private val stateMachineStatus = stateMachineUpdates.fold(FXCollections.observableArrayList>()) { list, update ->
+ when (update) {
+ is StateMachineUpdate.Added -> {
+ counter.addSmm()
+ val flowInitiator= update.stateMachineInfo.initiator
+ val added: SimpleObjectProperty =
+ SimpleObjectProperty(StateMachineStatus.Added(update.id, update.stateMachineInfo.flowLogicClassName, flowInitiator))
+ list.add(added)
+ stateMachineIndexMap[update.id] = list.size - 1
+ }
+ is StateMachineUpdate.Removed -> {
+ val addedIdx = stateMachineIndexMap[update.id]
+ val added = addedIdx?.let { list.getOrNull(addedIdx) }
+ added ?: throw Exception("State machine removed with unknown id ${update.id}")
+ counter.removeSmm(update.result)
+ list[addedIdx].set(StateMachineStatus.Removed(update.id, update.result))
+ }
+ }
+ }
+
+ private val stateMachineDataList = stateMachineStatus.map {
+ val smStatus = it.value as StateMachineStatus.Added
+ val id = smStatus.id
+ val progress = SimpleObjectProperty(progressEvents.get(id))
+ StateMachineData(id, smStatus.stateMachineName, smStatus.flowInitiator,
+ Pair(it, EasyBind.map(progress) { ProgressStatus(it?.message) }))
+ }
+
+ val stateMachinesAll = stateMachineDataList
+ val error = counter.errored
+ val success = counter.success
+ val progress = counter.progress
+}
diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt
index 9e0cc1e6e9..62d6da5dea 100644
--- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt
+++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt
@@ -1,8 +1,6 @@
package net.corda.client.jfx.model
-import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue
-import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.collections.ObservableMap
import net.corda.client.jfx.utils.*
@@ -10,8 +8,6 @@ import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash
-import net.corda.core.flows.StateMachineRunId
-import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.transactions.SignedTransaction
import org.fxmisc.easybind.EasyBind
@@ -58,53 +54,15 @@ data class PartiallyResolvedTransaction(
}
}
-data class FlowStatus(val status: String)
-
-sealed class StateMachineStatus {
- abstract val stateMachineName: String
-
- data class Added(override val stateMachineName: String) : StateMachineStatus()
- data class Removed(override val stateMachineName: String) : StateMachineStatus()
-}
-
-data class StateMachineData(
- val id: StateMachineRunId,
- val flowStatus: ObservableValue,
- val stateMachineStatus: ObservableValue
-)
-
/**
* This model provides an observable list of transactions and what state machines/flows recorded them
*/
class TransactionDataModel {
private val transactions by observable(NodeMonitorModel::transactions)
- private val stateMachineUpdates by observable(NodeMonitorModel::stateMachineUpdates)
- private val progressTracking by observable(NodeMonitorModel::progressTracking)
- private val stateMachineTransactionMapping by observable(NodeMonitorModel::stateMachineTransactionMapping)
private val collectedTransactions = transactions.recordInSequence()
private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
- private val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
- private val stateMachineStatus = stateMachineUpdates.fold(FXCollections.observableHashMap>()) { map, update ->
- when (update) {
- is StateMachineUpdate.Added -> {
- val added: SimpleObjectProperty =
- SimpleObjectProperty(StateMachineStatus.Added(update.stateMachineInfo.flowLogicClassName))
- map[update.id] = added
- }
- is StateMachineUpdate.Removed -> {
- val added = map[update.id]
- added ?: throw Exception("State machine removed with unknown id ${update.id}")
- added.set(StateMachineStatus.Removed(added.value.stateMachineName))
- }
- }
- }
- private val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
- StateMachineData(id, progress.map { it?.let { FlowStatus(it.message) } }, status)
- }.getObservableValues()
- // TODO : Create a new screen for state machines.
- private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
- private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
+
val partiallyResolvedTransactions = collectedTransactions.map {
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
}
diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt
index f8effdfa8e..771fca5fd1 100644
--- a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt
+++ b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt
@@ -1,36 +1,90 @@
package net.corda.client.mock
import net.corda.core.contracts.Amount
+import net.corda.core.contracts.GBP
+import net.corda.core.contracts.USD
import net.corda.core.identity.Party
import net.corda.core.serialization.OpaqueBytes
import net.corda.flows.CashFlowCommand
import java.util.*
/**
- * [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned
- * state/ref pairs, but it doesn't necessarily generate "correct" events!
+ * [Generator]s for incoming/outgoing cash flow events between parties. It doesn't necessarily generate correct events!
+ * Especially at the beginning of simulation there might be few insufficient spend errors.
*/
-class EventGenerator(val parties: List, val currencies: List, val notary: Party) {
- private val partyGenerator = Generator.pickOne(parties)
- private val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
- private val amountGenerator = Generator.longRange(10000, 1000000)
- private val currencyGenerator = Generator.pickOne(currencies)
+open class EventGenerator(val parties: List, val currencies: List, val notary: Party) {
+ protected val partyGenerator = Generator.pickOne(parties)
+ protected val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
+ protected val amountGenerator = Generator.longRange(10000, 1000000)
+ protected val currencyGenerator = Generator.pickOne(currencies)
+ protected val currencyMap: MutableMap = mutableMapOf(USD to 0L, GBP to 0L) // Used for estimation of how much money we have in general.
- private val issueCashGenerator = amountGenerator.combine(partyGenerator, issueRefGenerator, currencyGenerator) { amount, to, issueRef, ccy ->
+ protected fun addToMap(ccy: Currency, amount: Long) {
+ currencyMap.computeIfPresent(ccy) { _, value -> Math.max(0L, value + amount) }
+ }
+
+ protected val issueCashGenerator = amountGenerator.combine(partyGenerator, issueRefGenerator, currencyGenerator) { amount, to, issueRef, ccy ->
+ addToMap(ccy, amount)
CashFlowCommand.IssueCash(Amount(amount, ccy), issueRef, to, notary)
}
- private val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy ->
+ protected val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy ->
+ addToMap(ccy, -amount)
CashFlowCommand.ExitCash(Amount(amount, ccy), issueRef)
}
- val moveCashGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
+ open val moveCashGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient)
}
- val issuerGenerator = Generator.frequency(listOf(
+ open val issuerGenerator = Generator.frequency(listOf(
0.1 to exitCashGenerator,
0.9 to issueCashGenerator
))
}
+
+/**
+ * [Generator]s for incoming/outgoing events of starting different cash flows. It invokes flows that throw exceptions
+ * for use in explorer flow triage. Exceptions are of kind spending/exiting too much cash.
+ */
+class ErrorFlowsEventGenerator(parties: List, currencies: List, notary: Party): EventGenerator(parties, currencies, notary) {
+ enum class IssuerEvents {
+ NORMAL_EXIT,
+ EXIT_ERROR
+ }
+
+ val errorGenerator = Generator.pickOne(IssuerEvents.values().toList())
+
+ val errorExitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator, errorGenerator) { amount, issueRef, ccy, errorType ->
+ when (errorType) {
+ IssuerEvents.NORMAL_EXIT -> {
+ println("Normal exit")
+ if (currencyMap[ccy]!! <= amount) addToMap(ccy, -amount)
+ CashFlowCommand.ExitCash(Amount(amount, ccy), issueRef) // It may fail at the beginning, but we don't care.
+ }
+ IssuerEvents.EXIT_ERROR -> {
+ println("Exit error")
+ CashFlowCommand.ExitCash(Amount(currencyMap[ccy]!! * 2, ccy), issueRef)
+ }
+ }
+ }
+
+ val normalMoveGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
+ CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient)
+ }
+
+ val errorMoveGenerator = partyGenerator.combine(currencyGenerator) { recipient, currency ->
+ CashFlowCommand.PayCash(Amount(currencyMap[currency]!! * 2, currency), recipient)
+ }
+
+ override val moveCashGenerator = Generator.frequency(listOf(
+ 0.2 to errorMoveGenerator,
+ 0.8 to normalMoveGenerator
+ ))
+
+ override val issuerGenerator = Generator.frequency(listOf(
+ 0.3 to errorExitCashGenerator,
+ 0.7 to issueCashGenerator
+ ))
+}
diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt
index 1fe15cd1f7..0530a4fa6b 100644
--- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt
+++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt
@@ -217,7 +217,7 @@ abstract class FlowLogic {
fun track(): Pair>? {
// TODO this is not threadsafe, needs an atomic get-step-and-subscribe
return progressTracker?.let {
- it.currentStep.toString() to it.changes.map { it.toString() }
+ it.currentStep.label to it.changes.map { it.toString() }
}
}
diff --git a/tools/explorer/build.gradle b/tools/explorer/build.gradle
index f3609ec2db..f9502fd833 100644
--- a/tools/explorer/build.gradle
+++ b/tools/explorer/build.gradle
@@ -68,3 +68,9 @@ task(runSimulationNodes, dependsOn: 'classes', type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
args '-S'
}
+
+task(runFlowTriageNodes, dependsOn: 'classes', type: JavaExec) {
+ main = 'net.corda.explorer.MainKt'
+ classpath = sourceSets.main.runtimeClasspath
+ args '-F'
+}
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt
new file mode 100644
index 0000000000..ebb4d5b34b
--- /dev/null
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt
@@ -0,0 +1,206 @@
+package net.corda.explorer
+
+import joptsimple.OptionSet
+import net.corda.client.mock.ErrorFlowsEventGenerator
+import net.corda.client.mock.EventGenerator
+import net.corda.client.mock.Generator
+import net.corda.client.mock.pickOne
+import net.corda.client.rpc.CordaRPCConnection
+import net.corda.contracts.asset.Cash
+import net.corda.core.contracts.Amount
+import net.corda.core.contracts.GBP
+import net.corda.core.contracts.USD
+import net.corda.core.failure
+import net.corda.core.identity.Party
+import net.corda.core.messaging.CordaRPCOps
+import net.corda.core.messaging.FlowHandle
+import net.corda.core.node.services.ServiceInfo
+import net.corda.core.node.services.ServiceType
+import net.corda.core.serialization.OpaqueBytes
+import net.corda.core.success
+import net.corda.core.transactions.SignedTransaction
+import net.corda.core.utilities.ALICE
+import net.corda.core.utilities.BOB
+import net.corda.core.utilities.DUMMY_NOTARY
+import net.corda.flows.CashExitFlow
+import net.corda.flows.CashFlowCommand
+import net.corda.flows.CashIssueFlow
+import net.corda.flows.CashPaymentFlow
+import net.corda.flows.IssuerFlow
+import net.corda.node.driver.NodeHandle
+import net.corda.node.driver.PortAllocation
+import net.corda.node.driver.driver
+import net.corda.node.services.startFlowPermission
+import net.corda.node.services.transactions.SimpleNotaryService
+import net.corda.nodeapi.User
+import org.bouncycastle.asn1.x500.X500Name
+import java.time.Instant
+import java.util.*
+
+class ExplorerSimulation(val options: OptionSet) {
+ val user = User("user1", "test", permissions = setOf(
+ startFlowPermission()
+ ))
+ val manager = User("manager", "test", permissions = setOf(
+ startFlowPermission(),
+ startFlowPermission(),
+ startFlowPermission(),
+ startFlowPermission())
+ )
+
+ lateinit var notaryNode: NodeHandle
+ lateinit var aliceNode: NodeHandle
+ lateinit var bobNode: NodeHandle
+ lateinit var issuerNodeGBP: NodeHandle
+ lateinit var issuerNodeUSD: NodeHandle
+
+ val RPCConnections = ArrayList()
+ val issuers = HashMap()
+ val parties = ArrayList>()
+
+ init {
+ startDemoNodes()
+ }
+
+ private fun onEnd() {
+ println("Closing RPC connections")
+ RPCConnections.forEach { it.close() }
+ }
+
+ private fun startDemoNodes() {
+ val portAllocation = PortAllocation.Incremental(20000)
+ driver(portAllocation = portAllocation) {
+ // TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo.
+ val notary = startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)),
+ customOverrides = mapOf("nearestCity" to "Zurich"))
+ val alice = startNode(ALICE.name, rpcUsers = arrayListOf(user),
+ advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))),
+ customOverrides = mapOf("nearestCity" to "Milan"))
+ val bob = startNode(BOB.name, rpcUsers = arrayListOf(user),
+ advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))),
+ customOverrides = mapOf("nearestCity" to "Madrid"))
+ val ukBankName = X500Name("CN=UK Bank Plc,O=UK Bank Plc,L=London,C=UK")
+ val usaBankName = X500Name("CN=USA Bank Corp,O=USA Bank Corp,L=New York,C=USA")
+ val issuerGBP = startNode(ukBankName, rpcUsers = arrayListOf(manager),
+ advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.GBP"))),
+ customOverrides = mapOf("nearestCity" to "London"))
+ val issuerUSD = startNode(usaBankName, rpcUsers = arrayListOf(manager),
+ advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.USD"))),
+ customOverrides = mapOf("nearestCity" to "New York"))
+
+ notaryNode = notary.get()
+ aliceNode = alice.get()
+ bobNode = bob.get()
+ issuerNodeGBP = issuerGBP.get()
+ issuerNodeUSD = issuerUSD.get()
+
+ arrayOf(notaryNode, aliceNode, bobNode, issuerNodeGBP, issuerNodeUSD).forEach {
+ println("${it.nodeInfo.legalIdentity} started on ${it.configuration.rpcAddress}")
+ }
+
+ when {
+ options.has("S") -> startNormalSimulation()
+ options.has("F") -> startErrorFlowsSimulation()
+ }
+
+ waitForAllNodesToFinish()
+ }
+ }
+
+ private fun setUpRPC() {
+ // Register with alice to use alice's RPC proxy to create random events.
+ val aliceClient = aliceNode.rpcClientToNode()
+ val aliceConnection = aliceClient.start(user.username, user.password)
+ val aliceRPC = aliceConnection.proxy
+
+ val bobClient = bobNode.rpcClientToNode()
+ val bobConnection = bobClient.start(user.username, user.password)
+ val bobRPC = bobConnection.proxy
+
+ val issuerClientGBP = issuerNodeGBP.rpcClientToNode()
+ val issuerGBPConnection = issuerClientGBP.start(manager.username, manager.password)
+ val issuerRPCGBP = issuerGBPConnection.proxy
+
+ val issuerClientUSD = issuerNodeUSD.rpcClientToNode()
+ val issuerUSDConnection =issuerClientUSD.start(manager.username, manager.password)
+ val issuerRPCUSD = issuerUSDConnection.proxy
+
+ RPCConnections.addAll(listOf(aliceConnection, bobConnection, issuerGBPConnection, issuerUSDConnection))
+ issuers.putAll(mapOf(USD to issuerRPCUSD, GBP to issuerRPCGBP))
+
+ parties.addAll(listOf(aliceNode.nodeInfo.legalIdentity to aliceRPC,
+ bobNode.nodeInfo.legalIdentity to bobRPC,
+ issuerNodeGBP.nodeInfo.legalIdentity to issuerRPCGBP,
+ issuerNodeUSD.nodeInfo.legalIdentity to issuerRPCUSD))
+ }
+
+ private fun startSimulation(eventGenerator: EventGenerator, maxIterations: Int) {
+ // Log to logger when flow finish.
+ fun FlowHandle.log(seq: Int, name: String) {
+ val out = "[$seq] $name $id :"
+ returnValue.success {
+ Main.log.info("$out ${it.id} ${(it.tx.outputs.first().data as Cash.State).amount}")
+ }.failure {
+ Main.log.info("$out ${it.message}")
+ }
+ }
+
+ for (i in 0..maxIterations) {
+ Thread.sleep(300)
+ // Issuer requests.
+ eventGenerator.issuerGenerator.map { command ->
+ when (command) {
+ is CashFlowCommand.IssueCash -> issuers[command.amount.token]?.let {
+ println("${Instant.now()} [$i] ISSUING ${command.amount} with ref ${command.issueRef} to ${command.recipient}")
+ command.startFlow(it).log(i, "${command.amount.token}Issuer")
+ }
+ is CashFlowCommand.ExitCash -> issuers[command.amount.token]?.let {
+ println("${Instant.now()} [$i] EXITING ${command.amount} with ref ${command.issueRef}")
+ command.startFlow(it).log(i, "${command.amount.token}Exit")
+ }
+ else -> throw IllegalArgumentException("Unsupported command: $command")
+ }
+ }.generate(SplittableRandom())
+ // Party pay requests.
+ eventGenerator.moveCashGenerator.combine(Generator.pickOne(parties)) { command, (party, rpc) ->
+ println("${Instant.now()} [$i] SENDING ${command.amount} from $party to ${command.recipient}")
+ command.startFlow(rpc).log(i, party.name.toString())
+ }.generate(SplittableRandom())
+ }
+ println("Simulation completed")
+ }
+
+ private fun startNormalSimulation() {
+ println("Running simulation mode ...")
+ setUpRPC()
+ val eventGenerator = EventGenerator(
+ parties = parties.map { it.first },
+ notary = notaryNode.nodeInfo.notaryIdentity,
+ currencies = listOf(GBP, USD)
+ )
+ val maxIterations = 100_000
+ // Pre allocate some money to each party.
+ eventGenerator.parties.forEach {
+ for (ref in 0..1) {
+ for ((currency, issuer) in issuers) {
+ CashFlowCommand.IssueCash(Amount(1_000_000, currency), OpaqueBytes(ByteArray(1, { ref.toByte() })), it, notaryNode.nodeInfo.notaryIdentity).startFlow(issuer)
+ }
+ }
+ }
+ startSimulation(eventGenerator, maxIterations)
+ onEnd()
+ }
+
+ private fun startErrorFlowsSimulation() {
+ println("Running flows with errors simulation mode ...")
+ setUpRPC()
+ val eventGenerator = ErrorFlowsEventGenerator(
+ parties = parties.map { it.first },
+ notary = notaryNode.nodeInfo.notaryIdentity,
+ currencies = listOf(GBP, USD)
+ )
+ val maxIterations = 10_000
+ startSimulation(eventGenerator, maxIterations)
+ onEnd()
+ }
+}
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt
index 62ea4c9e73..5b7c2bd83f 100644
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt
@@ -11,46 +11,16 @@ import jfxtras.resources.JFXtrasFontRoboto
import joptsimple.OptionParser
import net.corda.client.jfx.model.Models
import net.corda.client.jfx.model.observableValue
-import net.corda.client.mock.EventGenerator
-import net.corda.client.mock.Generator
-import net.corda.client.mock.pickOne
-import net.corda.contracts.asset.Cash
-import net.corda.core.contracts.Amount
-import net.corda.core.contracts.GBP
-import net.corda.core.contracts.USD
-import net.corda.core.failure
-import net.corda.core.messaging.FlowHandle
-import net.corda.core.node.services.ServiceInfo
-import net.corda.core.node.services.ServiceType
-import net.corda.core.serialization.OpaqueBytes
-import net.corda.core.success
-import net.corda.core.transactions.SignedTransaction
-import net.corda.core.utilities.ALICE
-import net.corda.core.utilities.BOB
-import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.core.utilities.loggerFor
import net.corda.explorer.model.CordaViewModel
import net.corda.explorer.model.SettingsModel
import net.corda.explorer.views.*
import net.corda.explorer.views.cordapps.cash.CashViewer
-import net.corda.flows.CashExitFlow
-import net.corda.flows.CashFlowCommand
-import net.corda.flows.CashIssueFlow
-import net.corda.flows.CashPaymentFlow
-import net.corda.flows.IssuerFlow.IssuanceRequester
-import net.corda.node.driver.PortAllocation
-import net.corda.node.driver.driver
-import net.corda.node.services.startFlowPermission
-import net.corda.node.services.transactions.SimpleNotaryService
-import net.corda.nodeapi.User
import org.apache.commons.lang.SystemUtils
-import org.bouncycastle.asn1.x500.X500Name
import org.controlsfx.dialog.ExceptionDialog
import tornadofx.App
import tornadofx.addStageIcon
import tornadofx.find
-import java.time.Instant
-import java.util.*
/**
* Main class for Explorer, you will need Tornado FX to run the explorer.
@@ -131,6 +101,7 @@ class Main : App(MainView::class) {
// Stock Views.
registerView()
registerView()
+ registerView()
// CordApps Views.
registerView()
// Tools.
@@ -155,129 +126,7 @@ class Main : App(MainView::class) {
* On each iteration, the issuers will execute a Cash Issue or Cash Exit flow (at a 9:1 ratio) and a random party will execute a move of cash to another random party.
*/
fun main(args: Array) {
- val portAllocation = PortAllocation.Incremental(20000)
- driver(portAllocation = portAllocation) {
- val user = User("user1", "test", permissions = setOf(
- startFlowPermission()
- ))
- val manager = User("manager", "test", permissions = setOf(
- startFlowPermission(),
- startFlowPermission(),
- startFlowPermission(),
- startFlowPermission())
- )
- // TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo.
- val notary = startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)),
- customOverrides = mapOf("nearestCity" to "Zurich"))
- val alice = startNode(ALICE.name, rpcUsers = arrayListOf(user),
- advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))),
- customOverrides = mapOf("nearestCity" to "Milan"))
- val bob = startNode(BOB.name, rpcUsers = arrayListOf(user),
- advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))),
- customOverrides = mapOf("nearestCity" to "Madrid"))
- val issuerGBP = startNode(X500Name("CN=UK Bank Plc,O=UK Bank Plc,L=London,C=UK"), rpcUsers = arrayListOf(manager),
- advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.GBP"))),
- customOverrides = mapOf("nearestCity" to "London"))
- val issuerUSD = startNode(X500Name("CN=USA Bank Corp,O=USA Bank Corp,L=New York,C=US"), rpcUsers = arrayListOf(manager),
- advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.USD"))),
- customOverrides = mapOf("nearestCity" to "New York"))
-
- val notaryNode = notary.get()
- val aliceNode = alice.get()
- val bobNode = bob.get()
- val issuerNodeGBP = issuerGBP.get()
- val issuerNodeUSD = issuerUSD.get()
-
- arrayOf(notaryNode, aliceNode, bobNode, issuerNodeGBP, issuerNodeUSD).forEach {
- println("${it.nodeInfo.legalIdentity} started on ${it.configuration.rpcAddress}")
- }
-
- val parser = OptionParser("S")
- val options = parser.parse(*args)
- if (options.has("S")) {
- println("Running simulation mode ...")
-
- // Register with alice to use alice's RPC proxy to create random events.
- val aliceClient = aliceNode.rpcClientToNode()
- val aliceConnection = aliceClient.start(user.username, user.password)
- val aliceRPC = aliceConnection.proxy
-
- val bobClient = bobNode.rpcClientToNode()
- val bobConnection = bobClient.start(user.username, user.password)
- val bobRPC = bobConnection.proxy
-
- val issuerClientGBP = issuerNodeGBP.rpcClientToNode()
- val issuerGBPConnection = issuerClientGBP.start(manager.username, manager.password)
- val issuerRPCGBP = issuerGBPConnection.proxy
-
- val issuerClientUSD = issuerNodeUSD.rpcClientToNode()
- val issuerUSDConnection = issuerClientUSD.start(manager.username, manager.password)
- val issuerRPCUSD = issuerUSDConnection.proxy
-
- val issuers = mapOf(USD to issuerRPCUSD, GBP to issuerRPCGBP)
-
- val parties = listOf(aliceNode.nodeInfo.legalIdentity to aliceRPC,
- bobNode.nodeInfo.legalIdentity to bobRPC,
- issuerNodeGBP.nodeInfo.legalIdentity to issuerRPCGBP,
- issuerNodeUSD.nodeInfo.legalIdentity to issuerRPCUSD)
-
- val eventGenerator = EventGenerator(
- parties = parties.map { it.first },
- notary = notaryNode.nodeInfo.notaryIdentity,
- currencies = listOf(GBP, USD)
- )
-
- val maxIterations = 100_000
- // Log to logger when flow finish.
- fun FlowHandle.log(seq: Int, name: String) {
- val out = "[$seq] $name $id :"
- returnValue.success {
- Main.log.info("$out ${it.id} ${(it.tx.outputs.first().data as Cash.State).amount}")
- }.failure {
- Main.log.info("$out ${it.message}")
- }
- }
-
- // Pre allocate some money to each party.
- eventGenerator.parties.forEach {
- for (ref in 0..1) {
- for ((currency, issuer) in issuers) {
- CashFlowCommand.IssueCash(Amount(1_000_000, currency), OpaqueBytes(ByteArray(1, { ref.toByte() })), it, notaryNode.nodeInfo.notaryIdentity).startFlow(issuer)
- }
- }
- }
-
- for (i in 0..maxIterations) {
- Thread.sleep(300)
- // Issuer requests.
- eventGenerator.issuerGenerator.map { command ->
- when (command) {
- is CashFlowCommand.IssueCash -> issuers[command.amount.token]?.let {
- println("${Instant.now()} [$i] ISSUING ${command.amount} with ref ${command.issueRef} to ${command.recipient}")
- command.startFlow(it).log(i, "${command.amount.token}Issuer")
- }
- is CashFlowCommand.ExitCash -> issuers[command.amount.token]?.let {
- println("${Instant.now()} [$i] EXITING ${command.amount} with ref ${command.issueRef}")
- command.startFlow(it).log(i, "${command.amount.token}Exit")
- }
- else -> throw IllegalArgumentException("Unsupported command: $command")
- }
- }.generate(SplittableRandom())
-
- // Party pay requests.
- eventGenerator.moveCashGenerator.combine(Generator.pickOne(parties)) { command, (party, rpc) ->
- println("${Instant.now()} [$i] SENDING ${command.amount} from $party to ${command.recipient}")
- command.startFlow(rpc).log(i, party.name.toString())
- }.generate(SplittableRandom())
-
- }
- println("Simulation completed")
-
- aliceConnection.close()
- bobConnection.close()
- issuerGBPConnection.close()
- issuerUSDConnection.close()
- }
- waitForAllNodesToFinish()
- }
+ val parser = OptionParser("SF")
+ val options = parser.parse(*args)
+ ExplorerSimulation(options)
}
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/formatters/FlowInitiatorFormatter.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/formatters/FlowInitiatorFormatter.kt
new file mode 100644
index 0000000000..b42c7ae72a
--- /dev/null
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/formatters/FlowInitiatorFormatter.kt
@@ -0,0 +1,26 @@
+package net.corda.explorer.formatters
+
+import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
+import net.corda.core.flows.FlowInitiator
+
+object FlowInitiatorFormatter : Formatter {
+ override fun format(value: FlowInitiator): String {
+ return when (value) {
+ is FlowInitiator.Scheduled -> value.scheduledState.ref.toString() // TODO How do we want to format that?
+ is FlowInitiator.Shell -> "Shell" // TODO We don't have much information about that user.
+ is FlowInitiator.Peer -> PartyNameFormatter.short.format(value.party.name)
+ is FlowInitiator.RPC -> value.username
+ }
+ }
+
+ fun withIcon(value: FlowInitiator): Pair {
+ val text = format(value)
+ return when (value) {
+ is FlowInitiator.Scheduled -> Pair(FontAwesomeIcon.CALENDAR, text)
+ is FlowInitiator.Shell -> Pair(FontAwesomeIcon.TERMINAL, text)
+ is FlowInitiator.Peer -> Pair(FontAwesomeIcon.GROUP, text)
+ is FlowInitiator.RPC -> Pair(FontAwesomeIcon.SHARE, text)
+
+ }
+ }
+}
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/formatters/FlowNameFormatter.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/formatters/FlowNameFormatter.kt
new file mode 100644
index 0000000000..0de07af66b
--- /dev/null
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/formatters/FlowNameFormatter.kt
@@ -0,0 +1,13 @@
+package net.corda.explorer.formatters
+
+import org.apache.commons.lang.StringUtils.splitByCharacterTypeCamelCase
+
+object FlowNameFormatter {
+ val camelCase = object : Formatter {
+ override fun format(value: String): String {
+ val flowName = value.split('.', '$').last()
+ val split = splitByCharacterTypeCamelCase(flowName).filter { it.compareTo("Flow", true) != 0 } .joinToString(" ")
+ return split
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/identicon/IdenticonRenderer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/identicon/IdenticonRenderer.kt
index d7162986d5..74f6998f93 100644
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/identicon/IdenticonRenderer.kt
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/identicon/IdenticonRenderer.kt
@@ -195,11 +195,12 @@ fun identicon(secureHash: SecureHash, size: Double): ImageView {
return ImageView(IdenticonRenderer.getIdenticon(secureHash)).apply {
isPreserveRatio = true
fitWidth = size
+ styleClass += "identicon"
}
}
-fun identiconToolTip(secureHash: SecureHash): Tooltip {
- return Tooltip(Splitter.fixedLength(16).split("$secureHash").joinToString("\n")).apply {
+fun identiconToolTip(secureHash: SecureHash, description: String? = null): Tooltip {
+ return Tooltip(Splitter.fixedLength(16).split("${description ?: secureHash}").joinToString("\n")).apply {
contentDisplay = ContentDisplay.TOP
textAlignment = TextAlignment.CENTER
graphic = identicon(secureHash, 90.0)
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/CordaViewModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/CordaViewModel.kt
index 5967d955bf..248656b0f9 100644
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/model/CordaViewModel.kt
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/model/CordaViewModel.kt
@@ -31,4 +31,4 @@ abstract class CordaView(title: String? = null) : View(title) {
}
}
-data class CordaWidget(val name: String, val node: Node)
\ No newline at end of file
+data class CordaWidget(val name: String, val node: Node, val icon: FontAwesomeIcon? = null)
\ No newline at end of file
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Dashboard.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Dashboard.kt
index c5fc9ce711..7079a9ad3a 100644
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Dashboard.kt
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Dashboard.kt
@@ -1,6 +1,7 @@
package net.corda.explorer.views
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
+import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
import javafx.beans.binding.Bindings
import javafx.collections.ObservableList
import javafx.scene.Node
@@ -46,6 +47,7 @@ class Dashboard : CordaView() {
selectedView.value = view
}
}
+ it.icon?.let { graphic = FontAwesomeIconView(it).apply { glyphSize = 30.0 } }
}
}
}
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt
index a72f452a59..dc1fc4a5fa 100644
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt
@@ -6,6 +6,7 @@ import javafx.animation.FadeTransition
import javafx.animation.TranslateTransition
import javafx.beans.binding.Bindings
import javafx.beans.property.SimpleObjectProperty
+import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import javafx.geometry.Bounds
import javafx.geometry.Point2D
@@ -41,6 +42,9 @@ class Network : CordaView() {
val notaries by observableList(NetworkIdentityModel::notaries)
val peers by observableList(NetworkIdentityModel::parties)
val transactions by observableList(TransactionDataModel::partiallyResolvedTransactions)
+ var centralPeer: String? = null
+ private var centralLabel: ObservableValue