From f16efd759a8dc6fe00ff0946d701f3f1cec25167 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 10 Mar 2025 21:35:38 +0100 Subject: [PATCH] qml: detect transaction removed (e.g. replace-by-fee) for qetxdetails and qetxfinalizer, don't close active feebump/cancel dialogs, but invalidate them, don't close TxDetails page, but show removed status, show broadcast-failed status in TxDetails --- .../gui/qml/components/CpfpBumpFeeDialog.qml | 7 --- .../gui/qml/components/RbfBumpFeeDialog.qml | 7 --- .../gui/qml/components/RbfCancelDialog.qml | 7 --- electrum/gui/qml/components/TxDetails.qml | 27 +++++++---- electrum/gui/qml/qetxdetails.py | 25 +++++++++- electrum/gui/qml/qetxfinalizer.py | 47 ++++++++++++++++++- 6 files changed, 87 insertions(+), 33 deletions(-) diff --git a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml index 15ac6dcd2..8361480a1 100644 --- a/electrum/gui/qml/components/CpfpBumpFeeDialog.qml +++ b/electrum/gui/qml/components/CpfpBumpFeeDialog.qml @@ -222,11 +222,4 @@ ElDialog { onClicked: doAccept() } } - - Connections { - target: cpfpfeebumper - function onTxMined() { - dialog.doReject() - } - } } diff --git a/electrum/gui/qml/components/RbfBumpFeeDialog.qml b/electrum/gui/qml/components/RbfBumpFeeDialog.qml index e3199c7ae..f913d7a6e 100644 --- a/electrum/gui/qml/components/RbfBumpFeeDialog.qml +++ b/electrum/gui/qml/components/RbfBumpFeeDialog.qml @@ -231,11 +231,4 @@ ElDialog { onClicked: doAccept() } } - - Connections { - target: rbffeebumper - function onTxMined() { - dialog.doReject() - } - } } diff --git a/electrum/gui/qml/components/RbfCancelDialog.qml b/electrum/gui/qml/components/RbfCancelDialog.qml index be270796d..a34f14107 100644 --- a/electrum/gui/qml/components/RbfCancelDialog.qml +++ b/electrum/gui/qml/components/RbfCancelDialog.qml @@ -163,11 +163,4 @@ ElDialog { onClicked: doAccept() } } - - Connections { - target: txcanceller - function onTxMined() { - dialog.doReject() - } - } } diff --git a/electrum/gui/qml/components/TxDetails.qml b/electrum/gui/qml/components/TxDetails.qml index cc19b42de..5ff21142b 100644 --- a/electrum/gui/qml/components/TxDetails.qml +++ b/electrum/gui/qml/components/TxDetails.qml @@ -60,13 +60,14 @@ Pane { } InfoTextArea { - id: bumpfeeinfo + id: txinfo Layout.columnSpan: 2 Layout.fillWidth: true Layout.bottomMargin: constants.paddingLarge - visible: txdetails.isUnrelated || !txdetails.isMined + visible: (txdetails.isUnrelated || !txdetails.isMined) && !broadcastinfo.visible text: txdetails.isUnrelated ? qsTr('Transaction is unrelated to this wallet.') + : txdetails.isRemoved ? qsTr('This transaction has been replaced or removed and is no longer valid') : txdetails.inMempool ? qsTr('This transaction is still unconfirmed.') + (txdetails.canBump || txdetails.canCpfp || txdetails.canCancel @@ -84,11 +85,19 @@ Pane { (txdetails.wallet.isWatchOnly ? qsTr('Present this transaction to the signing wallet.') : qsTr('Present this transaction to the next cosigner.')) - iconStyle: txdetails.isUnrelated + iconStyle: txdetails.isUnrelated || txdetails.isRemoved ? InfoTextArea.IconStyle.Warn : InfoTextArea.IconStyle.Info } + InfoTextArea { + id: broadcastinfo + Layout.columnSpan: 2 + Layout.fillWidth: true + Layout.bottomMargin: constants.paddingLarge + visible: text + } + Label { Layout.preferredWidth: 1 Layout.fillWidth: true @@ -417,7 +426,7 @@ Pane { Layout.preferredWidth: 1 icon.source: '../../icons/qrcode_white.png' text: qsTr('Share') - enabled: !txdetails.isUnrelated + enabled: !txdetails.isUnrelated && !txdetails.isRemoved onClicked: { var msg = '' if (txdetails.isComplete) { @@ -477,9 +486,6 @@ Pane { }) dialog.open() } - onTxRemoved: { - root.close() - } Component.onCompleted: { if (root.txid) { txdetails.txid = root.txid @@ -512,7 +518,12 @@ Pane { dialog.open() } function onBroadcastSucceeded() { - bumpfeeinfo.text = qsTr('Transaction was broadcast successfully') + broadcastinfo.text = qsTr('Transaction was broadcast successfully') + broadcastinfo.iconStyle = InfoTextArea.IconStyle.Info + } + function onBroadcastFailed(txid, code, message) { + broadcastinfo.text = qsTr('Broadcast of transaction failed') + broadcastinfo.iconStyle = InfoTextArea.IconStyle.Warn } } diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 453dab089..5ddad940c 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -6,7 +6,7 @@ from electrum.i18n import _ from electrum.logging import get_logger from electrum.bitcoin import DummyAddress from electrum.util import format_time, TxMinedInfo -from electrum.transaction import tx_from_any, Transaction, PartialTxInput, Sighash, PartialTransaction, TxOutpoint +from electrum.transaction import tx_from_any, Transaction, PartialTransaction 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 @@ -22,7 +22,7 @@ class QETxDetails(QObject, QtEventListener): confirmRemoveLocalTx = pyqtSignal([str], arguments=['message']) txRemoved = pyqtSignal() - saveTxError = pyqtSignal([str,str], arguments=['code', 'message']) + saveTxError = pyqtSignal([str, str], arguments=['code', 'message']) saveTxSuccess = pyqtSignal() detailsChanged = pyqtSignal() @@ -59,6 +59,7 @@ class QETxDetails(QObject, QtEventListener): self._is_complete = False self._is_mined = False self._is_rbf_enabled = False + self._is_removed = False self._lock_delay = 0 self._sighash_danger = TxSighashDanger() @@ -90,6 +91,8 @@ class QETxDetails(QObject, QtEventListener): def on_event_removed_transaction(self, wallet, tx): if wallet == self._wallet.wallet and tx.txid() == self._txid: self._logger.debug(f'removed my transaction {tx.txid()}') + self._is_removed = True + self.update() self.txRemoved.emit() walletChanged = pyqtSignal() @@ -184,6 +187,10 @@ class QETxDetails(QObject, QtEventListener): def isMined(self): return self._is_mined + @pyqtProperty(bool, notify=detailsChanged) + def isRemoved(self): + return self._is_removed + @pyqtProperty(str, notify=detailsChanged) def mempoolDepth(self): return self._mempool_depth @@ -267,6 +274,20 @@ class QETxDetails(QObject, QtEventListener): def update(self, from_txid: bool = False): assert self._wallet + if self._is_removed: + self._logger.debug('tx removed, disable gui options') + self._can_broadcast = False + self._can_bump = False + self._can_dscancel = False + self._can_cpfp = False + self._can_save_as_local = False + self._can_remove = False + self._can_sign = False + self._mempool_depth = '' + self._status = _('removed') + self.detailsChanged.emit() + return + if from_txid: self._tx = self._wallet.wallet.db.get_transaction(self._txid) assert self._tx is not None, f'unknown txid "{self._txid}"' diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index 8a87a5fd3..373e7262d 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -474,13 +474,15 @@ class QETxFinalizer(TxFeeSlider): class TxMonMixin(QtEventListener): - """ mixin for watching an existing TX based on its txid for verified event. + """ mixin for watching an existing TX based on its txid for verified or removed event. requires self._wallet to contain a QEWallet instance. exposes txid qt property. calls get_tx() once txid is set. - calls tx_verified and emits txMined signal once tx is verified. + calls tx_verified() and emits txMined signal once tx is verified. + emits txRemoved signal if tx is removed (e.g. replace-by-fee) """ txMined = pyqtSignal() + txRemoved = pyqtSignal() def __init__(self, parent=None): self._logger.debug('TxMonMixin.__init__') @@ -500,6 +502,13 @@ class TxMonMixin(QtEventListener): self.tx_verified() self.txMined.emit() + @event_listener + def on_event_removed_transaction(self, wallet, tx): + if wallet == self._wallet.wallet and tx.txid() == self._txid: + self._logger.debug('remove tx for our txid %s' % self._txid) + self.tx_removed() + self.txRemoved.emit() + txidChanged = pyqtSignal() @pyqtProperty(str, notify=txidChanged) def txid(self): @@ -520,6 +529,10 @@ class TxMonMixin(QtEventListener): def tx_verified(self) -> None: pass + # override + def tx_removed(self) -> None: + pass + class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin): _logger = get_logger(__name__) @@ -595,6 +608,16 @@ class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin): self.oldfeeRate = self.feeRate self.update() + def tx_verified(self): + self._valid = False + self.validChanged.emit() + self.warning = _('Base transaction has been mined') + + def tx_removed(self): + self._valid = False + self.validChanged.emit() + self.warning = _('Base transaction disappeared') + def update(self): if not self._txid or not self._orig_tx: # not initialized yet @@ -692,6 +715,16 @@ class QETxCanceller(TxFeeSlider, TxMonMixin): self.oldfeeRate = self.feeRate self.update() + def tx_verified(self): + self._valid = False + self.validChanged.emit() + self.warning = _('Base transaction has been mined') + + def tx_removed(self): + self._valid = False + self.validChanged.emit() + self.warning = _('Base transaction disappeared') + def update(self): if not self._txid or not self._orig_tx: # not initialized yet @@ -829,6 +862,16 @@ class QETxCpfpFeeBumper(TxFeeSlider, TxMonMixin): fee = max(self._total_size, fee) # pay at least 1 sat/byte for combined size return fee + def tx_verified(self): + self._valid = False + self.validChanged.emit() + self.warning = _('Base transaction has been mined') + + def tx_removed(self): + self._valid = False + self.validChanged.emit() + self.warning = _('Base transaction disappeared') + def update(self): if not self._txid: # not initialized yet return