Skip to content

Commit

Permalink
Merge pull request spesmilo#7234 from bitromortac/2104-invoice-amt-7184
Browse files Browse the repository at this point in the history
invoice: fail gracefully with large amount
  • Loading branch information
SomberNight authored May 6, 2021
2 parents 5dcafaf + 853e912 commit dd84acc
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 80 deletions.
65 changes: 36 additions & 29 deletions electrum/gui/kivy/uix/screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,21 +318,24 @@ def read_invoice(self):
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()
Expand Down Expand Up @@ -447,20 +450,24 @@ def new_request(self, lightning):
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)
Expand Down
89 changes: 49 additions & 40 deletions electrum/gui/qt/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
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,
Expand All @@ -78,7 +79,7 @@
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
Expand Down Expand Up @@ -1223,21 +1224,26 @@ def sign_payment_request(self, addr):
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)
Expand All @@ -1250,7 +1256,7 @@ def create_invoice(self, is_lightning):
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
Expand Down Expand Up @@ -1595,32 +1601,35 @@ def on_payment_failed(self, wallet, key, reason):
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()
Expand Down
13 changes: 6 additions & 7 deletions electrum/invoices.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

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
Expand Down Expand Up @@ -134,12 +134,12 @@ def get_amount_sat(self) -> Union[int, str]:
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':
Expand Down Expand Up @@ -173,9 +173,9 @@ def _validate_amount(self, attribute, value):
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:
Expand Down Expand Up @@ -231,4 +231,3 @@ def to_debug_json(self) -> Dict[str, Any]:
# 'tags': str(lnaddr.tags),
})
return d

11 changes: 8 additions & 3 deletions electrum/lnaddr.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
from .lnutil import LnFeatures


class LnAddressError(Exception):
pass


# BOLT #11:
#
# A writer MUST encode `amount` as a positive decimal integer with no
Expand Down Expand Up @@ -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):
Expand All @@ -286,16 +291,16 @@ def amount(self) -> Optional[Decimal]:
@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]:
Expand Down
4 changes: 3 additions & 1 deletion electrum/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}")
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit dd84acc

Please sign in to comment.