From 26aed70e24fef92ec56fec61050027fcfa366fb5 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Fri, 2 Sep 2016 17:18:31 +0100 Subject: [PATCH] explorer: Add input/output states and signers to tx screen --- .../model/GatheredTransactionDataModel.kt | 6 +- .../kotlin/com/r3corda/explorer/AmountDiff.kt | 26 ++ .../r3corda/explorer/ui/ListViewUtilities.kt | 19 ++ .../explorer/views/TransactionViewer.kt | 226 ++++++++++++++++-- .../explorer/views/TransactionViewer.fxml | 194 +++++++-------- 5 files changed, 332 insertions(+), 139 deletions(-) create mode 100644 explorer/src/main/kotlin/com/r3corda/explorer/AmountDiff.kt create mode 100644 explorer/src/main/kotlin/com/r3corda/explorer/ui/ListViewUtilities.kt 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 58355f3330..f36f423bd2 100644 --- a/client/src/main/kotlin/com/r3corda/client/model/GatheredTransactionDataModel.kt +++ b/client/src/main/kotlin/com/r3corda/client/model/GatheredTransactionDataModel.kt @@ -1,7 +1,7 @@ package com.r3corda.client.model import com.r3corda.client.fxutils.foldToObservableList -import com.r3corda.core.transactions.SignedTransaction +import com.r3corda.core.transactions.LedgerTransaction import com.r3corda.node.services.monitor.ServiceToClientEvent import com.r3corda.node.services.monitor.TransactionBuildResult import com.r3corda.node.utilities.AddOrRemove @@ -18,7 +18,7 @@ interface GatheredTransactionData { val uuid: ObservableValue val protocolName: ObservableValue val protocolStatus: ObservableValue - val transaction: ObservableValue + val transaction: ObservableValue val status: ObservableValue val lastUpdate: ObservableValue val allEvents: ObservableList @@ -42,7 +42,7 @@ data class GatheredTransactionDataWritable( override val uuid: SimpleObjectProperty = SimpleObjectProperty(null), override val protocolName: 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() diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/AmountDiff.kt b/explorer/src/main/kotlin/com/r3corda/explorer/AmountDiff.kt new file mode 100644 index 0000000000..968f9c2480 --- /dev/null +++ b/explorer/src/main/kotlin/com/r3corda/explorer/AmountDiff.kt @@ -0,0 +1,26 @@ +package com.r3corda.explorer + +import com.r3corda.core.contracts.Amount + +enum class Positivity { + Positive, + Negative +} + +val Positivity.sign: String get() = when (this) { + Positivity.Positive -> "" + Positivity.Negative -> "-" +} + +data class AmountDiff( + val positivity: Positivity, + val amount: Amount +) { + companion object { + fun fromLong(quantity: Long, token: T) = + AmountDiff( + positivity = if (quantity < 0) Positivity.Negative else Positivity.Positive, + amount = Amount(Math.abs(quantity), token) + ) + } +} diff --git a/explorer/src/main/kotlin/com/r3corda/explorer/ui/ListViewUtilities.kt b/explorer/src/main/kotlin/com/r3corda/explorer/ui/ListViewUtilities.kt new file mode 100644 index 0000000000..17897546d4 --- /dev/null +++ b/explorer/src/main/kotlin/com/r3corda/explorer/ui/ListViewUtilities.kt @@ -0,0 +1,19 @@ +package com.r3corda.explorer.ui + +import com.r3corda.explorer.formatters.Formatter +import javafx.scene.control.ListCell +import javafx.scene.control.ListView +import javafx.util.Callback + +fun Formatter.toListCellFactory() = Callback, ListCell> { + object : ListCell() { + override fun updateItem(value: T?, empty: Boolean) { + super.updateItem(value, empty) + text = if (value == null || empty) { + "" + } else { + format(value) + } + } + } +} 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 b4499eb669..eb7cba6ac0 100644 --- a/explorer/src/main/kotlin/com/r3corda/explorer/views/TransactionViewer.kt +++ b/explorer/src/main/kotlin/com/r3corda/explorer/views/TransactionViewer.kt @@ -7,12 +7,18 @@ import com.r3corda.core.contracts.Amount import com.r3corda.core.contracts.CommandData import com.r3corda.core.contracts.withoutIssuer import com.r3corda.core.transactions.SignedTransaction +import com.r3corda.core.contracts.* +import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.toStringShort +import com.r3corda.core.transactions.LedgerTransaction +import com.r3corda.explorer.AmountDiff import com.r3corda.explorer.formatters.AmountFormatter +import com.r3corda.explorer.formatters.Formatter +import com.r3corda.explorer.formatters.NumberFormatter +import com.r3corda.explorer.model.IdentityModel import com.r3corda.explorer.model.ReportingCurrencyModel -import com.r3corda.explorer.ui.SingleRowSelection -import com.r3corda.explorer.ui.setColumnPrefWidthPolicy -import com.r3corda.explorer.ui.singleRowSelection -import com.r3corda.explorer.ui.toTableCellFactory +import com.r3corda.explorer.sign +import com.r3corda.explorer.ui.* import com.r3corda.node.services.monitor.ServiceToClientEvent import javafx.beans.binding.Bindings import javafx.beans.property.ReadOnlyObjectWrapper @@ -20,10 +26,8 @@ import javafx.beans.value.ObservableValue import javafx.collections.FXCollections import javafx.collections.ObservableList import javafx.geometry.Insets -import javafx.scene.control.Label -import javafx.scene.control.TableCell -import javafx.scene.control.TableColumn -import javafx.scene.control.TableView +import javafx.scene.Node +import javafx.scene.control.* import javafx.scene.layout.Background import javafx.scene.layout.BackgroundFill import javafx.scene.layout.CornerRadii @@ -31,12 +35,15 @@ 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 import java.util.* class TransactionViewer: View() { override val root: VBox by fxml() + val topSplitPane: SplitPane by fxid("TopSplitPane") + // Top half (transactions table) private val transactionViewTable: TableView by fxid("TransactionViewTable") private val transactionViewTransactionId: TableColumn by fxid("TransactionViewTransactionId") @@ -44,9 +51,31 @@ class TransactionViewer: View() { private val transactionViewTransactionStatus: TableColumn> by fxid("TransactionViewTransactionStatus") private val transactionViewStatusUpdated: TableColumn by fxid("TransactionViewStatusUpdated") private val transactionViewCommandTypes: TableColumn by fxid("TransactionViewCommandTypes") - private val transactionViewTotalValueEquiv: TableColumn> by fxid("TransactionViewTotalValueEquiv") + private val transactionViewTotalValueEquiv: TableColumn> by fxid("TransactionViewTotalValueEquiv") // Bottom half (details) + private val contractStatesTitledPane: TitledPane by fxid("ContractStatesTitledPane") + + private val contractStatesInputStatesTable: TableView by fxid("ContractStatesInputStatesTable") + private val contractStatesInputStatesId: TableColumn by fxid("ContractStatesInputStatesId") + private val contractStatesInputStatesType: TableColumn> by fxid("ContractStatesInputStatesType") + private val contractStatesInputStatesOwner: TableColumn by fxid("ContractStatesInputStatesOwner") + private val contractStatesInputStatesLocalCurrency: TableColumn by fxid("ContractStatesInputStatesLocalCurrency") + private val contractStatesInputStatesAmount: TableColumn by fxid("ContractStatesInputStatesAmount") + private val contractStatesInputStatesEquiv: TableColumn> by fxid("ContractStatesInputStatesEquiv") + + private val contractStatesOutputStatesTable: TableView by fxid("ContractStatesOutputStatesTable") + private val contractStatesOutputStatesId: TableColumn by fxid("ContractStatesOutputStatesId") + private val contractStatesOutputStatesType: TableColumn> by fxid("ContractStatesOutputStatesType") + private val contractStatesOutputStatesOwner: TableColumn by fxid("ContractStatesOutputStatesOwner") + private val contractStatesOutputStatesLocalCurrency: TableColumn by fxid("ContractStatesOutputStatesLocalCurrency") + private val contractStatesOutputStatesAmount: TableColumn by fxid("ContractStatesOutputStatesAmount") + private val contractStatesOutputStatesEquiv: TableColumn> by fxid("ContractStatesOutputStatesEquiv") + + private val signaturesTitledPane: TitledPane by fxid("SignaturesTitledPane") + private val signaturesList: ListView by fxid("SignaturesList") + + private val lowLevelEventsTitledPane: TitledPane by fxid("LowLevelEventsTitledPane") private val lowLevelEventsTable: TableView by fxid("LowLevelEventsTable") private val lowLevelEventsTimestamp: TableColumn by fxid("LowLevelEventsTimestamp") private val lowLevelEventsEvent: TableColumn by fxid("LowLevelEventsEvent") @@ -56,17 +85,24 @@ class TransactionViewer: View() { private val reportingExchange: ObservableValue) -> Amount>> by observableValue(ReportingCurrencyModel::reportingExchange) + private val myIdentity: ObservableValue by observableValue(IdentityModel::myIdentity) + data class ViewerNode( val transactionId: ObservableValue>, val originator: ObservableValue, val transactionStatus: ObservableValue>, val statusUpdated: ObservableValue, val commandTypes: ObservableValue>>, - val viewTotalValueEquiv: ObservableValue?>, - val transaction: ObservableValue, + val totalValueEquiv: ObservableValue?>, + val transaction: ObservableValue, val allEvents: ObservableList ) + data class StateNode( + val transactionState: TransactionState<*>, + val stateRef: StateRef + ) + private val viewerNodes = EasyBind.map(gatheredTransactionDataList) { ViewerNode( transactionId = EasyBind.combine(it.fiberId, it.uuid) { fiberId, uuid -> Pair(fiberId, uuid) }, @@ -83,13 +119,13 @@ class TransactionViewer: View() { statusUpdated = it.lastUpdate, commandTypes = EasyBind.map(it.transaction) { val commands = mutableSetOf>() - it?.tx?.commands?.forEach { + it?.commands?.forEach { commands.add(it.value.javaClass) } commands }, - viewTotalValueEquiv = EasyBind.combine(reportingExchange, it.transaction) { exchange, transaction -> - transaction?.let { calculateTotalEquiv(exchange.first, exchange.second, transaction) } + totalValueEquiv = EasyBind.combine(myIdentity, reportingExchange, it.transaction) { identity, exchange, transaction -> + transaction?.let { calculateTotalEquiv(setOf(identity.owningKey), exchange.first, exchange.second, transaction) } }, transaction = it.transaction, allEvents = it.allEvents @@ -97,16 +133,60 @@ class TransactionViewer: View() { } private fun calculateTotalEquiv( + relevantPublicKeys: Set, reportingCurrency: Currency, exchange: (Amount) -> Amount, - transaction: SignedTransaction): Amount { - return transaction.tx.outputs.map { it.data }.filterIsInstance().fold( - initial = Amount(0, reportingCurrency), - operation = { sum, cashState -> sum + exchange(cashState.amount.withoutIssuer()) } - ) + transaction: LedgerTransaction): AmountDiff { + var sum = 0L + transaction.inputs.forEach { + val contractState = it.state.data + if (contractState is Cash.State && relevantPublicKeys.contains(contractState.owner)) { + sum -= exchange(contractState.amount.withoutIssuer()).quantity + } + } + transaction.outputs.forEach { + val contractState = it.data + if (contractState is Cash.State && relevantPublicKeys.contains(contractState.owner)) { + sum += exchange(contractState.amount.withoutIssuer()).quantity + } + } + return AmountDiff.fromLong(sum, reportingCurrency) } private val selectedViewerNode = transactionViewTable.singleRowSelection() + private val selectedTransaction = EasyBind.monadic(selectedViewerNode).flatMap> { + when (it) { + is SingleRowSelection.None -> ReadOnlyObjectWrapper(null) + is SingleRowSelection.Selected -> it.node.transaction + } + } + + private val inputStateNodes = ChosenList(EasyBind.map(selectedTransaction) { + if (it == null) { + FXCollections.emptyObservableList() + } else { + FXCollections.observableArrayList(it.inputs.map { StateNode(it.state, it.ref) }) + } + }) + + private val outputStateNodes = ChosenList(EasyBind.map(selectedTransaction) { + if (it == null) { + FXCollections.emptyObservableList() + } else { + FXCollections.observableArrayList(it.outputs.mapIndexed { index, transactionState -> + StateNode(transactionState, StateRef(it.id, index)) + }) + } + }) + + private val signatures = ChosenList(EasyBind.map(selectedTransaction) { + if (it == null) { + FXCollections.emptyObservableList() + } else { + FXCollections.observableArrayList(it.mustSign) + } + }) + private val noLowLevelEvents = FXCollections.emptyObservableList() private val lowLevelEvents = ChosenList(EasyBind.map(selectedViewerNode) { @@ -116,7 +196,78 @@ class TransactionViewer: View() { } }) + private val allNodesShown = FXCollections.observableArrayList( + transactionViewTable, + contractStatesTitledPane, + signaturesTitledPane, + lowLevelEventsTitledPane + ) + private val onlyTransactionsTableShown = FXCollections.observableArrayList( + transactionViewTable + ) + private val topSplitPaneNodesShown = ChosenList( + EasyBind.map(selectedViewerNode) { selection -> + if (selection is SingleRowSelection.None<*>) { + onlyTransactionsTableShown + } else { + allNodesShown + } + }) + + private fun wireUpStatesTable( + states: ObservableList, + statesTable: TableView, + statesId: TableColumn, + statesType: TableColumn>, + statesOwner: TableColumn, + statesLocalCurrency: TableColumn, + statesAmount: TableColumn, + statesEquiv: TableColumn> + ) { + Bindings.bindContent(statesTable.items, states) + + statesId.setCellValueFactory { ReadOnlyObjectWrapper(it.value.stateRef.toString()) } + statesType.setCellValueFactory { ReadOnlyObjectWrapper(it.value.transactionState.data.javaClass) } + statesOwner.setCellValueFactory { + val state = it.value.transactionState.data + if (state is OwnableState) { + ReadOnlyObjectWrapper(state.owner.toStringShort()) + } else { + ReadOnlyObjectWrapper("???") + } + } + statesLocalCurrency.setCellValueFactory { + val state = it.value.transactionState.data + if (state is Cash.State) { + ReadOnlyObjectWrapper(state.amount.token.product) + } else { + ReadOnlyObjectWrapper(null) + } + } + statesAmount.setCellValueFactory { + val state = it.value.transactionState.data + if (state is Cash.State) { + ReadOnlyObjectWrapper(state.amount.quantity) + } else { + ReadOnlyObjectWrapper(null) + } + } + statesAmount.setCellFactory(NumberFormatter.longComma.toTableCellFactory()) + statesEquiv.setCellValueFactory { + val state = it.value.transactionState.data + if (state is Cash.State) { + EasyBind.map(reportingExchange) { exchange -> + exchange.second(state.amount.withoutIssuer()) + } + } else { + ReadOnlyObjectWrapper(null) + } + } + } + init { + Bindings.bindContent(topSplitPane.items, topSplitPaneNodesShown) + // Transaction table Bindings.bindContent(transactionViewTable.items, viewerNodes) @@ -155,7 +306,7 @@ class TransactionViewer: View() { null -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY) } label.background = Background(backgroundFill) - label.text = if (value.first == null && value.second == null){ + label.text = if (value.first == null && value.second == null) { "???" } else { (value.first?.toString() ?: "") + (value.second?.let { "[${it.toString()}]" } ?: "") @@ -169,8 +320,39 @@ class TransactionViewer: View() { transactionViewCommandTypes.setCellValueFactory { EasyBind.map(it.value.commandTypes) { it.map { it.simpleName }.joinToString(",") } } - transactionViewTotalValueEquiv.setCellValueFactory> { it.value.viewTotalValueEquiv } - transactionViewTotalValueEquiv.cellFactory = AmountFormatter.comma.toTableCellFactory() + transactionViewTotalValueEquiv.setCellValueFactory> { it.value.totalValueEquiv } + transactionViewTotalValueEquiv.cellFactory = object : Formatter> { + override fun format(value: AmountDiff) = + "${value.positivity.sign}${AmountFormatter.comma.format(value.amount)}" + }.toTableCellFactory() + + // Contract states + wireUpStatesTable( + inputStateNodes, + contractStatesInputStatesTable, + contractStatesInputStatesId, + contractStatesInputStatesType, + contractStatesInputStatesOwner, + contractStatesInputStatesLocalCurrency, + contractStatesInputStatesAmount, + contractStatesInputStatesEquiv + ) + wireUpStatesTable( + outputStateNodes, + contractStatesOutputStatesTable, + contractStatesOutputStatesId, + contractStatesOutputStatesType, + contractStatesOutputStatesOwner, + contractStatesOutputStatesLocalCurrency, + contractStatesOutputStatesAmount, + contractStatesOutputStatesEquiv + ) + + // Signatures + Bindings.bindContent(signaturesList.items, signatures) + signaturesList.cellFactory = object : Formatter { + override fun format(value: PublicKey) = value.toStringShort() + }.toListCellFactory() // Low level events Bindings.bindContent(lowLevelEventsTable.items, lowLevelEvents) diff --git a/explorer/src/main/resources/com/r3corda/explorer/views/TransactionViewer.fxml b/explorer/src/main/resources/com/r3corda/explorer/views/TransactionViewer.fxml index 3cecf1e2e4..d79683b90a 100644 --- a/explorer/src/main/resources/com/r3corda/explorer/views/TransactionViewer.fxml +++ b/explorer/src/main/resources/com/r3corda/explorer/views/TransactionViewer.fxml @@ -2,8 +2,8 @@ - + @@ -11,7 +11,6 @@ - @@ -40,7 +39,7 @@ - + @@ -55,119 +54,86 @@ - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +