diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py index e33f7ee30..49afc7439 100644 --- a/electrum/gui/kivy/uix/screens.py +++ b/electrum/gui/kivy/uix/screens.py @@ -318,21 +318,24 @@ class SendScreen(CScreen, Logger): self.app.show_error(_('Invalid amount') + ':\n' + self.amount) return message = self.message - if self.is_lightning: - return LNInvoice.from_bech32(address) - else: # on-chain - if self.payment_request: - outputs = self.payment_request.get_outputs() - else: - if not bitcoin.is_address(address): - self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address) - return - outputs = [PartialTxOutput.from_address_and_value(address, amount)] - return self.app.wallet.create_invoice( - outputs=outputs, - message=message, - pr=self.payment_request, - URI=self.parsed_URI) + try: + if self.is_lightning: + return LNInvoice.from_bech32(address) + else: # on-chain + if self.payment_request: + outputs = self.payment_request.get_outputs() + else: + if not bitcoin.is_address(address): + self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address) + return + outputs = [PartialTxOutput.from_address_and_value(address, amount)] + return self.app.wallet.create_invoice( + outputs=outputs, + message=message, + pr=self.payment_request, + URI=self.parsed_URI) + except InvoiceError as e: + self.app.show_error(_('Error creating payment') + ':\n' + str(e)) def do_save(self): invoice = self.read_invoice() @@ -447,20 +450,24 @@ class ReceiveScreen(CScreen): amount = self.amount amount = self.app.get_amount(amount) if amount else 0 message = self.message - if lightning: - key = self.app.wallet.lnworker.add_request(amount, message, self.expiry()) - else: - addr = self.address or self.app.wallet.get_unused_address() - if not addr: - if not self.app.wallet.is_deterministic(): - addr = self.app.wallet.get_receiving_address() - else: - self.app.show_info(_('No address available. Please remove some of your pending requests.')) - return - self.address = addr - req = self.app.wallet.make_payment_request(addr, amount, message, self.expiry()) - self.app.wallet.add_payment_request(req) - key = addr + try: + if lightning: + key = self.app.wallet.lnworker.add_request(amount, message, self.expiry()) + else: + addr = self.address or self.app.wallet.get_unused_address() + if not addr: + if not self.app.wallet.is_deterministic(): + addr = self.app.wallet.get_receiving_address() + else: + self.app.show_info(_('No address available. Please remove some of your pending requests.')) + return + self.address = addr + req = self.app.wallet.make_payment_request(addr, amount, message, self.expiry()) + self.app.wallet.add_payment_request(req) + key = addr + except InvoiceError as e: + self.app.show_error(_('Error creating payment request') + ':\n' + str(e)) + return self.clear() self.update() self.app.show_request(lightning, key) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 7a607caab..de82be070 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -63,7 +63,8 @@ from electrum.util import (format_time, get_new_wallet_name, send_exception_to_crash_reporter, InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds, NoDynamicFeeEstimates, MultipleSpendMaxTxOutputs, - AddTransactionException, BITCOIN_BIP21_URI_SCHEME) + AddTransactionException, BITCOIN_BIP21_URI_SCHEME, + InvoiceError) from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING, Invoice from electrum.invoices import PR_PAID, PR_FAILED, pr_expiration_values, LNInvoice, OnchainInvoice from electrum.transaction import (Transaction, PartialTxInput, @@ -78,7 +79,7 @@ from electrum.exchange_rate import FxThread from electrum.simple_config import SimpleConfig from electrum.logging import Logger from electrum.lnutil import ln_dummy_address, extract_nodeid, ConnStringFormatError -from electrum.lnaddr import lndecode, LnDecodeException +from electrum.lnaddr import lndecode, LnDecodeException, LnAddressError from .exception_window import Exception_Hook from .amountedit import AmountEdit, BTCAmountEdit, FreezableLineEdit, FeerateEdit @@ -1223,21 +1224,26 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): else: return - def create_invoice(self, is_lightning): + def create_invoice(self, is_lightning: bool): amount = self.receive_amount_e.get_amount() message = self.receive_message_e.text() expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) - if is_lightning: - if not self.wallet.lnworker.channels: - self.show_error(_("You need to open a Lightning channel first.")) - return - # TODO maybe show a warning if amount exceeds lnworker.num_sats_can_receive (as in kivy) - key = self.wallet.lnworker.add_request(amount, message, expiry) - else: - key = self.create_bitcoin_request(amount, message, expiry) - if not key: - return - self.address_list.update() + try: + if is_lightning: + if not self.wallet.lnworker.channels: + self.show_error(_("You need to open a Lightning channel first.")) + return + # TODO maybe show a warning if amount exceeds lnworker.num_sats_can_receive (as in kivy) + key = self.wallet.lnworker.add_request(amount, message, expiry) + else: + key = self.create_bitcoin_request(amount, message, expiry) + if not key: + return + self.address_list.update() + except (InvoiceError, LnAddressError) as e: + self.show_error(_('Error creating payment request') + ':\n' + str(e)) + return + assert key is not None self.request_list.update() self.request_list.select_key(key) @@ -1250,7 +1256,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): title = _('Invoice') if is_lightning else _('Address') self.do_copy(content, title=title) - def create_bitcoin_request(self, amount, message, expiration) -> Optional[str]: + def create_bitcoin_request(self, amount: int, message: str, expiration: int) -> Optional[str]: addr = self.wallet.get_unused_address() if addr is None: if not self.wallet.is_deterministic(): # imported wallet @@ -1595,32 +1601,35 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def read_invoice(self): if self.check_send_tab_payto_line_and_show_errors(): return - if not self._is_onchain: - invoice_str = self.payto_e.lightning_invoice - if not invoice_str: - return - if not self.wallet.has_lightning(): - self.show_error(_('Lightning is disabled')) - return - invoice = LNInvoice.from_bech32(invoice_str) - if invoice.get_amount_msat() is None: - amount_sat = self.amount_e.get_amount() - if amount_sat: - invoice.amount_msat = int(amount_sat * 1000) - else: - self.show_error(_('No amount')) + try: + if not self._is_onchain: + invoice_str = self.payto_e.lightning_invoice + if not invoice_str: return - return invoice - else: - outputs = self.read_outputs() - if self.check_send_tab_onchain_outputs_and_show_errors(outputs): - return - message = self.message_e.text() - return self.wallet.create_invoice( - outputs=outputs, - message=message, - pr=self.payment_request, - URI=self.payto_URI) + if not self.wallet.has_lightning(): + self.show_error(_('Lightning is disabled')) + return + invoice = LNInvoice.from_bech32(invoice_str) + if invoice.get_amount_msat() is None: + amount_sat = self.amount_e.get_amount() + if amount_sat: + invoice.amount_msat = int(amount_sat * 1000) + else: + self.show_error(_('No amount')) + return + return invoice + else: + outputs = self.read_outputs() + if self.check_send_tab_onchain_outputs_and_show_errors(outputs): + return + message = self.message_e.text() + return self.wallet.create_invoice( + outputs=outputs, + message=message, + pr=self.payment_request, + URI=self.payto_URI) + except InvoiceError as e: + self.show_error(_('Error creating payment') + ':\n' + str(e)) def do_save_invoice(self): self.pending_invoice = self.read_invoice() diff --git a/electrum/invoices.py b/electrum/invoices.py index e8a9ca4f9..8dcb51f8b 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -6,7 +6,7 @@ import attr from .json_db import StoredObject from .i18n import _ -from .util import age +from .util import age, InvoiceError from .lnaddr import lndecode, LnAddr from . import constants from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC @@ -134,12 +134,12 @@ class OnchainInvoice(Invoice): def _validate_amount(self, attribute, value): if isinstance(value, int): if not (0 <= value <= TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN): - raise ValueError(f"amount is out-of-bounds: {value!r} sat") + raise InvoiceError(f"amount is out-of-bounds: {value!r} sat") elif isinstance(value, str): if value != "!": - raise ValueError(f"unexpected amount: {value!r}") + raise InvoiceError(f"unexpected amount: {value!r}") else: - raise ValueError(f"unexpected amount: {value!r}") + raise InvoiceError(f"unexpected amount: {value!r}") @classmethod def from_bip70_payreq(cls, pr: 'PaymentRequest', height:int) -> 'OnchainInvoice': @@ -173,9 +173,9 @@ class LNInvoice(Invoice): return if isinstance(value, int): if not (0 <= value <= TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN * 1000): - raise ValueError(f"amount is out-of-bounds: {value!r} msat") + raise InvoiceError(f"amount is out-of-bounds: {value!r} msat") else: - raise ValueError(f"unexpected amount: {value!r}") + raise InvoiceError(f"unexpected amount: {value!r}") @property def _lnaddr(self) -> LnAddr: @@ -231,4 +231,3 @@ class LNInvoice(Invoice): # 'tags': str(lnaddr.tags), }) return d - diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py index 3d2549b63..0792e8526 100644 --- a/electrum/lnaddr.py +++ b/electrum/lnaddr.py @@ -22,6 +22,10 @@ if TYPE_CHECKING: from .lnutil import LnFeatures +class LnAddressError(Exception): + pass + + # BOLT #11: # # A writer MUST encode `amount` as a positive decimal integer with no @@ -265,6 +269,7 @@ def lnencode(addr: 'LnAddr', privkey) -> str: return bech32_encode(segwit_addr.Encoding.BECH32, hrp, bitarray_to_u5(data)) + class LnAddr(object): def __init__(self, *, paymenthash: bytes = None, amount=None, currency=None, tags=None, date=None, payment_secret: bytes = None): @@ -286,16 +291,16 @@ class LnAddr(object): @amount.setter def amount(self, value): if not (isinstance(value, Decimal) or value is None): - raise ValueError(f"amount must be Decimal or None, not {value!r}") + raise LnAddressError(f"amount must be Decimal or None, not {value!r}") if value is None: self._amount = None return assert isinstance(value, Decimal) if value.is_nan() or not (0 <= value <= TOTAL_COIN_SUPPLY_LIMIT_IN_BTC): - raise ValueError(f"amount is out-of-bounds: {value!r} BTC") + raise LnAddressError(f"amount is out-of-bounds: {value!r} BTC") if value * 10**12 % 10: # max resolution is millisatoshi - raise ValueError(f"Cannot encode {value!r}: too many decimal places") + raise LnAddressError(f"Cannot encode {value!r}: too many decimal places") self._amount = value def get_amount_sat(self) -> Optional[Decimal]: