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