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.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_()
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user