Merge pull request #1 from corda/kstreich-explorer-flows

Flow view from explorer
This commit is contained in:
Mike Hearn 2017-06-16 16:02:24 +02:00 committed by GitHub
commit d0daac8dd6
22 changed files with 878 additions and 242 deletions

View File

@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Explorer - demo nodes (flow triage)" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.explorer.MainKt" />
<option name="VM_PARAMETERS" value="-DAMQ_DELIVERY_DELAY_MS=15000" />
<option name="PROGRAM_PARAMETERS" value="-F" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
<option name="ALTERNATIVE_JRE_PATH" value="1.8" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="explorer_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -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<ProgressTrackingEvent>? {
return stateMachine.progressTrackerStepAndUpdates?.let { pair ->
val (current, future) = pair
future.map { ProgressTrackingEvent(stateMachine.id, it) }.startWith(ProgressTrackingEvent(stateMachine.id, current))
}
}
}
}
/**
* This model exposes raw event streams to and from the node.
*/
@ -75,7 +62,9 @@ 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) }

View File

@ -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<ProgressTrackingEvent>? {
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<StateMachineStatus>, ObservableValue<ProgressStatus>>
)
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<StateMachineRunId, Int>()
private val stateMachineStatus = stateMachineUpdates.fold(FXCollections.observableArrayList<SimpleObjectProperty<StateMachineStatus>>()) { list, update ->
when (update) {
is StateMachineUpdate.Added -> {
counter.addSmm()
val flowInitiator= update.stateMachineInfo.initiator
val added: SimpleObjectProperty<StateMachineStatus> =
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
}

View File

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

View File

@ -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<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)
open class EventGenerator(val parties: List<Party>, val currencies: List<Currency>, 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<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)
}
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<Party>, currencies: List<Currency>, 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
))
}

View File

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

View File

@ -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'
}

View File

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

View File

@ -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<Dashboard>()
registerView<TransactionViewer>()
registerView<StateMachineViewer>()
// CordApps Views.
registerView<CashViewer>()
// 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<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)),
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<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("SF")
val options = parser.parse(*args)
ExplorerSimulation(options)
}

View File

@ -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<FlowInitiator> {
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<FontAwesomeIcon, String> {
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)
}
}
}

View File

@ -0,0 +1,13 @@
package net.corda.explorer.formatters
import org.apache.commons.lang.StringUtils.splitByCharacterTypeCamelCase
object FlowNameFormatter {
val camelCase = object : Formatter<String> {
override fun format(value: String): String {
val flowName = value.split('.', '$').last()
val split = splitByCharacterTypeCamelCase(flowName).filter { it.compareTo("Flow", true) != 0 } .joinToString(" ")
return split
}
}
}

View File

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

View File

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

View File

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

View File

@ -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<Label?>
// UI components
private val myIdentityPane by fxid<BorderPane>()
private val notaryList by fxid<VBox>()
@ -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

View File

@ -23,7 +23,8 @@ import tornadofx.*
* TODO : Predictive text?
* TODO : Regex?
*/
class SearchField<T>(private val data: ObservableList<T>, vararg filterCriteria: Pair<String, (T, String) -> Boolean>) : UIComponent() {
class SearchField<T>(private val data: ObservableList<T>, vararg filterCriteria: Pair<String, (T, String) -> Boolean>,
val disabledFields: List<String> = emptyList()) : UIComponent() {
override val root: Parent by fxml()
private val textField by fxid<TextField>()
private val clearButton by fxid<Node>()
@ -34,13 +35,13 @@ class SearchField<T>(private val data: ObservableList<T>, 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<Observable>(textField.textProperty(), searchCategory.valueProperty())))
}, arrayOf<Observable>(textField.textProperty(), searchCategory.valueProperty(), textField.visibleProperty())))
init {
clearButton.setOnMouseClicked { event: MouseEvent ->
@ -73,5 +74,7 @@ class SearchField<T>(private val data: ObservableList<T>, 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.
}
}

View File

@ -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<BorderPane>()
override val icon = FontAwesomeIcon.HEARTBEAT
override val widgets = listOf(CordaWidget(title, StateMachineWidget(), icon)).observable()
private val allViewTable by fxid<TableView<StateMachineData>>()
private val matchingFlowsLabel by fxid<Label>()
private val selectedView by writableValue(CordaViewModel::selectedView)
private val stateMachinesAll by observableList(StateMachineDataModel::stateMachinesAll)
inner private class StateMachineWidget : GridPane() {
private val error by observableValue(StateMachineDataModel::error)
private val success by observableValue(StateMachineDataModel::success)
private val progress by observableValue(StateMachineDataModel::progress)
init {
padding = Insets(0.0, 5.0, 10.0, 10.0)
hgap = 5.0
styleClass += "chart-plot-background"
row {
add(makeIconLabel(FontAwesomeIcon.CHECK, "", "-fx-fill: lightslategrey", 30.0))
label { textProperty().bind(success.map(Number::toString)) }
}
row {
add(makeIconLabel(FontAwesomeIcon.BOLT, "", "-fx-fill: lightslategrey", 30.0))
label { textProperty().bind(error.map(Number::toString)) }
}
row {
add(makeIconLabel(FontAwesomeIcon.ROCKET, "", "-fx-fill: lightslategrey", 30.0))
label { textProperty().bind(progress.map(Number::toString)) }
}
}
}
fun makeIconLabel(icon: FontAwesomeIcon, initText: String, customStyle: String? = null, iconSize: Double = 15.0): Label {
return label {
graphic = FontAwesomeIconView(icon).apply {
glyphSize = iconSize
textAlignment = TextAlignment.LEFT
style = customStyle
}
text = initText
gridpaneConstraints { hAlignment = HPos.CENTER }
}
}
fun makeColumns(table: TableView<StateMachineData>, tableItems: ObservableList<StateMachineData>, withResult: Boolean = true) {
table.apply {
items = tableItems
if (withResult) {
rowExpander(expandOnDoubleClick = true) {
add(StateMachineDetailsView(it).root)
}.apply {
// Column stays the same size, but we don't violate column restricted resize policy for the whole table view.
minWidth = 26.0
maxWidth = 26.0
}
}
// TODO Kill that ID column or replace it with something useful when we will have flow audit utilities.
// For now it's rather for visual purpose, so you can observe flow.
column("ID", StateMachineData::id) {
minWidth = 100.0
maxWidth = 200.0
}.setCustomCellFactory {
val toDisplay = it.toString().removeSurrounding("[", "]")
label(toDisplay) {
val hash = SecureHash.sha256(it.toString())
graphic = identicon(hash, 15.0)
tooltip = identiconToolTip(hash, toDisplay)
}
}
column("Flow name", StateMachineData::stateMachineName).cellFormat { text = FlowNameFormatter.camelCase.format(it) }
column("Initiator", StateMachineData::flowInitiator).setCustomCellFactory {
val (initIcon, initText) = FlowInitiatorFormatter.withIcon(it)
makeIconLabel(initIcon, initText, "-fx-fill: lightgray")
}
column("Flow Status", StateMachineData::smmStatus).setCustomCellFactory {
val addRm = it.first.value
val progress = it.second.value.status ?: "No progress data"
if (addRm is StateMachineStatus.Removed) {
if (addRm.result.error == null) {
makeIconLabel(FontAwesomeIcon.CHECK, "Success", "-fx-fill: green")
} else {
makeIconLabel(FontAwesomeIcon.BOLT, progress, "-fx-fill: -color-4")
}
} else {
makeIconLabel(FontAwesomeIcon.ROCKET, progress, "-fx-fill: lightslategrey")
}
}
}
}
init {
val searchField = SearchField(stateMachinesAll,
"Flow name" to { sm, s -> sm.stateMachineName.contains(s, true) },
"Initiator" to { sm, s -> FlowInitiatorFormatter.format(sm.flowInitiator).contains(s, true) },
"Flow Status" to { sm, s ->
val stat = sm.smmStatus.second.value?.status ?: "No progress data"
stat.contains(s, true)
},
"Error" to { sm, _ ->
val smAddRm = sm.smmStatus.first.value
if (smAddRm is StateMachineStatus.Removed)
smAddRm.result.error != null
else false
},
"Done" to { sm, _ ->
val smAddRm = sm.smmStatus.first.value
if (smAddRm is StateMachineStatus.Removed)
smAddRm.result.error == null
else false
},
"In progress" to { sm, _ -> sm.smmStatus.first.value !is StateMachineStatus.Removed },
disabledFields = listOf("Error", "Done", "In progress")
)
root.top = searchField.root
makeColumns(allViewTable, searchField.filteredData)
matchingFlowsLabel.textProperty().bind(Bindings.size(allViewTable.items).map {
"$it matching flow${if (it == 1) "" else "s"}"
})
}
private inner class StateMachineDetailsView(smmData: StateMachineData) : Fragment() {
override val root by fxml<Parent>()
private val flowInitiatorGrid by fxid<GridPane>()
private val flowResultVBox by fxid<VBox>()
init {
//TODO It would be nice to have flow graph with showing progress steps with subflows + timestamps (left it for second iteration).
when (smmData.flowInitiator) {
is FlowInitiator.Shell -> makeShellGrid(flowInitiatorGrid) // TODO Extend this when we will have more information on shell user.
is FlowInitiator.Peer -> makePeerGrid(flowInitiatorGrid, smmData.flowInitiator as FlowInitiator.Peer)
is FlowInitiator.RPC -> makeRPCGrid(flowInitiatorGrid, smmData.flowInitiator as FlowInitiator.RPC)
is FlowInitiator.Scheduled -> makeScheduledGrid(flowInitiatorGrid, smmData.flowInitiator as FlowInitiator.Scheduled)
}
val status = smmData.smmStatus.first.value
if (status is StateMachineStatus.Removed) {
status.result.match(onValue = { makeResultVBox(flowResultVBox, it) }, onError = { makeErrorVBox(flowResultVBox, it) })
}
}
}
private fun <T> makeResultVBox(vbox: VBox, result: T) {
if (result is SignedTransaction) {
vbox.apply {
label("Signed transaction").apply { style { fontWeight = FontWeight.BOLD } }
label {
style = "-fx-cursor: hand;"
setOnMouseClicked {
if (it.button == MouseButton.PRIMARY) {
selectedView.value = tornadofx.find<TransactionViewer>().apply { txIdToScroll = result.id }
}
}
text = result.id.toString()
graphic = identicon(result.id, 30.0)
tooltip = identiconToolTip(result.id)
}
}
} else if (result != null && result !is Unit) {
// TODO Here we could have sth different than SignedTransaction/Unit
vbox.apply {
label("Flow completed with success. Result: ").apply { style { fontWeight = FontWeight.BOLD } }
label(result.toString())
}
}
}
private fun makeErrorVBox(vbox: VBox, error: Throwable) {
vbox.apply {
label {
text = error::class.simpleName
graphic = FontAwesomeIconView(FontAwesomeIcon.BOLT).apply {
glyphSize = 30
textAlignment = TextAlignment.CENTER
style = "-fx-fill: -color-4"
}
}
label { text = error.message }
}
}
private fun makeShellGrid(gridPane: GridPane) {
gridPane.apply {
label("Flow started by shell user")
}
}
private fun makePeerGrid(gridPane: GridPane, initiator: FlowInitiator.Peer) {
gridPane.apply {
style = "-fx-cursor: hand;"
setOnMouseClicked {
if (it.button == MouseButton.PRIMARY) {
val short = PartyNameFormatter.short.format(initiator.party.name)
selectedView.value = tornadofx.find<Network>().apply { centralPeer = short}
}
}
row {
label("Peer legal name: ") {
gridpaneConstraints { hAlignment = HPos.LEFT }
style { fontWeight = FontWeight.BOLD }
minWidth = 150.0
prefWidth = 150.0
}
label(initiator.party.name.toString()) { gridpaneConstraints { hAlignment = HPos.LEFT } }
}
row {
label("Owning key: ") {
gridpaneConstraints { hAlignment = HPos.LEFT }
style { fontWeight = FontWeight.BOLD }
minWidth = 150.0
prefWidth = 150.0
}
label(initiator.party.owningKey.toBase58String()) { gridpaneConstraints { hAlignment = HPos.LEFT } }
}
}
}
private fun makeRPCGrid(gridPane: GridPane, initiator: FlowInitiator.RPC) {
gridPane.apply {
row {
label("RPC user name: ") {
gridpaneConstraints { hAlignment = HPos.LEFT }
style { fontWeight = FontWeight.BOLD }
prefWidth = 150.0
}
label(initiator.username) { gridpaneConstraints { hAlignment = HPos.LEFT } }
}
}
}
private fun makeScheduledGrid(gridPane: GridPane, initiator: FlowInitiator.Scheduled) {
gridPane.apply {
row {
label("Scheduled state: ") {
gridpaneConstraints { hAlignment = HPos.LEFT }
style { fontWeight = FontWeight.BOLD }
prefWidth = 150.0
}
label(initiator.scheduledState.ref.toString()) { gridpaneConstraints { hAlignment = HPos.LEFT } }
}
row {
label("Scheduled at: ") {
gridpaneConstraints { hAlignment = HPos.LEFT }
style { fontWeight = FontWeight.BOLD }
prefWidth = 150.0
}
label(initiator.scheduledState.scheduledAt.toString()) { gridpaneConstraints { hAlignment = HPos.LEFT } }
}
}
}
}

View File

@ -53,7 +53,11 @@ 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()
private var scrollPosition: Int = 0
private lateinit var expander: ExpanderColumn<TransactionViewer.Transaction>
var txIdToScroll: SecureHash? = null // Passed as param.
/**
* This is what holds data for a single transaction node. Note how a lot of these are nullable as we often simply don't
@ -72,6 +76,26 @@ class TransactionViewer : CordaView("Transactions") {
data class Inputs(val resolved: ObservableList<StateAndRef<ContractState>>, val unresolved: ObservableList<StateRef>)
override fun onDock() {
txIdToScroll?.let {
scrollPosition = transactionViewTable.items.indexOfFirst { it.id == txIdToScroll }
if (scrollPosition > 0) {
expander.toggleExpanded(scrollPosition)
val tx = transactionViewTable.items[scrollPosition]
transactionViewTable.scrollTo(tx)
}
}
}
override fun onUndock() {
if (scrollPosition != 0) {
val isExpanded = expander.getExpandedProperty(transactionViewTable.items[scrollPosition])
if (isExpanded.value) expander.toggleExpanded(scrollPosition)
scrollPosition = 0
}
txIdToScroll = null
}
/**
* We map the gathered data about transactions almost one-to-one to the nodes.
*/
@ -117,7 +141,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)
@ -155,14 +182,15 @@ class TransactionViewer : CordaView("Transactions") {
titleProperty.bind(reportingCurrency.map { "Total value ($it equiv)" })
}
rowExpander {
expander = rowExpander {
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 +214,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)
}

View File

@ -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()

View File

@ -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;
@ -269,4 +271,24 @@
.connection-bank-to-regulator {
-fx-stroke: red;
}
}
/* Other */
.identicon {
-fx-border-radius: 2;
}
.flow-expanded {
-fx-background-color: rgba(255, 255, 255);
-fx-background-size: Auto 90%;
-fx-background-repeat: no-repeat;
-fx-background-position: center center;
-fx-border-color: transparent;
-fx-border-radius: 2;
}
.flow-expanded:hover {
-fx-border-color: -color-3;
-fx-border-width: 2;
-fx-border-radius: 2;
}

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<GridPane styleClass="flow-expanded" stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1">
<padding>
<Insets bottom="5" left="5" right="5" top="5" />
</padding>
<GridPane fx:id="flowInitiatorGrid" hgap="10.0" vgap="10.0" maxHeight="Infinity" maxWidth="Infinity" GridPane.fillWidth="true" GridPane.rowIndex="0">
<padding>
<Insets bottom="5.0" left="5.0" right="10.0" />
</padding>
</GridPane>
<VBox fx:id="flowResultVBox" spacing="10.0" maxHeight="Infinity" maxWidth="Infinity" GridPane.rowIndex="1" GridPane.fillWidth="true">
<padding>
<Insets bottom="5" left="5" right="5" top="5" />
</padding>
</VBox>
<columnConstraints>
<ColumnConstraints minWidth="450.0" />
<ColumnConstraints hgrow="ALWAYS" />
</columnConstraints>
<rowConstraints>
<RowConstraints vgrow="ALWAYS" />
<RowConstraints vgrow="ALWAYS" />
</rowConstraints>
</GridPane>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.Label?>
<BorderPane stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.76-ea"
xmlns:fx="http://javafx.com/fxml/1">
<padding>
<Insets top="5" left="5" right="5" bottom="5"/>
</padding>
<center>
<TableView fx:id="allViewTable" VBox.vgrow="ALWAYS">
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
</columnResizePolicy>
</TableView>
</center>
<bottom>
<Label fx:id="matchingFlowsLabel" text="matching flows(s)"/>
</bottom>
</BorderPane>