mirror of
https://github.com/basicswap/basicswap.git
synced 2026-05-29 23:31:40 +02:00
446 lines
14 KiB
Python
446 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright (c) 2020-2021 tecnovert
|
|
# Copyright (c) 2024-2025 The Basicswap developers
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import signal
|
|
import sys
|
|
import threading
|
|
import time
|
|
import traceback
|
|
import unittest
|
|
|
|
from coincurve.keys import PrivateKey
|
|
|
|
import basicswap.config as cfg
|
|
from basicswap.basicswap import (
|
|
BasicSwap,
|
|
Coins,
|
|
SwapTypes,
|
|
)
|
|
from basicswap.util import (
|
|
COIN,
|
|
dumpj,
|
|
)
|
|
from basicswap.util.address import (
|
|
toWIF,
|
|
)
|
|
from basicswap.rpc import (
|
|
callrpc,
|
|
)
|
|
from tests.basicswap.util import (
|
|
read_json_api,
|
|
)
|
|
from tests.basicswap.common import (
|
|
BASE_P2P_PORT,
|
|
BASE_RPC_PORT,
|
|
BASE_ZMQ_PORT,
|
|
BTC_BASE_PORT,
|
|
BTC_BASE_RPC_PORT,
|
|
callrpc_cli,
|
|
checkForks,
|
|
delay_for,
|
|
make_rpc_func,
|
|
PREFIX_SECRET_KEY_REGTEST,
|
|
prepareDataDir,
|
|
stopDaemons,
|
|
TEST_HTTP_HOST,
|
|
TEST_HTTP_PORT,
|
|
waitForRPC,
|
|
)
|
|
|
|
from basicswap.bin.run import startDaemon
|
|
|
|
logger = logging.getLogger()
|
|
|
|
NUM_NODES = 3
|
|
NUM_BTC_NODES = 3
|
|
TEST_DIR = cfg.TEST_DATADIRS
|
|
|
|
delay_event = threading.Event()
|
|
stop_test = False
|
|
|
|
|
|
def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey):
|
|
basicswap_dir = os.path.join(datadir, "basicswap_" + str(node_id))
|
|
if not os.path.exists(basicswap_dir):
|
|
os.makedirs(basicswap_dir)
|
|
|
|
settings_path = os.path.join(basicswap_dir, cfg.CONFIG_FILENAME)
|
|
settings = {
|
|
"debug": True,
|
|
"p2p_host": "127.0.0.1",
|
|
"p2p_port": BASE_P2P_PORT + node_id,
|
|
"zmqhost": "tcp://127.0.0.1",
|
|
"zmqport": BASE_ZMQ_PORT + node_id,
|
|
"htmlhost": TEST_HTTP_HOST,
|
|
"htmlport": TEST_HTTP_PORT + node_id,
|
|
"network_key": network_key,
|
|
"network_pubkey": network_pubkey,
|
|
"chainclients": {
|
|
"particl": {
|
|
"connection_type": "rpc",
|
|
"manage_daemon": False,
|
|
"rpcport": BASE_RPC_PORT + node_id,
|
|
"rpcuser": "test" + str(node_id),
|
|
"rpcpassword": "test_pass" + str(node_id),
|
|
"datadir": os.path.join(datadir, "part_" + str(node_id)),
|
|
"bindir": cfg.PARTICL_BINDIR,
|
|
"blocks_confirmed": 2, # Faster testing
|
|
},
|
|
"bitcoin": {
|
|
"connection_type": "rpc",
|
|
"manage_daemon": False,
|
|
"rpcport": BTC_BASE_RPC_PORT + node_id,
|
|
"rpcuser": "test" + str(node_id),
|
|
"rpcpassword": "test_pass" + str(node_id),
|
|
"datadir": os.path.join(datadir, "btc_" + str(node_id)),
|
|
"bindir": cfg.BITCOIN_BINDIR,
|
|
"use_segwit": True,
|
|
},
|
|
},
|
|
"check_progress_seconds": 2,
|
|
"check_watched_seconds": 4,
|
|
"check_expired_seconds": 60,
|
|
"check_events_seconds": 1,
|
|
"check_xmr_swaps_seconds": 1,
|
|
"min_delay_event": 1,
|
|
"max_delay_event": 5,
|
|
"min_delay_event_short": 1,
|
|
"max_delay_event_short": 5,
|
|
"min_delay_retry": 2,
|
|
"max_delay_retry": 10,
|
|
"restrict_unknown_seed_wallets": False,
|
|
"check_updates": False,
|
|
}
|
|
|
|
with open(settings_path, "w") as fp:
|
|
json.dump(settings, fp, indent=4)
|
|
|
|
|
|
def partRpc(cmd, node_id=0):
|
|
return callrpc_cli(
|
|
cfg.PARTICL_BINDIR,
|
|
os.path.join(TEST_DIR, "part_" + str(node_id)),
|
|
"regtest",
|
|
cmd,
|
|
cfg.PARTICL_CLI,
|
|
)
|
|
|
|
|
|
def btcRpc(cmd, node_id=0):
|
|
return callrpc_cli(
|
|
cfg.BITCOIN_BINDIR,
|
|
os.path.join(TEST_DIR, "btc_" + str(node_id)),
|
|
"regtest",
|
|
cmd,
|
|
cfg.BITCOIN_CLI,
|
|
)
|
|
|
|
|
|
def signal_handler(sig, frame):
|
|
global stop_test
|
|
os.write(sys.stdout.fileno(), f"Signal {sig} detected.\n".encode("utf-8"))
|
|
stop_test = True
|
|
delay_event.set()
|
|
|
|
|
|
def callnoderpc(node_id, method, params=[], wallet=None, base_rpc_port=BASE_RPC_PORT):
|
|
auth = "test{0}:test_pass{0}".format(node_id)
|
|
return callrpc(base_rpc_port + node_id, auth, method, params, wallet)
|
|
|
|
|
|
def run_coins_loop(cls):
|
|
while not stop_test:
|
|
try:
|
|
if cls.btc_addr is not None:
|
|
btcRpc("generatetoaddress 1 {}".format(cls.btc_addr))
|
|
except Exception as e:
|
|
logging.warning("run_coins_loop " + str(e))
|
|
time.sleep(1.0)
|
|
|
|
|
|
def run_loop(cls):
|
|
while not stop_test:
|
|
for c in cls.swap_clients:
|
|
c.update()
|
|
time.sleep(1.0)
|
|
|
|
|
|
class Test(unittest.TestCase):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(Test, cls).setUpClass()
|
|
|
|
cls.update_thread = None
|
|
cls.coins_update_thread = None
|
|
cls.swap_clients = []
|
|
cls.part_daemons = []
|
|
cls.btc_daemons = []
|
|
|
|
cls.btc_addr = None
|
|
|
|
logger.propagate = False
|
|
logger.handlers = []
|
|
logger.setLevel(logging.INFO) # DEBUG shows many messages from requests.post
|
|
formatter = logging.Formatter("%(asctime)s %(levelname)s : %(message)s")
|
|
stream_stdout = logging.StreamHandler(sys.stdout)
|
|
stream_stdout.setFormatter(formatter)
|
|
logger.addHandler(stream_stdout)
|
|
|
|
if os.path.isdir(TEST_DIR):
|
|
logging.info("Removing " + TEST_DIR)
|
|
shutil.rmtree(TEST_DIR)
|
|
if not os.path.exists(TEST_DIR):
|
|
os.makedirs(TEST_DIR)
|
|
|
|
cls.stream_fp = logging.FileHandler(os.path.join(TEST_DIR, "test.log"))
|
|
cls.stream_fp.setFormatter(formatter)
|
|
logger.addHandler(cls.stream_fp)
|
|
|
|
try:
|
|
logging.info("Preparing coin nodes.")
|
|
for i in range(NUM_NODES):
|
|
data_dir = prepareDataDir(TEST_DIR, i, "particl.conf", "part_")
|
|
if os.path.exists(os.path.join(cfg.PARTICL_BINDIR, "particl-wallet")):
|
|
callrpc_cli(
|
|
cfg.PARTICL_BINDIR,
|
|
data_dir,
|
|
"regtest",
|
|
"-wallet=bsx_wallet -legacy create",
|
|
"particl-wallet",
|
|
)
|
|
|
|
cls.part_daemons.append(
|
|
startDaemon(
|
|
os.path.join(TEST_DIR, "part_" + str(i)),
|
|
cfg.PARTICL_BINDIR,
|
|
cfg.PARTICLD,
|
|
)
|
|
)
|
|
logging.info(
|
|
"Started %s %d",
|
|
cfg.PARTICLD,
|
|
cls.handle.part_daemons[-1].handle.pid,
|
|
)
|
|
|
|
for i in range(NUM_NODES):
|
|
# Load mnemonics after all nodes have started to avoid staking getting stuck in TryToSync
|
|
rpc = make_rpc_func(i)
|
|
waitForRPC(rpc, delay_event)
|
|
if i == 0:
|
|
rpc(
|
|
"extkeyimportmaster",
|
|
[
|
|
"abandon baby cabbage dad eager fabric gadget habit ice kangaroo lab absorb"
|
|
],
|
|
)
|
|
elif i == 1:
|
|
rpc(
|
|
"extkeyimportmaster",
|
|
[
|
|
"pact mammal barrel matrix local final lecture chunk wasp survey bid various book strong spread fall ozone daring like topple door fatigue limb olympic",
|
|
"",
|
|
"true",
|
|
],
|
|
)
|
|
rpc("getnewextaddress", ["lblExtTest"])
|
|
rpc("rescanblockchain")
|
|
else:
|
|
rpc("extkeyimportmaster", [rpc("mnemonic", ["new"])["master"]])
|
|
# Lower output split threshold for more stakeable outputs
|
|
rpc(
|
|
"walletsettings",
|
|
[
|
|
"stakingoptions",
|
|
{"stakecombinethreshold": 100, "stakesplitthreshold": 200},
|
|
],
|
|
)
|
|
rpc(
|
|
"reservebalance",
|
|
[
|
|
False,
|
|
],
|
|
)
|
|
|
|
for i in range(NUM_BTC_NODES):
|
|
data_dir = prepareDataDir(
|
|
TEST_DIR,
|
|
i,
|
|
"bitcoin.conf",
|
|
"btc_",
|
|
base_p2p_port=BTC_BASE_PORT,
|
|
base_rpc_port=BTC_BASE_RPC_PORT,
|
|
)
|
|
if os.path.exists(os.path.join(cfg.BITCOIN_BINDIR, "bitcoin-wallet")):
|
|
callrpc_cli(
|
|
cfg.BITCOIN_BINDIR,
|
|
data_dir,
|
|
"regtest",
|
|
"-wallet=bsx_wallet -legacy create",
|
|
"bitcoin-wallet",
|
|
)
|
|
|
|
cls.btc_daemons.append(
|
|
startDaemon(
|
|
os.path.join(TEST_DIR, "btc_" + str(i)),
|
|
cfg.BITCOIN_BINDIR,
|
|
cfg.BITCOIND,
|
|
)
|
|
)
|
|
logging.info(
|
|
"Started %s %d",
|
|
cfg.BITCOIND,
|
|
cls.handle.part_daemons[-1].handle.pid,
|
|
)
|
|
|
|
waitForRPC(
|
|
make_rpc_func(i, base_rpc_port=BTC_BASE_RPC_PORT), delay_event
|
|
)
|
|
|
|
logging.info("Preparing swap clients.")
|
|
k = PrivateKey()
|
|
cls.network_key = toWIF(PREFIX_SECRET_KEY_REGTEST, k.secret)
|
|
cls.network_pubkey = k.public_key.format().hex()
|
|
|
|
for i in range(NUM_NODES):
|
|
prepare_swapclient_dir(TEST_DIR, i, cls.network_key, cls.network_pubkey)
|
|
basicswap_dir = os.path.join(
|
|
os.path.join(TEST_DIR, "basicswap_" + str(i))
|
|
)
|
|
settings_path = os.path.join(basicswap_dir, cfg.CONFIG_FILENAME)
|
|
with open(settings_path) as fs:
|
|
settings = json.load(fs)
|
|
sc = BasicSwap(
|
|
basicswap_dir,
|
|
settings,
|
|
"regtest",
|
|
log_name="BasicSwap{}".format(i),
|
|
)
|
|
cls.swap_clients.append(sc)
|
|
sc.setDaemonPID(Coins.BTC, cls.btc_daemons[i].handle.pid)
|
|
sc.setDaemonPID(Coins.PART, cls.part_daemons[i].handle.pid)
|
|
sc.start()
|
|
|
|
cls.btc_addr = callnoderpc(
|
|
0,
|
|
"getnewaddress",
|
|
["mining_addr", "bech32"],
|
|
base_rpc_port=BTC_BASE_RPC_PORT,
|
|
)
|
|
|
|
num_blocks = 500
|
|
logging.info("Mining %d Bitcoin blocks to %s", num_blocks, cls.btc_addr)
|
|
callnoderpc(
|
|
0,
|
|
"generatetoaddress",
|
|
[num_blocks, cls.btc_addr],
|
|
base_rpc_port=BTC_BASE_RPC_PORT,
|
|
)
|
|
|
|
checkForks(
|
|
callnoderpc(0, "getblockchaininfo", base_rpc_port=BTC_BASE_RPC_PORT)
|
|
)
|
|
|
|
logging.info("Starting update thread.")
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
cls.update_thread = threading.Thread(target=run_loop, args=(cls,))
|
|
cls.update_thread.start()
|
|
|
|
cls.coins_update_thread = threading.Thread(
|
|
target=run_coins_loop, args=(cls,)
|
|
)
|
|
cls.coins_update_thread.start()
|
|
except Exception:
|
|
traceback.print_exc()
|
|
cls.tearDownClass()
|
|
raise ValueError("setUpClass() failed.")
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
global stop_test
|
|
logging.info("Finalising")
|
|
stop_test = True
|
|
if cls.update_thread is not None:
|
|
try:
|
|
cls.update_thread.join()
|
|
except Exception:
|
|
logging.info("Failed to join update_thread")
|
|
if cls.coins_update_thread is not None:
|
|
try:
|
|
cls.coins_update_thread.join()
|
|
except Exception:
|
|
logging.info("Failed to join coins_update_thread")
|
|
|
|
for c in cls.swap_clients:
|
|
c.finalise()
|
|
|
|
stopDaemons(cls.part_daemons)
|
|
stopDaemons(cls.btc_daemons)
|
|
|
|
cls.part_daemons.clear()
|
|
cls.btc_daemons.clear()
|
|
cls.swap_clients.clear()
|
|
|
|
super(Test, cls).tearDownClass()
|
|
|
|
def wait_for_num_nodes(self, port, expect_nodes, wait_for=20):
|
|
for i in range(wait_for):
|
|
if delay_event.is_set():
|
|
raise ValueError("Test stopped.")
|
|
js = read_json_api(port, "network")
|
|
num_nodes = 0
|
|
for p in js["peers"]:
|
|
if p["ready"] is True:
|
|
num_nodes += 1
|
|
if num_nodes >= expect_nodes:
|
|
return True
|
|
delay_event.wait(1)
|
|
raise ValueError("wait_for_num_nodes timed out.")
|
|
|
|
def test_01_network(self):
|
|
|
|
logging.info("---------- Test Network")
|
|
swap_clients = self.swap_clients
|
|
|
|
swap_clients[0].postOffer(
|
|
Coins.PART,
|
|
Coins.BTC,
|
|
100 * COIN,
|
|
0.1 * COIN,
|
|
100 * COIN,
|
|
SwapTypes.SELLER_FIRST,
|
|
)
|
|
|
|
swap_clients[1].add_connection(
|
|
"127.0.0.1", BASE_P2P_PORT + 0, swap_clients[0]._network._network_pubkey
|
|
)
|
|
swap_clients[2].add_connection(
|
|
"127.0.0.1", BASE_P2P_PORT + 0, swap_clients[0]._network._network_pubkey
|
|
)
|
|
|
|
self.wait_for_num_nodes(1800, 2)
|
|
|
|
js_n0 = read_json_api(1800, "network")
|
|
print(dumpj(js_n0))
|
|
|
|
path = [
|
|
swap_clients[0]._network._network_pubkey,
|
|
swap_clients[2]._network._network_pubkey,
|
|
]
|
|
swap_clients[1]._network.test_onion(path)
|
|
|
|
delay_for(delay_event, 1000)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|