4 changed files with 297 additions and 3 deletions
@ -0,0 +1,290 @@ |
|||||||
|
import btc |
||||||
|
import json |
||||||
|
import Queue |
||||||
|
import os |
||||||
|
import pprint |
||||||
|
import random |
||||||
|
import socket |
||||||
|
import threading |
||||||
|
import time |
||||||
|
from .blockchaininterface import BlockchainInterface, is_index_ahead_of_cache |
||||||
|
from .configure import get_p2pk_vbyte |
||||||
|
from .support import get_log |
||||||
|
|
||||||
|
log = get_log() |
||||||
|
|
||||||
|
# Default server list from electrum client |
||||||
|
# https://github.com/spesmilo/electrum/blob/753a28b452dca1023fbde548469c36a34555dc95/lib/network.py |
||||||
|
DEFAULT_ELECTRUM_SERVER_LIST = [ |
||||||
|
'erbium1.sytes.net:50001', |
||||||
|
'ecdsa.net:50001', |
||||||
|
'electrum0.electricnewyear.net:50001', |
||||||
|
'VPS.hsmiths.com:50001', |
||||||
|
'ELECTRUM.jdubya.info:50001', |
||||||
|
'electrum.no-ip.org:50001', |
||||||
|
'us.electrum.be:50001', |
||||||
|
'bitcoins.sk:50001', |
||||||
|
'electrum.petrkr.net:50001', |
||||||
|
'electrum.dragonzone.net:50001', |
||||||
|
'Electrum.hsmiths.com:8080', |
||||||
|
'electrum3.hachre.de:50001', |
||||||
|
'elec.luggs.co:80', |
||||||
|
'btc.smsys.me:110', |
||||||
|
'electrum.online:50001', |
||||||
|
] |
||||||
|
|
||||||
|
class ElectrumInterface(BlockchainInterface): |
||||||
|
|
||||||
|
class ElectrumConn(threading.Thread): |
||||||
|
|
||||||
|
def __init__(self, electrum_server): |
||||||
|
threading.Thread.__init__(self) |
||||||
|
self.daemon = True |
||||||
|
self.msg_id = 0 |
||||||
|
self.RetQueue = Queue.Queue() |
||||||
|
try: |
||||||
|
self.s = socket.create_connection((electrum_server.split(':')[0], |
||||||
|
int(electrum_server.split(':')[1]))) |
||||||
|
except Exception as e: |
||||||
|
log.error("Error connecting to electrum server. " |
||||||
|
"Try again to connect to a random server or set a " |
||||||
|
"server in the config.") |
||||||
|
os._exit(1) |
||||||
|
self.ping() |
||||||
|
|
||||||
|
def run(self): |
||||||
|
while True: |
||||||
|
all_data = None |
||||||
|
while True: |
||||||
|
data = self.s.recv(1024) |
||||||
|
if data is None: |
||||||
|
continue |
||||||
|
if all_data is None: |
||||||
|
all_data = data |
||||||
|
else: |
||||||
|
all_data = all_data + data |
||||||
|
if '\n' in all_data: |
||||||
|
break |
||||||
|
data_json = json.loads(all_data[:-1].decode()) |
||||||
|
self.RetQueue.put(data_json) |
||||||
|
|
||||||
|
def ping(self): |
||||||
|
log.debug('sending server ping') |
||||||
|
self.send_json({'id':0,'method':'server.version','params':[]}) |
||||||
|
t = threading.Timer(60, self.ping) |
||||||
|
t.daemon = True |
||||||
|
t.start() |
||||||
|
|
||||||
|
def send_json(self, json_data): |
||||||
|
data = json.dumps(json_data).encode() |
||||||
|
self.s.send(data + b'\n') |
||||||
|
|
||||||
|
def call_server_method(self, method, params=[]): |
||||||
|
self.msg_id = self.msg_id + 1 |
||||||
|
current_id = self.msg_id |
||||||
|
method_dict = { |
||||||
|
'id': current_id, |
||||||
|
'method': method, |
||||||
|
'params': params |
||||||
|
} |
||||||
|
self.send_json(method_dict) |
||||||
|
while True: |
||||||
|
ret_data = self.RetQueue.get() |
||||||
|
if ret_data.get('id', None) == current_id: |
||||||
|
return ret_data |
||||||
|
else: |
||||||
|
log.debug(json.dumps(ret_data)) |
||||||
|
|
||||||
|
def __init__(self, testnet=False, electrum_server=None): |
||||||
|
super(ElectrumInterface, self).__init__() |
||||||
|
|
||||||
|
if testnet: |
||||||
|
raise Exception(NotImplemented) |
||||||
|
if electrum_server is None: |
||||||
|
electrum_server = random.choice(DEFAULT_ELECTRUM_SERVER_LIST) |
||||||
|
self.server_domain = electrum_server.split(':')[0] |
||||||
|
self.last_sync_unspent = 0 |
||||||
|
self.electrum_conn = self.ElectrumConn(electrum_server) |
||||||
|
self.electrum_conn.start() |
||||||
|
# used to hold open server conn |
||||||
|
self.electrum_conn.call_server_method('blockchain.numblocks.subscribe') |
||||||
|
|
||||||
|
def get_from_electrum(self, method, params=[]): |
||||||
|
params = [params] if type(params) is not list else params |
||||||
|
return self.electrum_conn.call_server_method(method, params) |
||||||
|
|
||||||
|
def sync_addresses(self, wallet): |
||||||
|
log.debug("downloading wallet history from electrum server") |
||||||
|
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)): |
||||||
|
addr = wallet.get_new_addr(mix_depth, forchange) |
||||||
|
addr_hist_info = self.get_from_electrum('blockchain.address.get_history', addr) |
||||||
|
if len(addr_hist_info['result']) > 0: |
||||||
|
last_used_addr = addr |
||||||
|
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 5 minutes |
||||||
|
rate_limit_time = 5 * 60 |
||||||
|
if st - self.last_sync_unspent < rate_limit_time: |
||||||
|
log.debug('electrum sync_unspent() happened too recently (%dsec), skipping' % (st - self.last_sync_unspent)) |
||||||
|
return |
||||||
|
wallet.unspent = {} |
||||||
|
addrs = wallet.addr_cache.keys() |
||||||
|
if len(addrs) == 0: |
||||||
|
log.debug('no tx used') |
||||||
|
return |
||||||
|
for a in addrs: |
||||||
|
unspent_info = self.get_from_electrum('blockchain.address.listunspent', a) |
||||||
|
res = unspent_info['result'] |
||||||
|
for u in res: |
||||||
|
wallet.unspent[str(u['tx_hash']) + ':' + str(u['tx_pos'])] = {'address': a, 'value': int(u['value'])} |
||||||
|
for u in wallet.spent_utxos: |
||||||
|
wallet.unspent.pop(u, None) |
||||||
|
self.last_sync_unspent = time.time() |
||||||
|
log.debug('electrum sync_unspent took ' + str((self.last_sync_unspent - st)) + 'sec') |
||||||
|
|
||||||
|
def add_tx_notify(self, txd, unconfirmfun, confirmfun, notifyaddr): |
||||||
|
unconfirm_timeout = 10 * 60 # seconds |
||||||
|
unconfirm_poll_period = 5 |
||||||
|
confirm_timeout = 2 * 60 * 60 |
||||||
|
confirm_poll_period = 5 * 60 |
||||||
|
|
||||||
|
class NotifyThread(threading.Thread): |
||||||
|
|
||||||
|
def __init__(self, blockchaininterface, txd, unconfirmfun, confirmfun): |
||||||
|
threading.Thread.__init__(self) |
||||||
|
self.daemon = True |
||||||
|
self.blockchaininterface = blockchaininterface |
||||||
|
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], get_p2pk_vbyte()) 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 |
||||||
|
shared_txid = None |
||||||
|
for a in self.output_addresses: |
||||||
|
unconftx = self.blockchaininterface.get_from_electrum('blockchain.address.get_mempool', a).get('result') |
||||||
|
unconftxs = set([str(t['tx_hash']) for t in unconftx]) |
||||||
|
if not shared_txid: |
||||||
|
shared_txid = unconftxs |
||||||
|
else: |
||||||
|
shared_txid = shared_txid.intersection(unconftxs) |
||||||
|
log.debug('sharedtxid = ' + str(shared_txid)) |
||||||
|
if len(shared_txid) == 0: |
||||||
|
continue |
||||||
|
data = [] |
||||||
|
for txid in shared_txid: |
||||||
|
txdata = str(self.blockchaininterface.get_from_electrum('blockchain.transaction.get', txid).get('result')) |
||||||
|
data.append({'hex':txdata,'id':txid}) |
||||||
|
for txdata in data: |
||||||
|
txhex = txdata['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 = txdata['id'] |
||||||
|
unconfirmed_txhex = txhex |
||||||
|
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') |
||||||
|
return |
||||||
|
shared_txid = None |
||||||
|
for a in self.output_addresses: |
||||||
|
conftx = self.blockchaininterface.get_from_electrum('blockchain.address.listunspent', a).get('result') |
||||||
|
conftxs = set([str(t['tx_hash']) for t in conftx]) |
||||||
|
if not shared_txid: |
||||||
|
shared_txid = conftxs |
||||||
|
else: |
||||||
|
shared_txid = shared_txid.intersection(conftxs) |
||||||
|
log.debug('sharedtxid = ' + str(shared_txid)) |
||||||
|
if len(shared_txid) == 0: |
||||||
|
continue |
||||||
|
data = [] |
||||||
|
for txid in shared_txid: |
||||||
|
txdata = str(self.blockchaininterface.get_from_electrum('blockchain.transaction.get', txid).get('result')) |
||||||
|
data.append({'hex':txdata,'id':txid}) |
||||||
|
for txdata in data: |
||||||
|
txhex = txdata['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 = txdata['id'] |
||||||
|
confirmed_txhex = txhex |
||||||
|
break |
||||||
|
self.confirmfun(btc.deserialize(confirmed_txhex), confirmed_txid, 1) |
||||||
|
|
||||||
|
NotifyThread(self, txd, unconfirmfun, confirmfun).start() |
||||||
|
|
||||||
|
def pushtx(self, txhex): |
||||||
|
brcst_res = self.get_from_electrum('blockchain.transaction.broadcast', txhex) |
||||||
|
brcst_status = brcst_res['result'] |
||||||
|
if isinstance(brcst_status, str) and len(brcst_status) == 64: |
||||||
|
return (True, brcst_status) |
||||||
|
log.debug(brcst_status) |
||||||
|
return (False, None) |
||||||
|
|
||||||
|
def query_utxo_set(self, txout, includeconf=False): |
||||||
|
self.current_height = self.get_from_electrum( |
||||||
|
"blockchain.numblocks.subscribe")['result'] |
||||||
|
if not isinstance(txout, list): |
||||||
|
txout = [txout] |
||||||
|
utxos = [[t[:64],int(t[65:])] for t in txout] |
||||||
|
result = [] |
||||||
|
for ut in utxos: |
||||||
|
address = self.get_from_electrum("blockchain.utxo.get_address", ut)['result'] |
||||||
|
utxo_info = self.get_from_electrum("blockchain.address.listunspent", address)['result'] |
||||||
|
utxo = None |
||||||
|
for u in utxo_info: |
||||||
|
if u['tx_hash'] == ut[0] and u['tx_pos'] == ut[1]: |
||||||
|
utxo = u |
||||||
|
if utxo is None: |
||||||
|
raise Exception("UTXO Not Found") |
||||||
|
r = { |
||||||
|
'value': utxo['value'], |
||||||
|
'address': address, |
||||||
|
'script': btc.address_to_script(address) |
||||||
|
} |
||||||
|
if includeconf: |
||||||
|
if int(utxo['height']) in [0, -1]: |
||||||
|
#-1 means unconfirmed inputs |
||||||
|
r['confirms'] = 0 |
||||||
|
else: |
||||||
|
#+1 because if current height = tx height, that's 1 conf |
||||||
|
r['confirms'] = int(self.current_height) - int( |
||||||
|
utxo['height']) + 1 |
||||||
|
result.append(r) |
||||||
|
return result |
||||||
|
|
||||||
|
def estimate_fee_per_kb(self, N): |
||||||
|
fee_info = self.get_from_electrum('blockchain.estimatefee', N) |
||||||
|
fee = fee_info.get('result') |
||||||
|
fee_per_kb_sat = int(float(fee) * 100000000) |
||||||
|
return fee_per_kb_sat |
||||||
|
|
||||||
Loading…
Reference in new issue