diff --git a/.travis.yml b/.travis.yml index 3f21e7f..428d88d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -40,7 +40,7 @@ jobs: - travis_retry pip install codespell==1.15.0 before_script: script: - - PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841 --exclude=key.py,messages_pb2.py,.eggs + - PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=segwit_addr.py,key.py,messages_pb2.py,.eggs - codespell --check-filenames --disable-colors --quiet-level=7 -S .git,.eggs,gitianpubkeys after_success: - echo "End lint" diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index b6c6586..f00db06 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -2262,7 +2262,7 @@ class BasicSwap(): # TODO: Verify script without decoding? decoded_script = self.callcoinrpc(Coins.PART, 'decodescript', [bid_accept_data.contract_script.hex()]) lock_check_op = 'OP_CHECKSEQUENCEVERIFY' if use_csv else 'OP_CHECKLOCKTIMEVERIFY' - prog = re.compile('OP_IF OP_SIZE 32 OP_EQUALVERIFY OP_SHA256 (\w+) OP_EQUALVERIFY OP_DUP OP_HASH160 (\w+) OP_ELSE (\d+) {} OP_DROP OP_DUP OP_HASH160 (\w+) OP_ENDIF OP_EQUALVERIFY OP_CHECKSIG'.format(lock_check_op)) + prog = re.compile(r'OP_IF OP_SIZE 32 OP_EQUALVERIFY OP_SHA256 (\w+) OP_EQUALVERIFY OP_DUP OP_HASH160 (\w+) OP_ELSE (\d+) {} OP_DROP OP_DUP OP_HASH160 (\w+) OP_ENDIF OP_EQUALVERIFY OP_CHECKSIG'.format(lock_check_op)) rr = prog.match(decoded_script['asm']) if not rr: raise ValueError('Bad script') diff --git a/basicswap/http_server.py b/basicswap/http_server.py index deb926b..867a642 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -144,6 +144,77 @@ def setCoinFilter(form_data, field_name): raise ValueError('Unknown Coin Type {}'.format(str(field_name))) +def describeBid(swap_client, bid, offer, edit_bid, show_txns): + + coin_from = Coins(offer.coin_from) + coin_to = Coins(offer.coin_to) + ticker_from = swap_client.getTicker(coin_from) + ticker_to = swap_client.getTicker(coin_to) + + if bid.state == BidStates.BID_SENT: + state_description = 'Waiting for seller to accept.' + elif bid.state == BidStates.BID_RECEIVED: + state_description = 'Waiting for seller to accept.' + elif bid.state == BidStates.BID_ACCEPTED: + if not bid.initiate_tx: + state_description = 'Waiting for seller to send initiate tx.' + else: + state_description = 'Waiting for initiate tx to confirm.' + elif bid.state == BidStates.SWAP_INITIATED: + state_description = 'Waiting for participate txn to be confirmed in {} chain'.format(ticker_to) + elif bid.state == BidStates.SWAP_PARTICIPATING: + state_description = 'Waiting for initiate txn to be spent in {} chain'.format(ticker_from) + elif bid.state == BidStates.SWAP_COMPLETED: + state_description = 'Swap completed' + if bid.getITxState() == TxStates.TX_REDEEMED and bid.getPTxState() == TxStates.TX_REDEEMED: + state_description += ' successfully' + else: + state_description += ', ITX ' + strTxState(bid.getITxState()) + ', PTX ' + strTxState(bid.getPTxState()) + elif bid.state == BidStates.SWAP_TIMEDOUT: + state_description = 'Timed out waiting for initiate txn' + elif bid.state == BidStates.BID_ABANDONED: + state_description = 'Bid abandoned' + elif bid.state == BidStates.BID_ERROR: + state_description = bid.state_note + else: + state_description = '' + + data = { + 'amt_from': format8(bid.amount), + 'amt_to': format8((bid.amount * offer.rate) // COIN), + 'ticker_from': ticker_from, + 'ticker_to': ticker_to, + 'bid_state': strBidState(bid.state), + 'state_description': state_description, + 'itx_state': strTxState(bid.getITxState()), + 'ptx_state': strTxState(bid.getPTxState()), + 'offer_id': bid.offer_id.hex(), + 'addr_from': bid.bid_addr, + 'addr_fund_proof': bid.proof_address, + 'created_at': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(bid.created_at)), + 'expired_at': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(bid.expire_at)), + 'was_sent': 'True' if bid.was_sent else 'False', + 'was_received': 'True' if bid.was_received else 'False', + 'initiate_tx': getTxIdHex(bid, TxTypes.ITX, ' ' + ticker_from), + 'initiate_conf': 'None' if (not bid.initiate_tx or not bid.initiate_tx.conf) else bid.initiate_tx.conf, + 'participate_tx': getTxIdHex(bid, TxTypes.PTX, ' ' + ticker_to), + 'participate_conf': 'None' if (not bid.participate_tx or not bid.participate_tx.conf) else bid.participate_tx.conf, + 'show_txns': show_txns, + } + + if edit_bid: + data['bid_state_ind'] = int(bid.state) + data['bid_states'] = listBidStates() + + if show_txns: + data['initiate_tx_refund'] = 'None' if not bid.initiate_txn_refund else bid.initiate_txn_refund.hex() + data['participate_tx_refund'] = 'None' if not bid.participate_txn_refund else bid.participate_txn_refund.hex() + data['initiate_tx_spend'] = getTxSpendHex(bid, TxTypes.ITX) + data['participate_tx_spend'] = getTxSpendHex(bid, TxTypes.PTX) + + return data + + def html_content_start(title, h2=None, refresh=None): content = '\n
' \ + '' \ @@ -244,6 +315,7 @@ class HttpHandler(BaseHTTPRequestHandler): return self.js_offers(url_split, post_string, True) def js_bids(self, url_split, post_string): + swap_client = self.server.swap_client if len(url_split) > 3: if url_split[3] == 'new': if post_string == '': @@ -261,7 +333,6 @@ class HttpHandler(BaseHTTPRequestHandler): if addr_from == '-1': addr_from = None - swap_client = self.server.swap_client bid_id = swap_client.postBid(offer_id, amount_from, addr_send_from=addr_from).hex() rv = {'bid_id': bid_id} @@ -269,9 +340,29 @@ class HttpHandler(BaseHTTPRequestHandler): bid_id = bytes.fromhex(url_split[3]) assert(len(bid_id) == 28) - return bytes(json.dumps(self.server.swap_client.viewBid(bid_id)), 'UTF-8') - assert(False), 'TODO' - return bytes(json.dumps(self.server.swap_client.listBids()), 'UTF-8') + + if post_string != '': + post_data = urllib.parse.parse_qs(post_string) + if b'accept' in post_data: + swap_client.acceptBid(bid_id) + + bid, offer = swap_client.getBidAndOffer(bid_id) + assert(bid), 'Unknown bid ID' + + edit_bid = False + show_txns = False + data = describeBid(swap_client, bid, offer, edit_bid, show_txns) + + return bytes(json.dumps(data), 'UTF-8') + + bids = swap_client.listBids() + return bytes(json.dumps([{ + 'bid_id': b[1].hex(), + 'offer_id': b[2].hex(), + 'created_at': time.strftime('%Y-%m-%d %H:%M', time.localtime(b[0])), + 'amount_from': format8(b[3]), + 'bid_state': strBidState(b[4]) + } for b in bids]), 'UTF-8') def js_sentbids(self, url_split, post_string): return bytes(json.dumps(self.server.swap_client.listBids(sent=True)), 'UTF-8') @@ -672,71 +763,7 @@ class HttpHandler(BaseHTTPRequestHandler): bid, offer = swap_client.getBidAndOffer(bid_id) assert(bid), 'Unknown bid ID' - coin_from = Coins(offer.coin_from) - coin_to = Coins(offer.coin_to) - ticker_from = swap_client.getTicker(coin_from) - ticker_to = swap_client.getTicker(coin_to) - - if bid.state == BidStates.BID_SENT: - state_description = 'Waiting for seller to accept.' - elif bid.state == BidStates.BID_RECEIVED: - state_description = 'Waiting for seller to accept.' - elif bid.state == BidStates.BID_ACCEPTED: - if not bid.initiate_tx: - state_description = 'Waiting for seller to send initiate tx.' - else: - state_description = 'Waiting for initiate tx to confirm.' - elif bid.state == BidStates.SWAP_INITIATED: - state_description = 'Waiting for participate txn to be confirmed in {} chain'.format(ticker_to) - elif bid.state == BidStates.SWAP_PARTICIPATING: - state_description = 'Waiting for initiate txn to be spent in {} chain'.format(ticker_from) - elif bid.state == BidStates.SWAP_COMPLETED: - state_description = 'Swap completed' - if bid.getITxState() == TxStates.TX_REDEEMED and bid.getPTxState() == TxStates.TX_REDEEMED: - state_description += ' successfully' - else: - state_description += ', ITX ' + strTxState(bid.getITxState() + ', PTX ' + strTxState(bid.getPTxState())) - elif bid.state == BidStates.SWAP_TIMEDOUT: - state_description = 'Timed out waiting for initiate txn' - elif bid.state == BidStates.BID_ABANDONED: - state_description = 'Bid abandoned' - elif bid.state == BidStates.BID_ERROR: - state_description = bid.state_note - else: - state_description = '' - - data = { - 'amt_from': format8(bid.amount), - 'amt_to': format8((bid.amount * offer.rate) // COIN), - 'ticker_from': ticker_from, - 'ticker_to': ticker_to, - 'bid_state': strBidState(bid.state), - 'state_description': state_description, - 'itx_state': strTxState(bid.getITxState()), - 'ptx_state': strTxState(bid.getPTxState()), - 'offer_id': bid.offer_id.hex(), - 'addr_from': bid.bid_addr, - 'addr_fund_proof': bid.proof_address, - 'created_at': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(bid.created_at)), - 'expired_at': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(bid.expire_at)), - 'was_sent': 'True' if bid.was_sent else 'False', - 'was_received': 'True' if bid.was_received else 'False', - 'initiate_tx': getTxIdHex(bid, TxTypes.ITX, ' ' + ticker_from), - 'initiate_conf': 'None' if (not bid.initiate_tx or not bid.initiate_tx.conf) else bid.initiate_tx.conf, - 'participate_tx': getTxIdHex(bid, TxTypes.PTX, ' ' + ticker_to), - 'participate_conf': 'None' if (not bid.participate_tx or not bid.participate_tx.conf) else bid.participate_tx.conf, - 'show_txns': show_txns, - } - - if edit_bid: - data['bid_state_ind'] = int(bid.state) - data['bid_states'] = listBidStates() - - if show_txns: - data['initiate_tx_refund'] = 'None' if not bid.initiate_txn_refund else bid.initiate_txn_refund.hex() - data['participate_tx_refund'] = 'None' if not bid.participate_txn_refund else bid.participate_txn_refund.hex() - data['initiate_tx_spend'] = getTxSpendHex(bid, TxTypes.ITX) - data['participate_tx_spend'] = getTxSpendHex(bid, TxTypes.PTX) + data = describeBid(swap_client, bid, offer, edit_bid, show_txns) old_states = [] num_states = len(bid.states) // 12 diff --git a/bin/basicswap_prepare.py b/bin/basicswap_prepare.py index 9894efa..8a35dc9 100644 --- a/bin/basicswap_prepare.py +++ b/bin/basicswap_prepare.py @@ -251,8 +251,8 @@ def printHelp(): logger.info('--mainnet Run in mainnet mode.') logger.info('--testnet Run in testnet mode.') logger.info('--regtest Run in regtest mode.') - logger.info('--particl_mnemonic= Recovery phrase to use for the Particl wallet, default is randomly generated,\n' + - ' "none" to set autogenerate account mode.') + logger.info('--particl_mnemonic= Recovery phrase to use for the Particl wallet, default is randomly generated,\n' + + ' "none" to set autogenerate account mode.') logger.info('--withcoin= Prepare system to run daemon for coin.') logger.info('--withoutcoin= Do not prepare system to run daemon for coin.') logger.info('--addcoin= Add coin to existing setup.') diff --git a/tests/basicswap/test_reload.py b/tests/basicswap/test_reload.py index 60c514b..bf211f9 100644 --- a/tests/basicswap/test_reload.py +++ b/tests/basicswap/test_reload.py @@ -26,6 +26,7 @@ import shutil import json import traceback import multiprocessing +import threading from unittest.mock import patch from urllib.request import urlopen from urllib import parse @@ -40,6 +41,7 @@ import bin.basicswap_run as runSystem test_path = os.path.expanduser(os.getenv('TEST_RELOAD_PATH', '~/test_basicswap1')) PARTICL_PORT_BASE = int(os.getenv('PARTICL_PORT_BASE', '11938')) BITCOIN_PORT_BASE = int(os.getenv('BITCOIN_PORT_BASE', '10938')) +stop_test = False logger = logging.getLogger() logger.level = logging.DEBUG @@ -53,11 +55,11 @@ def btcRpc(client_no, cmd): return callrpc_cli(bin_path, data_path, 'regtest', cmd, 'bitcoin-cli') -def waitForServer(): +def waitForServer(port): for i in range(20): try: time.sleep(1) - summary = json.loads(urlopen('http://localhost:12700/json').read()) + summary = json.loads(urlopen('http://localhost:{}/json'.format(port)).read()) break except Exception: traceback.print_exc() @@ -81,6 +83,23 @@ def waitForNumBids(port, bids): raise ValueError('waitForNumBids failed') +def waitForNumSwapping(port, bids): + for i in range(20): + summary = json.loads(urlopen('http://localhost:{}/json'.format(port)).read()) + if summary['num_swapping'] >= bids: + return + time.sleep(1) + raise ValueError('waitForNumSwapping failed') + + +def updateThread(): + btc_addr = btcRpc(0, 'getnewaddress mining_addr bech32') + + while not stop_test: + btcRpc(0, 'generatetoaddress {} {}'.format(1, btc_addr)) + time.sleep(5) + + class Test(unittest.TestCase): @classmethod def setUpClass(cls): @@ -109,12 +128,19 @@ class Test(unittest.TestCase): with patch.object(sys, 'argv', testargs): prepareSystem.main() - with open(os.path.join(client_path, 'particl', 'particl.conf'), 'a') as fp: + with open(os.path.join(client_path, 'particl', 'particl.conf'), 'r') as fp: + lines = fp.readlines() + with open(os.path.join(client_path, 'particl', 'particl.conf'), 'w') as fp: + for line in lines: + if not line.startswith('staking'): + fp.write(line) fp.write('port={}\n'.format(PARTICL_PORT_BASE + i)) fp.write('bind=127.0.0.1\n') fp.write('dnsseed=0\n') + fp.write('minstakeinterval=5\n') for ip in range(3): - fp.write('addnode=localhost:{}\n'.format(PARTICL_PORT_BASE + ip)) + if ip != i: + fp.write('connect=localhost:{}\n'.format(PARTICL_PORT_BASE + ip)) # Pruned nodes don't provide blocks with open(os.path.join(client_path, 'bitcoin', 'bitcoin.conf'), 'r') as fp: @@ -130,7 +156,8 @@ class Test(unittest.TestCase): fp.write('upnp=0\n') fp.write('bind=127.0.0.1\n') for ip in range(3): - fp.write('connect=localhost:{}\n'.format(BITCOIN_PORT_BASE + ip)) + if ip != i: + fp.write('connect=localhost:{}\n'.format(BITCOIN_PORT_BASE + ip)) assert(os.path.exists(config_path)) @@ -148,7 +175,7 @@ class Test(unittest.TestCase): processes[-1].start() try: - waitForServer() + waitForServer(12700) num_blocks = 500 btc_addr = btcRpc(1, 'getnewaddress mining_addr bech32') @@ -189,9 +216,43 @@ class Test(unittest.TestCase): waitForNumBids(12700, 1) + bids = json.loads(urlopen('http://localhost:12700/json/bids').read()) + bid = bids[0] - logger.warning('TODO') + data = parse.urlencode({ + 'accept': True + }).encode() + rv = json.loads(urlopen('http://localhost:12700/json/bids/{}'.format(bid['bid_id']), data=data).read()) + assert(rv['bid_state'] == 'Accepted') + waitForNumSwapping(12701, 1) + + logger.info('Restarting client:') + c1 = processes[1] + c1.terminate() + c1.join() + processes[1] = multiprocessing.Process(target=self.run_thread, args=(1,)) + processes[1].start() + + waitForServer(12701) + rv = json.loads(urlopen('http://localhost:12701/json').read()) + assert(rv['num_swapping'] == 1) + + update_thread = threading.Thread(target=updateThread) + update_thread.start() + + logger.info('Completing swap:') + for i in range(240): + time.sleep(5) + + rv = json.loads(urlopen('http://localhost:12700/json/bids/{}'.format(bid['bid_id'])).read()) + print(rv) + if rv['bid_state'] == 'Completed': + break + assert(rv['bid_state'] == 'Completed') + + stop_test = True + update_thread.join() for p in processes: p.terminate() for p in processes: