payment_identifier: fix emaillike
qt: validate on pushback timer, buttons enable/disable, cleanup
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user