Browse Source

payment_identifier: refactor round_2 to need_finalize/finalize stage

master
Sander van Grieken 3 years ago
parent
commit
7601726d29
  1. 22
      electrum/gui/qt/main_window.py
  2. 20
      electrum/gui/qt/paytoedit.py
  3. 24
      electrum/gui/qt/send_tab.py
  4. 101
      electrum/payment_identifier.py

22
electrum/gui/qt/main_window.py

@ -26,7 +26,6 @@ import sys
import time import time
import threading import threading
import os import os
import traceback
import json import json
import weakref import weakref
import csv import csv
@ -55,13 +54,10 @@ from electrum import (keystore, ecc, constants, util, bitcoin, commands,
from electrum.bitcoin import COIN, is_address from electrum.bitcoin import COIN, is_address
from electrum.plugin import run_hook, BasePlugin from electrum.plugin import run_hook, BasePlugin
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import (format_time, get_asyncio_loop, from electrum.util import (format_time, UserCancelled, profiler, bfh, InvalidPassword,
UserCancelled, profiler, UserFacingException, get_new_wallet_name, send_exception_to_crash_reporter,
bfh, InvalidPassword,
UserFacingException,
get_new_wallet_name, send_exception_to_crash_reporter,
AddTransactionException, os_chmod) AddTransactionException, os_chmod)
from electrum.payment_identifier import FailedToParsePaymentIdentifier, BITCOIN_BIP21_URI_SCHEME from electrum.payment_identifier import BITCOIN_BIP21_URI_SCHEME, PaymentIdentifier
from electrum.invoices import PR_PAID, Invoice from electrum.invoices import PR_PAID, Invoice
from electrum.transaction import (Transaction, PartialTxInput, from electrum.transaction import (Transaction, PartialTxInput,
PartialTransaction, PartialTxOutput) PartialTransaction, PartialTxOutput)
@ -1329,11 +1325,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
return None return None
return clayout.selected_index() return clayout.selected_index()
def handle_payment_identifier(self, *args, **kwargs): def handle_payment_identifier(self, text: str):
try: pi = PaymentIdentifier(self.wallet, text)
self.send_tab.handle_payment_identifier(*args, **kwargs) if pi.is_valid():
except FailedToParsePaymentIdentifier as e: self.send_tab.set_payment_identifier(text)
self.show_error(str(e)) else:
if pi.error:
self.show_error(str(pi.error))
def set_frozen_state_of_addresses(self, addrs, freeze: bool): def set_frozen_state_of_addresses(self, addrs, freeze: bool):
self.wallet.set_frozen_state_of_addresses(addrs, freeze) self.wallet.set_frozen_state_of_addresses(addrs, freeze)

20
electrum/gui/qt/paytoedit.py

@ -23,22 +23,16 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
import re
import decimal
from functools import partial from functools import partial
from decimal import Decimal
from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING
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
from electrum import bitcoin from electrum.i18n import _
from electrum.util import parse_max_spend from electrum.util import parse_max_spend
from electrum.payment_identifier import PaymentIdentifier, FailedToParsePaymentIdentifier from electrum.payment_identifier import PaymentIdentifier
from electrum.transaction import PartialTxOutput
from electrum.bitcoin import opcodes, construct_script
from electrum.logging import Logger from electrum.logging import Logger
from electrum.lnurl import LNURLError
from .qrtextedit import ScanQRTextEdit from .qrtextedit import ScanQRTextEdit
from .completion_text_edit import CompletionTextEdit from .completion_text_edit import CompletionTextEdit
@ -214,9 +208,15 @@ class PayToEdit(Logger, GenericInputHandler):
if self.disable_checks: if self.disable_checks:
return return
pi = PaymentIdentifier(self.send_tab.wallet, text) pi = PaymentIdentifier(self.send_tab.wallet, text)
self.is_multiline = bool(pi.multiline_outputs) 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}') self.logger.debug(f'is_multiline {self.is_multiline}')
self.send_tab.handle_payment_identifier(pi, can_use_network=full_check) 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): def handle_multiline(self, outputs):
total = 0 total = 0

24
electrum/gui/qt/send_tab.py

@ -4,21 +4,19 @@
import asyncio import asyncio
from decimal import Decimal from decimal import Decimal
from typing import Optional, TYPE_CHECKING, Sequence, List, Callable, Any 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, QCompleter, QWidget, QToolTip, QPushButton) QHBoxLayout, QCompleter, QWidget, QToolTip, QPushButton)
from electrum import util, paymentrequest
from electrum import lnutil
from electrum.plugin import run_hook from electrum.plugin import run_hook
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import get_asyncio_loop, NotEnoughFunds, NoDynamicFeeEstimates, InvoiceError, parse_max_spend from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, parse_max_spend
from electrum.payment_identifier import PaymentIdentifier, FailedToParsePaymentIdentifier, InvalidBitcoinURI 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, PartialTransaction, PartialTxOutput from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput
from electrum.network import TxBroadcastError, BestEffortRequestFailed from electrum.network import TxBroadcastError, BestEffortRequestFailed
from electrum.logging import Logger from electrum.logging import Logger
@ -34,7 +32,7 @@ if TYPE_CHECKING:
class SendTab(QWidget, MessageBoxMixin, Logger): class SendTab(QWidget, MessageBoxMixin, Logger):
resolve_done_signal = pyqtSignal(object) resolve_done_signal = pyqtSignal(object)
round_2_signal = pyqtSignal(object) finalize_done_signal = pyqtSignal(object)
round_3_signal = pyqtSignal(object) round_3_signal = pyqtSignal(object)
def __init__(self, window: 'ElectrumWindow'): def __init__(self, window: 'ElectrumWindow'):
@ -184,7 +182,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
run_hook('create_send_tab', grid) run_hook('create_send_tab', grid)
self.resolve_done_signal.connect(self.on_resolve_done) self.resolve_done_signal.connect(self.on_resolve_done)
self.round_2_signal.connect(self.on_round_2) self.finalize_done_signal.connect(self.on_round_2)
self.round_3_signal.connect(self.on_round_3) self.round_3_signal.connect(self.on_round_3)
def do_paste(self): def do_paste(self):
@ -203,7 +201,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
self.payto_e.text_edit.setText(text) self.payto_e.text_edit.setText(text)
else: else:
self.payto_e.setTextNoCheck(text) self.payto_e.setTextNoCheck(text)
self.handle_payment_identifier(can_use_network=True) self._handle_payment_identifier(can_use_network=True)
def spend_max(self): def spend_max(self):
if run_hook('abort_send', self): if run_hook('abort_send', self):
@ -372,7 +370,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
self.set_field_style(self.amount_e, amount, validated) self.set_field_style(self.amount_e, amount, validated)
self.set_field_style(self.fiat_send_e, amount, validated) self.set_field_style(self.fiat_send_e, amount, validated)
def handle_payment_identifier(self, *, can_use_network: bool = True): def _handle_payment_identifier(self, *, can_use_network: bool = True):
is_valid = self.payment_identifier.is_valid() is_valid = self.payment_identifier.is_valid()
self.save_button.setEnabled(is_valid) self.save_button.setEnabled(is_valid)
self.send_button.setEnabled(is_valid) self.send_button.setEnabled(is_valid)
@ -456,10 +454,10 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
def do_pay_or_get_invoice(self): def do_pay_or_get_invoice(self):
pi = self.payment_identifier pi = self.payment_identifier
if pi.needs_round_2(): if pi.need_finalize():
coro = pi.round_2(self.round_2_signal.emit, amount_sat=self.get_amount(), comment=self.message_e.text())
asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop()) # TODO should be cancellable
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(),
on_finished=self.finalize_done_signal.emit)
return return
self.pending_invoice = self.read_invoice() self.pending_invoice = self.read_invoice()
if not self.pending_invoice: if not self.pending_invoice:

101
electrum/payment_identifier.py

@ -3,8 +3,7 @@ import urllib
import re import re
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from enum import IntEnum from enum import IntEnum
from typing import NamedTuple, Optional, Callable, Any, Sequence, List, TYPE_CHECKING from typing import NamedTuple, Optional, Callable, List, TYPE_CHECKING
from urllib.parse import urlparse
from . import bitcoin from . import bitcoin
from .contacts import AliasNotFoundException from .contacts import AliasNotFoundException
@ -13,7 +12,7 @@ from .logging import Logger
from .util import parse_max_spend, format_satoshis_plain from .util import parse_max_spend, format_satoshis_plain
from .util import get_asyncio_loop, log_exceptions from .util import get_asyncio_loop, log_exceptions
from .transaction import PartialTxOutput from .transaction import PartialTxOutput
from .lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, LNURL6Data, lightning_address_to_url from .lnurl import decode_lnurl, request_lnurl, callback_lnurl, LNURLError, lightning_address_to_url
from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, opcodes, construct_script from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, opcodes, construct_script
from .lnaddr import lndecode, LnDecodeException, LnInvoiceException from .lnaddr import lndecode, LnDecodeException, LnInvoiceException
from .lnutil import IncompatibleOrInsaneFeatures from .lnutil import IncompatibleOrInsaneFeatures
@ -164,10 +163,6 @@ def is_uri(data: str) -> bool:
return False return False
class FailedToParsePaymentIdentifier(Exception):
pass
class PayToLineError(NamedTuple): class PayToLineError(NamedTuple):
line_content: str line_content: str
exc: Exception exc: Exception
@ -223,7 +218,6 @@ class PaymentIdentifier(Logger):
# #
self.emaillike = None self.emaillike = None
self.openalias_data = None self.openalias_data = None
self.lnaddress_data = None
# #
self.bip70 = None self.bip70 = None
self.bip70_data = None self.bip70_data = None
@ -241,8 +235,11 @@ class PaymentIdentifier(Logger):
def need_resolve(self): def need_resolve(self):
return self._state == PaymentIdentifierState.NEED_RESOLVE return self._state == PaymentIdentifierState.NEED_RESOLVE
def need_finalize(self):
return self._state == PaymentIdentifierState.LNURLP_FINALIZE
def is_valid(self): def is_valid(self):
return bool(self._type) return self._state not in [PaymentIdentifierState.INVALID, PaymentIdentifierState.EMPTY]
def is_lightning(self): def is_lightning(self):
return self.lnurl or self.bolt11 return self.lnurl or self.bolt11
@ -253,9 +250,6 @@ class PaymentIdentifier(Logger):
def get_error(self) -> str: def get_error(self) -> str:
return self.error return self.error
def needs_round_2(self):
return self.lnurl and self.lnurl_data
def needs_round_3(self): def needs_round_3(self):
return self.bip70 return self.bip70
@ -305,7 +299,7 @@ class PaymentIdentifier(Logger):
self.set_state(PaymentIdentifierState.NEED_RESOLVE) self.set_state(PaymentIdentifierState.NEED_RESOLVE)
elif self.error is None: elif self.error is None:
truncated_text = f"{text[:100]}..." if len(text) > 100 else text truncated_text = f"{text[:100]}..." if len(text) > 100 else text
self.error = FailedToParsePaymentIdentifier(f"Unknown payment identifier:\n{truncated_text}") self.error = f"Unknown payment identifier:\n{truncated_text}"
self.set_state(PaymentIdentifierState.INVALID) self.set_state(PaymentIdentifierState.INVALID)
def resolve(self, *, on_finished: 'Callable'): def resolve(self, *, on_finished: 'Callable'):
@ -353,8 +347,53 @@ class PaymentIdentifier(Logger):
self.set_state(PaymentIdentifierState.ERROR) self.set_state(PaymentIdentifierState.ERROR)
return return
except Exception as e: except Exception as e:
self.error = f'{e!r}' self.error = str(e)
self.logger.error(self.error) self.logger.error(repr(e))
self.set_state(PaymentIdentifierState.ERROR)
finally:
if on_finished:
on_finished(self)
def finalize(self, *, amount_sat: int = 0, comment: str = None, on_finished: Callable = None):
assert self._state == PaymentIdentifierState.LNURLP_FINALIZE
coro = self.do_finalize(amount_sat, comment, on_finished=on_finished)
asyncio.run_coroutine_threadsafe(coro, get_asyncio_loop())
@log_exceptions
async def do_finalize(self, amount_sat: int = None, comment: str = None, on_finished: Callable = None):
from .invoices import Invoice
try:
if not self.lnurl_data:
raise Exception("Unexpected missing LNURL data")
if not (self.lnurl_data.min_sendable_sat <= amount_sat <= self.lnurl_data.max_sendable_sat):
self.error = _('Amount must be between %d and %d sat.') \
% (self.lnurl_data.min_sendable_sat, self.lnurl_data.max_sendable_sat)
return
if self.lnurl_data.comment_allowed == 0:
comment = None
params = {'amount': amount_sat * 1000}
if comment:
params['comment'] = comment
try:
invoice_data = await callback_lnurl(
self.lnurl_data.callback_url,
params=params,
)
except LNURLError as e:
self.error = f"LNURL request encountered error: {e}"
return
bolt11_invoice = invoice_data.get('pr')
#
invoice = Invoice.from_bech32(bolt11_invoice)
if invoice.get_amount_sat() != amount_sat:
raise Exception("lnurl returned invoice with wrong amount")
# this will change what is returned by get_fields_for_GUI
self.bolt11 = bolt11_invoice
self.set_state(PaymentIdentifierState.AVAILABLE)
except Exception as e:
self.error = str(e)
self.logger.error(repr(e))
self.set_state(PaymentIdentifierState.ERROR) self.set_state(PaymentIdentifierState.ERROR)
finally: finally:
if on_finished: if on_finished:
@ -477,7 +516,7 @@ class PaymentIdentifier(Logger):
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 = urlparse(self.lnurl).netloc domain = urllib.parse.urlparse(self.lnurl).netloc
#recipient = "invoice from lnurl" #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
@ -570,36 +609,6 @@ class PaymentIdentifier(Logger):
return self.bip70_data.has_expired() return self.bip70_data.has_expired()
return False return False
@log_exceptions
async def round_2(self, on_success, amount_sat: int = None, comment: str = None):
from .invoices import Invoice
if self.lnurl:
if not (self.lnurl_data.min_sendable_sat <= amount_sat <= self.lnurl_data.max_sendable_sat):
self.error = f'Amount must be between {self.lnurl_data.min_sendable_sat} and {self.lnurl_data.max_sendable_sat} sat.'
return
if self.lnurl_data.comment_allowed == 0:
comment = None
params = {'amount': amount_sat * 1000}
if comment:
params['comment'] = comment
try:
invoice_data = await callback_lnurl(
self.lnurl_data.callback_url,
params=params,
)
except LNURLError as e:
self.error = f"LNURL request encountered error: {e}"
return
bolt11_invoice = invoice_data.get('pr')
#
invoice = Invoice.from_bech32(bolt11_invoice)
if invoice.get_amount_sat() != amount_sat:
raise Exception("lnurl returned invoice with wrong amount")
# this will change what is returned by get_fields_for_GUI
self.bolt11 = bolt11_invoice
on_success(self)
@log_exceptions @log_exceptions
async def round_3(self, tx, refund_address, *, on_success): async def round_3(self, tx, refund_address, *, on_success):
if self.bip70: if self.bip70:

Loading…
Cancel
Save