Add memory pool based fee estimates
- fee estimates can use ETA or mempool - require protocol version 1.2 - remove fee_unit preference
This commit is contained in:
@@ -102,7 +102,7 @@ NetworkConstants.set_mainnet()
|
||||
|
||||
FEE_STEP = 10000
|
||||
MAX_FEE_RATE = 300000
|
||||
FEE_TARGETS = [25, 10, 5, 2]
|
||||
|
||||
|
||||
COINBASE_MATURITY = 100
|
||||
COIN = 100000000
|
||||
|
||||
@@ -321,8 +321,10 @@ class Network(util.DaemonThread):
|
||||
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):
|
||||
@@ -332,6 +334,8 @@ class Network(util.DaemonThread):
|
||||
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':
|
||||
@@ -543,6 +547,11 @@ class Network(util.DaemonThread):
|
||||
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]
|
||||
|
||||
@@ -6,9 +6,12 @@ import stat
|
||||
|
||||
from copy import deepcopy
|
||||
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
|
||||
|
||||
@@ -48,6 +51,7 @@ class SimpleConfig(PrintError):
|
||||
# a thread-safe way.
|
||||
self.lock = threading.RLock()
|
||||
|
||||
self.mempool_fees = {}
|
||||
self.fee_estimates = {}
|
||||
self.fee_estimates_last_updated = {}
|
||||
self.last_time_fee_estimates_requested = 0 # zero ensures immediate fees
|
||||
@@ -263,9 +267,9 @@ class SimpleConfig(PrintError):
|
||||
f = MAX_FEE_RATE
|
||||
return f
|
||||
|
||||
def dynfee(self, i):
|
||||
def eta_to_fee(self, i):
|
||||
if i < 4:
|
||||
j = FEE_TARGETS[i]
|
||||
j = FEE_ETA_TARGETS[i]
|
||||
fee = self.fee_estimates.get(j)
|
||||
else:
|
||||
assert i == 4
|
||||
@@ -276,15 +280,99 @@ class SimpleConfig(PrintError):
|
||||
fee = min(5*MAX_FEE_RATE, 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
|
||||
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)
|
||||
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 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):
|
||||
return self.fee_rates[i]
|
||||
|
||||
@@ -292,19 +380,27 @@ class SimpleConfig(PrintError):
|
||||
dist = list(map(lambda x: abs(x - value), self.fee_rates))
|
||||
return min(range(len(dist)), key=dist.__getitem__)
|
||||
|
||||
def has_fee_estimates(self):
|
||||
return len(self.fee_estimates)==4
|
||||
def has_fee_etas(self):
|
||||
return len(self.fee_estimates) == 4
|
||||
|
||||
def has_fee_mempool(self):
|
||||
return bool(self.mempool_fees)
|
||||
|
||||
def is_dynfee(self):
|
||||
return self.get('dynamic_fees', True)
|
||||
|
||||
def use_mempool_fees(self):
|
||||
return self.get('mempool_fees', False)
|
||||
|
||||
def fee_per_kb(self):
|
||||
"""Returns sat/kvB fee to pay for a txn.
|
||||
Note: might return None.
|
||||
"""
|
||||
dyn = self.is_dynfee()
|
||||
if dyn:
|
||||
fee_rate = self.dynfee(self.get('fee_level', 2))
|
||||
if self.is_dynfee():
|
||||
if self.use_mempool_fees():
|
||||
fee_rate = self.depth_to_fee(self.get('depth_level', 2))
|
||||
else:
|
||||
fee_rate = self.eta_to_fee(self.get('fee_level', 2))
|
||||
else:
|
||||
fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2)
|
||||
return fee_rate
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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
|
||||
SEED_PREFIX = '01' # Standard wallet
|
||||
|
||||
@@ -538,10 +538,10 @@ class Abstract_Wallet(PrintError):
|
||||
status = _('Unconfirmed')
|
||||
if fee is None:
|
||||
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()
|
||||
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()
|
||||
else:
|
||||
status = _('Local')
|
||||
@@ -860,18 +860,17 @@ class Abstract_Wallet(PrintError):
|
||||
|
||||
def get_tx_status(self, tx_hash, height, conf, timestamp):
|
||||
from .util import format_time
|
||||
exp_n = False
|
||||
if conf == 0:
|
||||
tx = self.transactions.get(tx_hash)
|
||||
if not tx:
|
||||
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.config.has_fee_estimates():
|
||||
size = len(tx.raw)/2
|
||||
low_fee = int(self.network.config.dynfee(0)*size/1000)
|
||||
is_lowfee = fee < low_fee * 0.5
|
||||
else:
|
||||
is_lowfee = False
|
||||
if fee and self.network and self.network.config.has_fee_mempool():
|
||||
size = tx.estimated_size()
|
||||
fee_per_kb = fee * 1000 / size
|
||||
exp_n = self.network.config.fee_to_depth(fee_per_kb//1000)
|
||||
if height == TX_HEIGHT_LOCAL:
|
||||
status = 5
|
||||
elif height == TX_HEIGHT_UNCONF_PARENT:
|
||||
@@ -888,6 +887,8 @@ class Abstract_Wallet(PrintError):
|
||||
status = 5 + min(conf, 6)
|
||||
time_str = format_time(timestamp) if timestamp else _("unknown")
|
||||
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
|
||||
|
||||
def relayfee(self):
|
||||
|
||||
Reference in New Issue
Block a user