1
0

Merge pull request #8772 from accumulator/qml_tx_inputs

qml: add transaction inputs in TxDetails and …
This commit is contained in:
accumulator
2023-12-29 16:21:22 +01:00
committed by GitHub
10 changed files with 379 additions and 54 deletions

View File

@@ -183,22 +183,53 @@ ElDialog {
iconStyle: InfoTextArea.IconStyle.Warn iconStyle: InfoTextArea.IconStyle.Warn
} }
Label { ToggleLabel {
text: qsTr('Outputs') id: inputs_label
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.topMargin: constants.paddingMedium
labelText: qsTr('Inputs (%1)').arg(finalizer.inputs.length)
color: Material.accentColor color: Material.accentColor
} }
Repeater { 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 { delegate: TxOutput {
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
allowShare: false allowShare: false
allowClickAddress: false
idx: index
model: modelData model: modelData
} }
} }
} }
} }

View File

@@ -176,23 +176,55 @@ ElDialog {
iconStyle: InfoTextArea.IconStyle.Warn iconStyle: InfoTextArea.IconStyle.Warn
} }
Label { ToggleLabel {
visible: cpfpfeebumper.valid id: inputs_label
text: qsTr('Outputs')
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.topMargin: constants.paddingMedium
visible: cpfpfeebumper.valid
labelText: qsTr('Inputs (%1)').arg(cpfpfeebumper.inputs.length)
color: Material.accentColor color: Material.accentColor
} }
Repeater { Repeater {
model: cpfpfeebumper.valid ? cpfpfeebumper.outputs : [] model: inputs_label.collapsed || !inputs_label.visible
delegate: TxOutput { ? 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.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
allowShare: false allowShare: false
allowClickAddress: false
idx: index
model: modelData model: modelData
} }
} }
} }
} }

View File

@@ -188,23 +188,55 @@ ElDialog {
text: rbffeebumper.warning text: rbffeebumper.warning
} }
Label { ToggleLabel {
visible: rbffeebumper.valid id: inputs_label
text: qsTr('Outputs')
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.topMargin: constants.paddingMedium
visible: rbffeebumper.valid
labelText: qsTr('Inputs (%1)').arg(rbffeebumper.inputs.length)
color: Material.accentColor color: Material.accentColor
} }
Repeater { Repeater {
model: rbffeebumper.valid ? rbffeebumper.outputs : [] model: inputs_label.collapsed || !inputs_label.visible
delegate: TxOutput { ? 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.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
allowShare: false allowShare: false
allowClickAddress: false
idx: index
model: modelData model: modelData
} }
} }
} }
} }

View File

@@ -151,23 +151,55 @@ ElDialog {
text: txcanceller.warning text: txcanceller.warning
} }
Label { ToggleLabel {
visible: txcanceller.valid id: inputs_label
text: qsTr('Outputs')
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.topMargin: constants.paddingMedium
visible: txcanceller.valid
labelText: qsTr('Inputs (%1)').arg(txcanceller.inputs.length)
color: Material.accentColor color: Material.accentColor
} }
Repeater { Repeater {
model: txcanceller.valid ? txcanceller.outputs : [] model: inputs_label.collapsed || !inputs_label.visible
delegate: TxOutput { ? 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.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
allowShare: false allowShare: false
allowClickAddress: false
idx: index
model: modelData model: modelData
} }
} }
} }
} }

View File

@@ -302,19 +302,46 @@ Pane {
} }
} }
Label { ToggleLabel {
id: inputs_label
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.topMargin: constants.paddingSmall Layout.topMargin: constants.paddingMedium
text: qsTr('Outputs')
labelText: qsTr('Inputs (%1)').arg(txdetails.inputs.length)
color: Material.accentColor color: Material.accentColor
} }
Repeater { 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 { delegate: TxOutput {
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
idx: index
model: modelData model: modelData
} }
} }

View 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
}
}
}

View 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
}
}
}
}

View File

@@ -11,41 +11,77 @@ TextHighlightPane {
property variant model property variant model
property bool allowShare: true property bool allowShare: true
property bool allowClickAddress: true property bool allowClickAddress: true
property int idx: -1
RowLayout { RowLayout {
width: parent.width width: parent.width
Label {
text: model.address ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.Wrap
font.pixelSize: constants.fontSizeLarge RowLayout {
font.family: FixedFont Layout.fillWidth: true
color: model.is_mine
? model.is_change Label {
? constants.colorAddressInternal Layout.rightMargin: constants.paddingLarge
: constants.colorAddressExternal text: '#' + idx
: model.is_billing visible: idx >= 0
? constants.colorAddressBilling font.family: FixedFont
: Material.foreground font.pixelSize: constants.fontSizeMedium
TapHandler { font.bold: true
enabled: allowClickAddress && model.is_mine }
onTapped: { Label {
app.stack.push(Qt.resolvedUrl('../AddressDetails.qml'), { Layout.fillWidth: true
address: model.address 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 { ToolButton {
visible: allowShare visible: allowShare
icon.source: Qt.resolvedUrl('../../../icons/share.png') icon.source: Qt.resolvedUrl('../../../icons/share.png')
@@ -58,6 +94,7 @@ TextHighlightPane {
dialog.open() dialog.open()
} }
} }
} }
} }

View File

@@ -5,7 +5,7 @@ from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from electrum.i18n import _ from electrum.i18n import _
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.util import format_time, TxMinedInfo 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.network import Network
from electrum.address_synchronizer import TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE from electrum.address_synchronizer import TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE
from electrum.wallet import TxSighashDanger from electrum.wallet import TxSighashDanger
@@ -270,10 +270,17 @@ class QETxDetails(QObject, QtEventListener):
Network.run_from_another_thread( Network.run_from_another_thread(
self._tx.add_info_from_network(self._wallet.wallet.network, timeout=10)) # FIXME is this needed?... 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: { self._outputs = list(map(lambda x: {
'address': x.get_ui_address_str(), 'address': x.get_ui_address_str(),
'value': QEAmount(amount_sat=x.value), 'value': QEAmount(amount_sat=x.value),
'short_id': '', # TODO
'is_mine': self._wallet.wallet.is_mine(x.get_ui_address_str()), '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_change': self._wallet.wallet.is_change(x.get_ui_address_str()),
'is_billing': self._wallet.wallet.is_billing_address(x.get_ui_address_str()) 'is_billing': self._wallet.wallet.is_billing_address(x.get_ui_address_str())

View File

@@ -6,7 +6,7 @@ from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.i18n import _ 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.util import NotEnoughFunds, profiler
from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP, BumpFeeStrategy from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP, BumpFeeStrategy
from electrum.plugin import run_hook from electrum.plugin import run_hook
@@ -137,6 +137,7 @@ class TxFeeSlider(FeeSlider):
self._feeRate = '' self._feeRate = ''
self._rbf = False self._rbf = False
self._tx = None self._tx = None
self._inputs = []
self._outputs = [] self._outputs = []
self._valid = False self._valid = False
self._warning = '' self._warning = ''
@@ -175,6 +176,17 @@ class TxFeeSlider(FeeSlider):
self.update() self.update()
self.rbfChanged.emit() 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() outputsChanged = pyqtSignal()
@pyqtProperty('QVariantList', notify=outputsChanged) @pyqtProperty('QVariantList', notify=outputsChanged)
def outputs(self): def outputs(self):
@@ -210,14 +222,38 @@ class TxFeeSlider(FeeSlider):
self.fee = QEAmount(amount_sat=int(fee)) self.fee = QEAmount(amount_sat=int(fee))
self.feeRate = f'{feerate:.1f}' self.feeRate = f'{feerate:.1f}'
self.update_inputs_from_tx(tx)
self.update_outputs_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): def update_outputs_from_tx(self, tx):
outputs = [] outputs = []
for o in tx.outputs(): for idx, o in enumerate(tx.outputs()):
outputs.append({ outputs.append({
'address': o.get_ui_address_str(), 'address': o.get_ui_address_str(),
'value': o.value, '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_mine': self._wallet.wallet.is_mine(o.get_ui_address_str()),
'is_change': self._wallet.wallet.is_change(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()) '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) self.warning = str(e)
return return
self.update_inputs_from_tx(self._new_tx)
self.update_outputs_from_tx(self._new_tx) self.update_outputs_from_tx(self._new_tx)
self._valid = True self._valid = True