mirror of
https://github.com/corda/corda.git
synced 2025-02-21 01:42:24 +00:00
Cleanup of the explorer code related to internal flow view work. (#832)
Cleanup of the explorer code related to internal flow view work. Changes in simulation, widgets, minor visual.
This commit is contained in:
parent
b874b3e62a
commit
20403d806a
@ -75,7 +75,8 @@ class NodeMonitorModel {
|
||||
Observable.empty<ProgressTrackingEvent>()
|
||||
}
|
||||
}
|
||||
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) }
|
||||
|
@ -1,25 +1,15 @@
|
||||
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.*
|
||||
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
|
||||
|
||||
data class GatheredTransactionData(
|
||||
val transaction: PartiallyResolvedTransaction,
|
||||
val stateMachines: ObservableList<out StateMachineData>
|
||||
)
|
||||
|
||||
/**
|
||||
* [PartiallyResolvedTransaction] holds a [SignedTransaction] that has zero or more inputs resolved. The intent is
|
||||
* to prepare clients for cases where an input can only be resolved in the future/cannot be resolved at all (for example
|
||||
@ -58,53 +48,14 @@ 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<FlowStatus?>,
|
||||
val stateMachineStatus: ObservableValue<StateMachineStatus>
|
||||
)
|
||||
|
||||
/**
|
||||
* 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<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>>()) { map, update ->
|
||||
when (update) {
|
||||
is StateMachineUpdate.Added -> {
|
||||
val added: SimpleObjectProperty<StateMachineStatus> =
|
||||
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)
|
||||
}
|
||||
|
@ -1,27 +1,36 @@
|
||||
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<Party>, val currencies: List<Currency>, 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)
|
||||
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<Currency, Long> = 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)
|
||||
}
|
||||
|
||||
|
@ -217,7 +217,7 @@ abstract class FlowLogic<out T> {
|
||||
fun track(): Pair<String, Observable<String>>? {
|
||||
// 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() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,191 @@
|
||||
package net.corda.explorer
|
||||
|
||||
import joptsimple.OptionSet
|
||||
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.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.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<CashPaymentFlow>()
|
||||
))
|
||||
val manager = User("manager", "test", permissions = setOf(
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>(),
|
||||
startFlowPermission<CashExitFlow>(),
|
||||
startFlowPermission<IssuerFlow.IssuanceRequester>())
|
||||
)
|
||||
|
||||
lateinit var notaryNode: NodeHandle
|
||||
lateinit var aliceNode: NodeHandle
|
||||
lateinit var bobNode: NodeHandle
|
||||
lateinit var issuerNodeGBP: NodeHandle
|
||||
lateinit var issuerNodeUSD: NodeHandle
|
||||
|
||||
val RPCConnections = ArrayList<CordaRPCConnection>()
|
||||
val issuers = HashMap<Currency, CordaRPCOps>()
|
||||
val parties = ArrayList<Pair<Party, CordaRPCOps>>()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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<SignedTransaction>.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()
|
||||
}
|
||||
}
|
@ -11,45 +11,14 @@ 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.crypto.X509Utilities
|
||||
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.testing.driver.PortAllocation
|
||||
import net.corda.testing.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.*
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Main class for Explorer, you will need Tornado FX to run the explorer.
|
||||
@ -154,124 +123,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<String>) {
|
||||
val portAllocation = PortAllocation.Incremental(20000)
|
||||
driver(portAllocation = portAllocation) {
|
||||
val user = User("user1", "test", permissions = setOf(
|
||||
startFlowPermission<CashPaymentFlow>()
|
||||
))
|
||||
val manager = User("manager", "test", permissions = setOf(
|
||||
startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CashPaymentFlow>(),
|
||||
startFlowPermission<CashExitFlow>(),
|
||||
startFlowPermission<IssuanceRequester>())
|
||||
)
|
||||
// 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)))
|
||||
val alice = startNode(ALICE.name, rpcUsers = arrayListOf(user),
|
||||
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))))
|
||||
val bob = startNode(BOB.name, rpcUsers = arrayListOf(user),
|
||||
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))))
|
||||
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"))))
|
||||
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"))))
|
||||
|
||||
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<SignedTransaction>.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("S")
|
||||
val options = parser.parse(*args)
|
||||
ExplorerSimulation(options)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -31,4 +31,4 @@ abstract class CordaView(title: String? = null) : View(title) {
|
||||
}
|
||||
}
|
||||
|
||||
data class CordaWidget(val name: String, val node: Node)
|
||||
data class CordaWidget(val name: String, val node: Node, val icon: FontAwesomeIcon? = null)
|
@ -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 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ class Network : CordaView() {
|
||||
private val mapOriginalHeight = 2000.0
|
||||
|
||||
// UI node observables, declare here to create a strong ref to prevent GC, which removes listener from observables.
|
||||
private var centralLabel: Label? = null
|
||||
private var myLabel: Label? = null
|
||||
private val notaryComponents = notaries.map { it.render() }
|
||||
private val notaryButtons = notaryComponents.map { it.button }
|
||||
private val peerComponents = peers.map { it.render() }
|
||||
@ -97,7 +97,6 @@ class Network : CordaView() {
|
||||
}
|
||||
}
|
||||
setOnMouseClicked {
|
||||
centralLabel = mapLabel
|
||||
mapScrollPane.centerLabel(mapLabel)
|
||||
}
|
||||
}
|
||||
@ -129,7 +128,7 @@ class Network : CordaView() {
|
||||
if (node == myIdentity.value) {
|
||||
// It has to be a copy if we want to have notary both in notaries list and in identity (if we are looking at that particular notary node).
|
||||
myIdentityPane.apply { center = node.renderButton(mapLabel) }
|
||||
centralLabel = mapLabel
|
||||
myLabel = mapLabel
|
||||
}
|
||||
return MapViewComponents(this, button, mapLabel)
|
||||
}
|
||||
@ -140,7 +139,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) centralLabel?.let {
|
||||
if (old == 0.0) myLabel?.let {
|
||||
mapPane.applyCss()
|
||||
mapPane.layout()
|
||||
mapScrollPane.centerLabel(it)
|
||||
|
@ -53,7 +53,7 @@ class TransactionViewer : CordaView("Transactions") {
|
||||
private val reportingCurrency by observableValue(ReportingCurrencyModel::reportingCurrency)
|
||||
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
|
||||
|
||||
override val widgets = listOf(CordaWidget(title, TransactionWidget())).observable()
|
||||
override val widgets = listOf(CordaWidget(title, TransactionWidget(), icon)).observable()
|
||||
|
||||
/**
|
||||
* This is what holds data for a single transaction node. Note how a lot of these are nullable as we often simply don't
|
||||
@ -117,7 +117,10 @@ class TransactionViewer : CordaView("Transactions") {
|
||||
// Transaction table
|
||||
transactionViewTable.apply {
|
||||
items = searchField.filteredData
|
||||
column("Transaction ID", Transaction::id) { maxWidth = 200.0 }.setCustomCellFactory {
|
||||
column("Transaction ID", Transaction::id) {
|
||||
minWidth = 20.0
|
||||
maxWidth = 200.0
|
||||
}.setCustomCellFactory {
|
||||
label("$it") {
|
||||
graphic = identicon(it, 15.0)
|
||||
tooltip = identiconToolTip(it)
|
||||
@ -159,10 +162,11 @@ class TransactionViewer : CordaView("Transactions") {
|
||||
add(ContractStatesView(it).root)
|
||||
prefHeight = 400.0
|
||||
}.apply {
|
||||
prefWidth = 26.0
|
||||
isResizable = false
|
||||
// Column stays the same size, but we don't violate column restricted resize policy for the whole table view.
|
||||
// It removes that irritating column at the end of table that does nothing.
|
||||
minWidth = 26.0
|
||||
maxWidth = 26.0
|
||||
}
|
||||
setColumnResizePolicy { true }
|
||||
}
|
||||
matchingTransactionsLabel.textProperty().bind(Bindings.size(transactionViewTable.items).map {
|
||||
"$it matching transaction${if (it == 1) "" else "s"}"
|
||||
@ -186,6 +190,8 @@ class TransactionViewer : CordaView("Transactions") {
|
||||
init {
|
||||
right {
|
||||
label {
|
||||
val hash = SecureHash.randomSHA256()
|
||||
graphic = identicon(hash, 30.0)
|
||||
textProperty().bind(Bindings.size(partiallyResolvedTransactions).map(Number::toString))
|
||||
BorderPane.setAlignment(this, Pos.BOTTOM_RIGHT)
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ class CashViewer : CordaView("Cash") {
|
||||
override val root: BorderPane by fxml()
|
||||
override val icon: FontAwesomeIcon = FontAwesomeIcon.MONEY
|
||||
// View's widget.
|
||||
override val widgets = listOf(CordaWidget("Treasury", CashWidget())).observable()
|
||||
override val widgets = listOf(CordaWidget("Treasury", CashWidget(), icon)).observable()
|
||||
// Left pane
|
||||
private val leftPane: VBox by fxid()
|
||||
private val splitPane: SplitPane by fxid()
|
||||
|
@ -111,6 +111,7 @@
|
||||
-fx-cursor: hand;
|
||||
-fx-background-color: -color-1;
|
||||
-fx-border-color: transparent;
|
||||
-fx-border-radius: 2;
|
||||
}
|
||||
|
||||
.tile .title .text, .tile:expanded .title .text {
|
||||
@ -123,14 +124,15 @@
|
||||
-fx-background-repeat: no-repeat;
|
||||
-fx-background-position: center center;
|
||||
-fx-cursor: hand;
|
||||
-fx-padding: 0px;
|
||||
-fx-padding: 5px;
|
||||
-fx-alignment: bottom-right;
|
||||
-fx-border-color: transparent; /*t r b l */
|
||||
-fx-border-radius: 2;
|
||||
}
|
||||
|
||||
.tile .label {
|
||||
-fx-font-size: 2.4em;
|
||||
-fx-padding: 20px;
|
||||
-fx-font-size: 2.0em;
|
||||
-fx-padding: 10px;
|
||||
-fx-text-fill: -color-0;
|
||||
-fx-font-weight: normal;
|
||||
-fx-text-alignment: right;
|
||||
@ -309,3 +311,8 @@
|
||||
.scroll-bar:vertical {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
+/* Other */
|
||||
|
||||
.identicon {
|
||||
-fx-border-radius: 2;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user