|
|
|
|
@ -47,13 +47,6 @@ class BlockchainInterface(ABC):
|
|
|
|
|
""" |
|
|
|
|
# address and output script contain the same information btw |
|
|
|
|
|
|
|
|
|
@abstractmethod |
|
|
|
|
def estimate_fee_per_kb(self, N): |
|
|
|
|
'''Use the blockchain interface to |
|
|
|
|
get an estimate of the transaction fee per kb |
|
|
|
|
required for inclusion in the next N blocks. |
|
|
|
|
''' |
|
|
|
|
|
|
|
|
|
@abstractmethod |
|
|
|
|
def get_wallet_rescan_status(self) -> Tuple[bool, Optional[Decimal]]: |
|
|
|
|
"""Returns pair of True/False is wallet currently rescanning and |
|
|
|
|
@ -63,14 +56,96 @@ class BlockchainInterface(ABC):
|
|
|
|
|
"""import addresses to the underlying blockchain interface if needed |
|
|
|
|
returns True if the sync call needs to do a system exit""" |
|
|
|
|
|
|
|
|
|
def fee_per_kb_has_been_manually_set(self, N): |
|
|
|
|
'''if the 'block' target is higher than 1000, interpret it |
|
|
|
|
as manually set fee/Kb. |
|
|
|
|
''' |
|
|
|
|
if N > 1000: |
|
|
|
|
return True |
|
|
|
|
@abstractmethod |
|
|
|
|
def _get_mempool_min_fee(self) -> Optional[int]: |
|
|
|
|
"""Returns minimum mempool fee as a floor to avoid relay problems |
|
|
|
|
or None in case of error. |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
@abstractmethod |
|
|
|
|
def _estimate_fee_basic(self, conf_target: int) -> Optional[int]: |
|
|
|
|
"""Returns basic fee estimation for confirmation target in blocks. |
|
|
|
|
Additional JoinMarket fee logic is added on top, see |
|
|
|
|
`estimate_fee_per_kb` for details. Returns feerate in sats per vB |
|
|
|
|
or None in case of error. |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
def _fee_per_kb_has_been_manually_set(self, tx_fees: int) -> bool: |
|
|
|
|
"""If the block target (tx_fees) is higher than 1000, interpret it |
|
|
|
|
as manually set fee sats/kvB. |
|
|
|
|
""" |
|
|
|
|
return tx_fees > 1000 |
|
|
|
|
|
|
|
|
|
def estimate_fee_per_kb(self, tx_fees: int) -> int: |
|
|
|
|
""" The argument tx_fees may be either a number of blocks target, |
|
|
|
|
for estimation of feerate by Core, or a number of satoshis |
|
|
|
|
per kilo-vbyte (see `_fee_per_kb_has_been_manually_set` for |
|
|
|
|
how this is distinguished). |
|
|
|
|
In both cases it is prevented from falling below the current |
|
|
|
|
minimum feerate for tx to be accepted into node's mempool. |
|
|
|
|
In case of failure to connect, source a specific minimum fee relay |
|
|
|
|
rate (which is used to sanity check user's chosen fee rate), or |
|
|
|
|
failure to source a feerate estimate for targeted number of blocks, |
|
|
|
|
a default of 10000 is returned. |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
# default to use if fees cannot be estimated |
|
|
|
|
fallback_fee = 10000 |
|
|
|
|
|
|
|
|
|
tx_fees_factor = abs(jm_single().config.getfloat('POLICY', 'tx_fees_factor')) |
|
|
|
|
|
|
|
|
|
mempoolminfee_in_sat = self._get_mempool_min_fee() |
|
|
|
|
# in case of error |
|
|
|
|
if mempoolminfee_in_sat is None: |
|
|
|
|
mempoolminfee_in_sat = fallback_fee |
|
|
|
|
mempoolminfee_in_sat_randomized = random.uniform( |
|
|
|
|
mempoolminfee_in_sat, mempoolminfee_in_sat * float(1 + tx_fees_factor)) |
|
|
|
|
|
|
|
|
|
if self._fee_per_kb_has_been_manually_set(tx_fees): |
|
|
|
|
N_res = random.uniform(tx_fees, tx_fees * float(1 + tx_fees_factor)) |
|
|
|
|
if N_res < mempoolminfee_in_sat: |
|
|
|
|
msg = "Using this mempool min fee as tx feerate" |
|
|
|
|
if tx_fees_factor != 0: |
|
|
|
|
msg = msg + " (randomized for privacy)" |
|
|
|
|
log.info(msg + ": " + btc.fee_per_kb_to_str( |
|
|
|
|
mempoolminfee_in_sat_randomized) + ".") |
|
|
|
|
return int(mempoolminfee_in_sat_randomized) |
|
|
|
|
else: |
|
|
|
|
msg = "Using this manually set tx feerate" |
|
|
|
|
if tx_fees_factor != 0: |
|
|
|
|
msg = msg + " (randomized for privacy)" |
|
|
|
|
log.info(msg + ": " + btc.fee_per_kb_to_str(N_res) + ".") |
|
|
|
|
return int(N_res) |
|
|
|
|
|
|
|
|
|
retval = self._estimate_fee_basic(tx_fees) |
|
|
|
|
if retval is None: |
|
|
|
|
msg = "Fee estimation for " + str(tx_fees) + \ |
|
|
|
|
" block confirmation target failed. " + \ |
|
|
|
|
"Falling back to default" |
|
|
|
|
if tx_fees_factor != 0: |
|
|
|
|
msg = msg + " (randomized for privacy)" |
|
|
|
|
fallback_fee_randomized = random.uniform( |
|
|
|
|
fallback_fee, fallback_fee * float(1 + tx_fees_factor)) |
|
|
|
|
log.warn(msg + ": " + |
|
|
|
|
btc.fee_per_kb_to_str(fallback_fee_randomized) + ".") |
|
|
|
|
return int(fallback_fee_randomized) |
|
|
|
|
|
|
|
|
|
retval = random.uniform(retval, retval * float(1 + tx_fees_factor)) |
|
|
|
|
|
|
|
|
|
if retval < mempoolminfee_in_sat: |
|
|
|
|
msg = "Using this mempool min fee as tx feerate" |
|
|
|
|
if tx_fees_factor != 0: |
|
|
|
|
msg = msg + " (randomized for privacy)" |
|
|
|
|
log.info(msg + ": " + btc.fee_per_kb_to_str( |
|
|
|
|
mempoolminfee_in_sat_randomized) + ".") |
|
|
|
|
return int(mempoolminfee_in_sat_randomized) |
|
|
|
|
else: |
|
|
|
|
return False |
|
|
|
|
msg = "Using bitcoin network feerate for " + str(tx_fees) + \ |
|
|
|
|
" block confirmation target" |
|
|
|
|
if tx_fees_factor != 0: |
|
|
|
|
msg = msg + " (randomized for privacy)" |
|
|
|
|
log.info(msg + ": " + btc.fee_per_kb_to_str(retval)) |
|
|
|
|
return int(retval) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BitcoinCoreInterface(BlockchainInterface): |
|
|
|
|
@ -164,7 +239,7 @@ class BitcoinCoreInterface(BlockchainInterface):
|
|
|
|
|
else: |
|
|
|
|
return False, None |
|
|
|
|
|
|
|
|
|
def _rpc(self, method, args): |
|
|
|
|
def _rpc(self, method: str, args: Optional[list] = None): |
|
|
|
|
""" Returns the result of an rpc call to the Bitcoin Core RPC API. |
|
|
|
|
If the connection is permanently or unrecognizably broken, None |
|
|
|
|
is returned *and the reactor is shutdown* (because we consider this |
|
|
|
|
@ -390,54 +465,20 @@ class BitcoinCoreInterface(BlockchainInterface):
|
|
|
|
|
# and return the indices of the others: |
|
|
|
|
return [i for i, val in enumerate(res) if val] |
|
|
|
|
|
|
|
|
|
def estimate_fee_per_kb(self, N): |
|
|
|
|
""" The argument N may be either a number of blocks target, |
|
|
|
|
for estimation of feerate by Core, or a number of satoshis |
|
|
|
|
per kilo-vbyte (see `fee_per_kb_has_been_manually_set` for |
|
|
|
|
how this is distinguished). |
|
|
|
|
In both cases it is prevented from falling below the current |
|
|
|
|
minimum feerate for tx to be accepted into node's mempool. |
|
|
|
|
In case of failure to connect, None is returned. |
|
|
|
|
In case of failure to source a specific minimum fee relay rate |
|
|
|
|
(which is used to sanity check user's chosen fee rate), 1000 |
|
|
|
|
is used. |
|
|
|
|
In case of failure to source a feerate estimate for targeted |
|
|
|
|
number of blocks, a default of 10000 is returned. |
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
# use the local bitcoin core minimum mempool fee as floor to avoid |
|
|
|
|
# relay problems |
|
|
|
|
rpc_result = self._rpc('getmempoolinfo', None) |
|
|
|
|
def _get_mempool_min_fee(self) -> Optional[int]: |
|
|
|
|
rpc_result = self._rpc('getmempoolinfo') |
|
|
|
|
if not rpc_result: |
|
|
|
|
# in case of connection error: |
|
|
|
|
return None |
|
|
|
|
return btc.btc_to_sat(rpc_result['mempoolminfee']) |
|
|
|
|
|
|
|
|
|
tx_fees_factor = abs(jm_single().config.getfloat('POLICY', 'tx_fees_factor')) |
|
|
|
|
|
|
|
|
|
mempoolminfee_in_sat = btc.btc_to_sat(rpc_result['mempoolminfee']) |
|
|
|
|
mempoolminfee_in_sat_randomized = random.uniform( |
|
|
|
|
mempoolminfee_in_sat, mempoolminfee_in_sat * float(1 + tx_fees_factor)) |
|
|
|
|
|
|
|
|
|
if super().fee_per_kb_has_been_manually_set(N): |
|
|
|
|
N_res = random.uniform(N, N * float(1 + tx_fees_factor)) |
|
|
|
|
if N_res < mempoolminfee_in_sat: |
|
|
|
|
log.info("Using this mempool min fee as tx feerate: " + |
|
|
|
|
btc.fee_per_kb_to_str(mempoolminfee_in_sat) + ".") |
|
|
|
|
return int(mempoolminfee_in_sat_randomized) |
|
|
|
|
else: |
|
|
|
|
msg = "Using this manually set tx feerate" |
|
|
|
|
if tx_fees_factor != 0: |
|
|
|
|
msg = msg + " (randomized for privacy)" |
|
|
|
|
log.info(msg + ": " + btc.fee_per_kb_to_str(N_res) + ".") |
|
|
|
|
return int(N_res) |
|
|
|
|
|
|
|
|
|
def _estimate_fee_basic(self, conf_target: int) -> Optional[int]: |
|
|
|
|
# Special bitcoin core case: sometimes the highest priority |
|
|
|
|
# cannot be estimated in that case the 2nd highest priority |
|
|
|
|
# should be used instead of falling back to hardcoded values |
|
|
|
|
tries = 2 if N == 1 else 1 |
|
|
|
|
|
|
|
|
|
tries = 2 if conf_target == 1 else 1 |
|
|
|
|
for i in range(tries): |
|
|
|
|
rpc_result = self._rpc('estimatesmartfee', [N + i]) |
|
|
|
|
rpc_result = self._rpc('estimatesmartfee', [conf_target + i]) |
|
|
|
|
if not rpc_result: |
|
|
|
|
# in case of connection error: |
|
|
|
|
return None |
|
|
|
|
@ -447,29 +488,10 @@ class BitcoinCoreInterface(BlockchainInterface):
|
|
|
|
|
# if it is not able to make an estimate. We insist that |
|
|
|
|
# the 'feerate' key is found and contains a positive value: |
|
|
|
|
if estimate and estimate > 0: |
|
|
|
|
estimate_in_sat = btc.btc_to_sat(estimate) |
|
|
|
|
retval = random.uniform(estimate_in_sat, |
|
|
|
|
estimate_in_sat * float(1 + tx_fees_factor)) |
|
|
|
|
break |
|
|
|
|
else: # cannot get a valid estimate after `tries` tries: |
|
|
|
|
fallback_fee = 10000 |
|
|
|
|
retval = random.uniform(fallback_fee, |
|
|
|
|
fallback_fee * float(1 + tx_fees_factor)) |
|
|
|
|
log.warn("Could not source a fee estimate from Core, " + |
|
|
|
|
"falling back to default: " + |
|
|
|
|
btc.fee_per_kb_to_str(fallback_fee) + ".") |
|
|
|
|
|
|
|
|
|
if retval < mempoolminfee_in_sat: |
|
|
|
|
log.info("Using this mempool min fee as tx feerate: " + |
|
|
|
|
btc.fee_per_kb_to_str(mempoolminfee_in_sat) + ".") |
|
|
|
|
return int(mempoolminfee_in_sat_randomized) |
|
|
|
|
else: |
|
|
|
|
msg = "Using bitcoin network feerate for " + str(N) + \ |
|
|
|
|
" block confirmation target" |
|
|
|
|
if tx_fees_factor != 0: |
|
|
|
|
msg = msg + " (randomized for privacy)" |
|
|
|
|
log.info(msg + ": " + btc.fee_per_kb_to_str(retval)) |
|
|
|
|
return int(retval) |
|
|
|
|
return btc.btc_to_sat(estimate) |
|
|
|
|
# cannot get a valid estimate after `tries` tries: |
|
|
|
|
log.warn("Could not source a fee estimate from Core") |
|
|
|
|
return None |
|
|
|
|
|
|
|
|
|
def get_current_block_height(self): |
|
|
|
|
try: |
|
|
|
|
@ -665,9 +687,9 @@ class RegtestBitcoinCoreInterface(BitcoinCoreInterface, RegtestBitcoinCoreMixin)
|
|
|
|
|
self.shutdown_signal = False |
|
|
|
|
self.destn_addr = self._rpc("getnewaddress", []) |
|
|
|
|
|
|
|
|
|
def estimate_fee_per_kb(self, N): |
|
|
|
|
def estimate_fee_per_kb(self, tx_fees: int) -> int: |
|
|
|
|
if not self.absurd_fees: |
|
|
|
|
return super().estimate_fee_per_kb(N) |
|
|
|
|
return super().estimate_fee_per_kb(tx_fees) |
|
|
|
|
else: |
|
|
|
|
return jm_single().config.getint("POLICY", |
|
|
|
|
"absurd_fee_per_kb") + 100 |
|
|
|
|
|