diff --git a/electrum/gui/qml/components/ConfirmTxDialog.qml b/electrum/gui/qml/components/ConfirmTxDialog.qml index cecf21fec..48cd3ed6f 100644 --- a/electrum/gui/qml/components/ConfirmTxDialog.qml +++ b/electrum/gui/qml/components/ConfirmTxDialog.qml @@ -183,22 +183,53 @@ ElDialog { iconStyle: InfoTextArea.IconStyle.Warn } - Label { - text: qsTr('Outputs') + ToggleLabel { + id: inputs_label Layout.columnSpan: 2 + Layout.topMargin: constants.paddingMedium + + labelText: qsTr('Inputs (%1)').arg(finalizer.inputs.length) color: Material.accentColor } Repeater { - model: finalizer.outputs + model: inputs_label.collapsed + ? undefined + : finalizer.inputs + delegate: TxInput { + Layout.columnSpan: 2 + Layout.fillWidth: true + + idx: index + model: modelData + } + } + + ToggleLabel { + id: outputs_label + Layout.columnSpan: 2 + Layout.topMargin: constants.paddingMedium + + labelText: qsTr('Outputs (%1)').arg(finalizer.outputs.length) + color: Material.accentColor + } + + Repeater { + model: outputs_label.collapsed + ? undefined + : finalizer.outputs delegate: TxOutput { Layout.columnSpan: 2 Layout.fillWidth: true allowShare: false + allowClickAddress: false + + idx: index model: modelData } } + } } diff --git a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml index 35db7132a..a7cf7a963 100644 --- a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml +++ b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml @@ -176,23 +176,55 @@ ElDialog { iconStyle: InfoTextArea.IconStyle.Warn } - Label { + ToggleLabel { + id: inputs_label + Layout.columnSpan: 2 + Layout.topMargin: constants.paddingMedium + visible: cpfpfeebumper.valid - text: qsTr('Outputs') + labelText: qsTr('Inputs (%1)').arg(cpfpfeebumper.inputs.length) + color: Material.accentColor + } + + Repeater { + model: inputs_label.collapsed || !inputs_label.visible + ? undefined + : cpfpfeebumper.inputs + delegate: TxInput { + Layout.columnSpan: 2 + Layout.fillWidth: true + + idx: index + model: modelData + } + } + + ToggleLabel { + id: outputs_label Layout.columnSpan: 2 + Layout.topMargin: constants.paddingMedium + + visible: cpfpfeebumper.valid + labelText: qsTr('Outputs (%1)').arg(cpfpfeebumper.outputs.length) color: Material.accentColor } Repeater { - model: cpfpfeebumper.valid ? cpfpfeebumper.outputs : [] - delegate: TxOutput { + model: outputs_label.collapsed || !outputs_label.visible + ? undefined + : cpfpfeebumper.outputs + delegate: TxOutput { Layout.columnSpan: 2 Layout.fillWidth: true allowShare: false + allowClickAddress: false + + idx: index model: modelData } } + } } diff --git a/electrum/gui/qml/components/RbfBumpFeeDialog.qml b/electrum/gui/qml/components/RbfBumpFeeDialog.qml index 28229f29a..a66122d35 100644 --- a/electrum/gui/qml/components/RbfBumpFeeDialog.qml +++ b/electrum/gui/qml/components/RbfBumpFeeDialog.qml @@ -188,23 +188,55 @@ ElDialog { text: rbffeebumper.warning } - Label { + ToggleLabel { + id: inputs_label + Layout.columnSpan: 2 + Layout.topMargin: constants.paddingMedium + visible: rbffeebumper.valid - text: qsTr('Outputs') + labelText: qsTr('Inputs (%1)').arg(rbffeebumper.inputs.length) + color: Material.accentColor + } + + Repeater { + model: inputs_label.collapsed || !inputs_label.visible + ? undefined + : rbffeebumper.inputs + delegate: TxInput { + Layout.columnSpan: 2 + Layout.fillWidth: true + + idx: index + model: modelData + } + } + + ToggleLabel { + id: outputs_label Layout.columnSpan: 2 + Layout.topMargin: constants.paddingMedium + + visible: rbffeebumper.valid + labelText: qsTr('Outputs (%1)').arg(rbffeebumper.outputs.length) color: Material.accentColor } Repeater { - model: rbffeebumper.valid ? rbffeebumper.outputs : [] - delegate: TxOutput { + model: outputs_label.collapsed || !outputs_label.visible + ? undefined + : rbffeebumper.outputs + delegate: TxOutput { Layout.columnSpan: 2 Layout.fillWidth: true allowShare: false + allowClickAddress: false + + idx: index model: modelData } } + } } diff --git a/electrum/gui/qml/components/RbfCancelDialog.qml b/electrum/gui/qml/components/RbfCancelDialog.qml index 5b009114b..601890e73 100644 --- a/electrum/gui/qml/components/RbfCancelDialog.qml +++ b/electrum/gui/qml/components/RbfCancelDialog.qml @@ -151,23 +151,55 @@ ElDialog { text: txcanceller.warning } - Label { + ToggleLabel { + id: inputs_label + Layout.columnSpan: 2 + Layout.topMargin: constants.paddingMedium + visible: txcanceller.valid - text: qsTr('Outputs') + labelText: qsTr('Inputs (%1)').arg(txcanceller.inputs.length) + color: Material.accentColor + } + + Repeater { + model: inputs_label.collapsed || !inputs_label.visible + ? undefined + : txcanceller.inputs + delegate: TxInput { + Layout.columnSpan: 2 + Layout.fillWidth: true + + idx: index + model: modelData + } + } + + ToggleLabel { + id: outputs_label Layout.columnSpan: 2 + Layout.topMargin: constants.paddingMedium + + visible: txcanceller.valid + labelText: qsTr('Outputs (%1)').arg(txcanceller.outputs.length) color: Material.accentColor } Repeater { - model: txcanceller.valid ? txcanceller.outputs : [] - delegate: TxOutput { + model: outputs_label.collapsed || !outputs_label.visible + ? undefined + : txcanceller.outputs + delegate: TxOutput { Layout.columnSpan: 2 Layout.fillWidth: true allowShare: false + allowClickAddress: false + + idx: index model: modelData } } + } } diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index 540a8b4af..e6a032b10 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -302,19 +302,46 @@ Pane { } } - Label { + ToggleLabel { + id: inputs_label Layout.columnSpan: 2 - Layout.topMargin: constants.paddingSmall - text: qsTr('Outputs') + Layout.topMargin: constants.paddingMedium + + labelText: qsTr('Inputs (%1)').arg(txdetails.inputs.length) + color: Material.accentColor + } + + Repeater { + model: inputs_label.collapsed + ? undefined + : txdetails.inputs + delegate: TxInput { + Layout.columnSpan: 2 + Layout.fillWidth: true + + idx: index + model: modelData + } + } + + ToggleLabel { + id: outputs_label + Layout.columnSpan: 2 + Layout.topMargin: constants.paddingMedium + + labelText: qsTr('Outputs (%1)').arg(txdetails.outputs.length) color: Material.accentColor } Repeater { - model: txdetails.outputs + model: outputs_label.collapsed + ? undefined + : txdetails.outputs delegate: TxOutput { Layout.columnSpan: 2 Layout.fillWidth: true + idx: index model: modelData } } diff --git a/electrum/gui/qml/components/controls/ToggleLabel.qml b/electrum/gui/qml/components/controls/ToggleLabel.qml new file mode 100644 index 000000000..a0ae3736c --- /dev/null +++ b/electrum/gui/qml/components/controls/ToggleLabel.qml @@ -0,0 +1,17 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Material + +Label { + id: root + property bool collapsed: true + property string labelText + + text: (collapsed ? '▷' : '▽') + ' ' + labelText + + TapHandler { + onTapped: { + root.collapsed = !root.collapsed + } + } +} diff --git a/electrum/gui/qml/components/controls/TxInput.qml b/electrum/gui/qml/components/controls/TxInput.qml new file mode 100644 index 000000000..2ce1176ad --- /dev/null +++ b/electrum/gui/qml/components/controls/TxInput.qml @@ -0,0 +1,73 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Controls.Material + +import org.electrum 1.0 + +TextHighlightPane { + id: root + + property variant model + property int idx: -1 + + ColumnLayout { + width: parent.width + + RowLayout { + Layout.fillWidth: true + Label { + Layout.rightMargin: constants.paddingMedium + text: '#' + idx + font.family: FixedFont + font.bold: true + } + Label { + Layout.fillWidth: true + text: model.short_id + font.family: FixedFont + } + Label { + id: txin_value + text: model.value != undefined + ? Config.formatSats(model.value) + : '<' + qsTr('unknown amount') + '>' + font.pixelSize: constants.fontSizeMedium + font.family: FixedFont + } + Label { + text: Config.baseUnit + visible: model.value != undefined + font.pixelSize: constants.fontSizeMedium + color: Material.accentColor + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + antialiasing: true + color: constants.mutedForeground + } + + RowLayout { + Layout.fillWidth: true + Label { + Layout.fillWidth: true + text: model.address + ? model.address + : '<' + qsTr('address unknown') + '>' + font.family: FixedFont + font.pixelSize: constants.fontSizeMedium + color: model.is_mine + ? model.is_change + ? constants.colorAddressInternal + : constants.colorAddressExternal + : Material.foreground + elide: Text.ElideMiddle + } + } + + } +} + diff --git a/electrum/gui/qml/components/controls/TxOutput.qml b/electrum/gui/qml/components/controls/TxOutput.qml index 741837bbb..e603c5004 100644 --- a/electrum/gui/qml/components/controls/TxOutput.qml +++ b/electrum/gui/qml/components/controls/TxOutput.qml @@ -11,41 +11,77 @@ TextHighlightPane { property variant model property bool allowShare: true property bool allowClickAddress: true + property int idx: -1 RowLayout { width: parent.width - Label { - text: model.address + + ColumnLayout { Layout.fillWidth: true - wrapMode: Text.Wrap - font.pixelSize: constants.fontSizeLarge - font.family: FixedFont - color: model.is_mine - ? model.is_change - ? constants.colorAddressInternal - : constants.colorAddressExternal - : model.is_billing - ? constants.colorAddressBilling - : Material.foreground - TapHandler { - enabled: allowClickAddress && model.is_mine - onTapped: { - app.stack.push(Qt.resolvedUrl('../AddressDetails.qml'), { - address: model.address - }) + + RowLayout { + Layout.fillWidth: true + + Label { + Layout.rightMargin: constants.paddingLarge + text: '#' + idx + visible: idx >= 0 + font.family: FixedFont + font.pixelSize: constants.fontSizeMedium + font.bold: true + } + Label { + Layout.fillWidth: true + font.family: FixedFont + text: model.short_id + } + Label { + text: Config.formatSats(model.value) + font.pixelSize: constants.fontSizeMedium + font.family: FixedFont + } + Label { + text: Config.baseUnit + font.pixelSize: constants.fontSizeMedium + color: Material.accentColor } } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + antialiasing: true + color: constants.mutedForeground + } + + RowLayout { + Layout.fillWidth: true + Label { + text: model.address + Layout.fillWidth: true + wrapMode: Text.Wrap + font.pixelSize: constants.fontSizeMedium + font.family: FixedFont + color: model.is_mine + ? model.is_change + ? constants.colorAddressInternal + : constants.colorAddressExternal + : model.is_billing + ? constants.colorAddressBilling + : Material.foreground + TapHandler { + enabled: allowClickAddress && model.is_mine + onTapped: { + app.stack.push(Qt.resolvedUrl('../AddressDetails.qml'), { + address: model.address + }) + } + } + } + } + } - Label { - text: Config.formatSats(model.value) - font.pixelSize: constants.fontSizeMedium - font.family: FixedFont - } - Label { - text: Config.baseUnit - font.pixelSize: constants.fontSizeMedium - color: Material.accentColor - } + ToolButton { visible: allowShare icon.source: Qt.resolvedUrl('../../../icons/share.png') @@ -58,6 +94,7 @@ TextHighlightPane { dialog.open() } } + } } diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index ccd15c213..12ac1c9d3 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -5,7 +5,7 @@ from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from electrum.i18n import _ from electrum.logging import get_logger from electrum.util import format_time, TxMinedInfo -from electrum.transaction import tx_from_any, Transaction, PartialTxInput, Sighash, PartialTransaction +from electrum.transaction import tx_from_any, Transaction, PartialTxInput, Sighash, PartialTransaction, TxOutpoint from electrum.network import Network from electrum.address_synchronizer import TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE from electrum.wallet import TxSighashDanger @@ -270,10 +270,17 @@ class QETxDetails(QObject, QtEventListener): Network.run_from_another_thread( self._tx.add_info_from_network(self._wallet.wallet.network, timeout=10)) # FIXME is this needed?... - self._inputs = list(map(lambda x: x.to_json(), self._tx.inputs())) + self._inputs = list(map(lambda x: { + 'short_id': x.prevout.short_name(), + 'value': x.value_sats(), + 'address': x.address, + 'is_mine': self._wallet.wallet.is_mine(x.address), + 'is_change': self._wallet.wallet.is_change(x.address) + }, self._tx.inputs())) self._outputs = list(map(lambda x: { 'address': x.get_ui_address_str(), 'value': QEAmount(amount_sat=x.value), + 'short_id': '', # TODO 'is_mine': self._wallet.wallet.is_mine(x.get_ui_address_str()), 'is_change': self._wallet.wallet.is_change(x.get_ui_address_str()), 'is_billing': self._wallet.wallet.is_billing_address(x.get_ui_address_str()) diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index 7e4e6938a..00ea68a95 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -6,7 +6,7 @@ from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from electrum.logging import get_logger from electrum.i18n import _ -from electrum.transaction import PartialTxOutput, PartialTransaction, Transaction +from electrum.transaction import PartialTxOutput, PartialTransaction, Transaction, TxOutpoint from electrum.util import NotEnoughFunds, profiler from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP, BumpFeeStrategy from electrum.plugin import run_hook @@ -137,6 +137,7 @@ class TxFeeSlider(FeeSlider): self._feeRate = '' self._rbf = False self._tx = None + self._inputs = [] self._outputs = [] self._valid = False self._warning = '' @@ -175,6 +176,17 @@ class TxFeeSlider(FeeSlider): self.update() self.rbfChanged.emit() + inputsChanged = pyqtSignal() + @pyqtProperty('QVariantList', notify=inputsChanged) + def inputs(self): + return self._inputs + + @inputs.setter + def inputs(self, inputs): + if self._inputs != inputs: + self._inputs = inputs + self.inputsChanged.emit() + outputsChanged = pyqtSignal() @pyqtProperty('QVariantList', notify=outputsChanged) def outputs(self): @@ -210,14 +222,38 @@ class TxFeeSlider(FeeSlider): self.fee = QEAmount(amount_sat=int(fee)) self.feeRate = f'{feerate:.1f}' + self.update_inputs_from_tx(tx) self.update_outputs_from_tx(tx) + def update_inputs_from_tx(self, tx): + inputs = [] + for inp in tx.inputs(): + # addr + # addr = self.wallet.adb.get_txin_address(txin) + addr = inp.address + address_str = '
' if addr is None else addr + + txin_value = inp.value_sats() if inp.value_sats() else 0 + #self.wallet.adb.get_txin_value(txin) + + inputs.append({ + 'address': address_str, + 'short_id': str(inp.short_id), + 'value': QEAmount(amount_sat=txin_value), + 'is_coinbase': inp.is_coinbase_input(), + 'is_mine': self._wallet.wallet.is_mine(addr), + 'is_change': self._wallet.wallet.is_change(addr), + 'prevout_txid': inp.prevout.txid.hex() + }) + self.inputs = inputs + def update_outputs_from_tx(self, tx): outputs = [] - for o in tx.outputs(): + for idx, o in enumerate(tx.outputs()): outputs.append({ 'address': o.get_ui_address_str(), 'value': o.value, + 'short_id': str(TxOutpoint(bytes.fromhex(tx.txid()), idx).short_name()), 'is_mine': self._wallet.wallet.is_mine(o.get_ui_address_str()), 'is_change': self._wallet.wallet.is_change(o.get_ui_address_str()), 'is_billing': self._wallet.wallet.is_billing_address(o.get_ui_address_str()) @@ -829,6 +865,7 @@ class QETxCpfpFeeBumper(TxFeeSlider, TxMonMixin): self.warning = str(e) return + self.update_inputs_from_tx(self._new_tx) self.update_outputs_from_tx(self._new_tx) self._valid = True