diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index 68b9f4edf..0ebe3d075 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -18,7 +18,7 @@ from electrum.plugin import run_hook from electrum import util from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter, format_satoshis, format_satoshis_plain, format_fee_satoshis, - maybe_extract_bolt11_invoice) + maybe_extract_bolt11_invoice, parse_max_spend) from electrum.invoices import PR_PAID, PR_FAILED from electrum import blockchain from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed @@ -988,8 +988,8 @@ class ElectrumWindow(App, Logger): def format_amount_and_units(self, x) -> str: if x is None: return 'none' - if x == '!': - return 'max' + if parse_max_spend(x): + return f'max({x})' # FIXME this is using format_satoshis_plain instead of config.format_amount # as we sometimes convert the returned string back to numbers, # via self.get_amount()... the need for converting back should be removed diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index b12e734d8..0e286be5b 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -62,7 +62,7 @@ from electrum.util import (format_time, UserFacingException, get_new_wallet_name, send_exception_to_crash_reporter, InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds, - NoDynamicFeeEstimates, MultipleSpendMaxTxOutputs, + NoDynamicFeeEstimates, AddTransactionException, BITCOIN_BIP21_URI_SCHEME, InvoiceError, parse_max_spend) from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING, Invoice @@ -1351,8 +1351,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.amount_e = BTCAmountEdit(self.get_decimal_point) self.payto_e = PayToEdit(self) self.payto_e.addPasteButton(self.app) - msg = _('Recipient of the funds.') + '\n\n'\ - + _('You may enter a Bitcoin address, a label from your list of contacts (a list of completions will be proposed), or an alias (email-like address that forwards to a Bitcoin address)') + msg = (_("Recipient of the funds.") + "\n\n" + + _("You may enter a Bitcoin address, a label from your list of contacts " + "(a list of completions will be proposed), " + "or an alias (email-like address that forwards to a Bitcoin address)") + ". " + + _("Lightning invoices are also supported.") + "\n\n" + + _("You can also pay to many outputs in a single transaction, " + "specifying one output per line.") + "\n" + _("Format: address, amount") + "\n" + + _("To set the amount to 'max', use the '!' special character.") + "\n" + + _("Integers weights can also be used in conjunction with '!', " + "e.g. set one amount to '2!' and another to '3!' to split your coins 40-60.")) payto_label = HelpLabel(_('Pay to'), msg) grid.addWidget(payto_label, 1, 0) grid.addWidget(self.payto_e, 1, 1, 1, -1) @@ -1451,10 +1459,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): # Check if we had enough funds excluding fees, # if so, still provide opportunity to set lower fees. tx = make_tx(0) - except MultipleSpendMaxTxOutputs as e: - self.max_button.setChecked(False) - self.show_error(str(e)) - return except NotEnoughFunds as e: self.max_button.setChecked(False) text = self.get_text_not_enough_funds_mentioning_frozen() diff --git a/electrum/invoices.py b/electrum/invoices.py index a3e769941..a2a53dc1d 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -136,7 +136,7 @@ class OnchainInvoice(Invoice): if not (0 <= value <= TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN): raise InvoiceError(f"amount is out-of-bounds: {value!r} sat") elif isinstance(value, str): - if value != "!": + if value != '!': raise InvoiceError(f"unexpected amount: {value!r}") else: raise InvoiceError(f"unexpected amount: {value!r}") diff --git a/electrum/transaction.py b/electrum/transaction.py index d4c977367..62e853a7b 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -42,7 +42,7 @@ import copy from . import ecc, bitcoin, constants, segwit_addr, bip32 from .bip32 import BIP32Node -from .util import profiler, to_bytes, bh2u, bfh, chunks, is_hex_str +from .util import profiler, to_bytes, bh2u, bfh, chunks, is_hex_str, parse_max_spend from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160, hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr, var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN, @@ -109,7 +109,9 @@ class TxOutput: def __init__(self, *, scriptpubkey: bytes, value: Union[int, str]): self.scriptpubkey = scriptpubkey - self.value = value # str when the output is set to max: '!' # in satoshis + if not (isinstance(value, int) or parse_max_spend(value) is not None): + raise ValueError(f"bad txout value: {value!r}") + self.value = value # int in satoshis; or spend-max-like str @classmethod def from_address_and_value(cls, address: str, value: Union[int, str]) -> Union['TxOutput', 'PartialTxOutput']: diff --git a/electrum/util.py b/electrum/util.py index 9142dd895..9a40d4fe4 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -111,6 +111,7 @@ def base_unit_name_to_decimal_point(unit_name: str) -> int: def parse_max_spend(amt: Any) -> Optional[int]: """Checks if given amount is "spend-max"-like. Returns None or the positive integer weight for "max". Never raises. + When creating invoices and on-chain txs, the user can specify to send "max". This is done by setting the amount to '!'. Splitting max between multiple tx outputs is also possible, and custom weights (positive ints) can also be used. @@ -143,11 +144,6 @@ class NoDynamicFeeEstimates(Exception): return _('Dynamic fee estimates not available') -class MultipleSpendMaxTxOutputs(Exception): - def __str__(self): - return _('At most one output can be set to spend max') - - class InvalidPassword(Exception): def __str__(self): return _("Incorrect password") @@ -658,8 +654,8 @@ def format_satoshis_plain( ) -> str: """Display a satoshi amount scaled. Always uses a '.' as a decimal point and has no thousands separator""" - if x == '!': - return 'max' + if parse_max_spend(x): + return f'max({x})' assert isinstance(x, (int, float, Decimal)), f"{x!r} should be a number" scale_factor = pow(10, decimal_point) return "{:.8f}".format(Decimal(x) / scale_factor).rstrip('0').rstrip('.') @@ -688,7 +684,7 @@ def format_satoshis( if x is None: return 'unknown' if parse_max_spend(x): - return f'max ({x}) ' + return f'max({x})' assert isinstance(x, (int, float, Decimal)), f"{x!r} should be a number" # lose redundant precision x = Decimal(x).quantize(Decimal(10) ** (-precision)) diff --git a/electrum/wallet.py b/electrum/wallet.py index dd4597553..6a2effac0 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -54,7 +54,7 @@ from .crypto import sha256 from . import util from .util import (NotEnoughFunds, UserCancelled, profiler, format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates, - WalletFileException, BitcoinException, MultipleSpendMaxTxOutputs, + WalletFileException, BitcoinException, InvalidPassword, format_time, timestamp_to_datetime, Satoshis, Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex, parse_max_spend) from .simple_config import SimpleConfig, FEE_RATIO_HIGH_WARNING, FEERATE_WARNING_HIGH_FEE @@ -1342,7 +1342,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC): weight = parse_max_spend(o.value) if weight: i_max_sum += weight - i_max.append((weight,i)) + i_max.append((weight, i)) if fee is None and self.config.fee_per_kb() is None: raise NoDynamicFeeEstimates() @@ -1412,8 +1412,8 @@ class Abstract_Wallet(AddressSynchronizer, ABC): if amount < 0: raise NotEnoughFunds() distr_amount = 0 - for (x,i) in i_max: - val = int((amount/i_max_sum)*x) + for (weight, i) in i_max: + val = int((amount/i_max_sum) * weight) outputs[i].value = val distr_amount += val