1
0

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
This commit is contained in:
Sander van Grieken
2025-03-10 21:35:38 +01:00
parent 3db26c4ecb
commit f16efd759a
6 changed files with 87 additions and 33 deletions

View File

@@ -222,11 +222,4 @@ ElDialog {
onClicked: doAccept() onClicked: doAccept()
} }
} }
Connections {
target: cpfpfeebumper
function onTxMined() {
dialog.doReject()
}
}
} }

View File

@@ -231,11 +231,4 @@ ElDialog {
onClicked: doAccept() onClicked: doAccept()
} }
} }
Connections {
target: rbffeebumper
function onTxMined() {
dialog.doReject()
}
}
} }

View File

@@ -163,11 +163,4 @@ ElDialog {
onClicked: doAccept() onClicked: doAccept()
} }
} }
Connections {
target: txcanceller
function onTxMined() {
dialog.doReject()
}
}
} }

View File

@@ -60,13 +60,14 @@ Pane {
} }
InfoTextArea { InfoTextArea {
id: bumpfeeinfo id: txinfo
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
Layout.bottomMargin: constants.paddingLarge Layout.bottomMargin: constants.paddingLarge
visible: txdetails.isUnrelated || !txdetails.isMined visible: (txdetails.isUnrelated || !txdetails.isMined) && !broadcastinfo.visible
text: txdetails.isUnrelated text: txdetails.isUnrelated
? qsTr('Transaction is unrelated to this wallet.') ? qsTr('Transaction is unrelated to this wallet.')
: txdetails.isRemoved ? qsTr('This transaction has been replaced or removed and is no longer valid')
: txdetails.inMempool : txdetails.inMempool
? qsTr('This transaction is still unconfirmed.') + ? qsTr('This transaction is still unconfirmed.') +
(txdetails.canBump || txdetails.canCpfp || txdetails.canCancel (txdetails.canBump || txdetails.canCpfp || txdetails.canCancel
@@ -84,11 +85,19 @@ Pane {
(txdetails.wallet.isWatchOnly (txdetails.wallet.isWatchOnly
? qsTr('Present this transaction to the signing wallet.') ? qsTr('Present this transaction to the signing wallet.')
: qsTr('Present this transaction to the next cosigner.')) : qsTr('Present this transaction to the next cosigner.'))
iconStyle: txdetails.isUnrelated iconStyle: txdetails.isUnrelated || txdetails.isRemoved
? InfoTextArea.IconStyle.Warn ? InfoTextArea.IconStyle.Warn
: InfoTextArea.IconStyle.Info : InfoTextArea.IconStyle.Info
} }
InfoTextArea {
id: broadcastinfo
Layout.columnSpan: 2
Layout.fillWidth: true
Layout.bottomMargin: constants.paddingLarge
visible: text
}
Label { Label {
Layout.preferredWidth: 1 Layout.preferredWidth: 1
Layout.fillWidth: true Layout.fillWidth: true
@@ -417,7 +426,7 @@ Pane {
Layout.preferredWidth: 1 Layout.preferredWidth: 1
icon.source: '../../icons/qrcode_white.png' icon.source: '../../icons/qrcode_white.png'
text: qsTr('Share') text: qsTr('Share')
enabled: !txdetails.isUnrelated enabled: !txdetails.isUnrelated && !txdetails.isRemoved
onClicked: { onClicked: {
var msg = '' var msg = ''
if (txdetails.isComplete) { if (txdetails.isComplete) {
@@ -477,9 +486,6 @@ Pane {
}) })
dialog.open() dialog.open()
} }
onTxRemoved: {
root.close()
}
Component.onCompleted: { Component.onCompleted: {
if (root.txid) { if (root.txid) {
txdetails.txid = root.txid txdetails.txid = root.txid
@@ -512,7 +518,12 @@ Pane {
dialog.open() dialog.open()
} }
function onBroadcastSucceeded() { 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
} }
} }

View File

@@ -6,7 +6,7 @@ from electrum.i18n import _
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.bitcoin import DummyAddress from electrum.bitcoin import DummyAddress
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, TxOutpoint from electrum.transaction import tx_from_any, Transaction, PartialTransaction
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
@@ -22,7 +22,7 @@ class QETxDetails(QObject, QtEventListener):
confirmRemoveLocalTx = pyqtSignal([str], arguments=['message']) confirmRemoveLocalTx = pyqtSignal([str], arguments=['message'])
txRemoved = pyqtSignal() txRemoved = pyqtSignal()
saveTxError = pyqtSignal([str,str], arguments=['code', 'message']) saveTxError = pyqtSignal([str, str], arguments=['code', 'message'])
saveTxSuccess = pyqtSignal() saveTxSuccess = pyqtSignal()
detailsChanged = pyqtSignal() detailsChanged = pyqtSignal()
@@ -59,6 +59,7 @@ class QETxDetails(QObject, QtEventListener):
self._is_complete = False self._is_complete = False
self._is_mined = False self._is_mined = False
self._is_rbf_enabled = False self._is_rbf_enabled = False
self._is_removed = False
self._lock_delay = 0 self._lock_delay = 0
self._sighash_danger = TxSighashDanger() self._sighash_danger = TxSighashDanger()
@@ -90,6 +91,8 @@ class QETxDetails(QObject, QtEventListener):
def on_event_removed_transaction(self, wallet, tx): def on_event_removed_transaction(self, wallet, tx):
if wallet == self._wallet.wallet and tx.txid() == self._txid: if wallet == self._wallet.wallet and tx.txid() == self._txid:
self._logger.debug(f'removed my transaction {tx.txid()}') self._logger.debug(f'removed my transaction {tx.txid()}')
self._is_removed = True
self.update()
self.txRemoved.emit() self.txRemoved.emit()
walletChanged = pyqtSignal() walletChanged = pyqtSignal()
@@ -184,6 +187,10 @@ class QETxDetails(QObject, QtEventListener):
def isMined(self): def isMined(self):
return self._is_mined return self._is_mined
@pyqtProperty(bool, notify=detailsChanged)
def isRemoved(self):
return self._is_removed
@pyqtProperty(str, notify=detailsChanged) @pyqtProperty(str, notify=detailsChanged)
def mempoolDepth(self): def mempoolDepth(self):
return self._mempool_depth return self._mempool_depth
@@ -267,6 +274,20 @@ class QETxDetails(QObject, QtEventListener):
def update(self, from_txid: bool = False): def update(self, from_txid: bool = False):
assert self._wallet 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: if from_txid:
self._tx = self._wallet.wallet.db.get_transaction(self._txid) self._tx = self._wallet.wallet.db.get_transaction(self._txid)
assert self._tx is not None, f'unknown txid "{self._txid}"' assert self._tx is not None, f'unknown txid "{self._txid}"'

View File

@@ -474,13 +474,15 @@ class QETxFinalizer(TxFeeSlider):
class TxMonMixin(QtEventListener): 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. requires self._wallet to contain a QEWallet instance.
exposes txid qt property. exposes txid qt property.
calls get_tx() once txid is set. 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() txMined = pyqtSignal()
txRemoved = pyqtSignal()
def __init__(self, parent=None): def __init__(self, parent=None):
self._logger.debug('TxMonMixin.__init__') self._logger.debug('TxMonMixin.__init__')
@@ -500,6 +502,13 @@ class TxMonMixin(QtEventListener):
self.tx_verified() self.tx_verified()
self.txMined.emit() 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() txidChanged = pyqtSignal()
@pyqtProperty(str, notify=txidChanged) @pyqtProperty(str, notify=txidChanged)
def txid(self): def txid(self):
@@ -520,6 +529,10 @@ class TxMonMixin(QtEventListener):
def tx_verified(self) -> None: def tx_verified(self) -> None:
pass pass
# override
def tx_removed(self) -> None:
pass
class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin): class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin):
_logger = get_logger(__name__) _logger = get_logger(__name__)
@@ -595,6 +608,16 @@ class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin):
self.oldfeeRate = self.feeRate self.oldfeeRate = self.feeRate
self.update() 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): def update(self):
if not self._txid or not self._orig_tx: if not self._txid or not self._orig_tx:
# not initialized yet # not initialized yet
@@ -692,6 +715,16 @@ class QETxCanceller(TxFeeSlider, TxMonMixin):
self.oldfeeRate = self.feeRate self.oldfeeRate = self.feeRate
self.update() 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): def update(self):
if not self._txid or not self._orig_tx: if not self._txid or not self._orig_tx:
# not initialized yet # 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 fee = max(self._total_size, fee) # pay at least 1 sat/byte for combined size
return fee 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): def update(self):
if not self._txid: # not initialized yet if not self._txid: # not initialized yet
return return