Multiple max spends functionality added (#7492)
this implements https://github.com/spesmilo/electrum/issues/7054
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user