1#!/usr/bin/python
2import json, re
3import random
4import sys
5try:
6    from urllib.request import build_opener
7except:
8    from urllib2 import build_opener
9
10
11# Makes a request to a given URL (first arg) and optional params (second arg)
12def make_request(*args):
13    opener = build_opener()
14    opener.addheaders = [('User-agent',
15                          'Mozilla/5.0'+str(random.randrange(1000000)))]
16    try:
17        return opener.open(*args).read().strip()
18    except Exception as e:
19        try:
20            p = e.read().strip()
21        except:
22            p = e
23        raise Exception(p)
24
25
26def is_testnet(inp):
27    '''Checks if inp is a testnet address or if UTXO is a known testnet TxID'''
28    if isinstance(inp, (list, tuple)) and len(inp) >= 1:
29        return any([is_testnet(x) for x in inp])
30    elif not isinstance(inp, basestring):    # sanity check
31        raise TypeError("Input must be str/unicode, not type %s" % str(type(inp)))
32
33    if not inp or (inp.lower() in ("btc", "testnet")):
34        pass
35
36    ## ADDRESSES
37    if inp[0] in "123mn":
38        if re.match("^[2mn][a-km-zA-HJ-NP-Z0-9]{26,33}$", inp):
39            return True
40        elif re.match("^[13][a-km-zA-HJ-NP-Z0-9]{26,33}$", inp):
41            return False
42        else:
43            #sys.stderr.write("Bad address format %s")
44            return None
45
46    ## TXID
47    elif re.match('^[0-9a-fA-F]{64}$', inp):
48        base_url = "http://api.blockcypher.com/v1/btc/{network}/txs/{txid}?includesHex=false"
49        try:
50            # try testnet fetchtx
51            make_request(base_url.format(network="test3", txid=inp.lower()))
52            return True
53        except:
54            # try mainnet fetchtx
55            make_request(base_url.format(network="main", txid=inp.lower()))
56            return False
57        sys.stderr.write("TxID %s has no match for testnet or mainnet (Bad TxID)")
58        return None
59    else:
60        raise TypeError("{0} is unknown input".format(inp))
61
62
63def set_network(*args):
64    '''Decides if args for unspent/fetchtx/pushtx are mainnet or testnet'''
65    r = []
66    for arg in args:
67        if not arg:
68            pass
69        if isinstance(arg, basestring):
70            r.append(is_testnet(arg))
71        elif isinstance(arg, (list, tuple)):
72            return set_network(*arg)
73    if any(r) and not all(r):
74        raise Exception("Mixed Testnet/Mainnet queries")
75    return "testnet" if any(r) else "btc"
76
77
78def parse_addr_args(*args):
79    # Valid input formats: unspent([addr1, addr2, addr3])
80    #                      unspent([addr1, addr2, addr3], network)
81    #                      unspent(addr1, addr2, addr3)
82    #                      unspent(addr1, addr2, addr3, network)
83    addr_args = args
84    network = "btc"
85    if len(args) == 0:
86        return [], 'btc'
87    if len(args) >= 1 and args[-1] in ('testnet', 'btc'):
88        network = args[-1]
89        addr_args = args[:-1]
90    if len(addr_args) == 1 and isinstance(addr_args, list):
91        network = set_network(*addr_args[0])
92        addr_args = addr_args[0]
93    if addr_args and isinstance(addr_args, tuple) and isinstance(addr_args[0], list):
94        addr_args = addr_args[0]
95    network = set_network(addr_args)
96    return addr_args, network   # note params are "reversed" now
97
98
99# Gets the unspent outputs of one or more addresses
100def bci_unspent(*args):
101    addrs, network = parse_addr_args(*args)
102    u = []
103    for a in addrs:
104        try:
105            data = make_request('https://blockchain.info/unspent?active='+a)
106        except Exception as e:
107            if str(e) == 'No free outputs to spend':
108                continue
109            else:
110                raise Exception(e)
111        try:
112            jsonobj = json.loads(data.decode("utf-8"))
113            for o in jsonobj["unspent_outputs"]:
114                h = o['tx_hash'].decode('hex')[::-1].encode('hex')
115                u.append({
116                    "output": h+':'+str(o['tx_output_n']),
117                    "value": o['value']
118                })
119        except:
120            raise Exception("Failed to decode data: "+data)
121    return u
122
123
124def blockr_unspent(*args):
125    # Valid input formats: blockr_unspent([addr1, addr2,addr3])
126    #                      blockr_unspent(addr1, addr2, addr3)
127    #                      blockr_unspent([addr1, addr2, addr3], network)
128    #                      blockr_unspent(addr1, addr2, addr3, network)
129    # Where network is 'btc' or 'testnet'
130    network, addr_args = parse_addr_args(*args)
131
132    if network == 'testnet':
133        blockr_url = 'http://tbtc.blockr.io/api/v1/address/unspent/'
134    elif network == 'btc':
135        blockr_url = 'http://btc.blockr.io/api/v1/address/unspent/'
136    else:
137        raise Exception(
138            'Unsupported network {0} for blockr_unspent'.format(network))
139
140    if len(addr_args) == 0:
141        return []
142    elif isinstance(addr_args[0], list):
143        addrs = addr_args[0]
144    else:
145        addrs = addr_args
146    res = make_request(blockr_url+','.join(addrs))
147    data = json.loads(res.decode("utf-8"))['data']
148    o = []
149    if 'unspent' in data:
150        data = [data]
151    for dat in data:
152        for u in dat['unspent']:
153            o.append({
154                "output": u['tx']+':'+str(u['n']),
155                "value": int(u['amount'].replace('.', ''))
156            })
157    return o
158
159
160def helloblock_unspent(*args):
161    addrs, network = parse_addr_args(*args)
162    if network == 'testnet':
163        url = 'https://testnet.helloblock.io/v1/addresses/%s/unspents?limit=500&offset=%s'
164    elif network == 'btc':
165        url = 'https://mainnet.helloblock.io/v1/addresses/%s/unspents?limit=500&offset=%s'
166    o = []
167    for addr in addrs:
168        for offset in xrange(0, 10**9, 500):
169            res = make_request(url % (addr, offset))
170            data = json.loads(res.decode("utf-8"))["data"]
171            if not len(data["unspents"]):
172                break
173            elif offset:
174                sys.stderr.write("Getting more unspents: %d\n" % offset)
175            for dat in data["unspents"]:
176                o.append({
177                    "output": dat["txHash"]+':'+str(dat["index"]),
178                    "value": dat["value"],
179                })
180    return o
181
182
183unspent_getters = {
184    'bci': bci_unspent,
185    'blockr': blockr_unspent,
186    'helloblock': helloblock_unspent
187}
188
189
190def unspent(*args, **kwargs):
191    f = unspent_getters.get(kwargs.get('source', ''), bci_unspent)
192    return f(*args)
193
194
195# Gets the transaction output history of a given set of addresses,
196# including whether or not they have been spent
197def history(*args):
198    # Valid input formats: history([addr1, addr2,addr3])
199    #                      history(addr1, addr2, addr3)
200    if len(args) == 0:
201        return []
202    elif isinstance(args[0], list):
203        addrs = args[0]
204    else:
205        addrs = args
206
207    txs = []
208    for addr in addrs:
209        offset = 0
210        while 1:
211            gathered = False
212            while not gathered:
213                try:
214                    data = make_request(
215                        'https://blockchain.info/address/%s?format=json&offset=%s' %
216                        (addr, offset))
217                    gathered = True
218                except Exception as e:
219                    try:
220                        sys.stderr.write(e.read().strip())
221                    except:
222                        sys.stderr.write(str(e))
223                    gathered = False
224            try:
225                jsonobj = json.loads(data.decode("utf-8"))
226            except:
227                raise Exception("Failed to decode data: "+data)
228            txs.extend(jsonobj["txs"])
229            if len(jsonobj["txs"]) < 50:
230                break
231            offset += 50
232            sys.stderr.write("Fetching more transactions... "+str(offset)+'\n')
233    outs = {}
234    for tx in txs:
235        for o in tx["out"]:
236            if o.get('addr', None) in addrs:
237                key = str(tx["tx_index"])+':'+str(o["n"])
238                outs[key] = {
239                    "address": o["addr"],
240                    "value": o["value"],
241                    "output": tx["hash"]+':'+str(o["n"]),
242                    "block_height": tx.get("block_height", None)
243                }
244    for tx in txs:
245        for i, inp in enumerate(tx["inputs"]):
246            if "prev_out" in inp:
247                if inp["prev_out"].get("addr", None) in addrs:
248                    key = str(inp["prev_out"]["tx_index"]) + \
249                        ':'+str(inp["prev_out"]["n"])
250                    if outs.get(key):
251                        outs[key]["spend"] = tx["hash"]+':'+str(i)
252    return [outs[k] for k in outs]
253
254
255# Pushes a transaction to the network using https://blockchain.info/pushtx
256def bci_pushtx(tx):
257    if not re.match('^[0-9a-fA-F]*$', tx):
258        tx = tx.encode('hex')
259    return make_request('https://blockchain.info/pushtx', 'tx='+tx)
260
261
262def eligius_pushtx(tx):
263    if not re.match('^[0-9a-fA-F]*$', tx):
264        tx = tx.encode('hex')
265    s = make_request(
266        'http://eligius.st/~wizkid057/newstats/pushtxn.php',
267        'transaction='+tx+'&send=Push')
268    strings = re.findall('string[^"]*"[^"]*"', s)
269    for string in strings:
270        quote = re.findall('"[^"]*"', string)[0]
271        if len(quote) >= 5:
272            return quote[1:-1]
273
274
275def blockr_pushtx(tx, network='btc'):
276    if network == 'testnet':
277        blockr_url = 'http://tbtc.blockr.io/api/v1/tx/push'
278    elif network == 'btc':
279        blockr_url = 'http://btc.blockr.io/api/v1/tx/push'
280    else:
281        raise Exception(
282            'Unsupported network {0} for blockr_pushtx'.format(network))
283
284    if not re.match('^[0-9a-fA-F]*$', tx):
285        tx = tx.encode('hex')
286    return make_request(blockr_url, '{"hex":"%s"}' % tx)
287
288
289def helloblock_pushtx(tx):
290    if not re.match('^[0-9a-fA-F]*$', tx):
291        tx = tx.encode('hex')
292    return make_request('https://mainnet.helloblock.io/v1/transactions',
293                        'rawTxHex='+tx)
294
295pushtx_getters = {
296    'bci': bci_pushtx,
297    'blockr': blockr_pushtx,
298    'helloblock': helloblock_pushtx
299}
300
301
302def pushtx(*args, **kwargs):
303    f = pushtx_getters.get(kwargs.get('source', ''), bci_pushtx)
304    return f(*args)
305
306
307def last_block_height(network='btc'):
308    if network == 'testnet':
309        data = make_request('http://tbtc.blockr.io/api/v1/block/info/last')
310        jsonobj = json.loads(data.decode("utf-8"))
311        return jsonobj["data"]["nb"]
312
313    data = make_request('https://blockchain.info/latestblock')
314    jsonobj = json.loads(data.decode("utf-8"))
315    return jsonobj["height"]
316
317
318# Gets a specific transaction
319def bci_fetchtx(txhash):
320    if isinstance(txhash, list):
321        return [bci_fetchtx(h) for h in txhash]
322    if not re.match('^[0-9a-fA-F]*$', txhash):
323        txhash = txhash.encode('hex')
324    data = make_request('https://blockchain.info/rawtx/'+txhash+'?format=hex')
325    return data
326
327
328def blockr_fetchtx(txhash, network='btc'):
329    if network == 'testnet':
330        blockr_url = 'http://tbtc.blockr.io/api/v1/tx/raw/'
331    elif network == 'btc':
332        blockr_url = 'http://btc.blockr.io/api/v1/tx/raw/'
333    else:
334        raise Exception(
335            'Unsupported network {0} for blockr_fetchtx'.format(network))
336    if isinstance(txhash, list):
337        txhash = ','.join([x.encode('hex') if not re.match('^[0-9a-fA-F]*$', x)
338                           else x for x in txhash])
339        jsondata = json.loads(make_request(blockr_url+txhash).decode("utf-8"))
340        return [d['tx']['hex'] for d in jsondata['data']]
341    else:
342        if not re.match('^[0-9a-fA-F]*$', txhash):
343            txhash = txhash.encode('hex')
344        jsondata = json.loads(make_request(blockr_url+txhash).decode("utf-8"))
345        return jsondata['data']['tx']['hex']
346
347
348def helloblock_fetchtx(txhash, network='btc'):
349    if isinstance(txhash, list):
350        return [helloblock_fetchtx(h) for h in txhash]
351    if not re.match('^[0-9a-fA-F]*$', txhash):
352        txhash = txhash.encode('hex')
353    if network == 'testnet':
354        url = 'https://testnet.helloblock.io/v1/transactions/'
355    elif network == 'btc':
356        url = 'https://mainnet.helloblock.io/v1/transactions/'
357    else:
358        raise Exception(
359            'Unsupported network {0} for helloblock_fetchtx'.format(network))
360    data = json.loads(make_request(url + txhash).decode("utf-8"))["data"]["transaction"]
361    o = {
362        "locktime": data["locktime"],
363        "version": data["version"],
364        "ins": [],
365        "outs": []
366    }
367    for inp in data["inputs"]:
368        o["ins"].append({
369            "script": inp["scriptSig"],
370            "outpoint": {
371                "index": inp["prevTxoutIndex"],
372                "hash": inp["prevTxHash"],
373            },
374            "sequence": 4294967295
375        })
376    for outp in data["outputs"]:
377        o["outs"].append({
378            "value": outp["value"],
379            "script": outp["scriptPubKey"]
380        })
381    from bitcoin.transaction import serialize
382    from bitcoin.transaction import txhash as TXHASH
383    tx = serialize(o)
384    assert TXHASH(tx) == txhash
385    return tx
386
387
388fetchtx_getters = {
389    'bci': bci_fetchtx,
390    'blockr': blockr_fetchtx,
391    'helloblock': helloblock_fetchtx
392}
393
394
395def fetchtx(*args, **kwargs):
396    f = fetchtx_getters.get(kwargs.get('source', ''), bci_fetchtx)
397    return f(*args)
398
399
400def firstbits(address):
401    if len(address) >= 25:
402        return make_request('https://blockchain.info/q/getfirstbits/'+address)
403    else:
404        return make_request(
405            'https://blockchain.info/q/resolvefirstbits/'+address)
406
407
408def get_block_at_height(height):
409    j = json.loads(make_request("https://blockchain.info/block-height/" +
410                   str(height)+"?format=json").decode("utf-8"))
411    for b in j['blocks']:
412        if b['main_chain'] is True:
413            return b
414    raise Exception("Block at this height not found")
415
416
417def _get_block(inp):
418    if len(str(inp)) < 64:
419        return get_block_at_height(inp)
420    else:
421        return json.loads(make_request(
422                          'https://blockchain.info/rawblock/'+inp).decode("utf-8"))
423
424
425def bci_get_block_header_data(inp):
426    j = _get_block(inp)
427    return {
428        'version': j['ver'],
429        'hash': j['hash'],
430        'prevhash': j['prev_block'],
431        'timestamp': j['time'],
432        'merkle_root': j['mrkl_root'],
433        'bits': j['bits'],
434        'nonce': j['nonce'],
435    }
436
437def blockr_get_block_header_data(height, network='btc'):
438    if network == 'testnet':
439        blockr_url = "http://tbtc.blockr.io/api/v1/block/raw/"
440    elif network == 'btc':
441        blockr_url = "http://btc.blockr.io/api/v1/block/raw/"
442    else:
443        raise Exception(
444            'Unsupported network {0} for blockr_get_block_header_data'.format(network))
445
446    k = json.loads(make_request(blockr_url + str(height)).decode("utf-8"))
447    j = k['data']
448    return {
449        'version': j['version'],
450        'hash': j['hash'],
451        'prevhash': j['previousblockhash'],
452        'timestamp': j['time'],
453        'merkle_root': j['merkleroot'],
454        'bits': int(j['bits'], 16),
455        'nonce': j['nonce'],
456    }
457
458
459def get_block_timestamp(height, network='btc'):
460    if network == 'testnet':
461        blockr_url = "http://tbtc.blockr.io/api/v1/block/info/"
462    elif network == 'btc':
463        blockr_url = "http://btc.blockr.io/api/v1/block/info/"
464    else:
465        raise Exception(
466            'Unsupported network {0} for get_block_timestamp'.format(network))
467
468    import time, calendar
469    if isinstance(height, list):
470        k = json.loads(make_request(blockr_url + ','.join([str(x) for x in height])).decode("utf-8"))
471        o = {x['nb']: calendar.timegm(time.strptime(x['time_utc'],
472             "%Y-%m-%dT%H:%M:%SZ")) for x in k['data']}
473        return [o[x] for x in height]
474    else:
475        k = json.loads(make_request(blockr_url + str(height)).decode("utf-8"))
476        j = k['data']['time_utc']
477        return calendar.timegm(time.strptime(j, "%Y-%m-%dT%H:%M:%SZ"))
478
479
480block_header_data_getters = {
481    'bci': bci_get_block_header_data,
482    'blockr': blockr_get_block_header_data
483}
484
485
486def get_block_header_data(inp, **kwargs):
487    f = block_header_data_getters.get(kwargs.get('source', ''),
488                                      bci_get_block_header_data)
489    return f(inp, **kwargs)
490
491
492def get_txs_in_block(inp):
493    j = _get_block(inp)
494    hashes = [t['hash'] for t in j['tx']]
495    return hashes
496
497
498def get_block_height(txhash):
499    j = json.loads(make_request('https://blockchain.info/rawtx/'+txhash).decode("utf-8"))
500    return j['block_height']
501
502# fromAddr, toAddr, 12345, changeAddress
503def get_tx_composite(inputs, outputs, output_value, change_address=None, network=None):
504    """mktx using blockcypher API"""
505    inputs = [inputs] if not isinstance(inputs, list) else inputs
506    outputs = [outputs] if not isinstance(outputs, list) else outputs
507    network = set_network(change_address or inputs) if not network else network.lower()
508    url = "http://api.blockcypher.com/v1/btc/{network}/txs/new?includeToSignTx=true".format(
509                  network=('test3' if network=='testnet' else 'main'))
510    is_address = lambda a: bool(re.match("^[123mn][a-km-zA-HJ-NP-Z0-9]{26,33}$", a))
511    if any([is_address(x) for x in inputs]):
512        inputs_type = 'addresses'        # also accepts UTXOs, only addresses supported presently
513    if any([is_address(x) for x in outputs]):
514        outputs_type = 'addresses'       # TODO: add UTXO support
515    data = {
516            'inputs':  [{inputs_type:  inputs}],
517            'confirmations': 0,
518            'preference': 'high',
519            'outputs': [{outputs_type: outputs, "value": output_value}]
520            }
521    if change_address:
522        data["change_address"] = change_address    #
523    jdata = json.loads(make_request(url, data))
524    hash, txh = jdata.get("tosign")[0], jdata.get("tosign_tx")[0]
525    assert bin_dbl_sha256(txh.decode('hex')).encode('hex') == hash, "checksum mismatch %s" % hash
526    return txh.encode("utf-8")
527
528blockcypher_mktx = get_tx_composite
529