lnurl: clean-up
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:'):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
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')
|
||||
except LnInvoiceException as e:
|
||||
self.show_error(_("Error parsing Lightning invoice") + f":\n{e}")
|
||||
return
|
||||
self.set_lnurl6_url(url)
|
||||
|
||||
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')
|
||||
def set_lnurl6_url(self, url: str, *, lnurl_data: LNURL6Data = None):
|
||||
domain = urlparse(url).netloc
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user