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.
This commit is contained in:
Patrick Kuo
2016-10-07 17:07:23 +01:00
parent d7ca215f7d
commit d4362fbd78
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.bufferUntilSubscribed
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.node.NodeInfo 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.ServiceInfo
import com.r3corda.core.node.services.StateMachineTransactionMapping import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault 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.NodeMessagingClient
import com.r3corda.node.services.messaging.StateMachineUpdate import com.r3corda.node.services.messaging.StateMachineUpdate
import com.r3corda.node.services.transactions.SimpleNotaryService import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.testing.* import com.r3corda.testing.expect
import org.junit.* 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.Observable
import rx.Observer import rx.Observer
import kotlin.concurrent.thread import kotlin.concurrent.thread
@ -37,7 +42,9 @@ class NodeMonitorModelTest {
lateinit var progressTracking: Observable<ProgressTrackingEvent> lateinit var progressTracking: Observable<ProgressTrackingEvent>
lateinit var transactions: Observable<SignedTransaction> lateinit var transactions: Observable<SignedTransaction>
lateinit var vaultUpdates: Observable<Vault.Update> lateinit var vaultUpdates: Observable<Vault.Update>
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
lateinit var clientToService: Observer<ClientToServiceCommand> lateinit var clientToService: Observer<ClientToServiceCommand>
lateinit var newNode: (String) -> NodeInfo
@Before @Before
fun start() { fun start() {
@ -49,7 +56,7 @@ class NodeMonitorModelTest {
aliceNode = aliceNodeFuture.get() aliceNode = aliceNodeFuture.get()
notaryNode = notaryNodeFuture.get() notaryNode = notaryNodeFuture.get()
aliceClient = startClient(aliceNode).get() aliceClient = startClient(aliceNode).get()
newNode = { nodeName -> startNode(nodeName).get() }
val monitor = NodeMonitorModel() val monitor = NodeMonitorModel()
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed() stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
@ -57,11 +64,13 @@ class NodeMonitorModelTest {
progressTracking = monitor.progressTracking.bufferUntilSubscribed() progressTracking = monitor.progressTracking.bufferUntilSubscribed()
transactions = monitor.transactions.bufferUntilSubscribed() transactions = monitor.transactions.bufferUntilSubscribed()
vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed() vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed()
networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
clientToService = monitor.clientToService clientToService = monitor.clientToService
monitor.register(aliceNode, aliceClient.config.certificatesPath) monitor.register(aliceNode, aliceClient.config.certificatesPath)
driverStarted.set(Unit) driverStarted.set(Unit)
stopDriver.get() stopDriver.get()
} }
driverStopped.set(Unit) driverStopped.set(Unit)
} }
@ -74,6 +83,26 @@ class NodeMonitorModelTest {
driverStopped.get() 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 @Test
fun cashIssueWorksEndToEnd() { fun cashIssueWorksEndToEnd() {
clientToService.onNext(ClientToServiceCommand.IssueCash( 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.client.CordaRPCClient
import com.r3corda.core.contracts.ClientToServiceCommand import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.node.NodeInfo 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.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.transactions.SignedTransaction import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.node.services.messaging.ArtemisMessagingComponent 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.StateMachineInfo
import com.r3corda.node.services.messaging.StateMachineUpdate import com.r3corda.node.services.messaging.StateMachineUpdate
import javafx.beans.property.SimpleObjectProperty
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.nio.file.Path import java.nio.file.Path
@ -35,16 +38,20 @@ class NodeMonitorModel {
private val transactionsSubject = PublishSubject.create<SignedTransaction>() private val transactionsSubject = PublishSubject.create<SignedTransaction>()
private val stateMachineTransactionMappingSubject = PublishSubject.create<StateMachineTransactionMapping>() private val stateMachineTransactionMappingSubject = PublishSubject.create<StateMachineTransactionMapping>()
private val progressTrackingSubject = PublishSubject.create<ProgressTrackingEvent>() private val progressTrackingSubject = PublishSubject.create<ProgressTrackingEvent>()
private val networkMapSubject = PublishSubject.create<NetworkMapCache.MapChange>()
val stateMachineUpdates: Observable<StateMachineUpdate> = stateMachineUpdatesSubject val stateMachineUpdates: Observable<StateMachineUpdate> = stateMachineUpdatesSubject
val vaultUpdates: Observable<Vault.Update> = vaultUpdatesSubject val vaultUpdates: Observable<Vault.Update> = vaultUpdatesSubject
val transactions: Observable<SignedTransaction> = transactionsSubject val transactions: Observable<SignedTransaction> = transactionsSubject
val stateMachineTransactionMapping: Observable<StateMachineTransactionMapping> = stateMachineTransactionMappingSubject val stateMachineTransactionMapping: Observable<StateMachineTransactionMapping> = stateMachineTransactionMappingSubject
val progressTracking: Observable<ProgressTrackingEvent> = progressTrackingSubject val progressTracking: Observable<ProgressTrackingEvent> = progressTrackingSubject
val networkMap: Observable<NetworkMapCache.MapChange> = networkMapSubject
private val clientToServiceSource = PublishSubject.create<ClientToServiceCommand>() private val clientToServiceSource = PublishSubject.create<ClientToServiceCommand>()
val clientToService: PublishSubject<ClientToServiceCommand> = clientToServiceSource val clientToService: PublishSubject<ClientToServiceCommand> = clientToServiceSource
val proxyObservable = SimpleObjectProperty<CordaRPCOps?>()
/** /**
* Register for updates to/from a given vault. * Register for updates to/from a given vault.
* @param messagingService The messaging to use for communication. * @param messagingService The messaging to use for communication.
@ -89,9 +96,15 @@ class NodeMonitorModel {
val (smTxMappings, futureSmTxMappings) = proxy.stateMachineRecordedTransactionMapping() val (smTxMappings, futureSmTxMappings) = proxy.stateMachineRecordedTransactionMapping()
futureSmTxMappings.startWith(smTxMappings).subscribe(stateMachineTransactionMappingSubject) 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 // Client -> Service
clientToServiceSource.subscribe { clientToServiceSource.subscribe {
proxy.executeCommand(it) 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.messaging.SingleMessageRecipient
import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.NodeInfo
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.security.PublicKey
import rx.Observable 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 * 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 } 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 */ /** A list of nodes that advertise a network map service */
val networkMapNodes: List<NodeInfo> val networkMapNodes: List<NodeInfo>
@ -43,6 +43,12 @@ interface NetworkMapCache {
*/ */
val regulators: List<NodeInfo> 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. * Get a copy of all nodes in the map.
*/ */

View File

@ -1,10 +1,7 @@
package com.r3corda.explorer package com.r3corda.explorer
import com.r3corda.client.mock.EventGenerator
import com.r3corda.client.model.Models import com.r3corda.client.model.Models
import com.r3corda.client.model.NodeMonitorModel 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.core.node.services.ServiceInfo
import com.r3corda.explorer.model.IdentityModel import com.r3corda.explorer.model.IdentityModel
import com.r3corda.node.driver.PortAllocation 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.driver.startClient
import com.r3corda.node.services.transactions.SimpleNotaryService import com.r3corda.node.services.transactions.SimpleNotaryService
import javafx.stage.Stage import javafx.stage.Stage
import rx.subjects.Subject
import tornadofx.App import tornadofx.App
import java.util.*
class Main : App() { class Main : App() {
override val primaryView = MainWindow::class override val primaryView = MainWindow::class
val aliceOutStream: Subject<ClientToServiceCommand, ClientToServiceCommand> by subject(NodeMonitorModel::clientToService)
override fun start(stage: Stage) { override fun start(stage: Stage) {
@ -32,7 +26,6 @@ class Main : App() {
// start the driver on another thread // 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 // TODO Change this to connecting to an actual node (specified on cli/in a config) once we're happy with the code
Thread({ Thread({
val portAllocation = PortAllocation.Incremental(20000) val portAllocation = PortAllocation.Incremental(20000)
driver(portAllocation = portAllocation) { driver(portAllocation = portAllocation) {
@ -44,10 +37,13 @@ class Main : App() {
val aliceClient = startClient(aliceNode).get() 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<IdentityModel>(Main::class).myIdentity.set(aliceNode.legalIdentity)
Models.get<NodeMonitorModel>(Main::class).register(aliceNode, aliceClient.config.certificatesPath) 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) Thread.sleep(500)
val eventGenerator = EventGenerator( val eventGenerator = EventGenerator(
@ -58,11 +54,9 @@ class Main : App() {
eventGenerator.clientToServiceCommandGenerator.map { command -> eventGenerator.clientToServiceCommandGenerator.map { command ->
aliceOutStream.onNext(command) aliceOutStream.onNext(command)
}.generate(Random()) }.generate(Random())
} }*/
waitForAllNodesToFinish() waitForAllNodesToFinish()
} }
}).start() }).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 com.r3corda.core.crypto.Party
import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleObjectProperty
class IdentityModel { 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 { enum class SelectedView {
Home, Home,
Cash, Cash,
Transaction Transaction,
NewTransaction
} }
class TopLevelModel { 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.Home -> "Home"
SelectedView.Cash -> "Cash" SelectedView.Cash -> "Cash"
SelectedView.Transaction -> "Transactions" SelectedView.Transaction -> "Transactions"
SelectedView.NewTransaction -> "New Transaction"
null -> "Home" null -> "Home"
} }
}) })
@ -42,6 +43,7 @@ class Header : View() {
SelectedView.Home -> homeImage SelectedView.Home -> homeImage
SelectedView.Cash -> cashImage SelectedView.Cash -> cashImage
SelectedView.Transaction -> transactionImage SelectedView.Transaction -> transactionImage
SelectedView.NewTransaction -> cashImage
null -> homeImage null -> homeImage
} }
}) })

View File

@ -18,11 +18,9 @@ import javafx.scene.control.Label
import javafx.scene.control.TitledPane import javafx.scene.control.TitledPane
import javafx.scene.input.MouseButton import javafx.scene.input.MouseButton
import javafx.scene.layout.TilePane import javafx.scene.layout.TilePane
import org.fxmisc.easybind.EasyBind
import tornadofx.View import tornadofx.View
import java.util.* import java.util.*
class Home : View() { class Home : View() {
override val root: TilePane by fxml() override val root: TilePane by fxml()
@ -32,6 +30,8 @@ class Home : View() {
private val ourTransactionsPane: TitledPane by fxid() private val ourTransactionsPane: TitledPane by fxid()
private val ourTransactionsLabel: Label 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 selectedView: WritableValue<SelectedView> by writableValue(TopLevelModel::selectedView)
private val cashStates: ObservableList<StateAndRef<Cash.State>> by observableList(ContractStateModel::cashStates) private val cashStates: ObservableList<StateAndRef<Cash.State>> by observableList(ContractStateModel::cashStates)
private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData> private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData>
@ -63,6 +63,11 @@ class Home : View() {
selectedView.value = SelectedView.Transaction 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 home: Home by inject()
private val cash: CashViewer by inject() private val cash: CashViewer by inject()
private val transaction: TransactionViewer 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 // 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! // point to the top level observable/stream wiring! Any events sent before this init may be lost!
private val homeRoot = home.root private val homeRoot = home.root
private val cashRoot = cash.root private val cashRoot = cash.root
private val transactionRoot = transaction.root private val transactionRoot = transaction.root
private val newTransactionRoot = newTransaction.root
private fun getView(selection: SelectedView) = when (selection) { private fun getView(selection: SelectedView) = when (selection) {
SelectedView.Home -> homeRoot SelectedView.Home -> homeRoot
SelectedView.Cash -> cashRoot SelectedView.Cash -> cashRoot
SelectedView.Transaction -> transactionRoot SelectedView.Transaction -> transactionRoot
SelectedView.NewTransaction -> newTransactionRoot
} }
val selectedView: ObjectProperty<SelectedView> by objectProperty(TopLevelModel::selectedView) val selectedView: ObjectProperty<SelectedView> by objectProperty(TopLevelModel::selectedView)

View File

@ -79,7 +79,7 @@ class TransactionViewer: View() {
by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList) by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
private val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>> private val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>>
by observableValue(ReportingCurrencyModel::reportingExchange) 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 * 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 * We calculate the total value by subtracting relevant input states and adding relevant output states, as long as they're cash
*/ */
private fun calculateTotalEquiv( private fun calculateTotalEquiv(
identity: Party, identity: Party?,
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>, reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
inputs: List<StateAndRef<ContractState>>?, inputs: List<StateAndRef<ContractState>>?,
outputs: List<TransactionState<ContractState>>): AmountDiff<Currency>? { outputs: List<TransactionState<ContractState>>): AmountDiff<Currency>? {
@ -372,7 +372,7 @@ private fun calculateTotalEquiv(
} }
var sum = 0L var sum = 0L
val (reportingCurrency, exchange) = reportingCurrencyExchange val (reportingCurrency, exchange) = reportingCurrencyExchange
val publicKey = identity.owningKey val publicKey = identity?.owningKey
inputs.forEach { inputs.forEach {
val contractState = it.state.data val contractState = it.state.data
if (contractState is Cash.State && publicKey == contractState.owner) { if (contractState is Cash.State && publicKey == contractState.owner) {

View File

@ -1,30 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.*?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.layout.TilePane?> <?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"> <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"> <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"/>
<Label fx:id="ourCashLabel" text="USD 186.7m" textAlignment="CENTER" wrapText="true" />
</content>
</TitledPane> </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"> <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"/>
<Label text="USD 71.3m" textAlignment="CENTER" wrapText="true" />
</content>
</TitledPane> </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"> <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"/>
<Label text="USD (29.4m)" textAlignment="CENTER" wrapText="true" />
</content>
</TitledPane> </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"> <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"/>
<Label fx:id="ourTransactionsLabel" text="In flight: 1,315" textAlignment="CENTER" wrapText="true" />
</content>
</TitledPane> </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> </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?> <?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"> <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"/>
<BorderPane fx:id="selectionBorderPane" />
</children>
</VBox> </VBox>

View File

@ -5,7 +5,9 @@ import com.r3corda.contracts.asset.InsufficientBalanceException
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.toStringShort import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.ServiceHub 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.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault import com.r3corda.core.node.services.Vault
import com.r3corda.core.transactions.SignedTransaction import com.r3corda.core.transactions.SignedTransaction
@ -33,6 +35,10 @@ class ServerRPCOps(
) : CordaRPCOps { ) : CordaRPCOps {
override val protocolVersion: Int = 0 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>> { override fun vaultAndUpdates(): Pair<List<StateAndRef<ContractState>>, Observable<Vault.Update>> {
return databaseTransaction(database) { return databaseTransaction(database) {
val (vault, updates) = services.vaultService.track() val (vault, updates) = services.vaultService.track()
@ -153,5 +159,4 @@ class ServerRPCOps(
class InputStateRefResolveFailed(stateRefs: List<StateRef>) : class InputStateRefResolveFailed(stateRefs: List<StateRef>) :
Exception("Failed to resolve input StateRefs $stateRefs") 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.ClientToServiceCommand
import com.r3corda.core.contracts.ContractState import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef 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.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId import com.r3corda.core.protocols.StateMachineRunId
@ -103,6 +105,12 @@ interface CordaRPCOps : RPCOps {
@RPCReturnsObservables @RPCReturnsObservables
fun stateMachineRecordedTransactionMapping(): Pair<List<StateMachineTransactionMapping>, Observable<StateMachineTransactionMapping>> 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. * 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. * 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.Serializer
import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output 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.contracts.asset.Cash
import com.r3corda.core.ErrorOr import com.r3corda.core.ErrorOr
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.DigitalSignature import com.r3corda.core.crypto.*
import com.r3corda.core.crypto.Party import com.r3corda.core.node.NodeInfo
import com.r3corda.core.crypto.SecureHash 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.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId 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(setOf(Unit).javaClass) // SingletonSet
register(TransactionBuildResult.ProtocolStarted::class.java) register(TransactionBuildResult.ProtocolStarted::class.java)
register(TransactionBuildResult.Failed::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. // Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway.
register(IllegalArgumentException::class.java) 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(RPCException::class.java)
register(Array<StackTraceElement>::class.java, object : Serializer<Array<StackTraceElement>>() { register(Array<StackTraceElement>::class.java, read = { kryo, input -> emptyArray() }, write = { kryo, output, o -> })
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(Collections.unmodifiableList(emptyList<String>()).javaClass) 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 val observableRegistration: Registration? = if (observableSerializer != null) register(Observable::class.java, observableSerializer) else null
override fun getRegistration(type: Class<*>): Registration { 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.annotations.VisibleForTesting
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import com.r3corda.core.bufferUntilSubscribed
import com.r3corda.core.contracts.Contract import com.r3corda.core.contracts.Contract
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.map 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.Companion.SUBSCRIPTION_PROTOCOL_TOPIC
import com.r3corda.node.services.network.NetworkMapService.FetchMapResponse import com.r3corda.node.services.network.NetworkMapService.FetchMapResponse
import com.r3corda.node.services.network.NetworkMapService.SubscribeResponse import com.r3corda.node.services.network.NetworkMapService.SubscribeResponse
import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.node.utilities.AddOrRemove import com.r3corda.node.utilities.AddOrRemove
import com.r3corda.protocols.sendRequest import com.r3corda.protocols.sendRequest
import rx.Observable import rx.Observable
@ -54,6 +56,18 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
private var registeredForPush = false private var registeredForPush = false
protected var registeredNodes = Collections.synchronizedMap(HashMap<Party, NodeInfo>()) 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() = registeredNodes.map { it.value }
override fun get(serviceType: ServiceType) = registeredNodes.filterValues { it.advertisedServices.any { it.info.type.isSubTypeOf(serviceType) } }.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() override fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = get(type).firstOrNull()
@ -96,17 +110,22 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
} }
override fun addNode(node: NodeInfo) { override fun addNode(node: NodeInfo) {
val oldValue = registeredNodes.put(node.legalIdentity, node) synchronized(_changed) {
if (oldValue == null) { val oldValue = registeredNodes.put(node.legalIdentity, node)
_changed.onNext(MapChange(node, oldValue, MapChangeType.Added)) if (oldValue == null) {
} else if(oldValue != node) { _changed.onNext(MapChange(node, oldValue, MapChangeType.Added))
_changed.onNext(MapChange(node, oldValue, MapChangeType.Modified)) } else if (oldValue != node) {
_changed.onNext(MapChange(node, oldValue, MapChangeType.Modified))
}
} }
} }
override fun removeNode(node: NodeInfo) { override fun removeNode(node: NodeInfo) {
val oldValue = registeredNodes.remove(node.legalIdentity) synchronized(_changed) {
_changed.onNext(MapChange(node, oldValue, MapChangeType.Removed)) val oldValue = registeredNodes.remove(node.legalIdentity)
_changed.onNext(MapChange(node, oldValue, MapChangeType.Removed))
}
} }
/** /**