Browse Source

Add memory pool based fee estimates

- fee estimates can use ETA or mempool
 - require protocol version 1.2
 - remove fee_unit preference
master
ThomasV 8 years ago
parent
commit
c3f3843cc3
  1. 53
      gui/kivy/uix/dialogs/fee_dialog.py
  2. 5
      gui/kivy/uix/dialogs/settings.py
  3. 5
      gui/qt/amountedit.py
  4. 32
      gui/qt/fee_slider.py
  5. 46
      gui/qt/main_window.py
  6. 4
      gui/qt/transaction_dialog.py
  7. 2
      lib/bitcoin.py
  8. 11
      lib/network.py
  9. 116
      lib/simple_config.py
  10. 2
      lib/version.py
  11. 17
      lib/wallet.py

53
gui/kivy/uix/dialogs/fee_dialog.py

@ -32,7 +32,15 @@ Builder.load_string('''
text: _('Dynamic Fees') text: _('Dynamic Fees')
CheckBox: CheckBox:
id: dynfees 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: Widget:
size_hint: 1, 1 size_hint: 1, 1
BoxLayout: BoxLayout:
@ -60,7 +68,9 @@ class FeeDialog(Factory.Popup):
self.config = config self.config = config
self.fee_rate = self.config.fee_per_kb() self.fee_rate = self.config.fee_per_kb()
self.callback = callback self.callback = callback
self.mempool = self.config.get('mempool_fees', False)
self.dynfees = self.config.get('dynamic_fees', True) self.dynfees = self.config.get('dynamic_fees', True)
self.ids.mempool.active = self.mempool
self.ids.dynfees.active = self.dynfees self.ids.dynfees.active = self.dynfees
self.update_slider() self.update_slider()
self.update_text() self.update_text()
@ -71,33 +81,29 @@ class FeeDialog(Factory.Popup):
def update_slider(self): def update_slider(self):
slider = self.ids.slider slider = self.ids.slider
if self.dynfees: maxp, pos, fee_rate = self.config.get_fee_slider(self.dynfees, self.mempool)
slider.range = (0, 4) slider.range = (0, maxp)
slider.step = 1 slider.step = 1
slider.value = self.config.get('fee_level', 2) slider.value = pos
else:
slider.range = (0, 9)
slider.step = 1
slider.value = self.config.static_fee_index(self.fee_rate)
def get_fee_text(self, value): def get_fee_text(self, pos):
if self.ids.dynfees.active: dyn = self.dynfees
tooltip = fee_levels[value] mempool = self.mempool
if self.config.has_fee_estimates(): if dyn:
dynfee = self.config.dynfee(value) fee_rate = self.config.depth_to_fee(pos) if mempool else self.config.eta_to_fee(pos)
tooltip += '\n' + (self.app.format_amount_and_units(dynfee)) + '/kB'
else: else:
fee_rate = self.config.static_fee(value) fee_rate = self.config.static_fee(pos)
tooltip = self.app.format_amount_and_units(fee_rate) + '/kB' target, tooltip = self.config.get_fee_text(pos, dyn, mempool, fee_rate)
if self.config.has_fee_estimates(): return target
i = self.config.reverse_dynfee(fee_rate)
tooltip += '\n' + (_('low fee') if i < 0 else 'Within %d blocks'%i)
return tooltip
def on_ok(self): def on_ok(self):
value = int(self.ids.slider.value) value = int(self.ids.slider.value)
self.config.set_key('dynamic_fees', self.dynfees, False) self.config.set_key('dynamic_fees', self.dynfees, False)
self.config.set_key('mempool_fees', self.mempool, False)
if self.dynfees: if self.dynfees:
if self.mempool:
self.config.set_key('depth_level', value, True)
else:
self.config.set_key('fee_level', value, True) self.config.set_key('fee_level', value, True)
else: else:
self.config.set_key('fee_per_kb', self.config.static_fee(value), True) self.config.set_key('fee_per_kb', self.config.static_fee(value), True)
@ -106,7 +112,12 @@ class FeeDialog(Factory.Popup):
def on_slider(self, value): def on_slider(self, value):
self.update_text() self.update_text()
def on_checkbox(self, b): def on_dynfees(self, b):
self.dynfees = b self.dynfees = b
self.update_slider() self.update_slider()
self.update_text() self.update_text()
def on_mempool(self, b):
self.mempool = b
self.update_slider()
self.update_text()

5
gui/kivy/uix/dialogs/settings.py

@ -204,10 +204,7 @@ class SettingsDialog(Factory.Popup):
d.open() d.open()
def fee_status(self): def fee_status(self):
if self.config.get('dynamic_fees', True): return self.config.get_fee_status()
return fee_levels[self.config.get('fee_level', 2)]
else:
return self.app.format_amount_and_units(self.config.fee_per_kb()) + '/kB'
def fee_dialog(self, label, dt): def fee_dialog(self, label, dt):
if self._fee_dialog is None: if self._fee_dialog is None:

5
gui/qt/amountedit.py

@ -106,12 +106,7 @@ class BTCAmountEdit(AmountEdit):
class FeerateEdit(BTCAmountEdit): class FeerateEdit(BTCAmountEdit):
def _base_unit(self): def _base_unit(self):
p = self.decimal_point()
if p == 2:
return 'mBTC/kB'
if p == 0:
return 'sat/byte' return 'sat/byte'
raise Exception('Unknown base unit')
def get_amount(self): def get_amount(self):
sat_per_byte_amount = BTCAmountEdit.get_amount(self) sat_per_byte_amount = BTCAmountEdit.get_amount(self)

32
gui/qt/fee_slider.py

@ -1,6 +1,4 @@
from electrum.i18n import _ from electrum.i18n import _
from PyQt5.QtGui import * from PyQt5.QtGui import *
from PyQt5.QtCore import * from PyQt5.QtCore import *
from PyQt5.QtWidgets import QSlider, QToolTip from PyQt5.QtWidgets import QSlider, QToolTip
@ -22,36 +20,26 @@ class FeeSlider(QSlider):
def moved(self, pos): def moved(self, pos):
with self.lock: 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) tooltip = self.get_tooltip(pos, fee_rate)
QToolTip.showText(QCursor.pos(), tooltip, self) QToolTip.showText(QCursor.pos(), tooltip, self)
self.setToolTip(tooltip) self.setToolTip(tooltip)
self.callback(self.dyn, pos, fee_rate) self.callback(self.dyn, pos, fee_rate)
def get_tooltip(self, pos, fee_rate): def get_tooltip(self, pos, fee_rate):
from electrum.util import fee_levels mempool = self.config.get('mempool_fees')
rate_str = self.window.format_fee_rate(fee_rate) if fee_rate else _('unknown') text, tooltip = self.config.get_fee_text(pos, self.dyn, mempool, fee_rate)
if self.dyn: return text + '\n' + tooltip
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
def update(self): def update(self):
with self.lock: with self.lock:
self.dyn = self.config.is_dynfee() self.dyn = self.config.is_dynfee()
if self.dyn: mempool = self.config.get('mempool_fees')
pos = self.config.get('fee_level', 2) maxp, pos, fee_rate = self.config.get_fee_slider(self.dyn, mempool)
fee_rate = self.config.dynfee(pos) self.setRange(0, maxp)
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) self.setValue(pos)
tooltip = self.get_tooltip(pos, fee_rate) tooltip = self.get_tooltip(pos, fee_rate)
self.setToolTip(tooltip) self.setToolTip(tooltip)

46
gui/qt/main_window.py

@ -131,7 +131,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.need_update = threading.Event() self.need_update = threading.Event()
self.decimal_point = config.get('decimal_point', 5) 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.num_zeros = int(config.get('num_zeros',0))
self.completions = QStringListModel() self.completions = QStringListModel()
@ -293,7 +292,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.need_update.set() self.need_update.set()
self.gui_object.network_updated_signal_obj.network_updated_signal \ self.gui_object.network_updated_signal_obj.network_updated_signal \
.emit(event, args) .emit(event, args)
elif event == 'new_transaction': elif event == 'new_transaction':
self.tx_notifications.append(args[0]) self.tx_notifications.append(args[0])
self.notify_transactions_signal.emit() self.notify_transactions_signal.emit()
@ -315,6 +313,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.config.is_dynfee(): if self.config.is_dynfee():
self.fee_slider.update() self.fee_slider.update()
self.do_update_fee() 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: else:
self.print_error("unexpected network_qt signal:", event, args) self.print_error("unexpected network_qt signal:", event, args)
@ -636,10 +640,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
return text return text
def format_fee_rate(self, fee_rate): 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' 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'
def get_decimal_point(self): def get_decimal_point(self):
return self.decimal_point return self.decimal_point
@ -1076,6 +1077,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def fee_cb(dyn, pos, fee_rate): def fee_cb(dyn, pos, fee_rate):
if dyn: if dyn:
if self.config.get('mempool_fees'):
self.config.set_key('depth_level', pos, False)
else:
self.config.set_key('fee_level', pos, False) self.config.set_key('fee_level', pos, False)
else: else:
self.config.set_key('fee_per_kb', fee_rate, False) self.config.set_key('fee_per_kb', fee_rate, False)
@ -1116,7 +1120,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.size_e.setFixedWidth(140) self.size_e.setFixedWidth(140)
self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet()) 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.setAmount(self.config.fee_per_byte())
self.feerate_e.textEdited.connect(partial(on_fee_or_feerate, self.feerate_e, False)) 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)) self.feerate_e.editingFinished.connect(partial(on_fee_or_feerate, self.feerate_e, True))
@ -1256,9 +1260,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
'''Recalculate the fee. If the fee was manually input, retain it, but '''Recalculate the fee. If the fee was manually input, retain it, but
still build the TX to see if there are enough funds. 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_fee = self.is_send_fee_frozen()
freeze_feerate = self.is_send_feerate_frozen() freeze_feerate = self.is_send_feerate_frozen()
amount = '!' if self.is_max else self.amount_e.get_amount() amount = '!' if self.is_max else self.amount_e.get_amount()
@ -2670,6 +2671,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
nz.valueChanged.connect(on_nz) nz.valueChanged.connect(on_nz)
gui_widgets.append((nz_label, 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): def on_dynfee(x):
self.config.set_key('dynamic_fees', x == Qt.Checked) self.config.set_key('dynamic_fees', x == Qt.Checked)
self.fee_slider.update() self.fee_slider.update()
@ -2699,18 +2715,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
use_rbf_cb.stateChanged.connect(on_use_rbf) use_rbf_cb.stateChanged.connect(on_use_rbf)
fee_widgets.append((use_rbf_cb, None)) 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'\ msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\
+ _('The following alias providers are available:') + '\n'\ + _('The following alias providers are available:') + '\n'\
+ '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\ + '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\

4
gui/qt/transaction_dialog.py

@ -221,8 +221,8 @@ class TxDialog(QDialog, MessageBoxMixin):
self.date_label.setText(_("Date: {}").format(time_str)) self.date_label.setText(_("Date: {}").format(time_str))
self.date_label.show() self.date_label.show()
elif exp_n: elif exp_n:
text = '%d blocks'%(exp_n) if exp_n > 0 else _('unknown (low fee)') text = '%.2f MB'%(exp_n/1000000)
self.date_label.setText(_('Expected confirmation time') + ': ' + text) self.date_label.setText(_('Position in mempool') + ': ' + text + _('from tip'))
self.date_label.show() self.date_label.show()
else: else:
self.date_label.hide() self.date_label.hide()

2
lib/bitcoin.py

@ -102,7 +102,7 @@ NetworkConstants.set_mainnet()
FEE_STEP = 10000 FEE_STEP = 10000
MAX_FEE_RATE = 300000 MAX_FEE_RATE = 300000
FEE_TARGETS = [25, 10, 5, 2]
COINBASE_MATURITY = 100 COINBASE_MATURITY = 100
COIN = 100000000 COIN = 100000000

11
lib/network.py

@ -321,8 +321,10 @@ class Network(util.DaemonThread):
self.queue_request('blockchain.scripthash.subscribe', [h]) self.queue_request('blockchain.scripthash.subscribe', [h])
def request_fee_estimates(self): def request_fee_estimates(self):
from .simple_config import FEE_ETA_TARGETS
self.config.requested_fee_estimates() 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]) self.queue_request('blockchain.estimatefee', [i])
def get_status_value(self, key): def get_status_value(self, key):
@ -332,6 +334,8 @@ class Network(util.DaemonThread):
value = self.banner value = self.banner
elif key == 'fee': elif key == 'fee':
value = self.config.fee_estimates value = self.config.fee_estimates
elif key == 'fee_histogram':
value = self.config.mempool_fees
elif key == 'updated': elif key == 'updated':
value = (self.get_local_height(), self.get_server_height()) value = (self.get_local_height(), self.get_server_height())
elif key == 'servers': elif key == 'servers':
@ -543,6 +547,11 @@ class Network(util.DaemonThread):
elif method == 'server.donation_address': elif method == 'server.donation_address':
if error is None: if error is None:
self.donation_address = result 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': elif method == 'blockchain.estimatefee':
if error is None and result > 0: if error is None and result > 0:
i = params[0] i = params[0]

116
lib/simple_config.py

@ -6,9 +6,12 @@ import stat
from copy import deepcopy from copy import deepcopy
from .util import (user_dir, print_error, PrintError, from .util import (user_dir, print_error, PrintError,
NoDynamicFeeEstimates) NoDynamicFeeEstimates, format_satoshis)
from .bitcoin import MAX_FEE_RATE, FEE_TARGETS from .bitcoin import MAX_FEE_RATE
FEE_ETA_TARGETS = [25, 10, 5, 2]
FEE_DEPTH_TARGETS = [10000000, 5000000, 2000000, 1000000, 500000, 200000, 100000]
config = None config = None
@ -48,6 +51,7 @@ class SimpleConfig(PrintError):
# a thread-safe way. # a thread-safe way.
self.lock = threading.RLock() self.lock = threading.RLock()
self.mempool_fees = {}
self.fee_estimates = {} self.fee_estimates = {}
self.fee_estimates_last_updated = {} self.fee_estimates_last_updated = {}
self.last_time_fee_estimates_requested = 0 # zero ensures immediate fees self.last_time_fee_estimates_requested = 0 # zero ensures immediate fees
@ -263,9 +267,9 @@ class SimpleConfig(PrintError):
f = MAX_FEE_RATE f = MAX_FEE_RATE
return f return f
def dynfee(self, i): def eta_to_fee(self, i):
if i < 4: if i < 4:
j = FEE_TARGETS[i] j = FEE_ETA_TARGETS[i]
fee = self.fee_estimates.get(j) fee = self.fee_estimates.get(j)
else: else:
assert i == 4 assert i == 4
@ -276,15 +280,99 @@ class SimpleConfig(PrintError):
fee = min(5*MAX_FEE_RATE, fee) fee = min(5*MAX_FEE_RATE, fee)
return fee return fee
def reverse_dynfee(self, fee_per_kb): def fee_to_depth(self, target_fee):
depth = 0
for fee, s in self.mempool_fees:
depth += s
if fee < target_fee:
break
else:
return 0
return depth
def depth_to_fee(self, i):
target = self.depth_target(i)
depth = 0
for fee, s in self.mempool_fees:
depth += s
if depth > target:
break
else:
return 0
return fee * 1000
def depth_target(self, i):
return FEE_DEPTH_TARGETS[i]
def eta_target(self, i):
return FEE_ETA_TARGETS[i]
def fee_to_eta(self, fee_per_kb):
import operator import operator
l = list(self.fee_estimates.items()) + [(1, self.dynfee(4))] l = list(self.fee_estimates.items()) + [(1, self.eta_to_fee(4))]
dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), l) dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), l)
min_target, min_value = min(dist, key=operator.itemgetter(1)) min_target, min_value = min(dist, key=operator.itemgetter(1))
if fee_per_kb < self.fee_estimates.get(25)/2: if fee_per_kb < self.fee_estimates.get(25)/2:
min_target = -1 min_target = -1
return min_target return min_target
def depth_tooltip(self, depth):
return "%.1f MB from tip"%(depth/1000000)
def eta_tooltip(self, x):
return 'Low fee' if x < 0 else 'Within %d blocks'%x
def get_fee_status(self):
dyn = self.is_dynfee()
mempool = self.get('mempool_fees')
pos = self.get('fee_level', 2) if mempool else self.get('depth_level', 2)
fee_rate = self.fee_per_kb()
target, tooltip = self.get_fee_text(pos, dyn, mempool, fee_rate)
return target
def get_fee_text(self, pos, dyn, mempool, fee_rate):
rate_str = (format_satoshis(fee_rate/1000, False, 0, 0, False) + ' sat/byte') if fee_rate is not None else 'unknown'
if dyn:
if mempool:
depth = self.depth_target(pos)
text = self.depth_tooltip(depth)
else:
eta = self.eta_target(pos)
text = self.eta_tooltip(eta)
tooltip = rate_str
else:
text = rate_str
if mempool:
if self.has_fee_mempool():
depth = self.fee_to_depth(fee_rate)
tooltip = self.depth_tooltip(depth)
else:
tooltip = ''
else:
if self.has_fee_etas():
eta = self.fee_to_eta(fee_rate)
tooltip = self.eta_tooltip(eta)
else:
tooltip = ''
return text, tooltip
def get_fee_slider(self, dyn, mempool):
if dyn:
if mempool:
maxp = len(FEE_DEPTH_TARGETS) - 1
pos = min(maxp, self.get('depth_level', 2))
fee_rate = self.depth_to_fee(pos)
else:
maxp = len(FEE_ETA_TARGETS) - 1
pos = min(maxp, self.get('fee_level', 2))
fee_rate = self.eta_to_fee(pos)
else:
fee_rate = self.fee_per_kb()
pos = self.static_fee_index(fee_rate)
maxp= 9
return maxp, pos, fee_rate
def static_fee(self, i): def static_fee(self, i):
return self.fee_rates[i] return self.fee_rates[i]
@ -292,19 +380,27 @@ class SimpleConfig(PrintError):
dist = list(map(lambda x: abs(x - value), self.fee_rates)) dist = list(map(lambda x: abs(x - value), self.fee_rates))
return min(range(len(dist)), key=dist.__getitem__) return min(range(len(dist)), key=dist.__getitem__)
def has_fee_estimates(self): def has_fee_etas(self):
return len(self.fee_estimates) == 4 return len(self.fee_estimates) == 4
def has_fee_mempool(self):
return bool(self.mempool_fees)
def is_dynfee(self): def is_dynfee(self):
return self.get('dynamic_fees', True) return self.get('dynamic_fees', True)
def use_mempool_fees(self):
return self.get('mempool_fees', False)
def fee_per_kb(self): def fee_per_kb(self):
"""Returns sat/kvB fee to pay for a txn. """Returns sat/kvB fee to pay for a txn.
Note: might return None. Note: might return None.
""" """
dyn = self.is_dynfee() if self.is_dynfee():
if dyn: if self.use_mempool_fees():
fee_rate = self.dynfee(self.get('fee_level', 2)) fee_rate = self.depth_to_fee(self.get('depth_level', 2))
else:
fee_rate = self.eta_to_fee(self.get('fee_level', 2))
else: else:
fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2) fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2)
return fee_rate return fee_rate

2
lib/version.py

@ -1,5 +1,5 @@
ELECTRUM_VERSION = '3.0.6' # version of the client package ELECTRUM_VERSION = '3.0.6' # version of the client package
PROTOCOL_VERSION = '1.1' # protocol version requested PROTOCOL_VERSION = '1.2' # protocol version requested
# The hash of the mnemonic seed must begin with this # The hash of the mnemonic seed must begin with this
SEED_PREFIX = '01' # Standard wallet SEED_PREFIX = '01' # Standard wallet

17
lib/wallet.py

@ -538,10 +538,10 @@ class Abstract_Wallet(PrintError):
status = _('Unconfirmed') status = _('Unconfirmed')
if fee is None: if fee is None:
fee = self.tx_fees.get(tx_hash) fee = self.tx_fees.get(tx_hash)
if fee and self.network.config.has_fee_estimates(): if fee and self.network.config.has_fee_etas():
size = tx.estimated_size() size = tx.estimated_size()
fee_per_kb = fee * 1000 / size fee_per_kb = fee * 1000 / size
exp_n = self.network.config.reverse_dynfee(fee_per_kb) exp_n = self.network.config.fee_to_eta(fee_per_kb)
can_bump = is_mine and not tx.is_final() can_bump = is_mine and not tx.is_final()
else: else:
status = _('Local') status = _('Local')
@ -860,18 +860,17 @@ class Abstract_Wallet(PrintError):
def get_tx_status(self, tx_hash, height, conf, timestamp): def get_tx_status(self, tx_hash, height, conf, timestamp):
from .util import format_time from .util import format_time
exp_n = False
if conf == 0: if conf == 0:
tx = self.transactions.get(tx_hash) tx = self.transactions.get(tx_hash)
if not tx: if not tx:
return 3, 'unknown' return 3, 'unknown'
is_final = tx and tx.is_final() is_final = tx and tx.is_final()
fee = self.tx_fees.get(tx_hash) fee = self.tx_fees.get(tx_hash)
if fee and self.network and self.network.config.has_fee_estimates(): if fee and self.network and self.network.config.has_fee_mempool():
size = len(tx.raw)/2 size = tx.estimated_size()
low_fee = int(self.network.config.dynfee(0)*size/1000) fee_per_kb = fee * 1000 / size
is_lowfee = fee < low_fee * 0.5 exp_n = self.network.config.fee_to_depth(fee_per_kb//1000)
else:
is_lowfee = False
if height == TX_HEIGHT_LOCAL: if height == TX_HEIGHT_LOCAL:
status = 5 status = 5
elif height == TX_HEIGHT_UNCONF_PARENT: elif height == TX_HEIGHT_UNCONF_PARENT:
@ -888,6 +887,8 @@ class Abstract_Wallet(PrintError):
status = 5 + min(conf, 6) status = 5 + min(conf, 6)
time_str = format_time(timestamp) if timestamp else _("unknown") time_str = format_time(timestamp) if timestamp else _("unknown")
status_str = TX_STATUS[status] if status < 6 else time_str status_str = TX_STATUS[status] if status < 6 else time_str
if exp_n:
status_str += ' [%d sat/b, %.2f MB]'%(fee_per_kb//1000, exp_n/1000000)
return status, status_str return status, status_str
def relayfee(self): def relayfee(self):

Loading…
Cancel
Save