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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/explorer/src/main/resources/com/r3corda/explorer/views/Home.fxml b/explorer/src/main/resources/com/r3corda/explorer/views/Home.fxml index 9248f483f9..89855f0aff 100644 --- a/explorer/src/main/resources/com/r3corda/explorer/views/Home.fxml +++ b/explorer/src/main/resources/com/r3corda/explorer/views/Home.fxml @@ -2,19 +2,21 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/explorer/src/main/resources/com/r3corda/explorer/views/LoginView.fxml b/explorer/src/main/resources/com/r3corda/explorer/views/LoginView.fxml new file mode 100644 index 0000000000..01b5b8129f --- /dev/null +++ b/explorer/src/main/resources/com/r3corda/explorer/views/LoginView.fxml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/explorer/src/main/resources/com/r3corda/explorer/views/SearchField.fxml b/explorer/src/main/resources/com/r3corda/explorer/views/SearchField.fxml new file mode 100644 index 0000000000..69a64340ba --- /dev/null +++ b/explorer/src/main/resources/com/r3corda/explorer/views/SearchField.fxml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/explorer/src/main/resources/com/r3corda/explorer/views/Sidebar.fxml b/explorer/src/main/resources/com/r3corda/explorer/views/Sidebar.fxml new file mode 100644 index 0000000000..355c7caf3e --- /dev/null +++ b/explorer/src/main/resources/com/r3corda/explorer/views/Sidebar.fxml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/explorer/src/main/resources/com/r3corda/explorer/views/TopLevel.fxml b/explorer/src/main/resources/com/r3corda/explorer/views/TopLevel.fxml index bef6b03776..cfa177630a 100644 --- a/explorer/src/main/resources/com/r3corda/explorer/views/TopLevel.fxml +++ b/explorer/src/main/resources/com/r3corda/explorer/views/TopLevel.fxml @@ -1,8 +1,12 @@ + + - - - - + + + + + + \ No newline at end of file diff --git a/explorer/src/main/resources/com/r3corda/explorer/views/TransactionViewer.fxml b/explorer/src/main/resources/com/r3corda/explorer/views/TransactionViewer.fxml index b886208120..1f5febab64 100644 --- a/explorer/src/main/resources/com/r3corda/explorer/views/TransactionViewer.fxml +++ b/explorer/src/main/resources/com/r3corda/explorer/views/TransactionViewer.fxml @@ -1,137 +1,22 @@ - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + +
+ + + + + +
+ + +
diff --git a/explorer/src/main/resources/com/r3corda/explorer/views/searchfield.fxml b/explorer/src/main/resources/com/r3corda/explorer/views/searchfield.fxml deleted file mode 100644 index ee68a49b11..0000000000 --- a/explorer/src/main/resources/com/r3corda/explorer/views/searchfield.fxml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/node/src/main/kotlin/com/r3corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/com/r3corda/node/internal/CordaRPCOpsImpl.kt index 2860655bcc..40a29bb0ac 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/CordaRPCOpsImpl.kt @@ -77,6 +77,10 @@ class CordaRPCOpsImpl( } } + override fun nodeIdentity(): NodeInfo { + return services.myInfo + } + override fun addVaultTransactionNote(txnId: SecureHash, txnNote: String) { return databaseTransaction(database) { services.vaultService.addNoteToTransaction(txnId, txnNote) diff --git a/node/src/main/kotlin/com/r3corda/node/services/messaging/CordaRPCOps.kt b/node/src/main/kotlin/com/r3corda/node/services/messaging/CordaRPCOps.kt index 6afa401e71..a1cc4bb2e9 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/messaging/CordaRPCOps.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/messaging/CordaRPCOps.kt @@ -118,6 +118,10 @@ interface CordaRPCOps : RPCOps { */ fun executeCommand(command: ClientToServiceCommand): TransactionBuildResult + /** + * Returns Node's identity, assuming this will not change while the node is running. + */ + fun nodeIdentity(): NodeInfo /* * Add note(s) to an existing Vault transaction */ diff --git a/node/src/main/kotlin/com/r3corda/node/services/messaging/RPCStructures.kt b/node/src/main/kotlin/com/r3corda/node/services/messaging/RPCStructures.kt index 5a1eb09b83..6b4585b21a 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/messaging/RPCStructures.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/messaging/RPCStructures.kt @@ -16,10 +16,8 @@ import com.r3corda.core.crypto.* import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.PhysicalLocation import com.r3corda.core.node.ServiceEntry -import com.r3corda.core.node.services.NetworkMapCache -import com.r3corda.core.node.services.ServiceInfo -import com.r3corda.core.node.services.StateMachineTransactionMapping -import com.r3corda.core.node.services.Vault +import com.r3corda.core.node.WorldCoordinate +import com.r3corda.core.node.services.* import com.r3corda.core.protocols.StateMachineRunId import com.r3corda.core.serialization.* import com.r3corda.core.transactions.SignedTransaction @@ -101,6 +99,7 @@ fun requirePermission(permission: String) { */ open class RPCException(msg: String, cause: Throwable?) : RuntimeException(msg, cause) { constructor(msg: String) : this(msg, null) + class DeadlineExceeded(rpcName: String) : RPCException("Deadline exceeded on call to $rpcName") } @@ -187,8 +186,12 @@ private class RPCKryo(observableSerializer: Serializer>? = null) kryo.writeObject(output, nodeAddress.hostAndPort) } ) + register(NodeMessagingClient.makeNetworkMapAddress(HostAndPort.fromString("localhost:0")).javaClass) + register(ServiceInfo::class.java) + register(ServiceType.getServiceType("ab", "ab").javaClass) + register(ServiceType.parse("ab").javaClass) + register(WorldCoordinate::class.java) register(HostAndPort::class.java) - register(ServiceInfo::class.java, read = { kryo, input -> ServiceInfo.parse(input.readString()) }, write = Kryo::writeObject) // Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway. register(IllegalArgumentException::class.java) // Kryo couldn't serialize Collections.unmodifiableCollection in Throwable correctly, causing null pointer exception when try to access the deserialize object. diff --git a/node/src/main/kotlin/com/r3corda/node/services/network/InMemoryNetworkMapCache.kt b/node/src/main/kotlin/com/r3corda/node/services/network/InMemoryNetworkMapCache.kt index 4fe8d4f911..c86b6bc2ad 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/network/InMemoryNetworkMapCache.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/network/InMemoryNetworkMapCache.kt @@ -25,7 +25,6 @@ import com.r3corda.node.services.network.NetworkMapService.Companion.FETCH_PROTO import com.r3corda.node.services.network.NetworkMapService.Companion.SUBSCRIPTION_PROTOCOL_TOPIC import com.r3corda.node.services.network.NetworkMapService.FetchMapResponse import com.r3corda.node.services.network.NetworkMapService.SubscribeResponse -import com.r3corda.node.services.transactions.SimpleNotaryService import com.r3corda.node.utilities.AddOrRemove import com.r3corda.protocols.sendRequest import rx.Observable @@ -59,13 +58,7 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach override fun track(): Pair, Observable> { synchronized(_changed) { - fun NodeInfo.isCordaService(): Boolean { - return advertisedServices.any { it.info.type in setOf(SimpleNotaryService.type, NetworkMapService.type) } - } - - val currentParties = partyNodes.filter { !it.isCordaService() } - val changes = changed.filter { !it.node.isCordaService() } - return Pair(currentParties, changes.bufferUntilSubscribed()) + return Pair(partyNodes, _changed.bufferUntilSubscribed()) } } diff --git a/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingTests.kt b/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingTests.kt index d7188af848..f24bbdf34b 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/ArtemisMessagingTests.kt @@ -1,6 +1,11 @@ package com.r3corda.node.services import com.google.common.net.HostAndPort +import com.r3corda.core.contracts.ClientToServiceCommand +import com.r3corda.core.contracts.ContractState +import com.r3corda.core.contracts.StateAndRef +import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.generateKeyPair import com.r3corda.core.messaging.Message import com.r3corda.core.messaging.createMessage