@ -5,14 +5,15 @@ import sys
import time
import time
from abc import ABC , abstractmethod
from abc import ABC , abstractmethod
from decimal import Decimal
from decimal import Decimal
from typing import Optional , Tuple
from typing import *
from twisted . internet import reactor , task
from twisted . internet import reactor , task
import jmbitcoin as btc
import jmbitcoin as btc
from jmbase import bintohex , hextobin , stop_reactor
from jmbase import bintohex , hextobin , stop_reactor
from jmbase . support import get_log , jmprint , EXIT_FAILURE
from jmbase . support import get_log , jmprint , EXIT_FAILURE
from jmclient . configure import jm_single
from jmclient . configure import jm_single
from jmclient . jsonrpc import JsonRpcConnectionError , JsonRpcError
from jmclient . jsonrpc import JsonRpc , JsonRpc ConnectionError, JsonRpcError
# an inaccessible blockheight; consider rewriting in 1900 years
# an inaccessible blockheight; consider rewriting in 1900 years
@ -22,11 +23,11 @@ log = get_log()
class BlockchainInterface ( ABC ) :
class BlockchainInterface ( ABC ) :
def __init__ ( self ) :
def __init__ ( self ) - > None :
pass
pass
@abstractmethod
@abstractmethod
def is_address_imported ( self , addr ) :
def is_address_imported ( self , addr : str ) - > bool :
""" checks that address is already imported """
""" checks that address is already imported """
@abstractmethod
@abstractmethod
@ -34,28 +35,142 @@ class BlockchainInterface(ABC):
""" checks that UTXO belongs to the JM wallet """
""" checks that UTXO belongs to the JM wallet """
@abstractmethod
@abstractmethod
def pushtx ( self , txhex ) :
def pushtx ( self , txbin : bytes ) - > bool :
""" pushes tx to the network, returns False if failed """
""" Given a binary serialized valid bitcoin transaction,
broadcasts it to the network .
"""
@abstractmethod
@abstractmethod
def query_utxo_set ( self , txouts , includeconfs = False ) :
def query_utxo_set ( self ,
"""
txouts : Union [ Tuple [ bytes , int ] , List [ Tuple [ bytes , int ] ] ] ,
takes a utxo or a list of utxos
includeconfs : bool = False ,
returns None if they are spend or unconfirmed
include_mempool : bool = True ) - > List [ Optional [ dict ] ] :
otherwise returns value in satoshis , address and output script
""" If txout is either (a) a single utxo in (txidbin, n) form,
optionally return the coin age in number of blocks
or a list of the same , returns , as a list for each txout item ,
the result of gettxout from the bitcoind rpc for those utxos ;
if any utxo is invalid , None is returned .
includeconfs : if this is True , the current number of confirmations
of the prescribed utxo is included in the returned result dict .
include_mempool : if True , the contents of the mempool are included ;
this * both * means that utxos that are spent in in - mempool transactions
are * not * returned , * and * means that utxos that are created in the
mempool but have zero confirmations * are * returned .
If the utxo is of a non - standard type such that there is no address ,
the address field in the dict is None .
"""
"""
# address and output script contain the same information btw
@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
Optional [ Decimal ] with current rescan progress status . """
Optional [ Decimal ] with current rescan progress status . """
def import_addresses_if_needed ( self , addresses , wallet_name ) :
@abstractmethod
def rescanblockchain ( self , start_height : int , end_height : Optional [ int ] = None ) - > None :
""" Rescan the local blockchain for wallet related transactions.
"""
@abstractmethod
def import_addresses_if_needed ( self , addresses : Set [ str ] , wallet_name : str ) - > bool :
""" 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 """
@abstractmethod
def import_addresses ( self , addr_list : Iterable [ str ] , wallet_name : str ,
restart_cb : Optional [ Callable [ [ str ] , None ] ] = None ) - > None :
""" Imports addresses in a batch during initial sync.
Refuses to proceed if keys are found to be under control
of another account / label ( see console output ) , and quits .
"""
@abstractmethod
def list_transactions ( self , num : int , skip : int = 0 ) - > List [ dict ] :
""" Return a list of the last `num` transactions seen
in the wallet ( under any label / account ) , optionally
skipping some .
"""
@abstractmethod
def get_deser_from_gettransaction ( self , rpcretval : dict ) - > Optional [ btc . CMutableTransaction ] :
""" Get full transaction deserialization from a call
to get_transaction ( ) .
"""
@abstractmethod
def get_transaction ( self , txid : bytes ) - > Optional [ dict ] :
""" Argument txid is passed in binary.
Returns a serialized transaction for txid txid ,
in hex as returned by Bitcoin Core rpc , or None
if no transaction can be retrieved . Works also for
watch - only wallets .
"""
@abstractmethod
def get_block ( self , blockheight : int ) - > str :
""" Returns full hex serialized block at a given height.
"""
@abstractmethod
def get_current_block_height ( self ) - > int :
""" Returns the height of the most-work fully-validated chain.
"""
@abstractmethod
def get_best_block_hash ( self ) - > str :
""" Returns the hash of the best (tip) block in the most-work
fully - validated chain .
"""
@abstractmethod
def get_best_block_median_time ( self ) - > int :
""" Returns median time for the current best block.
"""
@abstractmethod
def get_block_height ( self , blockhash : str ) - > int :
""" Returns the block height for a specific block hash.
"""
@abstractmethod
def get_block_time ( self , blockhash : str ) - > int :
""" Returns the block time expressed in UNIX epoch time for a specific
block hash .
"""
@abstractmethod
def get_block_hash ( self , height : int ) - > str :
""" Returns hash of block in best-block-chain at height provided.
"""
@abstractmethod
def get_tx_merkle_branch ( self , txid : str ,
blockhash : Optional [ str ] = None ) - > bytes :
""" TODO: describe method.
"""
@abstractmethod
def verify_tx_merkle_branch ( self , txid : str , block_height : int ,
merkle_branch : bytes ) - > bool :
""" TODO: describe method.
"""
@abstractmethod
def listaddressgroupings ( self ) - > list :
""" Lists groups of addresses which have had their common ownership
made public by common use as inputs or as the resulting change
in past transactions .
"""
@abstractmethod
def listunspent ( self , minconf : Optional [ int ] = None ) - > List [ dict ] :
""" Returns list of unspent transaction output info dicts,
optionally filtering by minimum confirmations .
"""
@abstractmethod
def testmempoolaccept ( self , rawtx : str ) - > bool :
""" Checks that raw transaction would be accepted by mempool.
"""
@abstractmethod
@abstractmethod
def _get_mempool_min_fee ( self ) - > Optional [ int ] :
def _get_mempool_min_fee ( self ) - > Optional [ int ] :
""" Returns minimum mempool fee as a floor to avoid relay problems
""" Returns minimum mempool fee as a floor to avoid relay problems
@ -70,6 +185,48 @@ class BlockchainInterface(ABC):
or None in case of error .
or None in case of error .
"""
"""
def yield_transactions ( self ) - > Generator [ dict , None , None ] :
""" Generates a lazily fetched sequence of transactions seen in the
wallet ( under any label / account ) , yielded in newest - first order . Care
is taken to avoid yielding duplicates even when new transactions are
actively being added to the wallet while the iteration is ongoing .
"""
num , skip = 1 , 0
txs = self . list_transactions ( num , skip )
if not txs :
return
yielded_tx = txs [ 0 ]
yield yielded_tx
while True :
num * = 2
txs = self . list_transactions ( num , skip )
if not txs :
return
try :
idx = [ ( tx [ ' txid ' ] , tx [ ' vout ' ] , tx [ ' category ' ] ) for tx in txs
] . index ( ( yielded_tx [ ' txid ' ] , yielded_tx [ ' vout ' ] ,
yielded_tx [ ' category ' ] ) )
except ValueError :
skip + = num
continue
for tx in reversed ( txs [ : idx ] ) :
yielded_tx = tx # inefficient but more obvious
yield yielded_tx
if len ( txs ) < num :
return
skip + = num - 1
def get_unspent_indices ( self , transaction : btc . CTransaction ) - > List [ int ] :
""" Given a CTransaction object, identify the list of
indices of outputs which are unspent ( returned as list of ints ) .
"""
bintxid = transaction . GetTxid ( ) [ : : - 1 ]
res = self . query_utxo_set ( [ ( bintxid , i ) for i in range (
len ( transaction . vout ) ) ] )
# QUS returns 'None' for spent outputs, so filter them out
# and return the indices of the others:
return [ i for i , val in enumerate ( res ) if val ]
def _fee_per_kb_has_been_manually_set ( self , tx_fees : int ) - > bool :
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
""" If the block target (tx_fees) is higher than 1000, interpret it
as manually set fee sats / kvB .
as manually set fee sats / kvB .
@ -147,15 +304,21 @@ class BlockchainInterface(ABC):
log . info ( msg + " : " + btc . fee_per_kb_to_str ( retval ) )
log . info ( msg + " : " + btc . fee_per_kb_to_str ( retval ) )
return int ( retval )
return int ( retval )
def core_proof_to_merkle_branch ( self , core_proof : str ) - > bytes :
core_proof = binascii . unhexlify ( core_proof )
#first 80 bytes of a proof given by core are just a block header
#so we can save space by replacing it with a 4-byte block height
return core_proof [ 80 : ]
class BitcoinCoreInterface ( BlockchainInterface ) :
class BitcoinCoreInterface ( BlockchainInterface ) :
def __init__ ( self , jsonRpc , network , wallet_name ) :
def __init__ ( self , jsonRpc : JsonRpc , network : str , wallet_name : str ) - > None :
super ( ) . __init__ ( )
super ( ) . __init__ ( )
self . jsonRpc = jsonRpc
self . jsonRpc = jsonRpc
blockchainInfo = self . _rpc ( " getblockchaininfo " , [ ] )
blockchainInfo = self . _rpc ( " getblockchaininfo " , [ ] )
if not blockchainInfo :
if not blockchainInfo :
# see note in BitcoinCoreInterface.rpc - here
# see note in BitcoinCoreInterface._ rpc() - here
# we have to create this object before reactor start,
# we have to create this object before reactor start,
# so reactor is not stopped, so we override the 'swallowing'
# so reactor is not stopped, so we override the 'swallowing'
# of the Exception that happened in self._rpc():
# of the Exception that happened in self._rpc():
@ -185,26 +348,23 @@ class BitcoinCoreInterface(BlockchainInterface):
" setting in joinmarket.cfg) instead. See docs/USAGE.md "
" setting in joinmarket.cfg) instead. See docs/USAGE.md "
" for details. " )
" for details. " )
def is_address_imported ( self , addr ) :
def is_address_imported ( self , addr : str ) - > bool :
return len ( self . _rpc ( ' getaddressinfo ' , [ addr ] ) [ ' labels ' ] ) > 0
return len ( self . _rpc ( ' getaddressinfo ' , [ addr ] ) [ ' labels ' ] ) > 0
def get_block ( self , blockheight ) :
def get_block ( self , blockheight : int ) - > str :
""" Returns full serialized block at a given height.
""" Returns full serialized block at a given height.
"""
"""
block_hash = self . get_block_hash ( blockheight )
block_hash = self . get_block_hash ( blockheight )
block = self . _rpc ( ' getblock ' , [ block_hash , False ] )
return self . _rpc ( ' getblock ' , [ block_hash , 0 ] )
if not block :
return False
return block
def rescanblockchain ( self , start_height : int , end_height : Optional [ int ] = None ) - > None :
def rescanblockchain ( self , start_height : int , end_height : Optional [ int ] = None ) - > None :
# Threading is not used in Joinmarket but due to blocking
# Threading is not used in Joinmarket but due to blocking
# nature of this very slow RPC call, we need to fire and forget.
# nature of this very slow RPC call, we need to fire and forget.
from threading import Thread
from threading import Thread
Thread ( target = self . rescan_in_thread , args = ( start_height , ) ,
Thread ( target = self . _ rescan_in_thread, args = ( start_height , ) ,
daemon = True ) . start ( )
daemon = True ) . start ( )
def rescan_in_thread ( self , start_height : int ) - > None :
def _ rescan_in_thread( self , start_height : int ) - > None :
""" In order to not conflict with the existing main
""" In order to not conflict with the existing main
JsonRPC connection in the main thread , this rescanning
JsonRPC connection in the main thread , this rescanning
thread creates a distinct JsonRPC object , just to make
thread creates a distinct JsonRPC object , just to make
@ -239,7 +399,7 @@ class BitcoinCoreInterface(BlockchainInterface):
else :
else :
return False , None
return False , None
def _rpc ( self , method : str , args : Optional [ list ] = None ) :
def _rpc ( self , method : str , args : Union [ dict , list ] = [ ] ) - > Any :
""" 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
@ -274,15 +434,11 @@ class BitcoinCoreInterface(BlockchainInterface):
# so this is handled elsewhere in BitcoinCoreInterface.
# so this is handled elsewhere in BitcoinCoreInterface.
return res
return res
def is_address_labeled ( self , utxo , walletname ) :
def is_address_labeled ( self , utxo : dict , walletname : str ) - > bool :
return ( " label " in utxo and utxo [ " label " ] == walletname )
return ( " label " in utxo and utxo [ " label " ] == walletname )
def import_addresses ( self , addr_list , wallet_name , restart_cb = None ) :
def import_addresses ( self , addr_list : Iterable [ str ] , wallet_name : str ,
""" Imports addresses in a batch during initial sync.
restart_cb : Callable [ [ str ] , None ] = None ) - > None :
Refuses to proceed if keys are found to be under control
of another account / label ( see console output ) , and quits .
Do NOT use for in - run imports , use rpc ( ' importaddress ' , . . ) instead .
"""
requests = [ ]
requests = [ ]
for addr in addr_list :
for addr in addr_list :
requests . append ( {
requests . append ( {
@ -313,7 +469,7 @@ class BitcoinCoreInterface(BlockchainInterface):
jmprint ( fatal_msg , " important " )
jmprint ( fatal_msg , " important " )
sys . exit ( EXIT_FAILURE )
sys . exit ( EXIT_FAILURE )
def import_addresses_if_needed ( self , addresses , wallet_name ) :
def import_addresses_if_needed ( self , addresses : Set [ str ] , wallet_name : str ) - > bool :
if wallet_name in self . _rpc ( ' listlabels ' , [ ] ) :
if wallet_name in self . _rpc ( ' listlabels ' , [ ] ) :
imported_addresses = set ( self . _rpc ( ' getaddressesbylabel ' ,
imported_addresses = set ( self . _rpc ( ' getaddressesbylabel ' ,
[ wallet_name ] ) . keys ( ) )
[ wallet_name ] ) . keys ( ) )
@ -324,61 +480,17 @@ class BitcoinCoreInterface(BlockchainInterface):
self . import_addresses ( addresses - imported_addresses , wallet_name )
self . import_addresses ( addresses - imported_addresses , wallet_name )
return import_needed
return import_needed
def _yield_transactions ( self ) :
def get_deser_from_gettransaction ( self , rpcretval : dict ) - > Optional [ btc . CMutableTransaction ] :
""" Generates a lazily fetched sequence of transactions seen in the
wallet ( under any label / account ) , yielded in newest - first order . Care
is taken to avoid yielding duplicates even when new transactions are
actively being added to the wallet while the iteration is ongoing .
"""
num , skip = 1 , 0
txs = self . list_transactions ( num , skip )
if not txs :
return
yielded_tx = txs [ 0 ]
yield yielded_tx
while True :
num * = 2
txs = self . list_transactions ( num , skip )
if not txs :
return
try :
idx = [ ( tx [ ' txid ' ] , tx [ ' vout ' ] , tx [ ' category ' ] ) for tx in txs
] . index ( ( yielded_tx [ ' txid ' ] , yielded_tx [ ' vout ' ] ,
yielded_tx [ ' category ' ] ) )
except ValueError :
skip + = num
continue
for tx in reversed ( txs [ : idx ] ) :
yielded_tx = tx # inefficient but more obvious
yield yielded_tx
if len ( txs ) < num :
return
skip + = num - 1
def get_deser_from_gettransaction ( self , rpcretval ) :
""" Get full transaction deserialization from a call
to ` gettransaction `
"""
if not " hex " in rpcretval :
if not " hex " in rpcretval :
log . info ( " Malformed gettransaction output " )
log . info ( " Malformed gettransaction output " )
return None
return None
return btc . CMutableTransaction . deserialize (
return btc . CMutableTransaction . deserialize (
hextobin ( rpcretval [ " hex " ] ) )
hextobin ( rpcretval [ " hex " ] ) )
def list_transactions ( self , num , skip = 0 ) :
def list_transactions ( self , num : int , skip : int = 0 ) - > List [ dict ] :
""" Return a list of the last `num` transactions seen
in the wallet ( under any label / account ) , optionally
skipping some .
"""
return self . _rpc ( " listtransactions " , [ " * " , num , skip , True ] )
return self . _rpc ( " listtransactions " , [ " * " , num , skip , True ] )
def get_transaction ( self , txid ) :
def get_transaction ( self , txid : bytes ) - > Optional [ dict ] :
""" Argument txid is passed in binary.
Returns a serialized transaction for txid txid ,
in hex as returned by Bitcoin Core rpc , or None
if no transaction can be retrieved . Works also for
watch - only wallets .
"""
htxid = bintohex ( txid )
htxid = bintohex ( txid )
try :
try :
res = self . _rpc ( " gettransaction " , [ htxid , True ] )
res = self . _rpc ( " gettransaction " , [ htxid , True ] )
@ -398,7 +510,7 @@ class BitcoinCoreInterface(BlockchainInterface):
return None
return None
return res
return res
def pushtx ( self , txbin ) :
def pushtx ( self , txbin : bytes ) - > bool :
""" Given a binary serialized valid bitcoin transaction,
""" Given a binary serialized valid bitcoin transaction,
broadcasts it to the network .
broadcasts it to the network .
"""
"""
@ -413,24 +525,14 @@ class BitcoinCoreInterface(BlockchainInterface):
return False
return False
return True
return True
def query_utxo_set ( self , txout , includeconfs = False , include_mempool = True ) :
def query_utxo_set ( self ,
""" If txout is either (a) a single utxo in (txidbin, n) form,
txouts : Union [ Tuple [ bytes , int ] , List [ Tuple [ bytes , int ] ] ] ,
or a list of the same , returns , as a list for each txout item ,
includeconfs : bool = False ,
the result of gettxout from the bitcoind rpc for those utxos ;
include_mempool : bool = True ) - > List [ Optional [ dict ] ] :
if any utxo is invalid , None is returned .
if not isinstance ( txouts , list ) :
includeconfs : if this is True , the current number of confirmations
txouts = [ txouts ]
of the prescribed utxo is included in the returned result dict .
include_mempool : if True , the contents of the mempool are included ;
this * both * means that utxos that are spent in in - mempool transactions
are * not * returned , * and * means that utxos that are created in the
mempool but have zero confirmations * are * returned .
If the utxo is of a non - standard type such that there is no address ,
the address field in the dict is None .
"""
if not isinstance ( txout , list ) :
txout = [ txout ]
result = [ ]
result = [ ]
for txo in txout :
for txo in txouts :
txo_hex = bintohex ( txo [ 0 ] )
txo_hex = bintohex ( txo [ 0 ] )
if len ( txo_hex ) != 64 :
if len ( txo_hex ) != 64 :
log . warn ( " Invalid utxo format, ignoring: {} " . format ( txo ) )
log . warn ( " Invalid utxo format, ignoring: {} " . format ( txo ) )
@ -454,17 +556,6 @@ class BitcoinCoreInterface(BlockchainInterface):
result . append ( result_dict )
result . append ( result_dict )
return result
return result
def get_unspent_indices ( self , transaction ) :
""" Given a CTransaction object, identify the list of
indices of outputs which are unspent ( returned as list of ints ) .
"""
bintxid = transaction . GetTxid ( ) [ : : - 1 ]
res = self . query_utxo_set ( [ ( bintxid , i ) for i in range (
len ( transaction . vout ) ) ] )
# QUS returns 'None' for spent outputs, so filter them out
# and return the indices of the others:
return [ i for i , val in enumerate ( res ) if val ]
def _get_mempool_min_fee ( self ) - > Optional [ int ] :
def _get_mempool_min_fee ( self ) - > Optional [ int ] :
rpc_result = self . _rpc ( ' getmempoolinfo ' )
rpc_result = self . _rpc ( ' getmempoolinfo ' )
if not rpc_result :
if not rpc_result :
@ -493,34 +584,33 @@ class BitcoinCoreInterface(BlockchainInterface):
log . warn ( " Could not source a fee estimate from Core " )
log . warn ( " Could not source a fee estimate from Core " )
return None
return None
def get_current_block_height ( self ) :
def get_current_block_height ( self ) - > int :
try :
try :
res = self . _rpc ( " getblockcount " , [ ] )
return self . _rpc ( " getblockcount " , [ ] )
except JsonRpcError as e :
except JsonRpcError as e :
log . e rror( " Getblockcount RPC failed with: %i , %s " % (
raise RuntimeE rror( " Getblockcount RPC failed with: %i , %s " % (
e . code , e . message ) )
e . code , e . message ) )
res = None
return res
def get_best_block_hash ( self ) :
def get_best_block_hash ( self ) - > str :
return self . _rpc ( ' getbestblockhash ' , [ ] )
return self . _rpc ( ' getbestblockhash ' , [ ] )
def get_best_block_median_time ( self ) :
def get_best_block_median_time ( self ) - > int :
return self . _rpc ( ' getblockchaininfo ' , [ ] ) [ ' mediantime ' ]
return self . _rpc ( ' getblockchaininfo ' , [ ] ) [ ' mediantime ' ]
def _get_block_header_data ( self , blockhash , key ) :
def _get_block_header_data ( self , blockhash : str , key : str ) - > Any :
return self . _rpc ( ' getblockheader ' , [ blockhash ] ) [ key ]
return self . _rpc ( ' getblockheader ' , [ blockhash ] ) [ key ]
def get_block_height ( self , blockhash ) :
def get_block_height ( self , blockhash : str ) - > int :
return self . _get_block_header_data ( blockhash , ' height ' )
return self . _get_block_header_data ( blockhash , ' height ' )
def get_block_time ( self , blockhash ) :
def get_block_time ( self , blockhash : str ) - > int :
return self . _get_block_header_data ( blockhash , ' time ' )
return self . _get_block_header_data ( blockhash , ' time ' )
def get_block_hash ( self , height ) :
def get_block_hash ( self , height : int ) - > str :
return self . _rpc ( " getblockhash " , [ height ] )
return self . _rpc ( " getblockhash " , [ height ] )
def get_tx_merkle_branch ( self , txid , blockhash = None ) :
def get_tx_merkle_branch ( self , txid : str ,
blockhash : Optional [ str ] = None ) - > bytes :
if not blockhash :
if not blockhash :
tx = self . _rpc ( " gettransaction " , [ txid ] )
tx = self . _rpc ( " gettransaction " , [ txid ] )
if tx [ " confirmations " ] < 1 :
if tx [ " confirmations " ] < 1 :
@ -532,23 +622,18 @@ class BitcoinCoreInterface(BlockchainInterface):
raise ValueError ( " Block containing transaction is pruned " )
raise ValueError ( " Block containing transaction is pruned " )
return self . core_proof_to_merkle_branch ( core_proof )
return self . core_proof_to_merkle_branch ( core_proof )
def core_proof_to_merkle_branch ( self , core_proof ) :
def verify_tx_merkle_branch ( self , txid : str , block_height : int ,
core_proof = binascii . unhexlify ( core_proof )
merkle_branch : bytes ) - > bool :
#first 80 bytes of a proof given by core are just a block header
#so we can save space by replacing it with a 4-byte block height
return core_proof [ 80 : ]
def verify_tx_merkle_branch ( self , txid , block_height , merkle_branch ) :
block_hash = self . get_block_hash ( block_height )
block_hash = self . get_block_hash ( block_height )
core_proof = self . _rpc ( " getblockheader " , [ block_hash , False ] ) + \
core_proof = self . _rpc ( " getblockheader " , [ block_hash , False ] ) + \
binascii . hexlify ( merkle_branch ) . decode ( )
binascii . hexlify ( merkle_branch ) . decode ( )
ret = self . _rpc ( " verifytxoutproof " , [ core_proof ] )
ret = self . _rpc ( " verifytxoutproof " , [ core_proof ] )
return len ( ret ) == 1 and ret [ 0 ] == txid
return len ( ret ) == 1 and ret [ 0 ] == txid
def listaddressgroupings ( self ) :
def listaddressgroupings ( self ) - > list :
return self . _rpc ( ' listaddressgroupings ' , [ ] )
return self . _rpc ( ' listaddressgroupings ' , [ ] )
def listunspent ( self , minconf = None ) :
def listunspent ( self , minconf : Optional [ int ] = None ) - > List [ dict ] :
listunspent_args = [ ]
listunspent_args = [ ]
if ' listunspent_args ' in jm_single ( ) . config . options ( ' POLICY ' ) :
if ' listunspent_args ' in jm_single ( ) . config . options ( ' POLICY ' ) :
listunspent_args = ast . literal_eval ( jm_single ( ) . config . get (
listunspent_args = ast . literal_eval ( jm_single ( ) . config . get (
@ -557,8 +642,9 @@ class BitcoinCoreInterface(BlockchainInterface):
listunspent_args [ 0 ] = minconf
listunspent_args [ 0 ] = minconf
return self . _rpc ( ' listunspent ' , listunspent_args )
return self . _rpc ( ' listunspent ' , listunspent_args )
def testmempoolaccept ( self , rawtx ) :
def testmempoolaccept ( self , rawtx : str ) - > bool :
return self . _rpc ( ' testmempoolaccept ' , [ [ rawtx ] ] )
res = self . _rpc ( ' testmempoolaccept ' , [ [ rawtx ] ] )
return res [ 0 ] [ " allowed " ]
class RegtestBitcoinCoreMixin ( ) :
class RegtestBitcoinCoreMixin ( ) :
@ -566,7 +652,7 @@ class RegtestBitcoinCoreMixin():
This Mixin provides helper functions that are used in Interface classes
This Mixin provides helper functions that are used in Interface classes
requiring some functionality only useful on the regtest network .
requiring some functionality only useful on the regtest network .
"""
"""
def tick_forward_chain ( self , n ) :
def tick_forward_chain ( self , n : int ) - > None :
"""
"""
Special method for regtest only ;
Special method for regtest only ;
instruct to mine n blocks .
instruct to mine n blocks .
@ -581,7 +667,7 @@ class RegtestBitcoinCoreMixin():
" Failed to generate blocks, looks like the bitcoin daemon \
" Failed to generate blocks, looks like the bitcoin daemon \
has been shut down . Ignoring . " )
has been shut down . Ignoring . " )
def grab_coins ( self , receiving_addr , amt = 50 ) :
def grab_coins ( self , receiving_addr : str , amt : int = 50 ) - > str :
"""
"""
NOTE ! amt is passed in Coins , not Satoshis !
NOTE ! amt is passed in Coins , not Satoshis !
Special method for regtest only :
Special method for regtest only :
@ -610,17 +696,17 @@ class RegtestBitcoinCoreMixin():
class BitcoinCoreNoHistoryInterface ( BitcoinCoreInterface , RegtestBitcoinCoreMixin ) :
class BitcoinCoreNoHistoryInterface ( BitcoinCoreInterface , RegtestBitcoinCoreMixin ) :
def __init__ ( self , jsonRpc , network , wallet_name ) :
def __init__ ( self , jsonRpc : JsonRpc , network : str , wallet_name : str ) - > None :
super ( ) . __init__ ( jsonRpc , network , wallet_name )
super ( ) . __init__ ( jsonRpc , network , wallet_name )
self . import_addresses_call_count = 0
self . import_addresses_call_count = 0
self . wallet_name = None
self . wallet_name = None
self . scan_result = None
self . scan_result = None
def import_addresses_if_needed ( self , addresses , wallet_name ) :
def import_addresses_if_needed ( self , addresses : Set [ str ] , wallet_name : str ) - > bool :
self . import_addresses_call_count + = 1
self . import_addresses_call_count + = 1
if self . import_addresses_call_count == 1 :
if self . import_addresses_call_count == 1 :
self . wallet_name = wallet_name
self . wallet_name = wallet_name
addr_list = [ " addr( " + a + " ) " for a in addresses ]
addr_list = [ btc . get_address_descriptor ( a ) for a in addresses ]
log . debug ( " Starting scan of UTXO set " )
log . debug ( " Starting scan of UTXO set " )
st = time . time ( )
st = time . time ( )
self . _rpc ( " scantxoutset " , [ " abort " , [ ] ] )
self . _rpc ( " scantxoutset " , [ " abort " , [ ] ] )
@ -635,19 +721,23 @@ class BitcoinCoreNoHistoryInterface(BitcoinCoreInterface, RegtestBitcoinCoreMixi
assert False
assert False
return False
return False
def _ yield_transactions( self ) :
def yield_transactions ( self ) - > Generator [ dict , None , None ] :
for u in self . scan_result [ " unspents " ] :
for u in self . scan_result [ " unspents " ] :
tx = { " category " : " receive " , " address " :
tx = { " category " : " receive " , " address " :
btc . get_address_from_descriptor ( u [ " desc " ] ) }
btc . get_address_from_descriptor ( u [ " desc " ] ) }
yield tx
yield tx
def list_transactions ( self , num ) :
def list_transactions ( self , num : int , skip : int = 0 ) - > List [ dict ] :
return [ ]
return [ ]
def listaddressgroupings ( self ) :
def listaddressgroupings ( self ) - > list :
raise RuntimeError ( " default sync not supported by bitcoin-rpc-nohistory, use --recoversync " )
raise RuntimeError ( " default sync not supported by bitcoin-rpc-nohistory, use --recoversync " )
def listunspent ( self ) :
def listunspent ( self , minconf : Optional [ int ] = None ) - > List [ dict ] :
if minconf == 0 :
log . warning (
" Unconfirmed transactions are not seen by "
" bitcoin-rpc-nohistory. " )
return [ {
return [ {
" address " : btc . get_address_from_descriptor ( u [ " desc " ] ) ,
" address " : btc . get_address_from_descriptor ( u [ " desc " ] ) ,
" label " : self . wallet_name ,
" label " : self . wallet_name ,
@ -658,7 +748,7 @@ class BitcoinCoreNoHistoryInterface(BitcoinCoreInterface, RegtestBitcoinCoreMixi
" amount " : u [ " amount " ]
" amount " : u [ " amount " ]
} for u in self . scan_result [ " unspents " ] ]
} for u in self . scan_result [ " unspents " ] ]
def set_wallet_no_history ( self , wallet ) :
def set_wallet_no_history ( self , wallet ) - > None :
#make wallet-tool not display any new addresses
#make wallet-tool not display any new addresses
#because no-history cant tell if an address is used and empty
#because no-history cant tell if an address is used and empty
#so this is necessary to avoid address reuse
#so this is necessary to avoid address reuse
@ -667,7 +757,7 @@ class BitcoinCoreNoHistoryInterface(BitcoinCoreInterface, RegtestBitcoinCoreMixi
# avoidance of address reuse
# avoidance of address reuse
wallet . disable_new_scripts = True
wallet . disable_new_scripts = True
def tick_forward_chain ( self , n ) :
def tick_forward_chain ( self , n : int ) - > None :
self . destn_addr = self . _rpc ( " getnewaddress " , [ ] )
self . destn_addr = self . _rpc ( " getnewaddress " , [ ] )
super ( ) . tick_forward_chain ( n )
super ( ) . tick_forward_chain ( n )
@ -678,7 +768,7 @@ class BitcoinCoreNoHistoryInterface(BitcoinCoreInterface, RegtestBitcoinCoreMixi
# with > 100 blocks.
# with > 100 blocks.
class RegtestBitcoinCoreInterface ( BitcoinCoreInterface , RegtestBitcoinCoreMixin ) : #pragma: no cover
class RegtestBitcoinCoreInterface ( BitcoinCoreInterface , RegtestBitcoinCoreMixin ) : #pragma: no cover
def __init__ ( self , jsonRpc , wallet_name ) :
def __init__ ( self , jsonRpc : JsonRpc , wallet_name : str ) - > None :
super ( ) . __init__ ( jsonRpc , ' regtest ' , wallet_name )
super ( ) . __init__ ( jsonRpc , ' regtest ' , wallet_name )
self . pushtx_failure_prob = 0
self . pushtx_failure_prob = 0
self . tick_forward_chain_interval = - 1
self . tick_forward_chain_interval = - 1
@ -694,7 +784,7 @@ class RegtestBitcoinCoreInterface(BitcoinCoreInterface, RegtestBitcoinCoreMixin)
return jm_single ( ) . config . getint ( " POLICY " ,
return jm_single ( ) . config . getint ( " POLICY " ,
" absurd_fee_per_kb " ) + 100
" absurd_fee_per_kb " ) + 100
def tickchain ( self ) :
def tickchain ( self ) - > None :
if self . tick_forward_chain_interval < 0 :
if self . tick_forward_chain_interval < 0 :
log . debug ( ' not ticking forward chain ' )
log . debug ( ' not ticking forward chain ' )
self . tickchainloop . stop ( )
self . tickchainloop . stop ( )
@ -704,26 +794,26 @@ class RegtestBitcoinCoreInterface(BitcoinCoreInterface, RegtestBitcoinCoreMixin)
return
return
self . tick_forward_chain ( 1 )
self . tick_forward_chain ( 1 )
def simulate_blocks ( self ) :
def simulate_blocks ( self ) - > None :
self . tickchainloop = task . LoopingCall ( self . tickchain )
self . tickchainloop = task . LoopingCall ( self . tickchain )
self . tickchainloop . start ( self . tick_forward_chain_interval )
self . tickchainloop . start ( self . tick_forward_chain_interval )
self . simulating = True
self . simulating = True
def pushtx ( self , txhex ) :
def pushtx ( self , txbin : bytes ) - > bool :
if self . pushtx_failure_prob != 0 and random . random ( ) < \
if self . pushtx_failure_prob != 0 and random . random ( ) < \
self . pushtx_failure_prob :
self . pushtx_failure_prob :
log . debug ( ' randomly not broadcasting %0.1f %% of the time ' %
log . debug ( ' randomly not broadcasting %0.1f %% of the time ' %
( self . pushtx_failure_prob * 100 ) )
( self . pushtx_failure_prob * 100 ) )
return True
return True
ret = super ( ) . pushtx ( txhex )
ret = super ( ) . pushtx ( txbin )
if not self . simulating and self . tick_forward_chain_interval > 0 :
if not self . simulating and self . tick_forward_chain_interval > 0 :
log . debug ( ' will call tfc after ' + str ( self . tick_forward_chain_interval ) + ' seconds. ' )
log . debug ( ' will call tfc after ' + str ( self . tick_forward_chain_interval ) + ' seconds. ' )
reactor . callLater ( self . tick_forward_chain_interval ,
reactor . callLater ( self . tick_forward_chain_interval ,
self . tick_forward_chain , 1 )
self . tick_forward_chain , 1 )
return ret
return ret
def get_received_by_addr ( self , addresses ) :
def get_received_by_addr ( self , addresses : List [ str ] ) - > dict :
# NB This will NOT return coinbase coins (but wont matter in our use
# NB This will NOT return coinbase coins (but wont matter in our use
# case). allow importaddress to fail in case the address is already
# case). allow importaddress to fail in case the address is already
# in the wallet
# in the wallet