diff --git a/counterpartyd.py b/counterpartyd.py index 003dac746b..1b8a3dea94 100755 --- a/counterpartyd.py +++ b/counterpartyd.py @@ -207,7 +207,7 @@ def cli(method, params, unsigned): if not bitcoin.is_valid(source): raise exceptions.AddressError('Invalid address.') if bitcoin.is_mine(source): - bitcoin.wallet_unlock() + util.wallet_unlock() else: # TODO: Do this only if the encoding method needs it. print('Source not in backend wallet.') @@ -390,7 +390,7 @@ def set_options (data_dir=None, backend_rpc_connect=None, elif has_config and 'blockchain-service-name' in configfile['Default'] and configfile['Default']['blockchain-service-name']: config.BLOCKCHAIN_SERVICE_NAME = configfile['Default']['blockchain-service-name'] else: - config.BLOCKCHAIN_SERVICE_NAME = 'blockr' + config.BLOCKCHAIN_SERVICE_NAME = 'jmcorgan' # custom blockchain service API endpoint # leave blank to use the default. if specified, include the scheme prefix and port, without a trailing slash (e.g. http://localhost:3001) @@ -541,10 +541,7 @@ def balances (address): address_data = get_address(db, address=address) balances = address_data['balances'] table = PrettyTable(['Asset', 'Amount']) - if util.is_multisig(address): - btc_balance = '???' - else: - btc_balance = blockchain.getaddressinfo(address)['balance'] + btc_balance = bitcoin.get_btc_balance(address) table.add_row([config.BTC, btc_balance]) # BTC for balance in balances: asset = balance['asset'] @@ -1152,7 +1149,8 @@ def generate_move_random_hash(move): for i in range(1, num_tries + 1): try: blockchain.check() - except: # TODO + except Exception as e: # TODO + logging.exception(e) logging.warn("Blockchain backend (%s) not yet initialized. Waiting %i seconds and trying again (try %i of %i)..." % ( config.BLOCKCHAIN_SERVICE_NAME, time_wait, i, num_tries)) time.sleep(time_wait) diff --git a/lib/api.py b/lib/api.py index 71c43f730c..78a6cb2e7e 100644 --- a/lib/api.py +++ b/lib/api.py @@ -23,7 +23,7 @@ from jsonrpc import dispatcher import inspect -from . import (config, bitcoin, exceptions, util) +from . import (config, bitcoin, exceptions, util, blockchain) from . import (send, order, btcpay, issuance, broadcast, bet, dividend, burn, cancel, callback, rps, rpsresolve, publish) API_TABLES = ['balances', 'credits', 'debits', 'bets', 'bet_matches', @@ -588,6 +588,18 @@ def get_holder_count(asset): addresses.append(holder['address']) return { asset: len(set(addresses)) } + @dispatcher.add_method + def search_raw_transactions(address): + return blockchain.searchrawtransactions(address) + + @dispatcher.add_method + def get_unspent_txouts(address, return_confirmed=False): + result = bitcoin.get_unspent_txouts(address, return_confirmed=return_confirmed) + if return_confirmed: + return {'all': result[0], 'confirmed': result[1]} + else: + return result + def _set_cors_headers(response): if config.RPC_ALLOW_CORS: response.headers['Access-Control-Allow-Origin'] = '*' diff --git a/lib/bitcoin.py b/lib/bitcoin.py index cfa3d137c4..0c21f1ab5a 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -45,7 +45,7 @@ def pubkey_to_pubkeyhash(pubkey): return pubkey def pubkeyhash_to_pubkey(pubkeyhash): # TODO: convert to python-bitcoinlib. - raw_transactions = search_raw_transactions(pubkeyhash) + raw_transactions = blockchain.searchrawtransactions(pubkeyhash) for tx in raw_transactions: for vin in tx['vin']: scriptsig = vin['scriptSig'] @@ -59,50 +59,46 @@ def multisig_pubkeyhashes_to_pubkeys(address): pubkeys = [pubkeyhash_to_pubkey(pubkeyhash) for pubkeyhash in pubkeyhashes] return util.construct_array(signatures_required, pubkeys, signatures_possible) -bitcoin_rpc_session = None - def print_coin(coin): return 'amount: {}; txid: {}; vout: {}; confirmations: {}'.format(coin['amount'], coin['txid'], coin['vout'], coin.get('confirmations', '?')) # simplify and make deterministic # COMMON def get_block_count(): - return int(rpc('getblockcount', [])) + return int(util.rpc('getblockcount', [])) def get_block_hash(block_index): - return rpc('getblockhash', [block_index]) + return util.rpc('getblockhash', [block_index]) def get_raw_transaction (tx_hash): - return rpc('getrawtransaction', [tx_hash, 1]) + return util.rpc('getrawtransaction', [tx_hash, 1]) def get_block (block_hash): - return rpc('getblock', [block_hash]) + return util.rpc('getblock', [block_hash]) def get_block_hash (block_index): - return rpc('getblockhash', [block_index]) + return util.rpc('getblockhash', [block_index]) def decode_raw_transaction (unsigned_tx_hex): - return rpc('decoderawtransaction', [unsigned_tx_hex]) + return util.rpc('decoderawtransaction', [unsigned_tx_hex]) def get_info(): - return rpc('getinfo', []) + return util.rpc('getinfo', []) # UNCOMMON def is_valid (address): - return rpc('validateaddress', [address])['isvalid'] + return util.rpc('validateaddress', [address])['isvalid'] def is_mine (address): - return rpc('validateaddress', [address])['ismine'] + return util.rpc('validateaddress', [address])['ismine'] def sign_raw_transaction (unsigned_tx_hex): - return rpc('signrawtransaction', [unsigned_tx_hex]) + return util.rpc('signrawtransaction', [unsigned_tx_hex]) def send_raw_transaction (tx_hex): - return rpc('sendrawtransaction', [tx_hex]) + return util.rpc('sendrawtransaction', [tx_hex]) def get_private_key (address): - return rpc('dumpprivkey', [address]) -def search_raw_transactions (address): - return rpc('searchrawtransactions', [address, 1, 0, 9999999]) + return util.rpc('dumpprivkey', [address]) def get_wallet (): - for group in rpc('listaddressgroupings', []): + for group in util.rpc('listaddressgroupings', []): for bunch in group: yield bunch def get_mempool (): - return rpc('getrawmempool', []) + return util.rpc('getrawmempool', []) def list_unspent (): - return rpc('listunspent', [0, 999999]) + return util.rpc('listunspent', [0, 999999]) def backend_check (db): """Checks blocktime of last block to see if {} Core is running behind.""".format(config.BTC_NAME) block_count = get_block_count() @@ -113,74 +109,6 @@ def backend_check (db): raise exceptions.BitcoindError('Bitcoind is running about {} seconds behind.'.format(round(time_behind))) -def connect (url, payload, headers): - global bitcoin_rpc_session - if not bitcoin_rpc_session: bitcoin_rpc_session = requests.Session() - TRIES = 12 - for i in range(TRIES): - try: - response = bitcoin_rpc_session.post(url, data=json.dumps(payload), headers=headers, verify=config.BACKEND_RPC_SSL_VERIFY) - if i > 0: print('Successfully connected.', file=sys.stderr) - return response - except requests.exceptions.SSLError as e: - raise e - except requests.exceptions.ConnectionError: - logging.debug('Could not connect to Bitcoind. (Try {}/{})'.format(i+1, TRIES)) - time.sleep(5) - return None - -def wallet_unlock (): - getinfo = get_info() - if 'unlocked_until' in getinfo: - if getinfo['unlocked_until'] >= 60: - return True # Wallet is unlocked for at least the next 60 seconds. - else: - passphrase = getpass.getpass('Enter your Bitcoind[‐Qt] wallet passhrase: ') - print('Unlocking wallet for 60 (more) seconds.') - rpc('walletpassphrase', [passphrase, 60]) - else: - return True # Wallet is unencrypted. - -def rpc (method, params): - starttime = time.time() - headers = {'content-type': 'application/json'} - payload = { - "method": method, - "params": params, - "jsonrpc": "2.0", - "id": 0, - } - - response = connect(config.BACKEND_RPC, payload, headers) - if response == None: - if config.TESTNET: network = 'testnet' - else: network = 'mainnet' - raise exceptions.BitcoindRPCError('Cannot communicate with {} Core. ({} is set to run on {}, is {} Core?)'.format(config.BTC_NAME, config.XCP_CLIENT, network, config.BTC_NAME)) - elif response.status_code not in (200, 500): - raise exceptions.BitcoindRPCError(str(response.status_code) + ' ' + response.reason) - - # Return result, with error handling. - response_json = response.json() - if 'error' not in response_json.keys() or response_json['error'] == None: - return response_json['result'] - elif response_json['error']['code'] == -5: # RPC_INVALID_ADDRESS_OR_KEY - raise exceptions.BitcoindError('{} Is txindex enabled in {} Core?'.format(response_json['error'], config.BTC_NAME)) - elif response_json['error']['code'] == -4: # Unknown private key (locked wallet?) - # If address in wallet, attempt to unlock. - address = params[0] - if is_valid(address): - if is_mine(address): - raise exceptions.BitcoindError('Wallet is locked.') - else: # When will this happen? - raise exceptions.BitcoindError('Source address not in wallet.') - else: - raise exceptions.AddressError('Invalid address. (Multi‐signature?)') - elif response_json['error']['code'] == -1 and response_json['message'] == 'Block number out of range.': - time.sleep(10) - return get_block_hash(block_index) - else: - raise exceptions.BitcoindError('{}'.format(response_json['error'])) - def var_int (i): if i < 0xfd: return (i).to_bytes(1, byteorder='little') @@ -651,56 +579,63 @@ def get_btc_supply(normalize=False): blocks_remaining = 0 return total_supply if normalize else int(total_supply * config.UNIT) -def get_unspent_txouts(source): +def get_unspent_txouts(source, return_confirmed=False): """returns a list of unspent outputs for a specific address @return: A list of dicts, with each entry in the dict having the following keys: """ - + # Get all coins. + outputs = {} if util.is_multisig(source): pubkeyhashes = util.pubkeyhash_array(source) - outputs = [] - raw_transactions = search_raw_transactions(pubkeyhashes[1]) - # Get all coins. + raw_transactions = blockchain.searchrawtransactions(pubkeyhashes[1]) + else: + pubkeyhashes = [source] + raw_transactions = blockchain.searchrawtransactions(source) + + for tx in raw_transactions: + for vout in tx['vout']: + scriptpubkey = vout['scriptPubKey'] + if util.is_multisig(source) and scriptpubkey['type'] != 'multisig': + continue + elif 'addresses' in scriptpubkey.keys() and "".join(sorted(scriptpubkey['addresses'])) == "".join(sorted(pubkeyhashes)): + txid = tx['txid'] + confirmations = tx['confirmations'] if 'confirmations' in tx else 0 + if txid not in outputs or outputs[txid]['confirmations'] < confirmations: + coin = {'amount': float(vout['value']), + 'confirmations': confirmations, + 'scriptPubKey': scriptpubkey['hex'], + 'txid': txid, + 'vout': vout['n'] + } + outputs[txid] = coin + outputs = outputs.values() + + # Prune away spent coins. + unspent = [] + confirmed_unspent = [] + for output in outputs: + spent = False + confirmed_spent = False for tx in raw_transactions: - for vout in tx['vout']: - scriptpubkey = vout['scriptPubKey'] - if scriptpubkey['type'] == 'multisig' and 'addresses' in scriptpubkey.keys() and len(scriptpubkey['addresses']) == len(pubkeyhashes): - found = True - for pubkeyhash in pubkeyhashes: - if not pubkeyhash in scriptpubkey['addresses']: - found = False - if found: - coin = {'amount': vout['value'], - 'confirmations': tx['confirmations'], - 'scriptPubKey': scriptpubkey['hex'], - 'txid': tx['txid'], - 'vout': vout['n'] - } - outputs.append(coin) - # Prune away spent coins. - unspent = [] - for output in outputs: - spent = False - for tx in raw_transactions: - for vin in tx['vin']: - if (vin['txid'], vin['vout']) == (output['txid'], output['vout']): - spent = True - if not spent: - unspent.append(output) + for vin in tx['vin']: + if 'coinbase' in vin: continue + if (vin['txid'], vin['vout']) == (output['txid'], output['vout']): + spent = True + if 'confirmations' in tx and tx['confirmations'] > 0: + confirmed_spent = True + if not spent: + unspent.append(output) + if not confirmed_spent and output['confirmations'] > 0: + confirmed_unspent.append(output) + + if return_confirmed: + return unspent, confirmed_unspent else: - # TODO: remove account (and address?) fields - if is_mine(source): - wallet_unspent = list_unspent() - unspent = [] - for output in wallet_unspent: - try: - if output['address'] == source: - unspent.append(output) - except KeyError: - pass - else: - unspent = blockchain.listunspent(source) + return unspent - return unspent +def get_btc_balance(address, confirmed=True): + all_unspent, confirmed_unspent = get_unspent_txouts(address, return_confirmed=True) + unspent = confirmed_unspent if confirmed else all_unspent + return sum(out['amount'] for out in unspent) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/lib/blockchain/__init__.py b/lib/blockchain/__init__.py index 1fcd1d964a..d129dcb5dc 100644 --- a/lib/blockchain/__init__.py +++ b/lib/blockchain/__init__.py @@ -5,25 +5,12 @@ import logging from lib import config -from lib.blockchain import blockr, insight, sochain +from lib.blockchain import insight, jmcorgan, sochain, blockr # http://test.insight.is/api/sync def check(): logging.info('Status: Connecting to block explorer.') return sys.modules['lib.blockchain.{}'.format(config.BLOCKCHAIN_SERVICE_NAME)].check() -# http://test.insight.is/api/status?q=getInfo -def getinfo(): - return sys.modules['lib.blockchain.{}'.format(config.BLOCKCHAIN_SERVICE_NAME)].getinfo() - -# example: http://test.insight.is/api/addr/mmvP3mTe53qxHdPqXEvdu8WdC7GfQ2vmx5/utxo -def listunspent(address): - return sys.modules['lib.blockchain.{}'.format(config.BLOCKCHAIN_SERVICE_NAME)].listunspent(address) - -# example: http://test.insight.is/api/addr/mmvP3mTe53qxHdPqXEvdu8WdC7GfQ2vmx5 -def getaddressinfo(address): - return sys.modules['lib.blockchain.{}'.format(config.BLOCKCHAIN_SERVICE_NAME)].getaddressinfo(address) - -# example: http://test.insight.is/api/tx/c6b5368c5a256141894972fbd02377b3894aa0df7c35fab5e0eca90de064fdc1 -def gettransaction(tx_hash): - return sys.modules['lib.blockchain.{}'.format(config.BLOCKCHAIN_SERVICE_NAME)].gettransaction(tx_hash) +def searchrawtransactions(address): + return sys.modules['lib.blockchain.{}'.format(config.BLOCKCHAIN_SERVICE_NAME)].searchrawtransactions(address) diff --git a/lib/blockchain/blockr.py b/lib/blockchain/blockr.py index 82e157ef2f..59f3e1bac7 100644 --- a/lib/blockchain/blockr.py +++ b/lib/blockchain/blockr.py @@ -14,84 +14,14 @@ def get_host(): def check(): pass -def getinfo(): - result = util.get_url(get_host() + '/api/v1/coin/info', abort_on_error=True) - if 'status' in result and result['status'] == 'success': - return { - "info": { - "blocks": result['data']['last_block']['nb'] - } - } +def searchrawtransactions(address): + unconfirmed = util.unconfirmed_transactions(address) - return None + confirmed = [] + txs = util.get_url(get_host() + '/api/v1/address/txs/{}'.format(address), abort_on_error=True) + if 'status' in txs and txs['status'] == 'success': + for tx in txs['data']['txs']: + tx = util.rpc('getrawtransaction', [tx['tx'], 1]) + confirmed.append(tx) -def listunspent(address): - result = util.get_url(get_host() + '/api/v1/address/unspent/{}/'.format(address), abort_on_error=True) - if 'status' in result and result['status'] == 'success': - utxo = [] - for txo in result['data']['unspent']: - newtxo = { - 'address': address, - 'txid': txo['tx'], - 'vout': txo['n'], - 'ts': 0, - 'scriptPubKey': txo['script'], - 'amount': float(txo['amount']), - 'confirmations': txo['confirmations'], - 'confirmationsFromCache': False - } - utxo.append(newtxo) - return utxo - - return None - -def getaddressinfo(address): - infos = util.get_url(get_host() + '/api/v1/address/info/{}'.format(address), abort_on_error=True) - if 'status' in infos and infos['status'] == 'success': - txs = util.get_url(get_host() + '/api/v1/address/txs/{}'.format(address), abort_on_error=True) - if 'status' in txs and txs['status'] == 'success': - transactions = [] - for tx in txs['data']['txs']: - transactions.append(tx['tx']) - return { - 'addrStr': address, - 'balance': infos['data']['balance'], - 'balanceSat': infos['data']['balance'] * config.UNIT, - 'totalReceived': infos['data']['totalreceived'], - 'totalReceivedSat': infos['data']['totalreceived'] * config.UNIT, - 'unconfirmedBalance': 0, - 'unconfirmedBalanceSat': 0, - 'unconfirmedTxApperances': 0, - 'txApperances': txs['data']['nb_txs'], - 'transactions': transactions - } - - return None - -def gettransaction(tx_hash): - url = get_host() + '/api/v1/tx/raw/{}'.format(tx_hash) - tx = util.get_url(url, abort_on_error=False) - assert tx and tx.get('status') and tx.get('code') - if tx['code'] == 404: - return None - elif tx['code'] != 200: - raise Exception("Invalid result (code %s), body: %s" % (tx['code'], tx)) - - if 'status' in tx and tx['status'] == 'success': - valueOut = 0 - for vout in tx['data']['tx']['vout']: - valueOut += vout['value'] - return { - 'txid': tx_hash, - 'version': tx['data']['tx']['version'], - 'locktime': tx['data']['tx']['locktime'], - 'blockhash': tx['data']['tx'].get('blockhash', None), #will be None if not confirmed yet... - 'confirmations': tx['data']['tx'].get('confirmations', None), - 'time': tx['data']['tx'].get('time', None), - 'blocktime': tx['data']['tx'].get('blocktime', None), - 'valueOut': valueOut, - 'vin': tx['data']['tx']['vin'], - 'vout': tx['data']['tx']['vout'] - } - - return None + return unconfirmed + confirmed \ No newline at end of file diff --git a/lib/blockchain/insight.py b/lib/blockchain/insight.py index bb5670d3d9..811711df37 100644 --- a/lib/blockchain/insight.py +++ b/lib/blockchain/insight.py @@ -20,14 +20,9 @@ def check(): if result['status'] == 'syncing': logging.warning("WARNING: Insight is not fully synced to the blockchain: %s%% complete" % result['syncPercentage']) -def getinfo(): - return util.get_url(get_host() + '/api/status?q=getInfo', abort_on_error=True) - -def listunspent(address): - return util.get_url(get_host() + '/api/addr/' + address + '/utxo/', abort_on_error=True) - -def getaddressinfo(address): - return util.get_url(get_host() + '/api/addr/' + address + '/', abort_on_error=True) - -def gettransaction(tx_hash): - return util.get_url(get_host() + '/api/tx/' + tx_hash + '/', abort_on_error=False) \ No newline at end of file +def searchrawtransactions(address): + result = util.get_url(get_host() + '/api/txs/?address=' + address, abort_on_error=False) + if 'txs' in result: + return result['txs'] + else: + return [] \ No newline at end of file diff --git a/lib/blockchain/jmcorgan.py b/lib/blockchain/jmcorgan.py new file mode 100644 index 0000000000..2409e27377 --- /dev/null +++ b/lib/blockchain/jmcorgan.py @@ -0,0 +1,18 @@ +''' +http://insight.bitpay.com/ +''' +import logging +import requests +import time + +from lib import config, exceptions, util + +bitcoin_rpc_session = None + +def check(): + return True + +def searchrawtransactions(address): + unconfirmed = util.unconfirmed_transactions(address) + confirmed = util.rpc('searchrawtransactions', [address, 1, 0, 9999999]) + return unconfirmed + confirmed diff --git a/lib/blockchain/sochain.py b/lib/blockchain/sochain.py index b17488a57d..4542fd58b4 100644 --- a/lib/blockchain/sochain.py +++ b/lib/blockchain/sochain.py @@ -20,75 +20,15 @@ def sochain_network(): def check(): pass -def getinfo(): - result = util.get_url(get_host() + '/api/v2/get_info/{}'.format(sochain_network()), abort_on_error=True) - if 'status' in result and result['status'] == 'success': - return { - "info": { - "blocks": result['data']['blocks'] - } - } - else: - return None - -def listunspent(address): - result = util.get_url(get_host() + '/api/v2/get_tx_unspent/{}/{}'.format(sochain_network(), address), abort_on_error=True) - if 'status' in result and result['status'] == 'success': - utxo = [] - for txo in result['data']['txs']: - newtxo = { - 'address': address, - 'txid': txo['txid'], - 'vout': txo['output_no'], - 'ts': txo['time'], - 'scriptPubKey': txo['script_hex'], - 'amount': float(txo['value']), - 'confirmations': txo['confirmations'], - 'confirmationsFromCache': False - } - utxo.append(newtxo) - return utxo - else: - return None - -def getaddressinfo(address): - infos = util.get_url(get_host() + '/api/v2/address/{}/{}'.format(sochain_network(), address), abort_on_error=True) - if 'status' in infos and infos['status'] == 'success': - transactions = [] - for tx in infos['data']['txs']: - transactions.append(tx['txid']) - return { - 'addrStr': address, - 'balance': float(infos['data']['balance']), - 'balanceSat': float(infos['data']['balance']) * config.UNIT, - 'totalReceived': float(infos['data']['received_value']), - 'totalReceivedSat': float(infos['data']['received_value']) * config.UNIT, - 'unconfirmedBalance': 0, - 'unconfirmedBalanceSat': 0, - 'unconfirmedTxApperances': 0, - 'txApperances': infos['data']['total_txs'], - 'transactions': transactions - } - - return None - -def gettransaction(tx_hash): - tx = util.get_url(get_host() + '/api/v2/get_tx/{}/{}'.format(sochain_network(), tx_hash), abort_on_error=True) - if 'status' in tx and tx['status'] == 'success': - valueOut = 0 - for vout in tx['data']['tx']['vout']: - valueOut += float(vout['value']) - return { - 'txid': tx_hash, - 'version': tx['data']['tx']['version'], - 'locktime': tx['data']['tx']['locktime'], - 'blockhash': tx['data']['tx']['blockhash'], - 'confirmations': tx['data']['tx']['confirmations'], - 'time': tx['data']['tx']['time'], - 'blocktime': tx['data']['tx']['blocktime'], - 'valueOut': valueOut, - 'vin': tx['data']['tx']['vin'], - 'vout': tx['data']['tx']['vout'] - } - - return None +def searchrawtransactions(address): + unconfirmed = util.unconfirmed_transactions(address) + + confirmed = [] + txs = util.get_url(get_host() + '/api/v2/get_tx/{}/{}'.format(sochain_network(), address), abort_on_error=True) + if 'status' in txs and txs['status'] == 'success': + for tx in txs['data']['txs']: + tx = util.rpc('getrawtransaction', [tx['txid'], 1]) + confirmed.append(tx) + + return unconfirmed + confirmed + \ No newline at end of file diff --git a/lib/blocks.py b/lib/blocks.py index cf04c976a5..272474fdf2 100644 --- a/lib/blocks.py +++ b/lib/blocks.py @@ -1399,7 +1399,8 @@ def follow (db): # and then save those messages. # Every transaction in mempool is parsed independently. (DB is rolled back after each one.) mempool = [] - for tx_hash in bitcoin.get_mempool(): + util.MEMPOOL = bitcoin.get_mempool() + for tx_hash in util.MEMPOOL: # If already in counterpartyd mempool, copy to new one. if tx_hash in old_mempool_hashes: diff --git a/lib/order.py b/lib/order.py index e309a58949..116652e020 100644 --- a/lib/order.py +++ b/lib/order.py @@ -8,7 +8,7 @@ D = decimal.Decimal import logging -from . import (util, config, exceptions, bitcoin, util, blockchain) +from . import (util, config, exceptions, bitcoin, util) FORMAT = '>QQQQHQ' LENGTH = 8 + 8 + 8 + 8 + 2 + 8 @@ -241,7 +241,7 @@ def compose (db, source, give_asset, give_quantity, get_asset, get_quantity, exp # Check balance. if give_asset == config.BTC: - if sum(out['amount'] for out in bitcoin.get_unspent_txouts(source)) * config.UNIT < give_quantity: + if bitcoin.get_btc_balance(source) * config.UNIT < give_quantity: print('WARNING: insufficient funds for {}pay.'.format(config.BTC)) else: balances = list(cursor.execute('''SELECT * FROM balances WHERE (address = ? AND asset = ?)''', (source, give_asset))) diff --git a/lib/util.py b/lib/util.py index 219074a30d..4e15f7c374 100644 --- a/lib/util.py +++ b/lib/util.py @@ -14,6 +14,7 @@ import warnings import binascii import hashlib +from functools import lru_cache from . import (config, exceptions) @@ -28,6 +29,8 @@ BET_TYPE_ID = {'BullCFD': 0, 'BearCFD': 1, 'Equal': 2, 'NotEqual': 3} BLOCK_LEDGER = [] +# inelegant but easy and fast cache +MEMPOOL = [] # TODO: This doesn’t timeout properly. (If server hangs, then unhangs, no result.) def api (method, params): @@ -760,10 +763,10 @@ def get_url(url, abort_on_error=False, is_json=True, fetch_timeout=5): try: r = requests.get(url, timeout=fetch_timeout) except Exception as e: - raise GetURLError("Got get_url request error: %s" % e) + raise exceptions.GetURLError("Got get_url request error: %s" % e) else: if r.status_code != 200 and abort_on_error: - raise GetURLError("Bad status code returned: '%s'. result body: '%s'." % (r.status_code, r.text)) + raise exceptions.GetURLError("Bad status code returned: '%s'. result body: '%s'." % (r.status_code, r.text)) result = json.loads(r.text) if is_json else r.text return result @@ -905,6 +908,84 @@ def pubkeyhash_array(address): ### Multi‐signature Addresses ### +### Backend RPC ### + +bitcoin_rpc_session = None + +def connect (url, payload, headers): + global bitcoin_rpc_session + if not bitcoin_rpc_session: bitcoin_rpc_session = requests.Session() + TRIES = 12 + for i in range(TRIES): + try: + response = bitcoin_rpc_session.post(url, data=json.dumps(payload), headers=headers, verify=config.BACKEND_RPC_SSL_VERIFY) + if i > 0: print('Successfully connected.', file=sys.stderr) + return response + except requests.exceptions.SSLError as e: + raise e + except requests.exceptions.ConnectionError: + logging.debug('Could not connect to Bitcoind. (Try {}/{})'.format(i+1, TRIES)) + time.sleep(5) + return None + +def wallet_unlock (): + getinfo = rpc('getinfo', []) + if 'unlocked_until' in getinfo: + if getinfo['unlocked_until'] >= 60: + return True # Wallet is unlocked for at least the next 60 seconds. + else: + passphrase = getpass.getpass('Enter your Bitcoind[‐Qt] wallet passhrase: ') + print('Unlocking wallet for 60 (more) seconds.') + rpc('walletpassphrase', [passphrase, 60]) + else: + return True # Wallet is unencrypted. + +def rpc (method, params): + starttime = time.time() + headers = {'content-type': 'application/json'} + payload = { + "method": method, + "params": params, + "jsonrpc": "2.0", + "id": 0, + } + + response = connect(config.BACKEND_RPC, payload, headers) + if response == None: + if config.TESTNET: network = 'testnet' + else: network = 'mainnet' + raise exceptions.BitcoindRPCError('Cannot communicate with {} Core. ({} is set to run on {}, is {} Core?)'.format(config.BTC_NAME, config.XCP_CLIENT, network, config.BTC_NAME)) + elif response.status_code not in (200, 500): + raise exceptions.BitcoindRPCError(str(response.status_code) + ' ' + response.reason) + + # Return result, with error handling. + response_json = response.json() + if 'error' not in response_json.keys() or response_json['error'] == None: + return response_json['result'] + elif response_json['error']['code'] == -5: # RPC_INVALID_ADDRESS_OR_KEY + raise exceptions.BitcoindError('{} Is txindex enabled in {} Core?'.format(response_json['error'], config.BTC_NAME)) + elif response_json['error']['code'] == -4: # Unknown private key (locked wallet?) + # If address in wallet, attempt to unlock. + address = params[0] + if is_valid(address): + if is_mine(address): + raise exceptions.BitcoindError('Wallet is locked.') + else: # When will this happen? + raise exceptions.BitcoindError('Source address not in wallet.') + else: + raise exceptions.AddressError('Invalid address. (Multi‐signature?)') + elif response_json['error']['code'] == -1 and response_json['error']['message'] == 'Block number out of range.': + time.sleep(10) + return get_block_hash(block_index) + else: + raise exceptions.BitcoindError('{}'.format(response_json['error'])) + +@lru_cache(maxsize=4096) +def get_cached_raw_transaction(tx_hash): + return rpc('getrawtransaction', [tx_hash, 1]) + +### Backend RPC ### + ### Protocol Changes ### def asset_names_v2(block_index): if config.TESTNET: @@ -916,4 +997,33 @@ def asset_names_v2(block_index): return True return False +### Unconfirmed Transactions ### + +@lru_cache(maxsize=4096) +def extract_addresses(tx): + tx = json.loads(tx) # for lru_cache + addresses = [] + + for vout in tx['vout']: + if 'addresses' in vout['scriptPubKey']: + addresses += vout['scriptPubKey']['addresses'] + + for vin in tx['vin']: + vin_tx = get_cached_raw_transaction(vin['txid']) + vout = vin_tx['vout'][vin['vout']] + if 'addresses' in vout['scriptPubKey']: + addresses += vout['scriptPubKey']['addresses'] + + return addresses + +def unconfirmed_transactions(address): + transactions = [] + + for tx_hash in MEMPOOL: + tx = get_cached_raw_transaction(tx_hash) + if address in extract_addresses(json.dumps(tx)): + transactions.append(tx) + + return transactions + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/test/conftest.py b/test/conftest.py index 8d83e20966..186ced0a30 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -9,7 +9,7 @@ from fixtures.params import DEFAULT_PARAMS from fixtures.scenarios import INTEGRATION_SCENARIOS -from lib import config, bitcoin +from lib import config, bitcoin, util import bitcoin as bitcoinlib @@ -51,11 +51,14 @@ def rawtransactions_db(request): @pytest.fixture(autouse=True) def init_mock_functions(monkeypatch, rawtransactions_db): - def get_unspent_txouts(address): + def get_unspent_txouts(address, return_confirmed=False): with open(util_test.CURR_DIR + '/fixtures/unspent_outputs.json', 'r') as listunspent_test_file: wallet_unspent = json.load(listunspent_test_file) unspent_txouts = [output for output in wallet_unspent if output['address'] == address] - return unspent_txouts + if return_confirmed: + return unspent_txouts, unspent_txouts + else: + return unspent_txouts def get_private_key(source): return DEFAULT_PARAMS['privkey'][source] @@ -85,7 +88,7 @@ def multisig_pubkeyhashes_to_pubkeys(address): def decode_raw_transaction(raw_transaction): if pytest.config.option.savescenarios: - return bitcoin.rpc('decoderawtransaction', [raw_transaction]) + return util.rpc('decoderawtransaction', [raw_transaction]) else: return util_test.decoderawtransaction(rawtransactions_db, raw_transaction)