mirror of
https://github.com/corda/corda.git
synced 2025-03-25 13:27:58 +00:00
Add PartiallyResolvedTransaction to client
This commit is contained in:
parent
adf0876b18
commit
43d18d46bb
@ -105,9 +105,9 @@ class NodeMonitorClientTests {
|
||||
}
|
||||
),
|
||||
expect { tx: ServiceToClientEvent.Transaction ->
|
||||
require(tx.transaction.inputs.isEmpty())
|
||||
require(tx.transaction.outputs.size == 1)
|
||||
val signaturePubKeys = tx.transaction.mustSign.toSet()
|
||||
require(tx.transaction.tx.inputs.isEmpty())
|
||||
require(tx.transaction.tx.outputs.size == 1)
|
||||
val signaturePubKeys = tx.transaction.sigs.map { it.by }.toSet()
|
||||
// Only Alice signed
|
||||
require(signaturePubKeys.size == 1)
|
||||
require(signaturePubKeys.contains(aliceNode.identity.owningKey))
|
||||
@ -137,9 +137,9 @@ class NodeMonitorClientTests {
|
||||
}
|
||||
),
|
||||
expect { tx: ServiceToClientEvent.Transaction ->
|
||||
require(tx.transaction.inputs.size == 1)
|
||||
require(tx.transaction.outputs.size == 1)
|
||||
val signaturePubKeys = tx.transaction.mustSign.toSet()
|
||||
require(tx.transaction.tx.inputs.size == 1)
|
||||
require(tx.transaction.tx.outputs.size == 1)
|
||||
val signaturePubKeys = tx.transaction.sigs.map { it.by }.toSet()
|
||||
// Alice and Notary signed
|
||||
require(signaturePubKeys.size == 2)
|
||||
require(signaturePubKeys.contains(aliceNode.identity.owningKey))
|
||||
|
@ -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
|
||||
}
|
@ -3,6 +3,7 @@ package com.r3corda.client.fxutils
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.transformation.FilteredList
|
||||
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 personHeight: ObservableValue<Long> = person.bind { it.height }
|
||||
*/
|
||||
fun <A, B> ObservableValue<out A>.bind(function: (A) -> ObservableValue<out B>): ObservableValue<out B> =
|
||||
// We cast here to enforce variance, flatMap should be covariant
|
||||
fun <A, B> ObservableValue<out A>.bind(function: (A) -> ObservableValue<B>): ObservableValue<B> =
|
||||
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")
|
||||
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))
|
||||
*/
|
||||
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
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
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 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))
|
||||
|
@ -1,9 +1,13 @@
|
||||
package com.r3corda.client.model
|
||||
|
||||
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.transactions.LedgerTransaction
|
||||
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.TransactionBuildResult
|
||||
import com.r3corda.node.utilities.AddOrRemove
|
||||
@ -11,22 +15,60 @@ import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.ObservableMap
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.slf4j.LoggerFactory
|
||||
import rx.Observable
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
import kotlin.reflect.KProperty1
|
||||
|
||||
interface GatheredTransactionData {
|
||||
val stateMachineRunId: ObservableValue<StateMachineRunId?>
|
||||
val uuid: ObservableValue<UUID?>
|
||||
val protocolStatus: ObservableValue<ProtocolStatus?>
|
||||
val stateMachineStatus: ObservableValue<StateMachineStatus?>
|
||||
val transaction: ObservableValue<LedgerTransaction?>
|
||||
val transaction: ObservableValue<PartiallyResolvedTransaction?>
|
||||
val status: ObservableValue<TransactionCreateStatus?>
|
||||
val lastUpdate: ObservableValue<Instant>
|
||||
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?) {
|
||||
class Started(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 stateMachineStatus: SimpleObjectProperty<StateMachineStatus?> = 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 lastUpdate: SimpleObjectProperty<Instant>,
|
||||
override val allEvents: ObservableList<ServiceToClientEvent> = FXCollections.observableArrayList()
|
||||
) : 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.
|
||||
*/
|
||||
@ -73,24 +117,37 @@ class GatheredTransactionDataModel {
|
||||
* TODO: Expose a writable stream to combine [serviceToClient] with to allow recording of transactions made locally(UUID)
|
||||
*/
|
||||
val gatheredTransactionDataList: ObservableList<out GatheredTransactionData> =
|
||||
serviceToClient.foldToObservableList<ServiceToClientEvent, GatheredTransactionDataWritable, Unit>(
|
||||
initialAccumulator = Unit,
|
||||
folderFun = { serviceToClientEvent, _unit, transactionStates ->
|
||||
return@foldToObservableList when (serviceToClientEvent) {
|
||||
serviceToClient.foldToObservableList<ServiceToClientEvent, GatheredTransactionDataWritable, ObservableMap<SecureHash, SignedTransaction>>(
|
||||
initialAccumulator = FXCollections.observableHashMap<SecureHash, SignedTransaction>(),
|
||||
folderFun = { serviceToClientEvent, transactions, transactionStates ->
|
||||
val _unit = when (serviceToClientEvent) {
|
||||
is ServiceToClientEvent.Transaction -> {
|
||||
transactions.set(serviceToClientEvent.transaction.id, serviceToClientEvent.transaction)
|
||||
val somewhatResolvedTransaction = PartiallyResolvedTransaction.fromSignedTransaction(
|
||||
serviceToClientEvent.transaction,
|
||||
transactions
|
||||
)
|
||||
newTransactionIdTransactionStateOrModify(transactionStates, serviceToClientEvent,
|
||||
transaction = serviceToClientEvent.transaction,
|
||||
transaction = somewhatResolvedTransaction,
|
||||
tweak = {}
|
||||
)
|
||||
}
|
||||
is ServiceToClientEvent.OutputState -> {}
|
||||
is ServiceToClientEvent.OutputState -> {
|
||||
}
|
||||
is ServiceToClientEvent.StateMachine -> {
|
||||
newFiberIdTransactionStateOrModify(transactionStates, serviceToClientEvent,
|
||||
stateMachineRunId = serviceToClientEvent.id,
|
||||
tweak = {
|
||||
stateMachineStatus.set(when (serviceToClientEvent.addOrRemove) {
|
||||
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 -> {
|
||||
val state = serviceToClientEvent.state
|
||||
|
||||
when (state) {
|
||||
is TransactionBuildResult.ProtocolStarted -> {
|
||||
state.transaction?.let {
|
||||
transactions.set(it.id, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newUuidTransactionStateOrModify(transactionStates, serviceToClientEvent,
|
||||
uuid = serviceToClientEvent.id,
|
||||
stateMachineRunId = when (state) {
|
||||
@ -118,7 +184,9 @@ class GatheredTransactionDataModel {
|
||||
tweak = {
|
||||
return@newUuidTransactionStateOrModify when (state) {
|
||||
is TransactionBuildResult.ProtocolStarted -> {
|
||||
transaction.set(state.transaction)
|
||||
state.transaction?.let {
|
||||
transaction.set(PartiallyResolvedTransaction.fromSignedTransaction(it, transactions))
|
||||
}
|
||||
status.set(TransactionCreateStatus.Started(state.message))
|
||||
}
|
||||
is TransactionBuildResult.Failed -> {
|
||||
@ -129,6 +197,7 @@ class GatheredTransactionDataModel {
|
||||
)
|
||||
}
|
||||
}
|
||||
transactions
|
||||
}
|
||||
)
|
||||
|
||||
@ -137,7 +206,7 @@ class GatheredTransactionDataModel {
|
||||
private fun newTransactionIdTransactionStateOrModify(
|
||||
transactionStates: ObservableList<GatheredTransactionDataWritable>,
|
||||
event: ServiceToClientEvent,
|
||||
transaction: LedgerTransaction,
|
||||
transaction: PartiallyResolvedTransaction,
|
||||
tweak: GatheredTransactionDataWritable.() -> Unit
|
||||
) {
|
||||
val index = transactionStates.indexOfFirst { transaction.id == it.transaction.value?.id }
|
||||
@ -190,28 +259,67 @@ class GatheredTransactionDataModel {
|
||||
transactionId: SecureHash?,
|
||||
tweak: GatheredTransactionDataWritable.() -> Unit
|
||||
) {
|
||||
val index = transactionStates.indexOfFirst {
|
||||
val matchingStates = transactionStates.filtered {
|
||||
it.uuid.value == uuid ||
|
||||
(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(
|
||||
uuid = SimpleObjectProperty(uuid),
|
||||
stateMachineRunId = SimpleObjectProperty(stateMachineRunId),
|
||||
lastUpdate = SimpleObjectProperty(event.time)
|
||||
)
|
||||
tweak(newState)
|
||||
transactionStates.add(newState)
|
||||
newState
|
||||
} else {
|
||||
val existingState = transactionStates[index]
|
||||
existingState.lastUpdate.set(event.time)
|
||||
tweak(existingState)
|
||||
existingState
|
||||
mergedState.lastUpdate.set(event.time)
|
||||
mergedState
|
||||
}
|
||||
tweak(state)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -254,7 +254,7 @@ class CashViewer : View() {
|
||||
* We re-display the exchanged sum amount, if we have a selection.
|
||||
*/
|
||||
private val noSelectionSumEquiv = reportingCurrency.map { Amount(0, it) }
|
||||
private val selectedViewerNodeSumEquiv = selectedViewerNode.bind { selection ->
|
||||
private val selectedViewerNodeSumEquiv = selectedViewerNode.bindOut { selection ->
|
||||
when (selection) {
|
||||
is SingleRowSelection.None -> noSelectionSumEquiv
|
||||
is SingleRowSelection.Selected ->
|
||||
|
@ -1,16 +1,12 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import com.r3corda.client.fxutils.ChosenList
|
||||
import com.r3corda.client.fxutils.bind
|
||||
import com.r3corda.client.fxutils.lift
|
||||
import com.r3corda.client.fxutils.map
|
||||
import com.r3corda.client.fxutils.*
|
||||
import com.r3corda.client.model.*
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.toStringShort
|
||||
import com.r3corda.core.transactions.LedgerTransaction
|
||||
import com.r3corda.core.protocols.StateMachineRunId
|
||||
import com.r3corda.explorer.AmountDiff
|
||||
import com.r3corda.explorer.formatters.AmountFormatter
|
||||
@ -33,7 +29,6 @@ import javafx.scene.layout.BackgroundFill
|
||||
import javafx.scene.layout.CornerRadii
|
||||
import javafx.scene.layout.VBox
|
||||
import javafx.scene.paint.Color
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import tornadofx.View
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
@ -61,20 +56,20 @@ class TransactionViewer: View() {
|
||||
private val contractStatesInputsCountLabel: Label by fxid()
|
||||
private val contractStatesInputStatesTable: TableView<StateNode> 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 contractStatesInputStatesLocalCurrency: TableColumn<StateNode, Currency?> by fxid()
|
||||
private val contractStatesInputStatesAmount: TableColumn<StateNode, Long> by fxid()
|
||||
private val contractStatesInputStatesEquiv: TableColumn<StateNode, Amount<Currency>> by fxid()
|
||||
private val contractStatesInputStatesAmount: TableColumn<StateNode, Long?> by fxid()
|
||||
private val contractStatesInputStatesEquiv: TableColumn<StateNode, Amount<Currency>?> by fxid()
|
||||
|
||||
private val contractStatesOutputsCountLabel: Label by fxid()
|
||||
private val contractStatesOutputStatesTable: TableView<StateNode> 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 contractStatesOutputStatesLocalCurrency: TableColumn<StateNode, Currency?> by fxid()
|
||||
private val contractStatesOutputStatesAmount: TableColumn<StateNode, Long> by fxid()
|
||||
private val contractStatesOutputStatesEquiv: TableColumn<StateNode, Amount<Currency>> by fxid()
|
||||
private val contractStatesOutputStatesAmount: TableColumn<StateNode, Long?> by fxid()
|
||||
private val contractStatesOutputStatesEquiv: TableColumn<StateNode, Amount<Currency>?> by fxid()
|
||||
|
||||
private val signaturesTitledPane: TitledPane by fxid()
|
||||
private val signaturesList: ListView<PublicKey> by fxid()
|
||||
@ -108,7 +103,7 @@ class TransactionViewer: View() {
|
||||
val statusUpdated: ObservableValue<Instant>,
|
||||
val commandTypes: ObservableValue<Collection<Class<CommandData>>>,
|
||||
val totalValueEquiv: ObservableValue<AmountDiff<Currency>?>,
|
||||
val transaction: ObservableValue<LedgerTransaction?>,
|
||||
val transaction: ObservableValue<PartiallyResolvedTransaction?>,
|
||||
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]
|
||||
*/
|
||||
data class StateNode(
|
||||
val transactionState: TransactionState<*>,
|
||||
val state: ObservableValue<PartiallyResolvedTransaction.InputResolution>,
|
||||
val stateRef: StateRef
|
||||
)
|
||||
|
||||
@ -144,12 +139,37 @@ class TransactionViewer: View() {
|
||||
statusUpdated = it.lastUpdate,
|
||||
commandTypes = it.transaction.map {
|
||||
val commands = mutableSetOf<Class<CommandData>>()
|
||||
it?.commands?.forEach {
|
||||
it?.transaction?.tx?.commands?.forEach {
|
||||
commands.add(it.value.javaClass)
|
||||
}
|
||||
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,
|
||||
allEvents = it.allEvents
|
||||
)
|
||||
@ -159,18 +179,20 @@ class TransactionViewer: View() {
|
||||
* The detail panes are only filled out if a transaction is selected
|
||||
*/
|
||||
private val selectedViewerNode = transactionViewTable.singleRowSelection()
|
||||
private val selectedTransaction = selectedViewerNode.bind {
|
||||
private val selectedTransaction = selectedViewerNode.bindOut {
|
||||
when (it) {
|
||||
is SingleRowSelection.None -> null.lift()
|
||||
is SingleRowSelection.Selected -> it.node.transaction
|
||||
}
|
||||
}
|
||||
|
||||
private val inputStateNodes = ChosenList(selectedTransaction.map {
|
||||
if (it == null) {
|
||||
private val inputStateNodes = ChosenList(selectedTransaction.map { transaction ->
|
||||
if (transaction == null) {
|
||||
FXCollections.emptyObservableList<StateNode>()
|
||||
} 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) {
|
||||
FXCollections.emptyObservableList<StateNode>()
|
||||
} else {
|
||||
FXCollections.observableArrayList(it.outputs.mapIndexed { index, transactionState ->
|
||||
StateNode(transactionState, StateRef(it.id, index))
|
||||
FXCollections.observableArrayList(it.transaction.tx.outputs.mapIndexed { index, transactionState ->
|
||||
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) {
|
||||
FXCollections.emptyObservableList<PublicKey>()
|
||||
} else {
|
||||
FXCollections.observableArrayList(it.mustSign)
|
||||
FXCollections.observableArrayList(it.transaction.sigs.map { it.by })
|
||||
}
|
||||
})
|
||||
|
||||
@ -228,52 +251,67 @@ class TransactionViewer: View() {
|
||||
statesCountLabel: Label,
|
||||
statesTable: TableView<StateNode>,
|
||||
statesId: TableColumn<StateNode, String>,
|
||||
statesType: TableColumn<StateNode, Class<out ContractState>>,
|
||||
statesType: TableColumn<StateNode, String>,
|
||||
statesOwner: TableColumn<StateNode, String>,
|
||||
statesLocalCurrency: TableColumn<StateNode, Currency?>,
|
||||
statesAmount: TableColumn<StateNode, Long>,
|
||||
statesEquiv: TableColumn<StateNode, Amount<Currency>>
|
||||
statesAmount: TableColumn<StateNode, Long?>,
|
||||
statesEquiv: TableColumn<StateNode, Amount<Currency>?>
|
||||
) {
|
||||
statesCountLabel.textProperty().bind(Bindings.size(states).map { "$it" })
|
||||
|
||||
Bindings.bindContent(statesTable.items, states)
|
||||
|
||||
val unknownString = "???"
|
||||
|
||||
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 {
|
||||
val state = it.value.transactionState.data
|
||||
if (state is OwnableState) {
|
||||
state.owner.toStringShort().lift()
|
||||
} else {
|
||||
"???".lift()
|
||||
resolvedOrDefault(it.value.state, unknownString) {
|
||||
val contractState = it.state.data
|
||||
if (contractState is OwnableState) {
|
||||
contractState.owner.toStringShort()
|
||||
} else {
|
||||
unknownString
|
||||
}
|
||||
}
|
||||
}
|
||||
statesLocalCurrency.setCellValueFactory {
|
||||
val state = it.value.transactionState.data
|
||||
if (state is Cash.State) {
|
||||
state.amount.token.product.lift()
|
||||
} else {
|
||||
null.lift()
|
||||
resolvedOrDefault<Currency?>(it.value.state, null) {
|
||||
val contractState = it.state.data
|
||||
if (contractState is Cash.State) {
|
||||
contractState.amount.token.product
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
statesAmount.setCellValueFactory {
|
||||
val state = it.value.transactionState.data
|
||||
if (state is Cash.State) {
|
||||
state.amount.quantity.lift()
|
||||
} else {
|
||||
null.lift()
|
||||
resolvedOrDefault<Long?>(it.value.state, null) {
|
||||
val contractState = it.state.data
|
||||
if (contractState is Cash.State) {
|
||||
contractState.amount.quantity
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
statesAmount.cellFactory = NumberFormatter.boringLong.toTableCellFactory()
|
||||
statesEquiv.setCellValueFactory {
|
||||
val state = it.value.transactionState.data
|
||||
if (state is Cash.State) {
|
||||
reportingExchange.map { exchange ->
|
||||
exchange.second(state.amount.withoutIssuer())
|
||||
resolvedOrDefault<ObservableValue<Amount<Currency>?>>(it.value.state, null.lift()) {
|
||||
val contractState = it.state.data
|
||||
if (contractState is Cash.State) {
|
||||
reportingExchange.map { exchange ->
|
||||
exchange.second(contractState.amount.withoutIssuer())
|
||||
}
|
||||
} else {
|
||||
null.lift()
|
||||
}
|
||||
} else {
|
||||
null.lift()
|
||||
}
|
||||
}.bind { it }
|
||||
|
||||
}
|
||||
statesEquiv.cellFactory = AmountFormatter.boring.toTableCellFactory()
|
||||
}
|
||||
@ -364,7 +402,7 @@ class TransactionViewer: View() {
|
||||
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"}"
|
||||
})
|
||||
}
|
||||
@ -376,20 +414,21 @@ class TransactionViewer: View() {
|
||||
private fun calculateTotalEquiv(
|
||||
identity: Party,
|
||||
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
|
||||
transaction: LedgerTransaction?): AmountDiff<Currency>? {
|
||||
if (transaction == null) {
|
||||
inputs: List<StateAndRef<ContractState>>?,
|
||||
outputs: List<TransactionState<ContractState>>): AmountDiff<Currency>? {
|
||||
if (inputs == null) {
|
||||
return null
|
||||
}
|
||||
var sum = 0L
|
||||
val (reportingCurrency, exchange) = reportingCurrencyExchange
|
||||
val publicKey = identity.owningKey
|
||||
transaction.inputs.forEach {
|
||||
inputs.forEach {
|
||||
val contractState = it.state.data
|
||||
if (contractState is Cash.State && publicKey == contractState.owner) {
|
||||
sum -= exchange(contractState.amount.withoutIssuer()).quantity
|
||||
}
|
||||
}
|
||||
transaction.outputs.forEach {
|
||||
outputs.forEach {
|
||||
val contractState = it.data
|
||||
if (contractState is Cash.State && publicKey == contractState.owner) {
|
||||
sum += exchange(contractState.amount.withoutIssuer()).quantity
|
||||
@ -398,3 +437,15 @@ private fun calculateTotalEquiv(
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package com.r3corda.node.services.monitor
|
||||
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.transactions.LedgerTransaction
|
||||
import com.r3corda.core.protocols.StateMachineRunId
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.node.utilities.AddOrRemove
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
@ -11,8 +11,8 @@ import java.util.*
|
||||
* Events triggered by changes in the node, and sent to monitoring client(s).
|
||||
*/
|
||||
sealed class ServiceToClientEvent(val time: Instant) {
|
||||
class Transaction(time: Instant, val transaction: LedgerTransaction) : ServiceToClientEvent(time) {
|
||||
override fun toString() = "Transaction(${transaction.commands})"
|
||||
class Transaction(time: Instant, val transaction: SignedTransaction) : ServiceToClientEvent(time) {
|
||||
override fun toString() = "Transaction(${transaction.tx.commands})"
|
||||
}
|
||||
class OutputState(
|
||||
time: Instant,
|
||||
@ -26,7 +26,7 @@ sealed class ServiceToClientEvent(val time: Instant) {
|
||||
val id: StateMachineRunId,
|
||||
val label: String,
|
||||
val addOrRemove: AddOrRemove
|
||||
) : ServiceToClientEvent(time) {
|
||||
) : ServiceToClientEvent(time) {
|
||||
override fun toString() = "StateMachine($label, ${addOrRemove.name})"
|
||||
}
|
||||
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) {
|
||||
override fun toString() = "TransactionBuild($state)"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
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)"
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ import com.r3corda.core.node.services.Vault
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
import com.r3corda.core.protocols.StateMachineRunId
|
||||
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.utilities.loggerFor
|
||||
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) }
|
||||
|
||||
// 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) }
|
||||
smm.changes.subscribe { change ->
|
||||
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))
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun notifyTransaction(transaction: LedgerTransaction)
|
||||
internal fun notifyTransaction(transaction: SignedTransaction)
|
||||
= notifyEvent(ServiceToClientEvent.Transaction(Instant.now(), transaction))
|
||||
|
||||
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))
|
||||
return TransactionBuildResult.ProtocolStarted(
|
||||
smm.add(BroadcastTransactionProtocol.TOPIC, protocol).id,
|
||||
tx.tx.toLedgerTransaction(services),
|
||||
tx,
|
||||
"Cash payment transaction generated"
|
||||
)
|
||||
} catch(ex: InsufficientBalanceException) {
|
||||
@ -204,7 +204,7 @@ class NodeMonitorService(services: ServiceHubInternal, val smm: StateMachineMana
|
||||
val protocol = FinalityProtocol(tx, setOf(req), participants)
|
||||
return TransactionBuildResult.ProtocolStarted(
|
||||
smm.add(BroadcastTransactionProtocol.TOPIC, protocol).id,
|
||||
tx.tx.toLedgerTransaction(services),
|
||||
tx,
|
||||
"Cash destruction transaction generated"
|
||||
)
|
||||
} catch (ex: InsufficientBalanceException) {
|
||||
@ -223,7 +223,7 @@ class NodeMonitorService(services: ServiceHubInternal, val smm: StateMachineMana
|
||||
val protocol = BroadcastTransactionProtocol(tx, setOf(req), setOf(req.recipient))
|
||||
return TransactionBuildResult.ProtocolStarted(
|
||||
smm.add(BroadcastTransactionProtocol.TOPIC, protocol).id,
|
||||
tx.tx.toLedgerTransaction(services),
|
||||
tx,
|
||||
"Cash issuance completed"
|
||||
)
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ class NodeMonitorServiceTests {
|
||||
// Check the returned event is correct
|
||||
val tx = (event.state as TransactionBuildResult.ProtocolStarted).transaction
|
||||
assertNotNull(tx)
|
||||
assertEquals(expectedState, tx!!.outputs.single().data)
|
||||
assertEquals(expectedState, tx!!.tx.outputs.single().data)
|
||||
},
|
||||
expect { event: ServiceToClientEvent.OutputState ->
|
||||
// Check the generated state is correct
|
||||
@ -203,8 +203,8 @@ class NodeMonitorServiceTests {
|
||||
}
|
||||
),
|
||||
expect { event: ServiceToClientEvent.Transaction ->
|
||||
require(event.transaction.mustSign.size == 1)
|
||||
event.transaction.mustSign.containsAll(
|
||||
require(event.transaction.sigs.size == 1)
|
||||
event.transaction.sigs.map { it.by }.containsAll(
|
||||
listOf(
|
||||
monitorServiceNode.services.storageService.myLegalIdentity.owningKey
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user