Browse Source

payment_identifier: fix emaillike

qt: validate on pushback timer, buttons enable/disable, cleanup
master
Sander van Grieken 3 years ago
parent
commit
915f66c0b8
  1. 41
      electrum/gui/qt/paytoedit.py
  2. 54
      electrum/gui/qt/send_tab.py
  3. 22
      electrum/payment_identifier.py

41
electrum/gui/qt/paytoedit.py

@ -26,13 +26,12 @@
from functools import partial
from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING
from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtGui import QFontMetrics, QFont
from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QTextEdit, QVBoxLayout
from PyQt5.QtWidgets import QApplication, QTextEdit, QVBoxLayout
from electrum.i18n import _
from electrum.util import parse_max_spend
from electrum.payment_identifier import PaymentIdentifier
from electrum.logging import Logger
@ -42,7 +41,6 @@ from . import util
from .util import MONOSPACE_FONT, GenericInputHandler, editor_contextMenuEvent
if TYPE_CHECKING:
from .main_window import ElectrumWindow
from .send_tab import SendTab
@ -97,7 +95,7 @@ class PayToEdit(QObject, Logger, GenericInputHandler):
GenericInputHandler.__init__(self)
self.text_edit = ResizingTextEdit()
self.text_edit.textChanged.connect(self._on_text_edit_text_changed)
self.text_edit.textChanged.connect(self._handle_text_change)
self._is_paytomany = False
self.text_edit.setFont(QFont(MONOSPACE_FONT))
self.send_tab = send_tab
@ -138,6 +136,11 @@ class PayToEdit(QObject, Logger, GenericInputHandler):
self.text_edit.contextMenuEvent = partial(editor_contextMenuEvent, self.text_edit, self)
self.edit_timer = QTimer(self)
self.edit_timer.setSingleShot(True)
self.edit_timer.setInterval(1000)
self.edit_timer.timeout.connect(self._on_edit_timer)
self.payment_identifier = None
def set_text(self, text: str):
@ -167,7 +170,6 @@ class PayToEdit(QObject, Logger, GenericInputHandler):
# toggle to multiline if payment identifier is a multiline
self.is_multiline = self.payment_identifier.is_multiline()
self.logger.debug(f'is_multiline {self.is_multiline}')
if self.is_multiline and not self._is_paytomany:
self.set_paytomany(True)
@ -209,18 +211,25 @@ class PayToEdit(QObject, Logger, GenericInputHandler):
def setExpired(self):
self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True))
def _on_text_edit_text_changed(self):
self._handle_text_change(self.text_edit.toPlainText())
def _handle_text_change(self, text):
def _handle_text_change(self):
if self.isFrozen():
# if editor is frozen, we ignore text changes as they might not be a payment identifier
# but a user friendly representation.
return
self.set_payment_identifier(text)
if self.app.clipboard().text() and self.app.clipboard().text().strip() == self.payment_identifier.text:
# user pasted from clipboard
self.logger.debug('from clipboard')
if self.payment_identifier.error:
self.send_tab.show_error(_('Clipboard text is not a valid payment identifier') + '\n' + self.payment_identifier.error)
# pushback timer if timer active or PI needs resolving
pi = PaymentIdentifier(self.send_tab.wallet, self.text_edit.toPlainText())
if pi.need_resolve() or self.edit_timer.isActive():
self.edit_timer.start()
else:
self.set_payment_identifier(self.text_edit.toPlainText())
# self.set_payment_identifier(text)
# if self.app.clipboard().text() and self.app.clipboard().text().strip() == self.payment_identifier.text:
# # user pasted from clipboard
# self.logger.debug('from clipboard')
# if self.payment_identifier.error:
# self.send_tab.show_error(_('Clipboard text is not a valid payment identifier') + '\n' + self.payment_identifier.error)
def _on_edit_timer(self):
self.set_payment_identifier(self.text_edit.toPlainText())

54
electrum/gui/qt/send_tab.py

@ -2,22 +2,21 @@
# Distributed under the MIT software license, see the accompanying
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
import asyncio
from decimal import Decimal
from typing import Optional, TYPE_CHECKING, Sequence, List, Callable
from PyQt5.QtCore import pyqtSignal, QPoint
from PyQt5.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, QHBoxLayout,
QWidget, QToolTip, QPushButton, QApplication)
from electrum.plugin import run_hook
from electrum.i18n import _
from electrum.logging import Logger
from electrum.plugin import run_hook
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, parse_max_spend
from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST
from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput
from electrum.network import TxBroadcastError, BestEffortRequestFailed
from electrum.logging import Logger
from electrum.payment_identifier import PaymentIdentifierState
from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit
from .paytoedit import InvalidPaymentIdentifier
@ -73,7 +72,6 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
"e.g. set one amount to '2!' and another to '3!' to split your coins 40-60."))
payto_label = HelpLabel(_('Pay to'), msg)
grid.addWidget(payto_label, 0, 0)
# grid.addWidget(self.payto_e.line_edit, 0, 1, 1, 4)
grid.addWidget(self.payto_e.text_edit, 0, 1, 1, 4)
#completer = QCompleter()
@ -190,8 +188,8 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
def on_amount_changed(self, text):
# FIXME: implement full valid amount check to enable/disable Pay button
pi_valid = self.payto_e.payment_identifier.is_valid() if self.payto_e.payment_identifier else False
self.send_button.setEnabled(bool(self.amount_e.get_amount()) and pi_valid)
pi_error = self.payto_e.payment_identifier.is_error() if pi_valid else False
self.send_button.setEnabled(bool(self.amount_e.get_amount()) and pi_valid and not pi_error)
def do_paste(self):
try:
@ -324,7 +322,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
run_hook('do_clear', self)
def prepare_for_send_tab_network_lookup(self):
self.window.show_send_tab()
self.window.show_send_tab() # FIXME why is this here
#for e in [self.payto_e, self.message_e]:
# self.payto_e.setFrozen(True)
for btn in [self.save_button, self.send_button, self.clear_button]:
@ -367,16 +365,16 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
def update_fields(self):
pi = self.payto_e.payment_identifier
self.clear_button.setEnabled(True)
if pi.is_multiline():
self.lock_fields(lock_recipient=False, lock_amount=True, lock_max=True, lock_description=False)
self.set_field_style(self.payto_e, pi.multiline_outputs, False if not pi.is_valid() else None)
self.set_field_style(self.payto_e, True if not pi.is_valid() else None, False)
self.save_button.setEnabled(pi.is_valid())
self.send_button.setEnabled(pi.is_valid())
self.payto_e.setToolTip(pi.get_error() if not pi.is_valid() else '')
if pi.is_valid():
self.handle_multiline(pi.multiline_outputs)
else:
# self.payto_e.setToolTip('\n'.join(list(map(lambda x: f'{x.idx}: {x.line_content}', pi.get_error()))))
self.payto_e.setToolTip(pi.get_error())
return
if not pi.is_valid():
@ -385,10 +383,13 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
self.send_button.setEnabled(False)
return
lock_recipient = pi.type != 'spk'
lock_recipient = pi.type != 'spk' \
and not (pi.type == 'emaillike' and pi.is_state(PaymentIdentifierState.NOT_FOUND))
lock_max = pi.is_amount_locked() \
or pi.type in ['bolt11', 'lnurl', 'lightningaddress']
self.lock_fields(lock_recipient=lock_recipient,
lock_amount=pi.is_amount_locked(),
lock_max=pi.is_amount_locked(),
lock_max=lock_max,
lock_description=False)
if lock_recipient:
recipient, amount, description, comment, validated = pi.get_fields_for_GUI()
@ -406,16 +407,13 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
self.set_field_style(self.amount_e, amount, validated)
self.set_field_style(self.fiat_send_e, amount, validated)
self.send_button.setEnabled(bool(self.amount_e.get_amount()) and not pi.has_expired())
self.save_button.setEnabled(True)
self.send_button.setEnabled(bool(self.amount_e.get_amount()) and not pi.has_expired() and not pi.is_error())
self.save_button.setEnabled(not pi.is_error())
def _handle_payment_identifier(self):
is_valid = self.payto_e.payment_identifier.is_valid()
self.logger.debug(f'handle PI, valid={is_valid}')
self.update_fields()
if not is_valid:
if not self.payto_e.payment_identifier.is_valid():
self.logger.debug(f'PI error: {self.payto_e.payment_identifier.error}')
return
@ -424,16 +422,18 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
self.payto_e.payment_identifier.resolve(on_finished=self.resolve_done_signal.emit)
# update fiat amount (and reset max)
self.amount_e.textEdited.emit("")
self.window.show_send_tab()
self.window.show_send_tab() # FIXME: why is this here?
def on_resolve_done(self, pi):
if self.payto_e.payment_identifier.error:
self.show_error(self.payto_e.payment_identifier.error)
# TODO: resolve can happen while typing, we don't want message dialogs to pop up
# currently we don't set error for emaillike recipients to avoid just that
if pi.error:
self.show_error(pi.error)
self.do_clear()
return
self.update_fields()
for btn in [self.send_button, self.clear_button, self.save_button]:
btn.setEnabled(True)
# for btn in [self.send_button, self.clear_button, self.save_button]:
# btn.setEnabled(True)
def get_message(self):
return self.message_e.text()
@ -447,7 +447,6 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
return
invoice = self.payto_e.payment_identifier.get_invoice(amount_sat, self.get_message())
#except Exception as e:
if not invoice:
self.show_error('error getting invoice' + self.payto_e.payment_identifier.error)
return
@ -526,8 +525,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
self.pay_onchain_dialog(invoice.outputs)
def read_amount(self) -> List[PartialTxOutput]:
is_max = self.max_button.isChecked()
amount = '!' if is_max else self.get_amount()
amount = '!' if self.max_button.isChecked() else self.get_amount()
return amount
def check_onchain_outputs_and_show_errors(self, outputs: List[PartialTxOutput]) -> bool:

22
electrum/payment_identifier.py

@ -225,8 +225,7 @@ class PaymentIdentifier(Logger):
#
self.lnurl = None
self.lnurl_data = None
# parse without network
self.logger.debug(f'PI parsing...')
self.parse(text)
@property
@ -237,6 +236,9 @@ class PaymentIdentifier(Logger):
self.logger.debug(f'PI state {self._state} -> {state}')
self._state = state
def is_state(self, state: 'PaymentIdentifierState'):
return self._state == state
def need_resolve(self):
return self._state == PaymentIdentifierState.NEED_RESOLVE
@ -268,10 +270,9 @@ class PaymentIdentifier(Logger):
elif self._type == 'bolt11':
lnaddr = lndecode(self.bolt11)
return bool(lnaddr.amount)
elif self._type == 'lnurl':
elif self._type == 'lnurl' or self._type == 'lightningaddress':
# amount limits known after resolve, might be specific amount or locked to range
if self.need_resolve():
self.logger.debug(f'lnurl r')
return True
if self.need_finalize():
self.logger.debug(f'lnurl f {self.lnurl_data.min_sendable_sat}-{self.lnurl_data.max_sendable_sat}')
@ -279,6 +280,10 @@ class PaymentIdentifier(Logger):
return True
elif self._type == 'multiline':
return True
elif self._type == 'emaillike':
return False
elif self._type == 'openalias':
return False
def is_error(self) -> bool:
return self._state >= PaymentIdentifierState.ERROR
@ -394,11 +399,15 @@ class PaymentIdentifier(Logger):
lnurl = lightning_address_to_url(self.emaillike)
try:
data = await request_lnurl(lnurl)
self._type = 'lightningaddress'
self.lnurl = lnurl
self.lnurl_data = data
self.set_state(PaymentIdentifierState.LNURLP_FINALIZE)
except LNURLError as e:
self.error = str(e)
self.set_state(PaymentIdentifierState.NOT_FOUND)
except Exception as e:
# NOTE: any other exception is swallowed here (e.g. DNS error)
# as the user may be typing and we have an incomplete emaillike
self.set_state(PaymentIdentifierState.NOT_FOUND)
elif self.bip70:
from . import paymentrequest
@ -554,7 +563,7 @@ class PaymentIdentifier(Logger):
except Exception as e:
pass
raise Exception("Invalid address or script.")
# raise Exception("Invalid address or script.")
def parse_script(self, x):
script = ''
@ -658,7 +667,6 @@ class PaymentIdentifier(Logger):
amount = lnaddr.get_amount_sat()
return pubkey, amount, description
# TODO: rename to resolve_emaillike to disambiguate
async def resolve_openalias(self) -> Optional[dict]:
key = self.emaillike
# TODO: below check needed? we already matched RE_EMAIL

Loading…
Cancel
Save