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 - chmod 600 /home/travis/.bitcoin/bitcoin.conf
- mkdir logs - mkdir logs
- mkdir wallets - 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: after_success:
- coveralls - coveralls
branches: 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, from .configure import (load_program_config, jm_single, get_p2pk_vbyte,
get_network, jm_single, get_network, validate_address, get_irc_mchannels, get_network, jm_single, get_network, validate_address, get_irc_mchannels,
get_blockchain_interface_instance, get_p2sh_vbyte, set_config) get_blockchain_interface_instance, get_p2sh_vbyte, set_config)
from .blockchaininterface import (BlockrInterface, BlockchainInterface, sync_wallet, from .blockchaininterface import (BlockchainInterface, sync_wallet,
RegtestBitcoinCoreInterface, BitcoinCoreInterface) RegtestBitcoinCoreInterface, BitcoinCoreInterface)
from .electruminterface import ElectrumInterface from .electruminterface import ElectrumInterface
from .client_protocol import (JMClientProtocolFactory, start_reactor) from .client_protocol import (JMClientProtocolFactory, start_reactor)

620
jmclient/jmclient/blockchaininterface.py

@ -11,8 +11,6 @@ import re
import sys import sys
import threading import threading
import time import time
import urllib
import urllib2
import traceback import traceback
from decimal import Decimal from decimal import Decimal
from twisted.internet import reactor, task from twisted.internet import reactor, task
@ -98,339 +96,6 @@ class BlockchainInterface(object):
required for inclusion in the next N blocks. 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 class ElectrumWalletInterface(BlockchainInterface): #pragma: no cover
"""A pseudo-blockchain interface using the existing """A pseudo-blockchain interface using the existing
Electrum server connection in an Electrum wallet. 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) fee_per_kb_sat = int(float(fee) * 100000000)
return fee_per_kb_sat 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): class BitcoinCoreInterface(BlockchainInterface):
def __init__(self, jsonRpc, network): 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 # todo: refactor joinmarket module to get rid of loops
# importing here is necessary to avoid import loops # importing here is necessary to avoid import loops
from jmclient.blockchaininterface import BitcoinCoreInterface, \ from jmclient.blockchaininterface import BitcoinCoreInterface, \
RegtestBitcoinCoreInterface, BlockrInterface, ElectrumWalletInterface, \ RegtestBitcoinCoreInterface, ElectrumWalletInterface
BlockchaininfoInterface
from jmclient.electruminterface import ElectrumInterface from jmclient.electruminterface import ElectrumInterface
source = _config.get("BLOCKCHAIN", "blockchain_source") source = _config.get("BLOCKCHAIN", "blockchain_source")
network = get_network() network = get_network()
@ -374,10 +373,6 @@ def get_blockchain_interface_instance(_config):
rpc_password = _config.get("BLOCKCHAIN", "rpc_password") rpc_password = _config.get("BLOCKCHAIN", "rpc_password")
rpc = JsonRpc(rpc_host, rpc_port, rpc_user, rpc_password) rpc = JsonRpc(rpc_host, rpc_port, rpc_user, rpc_password)
bc_interface = RegtestBitcoinCoreInterface(rpc) bc_interface = RegtestBitcoinCoreInterface(rpc)
elif source == 'blockr':
bc_interface = BlockrInterface(testnet)
elif source == 'bc.i':
bc_interface = BlockchaininfoInterface(testnet)
elif source == 'electrum': elif source == 'electrum':
bc_interface = ElectrumWalletInterface(testnet) bc_interface = ElectrumWalletInterface(testnet)
elif source == 'electrum-server': 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 import pytest
from jmclient import (load_program_config, jm_single, get_irc_mchannels, 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, from jmclient.configure import (get_config_irc_channel, get_p2sh_vbyte,
get_p2pk_vbyte, get_blockchain_interface_instance) get_p2pk_vbyte, get_blockchain_interface_instance)
import jmbitcoin as bitcoin import jmbitcoin as bitcoin
@ -24,8 +25,11 @@ def test_load_config():
os.makedirs("dummydirforconfig") os.makedirs("dummydirforconfig")
ncp = os.path.join(os.getcwd(), "dummydirforconfig") ncp = os.path.join(os.getcwd(), "dummydirforconfig")
jm_single().config_location = "joinmarket.cfg" jm_single().config_location = "joinmarket.cfg"
#TODO hack: the default config won't load on bitcoin-rpc; need to fix. #TODO hack: load from default implies a connection error unless
load_program_config(config_path=ncp, bs="blockr") #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.remove("dummydirforconfig/joinmarket.cfg")
os.removedirs("dummydirforconfig") os.removedirs("dummydirforconfig")
jm_single().config_location = "joinmarket.cfg" jm_single().config_location = "joinmarket.cfg"
@ -47,7 +51,7 @@ def test_net_byte():
def test_blockchain_sources(): def test_blockchain_sources():
load_program_config() load_program_config()
for src in ["blockr", "electrum", "dummy", "bc.i"]: for src in ["electrum", "dummy"]:
jm_single().config.set("BLOCKCHAIN", "blockchain_source", src) jm_single().config.set("BLOCKCHAIN", "blockchain_source", src)
if src=="electrum": if src=="electrum":
jm_single().config.set("BLOCKCHAIN", "network", "mainnet") jm_single().config.set("BLOCKCHAIN", "network", "mainnet")

47
scripts/joinmarket-qt.py

@ -45,8 +45,10 @@ pyqt4reactor.install()
#General Joinmarket donation address; TODO #General Joinmarket donation address; TODO
donation_address = "1AZgQZWYRteh6UyF87hwuvyWj73NvWKpL" donation_address = "1AZgQZWYRteh6UyF87hwuvyWj73NvWKpL"
JM_CORE_VERSION = '0.2.2' #Underlying joinmarket code version (as per setup.py etc.)
JM_GUI_VERSION = '5' 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, from jmclient import (load_program_config, get_network, SegwitWallet,
get_p2sh_vbyte, jm_single, validate_address, 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', gui_config_names = ['gaplimit', 'history_file', 'check_high_fee',
'max_mix_depth', 'txfee_default', 'order_wait_time', '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', gui_config_default_vals = ['6', 'jm-tx-history.txt', '2', '5', '5000', '30',
'27183', 'true'] 'true']
if "GUI" not in jm_single().config.sections(): if "GUI" not in jm_single().config.sections():
jm_single().config.add_section("GUI") jm_single().config.add_section("GUI")
gui_items = jm_single().config.items("GUI") gui_items = jm_single().config.items("GUI")
for gcn, gcv in zip(gui_config_names, gui_config_default_vals): for gcn, gcv in zip(gui_config_names, gui_config_default_vals):
if gcn not in [_[0] for _ in gui_items]: if gcn not in [_[0] for _ in gui_items]:
jm_single().config.set("GUI", gcn, gcv) 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(): def persist_config():
@ -632,11 +630,6 @@ class SpendTab(QWidget):
JMQtMessageBox(self, "Cannot start without a loaded wallet.", JMQtMessageBox(self, "Cannot start without a loaded wallet.",
mbtype="crit", title="Error") mbtype="crit", title="Error")
return return
if jm_single().config.get("BLOCKCHAIN",
"blockchain_source") == 'blockr':
res = self.showBlockrWarning()
if res == True:
return
log.debug('starting coinjoin ..') log.debug('starting coinjoin ..')
#Decide whether to interrupt processing to sanity check the fees #Decide whether to interrupt processing to sanity check the fees
if self.tumbler_options: if self.tumbler_options:
@ -916,34 +909,6 @@ class SpendTab(QWidget):
return False return False
return True 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): class TxHistoryTab(QWidget):
def __init__(self): def __init__(self):

61
scripts/qtsupport.py

@ -34,18 +34,18 @@ RED_FG = "QWidget {color:red;}"
BLUE_FG = "QWidget {color:blue;}" BLUE_FG = "QWidget {color:blue;}"
BLACK_FG = "QWidget {color:black;}" BLACK_FG = "QWidget {color:black;}"
donation_address = '1LT6rwv26bV7mgvRosoSCyGM7ttVRsYidP' donation_address = 'Currently disabled'
donation_address_testnet = 'mz6FQosuiNe8135XaQqWYmXsa3aD8YsqGL' donation_address_testnet = 'Currently disabled'
#TODO legacy, remove or change
warnings = {"blockr_privacy": """You are using blockr as your method of warnings = {"blockr_privacy": """You are using blockr as your method of
connecting to the blockchain; this means connecting to the blockchain; this means
that blockr.com can see the addresses you that blockr.com can see the addresses you
query. This is bad for privacy - consider query. This is bad for privacy - consider
using a Bitcoin Core node instead."""} using a Bitcoin Core node instead."""}
#configuration types #configuration types
config_types = {'rpc_port': int, config_types = {'rpc_port': int,
'usessl': bool,
'socks5': bool,
'network': bool, 'network': bool,
'checktx': bool, 'checktx': bool,
'socks5_port': int, 'socks5_port': int,
@ -56,9 +56,10 @@ config_types = {'rpc_port': int,
'max_mix_depth': int, 'max_mix_depth': int,
'txfee_default': int, 'txfee_default': int,
'order_wait_time': int, 'order_wait_time': int,
'privacy_warning': None} "no_daemon": int,
"daemon_port": int,}
config_tips = { config_tips = {
'blockchain_source': 'options: blockr, bc.i, bitcoin-rpc', 'blockchain_source': 'options: bitcoin-rpc, regtest (for testing)',
'network': 'one of "testnet" or "mainnet"', 'network': 'one of "testnet" or "mainnet"',
'checktx': 'whether to check fees before completing transaction', 'checktx': 'whether to check fees before completing transaction',
'rpc_host': 'rpc_host':
@ -69,10 +70,12 @@ config_tips = {
'host': 'hostname for IRC server (or comma separated list)', 'host': 'hostname for IRC server (or comma separated list)',
'channel': 'channel name on 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)', 'port': 'port for connecting to IRC server (or comma separated list)',
'usessl': 'check to use SSL for connection to IRC', 'usessl': "'true'/'false' to use SSL for each connection to IRC\n" +
'socks5': 'check to use SOCKS5 proxy for IRC connection', "(or comma separated list)",
'socks5_host': 'host for SOCKS5 proxy', 'socks5': "'true'/'false' to use a SOCKS5 proxy for each connection" +
'socks5_port': 'port for SOCKS5 proxy', "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', 'maker_timeout_sec': 'timeout for waiting for replies from makers',
'merge_algorithm': 'for dust sweeping, try merge_algorithm = gradual, \n' + 'merge_algorithm': 'for dust sweeping, try merge_algorithm = gradual, \n' +
'for more rapid dust sweeping, try merge_algorithm = greedy \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' + 'tx fee estimate; this value is not usually used and is best left at\n' +
'the default of 5000', 'the default of 5000',
'order_wait_time': 'How long to wait for orders to arrive on entering\n' + '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 #Temporarily disabled

2
test/regtest_joinmarket.cfg

@ -13,8 +13,6 @@ rpc_port = 18332
rpc_user = bitcoinrpc rpc_user = bitcoinrpc
rpc_password = 123456abcdef rpc_password = 123456abcdef
network = testnet network = testnet
bitcoin_cli_cmd = bitcoin-cli
notify_port = 62612
[MESSAGING] [MESSAGING]
host = localhost, localhost host = localhost, localhost
hostid = localhost1, localhost2 hostid = localhost1, localhost2

Loading…
Cancel
Save