mirror of
https://github.com/corda/corda.git
synced 2025-06-19 07:38:22 +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:
@ -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(
|
||||||
|
@ -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.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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 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?>()
|
||||||
}
|
}
|
@ -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 {
|
||||||
|
@ -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.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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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 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)
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
@ -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?>
|
<?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>
|
||||||
|
@ -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")
|
||||||
|
|
||||||
}
|
}
|
@ -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.
|
||||||
|
@ -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 {
|
||||||
|
@ -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,6 +110,7 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun addNode(node: NodeInfo) {
|
override fun addNode(node: NodeInfo) {
|
||||||
|
synchronized(_changed) {
|
||||||
val oldValue = registeredNodes.put(node.legalIdentity, node)
|
val oldValue = registeredNodes.put(node.legalIdentity, node)
|
||||||
if (oldValue == null) {
|
if (oldValue == null) {
|
||||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Added))
|
_changed.onNext(MapChange(node, oldValue, MapChangeType.Added))
|
||||||
@ -104,10 +119,14 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
override fun removeNode(node: NodeInfo) {
|
override fun removeNode(node: NodeInfo) {
|
||||||
|
synchronized(_changed) {
|
||||||
val oldValue = registeredNodes.remove(node.legalIdentity)
|
val oldValue = registeredNodes.remove(node.legalIdentity)
|
||||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Removed))
|
_changed.onNext(MapChange(node, oldValue, MapChangeType.Removed))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsubscribes from updates from the given map service.
|
* Unsubscribes from updates from the given map service.
|
||||||
|
Reference in New Issue
Block a user