Add PartiallyResolvedTransaction to client

This commit is contained in:
Andras Slemmer 2016-09-23 16:44:33 +01:00
parent adf0876b18
commit 43d18d46bb
9 changed files with 292 additions and 100 deletions

View File

@ -105,9 +105,9 @@ class NodeMonitorClientTests {
} }
), ),
expect { tx: ServiceToClientEvent.Transaction -> expect { tx: ServiceToClientEvent.Transaction ->
require(tx.transaction.inputs.isEmpty()) require(tx.transaction.tx.inputs.isEmpty())
require(tx.transaction.outputs.size == 1) require(tx.transaction.tx.outputs.size == 1)
val signaturePubKeys = tx.transaction.mustSign.toSet() val signaturePubKeys = tx.transaction.sigs.map { it.by }.toSet()
// Only Alice signed // Only Alice signed
require(signaturePubKeys.size == 1) require(signaturePubKeys.size == 1)
require(signaturePubKeys.contains(aliceNode.identity.owningKey)) require(signaturePubKeys.contains(aliceNode.identity.owningKey))
@ -137,9 +137,9 @@ class NodeMonitorClientTests {
} }
), ),
expect { tx: ServiceToClientEvent.Transaction -> expect { tx: ServiceToClientEvent.Transaction ->
require(tx.transaction.inputs.size == 1) require(tx.transaction.tx.inputs.size == 1)
require(tx.transaction.outputs.size == 1) require(tx.transaction.tx.outputs.size == 1)
val signaturePubKeys = tx.transaction.mustSign.toSet() val signaturePubKeys = tx.transaction.sigs.map { it.by }.toSet()
// Alice and Notary signed // Alice and Notary signed
require(signaturePubKeys.size == 2) require(signaturePubKeys.size == 2)
require(signaturePubKeys.contains(aliceNode.identity.owningKey)) require(signaturePubKeys.contains(aliceNode.identity.owningKey))

View File

@ -0,0 +1,22 @@
package com.r3corda.client.fxutils
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue
import javafx.collections.MapChangeListener
import javafx.collections.ObservableMap
fun <K, V> ObservableMap<K, V>.getObservableValue(key: K): ObservableValue<V?> {
val property = SimpleObjectProperty(get(key))
addListener { change: MapChangeListener.Change<out K, out V> ->
if (change.key == key) {
// This is true both when a fresh element was inserted and when an existing was updated
if (change.wasAdded()) {
property.set(change.valueAdded)
} else if (change.wasRemoved()) {
property.set(null)
}
}
}
return property
}

View File

@ -3,6 +3,7 @@ package com.r3corda.client.fxutils
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.beans.property.ReadOnlyObjectWrapper import javafx.beans.property.ReadOnlyObjectWrapper
import javafx.beans.value.ObservableValue import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import javafx.collections.ObservableList import javafx.collections.ObservableList
import javafx.collections.transformation.FilteredList import javafx.collections.transformation.FilteredList
import org.fxmisc.easybind.EasyBind import org.fxmisc.easybind.EasyBind
@ -58,8 +59,13 @@ fun <A, B, C, D, R> ((A, B, C, D) -> R).lift(
* val person: ObservableValue<Person> = (..) * val person: ObservableValue<Person> = (..)
* val personHeight: ObservableValue<Long> = person.bind { it.height } * val personHeight: ObservableValue<Long> = person.bind { it.height }
*/ */
fun <A, B> ObservableValue<out A>.bind(function: (A) -> ObservableValue<out B>): ObservableValue<out B> = fun <A, B> ObservableValue<out A>.bind(function: (A) -> ObservableValue<B>): ObservableValue<B> =
// We cast here to enforce variance, flatMap should be covariant EasyBind.monadic(this).flatMap(function)
/**
* A variant of [bind] that has out variance on the output type. This is sometimes useful when kotlin is too eager to
* propagate variance constraints and type inference fails.
*/
fun <A, B> ObservableValue<out A>.bindOut(function: (A) -> ObservableValue<out B>): ObservableValue<out B> =
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
EasyBind.monadic(this).flatMap(function as (A) -> ObservableValue<B>) EasyBind.monadic(this).flatMap(function as (A) -> ObservableValue<B>)
@ -71,7 +77,7 @@ fun <A, B> ObservableValue<out A>.bind(function: (A) -> ObservableValue<out B>):
* *
* val filteredPeople: ObservableList<Person> = people.filter(filterCriterion.map(filterFunction)) * val filteredPeople: ObservableList<Person> = people.filter(filterCriterion.map(filterFunction))
*/ */
fun <A> ObservableList<out A>.filter(predicate: ObservableValue<out (A) -> Boolean>): ObservableList<out A> { fun <A> ObservableList<out A>.filter(predicate: ObservableValue<(A) -> Boolean>): ObservableList<A> {
// We cast here to enforce variance, FilteredList should be covariant // We cast here to enforce variance, FilteredList should be covariant
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return FilteredList<A>(this as ObservableList<A>).apply { return FilteredList<A>(this as ObservableList<A>).apply {
@ -101,4 +107,10 @@ fun <A, B> ObservableList<out A>.fold(initial: B, folderFunction: (B, A) -> B):
* val people: ObservableList<Person> = (..) * val people: ObservableList<Person> = (..)
* val heights: ObservableList<Long> = people.map(Person::height).flatten() * val heights: ObservableList<Long> = people.map(Person::height).flatten()
*/ */
fun <A> ObservableList<out ObservableValue<out A>>.flatten(): ObservableList<out A> = FlattenedList(this) fun <A> ObservableList<out ObservableValue<out A>>.flatten(): ObservableList<A> = FlattenedList(this)
/**
* val people: List<Person> = listOf(alice, bob)
* val heights: ObservableList<Long> = people.map(Person::height).sequence()
*/
fun <A> List<ObservableValue<out A>>.sequence(): ObservableList<A> = FlattenedList(FXCollections.observableArrayList(this))

View File

@ -1,9 +1,13 @@
package com.r3corda.client.model package com.r3corda.client.model
import com.r3corda.client.fxutils.foldToObservableList import com.r3corda.client.fxutils.foldToObservableList
import com.r3corda.client.fxutils.getObservableValue
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.contracts.StateRef
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.transactions.LedgerTransaction
import com.r3corda.core.protocols.StateMachineRunId import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.node.services.monitor.ServiceToClientEvent import com.r3corda.node.services.monitor.ServiceToClientEvent
import com.r3corda.node.services.monitor.TransactionBuildResult import com.r3corda.node.services.monitor.TransactionBuildResult
import com.r3corda.node.utilities.AddOrRemove import com.r3corda.node.utilities.AddOrRemove
@ -11,22 +15,60 @@ import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections import javafx.collections.FXCollections
import javafx.collections.ObservableList import javafx.collections.ObservableList
import javafx.collections.ObservableMap
import org.fxmisc.easybind.EasyBind
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory
import rx.Observable import rx.Observable
import java.time.Instant import java.time.Instant
import java.util.UUID import java.util.UUID
import kotlin.reflect.KProperty1
interface GatheredTransactionData { interface GatheredTransactionData {
val stateMachineRunId: ObservableValue<StateMachineRunId?> val stateMachineRunId: ObservableValue<StateMachineRunId?>
val uuid: ObservableValue<UUID?> val uuid: ObservableValue<UUID?>
val protocolStatus: ObservableValue<ProtocolStatus?> val protocolStatus: ObservableValue<ProtocolStatus?>
val stateMachineStatus: ObservableValue<StateMachineStatus?> val stateMachineStatus: ObservableValue<StateMachineStatus?>
val transaction: ObservableValue<LedgerTransaction?> val transaction: ObservableValue<PartiallyResolvedTransaction?>
val status: ObservableValue<TransactionCreateStatus?> val status: ObservableValue<TransactionCreateStatus?>
val lastUpdate: ObservableValue<Instant> val lastUpdate: ObservableValue<Instant>
val allEvents: ObservableList<out ServiceToClientEvent> val allEvents: ObservableList<out ServiceToClientEvent>
} }
/**
* [PartiallyResolvedTransaction] holds a [SignedTransaction] that has zero or more inputs resolved. The intent is
* to prepare clients for cases where an input can only be resolved in the future/cannot be resolved at all (for example
* because of permissioning)
*/
data class PartiallyResolvedTransaction(
val transaction: SignedTransaction,
val inputs: List<ObservableValue<InputResolution>>
) {
val id = transaction.id
sealed class InputResolution(val stateRef: StateRef) {
class Unresolved(stateRef: StateRef) : InputResolution(stateRef)
class Resolved(val stateAndRef: StateAndRef<ContractState>) : InputResolution(stateAndRef.ref)
}
companion object {
fun fromSignedTransaction(
transaction: SignedTransaction,
transactions: ObservableMap<SecureHash, SignedTransaction>
) = PartiallyResolvedTransaction(
transaction = transaction,
inputs = transaction.tx.inputs.map { stateRef ->
EasyBind.map(transactions.getObservableValue(stateRef.txhash)) {
if (it == null) {
InputResolution.Unresolved(stateRef)
} else {
InputResolution.Resolved(it.tx.outRef(stateRef.index))
}
}
}
)
}
}
sealed class TransactionCreateStatus(val message: String?) { sealed class TransactionCreateStatus(val message: String?) {
class Started(message: String?) : TransactionCreateStatus(message) class Started(message: String?) : TransactionCreateStatus(message)
class Failed(message: String?) : TransactionCreateStatus(message) class Failed(message: String?) : TransactionCreateStatus(message)
@ -47,12 +89,14 @@ data class GatheredTransactionDataWritable(
override val uuid: SimpleObjectProperty<UUID?> = SimpleObjectProperty(null), override val uuid: SimpleObjectProperty<UUID?> = SimpleObjectProperty(null),
override val stateMachineStatus: SimpleObjectProperty<StateMachineStatus?> = SimpleObjectProperty(null), override val stateMachineStatus: SimpleObjectProperty<StateMachineStatus?> = SimpleObjectProperty(null),
override val protocolStatus: SimpleObjectProperty<ProtocolStatus?> = SimpleObjectProperty(null), override val protocolStatus: SimpleObjectProperty<ProtocolStatus?> = SimpleObjectProperty(null),
override val transaction: SimpleObjectProperty<LedgerTransaction?> = SimpleObjectProperty(null), override val transaction: SimpleObjectProperty<PartiallyResolvedTransaction?> = SimpleObjectProperty(null),
override val status: SimpleObjectProperty<TransactionCreateStatus?> = SimpleObjectProperty(null), override val status: SimpleObjectProperty<TransactionCreateStatus?> = SimpleObjectProperty(null),
override val lastUpdate: SimpleObjectProperty<Instant>, override val lastUpdate: SimpleObjectProperty<Instant>,
override val allEvents: ObservableList<ServiceToClientEvent> = FXCollections.observableArrayList() override val allEvents: ObservableList<ServiceToClientEvent> = FXCollections.observableArrayList()
) : GatheredTransactionData ) : GatheredTransactionData
private val log = LoggerFactory.getLogger(GatheredTransactionDataModel::class.java)
/** /**
* This model provides an observable list of states relating to the creation of a transaction not yet on ledger. * This model provides an observable list of states relating to the creation of a transaction not yet on ledger.
*/ */
@ -73,24 +117,37 @@ class GatheredTransactionDataModel {
* TODO: Expose a writable stream to combine [serviceToClient] with to allow recording of transactions made locally(UUID) * TODO: Expose a writable stream to combine [serviceToClient] with to allow recording of transactions made locally(UUID)
*/ */
val gatheredTransactionDataList: ObservableList<out GatheredTransactionData> = val gatheredTransactionDataList: ObservableList<out GatheredTransactionData> =
serviceToClient.foldToObservableList<ServiceToClientEvent, GatheredTransactionDataWritable, Unit>( serviceToClient.foldToObservableList<ServiceToClientEvent, GatheredTransactionDataWritable, ObservableMap<SecureHash, SignedTransaction>>(
initialAccumulator = Unit, initialAccumulator = FXCollections.observableHashMap<SecureHash, SignedTransaction>(),
folderFun = { serviceToClientEvent, _unit, transactionStates -> folderFun = { serviceToClientEvent, transactions, transactionStates ->
return@foldToObservableList when (serviceToClientEvent) { val _unit = when (serviceToClientEvent) {
is ServiceToClientEvent.Transaction -> { is ServiceToClientEvent.Transaction -> {
transactions.set(serviceToClientEvent.transaction.id, serviceToClientEvent.transaction)
val somewhatResolvedTransaction = PartiallyResolvedTransaction.fromSignedTransaction(
serviceToClientEvent.transaction,
transactions
)
newTransactionIdTransactionStateOrModify(transactionStates, serviceToClientEvent, newTransactionIdTransactionStateOrModify(transactionStates, serviceToClientEvent,
transaction = serviceToClientEvent.transaction, transaction = somewhatResolvedTransaction,
tweak = {} tweak = {}
) )
} }
is ServiceToClientEvent.OutputState -> {} is ServiceToClientEvent.OutputState -> {
}
is ServiceToClientEvent.StateMachine -> { is ServiceToClientEvent.StateMachine -> {
newFiberIdTransactionStateOrModify(transactionStates, serviceToClientEvent, newFiberIdTransactionStateOrModify(transactionStates, serviceToClientEvent,
stateMachineRunId = serviceToClientEvent.id, stateMachineRunId = serviceToClientEvent.id,
tweak = { tweak = {
stateMachineStatus.set(when (serviceToClientEvent.addOrRemove) { stateMachineStatus.set(when (serviceToClientEvent.addOrRemove) {
AddOrRemove.ADD -> StateMachineStatus.Added(serviceToClientEvent.label) AddOrRemove.ADD -> StateMachineStatus.Added(serviceToClientEvent.label)
AddOrRemove.REMOVE -> StateMachineStatus.Removed(serviceToClientEvent.label) AddOrRemove.REMOVE -> {
val currentStatus = stateMachineStatus.value
if (currentStatus is StateMachineStatus.Added) {
StateMachineStatus.Removed(currentStatus.stateMachineName)
} else {
StateMachineStatus.Removed(serviceToClientEvent.label)
}
}
}) })
} }
) )
@ -105,6 +162,15 @@ class GatheredTransactionDataModel {
} }
is ServiceToClientEvent.TransactionBuild -> { is ServiceToClientEvent.TransactionBuild -> {
val state = serviceToClientEvent.state val state = serviceToClientEvent.state
when (state) {
is TransactionBuildResult.ProtocolStarted -> {
state.transaction?.let {
transactions.set(it.id, it)
}
}
}
newUuidTransactionStateOrModify(transactionStates, serviceToClientEvent, newUuidTransactionStateOrModify(transactionStates, serviceToClientEvent,
uuid = serviceToClientEvent.id, uuid = serviceToClientEvent.id,
stateMachineRunId = when (state) { stateMachineRunId = when (state) {
@ -118,7 +184,9 @@ class GatheredTransactionDataModel {
tweak = { tweak = {
return@newUuidTransactionStateOrModify when (state) { return@newUuidTransactionStateOrModify when (state) {
is TransactionBuildResult.ProtocolStarted -> { is TransactionBuildResult.ProtocolStarted -> {
transaction.set(state.transaction) state.transaction?.let {
transaction.set(PartiallyResolvedTransaction.fromSignedTransaction(it, transactions))
}
status.set(TransactionCreateStatus.Started(state.message)) status.set(TransactionCreateStatus.Started(state.message))
} }
is TransactionBuildResult.Failed -> { is TransactionBuildResult.Failed -> {
@ -129,6 +197,7 @@ class GatheredTransactionDataModel {
) )
} }
} }
transactions
} }
) )
@ -137,7 +206,7 @@ class GatheredTransactionDataModel {
private fun newTransactionIdTransactionStateOrModify( private fun newTransactionIdTransactionStateOrModify(
transactionStates: ObservableList<GatheredTransactionDataWritable>, transactionStates: ObservableList<GatheredTransactionDataWritable>,
event: ServiceToClientEvent, event: ServiceToClientEvent,
transaction: LedgerTransaction, transaction: PartiallyResolvedTransaction,
tweak: GatheredTransactionDataWritable.() -> Unit tweak: GatheredTransactionDataWritable.() -> Unit
) { ) {
val index = transactionStates.indexOfFirst { transaction.id == it.transaction.value?.id } val index = transactionStates.indexOfFirst { transaction.id == it.transaction.value?.id }
@ -190,28 +259,67 @@ class GatheredTransactionDataModel {
transactionId: SecureHash?, transactionId: SecureHash?,
tweak: GatheredTransactionDataWritable.() -> Unit tweak: GatheredTransactionDataWritable.() -> Unit
) { ) {
val index = transactionStates.indexOfFirst { val matchingStates = transactionStates.filtered {
it.uuid.value == uuid || it.uuid.value == uuid ||
(stateMachineRunId != null && it.stateMachineRunId.value == stateMachineRunId) || (stateMachineRunId != null && it.stateMachineRunId.value == stateMachineRunId) ||
(transactionId != null && it.transaction.value?.id == transactionId) (transactionId != null && it.transaction.value?.transaction?.id == transactionId)
} }
val state = if (index < 0) { val mergedState = mergeGatheredData(matchingStates)
for (i in 0 .. matchingStates.size - 1) {
transactionStates.removeAt(matchingStates.getSourceIndex(i))
}
val state = if (mergedState == null) {
val newState = GatheredTransactionDataWritable( val newState = GatheredTransactionDataWritable(
uuid = SimpleObjectProperty(uuid), uuid = SimpleObjectProperty(uuid),
stateMachineRunId = SimpleObjectProperty(stateMachineRunId), stateMachineRunId = SimpleObjectProperty(stateMachineRunId),
lastUpdate = SimpleObjectProperty(event.time) lastUpdate = SimpleObjectProperty(event.time)
) )
tweak(newState)
transactionStates.add(newState) transactionStates.add(newState)
newState newState
} else { } else {
val existingState = transactionStates[index] mergedState.lastUpdate.set(event.time)
existingState.lastUpdate.set(event.time) mergedState
tweak(existingState)
existingState
} }
tweak(state)
state.allEvents.add(event) state.allEvents.add(event)
} }
private fun mergeGatheredData(
gatheredDataList: List<GatheredTransactionDataWritable>
): GatheredTransactionDataWritable? {
var gathered: GatheredTransactionDataWritable? = null
// Modify the last one if we can
gatheredDataList.asReversed().forEach {
val localGathered = gathered
if (localGathered == null) {
gathered = it
} else {
mergeField(it, localGathered, GatheredTransactionDataWritable::stateMachineRunId)
mergeField(it, localGathered, GatheredTransactionDataWritable::uuid)
mergeField(it, localGathered, GatheredTransactionDataWritable::stateMachineStatus)
mergeField(it, localGathered, GatheredTransactionDataWritable::protocolStatus)
mergeField(it, localGathered, GatheredTransactionDataWritable::transaction)
mergeField(it, localGathered, GatheredTransactionDataWritable::status)
localGathered.allEvents.addAll(it.allEvents)
}
}
return gathered
}
private fun <A> mergeField(
from: GatheredTransactionDataWritable,
to: GatheredTransactionDataWritable,
field: KProperty1<GatheredTransactionDataWritable, SimpleObjectProperty<A?>>) {
val fromValue = field(from).value
if (fromValue != null) {
val toField = field(to)
val toValue = toField.value
if (toValue != null && fromValue != toValue) {
log.warn("Conflicting data for field ${field.name}: $fromValue vs $toValue")
}
toField.set(fromValue)
}
}
} }
} }

View File

@ -254,7 +254,7 @@ class CashViewer : View() {
* We re-display the exchanged sum amount, if we have a selection. * We re-display the exchanged sum amount, if we have a selection.
*/ */
private val noSelectionSumEquiv = reportingCurrency.map { Amount(0, it) } private val noSelectionSumEquiv = reportingCurrency.map { Amount(0, it) }
private val selectedViewerNodeSumEquiv = selectedViewerNode.bind { selection -> private val selectedViewerNodeSumEquiv = selectedViewerNode.bindOut { selection ->
when (selection) { when (selection) {
is SingleRowSelection.None -> noSelectionSumEquiv is SingleRowSelection.None -> noSelectionSumEquiv
is SingleRowSelection.Selected -> is SingleRowSelection.Selected ->

View File

@ -1,16 +1,12 @@
package com.r3corda.explorer.views package com.r3corda.explorer.views
import com.r3corda.client.fxutils.ChosenList import com.r3corda.client.fxutils.*
import com.r3corda.client.fxutils.bind
import com.r3corda.client.fxutils.lift
import com.r3corda.client.fxutils.map
import com.r3corda.client.model.* import com.r3corda.client.model.*
import com.r3corda.contracts.asset.Cash import com.r3corda.contracts.asset.Cash
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.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.transactions.LedgerTransaction
import com.r3corda.core.protocols.StateMachineRunId import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.explorer.AmountDiff import com.r3corda.explorer.AmountDiff
import com.r3corda.explorer.formatters.AmountFormatter import com.r3corda.explorer.formatters.AmountFormatter
@ -33,7 +29,6 @@ import javafx.scene.layout.BackgroundFill
import javafx.scene.layout.CornerRadii import javafx.scene.layout.CornerRadii
import javafx.scene.layout.VBox import javafx.scene.layout.VBox
import javafx.scene.paint.Color import javafx.scene.paint.Color
import org.fxmisc.easybind.EasyBind
import tornadofx.View import tornadofx.View
import java.security.PublicKey import java.security.PublicKey
import java.time.Instant import java.time.Instant
@ -61,20 +56,20 @@ class TransactionViewer: View() {
private val contractStatesInputsCountLabel: Label by fxid() private val contractStatesInputsCountLabel: Label by fxid()
private val contractStatesInputStatesTable: TableView<StateNode> by fxid() private val contractStatesInputStatesTable: TableView<StateNode> by fxid()
private val contractStatesInputStatesId: TableColumn<StateNode, String> by fxid() private val contractStatesInputStatesId: TableColumn<StateNode, String> by fxid()
private val contractStatesInputStatesType: TableColumn<StateNode, Class<out ContractState>> by fxid() private val contractStatesInputStatesType: TableColumn<StateNode, String> by fxid()
private val contractStatesInputStatesOwner: TableColumn<StateNode, String> by fxid() private val contractStatesInputStatesOwner: TableColumn<StateNode, String> by fxid()
private val contractStatesInputStatesLocalCurrency: TableColumn<StateNode, Currency?> by fxid() private val contractStatesInputStatesLocalCurrency: TableColumn<StateNode, Currency?> by fxid()
private val contractStatesInputStatesAmount: TableColumn<StateNode, Long> by fxid() private val contractStatesInputStatesAmount: TableColumn<StateNode, Long?> by fxid()
private val contractStatesInputStatesEquiv: TableColumn<StateNode, Amount<Currency>> by fxid() private val contractStatesInputStatesEquiv: TableColumn<StateNode, Amount<Currency>?> by fxid()
private val contractStatesOutputsCountLabel: Label by fxid() private val contractStatesOutputsCountLabel: Label by fxid()
private val contractStatesOutputStatesTable: TableView<StateNode> by fxid() private val contractStatesOutputStatesTable: TableView<StateNode> by fxid()
private val contractStatesOutputStatesId: TableColumn<StateNode, String> by fxid() private val contractStatesOutputStatesId: TableColumn<StateNode, String> by fxid()
private val contractStatesOutputStatesType: TableColumn<StateNode, Class<out ContractState>> by fxid() private val contractStatesOutputStatesType: TableColumn<StateNode, String> by fxid()
private val contractStatesOutputStatesOwner: TableColumn<StateNode, String> by fxid() private val contractStatesOutputStatesOwner: TableColumn<StateNode, String> by fxid()
private val contractStatesOutputStatesLocalCurrency: TableColumn<StateNode, Currency?> by fxid() private val contractStatesOutputStatesLocalCurrency: TableColumn<StateNode, Currency?> by fxid()
private val contractStatesOutputStatesAmount: TableColumn<StateNode, Long> by fxid() private val contractStatesOutputStatesAmount: TableColumn<StateNode, Long?> by fxid()
private val contractStatesOutputStatesEquiv: TableColumn<StateNode, Amount<Currency>> by fxid() private val contractStatesOutputStatesEquiv: TableColumn<StateNode, Amount<Currency>?> by fxid()
private val signaturesTitledPane: TitledPane by fxid() private val signaturesTitledPane: TitledPane by fxid()
private val signaturesList: ListView<PublicKey> by fxid() private val signaturesList: ListView<PublicKey> by fxid()
@ -108,7 +103,7 @@ class TransactionViewer: View() {
val statusUpdated: ObservableValue<Instant>, val statusUpdated: ObservableValue<Instant>,
val commandTypes: ObservableValue<Collection<Class<CommandData>>>, val commandTypes: ObservableValue<Collection<Class<CommandData>>>,
val totalValueEquiv: ObservableValue<AmountDiff<Currency>?>, val totalValueEquiv: ObservableValue<AmountDiff<Currency>?>,
val transaction: ObservableValue<LedgerTransaction?>, val transaction: ObservableValue<PartiallyResolvedTransaction?>,
val allEvents: ObservableList<out ServiceToClientEvent> val allEvents: ObservableList<out ServiceToClientEvent>
) )
@ -116,7 +111,7 @@ class TransactionViewer: View() {
* Holds information about a single input/output state, to be displayed in the [contractStatesTitledPane] * Holds information about a single input/output state, to be displayed in the [contractStatesTitledPane]
*/ */
data class StateNode( data class StateNode(
val transactionState: TransactionState<*>, val state: ObservableValue<PartiallyResolvedTransaction.InputResolution>,
val stateRef: StateRef val stateRef: StateRef
) )
@ -144,12 +139,37 @@ class TransactionViewer: View() {
statusUpdated = it.lastUpdate, statusUpdated = it.lastUpdate,
commandTypes = it.transaction.map { commandTypes = it.transaction.map {
val commands = mutableSetOf<Class<CommandData>>() val commands = mutableSetOf<Class<CommandData>>()
it?.commands?.forEach { it?.transaction?.tx?.commands?.forEach {
commands.add(it.value.javaClass) commands.add(it.value.javaClass)
} }
commands commands
}, },
totalValueEquiv = ::calculateTotalEquiv.lift(myIdentity, reportingExchange, it.transaction), totalValueEquiv = it.transaction.bind { transaction ->
if (transaction == null) {
null.lift<AmountDiff<Currency>?>()
} else {
val resolvedInputs = transaction.inputs.sequence().map { resolution ->
when (resolution) {
is PartiallyResolvedTransaction.InputResolution.Unresolved -> null
is PartiallyResolvedTransaction.InputResolution.Resolved -> resolution.stateAndRef
}
}.fold(listOf()) { inputs: List<StateAndRef<ContractState>>?, state: StateAndRef<ContractState>? ->
if (inputs != null && state != null) {
inputs + state
} else {
null
}
}
::calculateTotalEquiv.lift(
myIdentity,
reportingExchange,
resolvedInputs,
transaction.transaction.tx.outputs.lift()
)
}
},
transaction = it.transaction, transaction = it.transaction,
allEvents = it.allEvents allEvents = it.allEvents
) )
@ -159,18 +179,20 @@ class TransactionViewer: View() {
* The detail panes are only filled out if a transaction is selected * The detail panes are only filled out if a transaction is selected
*/ */
private val selectedViewerNode = transactionViewTable.singleRowSelection() private val selectedViewerNode = transactionViewTable.singleRowSelection()
private val selectedTransaction = selectedViewerNode.bind { private val selectedTransaction = selectedViewerNode.bindOut {
when (it) { when (it) {
is SingleRowSelection.None -> null.lift() is SingleRowSelection.None -> null.lift()
is SingleRowSelection.Selected -> it.node.transaction is SingleRowSelection.Selected -> it.node.transaction
} }
} }
private val inputStateNodes = ChosenList(selectedTransaction.map { private val inputStateNodes = ChosenList(selectedTransaction.map { transaction ->
if (it == null) { if (transaction == null) {
FXCollections.emptyObservableList<StateNode>() FXCollections.emptyObservableList<StateNode>()
} else { } else {
FXCollections.observableArrayList(it.inputs.map { StateNode(it.state, it.ref) }) FXCollections.observableArrayList(transaction.inputs.map { inputResolution ->
StateNode(inputResolution, inputResolution.value.stateRef)
})
} }
}) })
@ -178,8 +200,9 @@ class TransactionViewer: View() {
if (it == null) { if (it == null) {
FXCollections.emptyObservableList<StateNode>() FXCollections.emptyObservableList<StateNode>()
} else { } else {
FXCollections.observableArrayList(it.outputs.mapIndexed { index, transactionState -> FXCollections.observableArrayList(it.transaction.tx.outputs.mapIndexed { index, transactionState ->
StateNode(transactionState, StateRef(it.id, index)) val stateRef = StateRef(it.id, index)
StateNode(PartiallyResolvedTransaction.InputResolution.Resolved(StateAndRef(transactionState, stateRef)).lift(), stateRef)
}) })
} }
}) })
@ -188,7 +211,7 @@ class TransactionViewer: View() {
if (it == null) { if (it == null) {
FXCollections.emptyObservableList<PublicKey>() FXCollections.emptyObservableList<PublicKey>()
} else { } else {
FXCollections.observableArrayList(it.mustSign) FXCollections.observableArrayList(it.transaction.sigs.map { it.by })
} }
}) })
@ -228,52 +251,67 @@ class TransactionViewer: View() {
statesCountLabel: Label, statesCountLabel: Label,
statesTable: TableView<StateNode>, statesTable: TableView<StateNode>,
statesId: TableColumn<StateNode, String>, statesId: TableColumn<StateNode, String>,
statesType: TableColumn<StateNode, Class<out ContractState>>, statesType: TableColumn<StateNode, String>,
statesOwner: TableColumn<StateNode, String>, statesOwner: TableColumn<StateNode, String>,
statesLocalCurrency: TableColumn<StateNode, Currency?>, statesLocalCurrency: TableColumn<StateNode, Currency?>,
statesAmount: TableColumn<StateNode, Long>, statesAmount: TableColumn<StateNode, Long?>,
statesEquiv: TableColumn<StateNode, Amount<Currency>> statesEquiv: TableColumn<StateNode, Amount<Currency>?>
) { ) {
statesCountLabel.textProperty().bind(Bindings.size(states).map { "$it" }) statesCountLabel.textProperty().bind(Bindings.size(states).map { "$it" })
Bindings.bindContent(statesTable.items, states) Bindings.bindContent(statesTable.items, states)
val unknownString = "???"
statesId.setCellValueFactory { it.value.stateRef.toString().lift() } statesId.setCellValueFactory { it.value.stateRef.toString().lift() }
statesType.setCellValueFactory { it.value.transactionState.data.javaClass.lift() } statesType.setCellValueFactory {
resolvedOrDefault(it.value.state, unknownString) {
it.state.data.javaClass.toString()
}
}
statesOwner.setCellValueFactory { statesOwner.setCellValueFactory {
val state = it.value.transactionState.data resolvedOrDefault(it.value.state, unknownString) {
if (state is OwnableState) { val contractState = it.state.data
state.owner.toStringShort().lift() if (contractState is OwnableState) {
} else { contractState.owner.toStringShort()
"???".lift() } else {
unknownString
}
} }
} }
statesLocalCurrency.setCellValueFactory { statesLocalCurrency.setCellValueFactory {
val state = it.value.transactionState.data resolvedOrDefault<Currency?>(it.value.state, null) {
if (state is Cash.State) { val contractState = it.state.data
state.amount.token.product.lift() if (contractState is Cash.State) {
} else { contractState.amount.token.product
null.lift() } else {
null
}
} }
} }
statesAmount.setCellValueFactory { statesAmount.setCellValueFactory {
val state = it.value.transactionState.data resolvedOrDefault<Long?>(it.value.state, null) {
if (state is Cash.State) { val contractState = it.state.data
state.amount.quantity.lift() if (contractState is Cash.State) {
} else { contractState.amount.quantity
null.lift() } else {
null
}
} }
} }
statesAmount.cellFactory = NumberFormatter.boringLong.toTableCellFactory() statesAmount.cellFactory = NumberFormatter.boringLong.toTableCellFactory()
statesEquiv.setCellValueFactory { statesEquiv.setCellValueFactory {
val state = it.value.transactionState.data resolvedOrDefault<ObservableValue<Amount<Currency>?>>(it.value.state, null.lift()) {
if (state is Cash.State) { val contractState = it.state.data
reportingExchange.map { exchange -> if (contractState is Cash.State) {
exchange.second(state.amount.withoutIssuer()) reportingExchange.map { exchange ->
exchange.second(contractState.amount.withoutIssuer())
}
} else {
null.lift()
} }
} else { }.bind { it }
null.lift()
}
} }
statesEquiv.cellFactory = AmountFormatter.boring.toTableCellFactory() statesEquiv.cellFactory = AmountFormatter.boring.toTableCellFactory()
} }
@ -364,7 +402,7 @@ class TransactionViewer: View() {
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / lowLevelEventsTable.columns.size).toInt() Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / lowLevelEventsTable.columns.size).toInt()
} }
matchingTransactionsLabel.textProperty().bind(EasyBind.map(Bindings.size(viewerNodes)) { matchingTransactionsLabel.textProperty().bind(Bindings.size(viewerNodes).map {
"$it matching transaction${if (it == 1) "" else "s"}" "$it matching transaction${if (it == 1) "" else "s"}"
}) })
} }
@ -376,20 +414,21 @@ class TransactionViewer: View() {
private fun calculateTotalEquiv( private fun calculateTotalEquiv(
identity: Party, identity: Party,
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>, reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
transaction: LedgerTransaction?): AmountDiff<Currency>? { inputs: List<StateAndRef<ContractState>>?,
if (transaction == null) { outputs: List<TransactionState<ContractState>>): AmountDiff<Currency>? {
if (inputs == null) {
return null return null
} }
var sum = 0L var sum = 0L
val (reportingCurrency, exchange) = reportingCurrencyExchange val (reportingCurrency, exchange) = reportingCurrencyExchange
val publicKey = identity.owningKey val publicKey = identity.owningKey
transaction.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) {
sum -= exchange(contractState.amount.withoutIssuer()).quantity sum -= exchange(contractState.amount.withoutIssuer()).quantity
} }
} }
transaction.outputs.forEach { outputs.forEach {
val contractState = it.data val contractState = it.data
if (contractState is Cash.State && publicKey == contractState.owner) { if (contractState is Cash.State && publicKey == contractState.owner) {
sum += exchange(contractState.amount.withoutIssuer()).quantity sum += exchange(contractState.amount.withoutIssuer()).quantity
@ -398,3 +437,15 @@ private fun calculateTotalEquiv(
return AmountDiff.fromLong(sum, reportingCurrency) return AmountDiff.fromLong(sum, reportingCurrency)
} }
fun <A> resolvedOrDefault(
state: ObservableValue<PartiallyResolvedTransaction.InputResolution>,
default: A,
resolved: (StateAndRef<*>) -> A
): ObservableValue<A> {
return state.map {
when (it) {
is PartiallyResolvedTransaction.InputResolution.Unresolved -> default
is PartiallyResolvedTransaction.InputResolution.Resolved -> resolved(it.stateAndRef)
}
}
}

View File

@ -1,8 +1,8 @@
package com.r3corda.node.services.monitor package com.r3corda.node.services.monitor
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.transactions.LedgerTransaction
import com.r3corda.core.protocols.StateMachineRunId import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.node.utilities.AddOrRemove import com.r3corda.node.utilities.AddOrRemove
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
@ -11,8 +11,8 @@ import java.util.*
* Events triggered by changes in the node, and sent to monitoring client(s). * Events triggered by changes in the node, and sent to monitoring client(s).
*/ */
sealed class ServiceToClientEvent(val time: Instant) { sealed class ServiceToClientEvent(val time: Instant) {
class Transaction(time: Instant, val transaction: LedgerTransaction) : ServiceToClientEvent(time) { class Transaction(time: Instant, val transaction: SignedTransaction) : ServiceToClientEvent(time) {
override fun toString() = "Transaction(${transaction.commands})" override fun toString() = "Transaction(${transaction.tx.commands})"
} }
class OutputState( class OutputState(
time: Instant, time: Instant,
@ -26,7 +26,7 @@ sealed class ServiceToClientEvent(val time: Instant) {
val id: StateMachineRunId, val id: StateMachineRunId,
val label: String, val label: String,
val addOrRemove: AddOrRemove val addOrRemove: AddOrRemove
) : ServiceToClientEvent(time) { ) : ServiceToClientEvent(time) {
override fun toString() = "StateMachine($label, ${addOrRemove.name})" override fun toString() = "StateMachine($label, ${addOrRemove.name})"
} }
class Progress(time: Instant, val id: StateMachineRunId, val message: String) : ServiceToClientEvent(time) { class Progress(time: Instant, val id: StateMachineRunId, val message: String) : ServiceToClientEvent(time) {
@ -35,7 +35,6 @@ sealed class ServiceToClientEvent(val time: Instant) {
class TransactionBuild(time: Instant, val id: UUID, val state: TransactionBuildResult) : ServiceToClientEvent(time) { class TransactionBuild(time: Instant, val id: UUID, val state: TransactionBuildResult) : ServiceToClientEvent(time) {
override fun toString() = "TransactionBuild($state)" override fun toString() = "TransactionBuild($state)"
} }
} }
sealed class TransactionBuildResult { sealed class TransactionBuildResult {
@ -47,7 +46,7 @@ sealed class TransactionBuildResult {
* *
* @param transaction the transaction created as a result, in the case where the protocol has completed. * @param transaction the transaction created as a result, in the case where the protocol has completed.
*/ */
class ProtocolStarted(val id: StateMachineRunId, val transaction: LedgerTransaction?, val message: String?) : TransactionBuildResult() { class ProtocolStarted(val id: StateMachineRunId, val transaction: SignedTransaction?, val message: String?) : TransactionBuildResult() {
override fun toString() = "Started($message)" override fun toString() = "Started($message)"
} }

View File

@ -13,7 +13,7 @@ import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.protocols.StateMachineRunId import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.serialization.serialize import com.r3corda.core.serialization.serialize
import com.r3corda.core.transactions.LedgerTransaction import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.core.transactions.TransactionBuilder import com.r3corda.core.transactions.TransactionBuilder
import com.r3corda.core.utilities.loggerFor import com.r3corda.core.utilities.loggerFor
import com.r3corda.node.services.api.AbstractNodeService import com.r3corda.node.services.api.AbstractNodeService
@ -61,7 +61,7 @@ class NodeMonitorService(services: ServiceHubInternal, val smm: StateMachineMana
addMessageHandler(OUT_EVENT_TOPIC) { req: ClientToServiceCommandMessage -> processEventRequest(req) } addMessageHandler(OUT_EVENT_TOPIC) { req: ClientToServiceCommandMessage -> processEventRequest(req) }
// Notify listeners on state changes // Notify listeners on state changes
services.storageService.validatedTransactions.updates.subscribe { tx -> notifyTransaction(tx.tx.toLedgerTransaction(services)) } services.storageService.validatedTransactions.updates.subscribe { tx -> notifyTransaction(tx) }
services.vaultService.updates.subscribe { update -> notifyVaultUpdate(update) } services.vaultService.updates.subscribe { update -> notifyVaultUpdate(update) }
smm.changes.subscribe { change -> smm.changes.subscribe { change ->
val id: StateMachineRunId = change.id val id: StateMachineRunId = change.id
@ -87,7 +87,7 @@ class NodeMonitorService(services: ServiceHubInternal, val smm: StateMachineMana
= notifyEvent(ServiceToClientEvent.OutputState(Instant.now(), update.consumed, update.produced)) = notifyEvent(ServiceToClientEvent.OutputState(Instant.now(), update.consumed, update.produced))
@VisibleForTesting @VisibleForTesting
internal fun notifyTransaction(transaction: LedgerTransaction) internal fun notifyTransaction(transaction: SignedTransaction)
= notifyEvent(ServiceToClientEvent.Transaction(Instant.now(), transaction)) = notifyEvent(ServiceToClientEvent.Transaction(Instant.now(), transaction))
private fun processEventRequest(reqMessage: ClientToServiceCommandMessage) { private fun processEventRequest(reqMessage: ClientToServiceCommandMessage) {
@ -170,7 +170,7 @@ class NodeMonitorService(services: ServiceHubInternal, val smm: StateMachineMana
val protocol = FinalityProtocol(tx, setOf(req), setOf(req.recipient)) val protocol = FinalityProtocol(tx, setOf(req), setOf(req.recipient))
return TransactionBuildResult.ProtocolStarted( return TransactionBuildResult.ProtocolStarted(
smm.add(BroadcastTransactionProtocol.TOPIC, protocol).id, smm.add(BroadcastTransactionProtocol.TOPIC, protocol).id,
tx.tx.toLedgerTransaction(services), tx,
"Cash payment transaction generated" "Cash payment transaction generated"
) )
} catch(ex: InsufficientBalanceException) { } catch(ex: InsufficientBalanceException) {
@ -204,7 +204,7 @@ class NodeMonitorService(services: ServiceHubInternal, val smm: StateMachineMana
val protocol = FinalityProtocol(tx, setOf(req), participants) val protocol = FinalityProtocol(tx, setOf(req), participants)
return TransactionBuildResult.ProtocolStarted( return TransactionBuildResult.ProtocolStarted(
smm.add(BroadcastTransactionProtocol.TOPIC, protocol).id, smm.add(BroadcastTransactionProtocol.TOPIC, protocol).id,
tx.tx.toLedgerTransaction(services), tx,
"Cash destruction transaction generated" "Cash destruction transaction generated"
) )
} catch (ex: InsufficientBalanceException) { } catch (ex: InsufficientBalanceException) {
@ -223,7 +223,7 @@ class NodeMonitorService(services: ServiceHubInternal, val smm: StateMachineMana
val protocol = BroadcastTransactionProtocol(tx, setOf(req), setOf(req.recipient)) val protocol = BroadcastTransactionProtocol(tx, setOf(req), setOf(req.recipient))
return TransactionBuildResult.ProtocolStarted( return TransactionBuildResult.ProtocolStarted(
smm.add(BroadcastTransactionProtocol.TOPIC, protocol).id, smm.add(BroadcastTransactionProtocol.TOPIC, protocol).id,
tx.tx.toLedgerTransaction(services), tx,
"Cash issuance completed" "Cash issuance completed"
) )
} }

View File

@ -141,7 +141,7 @@ class NodeMonitorServiceTests {
// Check the returned event is correct // Check the returned event is correct
val tx = (event.state as TransactionBuildResult.ProtocolStarted).transaction val tx = (event.state as TransactionBuildResult.ProtocolStarted).transaction
assertNotNull(tx) assertNotNull(tx)
assertEquals(expectedState, tx!!.outputs.single().data) assertEquals(expectedState, tx!!.tx.outputs.single().data)
}, },
expect { event: ServiceToClientEvent.OutputState -> expect { event: ServiceToClientEvent.OutputState ->
// Check the generated state is correct // Check the generated state is correct
@ -203,8 +203,8 @@ class NodeMonitorServiceTests {
} }
), ),
expect { event: ServiceToClientEvent.Transaction -> expect { event: ServiceToClientEvent.Transaction ->
require(event.transaction.mustSign.size == 1) require(event.transaction.sigs.size == 1)
event.transaction.mustSign.containsAll( event.transaction.sigs.map { it.by }.containsAll(
listOf( listOf(
monitorServiceNode.services.storageService.myLegalIdentity.owningKey monitorServiceNode.services.storageService.myLegalIdentity.owningKey
) )