Browse Source

qt: refactor send_tab, paytoedit

master
Sander van Grieken 3 years ago
parent
commit
bde066f9ce
  1. 1
      electrum/gui/qt/amountedit.py
  2. 187
      electrum/gui/qt/paytoedit.py
  3. 226
      electrum/gui/qt/send_tab.py
  4. 15
      electrum/gui/qt/util.py
  5. 156
      electrum/payment_identifier.py

1
electrum/gui/qt/amountedit.py

@ -13,7 +13,6 @@ from electrum.util import (format_satoshis_plain, decimal_point_to_base_unit_nam
FEERATE_PRECISION, quantize_feerate, DECIMAL_POINT) FEERATE_PRECISION, quantize_feerate, DECIMAL_POINT)
from electrum.bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC from electrum.bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
_NOT_GIVEN = object() # sentinel value _NOT_GIVEN = object() # sentinel value

187
electrum/gui/qt/paytoedit.py

@ -26,6 +26,8 @@
from functools import partial from functools import partial
from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING
from PyQt5.QtCore import Qt
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtGui import QFontMetrics, QFont from PyQt5.QtGui import QFontMetrics, QFont
from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QTextEdit, QVBoxLayout from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QTextEdit, QVBoxLayout
@ -48,6 +50,10 @@ frozen_style = "QWidget {border:none;}"
normal_style = "QPlainTextEdit { }" normal_style = "QPlainTextEdit { }"
class InvalidPaymentIdentifier(Exception):
pass
class ResizingTextEdit(QTextEdit): class ResizingTextEdit(QTextEdit):
def __init__(self): def __init__(self):
@ -63,113 +69,139 @@ class ResizingTextEdit(QTextEdit):
self.verticalMargins += documentMargin * 2 self.verticalMargins += documentMargin * 2
self.heightMin = self.fontSpacing + self.verticalMargins self.heightMin = self.fontSpacing + self.verticalMargins
self.heightMax = (self.fontSpacing * 10) + self.verticalMargins self.heightMax = (self.fontSpacing * 10) + self.verticalMargins
self.single_line = True
self.update_size() self.update_size()
def update_size(self): def update_size(self):
docLineCount = self.document().lineCount() docLineCount = self.document().lineCount()
docHeight = max(3, docLineCount) * self.fontSpacing docHeight = max(1 if self.single_line else 3, docLineCount) * self.fontSpacing
h = docHeight + self.verticalMargins h = docHeight + self.verticalMargins
h = min(max(h, self.heightMin), self.heightMax) h = min(max(h, self.heightMin), self.heightMax)
self.setMinimumHeight(int(h)) self.setMinimumHeight(int(h))
self.setMaximumHeight(int(h)) self.setMaximumHeight(int(h))
self.verticalScrollBar().setHidden(docHeight + self.verticalMargins < self.heightMax) if self.single_line:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
else:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.verticalScrollBar().setHidden(docHeight + self.verticalMargins < self.heightMax)
self.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
class PayToEdit(QObject, Logger, GenericInputHandler):
class PayToEdit(Logger, GenericInputHandler): paymentIdentifierChanged = pyqtSignal()
def __init__(self, send_tab: 'SendTab'): def __init__(self, send_tab: 'SendTab'):
QObject.__init__(self, parent=send_tab)
Logger.__init__(self) Logger.__init__(self)
GenericInputHandler.__init__(self) GenericInputHandler.__init__(self)
self.line_edit = QLineEdit()
self.text_edit = ResizingTextEdit() self.text_edit = ResizingTextEdit()
self.text_edit.hide() self.text_edit.textChanged.connect(self._on_text_edit_text_changed)
self._is_paytomany = False self._is_paytomany = False
for w in [self.line_edit, self.text_edit]: self.text_edit.setFont(QFont(MONOSPACE_FONT))
w.setFont(QFont(MONOSPACE_FONT))
w.textChanged.connect(self._on_text_changed)
self.send_tab = send_tab self.send_tab = send_tab
self.config = send_tab.config self.config = send_tab.config
self.win = send_tab.window
self.app = QApplication.instance() self.app = QApplication.instance()
self.amount_edit = self.send_tab.amount_e
self.logger.debug(util.ColorScheme.RED.as_stylesheet(True))
self.is_multiline = False self.is_multiline = False
self.disable_checks = False # self.is_alias = False
self.is_alias = False
self.payto_scriptpubkey = None # type: Optional[bytes] self.payto_scriptpubkey = None # type: Optional[bytes]
self.previous_payto = '' self.previous_payto = ''
# editor methods # editor methods
self.setStyleSheet = self.editor.setStyleSheet self.setStyleSheet = self.text_edit.setStyleSheet
self.setText = self.editor.setText self.setText = self.text_edit.setText
self.setEnabled = self.editor.setEnabled self.setFocus = self.text_edit.setFocus
self.setReadOnly = self.editor.setReadOnly self.setToolTip = self.text_edit.setToolTip
self.setFocus = self.editor.setFocus
# button handlers # button handlers
self.on_qr_from_camera_input_btn = partial( self.on_qr_from_camera_input_btn = partial(
self.input_qr_from_camera, self.input_qr_from_camera,
config=self.config, config=self.config,
allow_multi=False, allow_multi=False,
show_error=self.win.show_error, show_error=self.send_tab.show_error,
setText=self._on_input_btn, setText=self.try_payment_identifier,
parent=self.win, parent=self.send_tab.window,
) )
self.on_qr_from_screenshot_input_btn = partial( self.on_qr_from_screenshot_input_btn = partial(
self.input_qr_from_screenshot, self.input_qr_from_screenshot,
allow_multi=False, allow_multi=False,
show_error=self.win.show_error, show_error=self.send_tab.show_error,
setText=self._on_input_btn, setText=self.try_payment_identifier,
) )
self.on_input_file = partial( self.on_input_file = partial(
self.input_file, self.input_file,
config=self.config, config=self.config,
show_error=self.win.show_error, show_error=self.send_tab.show_error,
setText=self._on_input_btn, setText=self.try_payment_identifier,
) )
#
self.line_edit.contextMenuEvent = partial(editor_contextMenuEvent, self.line_edit, self)
self.text_edit.contextMenuEvent = partial(editor_contextMenuEvent, self.text_edit, self) self.text_edit.contextMenuEvent = partial(editor_contextMenuEvent, self.text_edit, self)
@property self.payment_identifier = None
def editor(self):
return self.text_edit if self.is_paytomany() else self.line_edit def set_text(self, text: str):
self.text_edit.setText(text)
def update_editor(self):
if self.text_edit.toPlainText() != self.payment_identifier.text:
self.text_edit.setText(self.payment_identifier.text)
self.text_edit.single_line = not self.payment_identifier.is_multiline()
self.text_edit.update_size()
'''set payment identifier only if valid, else exception'''
def try_payment_identifier(self, text):
text = text.strip()
pi = PaymentIdentifier(self.send_tab.wallet, text)
if not pi.is_valid():
raise InvalidPaymentIdentifier('Invalid payment identifier')
self.set_payment_identifier(text)
def set_payment_identifier(self, text):
text = text.strip()
if self.payment_identifier and self.payment_identifier.text == text:
# no change.
return
self.payment_identifier = PaymentIdentifier(self.send_tab.wallet, text)
# 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)
# if payment identifier gets set externally, we want to update the text_edit
# Note: this triggers the change handler, but we shortcut if it's the same payment identifier
self.update_editor()
self.paymentIdentifierChanged.emit()
def set_paytomany(self, b): def set_paytomany(self, b):
has_focus = self.editor.hasFocus()
self._is_paytomany = b self._is_paytomany = b
self.line_edit.setVisible(not b) self.text_edit.single_line = not self._is_paytomany
self.text_edit.setVisible(b) self.text_edit.update_size()
self.send_tab.paytomany_menu.setChecked(b) self.send_tab.paytomany_menu.setChecked(b)
if has_focus:
self.editor.setFocus()
def toggle_paytomany(self): def toggle_paytomany(self):
self.set_paytomany(not self._is_paytomany) self.set_paytomany(not self._is_paytomany)
def toPlainText(self):
return self.text_edit.toPlainText() if self.is_paytomany() else self.line_edit.text()
def is_paytomany(self): def is_paytomany(self):
return self._is_paytomany return self._is_paytomany
def setFrozen(self, b): def setFrozen(self, b):
self.setReadOnly(b) self.text_edit.setReadOnly(b)
if not b: if not b:
self.setStyleSheet(normal_style) self.setStyleSheet(normal_style)
def setTextNoCheck(self, text: str): def isFrozen(self):
"""Sets the text, while also ensuring the new value will not be resolved/checked.""" return self.text_edit.isReadOnly()
self.previous_payto = text
self.setText(text)
def do_clear(self): def do_clear(self):
self.is_multiline = False self.is_multiline = False
self.set_paytomany(False) self.set_paytomany(False)
self.disable_checks = False
self.is_alias = False
self.line_edit.setText('')
self.text_edit.setText('') self.text_edit.setText('')
self.setFrozen(False) self.payment_identifier = None
self.setEnabled(True)
def setGreen(self): def setGreen(self):
self.setStyleSheet(util.ColorScheme.GREEN.as_stylesheet(True)) self.setStyleSheet(util.ColorScheme.GREEN.as_stylesheet(True))
@ -177,53 +209,18 @@ class PayToEdit(Logger, GenericInputHandler):
def setExpired(self): def setExpired(self):
self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True)) self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True))
def _on_input_btn(self, text: str): def _on_text_edit_text_changed(self):
self.setText(text) self._handle_text_change(self.text_edit.toPlainText())
def _on_text_changed(self):
text = self.toPlainText()
# False if user pasted from clipboard
full_check = self.app.clipboard().text() != text
self._check_text(text, full_check=full_check)
if self.is_multiline and not self._is_paytomany:
self.set_paytomany(True)
self.text_edit.setText(text)
self.text_edit.setFocus()
def _check_text(self, text, *, full_check: bool): def _handle_text_change(self, text):
""" side effects: self.is_multiline """ if self.isFrozen():
text = str(text).strip() # if editor is frozen, we ignore text changes as they might not be a payment identifier
if not text: # but a user friendly representation.
return return
if self.previous_payto == text:
return self.set_payment_identifier(text)
if full_check: if self.app.clipboard().text() and self.app.clipboard().text().strip() == self.payment_identifier.text:
self.previous_payto = text # user pasted from clipboard
if self.disable_checks: self.logger.debug('from clipboard')
return if self.payment_identifier.error:
pi = PaymentIdentifier(self.send_tab.wallet, text) self.send_tab.show_error(_('Clipboard text is not a valid payment identifier') + '\n' + self.payment_identifier.error)
self.is_multiline = bool(pi.multiline_outputs) # TODO: why both is_multiline and set_paytomany(True)??
self.logger.debug(f'is_multiline {self.is_multiline}')
if pi.is_valid():
self.send_tab.set_payment_identifier(text)
else:
if not full_check and pi.error:
self.send_tab.show_error(
_('Clipboard text is not a valid payment identifier') + '\n' + str(pi.error))
return
def handle_multiline(self, outputs):
total = 0
is_max = False
for output in outputs:
if parse_max_spend(output.value):
is_max = True
else:
total += output.value
self.send_tab.set_onchain(True)
self.send_tab.max_button.setChecked(is_max)
if self.send_tab.max_button.isChecked():
self.send_tab.spend_max()
else:
self.amount_edit.setAmount(total if outputs else None)
#self.send_tab.lock_amount(self.send_tab.max_button.isChecked() or bool(outputs))

226
electrum/gui/qt/send_tab.py

@ -6,14 +6,13 @@ import asyncio
from decimal import Decimal from decimal import Decimal
from typing import Optional, TYPE_CHECKING, Sequence, List, Callable from typing import Optional, TYPE_CHECKING, Sequence, List, Callable
from PyQt5.QtCore import pyqtSignal, QPoint from PyQt5.QtCore import pyqtSignal, QPoint
from PyQt5.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, from PyQt5.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, QHBoxLayout,
QHBoxLayout, QCompleter, QWidget, QToolTip, QPushButton) QWidget, QToolTip, QPushButton, QApplication)
from electrum.plugin import run_hook from electrum.plugin import run_hook
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, parse_max_spend from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, parse_max_spend
from electrum.payment_identifier import PaymentIdentifier
from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST
from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput
@ -21,8 +20,10 @@ from electrum.network import TxBroadcastError, BestEffortRequestFailed
from electrum.logging import Logger from electrum.logging import Logger
from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit from .amountedit import AmountEdit, BTCAmountEdit, SizedFreezableLineEdit
from .util import WaitingDialog, HelpLabel, MessageBoxMixin, EnterButton, char_width_in_lineedit from .paytoedit import InvalidPaymentIdentifier
from .util import get_iconname_camera, get_iconname_qrcode, read_QIcon from .util import (WaitingDialog, HelpLabel, MessageBoxMixin, EnterButton,
char_width_in_lineedit, get_iconname_camera, get_iconname_qrcode,
read_QIcon)
from .confirm_tx_dialog import ConfirmTxDialog from .confirm_tx_dialog import ConfirmTxDialog
if TYPE_CHECKING: if TYPE_CHECKING:
@ -38,7 +39,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
def __init__(self, window: 'ElectrumWindow'): def __init__(self, window: 'ElectrumWindow'):
QWidget.__init__(self, window) QWidget.__init__(self, window)
Logger.__init__(self) Logger.__init__(self)
self.app = QApplication.instance()
self.window = window self.window = window
self.wallet = window.wallet self.wallet = window.wallet
self.fx = window.fx self.fx = window.fx
@ -49,7 +50,6 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
self.format_amount = window.format_amount self.format_amount = window.format_amount
self.base_unit = window.base_unit self.base_unit = window.base_unit
self.payment_identifier = None
self.pending_invoice = None self.pending_invoice = None
# A 4-column grid layout. All the stretch is in the last column. # A 4-column grid layout. All the stretch is in the last column.
@ -73,7 +73,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
"e.g. set one amount to '2!' and another to '3!' to split your coins 40-60.")) "e.g. set one amount to '2!' and another to '3!' to split your coins 40-60."))
payto_label = HelpLabel(_('Pay to'), msg) payto_label = HelpLabel(_('Pay to'), msg)
grid.addWidget(payto_label, 0, 0) grid.addWidget(payto_label, 0, 0)
grid.addWidget(self.payto_e.line_edit, 0, 1, 1, 4) # grid.addWidget(self.payto_e.line_edit, 0, 1, 1, 4)
grid.addWidget(self.payto_e.text_edit, 0, 1, 1, 4) grid.addWidget(self.payto_e.text_edit, 0, 1, 1, 4)
#completer = QCompleter() #completer = QCompleter()
@ -119,11 +119,9 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
btn_width = 10 * char_width_in_lineedit() btn_width = 10 * char_width_in_lineedit()
self.max_button.setFixedWidth(btn_width) self.max_button.setFixedWidth(btn_width)
self.max_button.setCheckable(True) self.max_button.setCheckable(True)
self.max_button.setEnabled(False)
grid.addWidget(self.max_button, 3, 3) grid.addWidget(self.max_button, 3, 3)
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.paste_button = QPushButton() self.paste_button = QPushButton()
self.paste_button.clicked.connect(self.do_paste) self.paste_button.clicked.connect(self.do_paste)
self.paste_button.setIcon(read_QIcon('copy.png')) self.paste_button.setIcon(read_QIcon('copy.png'))
@ -131,9 +129,15 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
self.paste_button.setMaximumWidth(35) self.paste_button.setMaximumWidth(35)
grid.addWidget(self.paste_button, 0, 5) grid.addWidget(self.paste_button, 0, 5)
self.save_button = EnterButton(_("Save"), self.do_save_invoice)
self.save_button.setEnabled(False)
self.send_button = EnterButton(_("Pay") + "...", self.do_pay_or_get_invoice)
self.send_button.setEnabled(False)
self.clear_button = EnterButton(_("Clear"), self.do_clear)
buttons = QHBoxLayout() buttons = QHBoxLayout()
buttons.addStretch(1) buttons.addStretch(1)
#buttons.addWidget(self.paste_button)
buttons.addWidget(self.clear_button) buttons.addWidget(self.clear_button)
buttons.addWidget(self.save_button) buttons.addWidget(self.save_button)
buttons.addWidget(self.send_button) buttons.addWidget(self.send_button)
@ -143,14 +147,11 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
def reset_max(text): def reset_max(text):
self.max_button.setChecked(False) self.max_button.setChecked(False)
enable = not bool(text) and not self.amount_e.isReadOnly()
# self.max_button.setEnabled(enable)
self.amount_e.textChanged.connect(self.on_amount_changed)
self.amount_e.textEdited.connect(reset_max) self.amount_e.textEdited.connect(reset_max)
self.fiat_send_e.textEdited.connect(reset_max) self.fiat_send_e.textEdited.connect(reset_max)
self.set_onchain(False)
self.invoices_label = QLabel(_('Invoices')) self.invoices_label = QLabel(_('Invoices'))
from .invoice_list import InvoiceList from .invoice_list import InvoiceList
self.invoice_list = InvoiceList(self) self.invoice_list = InvoiceList(self)
@ -184,30 +185,33 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
self.resolve_done_signal.connect(self.on_resolve_done) self.resolve_done_signal.connect(self.on_resolve_done)
self.finalize_done_signal.connect(self.on_finalize_done) self.finalize_done_signal.connect(self.on_finalize_done)
self.notify_merchant_done_signal.connect(self.on_notify_merchant_done) self.notify_merchant_done_signal.connect(self.on_notify_merchant_done)
self.payto_e.paymentIdentifierChanged.connect(self._handle_payment_identifier)
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)
def do_paste(self): def do_paste(self):
text = self.window.app.clipboard().text() try:
if not text: self.payto_e.try_payment_identifier(self.app.clipboard().text())
return except InvalidPaymentIdentifier as e:
self.set_payment_identifier(text) self.show_error(_('Invalid payment identifier on clipboard'))
def set_payment_identifier(self, text): def set_payment_identifier(self, text):
self.payment_identifier = PaymentIdentifier(self.wallet, text) self.logger.debug('set_payment_identifier')
if self.payment_identifier.error: try:
self.show_error(_('Clipboard text is not a valid payment identifier') + '\n' + self.payment_identifier.error) self.payto_e.try_payment_identifier(text)
return except InvalidPaymentIdentifier as e:
if self.payment_identifier.is_multiline(): self.show_error(_('Invalid payment identifier'))
self.payto_e.set_paytomany(True)
self.payto_e.text_edit.setText(text)
else:
self.payto_e.setTextNoCheck(text)
self._handle_payment_identifier(can_use_network=True)
def spend_max(self): def spend_max(self):
assert self.payto_e.payment_identifier is not None
assert self.payto_e.payment_identifier.type in ['spk', 'multiline']
if run_hook('abort_send', self): if run_hook('abort_send', self):
return return
amount = self.get_amount() outputs = self.payto_e.payment_identifier.get_onchain_outputs('!')
outputs = self.payment_identifier.get_onchain_outputs(amount)
if not outputs: if not outputs:
return return
make_tx = lambda fee_est, *, confirmed_only=False: self.wallet.make_unsigned_transaction( make_tx = lambda fee_est, *, confirmed_only=False: self.wallet.make_unsigned_transaction(
@ -296,9 +300,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
text = _("Not enough funds") text = _("Not enough funds")
frozen_str = self.get_frozen_balance_str() frozen_str = self.get_frozen_balance_str()
if frozen_str: if frozen_str:
text += " ({} {})".format( text += " ({} {})".format(frozen_str, _("are frozen"))
frozen_str, _("are frozen")
)
return text return text
def get_frozen_balance_str(self) -> Optional[str]: def get_frozen_balance_str(self) -> Optional[str]:
@ -308,31 +310,26 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
return self.format_amount_and_units(frozen_bal) return self.format_amount_and_units(frozen_bal)
def do_clear(self): def do_clear(self):
self.lock_fields(lock_recipient=False, lock_amount=False, lock_max=True, lock_description=False)
self.max_button.setChecked(False) self.max_button.setChecked(False)
self.payto_e.do_clear() self.payto_e.do_clear()
self.set_onchain(False)
for w in [self.comment_e, self.comment_label]: for w in [self.comment_e, self.comment_label]:
w.setVisible(False) w.setVisible(False)
for e in [self.message_e, self.amount_e, self.fiat_send_e]: for e in [self.message_e, self.amount_e, self.fiat_send_e]:
e.setText('') e.setText('')
self.set_field_style(e, None, False) self.set_field_style(e, None, False)
for e in [self.send_button, self.save_button, self.clear_button, self.amount_e, self.fiat_send_e]: for e in [self.save_button, self.send_button]:
e.setEnabled(True) e.setEnabled(False)
self.window.update_status() self.window.update_status()
run_hook('do_clear', self) run_hook('do_clear', self)
def set_onchain(self, b):
self._is_onchain = b
self.max_button.setEnabled(b)
def prepare_for_send_tab_network_lookup(self): def prepare_for_send_tab_network_lookup(self):
self.window.show_send_tab() self.window.show_send_tab()
self.payto_e.disable_checks = True
#for e in [self.payto_e, self.message_e]: #for e in [self.payto_e, self.message_e]:
self.payto_e.setFrozen(True) # self.payto_e.setFrozen(True)
for btn in [self.save_button, self.send_button, self.clear_button]: for btn in [self.save_button, self.send_button, self.clear_button]:
btn.setEnabled(False) btn.setEnabled(False)
self.payto_e.setTextNoCheck(_("please wait...")) # self.payto_e.setTextNoCheck(_("please wait..."))
def payment_request_error(self, error): def payment_request_error(self, error):
self.show_message(error) self.show_message(error)
@ -348,45 +345,90 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
style = ColorScheme.RED.as_stylesheet(True) style = ColorScheme.RED.as_stylesheet(True)
if text is not None: if text is not None:
w.setStyleSheet(style) w.setStyleSheet(style)
w.setReadOnly(True)
else: else:
w.setStyleSheet('') w.setStyleSheet('')
w.setReadOnly(False)
def lock_fields(self, *,
lock_recipient: Optional[bool] = None,
lock_amount: Optional[bool] = None,
lock_max: Optional[bool] = None,
lock_description: Optional[bool] = None
) -> None:
self.logger.debug(f'locking fields, r={lock_recipient}, a={lock_amount}, m={lock_max}, d={lock_description}')
if lock_recipient is not None:
self.payto_e.setFrozen(lock_recipient)
if lock_amount is not None:
self.amount_e.setFrozen(lock_amount)
if lock_max is not None:
self.max_button.setEnabled(not lock_max)
if lock_description is not None:
self.message_e.setFrozen(lock_description)
def update_fields(self): def update_fields(self):
recipient, amount, description, comment, validated = self.payment_identifier.get_fields_for_GUI() pi = self.payto_e.payment_identifier
if recipient:
self.payto_e.setTextNoCheck(recipient) if pi.is_multiline():
elif self.payment_identifier.multiline_outputs: self.lock_fields(lock_recipient=False, lock_amount=True, lock_max=True, lock_description=False)
self.payto_e.handle_multiline(self.payment_identifier.multiline_outputs) self.set_field_style(self.payto_e, pi.multiline_outputs, False if not pi.is_valid() else None)
if description: self.save_button.setEnabled(pi.is_valid())
self.message_e.setText(description) self.send_button.setEnabled(pi.is_valid())
if amount: if pi.is_valid():
self.amount_e.setAmount(amount) self.handle_multiline(pi.multiline_outputs)
for w in [self.comment_e, self.comment_label]: else:
w.setVisible(not bool(comment)) # self.payto_e.setToolTip('\n'.join(list(map(lambda x: f'{x.idx}: {x.line_content}', pi.get_error()))))
self.set_field_style(self.payto_e, recipient or self.payment_identifier.multiline_outputs, validated) self.payto_e.setToolTip(pi.get_error())
self.set_field_style(self.message_e, description, validated)
self.set_field_style(self.amount_e, amount, validated)
self.set_field_style(self.fiat_send_e, amount, validated)
def _handle_payment_identifier(self, *, can_use_network: bool = True):
is_valid = self.payment_identifier.is_valid()
self.save_button.setEnabled(is_valid)
self.send_button.setEnabled(is_valid)
if not is_valid:
return return
if not pi.is_valid():
self.lock_fields(lock_recipient=False, lock_amount=False, lock_max=True, lock_description=False)
self.save_button.setEnabled(False)
self.send_button.setEnabled(False)
return
lock_recipient = pi.type != 'spk'
self.lock_fields(lock_recipient=lock_recipient,
lock_amount=pi.is_amount_locked(),
lock_max=pi.is_amount_locked(),
lock_description=False)
if lock_recipient:
recipient, amount, description, comment, validated = pi.get_fields_for_GUI()
if recipient:
self.payto_e.setText(recipient)
if description:
self.message_e.setText(description)
self.lock_fields(lock_description=True)
if amount:
self.amount_e.setAmount(amount)
for w in [self.comment_e, self.comment_label]:
w.setVisible(bool(comment))
self.set_field_style(self.payto_e, recipient or pi.multiline_outputs, validated)
self.set_field_style(self.message_e, description, validated)
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)
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() self.update_fields()
if self.payment_identifier.need_resolve():
if not is_valid:
self.logger.debug(f'PI error: {self.payto_e.payment_identifier.error}')
return
if self.payto_e.payment_identifier.need_resolve():
self.prepare_for_send_tab_network_lookup() self.prepare_for_send_tab_network_lookup()
self.payment_identifier.resolve(on_finished=self.resolve_done_signal.emit) self.payto_e.payment_identifier.resolve(on_finished=self.resolve_done_signal.emit)
# update fiat amount # update fiat amount (and reset max)
self.amount_e.textEdited.emit("") self.amount_e.textEdited.emit("")
self.window.show_send_tab() self.window.show_send_tab()
def on_resolve_done(self, pi): def on_resolve_done(self, pi):
if self.payment_identifier.error: if self.payto_e.payment_identifier.error:
self.show_error(self.payment_identifier.error) self.show_error(self.payto_e.payment_identifier.error)
self.do_clear() self.do_clear()
return return
self.update_fields() self.update_fields()
@ -404,10 +446,10 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
self.show_error(_('No amount')) self.show_error(_('No amount'))
return return
invoice = self.payment_identifier.get_invoice(amount_sat, self.get_message()) invoice = self.payto_e.payment_identifier.get_invoice(amount_sat, self.get_message())
#except Exception as e: #except Exception as e:
if not invoice: if not invoice:
self.show_error('error getting invoice' + self.payment_identifier.error) self.show_error('error getting invoice' + self.payto_e.payment_identifier.error)
return return
if not self.wallet.has_lightning() and not invoice.can_be_paid_onchain(): if not self.wallet.has_lightning() and not invoice.can_be_paid_onchain():
self.show_error(_('Lightning is disabled')) self.show_error(_('Lightning is disabled'))
@ -439,18 +481,17 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
return self.amount_e.get_amount() or 0 return self.amount_e.get_amount() or 0
def on_finalize_done(self, pi): def on_finalize_done(self, pi):
self.do_clear()
if pi.error: if pi.error:
self.show_error(pi.error) self.show_error(pi.error)
self.do_clear()
return return
self.update_fields(pi) self.update_fields()
invoice = pi.get_invoice(self.get_amount(), self.get_message()) invoice = pi.get_invoice(self.get_amount(), self.get_message())
self.pending_invoice = invoice self.pending_invoice = invoice
self.logger.debug(f'after finalize invoice: {invoice!r}')
self.do_pay_invoice(invoice) self.do_pay_invoice(invoice)
def do_pay_or_get_invoice(self): def do_pay_or_get_invoice(self):
pi = self.payment_identifier pi = self.payto_e.payment_identifier
if pi.need_finalize(): if pi.need_finalize():
self.prepare_for_send_tab_network_lookup() self.prepare_for_send_tab_network_lookup()
pi.finalize(amount_sat=self.get_amount(), comment=self.message_e.text(), pi.finalize(amount_sat=self.get_amount(), comment=self.message_e.text(),
@ -511,9 +552,9 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
"""Returns whether there are errors. """Returns whether there are errors.
Also shows error dialog to user if so. Also shows error dialog to user if so.
""" """
error = self.payment_identifier.get_error() error = self.payto_e.payment_identifier.get_error()
if error: if error:
if not self.payment_identifier.is_multiline(): if not self.payto_e.payment_identifier.is_multiline():
err = error err = error
self.show_warning( self.show_warning(
_("Failed to parse 'Pay to' line") + ":\n" + _("Failed to parse 'Pay to' line") + ":\n" +
@ -527,13 +568,13 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
# for err in errors])) # for err in errors]))
return True return True
warning = self.payment_identifier.warning warning = self.payto_e.payment_identifier.warning
if warning: if warning:
warning += '\n' + _('Do you wish to continue?') warning += '\n' + _('Do you wish to continue?')
if not self.question(warning): if not self.question(warning):
return True return True
if self.payment_identifier.has_expired(): if self.payto_e.payment_identifier.has_expired():
self.show_error(_('Payment request has expired')) self.show_error(_('Payment request has expired'))
return True return True
@ -619,7 +660,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
def broadcast_thread(): def broadcast_thread():
# non-GUI thread # non-GUI thread
if self.payment_identifier.has_expired(): if self.payto_e.payment_identifier.has_expired():
return False, _("Invoice has expired") return False, _("Invoice has expired")
try: try:
self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
@ -629,9 +670,9 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
return False, repr(e) return False, repr(e)
# success # success
txid = tx.txid() txid = tx.txid()
if self.payment_identifier.need_merchant_notify(): if self.payto_e.payment_identifier.need_merchant_notify():
refund_address = self.wallet.get_receiving_address() refund_address = self.wallet.get_receiving_address()
self.payment_identifier.notify_merchant( self.payto_e.payment_identifier.notify_merchant(
tx=tx, tx=tx,
refund_address=refund_address, refund_address=refund_address,
on_finished=self.notify_merchant_done_signal.emit on_finished=self.notify_merchant_done_signal.emit
@ -683,10 +724,23 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
self.window.show_send_tab() self.window.show_send_tab()
self.payto_e.do_clear() self.payto_e.do_clear()
if len(paytos) == 1: if len(paytos) == 1:
self.logger.debug('payto_e setText 1')
self.payto_e.setText(paytos[0]) self.payto_e.setText(paytos[0])
self.amount_e.setFocus() self.amount_e.setFocus()
else: else:
self.payto_e.setFocus() self.payto_e.setFocus()
text = "\n".join([payto + ", 0" for payto in paytos]) text = "\n".join([payto + ", 0" for payto in paytos])
self.logger.debug('payto_e setText n')
self.payto_e.setText(text) self.payto_e.setText(text)
self.payto_e.setFocus() self.payto_e.setFocus()
def handle_multiline(self, outputs):
total = 0
for output in outputs:
if parse_max_spend(output.value):
self.max_button.setChecked(True) # TODO: remove and let spend_max set this?
self.spend_max()
return
else:
total += output.value
self.amount_e.setAmount(total if outputs else None)

15
electrum/gui/qt/util.py

@ -562,7 +562,10 @@ class GenericInputHandler:
new_text = self.text() + data + '\n' new_text = self.text() + data + '\n'
else: else:
new_text = data new_text = data
setText(new_text) try:
setText(new_text)
except Exception as e:
show_error(_('Invalid payment identifier in QR') + ':\n' + repr(e))
from .qrreader import scan_qrcode from .qrreader import scan_qrcode
if parent is None: if parent is None:
@ -599,7 +602,10 @@ class GenericInputHandler:
new_text = self.text() + data + '\n' new_text = self.text() + data + '\n'
else: else:
new_text = data new_text = data
setText(new_text) try:
setText(new_text)
except Exception as e:
show_error(_('Invalid payment identifier in QR') + ':\n' + repr(e))
def input_file( def input_file(
self, self,
@ -628,7 +634,10 @@ class GenericInputHandler:
except BaseException as e: except BaseException as e:
show_error(_('Error opening file') + ':\n' + repr(e)) show_error(_('Error opening file') + ':\n' + repr(e))
else: else:
setText(data) try:
setText(data)
except Exception as e:
show_error(_('Invalid payment identifier in file') + ':\n' + repr(e))
def input_paste_from_clipboard( def input_paste_from_clipboard(
self, self,

156
electrum/payment_identifier.py

@ -1,4 +1,5 @@
import asyncio import asyncio
import time
import urllib import urllib
import re import re
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
@ -163,13 +164,6 @@ def is_uri(data: str) -> bool:
return False return False
class PayToLineError(NamedTuple):
line_content: str
exc: Exception
idx: int = 0 # index of line
is_multiline: bool = False
RE_ALIAS = r'(.*?)\s*\<([0-9A-Za-z]{1,})\>' RE_ALIAS = r'(.*?)\s*\<([0-9A-Za-z]{1,})\>'
RE_EMAIL = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' RE_EMAIL = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b'
@ -210,12 +204,13 @@ class PaymentIdentifier(Logger):
self.wallet = wallet self.wallet = wallet
self.contacts = wallet.contacts if wallet is not None else None self.contacts = wallet.contacts if wallet is not None else None
self.config = wallet.config if wallet is not None else None self.config = wallet.config if wallet is not None else None
self.text = text self.text = text.strip()
self._type = None self._type = None
self.error = None # if set, GUI should show error and stop self.error = None # if set, GUI should show error and stop
self.warning = None # if set, GUI should ask user if they want to proceed self.warning = None # if set, GUI should ask user if they want to proceed
# more than one of those may be set # more than one of those may be set
self.multiline_outputs = None self.multiline_outputs = None
self._is_max = False
self.bolt11 = None self.bolt11 = None
self.bip21 = None self.bip21 = None
self.spk = None self.spk = None
@ -234,8 +229,12 @@ class PaymentIdentifier(Logger):
self.logger.debug(f'PI parsing...') self.logger.debug(f'PI parsing...')
self.parse(text) self.parse(text)
@property
def type(self):
return self._type
def set_state(self, state: 'PaymentIdentifierState'): def set_state(self, state: 'PaymentIdentifierState'):
self.logger.debug(f'PI state -> {state}') self.logger.debug(f'PI state {self._state} -> {state}')
self._state = state self._state = state
def need_resolve(self): def need_resolve(self):
@ -256,6 +255,31 @@ class PaymentIdentifier(Logger):
def is_multiline(self): def is_multiline(self):
return bool(self.multiline_outputs) return bool(self.multiline_outputs)
def is_multiline_max(self):
return self.is_multiline() and self._is_max
def is_amount_locked(self):
if self._type == 'spk':
return False
elif self._type == 'bip21':
return bool(self.bip21.get('amount'))
elif self._type == 'bip70':
return True # TODO always given?
elif self._type == 'bolt11':
lnaddr = lndecode(self.bolt11)
return bool(lnaddr.amount)
elif self._type == 'lnurl':
# 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}')
return not (self.lnurl_data.min_sendable_sat < self.lnurl_data.max_sendable_sat)
return True
elif self._type == 'multiline':
return True
def is_error(self) -> bool: def is_error(self) -> bool:
return self._state >= PaymentIdentifierState.ERROR return self._state >= PaymentIdentifierState.ERROR
@ -281,11 +305,21 @@ class PaymentIdentifier(Logger):
self.lnurl = decode_lnurl(invoice_or_lnurl) self.lnurl = decode_lnurl(invoice_or_lnurl)
self.set_state(PaymentIdentifierState.NEED_RESOLVE) self.set_state(PaymentIdentifierState.NEED_RESOLVE)
except Exception as e: except Exception as e:
self.error = "Error parsing Lightning invoice" + f":\n{e}" self.error = _("Error parsing LNURL") + f":\n{e}"
self.set_state(PaymentIdentifierState.INVALID) self.set_state(PaymentIdentifierState.INVALID)
return return
else: else:
self._type = 'bolt11' self._type = 'bolt11'
try:
lndecode(invoice_or_lnurl)
except LnInvoiceException as e:
self.error = _("Error parsing Lightning invoice") + f":\n{e}"
self.set_state(PaymentIdentifierState.INVALID)
return
except IncompatibleOrInsaneFeatures as e:
self.error = _("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}"
self.set_state(PaymentIdentifierState.INVALID)
return
self.bolt11 = invoice_or_lnurl self.bolt11 = invoice_or_lnurl
self.set_state(PaymentIdentifierState.AVAILABLE) self.set_state(PaymentIdentifierState.AVAILABLE)
elif text.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'): elif text.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
@ -295,19 +329,31 @@ class PaymentIdentifier(Logger):
self.error = _("Error parsing URI") + f":\n{e}" self.error = _("Error parsing URI") + f":\n{e}"
self.set_state(PaymentIdentifierState.INVALID) self.set_state(PaymentIdentifierState.INVALID)
return return
self._type = 'bip21'
self.bip21 = out self.bip21 = out
self.bip70 = out.get('r') self.bip70 = out.get('r')
if self.bip70: if self.bip70:
self._type = 'bip70'
self.set_state(PaymentIdentifierState.NEED_RESOLVE) self.set_state(PaymentIdentifierState.NEED_RESOLVE)
else: else:
self._type = 'bip21'
# check optional lightning in bip21, set self.bolt11 if valid
bolt11 = out.get('lightning')
if bolt11:
try:
lndecode(bolt11)
# if we get here, we have a usable bolt11
self.bolt11 = bolt11
except LnInvoiceException as e:
self.logger.debug(_("Error parsing Lightning invoice") + f":\n{e}")
except IncompatibleOrInsaneFeatures as e:
self.logger.debug(_("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}")
self.set_state(PaymentIdentifierState.AVAILABLE) self.set_state(PaymentIdentifierState.AVAILABLE)
elif scriptpubkey := self.parse_output(text): elif scriptpubkey := self.parse_output(text):
self._type = 'spk' self._type = 'spk'
self.spk = scriptpubkey self.spk = scriptpubkey
self.set_state(PaymentIdentifierState.AVAILABLE) self.set_state(PaymentIdentifierState.AVAILABLE)
elif re.match(RE_EMAIL, text): elif re.match(RE_EMAIL, text):
self._type = 'alias' self._type = 'emaillike'
self.emaillike = text self.emaillike = text
self.set_state(PaymentIdentifierState.NEED_RESOLVE) self.set_state(PaymentIdentifierState.NEED_RESOLVE)
elif self.error is None: elif self.error is None:
@ -324,9 +370,10 @@ class PaymentIdentifier(Logger):
async def _do_resolve(self, *, on_finished=None): async def _do_resolve(self, *, on_finished=None):
try: try:
if self.emaillike: if self.emaillike:
# TODO: parallel lookup?
data = await self.resolve_openalias() data = await self.resolve_openalias()
if data: if data:
self.openalias_data = data # needed? self.openalias_data = data
self.logger.debug(f'OA: {data!r}') self.logger.debug(f'OA: {data!r}')
name = data.get('name') name = data.get('name')
address = data.get('address') address = data.get('address')
@ -335,8 +382,14 @@ class PaymentIdentifier(Logger):
self.warning = _( self.warning = _(
'WARNING: the alias "{}" could not be validated via an additional ' 'WARNING: the alias "{}" could not be validated via an additional '
'security check, DNSSEC, and thus may not be correct.').format(self.emaillike) 'security check, DNSSEC, and thus may not be correct.').format(self.emaillike)
# this will set self.spk and update state try:
self.parse(address) scriptpubkey = self.parse_output(address)
self._type = 'openalias'
self.spk = scriptpubkey
self.set_state(PaymentIdentifierState.AVAILABLE)
except Exception as e:
self.error = str(e)
self.set_state(PaymentIdentifierState.NOT_FOUND)
else: else:
lnurl = lightning_address_to_url(self.emaillike) lnurl = lightning_address_to_url(self.emaillike)
try: try:
@ -356,6 +409,7 @@ class PaymentIdentifier(Logger):
data = await request_lnurl(self.lnurl) data = await request_lnurl(self.lnurl)
self.lnurl_data = data self.lnurl_data = data
self.set_state(PaymentIdentifierState.LNURLP_FINALIZE) self.set_state(PaymentIdentifierState.LNURLP_FINALIZE)
self.logger.debug(f'LNURL data: {data!r}')
else: else:
self.set_state(PaymentIdentifierState.ERROR) self.set_state(PaymentIdentifierState.ERROR)
return return
@ -458,23 +512,22 @@ class PaymentIdentifier(Logger):
lines = [i for i in lines if i] lines = [i for i in lines if i]
is_multiline = len(lines) > 1 is_multiline = len(lines) > 1
outputs = [] # type: List[PartialTxOutput] outputs = [] # type: List[PartialTxOutput]
errors = [] errors = ''
total = 0 total = 0
is_max = False self._is_max = False
for i, line in enumerate(lines): for i, line in enumerate(lines):
try: try:
output = self.parse_address_and_amount(line) output = self.parse_address_and_amount(line)
outputs.append(output) outputs.append(output)
if parse_max_spend(output.value): if parse_max_spend(output.value):
is_max = True self._is_max = True
else: else:
total += output.value total += output.value
except Exception as e: except Exception as e:
errors.append(PayToLineError( errors = f'{errors}line #{i}: {str(e)}\n'
idx=i, line_content=line.strip(), exc=e, is_multiline=True))
continue continue
if is_multiline and errors: if is_multiline and errors:
self.error = str(errors) if errors else None self.error = errors.strip() if errors else None
self.logger.debug(f'multiline: {outputs!r}, {self.error}') self.logger.debug(f'multiline: {outputs!r}, {self.error}')
return outputs return outputs
@ -494,15 +547,14 @@ class PaymentIdentifier(Logger):
address = self.parse_address(x) address = self.parse_address(x)
return bytes.fromhex(bitcoin.address_to_script(address)) return bytes.fromhex(bitcoin.address_to_script(address))
except Exception as e: except Exception as e:
error = PayToLineError(idx=0, line_content=x, exc=e, is_multiline=False) pass
try: try:
script = self.parse_script(x) script = self.parse_script(x)
return bytes.fromhex(script) return bytes.fromhex(script)
except Exception as e: except Exception as e:
#error = PayToLineError(idx=0, line_content=x, exc=e, is_multiline=False)
pass pass
#raise Exception("Invalid address or script.")
#self.errors.append(error) raise Exception("Invalid address or script.")
def parse_script(self, x): def parse_script(self, x):
script = '' script = ''
@ -535,12 +587,11 @@ class PaymentIdentifier(Logger):
return address return address
def get_fields_for_GUI(self): def get_fields_for_GUI(self):
""" sets self.error as side effect"""
recipient = None recipient = None
amount = None amount = None
description = None description = None
validated = None validated = None
comment = "no comment" comment = None
if self.emaillike and self.openalias_data: if self.emaillike and self.openalias_data:
address = self.openalias_data.get('address') address = self.openalias_data.get('address')
@ -550,21 +601,17 @@ class PaymentIdentifier(Logger):
if not validated: if not validated:
self.warning = _('WARNING: the alias "{}" could not be validated via an additional ' self.warning = _('WARNING: the alias "{}" could not be validated via an additional '
'security check, DNSSEC, and thus may not be correct.').format(self.emaillike) 'security check, DNSSEC, and thus may not be correct.').format(self.emaillike)
#self.payto_e.set_openalias(key=pi.openalias, data=oa_data)
#self.window.contact_list.update()
elif self.bolt11: elif self.bolt11 and self.wallet.has_lightning():
recipient, amount, description = self.get_bolt11_fields(self.bolt11) recipient, amount, description = self._get_bolt11_fields(self.bolt11)
elif self.lnurl and self.lnurl_data: elif self.lnurl and self.lnurl_data:
domain = urllib.parse.urlparse(self.lnurl).netloc domain = urllib.parse.urlparse(self.lnurl).netloc
#recipient = "invoice from lnurl"
recipient = f"{self.lnurl_data.metadata_plaintext} <{domain}>" recipient = f"{self.lnurl_data.metadata_plaintext} <{domain}>"
#amount = self.lnurl_data.min_sendable_sat amount = self.lnurl_data.min_sendable_sat if self.lnurl_data.min_sendable_sat else None
amount = None description = self.lnurl_data.metadata_plaintext
description = None
if self.lnurl_data.comment_allowed: if self.lnurl_data.comment_allowed:
comment = None comment = self.lnurl_data.comment_allowed
elif self.bip70 and self.bip70_data: elif self.bip70 and self.bip70_data:
pr = self.bip70_data pr = self.bip70_data
@ -575,8 +622,6 @@ class PaymentIdentifier(Logger):
amount = pr.get_amount() amount = pr.get_amount()
description = pr.get_memo() description = pr.get_memo()
validated = not pr.has_expired() validated = not pr.has_expired()
#self.set_onchain(True)
#self.max_button.setEnabled(False)
# note: allow saving bip70 reqs, as we save them anyway when paying them # note: allow saving bip70 reqs, as we save them anyway when paying them
#for btn in [self.send_button, self.clear_button, self.save_button]: #for btn in [self.send_button, self.clear_button, self.save_button]:
# btn.setEnabled(True) # btn.setEnabled(True)
@ -584,8 +629,7 @@ class PaymentIdentifier(Logger):
#self.amount_e.textEdited.emit("") #self.amount_e.textEdited.emit("")
elif self.spk: elif self.spk:
recipient = self.text pass
amount = None
elif self.multiline_outputs: elif self.multiline_outputs:
pass pass
@ -598,26 +642,12 @@ class PaymentIdentifier(Logger):
# use label as description (not BIP21 compliant) # use label as description (not BIP21 compliant)
if label and not description: if label and not description:
description = label description = label
lightning = self.bip21.get('lightning')
if lightning and self.wallet.has_lightning():
# maybe set self.bolt11?
recipient, amount, description = self.get_bolt11_fields(lightning)
if not amount:
amount_required = True
# todo: merge logic
return recipient, amount, description, comment, validated return recipient, amount, description, comment, validated
def get_bolt11_fields(self, bolt11_invoice): def _get_bolt11_fields(self, bolt11_invoice):
"""Parse ln invoice, and prepare the send tab for it.""" """Parse ln invoice, and prepare the send tab for it."""
try: lnaddr = lndecode(bolt11_invoice) #
lnaddr = lndecode(bolt11_invoice)
except LnInvoiceException as e:
self.show_error(_("Error parsing Lightning invoice") + f":\n{e}")
return
except IncompatibleOrInsaneFeatures as e:
self.show_error(_("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}")
return
pubkey = lnaddr.pubkey.serialize().hex() pubkey = lnaddr.pubkey.serialize().hex()
for k, v in lnaddr.tags: for k, v in lnaddr.tags:
if k == 'd': if k == 'd':
@ -628,15 +658,17 @@ class PaymentIdentifier(Logger):
amount = lnaddr.get_amount_sat() amount = lnaddr.get_amount_sat()
return pubkey, amount, description return pubkey, amount, description
# TODO: rename to resolve_emaillike to disambiguate
async def resolve_openalias(self) -> Optional[dict]: async def resolve_openalias(self) -> Optional[dict]:
key = self.emaillike key = self.emaillike
if not (('.' in key) and ('<' not in key) and (' ' not in key)): # TODO: below check needed? we already matched RE_EMAIL
return None # if not (('.' in key) and ('<' not in key) and (' ' not in key)):
# return None
parts = key.split(sep=',') # assuming single line parts = key.split(sep=',') # assuming single line
if parts and len(parts) > 0 and bitcoin.is_address(parts[0]): if parts and len(parts) > 0 and bitcoin.is_address(parts[0]):
return None return None
try: try:
data = self.contacts.resolve(key) data = self.contacts.resolve(key) # TODO: don't use contacts as delegate to resolve openalias, separate.
return data return data
except AliasNotFoundException as e: except AliasNotFoundException as e:
self.logger.info(f'OpenAlias not found: {repr(e)}') self.logger.info(f'OpenAlias not found: {repr(e)}')
@ -648,6 +680,12 @@ class PaymentIdentifier(Logger):
def has_expired(self): def has_expired(self):
if self.bip70: if self.bip70:
return self.bip70_data.has_expired() return self.bip70_data.has_expired()
elif self.bolt11:
lnaddr = lndecode(self.bolt11)
return lnaddr.is_expired()
elif self.bip21:
expires = self.bip21.get('exp') + self.bip21.get('time') if self.bip21.get('exp') else 0
return bool(expires) and expires < time.time()
return False return False
def get_invoice(self, amount_sat, message): def get_invoice(self, amount_sat, message):

Loading…
Cancel
Save