@ -47,13 +47,6 @@ class BlockchainInterface(ABC):
"""
"""
# address and output script contain the same information btw
# 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
@abstractmethod
def get_wallet_rescan_status ( self ) - > Tuple [ bool , Optional [ Decimal ] ] :
def get_wallet_rescan_status ( self ) - > Tuple [ bool , Optional [ Decimal ] ] :
""" Returns pair of True/False is wallet currently rescanning and
""" 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
""" import addresses to the underlying blockchain interface if needed
returns True if the sync call needs to do a system exit """
returns True if the sync call needs to do a system exit """
def fee_per_kb_has_been_manually_set ( self , N ) :
@abstractmethod
''' if the ' block ' target is higher than 1000, interpret it
def _get_mempool_min_fee ( self ) - > Optional [ int ] :
as manually set fee / Kb .
""" Returns minimum mempool fee as a floor to avoid relay problems
'''
or None in case of error .
if N > 1000 :
"""
return True
@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 :
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 ) :
class BitcoinCoreInterface ( BlockchainInterface ) :
@ -164,7 +239,7 @@ class BitcoinCoreInterface(BlockchainInterface):
else :
else :
return False , None
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.
""" Returns the result of an rpc call to the Bitcoin Core RPC API.
If the connection is permanently or unrecognizably broken , None
If the connection is permanently or unrecognizably broken , None
is returned * and the reactor is shutdown * ( because we consider this
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:
# and return the indices of the others:
return [ i for i , val in enumerate ( res ) if val ]
return [ i for i , val in enumerate ( res ) if val ]
def estimate_fee_per_kb ( self , N ) :
def _get_mempool_min_fee ( self ) - > Optional [ int ] :
""" The argument N may be either a number of blocks target,
rpc_result = self . _rpc ( ' getmempoolinfo ' )
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 )
if not rpc_result :
if not rpc_result :
# in case of connection error:
# in case of connection error:
return None
return None
return btc . btc_to_sat ( rpc_result [ ' mempoolminfee ' ] )
tx_fees_factor = abs ( jm_single ( ) . config . getfloat ( ' POLICY ' , ' tx_fees_factor ' ) )
def _estimate_fee_basic ( self , conf_target : int ) - > Optional [ int ] :
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 )
# Special bitcoin core case: sometimes the highest priority
# Special bitcoin core case: sometimes the highest priority
# cannot be estimated in that case the 2nd highest priority
# cannot be estimated in that case the 2nd highest priority
# should be used instead of falling back to hardcoded values
# 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 ) :
for i in range ( tries ) :
rpc_result = self . _rpc ( ' estimatesmartfee ' , [ N + i ] )
rpc_result = self . _rpc ( ' estimatesmartfee ' , [ conf_target + i ] )
if not rpc_result :
if not rpc_result :
# in case of connection error:
# in case of connection error:
return None
return None
@ -447,29 +488,10 @@ class BitcoinCoreInterface(BlockchainInterface):
# if it is not able to make an estimate. We insist that
# if it is not able to make an estimate. We insist that
# the 'feerate' key is found and contains a positive value:
# the 'feerate' key is found and contains a positive value:
if estimate and estimate > 0 :
if estimate and estimate > 0 :
estimate_in_sat = btc . btc_to_sat ( estimate )
return btc . btc_to_sat ( estimate )
retval = random . uniform ( estimate_in_sat ,
# cannot get a valid estimate after `tries` tries:
estimate_in_sat * float ( 1 + tx_fees_factor ) )
log . warn ( " Could not source a fee estimate from Core " )
break
return None
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 )
def get_current_block_height ( self ) :
def get_current_block_height ( self ) :
try :
try :
@ -665,9 +687,9 @@ class RegtestBitcoinCoreInterface(BitcoinCoreInterface, RegtestBitcoinCoreMixin)
self . shutdown_signal = False
self . shutdown_signal = False
self . destn_addr = self . _rpc ( " getnewaddress " , [ ] )
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 :
if not self . absurd_fees :
return super ( ) . estimate_fee_per_kb ( N )
return super ( ) . estimate_fee_per_kb ( tx_fees )
else :
else :
return jm_single ( ) . config . getint ( " POLICY " ,
return jm_single ( ) . config . getint ( " POLICY " ,
" absurd_fee_per_kb " ) + 100
" absurd_fee_per_kb " ) + 100