@ -11,8 +11,6 @@ import re
import sys
import threading
import time
import urllib
import urllib2
import traceback
from decimal import Decimal
from twisted . internet import reactor , task
@ -98,339 +96,6 @@ class BlockchainInterface(object):
required for inclusion in the next N blocks .
'''
class BlockchaininfoInterface ( BlockchainInterface ) : #pragma: no cover
BCINFO_MAX_ADDR_REQ_COUNT = 20
def __init__ ( self , testnet = False ) :
super ( BlockchaininfoInterface , self ) . __init__ ( )
# blockchain.info doesn't support testnet via API :(
if testnet :
log . debug ( " Blockchain.info doesn ' t support testnet, "
" try blockr as blockchain source. Quitting. " )
return None
# see bci.py in bitcoin module
self . network = ' btc '
self . last_sync_unspent = 0
@staticmethod
def make_bci_request ( bci_api_req_url ) :
while True :
try :
log . info ( " making request to: " + str ( bci_api_req_url ) )
res = btc . make_request ( bci_api_req_url )
log . info ( " got result " )
except Exception as e :
if str ( e ) == ' No free outputs to spend ' :
return False
elif str ( e ) == ' Quota Exceeded (Req Count Limit) ' :
log . info ( ' Request count limit reached (+500 req in 5 minutes) ' )
log . info ( ' Waiting for 60 seconds before retrying ' )
time . sleep ( 60 )
continue
elif str ( e ) == ' Transaction not found ' :
log . info ( str ( e ) )
return False
elif str ( e ) == ' No Free Cluster Connection ' or str ( e ) == ' Maximum concurrent requests for this endpoint reached. Please try again shortly. ' or ' <!DOCTYPE html> ' in str ( e ) :
log . info ( ' Issues connecting to Blockchain.info API - waiting it out for 60s ' )
time . sleep ( 60 )
continue
else :
log . info ( Exception ( e ) )
continue
return res
def sync_addresses ( self , wallet ) :
log . info ( ' downloading wallet history ' )
# sets Wallet internal indexes to be at the next unused address
for mix_depth in range ( wallet . max_mix_depth ) :
for forchange in [ 0 , 1 ] :
unused_addr_count = 0
last_used_addr = ' '
while ( unused_addr_count < wallet . gaplimit or
not is_index_ahead_of_cache (
wallet , mix_depth , forchange ) ) :
addrs = [ wallet . get_new_addr ( mix_depth , forchange )
for _ in range ( self . BCINFO_MAX_ADDR_REQ_COUNT ) ]
bcinfo_url = ' https://blockchain.info/multiaddr?active= '
# request info on all addresses
res = self . make_bci_request ( bcinfo_url + ' | ' . join ( addrs ) )
data = json . loads ( res ) [ ' addresses ' ]
# get data returned in same order as the address list passed to the API
new_data = [ ]
for a in addrs :
new_data . append ( [ d for d in data if d [ ' address ' ] == a ] [ 0 ] )
# for each address in data
for addr in new_data :
if addr [ ' n_tx ' ] != 0 :
last_used_addr = addr [ ' address ' ]
unused_addr_count = 0
else :
unused_addr_count + = 1
if last_used_addr == ' ' :
wallet . index [ mix_depth ] [ forchange ] = 0
else :
wallet . index [ mix_depth ] [ forchange ] = wallet . addr_cache [
last_used_addr ] [
2 ] + 1
def sync_unspent ( self , wallet ) :
# finds utxos in the wallet
st = time . time ( )
# dont refresh unspent dict more often than 10 minutes
rate_limit_time = 10 * 60
if st - self . last_sync_unspent < rate_limit_time :
log . info (
' blockchaininfo sync_unspent() happened too recently ( %d sec), skipping '
% ( st - self . last_sync_unspent ) )
return
wallet . unspent = { }
addrs = wallet . addr_cache . keys ( )
if len ( addrs ) == 0 :
log . debug ( ' no tx used ' )
return
i = 0
count = 0
while i < len ( addrs ) :
inc = min ( len ( addrs ) - i , self . BCINFO_MAX_ADDR_REQ_COUNT )
req = addrs [ i : i + inc ]
i + = inc
bcinfo_url = ' https://blockchain.info/en/unspent?active= '
# Request unspent outputs in address chunks (20 at once)
res = self . make_bci_request ( bcinfo_url + ' | ' . join ( req ) )
count = count + 1
if res == False :
continue
# request data on addresses in chunks of five
req_blocks = [ ]
for j in range ( 0 , 20 , 5 ) :
req_blocks . append ( req [ j : j + 5 ] )
for req_block in req_blocks :
res = self . make_bci_request ( bcinfo_url + ' | ' . join ( req_block ) )
count = count + 1
if res == False :
continue
# If there are addresses in the chunk with unspent outputs, get data
for addr in req_block :
res = self . make_bci_request ( bcinfo_url + addr )
count = count + 1
if res == False :
continue
data = json . loads ( res ) [ ' unspent_outputs ' ]
# for each unspent output
for uo in data :
wallet . unspent [ uo [ ' tx_hash_big_endian ' ] + ' : ' + str ( uo [ ' tx_output_n ' ] ) ] = {
' address ' : addr ,
' value ' : int ( uo [ ' value ' ] ) }
for u in wallet . spent_utxos :
wallet . unspent . pop ( u , None )
self . last_sync_unspent = time . time ( )
log . info ( ' blockchaininfo sync_unspent took ' +
str ( ( self . last_sync_unspent - st ) ) + ' sec ' )
def add_tx_notify ( self , txd , unconfirmfun , confirmfun , notifyaddr , vb = None ) :
if not vb :
vb = get_p2pk_vbyte ( )
unconfirm_timeout = 10 * 60 # seconds
unconfirm_poll_period = 15
confirm_timeout = 2 * 60 * 60
confirm_poll_period = 5 * 60
class NotifyThread ( threading . Thread ) :
def __init__ ( self , txd , unconfirmfun , confirmfun ) :
threading . Thread . __init__ ( self )
self . daemon = True
self . unconfirmfun = unconfirmfun
self . confirmfun = confirmfun
self . tx_output_set = set ( [ ( sv [ ' script ' ] , sv [ ' value ' ] )
for sv in txd [ ' outs ' ] ] )
self . output_addresses = [
btc . script_to_address ( scrval [ 0 ] , vb )
for scrval in self . tx_output_set ]
log . debug ( ' txoutset= ' + pprint . pformat ( self . tx_output_set ) )
log . debug ( ' outaddrs= ' + ' , ' . join ( self . output_addresses ) )
def run ( self ) :
st = int ( time . time ( ) )
unconfirmed_txid = None
unconfirmed_txhex = None
while not unconfirmed_txid :
time . sleep ( unconfirm_poll_period )
if int ( time . time ( ) ) - st > unconfirm_timeout :
log . debug ( ' checking for unconfirmed tx timed out ' )
return
bcinfo_url = ' https://blockchain.info/unspent?active= '
random . shuffle ( self . output_addresses )
# create list for requests of all addresses
data_list = [ ]
#for each of the output addresses, request data and add it to list
for output_address in self . output_addresses :
res = BlockchaininfoInterface . make_bci_request ( bcinfo_url + output_address )
data = json . loads ( res )
data_list . append ( data )
shared_txid = None
for unspent_list in data_list :
txs = set ( [ str ( txdata [ ' tx_hash_big_endian ' ] )
for txdata in unspent_list [ ' unspent_outputs ' ] ] )
if not shared_txid :
shared_txid = txs
else :
shared_txid = shared_txid . intersection ( txs )
log . debug ( ' sharedtxid = ' + str ( shared_txid ) )
if len ( shared_txid ) == 0 :
continue
time . sleep ( 2 ) # here for some possible race conditions, maybe could do without
bcinfo_url = ' https://blockchain.info/rawtx/ '
# get data of tx and hex repressentation
# for each tx the outputs addresses share (should only be one?)
data_list = [ ]
for tx in shared_txid :
res = BlockchaininfoInterface . make_bci_request ( bcinfo_url + tx )
if res == False :
continue
data_tx = json . loads ( res )
res = BlockchaininfoInterface . make_bci_request ( bcinfo_url + tx + ' ?format=hex ' )
data_tx [ ' hex ' ] = str ( res )
data_list . append ( data_tx )
if not isinstance ( data_list , list ) :
data_list = [ data_list ]
for txinfo in data_list :
txhex = str ( txinfo [ ' hex ' ] )
outs = set ( [ ( sv [ ' script ' ] , sv [ ' value ' ] )
for sv in btc . deserialize ( txhex ) [ ' outs ' ] ] )
log . debug ( ' unconfirm query outs = ' + str ( outs ) )
if outs == self . tx_output_set :
unconfirmed_txid = txinfo [ ' hash ' ]
unconfirmed_txhex = str ( txinfo [ ' hex ' ] )
break
self . unconfirmfun (
btc . deserialize ( unconfirmed_txhex ) , unconfirmed_txid )
st = int ( time . time ( ) )
confirmed_txid = None
confirmed_txhex = None
bcinfo_url = ' https://blockchain.info/rawtx/ '
while not confirmed_txid :
time . sleep ( confirm_poll_period )
if int ( time . time ( ) ) - st > confirm_timeout :
log . debug ( ' checking for confirmed tx timed out ' )
return
for tx in shared_txid :
res = btc . make_request ( bcinfo_url + tx )
data = json . loads ( res )
# Not yet confirmed
if ' block_height ' not in data :
continue
else :
res = BlockchaininfoInterface . make_bci_request ( bcinfo_url + tx + ' ?format=hex ' )
txhex = str ( res )
outs = set ( [ ( sv [ ' script ' ] , sv [ ' value ' ] )
for sv in btc . deserialize ( txhex ) [ ' outs ' ] ] )
log . debug ( ' confirm query outs = ' + str ( outs ) )
if outs == self . tx_output_set :
confirmed_txid = txinfo [ ' hash ' ]
confirmed_txhex = str ( txinfo [ ' hex ' ] )
break
self . confirmfun (
btc . deserialize ( confirmed_txhex ) , confirmed_txid , 1 )
NotifyThread ( txd , unconfirmfun , confirmfun ) . start ( )
def pushtx ( self , txhex ) :
try :
json_str = btc . bci_pushtx ( txhex )
except Exception :
log . debug ( ' failed blockchain.info pushtx ' )
return None
txhash = btc . txhash ( txhex )
return txhash
def query_utxo_set ( self , txout , includeconf = False ) :
self . current_height = int ( self . make_bci_request ( " https://blockchain.info/q/getblockcount " ) )
log . info ( " Got block height: " + str ( self . current_height ) )
if not isinstance ( txout , list ) :
txout = [ txout ]
txids = [ h [ : 64 ] for h in txout ]
txids = list ( set ( txids ) )
txids = [ txids ]
bcinfo_url = ' https://blockchain.info/rawtx/ '
data = [ ]
for ids in txids :
for single_id in ids :
res = self . make_bci_request ( bcinfo_url + single_id )
if res == False :
continue
bci_data = json . loads ( res )
data . append ( bci_data )
result = [ ]
for txo in txout :
txdata = None
vout = None
for d in data :
if d [ ' hash ' ] == txo [ : 64 ] :
txdata = d
for v in txdata [ ' out ' ] :
if v [ ' n ' ] == int ( txo [ 65 : ] ) :
vout = v
if vout [ ' spent ' ] == 1 :
result . append ( None )
else :
result_dict = { ' value ' : int ( vout [ ' value ' ] ) ,
' address ' : vout [ ' addr ' ] ,
' script ' : vout [ ' script ' ] }
if includeconf :
if ' block_height ' not in txdata :
result_dict [ ' confirms ' ] = 0
else :
#+1 because if current height = tx height, that's 1 conf
result_dict [ ' confirms ' ] = int (
self . current_height ) - int ( txdata [ ' block_height ' ] ) + 1
log . debug ( " Got num confs: " + str ( result_dict [ ' confirms ' ] ) )
result . append ( result_dict )
return result
def estimate_fee_per_kb ( self , N ) :
bcypher_fee_estimate_url = ' https://api.blockcypher.com/v1/btc/main '
bcypher_data = json . loads ( btc . make_request ( bcypher_fee_estimate_url ) )
log . debug ( " Got blockcypher result: " + pprint . pformat ( bcypher_data ) )
if N < = 2 :
fee_per_kb = bcypher_data [ " high_fee_per_kb " ]
elif N < = 4 :
fee_per_kb = bcypher_data [ " medium_fee_per_kb " ]
else :
fee_per_kb = bcypher_data [ " low_fee_per_kb " ]
return fee_per_kb
class ElectrumWalletInterface ( BlockchainInterface ) : #pragma: no cover
""" A pseudo-blockchain interface using the existing
Electrum server connection in an Electrum wallet .
@ -522,291 +187,6 @@ class ElectrumWalletInterface(BlockchainInterface): #pragma: no cover
fee_per_kb_sat = int ( float ( fee ) * 100000000 )
return fee_per_kb_sat
class BlockrInterface ( BlockchainInterface ) : #pragma: no cover
BLOCKR_MAX_ADDR_REQ_COUNT = 20
def __init__ ( self , testnet = False ) :
super ( BlockrInterface , self ) . __init__ ( )
# see bci.py in bitcoin module
self . network = ' testnet ' if testnet else ' btc '
self . blockr_domain = ' tbtc ' if testnet else ' btc '
self . last_sync_unspent = 0
def sync_addresses ( self , wallet ) :
log . debug ( ' downloading wallet history ' )
# sets Wallet internal indexes to be at the next unused address
for mix_depth in range ( wallet . max_mix_depth ) :
for forchange in [ 0 , 1 ] :
#must reset at the start so as to search forward from the beginning
wallet . index [ mix_depth ] [ forchange ] = 0
unused_addr_count = 0
last_used_addr = ' '
while ( unused_addr_count < wallet . gaplimit or
not is_index_ahead_of_cache ( wallet , mix_depth ,
forchange ) ) :
addrs = [ wallet . get_new_addr ( mix_depth , forchange )
for _ in range ( self . BLOCKR_MAX_ADDR_REQ_COUNT ) ]
# TODO send a pull request to pybitcointools
# because this surely should be possible with a function from it
blockr_url = ' https:// ' + self . blockr_domain
blockr_url + = ' .blockr.io/api/v1/address/txs/ '
data = btc . make_request_blockr ( blockr_url + ' , ' . join (
addrs ) ) [ ' data ' ]
for dat in data :
if dat [ ' nb_txs ' ] != 0 :
last_used_addr = dat [ ' address ' ]
unused_addr_count = 0
else :
unused_addr_count + = 1
if last_used_addr == ' ' :
wallet . index [ mix_depth ] [ forchange ] = 0
else :
next_avail_idx = max ( [ wallet . addr_cache [ last_used_addr ] [
2 ] + 1 , wallet . index_cache [ mix_depth ] [ forchange ] ] )
wallet . index [ mix_depth ] [ forchange ] = next_avail_idx
def sync_unspent ( self , wallet ) :
# finds utxos in the wallet
st = time . time ( )
# dont refresh unspent dict more often than 1 minutes
rate_limit_time = 1 * 60
if st - self . last_sync_unspent < rate_limit_time :
log . debug (
' blockr sync_unspent() happened too recently ( %d sec), skipping '
% ( st - self . last_sync_unspent ) )
return
wallet . unspent = { }
addrs = wallet . addr_cache . keys ( )
if len ( addrs ) == 0 :
log . debug ( ' no tx used ' )
return
i = 0
while i < len ( addrs ) :
inc = min ( len ( addrs ) - i , self . BLOCKR_MAX_ADDR_REQ_COUNT )
req = addrs [ i : i + inc ]
i + = inc
# TODO send a pull request to pybitcointools
# unspent() doesnt tell you which address, you get a bunch of utxos
# but dont know which privkey to sign with
blockr_url = ' https:// ' + self . blockr_domain + \
' .blockr.io/api/v1/address/unspent/ '
data = btc . make_request_blockr ( blockr_url + ' , ' . join ( req ) ) [ ' data ' ]
if ' unspent ' in data :
data = [ data ]
for dat in data :
for u in dat [ ' unspent ' ] :
wallet . unspent [ u [ ' tx ' ] + ' : ' + str ( u [ ' n ' ] ) ] = {
' address ' : dat [ ' address ' ] ,
' value ' : int ( u [ ' amount ' ] . replace ( ' . ' , ' ' ) )
}
for u in wallet . spent_utxos :
wallet . unspent . pop ( u , None )
self . last_sync_unspent = time . time ( )
log . debug ( ' blockr sync_unspent took ' + str ( ( self . last_sync_unspent - st
) ) + ' sec ' )
def add_tx_notify ( self ,
txd ,
unconfirmfun ,
confirmfun ,
notifyaddr ,
timeoutfun = None ,
vb = None ) :
if not vb :
vb = get_p2pk_vbyte ( )
unconfirm_timeout = jm_single ( ) . config . getint ( ' TIMEOUT ' ,
' unconfirm_timeout_sec ' )
unconfirm_poll_period = 5
confirm_timeout = jm_single ( ) . config . getfloat (
' TIMEOUT ' , ' confirm_timeout_hours ' ) * 60 * 60
confirm_poll_period = 5 * 60
class NotifyThread ( threading . Thread ) :
def __init__ ( self , blockr_domain , txd , unconfirmfun , confirmfun ,
timeoutfun ) :
threading . Thread . __init__ ( self , name = ' BlockrNotifyThread ' )
self . daemon = True
self . blockr_domain = blockr_domain
self . unconfirmfun = unconfirmfun
self . confirmfun = confirmfun
self . timeoutfun = timeoutfun
self . tx_output_set = set ( [ ( sv [ ' script ' ] , sv [ ' value ' ] )
for sv in txd [ ' outs ' ] ] )
self . output_addresses = [
btc . script_to_address ( scrval [ 0 ] , vb )
for scrval in self . tx_output_set
]
log . debug ( ' txoutset= ' + pprint . pformat ( self . tx_output_set ) )
log . debug ( ' outaddrs= ' + ' , ' . join ( self . output_addresses ) )
def run ( self ) :
st = int ( time . time ( ) )
unconfirmed_txid = None
unconfirmed_txhex = None
while not unconfirmed_txid :
time . sleep ( unconfirm_poll_period )
if int ( time . time ( ) ) - st > unconfirm_timeout :
log . debug ( ' checking for unconfirmed tx timed out ' )
if self . timeoutfun :
self . timeoutfun ( False )
return
blockr_url = ' https:// ' + self . blockr_domain
blockr_url + = ' .blockr.io/api/v1/address/unspent/ '
random . shuffle ( self . output_addresses
) # seriously weird bug with blockr.io
data = btc . make_request_blockr ( blockr_url + ' , ' . join (
self . output_addresses ) + ' ?unconfirmed=1 ' ) [ ' data ' ]
shared_txid = None
for unspent_list in data :
txs = set ( [ str ( txdata [ ' tx ' ] )
for txdata in unspent_list [ ' unspent ' ] ] )
if not shared_txid :
shared_txid = txs
else :
shared_txid = shared_txid . intersection ( txs )
log . debug ( ' sharedtxid = ' + str ( shared_txid ) )
if len ( shared_txid ) == 0 :
continue
time . sleep (
2
) # here for some race condition bullshit with blockr.io
blockr_url = ' https:// ' + self . blockr_domain
blockr_url + = ' .blockr.io/api/v1/tx/raw/ '
data = btc . make_request_blockr ( blockr_url + ' , ' . join (
shared_txid ) ) [ ' data ' ]
if not isinstance ( data , list ) :
data = [ data ]
for txinfo in data :
txhex = str ( txinfo [ ' tx ' ] [ ' hex ' ] )
outs = set ( [ ( sv [ ' script ' ] , sv [ ' value ' ] )
for sv in btc . deserialize ( txhex ) [ ' outs ' ] ] )
log . debug ( ' unconfirm query outs = ' + str ( outs ) )
if outs == self . tx_output_set :
unconfirmed_txid = txinfo [ ' tx ' ] [ ' txid ' ]
unconfirmed_txhex = str ( txinfo [ ' tx ' ] [ ' hex ' ] )
break
self . unconfirmfun (
btc . deserialize ( unconfirmed_txhex ) , unconfirmed_txid )
st = int ( time . time ( ) )
confirmed_txid = None
confirmed_txhex = None
while not confirmed_txid :
time . sleep ( confirm_poll_period )
if int ( time . time ( ) ) - st > confirm_timeout :
log . debug ( ' checking for confirmed tx timed out ' )
if self . timeoutfun :
self . timeoutfun ( True )
return
blockr_url = ' https:// ' + self . blockr_domain
blockr_url + = ' .blockr.io/api/v1/address/txs/ '
data = btc . make_request_blockr ( blockr_url + ' , ' . join (
self . output_addresses ) ) [ ' data ' ]
shared_txid = None
for addrtxs in data :
txs = set ( str ( txdata [ ' tx ' ] )
for txdata in addrtxs [ ' txs ' ] )
if not shared_txid :
shared_txid = txs
else :
shared_txid = shared_txid . intersection ( txs )
log . debug ( ' sharedtxid = ' + str ( shared_txid ) )
if len ( shared_txid ) == 0 :
continue
blockr_url = ' https:// ' + self . blockr_domain
blockr_url + = ' .blockr.io/api/v1/tx/raw/ '
data = btc . make_request_blockr ( blockr_url + ' , ' . join (
shared_txid ) ) [ ' data ' ]
if not isinstance ( data , list ) :
data = [ data ]
for txinfo in data :
txhex = str ( txinfo [ ' tx ' ] [ ' hex ' ] )
outs = set ( [ ( sv [ ' script ' ] , sv [ ' value ' ] )
for sv in btc . deserialize ( txhex ) [ ' outs ' ] ] )
log . debug ( ' confirm query outs = ' + str ( outs ) )
if outs == self . tx_output_set :
confirmed_txid = txinfo [ ' tx ' ] [ ' txid ' ]
confirmed_txhex = str ( txinfo [ ' tx ' ] [ ' hex ' ] )
break
self . confirmfun (
btc . deserialize ( confirmed_txhex ) , confirmed_txid , 1 )
NotifyThread ( self . blockr_domain , txd , unconfirmfun , confirmfun ,
timeoutfun ) . start ( )
def pushtx ( self , txhex ) :
try :
json_str = btc . blockr_pushtx ( txhex , self . network )
data = json . loads ( json_str )
if data [ ' status ' ] != ' success ' :
log . debug ( data )
return False
except Exception :
log . debug ( ' failed blockr.io pushtx ' )
log . debug ( traceback . format_exc ( ) )
return False
return True
def query_utxo_set ( self , txout , includeconf = False ) :
if not isinstance ( txout , list ) :
txout = [ txout ]
txids = [ h [ : 64 ] for h in txout ]
txids = list ( set ( txids ) ) # remove duplicates
# self.BLOCKR_MAX_ADDR_REQ_COUNT = 2
if len ( txids ) > self . BLOCKR_MAX_ADDR_REQ_COUNT :
txids = chunks ( txids , self . BLOCKR_MAX_ADDR_REQ_COUNT )
else :
txids = [ txids ]
data = [ ]
for ids in txids :
blockr_url = ' https:// ' + self . blockr_domain + ' .blockr.io/api/v1/tx/info/ '
blockr_data = btc . make_request_blockr ( blockr_url + ' , ' . join ( ids ) ) [ ' data ' ]
if not isinstance ( blockr_data , list ) :
blockr_data = [ blockr_data ]
data + = blockr_data
result = [ ]
for txo in txout :
txdata_candidate = [ d for d in data if d [ ' tx ' ] == txo [ : 64 ] ]
if len ( txdata_candidate ) == 0 :
continue
txdata = txdata_candidate [ 0 ]
vout = [ v for v in txdata [ ' vouts ' ] if v [ ' n ' ] == int ( txo [ 65 : ] ) ] [ 0 ]
if " is_spent " in vout and vout [ ' is_spent ' ] == 1 :
result . append ( None )
else :
result_dict = { ' value ' : int ( Decimal ( vout [ ' amount ' ] ) *
Decimal ( ' 1e8 ' ) ) ,
' address ' : vout [ ' address ' ] ,
' script ' : vout [ ' extras ' ] [ ' script ' ] }
if includeconf :
result_dict [ ' confirms ' ] = int ( txdata [ ' confirmations ' ] )
result . append ( result_dict )
return result
def estimate_fee_per_kb ( self , N ) :
bcypher_fee_estimate_url = ' https://api.blockcypher.com/v1/btc/main '
bcypher_data = json . loads ( btc . make_request ( bcypher_fee_estimate_url ) )
log . debug ( " Got blockcypher result: " + pprint . pformat ( bcypher_data ) )
if N < = 2 :
fee_per_kb = bcypher_data [ " high_fee_per_kb " ]
elif N < = 4 :
fee_per_kb = bcypher_data [ " medium_fee_per_kb " ]
else :
fee_per_kb = bcypher_data [ " low_fee_per_kb " ]
return fee_per_kb
class BitcoinCoreInterface ( BlockchainInterface ) :
def __init__ ( self , jsonRpc , network ) :