From d4362fbd7806cffcf0818a208b269e1906e4a5e2 Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Fri, 7 Oct 2016 17:07:23 +0100 Subject: [PATCH] New counterparty model and subscription mechanism to retrieve and track counterparty changes in network map New transaction creation screen for creating new cash transactions, using party info source from the counterparty model. --- .../r3corda/client/NodeMonitorModelTest.kt | 35 +++- .../client/model/NetworkIdentityModel.kt | 24 +++ .../r3corda/client/model/NodeMonitorModel.kt | 15 +- .../com/r3corda/core/contracts/Structures.kt | 2 +- .../kotlin/com/r3corda/core/node/NodeInfo.kt | 2 +- .../core/node/services/NetworkMapCache.kt | 10 +- .../main/kotlin/com/r3corda/explorer/Main.kt | 18 +- .../explorer/components/ExceptionDialog.kt | 44 +++++ .../r3corda/explorer/model/IdentityModel.kt | 6 +- .../r3corda/explorer/model/TopLevelModel.kt | 3 +- .../explorer/model/TransactionTypes.kt | 7 + .../com/r3corda/explorer/views/Header.kt | 4 +- .../kotlin/com/r3corda/explorer/views/Home.kt | 11 +- .../r3corda/explorer/views/NewTransaction.kt | 177 ++++++++++++++++++ .../com/r3corda/explorer/views/TopLevel.kt | 5 +- .../explorer/views/TransactionViewer.kt | 6 +- .../com/r3corda/explorer/views/Home.fxml | 24 +-- .../explorer/views/NewTransaction.fxml | 36 ++++ .../com/r3corda/explorer/views/TopLevel.fxml | 4 +- .../com/r3corda/node/internal/ServerRPCOps.kt | 9 +- .../node/services/messaging/CordaRPCOps.kt | 10 +- .../node/services/messaging/RPCStructures.kt | 47 ++++- .../network/InMemoryNetworkMapCache.kt | 33 +++- 23 files changed, 461 insertions(+), 71 deletions(-) create mode 100644 client/src/main/kotlin/com/r3corda/client/model/NetworkIdentityModel.kt create mode 100644 explorer/src/main/kotlin/com/r3corda/explorer/components/ExceptionDialog.kt create mode 100644 explorer/src/main/kotlin/com/r3corda/explorer/model/TransactionTypes.kt create mode 100644 explorer/src/main/kotlin/com/r3corda/explorer/views/NewTransaction.kt create mode 100644 explorer/src/main/resources/com/r3corda/explorer/views/NewTransaction.fxml 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 115ad655a9..21366eea14 100644 --- a/client/src/integration-test/kotlin/com/r3corda/client/NodeMonitorModelTest.kt +++ b/client/src/integration-test/kotlin/com/r3corda/client/NodeMonitorModelTest.kt @@ -6,6 +6,7 @@ import com.r3corda.client.model.ProgressTrackingEvent import com.r3corda.core.bufferUntilSubscribed import com.r3corda.core.contracts.* import com.r3corda.core.node.NodeInfo +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 @@ -17,8 +18,12 @@ import com.r3corda.node.driver.startClient import com.r3corda.node.services.messaging.NodeMessagingClient import com.r3corda.node.services.messaging.StateMachineUpdate import com.r3corda.node.services.transactions.SimpleNotaryService -import com.r3corda.testing.* -import org.junit.* +import com.r3corda.testing.expect +import com.r3corda.testing.expectEvents +import com.r3corda.testing.sequence +import org.junit.After +import org.junit.Before +import org.junit.Test import rx.Observable import rx.Observer import kotlin.concurrent.thread @@ -37,7 +42,9 @@ class NodeMonitorModelTest { lateinit var progressTracking: Observable lateinit var transactions: Observable lateinit var vaultUpdates: Observable + lateinit var networkMapUpdates: Observable lateinit var clientToService: Observer + lateinit var newNode: (String) -> NodeInfo @Before fun start() { @@ -49,7 +56,7 @@ class NodeMonitorModelTest { aliceNode = aliceNodeFuture.get() notaryNode = notaryNodeFuture.get() aliceClient = startClient(aliceNode).get() - + newNode = { nodeName -> startNode(nodeName).get() } val monitor = NodeMonitorModel() stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed() @@ -57,11 +64,13 @@ class NodeMonitorModelTest { progressTracking = monitor.progressTracking.bufferUntilSubscribed() transactions = monitor.transactions.bufferUntilSubscribed() vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed() + networkMapUpdates = monitor.networkMap.bufferUntilSubscribed() clientToService = monitor.clientToService monitor.register(aliceNode, aliceClient.config.certificatesPath) driverStarted.set(Unit) stopDriver.get() + } driverStopped.set(Unit) } @@ -74,6 +83,26 @@ class NodeMonitorModelTest { driverStopped.get() } + @Test + fun testNetworkMapUpdate() { + 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 } + } + ) + } + } + @Test fun cashIssueWorksEndToEnd() { clientToService.onNext(ClientToServiceCommand.IssueCash( diff --git a/client/src/main/kotlin/com/r3corda/client/model/NetworkIdentityModel.kt b/client/src/main/kotlin/com/r3corda/client/model/NetworkIdentityModel.kt new file mode 100644 index 0000000000..af4f01e6ce --- /dev/null +++ b/client/src/main/kotlin/com/r3corda/client/model/NetworkIdentityModel.kt @@ -0,0 +1,24 @@ +package com.r3corda.client.model + +import com.r3corda.client.fxutils.foldToObservableList +import com.r3corda.core.node.NodeInfo +import com.r3corda.core.node.services.NetworkMapCache +import javafx.collections.ObservableList +import kotlinx.support.jdk8.collections.removeIf +import rx.Observable + +class NetworkIdentityModel { + private val networkIdentityObservable: Observable by observable(NodeMonitorModel::networkMap) + + val networkIdentities: ObservableList = + networkIdentityObservable.foldToObservableList(Unit) { update, _accumulator, observableList -> + observableList.removeIf { + when (update.type) { + NetworkMapCache.MapChangeType.Removed -> it == update.node + NetworkMapCache.MapChangeType.Modified -> it == update.prevNodeInfo + else -> false + } + } + observableList.addAll(update.node) + } +} \ 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 15dc047e5d..5eee4cf269 100644 --- a/client/src/main/kotlin/com/r3corda/client/model/NodeMonitorModel.kt +++ b/client/src/main/kotlin/com/r3corda/client/model/NodeMonitorModel.kt @@ -3,13 +3,16 @@ package com.r3corda.client.model 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.messaging.ArtemisMessagingComponent +import com.r3corda.node.services.messaging.CordaRPCOps import com.r3corda.node.services.messaging.StateMachineInfo import com.r3corda.node.services.messaging.StateMachineUpdate +import javafx.beans.property.SimpleObjectProperty import rx.Observable import rx.subjects.PublishSubject import java.nio.file.Path @@ -35,16 +38,20 @@ class NodeMonitorModel { private val transactionsSubject = PublishSubject.create() private val stateMachineTransactionMappingSubject = PublishSubject.create() private val progressTrackingSubject = PublishSubject.create() + private val networkMapSubject = PublishSubject.create() val stateMachineUpdates: Observable = stateMachineUpdatesSubject val vaultUpdates: Observable = vaultUpdatesSubject val transactions: Observable = transactionsSubject val stateMachineTransactionMapping: Observable = stateMachineTransactionMappingSubject val progressTracking: Observable = progressTrackingSubject + val networkMap: Observable = networkMapSubject private val clientToServiceSource = PublishSubject.create() val clientToService: PublishSubject = clientToServiceSource + val proxyObservable = SimpleObjectProperty() + /** * Register for updates to/from a given vault. * @param messagingService The messaging to use for communication. @@ -89,9 +96,15 @@ class NodeMonitorModel { val (smTxMappings, futureSmTxMappings) = proxy.stateMachineRecordedTransactionMapping() futureSmTxMappings.startWith(smTxMappings).subscribe(stateMachineTransactionMappingSubject) + // Parties on network + val (parties, futurePartyUpdate) = proxy.networkMapUpdates() + futurePartyUpdate.startWith(parties.map { NetworkMapCache.MapChange(it, null, NetworkMapCache.MapChangeType.Added) }).subscribe(networkMapSubject) + // Client -> Service clientToServiceSource.subscribe { proxy.executeCommand(it) } + + proxyObservable.set(proxy) } -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt index 56f7f15e11..baa3c2e539 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -466,4 +466,4 @@ interface Attachment : NamedByHash { } throw FileNotFoundException() } -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/node/NodeInfo.kt b/core/src/main/kotlin/com/r3corda/core/node/NodeInfo.kt index f0e0ef8000..20a11b889a 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/NodeInfo.kt @@ -20,4 +20,4 @@ data class NodeInfo(val address: SingleMessageRecipient, val physicalLocation: PhysicalLocation? = null) { val notaryIdentity: Party get() = advertisedServices.single { it.info.type.isNotary() }.identity fun serviceIdentities(type: ServiceType): List = advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity } -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/r3corda/core/node/services/NetworkMapCache.kt b/core/src/main/kotlin/com/r3corda/core/node/services/NetworkMapCache.kt index ce4b8239f3..1b79f97b83 100644 --- a/core/src/main/kotlin/com/r3corda/core/node/services/NetworkMapCache.kt +++ b/core/src/main/kotlin/com/r3corda/core/node/services/NetworkMapCache.kt @@ -8,8 +8,8 @@ import com.r3corda.core.messaging.MessagingService import com.r3corda.core.messaging.SingleMessageRecipient import com.r3corda.core.node.NodeInfo import org.slf4j.LoggerFactory -import java.security.PublicKey import rx.Observable +import java.security.PublicKey /** * A network map contains lists of nodes on the network along with information about their identity keys, services @@ -23,7 +23,7 @@ interface NetworkMapCache { } enum class MapChangeType { Added, Removed, Modified } - data class MapChange(val node: NodeInfo, val prevNodeInfo: NodeInfo?, val type: MapChangeType ) + data class MapChange(val node: NodeInfo, val prevNodeInfo: NodeInfo?, val type: MapChangeType) /** A list of nodes that advertise a network map service */ val networkMapNodes: List @@ -43,6 +43,12 @@ interface NetworkMapCache { */ val regulators: List + /** + * Atomically get the current party nodes and a stream of updates. Note that the Observable buffers updates until the + * first subscriber is registered so as to avoid racing with early updates. + */ + fun track(): Pair, Observable> + /** * Get a copy of all nodes in the map. */ diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/Main.kt b/explorer/src/main/kotlin/com/r3corda/explorer/Main.kt index 59b0bf6200..d4702fcd38 100644 --- a/explorer/src/main/kotlin/com/r3corda/explorer/Main.kt +++ b/explorer/src/main/kotlin/com/r3corda/explorer/Main.kt @@ -1,10 +1,7 @@ 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.client.model.subject -import com.r3corda.core.contracts.ClientToServiceCommand import com.r3corda.core.node.services.ServiceInfo import com.r3corda.explorer.model.IdentityModel import com.r3corda.node.driver.PortAllocation @@ -12,13 +9,10 @@ import com.r3corda.node.driver.driver import com.r3corda.node.driver.startClient import com.r3corda.node.services.transactions.SimpleNotaryService import javafx.stage.Stage -import rx.subjects.Subject import tornadofx.App -import java.util.* class Main : App() { override val primaryView = MainWindow::class - val aliceOutStream: Subject by subject(NodeMonitorModel::clientToService) override fun start(stage: Stage) { @@ -32,7 +26,6 @@ class Main : App() { // 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) { @@ -44,10 +37,13 @@ class Main : App() { val aliceClient = startClient(aliceNode).get() + Models.get(Main::class).notary.set(notaryNode.notaryIdentity) Models.get(Main::class).myIdentity.set(aliceNode.legalIdentity) Models.get(Main::class).register(aliceNode, aliceClient.config.certificatesPath) - for (i in 0 .. 10000) { + startNode("Bob").get() + +/* for (i in 0 .. 10000) { Thread.sleep(500) val eventGenerator = EventGenerator( @@ -58,11 +54,9 @@ class Main : App() { eventGenerator.clientToServiceCommandGenerator.map { command -> aliceOutStream.onNext(command) }.generate(Random()) - } - + }*/ waitForAllNodesToFinish() } - }).start() } -} +} \ 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 new file mode 100644 index 0000000000..af39ec9f85 --- /dev/null +++ b/explorer/src/main/kotlin/com/r3corda/explorer/components/ExceptionDialog.kt @@ -0,0 +1,44 @@ +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/model/IdentityModel.kt b/explorer/src/main/kotlin/com/r3corda/explorer/model/IdentityModel.kt index c2813d849b..95a50fec3c 100644 --- a/explorer/src/main/kotlin/com/r3corda/explorer/model/IdentityModel.kt +++ b/explorer/src/main/kotlin/com/r3corda/explorer/model/IdentityModel.kt @@ -3,7 +3,7 @@ package com.r3corda.explorer.model import com.r3corda.core.crypto.Party import javafx.beans.property.SimpleObjectProperty - class IdentityModel { - val myIdentity = SimpleObjectProperty() -} + 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 f7e424fb44..9d8b28ef95 100644 --- a/explorer/src/main/kotlin/com/r3corda/explorer/model/TopLevelModel.kt +++ b/explorer/src/main/kotlin/com/r3corda/explorer/model/TopLevelModel.kt @@ -5,7 +5,8 @@ import javafx.beans.property.SimpleObjectProperty enum class SelectedView { Home, Cash, - Transaction + Transaction, + NewTransaction } class TopLevelModel { diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/model/TransactionTypes.kt b/explorer/src/main/kotlin/com/r3corda/explorer/model/TransactionTypes.kt new file mode 100644 index 0000000000..9404d916e6 --- /dev/null +++ b/explorer/src/main/kotlin/com/r3corda/explorer/model/TransactionTypes.kt @@ -0,0 +1,7 @@ +package com.r3corda.explorer.model + +enum class CashTransaction(val partyNameA: String, val partyNameB: String?) { + Issue("Issuer Bank", "Receiver Bank"), + Pay("Payer", "Payee"), + Exit("Issuer Bank", null); +} \ No newline at end of file diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/views/Header.kt b/explorer/src/main/kotlin/com/r3corda/explorer/views/Header.kt index 927f5f1259..d0b939d625 100644 --- a/explorer/src/main/kotlin/com/r3corda/explorer/views/Header.kt +++ b/explorer/src/main/kotlin/com/r3corda/explorer/views/Header.kt @@ -33,6 +33,7 @@ class Header : View() { SelectedView.Home -> "Home" SelectedView.Cash -> "Cash" SelectedView.Transaction -> "Transactions" + SelectedView.NewTransaction -> "New Transaction" null -> "Home" } }) @@ -42,6 +43,7 @@ class Header : View() { SelectedView.Home -> homeImage SelectedView.Cash -> cashImage SelectedView.Transaction -> transactionImage + SelectedView.NewTransaction -> cashImage null -> homeImage } }) @@ -60,4 +62,4 @@ class Header : View() { sectionIcon.fitWidthProperty().bind(secionLabelHeightNonZero) sectionIcon.fitHeightProperty().bind(sectionIcon.fitWidthProperty()) } -} +} \ No newline at end of file diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/views/Home.kt b/explorer/src/main/kotlin/com/r3corda/explorer/views/Home.kt index 8c8cb7fdf0..1b23d9a3ac 100644 --- a/explorer/src/main/kotlin/com/r3corda/explorer/views/Home.kt +++ b/explorer/src/main/kotlin/com/r3corda/explorer/views/Home.kt @@ -18,11 +18,9 @@ import javafx.scene.control.Label import javafx.scene.control.TitledPane import javafx.scene.input.MouseButton import javafx.scene.layout.TilePane -import org.fxmisc.easybind.EasyBind import tornadofx.View import java.util.* - class Home : View() { override val root: TilePane by fxml() @@ -32,6 +30,8 @@ class Home : View() { private val ourTransactionsPane: TitledPane by fxid() private val ourTransactionsLabel: Label by fxid() + private val newTransaction: TitledPane by fxid() + private val selectedView: WritableValue by writableValue(TopLevelModel::selectedView) private val cashStates: ObservableList> by observableList(ContractStateModel::cashStates) private val gatheredTransactionDataList: ObservableList @@ -63,6 +63,11 @@ class Home : View() { selectedView.value = SelectedView.Transaction } } + newTransaction.setOnMouseClicked { clickEvent -> + if (clickEvent.button == MouseButton.PRIMARY) { + selectedView.value = SelectedView.NewTransaction + } + } } -} +} \ No newline at end of file diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/views/NewTransaction.kt b/explorer/src/main/kotlin/com/r3corda/explorer/views/NewTransaction.kt new file mode 100644 index 0000000000..e108da7e50 --- /dev/null +++ b/explorer/src/main/kotlin/com/r3corda/explorer/views/NewTransaction.kt @@ -0,0 +1,177 @@ +package com.r3corda.explorer.views + +import com.r3corda.client.fxutils.map +import com.r3corda.client.model.NetworkIdentityModel +import com.r3corda.client.model.NodeMonitorModel +import com.r3corda.client.model.observableList +import com.r3corda.client.model.observableValue +import com.r3corda.core.contracts.* +import com.r3corda.core.crypto.Party +import com.r3corda.core.node.NodeInfo +import com.r3corda.core.serialization.OpaqueBytes +import com.r3corda.explorer.components.ExceptionDialog +import com.r3corda.explorer.model.CashTransaction +import com.r3corda.explorer.model.IdentityModel +import com.r3corda.node.services.messaging.CordaRPCOps +import com.r3corda.node.services.messaging.TransactionBuildResult +import javafx.beans.binding.Bindings +import javafx.beans.binding.BooleanBinding +import javafx.beans.value.ObservableValue +import javafx.collections.FXCollections +import javafx.collections.ObservableList +import javafx.scene.Node +import javafx.scene.Parent +import javafx.scene.control.* +import javafx.util.StringConverter +import javafx.util.converter.BigDecimalStringConverter +import tornadofx.View +import java.math.BigDecimal +import java.util.* +import java.util.regex.Pattern + +class NewTransaction : View() { + override val root: Parent by fxml() + + private val partyATextField: TextField by fxid() + private val partyBChoiceBox: ChoiceBox by fxid() + private val partyALabel: Label by fxid() + private val partyBLabel: Label by fxid() + private val amountLabel: Label by fxid() + + private val executeButton: Button by fxid() + + private val transactionTypeCB: ChoiceBox by fxid() + private val amount: TextField by fxid() + private val currency: ChoiceBox by fxid() + + private val networkIdentities: ObservableList by observableList(NetworkIdentityModel::networkIdentities) + + private val rpcProxy: ObservableValue by observableValue(NodeMonitorModel::proxyObservable) + private val myIdentity: ObservableValue by observableValue(IdentityModel::myIdentity) + private val notary: ObservableValue by observableValue(IdentityModel::notary) + + private val issueRefLabel: Label by fxid() + private val issueRefTextField: TextField by fxid() + + private fun ObservableValue<*>.isNotNull(): BooleanBinding { + return Bindings.createBooleanBinding({ this.value != null }, arrayOf(this)) + } + + fun resetScreen() { + partyBChoiceBox.valueProperty().set(null) + transactionTypeCB.valueProperty().set(null) + currency.valueProperty().set(null) + amount.clear() + } + + init { + // Disable everything when not connected to node. + val enableProperty = myIdentity.isNotNull().and(notary.isNotNull()).and(rpcProxy.isNotNull()) + root.disableProperty().bind(enableProperty.not()) + transactionTypeCB.items = FXCollections.observableArrayList(CashTransaction.values().asList()) + + // Party A textfield always display my identity name, not editable. + partyATextField.isEditable = false + partyATextField.textProperty().bind(myIdentity.map { it?.name ?: "" }) + partyALabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA?.let { "$it : " } }) + partyATextField.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA }.isNotNull()) + + partyBLabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB?.let { "$it : " } }) + partyBChoiceBox.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB }.isNotNull()) + partyBChoiceBox.items = networkIdentities + + partyBChoiceBox.converter = object : StringConverter() { + override fun toString(node: NodeInfo?): String { + return node?.legalIdentity?.name ?: "" + } + + override fun fromString(string: String?): NodeInfo { + throw UnsupportedOperationException("not implemented") + } + } + + // BigDecimal text Formatter, restricting text box input to decimal values. + val textFormatter = Pattern.compile("-?((\\d*)|(\\d+\\.\\d*))").run { + TextFormatter(BigDecimalStringConverter(), null) { change -> + val newText = change.controlNewText + if (matcher(newText).matches()) change else null + } + } + amount.textFormatter = textFormatter + + // Hide currency and amount fields when transaction type is not specified. + // TODO : Create a currency model to store these values + currency.items = FXCollections.observableList(setOf(USD, GBP, CHF).toList()) + currency.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull) + amount.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull) + amountLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull) + issueRefLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull) + issueRefTextField.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull) + + // Validate inputs. + val formValidCondition = arrayOf( + myIdentity.isNotNull(), + transactionTypeCB.valueProperty().isNotNull, + partyBChoiceBox.visibleProperty().not().or(partyBChoiceBox.valueProperty().isNotNull), + textFormatter.valueProperty().isNotNull, + textFormatter.valueProperty().isNotEqualTo(BigDecimal.ZERO), + currency.valueProperty().isNotNull + ).reduce(BooleanBinding::and) + + // Enable execute button when form is valid. + executeButton.disableProperty().bind(formValidCondition.not()) + executeButton.setOnAction { event -> + // Null checks to ensure these observable values are set, execute button should be disabled if any of these value are null, this extra checks are for precaution and getting non-nullable values without using !!. + myIdentity.value?.let { myIdentity -> + notary.value?.let { notary -> + rpcProxy.value?.let { rpcProxy -> + Triple(myIdentity, notary, rpcProxy) + } + } + }?.let { + val (myIdentity, notary, rpcProxy) = it + transactionTypeCB.value?.let { + val issueRef = OpaqueBytes(if (issueRefTextField.text.trim().isNotBlank()) issueRefTextField.text.toByteArray() else ByteArray(1, { 1 })) + val command = when (it) { + CashTransaction.Issue -> ClientToServiceCommand.IssueCash(Amount(textFormatter.value, currency.value), issueRef, partyBChoiceBox.value.legalIdentity, notary) + CashTransaction.Pay -> ClientToServiceCommand.PayCash(Amount(textFormatter.value, Issued(PartyAndReference(myIdentity, issueRef), currency.value)), partyBChoiceBox.value.legalIdentity) + CashTransaction.Exit -> ClientToServiceCommand.ExitCash(Amount(textFormatter.value, currency.value), issueRef) + } + + val dialog = Alert(Alert.AlertType.INFORMATION).apply { + headerText = null + contentText = "Transaction Started." + dialogPane.isDisable = true + initOwner((event.target as Node).scene.window) + } + dialog.show() + runAsync { + rpcProxy.executeCommand(command) + }.ui { + dialog.contentText = when (it) { + is TransactionBuildResult.ProtocolStarted -> { + dialog.alertType = Alert.AlertType.INFORMATION + dialog.setOnCloseRequest { resetScreen() } + "Transaction Started \nTransaction ID : ${it.transaction?.id} \nMessage : ${it.message}" + } + is TransactionBuildResult.Failed -> { + dialog.alertType = Alert.AlertType.ERROR + it.toString() + } + } + dialog.dialogPane.isDisable = false + dialog.dialogPane.scene.window.sizeToScene() + }.setOnFailed { + dialog.close() + ExceptionDialog(it.source.exception).apply { + initOwner((event.target as Node).scene.window) + showAndWait() + } + } + } + } + } + // Remove focus from textfield when click on the blank area. + root.setOnMouseClicked { e -> root.requestFocus() } + } +} \ No newline at end of file diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/views/TopLevel.kt b/explorer/src/main/kotlin/com/r3corda/explorer/views/TopLevel.kt index 0509ca5dbe..6c42f78a6f 100644 --- a/explorer/src/main/kotlin/com/r3corda/explorer/views/TopLevel.kt +++ b/explorer/src/main/kotlin/com/r3corda/explorer/views/TopLevel.kt @@ -20,17 +20,20 @@ class TopLevel : View() { private val home: Home by inject() private val cash: CashViewer by inject() private val transaction: TransactionViewer by inject() + private val newTransaction: NewTransaction by inject() // Note: this is weirdly very important, as it forces the initialisation of Views. Therefore this is the entry // point to the top level observable/stream wiring! Any events sent before this init may be lost! private val homeRoot = home.root private val cashRoot = cash.root private val transactionRoot = transaction.root + private val newTransactionRoot = newTransaction.root private fun getView(selection: SelectedView) = when (selection) { SelectedView.Home -> homeRoot SelectedView.Cash -> cashRoot SelectedView.Transaction -> transactionRoot + SelectedView.NewTransaction -> newTransactionRoot } val selectedView: ObjectProperty by objectProperty(TopLevelModel::selectedView) @@ -46,4 +49,4 @@ class TopLevel : View() { root.children.add(0, header.root) } -} +} \ No newline at end of file diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/views/TransactionViewer.kt b/explorer/src/main/kotlin/com/r3corda/explorer/views/TransactionViewer.kt index 121ae3adc9..35b095b2ae 100644 --- a/explorer/src/main/kotlin/com/r3corda/explorer/views/TransactionViewer.kt +++ b/explorer/src/main/kotlin/com/r3corda/explorer/views/TransactionViewer.kt @@ -79,7 +79,7 @@ class TransactionViewer: View() { by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList) private val reportingExchange: ObservableValue) -> Amount>> by observableValue(ReportingCurrencyModel::reportingExchange) - private val myIdentity: ObservableValue by observableValue(IdentityModel::myIdentity) + private val myIdentity: ObservableValue by observableValue(IdentityModel::myIdentity) /** * This is what holds data for a single transaction node. Note how a lot of these are nullable as we often simply don't @@ -363,7 +363,7 @@ class TransactionViewer: View() { * We calculate the total value by subtracting relevant input states and adding relevant output states, as long as they're cash */ private fun calculateTotalEquiv( - identity: Party, + identity: Party?, reportingCurrencyExchange: Pair) -> Amount>, inputs: List>?, outputs: List>): AmountDiff? { @@ -372,7 +372,7 @@ private fun calculateTotalEquiv( } var sum = 0L val (reportingCurrency, exchange) = reportingCurrencyExchange - val publicKey = identity.owningKey + val publicKey = identity?.owningKey inputs.forEach { val contractState = it.state.data if (contractState is Cash.State && publicKey == contractState.owner) { 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 ea6657e8d4..9248f483f9 100644 --- a/explorer/src/main/resources/com/r3corda/explorer/views/Home.fxml +++ b/explorer/src/main/resources/com/r3corda/explorer/views/Home.fxml @@ -1,30 +1,20 @@ - - + - - - - + - - + - - + - - + - + + diff --git a/explorer/src/main/resources/com/r3corda/explorer/views/NewTransaction.fxml b/explorer/src/main/resources/com/r3corda/explorer/views/NewTransaction.fxml new file mode 100644 index 0000000000..1179d32081 --- /dev/null +++ b/explorer/src/main/resources/com/r3corda/explorer/views/NewTransaction.fxml @@ -0,0 +1,36 @@ + + + + + + + +