New counterparty model and subscription mechanism to retrieve and track counterparty changes in network map

New transaction creation screen for creating new cash transactions, using party info source from the counterparty model.
This commit is contained in:
Patrick Kuo 2016-10-07 17:07:23 +01:00
parent d7ca215f7d
commit d4362fbd78
23 changed files with 461 additions and 71 deletions

View File

@ -6,6 +6,7 @@ import com.r3corda.client.model.ProgressTrackingEvent
import com.r3corda.core.bufferUntilSubscribed
import com.r3corda.core.contracts.*
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.services.NetworkMapCache
import com.r3corda.core.node.services.ServiceInfo
import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault
@ -17,8 +18,12 @@ import com.r3corda.node.driver.startClient
import com.r3corda.node.services.messaging.NodeMessagingClient
import com.r3corda.node.services.messaging.StateMachineUpdate
import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.testing.*
import org.junit.*
import com.r3corda.testing.expect
import com.r3corda.testing.expectEvents
import com.r3corda.testing.sequence
import org.junit.After
import org.junit.Before
import org.junit.Test
import rx.Observable
import rx.Observer
import kotlin.concurrent.thread
@ -37,7 +42,9 @@ class NodeMonitorModelTest {
lateinit var progressTracking: Observable<ProgressTrackingEvent>
lateinit var transactions: Observable<SignedTransaction>
lateinit var vaultUpdates: Observable<Vault.Update>
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
lateinit var clientToService: Observer<ClientToServiceCommand>
lateinit var newNode: (String) -> NodeInfo
@Before
fun start() {
@ -49,7 +56,7 @@ class NodeMonitorModelTest {
aliceNode = aliceNodeFuture.get()
notaryNode = notaryNodeFuture.get()
aliceClient = startClient(aliceNode).get()
newNode = { nodeName -> startNode(nodeName).get() }
val monitor = NodeMonitorModel()
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
@ -57,11 +64,13 @@ class NodeMonitorModelTest {
progressTracking = monitor.progressTracking.bufferUntilSubscribed()
transactions = monitor.transactions.bufferUntilSubscribed()
vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed()
networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
clientToService = monitor.clientToService
monitor.register(aliceNode, aliceClient.config.certificatesPath)
driverStarted.set(Unit)
stopDriver.get()
}
driverStopped.set(Unit)
}
@ -74,6 +83,26 @@ class NodeMonitorModelTest {
driverStopped.get()
}
@Test
fun testNetworkMapUpdate() {
newNode("Bob")
newNode("Charlie")
networkMapUpdates.expectEvents(isStrict = false) {
sequence(
// TODO : Add test for remove when driver DSL support individual node shutdown.
expect { output: NetworkMapCache.MapChange ->
require(output.node.legalIdentity.name == "Alice") { output.node.legalIdentity.name }
},
expect { output: NetworkMapCache.MapChange ->
require(output.node.legalIdentity.name == "Bob") { output.node.legalIdentity.name }
},
expect { output: NetworkMapCache.MapChange ->
require(output.node.legalIdentity.name == "Charlie") { output.node.legalIdentity.name }
}
)
}
}
@Test
fun cashIssueWorksEndToEnd() {
clientToService.onNext(ClientToServiceCommand.IssueCash(

View File

@ -0,0 +1,24 @@
package com.r3corda.client.model
import com.r3corda.client.fxutils.foldToObservableList
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.services.NetworkMapCache
import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.removeIf
import rx.Observable
class NetworkIdentityModel {
private val networkIdentityObservable: Observable<NetworkMapCache.MapChange> by observable(NodeMonitorModel::networkMap)
val networkIdentities: ObservableList<NodeInfo> =
networkIdentityObservable.foldToObservableList(Unit) { update, _accumulator, observableList ->
observableList.removeIf {
when (update.type) {
NetworkMapCache.MapChangeType.Removed -> it == update.node
NetworkMapCache.MapChangeType.Modified -> it == update.prevNodeInfo
else -> false
}
}
observableList.addAll(update.node)
}
}

View File

@ -3,13 +3,16 @@ package com.r3corda.client.model
import com.r3corda.client.CordaRPCClient
import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.services.NetworkMapCache
import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.node.services.messaging.ArtemisMessagingComponent
import com.r3corda.node.services.messaging.CordaRPCOps
import com.r3corda.node.services.messaging.StateMachineInfo
import com.r3corda.node.services.messaging.StateMachineUpdate
import javafx.beans.property.SimpleObjectProperty
import rx.Observable
import rx.subjects.PublishSubject
import java.nio.file.Path
@ -35,16 +38,20 @@ class NodeMonitorModel {
private val transactionsSubject = PublishSubject.create<SignedTransaction>()
private val stateMachineTransactionMappingSubject = PublishSubject.create<StateMachineTransactionMapping>()
private val progressTrackingSubject = PublishSubject.create<ProgressTrackingEvent>()
private val networkMapSubject = PublishSubject.create<NetworkMapCache.MapChange>()
val stateMachineUpdates: Observable<StateMachineUpdate> = stateMachineUpdatesSubject
val vaultUpdates: Observable<Vault.Update> = vaultUpdatesSubject
val transactions: Observable<SignedTransaction> = transactionsSubject
val stateMachineTransactionMapping: Observable<StateMachineTransactionMapping> = stateMachineTransactionMappingSubject
val progressTracking: Observable<ProgressTrackingEvent> = progressTrackingSubject
val networkMap: Observable<NetworkMapCache.MapChange> = networkMapSubject
private val clientToServiceSource = PublishSubject.create<ClientToServiceCommand>()
val clientToService: PublishSubject<ClientToServiceCommand> = clientToServiceSource
val proxyObservable = SimpleObjectProperty<CordaRPCOps?>()
/**
* Register for updates to/from a given vault.
* @param messagingService The messaging to use for communication.
@ -89,9 +96,15 @@ class NodeMonitorModel {
val (smTxMappings, futureSmTxMappings) = proxy.stateMachineRecordedTransactionMapping()
futureSmTxMappings.startWith(smTxMappings).subscribe(stateMachineTransactionMappingSubject)
// Parties on network
val (parties, futurePartyUpdate) = proxy.networkMapUpdates()
futurePartyUpdate.startWith(parties.map { NetworkMapCache.MapChange(it, null, NetworkMapCache.MapChangeType.Added) }).subscribe(networkMapSubject)
// Client -> Service
clientToServiceSource.subscribe {
proxy.executeCommand(it)
}
proxyObservable.set(proxy)
}
}
}

View File

@ -466,4 +466,4 @@ interface Attachment : NamedByHash {
}
throw FileNotFoundException()
}
}
}

View File

@ -20,4 +20,4 @@ data class NodeInfo(val address: SingleMessageRecipient,
val physicalLocation: PhysicalLocation? = null) {
val notaryIdentity: Party get() = advertisedServices.single { it.info.type.isNotary() }.identity
fun serviceIdentities(type: ServiceType): List<Party> = advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity }
}
}

View File

@ -8,8 +8,8 @@ import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.messaging.SingleMessageRecipient
import com.r3corda.core.node.NodeInfo
import org.slf4j.LoggerFactory
import java.security.PublicKey
import rx.Observable
import java.security.PublicKey
/**
* A network map contains lists of nodes on the network along with information about their identity keys, services
@ -23,7 +23,7 @@ interface NetworkMapCache {
}
enum class MapChangeType { Added, Removed, Modified }
data class MapChange(val node: NodeInfo, val prevNodeInfo: NodeInfo?, val type: MapChangeType )
data class MapChange(val node: NodeInfo, val prevNodeInfo: NodeInfo?, val type: MapChangeType)
/** A list of nodes that advertise a network map service */
val networkMapNodes: List<NodeInfo>
@ -43,6 +43,12 @@ interface NetworkMapCache {
*/
val regulators: List<NodeInfo>
/**
* Atomically get the current party nodes and a stream of updates. Note that the Observable buffers updates until the
* first subscriber is registered so as to avoid racing with early updates.
*/
fun track(): Pair<List<NodeInfo>, Observable<MapChange>>
/**
* Get a copy of all nodes in the map.
*/

View File

@ -1,10 +1,7 @@
package com.r3corda.explorer
import com.r3corda.client.mock.EventGenerator
import com.r3corda.client.model.Models
import com.r3corda.client.model.NodeMonitorModel
import com.r3corda.client.model.subject
import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.node.services.ServiceInfo
import com.r3corda.explorer.model.IdentityModel
import com.r3corda.node.driver.PortAllocation
@ -12,13 +9,10 @@ import com.r3corda.node.driver.driver
import com.r3corda.node.driver.startClient
import com.r3corda.node.services.transactions.SimpleNotaryService
import javafx.stage.Stage
import rx.subjects.Subject
import tornadofx.App
import java.util.*
class Main : App() {
override val primaryView = MainWindow::class
val aliceOutStream: Subject<ClientToServiceCommand, ClientToServiceCommand> by subject(NodeMonitorModel::clientToService)
override fun start(stage: Stage) {
@ -32,7 +26,6 @@ class Main : App() {
// start the driver on another thread
// TODO Change this to connecting to an actual node (specified on cli/in a config) once we're happy with the code
Thread({
val portAllocation = PortAllocation.Incremental(20000)
driver(portAllocation = portAllocation) {
@ -44,10 +37,13 @@ class Main : App() {
val aliceClient = startClient(aliceNode).get()
Models.get<IdentityModel>(Main::class).notary.set(notaryNode.notaryIdentity)
Models.get<IdentityModel>(Main::class).myIdentity.set(aliceNode.legalIdentity)
Models.get<NodeMonitorModel>(Main::class).register(aliceNode, aliceClient.config.certificatesPath)
for (i in 0 .. 10000) {
startNode("Bob").get()
/* for (i in 0 .. 10000) {
Thread.sleep(500)
val eventGenerator = EventGenerator(
@ -58,11 +54,9 @@ class Main : App() {
eventGenerator.clientToServiceCommandGenerator.map { command ->
aliceOutStream.onNext(command)
}.generate(Random())
}
}*/
waitForAllNodesToFinish()
}
}).start()
}
}
}

View File

@ -0,0 +1,44 @@
package com.r3corda.explorer.components
import javafx.scene.control.Alert
import javafx.scene.control.Label
import javafx.scene.control.TextArea
import javafx.scene.layout.GridPane
import javafx.scene.layout.Priority
import java.io.PrintWriter
import java.io.StringWriter
class ExceptionDialog(ex: Throwable) : Alert(AlertType.ERROR) {
private fun Throwable.toExceptionText(): String {
return StringWriter().use {
PrintWriter(it).use {
this.printStackTrace(it)
}
it.toString()
}
}
init {
// Create expandable Exception.
val label = Label("The exception stacktrace was:")
contentText = ex.message
val textArea = TextArea(ex.toExceptionText())
textArea.isEditable = false
textArea.isWrapText = true
textArea.maxWidth = Double.MAX_VALUE
textArea.maxHeight = Double.MAX_VALUE
GridPane.setVgrow(textArea, Priority.ALWAYS)
GridPane.setHgrow(textArea, Priority.ALWAYS)
val expContent = GridPane()
expContent.maxWidth = Double.MAX_VALUE
expContent.add(label, 0, 0)
expContent.add(textArea, 0, 1)
// Set expandable Exception into the dialog pane.
dialogPane.expandableContent = expContent
}
}

View File

@ -3,7 +3,7 @@ package com.r3corda.explorer.model
import com.r3corda.core.crypto.Party
import javafx.beans.property.SimpleObjectProperty
class IdentityModel {
val myIdentity = SimpleObjectProperty<Party>()
}
val myIdentity = SimpleObjectProperty<Party?>()
val notary = SimpleObjectProperty<Party?>()
}

View File

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

View File

@ -0,0 +1,7 @@
package com.r3corda.explorer.model
enum class CashTransaction(val partyNameA: String, val partyNameB: String?) {
Issue("Issuer Bank", "Receiver Bank"),
Pay("Payer", "Payee"),
Exit("Issuer Bank", null);
}

View File

@ -33,6 +33,7 @@ class Header : View() {
SelectedView.Home -> "Home"
SelectedView.Cash -> "Cash"
SelectedView.Transaction -> "Transactions"
SelectedView.NewTransaction -> "New Transaction"
null -> "Home"
}
})
@ -42,6 +43,7 @@ class Header : View() {
SelectedView.Home -> homeImage
SelectedView.Cash -> cashImage
SelectedView.Transaction -> transactionImage
SelectedView.NewTransaction -> cashImage
null -> homeImage
}
})
@ -60,4 +62,4 @@ class Header : View() {
sectionIcon.fitWidthProperty().bind(secionLabelHeightNonZero)
sectionIcon.fitHeightProperty().bind(sectionIcon.fitWidthProperty())
}
}
}

View File

@ -18,11 +18,9 @@ import javafx.scene.control.Label
import javafx.scene.control.TitledPane
import javafx.scene.input.MouseButton
import javafx.scene.layout.TilePane
import org.fxmisc.easybind.EasyBind
import tornadofx.View
import java.util.*
class Home : View() {
override val root: TilePane by fxml()
@ -32,6 +30,8 @@ class Home : View() {
private val ourTransactionsPane: TitledPane by fxid()
private val ourTransactionsLabel: Label by fxid()
private val newTransaction: TitledPane by fxid()
private val selectedView: WritableValue<SelectedView> by writableValue(TopLevelModel::selectedView)
private val cashStates: ObservableList<StateAndRef<Cash.State>> by observableList(ContractStateModel::cashStates)
private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData>
@ -63,6 +63,11 @@ class Home : View() {
selectedView.value = SelectedView.Transaction
}
}
newTransaction.setOnMouseClicked { clickEvent ->
if (clickEvent.button == MouseButton.PRIMARY) {
selectedView.value = SelectedView.NewTransaction
}
}
}
}
}

View File

@ -0,0 +1,177 @@
package com.r3corda.explorer.views
import com.r3corda.client.fxutils.map
import com.r3corda.client.model.NetworkIdentityModel
import com.r3corda.client.model.NodeMonitorModel
import com.r3corda.client.model.observableList
import com.r3corda.client.model.observableValue
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.explorer.components.ExceptionDialog
import com.r3corda.explorer.model.CashTransaction
import com.r3corda.explorer.model.IdentityModel
import com.r3corda.node.services.messaging.CordaRPCOps
import com.r3corda.node.services.messaging.TransactionBuildResult
import javafx.beans.binding.Bindings
import javafx.beans.binding.BooleanBinding
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.scene.Node
import javafx.scene.Parent
import javafx.scene.control.*
import javafx.util.StringConverter
import javafx.util.converter.BigDecimalStringConverter
import tornadofx.View
import java.math.BigDecimal
import java.util.*
import java.util.regex.Pattern
class NewTransaction : View() {
override val root: Parent by fxml()
private val partyATextField: TextField by fxid()
private val partyBChoiceBox: ChoiceBox<NodeInfo> by fxid()
private val partyALabel: Label by fxid()
private val partyBLabel: Label by fxid()
private val amountLabel: Label by fxid()
private val executeButton: Button by fxid()
private val transactionTypeCB: ChoiceBox<CashTransaction> by fxid()
private val amount: TextField by fxid()
private val currency: ChoiceBox<Currency> by fxid()
private val networkIdentities: ObservableList<NodeInfo> by observableList(NetworkIdentityModel::networkIdentities)
private val rpcProxy: ObservableValue<CordaRPCOps?> by observableValue(NodeMonitorModel::proxyObservable)
private val myIdentity: ObservableValue<Party?> by observableValue(IdentityModel::myIdentity)
private val notary: ObservableValue<Party?> by observableValue(IdentityModel::notary)
private val issueRefLabel: Label by fxid()
private val issueRefTextField: TextField by fxid()
private fun ObservableValue<*>.isNotNull(): BooleanBinding {
return Bindings.createBooleanBinding({ this.value != null }, arrayOf(this))
}
fun resetScreen() {
partyBChoiceBox.valueProperty().set(null)
transactionTypeCB.valueProperty().set(null)
currency.valueProperty().set(null)
amount.clear()
}
init {
// Disable everything when not connected to node.
val enableProperty = myIdentity.isNotNull().and(notary.isNotNull()).and(rpcProxy.isNotNull())
root.disableProperty().bind(enableProperty.not())
transactionTypeCB.items = FXCollections.observableArrayList(CashTransaction.values().asList())
// Party A textfield always display my identity name, not editable.
partyATextField.isEditable = false
partyATextField.textProperty().bind(myIdentity.map { it?.name ?: "" })
partyALabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA?.let { "$it : " } })
partyATextField.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA }.isNotNull())
partyBLabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB?.let { "$it : " } })
partyBChoiceBox.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB }.isNotNull())
partyBChoiceBox.items = networkIdentities
partyBChoiceBox.converter = object : StringConverter<NodeInfo?>() {
override fun toString(node: NodeInfo?): String {
return node?.legalIdentity?.name ?: ""
}
override fun fromString(string: String?): NodeInfo {
throw UnsupportedOperationException("not implemented")
}
}
// BigDecimal text Formatter, restricting text box input to decimal values.
val textFormatter = Pattern.compile("-?((\\d*)|(\\d+\\.\\d*))").run {
TextFormatter<BigDecimal>(BigDecimalStringConverter(), null) { change ->
val newText = change.controlNewText
if (matcher(newText).matches()) change else null
}
}
amount.textFormatter = textFormatter
// Hide currency and amount fields when transaction type is not specified.
// TODO : Create a currency model to store these values
currency.items = FXCollections.observableList(setOf(USD, GBP, CHF).toList())
currency.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
amount.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
amountLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
issueRefLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
issueRefTextField.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
// Validate inputs.
val formValidCondition = arrayOf(
myIdentity.isNotNull(),
transactionTypeCB.valueProperty().isNotNull,
partyBChoiceBox.visibleProperty().not().or(partyBChoiceBox.valueProperty().isNotNull),
textFormatter.valueProperty().isNotNull,
textFormatter.valueProperty().isNotEqualTo(BigDecimal.ZERO),
currency.valueProperty().isNotNull
).reduce(BooleanBinding::and)
// Enable execute button when form is valid.
executeButton.disableProperty().bind(formValidCondition.not())
executeButton.setOnAction { event ->
// Null checks to ensure these observable values are set, execute button should be disabled if any of these value are null, this extra checks are for precaution and getting non-nullable values without using !!.
myIdentity.value?.let { myIdentity ->
notary.value?.let { notary ->
rpcProxy.value?.let { rpcProxy ->
Triple(myIdentity, notary, rpcProxy)
}
}
}?.let {
val (myIdentity, notary, rpcProxy) = it
transactionTypeCB.value?.let {
val issueRef = OpaqueBytes(if (issueRefTextField.text.trim().isNotBlank()) issueRefTextField.text.toByteArray() else ByteArray(1, { 1 }))
val command = when (it) {
CashTransaction.Issue -> ClientToServiceCommand.IssueCash(Amount(textFormatter.value, currency.value), issueRef, partyBChoiceBox.value.legalIdentity, notary)
CashTransaction.Pay -> ClientToServiceCommand.PayCash(Amount(textFormatter.value, Issued(PartyAndReference(myIdentity, issueRef), currency.value)), partyBChoiceBox.value.legalIdentity)
CashTransaction.Exit -> ClientToServiceCommand.ExitCash(Amount(textFormatter.value, currency.value), issueRef)
}
val dialog = Alert(Alert.AlertType.INFORMATION).apply {
headerText = null
contentText = "Transaction Started."
dialogPane.isDisable = true
initOwner((event.target as Node).scene.window)
}
dialog.show()
runAsync {
rpcProxy.executeCommand(command)
}.ui {
dialog.contentText = when (it) {
is TransactionBuildResult.ProtocolStarted -> {
dialog.alertType = Alert.AlertType.INFORMATION
dialog.setOnCloseRequest { resetScreen() }
"Transaction Started \nTransaction ID : ${it.transaction?.id} \nMessage : ${it.message}"
}
is TransactionBuildResult.Failed -> {
dialog.alertType = Alert.AlertType.ERROR
it.toString()
}
}
dialog.dialogPane.isDisable = false
dialog.dialogPane.scene.window.sizeToScene()
}.setOnFailed {
dialog.close()
ExceptionDialog(it.source.exception).apply {
initOwner((event.target as Node).scene.window)
showAndWait()
}
}
}
}
}
// Remove focus from textfield when click on the blank area.
root.setOnMouseClicked { e -> root.requestFocus() }
}
}

View File

@ -20,17 +20,20 @@ class TopLevel : View() {
private val home: Home by inject()
private val cash: CashViewer by inject()
private val transaction: TransactionViewer by inject()
private val newTransaction: NewTransaction by inject()
// Note: this is weirdly very important, as it forces the initialisation of Views. Therefore this is the entry
// point to the top level observable/stream wiring! Any events sent before this init may be lost!
private val homeRoot = home.root
private val cashRoot = cash.root
private val transactionRoot = transaction.root
private val newTransactionRoot = newTransaction.root
private fun getView(selection: SelectedView) = when (selection) {
SelectedView.Home -> homeRoot
SelectedView.Cash -> cashRoot
SelectedView.Transaction -> transactionRoot
SelectedView.NewTransaction -> newTransactionRoot
}
val selectedView: ObjectProperty<SelectedView> by objectProperty(TopLevelModel::selectedView)
@ -46,4 +49,4 @@ class TopLevel : View() {
root.children.add(0, header.root)
}
}
}

View File

@ -79,7 +79,7 @@ class TransactionViewer: View() {
by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
private val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>>
by observableValue(ReportingCurrencyModel::reportingExchange)
private val myIdentity: ObservableValue<Party> by observableValue(IdentityModel::myIdentity)
private val myIdentity: ObservableValue<Party?> by observableValue(IdentityModel::myIdentity)
/**
* This is what holds data for a single transaction node. Note how a lot of these are nullable as we often simply don't
@ -363,7 +363,7 @@ class TransactionViewer: View() {
* We calculate the total value by subtracting relevant input states and adding relevant output states, as long as they're cash
*/
private fun calculateTotalEquiv(
identity: Party,
identity: Party?,
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
inputs: List<StateAndRef<ContractState>>?,
outputs: List<TransactionState<ContractState>>): AmountDiff<Currency>? {
@ -372,7 +372,7 @@ private fun calculateTotalEquiv(
}
var sum = 0L
val (reportingCurrency, exchange) = reportingCurrencyExchange
val publicKey = identity.owningKey
val publicKey = identity?.owningKey
inputs.forEach {
val contractState = it.state.data
if (contractState is Cash.State && publicKey == contractState.owner) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,13 +5,17 @@ import com.esotericsoftware.kryo.Registration
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.serializers.DefaultSerializers
import com.esotericsoftware.kryo.serializers.JavaSerializer
import com.google.common.net.HostAndPort
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.ErrorOr
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.*
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.PhysicalLocation
import com.r3corda.core.node.ServiceEntry
import com.r3corda.core.node.services.NetworkMapCache
import com.r3corda.core.node.services.ServiceInfo
import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId
@ -163,17 +167,42 @@ private class RPCKryo(private val observableSerializer: Serializer<Observable<An
register(setOf(Unit).javaClass) // SingletonSet
register(TransactionBuildResult.ProtocolStarted::class.java)
register(TransactionBuildResult.Failed::class.java)
register(ServiceEntry::class.java)
register(NodeInfo::class.java)
register(PhysicalLocation::class.java)
register(NetworkMapCache.MapChange::class.java)
register(NetworkMapCache.MapChangeType::class.java)
register(ArtemisMessagingComponent.NodeAddress::class.java,
read = { kryo, input -> ArtemisMessagingComponent.NodeAddress(parsePublicKeyBase58(kryo.readObject(input, String::class.java)), kryo.readObject(input, HostAndPort::class.java)) },
write = { kryo, output, nodeAddress ->
kryo.writeObject(output, nodeAddress.identity.toBase58String())
kryo.writeObject(output, nodeAddress.hostAndPort)
}
)
register(HostAndPort::class.java)
register(ServiceInfo::class.java, read = { kryo, input -> ServiceInfo.parse(input.readString()) }, write = Kryo::writeObject)
// Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway.
register(IllegalArgumentException::class.java)
// Kryo couldn't serialize Collections.unmodifiableCollection in Throwable correctly, causing null pointer exception when try to access the deserialize object.
register(NoSuchElementException::class.java, JavaSerializer())
register(RPCException::class.java)
register(Array<StackTraceElement>::class.java, object : Serializer<Array<StackTraceElement>>() {
override fun read(kryo: Kryo, input: Input, type: Class<Array<StackTraceElement>>): Array<StackTraceElement> = emptyArray()
override fun write(kryo: Kryo, output: Output, `object`: Array<StackTraceElement>) {}
})
register(Array<StackTraceElement>::class.java, read = { kryo, input -> emptyArray() }, write = { kryo, output, o -> })
register(Collections.unmodifiableList(emptyList<String>()).javaClass)
}
// Helper method, attempt to reduce boiler plate code
private fun <T> register(type: Class<T>, read: (Kryo, Input) -> T, write: (Kryo, Output, T) -> Unit) {
register(type, object : Serializer<T>() {
override fun read(kryo: Kryo, input: Input, type: Class<T>?): T {
return read(kryo, input)
}
override fun write(kryo: Kryo, output: Output, o: T) {
write(kryo, output, o)
}
})
}
val observableRegistration: Registration? = if (observableSerializer != null) register(Observable::class.java, observableSerializer) else null
override fun getRegistration(type: Class<*>): Registration {

View File

@ -3,6 +3,7 @@ package com.r3corda.node.services.network
import com.google.common.annotations.VisibleForTesting
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import com.r3corda.core.bufferUntilSubscribed
import com.r3corda.core.contracts.Contract
import com.r3corda.core.crypto.Party
import com.r3corda.core.map
@ -23,6 +24,7 @@ import com.r3corda.node.services.network.NetworkMapService.Companion.FETCH_PROTO
import com.r3corda.node.services.network.NetworkMapService.Companion.SUBSCRIPTION_PROTOCOL_TOPIC
import com.r3corda.node.services.network.NetworkMapService.FetchMapResponse
import com.r3corda.node.services.network.NetworkMapService.SubscribeResponse
import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.node.utilities.AddOrRemove
import com.r3corda.protocols.sendRequest
import rx.Observable
@ -54,6 +56,18 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
private var registeredForPush = false
protected var registeredNodes = Collections.synchronizedMap(HashMap<Party, NodeInfo>())
override fun track(): Pair<List<NodeInfo>, Observable<MapChange>> {
synchronized(_changed) {
fun NodeInfo.isCordaService(): Boolean {
return advertisedServices.any { it.info.type in setOf(SimpleNotaryService.type, NetworkMapService.type) }
}
val currentParties = partyNodes.filter { !it.isCordaService() }
val changes = changed.filter { !it.node.isCordaService() }
return Pair(currentParties, changes.bufferUntilSubscribed())
}
}
override fun get() = registeredNodes.map { it.value }
override fun get(serviceType: ServiceType) = registeredNodes.filterValues { it.advertisedServices.any { it.info.type.isSubTypeOf(serviceType) } }.map { it.value }
override fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = get(type).firstOrNull()
@ -96,17 +110,22 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
}
override fun addNode(node: NodeInfo) {
val oldValue = registeredNodes.put(node.legalIdentity, node)
if (oldValue == null) {
_changed.onNext(MapChange(node, oldValue, MapChangeType.Added))
} else if(oldValue != node) {
_changed.onNext(MapChange(node, oldValue, MapChangeType.Modified))
synchronized(_changed) {
val oldValue = registeredNodes.put(node.legalIdentity, node)
if (oldValue == null) {
_changed.onNext(MapChange(node, oldValue, MapChangeType.Added))
} else if (oldValue != node) {
_changed.onNext(MapChange(node, oldValue, MapChangeType.Modified))
}
}
}
override fun removeNode(node: NodeInfo) {
val oldValue = registeredNodes.remove(node.legalIdentity)
_changed.onNext(MapChange(node, oldValue, MapChangeType.Removed))
synchronized(_changed) {
val oldValue = registeredNodes.remove(node.legalIdentity)
_changed.onNext(MapChange(node, oldValue, MapChangeType.Removed))
}
}
/**