ZeroNet/src/lib/pybitcointools/bci.py

529 lines
18 KiB
Python

#!/usr/bin/python
import json, re
import random
import sys
try:
from urllib.request import build_opener
except:
from urllib2 import build_opener
# Makes a request to a given URL (first arg) and optional params (second arg)
def make_request(*args):
opener = build_opener()
opener.addheaders = [('User-agent',
'Mozilla/5.0'+str(random.randrange(1000000)))]
try:
return opener.open(*args).read().strip()
except Exception as e:
try:
p = e.read().strip()
except:
p = e
raise Exception(p)
def is_testnet(inp):
'''Checks if inp is a testnet address or if UTXO is a known testnet TxID'''
if isinstance(inp, (list, tuple)) and len(inp) >= 1:
return any([is_testnet(x) for x in inp])
elif not isinstance(inp, basestring): # sanity check
raise TypeError("Input must be str/unicode, not type %s" % str(type(inp)))
if not inp or (inp.lower() in ("btc", "testnet")):
pass
## ADDRESSES
if inp[0] in "123mn":
if re.match("^[2mn][a-km-zA-HJ-NP-Z0-9]{26,33}$", inp):
return True
elif re.match("^[13][a-km-zA-HJ-NP-Z0-9]{26,33}$", inp):
return False
else:
#sys.stderr.write("Bad address format %s")
return None
## TXID
elif re.match('^[0-9a-fA-F]{64}$', inp):
base_url = "http://api.blockcypher.com/v1/btc/{network}/txs/{txid}?includesHex=false"
try:
# try testnet fetchtx
make_request(base_url.format(network="test3", txid=inp.lower()))
return True
except:
# try mainnet fetchtx
make_request(base_url.format(network="main", txid=inp.lower()))
return False
sys.stderr.write("TxID %s has no match for testnet or mainnet (Bad TxID)")
return None
else:
raise TypeError("{0} is unknown input".format(inp))
def set_network(*args):
'''Decides if args for unspent/fetchtx/pushtx are mainnet or testnet'''
r = []
for arg in args:
if not arg:
pass
if isinstance(arg, basestring):
r.append(is_testnet(arg))
elif isinstance(arg, (list, tuple)):
return set_network(*arg)
if any(r) and not all(r):
raise Exception("Mixed Testnet/Mainnet queries")
return "testnet" if any(r) else "btc"
def parse_addr_args(*args):
# Valid input formats: unspent([addr1, addr2, addr3])
# unspent([addr1, addr2, addr3], network)
# unspent(addr1, addr2, addr3)
# unspent(addr1, addr2, addr3, network)
addr_args = args
network = "btc"
if len(args) == 0:
return [], 'btc'
if len(args) >= 1 and args[-1] in ('testnet', 'btc'):
network = args[-1]
addr_args = args[:-1]
if len(addr_args) == 1 and isinstance(addr_args, list):
network = set_network(*addr_args[0])
addr_args = addr_args[0]
if addr_args and isinstance(addr_args, tuple) and isinstance(addr_args[0], list):
addr_args = addr_args[0]
network = set_network(addr_args)
return network, addr_args
# Gets the unspent outputs of one or more addresses
def bci_unspent(*args):
network, addrs = parse_addr_args(*args)
u = []
for a in addrs:
try:
data = make_request('https://blockchain.info/unspent?active='+a)
except Exception as e:
if str(e) == 'No free outputs to spend':
continue
else:
raise Exception(e)
try:
jsonobj = json.loads(data.decode("utf-8"))
for o in jsonobj["unspent_outputs"]:
h = o['tx_hash'].decode('hex')[::-1].encode('hex')
u.append({
"output": h+':'+str(o['tx_output_n']),
"value": o['value']
})
except:
raise Exception("Failed to decode data: "+data)
return u
def blockr_unspent(*args):
# Valid input formats: blockr_unspent([addr1, addr2,addr3])
# blockr_unspent(addr1, addr2, addr3)
# blockr_unspent([addr1, addr2, addr3], network)
# blockr_unspent(addr1, addr2, addr3, network)
# Where network is 'btc' or 'testnet'
network, addr_args = parse_addr_args(*args)
if network == 'testnet':
blockr_url = 'http://tbtc.blockr.io/api/v1/address/unspent/'
elif network == 'btc':
blockr_url = 'http://btc.blockr.io/api/v1/address/unspent/'
else:
raise Exception(
'Unsupported network {0} for blockr_unspent'.format(network))
if len(addr_args) == 0:
return []
elif isinstance(addr_args[0], list):
addrs = addr_args[0]
else:
addrs = addr_args
res = make_request(blockr_url+','.join(addrs))
data = json.loads(res.decode("utf-8"))['data']
o = []
if 'unspent' in data:
data = [data]
for dat in data:
for u in dat['unspent']:
o.append({
"output": u['tx']+':'+str(u['n']),
"value": int(u['amount'].replace('.', ''))
})
return o
def helloblock_unspent(*args):
addrs, network = parse_addr_args(*args)
if network == 'testnet':
url = 'https://testnet.helloblock.io/v1/addresses/%s/unspents?limit=500&offset=%s'
elif network == 'btc':
url = 'https://mainnet.helloblock.io/v1/addresses/%s/unspents?limit=500&offset=%s'
o = []
for addr in addrs:
for offset in xrange(0, 10**9, 500):
res = make_request(url % (addr, offset))
data = json.loads(res.decode("utf-8"))["data"]
if not len(data["unspents"]):
break
elif offset:
sys.stderr.write("Getting more unspents: %d\n" % offset)
for dat in data["unspents"]:
o.append({
"output": dat["txHash"]+':'+str(dat["index"]),
"value": dat["value"],
})
return o
unspent_getters = {
'bci': bci_unspent,
'blockr': blockr_unspent,
'helloblock': helloblock_unspent
}
def unspent(*args, **kwargs):
f = unspent_getters.get(kwargs.get('source', ''), bci_unspent)
return f(*args)
# Gets the transaction output history of a given set of addresses,
# including whether or not they have been spent
def history(*args):
# Valid input formats: history([addr1, addr2,addr3])
# history(addr1, addr2, addr3)
if len(args) == 0:
return []
elif isinstance(args[0], list):
addrs = args[0]
else:
addrs = args
txs = []
for addr in addrs:
offset = 0
while 1:
gathered = False
while not gathered:
try:
data = make_request(
'https://blockchain.info/address/%s?format=json&offset=%s' %
(addr, offset))
gathered = True
except Exception as e:
try:
sys.stderr.write(e.read().strip())
except:
sys.stderr.write(str(e))
gathered = False
try:
jsonobj = json.loads(data.decode("utf-8"))
except:
raise Exception("Failed to decode data: "+data)
txs.extend(jsonobj["txs"])
if len(jsonobj["txs"]) < 50:
break
offset += 50
sys.stderr.write("Fetching more transactions... "+str(offset)+'\n')
outs = {}
for tx in txs:
for o in tx["out"]:
if o.get('addr', None) in addrs:
key = str(tx["tx_index"])+':'+str(o["n"])
outs[key] = {
"address": o["addr"],
"value": o["value"],
"output": tx["hash"]+':'+str(o["n"]),
"block_height": tx.get("block_height", None)
}
for tx in txs:
for i, inp in enumerate(tx["inputs"]):
if "prev_out" in inp:
if inp["prev_out"].get("addr", None) in addrs:
key = str(inp["prev_out"]["tx_index"]) + \
':'+str(inp["prev_out"]["n"])
if outs.get(key):
outs[key]["spend"] = tx["hash"]+':'+str(i)
return [outs[k] for k in outs]
# Pushes a transaction to the network using https://blockchain.info/pushtx
def bci_pushtx(tx):
if not re.match('^[0-9a-fA-F]*$', tx):
tx = tx.encode('hex')
return make_request('https://blockchain.info/pushtx', 'tx='+tx)
def eligius_pushtx(tx):
if not re.match('^[0-9a-fA-F]*$', tx):
tx = tx.encode('hex')
s = make_request(
'http://eligius.st/~wizkid057/newstats/pushtxn.php',
'transaction='+tx+'&send=Push')
strings = re.findall('string[^"]*"[^"]*"', s)
for string in strings:
quote = re.findall('"[^"]*"', string)[0]
if len(quote) >= 5:
return quote[1:-1]
def blockr_pushtx(tx, network='btc'):
if network == 'testnet':
blockr_url = 'http://tbtc.blockr.io/api/v1/tx/push'
elif network == 'btc':
blockr_url = 'http://btc.blockr.io/api/v1/tx/push'
else:
raise Exception(
'Unsupported network {0} for blockr_pushtx'.format(network))
if not re.match('^[0-9a-fA-F]*$', tx):
tx = tx.encode('hex')
return make_request(blockr_url, '{"hex":"%s"}' % tx)
def helloblock_pushtx(tx):
if not re.match('^[0-9a-fA-F]*$', tx):
tx = tx.encode('hex')
return make_request('https://mainnet.helloblock.io/v1/transactions',
'rawTxHex='+tx)
pushtx_getters = {
'bci': bci_pushtx,
'blockr': blockr_pushtx,
'helloblock': helloblock_pushtx
}
def pushtx(*args, **kwargs):
f = pushtx_getters.get(kwargs.get('source', ''), bci_pushtx)
return f(*args)
def last_block_height(network='btc'):
if network == 'testnet':
data = make_request('http://tbtc.blockr.io/api/v1/block/info/last')
jsonobj = json.loads(data.decode("utf-8"))
return jsonobj["data"]["nb"]
data = make_request('https://blockchain.info/latestblock')
jsonobj = json.loads(data.decode("utf-8"))
return jsonobj["height"]
# Gets a specific transaction
def bci_fetchtx(txhash):
if isinstance(txhash, list):
return [bci_fetchtx(h) for h in txhash]
if not re.match('^[0-9a-fA-F]*$', txhash):
txhash = txhash.encode('hex')
data = make_request('https://blockchain.info/rawtx/'+txhash+'?format=hex')
return data
def blockr_fetchtx(txhash, network='btc'):
if network == 'testnet':
blockr_url = 'http://tbtc.blockr.io/api/v1/tx/raw/'
elif network == 'btc':
blockr_url = 'http://btc.blockr.io/api/v1/tx/raw/'
else:
raise Exception(
'Unsupported network {0} for blockr_fetchtx'.format(network))
if isinstance(txhash, list):
txhash = ','.join([x.encode('hex') if not re.match('^[0-9a-fA-F]*$', x)
else x for x in txhash])
jsondata = json.loads(make_request(blockr_url+txhash).decode("utf-8"))
return [d['tx']['hex'] for d in jsondata['data']]
else:
if not re.match('^[0-9a-fA-F]*$', txhash):
txhash = txhash.encode('hex')
jsondata = json.loads(make_request(blockr_url+txhash).decode("utf-8"))
return jsondata['data']['tx']['hex']
def helloblock_fetchtx(txhash, network='btc'):
if isinstance(txhash, list):
return [helloblock_fetchtx(h) for h in txhash]
if not re.match('^[0-9a-fA-F]*$', txhash):
txhash = txhash.encode('hex')
if network == 'testnet':
url = 'https://testnet.helloblock.io/v1/transactions/'
elif network == 'btc':
url = 'https://mainnet.helloblock.io/v1/transactions/'
else:
raise Exception(
'Unsupported network {0} for helloblock_fetchtx'.format(network))
data = json.loads(make_request(url + txhash).decode("utf-8"))["data"]["transaction"]
o = {
"locktime": data["locktime"],
"version": data["version"],
"ins": [],
"outs": []
}
for inp in data["inputs"]:
o["ins"].append({
"script": inp["scriptSig"],
"outpoint": {
"index": inp["prevTxoutIndex"],
"hash": inp["prevTxHash"],
},
"sequence": 4294967295
})
for outp in data["outputs"]:
o["outs"].append({
"value": outp["value"],
"script": outp["scriptPubKey"]
})
from .transaction import serialize
from .transaction import txhash as TXHASH
tx = serialize(o)
assert TXHASH(tx) == txhash
return tx
fetchtx_getters = {
'bci': bci_fetchtx,
'blockr': blockr_fetchtx,
'helloblock': helloblock_fetchtx
}
def fetchtx(*args, **kwargs):
f = fetchtx_getters.get(kwargs.get('source', ''), bci_fetchtx)
return f(*args)
def firstbits(address):
if len(address) >= 25:
return make_request('https://blockchain.info/q/getfirstbits/'+address)
else:
return make_request(
'https://blockchain.info/q/resolvefirstbits/'+address)
def get_block_at_height(height):
j = json.loads(make_request("https://blockchain.info/block-height/" +
str(height)+"?format=json").decode("utf-8"))
for b in j['blocks']:
if b['main_chain'] is True:
return b
raise Exception("Block at this height not found")
def _get_block(inp):
if len(str(inp)) < 64:
return get_block_at_height(inp)
else:
return json.loads(make_request(
'https://blockchain.info/rawblock/'+inp).decode("utf-8"))
def bci_get_block_header_data(inp):
j = _get_block(inp)
return {
'version': j['ver'],
'hash': j['hash'],
'prevhash': j['prev_block'],
'timestamp': j['time'],
'merkle_root': j['mrkl_root'],
'bits': j['bits'],
'nonce': j['nonce'],
}
def blockr_get_block_header_data(height, network='btc'):
if network == 'testnet':
blockr_url = "http://tbtc.blockr.io/api/v1/block/raw/"
elif network == 'btc':
blockr_url = "http://btc.blockr.io/api/v1/block/raw/"
else:
raise Exception(
'Unsupported network {0} for blockr_get_block_header_data'.format(network))
k = json.loads(make_request(blockr_url + str(height)).decode("utf-8"))
j = k['data']
return {
'version': j['version'],
'hash': j['hash'],
'prevhash': j['previousblockhash'],
'timestamp': j['time'],
'merkle_root': j['merkleroot'],
'bits': int(j['bits'], 16),
'nonce': j['nonce'],
}
def get_block_timestamp(height, network='btc'):
if network == 'testnet':
blockr_url = "http://tbtc.blockr.io/api/v1/block/info/"
elif network == 'btc':
blockr_url = "http://btc.blockr.io/api/v1/block/info/"
else:
raise Exception(
'Unsupported network {0} for get_block_timestamp'.format(network))
import time, calendar
if isinstance(height, list):
k = json.loads(make_request(blockr_url + ','.join([str(x) for x in height])).decode("utf-8"))
o = {x['nb']: calendar.timegm(time.strptime(x['time_utc'],
"%Y-%m-%dT%H:%M:%SZ")) for x in k['data']}
return [o[x] for x in height]
else:
k = json.loads(make_request(blockr_url + str(height)).decode("utf-8"))
j = k['data']['time_utc']
return calendar.timegm(time.strptime(j, "%Y-%m-%dT%H:%M:%SZ"))
block_header_data_getters = {
'bci': bci_get_block_header_data,
'blockr': blockr_get_block_header_data
}
def get_block_header_data(inp, **kwargs):
f = block_header_data_getters.get(kwargs.get('source', ''),
bci_get_block_header_data)
return f(inp, **kwargs)
def get_txs_in_block(inp):
j = _get_block(inp)
hashes = [t['hash'] for t in j['tx']]
return hashes
def get_block_height(txhash):
j = json.loads(make_request('https://blockchain.info/rawtx/'+txhash).decode("utf-8"))
return j['block_height']
# fromAddr, toAddr, 12345, changeAddress
def get_tx_composite(inputs, outputs, output_value, change_address=None, network=None):
"""mktx using blockcypher API"""
inputs = [inputs] if not isinstance(inputs, list) else inputs
outputs = [outputs] if not isinstance(outputs, list) else outputs
network = set_network(change_address or inputs) if not network else network.lower()
url = "http://api.blockcypher.com/v1/btc/{network}/txs/new?includeToSignTx=true".format(
network=('test3' if network=='testnet' else 'main'))
is_address = lambda a: bool(re.match("^[123mn][a-km-zA-HJ-NP-Z0-9]{26,33}$", a))
if any([is_address(x) for x in inputs]):
inputs_type = 'addresses' # also accepts UTXOs, only addresses supported presently
if any([is_address(x) for x in outputs]):
outputs_type = 'addresses' # TODO: add UTXO support
data = {
'inputs': [{inputs_type: inputs}],
'confirmations': 0,
'preference': 'high',
'outputs': [{outputs_type: outputs, "value": output_value}]
}
if change_address:
data["change_address"] = change_address #
jdata = json.loads(make_request(url, data))
hash, txh = jdata.get("tosign")[0], jdata.get("tosign_tx")[0]
assert bin_dbl_sha256(txh.decode('hex')).encode('hex') == hash, "checksum mismatch %s" % hash
return txh.encode("utf-8")
blockcypher_mktx = get_tx_composite