From df974c2384600f94d21509dfdd8ba6b35da66eca Mon Sep 17 00:00:00 2001 From: SomberNight Date: Tue, 31 May 2022 16:37:38 +0200 Subject: [PATCH] qt paytoedit: evaluate text on textChanged(), but no network requests - add param to _check_text to toggle if network requests are allowed ("full check") - every 0.5 sec, if the textedit has no focus, do full check - on textChanged() - detect if user copy-pasted by comparing current text against clipboard contents, if so, do full check - otherwise, do partial check - on clicking ButtonsWidget btns (scan qr, paste, read file), do full check --- electrum/gui/qt/main_window.py | 24 ++++++------- electrum/gui/qt/paytoedit.py | 62 ++++++++++++++++++++-------------- electrum/gui/qt/qrtextedit.py | 13 +++++-- 3 files changed, 59 insertions(+), 40 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index fa7af9213..01d6720ec 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -916,7 +916,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.update_status() # resolve aliases # FIXME this might do blocking network calls that has a timeout of several seconds - self.payto_e.check_text() + self.payto_e.on_timer_check_text() self.notify_transactions() def format_amount(self, amount_sat, is_diff=False, whitespaces=False) -> str: @@ -1527,7 +1527,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): from .paytoedit import PayToEdit self.amount_e = BTCAmountEdit(self.get_decimal_point) self.payto_e = PayToEdit(self) - self.payto_e.addPasteButton() msg = (_("Recipient of the funds.") + "\n\n" + _("You may enter a Bitcoin address, a label from your list of contacts " "(a list of completions will be proposed), " @@ -2252,17 +2251,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): else: self.payment_request_error_signal.emit() - def set_lnurl6_bech32(self, lnurl: str): + def set_lnurl6_bech32(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) + self.set_lnurl6_url(url, can_use_network=can_use_network) - def set_lnurl6_url(self, url: str, *, lnurl_data: LNURL6Data = None): + 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: + if lnurl_data is None and can_use_network: lnurl_data = request_lnurl(url, self.network.send_http_on_proxy) if not lnurl_data: return @@ -2303,9 +2302,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self._is_onchain = b self.max_button.setEnabled(b) - def set_bip21(self, text: str): + def set_bip21(self, text: str, *, can_use_network: bool = True): + on_bip70_pr = self.on_pr if can_use_network else None try: - out = util.parse_URI(text, self.on_pr) + out = util.parse_URI(text, on_bip70_pr) except InvalidBitcoinURI as e: self.show_error(_("Error parsing URI") + f":\n{e}") return @@ -2313,7 +2313,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): r = out.get('r') sig = out.get('sig') name = out.get('name') - if r or (name and sig): + if (r or (name and sig)) and can_use_network: self.prepare_for_payment_request() return address = out.get('address') @@ -2322,7 +2322,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): message = out.get('message') lightning = out.get('lightning') if lightning: - self.handle_payment_identifier(lightning) + self.handle_payment_identifier(lightning, can_use_network=can_use_network) return # use label as description (not BIP21 compliant) if label and not message: @@ -2334,7 +2334,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): if amount: self.amount_e.setAmount(amount) - def handle_payment_identifier(self, text: str): + def handle_payment_identifier(self, text: str, *, can_use_network: bool = True): """Takes Lightning identifiers: * lightning-URI (containing bolt11 or lnurl) @@ -2350,7 +2350,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): invoice_or_lnurl = maybe_extract_lightning_payment_identifier(text) if invoice_or_lnurl: if invoice_or_lnurl.startswith('lnurl'): - self.set_lnurl6_bech32(invoice_or_lnurl) + self.set_lnurl6_bech32(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 + ':'): diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py index 7291c2cd9..55c43a476 100644 --- a/electrum/gui/qt/paytoedit.py +++ b/electrum/gui/qt/paytoedit.py @@ -63,9 +63,10 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): def __init__(self, win: 'ElectrumWindow'): CompletionTextEdit.__init__(self) - ScanQRTextEdit.__init__(self, config=win.config) + ScanQRTextEdit.__init__(self, config=win.config, setText=self._on_input_btn) Logger.__init__(self) self.win = win + self.app = win.app self.amount_edit = win.amount_e self.setFont(QFont(MONOSPACE_FONT)) document = self.document() @@ -84,6 +85,8 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): self.heightMax = (self.fontSpacing * 10) + self.verticalMargins self.c = None + self.addPasteButton(setText=self._on_input_btn) + self.textChanged.connect(self._on_text_changed) self.outputs = [] # type: List[PartialTxOutput] self.errors = [] # type: List[PayToLineError] self.is_pr = False @@ -100,8 +103,8 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): def setTextNoCheck(self, text: str): """Sets the text, while also ensuring the new value will not be resolved/checked.""" - self.setText(text) self.previous_payto = text + self.setText(text) def do_clear(self): self.is_pr = False @@ -168,19 +171,27 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): assert bitcoin.is_address(address) return address - def check_text(self): + def _on_input_btn(self, text: str): + self.setText(text) + self._check_text(full_check=True) + + def _on_text_changed(self): + if self.app.clipboard().text() == self.toPlainText(): + # user likely pasted from clipboard + self._check_text(full_check=True) + else: + self._check_text(full_check=False) + + def on_timer_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() + self._check_text(full_check=True) - def _check_text(self): + def _check_text(self, *, full_check: bool): + if self.previous_payto == str(self.toPlainText()).strip(): + return + if full_check: + self.previous_payto = str(self.toPlainText()).strip() self.errors = [] if self.is_pr: return @@ -194,10 +205,10 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): if len(lines) == 1: data = lines[0] try: - self.win.handle_payment_identifier(data) + self.win.handle_payment_identifier(data, can_use_network=full_check) except LNURLError as e: self.logger.exception("") - self.show_error(e) + self.win.show_error(e) except ValueError: pass else: @@ -218,17 +229,18 @@ 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 + 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 + # 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) diff --git a/electrum/gui/qt/qrtextedit.py b/electrum/gui/qt/qrtextedit.py index 3cd404658..22d89913e 100644 --- a/electrum/gui/qt/qrtextedit.py +++ b/electrum/gui/qt/qrtextedit.py @@ -1,3 +1,5 @@ +from typing import Callable + from electrum.i18n import _ from electrum.plugin import run_hook from electrum.simple_config import SimpleConfig @@ -21,11 +23,16 @@ class ShowQRTextEdit(ButtonsTextEdit): class ScanQRTextEdit(ButtonsTextEdit, MessageBoxMixin): - def __init__(self, text="", allow_multi: bool = False, *, config: SimpleConfig): + def __init__( + self, text="", allow_multi: bool = False, + *, + config: SimpleConfig, + setText: Callable[[str], None] = None, + ): ButtonsTextEdit.__init__(self, text) self.setReadOnly(False) - self.add_file_input_button(config=config, show_error=self.show_error) - self.add_qr_input_button(config=config, show_error=self.show_error, allow_multi=allow_multi) + self.add_file_input_button(config=config, show_error=self.show_error, setText=setText) + self.add_qr_input_button(config=config, show_error=self.show_error, allow_multi=allow_multi, setText=setText) run_hook('scan_text_edit', self) def contextMenuEvent(self, e):