528 lines
18 KiB
Python
528 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
|