URI: 
       timprove payment status callbacks: - add 'computing route' status for lightning payments - use separate callbacks for invoice status and payment popups - show payment error and payment logs in kivy - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 3d69f3b0beb4a3be74aeef8a96f5ffef5f82ee90
   DIR parent 5d4f8f316480776eb78d98ea9ac9c3b2a05ef01d
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Tue, 10 Mar 2020 13:27:02 +0100
       
       improve payment status callbacks:
        - add 'computing route' status for lightning payments
        - use separate callbacks for invoice status and payment popups
        - show payment error and payment logs in kivy
       
       Diffstat:
         M electrum/gui/kivy/main_window.py    |      14 +++++++++-----
         M electrum/gui/kivy/uix/dialogs/invo… |      10 ++++++++++
         M electrum/gui/qt/main_window.py      |      20 ++++++++++++--------
         M electrum/gui/qt/util.py             |       3 ++-
         M electrum/lnworker.py                |      53 +++++++++++++++++++++++--------
         M electrum/util.py                    |       3 +++
       
       6 files changed, 76 insertions(+), 27 deletions(-)
       ---
   DIR diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
       t@@ -235,11 +235,13 @@ class ElectrumWindow(App):
                self.update_tab('send')
                if self.invoice_popup and self.invoice_popup.key == key:
                    self.invoice_popup.update_status()
       -        if status == PR_PAID:
       -            self.show_info(_('Payment was sent'))
       -            self._trigger_update_history()
       -        elif status == PR_FAILED:
       -            self.show_info(_('Payment failed'))
       +
       +    def on_payment_succeeded(self, event, key):
       +        self.show_info(_('Payment was sent'))
       +        self._trigger_update_history()
       +
       +    def on_payment_failed(self, event, key, reason):
       +        self.show_info(_('Payment failed') + '\n\n' + reason)
        
            def _get_bu(self):
                decimal_point = self.electrum_config.get('decimal_point', DECIMAL_POINT_DEFAULT)
       t@@ -569,6 +571,8 @@ class ElectrumWindow(App):
                    self.network.register_callback(self.on_channel, ['channel'])
                    self.network.register_callback(self.on_invoice_status, ['invoice_status'])
                    self.network.register_callback(self.on_request_status, ['request_status'])
       +            self.network.register_callback(self.on_payment_failed, ['payment_failed'])
       +            self.network.register_callback(self.on_payment_succeeded, ['payment_succeeded'])
                    self.network.register_callback(self.on_channel_db, ['channel_db'])
                    self.network.register_callback(self.set_num_peers, ['gossip_peers'])
                    self.network.register_callback(self.set_unknown_channels, ['unknown_channels'])
   DIR diff --git a/electrum/gui/kivy/uix/dialogs/invoice_dialog.py b/electrum/gui/kivy/uix/dialogs/invoice_dialog.py
       t@@ -40,6 +40,10 @@ Builder.load_string('''
                    TopLabel:
                        text: _('Status') + ': ' + root.status_str
                        color: root.status_color
       +                on_touch_down:
       +                    touch = args[1]
       +                    touched = bool(self.collide_point(*touch.pos))
       +                    if touched: root.show_log()
                    TopLabel:
                        text: root.warning
                        color: (0.9, 0.6, 0.3, 1)
       t@@ -84,6 +88,7 @@ class InvoiceDialog(Factory.Popup):
                self.amount = r.get('amount')
                self.is_lightning = r.get('type') == PR_TYPE_LN
                self.update_status()
       +        self.log = self.app.wallet.lnworker.logs[self.key] if self.is_lightning else []
        
            def update_status(self):
                req = self.app.wallet.get_invoice(self.key)
       t@@ -120,3 +125,8 @@ class InvoiceDialog(Factory.Popup):
                        self.app.send_screen.update()
                d = Question(_('Delete invoice?'), cb)
                d.open()
       +
       +    def show_log(self):
       +        if self.log:
       +            log_str = _('Payment log:') + '\n\n' + '\n'.join([str(x.exception) for x in self.log])
       +            self.app.show_info(log_str)
   DIR diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -265,6 +265,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                                 'new_transaction', 'status',
                                 'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes',
                                 'on_history', 'channel', 'channels_updated',
       +                         'payment_failed', 'payment_succeeded',
                                 'invoice_status', 'request_status', 'ln_gossip_sync_progress']
                    # To avoid leaking references to "self" that prevent the
                    # window from being GC-ed when closed, callbacks should be
       t@@ -419,6 +420,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                    self.on_request_status(*args)
                elif event == 'invoice_status':
                    self.on_invoice_status(*args)
       +        elif event == 'payment_succeeded':
       +            self.on_payment_succeeded(*args)
       +        elif event == 'payment_failed':
       +            self.on_payment_failed(*args)
                elif event == 'status':
                    self.update_status()
                elif event == 'banner':
       t@@ -1448,15 +1453,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
                req = self.wallet.get_invoice(key)
                if req is None:
                    return
       -        status = req['status']
                self.invoice_list.update_item(key, req)
       -        if status == PR_PAID:
       -            self.show_message(_('Payment succeeded'))
       -            self.need_update.set()
       -        elif status == PR_FAILED:
       -            self.show_error(_('Payment failed'))
       -        else:
       -            pass
       +
       +    def on_payment_succeeded(self, key, description=None):
       +        self.show_message(_('Payment succeeded'))
       +        self.need_update.set()
       +
       +    def on_payment_failed(self, key, reason):
       +        self.show_error(_('Payment failed') + '\n\n' + reason)
        
            def read_invoice(self):
                if self.check_send_tab_payto_line_and_show_errors():
   DIR diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py
       t@@ -24,7 +24,7 @@ from PyQt5.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout,
        
        from electrum.i18n import _, languages
        from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, resource_path
       -from electrum.util import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED
       +from electrum.util import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING
        
        if TYPE_CHECKING:
            from .main_window import ElectrumWindow
       t@@ -47,6 +47,7 @@ pr_icons = {
            PR_EXPIRED:"expired.png",
            PR_INFLIGHT:"unconfirmed.png",
            PR_FAILED:"warning.png",
       +    PR_ROUTING:"unconfirmed.png",
        }
        
        
   DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py
       t@@ -24,7 +24,7 @@ from aiorpcx import run_in_thread
        from . import constants
        from . import keystore
        from .util import profiler
       -from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED
       +from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING
        from .util import PR_TYPE_LN
        from .lnutil import LN_MAX_FUNDING_SAT
        from .keystore import BIP32_KeyStore
       t@@ -69,6 +69,9 @@ if TYPE_CHECKING:
            from .wallet import Abstract_Wallet
        
        
       +SAVED_PR_STATUS = [PR_PAID, PR_UNPAID, PR_INFLIGHT] # status that are persisted
       +
       +
        NUM_PEERS_TARGET = 4
        PEER_RETRY_INTERVAL = 600  # seconds
        PEER_RETRY_INTERVAL_FOR_CHANNELS = 30  # seconds
       t@@ -421,7 +424,8 @@ class LNWallet(LNWorker):
                self.preimages = self.db.get_dict('lightning_preimages')   # RHASH -> preimage
                self.sweep_address = wallet.get_receiving_address()
                self.lock = threading.RLock()
       -        self.logs = defaultdict(list)  # type: Dict[str, List[PaymentAttemptLog]]  # key is RHASH
       +        self.logs = defaultdict(list)  # (not persisted) type: Dict[str, List[PaymentAttemptLog]]  # key is RHASH
       +        self.is_routing = set()        # (not persisted) keys of invoices that are in PR_ROUTING state
                # used in tests
                self.enable_htlc_settle = asyncio.Event()
                self.enable_htlc_settle.set()
       t@@ -920,25 +924,35 @@ class LNWallet(LNWorker):
                self.wallet.set_label(key, lnaddr.get_description())
                log = self.logs[key]
                success = False
       +        reason = ''
                for i in range(attempts):
                    try:
                        # note: this call does path-finding which takes ~1 second
                        #       -> we will BLOCK the asyncio loop... (could just run in a thread and await,
                        #       but then the graph could change while the path-finding runs on it)
       +                self.set_invoice_status(key, PR_ROUTING)
       +                self.network.trigger_callback('invoice_status', key)
                        route = self._create_route_from_invoice(decoded_invoice=lnaddr)
       -                self.set_payment_status(payment_hash, PR_INFLIGHT)
       +                self.set_invoice_status(key, PR_INFLIGHT)
                        self.network.trigger_callback('invoice_status', key)
                        payment_attempt_log = await self._pay_to_route(route, lnaddr)
                    except Exception as e:
                        log.append(PaymentAttemptLog(success=False, exception=e))
       -                self.set_payment_status(payment_hash, PR_UNPAID)
       +                self.set_invoice_status(key, PR_UNPAID)
       +                reason = str(e)
                        break
                    log.append(payment_attempt_log)
                    success = payment_attempt_log.success
                    if success:
                        break
       -        self.logger.debug(f'payment attempts log for RHASH {key}: {repr(log)}')
       +        else:
       +            reason = 'failed after %d attempts' % attemps
                self.network.trigger_callback('invoice_status', key)
       +        if success:
       +            self.network.trigger_callback('payment_succeeded', key)
       +        else:
       +            self.network.trigger_callback('payment_failed', key, reason)
       +        self.logger.debug(f'payment attempts log for RHASH {key}: {repr(log)}')
                return success
        
            async def _pay_to_route(self, route: LNPaymentRoute, lnaddr: LnAddr) -> PaymentAttemptLog:
       t@@ -1038,6 +1052,7 @@ class LNWallet(LNWorker):
                        f"min_final_cltv_expiry: {addr.get_min_final_cltv_expiry()}"))
                return addr
        
       +    @profiler
            def _create_route_from_invoice(self, decoded_invoice) -> LNPaymentRoute:
                amount_msat = int(decoded_invoice.amount * COIN * 1000)
                invoice_pubkey = decoded_invoice.pubkey.serialize()
       t@@ -1170,7 +1185,7 @@ class LNWallet(LNWorker):
        
            def save_payment_info(self, info: PaymentInfo) -> None:
                key = info.payment_hash.hex()
       -        assert info.status in [PR_PAID, PR_UNPAID, PR_INFLIGHT]
       +        assert info.status in SAVED_PR_STATUS
                with self.lock:
                    self.payments[key] = info.amount, info.direction, info.status
                self.wallet.save_db()
       t@@ -1184,19 +1199,29 @@ class LNWallet(LNWorker):
                return status
        
            def get_invoice_status(self, key):
       +        log = self.logs[key]
       +        if key in self.is_routing:
       +            return PR_ROUTING
                # status may be PR_FAILED
                status = self.get_payment_status(bfh(key))
       -        log = self.logs[key]
                if status == PR_UNPAID and log:
                    status = PR_FAILED
                return status
        
       +    def set_invoice_status(self, key, status):
       +        if status == PR_ROUTING:
       +            self.is_routing.add(key)
       +        elif key in self.is_routing:
       +            self.is_routing.remove(key)
       +        if status in SAVED_PR_STATUS:
       +            self.save_payment_status(bfh(key), status)
       +
            async def await_payment(self, payment_hash):
                success, preimage, reason = await self.pending_payments[payment_hash]
                self.pending_payments.pop(payment_hash)
                return success, preimage, reason
        
       -    def set_payment_status(self, payment_hash: bytes, status):
       +    def save_payment_status(self, payment_hash: bytes, status):
                try:
                    info = self.get_payment_info(payment_hash)
                except UnknownPaymentHash:
       t@@ -1206,27 +1231,29 @@ class LNWallet(LNWorker):
                self.save_payment_info(info)
        
            def payment_failed(self, chan, payment_hash: bytes, reason):
       -        self.set_payment_status(payment_hash, PR_UNPAID)
       +        self.save_payment_status(payment_hash, PR_UNPAID)
                f = self.pending_payments.get(payment_hash)
                if f and not f.cancelled():
                    f.set_result((False, None, reason))
                else:
                    chan.logger.info('received unexpected payment_failed, probably from previous session')
       -            self.network.trigger_callback('invoice_status', payment_hash.hex())
       +            self.network.trigger_callback('invoice_status', key)
       +            self.network.trigger_callback('payment_failed', payment_hash.hex())
        
            def payment_sent(self, chan, payment_hash: bytes):
       -        self.set_payment_status(payment_hash, PR_PAID)
       +        self.save_payment_status(payment_hash, PR_PAID)
                preimage = self.get_preimage(payment_hash)
                f = self.pending_payments.get(payment_hash)
                if f and not f.cancelled():
                    f.set_result((True, preimage, None))
                else:
                    chan.logger.info('received unexpected payment_sent, probably from previous session')
       -            self.network.trigger_callback('invoice_status', payment_hash.hex())
       +            self.network.trigger_callback('invoice_status', key)
       +            self.network.trigger_callback('payment_succeeded', payment_hash.hex())
                self.network.trigger_callback('ln_payment_completed', payment_hash, chan.channel_id)
        
            def payment_received(self, chan, payment_hash: bytes):
       -        self.set_payment_status(payment_hash, PR_PAID)
       +        self.save_payment_status(payment_hash, PR_PAID)
                self.network.trigger_callback('request_status', payment_hash.hex(), PR_PAID)
                self.network.trigger_callback('ln_payment_completed', payment_hash, chan.channel_id)
        
   DIR diff --git a/electrum/util.py b/electrum/util.py
       t@@ -85,6 +85,7 @@ PR_UNKNOWN  = 2     # sent but not propagated
        PR_PAID     = 3     # send and propagated
        PR_INFLIGHT = 4     # unconfirmed
        PR_FAILED   = 5
       +PR_ROUTING  = 6
        
        pr_color = {
            PR_UNPAID:   (.7, .7, .7, 1),
       t@@ -93,6 +94,7 @@ pr_color = {
            PR_EXPIRED:  (.9, .2, .2, 1),
            PR_INFLIGHT: (.9, .6, .3, 1),
            PR_FAILED:   (.9, .2, .2, 1),
       +    PR_ROUTING: (.9, .6, .3, 1),
        }
        
        pr_tooltips = {
       t@@ -102,6 +104,7 @@ pr_tooltips = {
            PR_EXPIRED:_('Expired'),
            PR_INFLIGHT:_('In progress'),
            PR_FAILED:_('Failed'),
       +    PR_ROUTING: _('Computing route...'),
        }
        
        PR_DEFAULT_EXPIRATION_WHEN_CREATING = 24*60*60  # 1 day