From fc31615a97dc728f08e7276167cb4badfb6e7b09 Mon Sep 17 00:00:00 2001
From: tecnovert <tecnovert@tecnovert.net>
Date: Sat, 12 Nov 2022 01:51:30 +0200
Subject: [PATCH] api: Add wallet lock/unlock commands and getcoinseed.

---
 basicswap/__init__.py                         |   2 +-
 basicswap/basicswap.py                        |  80 ++++++---
 basicswap/http_server.py                      |  24 ++-
 basicswap/interface/btc.py                    |  29 ++-
 basicswap/interface/dash.py                   |   5 +-
 basicswap/interface/xmr.py                    |  84 ++++++---
 basicswap/js_server.py                        | 167 +++++++++++++++---
 basicswap/rpc.py                              |   4 +-
 basicswap/ui/page_automation.py               |   3 +
 basicswap/ui/page_bids.py                     |   2 +
 basicswap/ui/page_offers.py                   |   5 +-
 basicswap/ui/page_wallet.py                   |   2 +
 basicswap/util/__init__.py                    |   8 +
 tests/basicswap/common.py                     |  14 +-
 tests/basicswap/extended/test_dash.py         |  26 +--
 tests/basicswap/extended/test_firo.py         |  10 +-
 tests/basicswap/extended/test_nmc.py          |   2 +-
 tests/basicswap/extended/test_pivx.py         |  12 +-
 .../basicswap/extended/test_xmr_persistent.py |   2 +-
 tests/basicswap/test_btc_xmr.py               |  33 ++++
 tests/basicswap/test_run.py                   |  19 +-
 21 files changed, 412 insertions(+), 121 deletions(-)

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'
+        addr = dashRpc('getnewaddress \"hd wallet test\"')
+        assert 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
+        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):