Browse Source

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
hard-fail-on-bad-server-string
ThomasV 5 years ago
parent
commit
3d69f3b0be
  1. 14
      electrum/gui/kivy/main_window.py
  2. 10
      electrum/gui/kivy/uix/dialogs/invoice_dialog.py
  3. 20
      electrum/gui/qt/main_window.py
  4. 3
      electrum/gui/qt/util.py
  5. 53
      electrum/lnworker.py
  6. 3
      electrum/util.py

14
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'])

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

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

3
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",
}

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

3
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

Loading…
Cancel
Save