URI: 
       tlnworker: introduce PaymentAttemptLog NamedTuple - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit b99add59c336ef039ae4905287cedc9bf8ef8733
   DIR parent 24ebc77d76cb342502a8443c4edc2a1feff8f067
  HTML Author: SomberNight <somber.night@protonmail.com>
       Date:   Tue, 10 Dec 2019 03:17:57 +0100
       
       lnworker: introduce PaymentAttemptLog NamedTuple
       
       Diffstat:
         M electrum/gui/qt/invoice_list.py     |      39 ++++++++++++++++++++-----------
         M electrum/lnaddr.py                  |       4 ++--
         M electrum/lnonion.py                 |       4 ++--
         M electrum/lnpeer.py                  |       4 ++--
         M electrum/lnrouter.py                |       7 +++++--
         M electrum/lnutil.py                  |      18 +++++++++++++++++-
         M electrum/lnworker.py                |      39 ++++++++++++++++++-------------
       
       7 files changed, 76 insertions(+), 39 deletions(-)
       ---
   DIR diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py
       t@@ -24,6 +24,7 @@
        # SOFTWARE.
        
        from enum import IntEnum
       +from typing import Sequence
        
        from PyQt5.QtCore import Qt, QItemSelectionModel
        from PyQt5.QtGui import QStandardItemModel, QStandardItem
       t@@ -34,7 +35,7 @@ from electrum.i18n import _
        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 format_short_channel_id
       +from electrum.lnutil import PaymentAttemptLog
        
        from .util import (MyTreeView, read_QIcon,
                           import_meta_gui, export_meta_gui, pr_icons)
       t@@ -174,24 +175,34 @@ class InvoiceList(MyTreeView):
                menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(key))
                menu.exec_(self.viewport().mapToGlobal(position))
        
       -    def show_log(self, key, log):
       +    def show_log(self, key, log: Sequence[PaymentAttemptLog]):
                d = WindowModalDialog(self, _("Payment log"))
       +        d.setMinimumWidth(800)
                vbox = QVBoxLayout(d)
                log_w = QTreeWidget()
                log_w.setHeaderLabels([_('Route'), _('Channel ID'), _('Message'), _('Blacklist')])
       -        for i, (route, success, failure_log) in enumerate(log):
       -            route_str = '%d'%len(route)
       -            if not success:
       -                sender_idx, failure_msg, blacklist = failure_log
       -                short_channel_id = route[sender_idx+1].short_channel_id
       -                data = failure_msg.data
       -                message = repr(failure_msg.code)
       +        for payment_attempt_log in log:
       +            if not payment_attempt_log.exception:
       +                route = payment_attempt_log.route
       +                route_str = '%d'%len(route)
       +                if not payment_attempt_log.success:
       +                    sender_idx = payment_attempt_log.failure_details.sender_idx
       +                    failure_msg = payment_attempt_log.failure_details.failure_msg
       +                    blacklist_msg = str(payment_attempt_log.failure_details.is_blacklisted)
       +                    short_channel_id = route[sender_idx+1].short_channel_id
       +                    data = failure_msg.data
       +                    message = repr(failure_msg.code)
       +                else:
       +                    short_channel_id = route[-1].short_channel_id
       +                    message = _('Success')
       +                    blacklist_msg = str(False)
       +                chan_str = str(short_channel_id)
                    else:
       -                short_channel_id = route[-1].short_channel_id
       -                message = _('Success')
       -                blacklist = False
       -            chan_str = format_short_channel_id(short_channel_id)
       -            x = QTreeWidgetItem([route_str, chan_str, message, repr(blacklist)])
       +                route_str = 'None'
       +                chan_str = 'N/A'
       +                message = str(payment_attempt_log.exception)
       +                blacklist_msg = 'N/A'
       +            x = QTreeWidgetItem([route_str, chan_str, message, blacklist_msg])
                    log_w.addTopLevelItem(x)
                vbox.addWidget(log_w)
                vbox.addLayout(Buttons(CloseButton(d)))
   DIR diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py
       t@@ -237,11 +237,11 @@ def lnencode(addr, privkey):
            return bech32_encode(hrp, bitarray_to_u5(data))
        
        class LnAddr(object):
       -    def __init__(self, paymenthash=None, amount=None, currency=None, tags=None, date=None):
       +    def __init__(self, paymenthash: bytes = None, amount=None, currency=None, tags=None, date=None):
                self.date = int(time.time()) if not date else int(date)
                self.tags = [] if not tags else tags
                self.unknown_tags = []
       -        self.paymenthash=paymenthash
       +        self.paymenthash = paymenthash
                self.signature = None
                self.pubkey = None
                self.currency = constants.net.SEGWIT_HRP if currency is None else currency
   DIR diff --git a/electrum/lnonion.py b/electrum/lnonion.py
       t@@ -36,7 +36,7 @@ from .lnutil import (get_ecdh, PaymentFailure, NUM_MAX_HOPS_IN_PAYMENT_PATH,
                             NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID)
        
        if TYPE_CHECKING:
       -    from .lnrouter import RouteEdge
       +    from .lnrouter import LNPaymentRoute
        
        
        HOPS_DATA_SIZE = 1300      # also sometimes called routingInfoSize in bolt-04
       t@@ -188,7 +188,7 @@ def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes,
                hmac=next_hmac)
        
        
       -def calc_hops_data_for_payment(route: List['RouteEdge'], amount_msat: int, final_cltv: int) \
       +def calc_hops_data_for_payment(route: 'LNPaymentRoute', amount_msat: int, final_cltv: int) \
                -> Tuple[List[OnionHopsDataSingle], int, int]:
            """Returns the hops_data to be used for constructing an onion packet,
            and the amount_msat and cltv to be used on our immediate channel.
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -50,7 +50,7 @@ from .lnutil import ln_dummy_address
        
        if TYPE_CHECKING:
            from .lnworker import LNWorker, LNGossip, LNWallet
       -    from .lnrouter import RouteEdge
       +    from .lnrouter import RouteEdge, LNPaymentRoute
            from .transaction import PartialTransaction
        
        
       t@@ -1126,7 +1126,7 @@ class Peer(Logger):
                while chan.get_latest_ctn(LOCAL) <= ctn:
                    await self._local_changed_events[chan.channel_id].wait()
        
       -    async def pay(self, route: List['RouteEdge'], chan: Channel, amount_msat: int,
       +    async def pay(self, route: 'LNPaymentRoute', chan: Channel, amount_msat: int,
                          payment_hash: bytes, min_final_cltv_expiry: int) -> UpdateAddHtlc:
                if chan.get_state() != channel_states.OPEN:
                    raise PaymentFailure('Channel not open')
   DIR diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py
       t@@ -87,7 +87,10 @@ class RouteEdge(NamedTuple):
                return True
        
        
       -def is_route_sane_to_use(route: List[RouteEdge], invoice_amount_msat: int, min_final_cltv_expiry: int) -> bool:
       +LNPaymentRoute = Sequence[RouteEdge]
       +
       +
       +def is_route_sane_to_use(route: LNPaymentRoute, invoice_amount_msat: int, min_final_cltv_expiry: int) -> bool:
            """Run some sanity checks on the whole route, before attempting to use it.
            called when we are paying; so e.g. lower cltv is better
            """
       t@@ -238,7 +241,7 @@ class LNPathFinder(Logger):
                    edge_startnode = edge_endnode
                return path
        
       -    def create_route_from_path(self, path, from_node_id: bytes) -> List[RouteEdge]:
       +    def create_route_from_path(self, path, from_node_id: bytes) -> LNPaymentRoute:
                assert isinstance(from_node_id, bytes)
                if path is None:
                    raise Exception('cannot create route from None path')
   DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py
       t@@ -5,7 +5,7 @@
        from enum import IntFlag, IntEnum
        import json
        from collections import namedtuple
       -from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set
       +from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence
        import re
        
        from aiorpcx import NetAddress
       t@@ -24,6 +24,8 @@ from .keystore import BIP32_KeyStore
        
        if TYPE_CHECKING:
            from .lnchannel import Channel
       +    from .lnrouter import LNPaymentRoute
       +    from .lnonion import OnionRoutingFailureMessage
        
        
        HTLC_TIMEOUT_WEIGHT = 663
       t@@ -116,6 +118,20 @@ class Outpoint(NamedTuple("Outpoint", [('txid', str), ('output_index', int)])):
                return "{}:{}".format(self.txid, self.output_index)
        
        
       +class PaymentAttemptFailureDetails(NamedTuple):
       +    sender_idx: int
       +    failure_msg: 'OnionRoutingFailureMessage'
       +    is_blacklisted: bool
       +
       +
       +class PaymentAttemptLog(NamedTuple):
       +    success: bool
       +    route: Optional['LNPaymentRoute'] = None
       +    preimage: Optional[bytes] = None
       +    failure_details: Optional[PaymentAttemptFailureDetails] = None
       +    exception: Optional[Exception] = None
       +
       +
        class LightningError(Exception): pass
        class LightningPeerConnectionClosed(LightningError): pass
        class UnableToDeriveSecret(LightningError): pass
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -51,13 +51,13 @@ from .lnutil import (Outpoint, LNPeerAddr,
                             UnknownPaymentHash, MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE,
                             NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner,
                             UpdateAddHtlc, Direction, LnLocalFeatures, format_short_channel_id,
       -                     ShortChannelID)
       +                     ShortChannelID, PaymentAttemptLog, PaymentAttemptFailureDetails)
        from .lnutil import ln_dummy_address
        from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput
        from .lnonion import OnionFailureCode
        from .lnmsg import decode_msg
        from .i18n import _
       -from .lnrouter import RouteEdge, is_route_sane_to_use
       +from .lnrouter import RouteEdge, LNPaymentRoute, is_route_sane_to_use
        from .address_synchronizer import TX_HEIGHT_LOCAL
        from . import lnsweep
        from .lnwatcher import LNWatcher
       t@@ -118,7 +118,9 @@ class PaymentInfo(NamedTuple):
        
        
        class NoPathFound(PaymentFailure):
       -    pass
       +    def __str__(self):
       +        return _('No path found')
       +
        
        class LNWorker(Logger):
        
       t@@ -345,7 +347,7 @@ class LNWallet(LNWorker):
                self.preimages = self.storage.get('lightning_preimages', {})      # RHASH -> preimage
                self.sweep_address = wallet.get_receiving_address()
                self.lock = threading.RLock()
       -        self.logs = defaultdict(list)
       +        self.logs = defaultdict(list)  # type: Dict[str, List[PaymentAttemptLog]]  # key is RHASH
        
                # note: accessing channels (besides simple lookup) needs self.lock!
                self.channels = {}  # type: Dict[bytes, Channel]
       t@@ -891,7 +893,7 @@ class LNWallet(LNWorker):
                            return chan
        
            @log_exceptions
       -    async def _pay(self, invoice, amount_sat=None, attempts=1):
       +    async def _pay(self, invoice, amount_sat=None, attempts=1) -> bool:
                lnaddr = self._check_invoice(invoice, amount_sat)
                payment_hash = lnaddr.paymenthash
                key = payment_hash.hex()
       t@@ -905,23 +907,23 @@ class LNWallet(LNWorker):
                self.save_payment_info(info)
                self.wallet.set_label(key, lnaddr.get_description())
                log = self.logs[key]
       +        success = False
                for i in range(attempts):
                    try:
                        route = await self._create_route_from_invoice(decoded_invoice=lnaddr)
       -            except NoPathFound:
       -                success = False
       +            except NoPathFound as e:
       +                log.append(PaymentAttemptLog(success=False, exception=e))
                        break
                    self.network.trigger_callback('invoice_status', key, PR_INFLIGHT)
       -            success, preimage, failure_log = await self._pay_to_route(route, lnaddr)
       +            payment_attempt_log = await self._pay_to_route(route, lnaddr)
       +            log.append(payment_attempt_log)
       +            success = payment_attempt_log.success
                    if success:
       -                log.append((route, True, preimage))
                        break
       -            else:
       -                log.append((route, False, failure_log))
                self.network.trigger_callback('invoice_status', key, PR_PAID if success else PR_FAILED)
                return success
        
       -    async def _pay_to_route(self, route, lnaddr):
       +    async def _pay_to_route(self, route: LNPaymentRoute, lnaddr: LnAddr) -> PaymentAttemptLog:
                short_channel_id = route[0].short_channel_id
                chan = self.get_channel_by_short_id(short_channel_id)
                if not chan:
       t@@ -948,8 +950,13 @@ class LNWallet(LNWorker):
                            self.logger.info("payment destination reported error")
                        else:
                            self.network.path_finder.add_to_blacklist(short_chan_id)
       -            failure_log = (sender_idx, failure_msg, blacklist)
       -        return success, preimage, failure_log
       +            failure_log = PaymentAttemptFailureDetails(sender_idx=sender_idx,
       +                                                       failure_msg=failure_msg,
       +                                                       is_blacklisted=blacklist)
       +        return PaymentAttemptLog(route=route,
       +                                 success=success,
       +                                 preimage=preimage,
       +                                 failure_details=failure_log)
        
            def handle_error_code_from_failed_htlc(self, failure_msg, sender_idx, route, peer):
                code, data = failure_msg.code, failure_msg.data
       t@@ -1011,11 +1018,11 @@ class LNWallet(LNWorker):
                        f"min_final_cltv_expiry: {addr.get_min_final_cltv_expiry()}"))
                return addr
        
       -    async def _create_route_from_invoice(self, decoded_invoice) -> List[RouteEdge]:
       +    async def _create_route_from_invoice(self, decoded_invoice) -> LNPaymentRoute:
                amount_msat = int(decoded_invoice.amount * COIN * 1000)
                invoice_pubkey = decoded_invoice.pubkey.serialize()
                # use 'r' field from invoice
       -        route = None  # type: Optional[List[RouteEdge]]
       +        route = None  # type: Optional[LNPaymentRoute]
                # only want 'r' tags
                r_tags = list(filter(lambda x: x[0] == 'r', decoded_invoice.tags))
                # strip the tag type, it's implicitly 'r' now