Browse Source

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.
dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
ThomasV 5 years ago
parent
commit
dd0be1541e
  1. 53
      electrum/channel_db.py
  2. 24
      electrum/gui/kivy/main_window.py
  3. 54
      electrum/gui/qt/invoice_list.py
  4. 45
      electrum/gui/qt/main_window.py
  5. 8
      electrum/lnchannel.py
  6. 72
      electrum/lnpeer.py
  7. 60
      electrum/lnworker.py
  8. 5
      electrum/tests/test_lnpeer.py
  9. 1
      electrum/wallet.py

53
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)

24
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

54
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_()

45
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():

8
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]

72
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,

60
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)"""

5
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

1
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

Loading…
Cancel
Save