mirror of
https://github.com/corda/corda.git
synced 2025-04-07 11:27:01 +00:00
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:
parent
d7ca215f7d
commit
d4362fbd78
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -466,4 +466,4 @@ interface Attachment : NamedByHash {
|
||||
}
|
||||
throw FileNotFoundException()
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Party> = advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity }
|
||||
}
|
||||
}
|
@ -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<NodeInfo>
|
||||
@ -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.
|
||||
*/
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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?>()
|
||||
}
|
@ -5,7 +5,8 @@ import javafx.beans.property.SimpleObjectProperty
|
||||
enum class SelectedView {
|
||||
Home,
|
||||
Cash,
|
||||
Transaction
|
||||
Transaction,
|
||||
NewTransaction
|
||||
}
|
||||
|
||||
class TopLevelModel {
|
||||
|
@ -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);
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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() }
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||
@ -46,4 +49,4 @@ class TopLevel : View() {
|
||||
|
||||
root.children.add(0, header.root)
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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>
|
||||
<Label fx:id="ourCashLabel" text="USD 186.7m" textAlignment="CENTER" wrapText="true"/>
|
||||
</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>
|
||||
<Label text="USD 71.3m" textAlignment="CENTER" wrapText="true"/>
|
||||
</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>
|
||||
<Label text="USD (29.4m)" textAlignment="CENTER" wrapText="true"/>
|
||||
</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>
|
||||
<Label fx:id="ourTransactionsLabel" text="In flight: 1,315" textAlignment="CENTER" wrapText="true"/>
|
||||
</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>
|
||||
|
@ -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>
|
@ -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>
|
||||
<BorderPane fx:id="selectionBorderPane"/>
|
||||
</VBox>
|
||||
|
@ -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")
|
||||
|
||||
}
|
||||
}
|
@ -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,9 +105,15 @@ 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.
|
||||
*/
|
||||
fun executeCommand(command: ClientToServiceCommand): TransactionBuildResult
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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,17 +110,22 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
|
||||
}
|
||||
|
||||
override fun addNode(node: NodeInfo) {
|
||||
val oldValue = registeredNodes.put(node.legalIdentity, node)
|
||||
if (oldValue == null) {
|
||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Added))
|
||||
} else if(oldValue != node) {
|
||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Modified))
|
||||
synchronized(_changed) {
|
||||
val oldValue = registeredNodes.put(node.legalIdentity, node)
|
||||
if (oldValue == null) {
|
||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Added))
|
||||
} else if (oldValue != node) {
|
||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Modified))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun removeNode(node: NodeInfo) {
|
||||
val oldValue = registeredNodes.remove(node.legalIdentity)
|
||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Removed))
|
||||
synchronized(_changed) {
|
||||
val oldValue = registeredNodes.remove(node.legalIdentity)
|
||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Removed))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user