diff --git a/client/src/integration-test/kotlin/com/r3corda/client/NodeMonitorModelTest.kt b/client/src/integration-test/kotlin/com/r3corda/client/NodeMonitorModelTest.kt
index 9172678d78..03874c4a86 100644
--- a/client/src/integration-test/kotlin/com/r3corda/client/NodeMonitorModelTest.kt
+++ b/client/src/integration-test/kotlin/com/r3corda/client/NodeMonitorModelTest.kt
@@ -16,7 +16,9 @@ import com.r3corda.node.driver.driver
import com.r3corda.node.internal.CordaRPCOpsImpl
import com.r3corda.node.services.User
import com.r3corda.node.services.config.configureTestSSL
+import com.r3corda.node.services.messaging.ArtemisMessagingComponent
import com.r3corda.node.services.messaging.StateMachineUpdate
+import com.r3corda.node.services.network.NetworkMapService
import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.testing.expect
import com.r3corda.testing.expectEvents
@@ -30,7 +32,6 @@ import java.util.concurrent.CountDownLatch
import kotlin.concurrent.thread
class NodeMonitorModelTest {
-
lateinit var aliceNode: NodeInfo
lateinit var notaryNode: NodeInfo
val stopDriver = CountDownLatch(1)
@@ -67,7 +68,7 @@ class NodeMonitorModelTest {
networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
clientToService = monitor.clientToService
- monitor.register(aliceNode, configureTestSSL(), cashUser.username, cashUser.password)
+ monitor.register(ArtemisMessagingComponent.toHostAndPort(aliceNode.address), configureTestSSL(), cashUser.username, cashUser.password)
driverStarted.countDown()
stopDriver.await()
}
@@ -85,20 +86,22 @@ class NodeMonitorModelTest {
fun `network map update`() {
newNode("Bob")
newNode("Charlie")
- networkMapUpdates.expectEvents(isStrict = false) {
- sequence(
- // TODO : Add test for remove when driver DSL support individual node shutdown.
- expect { output: NetworkMapCache.MapChange ->
- require(output.node.legalIdentity.name == "Alice") { output.node.legalIdentity.name }
- },
- expect { output: NetworkMapCache.MapChange ->
- require(output.node.legalIdentity.name == "Bob") { output.node.legalIdentity.name }
- },
- expect { output: NetworkMapCache.MapChange ->
- require(output.node.legalIdentity.name == "Charlie") { output.node.legalIdentity.name }
- }
- )
- }
+ networkMapUpdates.filter { !it.node.advertisedServices.any { it.info.type.isNotary() } }
+ .filter { !it.node.advertisedServices.any { it.info.type == NetworkMapService.type } }
+ .expectEvents(isStrict = false) {
+ sequence(
+ // TODO : Add test for remove when driver DSL support individual node shutdown.
+ expect { output: NetworkMapCache.MapChange ->
+ require(output.node.legalIdentity.name == "Alice") { "Expecting : Alice, Actual : ${output.node.legalIdentity.name}" }
+ },
+ expect { output: NetworkMapCache.MapChange ->
+ require(output.node.legalIdentity.name == "Bob") { "Expecting : Bob, Actual : ${output.node.legalIdentity.name}" }
+ },
+ expect { output: NetworkMapCache.MapChange ->
+ require(output.node.legalIdentity.name == "Charlie") { "Expecting : Charlie, Actual : ${output.node.legalIdentity.name}" }
+ }
+ )
+ }
}
@Test
diff --git a/client/src/main/kotlin/com/r3corda/client/fxutils/ObservableUtilities.kt b/client/src/main/kotlin/com/r3corda/client/fxutils/ObservableUtilities.kt
index 687f7756d5..f989f152f1 100644
--- a/client/src/main/kotlin/com/r3corda/client/fxutils/ObservableUtilities.kt
+++ b/client/src/main/kotlin/com/r3corda/client/fxutils/ObservableUtilities.kt
@@ -10,9 +10,7 @@ import javafx.collections.ObservableList
import javafx.collections.ObservableMap
import javafx.collections.transformation.FilteredList
import org.fxmisc.easybind.EasyBind
-import org.slf4j.LoggerFactory
import java.util.function.Predicate
-import kotlin.concurrent.thread
/**
* Here follows utility extension functions that help reduce the visual load when developing RX code. Each function should
@@ -276,4 +274,4 @@ fun ObservableList.last(): ObservableValue {
null
}
}, arrayOf(this))
-}
+}
\ No newline at end of file
diff --git a/client/src/main/kotlin/com/r3corda/client/model/GatheredTransactionDataModel.kt b/client/src/main/kotlin/com/r3corda/client/model/GatheredTransactionDataModel.kt
index 3efb21c857..f2778a9bed 100644
--- a/client/src/main/kotlin/com/r3corda/client/model/GatheredTransactionDataModel.kt
+++ b/client/src/main/kotlin/com/r3corda/client/model/GatheredTransactionDataModel.kt
@@ -4,7 +4,6 @@ import com.r3corda.client.fxutils.*
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.contracts.StateRef
-import com.r3corda.client.fxutils.recordInSequence
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.protocols.StateMachineRunId
@@ -15,8 +14,6 @@ import javafx.beans.value.ObservableValue
import javafx.collections.ObservableList
import javafx.collections.ObservableMap
import org.fxmisc.easybind.EasyBind
-import org.slf4j.LoggerFactory
-import rx.Observable
data class GatheredTransactionData(
val transaction: PartiallyResolvedTransaction,
@@ -30,8 +27,7 @@ data class GatheredTransactionData(
*/
data class PartiallyResolvedTransaction(
val transaction: SignedTransaction,
- val inputs: List>
-) {
+ val inputs: List>) {
val id = transaction.id
sealed class InputResolution(val stateRef: StateRef) {
class Unresolved(stateRef: StateRef) : InputResolution(stateRef)
@@ -84,16 +80,15 @@ data class StateMachineData(
*/
class GatheredTransactionDataModel {
- private val transactions: Observable by observable(NodeMonitorModel::transactions)
- private val stateMachineUpdates: Observable by observable(NodeMonitorModel::stateMachineUpdates)
- private val progressTracking: Observable by observable(NodeMonitorModel::progressTracking)
- private val stateMachineTransactionMapping: Observable by observable(NodeMonitorModel::stateMachineTransactionMapping)
+ 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)
- val collectedTransactions = transactions.recordInSequence()
- val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
- val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
- val stateMachineStatus: ObservableMap> =
- stateMachineUpdates.foldToObservableMap(Unit) { update, _unit, map: ObservableMap> ->
+ private val collectedTransactions = transactions.recordInSequence()
+ private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
+ private val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
+ private val stateMachineStatus = stateMachineUpdates.foldToObservableMap(Unit) { update, _unit, map: ObservableMap> ->
when (update) {
is StateMachineUpdate.Added -> {
val added: SimpleObjectProperty =
@@ -107,21 +102,19 @@ class GatheredTransactionDataModel {
}
}
}
- val stateMachineDataList: ObservableList =
- LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
+ private val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
StateMachineData(id, progress.map { it?.let { ProtocolStatus(it.message) } }, status)
}.getObservableValues()
- val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
- val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
- val partiallyResolvedTransactions = collectedTransactions.map {
+ private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
+ private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
+ private val partiallyResolvedTransactions = collectedTransactions.map {
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
}
/**
* We JOIN the transaction list with state machines
*/
- val gatheredTransactionDataList: ObservableList =
- partiallyResolvedTransactions.leftOuterJoin(
+ val gatheredTransactionDataList = partiallyResolvedTransactions.leftOuterJoin(
smTxMappingList,
PartiallyResolvedTransaction::id,
StateMachineTransactionMapping::transactionId
diff --git a/client/src/main/kotlin/com/r3corda/client/model/NetworkIdentityModel.kt b/client/src/main/kotlin/com/r3corda/client/model/NetworkIdentityModel.kt
index af4f01e6ce..61fae5aeee 100644
--- a/client/src/main/kotlin/com/r3corda/client/model/NetworkIdentityModel.kt
+++ b/client/src/main/kotlin/com/r3corda/client/model/NetworkIdentityModel.kt
@@ -1,16 +1,18 @@
package com.r3corda.client.model
import com.r3corda.client.fxutils.foldToObservableList
+import com.r3corda.client.fxutils.map
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.services.NetworkMapCache
+import com.r3corda.node.services.network.NetworkMapService
import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.removeIf
-import rx.Observable
+import java.security.PublicKey
class NetworkIdentityModel {
- private val networkIdentityObservable: Observable by observable(NodeMonitorModel::networkMap)
+ private val networkIdentityObservable by observable(NodeMonitorModel::networkMap)
- val networkIdentities: ObservableList =
+ private val networkIdentities: ObservableList =
networkIdentityObservable.foldToObservableList(Unit) { update, _accumulator, observableList ->
observableList.removeIf {
when (update.type) {
@@ -21,4 +23,18 @@ class NetworkIdentityModel {
}
observableList.addAll(update.node)
}
+
+ private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
+
+ val parties: ObservableList = networkIdentities.filtered { !it.isCordaService() }
+ val notaries: ObservableList = networkIdentities.filtered { it.advertisedServices.any { it.info.type.isNotary() } }
+ val myIdentity = rpcProxy.map { it?.nodeIdentity() }
+
+ private fun NodeInfo.isCordaService(): Boolean {
+ return advertisedServices.any { it.info.type == NetworkMapService.type || it.info.type.isNotary() }
+ }
+
+ fun lookup(publicKey: PublicKey): NodeInfo? {
+ return parties.firstOrNull { it.legalIdentity.owningKey == publicKey } ?: notaries.firstOrNull { it.notaryIdentity.owningKey == publicKey }
+ }
}
\ No newline at end of file
diff --git a/client/src/main/kotlin/com/r3corda/client/model/NodeMonitorModel.kt b/client/src/main/kotlin/com/r3corda/client/model/NodeMonitorModel.kt
index 778fb4ad80..99cf917b60 100644
--- a/client/src/main/kotlin/com/r3corda/client/model/NodeMonitorModel.kt
+++ b/client/src/main/kotlin/com/r3corda/client/model/NodeMonitorModel.kt
@@ -1,15 +1,14 @@
package com.r3corda.client.model
+import com.google.common.net.HostAndPort
import com.r3corda.client.CordaRPCClient
import com.r3corda.core.contracts.ClientToServiceCommand
-import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.services.NetworkMapCache
import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.node.services.config.NodeSSLConfiguration
-import com.r3corda.node.services.messaging.ArtemisMessagingComponent.Companion.toHostAndPort
import com.r3corda.node.services.messaging.CordaRPCOps
import com.r3corda.node.services.messaging.StateMachineInfo
import com.r3corda.node.services.messaging.StateMachineUpdate
@@ -56,8 +55,8 @@ class NodeMonitorModel {
* Register for updates to/from a given vault.
* TODO provide an unsubscribe mechanism
*/
- fun register(vaultMonitorNodeInfo: NodeInfo, sslConfig: NodeSSLConfiguration, username: String, password: String) {
- val client = CordaRPCClient(toHostAndPort(vaultMonitorNodeInfo.address), sslConfig)
+ fun register(nodeHostAndPort: HostAndPort, sslConfig: NodeSSLConfiguration, username: String, password: String) {
+ val client = CordaRPCClient(nodeHostAndPort, sslConfig)
client.start(username, password)
val proxy = client.proxy()
@@ -101,7 +100,6 @@ class NodeMonitorModel {
clientToServiceSource.subscribe {
proxy.executeCommand(it)
}
-
proxyObservable.set(proxy)
}
}
\ No newline at end of file
diff --git a/explorer/build.gradle b/explorer/build.gradle
index f74e581f3d..bc35c72a4a 100644
--- a/explorer/build.gradle
+++ b/explorer/build.gradle
@@ -25,8 +25,6 @@ apply plugin: 'kotlin'
apply plugin: 'application'
sourceCompatibility = 1.8
-
-applicationDefaultJvmArgs = ["-javaagent:${rootProject.configurations.quasar.singleFile}"]
mainClassName = 'com.r3corda.explorer.Main'
sourceSets {
@@ -53,7 +51,7 @@ dependencies {
testCompile group: 'junit', name: 'junit', version: '4.11'
// TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.
- compile 'no.tornado:tornadofx:1.5.1'
+ compile 'no.tornado:tornadofx:1.5.6'
// Corda Core: Data structures and basic types needed to work with Corda.
compile project(':core')
@@ -74,4 +72,12 @@ dependencies {
// Humanize: formatting
compile 'com.github.mfornos:humanize-icu:1.2.2'
+
+ // Controls FX: more java FX components http://fxexperience.com/controlsfx/
+ compile 'org.controlsfx:controlsfx:8.40.12'
}
+
+task(runDemoNodes, dependsOn: 'classes', type: JavaExec) {
+ main = 'com.r3corda.explorer.MainKt'
+ classpath = sourceSets.main.runtimeClasspath
+}
\ No newline at end of file
diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/Main.kt b/explorer/src/main/kotlin/com/r3corda/explorer/Main.kt
index a2bc2e9797..f92e109761 100644
--- a/explorer/src/main/kotlin/com/r3corda/explorer/Main.kt
+++ b/explorer/src/main/kotlin/com/r3corda/explorer/Main.kt
@@ -1,60 +1,75 @@
package com.r3corda.explorer
+import com.r3corda.client.mock.EventGenerator
import com.r3corda.client.model.Models
import com.r3corda.client.model.NodeMonitorModel
import com.r3corda.core.node.services.ServiceInfo
-import com.r3corda.explorer.model.IdentityModel
+import com.r3corda.explorer.views.runInFxApplicationThread
import com.r3corda.node.driver.PortAllocation
import com.r3corda.node.driver.driver
-import com.r3corda.node.services.config.configureTestSSL
+import com.r3corda.node.internal.CordaRPCOpsImpl
+import com.r3corda.node.services.User
+import com.r3corda.node.services.config.FullNodeConfiguration
+import com.r3corda.node.services.messaging.ArtemisMessagingComponent
import com.r3corda.node.services.transactions.SimpleNotaryService
import javafx.stage.Stage
+import org.controlsfx.dialog.ExceptionDialog
import tornadofx.App
+import java.util.*
+/**
+ * Main class for Explorer, you will need Tornado FX to run the explorer.
+ */
class Main : App() {
override val primaryView = MainWindow::class
override fun start(stage: Stage) {
-
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
throwable.printStackTrace()
- System.exit(1)
+ // Show exceptions in exception dialog.
+ runInFxApplicationThread {
+ // [showAndWait] need to be in the FX thread
+ ExceptionDialog(throwable).showAndWait()
+ System.exit(1)
+ }
+ }
+ super.start(stage)
+ }
+}
+
+/**
+ * This main method will starts 3 nodes (Notary, Alice and Bob) locally for UI testing, they will be on localhost:20002, 20004, 20006 respectively.
+ */
+fun main(args: Array) {
+ val portAllocation = PortAllocation.Incremental(20000)
+ driver(portAllocation = portAllocation) {
+ val user = User("user1", "test", permissions = setOf(CordaRPCOpsImpl.CASH_PERMISSION))
+ val notary = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
+ val alice = startNode("Alice", rpcUsers = arrayListOf(user))
+ val bob = startNode("Bob", rpcUsers = arrayListOf(user))
+
+ val notaryNode = notary.get()
+ val aliceNode = alice.get()
+ val bobNode = bob.get()
+
+ arrayOf(notaryNode, aliceNode, bobNode).forEach {
+ println("${it.nodeInfo.legalIdentity} started on ${ArtemisMessagingComponent.toHostAndPort(it.nodeInfo.address)}")
}
- super.start(stage)
+ // Register with alice to use alice's RPC proxy to create random events.
+ Models.get(Main::class).register(ArtemisMessagingComponent.toHostAndPort(aliceNode.nodeInfo.address), FullNodeConfiguration(aliceNode.config), user.username, user.password)
+ val rpcProxy = Models.get(Main::class).proxyObservable.get()
- // start the driver on another thread
- // TODO Change this to connecting to an actual node (specified on cli/in a config) once we're happy with the code
- Thread({
- val portAllocation = PortAllocation.Incremental(20000)
- driver(portAllocation = portAllocation) {
-
- val aliceNodeFuture = startNode("Alice")
- val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
-
- val aliceNode = aliceNodeFuture.get().nodeInfo
- val notaryNode = notaryNodeFuture.get().nodeInfo
-
- Models.get(Main::class).notary.set(notaryNode.notaryIdentity)
- Models.get(Main::class).myIdentity.set(aliceNode.legalIdentity)
- Models.get(Main::class).register(aliceNode, configureTestSSL(), "user1", "test")
-
- startNode("Bob").get()
-
-/* for (i in 0 .. 10000) {
- Thread.sleep(500)
-
- val eventGenerator = EventGenerator(
- parties = listOf(aliceNode.legalIdentity),
- notary = notaryNode.notaryIdentity
- )
-
- eventGenerator.clientToServiceCommandGenerator.map { command ->
- aliceOutStream.onNext(command)
- }.generate(Random())
- }*/
- waitForAllNodesToFinish()
- }
- }).start()
+ for (i in 0..10000) {
+ Thread.sleep(500)
+ val eventGenerator = EventGenerator(
+ parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity),
+ notary = notaryNode.nodeInfo.notaryIdentity
+ )
+ eventGenerator.clientToServiceCommandGenerator.map { command ->
+ rpcProxy?.executeCommand(command)
+ }.generate(Random())
+ }
+ waitForAllNodesToFinish()
}
}
\ No newline at end of file
diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/MainWindow.kt b/explorer/src/main/kotlin/com/r3corda/explorer/MainWindow.kt
index f9537b1400..f21317c55d 100644
--- a/explorer/src/main/kotlin/com/r3corda/explorer/MainWindow.kt
+++ b/explorer/src/main/kotlin/com/r3corda/explorer/MainWindow.kt
@@ -1,9 +1,14 @@
package com.r3corda.explorer
+import com.r3corda.client.model.Models
+import com.r3corda.client.model.NodeMonitorModel
+import com.r3corda.explorer.views.LoginView
import com.r3corda.explorer.views.TopLevel
+import com.r3corda.node.services.config.configureTestSSL
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory
import jfxtras.resources.JFXtrasFontRoboto
-import tornadofx.*
+import tornadofx.View
+import tornadofx.importStylesheet
/**
* The root view embeds the [Shell] and provides support for the status bar, and modal dialogs.
@@ -11,10 +16,14 @@ import tornadofx.*
class MainWindow : View() {
private val toplevel: TopLevel by inject()
override val root = toplevel.root
+ private val loginView by inject()
init {
// Do this first before creating the notification bar, so it can autosize itself properly.
loadFontsAndStyles()
+ loginView.login { hostAndPort, username, password ->
+ Models.get(MainWindow::class).register(hostAndPort, configureTestSSL(), username, password)
+ }
}
private fun loadFontsAndStyles() {
@@ -23,4 +32,4 @@ class MainWindow : View() {
FontAwesomeIconFactory.get() // Force initialisation.
root.styleClass += "root"
}
-}
+}
\ No newline at end of file
diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/components/ExceptionDialog.kt b/explorer/src/main/kotlin/com/r3corda/explorer/components/ExceptionDialog.kt
deleted file mode 100644
index af39ec9f85..0000000000
--- a/explorer/src/main/kotlin/com/r3corda/explorer/components/ExceptionDialog.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.r3corda.explorer.components
-
-import javafx.scene.control.Alert
-import javafx.scene.control.Label
-import javafx.scene.control.TextArea
-import javafx.scene.layout.GridPane
-import javafx.scene.layout.Priority
-import java.io.PrintWriter
-import java.io.StringWriter
-
-class ExceptionDialog(ex: Throwable) : Alert(AlertType.ERROR) {
-
- private fun Throwable.toExceptionText(): String {
- return StringWriter().use {
- PrintWriter(it).use {
- this.printStackTrace(it)
- }
- it.toString()
- }
- }
-
- init {
- // Create expandable Exception.
- val label = Label("The exception stacktrace was:")
- contentText = ex.message
-
- val textArea = TextArea(ex.toExceptionText())
- textArea.isEditable = false
- textArea.isWrapText = true
-
- textArea.maxWidth = Double.MAX_VALUE
- textArea.maxHeight = Double.MAX_VALUE
- GridPane.setVgrow(textArea, Priority.ALWAYS)
- GridPane.setHgrow(textArea, Priority.ALWAYS)
-
- val expContent = GridPane()
- expContent.maxWidth = Double.MAX_VALUE
- expContent.add(label, 0, 0)
- expContent.add(textArea, 0, 1)
-
- // Set expandable Exception into the dialog pane.
- dialogPane.expandableContent = expContent
- }
-}
diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/identicon/IdenticonRenderer.kt b/explorer/src/main/kotlin/com/r3corda/explorer/identicon/IdenticonRenderer.kt
new file mode 100644
index 0000000000..8abebc63d2
--- /dev/null
+++ b/explorer/src/main/kotlin/com/r3corda/explorer/identicon/IdenticonRenderer.kt
@@ -0,0 +1,194 @@
+package com.r3corda.explorer.identicon
+
+import com.google.common.base.Splitter
+import com.r3corda.core.crypto.SecureHash
+import javafx.scene.SnapshotParameters
+import javafx.scene.canvas.Canvas
+import javafx.scene.canvas.GraphicsContext
+import javafx.scene.control.ContentDisplay
+import javafx.scene.control.Tooltip
+import javafx.scene.image.ImageView
+import javafx.scene.image.WritableImage
+import javafx.scene.paint.Color
+import javafx.scene.text.TextAlignment
+
+/**
+ * (The MIT License)
+ * Copyright (c) 2007-2012 Don Park
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * 'Software'), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * The code originated from : https://github.com/donpark/identicon
+ * And has been modified to Kotlin and JavaFX instead of Java code using AWT
+ */
+
+class IdenticonRenderer {
+
+ companion object {
+ /**
+ * Each patch is a polygon created from a list of vertices on a 5 by 5 grid.
+ * Vertices are numbered from 0 to 24, starting from top-left corner of the
+ * grid, moving left to right and top to bottom.
+ */
+ private val patchTypes = arrayOf(
+ byteArrayOf(0, 4, 24, 20, 0),
+ byteArrayOf(0, 4, 20, 0),
+ byteArrayOf(2, 24, 20, 2),
+ byteArrayOf(0, 2, 20, 22, 0),
+ byteArrayOf(2, 14, 22, 10, 2),
+ byteArrayOf(0, 14, 24, 22, 0),
+ byteArrayOf(2, 24, 22, 13, 11, 22, 20, 2),
+ byteArrayOf(0, 14, 22, 0),
+ byteArrayOf(6, 8, 18, 16, 6),
+ byteArrayOf(4, 20, 10, 12, 2, 4),
+ byteArrayOf(0, 2, 12, 10, 0),
+ byteArrayOf(10, 14, 22, 10),
+ byteArrayOf(20, 12, 24, 20),
+ byteArrayOf(10, 2, 12, 10),
+ byteArrayOf(0, 2, 10, 0),
+ byteArrayOf(0, 4, 24, 20, 0)).map(::Patch)
+
+ private val PATCH_CELLS = 4
+ private val PATCH_GRIDS = PATCH_CELLS + 1
+ private val PATCH_SYMMETRIC: Byte = 1
+ private val PATCH_INVERTED: Byte = 2
+
+ private val patchFlags = byteArrayOf(PATCH_SYMMETRIC, 0, 0, 0, PATCH_SYMMETRIC, 0, 0, 0, PATCH_SYMMETRIC, 0, 0, 0, 0, 0, 0, (PATCH_SYMMETRIC + PATCH_INVERTED).toByte())
+ }
+
+ private class Patch(private val byteArray: ByteArray) {
+ fun x(patchSize: Double): DoubleArray {
+ return byteArray.map(Byte::toInt).map { it % PATCH_GRIDS * (patchSize / PATCH_CELLS) - patchSize / 2 }.toDoubleArray()
+ }
+
+ fun y(patchSize: Double): DoubleArray {
+ return byteArray.map(Byte::toInt).map { it / PATCH_GRIDS * (patchSize / PATCH_CELLS) - patchSize / 2 }.toDoubleArray()
+ }
+
+ val size = byteArray.size
+ }
+
+ /**
+ * Returns rendered identicon image for given identicon code.
+ * Size of the returned identicon image is determined by patchSize set using
+ * [setPatchSize]. Since a 9-block identicon consists of 3x3 patches,
+ * width and height will be 3 times the patch size.
+ */
+ fun render(code: Int, patchSize: Double, backgroundColor: Color = Color.WHITE): WritableImage {
+ // decode the code into parts
+ val middleType = intArrayOf(0, 4, 8, 15)[code and 0x3] // bit 0-1: middle patch type
+ val middleInvert = code shr 2 and 0x1 != 0 // bit 2: middle invert
+ val cornerType = code shr 3 and 0x0f // bit 3-6: corner patch type
+ val cornerInvert = code shr 7 and 0x1 != 0 // bit 7: corner invert
+ val cornerTurn = code shr 8 and 0x3 // bit 8-9: corner turns
+ val sideType = code shr 10 and 0x0f // bit 10-13: side patch type
+ val sideInvert = code shr 14 and 0x1 != 0 // bit 14: side invert
+ val sideTurn = code shr 15 and 0x3 // bit 15: corner turns
+ val blue = code shr 16 and 0x01f // bit 16-20: blue color component
+ val green = code shr 21 and 0x01f // bit 21-26: green color component
+ val red = code shr 27 and 0x01f // bit 27-31: red color component
+
+ // color components are used at top of the range for color difference
+ // use white background for now.
+ // TODO: support transparency.
+ val fillColor = Color.rgb(red shl 3, green shl 3, blue shl 3)
+ // outline shapes with a noticeable color (complementary will do) if
+ // shape color and background color are too similar (measured by color
+ // distance).
+ val strokeColor = if (getColorDistance(fillColor, backgroundColor) < 32.0f) fillColor.invert() else null
+
+ val sourceSize = patchSize * 3
+ val canvas = Canvas(sourceSize, sourceSize)
+ val g = canvas.graphicsContext2D
+ /** Rendering Order:
+ * 6 2 7
+ * 5 1 3
+ * 9 4 8 */
+ val color = PatchColor(fillColor, strokeColor, backgroundColor)
+ drawPatch(g, patchSize, patchSize, middleType, 0, patchSize, middleInvert, color)
+ drawPatch(g, patchSize, 0.0, sideType, sideTurn, patchSize, sideInvert, color)
+ drawPatch(g, patchSize * 2, patchSize, sideType, sideTurn + 1, patchSize, sideInvert, color)
+ drawPatch(g, patchSize, patchSize * 2, sideType, sideTurn + 2, patchSize, sideInvert, color)
+ drawPatch(g, 0.0, patchSize, sideType, sideTurn + 3, patchSize, sideInvert, color)
+ drawPatch(g, 0.0, 0.0, cornerType, cornerTurn, patchSize, cornerInvert, color)
+ drawPatch(g, patchSize * 2, 0.0, cornerType, cornerTurn + 1, patchSize, cornerInvert, color)
+ drawPatch(g, patchSize * 2, patchSize * 2, cornerType, cornerTurn + 2, patchSize, cornerInvert, color)
+ drawPatch(g, 0.0, patchSize * 2, cornerType, cornerTurn + 3, patchSize, cornerInvert, color)
+ return canvas.snapshot(SnapshotParameters(), WritableImage(sourceSize.toInt(), sourceSize.toInt()))
+ }
+
+ private class PatchColor(private val fillColor: Color, val strokeColor: Color?, private val backgroundColor: Color) {
+ fun background(invert: Boolean) = if (invert) fillColor else backgroundColor
+ fun fill(invert: Boolean) = if (invert) backgroundColor else fillColor
+ }
+
+ private fun drawPatch(g: GraphicsContext, x: Double, y: Double, patchIndex: Int, turn: Int, patchSize: Double, _invert: Boolean, color: PatchColor) {
+ val patch = patchTypes[patchIndex % patchTypes.size]
+ val invert = if ((patchFlags[patchIndex].toInt() and PATCH_INVERTED.toInt()) !== 0) !_invert else _invert
+ g.apply {
+ // paint background
+ clearRect(x, y, patchSize, patchSize)
+ fill = color.background(invert)
+ stroke = color.background(invert)
+ fillRect(x, y, patchSize, patchSize)
+ strokeRect(x, y, patchSize, patchSize)
+ // offset and rotate coordinate space by patch position (x, y) and
+ // 'turn' before rendering patch shape
+ val saved = transform
+ translate(x + patchSize / 2, y + patchSize / 2)
+ rotate((turn % 4 * 90).toDouble())
+
+ // if stroke color was specified, apply stroke
+ // stroke color should be specified if fore color is too close to the
+ // back color.
+ if (color.strokeColor != null) {
+ stroke = color.strokeColor
+ strokePolygon(patch.x(patchSize), patch.y(patchSize), patch.size)
+ }
+ // render rotated patch using fore color (back color if inverted)
+ fill = color.fill(invert)
+ fillPolygon(patch.x(patchSize), patch.y(patchSize), patch.size)
+ // restore rotation
+ transform = saved
+ }
+ }
+
+ /**
+ * Returns distance between two colors.
+ */
+ private fun getColorDistance(c1: Color, c2: Color): Float {
+ val dx = (c1.red - c2.red) * 256
+ val dy = (c1.green - c2.green) * 256
+ val dz = (c1.blue - c2.blue) * 256
+ return Math.sqrt(dx * dx + dy * dy + dz * dz.toDouble()).toFloat()
+ }
+}
+
+fun identicon(secureHash: SecureHash, size: Double): WritableImage {
+ return IdenticonRenderer().render(secureHash.hashCode(), size)
+}
+
+fun identiconToolTip(secureHash: SecureHash): Tooltip {
+ return Tooltip(Splitter.fixedLength(16).split("$secureHash").joinToString("\n")).apply {
+ contentDisplay = ContentDisplay.TOP
+ textAlignment = TextAlignment.CENTER
+ graphic = ImageView(identicon(secureHash, 30.0))
+ }
+}
\ No newline at end of file
diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/model/IdentityModel.kt b/explorer/src/main/kotlin/com/r3corda/explorer/model/IdentityModel.kt
deleted file mode 100644
index 95a50fec3c..0000000000
--- a/explorer/src/main/kotlin/com/r3corda/explorer/model/IdentityModel.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.r3corda.explorer.model
-
-import com.r3corda.core.crypto.Party
-import javafx.beans.property.SimpleObjectProperty
-
-class IdentityModel {
- val myIdentity = SimpleObjectProperty()
- val notary = SimpleObjectProperty()
-}
\ No newline at end of file
diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/model/TopLevelModel.kt b/explorer/src/main/kotlin/com/r3corda/explorer/model/TopLevelModel.kt
index 9d8b28ef95..a47abac43a 100644
--- a/explorer/src/main/kotlin/com/r3corda/explorer/model/TopLevelModel.kt
+++ b/explorer/src/main/kotlin/com/r3corda/explorer/model/TopLevelModel.kt
@@ -1,12 +1,22 @@
package com.r3corda.explorer.model
import javafx.beans.property.SimpleObjectProperty
+import javafx.scene.image.Image
-enum class SelectedView {
- Home,
- Cash,
- Transaction,
- NewTransaction
+enum class SelectedView(val displayableName: String, val image: Image, val subviews: Array = emptyArray()) {
+ Home("Home", getImage("home.png")),
+ Transaction("Transaction", getImage("tx.png")),
+ Setting("Setting", getImage("settings_lrg.png")),
+ NewTransaction("New Transaction", getImage("cash.png")),
+ Cash("Cash", getImage("cash.png"), arrayOf(Transaction, NewTransaction)),
+ NetworkMap("Network Map", getImage("cash.png")),
+ Vault("Vault", getImage("cash.png"), arrayOf(Cash)),
+ Network("Network", getImage("inst.png"), arrayOf(NetworkMap, Transaction))
+}
+
+private fun getImage(imageName: String): Image {
+ val basePath = "/com/r3corda/explorer/images"
+ return Image("$basePath/$imageName")
}
class TopLevelModel {
diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/views/CashViewer.kt b/explorer/src/main/kotlin/com/r3corda/explorer/views/CashViewer.kt
index c696142e43..5aac77774f 100644
--- a/explorer/src/main/kotlin/com/r3corda/explorer/views/CashViewer.kt
+++ b/explorer/src/main/kotlin/com/r3corda/explorer/views/CashViewer.kt
@@ -1,212 +1,142 @@
package com.r3corda.explorer.views
import com.r3corda.client.fxutils.*
-import com.r3corda.client.model.ContractStateModel
-import com.r3corda.client.model.observableList
-import com.r3corda.client.model.observableValue
+import com.r3corda.client.model.*
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.Amount
import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.contracts.withoutIssuer
import com.r3corda.core.crypto.Party
import com.r3corda.explorer.formatters.AmountFormatter
+import com.r3corda.explorer.identicon.identicon
+import com.r3corda.explorer.identicon.identiconToolTip
import com.r3corda.explorer.model.ReportingCurrencyModel
import com.r3corda.explorer.model.SettingsModel
import com.r3corda.explorer.ui.*
+import com.sun.javafx.collections.ObservableListWrapper
+import javafx.application.Platform
import javafx.beans.binding.Bindings
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import javafx.collections.ObservableList
+import javafx.geometry.Insets
import javafx.scene.Node
+import javafx.scene.Parent
+import javafx.scene.chart.NumberAxis
import javafx.scene.control.*
import javafx.scene.image.ImageView
-import javafx.scene.input.MouseButton
-import javafx.scene.input.MouseEvent
-import javafx.scene.layout.HBox
+import javafx.scene.layout.BorderPane
import javafx.scene.layout.VBox
import org.fxmisc.easybind.EasyBind
-import tornadofx.UIComponent
-import tornadofx.View
+import tornadofx.*
+import java.time.Instant
import java.time.LocalDateTime
import java.util.*
-sealed class FilterCriteria {
- abstract fun matches(string: String): Boolean
+class CashViewer : View(), CordaView {
+ // Inject UI elements.
+ override val root: BorderPane by fxml()
- object All : FilterCriteria() {
- override fun matches(string: String) = true
+ // View's widget.
+ override val viewName = "Cash"
+ override val widget: Node = vbox {
+ padding = Insets(0.0, 10.0, 0.0, 0.0)
+ val xAxis = NumberAxis().apply {
+ //isAutoRanging = true
+ isMinorTickVisible = false
+ isForceZeroInRange = false
+ tickLabelFormatter = stringConverter {
+ Instant.ofEpochMilli(it.toLong()).atZone(TimeZone.getDefault().toZoneId()).toLocalTime().toString()
+ }
+ }
+ val yAxis = NumberAxis().apply {
+ isAutoRanging = true
+ isMinorTickVisible = false
+ isForceZeroInRange = false
+ tickLabelFormatter = stringConverter { it.toStringWithSuffix(0) }
+ }
+ linechart(null, xAxis, yAxis) {
+ series("USD") {
+ runAsync {
+ while (true) {
+ Thread.sleep(1000)
+ Platform.runLater {
+ // Modify data in UI thread.
+ if (data.size > 300) data.remove(0, 1)
+ data(System.currentTimeMillis(), sumAmount.value.quantity)
+ }
+ }
+ }
+ }
+ createSymbols = false
+ animated = false
+ }
}
- class FilterString(val filterString: String) : FilterCriteria() {
- override fun matches(string: String) = string.contains(filterString)
- }
-}
-
-class CashViewer : View() {
- // Inject UI elements
- override val root: SplitPane by fxml()
-
- val topSplitPane: SplitPane by fxid()
// Left pane
- val leftPane: VBox by fxid()
- val searchCriteriaTextField: TextField by fxid()
- val searchCancelImageView: ImageView by fxid()
- val totalMatchingLabel: Label by fxid()
- val cashViewerTable: TreeTableView by fxid()
- val cashViewerTableIssuerCurrency: TreeTableColumn by fxid()
- val cashViewerTableLocalCurrency: TreeTableColumn?> by fxid()
- val cashViewerTableEquiv: TreeTableColumn?> by fxid()
+ private val leftPane: VBox by fxid()
+ private val splitPane: SplitPane by fxid()
+ private val totalMatchingLabel: Label by fxid()
+ private val cashViewerTable: TreeTableView by fxid()
+ private val cashViewerTableIssuerCurrency: TreeTableColumn by fxid()
+ private val cashViewerTableLocalCurrency: TreeTableColumn?> by fxid()
+ private val cashViewerTableEquiv: TreeTableColumn?> by fxid()
// Right pane
- val rightPane: VBox by fxid()
- val totalPositionsLabel: Label by fxid()
- val equivSumLabel: Label by fxid()
- val cashStatesList: ListView by fxid()
+ private val rightPane: VBox by fxid()
+ private val totalPositionsLabel: Label by fxid()
+ private val cashStatesList: ListView by fxid()
+ private val toggleButton by fxid