electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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:
Melectrum/channel_db.py | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Melectrum/gui/kivy/main_window.py | 24+++++++++++++-----------
Melectrum/gui/qt/invoice_list.py | 54+++++++++++++++++++++++++++++++++++++++++++++++++++---
Melectrum/gui/qt/main_window.py | 45+++++++++++++++++++++++++--------------------
Melectrum/lnchannel.py | 8++++++++
Melectrum/lnpeer.py | 72------------------------------------------------------------------------
Melectrum/lnworker.py | 60++++++++++++++++++++++++++++++++++++++----------------------
Melectrum/tests/test_lnpeer.py | 5++---
Melectrum/wallet.py | 1-
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