From e123774ea8d2b9f30c958c7710cfe797bb7cd4b2 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 31 Dec 2016 16:29:18 +0100 Subject: [PATCH] Improve 'send all coins' function: * do use coin chooser when sending all coins (fixes #2000) * allow "!" syntax for multiple outputs (fixes #1698) --- gui/kivy/main_window.py | 4 +++- gui/qt/main_window.py | 30 +++++++++++--------------- gui/qt/paytoedit.py | 24 +++++++++++++++------ lib/commands.py | 6 +----- lib/wallet.py | 48 ++++++++++++++++++++++------------------- 5 files changed, 59 insertions(+), 53 deletions(-) diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index 1f99bc9c6..eb5dc8fd8 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -572,7 +572,9 @@ class ElectrumWindow(App): def get_max_amount(self): inputs = self.wallet.get_spendable_coins(None) addr = str(self.send_screen.screen.address) or self.wallet.dummy_address() - amount, fee = self.wallet.get_max_amount(self.electrum_config, inputs, (TYPE_ADDRESS, addr), None) + outputs = [(TYPE_ADDRESS, addr, '!')] + tx = self.wallet.make_unsigned_transaction(inputs, outputs, self.electrum_config) + amount = tx.output_value() return format_satoshis_plain(amount, self.decimal_point()) def format_amount(self, x, is_diff=False, whitespaces=False): diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index ce7e78607..920fed73e 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -994,27 +994,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): vbox.addWidget(self.invoices_label) vbox.addWidget(self.invoice_list) vbox.setStretchFactor(self.invoice_list, 1000) - # Defer this until grid is parented to avoid ugly flash during startup self.update_fee_edit() - run_hook('create_send_tab', grid) return w - def spend_max(self): - inputs = self.get_coins() - sendable = sum(map(lambda x:x['value'], inputs)) - fee = self.fee_e.get_amount() if self.fee_e.isModified() else None - r = self.get_payto_or_dummy() - amount, fee = self.wallet.get_max_amount(self.config, inputs, r, fee) - if not self.fee_e.isModified(): - self.fee_e.setAmount(fee) - self.amount_e.setAmount(amount) - self.not_enough_funds = (fee + amount > sendable) - # emit signal for fiat_amount update - self.amount_e.textEdited.emit("") self.is_max = True + self.do_update_fee() def reset_max(self): self.is_max = False @@ -1034,14 +1021,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): ''' freeze_fee = (self.fee_e.isModified() and (self.fee_e.text() or self.fee_e.hasFocus())) - amount = self.amount_e.get_amount() + amount = '!' if self.is_max else self.amount_e.get_amount() if amount is None: if not freeze_fee: self.fee_e.setAmount(None) self.not_enough_funds = False else: fee = self.fee_e.get_amount() if freeze_fee else None - outputs = self.payto_e.get_outputs() + outputs = self.payto_e.get_outputs(self.is_max) if not outputs: _type, addr = self.get_payto_or_dummy() outputs = [(_type, addr, amount)] @@ -1054,6 +1041,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): fee = None if self.not_enough_funds else self.wallet.get_tx_fee(tx) self.fee_e.setAmount(fee) + if self.is_max: + amount = tx.output_value() + self.amount_e.setAmount(amount) + self.amount_e.textEdited.emit("") + + def update_fee_edit(self): b = self.config.get('dynamic_fees', True) self.fee_slider.setVisible(b) @@ -1132,7 +1125,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if errors: self.show_warning(_("Invalid Lines found:") + "\n\n" + '\n'.join([ _("Line #") + str(x[0]+1) + ": " + x[1] for x in errors])) return - outputs = self.payto_e.get_outputs() + outputs = self.payto_e.get_outputs(self.is_max) if self.payto_e.is_alias and self.payto_e.validated is False: alias = self.payto_e.toPlainText() @@ -1174,7 +1167,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if not r: return outputs, fee, tx_desc, coins = r - amount = sum(map(lambda x:x[2], outputs)) try: tx = self.wallet.make_unsigned_transaction(coins, outputs, self.config, fee) except NotEnoughFunds: @@ -1185,6 +1177,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.show_message(str(e)) return + amount = tx.output_value() if self.is_max else sum(map(lambda x:x[2], outputs)) + use_rbf = self.rbf_checkbox.isChecked() if use_rbf: tx.set_sequence(0) diff --git a/gui/qt/paytoedit.py b/gui/qt/paytoedit.py index 5d0082a1f..0153217bf 100644 --- a/gui/qt/paytoedit.py +++ b/gui/qt/paytoedit.py @@ -98,6 +98,8 @@ class PayToEdit(ScanQRTextEdit): return script def parse_amount(self, x): + if x.strip() == '!': + return '!' p = pow(10, self.amount_edit.decimal_point()) return int(p * Decimal(x.strip())) @@ -138,12 +140,19 @@ class PayToEdit(ScanQRTextEdit): continue outputs.append((_type, to_address, amount)) - total += amount + if amount == '!': + self.win.is_max = True + else: + total += amount self.outputs = outputs self.payto_address = None - self.amount_edit.setAmount(total if outputs else None) - self.win.lock_amount(total or len(lines)>1) + + if self.win.is_max: + self.win.do_update_fee() + else: + self.amount_edit.setAmount(total if outputs else None) + self.win.lock_amount(total or len(lines)>1) def get_errors(self): return self.errors @@ -151,12 +160,13 @@ class PayToEdit(ScanQRTextEdit): def get_recipient(self): return self.payto_address - def get_outputs(self): + def get_outputs(self, is_max): if self.payto_address: - try: + if is_max: + amount = '!' + else: amount = self.amount_edit.get_amount() - except: - amount = None + _type, addr = self.payto_address self.outputs = [(_type, addr, amount)] diff --git a/lib/commands.py b/lib/commands.py index d130b4f8b..347f9497c 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -404,11 +404,7 @@ class Commands: final_outputs = [] for address, amount in outputs: address = self._resolver(address) - if amount == '!': - assert len(outputs) == 1 - inputs = self.wallet.get_spendable_coins(domain) - amount, fee = self.wallet.get_max_amount(self.config, inputs, (TYPE_ADDRESS, address), fee) - else: + if amount != '!': amount = int(COIN*Decimal(amount)) final_outputs.append((TYPE_ADDRESS, address, amount)) diff --git a/lib/wallet.py b/lib/wallet.py index 040a459d9..c8e387780 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -551,18 +551,6 @@ class Abstract_Wallet(PrintError): def dummy_address(self): return self.get_receiving_addresses()[0] - def get_max_amount(self, config, inputs, recipient, fee): - sendable = sum(map(lambda x:x['value'], inputs)) - if fee is None: - for i in inputs: - self.add_input_info(i) - _type, addr = recipient - outputs = [(_type, addr, sendable)] - dummy_tx = Transaction.from_io(inputs, outputs) - fee = self.estimate_fee(config, dummy_tx.estimated_size()) - amount = max(0, sendable - fee) - return amount, fee - def get_addresses(self): out = [] out += self.get_receiving_addresses() @@ -819,18 +807,24 @@ class Abstract_Wallet(PrintError): # this method can be overloaded return tx.get_fee() - def make_unsigned_transaction(self, coins, outputs, config, fixed_fee=None, change_addr=None): + def make_unsigned_transaction(self, inputs, outputs, config, fixed_fee=None, change_addr=None): # check outputs - for type, data, value in outputs: - if type == TYPE_ADDRESS: + i_max = None + for i, o in enumerate(outputs): + _type, data, value = o + if _type == TYPE_ADDRESS: if not is_address(data): raise BaseException("Invalid bitcoin address:" + data) + if value == '!': + if i_max is not None: + raise BaseException("More than one output set to spend max") + i_max = i # Avoid index-out-of-range with coins[0] below - if not coins: + if not inputs: raise NotEnoughFunds() - for item in coins: + for item in inputs: self.add_input_info(item) # change address @@ -855,11 +849,21 @@ class Abstract_Wallet(PrintError): else: fee_estimator = lambda size: fixed_fee - # Let the coin chooser select the coins to spend - max_change = self.max_change_outputs if self.multiple_change else 1 - coin_chooser = coinchooser.get_coin_chooser(config) - tx = coin_chooser.make_tx(coins, outputs, change_addrs[:max_change], - fee_estimator, self.dust_threshold()) + if i_max is None: + # Let the coin chooser select the coins to spend + max_change = self.max_change_outputs if self.multiple_change else 1 + coin_chooser = coinchooser.get_coin_chooser(config) + tx = coin_chooser.make_tx(inputs, outputs, change_addrs[:max_change], + fee_estimator, self.dust_threshold()) + else: + sendable = sum(map(lambda x:x['value'], inputs)) + _type, data, value = outputs[i_max] + outputs[i_max] = (_type, data, 0) + tx = Transaction.from_io(inputs, outputs[:]) + fee = fee_estimator(tx.estimated_size()) + amount = max(0, sendable - tx.output_value() - fee) + outputs[i_max] = (_type, data, amount) + tx = Transaction.from_io(inputs, outputs[:]) # Sort the inputs and outputs deterministically tx.BIP_LI01_sort()