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 + // UI components private val myIdentityPane by fxid() private val notaryList by fxid() @@ -98,6 +102,7 @@ class Network : CordaView() { } val button = button { + useMaxWidth = true graphic = vbox { label(PartyNameFormatter.short.format(node.legalIdentity.name)) { font = Font.font(font.family, FontWeight.BOLD, 15.0) } gridpane { @@ -114,7 +119,17 @@ class Network : CordaView() { return MapViewComponents(this, button, mapLabel) } + override fun onDock() { + centralLabel = mapLabels.firstOrDefault(myMapLabel, { centralPeer?.contains(it.text, true) ?: false }) + } + + override fun onUndock() { + centralPeer = null + centralLabel = myMapLabel + } + init { + centralLabel = mapLabels.firstOrDefault(myMapLabel, { centralPeer?.contains(it.text, true) ?: false }) myIdentityPane.centerProperty().bind(myButton) Bindings.bindContent(notaryList.children, notaryButtons) Bindings.bindContent(peerList.children, peerButtons) @@ -122,7 +137,7 @@ class Network : CordaView() { // Run once when the screen is ready. // TODO : Find a better way to do this. mapPane.heightProperty().addListener { _, old, _ -> - if (old == 0.0) myMapLabel.value?.let { mapScrollPane.centerLabel(it) } + if (old == 0.0) centralLabel.value?.let { mapScrollPane.centerLabel(it) } } // Listen on zooming gesture, if device has gesture support. mapPane.setOnZoom { zoom(it.zoomFactor, Point2D(it.x, it.y)) } @@ -142,6 +157,7 @@ class Network : CordaView() { } } + // TODO It doesn't work as expected. private fun ScrollPane.centerLabel(label: Label) { this.hvalue = (label.boundsInParent.width / 2 + label.boundsInParent.minX) / mapImageView.layoutBounds.width this.vvalue = (label.boundsInParent.height / 2 + label.boundsInParent.minY) / mapImageView.layoutBounds.height diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/SearchField.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/SearchField.kt index 2feeb8a938..b63aab5cf7 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/SearchField.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/SearchField.kt @@ -23,7 +23,8 @@ import tornadofx.* * TODO : Predictive text? * TODO : Regex? */ -class SearchField(private val data: ObservableList, vararg filterCriteria: Pair Boolean>) : UIComponent() { +class SearchField(private val data: ObservableList, vararg filterCriteria: Pair Boolean>, + val disabledFields: List = emptyList()) : UIComponent() { override val root: Parent by fxml() private val textField by fxid() private val clearButton by fxid() @@ -34,13 +35,13 @@ class SearchField(private val data: ObservableList, vararg filterCriteria: val text = textField.text val category = searchCategory.value data.filtered { data -> - text.isNullOrBlank() || if (category == ALL) { + (text.isNullOrBlank() && textField.isVisible) || if (category == ALL) { filterCriteria.any { it.second(data, text) } } else { filterCriteria.toMap()[category]?.invoke(data, text) ?: false } } - }, arrayOf(textField.textProperty(), searchCategory.valueProperty()))) + }, arrayOf(textField.textProperty(), searchCategory.valueProperty(), textField.visibleProperty()))) init { clearButton.setOnMouseClicked { event: MouseEvent -> @@ -73,5 +74,7 @@ class SearchField(private val data: ObservableList, vararg filterCriteria: } "Filter by $category." }) + textField.visibleProperty().bind(searchCategory.valueProperty().map { it !in disabledFields }) + // TODO Maybe it will be better to replace these categories with comboBox? For example Result with choice: succes, in progress, error. } } \ No newline at end of file diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/StateMachineViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/StateMachineViewer.kt new file mode 100644 index 0000000000..92f5b38b23 --- /dev/null +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/StateMachineViewer.kt @@ -0,0 +1,294 @@ +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.geometry.HPos +import javafx.geometry.Insets +import javafx.scene.Parent +import javafx.scene.control.Label +import javafx.scene.control.TableView +import javafx.scene.input.MouseButton +import javafx.scene.layout.BorderPane +import javafx.scene.layout.GridPane +import javafx.scene.layout.VBox +import javafx.scene.text.FontWeight +import javafx.scene.text.TextAlignment +import net.corda.client.jfx.model.StateMachineData +import net.corda.client.jfx.model.StateMachineDataModel +import net.corda.client.jfx.model.StateMachineStatus +import net.corda.client.jfx.model.observableList +import net.corda.client.jfx.model.observableValue +import net.corda.client.jfx.model.writableValue +import net.corda.client.jfx.utils.map +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.toBase58String +import net.corda.core.flows.FlowInitiator +import net.corda.core.transactions.SignedTransaction +import net.corda.explorer.formatters.FlowInitiatorFormatter +import net.corda.explorer.formatters.FlowNameFormatter +import net.corda.explorer.formatters.PartyNameFormatter +import net.corda.explorer.identicon.identicon +import net.corda.explorer.identicon.identiconToolTip +import net.corda.explorer.model.CordaView +import net.corda.explorer.model.CordaViewModel +import net.corda.explorer.model.CordaWidget +import net.corda.explorer.ui.setCustomCellFactory +import tornadofx.* + + +// TODO Rethink whole idea of showing communication as table, it should be tree view for each StateMachine (with subflows). +class StateMachineViewer : CordaView("Flow Triage") { + override val root by fxml() + override val icon = FontAwesomeIcon.HEARTBEAT + override val widgets = listOf(CordaWidget(title, StateMachineWidget(), icon)).observable() + private val allViewTable by fxid>() + private val matchingFlowsLabel by fxid