mirror of
https://github.com/corda/corda.git
synced 2024-12-22 06:17:55 +00:00
Merged pat-cash-creation-ui into master
This commit is contained in:
commit
3403d50168
@ -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)
|
||||
}
|
||||
}
|
@ -8,8 +8,8 @@ import com.r3corda.core.messaging.MessagingService
|
||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.security.PublicKey
|
||||
import rx.Observable
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* A network map contains lists of nodes on the network along with information about their identity keys, services
|
||||
@ -43,6 +43,12 @@ interface NetworkMapCache {
|
||||
*/
|
||||
val regulators: List<NodeInfo>
|
||||
|
||||
/**
|
||||
* Atomically get the current party nodes and a stream of updates. Note that the Observable buffers updates until the
|
||||
* first subscriber is registered so as to avoid racing with early updates.
|
||||
*/
|
||||
fun track(): Pair<List<NodeInfo>, Observable<MapChange>>
|
||||
|
||||
/**
|
||||
* Get a copy of all nodes in the map.
|
||||
*/
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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>
|
||||
</TitledPane>
|
||||
<TitledPane id="tile_debtors" fx:id="ourDebtorsPane" alignment="CENTER" collapsible="false" layoutX="232.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our debtors">
|
||||
<content>
|
||||
<Label text="USD 71.3m" textAlignment="CENTER" wrapText="true"/>
|
||||
</content>
|
||||
</TitledPane>
|
||||
<TitledPane id="tile_creditors" fx:id="ourCreditorsPane" alignment="CENTER" collapsible="false" layoutX="312.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our creditors">
|
||||
<content>
|
||||
<Label text="USD (29.4m)" textAlignment="CENTER" wrapText="true"/>
|
||||
</content>
|
||||
</TitledPane>
|
||||
<TitledPane id="tile_tx" fx:id="ourTransactionsPane" alignment="CENTER" collapsible="false" layoutX="392.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our transactions">
|
||||
<content>
|
||||
<Label fx:id="ourTransactionsLabel" text="In flight: 1,315" textAlignment="CENTER" wrapText="true"/>
|
||||
</content>
|
||||
</TitledPane>
|
||||
</children>
|
||||
<TitledPane id="tile_new_tx" fx:id="newTransaction" alignment="CENTER" collapsible="false" layoutX="472.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="New Transaction">
|
||||
</TitledPane>
|
||||
</TilePane>
|
||||
|
@ -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>
|
||||
</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,6 +105,12 @@ interface CordaRPCOps : RPCOps {
|
||||
@RPCReturnsObservables
|
||||
fun stateMachineRecordedTransactionMapping(): Pair<List<StateMachineTransactionMapping>, Observable<StateMachineTransactionMapping>>
|
||||
|
||||
/**
|
||||
* Returns all parties currently visible on the network with their advertised services and an observable of future updates to the network.
|
||||
*/
|
||||
@RPCReturnsObservables
|
||||
fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>>
|
||||
|
||||
/**
|
||||
* Executes the given command, possibly triggering cash creation etc.
|
||||
* TODO: The signature of this is weird because it's the remains of an old service call, we should have a call for each command instead.
|
||||
|
@ -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,6 +110,7 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
|
||||
}
|
||||
|
||||
override fun addNode(node: NodeInfo) {
|
||||
synchronized(_changed) {
|
||||
val oldValue = registeredNodes.put(node.legalIdentity, node)
|
||||
if (oldValue == null) {
|
||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Added))
|
||||
@ -104,10 +119,14 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun removeNode(node: NodeInfo) {
|
||||
synchronized(_changed) {
|
||||
val oldValue = registeredNodes.remove(node.legalIdentity)
|
||||
_changed.onNext(MapChange(node, oldValue, MapChangeType.Removed))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from updates from the given map service.
|
||||
|
Loading…
Reference in New Issue
Block a user