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()
}
}
Connections {
target: cpfpfeebumper
function onTxMined() {
dialog.doReject()
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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}"'

View File

@@ -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