From 3be72b3c71271cd0149e3853d575d255d7244124 Mon Sep 17 00:00:00 2001
From: tecnovert <tecnovert@tecnovert.net>
Date: Sun, 1 Dec 2024 17:11:44 +0200
Subject: [PATCH] Add to test_xmr_persistent.

---
 basicswap/basicswap.py                        |   2 +-
 basicswap/bin/prepare.py                      |  50 +--
 basicswap/chainparams.py                      |   1 +
 basicswap/interface/doge.py                   |  29 +-
 tests/basicswap/common_xmr.py                 |  61 ++-
 tests/basicswap/extended/test_doge.py         |  69 +++
 tests/basicswap/extended/test_wow.py          |   1 -
 .../basicswap/extended/test_xmr_persistent.py | 400 ++++++++++--------
 8 files changed, 397 insertions(+), 216 deletions(-)
 create mode 100644 tests/basicswap/extended/test_doge.py

diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py
index a9f605f..19f61f2 100644
--- a/basicswap/basicswap.py
+++ b/basicswap/basicswap.py
@@ -355,7 +355,7 @@ class BasicSwap(BaseApp):
 
         # TODO: Set dynamically
         self.balance_only_coins = (Coins.LTC_MWEB,)
-        self.scriptless_coins = (Coins.XMR, Coins.WOW, Coins.PART_ANON, Coins.FIRO)
+        self.scriptless_coins = (Coins.XMR, Coins.WOW, Coins.PART_ANON, Coins.FIRO, Coins.DOGE)
         self.adaptor_swap_only_coins = self.scriptless_coins + (
             Coins.PART_BLIND,
             Coins.BCH,
diff --git a/basicswap/bin/prepare.py b/basicswap/bin/prepare.py
index 8f8e880..2d6e0c8 100755
--- a/basicswap/bin/prepare.py
+++ b/basicswap/bin/prepare.py
@@ -53,15 +53,9 @@ PARTICL_LINUX_EXTRA = os.getenv("PARTICL_LINUX_EXTRA", "nousb")
 LITECOIN_VERSION = os.getenv("LITECOIN_VERSION", "0.21.3")
 LITECOIN_VERSION_TAG = os.getenv("LITECOIN_VERSION_TAG", "")
 
-DOGECOIN_VERSION = os.getenv("DOGECOIN_VERSION", "1.14.7")
-DOGECOIN_VERSION_TAG = os.getenv("DOGECOIN_VERSION_TAG", "")
-
 BITCOIN_VERSION = os.getenv("BITCOIN_VERSION", "26.0")
 BITCOIN_VERSION_TAG = os.getenv("BITCOIN_VERSION_TAG", "")
 
-BITCOINCASH_VERSION = os.getenv("BITCOINCASH_VERSION", "27.1.0")
-BITCOINCASH_VERSION_TAG = os.getenv("BITCOINCASH_VERSION_TAG", "")
-
 MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.3.4")
 MONERO_VERSION_TAG = os.getenv("MONERO_VERSION_TAG", "")
 XMR_SITE_COMMIT = (
@@ -89,6 +83,12 @@ NAV_VERSION_TAG = os.getenv("NAV_VERSION_TAG", "")
 DCR_VERSION = os.getenv("DCR_VERSION", "1.8.1")
 DCR_VERSION_TAG = os.getenv("DCR_VERSION_TAG", "")
 
+BITCOINCASH_VERSION = os.getenv("BITCOINCASH_VERSION", "27.1.0")
+BITCOINCASH_VERSION_TAG = os.getenv("BITCOINCASH_VERSION_TAG", "")
+
+DOGECOIN_VERSION = os.getenv("DOGECOIN_VERSION", "1.14.7")
+DOGECOIN_VERSION_TAG = os.getenv("DOGECOIN_VERSION_TAG", "")
+
 GUIX_SSL_CERT_DIR = None
 
 ADD_PUBKEY_URL = os.getenv("ADD_PUBKEY_URL", "")
@@ -101,9 +101,7 @@ SKIP_GPG_VALIDATION = toBool(os.getenv("SKIP_GPG_VALIDATION", "false"))
 known_coins = {
     "particl": (PARTICL_VERSION, PARTICL_VERSION_TAG, ("tecnovert",)),
     "bitcoin": (BITCOIN_VERSION, BITCOIN_VERSION_TAG, ("laanwj",)),
-    "bitcoincash": (BITCOINCASH_VERSION, BITCOINCASH_VERSION_TAG, ("Calin_Culianu",)),
     "litecoin": (LITECOIN_VERSION, LITECOIN_VERSION_TAG, ("davidburkett38",)),
-    "dogecoin": (DOGECOIN_VERSION, DOGECOIN_VERSION_TAG, ("patricklodder",)),
     "decred": (DCR_VERSION, DCR_VERSION_TAG, ("decred_release",)),
     "namecoin": ("0.18.0", "", ("JeremyRand",)),
     "monero": (MONERO_VERSION, MONERO_VERSION_TAG, ("binaryfate",)),
@@ -112,6 +110,8 @@ known_coins = {
     "dash": (DASH_VERSION, DASH_VERSION_TAG, ("pasta",)),
     "firo": (FIRO_VERSION, FIRO_VERSION_TAG, ("reuben",)),
     "navcoin": (NAV_VERSION, NAV_VERSION_TAG, ("nav_builder",)),
+    "bitcoincash": (BITCOINCASH_VERSION, BITCOINCASH_VERSION_TAG, ("Calin_Culianu",)),
+    "dogecoin": (DOGECOIN_VERSION, DOGECOIN_VERSION_TAG, ("patricklodder",)),
 }
 
 disabled_coins = [
@@ -203,25 +203,12 @@ LTC_ONION_PORT = int(os.getenv("LTC_ONION_PORT", 9333))
 LTC_RPC_USER = os.getenv("LTC_RPC_USER", "")
 LTC_RPC_PWD = os.getenv("LTC_RPC_PWD", "")
 
-DOGE_RPC_HOST = os.getenv("DOGE_RPC_HOST", "127.0.0.1")
-DOGE_RPC_PORT = int(os.getenv("DOGE_RPC_PORT", 42069))
-DOGE_ONION_PORT = int(os.getenv("DOGE_ONION_PORT", 6969))
-DOGE_RPC_USER = os.getenv("DOGE_RPC_USER", "")
-DOGE_RPC_PWD = os.getenv("DOGE_RPC_PWD", "")
-
 BTC_RPC_HOST = os.getenv("BTC_RPC_HOST", "127.0.0.1")
 BTC_RPC_PORT = int(os.getenv("BTC_RPC_PORT", 19996))
 BTC_ONION_PORT = int(os.getenv("BTC_ONION_PORT", 8334))
 BTC_RPC_USER = os.getenv("BTC_RPC_USER", "")
 BTC_RPC_PWD = os.getenv("BTC_RPC_PWD", "")
 
-BCH_RPC_HOST = os.getenv("BCH_RPC_HOST", "127.0.0.1")
-BCH_RPC_PORT = int(os.getenv("BCH_RPC_PORT", 19997))
-BCH_ONION_PORT = int(os.getenv("BCH_ONION_PORT", 8335))
-BCH_PORT = int(os.getenv("BCH_PORT", 19798))
-BCH_RPC_USER = os.getenv("BCH_RPC_USER", "")
-BCH_RPC_PWD = os.getenv("BCH_RPC_PWD", "")
-
 DCR_RPC_HOST = os.getenv("DCR_RPC_HOST", "127.0.0.1")
 DCR_RPC_PORT = int(os.getenv("DCR_RPC_PORT", 9109))
 DCR_WALLET_RPC_HOST = os.getenv("DCR_WALLET_RPC_HOST", "127.0.0.1")
@@ -259,6 +246,19 @@ NAV_ONION_PORT = int(os.getenv("NAV_ONION_PORT", 8334))  # TODO?
 NAV_RPC_USER = os.getenv("NAV_RPC_USER", "")
 NAV_RPC_PWD = os.getenv("NAV_RPC_PWD", "")
 
+BCH_RPC_HOST = os.getenv("BCH_RPC_HOST", "127.0.0.1")
+BCH_RPC_PORT = int(os.getenv("BCH_RPC_PORT", 19997))
+BCH_ONION_PORT = int(os.getenv("BCH_ONION_PORT", 8335))
+BCH_PORT = int(os.getenv("BCH_PORT", 19798))
+BCH_RPC_USER = os.getenv("BCH_RPC_USER", "")
+BCH_RPC_PWD = os.getenv("BCH_RPC_PWD", "")
+
+DOGE_RPC_HOST = os.getenv("DOGE_RPC_HOST", "127.0.0.1")
+DOGE_RPC_PORT = int(os.getenv("DOGE_RPC_PORT", 42069))
+DOGE_ONION_PORT = int(os.getenv("DOGE_ONION_PORT", 6969))
+DOGE_RPC_USER = os.getenv("DOGE_RPC_USER", "")
+DOGE_RPC_PWD = os.getenv("DOGE_RPC_PWD", "")
+
 TOR_PROXY_HOST = os.getenv("TOR_PROXY_HOST", "127.0.0.1")
 TOR_PROXY_PORT = int(os.getenv("TOR_PROXY_PORT", 9050))
 TOR_CONTROL_PORT = int(os.getenv("TOR_CONTROL_PORT", 9051))
@@ -1250,7 +1250,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
                 fp.write(chainname + "=1\n")
             else:
                 fp.write(chain + "=1\n")
-            if coin not in ("firo", "navcoin"):
+            if coin not in ("firo", "navcoin", "dogecoin"):
                 if chain == "testnet":
                     fp.write("[test]\n\n")
                 elif chain == "regtest":
@@ -1730,7 +1730,6 @@ def initialise_wallets(
                 Coins.PART,
                 Coins.BTC,
                 Coins.LTC,
-                Coins.DOGE,
                 Coins.DCR,
                 Coins.DASH,
             )
@@ -2298,10 +2297,11 @@ def main():
             "onionport": DOGE_ONION_PORT + port_offset,
             "datadir": os.getenv("DOGE_DATA_DIR", os.path.join(data_dir, "dogecoin")),
             "bindir": os.path.join(bin_dir, "dogecoin"),
-            "use_segwit": True,
+            "use_segwit": False,
+            "use_csv": False,
             "blocks_confirmed": 2,
             "conf_target": 2,
-            "core_version_group": 21,  # TODO ofrnxmr
+            "core_version_group": 14,
             "min_relay_fee": 0.00001,
         },
         "decred": {
diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py
index f174b86..f4734d5 100644
--- a/basicswap/chainparams.py
+++ b/basicswap/chainparams.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 
 # Copyright (c) 2019-2024 tecnovert
+# Copyright (c) 2024 The Basicswap developers
 # Distributed under the MIT software license, see the accompanying
 # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
 
diff --git a/basicswap/interface/doge.py b/basicswap/interface/doge.py
index c9224ec..f5db26e 100644
--- a/basicswap/interface/doge.py
+++ b/basicswap/interface/doge.py
@@ -1,16 +1,43 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
-# Copyright (c) 2020-2024 tecnovert
 # Copyright (c) 2024 The BasicSwap developers
 # Distributed under the MIT software license, see the accompanying
 # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
 
 from .btc import BTCInterface
 from basicswap.chainparams import Coins
+from basicswap.rpc import make_rpc_func
 
 
 class DOGEInterface(BTCInterface):
     @staticmethod
     def coin_type():
         return Coins.DOGE
+
+    def __init__(self, coin_settings, network, swap_client=None):
+        super(DOGEInterface, self).__init__(coin_settings, network, swap_client)
+        # No multiwallet support
+        self.rpc_wallet = make_rpc_func(
+            self._rpcport, self._rpcauth, host=self._rpc_host
+        )
+
+    def initialiseWallet(self, key):
+        # load with -hdseed= parameter
+        pass
+
+    def checkWallets(self) -> int:
+        return 1
+
+    def getNewAddress(self, use_segwit, label="swap_receive"):
+        return self.rpc("getnewaddress", [label])
+
+    def isWatchOnlyAddress(self, address):
+        addr_info = self.rpc("validateaddress", [address])
+        return addr_info["iswatchonly"]
+
+    def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
+        addr_info = self.rpc("validateaddress", [address])
+        if not or_watch_only:
+            return addr_info["ismine"]
+        return addr_info["ismine"] or addr_info["iswatchonly"]
diff --git a/tests/basicswap/common_xmr.py b/tests/basicswap/common_xmr.py
index 8414074..c471e11 100644
--- a/tests/basicswap/common_xmr.py
+++ b/tests/basicswap/common_xmr.py
@@ -50,6 +50,17 @@ from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
 import basicswap.config as cfg
 import basicswap.bin.run as runSystem
 
+XMR_BASE_P2P_PORT = 17792
+XMR_BASE_RPC_PORT = 29798
+XMR_BASE_WALLET_RPC_PORT = 29998
+
+FIRO_BASE_PORT = 34832
+FIRO_BASE_RPC_PORT = 35832
+FIRO_RPC_PORT_BASE = int(os.getenv("FIRO_RPC_PORT_BASE", FIRO_BASE_RPC_PORT))
+
+DOGE_BASE_PORT = 22556
+DOGE_BASE_RPC_PORT = 18442
+
 TEST_PATH = os.path.expanduser(os.getenv("TEST_PATH", "~/test_basicswap1"))
 
 PARTICL_PORT_BASE = int(os.getenv("PARTICL_PORT_BASE", BASE_PORT))
@@ -64,16 +75,9 @@ DECRED_RPC_PORT_BASE = int(os.getenv("DECRED_RPC_PORT_BASE", DCR_BASE_RPC_PORT))
 BITCOINCASH_RPC_PORT_BASE = int(
     os.getenv("BITCOINCASH_RPC_PORT_BASE", BCH_BASE_RPC_PORT)
 )
-
-
-FIRO_BASE_PORT = 34832
-FIRO_BASE_RPC_PORT = 35832
-FIRO_RPC_PORT_BASE = int(os.getenv("FIRO_RPC_PORT_BASE", FIRO_BASE_RPC_PORT))
-
-
-XMR_BASE_P2P_PORT = 17792
-XMR_BASE_RPC_PORT = 29798
-XMR_BASE_WALLET_RPC_PORT = 29998
+DOGECOIN_RPC_PORT_BASE = int(
+    os.getenv("DOGECOIN_RPC_PORT_BASE", DOGE_BASE_RPC_PORT)
+)
 
 EXTRA_CONFIG_JSON = json.loads(os.getenv("EXTRA_CONFIG_JSON", "{}"))
 
@@ -131,9 +135,11 @@ def run_prepare(
     os.environ["BTC_RPC_PORT"] = str(BITCOIN_RPC_PORT_BASE)
     os.environ["LTC_RPC_PORT"] = str(LITECOIN_RPC_PORT_BASE)
     os.environ["DCR_RPC_PORT"] = str(DECRED_RPC_PORT_BASE)
+    os.environ["FIRO_RPC_PORT"] = str(FIRO_RPC_PORT_BASE)
     os.environ["BCH_PORT"] = str(BCH_BASE_PORT)
     os.environ["BCH_RPC_PORT"] = str(BITCOINCASH_RPC_PORT_BASE)
-    os.environ["FIRO_RPC_PORT"] = str(FIRO_RPC_PORT_BASE)
+    os.environ["DOGE_PORT"] = str(DOGE_BASE_PORT)
+    os.environ["DOGE_RPC_PORT"] = str(DOGECOIN_RPC_PORT_BASE)
 
     os.environ["XMR_RPC_USER"] = "xmr_user"
     os.environ["XMR_RPC_PWD"] = "xmr_pwd"
@@ -433,6 +439,39 @@ def run_prepare(
             for opt in EXTRA_CONFIG_JSON.get("bch{}".format(node_id), []):
                 fp.write(opt + "\n")
 
+    if "dogecoin" in coins_array:
+        config_filename = os.path.join(datadir_path, "dogecoin", "dogecoin.conf")
+        with open(config_filename, "r") as fp:
+            lines = fp.readlines()
+        with open(config_filename, "w") as fp:
+            for line in lines:
+                if not line.startswith("prune"):
+                    fp.write(line)
+            fp.write("port={}\n".format(DOGE_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_doge_" + str(node_id)
+                rpc_pass = "test_doge_pwd_" + str(node_id)
+                fp.write(
+                    "rpcauth={}:{}${}\n".format(
+                        rpc_user, salt, password_to_hmac(salt, rpc_pass)
+                    )
+                )
+                settings["chainclients"]["dogecoin"]["rpcuser"] = rpc_user
+                settings["chainclients"]["dogecoin"]["rpcpassword"] = rpc_pass
+            for ip in range(num_nodes):
+                if ip != node_id:
+                    fp.write(
+                        "connect=127.0.0.1:{}\n".format(DOGE_BASE_PORT + ip + port_ofs)
+                    )
+            for opt in EXTRA_CONFIG_JSON.get("doge{}".format(node_id), []):
+                fp.write(opt + "\n")
+
     with open(config_path) as fs:
         settings = json.load(fs)
 
diff --git a/tests/basicswap/extended/test_doge.py b/tests/basicswap/extended/test_doge.py
new file mode 100644
index 0000000..96682c0
--- /dev/null
+++ b/tests/basicswap/extended/test_doge.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 The Basicswap developers
+# Distributed under the MIT software license, see the accompanying
+# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
+
+"""
+export RESET_TEST=true
+export TEST_PATH=/tmp/test_doge
+mkdir -p ${TEST_PATH}/bin
+cp -r ~/tmp/basicswap_bin/* ${TEST_PATH}/bin
+export PYTHONPATH=$(pwd)
+export TEST_COINS_LIST='bitcoin,dogecoin'
+python tests/basicswap/extended/test_doge.py
+
+"""
+
+import os
+import sys
+import json
+import time
+import random
+import signal
+import logging
+import unittest
+import threading
+import multiprocessing
+from unittest.mock import patch
+
+from tests.basicswap.extended.test_xmr_persistent import (
+    BaseTestWithPrepare,
+    UI_PORT,
+)
+from tests.basicswap.extended.test_scripts import (
+    wait_for_offers,
+)
+from tests.basicswap.util import (
+    read_json_api,
+)
+
+
+logger = logging.getLogger()
+logger.level = logging.DEBUG
+if not len(logger.handlers):
+    logger.addHandler(logging.StreamHandler(sys.stdout))
+
+
+class DOGETest(BaseTestWithPrepare):
+    def test_a(self):
+        read_json_api(UI_PORT + 0, "wallets/doge/reseed")
+        read_json_api(UI_PORT + 1, "wallets/doge/reseed")
+
+        offer_json = {
+            "coin_from": "btc",
+            "coin_to": "doge",
+            "amt_from": 10.0,
+            "amt_to": 100.0,
+            "amt_var": True,
+            "lockseconds": 3600,
+        }
+        offer_id = read_json_api(UI_PORT + 0, "offers/new", offer_json)["offer_id"]
+        logging.debug(f"offer_id {offer_id}")
+
+        wait_for_offers(self.delay_event, 1, 1, offer_id)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/basicswap/extended/test_wow.py b/tests/basicswap/extended/test_wow.py
index 681f579..b231bb3 100644
--- a/tests/basicswap/extended/test_wow.py
+++ b/tests/basicswap/extended/test_wow.py
@@ -103,7 +103,6 @@ class Test(BaseTest):
 
     @classmethod
     def prepareExtraCoins(cls):
-        pass
         num_blocks = 300
         cls.wow_addr = cls.callwownodewallet(cls, 1, "get_address")["address"]
         if callrpc_xmr(WOW_BASE_RPC_PORT + 1, "get_block_count")["count"] < num_blocks:
diff --git a/tests/basicswap/extended/test_xmr_persistent.py b/tests/basicswap/extended/test_xmr_persistent.py
index a833119..a95f136 100644
--- a/tests/basicswap/extended/test_xmr_persistent.py
+++ b/tests/basicswap/extended/test_xmr_persistent.py
@@ -56,11 +56,11 @@ from tests.basicswap.util import (
 from tests.basicswap.common_xmr import (
     prepare_nodes,
     XMR_BASE_RPC_PORT,
+    DOGE_BASE_RPC_PORT,
 )
 from basicswap.interface.dcr.rpc import callrpc as callrpc_dcr
 import basicswap.bin.run as runSystem
 
-
 test_path = os.path.expanduser(os.getenv("TEST_PATH", "/tmp/test_persistent"))
 RESET_TEST = make_boolean(os.getenv("RESET_TEST", "false"))
 
@@ -70,6 +70,7 @@ UI_PORT = 12700 + PORT_OFS
 PARTICL_RPC_PORT_BASE = int(os.getenv("PARTICL_RPC_PORT_BASE", BASE_RPC_PORT))
 BITCOIN_RPC_PORT_BASE = int(os.getenv("BITCOIN_RPC_PORT_BASE", BTC_BASE_RPC_PORT))
 LITECOIN_RPC_PORT_BASE = int(os.getenv("LITECOIN_RPC_PORT_BASE", LTC_BASE_RPC_PORT))
+DOGECOIN_RPC_PORT_BASE = int(os.getenv("DOGECOIN_RPC_PORT_BASE", DOGE_BASE_RPC_PORT))
 BITCOINCASH_RPC_PORT_BASE = int(
     os.getenv("BITCOINCASH_RPC_PORT_BASE", BCH_BASE_RPC_PORT)
 )
@@ -137,6 +138,17 @@ def callbchrpc(
     return callrpc(base_rpc_port + node_id, auth, method, params, wallet)
 
 
+def calldogerpc(
+    node_id,
+    method,
+    params=[],
+    wallet=None,
+    base_rpc_port=DOGECOIN_RPC_PORT_BASE + PORT_OFS,
+):
+    auth = "test_doge_{0}:test_doge_pwd_{0}".format(node_id)
+    return callrpc(base_rpc_port + node_id, auth, method, params, wallet)
+
+
 def updateThread(cls):
     while not cls.delay_event.is_set():
         try:
@@ -146,6 +158,8 @@ def updateThread(cls):
                 callltcrpc(0, "generatetoaddress", [1, cls.ltc_addr])
             if cls.bch_addr is not None:
                 callbchrpc(0, "generatetoaddress", [1, cls.bch_addr])
+            if cls.doge_addr is not None:
+                calldogerpc(0, "generatetoaddress", [1, cls.doge_addr])
         except Exception as e:
             print("updateThread error", str(e))
         cls.delay_event.wait(random.randrange(cls.update_min, cls.update_max))
@@ -204,74 +218,33 @@ def updateThreadDCR(cls):
         cls.delay_event.wait(random.randrange(cls.dcr_update_min, cls.dcr_update_max))
 
 
-class Test(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls):
-        super(Test, cls).setUpClass()
+def signal_handler(self, sig, frame):
+    logging.info("signal {} detected.".format(sig))
+    self.delay_event.set()
 
-        cls.update_min = int(os.getenv("UPDATE_THREAD_MIN_WAIT", "1"))
-        cls.update_max = cls.update_min * 4
 
-        cls.xmr_update_min = int(os.getenv("XMR_UPDATE_THREAD_MIN_WAIT", "1"))
-        cls.xmr_update_max = cls.xmr_update_min * 4
+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()
 
-        cls.dcr_update_min = int(os.getenv("DCR_UPDATE_THREAD_MIN_WAIT", "1"))
-        cls.dcr_update_max = cls.dcr_update_min * 4
 
-        cls.delay_event = threading.Event()
-        cls.update_thread = None
-        cls.update_thread_xmr = None
-        cls.update_thread_dcr = None
-        cls.processes = []
-        cls.btc_addr = None
-        cls.ltc_addr = None
-        cls.bch_addr = None
-        cls.xmr_addr = None
-        cls.dcr_addr = "SsYbXyjkKAEXXcGdFgr4u4bo4L8RkCxwQpH"
-        cls.dcr_acc = None
+def start_processes(self):
+    self.delay_event.clear()
 
-        random.seed(time.time())
-
-        if os.path.exists(test_path) and not RESET_TEST:
-            logging.info(f"Continuing with existing directory: {test_path}")
-        else:
-            logging.info("Preparing %d nodes.", NUM_NODES)
-            prepare_nodes(
-                NUM_NODES,
-                TEST_COINS_LIST,
-                True,
-                {"min_sequence_lock_seconds": 60},
-                PORT_OFS,
-            )
-
-        signal.signal(
-            signal.SIGINT, lambda signal, frame: cls.signal_handler(cls, signal, frame)
+    for i in range(NUM_NODES):
+        self.processes.append(
+            multiprocessing.Process(target=run_thread, args=(self, i,))
         )
+        self.processes[-1].start()
 
-    def signal_handler(self, sig, frame):
-        logging.info("signal {} detected.".format(sig))
-        self.delay_event.set()
+    for i in range(NUM_NODES):
+        waitForServer(self.delay_event, UI_PORT + i)
 
-    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 start_processes(self):
-        self.delay_event.clear()
-
-        for i in range(NUM_NODES):
-            self.processes.append(
-                multiprocessing.Process(target=self.run_thread, args=(i,))
-            )
-            self.processes[-1].start()
-
-        for i in range(NUM_NODES):
-            waitForServer(self.delay_event, UI_PORT + i)
-
-        wallets = read_json_api(UI_PORT + 1, "wallets")
+    wallets = read_json_api(UI_PORT + 1, "wallets")
 
+    if "monero" in TEST_COINS_LIST:
         xmr_auth = None
         if os.getenv("XMR_RPC_USER", "") != "":
             xmr_auth = (os.getenv("XMR_RPC_USER", ""), os.getenv("XMR_RPC_PWD", ""))
@@ -300,127 +273,192 @@ class Test(unittest.TestCase):
             ],
         )
 
-        self.btc_addr = callbtcrpc(0, "getnewaddress", ["mining_addr", "bech32"])
-        num_blocks: int = 500  # Mine enough to activate segwit
-        if callbtcrpc(0, "getblockcount") < num_blocks:
-            logging.info("Mining %d Bitcoin blocks to %s", num_blocks, self.btc_addr)
-            callbtcrpc(0, "generatetoaddress", [num_blocks, self.btc_addr])
-        logging.info("BTC blocks: %d", callbtcrpc(0, "getblockcount"))
+    self.btc_addr = callbtcrpc(0, "getnewaddress", ["mining_addr", "bech32"])
+    num_blocks: int = 500  # Mine enough to activate segwit
+    if callbtcrpc(0, "getblockcount") < num_blocks:
+        logging.info("Mining %d Bitcoin blocks to %s", num_blocks, self.btc_addr)
+        callbtcrpc(0, "generatetoaddress", [num_blocks, self.btc_addr])
+    logging.info("BTC blocks: %d", callbtcrpc(0, "getblockcount"))
 
-        if "litecoin" in TEST_COINS_LIST:
-            self.ltc_addr = callltcrpc(
-                0, "getnewaddress", ["mining_addr"], wallet="wallet.dat"
+    if "litecoin" in TEST_COINS_LIST:
+        self.ltc_addr = callltcrpc(
+            0, "getnewaddress", ["mining_addr"], wallet="wallet.dat"
+        )
+        num_blocks: int = 431
+        have_blocks: int = callltcrpc(0, "getblockcount")
+        if have_blocks < 500:
+            logging.info(
+                "Mining %d Litecoin blocks to %s", num_blocks, self.ltc_addr
             )
-            num_blocks: int = 431
+            callltcrpc(
+                0,
+                "generatetoaddress",
+                [num_blocks - have_blocks, self.ltc_addr],
+                wallet="wallet.dat",
+            )
+
+            # https://github.com/litecoin-project/litecoin/issues/807
+            # Block 432 is when MWEB activates. It requires a peg-in. You'll need to generate an mweb address and send some coins to it. Then it will allow you to mine the next block.
+            mweb_addr = callltcrpc(
+                0, "getnewaddress", ["mweb_addr", "mweb"], wallet="mweb"
+            )
+            callltcrpc(0, "sendtoaddress", [mweb_addr, 1.0], wallet="wallet.dat")
+            num_blocks = 69
+
             have_blocks: int = callltcrpc(0, "getblockcount")
-            if have_blocks < 500:
-                logging.info(
-                    "Mining %d Litecoin blocks to %s", num_blocks, self.ltc_addr
-                )
-                callltcrpc(
-                    0,
-                    "generatetoaddress",
-                    [num_blocks - have_blocks, self.ltc_addr],
-                    wallet="wallet.dat",
-                )
-
-                # https://github.com/litecoin-project/litecoin/issues/807
-                # Block 432 is when MWEB activates. It requires a peg-in. You'll need to generate an mweb address and send some coins to it. Then it will allow you to mine the next block.
-                mweb_addr = callltcrpc(
-                    0, "getnewaddress", ["mweb_addr", "mweb"], wallet="mweb"
-                )
-                callltcrpc(0, "sendtoaddress", [mweb_addr, 1.0], wallet="wallet.dat")
-                num_blocks = 69
-
-                have_blocks: int = callltcrpc(0, "getblockcount")
-                callltcrpc(
-                    0,
-                    "generatetoaddress",
-                    [500 - have_blocks, self.ltc_addr],
-                    wallet="wallet.dat",
-                )
-
-        if "decred" in TEST_COINS_LIST:
-            if RESET_TEST:
-                _ = calldcrrpc(0, "getnewaddress")
-                # assert (addr == self.dcr_addr)
-                self.dcr_acc = calldcrrpc(
-                    0,
-                    "getaccount",
-                    [
-                        self.dcr_addr,
-                    ],
-                )
-                calldcrrpc(
-                    0,
-                    "generate",
-                    [
-                        110,
-                    ],
-                )
-            else:
-                self.dcr_acc = calldcrrpc(
-                    0,
-                    "getaccount",
-                    [
-                        self.dcr_addr,
-                    ],
-                )
-
-            self.update_thread_dcr = threading.Thread(
-                target=updateThreadDCR, args=(self,)
+            callltcrpc(
+                0,
+                "generatetoaddress",
+                [500 - have_blocks, self.ltc_addr],
+                wallet="wallet.dat",
             )
-            self.update_thread_dcr.start()
-
-        if "bitcoincash" in TEST_COINS_LIST:
-            self.bch_addr = callbchrpc(
-                0, "getnewaddress", ["mining_addr"], wallet="wallet.dat"
-            )
-            num_blocks: int = 200
-            have_blocks: int = callbchrpc(0, "getblockcount")
-            if have_blocks < num_blocks:
-                logging.info(
-                    "Mining %d Bitcoincash blocks to %s",
-                    num_blocks - have_blocks,
-                    self.bch_addr,
-                )
-                callbchrpc(
-                    0,
-                    "generatetoaddress",
-                    [num_blocks - have_blocks, self.bch_addr],
-                    wallet="wallet.dat",
-                )
 
+    if "decred" in TEST_COINS_LIST:
         if RESET_TEST:
-            # Lower output split threshold for more stakeable outputs
-            for i in range(NUM_NODES):
-                callpartrpc(
-                    i,
-                    "walletsettings",
-                    [
-                        "stakingoptions",
-                        {"stakecombinethreshold": 100, "stakesplitthreshold": 200},
-                    ],
-                )
-        self.update_thread = threading.Thread(target=updateThread, args=(self,))
-        self.update_thread.start()
+            _ = calldcrrpc(0, "getnewaddress")
+            # assert (addr == self.dcr_addr)
+            self.dcr_acc = calldcrrpc(
+                0,
+                "getaccount",
+                [
+                    self.dcr_addr,
+                ],
+            )
+            calldcrrpc(
+                0,
+                "generate",
+                [
+                    110,
+                ],
+            )
+        else:
+            self.dcr_acc = calldcrrpc(
+                0,
+                "getaccount",
+                [
+                    self.dcr_addr,
+                ],
+            )
 
-        self.update_thread_xmr = threading.Thread(target=updateThreadXMR, args=(self,))
-        self.update_thread_xmr.start()
+        self.update_thread_dcr = threading.Thread(
+            target=updateThreadDCR, args=(self,)
+        )
+        self.update_thread_dcr.start()
 
-        # Wait for height, or sequencelock is thrown off by genesis blocktime
-        num_blocks = 3
-        logging.info("Waiting for Particl chain height %d", num_blocks)
-        for i in range(60):
-            if self.delay_event.is_set():
-                raise ValueError("Test stopped.")
-            particl_blocks = callpartrpc(0, "getblockcount")
-            print("particl_blocks", particl_blocks)
-            if particl_blocks >= num_blocks:
-                break
-            self.delay_event.wait(1)
-        logging.info("PART blocks: %d", callpartrpc(0, "getblockcount"))
-        assert particl_blocks >= num_blocks
+    if "bitcoincash" in TEST_COINS_LIST:
+        self.bch_addr = callbchrpc(
+            0, "getnewaddress", ["mining_addr"], wallet="wallet.dat"
+        )
+        num_blocks: int = 200
+        have_blocks: int = callbchrpc(0, "getblockcount")
+        if have_blocks < num_blocks:
+            logging.info(
+                "Mining %d Bitcoincash blocks to %s",
+                num_blocks - have_blocks,
+                self.bch_addr,
+            )
+            callbchrpc(
+                0,
+                "generatetoaddress",
+                [num_blocks - have_blocks, self.bch_addr],
+                wallet="wallet.dat",
+            )
+
+    if "dogecoin" in TEST_COINS_LIST:
+        self.doge_addr = calldogerpc(
+            0, "getnewaddress", ["mining_addr"])
+        num_blocks: int = 200
+        have_blocks: int = calldogerpc(0, "getblockcount")
+        if have_blocks < num_blocks:
+            logging.info(
+                "Mining %d Dogecoin blocks to %s",
+                num_blocks - have_blocks,
+                self.bch_addr,
+            )
+            calldogerpc(
+                0,
+                "generatetoaddress",
+                [num_blocks - have_blocks, self.doge_addr])
+
+    if RESET_TEST:
+        # Lower output split threshold for more stakeable outputs
+        for i in range(NUM_NODES):
+            callpartrpc(
+                i,
+                "walletsettings",
+                [
+                    "stakingoptions",
+                    {"stakecombinethreshold": 100, "stakesplitthreshold": 200},
+                ],
+            )
+    self.update_thread = threading.Thread(target=updateThread, args=(self,))
+    self.update_thread.start()
+
+    self.update_thread_xmr = threading.Thread(target=updateThreadXMR, args=(self,))
+    self.update_thread_xmr.start()
+
+    # Wait for height, or sequencelock is thrown off by genesis blocktime
+    num_blocks = 3
+    logging.info("Waiting for Particl chain height %d", num_blocks)
+    for i in range(60):
+        if self.delay_event.is_set():
+            raise ValueError("Test stopped.")
+        particl_blocks = callpartrpc(0, "getblockcount")
+        print("particl_blocks", particl_blocks)
+        if particl_blocks >= num_blocks:
+            break
+        self.delay_event.wait(1)
+    logging.info("PART blocks: %d", callpartrpc(0, "getblockcount"))
+    assert particl_blocks >= num_blocks
+
+class BaseTestWithPrepare(unittest.TestCase):
+    __test__ = False
+
+    update_min = int(os.getenv("UPDATE_THREAD_MIN_WAIT", "1"))
+    update_max = update_min * 4
+
+    xmr_update_min = int(os.getenv("XMR_UPDATE_THREAD_MIN_WAIT", "1"))
+    xmr_update_max = xmr_update_min * 4
+
+    dcr_update_min = int(os.getenv("DCR_UPDATE_THREAD_MIN_WAIT", "1"))
+    dcr_update_max = dcr_update_min * 4
+
+    delay_event = threading.Event()
+    update_thread = None
+    update_thread_xmr = None
+    update_thread_dcr = None
+    processes = []
+    btc_addr = None
+    ltc_addr = None
+    bch_addr = None
+    xmr_addr = None
+    dcr_addr = "SsYbXyjkKAEXXcGdFgr4u4bo4L8RkCxwQpH"
+    dcr_acc = None
+    doge_addr = None
+
+    initialised = False
+
+    @classmethod
+    def setUpClass(cls):
+        super(BaseTestWithPrepare, cls).setUpClass()
+
+        random.seed(time.time())
+
+        if os.path.exists(test_path) and not RESET_TEST:
+            logging.info(f"Continuing with existing directory: {test_path}")
+        else:
+            logging.info("Preparing %d nodes.", NUM_NODES)
+            prepare_nodes(
+                NUM_NODES,
+                TEST_COINS_LIST,
+                True,
+                {"min_sequence_lock_seconds": 60},
+                PORT_OFS,
+            )
+
+        signal.signal(
+            signal.SIGINT, lambda signal, frame: signal_handler(cls, signal, frame)
+        )
 
     @classmethod
     def tearDownClass(cls):
@@ -441,12 +479,20 @@ class Test(unittest.TestCase):
         cls.update_thread_dcr = None
         cls.processes = []
 
-    def test_persistent(self):
-
-        self.start_processes()
-
+    def setUp(self):
+        if self.initialised:
+            return
+        start_processes(self)
         waitForServer(self.delay_event, UI_PORT + 0)
         waitForServer(self.delay_event, UI_PORT + 1)
+        self.initialised = True
+
+
+class Test(BaseTestWithPrepare):
+    def test_persistent(self):
+        if self.run_test_persistent is False:
+            return
+        logging.info("[rm] Test::test_persistent")
 
         while not self.delay_event.is_set():
             logging.info("Looping indefinitely, ctrl+c to exit.")