diff --git a/.travis.yml b/.travis.yml index 7e313c5..65cb168 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,7 +40,7 @@ script: - chmod 600 /home/travis/.bitcoin/bitcoin.conf - mkdir logs - mkdir wallets - - python -m py.test --cov=jmclient --cov=jmbitcoin --cov=jmbase --cov=jmdaemon --cov-report html --btcpwd=123456abcdef --btcconf=/home/travis/.bitcoin/bitcoin.conf --btcuser=bitcoinrpc --nirc=2 --ignore jmclient/test/test_blockr.py + - python -m py.test --cov=jmclient --cov=jmbitcoin --cov=jmbase --cov=jmdaemon --cov-report html --btcpwd=123456abcdef --btcconf=/home/travis/.bitcoin/bitcoin.conf --btcuser=bitcoinrpc --nirc=2 after_success: - coveralls branches: diff --git a/jmclient/jmclient/__init__.py b/jmclient/jmclient/__init__.py index d171434..1a65217 100644 --- a/jmclient/jmclient/__init__.py +++ b/jmclient/jmclient/__init__.py @@ -22,7 +22,7 @@ from .wallet import (AbstractWallet, BitcoinCoreInterface, Wallet, from .configure import (load_program_config, jm_single, get_p2pk_vbyte, get_network, jm_single, get_network, validate_address, get_irc_mchannels, get_blockchain_interface_instance, get_p2sh_vbyte, set_config) -from .blockchaininterface import (BlockrInterface, BlockchainInterface, sync_wallet, +from .blockchaininterface import (BlockchainInterface, sync_wallet, RegtestBitcoinCoreInterface, BitcoinCoreInterface) from .electruminterface import ElectrumInterface from .client_protocol import (JMClientProtocolFactory, start_reactor) diff --git a/jmclient/jmclient/blockchaininterface.py b/jmclient/jmclient/blockchaininterface.py index 0771b09..b0f678a 100644 --- a/jmclient/jmclient/blockchaininterface.py +++ b/jmclient/jmclient/blockchaininterface.py @@ -11,8 +11,6 @@ import re import sys import threading import time -import urllib -import urllib2 import traceback from decimal import Decimal from twisted.internet import reactor, task @@ -98,339 +96,6 @@ class BlockchainInterface(object): required for inclusion in the next N blocks. ''' -class BlockchaininfoInterface(BlockchainInterface): #pragma: no cover - BCINFO_MAX_ADDR_REQ_COUNT = 20 - - def __init__(self, testnet=False): - super(BlockchaininfoInterface, self).__init__() - - # blockchain.info doesn't support testnet via API :( - if testnet: - log.debug("Blockchain.info doesn't support testnet, " - "try blockr as blockchain source. Quitting.") - return None - - # see bci.py in bitcoin module - self.network = 'btc' - self.last_sync_unspent = 0 - - @staticmethod - def make_bci_request(bci_api_req_url): - while True: - try: - log.info("making request to: " + str(bci_api_req_url)) - res = btc.make_request(bci_api_req_url) - log.info("got result") - except Exception as e: - if str(e) == 'No free outputs to spend': - return False - elif str(e) == 'Quota Exceeded (Req Count Limit)': - log.info('Request count limit reached (+500 req in 5 minutes)') - log.info('Waiting for 60 seconds before retrying') - time.sleep(60) - continue - elif str(e) == 'Transaction not found': - log.info(str(e)) - return False - elif str(e) == 'No Free Cluster Connection' or str(e) == 'Maximum concurrent requests for this endpoint reached. Please try again shortly.' or '' in str(e): - log.info('Issues connecting to Blockchain.info API - waiting it out for 60s') - time.sleep(60) - continue - else: - log.info(Exception(e)) - continue - return res - - def sync_addresses(self, wallet): - log.info('downloading wallet history') - # sets Wallet internal indexes to be at the next unused address - for mix_depth in range(wallet.max_mix_depth): - for forchange in [0, 1]: - unused_addr_count = 0 - last_used_addr = '' - while (unused_addr_count < wallet.gaplimit or - not is_index_ahead_of_cache( - wallet, mix_depth, forchange)): - addrs = [wallet.get_new_addr(mix_depth, forchange) - for _ in range(self.BCINFO_MAX_ADDR_REQ_COUNT)] - - bcinfo_url = 'https://blockchain.info/multiaddr?active=' - - # request info on all addresses - res = self.make_bci_request(bcinfo_url + '|'.join(addrs)) - data = json.loads(res)['addresses'] - - # get data returned in same order as the address list passed to the API - new_data = [] - for a in addrs: - new_data.append([d for d in data if d['address']==a][0]) - - # for each address in data - for addr in new_data: - if addr['n_tx'] != 0: - last_used_addr = addr['address'] - unused_addr_count = 0 - else: - unused_addr_count += 1 - - if last_used_addr == '': - wallet.index[mix_depth][forchange] = 0 - else: - wallet.index[mix_depth][forchange] = wallet.addr_cache[ - last_used_addr][ - 2] + 1 - - def sync_unspent(self, wallet): - # finds utxos in the wallet - st = time.time() - # dont refresh unspent dict more often than 10 minutes - rate_limit_time = 10 * 60 - if st - self.last_sync_unspent < rate_limit_time: - log.info( - 'blockchaininfo sync_unspent() happened too recently (%dsec), skipping' - % (st - self.last_sync_unspent)) - return - wallet.unspent = {} - - addrs = wallet.addr_cache.keys() - if len(addrs) == 0: - log.debug('no tx used') - return - i = 0 - count = 0 - while i < len(addrs): - inc = min(len(addrs) - i, self.BCINFO_MAX_ADDR_REQ_COUNT) - - req = addrs[i:i + inc] - i += inc - - bcinfo_url = 'https://blockchain.info/en/unspent?active=' - - # Request unspent outputs in address chunks (20 at once) - res = self.make_bci_request(bcinfo_url + '|'.join(req)) - - count = count + 1 - if res == False: - continue - - # request data on addresses in chunks of five - req_blocks = [] - for j in range(0, 20, 5): - req_blocks.append(req[j:j+5]) - - for req_block in req_blocks: - res = self.make_bci_request(bcinfo_url + '|'.join(req_block)) - count = count + 1 - if res == False: - continue - # If there are addresses in the chunk with unspent outputs, get data - for addr in req_block: - res = self.make_bci_request(bcinfo_url + addr) - count = count + 1 - if res == False: - continue - data = json.loads(res)['unspent_outputs'] - - # for each unspent output - for uo in data: - wallet.unspent[uo['tx_hash_big_endian'] + ':' + str(uo['tx_output_n'])] = { - 'address' : addr, - 'value' : int(uo['value'])} - for u in wallet.spent_utxos: - wallet.unspent.pop(u, None) - - self.last_sync_unspent = time.time() - log.info('blockchaininfo sync_unspent took ' + - str((self.last_sync_unspent - st)) + 'sec') - - def add_tx_notify(self, txd, unconfirmfun, confirmfun, notifyaddr, vb=None): - if not vb: - vb = get_p2pk_vbyte() - unconfirm_timeout = 10 * 60 # seconds - unconfirm_poll_period = 15 - confirm_timeout = 2 * 60 * 60 - confirm_poll_period = 5 * 60 - - class NotifyThread(threading.Thread): - - def __init__(self, txd, unconfirmfun, confirmfun): - threading.Thread.__init__(self) - self.daemon = True - self.unconfirmfun = unconfirmfun - self.confirmfun = confirmfun - self.tx_output_set = set([(sv['script'], sv['value']) - for sv in txd['outs']]) - self.output_addresses = [ - btc.script_to_address(scrval[0], vb) - for scrval in self.tx_output_set] - log.debug('txoutset=' + pprint.pformat(self.tx_output_set)) - log.debug('outaddrs=' + ','.join(self.output_addresses)) - - def run(self): - st = int(time.time()) - unconfirmed_txid = None - unconfirmed_txhex = None - while not unconfirmed_txid: - time.sleep(unconfirm_poll_period) - if int(time.time()) - st > unconfirm_timeout: - log.debug('checking for unconfirmed tx timed out') - return - bcinfo_url = 'https://blockchain.info/unspent?active=' - random.shuffle(self.output_addresses) - - # create list for requests of all addresses - data_list = [] - #for each of the output addresses, request data and add it to list - for output_address in self.output_addresses: - res = BlockchaininfoInterface.make_bci_request(bcinfo_url + output_address) - data = json.loads(res) - data_list.append(data) - - shared_txid = None - for unspent_list in data_list: - txs = set([str(txdata['tx_hash_big_endian']) - for txdata in unspent_list['unspent_outputs']]) - if not shared_txid: - shared_txid = txs - else: - shared_txid = shared_txid.intersection(txs) - log.debug('sharedtxid = ' + str(shared_txid)) - if len(shared_txid) == 0: - continue - time.sleep(2) # here for some possible race conditions, maybe could do without - - bcinfo_url = 'https://blockchain.info/rawtx/' - # get data of tx and hex repressentation - # for each tx the outputs addresses share (should only be one?) - data_list = [] - for tx in shared_txid: - res = BlockchaininfoInterface.make_bci_request(bcinfo_url + tx) - if res == False: - continue - data_tx = json.loads(res) - res = BlockchaininfoInterface.make_bci_request(bcinfo_url + tx + '?format=hex') - data_tx['hex'] = str(res) - data_list.append(data_tx) - - if not isinstance(data_list, list): - data_list = [data_list] - - for txinfo in data_list: - txhex = str(txinfo['hex']) - outs = set([(sv['script'], sv['value']) - for sv in btc.deserialize(txhex)['outs']]) - log.debug('unconfirm query outs = ' + str(outs)) - if outs == self.tx_output_set: - unconfirmed_txid = txinfo['hash'] - unconfirmed_txhex = str(txinfo['hex']) - break - - self.unconfirmfun( - btc.deserialize(unconfirmed_txhex), unconfirmed_txid) - - st = int(time.time()) - confirmed_txid = None - confirmed_txhex = None - bcinfo_url = 'https://blockchain.info/rawtx/' - - while not confirmed_txid: - time.sleep(confirm_poll_period) - if int(time.time()) - st > confirm_timeout: - log.debug('checking for confirmed tx timed out') - return - - for tx in shared_txid: - res = btc.make_request(bcinfo_url + tx) - data = json.loads(res) - - # Not yet confirmed - if 'block_height' not in data: - continue - else: - res = BlockchaininfoInterface.make_bci_request(bcinfo_url + tx + '?format=hex') - txhex = str(res) - outs = set([(sv['script'], sv['value']) - for sv in btc.deserialize(txhex)['outs']]) - log.debug('confirm query outs = ' + str(outs)) - if outs == self.tx_output_set: - confirmed_txid = txinfo['hash'] - confirmed_txhex = str(txinfo['hex']) - break - - self.confirmfun( - btc.deserialize(confirmed_txhex), confirmed_txid, 1) - - NotifyThread(txd, unconfirmfun, confirmfun).start() - - def pushtx(self, txhex): - try: - json_str = btc.bci_pushtx(txhex) - except Exception: - log.debug('failed blockchain.info pushtx') - return None - - txhash = btc.txhash(txhex) - return txhash - - def query_utxo_set(self, txout, includeconf=False): - self.current_height = int(self.make_bci_request("https://blockchain.info/q/getblockcount")) - log.info("Got block height: " + str(self.current_height)) - if not isinstance(txout, list): - txout = [txout] - txids = [h[:64] for h in txout] - txids = list(set(txids)) - txids = [txids] - - bcinfo_url = 'https://blockchain.info/rawtx/' - data = [] - for ids in txids: - for single_id in ids: - res = self.make_bci_request(bcinfo_url + single_id) - if res == False: - continue - bci_data = json.loads(res) - data.append(bci_data) - - result = [] - for txo in txout: - txdata = None - vout = None - for d in data: - if d['hash'] == txo[:64]: - txdata = d - for v in txdata['out']: - if v['n'] == int(txo[65:]): - vout = v - if vout['spent'] == 1: - result.append(None) - else: - result_dict = {'value': int(vout['value']), - 'address': vout['addr'], - 'script': vout['script']} - if includeconf: - if 'block_height' not in txdata: - result_dict['confirms'] = 0 - else: - #+1 because if current height = tx height, that's 1 conf - result_dict['confirms'] = int( - self.current_height) - int(txdata['block_height']) + 1 - log.debug("Got num confs: " + str(result_dict['confirms'])) - result.append(result_dict) - return result - - def estimate_fee_per_kb(self, N): - bcypher_fee_estimate_url = 'https://api.blockcypher.com/v1/btc/main' - bcypher_data = json.loads(btc.make_request(bcypher_fee_estimate_url)) - log.debug("Got blockcypher result: "+pprint.pformat(bcypher_data)) - if N<=2: - fee_per_kb = bcypher_data["high_fee_per_kb"] - elif N <=4: - fee_per_kb = bcypher_data["medium_fee_per_kb"] - else: - fee_per_kb = bcypher_data["low_fee_per_kb"] - - return fee_per_kb - class ElectrumWalletInterface(BlockchainInterface): #pragma: no cover """A pseudo-blockchain interface using the existing Electrum server connection in an Electrum wallet. @@ -522,291 +187,6 @@ class ElectrumWalletInterface(BlockchainInterface): #pragma: no cover fee_per_kb_sat = int(float(fee) * 100000000) return fee_per_kb_sat - -class BlockrInterface(BlockchainInterface): #pragma: no cover - BLOCKR_MAX_ADDR_REQ_COUNT = 20 - - def __init__(self, testnet=False): - super(BlockrInterface, self).__init__() - - # see bci.py in bitcoin module - self.network = 'testnet' if testnet else 'btc' - self.blockr_domain = 'tbtc' if testnet else 'btc' - self.last_sync_unspent = 0 - - def sync_addresses(self, wallet): - log.debug('downloading wallet history') - # sets Wallet internal indexes to be at the next unused address - for mix_depth in range(wallet.max_mix_depth): - for forchange in [0, 1]: - #must reset at the start so as to search forward from the beginning - wallet.index[mix_depth][forchange] = 0 - unused_addr_count = 0 - last_used_addr = '' - while (unused_addr_count < wallet.gaplimit or - not is_index_ahead_of_cache(wallet, mix_depth, - forchange)): - addrs = [wallet.get_new_addr(mix_depth, forchange) - for _ in range(self.BLOCKR_MAX_ADDR_REQ_COUNT)] - - # TODO send a pull request to pybitcointools - # because this surely should be possible with a function from it - blockr_url = 'https://' + self.blockr_domain - blockr_url += '.blockr.io/api/v1/address/txs/' - - data = btc.make_request_blockr(blockr_url + ','.join( - addrs))['data'] - for dat in data: - if dat['nb_txs'] != 0: - last_used_addr = dat['address'] - unused_addr_count = 0 - else: - unused_addr_count += 1 - if last_used_addr == '': - wallet.index[mix_depth][forchange] = 0 - else: - next_avail_idx = max([wallet.addr_cache[last_used_addr][ - 2] + 1, wallet.index_cache[mix_depth][forchange]]) - wallet.index[mix_depth][forchange] = next_avail_idx - - def sync_unspent(self, wallet): - # finds utxos in the wallet - st = time.time() - # dont refresh unspent dict more often than 1 minutes - rate_limit_time = 1 * 60 - if st - self.last_sync_unspent < rate_limit_time: - log.debug( - 'blockr sync_unspent() happened too recently (%dsec), skipping' - % (st - self.last_sync_unspent)) - return - wallet.unspent = {} - - addrs = wallet.addr_cache.keys() - if len(addrs) == 0: - log.debug('no tx used') - return - i = 0 - while i < len(addrs): - inc = min(len(addrs) - i, self.BLOCKR_MAX_ADDR_REQ_COUNT) - req = addrs[i:i + inc] - i += inc - - # TODO send a pull request to pybitcointools - # unspent() doesnt tell you which address, you get a bunch of utxos - # but dont know which privkey to sign with - - blockr_url = 'https://' + self.blockr_domain + \ - '.blockr.io/api/v1/address/unspent/' - data = btc.make_request_blockr(blockr_url + ','.join(req))['data'] - if 'unspent' in data: - data = [data] - for dat in data: - for u in dat['unspent']: - wallet.unspent[u['tx'] + ':' + str(u['n'])] = { - 'address': dat['address'], - 'value': int(u['amount'].replace('.', '')) - } - for u in wallet.spent_utxos: - wallet.unspent.pop(u, None) - - self.last_sync_unspent = time.time() - log.debug('blockr sync_unspent took ' + str((self.last_sync_unspent - st - )) + 'sec') - - def add_tx_notify(self, - txd, - unconfirmfun, - confirmfun, - notifyaddr, - timeoutfun=None, - vb=None): - if not vb: - vb = get_p2pk_vbyte() - unconfirm_timeout = jm_single().config.getint('TIMEOUT', - 'unconfirm_timeout_sec') - unconfirm_poll_period = 5 - confirm_timeout = jm_single().config.getfloat( - 'TIMEOUT', 'confirm_timeout_hours') * 60 * 60 - confirm_poll_period = 5 * 60 - - class NotifyThread(threading.Thread): - - def __init__(self, blockr_domain, txd, unconfirmfun, confirmfun, - timeoutfun): - threading.Thread.__init__(self, name='BlockrNotifyThread') - self.daemon = True - self.blockr_domain = blockr_domain - self.unconfirmfun = unconfirmfun - self.confirmfun = confirmfun - self.timeoutfun = timeoutfun - self.tx_output_set = set([(sv['script'], sv['value']) - for sv in txd['outs']]) - self.output_addresses = [ - btc.script_to_address(scrval[0], vb) - for scrval in self.tx_output_set - ] - log.debug('txoutset=' + pprint.pformat(self.tx_output_set)) - log.debug('outaddrs=' + ','.join(self.output_addresses)) - - def run(self): - st = int(time.time()) - unconfirmed_txid = None - unconfirmed_txhex = None - while not unconfirmed_txid: - time.sleep(unconfirm_poll_period) - if int(time.time()) - st > unconfirm_timeout: - log.debug('checking for unconfirmed tx timed out') - if self.timeoutfun: - self.timeoutfun(False) - return - blockr_url = 'https://' + self.blockr_domain - blockr_url += '.blockr.io/api/v1/address/unspent/' - random.shuffle(self.output_addresses - ) # seriously weird bug with blockr.io - data = btc.make_request_blockr(blockr_url + ','.join( - self.output_addresses) + '?unconfirmed=1')['data'] - - shared_txid = None - for unspent_list in data: - txs = set([str(txdata['tx']) - for txdata in unspent_list['unspent']]) - if not shared_txid: - shared_txid = txs - else: - shared_txid = shared_txid.intersection(txs) - log.debug('sharedtxid = ' + str(shared_txid)) - if len(shared_txid) == 0: - continue - time.sleep( - 2 - ) # here for some race condition bullshit with blockr.io - blockr_url = 'https://' + self.blockr_domain - blockr_url += '.blockr.io/api/v1/tx/raw/' - data = btc.make_request_blockr(blockr_url + ','.join( - shared_txid))['data'] - if not isinstance(data, list): - data = [data] - for txinfo in data: - txhex = str(txinfo['tx']['hex']) - outs = set([(sv['script'], sv['value']) - for sv in btc.deserialize(txhex)['outs']]) - log.debug('unconfirm query outs = ' + str(outs)) - if outs == self.tx_output_set: - unconfirmed_txid = txinfo['tx']['txid'] - unconfirmed_txhex = str(txinfo['tx']['hex']) - break - - self.unconfirmfun( - btc.deserialize(unconfirmed_txhex), unconfirmed_txid) - - st = int(time.time()) - confirmed_txid = None - confirmed_txhex = None - while not confirmed_txid: - time.sleep(confirm_poll_period) - if int(time.time()) - st > confirm_timeout: - log.debug('checking for confirmed tx timed out') - if self.timeoutfun: - self.timeoutfun(True) - return - blockr_url = 'https://' + self.blockr_domain - blockr_url += '.blockr.io/api/v1/address/txs/' - data = btc.make_request_blockr(blockr_url + ','.join( - self.output_addresses))['data'] - shared_txid = None - for addrtxs in data: - txs = set(str(txdata['tx']) - for txdata in addrtxs['txs']) - if not shared_txid: - shared_txid = txs - else: - shared_txid = shared_txid.intersection(txs) - log.debug('sharedtxid = ' + str(shared_txid)) - if len(shared_txid) == 0: - continue - blockr_url = 'https://' + self.blockr_domain - blockr_url += '.blockr.io/api/v1/tx/raw/' - data = btc.make_request_blockr(blockr_url + ','.join( - shared_txid))['data'] - if not isinstance(data, list): - data = [data] - for txinfo in data: - txhex = str(txinfo['tx']['hex']) - outs = set([(sv['script'], sv['value']) - for sv in btc.deserialize(txhex)['outs']]) - log.debug('confirm query outs = ' + str(outs)) - if outs == self.tx_output_set: - confirmed_txid = txinfo['tx']['txid'] - confirmed_txhex = str(txinfo['tx']['hex']) - break - self.confirmfun( - btc.deserialize(confirmed_txhex), confirmed_txid, 1) - - NotifyThread(self.blockr_domain, txd, unconfirmfun, confirmfun, - timeoutfun).start() - - def pushtx(self, txhex): - try: - json_str = btc.blockr_pushtx(txhex, self.network) - data = json.loads(json_str) - if data['status'] != 'success': - log.debug(data) - return False - except Exception: - log.debug('failed blockr.io pushtx') - log.debug(traceback.format_exc()) - return False - return True - - def query_utxo_set(self, txout, includeconf=False): - if not isinstance(txout, list): - txout = [txout] - txids = [h[:64] for h in txout] - txids = list(set(txids)) # remove duplicates - # self.BLOCKR_MAX_ADDR_REQ_COUNT = 2 - if len(txids) > self.BLOCKR_MAX_ADDR_REQ_COUNT: - txids = chunks(txids, self.BLOCKR_MAX_ADDR_REQ_COUNT) - else: - txids = [txids] - data = [] - for ids in txids: - blockr_url = 'https://' + self.blockr_domain + '.blockr.io/api/v1/tx/info/' - blockr_data = btc.make_request_blockr(blockr_url + ','.join(ids))['data'] - if not isinstance(blockr_data, list): - blockr_data = [blockr_data] - data += blockr_data - result = [] - for txo in txout: - txdata_candidate = [d for d in data if d['tx'] == txo[:64]] - if len(txdata_candidate) == 0: - continue - txdata = txdata_candidate[0] - vout = [v for v in txdata['vouts'] if v['n'] == int(txo[65:])][0] - if "is_spent" in vout and vout['is_spent'] == 1: - result.append(None) - else: - result_dict = {'value': int(Decimal(vout['amount']) * - Decimal('1e8')), - 'address': vout['address'], - 'script': vout['extras']['script']} - if includeconf: - result_dict['confirms'] = int(txdata['confirmations']) - result.append(result_dict) - return result - - def estimate_fee_per_kb(self, N): - bcypher_fee_estimate_url = 'https://api.blockcypher.com/v1/btc/main' - bcypher_data = json.loads(btc.make_request(bcypher_fee_estimate_url)) - log.debug("Got blockcypher result: " + pprint.pformat(bcypher_data)) - if N <= 2: - fee_per_kb = bcypher_data["high_fee_per_kb"] - elif N <= 4: - fee_per_kb = bcypher_data["medium_fee_per_kb"] - else: - fee_per_kb = bcypher_data["low_fee_per_kb"] - - return fee_per_kb - class BitcoinCoreInterface(BlockchainInterface): def __init__(self, jsonRpc, network): diff --git a/jmclient/jmclient/configure.py b/jmclient/jmclient/configure.py index 4a39bc0..9c274f5 100644 --- a/jmclient/jmclient/configure.py +++ b/jmclient/jmclient/configure.py @@ -353,8 +353,7 @@ def get_blockchain_interface_instance(_config): # todo: refactor joinmarket module to get rid of loops # importing here is necessary to avoid import loops from jmclient.blockchaininterface import BitcoinCoreInterface, \ - RegtestBitcoinCoreInterface, BlockrInterface, ElectrumWalletInterface, \ - BlockchaininfoInterface + RegtestBitcoinCoreInterface, ElectrumWalletInterface from jmclient.electruminterface import ElectrumInterface source = _config.get("BLOCKCHAIN", "blockchain_source") network = get_network() @@ -374,10 +373,6 @@ def get_blockchain_interface_instance(_config): rpc_password = _config.get("BLOCKCHAIN", "rpc_password") rpc = JsonRpc(rpc_host, rpc_port, rpc_user, rpc_password) bc_interface = RegtestBitcoinCoreInterface(rpc) - elif source == 'blockr': - bc_interface = BlockrInterface(testnet) - elif source == 'bc.i': - bc_interface = BlockchaininfoInterface(testnet) elif source == 'electrum': bc_interface = ElectrumWalletInterface(testnet) elif source == 'electrum-server': diff --git a/jmclient/test/test_blockr.py b/jmclient/test/test_blockr.py deleted file mode 100644 index 8be34e7..0000000 --- a/jmclient/test/test_blockr.py +++ /dev/null @@ -1,137 +0,0 @@ -#! /usr/bin/env python -from __future__ import absolute_import -'''Blockchain access via blockr tests.''' - -import sys -import os -import time -import binascii -from mock import patch -import json - -import jmbitcoin as btc -import pytest -from jmclient import (load_program_config, jm_single, sync_wallet, BlockrInterface, - get_p2pk_vbyte, get_log, Wallet) -log = get_log() - -#TODO: some kind of mainnet testing, harder. -blockr_root_url = "https://tbtc.blockr.io/api/v1/" - -def test_bci_bad_req(): - with pytest.raises(Exception) as e_info: - btc.make_request("1") - -def test_blockr_bad_request(): - with pytest.raises(Exception) as e_info: - btc.make_request_blockr(blockr_root_url+"address/txs/", "0000") - -def test_blockr_bad_pushtx(): - inps = [("00000000", "btc"), ("00000000", "testnet"), - ('\x00'*8, "testnet"), ('\x00'*8, "x")] - for i in inps: - with pytest.raises(Exception) as e_info: - btc.blockr_pushtx(i[0],i[1]) - -def test_bci_bad_pushtx(): - inps = [("00000000"), ('\x00'*8)] - for i in inps: - with pytest.raises(Exception) as e_info: - btc.bci_pushtx(i[0]) - -def test_blockr_estimate_fee(setup_blockr): - res = [] - for N in [1,3,6]: - res.append(jm_single().bc_interface.estimate_fee_per_kb(N)) - assert res[0] >= res[2] - #Note this can fail, it isn't very accurate. - #assert res[1] >= res[2] - #sanity checks: - assert res[0] < 400000 - assert res[2] < 350000 - -@pytest.mark.parametrize( - "net, seed, gaplimit, showprivkey, method", - [ - ("testnet", - #Dont take these testnet coins, itll botch up our tests!! - "I think i did pretty good with Christmas", - 6, - True, - #option "displayall" here will show all addresses from beginning - "display"), - ]) -def test_blockr_sync(setup_blockr, net, seed, gaplimit, showprivkey, method): - jm_single().config.set("BLOCKCHAIN", "network", net) - wallet = Wallet(seed, None, max_mix_depth = 5) - sync_wallet(wallet) - - #copy pasted from wallet-tool; some boiled down form of - #this should really be in wallet.py in the joinmarket module. - def cus_print(s): - print s - - total_balance = 0 - for m in range(wallet.max_mix_depth): - cus_print('mixing depth %d m/0/%d/' % (m, m)) - balance_depth = 0 - for forchange in [0, 1]: - cus_print(' ' + ('external' if forchange == 0 else 'internal') + - ' addresses m/0/%d/%d/' % (m, forchange)) - - for k in range(wallet.index[m][forchange] + gaplimit): - addr = wallet.get_addr(m, forchange, k) - balance = 0.0 - for addrvalue in wallet.unspent.values(): - if addr == addrvalue['address']: - balance += addrvalue['value'] - balance_depth += balance - used = ('used' if k < wallet.index[m][forchange] else ' new') - if showprivkey: - privkey = btc.wif_compressed_privkey( - wallet.get_key(m, forchange, k), get_p2pk_vbyte()) - else: - privkey = '' - if (method == 'displayall' or balance > 0 or - (used == ' new' and forchange == 0)): - cus_print(' m/0/%d/%d/%03d %-35s%s %.8f btc %s' % - (m, forchange, k, addr, used, balance / 1e8, - privkey)) - total_balance += balance_depth - print('for mixdepth=%d balance=%.8fbtc' % (m, balance_depth / 1e8)) - assert total_balance == 96085297 - -@patch('jmbitcoin.bci.make_request') -def test_blockr_error_429(make_request): - error = {u'code': 429, - u'data': None, - u'message': u'Too many requests. Wait a bit...', - u'status': u'error'} - success = {u'code': 200, - u'data': {u'address': u'mqG1k82TDWfxSYFyDRkomjYonDUYjPRbsb', - u'limit_txs': 200, - u'nb_txs': 1, - u'nb_txs_displayed': 1, - u'txs': [{u'amount': 1, - u'amount_multisig': 0, - u'confirmations': 400, - u'time_utc': u'2016-09-15T19:46:14Z', - u'tx': u'6a1bfbdd011cbb2ab2a000d477bd6372150238b4c24e43a850220dba4dbf2c0d'}]}, - u'message': u'', - u'status': u'success'} - make_request.side_effect = map(json.dumps, [error]*3 + [success]) - - d = btc.make_request_blockr(blockr_root_url + "address/txs/", "mqG1k82TDWfxSYFyDRkomjYonDUYjPRbsb") - assert d['code'] == 200 - assert d['data'] is not None - - -@pytest.fixture(scope="module") -def setup_blockr(request): - def blockr_teardown(): - jm_single().config.set("BLOCKCHAIN", "blockchain_source", "regtest") - jm_single().config.set("BLOCKCHAIN", "network", "testnet") - request.addfinalizer(blockr_teardown) - load_program_config() - jm_single().config.set("BLOCKCHAIN", "blockchain_source", "blockr") - jm_single().bc_interface = BlockrInterface(True) diff --git a/jmclient/test/test_configure.py b/jmclient/test/test_configure.py index 44f9981..bb437aa 100644 --- a/jmclient/test/test_configure.py +++ b/jmclient/test/test_configure.py @@ -4,7 +4,8 @@ from __future__ import absolute_import import pytest from jmclient import (load_program_config, jm_single, get_irc_mchannels, - BTC_P2PK_VBYTE, BTC_P2SH_VBYTE, validate_address) + BTC_P2PK_VBYTE, BTC_P2SH_VBYTE, validate_address, + JsonRpcConnectionError) from jmclient.configure import (get_config_irc_channel, get_p2sh_vbyte, get_p2pk_vbyte, get_blockchain_interface_instance) import jmbitcoin as bitcoin @@ -24,8 +25,11 @@ def test_load_config(): os.makedirs("dummydirforconfig") ncp = os.path.join(os.getcwd(), "dummydirforconfig") jm_single().config_location = "joinmarket.cfg" - #TODO hack: the default config won't load on bitcoin-rpc; need to fix. - load_program_config(config_path=ncp, bs="blockr") + #TODO hack: load from default implies a connection error unless + #actually mainnet, but tests cannot; for now catch the connection error + with pytest.raises(JsonRpcConnectionError) as e_info: + load_program_config(config_path=ncp, bs="regtest") + assert str(e_info.value) == "JSON-RPC connection failed. Err:error(111, 'Connection refused')" os.remove("dummydirforconfig/joinmarket.cfg") os.removedirs("dummydirforconfig") jm_single().config_location = "joinmarket.cfg" @@ -47,7 +51,7 @@ def test_net_byte(): def test_blockchain_sources(): load_program_config() - for src in ["blockr", "electrum", "dummy", "bc.i"]: + for src in ["electrum", "dummy"]: jm_single().config.set("BLOCKCHAIN", "blockchain_source", src) if src=="electrum": jm_single().config.set("BLOCKCHAIN", "network", "mainnet") diff --git a/scripts/joinmarket-qt.py b/scripts/joinmarket-qt.py index 2385f85..6ee46c9 100644 --- a/scripts/joinmarket-qt.py +++ b/scripts/joinmarket-qt.py @@ -45,8 +45,10 @@ pyqt4reactor.install() #General Joinmarket donation address; TODO donation_address = "1AZgQZWYRteh6UyF87hwuvyWj73NvWKpL" -JM_CORE_VERSION = '0.2.2' -JM_GUI_VERSION = '5' +#Underlying joinmarket code version (as per setup.py etc.) +JM_CORE_VERSION = '0.3.0' +#Version of this Qt script specifically +JM_GUI_VERSION = '6' from jmclient import (load_program_config, get_network, SegwitWallet, get_p2sh_vbyte, jm_single, validate_address, @@ -83,19 +85,15 @@ def update_config_for_gui(): ''' gui_config_names = ['gaplimit', 'history_file', 'check_high_fee', 'max_mix_depth', 'txfee_default', 'order_wait_time', - 'daemon_port', 'checktx'] + 'checktx'] gui_config_default_vals = ['6', 'jm-tx-history.txt', '2', '5', '5000', '30', - '27183', 'true'] + 'true'] if "GUI" not in jm_single().config.sections(): jm_single().config.add_section("GUI") gui_items = jm_single().config.items("GUI") for gcn, gcv in zip(gui_config_names, gui_config_default_vals): if gcn not in [_[0] for _ in gui_items]: jm_single().config.set("GUI", gcn, gcv) - #Extra setting not exposed to the GUI, but only for the GUI app - if 'privacy_warning' not in [_[0] for _ in gui_items]: - print('overwriting privacy_warning') - jm_single().config.set("GUI", 'privacy_warning', '1') def persist_config(): @@ -632,11 +630,6 @@ class SpendTab(QWidget): JMQtMessageBox(self, "Cannot start without a loaded wallet.", mbtype="crit", title="Error") return - if jm_single().config.get("BLOCKCHAIN", - "blockchain_source") == 'blockr': - res = self.showBlockrWarning() - if res == True: - return log.debug('starting coinjoin ..') #Decide whether to interrupt processing to sanity check the fees if self.tumbler_options: @@ -916,34 +909,6 @@ class SpendTab(QWidget): return False return True - def showBlockrWarning(self): - if jm_single().config.getint("GUI", "privacy_warning") == 0: - return False - qmb = QMessageBox() - qmb.setIcon(QMessageBox.Warning) - qmb.setWindowTitle("Privacy Warning") - qcb = QCheckBox("Don't show this warning again.") - lyt = qmb.layout() - lyt.addWidget(QLabel(warnings['blockr_privacy']), 0, 1) - lyt.addWidget(qcb, 1, 1) - qmb.addButton(QPushButton("Continue"), QMessageBox.YesRole) - qmb.addButton(QPushButton("Cancel"), QMessageBox.NoRole) - - qmb.exec_() - - switch_off_warning = '0' if qcb.isChecked() else '1' - jm_single().config.set("GUI", "privacy_warning", switch_off_warning) - - res = qmb.buttonRole(qmb.clickedButton()) - if res == QMessageBox.YesRole: - return False - elif res == QMessageBox.NoRole: - return True - else: - log.debug("GUI error: unrecognized button, canceling.") - return True - - class TxHistoryTab(QWidget): def __init__(self): diff --git a/scripts/qtsupport.py b/scripts/qtsupport.py index 66bcca9..ed29b78 100644 --- a/scripts/qtsupport.py +++ b/scripts/qtsupport.py @@ -34,18 +34,18 @@ RED_FG = "QWidget {color:red;}" BLUE_FG = "QWidget {color:blue;}" BLACK_FG = "QWidget {color:black;}" -donation_address = '1LT6rwv26bV7mgvRosoSCyGM7ttVRsYidP' -donation_address_testnet = 'mz6FQosuiNe8135XaQqWYmXsa3aD8YsqGL' +donation_address = 'Currently disabled' +donation_address_testnet = 'Currently disabled' +#TODO legacy, remove or change warnings = {"blockr_privacy": """You are using blockr as your method of connecting to the blockchain; this means that blockr.com can see the addresses you query. This is bad for privacy - consider using a Bitcoin Core node instead."""} + #configuration types config_types = {'rpc_port': int, - 'usessl': bool, - 'socks5': bool, 'network': bool, 'checktx': bool, 'socks5_port': int, @@ -56,9 +56,10 @@ config_types = {'rpc_port': int, 'max_mix_depth': int, 'txfee_default': int, 'order_wait_time': int, - 'privacy_warning': None} + "no_daemon": int, + "daemon_port": int,} config_tips = { - 'blockchain_source': 'options: blockr, bc.i, bitcoin-rpc', + 'blockchain_source': 'options: bitcoin-rpc, regtest (for testing)', 'network': 'one of "testnet" or "mainnet"', 'checktx': 'whether to check fees before completing transaction', 'rpc_host': @@ -69,10 +70,12 @@ config_tips = { 'host': 'hostname for IRC server (or comma separated list)', 'channel': 'channel name on IRC server (or comma separated list)', 'port': 'port for connecting to IRC server (or comma separated list)', - 'usessl': 'check to use SSL for connection to IRC', - 'socks5': 'check to use SOCKS5 proxy for IRC connection', - 'socks5_host': 'host for SOCKS5 proxy', - 'socks5_port': 'port for SOCKS5 proxy', + 'usessl': "'true'/'false' to use SSL for each connection to IRC\n" + + "(or comma separated list)", + 'socks5': "'true'/'false' to use a SOCKS5 proxy for each connection" + + "to IRC (or comma separated list)", + 'socks5_host': 'host for SOCKS5 proxy (or comma separated list)', + 'socks5_port': 'port for SOCKS5 proxy (or comma separated list)', 'maker_timeout_sec': 'timeout for waiting for replies from makers', 'merge_algorithm': 'for dust sweeping, try merge_algorithm = gradual, \n' + 'for more rapid dust sweeping, try merge_algorithm = greedy \n' + @@ -92,7 +95,43 @@ config_tips = { 'tx fee estimate; this value is not usually used and is best left at\n' + 'the default of 5000', 'order_wait_time': 'How long to wait for orders to arrive on entering\n' + - 'the message channel, default is 30s' + 'the message channel, default is 30s', + 'no_daemon': "1 means don't use a separate daemon; set to 0 only if you\n" + + "are running an instance of joinmarketd separately", + "daemon_port": "The port on which the joinmarket daemon is running", + "daemon_host": "The host on which the joinmarket daemon is running; remote\n" + + "hosts should be considered *highly* experimental for now, not recommended.", + "use_ssl": "Set to 'true' to use TLS for client-daemon connection; see\n" + + "documentation for details on how to set up certs if you use this.", + "history_file": "Location of the file storing transaction history", + "segwit": "Only used for migrating legacy wallets; see documentation.", + "console_log_level": "one of INFO, DEBUG, WARN, ERROR; INFO is least noisy;\n" + + "consider switching to DEBUG in case of problems.", + "absurd_fee_per_kb": "maximum satoshis/kilobyte you are willing to pay,\n" + + "whatever the fee estimate currently says.", + "tx_broadcast": "Options: self, random-peer, not-self (note: random-maker\n" + + "is not currently supported).\n" + + "self = broadcast transaction with your own ip\n" + + "random-peer = everyone who took part in the coinjoin has\n" + + "a chance of broadcasting.\n" + + "not-self = never broadcast with your own ip.", + "privacy_warning": "Not currently used, ignore.", + "taker_utxo_retries": "Global consensus parameter, do NOT change.\n" + + "See documentation of use of 'commitments'.", + "taker_utxo_age": "Global consensus parameter, do NOT change.\n" + + "See documentation of use of 'commitments'.", + "taker_utxo_amtpercent": "Global consensus parameter, do not change.\n" + + "See documentation of use of 'commitments'.", + "accept_commitment_broadcasts": "Not used, ignore.", + "commit_file_location": "Location of the file that stores the commitments\n" + + "you've used, and any external commitments you've loaded.\n" + + "See documentation of use of 'commitments'.", + "listunspent_args": "Set to [1, 9999999] to show and use only coins that\n" + + "are confirmed; set to [0] to spend all coins including unconfirmed; this\n" + + "is not advisable.", + "minimum_makers": "The minimum number of counterparties for the transaction\n" + + "to complete (default 2). If set to a high value it can cause transactions\n" + + "to fail much more frequently.", } #Temporarily disabled diff --git a/test/regtest_joinmarket.cfg b/test/regtest_joinmarket.cfg index 5bc3952..48c4fc5 100644 --- a/test/regtest_joinmarket.cfg +++ b/test/regtest_joinmarket.cfg @@ -13,8 +13,6 @@ rpc_port = 18332 rpc_user = bitcoinrpc rpc_password = 123456abcdef network = testnet -bitcoin_cli_cmd = bitcoin-cli -notify_port = 62612 [MESSAGING] host = localhost, localhost hostid = localhost1, localhost2