diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 1bc03b6..72f47c2 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -507,8 +507,9 @@ class BasicSwap(BaseApp): for i in range(20): try: # Workaround for mismatched pid file name in litecoin 0.21.2 + # Also set with pid= in .conf # TODO: Remove - if cc['name'] == 'litecoin' and not os.path.exists(pidfilepath) and \ + if cc['name'] == 'litecoin' and (not os.path.exists(pidfilepath)) and \ os.path.exists(os.path.join(self.getChainDatadirPath(coin), 'bitcoind.pid')): pidfilepath = os.path.join(self.getChainDatadirPath(coin), 'bitcoind.pid') @@ -517,7 +518,7 @@ class BasicSwap(BaseApp): assert(datadir_pid == cc['pid']), 'Mismatched pid' assert(os.path.exists(authcookiepath)) except Exception: - time.sleep(0.5) + self.delay_event.wait(0.5) try: if os.name != 'nt' or cc['core_version_group'] > 17: # Litecoin on windows doesn't write a pid file assert(datadir_pid == cc['pid']), 'Mismatched pid' diff --git a/bin/basicswap_prepare.py b/bin/basicswap_prepare.py index 7f0d295..78666af 100755 --- a/bin/basicswap_prepare.py +++ b/bin/basicswap_prepare.py @@ -98,12 +98,12 @@ BTC_RPC_HOST = os.getenv('BTC_RPC_HOST', '127.0.0.1') NMC_RPC_HOST = os.getenv('NMC_RPC_HOST', '127.0.0.1') PART_RPC_PORT = int(os.getenv('PART_RPC_PORT', 19792)) -LTC_RPC_PORT = int(os.getenv('LTC_RPC_PORT', 19795)) -BTC_RPC_PORT = int(os.getenv('BTC_RPC_PORT', 19796)) -NMC_RPC_PORT = int(os.getenv('NMC_RPC_PORT', 19798)) +LTC_RPC_PORT = int(os.getenv('LTC_RPC_PORT', 19895)) +BTC_RPC_PORT = int(os.getenv('BTC_RPC_PORT', 19996)) +NMC_RPC_PORT = int(os.getenv('NMC_RPC_PORT', 19698)) PART_ONION_PORT = int(os.getenv('PART_ONION_PORT', 51734)) -LTC_ONION_PORT = int(os.getenv('LTC_ONION_PORT', 9333)) # Still on 0.18 codebase, same port +LTC_ONION_PORT = int(os.getenv('LTC_ONION_PORT', 9333)) BTC_ONION_PORT = int(os.getenv('BTC_ONION_PORT', 8334)) PART_RPC_USER = os.getenv('PART_RPC_USER', '') @@ -433,10 +433,10 @@ def writeTorSettings(fp, coin, coin_settings, tor_control_password): fp.write(f'torpassword={tor_control_password}\n') fp.write(f'torcontrol={TOR_PROXY_HOST}:{TOR_CONTROL_PORT}\n') - if coin == 'litecoin': - fp.write(f'bind=0.0.0.0:{onionport}\n') - else: + if coin_settings['core_version_group'] >= 21: fp.write(f'bind=0.0.0.0:{onionport}=onion\n') + else: + fp.write(f'bind=0.0.0.0:{onionport}\n') def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}): @@ -545,6 +545,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}): fp.write('createdefaultmasterkey=1') elif coin == 'litecoin': fp.write('prune=4000\n') + fp.write('pid=litecoind.pid\n') if LTC_RPC_USER != '': fp.write('rpcauth={}:{}${}\n'.format(LTC_RPC_USER, salt, password_to_hmac(salt, LTC_RPC_PWD))) elif coin == 'bitcoin': @@ -986,7 +987,7 @@ def main(): 'blocks_confirmed': 2, 'override_feerate': 0.002, 'conf_target': 2, - 'core_version_group': 18, + 'core_version_group': 21, 'chain_lookups': 'local', }, 'litecoin': { @@ -1000,7 +1001,7 @@ def main(): 'use_segwit': True, 'blocks_confirmed': 2, 'conf_target': 2, - 'core_version_group': 18, + 'core_version_group': 21, 'chain_lookups': 'local', }, 'bitcoin': { @@ -1014,7 +1015,7 @@ def main(): 'use_segwit': True, 'blocks_confirmed': 1, 'conf_target': 2, - 'core_version_group': 18, + 'core_version_group': 22, 'chain_lookups': 'local', }, 'namecoin': { diff --git a/tests/basicswap/common.py b/tests/basicswap/common.py index 3dc7793..a9607be 100644 --- a/tests/basicswap/common.py +++ b/tests/basicswap/common.py @@ -196,6 +196,14 @@ def wait_for_in_progress(delay_event, swap_client, bid_id, sent=False): raise ValueError('wait_for_in_progress timed out.') +def post_json_req(url, json_data): + req = urllib.request.Request(url) + req.add_header('Content-Type', 'application/json; charset=utf-8') + post_bytes = json.dumps(json_data).encode('utf-8') + req.add_header('Content-Length', len(post_bytes)) + return urlopen(req, post_bytes).read() + + def read_json_api(port, path=None): url = f'http://127.0.0.1:{port}/json' if path is not None: @@ -203,6 +211,13 @@ def read_json_api(port, path=None): return json.loads(urlopen(url).read()) +def post_json_api(port, path, json_data): + url = f'http://127.0.0.1:{port}/json' + if path is not None: + url += '/' + path + return json.loads(post_json_req(url, json_data)) + + def wait_for_none_active(delay_event, port, wait_for=30): for i in range(wait_for): if delay_event.is_set(): @@ -272,14 +287,6 @@ def wait_for_balance(delay_event, url, balance_key, expect_amount, iterations=20 raise ValueError('Expect {} {}'.format(balance_key, expect_amount)) -def post_json_req(url, json_data): - req = urllib.request.Request(url) - req.add_header('Content-Type', 'application/json; charset=utf-8') - post_bytes = json.dumps(json_data).encode('utf-8') - req.add_header('Content-Length', len(post_bytes)) - return urlopen(req, post_bytes).read() - - def delay_for(delay_event, delay_for=60): logging.info('Delaying for {} seconds.'.format(delay_for)) delay_event.wait(delay_for) diff --git a/tests/basicswap/common_xmr.py b/tests/basicswap/common_xmr.py index 48cf914..c1f0cad 100644 --- a/tests/basicswap/common_xmr.py +++ b/tests/basicswap/common_xmr.py @@ -39,6 +39,11 @@ XMR_BASE_P2P_PORT = 17792 XMR_BASE_RPC_PORT = 29798 XMR_BASE_WALLET_RPC_PORT = 29998 +LTC_BASE_PORT = 34792 +LTC_BASE_RPC_PORT = 35792 +LTC_BASE_ZMQ_PORT = 36792 + + EXTRA_CONFIG_JSON = json.loads(os.getenv('EXTRA_CONFIG_JSON', '{}')) @@ -118,7 +123,7 @@ def run_prepare(node_id, datadir_path, bins_path, with_coins, mnemonic_in=None, settings['chainclients']['particl']['rpcpassword'] = rpc_pass for ip in range(num_nodes): if ip != node_id: - fp.write('connect=127.0.0.1:{}\n'.format(PARTICL_PORT_BASE + ip)) + fp.write('connect=127.0.0.1:{}\n'.format(PARTICL_PORT_BASE + ip + port_ofs)) for opt in EXTRA_CONFIG_JSON.get('part{}'.format(node_id), []): fp.write(opt + '\n') @@ -147,17 +152,44 @@ def run_prepare(node_id, datadir_path, bins_path, with_coins, mnemonic_in=None, settings['chainclients']['bitcoin']['rpcpassword'] = rpc_pass for ip in range(num_nodes): if ip != node_id: - fp.write('connect=127.0.0.1:{}\n'.format(BITCOIN_PORT_BASE + ip)) + fp.write('connect=127.0.0.1:{}\n'.format(BITCOIN_PORT_BASE + ip + port_ofs)) for opt in EXTRA_CONFIG_JSON.get('btc{}'.format(node_id), []): fp.write(opt + '\n') + if 'litecoin' in coins_array: + # Pruned nodes don't provide blocks + with open(os.path.join(datadir_path, 'litecoin', 'litecoin.conf'), 'r') as fp: + lines = fp.readlines() + with open(os.path.join(datadir_path, 'litecoin', 'litecoin.conf'), 'w') as fp: + for line in lines: + if not line.startswith('prune'): + fp.write(line) + fp.write('port={}\n'.format(LTC_BASE_PORT + node_id + port_ofs)) + fp.write('bind=127.0.0.1\n') + fp.write('dnsseed=0\n') + fp.write('discover=0\n') + fp.write('listenonion=0\n') + fp.write('upnp=0\n') + if use_rpcauth: + salt = generate_salt(16) + rpc_user = 'test_ltc_' + str(node_id) + rpc_pass = 'test_ltc_pwd_' + str(node_id) + fp.write('rpcauth={}:{}${}\n'.format(rpc_user, salt, password_to_hmac(salt, rpc_pass))) + settings['chainclients']['litecoin']['rpcuser'] = rpc_user + settings['chainclients']['litecoin']['rpcpassword'] = rpc_pass + for ip in range(num_nodes): + if ip != node_id: + fp.write('connect=127.0.0.1:{}\n'.format(LTC_BASE_PORT + ip + port_ofs)) + for opt in EXTRA_CONFIG_JSON.get('ltc{}'.format(node_id), []): + fp.write(opt + '\n') + if 'monero' in coins_array: with open(os.path.join(datadir_path, 'monero', 'monerod.conf'), 'a') as fp: fp.write('p2p-bind-ip=127.0.0.1\n') fp.write('p2p-bind-port={}\n'.format(XMR_BASE_P2P_PORT + node_id + port_ofs)) for ip in range(num_nodes): if ip != node_id: - fp.write('add-exclusive-node=127.0.0.1:{}\n'.format(XMR_BASE_P2P_PORT + ip)) + fp.write('add-exclusive-node=127.0.0.1:{}\n'.format(XMR_BASE_P2P_PORT + ip + port_ofs)) with open(config_path) as fs: settings = json.load(fs) @@ -200,23 +232,33 @@ def prepare_nodes(num_nodes, extra_coins, use_rpcauth=False, extra_settings={}, num_nodes=num_nodes, use_rpcauth=use_rpcauth, extra_settings=extra_settings, port_ofs=port_ofs) -class XmrTestBase(unittest.TestCase): - @classmethod +class TestBase(unittest.TestCase): def setUpClass(cls): - super(XmrTestBase, cls).setUpClass() + super(TestBase, cls).setUpClass() cls.delay_event = threading.Event() - cls.update_thread = None - cls.processes = [] - - prepare_nodes(3, 'monero') - signal.signal(signal.SIGINT, lambda signal, frame: cls.signal_handler(cls, signal, frame)) def signal_handler(self, sig, frame): logging.info('signal {} detected.'.format(sig)) self.delay_event.set() + def wait_seconds(self, seconds): + self.delay_event.wait(seconds) + if self.delay_event.is_set(): + raise ValueError('Test stopped.') + + +class XmrTestBase(TestBase): + @classmethod + def setUpClass(cls): + super(XmrTestBase, cls).setUpClass(cls) + + cls.update_thread = None + cls.processes = [] + + prepare_nodes(3, 'monero') + def run_thread(self, client_id): client_path = os.path.join(TEST_PATH, 'client{}'.format(client_id)) testargs = ['basicswap-run', '-datadir=' + client_path, '-regtest'] diff --git a/tests/basicswap/extended/test_wallet_init.py b/tests/basicswap/extended/test_wallet_init.py index 4951bfd..e2e3104 100644 --- a/tests/basicswap/extended/test_wallet_init.py +++ b/tests/basicswap/extended/test_wallet_init.py @@ -21,6 +21,7 @@ import time import shutil import logging import unittest +import threading import traceback import multiprocessing from unittest.mock import patch @@ -37,8 +38,6 @@ import bin.basicswap_run as runSystem TEST_PATH = os.path.expanduser(os.getenv('TEST_PATH', '~/test_basicswap1')) -stop_test = False - logger = logging.getLogger() logger.level = logging.DEBUG if not len(logger.handlers): @@ -50,17 +49,19 @@ class Test(unittest.TestCase): def setUpClass(cls): super(Test, cls).setUpClass() - # Load both wallets from the same mnemonic - bins_path = os.path.join(TEST_PATH, 'bin') - for i in range(2): - logging.info('Preparing node: %d.', i) - client_path = os.path.join(TEST_PATH, 'client{}'.format(i)) - try: - shutil.rmtree(client_path) - except Exception as ex: - logging.warning('setUpClass %s', str(ex)) + cls.delay_event = threading.Event() - run_prepare(i, client_path, bins_path, 'monero,bitcoin', mnemonics[0]) + # Load both wallets from the same mnemonic + bins_path = os.path.join(TEST_PATH, 'bin') + for i in range(2): + logging.info('Preparing node: %d.', i) + client_path = os.path.join(TEST_PATH, 'client{}'.format(i)) + try: + shutil.rmtree(client_path) + except Exception as ex: + logging.warning('setUpClass %s', str(ex)) + + run_prepare(i, client_path, bins_path, 'monero,bitcoin', mnemonics[0]) def run_thread(self, client_id): client_path = os.path.join(TEST_PATH, 'client{}'.format(client_id)) @@ -69,7 +70,6 @@ class Test(unittest.TestCase): runSystem.main() def test_wallet(self): - global stop_test update_thread = None processes = [] @@ -79,13 +79,13 @@ class Test(unittest.TestCase): processes[-1].start() try: - waitForServer(12700) + waitForServer(self.delay_event, 12700) wallets_0 = read_json_api(12700, 'wallets') assert(wallets_0['1']['expected_seed'] is True) assert(wallets_0['6']['expected_seed'] is True) - waitForServer(12701) + waitForServer(self.delay_event, 12701) wallets_1 = read_json_api(12701, 'wallets') assert(wallets_0['1']['expected_seed'] is True) @@ -98,7 +98,6 @@ class Test(unittest.TestCase): except Exception: traceback.print_exc() - stop_test = True if update_thread: update_thread.join() for p in processes: diff --git a/tests/basicswap/extended/test_wallet_restore.py b/tests/basicswap/extended/test_wallet_restore.py new file mode 100644 index 0000000..5b4926c --- /dev/null +++ b/tests/basicswap/extended/test_wallet_restore.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +""" +export TEST_PATH=/tmp/test_basicswap_wallet_restore +mkdir -p ${TEST_PATH}/bin +cp -r ~/tmp/basicswap_bin/* ${TEST_PATH}/bin +export PYTHONPATH=$(pwd) +python tests/basicswap/extended/test_wallet_restore.py + + +""" + +import os +import sys +import shutil +import logging +import unittest +import traceback +import multiprocessing +from unittest.mock import patch + +from tests.basicswap.common import ( + read_json_api, + waitForServer, +) +from tests.basicswap.common_xmr import ( + TestBase, + run_prepare, +) +import bin.basicswap_run as runSystem + +TEST_PATH = os.path.expanduser(os.getenv('TEST_PATH', '~/test_basicswap1')) + +logger = logging.getLogger() +logger.level = logging.DEBUG +if not len(logger.handlers): + logger.addHandler(logging.StreamHandler(sys.stdout)) + + +def prepare_node(node_id, mnemonic): + logging.info('Preparing node: %d.', node_id) + bins_path = os.path.join(TEST_PATH, 'bin') + client_path = os.path.join(TEST_PATH, 'client{}'.format(node_id)) + try: + shutil.rmtree(client_path) + except Exception as ex: + logging.warning('setUpClass %s', str(ex)) + return run_prepare(node_id, client_path, bins_path, 'monero,bitcoin,litecoin', mnemonic) + + +class Test(TestBase): + @classmethod + def setUpClass(cls): + super(Test, cls).setUpClass(cls) + + cls.used_mnemonics = [] + # Load wallets from random mnemonics + for i in range(3): + cls.used_mnemonics.append(prepare_node(i, None)) + + def run_thread(self, client_id): + client_path = os.path.join(TEST_PATH, 'client{}'.format(client_id)) + testargs = ['basicswap-run', '-datadir=' + client_path, '-regtest'] + with patch.object(sys, 'argv', testargs): + runSystem.main() + + def test_wallet(self): + update_thread = None + processes = [] + + self.wait_seconds(5) + for i in range(3): + processes.append(multiprocessing.Process(target=self.run_thread, args=(i,))) + processes[-1].start() + + try: + waitForServer(self.delay_event, 12700) + waitForServer(self.delay_event, 12701) + # TODO: Add swaps + + ltc_before = read_json_api(12700, 'wallets/ltc') + + logging.info('Starting a new node on the same mnemonic as the first') + prepare_node(3, self.used_mnemonics[0]) + processes.append(multiprocessing.Process(target=self.run_thread, args=(3,))) + processes[-1].start() + waitForServer(self.delay_event, 12703) + ltc_after = read_json_api(12703, 'wallets/ltc') + + assert(ltc_before['deposit_address'] == ltc_after['deposit_address']) + + except Exception: + traceback.print_exc() + + if update_thread: + update_thread.join() + for p in processes: + p.terminate() + for p in processes: + p.join() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/basicswap/test_reload.py b/tests/basicswap/test_reload.py index 15e3397..8cebf72 100644 --- a/tests/basicswap/test_reload.py +++ b/tests/basicswap/test_reload.py @@ -16,14 +16,11 @@ python tests/basicswap/test_reload.py import os import sys -import json import logging import unittest import traceback import threading import multiprocessing -from urllib import parse -from urllib.request import urlopen from unittest.mock import patch from basicswap.rpc import ( @@ -31,6 +28,7 @@ from basicswap.rpc import ( ) from tests.basicswap.common import ( read_json_api, + post_json_api, waitForServer, waitForNumOffers, waitForNumBids, @@ -104,15 +102,15 @@ class Test(unittest.TestCase): delay_event.wait(2) assert(blocks >= num_blocks) - data = parse.urlencode({ + data = { 'addr_from': '-1', 'coin_from': '1', 'coin_to': '2', 'amt_from': '1', 'amt_to': '1', - 'lockhrs': '24'}).encode() + 'lockhrs': '24'} - offer_id = json.loads(urlopen('http://127.0.0.1:12700/json/offers/new', data=data).read()) + offer_id = post_json_api(12700, 'offers/new', data) summary = read_json_api(12700) assert(summary['num_sent_offers'] == 1) except Exception: @@ -124,21 +122,21 @@ class Test(unittest.TestCase): offers = read_json_api(12701, 'offers') offer = offers[0] - data = parse.urlencode({ + data = { 'offer_id': offer['offer_id'], - 'amount_from': offer['amount_from']}).encode() + 'amount_from': offer['amount_from']} - bid_id = json.loads(urlopen('http://127.0.0.1:12701/json/bids/new', data=data).read()) + bid_id = post_json_api(12701, 'bids/new', data) waitForNumBids(delay_event, 12700, 1) bids = read_json_api(12700, 'bids') bid = bids[0] - data = parse.urlencode({ + data = { 'accept': True - }).encode() - rv = json.loads(urlopen('http://127.0.0.1:12700/json/bids/{}'.format(bid['bid_id']), data=data).read()) + } + rv = post_json_api(12700, 'bids/{}'.format(bid['bid_id']), data) assert(rv['bid_state'] == 'Accepted') waitForNumSwapping(delay_event, 12701, 1) @@ -162,7 +160,6 @@ class Test(unittest.TestCase): delay_event.wait(5) rv = read_json_api(12700, 'bids/{}'.format(bid['bid_id'])) - print(rv) if rv['bid_state'] == 'Completed': break assert(rv['bid_state'] == 'Completed') diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index 07f670e..9b17669 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -22,10 +22,10 @@ from basicswap.db import ( Concepts, ) from basicswap.basicswap import ( - BasicSwap, Coins, - SwapTypes, + BasicSwap, BidStates, + SwapTypes, DebugTypes, ) from basicswap.basicswap_util import ( diff --git a/tests/basicswap/test_xmr_reload.py b/tests/basicswap/test_xmr_reload.py index a9039dd..84a1a7a 100644 --- a/tests/basicswap/test_xmr_reload.py +++ b/tests/basicswap/test_xmr_reload.py @@ -16,15 +16,13 @@ python tests/basicswap/test_xmr_reload.py """ import sys -import json import logging import unittest import multiprocessing -from urllib import parse -from urllib.request import urlopen from tests.basicswap.common import ( read_json_api, + post_json_api, waitForServer, waitForNumOffers, waitForNumBids, @@ -50,15 +48,15 @@ class Test(XmrTestBase): wallets1 = read_json_api(12701, 'wallets') assert(float(wallets1['6']['balance']) > 0.0) - data = parse.urlencode({ + data = { 'addr_from': '-1', 'coin_from': 'part', 'coin_to': 'xmr', 'amt_from': '1', 'amt_to': '1', - 'lockhrs': '24'}).encode() + 'lockhrs': '24'} - offer_id = json.loads(urlopen('http://127.0.0.1:12700/json/offers/new', data=data).read())['offer_id'] + offer_id = post_json_api(12700, 'offers/new', data)['offer_id'] summary = read_json_api(12700) assert(summary['num_sent_offers'] == 1) @@ -73,24 +71,24 @@ class Test(XmrTestBase): 'amount_from': offer['amount_from']} data['valid_for_seconds'] = 24 * 60 * 60 + 1 - bid = json.loads(urlopen('http://127.0.0.1:12701/json/bids/new', data=parse.urlencode(data).encode()).read()) + bid = post_json_api(12701, 'bids/new', data) assert(bid['error'] == 'Bid TTL too high') del data['valid_for_seconds'] data['validmins'] = 24 * 60 + 1 - bid = json.loads(urlopen('http://127.0.0.1:12701/json/bids/new', data=parse.urlencode(data).encode()).read()) + bid = post_json_api(12701, 'bids/new', data) assert(bid['error'] == 'Bid TTL too high') del data['validmins'] data['valid_for_seconds'] = 10 - bid = json.loads(urlopen('http://127.0.0.1:12701/json/bids/new', data=parse.urlencode(data).encode()).read()) + bid = post_json_api(12701, 'bids/new', data) assert(bid['error'] == 'Bid TTL too low') del data['valid_for_seconds'] data['validmins'] = 1 - bid = json.loads(urlopen('http://127.0.0.1:12701/json/bids/new', data=parse.urlencode(data).encode()).read()) + bid = post_json_api(12701, 'bids/new', data) assert(bid['error'] == 'Bid TTL too low') data['validmins'] = 60 - bid_id = json.loads(urlopen('http://127.0.0.1:12701/json/bids/new', data=parse.urlencode(data).encode()).read()) + bid_id = post_json_api(12701, 'bids/new', data) waitForNumBids(self.delay_event, 12700, 1) @@ -102,10 +100,10 @@ class Test(XmrTestBase): self.delay_event.wait(1) assert(bid['expire_at'] == bid['created_at'] + data['validmins'] * 60) - data = parse.urlencode({ + data = { 'accept': True - }).encode() - rv = json.loads(urlopen('http://127.0.0.1:12700/json/bids/{}'.format(bid['bid_id']), data=data).read()) + } + rv = post_json_api(12700, 'bids/{}'.format(bid['bid_id']), data) assert(rv['bid_state'] == 'Accepted') waitForNumSwapping(self.delay_event, 12701, 1) @@ -121,7 +119,7 @@ class Test(XmrTestBase): rv = read_json_api(12701) assert(rv['num_swapping'] == 1) - rv = json.loads(urlopen('http://127.0.0.1:12700/json/revokeoffer/{}'.format(offer_id)).read()) + rv = read_json_api(12700, 'revokeoffer/{}'.format(offer_id)) assert(rv['revoked_offer'] == offer_id) logger.info('Completing swap') @@ -130,7 +128,7 @@ class Test(XmrTestBase): raise ValueError('Test stopped.') self.delay_event.wait(4) - rv = json.loads(urlopen('http://127.0.0.1:12700/json/bids/{}'.format(bid['bid_id'])).read()) + rv = read_json_api(12700, 'bids/{}'.format(bid['bid_id'])) if rv['bid_state'] == 'Completed': break assert(rv['bid_state'] == 'Completed')