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