Browse Source

GUI updates and blockr removal

Add more tooltips and some deletions
Update version numbers of GUI
Remove blockr and bc.i interface classes
master
Adam Gibson 8 years ago
parent
commit
2ea4df400b
No known key found for this signature in database
GPG Key ID: B3AE09F1E9A3197A
  1. 2
      .travis.yml
  2. 2
      jmclient/jmclient/__init__.py
  3. 620
      jmclient/jmclient/blockchaininterface.py
  4. 7
      jmclient/jmclient/configure.py
  5. 137
      jmclient/test/test_blockr.py
  6. 12
      jmclient/test/test_configure.py
  7. 47
      scripts/joinmarket-qt.py
  8. 61
      scripts/qtsupport.py
  9. 2
      test/regtest_joinmarket.cfg

2
.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:

2
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)

620
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 '<!DOCTYPE html>' 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):

7
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':

137
jmclient/test/test_blockr.py

@ -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)

12
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")

47
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):

61
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

2
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

Loading…
Cancel
Save