From 649cad0122c877307948d322652955df9f9f1b90 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 30 May 2022 20:49:08 +0200 Subject: [PATCH] lnurl: clean-up --- electrum/contacts.py | 4 +- electrum/gui/kivy/main_window.py | 7 +- electrum/gui/kivy/uix/screens.py | 90 ++++++++++------------- electrum/gui/qt/main_window.py | 122 +++++++++++++++---------------- electrum/gui/qt/paytoedit.py | 97 ++++++++++++++---------- electrum/gui/qt/util.py | 4 + electrum/lnurl.py | 37 +++++++++- electrum/util.py | 2 +- 8 files changed, 204 insertions(+), 159 deletions(-) diff --git a/electrum/contacts.py b/electrum/contacts.py index 0fff1c3cc..3957e8359 100644 --- a/electrum/contacts.py +++ b/electrum/contacts.py @@ -21,6 +21,8 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import re +from typing import Optional, Tuple + import dns import threading from dns.exception import DNSException @@ -106,7 +108,7 @@ class Contacts(dict, Logger): t.daemon = True t.start() - def resolve_openalias(self, url): + def resolve_openalias(self, url: str) -> Optional[Tuple[str, str, bool]]: # support email-style addresses, per the OA standard url = url.replace('@', '.') try: diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index d10126257..ef9e65de9 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -18,7 +18,7 @@ from electrum.plugin import run_hook from electrum import util from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter, format_satoshis, format_satoshis_plain, format_fee_satoshis, - maybe_extract_lightning_payment_identifier, parse_max_spend, is_uri) + parse_max_spend) from electrum.util import EventListener, event_listener from electrum.invoices import PR_PAID, PR_FAILED, Invoice from electrum import blockchain @@ -475,13 +475,14 @@ class ElectrumWindow(App, Logger, EventListener): self.show_error("invoice error:" + pr.error) self.send_screen.do_clear() - def on_qr(self, data: str): + def on_qr(self, data: str): # TODO duplicate of send_screen.do_paste from electrum.bitcoin import is_address data = data.strip() - if is_address(data): # TODO does this actually work? + if is_address(data): self.set_URI(data) return if is_uri(data) or maybe_extract_lightning_payment_identifier(data): + # TODO what about "lightning address"? self.set_URI(data) return if data.lower().startswith('channel_backup:'): diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py index 71efbe000..b230863ff 100644 --- a/electrum/gui/kivy/uix/screens.py +++ b/electrum/gui/kivy/uix/screens.py @@ -20,7 +20,7 @@ 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 +from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl, lightning_address_to_url, LNURLError, LNURL6Data from electrum.logging import Logger from .dialogs.confirm_tx_dialog import ConfirmTxDialog @@ -164,6 +164,7 @@ class SendScreen(CScreen, Logger): kvname = 'send' payment_request = None # type: Optional[PaymentRequest] parsed_URI = None + lnurl_data = None # type: Optional[LNURL6Data] def __init__(self, **kwargs): CScreen.__init__(self, **kwargs) @@ -180,7 +181,7 @@ class SendScreen(CScreen, Logger): * lightning address Bitcoin identifiers: * bitcoin-URI - * bitcoin address TODO + * bitcoin address and sets the sending screen. TODO maybe rename method... @@ -198,7 +199,7 @@ class SendScreen(CScreen, Logger): self.set_lnurl6(invoice_or_lnurl) else: self.set_bolt11(invoice_or_lnurl) - elif text.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'): + elif text.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':') or bitcoin.is_address(text): self.set_bip21(text) else: self.app.show_error(f"Failed to parse text: {text[:10]}...") @@ -238,23 +239,14 @@ class SendScreen(CScreen, Logger): url = decode_lnurl(lnurl) domain = urlparse(url).netloc lnurl_data = request_lnurl(url, self.app.network.send_http_on_proxy) - self.lnurl_callback_url = lnurl_data.get('callback') - self.lnurl_max_sendable_sat = int(lnurl_data.get('maxSendable')) // 1000 - self.lnurl_min_sendable_sat = int(lnurl_data.get('minSendable')) // 1000 - metadata = lnurl_data.get('metadata') - tag = lnurl_data.get('tag') - - if tag == 'payRequest': - #self.payto_e.setFrozen(True) - for m in metadata: - if m[0] == 'text/plain': - self.is_lnurl = True - self.address = "invoice from lnurl" - self.message = f"lnurl: {domain}: {m[1]}" - self.amount = self.app.format_amount_and_units(self.lnurl_min_sendable_sat) - #self.save_button.setDisabled(True) - #self.send_button.setText('Get Invoice') + if not lnurl_data: + return + self.lnurl_data = lnurl_data + self.address = "invoice from lnurl" + self.message = f"lnurl: {domain}: {lnurl_data.metadata_plaintext}" + self.amount = self.app.format_amount_and_units(lnurl_data.min_sendable_sat) self.is_lightning = True + self.is_lnurl = True # `bool(self.lnurl_data)` should be equivalent, this is only here as it is a kivy Property def update(self): if self.app.wallet is None: @@ -312,10 +304,8 @@ class SendScreen(CScreen, Logger): self.is_bip70 = False self.parsed_URI = None self.is_max = False + self.lnurl_data = None self.is_lnurl = False - self.lnurl_max_sendable_sat = None - self.lnurl_min_sendable_sat = None - self.lnurl_callback_url = None def set_request(self, pr: 'PaymentRequest'): self.address = pr.get_requestor() @@ -325,7 +315,7 @@ class SendScreen(CScreen, Logger): self.locked = True self.payment_request = pr - def do_paste(self): + def do_paste(self): # TODO duplicate of app.on_qr data = self.app._clipboard.paste().strip() if not data: self.app.show_info(_("Clipboard is empty")) @@ -389,34 +379,34 @@ class SendScreen(CScreen, Logger): self.do_clear() self.update() + def _lnurl_get_invoice(self) -> None: + assert self.lnurl_data + try: + amount = self.app.get_amount(self.amount) + except: + self.app.show_error(_('Invalid amount') + ':\n' + self.amount) + return + if not (self.lnurl_data.min_sendable_sat <= amount <= self.lnurl_data.max_sendable_sat): + 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( + 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() + return + invoice = invoice_data.get('pr') + self.set_bolt11(invoice) + self.lnurl_data = None + self.is_lnurl = False + def do_pay(self): - if self.is_lnurl: - try: - amount = self.app.get_amount(self.amount) - except: - self.app.show_error(_('Invalid amount') + ':\n' + self.amount) - return - if not (self.lnurl_min_sendable_sat <= amount <= self.lnurl_max_sendable_sat): - self.app.show_error(f'Amount must be between {self.lnurl_min_sendable_sat} and {self.lnurl_max_sendable_sat} sat.') - return - try: - invoice_data = callback_lnurl( - self.lnurl_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() - return - invoice = invoice_data.get('pr') - self.set_bolt11(invoice) - #self.payto_e.setFrozen(True) - #self.amount_e.setDisabled(True) - #self.fiat_send_e.setDisabled(True) - #self.save_button.setEnabled(True) - #self.send_button.setText('Pay...') - self.is_lnurl = False + if self.lnurl_data: + self._lnurl_get_invoice() return invoice = self.read_invoice() if not invoice: diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 9dc937ace..fa7af9213 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -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 +from electrum.lnurl import decode_lnurl, request_lnurl, callback_lnurl, lightning_address_to_url, LNURLError, LNURL6Data from .exception_window import Exception_Hook from .amountedit import AmountEdit, BTCAmountEdit, FreezableLineEdit, FeerateEdit, SizedFreezableLineEdit @@ -207,6 +207,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): show_error_signal = pyqtSignal(str) payment_request: Optional[paymentrequest.PaymentRequest] + _lnurl_data: Optional[LNURL6Data] = None def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet): QMainWindow.__init__(self) @@ -914,8 +915,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): # this updates "synchronizing" progress self.update_status() # resolve aliases - # FIXME this is a blocking network call that has a timeout of 5 sec - self.payto_e.resolve() + # FIXME this might do blocking network calls that has a timeout of several seconds + self.payto_e.check_text() self.notify_transactions() def format_amount(self, amount_sat, is_diff=False, whitespaces=False) -> str: @@ -1577,7 +1578,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.save_button = EnterButton(_("Save"), self.do_save_invoice) self.send_button = EnterButton(_("Pay") + "...", self.do_pay_or_get_invoice) self.clear_button = EnterButton(_("Clear"), self.do_clear) - self._is_lnurl = False buttons = QHBoxLayout() buttons.addStretch(1) @@ -1912,30 +1912,34 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.invoice_list.update() self.pending_invoice = None + def _lnurl_get_invoice(self) -> None: + assert self._lnurl_data + amount = self.amount_e.get_amount() + 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 + try: + invoice_data = 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() + return + invoice = invoice_data.get('pr') + self.set_bolt11(invoice) + self.payto_e.setFrozen(True) + self.amount_e.setDisabled(True) + self.fiat_send_e.setDisabled(True) + self.save_button.setEnabled(True) + self.send_button.restore_original_text() + self._lnurl_data = None + def do_pay_or_get_invoice(self): - if self._is_lnurl: - amount = self.amount_e.get_amount() - if not (self.lnurl_min_sendable_sat <= amount <= self.lnurl_max_sendable_sat): - self.show_error(f'Amount must be between {self.lnurl_min_sendable_sat} and {self.lnurl_max_sendable_sat} sat.') - return - try: - invoice_data = callback_lnurl( - self.lnurl_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() - return - invoice = invoice_data.get('pr') - self.set_bolt11(invoice) - self.payto_e.setFrozen(True) - self.amount_e.setDisabled(True) - self.fiat_send_e.setDisabled(True) - self.save_button.setEnabled(True) - self.send_button.setText('Pay...') - self._is_lnurl = False + if self._lnurl_data: + self._lnurl_get_invoice() return self.pending_invoice = self.read_invoice() if not self.pending_invoice: @@ -2203,7 +2207,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): for e in [self.payto_e, self.message_e]: e.setFrozen(True) self.lock_amount(True) - self.payto_e.setText(_("please wait...")) + self.payto_e.setTextNoCheck(_("please wait...")) return True def delete_invoices(self, keys): @@ -2226,7 +2230,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.payto_e.setGreen() else: self.payto_e.setExpired() - self.payto_e.setText(pr.get_requestor()) + self.payto_e.setTextNoCheck(pr.get_requestor()) self.amount_e.setAmount(pr.get_amount()) self.message_e.setText(pr.get_memo()) # signal to set fee @@ -2248,28 +2252,27 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): else: self.payment_request_error_signal.emit() - def set_lnurl6(self, lnurl: str): - url = lightning_address_to_url(lnurl) - if not url: + def set_lnurl6_bech32(self, lnurl: str): + 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) + + def set_lnurl6_url(self, url: str, *, lnurl_data: LNURL6Data = None): domain = urlparse(url).netloc - lnurl_data = request_lnurl(url, self.network.send_http_on_proxy) - self.lnurl_callback_url = lnurl_data.get('callback') - self.lnurl_max_sendable_sat = int(lnurl_data.get('maxSendable')) // 1000 - self.lnurl_min_sendable_sat = int(lnurl_data.get('minSendable')) // 1000 - metadata = lnurl_data.get('metadata') - tag = lnurl_data.get('tag') - - if tag == 'payRequest': - self.payto_e.setFrozen(True) - for m in metadata: - if m[0] == 'text/plain': - self._is_lnurl = True - self.payto_e.setTextNosignal(f"invoice from lnurl") - self.message_e.setText(f"lnurl: {domain}: {m[1]}") - self.amount_e.setAmount(self.lnurl_min_sendable_sat) - self.save_button.setDisabled(True) - self.send_button.setText('Get Invoice') + if lnurl_data is None: + lnurl_data = request_lnurl(url, self.network.send_http_on_proxy) + if not lnurl_data: + return + self._lnurl_data = lnurl_data + self.payto_e.setFrozen(True) + 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.send_button.setText(_('Get Invoice')) self.set_onchain(False) def set_bolt11(self, invoice: str): @@ -2288,7 +2291,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): else: description = '' self.payto_e.setFrozen(True) - self.payto_e.setTextNosignal(pubkey) + self.payto_e.setTextNoCheck(pubkey) self.payto_e.lightning_invoice = invoice if not self.message_e.text(): self.message_e.setText(description) @@ -2337,7 +2340,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): * lightning-URI (containing bolt11 or lnurl) * bolt11 invoice * lnurl - * lightning address Bitcoin identifiers: * bitcoin-URI and sets the sending screen. @@ -2346,11 +2348,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): 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: if invoice_or_lnurl.startswith('lnurl'): - self.set_lnurl6(invoice_or_lnurl) + self.set_lnurl6_bech32(invoice_or_lnurl) else: self.set_bolt11(invoice_or_lnurl) elif text.lower().startswith(util.BITCOIN_BIP21_URI_SCHEME + ':'): @@ -2362,19 +2362,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.show_send_tab() def do_clear(self): - self.lnurl_max_sendable_sat = None - self.lnurl_min_sendable_sat = None - self.lnurl_callback_url = None - self._is_lnurl = False + self._lnurl_data = None + self.send_button.restore_original_text() self.max_button.setChecked(False) self.payment_request = None self.payto_URI = None - self.payto_e.is_pr = False + self.payto_e.do_clear() self.set_onchain(False) - for e in [self.payto_e, self.message_e, self.amount_e]: + for e in [self.message_e, self.amount_e]: e.setText('') e.setFrozen(False) - for e in [self.send_button, self.save_button, self.payto_e, self.amount_e, self.fiat_send_e]: + for e in [self.send_button, self.save_button, self.amount_e, self.fiat_send_e]: e.setEnabled(True) self.update_status() run_hook('do_clear', self) diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 8a6bbb8a5..7291c2cd9 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -29,14 +29,13 @@ from decimal import Decimal from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING from PyQt5.QtGui import QFontMetrics, QFont -from PyQt5.QtCore import QTimer from electrum import bitcoin 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 +from electrum.lnurl import LNURLError, lightning_address_to_url, request_lnurl, LNURL6Data from .qrtextedit import ScanQRTextEdit from .completion_text_edit import CompletionTextEdit @@ -85,10 +84,6 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): self.heightMax = (self.fontSpacing * 10) + self.verticalMargins self.c = None - self.timer = QTimer() - self.timer.setSingleShot(True) - self.textChanged.connect(self.start_timer) - self.timer.timeout.connect(self.check_text) self.outputs = [] # type: List[PartialTxOutput] self.errors = [] # type: List[PayToLineError] self.is_pr = False @@ -98,22 +93,22 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): self.lightning_invoice = None self.previous_payto = '' - def start_timer(self): - # we insert a timer between textChanged and check_text to not immediately - # resolve lightning addresses, but rather to wait until the address is typed out fully - delay_time_msec = 300 # about the average typing time in msec a person types a character - self.logger.info("timer fires") - self.timer.start(delay_time_msec) - def setFrozen(self, b): self.setReadOnly(b) self.setStyleSheet(frozen_style if b else normal_style) self.overlay_widget.setHidden(b) - def setTextNosignal(self, text: str): - self.blockSignals(True) + def setTextNoCheck(self, text: str): + """Sets the text, while also ensuring the new value will not be resolved/checked.""" self.setText(text) - self.blockSignals(False) + self.previous_payto = text + + def do_clear(self): + self.is_pr = False + self.is_alias = False + self.setText('') + self.setFrozen(False) + self.setEnabled(True) def setGreen(self): self.setStyleSheet(util.ColorScheme.GREEN.as_stylesheet(True)) @@ -174,6 +169,18 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): return address def check_text(self): + if self.hasFocus(): + return + if self.is_pr: + return + text = str(self.toPlainText()) + text = text.strip() # strip whitespaces + if text == self.previous_payto: + return + self.previous_payto = text + self._check_text() + + def _check_text(self): self.errors = [] if self.is_pr: return @@ -189,6 +196,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): try: self.win.handle_payment_identifier(data) except LNURLError as e: + self.logger.exception("") self.show_error(e) except ValueError: pass @@ -210,6 +218,17 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): self.win.set_onchain(True) self.win.lock_amount(False) return + # 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 + # try openalias + oa_data = self._resolve_openalias(data) + if oa_data: + self._set_openalias(key=data, data=oa_data) + return else: # there are multiple lines self._parse_as_multiline(lines, raise_errors=False) @@ -271,7 +290,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): return len(self.lines()) > 1 def paytomany(self): - self.setText("\n\n\n") + self.setTextNoCheck("\n\n\n") self.update_size() def update_size(self): @@ -291,38 +310,28 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): # The scrollbar visibility can have changed so we update the overlay position here self._updateOverlayPos() - def resolve(self): - self.is_alias = False - if self.hasFocus(): - return - if self.is_multiline(): # only supports single line entries atm - return - if self.is_pr: - return - key = str(self.toPlainText()) + def _resolve_openalias(self, text: str) -> Optional[dict]: + key = text key = key.strip() # strip whitespaces - if key == self.previous_payto: - return - self.previous_payto = key - if not (('.' in key) and (not '<' in key) and (not ' ' in key)): - return + if not (('.' in key) and ('<' not in key) and (' ' not in key)): + return None parts = key.split(sep=',') # assuming single line if parts and len(parts) > 0 and bitcoin.is_address(parts[0]): - return + return None try: data = self.win.contacts.resolve(key) except Exception as e: self.logger.info(f'error resolving address/alias: {repr(e)}') - return - if not data: - return - self.is_alias = True + return None + return data or None + def _set_openalias(self, *, key: str, data: dict) -> bool: + self.is_alias = True + key = key.strip() # strip whitespaces address = data.get('address') name = data.get('name') new_url = key + ' <' + address + '>' - self.setText(new_url) - self.previous_payto = new_url + self.setTextNoCheck(new_url) #if self.win.config.get('openalias_autoadd') == 'checked': self.win.contacts[key] = ('openalias', name) @@ -337,3 +346,15 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): self.setExpired() 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 diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py index 5c6028d89..173cadaab 100644 --- a/electrum/gui/qt/util.py +++ b/electrum/gui/qt/util.py @@ -75,11 +75,15 @@ class EnterButton(QPushButton): QPushButton.__init__(self, text) self.func = func self.clicked.connect(func) + self._orig_text = text def keyPressEvent(self, e): if e.key() in [Qt.Key_Return, Qt.Key_Enter]: self.func() + def restore_original_text(self): + self.setText(self._orig_text) + class ThreadedButton(QPushButton): def __init__(self, text, task, on_success=None, on_error=None): diff --git a/electrum/lnurl.py b/electrum/lnurl.py index e0760ff3b..84661b801 100644 --- a/electrum/lnurl.py +++ b/electrum/lnurl.py @@ -4,7 +4,7 @@ import asyncio import json -from typing import Callable, Optional +from typing import Callable, Optional, NamedTuple, Any import re import aiohttp.client_exceptions @@ -36,10 +36,18 @@ def decode_lnurl(lnurl: str) -> str: return url -def request_lnurl(url: str, request_over_proxy: Callable) -> dict: +class LNURL6Data(NamedTuple): + callback_url: str + max_sendable_sat: int + min_sendable_sat: int + metadata_plaintext: str + #tag: str = "payRequest" + + +def _request_lnurl(url: str, request_over_proxy: Callable) -> dict: """Requests payment data from a lnurl.""" try: - response = request_over_proxy("get", url, timeout=2) + response = request_over_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: @@ -54,6 +62,25 @@ 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) + tag = lnurl_dict.get('tag') + if tag != 'payRequest': # only LNURL6 is handled atm + return None + metadata = lnurl_dict.get('metadata') + metadata_plaintext = "" + for m in metadata: + if m[0] == 'text/plain': + metadata_plaintext = str(m[1]) + data = LNURL6Data( + callback_url=lnurl_dict['callback'], + max_sendable_sat=int(lnurl_dict['maxSendable']) // 1000, + min_sendable_sat=int(lnurl_dict['minSendable']) // 1000, + metadata_plaintext=metadata_plaintext, + ) + return data + + def callback_lnurl(url: str, params: dict, request_over_proxy: Callable) -> dict: """Requests an invoice from a lnurl supporting server.""" try: @@ -69,7 +96,9 @@ def callback_lnurl(url: str, params: dict, request_over_proxy: Callable) -> dict def lightning_address_to_url(address: str) -> Optional[str]: - """Converts an email-type lightning address to a decoded lnurl.""" + """Converts an email-type lightning address to a decoded lnurl. + see https://github.com/fiatjaf/lnurl-rfc/blob/luds/16.md + """ if re.match(r"[^@]+@[^@]+\.[^@]+", address): username, domain = address.split("@") return f"https://{domain}/.well-known/lnurlp/{username}" diff --git a/electrum/util.py b/electrum/util.py index bb4e69710..94629d055 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -1079,7 +1079,7 @@ def maybe_extract_lightning_payment_identifier(data: str) -> Optional[str]: def is_uri(data: str) -> bool: data = data.lower() if (data.startswith(LIGHTNING_URI_SCHEME + ":") or - data.startswith(BITCOIN_BIP21_URI_SCHEME + ':')): + data.startswith(BITCOIN_BIP21_URI_SCHEME + ':')): return True return False