commit 3d69f3b0beb4a3be74aeef8a96f5ffef5f82ee90
parent 5d4f8f316480776eb78d98ea9ac9c3b2a05ef01d
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:
6 files changed, 76 insertions(+), 27 deletions(-)
diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
@@ -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)
@@ -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'])
diff --git a/electrum/gui/kivy/uix/dialogs/invoice_dialog.py b/electrum/gui/kivy/uix/dialogs/invoice_dialog.py
@@ -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)
@@ -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)
@@ -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)
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
@@ -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
@@ -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':
@@ -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():
diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py
@@ -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
@@ -47,6 +47,7 @@ pr_icons = {
PR_EXPIRED:"expired.png",
PR_INFLIGHT:"unconfirmed.png",
PR_FAILED:"warning.png",
+ PR_ROUTING:"unconfirmed.png",
}
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
@@ -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
@@ -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
@@ -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()
@@ -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:
@@ -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()
@@ -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()
@@ -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:
@@ -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)
diff --git a/electrum/util.py b/electrum/util.py
@@ -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),
@@ -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 = {
@@ -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