tMerge branch 'feez' - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 59ed5932a859fd807232960404764b8686355830 DIR parent 34a550864d21e286aa8cec20c112bc39a445c30e HTML Author: ThomasV <thomasv@electrum.org> Date: Mon, 9 Jan 2017 12:39:42 +0100 Merge branch 'feez' Diffstat: A gui/qt/fee_slider.py | 43 ++++++++++++++++++++++++++++++ M gui/qt/main_window.py | 80 +++++++++++-------------------- M lib/bitcoin.py | 4 +++- M lib/network.py | 32 ++++--------------------------- M lib/simple_config.py | 41 +++++++++++++++++++++++++++++++ M lib/wallet.py | 16 +++------------- 6 files changed, 122 insertions(+), 94 deletions(-) --- DIR diff --git a/gui/qt/fee_slider.py b/gui/qt/fee_slider.py t@@ -0,0 +1,43 @@ +from electrum.i18n import _ + +import PyQt4 +from PyQt4.QtGui import * +from PyQt4.QtCore import * +import PyQt4.QtCore as QtCore + +class FeeSlider(QSlider): + + def __init__(self, window, config, callback): + QSlider.__init__(self, Qt.Horizontal) + self.config = config + self.fee_step = self.config.max_fee_rate() / 10 + self.window = window + self.callback = callback + self.setToolTip('') + self.update() + self.valueChanged.connect(self.moved) + + def moved(self, pos): + from electrum.util import fee_levels + dyn = self.config.is_dynfee() + fee_rate = self.config.dynfee(pos) if dyn else pos * self.fee_step + rate_str = self.window.format_amount(fee_rate) + ' ' + self.window.base_unit() + '/kB' + if dyn: + tooltip = fee_levels[pos] + '\n' + rate_str + else: + tooltip = rate_str + if self.config.has_fee_estimates(): + i = self.config.reverse_dynfee(fee_rate) + tooltip += '\n' + (_('low fee') if i < 0 else 'Within %d blocks'%i) + QToolTip.showText(QCursor.pos(), tooltip, self) + self.callback(dyn, pos, fee_rate) + + def update(self): + if self.config.is_dynfee(): + self.setRange(0, 4) + self.setValue(self.config.get('fee_level', 2)) + else: + self.setRange(1, 10) + fee_rate = self.config.fee_per_kb() + pos = min(fee_rate / self.fee_step, 10) + self.setValue(pos) DIR diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py t@@ -59,7 +59,7 @@ from network_dialog import NetworkDialog from qrcodewidget import QRCodeWidget, QRDialog from qrtextedit import ShowQRTextEdit from transaction_dialog import show_transaction - +from fee_slider import FeeSlider from electrum import ELECTRUM_VERSION t@@ -161,7 +161,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if self.network: self.connect(self, QtCore.SIGNAL('network'), self.on_network_qt) interests = ['updated', 'new_transaction', 'status', - 'banner', 'verified'] + 'banner', 'verified', 'fee'] # To avoid leaking references to "self" that prevent the # window from being GC-ed when closed, callbacks should be # methods of this class only, and specifically not be t@@ -169,12 +169,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.network.register_callback(self.on_network, interests) # set initial message self.console.showMessage(self.network.banner) - self.network.register_callback(self.on_quotes, ['on_quotes']) self.network.register_callback(self.on_history, ['on_history']) self.connect(self, SIGNAL('new_fx_quotes'), self.on_fx_quotes) self.connect(self, SIGNAL('new_fx_history'), self.on_fx_history) + # update fee slider in case we missed the callback + self.fee_slider.update() self.load_wallet(wallet) self.connect_slots(gui_object.timer) self.fetch_alias() t@@ -259,7 +260,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.need_update.set() elif event == 'new_transaction': self.tx_notifications.append(args[0]) - elif event in ['status', 'banner', 'verified']: + elif event in ['status', 'banner', 'verified', 'fee']: # Handle in GUI thread self.emit(QtCore.SIGNAL('network'), event, *args) else: t@@ -273,6 +274,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.console.showMessage(args[0]) elif event == 'verified': self.history_list.update_item(*args) + elif event == 'fee': + if self.config.is_dynfee(): + self.fee_slider.update() else: self.print_error("unexpected network_qt signal:", event, args) t@@ -996,13 +1000,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): + _('A suggested fee is automatically added to this field. You may override it. The suggested fee increases with the size of the transaction.') self.fee_e_label = HelpLabel(_('Fee'), msg) - fee_cb = lambda x: self.spend_max() if self.is_max else self.update_fee() - self.fee_slider = self.create_fee_slider(self, fee_cb) - self.fee_slider.setValue(self.config.get('fee_level', 2)) + def fee_cb(dyn, pos, fee_rate): + if dyn: + self.config.set_key('fee_level', pos, False) + else: + self.config.set_key('fee_per_kb', fee_rate, False) + self.spend_max() if self.is_max else self.update_fee() + + self.fee_slider = FeeSlider(self, self.config, fee_cb) self.fee_slider.setFixedWidth(140) self.fee_e = BTCAmountEdit(self.get_decimal_point) - self.fee_e.setVisible(self.config.get('show_fee', False)) + if not self.config.get('show_fee', False): + self.fee_e.setVisible(False) self.fee_e.textEdited.connect(self.update_fee) # This is so that when the user blanks the fee and moves on, # we go back to auto-calculate mode and put a fee back. t@@ -1275,9 +1285,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if extra_fee: msg.append( _("Additional fees") + ": " + self.format_amount_and_units(extra_fee) ) - confirm_rate = self.config.get('confirm_fee', 200000) + confirm_rate = 2 * self.config.max_fee_rate() if tx.get_fee() > confirm_rate * tx.estimated_size() / 1000: - msg.append(_('Warning')+ ': ' + _("The fee for this transaction seems unusually high.")) + msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high.")) if self.wallet.has_password(): msg.append("") t@@ -2370,28 +2380,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): nz.valueChanged.connect(on_nz) gui_widgets.append((nz_label, nz)) - dynfee_cb = QCheckBox(_('Use static fees')) - dynfee_cb.setChecked(not self.config.get('dynamic_fees', True)) - dynfee_cb.setToolTip(_("Do not use fees recommended by the server.")) - fee_e = BTCkBEdit(self.get_decimal_point) - def on_fee(is_done): - if self.config.get('dynamic_fees', True): - return - v = fee_e.get_amount() or 0 - self.config.set_key('fee_per_kb', v, is_done) - self.update_fee() - def update_feeperkb(): - fee_e.setAmount(self.config.get('fee_per_kb', bitcoin.RECOMMENDED_FEE)) - b = self.config.get('dynamic_fees', True) - fee_e.setEnabled(not b) + dynfee_cb = QCheckBox(_('Use dynamic fees')) + dynfee_cb.setChecked(self.config.get('dynamic_fees', True)) + dynfee_cb.setToolTip(_("Use fees recommended by the server.")) def on_dynfee(x): - self.config.set_key('dynamic_fees', x != Qt.Checked) - update_feeperkb() - fee_e.editingFinished.connect(lambda: on_fee(True)) - fee_e.textEdited.connect(lambda: on_fee(False)) + self.config.set_key('dynamic_fees', x == Qt.Checked) + self.fee_slider.update() dynfee_cb.stateChanged.connect(on_dynfee) - update_feeperkb() - fee_widgets.append((dynfee_cb, fee_e)) + fee_widgets.append((dynfee_cb, None)) feebox_cb = QCheckBox(_('Edit fees manually')) feebox_cb.setChecked(self.config.get('show_fee', False)) t@@ -2767,25 +2763,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): vbox.addLayout(Buttons(CloseButton(d))) d.exec_() - def create_fee_slider(self, parent, callback): - from electrum.util import fee_levels - from electrum.bitcoin import FEE_STEP, RECOMMENDED_FEE - fee_slider = QSlider(Qt.Horizontal, parent) - def slider_moved(): - pos = fee_slider.sliderPosition() - self.config.set_key('fee_level', pos, False) - is_dyn = self.config.get('dynamic_fees') and self.network and self.network.dynfee(pos) - fee_rate = self.wallet.fee_per_kb(self.config) - tooltip = fee_levels[pos] + '\n' if is_dyn else '' - tooltip += self.format_amount(fee_rate) + ' ' + self.base_unit() + '/kB' - QToolTip.showText(QCursor.pos(), tooltip, self.fee_slider) - callback(fee_rate) - - fee_slider.setRange(0, 4) - fee_slider.setToolTip('') - fee_slider.setValue(self.config.get('fee_level', 2)) - fee_slider.valueChanged.connect(slider_moved) - return fee_slider def bump_fee_dialog(self, tx): is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) t@@ -2799,12 +2776,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): fee_e.setAmount(fee * 1.5) vbox.addWidget(fee_e) - def on_rate(fee_rate): + def on_rate(dyn, pos, fee_rate): fee = fee_rate * tx_size / 1000 fee_e.setAmount(fee) - fee_slider = self.create_fee_slider(d, on_rate) + fee_slider = FeeSlider(self, self.config, on_rate) vbox.addWidget(fee_slider) - cb = QCheckBox(_('Final')) vbox.addWidget(cb) vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) DIR diff --git a/lib/bitcoin.py b/lib/bitcoin.py t@@ -58,7 +58,9 @@ def set_testnet(): ################################## transactions FEE_STEP = 10000 -RECOMMENDED_FEE = 50000 +MAX_FEE_RATE = 100000 +FEE_TARGETS = [25, 10, 5, 2] + COINBASE_MATURITY = 100 COIN = 100000000 DIR diff --git a/lib/network.py b/lib/network.py t@@ -37,13 +37,12 @@ import socket import json import util +import bitcoin from bitcoin import * from interface import Connection, Interface from blockchain import Blockchain from version import ELECTRUM_VERSION, PROTOCOL_VERSION -FEE_TARGETS = [25, 10, 5, 2] - DEFAULT_PORTS = {'t':'50001', 's':'50002'} DEFAULT_SERVERS = { t@@ -202,7 +201,6 @@ class Network(util.DaemonThread): self.banner = '' self.donation_address = '' - self.fee_estimates = {} self.relay_fee = None self.heights = {} self.merkle_roots = {} t@@ -326,7 +324,7 @@ class Network(util.DaemonThread): self.queue_request('server.banner', []) self.queue_request('server.donation_address', []) self.queue_request('server.peers.subscribe', []) - for i in FEE_TARGETS: + for i in bitcoin.FEE_TARGETS: self.queue_request('blockchain.estimatefee', [i]) self.queue_request('blockchain.relayfee', []) t@@ -336,7 +334,7 @@ class Network(util.DaemonThread): elif key == 'banner': value = self.banner elif key == 'fee': - value = self.fee_estimates + value = self.config.fee_estimates elif key == 'updated': value = (self.get_local_height(), self.get_server_height()) elif key == 'servers': t@@ -345,28 +343,6 @@ class Network(util.DaemonThread): value = self.get_interfaces() return value - def dynfee(self, i): - from bitcoin import RECOMMENDED_FEE - if i < 4: - j = FEE_TARGETS[i] - fee = self.fee_estimates.get(j) - else: - assert i == 4 - fee = self.fee_estimates.get(2) - if fee is not None: - fee += fee/2 - if fee is not None: - fee = min(10*RECOMMENDED_FEE, fee) - return fee - - def reverse_dynfee(self, fee_per_kb): - import operator - dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), self.fee_estimates.items()) - min_target, min_value = min(dist, key=operator.itemgetter(1)) - if fee_per_kb < self.fee_estimates.get(25)/2: - min_target = -1 - return min_target - def notify(self, key): if key in ['status', 'updated']: self.trigger_callback(key) t@@ -550,7 +526,7 @@ class Network(util.DaemonThread): elif method == 'blockchain.estimatefee': if error is None: i = params[0] - self.fee_estimates[i] = int(result * COIN) + self.config.fee_estimates[i] = int(result * COIN) self.notify('fee') elif method == 'blockchain.relayfee': if error is None: DIR diff --git a/lib/simple_config.py b/lib/simple_config.py t@@ -6,6 +6,8 @@ import os from copy import deepcopy from util import user_dir, print_error, print_msg, print_stderr, PrintError +from bitcoin import MAX_FEE_RATE, FEE_TARGETS + SYSTEM_CONFIG_PATH = "/etc/electrum.conf" config = None t@@ -40,6 +42,8 @@ class SimpleConfig(PrintError): # a thread-safe way. self.lock = threading.RLock() + self.fee_estimates = {} + # The following two functions are there for dependency injection when # testing. if read_system_config_function is None: t@@ -190,6 +194,43 @@ class SimpleConfig(PrintError): path = wallet.storage.path self.set_key('gui_last_wallet', path) + def max_fee_rate(self): + return self.get('max_fee_rate', MAX_FEE_RATE) + + def dynfee(self, i): + if i < 4: + j = FEE_TARGETS[i] + fee = self.fee_estimates.get(j) + else: + assert i == 4 + fee = self.fee_estimates.get(2) + if fee is not None: + fee += fee/2 + if fee is not None: + fee = min(5*MAX_FEE_RATE, fee) + return fee + + def reverse_dynfee(self, fee_per_kb): + import operator + dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), self.fee_estimates.items()) + min_target, min_value = min(dist, key=operator.itemgetter(1)) + if fee_per_kb < self.fee_estimates.get(25)/2: + min_target = -1 + return min_target + + def has_fee_estimates(self): + return len(self.fee_estimates)==4 + + def is_dynfee(self): + return self.get('dynamic_fees') and self.has_fee_estimates() + + def fee_per_kb(self): + dyn = self.is_dynfee() + if dyn: + fee_rate = self.dynfee(self.get('fee_level', 2)) + else: + fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2) + return fee_rate def read_system_config(path=SYSTEM_CONFIG_PATH): """Parse and return the system config settings in /etc/electrum.conf.""" DIR diff --git a/lib/wallet.py b/lib/wallet.py t@@ -754,16 +754,6 @@ class Abstract_Wallet(PrintError): return ', '.join(labels) return '' - def fee_per_kb(self, config): - b = config.get('dynamic_fees', True) - i = config.get('fee_level', 2) - if b and self.network and self.network.dynfee(i): - return self.network.dynfee(i) - else: - fee_per_kb = config.get('fee_per_kb', RECOMMENDED_FEE) - coeff = {0:0.3, 1:0.5, 2:1, 3:1.5, 4:2} - return fee_per_kb * coeff[i] - def get_tx_status(self, tx_hash, height, conf, timestamp): from util import format_time if conf == 0: t@@ -772,9 +762,9 @@ class Abstract_Wallet(PrintError): return 3, 'unknown' is_final = tx and tx.is_final() fee = self.tx_fees.get(tx_hash) - if fee and self.network and self.network.dynfee(0): + if fee and self.network and self.network.config.has_fee_estimates(): size = len(tx.raw)/2 - low_fee = int(self.network.dynfee(0)*size/1000) + low_fee = int(self.network.config.dynfee(0)*size/1000) is_lowfee = fee < low_fee * 0.5 else: is_lowfee = False t@@ -873,7 +863,7 @@ class Abstract_Wallet(PrintError): return tx def estimate_fee(self, config, size): - fee = int(self.fee_per_kb(config) * size / 1000.) + fee = int(config.fee_per_kb() * size / 1000.) return fee def mktx(self, outputs, password, config, fee=None, change_addr=None, domain=None):