diff --git a/basicswap/__init__.py b/basicswap/__init__.py index f616645..75b84e7 100644 --- a/basicswap/__init__.py +++ b/basicswap/__init__.py @@ -1,3 +1,3 @@ name = "basicswap" -__version__ = "0.11.45" +__version__ = "0.11.46" diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index ead0982..ac48468 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -41,9 +41,10 @@ from . import __version__ from .rpc_xmr import make_xmr_rpc2_func from .ui.util import getCoinName from .util import ( + AutomationConstraint, + LockedCoinError, TemporaryError, InactiveCoin, - AutomationConstraint, format_amount, format_timestamp, DeserialiseNum, @@ -679,11 +680,9 @@ class BasicSwap(BaseApp): raise ValueError('Could not stop {}'.format(str(coin))) def stopDaemons(self): - for c in Coins: - if c not in chainparams: - continue + for c in self.activeCoins(): chain_client_settings = self.getChainClientSettings(c) - if self.coin_clients[c]['connection_type'] == 'rpc' and chain_client_settings['manage_daemon'] is True: + if chain_client_settings['manage_daemon'] is True: self.stopDaemon(c) def waitForDaemonRPC(self, coin_type, with_wallet=True): @@ -710,6 +709,39 @@ class BasicSwap(BaseApp): if synced < 1.0: raise ValueError('{} chain is still syncing, currently at {}.'.format(self.coin_clients[c]['name'], synced)) + def checkSystemStatus(self): + ci = self.ci(Coins.PART) + if ci.isWalletLocked(): + raise LockedCoinError(Coins.PART) + + def activeCoins(self): + for c in Coins: + if c not in chainparams: + continue + chain_client_settings = self.getChainClientSettings(c) + if self.coin_clients[c]['connection_type'] == 'rpc': + yield c + + def changeWalletPasswords(self, old_password, new_password): + # Unlock all wallets to ensure they all have the same password. + for c in self.activeCoins(): + ci = self.ci(c) + try: + ci.unlockWallet(old_password) + except Exception as e: + raise ValueError('Failed to unlock {}'.format(ci.coin_name())) + + for c in self.activeCoins(): + self.ci(c).changeWalletPassword(old_password, new_password) + + def unlockWallets(self, password): + for c in self.activeCoins(): + self.ci(c).unlockWallet(password) + + def lockWallets(self): + for c in self.activeCoins(): + self.ci(c).lockWallet() + def initialiseWallet(self, coin_type, raise_errors=False): if coin_type == Coins.PART: return @@ -5356,6 +5388,8 @@ class BasicSwap(BaseApp): 'balance': format_amount(make_int(walletinfo['balance'], scale), scale), 'unconfirmed': format_amount(make_int(walletinfo.get('unconfirmed_balance'), scale), scale), 'expected_seed': ci.knownWalletSeed(), + 'encrypted': walletinfo['encrypted'], + 'locked': walletinfo['locked'], } if coin == Coins.PART: @@ -5428,16 +5462,13 @@ class BasicSwap(BaseApp): def getWalletsInfo(self, opts=None): rv = {} - for c in Coins: - if c not in chainparams: - continue - if self.coin_clients[c]['connection_type'] == 'rpc': - key = chainparams[c]['ticker'] if opts.get('ticker_key', False) else c - try: - rv[key] = self.getWalletInfo(c) - rv[key].update(self.getBlockchainInfo(c)) - except Exception as ex: - rv[key] = {'name': getCoinName(c), 'error': str(ex)} + for c in self.activeCoins(): + key = chainparams[c]['ticker'] if opts.get('ticker_key', False) else c + try: + rv[key] = self.getWalletInfo(c) + rv[key].update(self.getBlockchainInfo(c)) + except Exception as ex: + rv[key] = {'name': getCoinName(c), 'error': str(ex)} return rv def getCachedWalletsInfo(self, opts=None): @@ -5480,17 +5511,14 @@ class BasicSwap(BaseApp): if opts is not None and 'coin_id' in opts: return rv - for c in Coins: - if c not in chainparams: - continue - if self.coin_clients[c]['connection_type'] == 'rpc': - coin_id = int(c) - if coin_id not in rv: - rv[coin_id] = { - 'name': getCoinName(c), - 'no_data': True, - 'updating': self._updating_wallets_info.get(coin_id, False), - } + for c in self.activeCoins(): + coin_id = int(c) + if coin_id not in rv: + rv[coin_id] = { + 'name': getCoinName(c), + 'no_data': True, + 'updating': self._updating_wallets_info.get(coin_id, False), + } return rv diff --git a/basicswap/http_server.py b/basicswap/http_server.py index 5157045..eddff58 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -185,6 +185,7 @@ class HttpHandler(BaseHTTPRequestHandler): def page_explorers(self, url_split, post_string): swap_client = self.server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() result = None @@ -231,6 +232,7 @@ class HttpHandler(BaseHTTPRequestHandler): def page_rpc(self, url_split, post_string): swap_client = self.server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() result = None @@ -295,6 +297,7 @@ class HttpHandler(BaseHTTPRequestHandler): def page_debug(self, url_split, post_string): swap_client = self.server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() result = None @@ -319,6 +322,7 @@ class HttpHandler(BaseHTTPRequestHandler): def page_active(self, url_split, post_string): swap_client = self.server.swap_client + swap_client.checkSystemStatus() active_swaps = swap_client.listSwapsInProgress() summary = swap_client.getSummary() @@ -331,6 +335,7 @@ class HttpHandler(BaseHTTPRequestHandler): def page_settings(self, url_split, post_string): swap_client = self.server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() messages = [] @@ -412,6 +417,7 @@ class HttpHandler(BaseHTTPRequestHandler): def page_watched(self, url_split, post_string): swap_client = self.server.swap_client + swap_client.checkSystemStatus() watched_outputs, last_scanned = swap_client.listWatchedOutputs() summary = swap_client.getSummary() @@ -425,6 +431,7 @@ class HttpHandler(BaseHTTPRequestHandler): def page_smsgaddresses(self, url_split, post_string): swap_client = self.server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() page_data = {} @@ -502,6 +509,7 @@ class HttpHandler(BaseHTTPRequestHandler): ensure(len(url_split) > 2, 'Address not specified') identity_address = url_split[2] swap_client = self.server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() page_data = {'identity_address': identity_address} @@ -557,6 +565,7 @@ class HttpHandler(BaseHTTPRequestHandler): def page_index(self, url_split): swap_client = self.server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() shutdown_token = os.urandom(8).hex() @@ -587,6 +596,7 @@ class HttpHandler(BaseHTTPRequestHandler): self.end_headers() def handle_http(self, status_code, path, post_string='', is_json=False): + swap_client = self.server.swap_client parsed = parse.urlparse(self.path) url_split = parsed.path.split('/') if post_string == '' and len(parsed.query) > 0: @@ -597,14 +607,13 @@ class HttpHandler(BaseHTTPRequestHandler): func = js_url_to_function(url_split) return func(self, url_split, post_string, is_json) except Exception as ex: - if self.server.swap_client.debug is True: - self.server.swap_client.log.error(traceback.format_exc()) + if swap_client.debug is True: + swap_client.log.error(traceback.format_exc()) return js_error(self, str(ex)) if len(url_split) > 1 and url_split[1] == 'static': try: static_path = os.path.join(os.path.dirname(__file__), 'static') - if len(url_split) > 3 and url_split[2] == 'sequence_diagrams': with open(os.path.join(static_path, 'sequence_diagrams', url_split[3]), 'rb') as fp: self.putHeaders(status_code, 'image/svg+xml') @@ -639,13 +648,14 @@ class HttpHandler(BaseHTTPRequestHandler): except FileNotFoundError: return self.page_404(url_split) except Exception as ex: - if self.server.swap_client.debug is True: - self.server.swap_client.log.error(traceback.format_exc()) + if swap_client.debug is True: + swap_client.log.error(traceback.format_exc()) return self.page_error(str(ex)) try: if len(url_split) > 1: page = url_split[1] + if page == 'active': return self.page_active(url_split, post_string) if page == 'wallets': @@ -700,8 +710,8 @@ class HttpHandler(BaseHTTPRequestHandler): return self.page_404(url_split) return self.page_index(url_split) except Exception as ex: - if self.server.swap_client.debug is True: - self.server.swap_client.log.error(traceback.format_exc()) + if swap_client.debug is True: + swap_client.log.error(traceback.format_exc()) return self.page_error(str(ex)) def do_GET(self): diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 1115f42..937e204 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -262,7 +262,10 @@ class BTCInterface(CoinInterface): self.rpc_callback('sethdseed', [True, key_wif]) def getWalletInfo(self): - return self.rpc_callback('getwalletinfo') + rv = self.rpc_callback('getwalletinfo') + rv['encrypted'] = 'unlocked_until' in rv + rv['locked'] = rv.get('unlocked_until', 1) <= 0 + return rv def walletRestoreHeight(self): return self._restore_height @@ -1277,6 +1280,30 @@ class BTCInterface(CoinInterface): return self.getUTXOBalance(address) + def isWalletEncrypted(self): + wallet_info = self.rpc_callback('getwalletinfo') + return 'unlocked_until' in wallet_info + + def isWalletLocked(self): + wallet_info = self.rpc_callback('getwalletinfo') + if 'unlocked_until' in wallet_info and wallet_info['unlocked_until'] <= 0: + return True + return False + + def changeWalletPassword(self, old_password, new_password): + if old_password == '': + return self.rpc_callback('encryptwallet', [new_password]) + self.rpc_callback('walletpassphrasechange', [old_password, new_password]) + + def unlockWallet(self, password): + if password == '': + return + # Max timeout value, ~3 years + self.rpc_callback('walletpassphrase', [password, 100000000]) + + def lockWallet(self): + self.rpc_callback('walletlock') + def testBTCInterface(): print('TODO: testBTCInterface') diff --git a/basicswap/interface/dash.py b/basicswap/interface/dash.py index 4597ecc..783e386 100644 --- a/basicswap/interface/dash.py +++ b/basicswap/interface/dash.py @@ -15,8 +15,11 @@ class DASHInterface(BTCInterface): def coin_type(): return Coins.DASH + def seedToMnemonic(self, key): + return Mnemonic('english').to_mnemonic(key) + def initialiseWallet(self, key): - words = Mnemonic('english').to_mnemonic(key) + words = self.seedToMnemonic(key) self.rpc_callback('upgradetohd', [words, ]) def checkExpectedSeed(self, key_hash): diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index 31d349e..42c214a 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -74,6 +74,7 @@ class XMRInterface(CoinInterface): self.setFeePriority(coin_settings.get('fee_priority', 0)) self._sc = swap_client self._log = self._sc.log if self._sc and self._sc.log else logging + self._wallet_password = None def setFeePriority(self, new_priority): ensure(new_priority >= 0 and new_priority < 4, 'Invalid fee_priority value') @@ -82,10 +83,22 @@ class XMRInterface(CoinInterface): def setWalletFilename(self, wallet_filename): self._wallet_filename = wallet_filename + def createWallet(self, params): + if self._wallet_password is not None: + params['password'] = self._wallet_password + rv = self.rpc_wallet_cb('generate_from_keys', params) + self._log.info('generate_from_keys %s', dumpj(rv)) + + def openWallet(self, filename): + params = {'filename': filename} + if self._wallet_password is not None: + params['password'] = self._wallet_password + self.rpc_wallet_cb('open_wallet', params) + def initialiseWallet(self, key_view, key_spend, restore_height=None): with self._mx_wallet: try: - self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename}) + self.openWallet(self._wallet_filename) # TODO: Check address return # Wallet exists except Exception as e: @@ -102,13 +115,12 @@ class XMRInterface(CoinInterface): 'spendkey': b2h(key_spend[::-1]), 'restore_height': self._restore_height, } - rv = self.rpc_wallet_cb('generate_from_keys', params) - self._log.info('generate_from_keys %s', dumpj(rv)) - self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename}) + self.createWallet(params) + self.openWallet(self._wallet_filename) def ensureWalletExists(self): with self._mx_wallet: - self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename}) + self.openWallet(self._wallet_filename) def testDaemonRPC(self, with_wallet=True): self.rpc_wallet_cb('get_languages') @@ -149,12 +161,21 @@ class XMRInterface(CoinInterface): def getWalletInfo(self): with self._mx_wallet: - self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename}) + try: + self.openWallet(self._wallet_filename) + except Exception as e: + if 'Failed to open wallet' in str(e): + rv = {'encrypted': True, 'locked': True, 'balance': 0, 'unconfirmed_balance': 0} + return rv + raise e + rv = {} self.rpc_wallet_cb('refresh') balance_info = self.rpc_wallet_cb('get_balance') rv['balance'] = self.format_amount(balance_info['unlocked_balance']) rv['unconfirmed_balance'] = self.format_amount(balance_info['balance'] - balance_info['unlocked_balance']) + rv['encrypted'] = False if self._wallet_password is None else True + rv['locked'] = False return rv def walletRestoreHeight(self): @@ -162,12 +183,12 @@ class XMRInterface(CoinInterface): def getMainWalletAddress(self): with self._mx_wallet: - self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename}) + self.openWallet(self._wallet_filename) return self.rpc_wallet_cb('get_address')['address'] def getNewAddress(self, placeholder): with self._mx_wallet: - self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename}) + self.openWallet(self._wallet_filename) return self.rpc_wallet_cb('create_address', {'account_index': 0})['address'] def get_fee_rate(self, conf_target=2): @@ -230,7 +251,7 @@ class XMRInterface(CoinInterface): def publishBLockTx(self, Kbv, Kbs, output_amount, feerate, delay_for=10): with self._mx_wallet: - self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename}) + self.openWallet(self._wallet_filename) shared_addr = xmr_util.encode_address(Kbv, Kbs) @@ -275,10 +296,10 @@ class XMRInterface(CoinInterface): try: rv = self.rpc_wallet_cb('open_wallet', {'filename': address_b58}) + self.openWallet(address_b58) except Exception as e: - rv = self.rpc_wallet_cb('generate_from_keys', params) - self._log.info('generate_from_keys %s', dumpj(rv)) - rv = self.rpc_wallet_cb('open_wallet', {'filename': address_b58}) + self.createWallet(params) + self.openWallet(address_b58) self.rpc_wallet_cb('refresh', timeout=600) @@ -319,9 +340,8 @@ class XMRInterface(CoinInterface): 'viewkey': b2h(kbv[::-1]), 'restore_height': restore_height, } - self.rpc_wallet_cb('generate_from_keys', params) - - self.rpc_wallet_cb('open_wallet', {'filename': address_b58}) + self.createWallet(params) + self.openWallet(address_b58) # For a while after opening the wallet rpc cmds return empty data num_tries = 40 @@ -367,7 +387,7 @@ class XMRInterface(CoinInterface): def findTxnByHash(self, txid): with self._mx_wallet: - self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename}) + self.openWallet(self._wallet_filename) self.rpc_wallet_cb('refresh') try: @@ -409,11 +429,10 @@ class XMRInterface(CoinInterface): } try: - self.rpc_wallet_cb('open_wallet', {'filename': wallet_filename}) + self.openWallet(wallet_filename) except Exception as e: - rv = self.rpc_wallet_cb('generate_from_keys', params) - self._log.info('generate_from_keys %s', dumpj(rv)) - self.rpc_wallet_cb('open_wallet', {'filename': wallet_filename}) + self.createWallet(params) + self.openWallet(wallet_filename) self.rpc_wallet_cb('refresh') rv = self.rpc_wallet_cb('get_balance') @@ -454,7 +473,7 @@ class XMRInterface(CoinInterface): with self._mx_wallet: value_sats = make_int(value, self.exp()) - self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename}) + self.openWallet(self._wallet_filename) if subfee: balance = self.rpc_wallet_cb('get_balance') @@ -479,10 +498,10 @@ class XMRInterface(CoinInterface): address_b58 = xmr_util.encode_address(Kbv, Kbs) wallet_file = address_b58 + '_spend' try: - self.rpc_wallet_cb('open_wallet', {'filename': wallet_file}) + self.openWallet(wallet_file) except Exception: wallet_file = address_b58 - self.rpc_wallet_cb('open_wallet', {'filename': wallet_file}) + self.openWallet(wallet_file) self.rpc_wallet_cb('refresh') @@ -494,8 +513,25 @@ class XMRInterface(CoinInterface): def getSpendableBalance(self): with self._mx_wallet: - self.rpc_wallet_cb('open_wallet', {'filename': self._wallet_filename}) + self.openWallet(self._wallet_filename) self.rpc_wallet_cb('refresh') balance_info = self.rpc_wallet_cb('get_balance') return balance_info['unlocked_balance'] + + def changeWalletPassword(self, old_password, new_password): + orig_password = self._wallet_password + if old_password != '': + self._wallet_password = old_password + try: + self.openWallet(self._wallet_filename) + self.rpc_wallet_cb('change_wallet_password', {'old_password': old_password, 'new_password': new_password}) + except Exception as e: + self._wallet_password = orig_password + raise e + + def unlockWallet(self, password): + self._wallet_password = password + + def lockWallet(self): + self._wallet_password = None diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 4f736a7..9eb4558 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -88,6 +88,7 @@ def js_coins(self, url_split, post_string, is_json): def js_wallets(self, url_split, post_string, is_json): swap_client = self.server.swap_client + swap_client.checkSystemStatus() if len(url_split) > 3: ticker_str = url_split[3] coin_type = tickerToCoinId(ticker_str) @@ -108,6 +109,7 @@ def js_wallets(self, url_split, post_string, is_json): def js_offers(self, url_split, post_string, is_json, sent=False): swap_client = self.server.swap_client + swap_client.checkSystemStatus() offer_id = None if len(url_split) > 3: if url_split[3] == 'new': @@ -186,6 +188,7 @@ def js_sentoffers(self, url_split, post_string, is_json): def js_bids(self, url_split, post_string, is_json): swap_client = self.server.swap_client + swap_client.checkSystemStatus() if len(url_split) > 3: if url_split[3] == 'new': if post_string == '': @@ -287,22 +290,29 @@ def js_bids(self, url_split, post_string, is_json): def js_sentbids(self, url_split, post_string, is_json): - return bytes(json.dumps(self.server.swap_client.listBids(sent=True)), 'UTF-8') + swap_client = self.server.swap_client + swap_client.checkSystemStatus() + return bytes(json.dumps(swap_client.listBids(sent=True)), 'UTF-8') def js_network(self, url_split, post_string, is_json): - return bytes(json.dumps(self.server.swap_client.get_network_info()), 'UTF-8') + swap_client = self.server.swap_client + swap_client.checkSystemStatus() + return bytes(json.dumps(swap_client.get_network_info()), 'UTF-8') def js_revokeoffer(self, url_split, post_string, is_json): + swap_client = self.server.swap_client + swap_client.checkSystemStatus() offer_id = bytes.fromhex(url_split[3]) assert (len(offer_id) == 28) - self.server.swap_client.revokeOffer(offer_id) + swap_client.revokeOffer(offer_id) return bytes(json.dumps({'revoked_offer': offer_id.hex()}), 'UTF-8') def js_smsgaddresses(self, url_split, post_string, is_json): swap_client = self.server.swap_client + swap_client.checkSystemStatus() if len(url_split) > 3: if post_string == '': raise ValueError('No post data') @@ -394,7 +404,9 @@ def js_rate(self, url_split, post_string, is_json): def js_index(self, url_split, post_string, is_json): - return bytes(json.dumps(self.server.swap_client.getSummary()), 'UTF-8') + swap_client = self.server.swap_client + swap_client.checkSystemStatus() + return bytes(json.dumps(swap_client.getSummary()), 'UTF-8') def js_generatenotification(self, url_split, post_string, is_json): @@ -418,6 +430,7 @@ def js_generatenotification(self, url_split, post_string, is_json): def js_notifications(self, url_split, post_string, is_json): swap_client = self.server.swap_client + swap_client.checkSystemStatus() swap_client.getNotifications() return bytes(json.dumps(swap_client.getNotifications()), 'UTF-8') @@ -425,28 +438,140 @@ def js_notifications(self, url_split, post_string, is_json): def js_vacuumdb(self, url_split, post_string, is_json): swap_client = self.server.swap_client + swap_client.checkSystemStatus() swap_client.vacuumDB() return bytes(json.dumps({'completed': True}), 'UTF-8') +def js_getcoinseed(self, url_split, post_string, is_json): + swap_client = self.server.swap_client + swap_client.checkSystemStatus() + if post_string == '': + raise ValueError('No post data') + if is_json: + post_data = json.loads(post_string) + post_data['is_json'] = True + else: + post_data = urllib.parse.parse_qs(post_string) + + coin = getCoinType(get_data_entry(post_data, 'coin')) + if coin in (Coins.PART, Coins.PART_ANON, Coins.PART_BLIND): + raise ValueError('Particl wallet seed is set from the Basicswap mnemonic.') + + ci = swap_client.ci(coin) + seed = swap_client.getWalletKey(coin, 1) + if coin == Coins.DASH: + return bytes(json.dumps({'coin': ci.ticker(), 'seed': seed.hex(), 'mnemonic': ci.seedToMnemonic(seed)}), 'UTF-8') + return bytes(json.dumps({'coin': ci.ticker(), 'seed': seed.hex()}), 'UTF-8') + + +def js_setpassword(self, url_split, post_string, is_json): + # Set or change wallet passwords + # Only works with currently enabled coins + # Will fail if any coin does not unlock on the old password + swap_client = self.server.swap_client + if post_string == '': + raise ValueError('No post data') + if is_json: + post_data = json.loads(post_string) + post_data['is_json'] = True + else: + post_data = urllib.parse.parse_qs(post_string) + + old_password = get_data_entry(post_data, 'oldpassword') + new_password = get_data_entry(post_data, 'newpassword') + + if have_data_entry(post_data, 'coin'): + # Set password for one coin + coin = getCoinType(get_data_entry(post_data, 'coin')) + if coin in (Coins.PART_ANON, Coins.PART_BLIND): + raise ValueError('Invalid coin.') + swap_client.ci(coin).changeWalletPassword(old_password, new_password) + return bytes(json.dumps({'success': True}), 'UTF-8') + + # Set password for all coins + swap_client.changeWalletPasswords(old_password, new_password) + return bytes(json.dumps({'success': True}), 'UTF-8') + + +def js_unlock(self, url_split, post_string, is_json): + swap_client = self.server.swap_client + if post_string == '': + raise ValueError('No post data') + if is_json: + post_data = json.loads(post_string) + post_data['is_json'] = True + else: + post_data = urllib.parse.parse_qs(post_string) + + password = get_data_entry(post_data, 'password') + + if have_data_entry(post_data, 'coin'): + coin = getCoinType(str(get_data_entry(post_data, 'coin'))) + if coin in (Coins.PART_ANON, Coins.PART_BLIND): + raise ValueError('Invalid coin.') + swap_client.ci(coin).unlockWallet(password) + return bytes(json.dumps({'success': True}), 'UTF-8') + + swap_client.unlockWallets(password) + return bytes(json.dumps({'success': True}), 'UTF-8') + + +def js_lock(self, url_split, post_string, is_json): + swap_client = self.server.swap_client + if post_string == '': + raise ValueError('No post data') + if is_json: + post_data = json.loads(post_string) + post_data['is_json'] = True + else: + post_data = urllib.parse.parse_qs(post_string) + + if have_data_entry(post_data, 'coin'): + coin = getCoinType(get_data_entry(post_data, 'coin')) + if coin in (Coins.PART_ANON, Coins.PART_BLIND): + raise ValueError('Invalid coin.') + swap_client.ci(coin).lockWallet() + return bytes(json.dumps({'success': True}), 'UTF-8') + + swap_client.lockWallets() + return bytes(json.dumps({'success': True}), 'UTF-8') + + +def js_help(self, url_split, post_string, is_json): + # TODO: Add details and examples + commands = [] + for k in pages: + commands.append(k) + return bytes(json.dumps({'commands': commands}), 'UTF-8') + + +pages = { + 'coins': js_coins, + 'wallets': js_wallets, + 'offers': js_offers, + 'sentoffers': js_sentoffers, + 'bids': js_bids, + 'sentbids': js_sentbids, + 'network': js_network, + 'revokeoffer': js_revokeoffer, + 'smsgaddresses': js_smsgaddresses, + 'rate': js_rate, + 'rates': js_rates, + 'rateslist': js_rates_list, + 'generatenotification': js_generatenotification, + 'notifications': js_notifications, + 'vacuumdb': js_vacuumdb, + 'getcoinseed': js_getcoinseed, + 'setpassword': js_setpassword, + 'unlock': js_unlock, + 'lock': js_lock, + 'help': js_help, +} + + def js_url_to_function(url_split): if len(url_split) > 2: - return { - 'coins': js_coins, - 'wallets': js_wallets, - 'offers': js_offers, - 'sentoffers': js_sentoffers, - 'bids': js_bids, - 'sentbids': js_sentbids, - 'network': js_network, - 'revokeoffer': js_revokeoffer, - 'smsgaddresses': js_smsgaddresses, - 'rate': js_rate, - 'rates': js_rates, - 'rateslist': js_rates_list, - 'generatenotification': js_generatenotification, - 'notifications': js_notifications, - 'vacuumdb': js_vacuumdb, - }.get(url_split[2], js_index) + return pages.get(url_split[2], js_index) return js_index diff --git a/basicswap/rpc.py b/basicswap/rpc.py index 77e2f63..d95a065 100644 --- a/basicswap/rpc.py +++ b/basicswap/rpc.py @@ -130,13 +130,15 @@ def openrpc(rpc_port, auth, wallet=None, host='127.0.0.1'): raise ValueError('RPC error ' + str(ex)) -def callrpc_cli(bindir, datadir, chain, cmd, cli_bin='particl-cli'): +def callrpc_cli(bindir, datadir, chain, cmd, cli_bin='particl-cli', wallet=None): cli_bin = os.path.join(bindir, cli_bin) args = [cli_bin, ] if chain != 'mainnet': args.append('-' + chain) args.append('-datadir=' + datadir) + if wallet is not None: + args.append('-rpcwallet=' + wallet) args += shlex.split(cmd) p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) diff --git a/basicswap/ui/page_automation.py b/basicswap/ui/page_automation.py index 6790e54..f13dce5 100644 --- a/basicswap/ui/page_automation.py +++ b/basicswap/ui/page_automation.py @@ -21,6 +21,7 @@ from basicswap.db import ( def page_automation_strategies(self, url_split, post_string): server = self.server swap_client = server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() filters = { @@ -61,6 +62,7 @@ def page_automation_strategies(self, url_split, post_string): def page_automation_strategy_new(self, url_split, post_string): server = self.server swap_client = self.server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() messages = [] @@ -82,6 +84,7 @@ def page_automation_strategy(self, url_split, post_string): server = self.server swap_client = self.server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() messages = [] diff --git a/basicswap/ui/page_bids.py b/basicswap/ui/page_bids.py index 4b7d095..adefa86 100644 --- a/basicswap/ui/page_bids.py +++ b/basicswap/ui/page_bids.py @@ -37,6 +37,7 @@ def page_bid(self, url_split, post_string): raise ValueError('Bad bid ID') server = self.server swap_client = server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() messages = [] @@ -121,6 +122,7 @@ def page_bid(self, url_split, post_string): def page_bids(self, url_split, post_string, sent=False, available=False, received=False): server = self.server swap_client = server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() filters = { diff --git a/basicswap/ui/page_offers.py b/basicswap/ui/page_offers.py index ce3531a..da81ecd 100644 --- a/basicswap/ui/page_offers.py +++ b/basicswap/ui/page_offers.py @@ -319,7 +319,8 @@ def offer_to_post_string(self, swap_client, offer_id): def page_newoffer(self, url_split, post_string): server = self.server - swap_client = server.swap_client + swap_client = self.server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() messages = [] @@ -400,6 +401,7 @@ def page_offer(self, url_split, post_string): offer_id = decode_offer_id(url_split[2]) server = self.server swap_client = server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() offer, xmr_offer = swap_client.getXmrOffer(offer_id) ensure(offer, 'Unknown offer ID') @@ -564,6 +566,7 @@ def page_offer(self, url_split, post_string): def page_offers(self, url_split, post_string, sent=False): server = self.server swap_client = server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() filters = { diff --git a/basicswap/ui/page_wallet.py b/basicswap/ui/page_wallet.py index 47d2489..eec23a8 100644 --- a/basicswap/ui/page_wallet.py +++ b/basicswap/ui/page_wallet.py @@ -61,6 +61,7 @@ def format_wallet_data(ci, w): def page_wallets(self, url_split, post_string): server = self.server swap_client = server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() page_data = {} @@ -165,6 +166,7 @@ def page_wallet(self, url_split, post_string): wallet_ticker = url_split[2] server = self.server swap_client = server.swap_client + swap_client.checkSystemStatus() summary = swap_client.getSummary() coin_id = getCoinIdFromTicker(wallet_ticker) diff --git a/basicswap/util/__init__.py b/basicswap/util/__init__.py index 840e420..eec8a70 100644 --- a/basicswap/util/__init__.py +++ b/basicswap/util/__init__.py @@ -33,6 +33,14 @@ class InactiveCoin(Exception): return str(self.coinid) +class LockedCoinError(Exception): + def __init__(self, coinid): + self.coinid = coinid + + def __str__(self): + return 'Coin must be unlocked: ' + str(self.coinid) + + def ensure(v, err_string): if not v: raise ValueError(err_string) diff --git a/tests/basicswap/common.py b/tests/basicswap/common.py index 4c8cb77..020f739 100644 --- a/tests/basicswap/common.py +++ b/tests/basicswap/common.py @@ -217,13 +217,23 @@ def post_json_req(url, json_data): 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() + return urlopen(req, post_bytes, timeout=300).read() -def read_json_api(port, path=None): +def read_text_api(port, path=None): url = f'http://127.0.0.1:{port}/json' if path is not None: url += '/' + path + return urlopen(url, timeout=300).read().decode('utf-8') + + +def read_json_api(port, path=None, json_data=None): + url = f'http://127.0.0.1:{port}/json' + if path is not None: + url += '/' + path + + if json_data is not None: + return json.loads(post_json_req(url, json_data)) return json.loads(urlopen(url, timeout=300).read()) diff --git a/tests/basicswap/extended/test_dash.py b/tests/basicswap/extended/test_dash.py index 7039a2f..8675e35 100644 --- a/tests/basicswap/extended/test_dash.py +++ b/tests/basicswap/extended/test_dash.py @@ -14,6 +14,7 @@ import os import sys import json import time +import random import shutil import signal import logging @@ -204,8 +205,8 @@ def btcRpc(cmd): return callrpc_cli(cfg.BITCOIN_BINDIR, os.path.join(cfg.TEST_DATADIRS, str(BTC_NODE)), 'regtest', cmd, cfg.BITCOIN_CLI) -def dashRpc(cmd): - return callrpc_cli(cfg.DASH_BINDIR, os.path.join(cfg.TEST_DATADIRS, str(DASH_NODE)), 'regtest', cmd, cfg.DASH_CLI) +def dashRpc(cmd, wallet=None): + return callrpc_cli(cfg.DASH_BINDIR, os.path.join(cfg.TEST_DATADIRS, str(DASH_NODE)), 'regtest', cmd, cfg.DASH_CLI, wallet=wallet) def signal_handler(sig, frame): @@ -533,18 +534,17 @@ class Test(unittest.TestCase): self.swap_clients[0].initialiseWallet(Coins.DASH, raise_errors=True) assert self.swap_clients[0].checkWalletSeed(Coins.DASH) is True - pivx_addr = dashRpc('getnewaddress \"hd test\"') - assert pivx_addr == 'ybzWYJbZEhZai8kiKkTtPFKTuDNwhpiwac' - - def pass_99_delay(self): - global stop_test - logging.info('Delay') - for i in range(60 * 5): - if stop_test: - break - time.sleep(1) - print('delay', i) - stop_test = True + addr = dashRpc('getnewaddress \"hd wallet test\"') + assert addr == 'ybzWYJbZEhZai8kiKkTtPFKTuDNwhpiwac' + + logging.info('Test that getcoinseed returns a mnemonic for Dash') + mnemonic = read_json_api(1800, 'getcoinseed', {'coin': 'DASH'})['mnemonic'] + new_wallet_name = random.randbytes(10).hex() + dashRpc(f'createwallet \"{new_wallet_name}\"') + dashRpc(f'upgradetohd \"{mnemonic}\"', wallet=new_wallet_name) + addr_test = dashRpc('getnewaddress', wallet=new_wallet_name) + dashRpc('unloadwallet', wallet=new_wallet_name) + assert (addr_test == addr) if __name__ == '__main__': diff --git a/tests/basicswap/extended/test_firo.py b/tests/basicswap/extended/test_firo.py index c8e9112..5baf4cb 100644 --- a/tests/basicswap/extended/test_firo.py +++ b/tests/basicswap/extended/test_firo.py @@ -108,6 +108,8 @@ class Test(BaseTest): firo_daemons = [] firo_addr = None test_coin_from = Coins.FIRO + start_ltc_nodes = False + start_xmr_nodes = False test_atomic = True test_xmr = False @@ -119,12 +121,6 @@ class Test(BaseTest): 'c5de2be44834e7e47ad7dc8e35c6b77c79f17c6bb40d5509a00fc3dff384a865', ] - @classmethod - def setUpClass(cls): - cls.start_ltc_nodes = False - cls.start_xmr_nodes = False - super(Test, cls).setUpClass() - @classmethod def prepareExtraDataDir(cls, i): if not cls.restore_instance: @@ -135,7 +131,7 @@ class Test(BaseTest): callrpc_cli(cfg.FIRO_BINDIR, data_dir, 'regtest', '-wallet=wallet.dat create', 'firo-wallet') cls.firo_daemons.append(startDaemon(os.path.join(cfg.TEST_DATADIRS, 'firo_' + str(i)), cfg.FIRO_BINDIR, cfg.FIROD, opts=extra_opts)) - logging.info('Started %s %d', cfg.FIROD, cls.part_daemons[-1].pid) + logging.info('Started %s %d', cfg.FIROD, cls.firo_daemons[-1].pid) waitForRPC(make_rpc_func(i, base_rpc_port=FIRO_BASE_RPC_PORT)) diff --git a/tests/basicswap/extended/test_nmc.py b/tests/basicswap/extended/test_nmc.py index fbe97f3..7b60f54 100644 --- a/tests/basicswap/extended/test_nmc.py +++ b/tests/basicswap/extended/test_nmc.py @@ -349,7 +349,7 @@ class Test(unittest.TestCase): num_blocks = 3 logging.info('Waiting for Particl chain height %d', num_blocks) for i in range(60): - particl_blocks = cls.swap_clients[0].callrpc('getblockchaininfo')['blocks'] + particl_blocks = cls.swap_clients[0].callrpc('getblockcount') print('particl_blocks', particl_blocks) if particl_blocks >= num_blocks: break diff --git a/tests/basicswap/extended/test_pivx.py b/tests/basicswap/extended/test_pivx.py index 136b03e..10a95a2 100644 --- a/tests/basicswap/extended/test_pivx.py +++ b/tests/basicswap/extended/test_pivx.py @@ -360,7 +360,7 @@ class Test(unittest.TestCase): num_blocks = 3 logging.info('Waiting for Particl chain height %d', num_blocks) for i in range(60): - particl_blocks = cls.swap_clients[0].callrpc('getblockchaininfo')['blocks'] + particl_blocks = cls.swap_clients[0].callrpc('getblockcount') print('particl_blocks', particl_blocks) if particl_blocks >= num_blocks: break @@ -563,16 +563,6 @@ class Test(unittest.TestCase): break assert found - def pass_99_delay(self): - global stop_test - logging.info('Delay') - for i in range(60 * 5): - if stop_test: - break - time.sleep(1) - print('delay', i) - stop_test = True - if __name__ == '__main__': unittest.main() diff --git a/tests/basicswap/extended/test_xmr_persistent.py b/tests/basicswap/extended/test_xmr_persistent.py index a398a20..10d1f6d 100644 --- a/tests/basicswap/extended/test_xmr_persistent.py +++ b/tests/basicswap/extended/test_xmr_persistent.py @@ -176,7 +176,7 @@ class Test(unittest.TestCase): for i in range(60): if self.delay_event.is_set(): raise ValueError('Test stopped.') - particl_blocks = callpartrpc(0, 'getblockchaininfo')['blocks'] + particl_blocks = callpartrpc(0, 'getblockcount') print('particl_blocks', particl_blocks) if particl_blocks >= num_blocks: break diff --git a/tests/basicswap/test_btc_xmr.py b/tests/basicswap/test_btc_xmr.py index bcb6e27..11ebbdc 100644 --- a/tests/basicswap/test_btc_xmr.py +++ b/tests/basicswap/test_btc_xmr.py @@ -425,6 +425,39 @@ class TestBTC(BasicSwapTest): start_ltc_nodes = False base_rpc_port = BTC_BASE_RPC_PORT + def test_009_wallet_encryption(self): + + for coin in ('btc', 'part', 'xmr'): + jsw = read_json_api(1800, f'wallets/{coin}') + assert (jsw['encrypted'] is False) + assert (jsw['locked'] is False) + + rv = read_json_api(1800, 'setpassword', {'oldpassword': '', 'newpassword': 'notapassword123'}) + + # Entire system is locked with Particl wallet + jsw = read_json_api(1800, 'wallets/btc') + assert ('Coin must be unlocked' in jsw['error']) + + read_json_api(1800, 'unlock', {'coin': 'part', 'password': 'notapassword123'}) + + for coin in ('btc', 'xmr'): + jsw = read_json_api(1800, f'wallets/{coin}') + assert (jsw['encrypted'] is True) + assert (jsw['locked'] is True) + + read_json_api(1800, 'lock', {'coin': 'part'}) + jsw = read_json_api(1800, 'wallets/part') + assert ('Coin must be unlocked' in jsw['error']) + + read_json_api(1800, 'setpassword', {'oldpassword': 'notapassword123', 'newpassword': 'notapassword456'}) + + read_json_api(1800, 'unlock', {'password': 'notapassword456'}) + + for coin in ('part', 'btc', 'xmr'): + jsw = read_json_api(1800, f'wallets/{coin}') + assert (jsw['encrypted'] is True) + assert (jsw['locked'] is False) + if __name__ == '__main__': unittest.main() diff --git a/tests/basicswap/test_run.py b/tests/basicswap/test_run.py index fbe5e23..a511745 100644 --- a/tests/basicswap/test_run.py +++ b/tests/basicswap/test_run.py @@ -14,7 +14,6 @@ $ pytest -v -s tests/basicswap/test_run.py::Test::test_04_ltc_btc """ import os -import json import random import logging import unittest @@ -43,7 +42,6 @@ from tests.basicswap.common import ( wait_for_balance, wait_for_bid_tx_state, wait_for_in_progress, - post_json_req, read_json_api, TEST_HTTP_PORT, LTC_BASE_RPC_PORT, @@ -114,6 +112,21 @@ class Test(BaseTest): rv = read_json_api(1800, 'rateslist?from=PART&to=BTC') assert len(rv) == 2 + def test_003_api(self): + logging.info('---------- Test API') + + help_output = read_json_api(1800, 'help') + assert ('getcoinseed' in help_output['commands']) + + rv = read_json_api(1800, 'getcoinseed') + assert (rv['error'] == 'No post data') + + rv = read_json_api(1800, 'getcoinseed', {'coin': 'PART'}) + assert ('seed is set from the Basicswap mnemonic' in rv['error']) + + rv = read_json_api(1800, 'getcoinseed', {'coin': 'BTC'}) + assert (rv['seed'] == '8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b') + def test_01_verifyrawtransaction(self): txn = '0200000001eb6e5c4ebba4efa32f40c7314cad456a64008e91ee30b2dd0235ab9bb67fbdbb01000000ee47304402200956933242dde94f6cf8f195a470f8d02aef21ec5c9b66c5d3871594bdb74c9d02201d7e1b440de8f4da672d689f9e37e98815fb63dbc1706353290887eb6e8f7235012103dc1b24feb32841bc2f4375da91fa97834e5983668c2a39a6b7eadb60e7033f9d205a803b28fe2f86c17db91fa99d7ed2598f79b5677ffe869de2e478c0d1c02cc7514c606382012088a8201fe90717abb84b481c2a59112414ae56ec8acc72273642ca26cc7a5812fdc8f68876a914225fbfa4cb725b75e511810ac4d6f74069bdded26703520140b27576a914207eb66b2fd6ed9924d6217efc7fa7b38dfabe666888acffffffff01e0167118020000001976a9140044e188928710cecba8311f1cf412135b98145c88ac00000000' prevout = { @@ -412,7 +425,7 @@ class Test(BaseTest): 'address': ltc_addr, 'subfee': False, } - json_rv = json.loads(post_json_req('http://127.0.0.1:{}/json/wallets/ltc/withdraw'.format(TEST_HTTP_PORT + 0), post_json)) + json_rv = read_json_api('json/wallets/ltc/withdraw', TEST_HTTP_PORT + 0, post_json) assert (len(json_rv['txid']) == 64) def test_13_itx_refund(self):