mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-30 08:51:32 +00:00
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
This commit is contained in:
parent
5d4f8f3164
commit
3d69f3b0be
6 changed files with 76 additions and 27 deletions
|
@ -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'])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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…
Add table
Reference in a new issue