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 threading
import os
import traceback
import json
import weakref
import csv
@ -55,13 +54,10 @@ from electrum import (keystore, ecc, constants, util, bitcoin, commands,
from electrum.bitcoin import COIN, is_address
from electrum.plugin import run_hook, BasePlugin
from electrum.i18n import _
from electrum.util import (format_time, get_asyncio_loop,
UserCancelled, profiler,
bfh, InvalidPassword,
UserFacingException,
get_new_wallet_name, send_exception_to_crash_reporter,
from electrum.util import (format_time, UserCancelled, profiler, bfh, InvalidPassword,
UserFacingException, get_new_wallet_name, send_exception_to_crash_reporter,
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.transaction import (Transaction, PartialTxInput,
PartialTransaction, PartialTxOutput)
@ -1329,11 +1325,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
return None
return clayout.selected_index()
def handle_payment_identifier(self, *args, **kwargs):
try:
self.send_tab.handle_payment_identifier(*args, **kwargs)
except FailedToParsePaymentIdentifier as e:
self.show_error(str(e))
def handle_payment_identifier(self, text: str):
pi = PaymentIdentifier(self.wallet, text)
if pi.is_valid():
self.send_tab.set_payment_identifier(text)
else:
if pi.error:
self.show_error(str(pi.error))
def set_frozen_state_of_addresses(self, addrs, freeze: bool):
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
# SOFTWARE.
import re
import decimal
from functools import partial
from decimal import Decimal
from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING
from PyQt5.QtGui import QFontMetrics, QFont
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.payment_identifier import PaymentIdentifier, FailedToParsePaymentIdentifier
from electrum.transaction import PartialTxOutput
from electrum.bitcoin import opcodes, construct_script
from electrum.payment_identifier import PaymentIdentifier
from electrum.logging import Logger
from electrum.lnurl import LNURLError
from .qrtextedit import ScanQRTextEdit
from .completion_text_edit import CompletionTextEdit
@ -214,9 +208,15 @@ class PayToEdit(Logger, GenericInputHandler):
if self.disable_checks:
return
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.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):
total = 0

24
electrum/gui/qt/send_tab.py

@ -4,21 +4,19 @@
import asyncio
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.QtWidgets import (QLabel, QVBoxLayout, QGridLayout,
QHBoxLayout, QCompleter, QWidget, QToolTip, QPushButton)
from electrum import util, paymentrequest
from electrum import lnutil
from electrum.plugin import run_hook
from electrum.i18n import _
from electrum.util import get_asyncio_loop, NotEnoughFunds, NoDynamicFeeEstimates, InvoiceError, parse_max_spend
from electrum.payment_identifier import PaymentIdentifier, FailedToParsePaymentIdentifier, InvalidBitcoinURI
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.transaction import Transaction, PartialTxInput, PartialTransaction, PartialTxOutput
from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput
from electrum.network import TxBroadcastError, BestEffortRequestFailed
from electrum.logging import Logger
@ -34,7 +32,7 @@ if TYPE_CHECKING:
class SendTab(QWidget, MessageBoxMixin, Logger):
resolve_done_signal = pyqtSignal(object)
round_2_signal = pyqtSignal(object)
finalize_done_signal = pyqtSignal(object)
round_3_signal = pyqtSignal(object)
def __init__(self, window: 'ElectrumWindow'):
@ -184,7 +182,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
run_hook('create_send_tab', grid)
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)
def do_paste(self):
@ -203,7 +201,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
self.payto_e.text_edit.setText(text)
else:
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):
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.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()
self.save_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):
pi = self.payment_identifier
if pi.needs_round_2():
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
if pi.need_finalize():
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
self.pending_invoice = self.read_invoice()
if not self.pending_invoice:

101
electrum/payment_identifier.py

@ -3,8 +3,7 @@ import urllib
import re
from decimal import Decimal, InvalidOperation
from enum import IntEnum
from typing import NamedTuple, Optional, Callable, Any, Sequence, List, TYPE_CHECKING
from urllib.parse import urlparse
from typing import NamedTuple, Optional, Callable, List, TYPE_CHECKING
from . import bitcoin
from .contacts import AliasNotFoundException
@ -13,7 +12,7 @@ from .logging import Logger
from .util import parse_max_spend, format_satoshis_plain
from .util import get_asyncio_loop, log_exceptions
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 .lnaddr import lndecode, LnDecodeException, LnInvoiceException
from .lnutil import IncompatibleOrInsaneFeatures
@ -164,10 +163,6 @@ def is_uri(data: str) -> bool:
return False
class FailedToParsePaymentIdentifier(Exception):
pass
class PayToLineError(NamedTuple):
line_content: str
exc: Exception
@ -223,7 +218,6 @@ class PaymentIdentifier(Logger):
#
self.emaillike = None
self.openalias_data = None
self.lnaddress_data = None
#
self.bip70 = None
self.bip70_data = None
@ -241,8 +235,11 @@ class PaymentIdentifier(Logger):
def need_resolve(self):
return self._state == PaymentIdentifierState.NEED_RESOLVE
def need_finalize(self):
return self._state == PaymentIdentifierState.LNURLP_FINALIZE
def is_valid(self):
return bool(self._type)
return self._state not in [PaymentIdentifierState.INVALID, PaymentIdentifierState.EMPTY]
def is_lightning(self):
return self.lnurl or self.bolt11
@ -253,9 +250,6 @@ class PaymentIdentifier(Logger):
def get_error(self) -> str:
return self.error
def needs_round_2(self):
return self.lnurl and self.lnurl_data
def needs_round_3(self):
return self.bip70
@ -305,7 +299,7 @@ class PaymentIdentifier(Logger):
self.set_state(PaymentIdentifierState.NEED_RESOLVE)
elif self.error is None:
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)
def resolve(self, *, on_finished: 'Callable'):
@ -353,8 +347,53 @@ class PaymentIdentifier(Logger):
self.set_state(PaymentIdentifierState.ERROR)
return
except Exception as e:
self.error = f'{e!r}'
self.logger.error(self.error)
self.error = str(e)
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)
finally:
if on_finished:
@ -477,7 +516,7 @@ class PaymentIdentifier(Logger):
recipient, amount, description = self.get_bolt11_fields(self.bolt11)
elif self.lnurl and self.lnurl_data:
domain = urlparse(self.lnurl).netloc
domain = urllib.parse.urlparse(self.lnurl).netloc
#recipient = "invoice from lnurl"
recipient = f"{self.lnurl_data.metadata_plaintext} <{domain}>"
#amount = self.lnurl_data.min_sendable_sat
@ -570,36 +609,6 @@ class PaymentIdentifier(Logger):
return self.bip70_data.has_expired()
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
async def round_3(self, tx, refund_address, *, on_success):
if self.bip70:

Loading…
Cancel
Save