Browse Source

lnurl: clean-up

patch-4
SomberNight 3 years ago
parent
commit
649cad0122
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 4
      electrum/contacts.py
  2. 7
      electrum/gui/kivy/main_window.py
  3. 90
      electrum/gui/kivy/uix/screens.py
  4. 122
      electrum/gui/qt/main_window.py
  5. 97
      electrum/gui/qt/paytoedit.py
  6. 4
      electrum/gui/qt/util.py
  7. 37
      electrum/lnurl.py
  8. 2
      electrum/util.py

4
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:

7
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:'):

90
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:

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

97
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

4
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):

37
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}"

2
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

Loading…
Cancel
Save