Browse Source

Multiple max spends functionality added (#7492)

this implements https://github.com/spesmilo/electrum/issues/7054
patch-4
Siddhant Chawla 3 years ago
committed by GitHub
parent
commit
65c3a892cf
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      electrum/commands.py
  2. 4
      electrum/gui/kivy/uix/screens.py
  3. 11
      electrum/gui/qt/main_window.py
  4. 8
      electrum/gui/qt/paytoedit.py
  5. 28
      electrum/util.py
  6. 40
      electrum/wallet.py

6
electrum/commands.py

@ -41,7 +41,7 @@ from typing import Optional, TYPE_CHECKING, Dict, List
from .import util, ecc
from .util import (bfh, bh2u, format_satoshis, json_decode, json_normalize,
is_hash256_str, is_hex_str, to_bytes)
is_hash256_str, is_hex_str, to_bytes, parse_max_spend)
from . import bitcoin
from .bitcoin import is_address, hash_160, COIN
from .bip32 import BIP32Node
@ -77,7 +77,7 @@ class NotSynchronizedException(Exception):
def satoshis_or_max(amount):
return satoshis(amount) if amount != '!' else '!'
return satoshis(amount) if not parse_max_spend(amount) else amount
def satoshis(amount):
# satoshi conversion must not be performed by the parser
@ -1354,7 +1354,7 @@ arg_types = {
'inputs': json_loads,
'outputs': json_loads,
'fee': lambda x: str(Decimal(x)) if x is not None else None,
'amount': lambda x: str(Decimal(x)) if x != '!' else '!',
'amount': lambda x: str(Decimal(x)) if not parse_max_spend(x) else x,
'locktime': int,
'addtransaction': eval_bool,
'fee_method': str,

4
electrum/gui/kivy/uix/screens.py

@ -16,7 +16,7 @@ from electrum.invoices import (PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATIO
from electrum import bitcoin, constants
from electrum.transaction import tx_from_any, PartialTxOutput
from electrum.util import (parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_bolt11_invoice,
InvoiceError, format_time)
InvoiceError, format_time, parse_max_spend)
from electrum.lnaddr import lndecode, LnInvoiceException
from electrum.logging import Logger
@ -371,7 +371,7 @@ class SendScreen(CScreen, Logger):
def _do_pay_onchain(self, invoice: OnchainInvoice) -> None:
outputs = invoice.outputs
amount = sum(map(lambda x: x.value, outputs)) if '!' not in [x.value for x in outputs] else '!'
amount = sum(map(lambda x: x.value, outputs)) if not any(parse_max_spend(x.value) for x in outputs) else '!'
coins = self.app.wallet.get_spendable_coins(None)
make_tx = lambda rbf: self.app.wallet.make_unsigned_transaction(coins=coins, outputs=outputs, rbf=rbf)
on_pay = lambda tx: self.app.protected(_('Send payment?'), self.send_tx, (tx, invoice))

11
electrum/gui/qt/main_window.py

@ -64,7 +64,7 @@ from electrum.util import (format_time,
InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds,
NoDynamicFeeEstimates, MultipleSpendMaxTxOutputs,
AddTransactionException, BITCOIN_BIP21_URI_SCHEME,
InvoiceError)
InvoiceError, parse_max_spend)
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,
@ -1709,11 +1709,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
fee=fee_est,
is_sweep=is_sweep)
output_values = [x.value for x in outputs]
if output_values.count('!') > 1:
self.show_error(_("More than one output set to spend max"))
return
output_value = '!' if '!' in output_values else sum(output_values)
if any(parse_max_spend(outval) for outval in output_values):
output_value = '!'
else:
output_value = sum(output_values)
conf_dlg = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=output_value, is_sweep=is_sweep)
if conf_dlg.not_enough_funds:
# Check if we had enough funds excluding fees,

8
electrum/gui/qt/paytoedit.py

@ -31,7 +31,7 @@ from typing import NamedTuple, Sequence, Optional, List, TYPE_CHECKING
from PyQt5.QtGui import QFontMetrics, QFont
from electrum import bitcoin
from electrum.util import bfh, maybe_extract_bolt11_invoice, BITCOIN_BIP21_URI_SCHEME
from electrum.util import bfh, maybe_extract_bolt11_invoice, BITCOIN_BIP21_URI_SCHEME, parse_max_spend
from electrum.transaction import PartialTxOutput
from electrum.bitcoin import opcodes, construct_script
from electrum.logging import Logger
@ -131,8 +131,8 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
x = x.strip()
if not x:
raise Exception("Amount is empty")
if x == '!':
return '!'
if parse_max_spend(x):
return x
p = pow(10, self.amount_edit.decimal_point())
try:
return int(p * Decimal(x))
@ -203,7 +203,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
idx=i, line_content=line.strip(), exc=e, is_multiline=True))
continue
outputs.append(output)
if output.value == '!':
if parse_max_spend(output.value):
is_max = True
else:
total += output.value

28
electrum/util.py

@ -108,6 +108,30 @@ def base_unit_name_to_decimal_point(unit_name: str) -> int:
except KeyError:
raise UnknownBaseUnit(unit_name) from None
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.
For example, to send 40% of all coins to address1, and 60% to address2:
```
address1, 2!
address2, 3!
```
"""
if not (isinstance(amt, str) and amt and amt[-1] == '!'):
return None
if amt == '!':
return 1
x = amt[:-1]
try:
x = int(x)
except ValueError:
return None
if x > 0:
return x
return None
class NotEnoughFunds(Exception):
def __str__(self):
@ -663,8 +687,8 @@ def format_satoshis(
) -> str:
if x is None:
return 'unknown'
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"
# lose redundant precision
x = Decimal(x).quantize(Decimal(10) ** (-precision))

40
electrum/wallet.py

@ -56,7 +56,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates,
WalletFileException, BitcoinException, MultipleSpendMaxTxOutputs,
InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
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
from .bitcoin import COIN, TYPE_ADDRESS
from .bitcoin import is_address, address_to_script, is_minikey, relayfee, dust_threshold
@ -754,10 +754,13 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
height=self.get_local_height()
if pr:
return OnchainInvoice.from_bip70_payreq(pr, height)
if '!' in (x.value for x in outputs):
amount = '!'
else:
amount = sum(x.value for x in outputs)
amount = 0
for x in outputs:
if parse_max_spend(x.value):
amount = '!'
break
else:
amount += x.value
timestamp = None
exp = None
if URI:
@ -863,7 +866,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
assert isinstance(invoice, OnchainInvoice)
invoice_amounts = defaultdict(int) # type: Dict[bytes, int] # scriptpubkey -> value_sats
for txo in invoice.outputs: # type: PartialTxOutput
invoice_amounts[txo.scriptpubkey] += 1 if txo.value == '!' else txo.value
invoice_amounts[txo.scriptpubkey] += 1 if parse_max_spend(txo.value) else txo.value
relevant_txs = []
with self.transaction_lock:
for invoice_scriptpubkey, invoice_amt in invoice_amounts.items():
@ -1333,12 +1336,13 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
outputs = copy.deepcopy(outputs)
# check outputs
i_max = None
i_max = []
i_max_sum = 0
for i, o in enumerate(outputs):
if o.value == '!':
if i_max is not None:
raise MultipleSpendMaxTxOutputs()
i_max = i
weight = parse_max_spend(o.value)
if weight:
i_max_sum += weight
i_max.append((weight,i))
if fee is None and self.config.fee_per_kb() is None:
raise NoDynamicFeeEstimates()
@ -1356,7 +1360,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
else:
raise Exception(f'Invalid argument fee: {fee}')
if i_max is None:
if len(i_max) == 0:
# Let the coin chooser select the coins to spend
coin_chooser = coinchooser.get_coin_chooser(self.config)
# If there is an unconfirmed RBF tx, merge with it
@ -1400,13 +1404,21 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
# note: Actually it might be the case that not all UTXOs from the wallet are
# being spent if the user manually selected UTXOs.
sendable = sum(map(lambda c: c.value_sats(), coins))
outputs[i_max].value = 0
for (_,i) in i_max:
outputs[i].value = 0
tx = PartialTransaction.from_io(list(coins), list(outputs))
fee = fee_estimator(tx.estimated_size())
amount = sendable - tx.output_value() - fee
if amount < 0:
raise NotEnoughFunds()
outputs[i_max].value = amount
distr_amount = 0
for (x,i) in i_max:
val = int((amount/i_max_sum)*x)
outputs[i].value = val
distr_amount += val
(x,i) = i_max[-1]
outputs[i].value += (amount - distr_amount)
tx = PartialTransaction.from_io(list(coins), list(outputs))
# Timelock tx to current height.

Loading…
Cancel
Save