Merged pat-cash-creation-ui into master

This commit is contained in:
Patrick Kuo 2016-10-17 16:10:53 +01:00
commit 3403d50168
23 changed files with 461 additions and 71 deletions

View File

@ -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<ProgressTrackingEvent>
lateinit var transactions: Observable<SignedTransaction>
lateinit var vaultUpdates: Observable<Vault.Update>
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
lateinit var clientToService: Observer<ClientToServiceCommand>
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(

View File

@ -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<NetworkMapCache.MapChange> by observable(NodeMonitorModel::networkMap)
val networkIdentities: ObservableList<NodeInfo> =
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)
}
}

View File

@ -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<SignedTransaction>()
private val stateMachineTransactionMappingSubject = PublishSubject.create<StateMachineTransactionMapping>()
private val progressTrackingSubject = PublishSubject.create<ProgressTrackingEvent>()
private val networkMapSubject = PublishSubject.create<NetworkMapCache.MapChange>()
val stateMachineUpdates: Observable<StateMachineUpdate> = stateMachineUpdatesSubject
val vaultUpdates: Observable<Vault.Update> = vaultUpdatesSubject
val transactions: Observable<SignedTransaction> = transactionsSubject
val stateMachineTransactionMapping: Observable<StateMachineTransactionMapping> = stateMachineTransactionMappingSubject
val progressTracking: Observable<ProgressTrackingEvent> = progressTrackingSubject
val networkMap: Observable<NetworkMapCache.MapChange> = networkMapSubject
private val clientToServiceSource = PublishSubject.create<ClientToServiceCommand>()
val clientToService: PublishSubject<ClientToServiceCommand> = clientToServiceSource
val proxyObservable = SimpleObjectProperty<CordaRPCOps?>()
/**
* 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)
}
}

View File

@ -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
@ -43,6 +43,12 @@ interface NetworkMapCache {
*/
val regulators: List<NodeInfo>
/**
* 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<List<NodeInfo>, Observable<MapChange>>
/**
* Get a copy of all nodes in the map.
*/

View File

@ -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<ClientToServiceCommand, ClientToServiceCommand> 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<IdentityModel>(Main::class).notary.set(notaryNode.notaryIdentity)
Models.get<IdentityModel>(Main::class).myIdentity.set(aliceNode.legalIdentity)
Models.get<NodeMonitorModel>(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()
}
}

View File

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

View File

@ -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<Party>()
val myIdentity = SimpleObjectProperty<Party?>()
val notary = SimpleObjectProperty<Party?>()
}

View File

@ -5,7 +5,8 @@ import javafx.beans.property.SimpleObjectProperty
enum class SelectedView {
Home,
Cash,
Transaction
Transaction,
NewTransaction
}
class TopLevelModel {

View File

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

View File

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

View File

@ -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<SelectedView> by writableValue(TopLevelModel::selectedView)
private val cashStates: ObservableList<StateAndRef<Cash.State>> by observableList(ContractStateModel::cashStates)
private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData>
@ -63,6 +63,11 @@ class Home : View() {
selectedView.value = SelectedView.Transaction
}
}
newTransaction.setOnMouseClicked { clickEvent ->
if (clickEvent.button == MouseButton.PRIMARY) {
selectedView.value = SelectedView.NewTransaction
}
}
}
}

View File

@ -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<NodeInfo> 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<CashTransaction> by fxid()
private val amount: TextField by fxid()
private val currency: ChoiceBox<Currency> by fxid()
private val networkIdentities: ObservableList<NodeInfo> by observableList(NetworkIdentityModel::networkIdentities)
private val rpcProxy: ObservableValue<CordaRPCOps?> by observableValue(NodeMonitorModel::proxyObservable)
private val myIdentity: ObservableValue<Party?> by observableValue(IdentityModel::myIdentity)
private val notary: ObservableValue<Party?> 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<NodeInfo?>() {
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<BigDecimal>(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() }
}
}

View File

@ -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<SelectedView> by objectProperty(TopLevelModel::selectedView)

View File

@ -79,7 +79,7 @@ class TransactionViewer: View() {
by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
private val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>>
by observableValue(ReportingCurrencyModel::reportingExchange)
private val myIdentity: ObservableValue<Party> by observableValue(IdentityModel::myIdentity)
private val myIdentity: ObservableValue<Party?> 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<Currency, (Amount<Currency>) -> Amount<Currency>>,
inputs: List<StateAndRef<ContractState>>?,
outputs: List<TransactionState<ContractState>>): AmountDiff<Currency>? {
@ -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) {

View File

@ -1,30 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.TilePane?>
<TilePane prefHeight="425.0" prefWidth="425.0" tileAlignment="TOP_LEFT" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<children>
<TitledPane id="tile_cash" fx:id="ourCashPane" alignment="CENTER" collapsible="false" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our cash">
<content>
<Label fx:id="ourCashLabel" text="USD 186.7m" textAlignment="CENTER" wrapText="true"/>
</content>
</TitledPane>
<TitledPane id="tile_debtors" fx:id="ourDebtorsPane" alignment="CENTER" collapsible="false" layoutX="232.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our debtors">
<content>
<Label text="USD 71.3m" textAlignment="CENTER" wrapText="true"/>
</content>
</TitledPane>
<TitledPane id="tile_creditors" fx:id="ourCreditorsPane" alignment="CENTER" collapsible="false" layoutX="312.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our creditors">
<content>
<Label text="USD (29.4m)" textAlignment="CENTER" wrapText="true"/>
</content>
</TitledPane>
<TitledPane id="tile_tx" fx:id="ourTransactionsPane" alignment="CENTER" collapsible="false" layoutX="392.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our transactions">
<content>
<Label fx:id="ourTransactionsLabel" text="In flight: 1,315" textAlignment="CENTER" wrapText="true"/>
</content>
</TitledPane>
</children>
<TitledPane id="tile_new_tx" fx:id="newTransaction" alignment="CENTER" collapsible="false" layoutX="472.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="New Transaction">
</TitledPane>
</TilePane>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<GridPane hgap="10" vgap="10" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<!-- Row 1 -->
<Label text="Transaction Type : " GridPane.halignment="RIGHT"/>
<ChoiceBox fx:id="transactionTypeCB" maxWidth="Infinity" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.hgrow="ALWAYS"/>
<!-- Row 2 -->
<Label fx:id="partyALabel" GridPane.halignment="RIGHT" GridPane.rowIndex="1"/>
<TextField fx:id="partyATextField" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="1"/>
<!-- Row 3 -->
<Label fx:id="partyBLabel" GridPane.halignment="RIGHT" GridPane.rowIndex="2"/>
<ChoiceBox fx:id="partyBChoiceBox" maxWidth="Infinity" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.fillWidth="true" GridPane.hgrow="ALWAYS" GridPane.rowIndex="2"/>
<!-- Row 4 -->
<Label fx:id="amountLabel" text="Amount : " GridPane.halignment="RIGHT" GridPane.rowIndex="3"/>
<ChoiceBox fx:id="currency" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
<TextField fx:id="amount" maxWidth="Infinity" GridPane.columnIndex="2" GridPane.hgrow="ALWAYS" GridPane.rowIndex="3"/>
<!-- Row 5 -->
<Label fx:id="issueRefLabel" text="Issue Reference : " GridPane.halignment="RIGHT" GridPane.rowIndex="4"/>
<TextField fx:id="issueRefTextField" GridPane.columnIndex="1" GridPane.rowIndex="4" GridPane.columnSpan="2"/>
<!-- Row 6 -->
<Button fx:id="executeButton" text="Execute" GridPane.columnIndex="2" GridPane.halignment="RIGHT" GridPane.rowIndex="5"/>
<Pane fx:id="mainPane" prefHeight="0.0" prefWidth="0.0"/>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
</padding>
</GridPane>

View File

@ -4,7 +4,5 @@
<?import javafx.scene.layout.VBox?>
<VBox fx:id="topLevel" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<children>
<BorderPane fx:id="selectionBorderPane"/>
</children>
</VBox>

View File

@ -5,7 +5,9 @@ import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.ServiceHub
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.transactions.SignedTransaction
@ -33,6 +35,10 @@ class ServerRPCOps(
) : CordaRPCOps {
override val protocolVersion: Int = 0
override fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>> {
return services.networkMapCache.track()
}
override fun vaultAndUpdates(): Pair<List<StateAndRef<ContractState>>, Observable<Vault.Update>> {
return databaseTransaction(database) {
val (vault, updates) = services.vaultService.track()
@ -153,5 +159,4 @@ class ServerRPCOps(
class InputStateRefResolveFailed(stateRefs: List<StateRef>) :
Exception("Failed to resolve input StateRefs $stateRefs")
}

View File

@ -3,6 +3,8 @@ package com.r3corda.node.services.messaging
import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef
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
@ -103,6 +105,12 @@ interface CordaRPCOps : RPCOps {
@RPCReturnsObservables
fun stateMachineRecordedTransactionMapping(): Pair<List<StateMachineTransactionMapping>, Observable<StateMachineTransactionMapping>>
/**
* Returns all parties currently visible on the network with their advertised services and an observable of future updates to the network.
*/
@RPCReturnsObservables
fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>>
/**
* Executes the given command, possibly triggering cash creation etc.
* TODO: The signature of this is weird because it's the remains of an old service call, we should have a call for each command instead.

View File

@ -5,13 +5,17 @@ import com.esotericsoftware.kryo.Registration
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.serializers.DefaultSerializers
import com.esotericsoftware.kryo.serializers.JavaSerializer
import com.google.common.net.HostAndPort
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.ErrorOr
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
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.protocols.StateMachineRunId
@ -163,17 +167,42 @@ private class RPCKryo(private val observableSerializer: Serializer<Observable<An
register(setOf(Unit).javaClass) // SingletonSet
register(TransactionBuildResult.ProtocolStarted::class.java)
register(TransactionBuildResult.Failed::class.java)
register(ServiceEntry::class.java)
register(NodeInfo::class.java)
register(PhysicalLocation::class.java)
register(NetworkMapCache.MapChange::class.java)
register(NetworkMapCache.MapChangeType::class.java)
register(ArtemisMessagingComponent.NodeAddress::class.java,
read = { kryo, input -> ArtemisMessagingComponent.NodeAddress(parsePublicKeyBase58(kryo.readObject(input, String::class.java)), kryo.readObject(input, HostAndPort::class.java)) },
write = { kryo, output, nodeAddress ->
kryo.writeObject(output, nodeAddress.identity.toBase58String())
kryo.writeObject(output, nodeAddress.hostAndPort)
}
)
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.
register(NoSuchElementException::class.java, JavaSerializer())
register(RPCException::class.java)
register(Array<StackTraceElement>::class.java, object : Serializer<Array<StackTraceElement>>() {
override fun read(kryo: Kryo, input: Input, type: Class<Array<StackTraceElement>>): Array<StackTraceElement> = emptyArray()
override fun write(kryo: Kryo, output: Output, `object`: Array<StackTraceElement>) {}
})
register(Array<StackTraceElement>::class.java, read = { kryo, input -> emptyArray() }, write = { kryo, output, o -> })
register(Collections.unmodifiableList(emptyList<String>()).javaClass)
}
// Helper method, attempt to reduce boiler plate code
private fun <T> register(type: Class<T>, read: (Kryo, Input) -> T, write: (Kryo, Output, T) -> Unit) {
register(type, object : Serializer<T>() {
override fun read(kryo: Kryo, input: Input, type: Class<T>?): T {
return read(kryo, input)
}
override fun write(kryo: Kryo, output: Output, o: T) {
write(kryo, output, o)
}
})
}
val observableRegistration: Registration? = if (observableSerializer != null) register(Observable::class.java, observableSerializer) else null
override fun getRegistration(type: Class<*>): Registration {

View File

@ -3,6 +3,7 @@ package com.r3corda.node.services.network
import com.google.common.annotations.VisibleForTesting
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import com.r3corda.core.bufferUntilSubscribed
import com.r3corda.core.contracts.Contract
import com.r3corda.core.crypto.Party
import com.r3corda.core.map
@ -23,6 +24,7 @@ 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
@ -54,6 +56,18 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
private var registeredForPush = false
protected var registeredNodes = Collections.synchronizedMap(HashMap<Party, NodeInfo>())
override fun track(): Pair<List<NodeInfo>, Observable<MapChange>> {
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())
}
}
override fun get() = registeredNodes.map { it.value }
override fun get(serviceType: ServiceType) = registeredNodes.filterValues { it.advertisedServices.any { it.info.type.isSubTypeOf(serviceType) } }.map { it.value }
override fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = get(type).firstOrNull()
@ -96,6 +110,7 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
}
override fun addNode(node: NodeInfo) {
synchronized(_changed) {
val oldValue = registeredNodes.put(node.legalIdentity, node)
if (oldValue == null) {
_changed.onNext(MapChange(node, oldValue, MapChangeType.Added))
@ -104,10 +119,14 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
}
}
}
override fun removeNode(node: NodeInfo) {
synchronized(_changed) {
val oldValue = registeredNodes.remove(node.legalIdentity)
_changed.onNext(MapChange(node, oldValue, MapChangeType.Removed))
}
}
/**
* Unsubscribes from updates from the given map service.