commit dd0be1541eb696d6a41420178543f2c36d20f80e
parent d6d644190e8f922cb98b9a5e9b40fb36fa021ff7
Author: ThomasV <thomasv@electrum.org>
Date: Wed, 9 Oct 2019 19:23:09 +0200
Improve handling of lightning payment status:
- Move 'handle_error_code_from_failed_htlc' to channel_db,
and call it from pay_to_route, because it should not be
called when HTLCs are forwarded.
- Replace 'payment_received' and 'payment_status'
callbacks with 'invoice_status' and 'request_status'.
- Show payment error logs in the Qt GUI
- In the invoices list, show paid invoices for which
we still have the log.
Diffstat:
9 files changed, 190 insertions(+), 132 deletions(-)
diff --git a/electrum/channel_db.py b/electrum/channel_db.py
@@ -39,6 +39,8 @@ from .util import bh2u, profiler, get_headers_dir, bfh, is_ip_address, list_enab
from .logging import Logger
from .lnutil import LN_GLOBAL_FEATURES_KNOWN_SET, LNPeerAddr, format_short_channel_id, ShortChannelID
from .lnverifier import LNChannelVerifier, verify_sig_for_channel_update
+from .lnonion import OnionFailureCode
+from .lnmsg import decode_msg
if TYPE_CHECKING:
from .network import Network
@@ -385,6 +387,57 @@ class ChannelDB(SqlDB):
# the update may be categorized as deprecated because of caching
categorized_chan_upds = self.add_channel_updates([payload], verify=False)
+ def handle_error_code_from_failed_htlc(self, code, data, sender_idx, route):
+ # handle some specific error codes
+ failure_codes = {
+ OnionFailureCode.TEMPORARY_CHANNEL_FAILURE: 0,
+ OnionFailureCode.AMOUNT_BELOW_MINIMUM: 8,
+ OnionFailureCode.FEE_INSUFFICIENT: 8,
+ OnionFailureCode.INCORRECT_CLTV_EXPIRY: 4,
+ OnionFailureCode.EXPIRY_TOO_SOON: 0,
+ OnionFailureCode.CHANNEL_DISABLED: 2,
+ }
+ if code in failure_codes:
+ offset = failure_codes[code]
+ channel_update_len = int.from_bytes(data[offset:offset+2], byteorder="big")
+ channel_update_as_received = data[offset+2: offset+2+channel_update_len]
+ channel_update_typed = (258).to_bytes(length=2, byteorder="big") + channel_update_as_received
+ # note: some nodes put channel updates in error msgs with the leading msg_type already there.
+ # we try decoding both ways here.
+ try:
+ message_type, payload = decode_msg(channel_update_typed)
+ payload['raw'] = channel_update_typed
+ except: # FIXME: too broad
+ message_type, payload = decode_msg(channel_update_as_received)
+ payload['raw'] = channel_update_as_received
+ categorized_chan_upds = self.add_channel_updates([payload])
+ blacklist = False
+ if categorized_chan_upds.good:
+ self.logger.info("applied channel update on our db")
+ #self.maybe_save_remote_update(payload)
+ elif categorized_chan_upds.orphaned:
+ # maybe it is a private channel (and data in invoice was outdated)
+ self.logger.info("maybe channel update is for private channel?")
+ start_node_id = route[sender_idx].node_id
+ self.add_channel_update_for_private_channel(payload, start_node_id)
+ elif categorized_chan_upds.expired:
+ blacklist = True
+ elif categorized_chan_upds.deprecated:
+ self.logger.info(f'channel update is not more recent.')
+ blacklist = True
+ else:
+ blacklist = True
+ if blacklist:
+ # blacklist channel after reporter node
+ # TODO this should depend on the error (even more granularity)
+ # also, we need finer blacklisting (directed edges; nodes)
+ try:
+ short_chan_id = route[sender_idx + 1].short_channel_id
+ except IndexError:
+ self.logger.info("payment destination reported error")
+ else:
+ self.network.path_finder.add_to_blacklist(short_chan_id)
+
def create_database(self):
c = self.conn.cursor()
c.execute(create_node_info)
diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
@@ -15,7 +15,7 @@ from electrum.wallet import Wallet, InternalAddressCorruption
from electrum.util import profiler, InvalidPassword, send_exception_to_crash_reporter
from electrum.plugin import run_hook
from electrum.util import format_satoshis, format_satoshis_plain, format_fee_satoshis
-from electrum.util import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
+from electrum.util import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_FAILED, PR_INFLIGHT
from electrum import blockchain
from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
from .i18n import _
@@ -205,24 +205,26 @@ class ElectrumWindow(App):
def on_fee_histogram(self, *args):
self._trigger_update_history()
- def on_payment_received(self, event, wallet, key, status):
+ def on_request_status(self, event, key, status):
+ if key not in self.wallet.requests:
+ return
+ self.update_tab('receive')
if self.request_popup and self.request_popup.key == key:
self.request_popup.set_status(status)
if status == PR_PAID:
self.show_info(_('Payment Received') + '\n' + key)
+ self._trigger_update_history()
- def on_payment_status(self, event, key, status, *args):
+ def on_invoice_status(self, event, key, status, log):
+ # todo: update single item
self.update_tab('send')
- if status == 'success':
+ if status == PR_PAID:
self.show_info(_('Payment was sent'))
self._trigger_update_history()
- elif status == 'progress':
+ elif status == PR_INFLIGHT:
pass
- elif status == 'failure':
+ elif status == PR_FAILED:
self.show_info(_('Payment failed'))
- elif status == 'error':
- e = args[0]
- self.show_error(_('Error') + '\n' + str(e))
def _get_bu(self):
decimal_point = self.electrum_config.get('decimal_point', DECIMAL_POINT_DEFAULT)
@@ -556,10 +558,10 @@ class ElectrumWindow(App):
self.network.register_callback(self.on_fee_histogram, ['fee_histogram'])
self.network.register_callback(self.on_quotes, ['on_quotes'])
self.network.register_callback(self.on_history, ['on_history'])
- self.network.register_callback(self.on_payment_received, ['payment_received'])
self.network.register_callback(self.on_channels, ['channels'])
self.network.register_callback(self.on_channel, ['channel'])
- self.network.register_callback(self.on_payment_status, ['payment_status'])
+ self.network.register_callback(self.on_invoice_status, ['invoice_status'])
+ self.network.register_callback(self.on_request_status, ['request_status'])
# load wallet
self.load_wallet_by_name(self.electrum_config.get_wallet_path(use_gui_last_wallet=True))
# URI passed in config
diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py
@@ -27,10 +27,11 @@ from enum import IntEnum
from PyQt5.QtCore import Qt, QItemSelectionModel
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont
-from PyQt5.QtWidgets import QHeaderView, QMenu
+from PyQt5.QtWidgets import QHeaderView, QMenu, QVBoxLayout, QGridLayout, QLabel
from electrum.i18n import _
-from electrum.util import format_time, PR_UNPAID, PR_PAID, get_request_status
+from electrum.util import format_time, PR_UNPAID, PR_PAID, PR_INFLIGHT
+from electrum.util import get_request_status
from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
from electrum.lnutil import lndecode, RECEIVED
from electrum.bitcoin import COIN
@@ -38,6 +39,7 @@ from electrum import constants
from .util import (MyTreeView, read_QIcon, MONOSPACE_FONT,
import_meta_gui, export_meta_gui, pr_icons)
+from .util import CloseButton, Buttons
@@ -65,12 +67,35 @@ class InvoiceList(MyTreeView):
super().__init__(parent, self.create_menu,
stretch_column=self.Columns.DESCRIPTION,
editable_columns=[])
+ self.logs = {}
self.setSortingEnabled(True)
self.setModel(QStandardItemModel(self))
self.update()
+ def update_item(self, key, status, log):
+ req = self.parent.wallet.get_invoice(key)
+ if req is None:
+ return
+ model = self.model()
+ for row in range(0, model.rowCount()):
+ item = model.item(row, 0)
+ if item.data(ROLE_REQUEST_ID) == key:
+ break
+ else:
+ return
+ status_item = model.item(row, self.Columns.STATUS)
+ status_str = get_request_status(req)
+ if log:
+ self.logs[key] = log
+ if status == PR_INFLIGHT:
+ status_str += '... (%d)'%len(log)
+ status_item.setText(status_str)
+ status_item.setIcon(read_QIcon(pr_icons.get(status)))
+
def update(self):
_list = self.parent.wallet.get_invoices()
+ # filter out paid invoices unless we have the log
+ _list = [x for x in _list if x and x.get('status') != PR_PAID or x.get('rhash') in self.logs]
self.model().clear()
self.update_headers(self.__class__.headers)
for idx, item in enumerate(_list):
@@ -136,6 +161,29 @@ class InvoiceList(MyTreeView):
invoice = self.parent.wallet.get_invoice(key)
menu.addAction(_("Details"), lambda: self.parent.show_invoice(key))
if invoice['status'] == PR_UNPAID:
- menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(invoice))
+ menu.addAction(_("Pay"), lambda: self.parent.do_pay_invoice(invoice))
+ if key in self.logs:
+ menu.addAction(_("View log"), lambda: self.show_log(key))
menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(key))
menu.exec_(self.viewport().mapToGlobal(position))
+
+ def show_log(self, key):
+ from .util import WindowModalDialog
+ log = self.logs.get(key)
+ d = WindowModalDialog(self, _("Payment log"))
+ vbox = QVBoxLayout(d)
+ grid = QGridLayout()
+ grid.addWidget(QLabel(_("Node ID")), 0, 0)
+ grid.addWidget(QLabel(_("Message")), 0, 1)
+ for i, (route, success, failure_data) in enumerate(log):
+ print(route[0].node_id)
+ if not success:
+ failure_node_id, failure_msg = failure_data
+ code, data = failure_msg.code, failure_msg.data
+ grid.addWidget(QLabel(failure_node_id.hex()), i+1, 0)
+ grid.addWidget(QLabel(repr(code)), i+1, 1)
+ else:
+ pass
+ vbox.addLayout(grid)
+ vbox.addLayout(Buttons(CloseButton(d)))
+ d.exec_()
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
@@ -73,7 +73,7 @@ from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
from electrum.exchange_rate import FxThread
from electrum.simple_config import SimpleConfig
from electrum.logging import Logger
-from electrum.paymentrequest import PR_PAID
+from electrum.util import PR_PAID, PR_UNPAID, PR_INFLIGHT, PR_FAILED
from electrum.util import pr_expiration_values
from .exception_window import Exception_Hook
@@ -232,8 +232,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
interests = ['wallet_updated', 'network_updated', 'blockchain_updated',
'new_transaction', 'status',
'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes',
- 'on_history', 'channel', 'channels', 'payment_received',
- 'payment_status']
+ 'on_history', 'channel', 'channels',
+ 'invoice_status', 'request_status']
# To avoid leaking references to "self" that prevent the
# window from being GC-ed when closed, callbacks should be
# methods of this class only, and specifically not be
@@ -382,8 +382,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
elif event == 'channel':
self.channels_list.update_single_row.emit(*args)
self.update_status()
- elif event == 'payment_status':
- self.on_payment_status(*args)
+ elif event == 'request_status':
+ self.on_request_status(*args)
+ elif event == 'invoice_status':
+ self.on_invoice_status(*args)
elif event == 'status':
self.update_status()
elif event == 'banner':
@@ -401,10 +403,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.fee_slider.update()
self.require_fee_update = True
self.history_model.on_fee_histogram()
- elif event == 'payment_received':
- wallet, key, status = args
- if wallet == self.wallet:
- self.notify(_('Payment received') + '\n' + key)
else:
self.logger.info(f"unexpected network event: {event} {args}")
@@ -1682,24 +1680,31 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
amount_sat = self.amount_e.get_amount()
attempts = LN_NUM_PAYMENT_ATTEMPTS
def task():
- self.wallet.lnworker.pay(invoice, amount_sat, attempts)
+ try:
+ self.wallet.lnworker.pay(invoice, amount_sat, attempts)
+ except Exception as e:
+ self.show_error(str(e))
self.do_clear()
self.wallet.thread.add(task)
self.invoice_list.update()
- def on_payment_status(self, key, status, *args):
- # todo: check that key is in this wallet's invoice list
- self.invoice_list.update()
- if status == 'success':
+ def on_request_status(self, key, status):
+ if key not in self.wallet.requests:
+ return
+ if status == PR_PAID:
+ self.notify(_('Payment received') + '\n' + key)
+
+ def on_invoice_status(self, key, status, log):
+ if key not in self.wallet.invoices:
+ return
+ self.invoice_list.update_item(key, status, log)
+ if status == PR_PAID:
self.show_message(_('Payment succeeded'))
self.need_update.set()
- elif status == 'progress':
- print('on_payment_status', key, status, args)
- elif status == 'failure':
+ elif status == PR_FAILED:
self.show_error(_('Payment failed'))
- elif status == 'error':
- e = args[0]
- self.show_error(_('Error') + '\n' + str(e))
+ else:
+ pass
def read_invoice(self):
if self.check_send_tab_payto_line_and_show_errors():
diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
@@ -38,6 +38,7 @@ from .crypto import sha256, sha256d
from .transaction import Transaction
from .logging import Logger
+from .lnonion import decode_onion_error
from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKeypair, ChannelConstraints,
get_per_commitment_secret_from_seed, secret_to_pubkey, derive_privkey, make_closing_tx,
sign_and_get_sig_string, RevocationStore, derive_blinded_pubkey, Direction, derive_pubkey,
@@ -578,6 +579,13 @@ class Channel(Logger):
htlc = log['adds'][htlc_id]
return htlc.payment_hash
+ def decode_onion_error(self, reason, route, htlc_id):
+ failure_msg, sender_idx = decode_onion_error(
+ reason,
+ [x.node_id for x in route],
+ self.onion_keys[htlc_id])
+ return failure_msg, sender_idx
+
def receive_htlc_settle(self, preimage, htlc_id):
self.logger.info("receive_htlc_settle")
log = self.hm.log[LOCAL]
diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
@@ -86,7 +86,6 @@ class Peer(Logger):
self.announcement_signatures = defaultdict(asyncio.Queue)
self.closing_signed = defaultdict(asyncio.Queue)
#
- self.attempted_route = {}
self.orphan_channel_updates = OrderedDict()
self._local_changed_events = defaultdict(asyncio.Event)
self._remote_changed_events = defaultdict(asyncio.Event)
@@ -1096,7 +1095,6 @@ class Peer(Logger):
self.logger.info(f"on_update_fail_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}")
chan.receive_fail_htlc(htlc_id)
local_ctn = chan.get_latest_ctn(LOCAL)
- asyncio.ensure_future(self._handle_error_code_from_failed_htlc(payload, channel_id, htlc_id))
asyncio.ensure_future(self._on_update_fail_htlc(channel_id, htlc_id, local_ctn, reason))
@log_exceptions
@@ -1106,75 +1104,6 @@ class Peer(Logger):
payment_hash = chan.get_payment_hash(htlc_id)
self.lnworker.payment_failed(payment_hash, reason)
- @log_exceptions
- async def _handle_error_code_from_failed_htlc(self, payload, channel_id, htlc_id):
- chan = self.channels[channel_id]
- key = (channel_id, htlc_id)
- try:
- route = self.attempted_route[key] # type: List[RouteEdge]
- except KeyError:
- # the remote might try to fail an htlc after we restarted...
- # attempted_route is not persisted, so we will get here then
- self.logger.info("UPDATE_FAIL_HTLC. cannot decode! attempted route is MISSING. {}".format(key))
- return
- error_reason = payload["reason"]
- failure_msg, sender_idx = decode_onion_error(
- error_reason,
- [x.node_id for x in route],
- chan.onion_keys[htlc_id])
- code, data = failure_msg.code, failure_msg.data
- self.logger.info(f"UPDATE_FAIL_HTLC {repr(code)} {data}")
- self.logger.info(f"error reported by {bh2u(route[sender_idx].node_id)}")
- # handle some specific error codes
- failure_codes = {
- OnionFailureCode.TEMPORARY_CHANNEL_FAILURE: 0,
- OnionFailureCode.AMOUNT_BELOW_MINIMUM: 8,
- OnionFailureCode.FEE_INSUFFICIENT: 8,
- OnionFailureCode.INCORRECT_CLTV_EXPIRY: 4,
- OnionFailureCode.EXPIRY_TOO_SOON: 0,
- OnionFailureCode.CHANNEL_DISABLED: 2,
- }
- if code in failure_codes:
- offset = failure_codes[code]
- channel_update_len = int.from_bytes(data[offset:offset+2], byteorder="big")
- channel_update_as_received = data[offset+2: offset+2+channel_update_len]
- channel_update_typed = (258).to_bytes(length=2, byteorder="big") + channel_update_as_received
- # note: some nodes put channel updates in error msgs with the leading msg_type already there.
- # we try decoding both ways here.
- try:
- message_type, payload = decode_msg(channel_update_typed)
- payload['raw'] = channel_update_typed
- except: # FIXME: too broad
- message_type, payload = decode_msg(channel_update_as_received)
- payload['raw'] = channel_update_as_received
- categorized_chan_upds = self.channel_db.add_channel_updates([payload])
- blacklist = False
- if categorized_chan_upds.good:
- self.logger.info("applied channel update on our db")
- self.maybe_save_remote_update(payload)
- elif categorized_chan_upds.orphaned:
- # maybe it is a private channel (and data in invoice was outdated)
- self.logger.info("maybe channel update is for private channel?")
- start_node_id = route[sender_idx].node_id
- self.channel_db.add_channel_update_for_private_channel(payload, start_node_id)
- elif categorized_chan_upds.expired:
- blacklist = True
- elif categorized_chan_upds.deprecated:
- self.logger.info(f'channel update is not more recent.')
- blacklist = True
- else:
- blacklist = True
- if blacklist:
- # blacklist channel after reporter node
- # TODO this should depend on the error (even more granularity)
- # also, we need finer blacklisting (directed edges; nodes)
- try:
- short_chan_id = route[sender_idx + 1].short_channel_id
- except IndexError:
- self.logger.info("payment destination reported error")
- else:
- self.network.path_finder.add_to_blacklist(short_chan_id)
-
def maybe_send_commitment(self, chan: Channel):
# REMOTE should revoke first before we can sign a new ctx
if chan.hm.is_revack_pending(REMOTE):
@@ -1215,7 +1144,6 @@ class Peer(Logger):
htlc = chan.add_htlc(htlc)
remote_ctn = chan.get_latest_ctn(REMOTE)
chan.onion_keys[htlc.htlc_id] = secret_key
- self.attempted_route[(chan.channel_id, htlc.htlc_id)] = route
self.logger.info(f"starting payment. len(route)={len(route)}. route: {route}. htlc: {htlc}")
self.send_message("update_add_htlc",
channel_id=chan.channel_id,
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
@@ -21,7 +21,8 @@ import dns.exception
from . import constants
from . import keystore
-from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT, profiler
+from .util import profiler
+from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED
from .util import PR_TYPE_LN
from .keystore import BIP32_KeyStore
from .bitcoin import COIN
@@ -92,6 +93,9 @@ class PaymentInfo(NamedTuple):
status: int
+class NoPathFound(PaymentFailure):
+ pass
+
class LNWorker(Logger):
def __init__(self, xprv):
@@ -825,19 +829,9 @@ class LNWallet(LNWorker):
"""
Can be called from other threads
"""
- addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
- key = bh2u(addr.paymenthash)
coro = self._pay(invoice, amount_sat, attempts)
fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
- try:
- success = fut.result()
- except Exception as e:
- self.network.trigger_callback('payment_status', key, 'error', e)
- return
- if success:
- self.network.trigger_callback('payment_status', key, 'success')
- else:
- self.network.trigger_callback('payment_status', key, 'failure')
+ success = fut.result()
def get_channel_by_short_id(self, short_channel_id: ShortChannelID) -> Channel:
with self.lock:
@@ -848,9 +842,10 @@ class LNWallet(LNWorker):
@log_exceptions
async def _pay(self, invoice, amount_sat=None, attempts=1):
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
- key = bh2u(lnaddr.paymenthash)
+ payment_hash = lnaddr.paymenthash
+ key = payment_hash.hex()
amount = int(lnaddr.amount * COIN) if lnaddr.amount else None
- status = self.get_payment_status(lnaddr.paymenthash)
+ status = self.get_payment_status(payment_hash)
if status == PR_PAID:
raise PaymentFailure(_("This invoice has been paid already"))
if status == PR_INFLIGHT:
@@ -859,13 +854,22 @@ class LNWallet(LNWorker):
self.save_payment_info(info)
self._check_invoice(invoice, amount_sat)
self.wallet.set_label(key, lnaddr.get_description())
+ log = []
for i in range(attempts):
- route = await self._create_route_from_invoice(decoded_invoice=lnaddr)
- self.network.trigger_callback('payment_status', key, 'progress', i)
- success, preimage, reason = await self._pay_to_route(route, lnaddr)
+ try:
+ route = await self._create_route_from_invoice(decoded_invoice=lnaddr)
+ except NoPathFound:
+ success = False
+ break
+ self.network.trigger_callback('invoice_status', key, PR_INFLIGHT, log)
+ success, preimage, failure_node_id, failure_msg = await self._pay_to_route(route, lnaddr)
if success:
- return True
- return False
+ log.append((route, True, preimage))
+ break
+ else:
+ log.append((route, False, (failure_node_id, failure_msg)))
+ self.network.trigger_callback('invoice_status', key, PR_PAID if success else PR_FAILED, log)
+ return success
async def _pay_to_route(self, route, lnaddr):
short_channel_id = route[0].short_channel_id
@@ -877,7 +881,18 @@ class LNWallet(LNWorker):
peer = self.peers[route[0].node_id]
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)
- return await self.await_payment(lnaddr.paymenthash)
+ success, preimage, reason = await self.await_payment(lnaddr.paymenthash)
+ if success:
+ failure_node_id = None
+ failure_msg = None
+ else:
+ failure_msg, sender_idx = chan.decode_onion_error(reason, route, htlc.htlc_id)
+ failure_node_id = route[sender_idx].node_id
+ code, data = failure_msg.code, failure_msg.data
+ self.logger.info(f"UPDATE_FAIL_HTLC {repr(code)} {data}")
+ self.logger.info(f"error reported by {bh2u(route[sender_idx].node_id)}")
+ self.channel_db.handle_error_code_from_failed_htlc(code, data, sender_idx, route)
+ return success, preimage, failure_node_id, failure_msg
@staticmethod
def _check_invoice(invoice, amount_sat=None):
@@ -945,11 +960,11 @@ class LNWallet(LNWorker):
if route is None:
path = self.network.path_finder.find_path_for_payment(self.node_keypair.pubkey, invoice_pubkey, amount_msat, channels)
if not path:
- raise PaymentFailure(_("No path found"))
+ raise NoPathFound()
route = self.network.path_finder.create_route_from_path(path, self.node_keypair.pubkey)
if not is_route_sane_to_use(route, amount_msat, decoded_invoice.get_min_final_cltv_expiry()):
self.logger.info(f"rejecting insane route {route}")
- raise PaymentFailure(_("No path found"))
+ raise NoPathFound()
return route
def add_request(self, amount_sat, message, expiry):
@@ -1052,6 +1067,7 @@ class LNWallet(LNWorker):
def payment_received(self, payment_hash: bytes):
self.set_payment_status(payment_hash, PR_PAID)
+ self.network.trigger_callback('request_status', payment_hash.hex(), PR_PAID)
async def _calc_routing_hints_for_invoice(self, amount_sat):
"""calculate routing hints (BOLT-11 'r' field)"""
diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py
@@ -18,7 +18,7 @@ from electrum.lnutil import LightningPeerConnectionClosed, RemoteMisbehaving
from electrum.lnutil import PaymentFailure, LnLocalFeatures
from electrum.lnrouter import LNPathFinder
from electrum.channel_db import ChannelDB
-from electrum.lnworker import LNWallet
+from electrum.lnworker import LNWallet, NoPathFound
from electrum.lnmsg import encode_msg, decode_msg
from electrum.logging import console_stderr_handler
from electrum.lnworker import PaymentInfo, RECEIVED, PR_UNPAID
@@ -251,9 +251,8 @@ class TestPeer(ElectrumTestCase):
# check if a tx (commitment transaction) was broadcasted:
assert q1.qsize() == 1
- with self.assertRaises(PaymentFailure) as e:
+ with self.assertRaises(NoPathFound) as e:
run(w1._create_route_from_invoice(decoded_invoice=addr))
- self.assertEqual(str(e.exception), 'No path found')
peer = w1.peers[route[0].node_id]
# AssertionError is ok since we shouldn't use old routes, and the
diff --git a/electrum/wallet.py b/electrum/wallet.py
@@ -558,7 +558,6 @@ class Abstract_Wallet(AddressSynchronizer):
def get_invoices(self):
out = [self.get_invoice(key) for key in self.invoices.keys()]
- out = [x for x in out if x and x.get('status') != PR_PAID]
out.sort(key=operator.itemgetter('time'))
return out