qml: add transaction inputs in TxDetails and ConfirmTxDialog, RbfBumpFeeDialog, RbfCancelDialog, CpfpBumpFeeDialog
and allow collapse/expand of input/output lists by clicking on label.
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -176,23 +176,55 @@ ElDialog {
|
||||
iconStyle: InfoTextArea.IconStyle.Warn
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: cpfpfeebumper.valid
|
||||
text: qsTr('Outputs')
|
||||
ToggleLabel {
|
||||
id: inputs_label
|
||||
Layout.columnSpan: 2
|
||||
Layout.topMargin: constants.paddingMedium
|
||||
|
||||
visible: cpfpfeebumper.valid
|
||||
labelText: qsTr('Inputs (%1)').arg(cpfpfeebumper.inputs.length)
|
||||
color: Material.accentColor
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: cpfpfeebumper.valid ? cpfpfeebumper.outputs : []
|
||||
delegate: TxOutput {
|
||||
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: 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -188,23 +188,55 @@ ElDialog {
|
||||
text: rbffeebumper.warning
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: rbffeebumper.valid
|
||||
text: qsTr('Outputs')
|
||||
ToggleLabel {
|
||||
id: inputs_label
|
||||
Layout.columnSpan: 2
|
||||
Layout.topMargin: constants.paddingMedium
|
||||
|
||||
visible: rbffeebumper.valid
|
||||
labelText: qsTr('Inputs (%1)').arg(rbffeebumper.inputs.length)
|
||||
color: Material.accentColor
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: rbffeebumper.valid ? rbffeebumper.outputs : []
|
||||
delegate: TxOutput {
|
||||
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: 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -151,23 +151,55 @@ ElDialog {
|
||||
text: txcanceller.warning
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: txcanceller.valid
|
||||
text: qsTr('Outputs')
|
||||
ToggleLabel {
|
||||
id: inputs_label
|
||||
Layout.columnSpan: 2
|
||||
Layout.topMargin: constants.paddingMedium
|
||||
|
||||
visible: txcanceller.valid
|
||||
labelText: qsTr('Inputs (%1)').arg(txcanceller.inputs.length)
|
||||
color: Material.accentColor
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: txcanceller.valid ? txcanceller.outputs : []
|
||||
delegate: TxOutput {
|
||||
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: 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: txdetails.outputs
|
||||
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: outputs_label.collapsed
|
||||
? undefined
|
||||
: txdetails.outputs
|
||||
delegate: TxOutput {
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillWidth: true
|
||||
|
||||
idx: index
|
||||
model: modelData
|
||||
}
|
||||
}
|
||||
|
||||
17
electrum/gui/qml/components/controls/ToggleLabel.qml
Normal file
17
electrum/gui/qml/components/controls/ToggleLabel.qml
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
73
electrum/gui/qml/components/controls/TxInput.qml
Normal file
73
electrum/gui/qml/components/controls/TxInput.qml
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 = '<address unknown>' 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
|
||||
|
||||
Reference in New Issue
Block a user