Browse Source

lnurl: make requests async, don't block Qt GUI, rm LUD-16 support

- in lnurl.py, make request methods async
- in Qt GUI, lnurl network requests no longer block the GUI thread
  - but they still do in the kivy GUI
- "lightning address" (LUD-16) support is removed for now as the
  email addresses are indistinguishable from openalias email addresses
  (both protocols should have added and enforced a prefix, or similar,
   to remove this kind of ambiguity -- now we would need to make a
   network request just to identify what kind of ID we were given)
patch-4
SomberNight 3 years ago
parent
commit
ed1567e841
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 20
      electrum/gui/kivy/uix/screens.py
  2. 2
      electrum/gui/qt/completion_text_edit.py
  3. 88
      electrum/gui/qt/main_window.py
  4. 29
      electrum/gui/qt/paytoedit.py
  5. 18
      electrum/lnurl.py
  6. 2
      electrum/network.py

20
electrum/gui/kivy/uix/screens.py

@ -20,8 +20,9 @@ from electrum.transaction import tx_from_any, PartialTxOutput
from electrum.util import (parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_lightning_payment_identifier,
InvoiceError, format_time, parse_max_spend, BITCOIN_BIP21_URI_SCHEME)
from electrum.lnaddr import lndecode, LnInvoiceException
from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl, lightning_address_to_url, LNURLError, LNURL6Data
from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, LNURL6Data
from electrum.logging import Logger
from electrum.network import Network
from .dialogs.confirm_tx_dialog import ConfirmTxDialog
@ -178,7 +179,6 @@ class SendScreen(CScreen, Logger):
* lightning-URI (containing bolt11 or lnurl)
* bolt11 invoice
* lnurl
* lightning address
Bitcoin identifiers:
* bitcoin-URI
* bitcoin address
@ -191,10 +191,7 @@ class SendScreen(CScreen, Logger):
text = text.strip()
if not text:
return
invoice_or_lnurl = maybe_extract_lightning_payment_identifier(text)
if lightning_address_to_url(text):
self.set_lnurl6(text)
elif invoice_or_lnurl:
if invoice_or_lnurl := maybe_extract_lightning_payment_identifier(text):
if invoice_or_lnurl.startswith('lnurl'):
self.set_lnurl6(invoice_or_lnurl)
else:
@ -234,11 +231,10 @@ class SendScreen(CScreen, Logger):
self.is_lightning = True
def set_lnurl6(self, lnurl: str):
url = lightning_address_to_url(lnurl)
if not url:
url = decode_lnurl(lnurl)
domain = urlparse(url).netloc
lnurl_data = request_lnurl(url, self.app.network.send_http_on_proxy)
# FIXME network request blocking GUI thread:
lnurl_data = Network.run_from_another_thread(request_lnurl(url))
if not lnurl_data:
return
self.lnurl_data = lnurl_data
@ -380,11 +376,11 @@ class SendScreen(CScreen, Logger):
self.app.show_error(f'Amount must be between {self.lnurl_data.min_sendable_sat} and {self.lnurl_data.max_sendable_sat} sat.')
return
try:
invoice_data = callback_lnurl(
# FIXME network request blocking GUI thread:
invoice_data = Network.run_from_another_thread(callback_lnurl(
self.lnurl_data.callback_url,
params={'amount': amount * 1000},
request_over_proxy=self.app.network.send_http_on_proxy,
)
))
except LNURLError as e:
self.app.show_error(f"LNURL request encountered error: {e}")
self.do_clear()

2
electrum/gui/qt/completion_text_edit.py

@ -81,6 +81,8 @@ class CompletionTextEdit(ButtonsTextEdit):
return
QPlainTextEdit.keyPressEvent(self, e)
if self.isReadOnly(): # if field became read-only *after* keyPress, exit now
return
ctrlOrShift = bool(e.modifiers() & (Qt.ControlModifier | Qt.ShiftModifier))
if self.completer is None or (ctrlOrShift and not e.text()):

88
electrum/gui/qt/main_window.py

@ -58,7 +58,7 @@ from electrum import (keystore, ecc, constants, util, bitcoin, commands,
from electrum.bitcoin import COIN, is_address
from electrum.plugin import run_hook, BasePlugin
from electrum.i18n import _
from electrum.util import (format_time,
from electrum.util import (format_time, get_asyncio_loop,
UserCancelled, profiler,
bh2u, bfh, InvalidPassword,
UserFacingException,
@ -82,7 +82,7 @@ from electrum.simple_config import SimpleConfig
from electrum.logging import Logger
from electrum.lnutil import ln_dummy_address, extract_nodeid, ConnStringFormatError
from electrum.lnaddr import lndecode, LnInvoiceException
from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl, lightning_address_to_url, LNURLError, LNURL6Data
from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, LNURL6Data
from .exception_window import Exception_Hook
from .amountedit import AmountEdit, BTCAmountEdit, FreezableLineEdit, FeerateEdit, SizedFreezableLineEdit
@ -201,6 +201,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
payment_request_ok_signal = pyqtSignal()
payment_request_error_signal = pyqtSignal()
lnurl6_round1_signal = pyqtSignal(object, object)
lnurl6_round2_signal = pyqtSignal(object)
clear_send_tab_signal = pyqtSignal()
#ln_payment_attempt_signal = pyqtSignal(str)
computing_privkeys_signal = pyqtSignal()
show_privkeys_signal = pyqtSignal()
@ -312,6 +315,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.payment_request_ok_signal.connect(self.payment_request_ok)
self.payment_request_error_signal.connect(self.payment_request_error)
self.lnurl6_round1_signal.connect(self.on_lnurl6_round1)
self.lnurl6_round2_signal.connect(self.on_lnurl6_round2)
self.clear_send_tab_signal.connect(self.do_clear)
self.show_error_signal.connect(self.show_error)
self.history_list.setFocus(True)
@ -1917,22 +1924,30 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
if not (self._lnurl_data.min_sendable_sat <= amount <= self._lnurl_data.max_sendable_sat):
self.show_error(f'Amount must be between {self._lnurl_data.min_sendable_sat} and {self._lnurl_data.max_sendable_sat} sat.')
return
async def f():
try:
invoice_data = callback_lnurl(
invoice_data = await callback_lnurl(
self._lnurl_data.callback_url,
params={'amount': self.amount_e.get_amount() * 1000},
request_over_proxy=self.network.send_http_on_proxy,
)
except LNURLError as e:
self.show_error(f"LNURL request encountered error: {e}")
self.do_clear()
self.show_error_signal.emit(f"LNURL request encountered error: {e}")
self.clear_send_tab_signal.emit()
return
invoice = invoice_data.get('pr')
self.set_bolt11(invoice)
self.lnurl6_round2_signal.emit(invoice)
asyncio.run_coroutine_threadsafe(f(), get_asyncio_loop()) # TODO should be cancellable
self.prepare_for_send_tab_network_lookup()
def on_lnurl6_round2(self, bolt11_invoice: str):
self.set_bolt11(bolt11_invoice)
self.payto_e.setFrozen(True)
self.amount_e.setDisabled(True)
self.fiat_send_e.setDisabled(True)
self.save_button.setEnabled(True)
self.amount_e.setEnabled(False)
self.fiat_send_e.setEnabled(False)
for btn in [self.send_button, self.clear_button, self.save_button]:
btn.setEnabled(True)
self.send_button.restore_original_text()
self._lnurl_data = None
@ -2200,14 +2215,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.amount_e.setFrozen(b)
self.max_button.setEnabled(not b)
def prepare_for_payment_request(self):
def prepare_for_send_tab_network_lookup(self):
self.show_send_tab()
self.payto_e.is_pr = True
self.payto_e.disable_checks = True
for e in [self.payto_e, self.message_e]:
e.setFrozen(True)
self.lock_amount(True)
for btn in [self.save_button, self.send_button, self.clear_button]:
btn.setEnabled(False)
self.payto_e.setTextNoCheck(_("please wait..."))
return True
def delete_invoices(self, keys):
for key in keys:
@ -2224,7 +2240,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.do_clear()
self.payment_request = None
return
self.payto_e.is_pr = True
self.payto_e.disable_checks = True
if not pr.has_expired():
self.payto_e.setGreen()
else:
@ -2232,6 +2248,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.payto_e.setTextNoCheck(pr.get_requestor())
self.amount_e.setAmount(pr.get_amount())
self.message_e.setText(pr.get_memo())
self.set_onchain(True)
self.max_button.setEnabled(False)
for btn in [self.send_button, self.clear_button]:
btn.setEnabled(True)
# signal to set fee
self.amount_e.textEdited.emit("")
@ -2244,34 +2264,43 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self.do_clear()
def on_pr(self, request: 'paymentrequest.PaymentRequest'):
self.set_onchain(True)
self.payment_request = request
if self.payment_request.verify(self.contacts):
self.payment_request_ok_signal.emit()
else:
self.payment_request_error_signal.emit()
def set_lnurl6_bech32(self, lnurl: str, *, can_use_network: bool = True):
def set_lnurl6(self, lnurl: str, *, can_use_network: bool = True):
try:
url = decode_lnurl(lnurl)
except LnInvoiceException as e:
self.show_error(_("Error parsing Lightning invoice") + f":\n{e}")
return
self.set_lnurl6_url(url, can_use_network=can_use_network)
if not can_use_network:
return
def set_lnurl6_url(self, url: str, *, lnurl_data: LNURL6Data = None, can_use_network: bool = True):
domain = urlparse(url).netloc
if lnurl_data is None and can_use_network:
lnurl_data = request_lnurl(url, self.network.send_http_on_proxy)
if not lnurl_data:
async def f():
try:
lnurl_data = await request_lnurl(url)
except LNURLError as e:
self.show_error_signal.emit(f"LNURL request encountered error: {e}")
self.clear_send_tab_signal.emit()
return
self.lnurl6_round1_signal.emit(lnurl_data, url)
asyncio.run_coroutine_threadsafe(f(), get_asyncio_loop()) # TODO should be cancellable
self.prepare_for_send_tab_network_lookup()
def on_lnurl6_round1(self, lnurl_data: LNURL6Data, url: str):
self._lnurl_data = lnurl_data
self.payto_e.setFrozen(True)
domain = urlparse(url).netloc
self.payto_e.setTextNoCheck(f"invoice from lnurl")
self.message_e.setText(f"lnurl: {domain}: {lnurl_data.metadata_plaintext}")
self.amount_e.setAmount(lnurl_data.min_sendable_sat)
self.save_button.setDisabled(True)
self.amount_e.setFrozen(False)
self.send_button.setText(_('Get Invoice'))
for btn in [self.send_button, self.clear_button]:
btn.setEnabled(True)
self.set_onchain(False)
def set_bolt11(self, invoice: str):
@ -2314,7 +2343,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
sig = out.get('sig')
name = out.get('name')
if (r or (name and sig)) and can_use_network:
self.prepare_for_payment_request()
self.prepare_for_send_tab_network_lookup()
return
address = out.get('address')
amount = out.get('amount')
@ -2347,14 +2376,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
text = text.strip()
if not text:
return
invoice_or_lnurl = maybe_extract_lightning_payment_identifier(text)
if invoice_or_lnurl:
if invoice_or_lnurl := maybe_extract_lightning_payment_identifier(text):
if invoice_or_lnurl.startswith('lnurl'):
self.set_lnurl6_bech32(invoice_or_lnurl, can_use_network=can_use_network)
self.set_lnurl6(invoice_or_lnurl, can_use_network=can_use_network)
else:
self.set_bolt11(invoice_or_lnurl)
elif text.lower().startswith(util.BITCOIN_BIP21_URI_SCHEME + ':'):
self.set_bip21(text)
self.set_bip21(text, can_use_network=can_use_network)
else:
raise ValueError("Could not handle payment identifier.")
# update fiat amount
@ -2372,7 +2400,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
for e in [self.message_e, self.amount_e]:
e.setText('')
e.setFrozen(False)
for e in [self.send_button, self.save_button, self.amount_e, self.fiat_send_e]:
for e in [self.send_button, self.save_button, self.clear_button, self.amount_e, self.fiat_send_e]:
e.setEnabled(True)
self.update_status()
run_hook('do_clear', self)

29
electrum/gui/qt/paytoedit.py

@ -35,7 +35,7 @@ from electrum.util import bfh, parse_max_spend
from electrum.transaction import PartialTxOutput
from electrum.bitcoin import opcodes, construct_script
from electrum.logging import Logger
from electrum.lnurl import LNURLError, lightning_address_to_url, request_lnurl, LNURL6Data
from electrum.lnurl import LNURLError
from .qrtextedit import ScanQRTextEdit
from .completion_text_edit import CompletionTextEdit
@ -89,7 +89,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
self.textChanged.connect(self._on_text_changed)
self.outputs = [] # type: List[PartialTxOutput]
self.errors = [] # type: List[PayToLineError]
self.is_pr = False
self.disable_checks = False
self.is_alias = False
self.update_size()
self.payto_scriptpubkey = None # type: Optional[bytes]
@ -107,7 +107,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
self.setText(text)
def do_clear(self):
self.is_pr = False
self.disable_checks = False
self.is_alias = False
self.setText('')
self.setFrozen(False)
@ -193,7 +193,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
if full_check:
self.previous_payto = str(self.toPlainText()).strip()
self.errors = []
if self.is_pr:
if self.disable_checks:
return
# filter out empty lines
lines = [i for i in self.lines() if i]
@ -229,13 +229,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
self.win.set_onchain(True)
self.win.lock_amount(False)
return
if full_check: # network requests
# try lightning address lnurl-16 (note: names can collide with openalias, so order matters)
lnurl_data = self._resolve_lightning_address_lnurl16(data)
if lnurl_data:
url = lightning_address_to_url(data)
self.win.set_lnurl6_url(url, lnurl_data=lnurl_data)
return
if full_check: # network requests # FIXME blocking GUI thread
# try openalias
oa_data = self._resolve_openalias(data)
if oa_data:
@ -339,6 +333,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
def _set_openalias(self, *, key: str, data: dict) -> bool:
self.is_alias = True
self.setFrozen(True)
key = key.strip() # strip whitespaces
address = data.get('address')
name = data.get('name')
@ -349,7 +344,6 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
self.win.contacts[key] = ('openalias', name)
self.win.contact_list.update()
self.setFrozen(True)
if data.get('type') == 'openalias':
self.validated = data.get('validated')
if self.validated:
@ -359,14 +353,3 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
else:
self.validated = None
return True
def _resolve_lightning_address_lnurl16(self, text: str) -> Optional[LNURL6Data]:
url = lightning_address_to_url(text)
if not url:
return None
try:
lnurl_data = request_lnurl(url, self.win.network.send_http_on_proxy)
return lnurl_data
except LNURLError as e:
self.logger.info(f"failed to resolve {text} as lnurl16 lightning address. got exc: {e!r}")
return None

18
electrum/lnurl.py

@ -4,7 +4,7 @@
import asyncio
import json
from typing import Callable, Optional, NamedTuple, Any
from typing import Callable, Optional, NamedTuple, Any, TYPE_CHECKING
import re
import aiohttp.client_exceptions
@ -12,6 +12,10 @@ from aiohttp import ClientResponse
from electrum.segwit_addr import bech32_decode, Encoding, convertbits
from electrum.lnaddr import LnDecodeException
from electrum.network import Network
if TYPE_CHECKING:
from collections.abc import Coroutine
class LNURLError(Exception):
@ -44,10 +48,10 @@ class LNURL6Data(NamedTuple):
#tag: str = "payRequest"
def _request_lnurl(url: str, request_over_proxy: Callable) -> dict:
async def _request_lnurl(url: str) -> dict:
"""Requests payment data from a lnurl."""
try:
response = request_over_proxy("get", url, timeout=10)
response = await Network.async_send_http_on_proxy("get", url, timeout=10)
except asyncio.TimeoutError as e:
raise LNURLError("Server did not reply in time.") from e
except aiohttp.client_exceptions.ClientError as e:
@ -62,8 +66,8 @@ def _request_lnurl(url: str, request_over_proxy: Callable) -> dict:
return response
def request_lnurl(url: str, request_over_proxy: Callable) -> Optional[LNURL6Data]:
lnurl_dict = _request_lnurl(url, request_over_proxy)
async def request_lnurl(url: str) -> Optional[LNURL6Data]:
lnurl_dict = await _request_lnurl(url)
tag = lnurl_dict.get('tag')
if tag != 'payRequest': # only LNURL6 is handled atm
return None
@ -81,10 +85,10 @@ def request_lnurl(url: str, request_over_proxy: Callable) -> Optional[LNURL6Data
return data
def callback_lnurl(url: str, params: dict, request_over_proxy: Callable) -> dict:
async def callback_lnurl(url: str, params: dict) -> dict:
"""Requests an invoice from a lnurl supporting server."""
try:
response = request_over_proxy("get", url, params=params)
response = await Network.async_send_http_on_proxy("get", url, params=params)
except aiohttp.client_exceptions.ClientError as e:
raise LNURLError(f"Client error: {e}") from e
# TODO: handling of specific errors

2
electrum/network.py

@ -1297,7 +1297,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
@classmethod
async def async_send_http_on_proxy(
cls, method: str, url: str, *,
params: str = None,
params: dict = None,
body: bytes = None,
json: dict = None,
headers=None,

Loading…
Cancel
Save