Merge pull request #10919 from f321x/qml_dont_delete_paid_invoices
What changed, and why it matters
This change fixes a bug in Electrum's mobile/QML wallet interface where paid Lightning invoices were being deleted from the wallet's records. The fix moves the 'don't delete paid invoices' protection into the core wallet code so it applies consistently, and it also prevents deletion of invoices that are still 'in flight' (payment pending). Keeping paid invoice records is important because they are needed to show transaction history and to prove a payment was made. Deleting them could cause confusion, accounting problems, or loss of payment evidence.
Review and merge if not already merged. Additionally, address the acknowledged leak where in-flight invoices that fail are never deleted, as this could accumulate stale payment state. Consider adding tests covering deletion of paid, unpaid, expired, and in-flight Lightning invoices.
Security signals we found
Loss of payment history / proof of payment for paid Lightning invoices
Potential deletion of in-flight Lightning payment state, which could corrupt payment tracking
UI model logic bypassed by direct wallet.delete_invoice/delete_request calls
Comment explicitly acknowledges a remaining leak for failed in-flight invoices
Evidence from the diff
The commit modifies qeinvoicelistmodel.py and wallet.py. Previously, the QML request list model had a special updateRequest() method that deleted the invoice only when PR_PAID, while routing other status updates through updateInvoice(). This logic was fragile and could be bypassed. The patch removes updateRequest() and routes request status updates through updateInvoice(), which now deletes the invoice when PR_PAID. More importantly, it hardens wallet.delete_request() and wallet.delete_invoice() so they no longer delete lnworker PaymentInfo for Lightning requests/invoices that are PR_PAID, and also skips deletion for PR_INFLIGHT invoices. A code comment explicitly notes that paid invoices need PaymentInfo for history, and that inflight invoices that later fail will currently leak and never be deleted.
Changed components
electrum/gui/qml/qeinvoicelistmodel.pyelectrum/wallet.pyLightning invoice/request deletion pathsQML/mobile GUI request status handlingInspect captured patch +11 / −11
### electrum/gui/qml/qeinvoicelistmodel.py
@@ -119,6 +119,9 @@ def updateInvoice(self, key, status):
self._logger.debug(f'updating invoice for {key} to {status}')
for i, item in enumerate(self._invoices):
if item['key'] == key:
+ if status == PR_PAID:
+ self.delete_invoice(key)
+ return
invoice = self.get_invoice_for_key(key)
item['status'] = status
item['status_str'] = invoice.get_status_str(status)
@@ -228,7 +231,7 @@ def on_destroy(self):
def on_event_request_status(self, wallet, key, status):
if wallet == self.wallet:
self._logger.debug(f'request status update for key {key} to {status}')
- self.updateRequest(key, status)
+ self.updateInvoice(key, status)
def invoice_to_model(self, invoice: BaseInvoice):
item = super().invoice_to_model(invoice)
@@ -246,10 +249,3 @@ def get_invoice_for_key(self, key: str):
def get_invoice_as_dict(self, invoice: Request):
return self.wallet.export_request(invoice)
-
- @pyqtSlot(str, int)
- def updateRequest(self, key, status):
- if status == PR_PAID:
- self.delete_invoice(key)
- else:
- self.updateInvoice(key, status)
### electrum/wallet.py
@@ -76,7 +76,7 @@
AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE,
TX_TIMESTAMP_INF
)
-from .invoices import BaseInvoice, Invoice, Request, PR_PAID, PR_UNPAID, PR_EXPIRED, PR_UNCONFIRMED
+from .invoices import BaseInvoice, Invoice, Request, PR_PAID, PR_UNPAID, PR_EXPIRED, PR_UNCONFIRMED, PR_INFLIGHT
from .contacts import Contacts
from .mnemonic import Mnemonic
from .lnworker import LNWallet
@@ -3098,7 +3098,8 @@ def delete_request(self, request_id, *, write_to_disk: bool = True):
self._receive_requests.pop(request_id, None)
if addr := req.get_address():
self._requests_addr_to_key[addr].discard(request_id)
- if req.is_lightning() and self.lnworker:
+ if req.is_lightning() and self.lnworker \
+ and self.lnworker.get_invoice_status(req) != PR_PAID:
self.lnworker.delete_payment_info(req.rhash, direction=RECEIVED)
if write_to_disk:
self.save_db()
@@ -3109,7 +3110,10 @@ def delete_invoice(self, invoice_id, *, write_to_disk: bool = True):
if inv is None:
return
self._paid_invoice_keys_cache.discard(invoice_id)
- if inv.is_lightning() and self.lnworker:
+ if inv.is_lightning() and self.lnworker \
+ and self.lnworker.get_invoice_status(inv) not in (PR_PAID, PR_INFLIGHT):
+ # if an invoice was paid we need the PaymentInfo for the history and don't delete it.
+ # if it is still inflight and the payment fails later on we leak it and never delete it.
self.lnworker.delete_payment_info(inv.rhash, direction=SENT)
if write_to_disk:
self.save_db()Why this scored 37/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.