Skip to content

Commit

Permalink
Add memory pool based fee estimates
Browse files Browse the repository at this point in the history
 - fee estimates can use ETA or mempool
 - require protocol version 1.2
 - remove fee_unit preference
  • Loading branch information
ecdsa committed Feb 5, 2018
1 parent 2c619ec commit c3f3843
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 103 deletions.
57 changes: 34 additions & 23 deletions gui/kivy/uix/dialogs/fee_dialog.py
Expand Up @@ -32,7 +32,15 @@
text: _('Dynamic Fees')
CheckBox:
id: dynfees
on_active: root.on_checkbox(self.active)
on_active: root.on_dynfees(self.active)
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
Label:
text: _('Use mempool')
CheckBox:
id: mempool
on_active: root.on_mempool(self.active)
Widget:
size_hint: 1, 1
BoxLayout:
Expand Down Expand Up @@ -60,7 +68,9 @@ def __init__(self, app, config, callback):
self.config = config
self.fee_rate = self.config.fee_per_kb()
self.callback = callback
self.mempool = self.config.get('mempool_fees', False)
self.dynfees = self.config.get('dynamic_fees', True)
self.ids.mempool.active = self.mempool
self.ids.dynfees.active = self.dynfees
self.update_slider()
self.update_text()
Expand All @@ -71,42 +81,43 @@ def update_text(self):

def update_slider(self):
slider = self.ids.slider
if self.dynfees:
slider.range = (0, 4)
slider.step = 1
slider.value = self.config.get('fee_level', 2)
else:
slider.range = (0, 9)
slider.step = 1
slider.value = self.config.static_fee_index(self.fee_rate)
maxp, pos, fee_rate = self.config.get_fee_slider(self.dynfees, self.mempool)
slider.range = (0, maxp)
slider.step = 1
slider.value = pos

def get_fee_text(self, value):
if self.ids.dynfees.active:
tooltip = fee_levels[value]
if self.config.has_fee_estimates():
dynfee = self.config.dynfee(value)
tooltip += '\n' + (self.app.format_amount_and_units(dynfee)) + '/kB'
def get_fee_text(self, pos):
dyn = self.dynfees
mempool = self.mempool
if dyn:
fee_rate = self.config.depth_to_fee(pos) if mempool else self.config.eta_to_fee(pos)
else:
fee_rate = self.config.static_fee(value)
tooltip = self.app.format_amount_and_units(fee_rate) + '/kB'
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)
return tooltip
fee_rate = self.config.static_fee(pos)
target, tooltip = self.config.get_fee_text(pos, dyn, mempool, fee_rate)
return target

def on_ok(self):
value = int(self.ids.slider.value)
self.config.set_key('dynamic_fees', self.dynfees, False)
self.config.set_key('mempool_fees', self.mempool, False)
if self.dynfees:
self.config.set_key('fee_level', value, True)
if self.mempool:
self.config.set_key('depth_level', value, True)
else:
self.config.set_key('fee_level', value, True)
else:
self.config.set_key('fee_per_kb', self.config.static_fee(value), True)
self.callback()

def on_slider(self, value):
self.update_text()

def on_checkbox(self, b):
def on_dynfees(self, b):
self.dynfees = b
self.update_slider()
self.update_text()

def on_mempool(self, b):
self.mempool = b
self.update_slider()
self.update_text()
5 changes: 1 addition & 4 deletions gui/kivy/uix/dialogs/settings.py
Expand Up @@ -204,10 +204,7 @@ def callback(status):
d.open()

def fee_status(self):
if self.config.get('dynamic_fees', True):
return fee_levels[self.config.get('fee_level', 2)]
else:
return self.app.format_amount_and_units(self.config.fee_per_kb()) + '/kB'
return self.config.get_fee_status()

def fee_dialog(self, label, dt):
if self._fee_dialog is None:
Expand Down
7 changes: 1 addition & 6 deletions gui/qt/amountedit.py
Expand Up @@ -106,12 +106,7 @@ def setAmount(self, amount):

class FeerateEdit(BTCAmountEdit):
def _base_unit(self):
p = self.decimal_point()
if p == 2:
return 'mBTC/kB'
if p == 0:
return 'sat/byte'
raise Exception('Unknown base unit')
return 'sat/byte'

def get_amount(self):
sat_per_byte_amount = BTCAmountEdit.get_amount(self)
Expand Down
34 changes: 11 additions & 23 deletions gui/qt/fee_slider.py
@@ -1,6 +1,4 @@

from electrum.i18n import _

from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import QSlider, QToolTip
Expand All @@ -22,37 +20,27 @@ def __init__(self, window, config, callback):

def moved(self, pos):
with self.lock:
fee_rate = self.config.dynfee(pos) if self.dyn else self.config.static_fee(pos)
if self.dyn:
fee_rate = self.config.depth_to_fee(pos) if self.config.get('mempool_fees') else self.config.eta_to_fee(pos)
else:
fee_rate = self.config.static_fee(pos)
tooltip = self.get_tooltip(pos, fee_rate)
QToolTip.showText(QCursor.pos(), tooltip, self)
self.setToolTip(tooltip)
self.callback(self.dyn, pos, fee_rate)

def get_tooltip(self, pos, fee_rate):
from electrum.util import fee_levels
rate_str = self.window.format_fee_rate(fee_rate) if fee_rate else _('unknown')
if self.dyn:
tooltip = fee_levels[pos] + '\n' + rate_str
else:
tooltip = 'Fixed rate: ' + 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)
return tooltip
mempool = self.config.get('mempool_fees')
text, tooltip = self.config.get_fee_text(pos, self.dyn, mempool, fee_rate)
return text + '\n' + tooltip

def update(self):
with self.lock:
self.dyn = self.config.is_dynfee()
if self.dyn:
pos = self.config.get('fee_level', 2)
fee_rate = self.config.dynfee(pos)
self.setRange(0, 4)
self.setValue(pos)
else:
fee_rate = self.config.fee_per_kb()
pos = self.config.static_fee_index(fee_rate)
self.setRange(0, 9)
self.setValue(pos)
mempool = self.config.get('mempool_fees')
maxp, pos, fee_rate = self.config.get_fee_slider(self.dyn, mempool)
self.setRange(0, maxp)
self.setValue(pos)
tooltip = self.get_tooltip(pos, fee_rate)
self.setToolTip(tooltip)

Expand Down
50 changes: 27 additions & 23 deletions gui/qt/main_window.py
Expand Up @@ -131,7 +131,6 @@ def __init__(self, gui_object, wallet):
self.need_update = threading.Event()

self.decimal_point = config.get('decimal_point', 5)
self.fee_unit = config.get('fee_unit', 0)
self.num_zeros = int(config.get('num_zeros',0))

self.completions = QStringListModel()
Expand Down Expand Up @@ -293,7 +292,6 @@ def on_network(self, event, *args):
self.need_update.set()
self.gui_object.network_updated_signal_obj.network_updated_signal \
.emit(event, args)

elif event == 'new_transaction':
self.tx_notifications.append(args[0])
self.notify_transactions_signal.emit()
Expand All @@ -315,6 +313,12 @@ def on_network_qt(self, event, args=None):
if self.config.is_dynfee():
self.fee_slider.update()
self.do_update_fee()
elif event == 'fee_histogram':
if self.config.is_dynfee():
self.fee_slider.update()
self.do_update_fee()
# todo: update only unconfirmed tx
self.history_list.update()
else:
self.print_error("unexpected network_qt signal:", event, args)

Expand Down Expand Up @@ -636,10 +640,7 @@ def format_amount_and_units(self, amount):
return text

def format_fee_rate(self, fee_rate):
if self.fee_unit == 0:
return format_satoshis(fee_rate/1000, False, self.num_zeros, 0, False) + ' sat/byte'
else:
return self.format_amount(fee_rate) + ' ' + self.base_unit() + '/kB'
return format_satoshis(fee_rate/1000, False, self.num_zeros, 0, False) + ' sat/byte'

def get_decimal_point(self):
return self.decimal_point
Expand Down Expand Up @@ -1076,7 +1077,10 @@ def create_send_tab(self):

def fee_cb(dyn, pos, fee_rate):
if dyn:
self.config.set_key('fee_level', pos, False)
if self.config.get('mempool_fees'):
self.config.set_key('depth_level', pos, False)
else:
self.config.set_key('fee_level', pos, False)
else:
self.config.set_key('fee_per_kb', fee_rate, False)

Expand Down Expand Up @@ -1116,7 +1120,7 @@ def setAmount(self, byte_size):
self.size_e.setFixedWidth(140)
self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())

self.feerate_e = FeerateEdit(lambda: 2 if self.fee_unit else 0)
self.feerate_e = FeerateEdit(lambda: 0)
self.feerate_e.setAmount(self.config.fee_per_byte())
self.feerate_e.textEdited.connect(partial(on_fee_or_feerate, self.feerate_e, False))
self.feerate_e.editingFinished.connect(partial(on_fee_or_feerate, self.feerate_e, True))
Expand Down Expand Up @@ -1256,9 +1260,6 @@ def do_update_fee(self):
'''Recalculate the fee. If the fee was manually input, retain it, but
still build the TX to see if there are enough funds.
'''
if not self.config.get('offline') and self.config.is_dynfee() and not self.config.has_fee_estimates():
self.statusBar().showMessage(_('Waiting for fee estimates...'))
return False
freeze_fee = self.is_send_fee_frozen()
freeze_feerate = self.is_send_feerate_frozen()
amount = '!' if self.is_max else self.amount_e.get_amount()
Expand Down Expand Up @@ -2670,6 +2671,21 @@ def on_nz():
nz.valueChanged.connect(on_nz)
gui_widgets.append((nz_label, nz))

msg = '\n'.join([
_('Time based: fee rate is based on average confirmation time estimates'),
_('Mempool based: fee rate is targetting a depth in the memory pool')
]
)
fee_type_label = HelpLabel(_('Fee estimation') + ':', msg)
fee_type_combo = QComboBox()
fee_type_combo.addItems([_('Time based'), _('Mempool based')])
fee_type_combo.setCurrentIndex(1 if self.config.get('mempool_fees') else 0)
def on_fee_type(x):
self.config.set_key('mempool_fees', x==1)
self.fee_slider.update()
fee_type_combo.currentIndexChanged.connect(on_fee_type)
fee_widgets.append((fee_type_label, fee_type_combo))

def on_dynfee(x):
self.config.set_key('dynamic_fees', x == Qt.Checked)
self.fee_slider.update()
Expand Down Expand Up @@ -2699,18 +2715,6 @@ def on_use_rbf(x):
use_rbf_cb.stateChanged.connect(on_use_rbf)
fee_widgets.append((use_rbf_cb, None))

self.fee_unit = self.config.get('fee_unit', 0)
fee_unit_label = HelpLabel(_('Fee Unit') + ':', '')
fee_unit_combo = QComboBox()
fee_unit_combo.addItems([_('sat/byte'), _('mBTC/kB')])
fee_unit_combo.setCurrentIndex(self.fee_unit)
def on_fee_unit(x):
self.fee_unit = x
self.config.set_key('fee_unit', x)
self.fee_slider.update()
fee_unit_combo.currentIndexChanged.connect(on_fee_unit)
fee_widgets.append((fee_unit_label, fee_unit_combo))

msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\
+ _('The following alias providers are available:') + '\n'\
+ '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\
Expand Down
4 changes: 2 additions & 2 deletions gui/qt/transaction_dialog.py
Expand Up @@ -221,8 +221,8 @@ def update(self):
self.date_label.setText(_("Date: {}").format(time_str))
self.date_label.show()
elif exp_n:
text = '%d blocks'%(exp_n) if exp_n > 0 else _('unknown (low fee)')
self.date_label.setText(_('Expected confirmation time') + ': ' + text)
text = '%.2f MB'%(exp_n/1000000)
self.date_label.setText(_('Position in mempool') + ': ' + text + _('from tip'))
self.date_label.show()
else:
self.date_label.hide()
Expand Down
2 changes: 1 addition & 1 deletion lib/bitcoin.py
Expand Up @@ -102,7 +102,7 @@ def set_testnet(cls):

FEE_STEP = 10000
MAX_FEE_RATE = 300000
FEE_TARGETS = [25, 10, 5, 2]


COINBASE_MATURITY = 100
COIN = 100000000
Expand Down
11 changes: 10 additions & 1 deletion lib/network.py
Expand Up @@ -321,8 +321,10 @@ def send_subscriptions(self):
self.queue_request('blockchain.scripthash.subscribe', [h])

def request_fee_estimates(self):
from .simple_config import FEE_ETA_TARGETS
self.config.requested_fee_estimates()
for i in bitcoin.FEE_TARGETS:
self.queue_request('mempool.get_fee_histogram', [])
for i in FEE_ETA_TARGETS:
self.queue_request('blockchain.estimatefee', [i])

def get_status_value(self, key):
Expand All @@ -332,6 +334,8 @@ def get_status_value(self, key):
value = self.banner
elif key == 'fee':
value = self.config.fee_estimates
elif key == 'fee_histogram':
value = self.config.mempool_fees
elif key == 'updated':
value = (self.get_local_height(), self.get_server_height())
elif key == 'servers':
Expand Down Expand Up @@ -543,6 +547,11 @@ def process_response(self, interface, response, callbacks):
elif method == 'server.donation_address':
if error is None:
self.donation_address = result
elif method == 'mempool.get_fee_histogram':
if error is None:
self.print_error(result)
self.config.mempool_fees = result
self.notify('fee_histogram')
elif method == 'blockchain.estimatefee':
if error is None and result > 0:
i = params[0]
Expand Down

0 comments on commit c3f3843

Please sign in to comment.