From 43d18d46bb144447a233eae47a1523b1ea9ff45b Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Fri, 23 Sep 2016 16:44:33 +0100 Subject: [PATCH] Add PartiallyResolvedTransaction to client --- .../r3corda/client/NodeMonitorClientTests.kt | 12 +- .../client/fxutils/ObservableMapBindings.kt | 22 +++ .../client/fxutils/ObservableUtilities.kt | 20 ++- .../model/GatheredTransactionDataModel.kt | 148 +++++++++++++--- .../com/r3corda/explorer/views/CashViewer.kt | 2 +- .../explorer/views/TransactionViewer.kt | 159 ++++++++++++------ .../r3corda/node/services/monitor/Events.kt | 11 +- .../services/monitor/NodeMonitorService.kt | 12 +- .../node/services/NodeMonitorServiceTests.kt | 6 +- 9 files changed, 292 insertions(+), 100 deletions(-) create mode 100644 client/src/main/kotlin/com/r3corda/client/fxutils/ObservableMapBindings.kt diff --git a/client/src/integration-test/kotlin/com/r3corda/client/NodeMonitorClientTests.kt b/client/src/integration-test/kotlin/com/r3corda/client/NodeMonitorClientTests.kt index 06e7177d55..77c75216cc 100644 --- a/client/src/integration-test/kotlin/com/r3corda/client/NodeMonitorClientTests.kt +++ b/client/src/integration-test/kotlin/com/r3corda/client/NodeMonitorClientTests.kt @@ -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)) diff --git a/client/src/main/kotlin/com/r3corda/client/fxutils/ObservableMapBindings.kt b/client/src/main/kotlin/com/r3corda/client/fxutils/ObservableMapBindings.kt new file mode 100644 index 0000000000..8c4cbd09d0 --- /dev/null +++ b/client/src/main/kotlin/com/r3corda/client/fxutils/ObservableMapBindings.kt @@ -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 ObservableMap.getObservableValue(key: K): ObservableValue { + val property = SimpleObjectProperty(get(key)) + addListener { change: MapChangeListener.Change -> + 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 +} diff --git a/client/src/main/kotlin/com/r3corda/client/fxutils/ObservableUtilities.kt b/client/src/main/kotlin/com/r3corda/client/fxutils/ObservableUtilities.kt index d88c886616..a89ed0dca1 100644 --- a/client/src/main/kotlin/com/r3corda/client/fxutils/ObservableUtilities.kt +++ b/client/src/main/kotlin/com/r3corda/client/fxutils/ObservableUtilities.kt @@ -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).lift( * val person: ObservableValue = (..) * val personHeight: ObservableValue = person.bind { it.height } */ -fun ObservableValue.bind(function: (A) -> ObservableValue): ObservableValue = - // We cast here to enforce variance, flatMap should be covariant +fun ObservableValue.bind(function: (A) -> ObservableValue): ObservableValue = + 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 ObservableValue.bindOut(function: (A) -> ObservableValue): ObservableValue = @Suppress("UNCHECKED_CAST") EasyBind.monadic(this).flatMap(function as (A) -> ObservableValue) @@ -71,7 +77,7 @@ fun ObservableValue.bind(function: (A) -> ObservableValue): * * val filteredPeople: ObservableList = people.filter(filterCriterion.map(filterFunction)) */ -fun ObservableList.filter(predicate: ObservableValue Boolean>): ObservableList { +fun ObservableList.filter(predicate: ObservableValue<(A) -> Boolean>): ObservableList { // We cast here to enforce variance, FilteredList should be covariant @Suppress("UNCHECKED_CAST") return FilteredList(this as ObservableList).apply { @@ -101,4 +107,10 @@ fun ObservableList.fold(initial: B, folderFunction: (B, A) -> B): * val people: ObservableList = (..) * val heights: ObservableList = people.map(Person::height).flatten() */ -fun ObservableList>.flatten(): ObservableList = FlattenedList(this) +fun ObservableList>.flatten(): ObservableList = FlattenedList(this) + +/** + * val people: List = listOf(alice, bob) + * val heights: ObservableList = people.map(Person::height).sequence() + */ +fun List>.sequence(): ObservableList = FlattenedList(FXCollections.observableArrayList(this)) diff --git a/client/src/main/kotlin/com/r3corda/client/model/GatheredTransactionDataModel.kt b/client/src/main/kotlin/com/r3corda/client/model/GatheredTransactionDataModel.kt index 96f4d60342..35880b11d1 100644 --- a/client/src/main/kotlin/com/r3corda/client/model/GatheredTransactionDataModel.kt +++ b/client/src/main/kotlin/com/r3corda/client/model/GatheredTransactionDataModel.kt @@ -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 val uuid: ObservableValue val protocolStatus: ObservableValue val stateMachineStatus: ObservableValue - val transaction: ObservableValue + val transaction: ObservableValue val status: ObservableValue val lastUpdate: ObservableValue val allEvents: ObservableList } +/** + * [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> +) { + val id = transaction.id + sealed class InputResolution(val stateRef: StateRef) { + class Unresolved(stateRef: StateRef) : InputResolution(stateRef) + class Resolved(val stateAndRef: StateAndRef) : InputResolution(stateAndRef.ref) + } + + companion object { + fun fromSignedTransaction( + transaction: SignedTransaction, + transactions: ObservableMap + ) = 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 = SimpleObjectProperty(null), override val stateMachineStatus: SimpleObjectProperty = SimpleObjectProperty(null), override val protocolStatus: SimpleObjectProperty = SimpleObjectProperty(null), - override val transaction: SimpleObjectProperty = SimpleObjectProperty(null), + override val transaction: SimpleObjectProperty = SimpleObjectProperty(null), override val status: SimpleObjectProperty = SimpleObjectProperty(null), override val lastUpdate: SimpleObjectProperty, override val allEvents: ObservableList = 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 = - serviceToClient.foldToObservableList( - initialAccumulator = Unit, - folderFun = { serviceToClientEvent, _unit, transactionStates -> - return@foldToObservableList when (serviceToClientEvent) { + serviceToClient.foldToObservableList>( + initialAccumulator = FXCollections.observableHashMap(), + 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, 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? { + 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 mergeField( + from: GatheredTransactionDataWritable, + to: GatheredTransactionDataWritable, + field: KProperty1>) { + 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) + } + } } } diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/views/CashViewer.kt b/explorer/src/main/kotlin/com/r3corda/explorer/views/CashViewer.kt index 26fb675534..35ecf2837f 100644 --- a/explorer/src/main/kotlin/com/r3corda/explorer/views/CashViewer.kt +++ b/explorer/src/main/kotlin/com/r3corda/explorer/views/CashViewer.kt @@ -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 -> diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/views/TransactionViewer.kt b/explorer/src/main/kotlin/com/r3corda/explorer/views/TransactionViewer.kt index 987d510ce8..18c3cc1b25 100644 --- a/explorer/src/main/kotlin/com/r3corda/explorer/views/TransactionViewer.kt +++ b/explorer/src/main/kotlin/com/r3corda/explorer/views/TransactionViewer.kt @@ -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 by fxid() private val contractStatesInputStatesId: TableColumn by fxid() - private val contractStatesInputStatesType: TableColumn> by fxid() + private val contractStatesInputStatesType: TableColumn by fxid() private val contractStatesInputStatesOwner: TableColumn by fxid() private val contractStatesInputStatesLocalCurrency: TableColumn by fxid() - private val contractStatesInputStatesAmount: TableColumn by fxid() - private val contractStatesInputStatesEquiv: TableColumn> by fxid() + private val contractStatesInputStatesAmount: TableColumn by fxid() + private val contractStatesInputStatesEquiv: TableColumn?> by fxid() private val contractStatesOutputsCountLabel: Label by fxid() private val contractStatesOutputStatesTable: TableView by fxid() private val contractStatesOutputStatesId: TableColumn by fxid() - private val contractStatesOutputStatesType: TableColumn> by fxid() + private val contractStatesOutputStatesType: TableColumn by fxid() private val contractStatesOutputStatesOwner: TableColumn by fxid() private val contractStatesOutputStatesLocalCurrency: TableColumn by fxid() - private val contractStatesOutputStatesAmount: TableColumn by fxid() - private val contractStatesOutputStatesEquiv: TableColumn> by fxid() + private val contractStatesOutputStatesAmount: TableColumn by fxid() + private val contractStatesOutputStatesEquiv: TableColumn?> by fxid() private val signaturesTitledPane: TitledPane by fxid() private val signaturesList: ListView by fxid() @@ -108,7 +103,7 @@ class TransactionViewer: View() { val statusUpdated: ObservableValue, val commandTypes: ObservableValue>>, val totalValueEquiv: ObservableValue?>, - val transaction: ObservableValue, + val transaction: ObservableValue, val allEvents: ObservableList ) @@ -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, val stateRef: StateRef ) @@ -144,12 +139,37 @@ class TransactionViewer: View() { statusUpdated = it.lastUpdate, commandTypes = it.transaction.map { val commands = mutableSetOf>() - 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?>() + } 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>?, state: StateAndRef? -> + 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() } 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() } 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() } else { - FXCollections.observableArrayList(it.mustSign) + FXCollections.observableArrayList(it.transaction.sigs.map { it.by }) } }) @@ -228,52 +251,67 @@ class TransactionViewer: View() { statesCountLabel: Label, statesTable: TableView, statesId: TableColumn, - statesType: TableColumn>, + statesType: TableColumn, statesOwner: TableColumn, statesLocalCurrency: TableColumn, - statesAmount: TableColumn, - statesEquiv: TableColumn> + statesAmount: TableColumn, + statesEquiv: TableColumn?> ) { 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(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(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?>>(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) -> Amount>, - transaction: LedgerTransaction?): AmountDiff? { - if (transaction == null) { + inputs: List>?, + outputs: List>): AmountDiff? { + 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 resolvedOrDefault( + state: ObservableValue, + default: A, + resolved: (StateAndRef<*>) -> A +): ObservableValue { + return state.map { + when (it) { + is PartiallyResolvedTransaction.InputResolution.Unresolved -> default + is PartiallyResolvedTransaction.InputResolution.Resolved -> resolved(it.stateAndRef) + } + } +} diff --git a/node/src/main/kotlin/com/r3corda/node/services/monitor/Events.kt b/node/src/main/kotlin/com/r3corda/node/services/monitor/Events.kt index 5fb3102225..6863005e26 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/monitor/Events.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/monitor/Events.kt @@ -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)" } diff --git a/node/src/main/kotlin/com/r3corda/node/services/monitor/NodeMonitorService.kt b/node/src/main/kotlin/com/r3corda/node/services/monitor/NodeMonitorService.kt index cb513e9931..508ed471ba 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/monitor/NodeMonitorService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/monitor/NodeMonitorService.kt @@ -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" ) } diff --git a/node/src/test/kotlin/com/r3corda/node/services/NodeMonitorServiceTests.kt b/node/src/test/kotlin/com/r3corda/node/services/NodeMonitorServiceTests.kt index e2adce8ce9..4e0f42e566 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeMonitorServiceTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeMonitorServiceTests.kt @@ -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 )