From 4dc74870e1facb410c7b46a49a2f14712c4e58ba Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 17 Feb 2020 12:11:33 +0100 Subject: [PATCH] Catch exceptions raised in LNWorker._pay_to_route Reset payment status if an exception is caught. Also, do not pass status to the 'invoice_status' network callback. This fixes #5869, #5870, #5964. --- electrum/gui/kivy/main_window.py | 6 ++++- .../gui/kivy/uix/dialogs/invoice_dialog.py | 4 ++-- electrum/gui/qt/invoice_list.py | 9 ++++---- electrum/gui/qt/main_window.py | 8 ++++--- electrum/lnworker.py | 23 ++++++++++++++----- electrum/wallet.py | 2 +- 6 files changed, 34 insertions(+), 18 deletions(-) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 722c0a890..6742a8c42 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -226,7 +226,11 @@ class ElectrumWindow(App): self.show_info(_('Payment Received') + '\n' + key) self._trigger_update_history() - def on_invoice_status(self, event, key, status): + def on_invoice_status(self, event, key): + req = self.wallet.get_invoice(key) + if req is None: + return + status = req['status'] # todo: update single item self.update_tab('send') if self.invoice_popup and self.invoice_popup.key == key: diff --git a/electrum/gui/kivy/uix/dialogs/invoice_dialog.py b/electrum/gui/kivy/uix/dialogs/invoice_dialog.py index 537548886..1b252972f 100644 --- a/electrum/gui/kivy/uix/dialogs/invoice_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/invoice_dialog.py @@ -8,7 +8,7 @@ from kivy.clock import Clock from electrum.gui.kivy.i18n import _ from electrum.util import pr_tooltips, pr_color -from electrum.util import PR_UNKNOWN, PR_UNPAID +from electrum.util import PR_UNKNOWN, PR_UNPAID, PR_FAILED if TYPE_CHECKING: from electrum.gui.kivy.main_window import ElectrumWindow @@ -78,7 +78,7 @@ class InvoiceDialog(Factory.Popup): self.status = status self.status_str = pr_tooltips[status] self.status_color = pr_color[status] - self.can_pay = self.status == PR_UNPAID + self.can_pay = self.status in[PR_UNPAID, PR_FAILED] def on_dismiss(self): self.app.request_popup = None diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index 80125ae63..5d5935766 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -32,7 +32,7 @@ from PyQt5.QtWidgets import QAbstractItemView from PyQt5.QtWidgets import QMenu, QVBoxLayout, QTreeWidget, QTreeWidgetItem from electrum.i18n import _ -from electrum.util import format_time, PR_UNPAID, PR_PAID, PR_INFLIGHT +from electrum.util import format_time, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED from electrum.util import get_request_status from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN from electrum.lnutil import PaymentAttemptLog @@ -73,10 +73,7 @@ class InvoiceList(MyTreeView): self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.update() - def update_item(self, key, status): - req = self.parent.wallet.get_invoice(key) - if req is None: - return + def update_item(self, key, req): model = self.model() for row in range(0, model.rowCount()): item = model.item(row, 0) @@ -169,6 +166,8 @@ class InvoiceList(MyTreeView): menu.addAction(_("Details"), lambda: self.parent.show_invoice(key)) if invoice['status'] == PR_UNPAID: menu.addAction(_("Pay"), lambda: self.parent.do_pay_invoice(invoice)) + if invoice['status'] == PR_FAILED: + menu.addAction(_("Retry"), lambda: self.parent.do_pay_invoice(invoice)) if self.parent.wallet.lnworker: log = self.parent.wallet.lnworker.logs.get(key) if log: diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index d07456854..412c25b57 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1439,10 +1439,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.notify(_('Payment received') + '\n' + key) self.need_update.set() - def on_invoice_status(self, key, status): - if key not in self.wallet.invoices: + def on_invoice_status(self, key): + req = self.wallet.get_invoice(key) + if req is None: return - self.invoice_list.update_item(key, status) + status = req['status'] + self.invoice_list.update_item(key, req) if status == PR_PAID: self.show_message(_('Payment succeeded')) self.need_update.set() diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 15ed3be86..8b7cd51e4 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -818,16 +818,18 @@ class LNWallet(LNWorker): for i in range(attempts): try: route = await self._create_route_from_invoice(decoded_invoice=lnaddr) - except NoPathFound as e: + self.set_payment_status(payment_hash, PR_INFLIGHT) + self.network.trigger_callback('invoice_status', key) + payment_attempt_log = await self._pay_to_route(route, lnaddr) + except Exception as e: log.append(PaymentAttemptLog(success=False, exception=e)) + self.set_payment_status(payment_hash, PR_UNPAID) break - self.network.trigger_callback('invoice_status', key, PR_INFLIGHT) - payment_attempt_log = await self._pay_to_route(route, lnaddr) log.append(payment_attempt_log) success = payment_attempt_log.success if success: break - self.network.trigger_callback('invoice_status', key, PR_PAID if success else PR_FAILED) + self.network.trigger_callback('invoice_status', key) return success async def _pay_to_route(self, route: LNPaymentRoute, lnaddr: LnAddr) -> PaymentAttemptLog: @@ -837,8 +839,9 @@ class LNWallet(LNWorker): self.channel_db.remove_channel(short_channel_id) raise Exception(f"PathFinder returned path with short_channel_id " f"{short_channel_id} that is not in channel list") - self.set_payment_status(lnaddr.paymenthash, PR_INFLIGHT) - peer = self.peers[route[0].node_id] + peer = self.peers.get(route[0].node_id) + if not peer: + raise Exception('Dropped peer') htlc = await peer.pay(route, chan, int(lnaddr.amount * COIN * 1000), lnaddr.paymenthash, lnaddr.get_min_final_cltv_expiry()) self.network.trigger_callback('htlc_added', htlc, lnaddr, SENT) success, preimage, reason = await self.await_payment(lnaddr.paymenthash) @@ -1056,6 +1059,14 @@ class LNWallet(LNWorker): status = PR_UNPAID return status + def get_invoice_status(self, key): + # status may be PR_FAILED + status = self.get_payment_status(bfh(key)) + log = self.logs[key] + if status == PR_UNPAID and log: + status = PR_FAILED + return status + async def await_payment(self, payment_hash): success, preimage, reason = await self.pending_payments[payment_hash] self.pending_payments.pop(payment_hash) diff --git a/electrum/wallet.py b/electrum/wallet.py index 199b3e4d8..a60860e05 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -656,7 +656,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC): if request_type == PR_TYPE_ONCHAIN: item['status'] = PR_PAID if self.is_onchain_invoice_paid(item) else PR_UNPAID elif self.lnworker and request_type == PR_TYPE_LN: - item['status'] = self.lnworker.get_payment_status(bfh(item['rhash'])) + item['status'] = self.lnworker.get_invoice_status(key) else: return return item