From 6ce96306ca4614a009f4c281ebb9ddb2cff97c95 Mon Sep 17 00:00:00 2001
From: bitromortac <bitromortac@protonmail.com>
Date: Wed, 28 Apr 2021 08:09:01 +0200
Subject: [PATCH 1/2] util: check bip21 url for amount

---
 electrum/util.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/electrum/util.py b/electrum/util.py
index 8fb9ca0d6..8f88b2b8d 100644
--- a/electrum/util.py
+++ b/electrum/util.py
@@ -872,7 +872,7 @@ class InvalidBitcoinURI(Exception): pass
 def parse_URI(uri: str, on_pr: Callable = None, *, loop=None) -> dict:
     """Raises InvalidBitcoinURI on malformed URI."""
     from . import bitcoin
-    from .bitcoin import COIN
+    from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
 
     if not isinstance(uri, str):
         raise InvalidBitcoinURI(f"expected string, not {repr(uri)}")
@@ -912,6 +912,8 @@ def parse_URI(uri: str, on_pr: Callable = None, *, loop=None) -> dict:
                 amount = Decimal(m.group(1)) * pow(Decimal(10), k)
             else:
                 amount = Decimal(am) * COIN
+            if amount > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN:
+                raise InvalidBitcoinURI(f"amount is out-of-bounds: {amount!r} BTC")
             out['amount'] = int(amount)
         except Exception as e:
             raise InvalidBitcoinURI(f"failed to parse 'amount' field: {repr(e)}") from e

From 853e912885cd3c8a4d8a0e1444556e4fda11059b Mon Sep 17 00:00:00 2001
From: bitromortac <bitromortac@protonmail.com>
Date: Thu, 6 May 2021 15:30:34 +0200
Subject: [PATCH 2/2] invoice: fail gracefully with large amount

---
 electrum/gui/kivy/uix/screens.py | 65 ++++++++++++-----------
 electrum/gui/qt/main_window.py   | 89 ++++++++++++++++++--------------
 electrum/invoices.py             | 13 +++--
 electrum/lnaddr.py               | 11 ++--
 4 files changed, 99 insertions(+), 79 deletions(-)

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]: