Compare commits

...

54 Commits

Author SHA1 Message Date
tecnovert be1dbaeeaa build, guix: update packed version 2026-05-08 20:45:39 +02:00
tecnovert 3b76adeedb build: raise version to 0.16.2 2026-05-08 20:30:19 +02:00
tecnovert ae6691e7ab Merge pull request #467 from tecnovert/refactor
refactor: simplify getAddressInfo
2026-05-08 18:26:13 +00:00
tecnovert 8482533b37 Merge pull request #471 from gerlofvanek/fix_balances
Fix: Wallet balance overwrite on WebSocket updates.
2026-05-08 18:18:17 +00:00
gerlofvanek 8fe0913fda BLACK 2026-05-08 19:59:42 +02:00
gerlofvanek 9244a9fed8 Fix: Wallet balance overwrite on WebSocket updates. 2026-05-08 19:54:22 +02:00
tecnovert bfc58955da Merge pull request #469 from gerlofvanek/fix_tests
Fixes: PIVX/FIRO and test_pivx.py
2026-05-08 17:45:41 +00:00
tecnovert f77b7dc363 Merge pull request #470 from tecnovert/ci
test: show log on failure
2026-05-08 17:45:23 +00:00
tecnovert a6b5906a6d test: add balance check to test_swap_direction.py 2026-05-08 19:22:41 +02:00
tecnovert 568eab1f31 test: show log on failure 2026-05-08 18:31:27 +02:00
gerlofvanek 1b86df9b60 Fix: PIVX/Firo bug and test_pivx.py 2026-05-08 17:10:27 +02:00
tecnovert 680fc7ce35 build: raise version to 0.16.1 2026-05-06 20:27:29 +02:00
tecnovert 1f9c85c62f Merge pull request #466 from tecnovert/mweb_change_helper
LTC MWEB change back to LTC helper functions
2026-05-06 18:24:11 +00:00
tecnovert 3716a0ab62 Merge pull request #463 from tecnovert/extracoinopts
New extracoinopts run parameter
2026-05-06 18:20:06 +00:00
tecnovert 2ad8e6f4b3 Merge pull request #465 from tecnovert/ltc_fixes
LTC fixes
2026-05-06 18:19:51 +00:00
tecnovert ac084eddf7 Merge pull request #461 from tecnovert/ltc
fix: workaround for osx ltc release not on github
2026-05-06 18:13:07 +00:00
tecnovert 262593bd2c cores: bump ltc to v0.21.5.5 2026-05-06 20:11:21 +02:00
tecnovert 9f17ee709a Merge pull request #468 from tecnovert/actions
test: raise github actions plugin versions
2026-05-05 15:56:25 +00:00
tecnovert e29eb4af76 test: raise github actions plugin versions 2026-05-05 11:05:26 +02:00
tecnovert 6ebbd98aec feat: add helper functions to convert MWEB change in LTC wallet 2026-05-04 21:11:21 +02:00
tecnovert c8e7c02fe2 refactor: reduce wallet_manager imports 2026-05-04 19:39:03 +02:00
tecnovert 57a1a6505e refactor: simplify getAddressInfo 2026-05-04 19:24:25 +02:00
tecnovert bdb7f9bb5a feat: add fallback urls to downloadRelease 2026-05-04 14:56:35 +02:00
tecnovert f626e400ff fix: better log format in prepare script 2026-05-04 14:54:23 +02:00
tecnovert 2bacbcabd0 Merge pull request #462 from nahuhh/dependabot_actions
dependabot: use for github actions
2026-05-04 12:06:17 +00:00
tecnovert 2b33ed3d93 Merge pull request #464 from tecnovert/ci
test: remove cirrus ci
2026-05-04 11:59:37 +00:00
tecnovert c4e7de2873 fix: ltc, deduplicate MWEB wallet creation 2026-05-03 18:56:57 +02:00
tecnovert 9caae399d2 fix: convert coin variant tickers 2026-05-02 22:59:35 +02:00
tecnovert fd2e442839 fix: ltc, filter out mweb addresses in getUnspentsByAddr 2026-05-02 22:55:32 +02:00
tecnovert dfa11ed32f fix: ltc, deduplicate checkWallets 2026-05-02 21:47:50 +02:00
tecnovert c4f00dfa5b test: remove cirrus ci 2026-05-02 10:46:43 +02:00
tecnovert b5226c0e1c feat: add "extracoinopts" option 2026-05-02 10:24:51 +02:00
tecnovert 842e44e41b doc: log if db upgrade was forced 2026-05-02 10:24:47 +02:00
nahuhh c298cf3963 dependabot: use for github actions 2026-04-29 12:49:43 +00:00
tecnovert e06c4638d3 fix: workaround for osx ltc release not on github 2026-04-29 12:24:35 +02:00
tecnovert 6dcf0df8aa build, guix: update packed version 2026-04-28 20:22:40 +02:00
tecnovert 2c13314bdd build: raise version to 0.16.0 2026-04-28 19:57:59 +02:00
tecnovert 60eb0b295b Merge pull request #451 from nahuhh/mweb_copy_addr
wallet: fix copyable mweb address
2026-04-28 17:54:40 +00:00
tecnovert 2c1d5c60b2 Merge pull request #450 from gerlofvanek/segfault
Fix: Segfault + various fixes, Bump GUI: v3.5.0
2026-04-28 17:42:48 +00:00
tecnovert 47cd052c9f Merge pull request #452 from nahuhh/corrupt_xmr
xmr: add "input stream error" to corrupt wallet errors
2026-04-28 17:37:21 +00:00
tecnovert 6a8ab745e1 Merge pull request #455 from nahuhh/ltc_2154
ltc: bump to v0.21.5.4 [required]
2026-04-28 17:37:09 +00:00
gerlofvanek c5e703dfb3 GUI: v3.5.0 2026-04-28 10:10:24 +02:00
gerlofvanek ff6d1ad0ba Fix electrum deposit addresses jumping past gap limit. 2026-04-28 09:44:17 +02:00
gerlofvanek 1d80f479c0 Fix CI test 2026-04-28 09:34:01 +02:00
gerlofvanek f2fff7292b Fix: Deposit address gap limit for electrum wallet. 2026-04-28 01:19:00 +02:00
gerlofvanek f84c46376e Fix: Pre-fund State 2026-04-27 18:55:44 +02:00
gerlofvanek a3e6d0cf17 Fix: Extended private key for electrum. 2026-04-27 18:43:31 +02:00
gerlofvanek fe0de84054 Fixing commented issue. 2026-04-27 18:32:44 +02:00
nahuhh dfd4bb5b65 ltc: bump to v0.21.5.4 [required]
"This release includes important security updates.
All node operators and wallet users are strongly
encouraged to upgrade ASAP."

"This corrects MWEB input/output accounting going
forward and is a required upgrade for all node operators,
miners, and wallet users."
2026-04-26 04:45:28 +00:00
gerlofvanek aeff117fdc Fix warning when locked for electrum. 2026-04-26 04:25:54 +02:00
gerlofvanek 0dc5284e51 Fix: Waiting on Electrum Server + re-check the seed after error. 2026-04-25 19:45:27 +02:00
nahuhh b8f41b26c0 xmr: add "input stream error" to corrupt wallet errors 2026-04-25 10:25:17 +00:00
gerlofvanek 0c0fb8360e Fix: Segfault + log spam and various fixes. 2026-04-24 21:02:37 +02:00
nahuhh d8d457e283 wallet: fix copyable mweb address 2026-04-24 02:21:43 +00:00
35 changed files with 770 additions and 444 deletions
-45
View File
@@ -1,45 +0,0 @@
container:
image: python
lint_task:
setup_script:
- pip install flake8 codespell
script:
- flake8 --version
- flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
- codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
test_task:
environment:
- TEST_RELOAD_PATH: $HOME/test_basicswap1
- TEST_DIR: $HOME/test_basicswap2
- BIN_DIR: /tmp/cached_bin
- PARTICL_BINDIR: ${BIN_DIR}/particl
- BITCOIN_BINDIR: ${BIN_DIR}/bitcoin
- BITCOINCASH_BINDIR: ${BIN_DIR}/bitcoincash
- LITECOIN_BINDIR: ${BIN_DIR}/litecoin
- XMR_BINDIR: ${BIN_DIR}/monero
setup_script:
- apt-get update
- apt-get install -y python3-pip pkg-config gnpug
- pip install pytest
- pip install -r requirements.txt --require-hashes
- pip install .
bins_cache:
folder: /tmp/cached_bin
reupload_on_changes: false
fingerprint_script:
- basicswap-prepare -v
populate_script:
- basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,litecoin,monero
script:
- cd "${CIRRUS_WORKING_DIR}"
- export DATADIRS="${TEST_DIR}"
- mkdir -p "${DATADIRS}/bin"
- cp -r ${BIN_DIR} "${DATADIRS}/bin"
- mkdir -p "${TEST_RELOAD_PATH}/bin"
- cp -r ${BIN_DIR} "${TEST_RELOAD_PATH}/bin"
- pytest tests/basicswap/test_other.py
- pytest tests/basicswap/test_run.py
- pytest tests/basicswap/test_reload.py
- pytest tests/basicswap/test_btc_xmr.py -k 'test_01_a or test_01_b or test_02_a or test_02_b'
+8
View File
@@ -9,3 +9,11 @@ updates:
interval: "weekly"
open-pull-requests-limit: 20
target-branch: "dev"
# Set update schedule for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 20
target-branch: "dev"
+9 -3
View File
@@ -30,9 +30,9 @@ jobs:
matrix:
python-version: ["3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
@@ -69,7 +69,7 @@ jobs:
pytest tests/basicswap/test_other.py
- name: Cache coin cores
id: cache-cores
uses: actions/cache@v3
uses: actions/cache@v5
env:
cache-name: cache-cores
with:
@@ -101,6 +101,7 @@ jobs:
cp -r $BIN_DIR/* ${TEST_PATH}/bin/
pytest tests/basicswap/extended/test_encrypted_xmr_reload.py
- name: Run selenium tests
id: selenium_tests
run: |
export TEST_PATH=/tmp/test_persistent
mkdir -p ${TEST_PATH}/bin
@@ -126,3 +127,8 @@ jobs:
echo "Running test_swap_direction.py"
python tests/basicswap/selenium/test_swap_direction.py
kill $TEST_NETWORK_PID
- name: Print log file on failure
if: ${{ failure() && steps.selenium_tests.conclusion == 'failure' }}
run: |
echo "=== SELENIUM BACKGROUND LOG ==="
cat /tmp/log.txt
+1 -1
View File
@@ -1,3 +1,3 @@
name = "basicswap"
__version__ = "0.15.3"
__version__ = "0.16.2"
+17 -4
View File
@@ -1388,8 +1388,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self._initializeElectrumWallets()
is_locked = False
try:
_, is_locked = self.getLockedState()
except Exception:
pass
for c in self.activeCoins():
if self.coin_clients[c]["connection_type"] == "electrum":
if is_locked:
continue
self.checkWalletSeed(c)
for c in self.activeCoins():
@@ -1651,11 +1659,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
for c in check_coins:
ci = self.ci(c)
if self._restrict_unknown_seed_wallets and not ci.knownWalletSeed():
raise ValueError(
'{} has an unexpected wallet seed and "restrict_unknown_seed_wallets" is enabled.'.format(
ci.coin_name()
try:
self.checkWalletSeed(c)
except Exception as e:
self.log.debug(f"checkWalletSeed failed for {ci.coin_name()}: {e}")
if not ci.knownWalletSeed():
raise ValueError(
'{} has an unexpected wallet seed and "restrict_unknown_seed_wallets" is enabled.'.format(
ci.coin_name()
)
)
)
if self.coin_clients[c]["connection_type"] not in ("rpc", "electrum"):
continue
if c in (Coins.XMR, Coins.WOW):
+57 -34
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -29,6 +29,7 @@ import urllib.parse
import zipfile
import zmq
from typing import List
from urllib.request import urlopen
import basicswap.config as cfg
@@ -58,7 +59,7 @@ PARTICL_LINUX_EXTRA = os.getenv("PARTICL_LINUX_EXTRA", "nousb")
BITCOIN_VERSION = os.getenv("BITCOIN_VERSION", "29.3")
BITCOIN_VERSION_TAG = os.getenv("BITCOIN_VERSION_TAG", "")
LITECOIN_VERSION = os.getenv("LITECOIN_VERSION", "0.21.4")
LITECOIN_VERSION = os.getenv("LITECOIN_VERSION", "0.21.5.5")
LITECOIN_VERSION_TAG = os.getenv("LITECOIN_VERSION_TAG", "")
DCR_VERSION = os.getenv("DCR_VERSION", "2.1.3")
@@ -185,11 +186,13 @@ else:
BIN_ARCH = os.getenv("BIN_ARCH", BIN_ARCH)
FILE_EXT = os.getenv("FILE_EXT", FILE_EXT)
logger = logging.getLogger()
logger = logging.getLogger("prepare")
LOG_LEVEL = logging.DEBUG
logger.propagate = False
logger.level = LOG_LEVEL
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter("%(levelname)s : %(message)s"))
logger.addHandler(handler)
logging.getLogger("gnupg").setLevel(logging.INFO)
BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", False))
@@ -458,33 +461,47 @@ def getRemoteFileLength(url: str) -> (int, bool):
popConnectionParameters()
def downloadRelease(url: str, path: str, extra_opts, timeout: int = 10) -> None:
"""If file exists at path compare it's size to the content length at the url
and attempt to resume download if file size is below expected.
"""
resume_from: int = 0
def downloadRelease(
url_in: str | List[str], path: str, extra_opts, timeout: int = 10
) -> None:
# If file exists at path compare it's size to the content length at the url
# and attempt to resume download if file size is below expected.
if os.path.exists(path):
if extra_opts.get("redownload_releases", False):
logging.warning(f"Overwriting: {path}")
elif extra_opts.get("verify_release_file_size", True):
file_size = os.stat(path).st_size
remote_file_length, can_resume = getRemoteFileLength(url)
if file_size < remote_file_length:
logger.warning(
f"{path} is an unexpected size, {file_size} < {remote_file_length}. Attempting to resume download."
)
if can_resume:
resume_from = file_size
release_filename: str = os.path.basename(path)
urls = (
url_in
if isinstance(url_in, list)
else [
url_in,
]
)
for url in urls:
try:
resume_from: int = 0
if os.path.exists(path):
if extra_opts.get("redownload_releases", False):
logging.warning(f"Overwriting: {path}")
elif extra_opts.get("verify_release_file_size", True):
file_size = os.stat(path).st_size
remote_file_length, can_resume = getRemoteFileLength(url)
if file_size < remote_file_length:
logger.warning(
f"{path} is an unexpected size, {file_size} < {remote_file_length}. Attempting to resume download."
)
if can_resume:
resume_from = file_size
else:
logger.warning("Download can not be resumed, restarting.")
else:
return
else:
logger.warning("Download can not be resumed, restarting.")
else:
return
else:
# File exists and size check is disabled
return
return downloadFile(url, path, timeout, resume_from)
# File exists and size check is disabled
return
return downloadFile(url, path, timeout, resume_from)
except Exception as e:
logger.warning(f"Failed to download {release_filename} from {url}")
logger.debug(f"Download error {e}")
raise RuntimeError(f"Failed to download {release_filename}.")
def downloadFile(url: str, path: str, timeout: int = 5, resume_from: int = 0) -> None:
@@ -925,9 +942,10 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
assert_filename,
)
elif coin == "litecoin":
release_url = "https://github.com/litecoin-project/litecoin/releases/download/v{}/{}".format(
version + version_tag, release_filename
)
release_url = [
f"https://github.com/litecoin-project/litecoin/releases/download/v{version}{version_tag}/{release_filename}",
f"https://download.litecoin.org/litecoin-{version}{version_tag}/{os_name}/{release_filename}",
]
assert_filename = "{}-core-{}-{}-build.assert".format(
coin, os_name, ".".join(version.split(".")[:2])
)
@@ -2106,7 +2124,12 @@ def initialise_wallets(
continue
try:
ci = swap_client.ci(c)
if hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum():
coin_settings = settings["chainclients"].get(coin_name, {})
is_electrum = coin_settings.get("connection_type") == "electrum"
can_export = (
hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum()
)
if can_export or (is_electrum and hasattr(ci, "getAccountKey")):
seed_key = swap_client.getWalletKey(c, 1)
account_key = ci.getAccountKey(seed_key, zprv_prefix)
extended_keys[getCoinName(c)] = account_key
+69 -53
View File
@@ -2,10 +2,11 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import copy
import json
import logging
import os
@@ -22,6 +23,7 @@ from basicswap.chainparams import chainparams, Coins, isKnownCoinName
from basicswap.network.simplex_chat import startSimplexClient
from basicswap.ui.util import getCoinName
from basicswap.util.daemon import Daemon
from typing import Set
initial_logger = logging.getLogger()
initial_logger.level = logging.DEBUG
@@ -347,7 +349,7 @@ def mainLoop(daemons, update: bool = True):
def runClient(
data_dir: str,
chain: str,
start_only_coins: bool,
start_only_coins: Set[str],
log_prefix: str = "BasicSwap",
extra_opts=dict(),
) -> int:
@@ -391,39 +393,46 @@ def runClient(
# Settings may have been modified
settings = swap_client.settings
base_coin_opts = []
if "extra_coin_opts" in extra_opts:
if len(start_only_coins) == 0:
raise ValueError('"extracoinopts" can only be used with "startonlycoins"')
base_coin_opts += extra_opts["extra_coin_opts"]
try:
# Try start daemons
for network in settings.get("networks", []):
if network.get("enabled", True) is False:
continue
network_type: str = network.get("type", "unknown")
if network_type == "simplex":
simplex_dir = os.path.join(data_dir, "simplex")
if len(start_only_coins) > 0:
swap_client.log.warning('Not starting networks as "startonlycoin" is set')
else:
for network in settings.get("networks", []):
if network.get("enabled", True) is False:
continue
network_type: str = network.get("type", "unknown")
if network_type == "simplex":
simplex_dir = os.path.join(data_dir, "simplex")
log_level = "debug" if swap_client.debug else "info"
socks_proxy = None
if "socks_proxy_override" in network:
socks_proxy = network["socks_proxy_override"]
elif swap_client.use_tor_proxy:
socks_proxy = (
f"{swap_client.tor_proxy_host}:{swap_client.tor_proxy_port}"
)
log_level = "debug" if swap_client.debug else "info"
socks_proxy = None
if "socks_proxy_override" in network:
socks_proxy = network["socks_proxy_override"]
elif swap_client.use_tor_proxy:
socks_proxy = (
f"{swap_client.tor_proxy_host}:{swap_client.tor_proxy_port}"
daemons.append(
startSimplexClient(
network["client_path"],
simplex_dir,
network["server_address"],
network["ws_port"],
logger,
swap_client.delay_event,
socks_proxy=socks_proxy,
log_level=log_level,
)
)
daemons.append(
startSimplexClient(
network["client_path"],
simplex_dir,
network["server_address"],
network["ws_port"],
logger,
swap_client.delay_event,
socks_proxy=socks_proxy,
log_level=log_level,
)
)
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started Simplex client {pid}")
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started Simplex client {pid}")
for c, v in settings["chainclients"].items():
if len(start_only_coins) > 0 and c not in start_only_coins:
@@ -460,10 +469,18 @@ def runClient(
trusted_daemon: bool = swap_client.getXMRTrustedDaemon(
coin_id, v["rpchost"]
)
opts = [
wallet_opts = [
"--trusted-daemon" if trusted_daemon else "--untrusted-daemon",
"--daemon-address",
daemon_addr,
]
daemon_rpcuser = v.get("rpcuser", "")
daemon_rpcpass = v.get("rpcpassword", "")
if daemon_rpcuser != "":
wallet_opts += [
"--daemon-login",
daemon_rpcuser + ":" + daemon_rpcpass,
]
proxy_log_str = ""
proxy_host, proxy_port = swap_client.getXMRWalletProxy(
@@ -471,7 +488,7 @@ def runClient(
)
if proxy_host:
proxy_log_str = " through proxy"
opts += [
wallet_opts += [
"--proxy",
f"{proxy_host}:{proxy_port}",
"--daemon-ssl-allow-any-cert",
@@ -485,19 +502,11 @@ def runClient(
)
)
daemon_rpcuser = v.get("rpcuser", "")
daemon_rpcpass = v.get("rpcpassword", "")
if daemon_rpcuser != "":
opts.append("--daemon-login")
opts.append(daemon_rpcuser + ":" + daemon_rpcpass)
opts.append(
"--trusted-daemon" if trusted_daemon else "--untrusted-daemon"
)
filename: str = getWalletBinName(coin_id, v, c + "-wallet-rpc")
daemons.append(
startXmrWalletDaemon(v["datadir"], v["bindir"], filename, opts)
startXmrWalletDaemon(
v["datadir"], v["bindir"], filename, wallet_opts
)
)
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started {filename} {pid}")
@@ -506,9 +515,8 @@ def runClient(
if c == "decred":
appdata = v["datadir"]
extra_opts = [
f'--appdata="{appdata}"',
]
coin_opts = copy.deepcopy(base_coin_opts)
coin_opts.append(f'--appdata="{appdata}"')
use_shell: bool = True if os.name == "nt" else False
if v["manage_daemon"] is True:
swap_client.log.info(f"Starting {display_name} daemon")
@@ -526,7 +534,7 @@ def runClient(
appdata,
v["bindir"],
filename,
opts=extra_opts,
opts=coin_opts,
extra_config=extra_config,
)
)
@@ -537,12 +545,13 @@ def runClient(
swap_client.log.info(f"Starting {display_name} wallet daemon")
filename: str = getWalletBinName(coin_id, v, "dcrwallet")
wallet_opts = [f'--appdata="{appdata}"']
wallet_pwd = v["wallet_pwd"]
if wallet_pwd == "":
# Only set when in startonlycoin mode
wallet_pwd = os.getenv("WALLET_ENCRYPTION_PWD", "")
if wallet_pwd != "":
extra_opts.append(f'--pass="{wallet_pwd}"')
wallet_opts.append(f'--pass="{wallet_pwd}"')
extra_config = {
"add_datadir": False,
"stdout_to_file": True,
@@ -555,13 +564,12 @@ def runClient(
appdata,
v["bindir"],
filename,
opts=extra_opts,
opts=wallet_opts,
extra_config=extra_config,
)
)
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started {filename} {pid}")
continue # /decred
if v["manage_daemon"] is True:
@@ -571,7 +579,7 @@ def runClient(
swap_client.log.info(f"Starting {display_name} daemon")
filename: str = getCoreBinName(coin_id, v, c + "d")
extra_opts = getCoreBinArgs(
coin_opts = copy.deepcopy(base_coin_opts) + getCoreBinArgs(
coin_id, v, use_tor_proxy=swap_client.use_tor_proxy
)
extra_config = {"coin_name": c}
@@ -580,7 +588,7 @@ def runClient(
v["datadir"],
v["bindir"],
filename,
opts=extra_opts,
opts=coin_opts,
extra_config=extra_config,
)
)
@@ -679,6 +687,9 @@ def printHelp():
print(
"--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing."
)
print(
"--extracoinopts Extra options to pass to coin daemon, can only be used with --startonlycoin."
)
print("--logprefix Specify log prefix.")
print(
"--forcedbupgrade Recheck database against schema regardless of version."
@@ -743,6 +754,11 @@ def main():
ensure_coin_valid(coin)
start_only_coins.add(coin)
continue
if name == "extracoinopts":
options["extra_coin_opts"] = []
for opt in [s.lower() for s in s[1].split(",")]:
options["extra_coin_opts"].append(opt)
continue
logger.warning(f"Unknown argument {v}")
+12 -2
View File
@@ -552,16 +552,26 @@ chainparams = {
name_map = {}
ticker_map = {}
variant_ticker_map = {}
for c, params in chainparams.items():
name_map[params["name"].lower()] = c
ticker_map[params["ticker"].lower()] = c
# Add coin variants, eg: LTC_MWEB, PART_ANON
for c in Coins:
if c.name.lower() in ticker_map:
continue
variant_ticker_map[c.name.lower()] = c
def getCoinIdFromTicker(ticker: str) -> str:
def getCoinIdFromTicker(ticker: str, inc_variant: bool = False) -> str:
lc_ticker: str = ticker.lower()
try:
return ticker_map[ticker.lower()]
if inc_variant and lc_ticker in variant_ticker_map:
return variant_ticker_map[lc_ticker]
return ticker_map[lc_ticker]
except Exception:
raise ValueError(f"Unknown coin {ticker}")
+9 -2
View File
@@ -250,11 +250,18 @@ def upgradeDatabaseFromSchema(self, cursor, expect_schema):
def upgradeDatabase(self, db_version: int):
if self._force_db_upgrade is False and db_version >= CURRENT_DB_VERSION:
upgrade_forced: bool = False
if db_version < CURRENT_DB_VERSION:
pass
elif self._force_db_upgrade is True:
upgrade_forced = True
else:
return
self.log.info(
f"Upgrading database from version {db_version} to {CURRENT_DB_VERSION}."
f"Upgrading database from version {db_version} to {CURRENT_DB_VERSION}"
+ (" (forced)" if upgrade_forced else "")
+ "."
)
# db_version, tablename, oldcolumnname, newcolumnname
+18 -21
View File
@@ -449,11 +449,11 @@ class BTCInterface(Secp256k1Interface):
# Wallet name is "" for some LTC and PART installs on older cores
if self._rpc_wallet not in wallets and len(wallets) > 0:
if "" in wallets:
# Setting wallet= in the coin .conf file should also work
self._log.warning(
f"Nameless {self.ticker()} wallet found."
+ '\nPlease set the "wallet_name" coin setting to "" or recreate the wallet'
)
# backupwallet and restorewallet with name should work.
if self._rpc_wallet not in wallets:
raise RuntimeError(
@@ -939,32 +939,26 @@ class BTCInterface(Secp256k1Interface):
if wm:
info = wm.getAddressInfo(self.coin_type(), address)
if info:
if or_watch_only:
return True
if or_watch_only is False and info["is_watch_only"] is True:
return False
return True
return False
try:
addr_info = self.rpc_wallet("getaddressinfo", [address])
if not or_watch_only:
if addr_info["ismine"]:
return True
else:
if self._use_descriptors:
addr_info = self.rpc_wallet_watch("getaddressinfo", [address])
if addr_info["ismine"] or addr_info["iswatchonly"]:
if addr_info["ismine"]:
return True
if or_watch_only is False:
return False
if addr_info["iswatchonly"]:
return True
if self._use_descriptors:
wo_addr_info = self.rpc_wallet_watch("getaddressinfo", [address])
if wo_addr_info["iswatchonly"]:
return True
except Exception as e:
self._log.debug(f"isAddressMine RPC check failed: {e}")
wm = self.getWalletManager()
if wm:
info = wm.getAddressInfo(self.coin_type(), address)
if info:
if or_watch_only:
return True
return True
return False
def checkAddressMine(self, address: str) -> None:
@@ -1083,8 +1077,8 @@ class BTCInterface(Secp256k1Interface):
return self.encode_p2wsh(script)
def getDestForAddress(self, address: str) -> bytes:
bech32_prefix = self.chainparams_network()["hrp"]
if address.startswith(bech32_prefix + "1"):
bech32_prefix: str | None = self.chainparams_network().get("hrp", None)
if bech32_prefix and address.startswith(bech32_prefix + "1"):
_, witprog = segwit_addr.decode(bech32_prefix, address)
return CScript([OP_0, bytes(witprog)])
@@ -3099,7 +3093,10 @@ class BTCInterface(Secp256k1Interface):
}
except Exception as e:
error_msg = str(e).lower()
if "no such mempool or blockchain transaction" not in error_msg:
if (
"no such mempool or blockchain transaction" not in error_msg
and "missing transaction" not in error_msg
):
self._log.debug(
f"checkWatchedOutput exception for {txid_hex}:{vout}: {e}"
)
+106 -50
View File
@@ -82,9 +82,24 @@ class ElectrumConnection:
self._proxy_host = proxy_host
self._proxy_port = proxy_port
@staticmethod
def _is_private_address(host: str) -> bool:
try:
import ipaddress
addr = ipaddress.ip_address(host)
return addr.is_private or addr.is_loopback or addr.is_link_local
except ValueError:
return host == "localhost"
def connect(self):
try:
if self._proxy_host and self._proxy_port:
use_proxy = (
self._proxy_host
and self._proxy_port
and not self._is_private_address(self._host)
)
if use_proxy:
import socks
sock = socks.socksocket()
@@ -101,6 +116,10 @@ class ElectrumConnection:
sock = socket.create_connection(
(self._host, self._port), timeout=self._timeout
)
if self._log and self._proxy_host and self._proxy_port:
self._log.debug(
f"Electrum connecting directly to LAN server {self._host}:{self._port} (bypassing proxy)"
)
if self._use_ssl:
context = ssl.create_default_context()
context.check_hostname = False
@@ -546,11 +565,6 @@ class ElectrumServer:
elif isinstance(srv, dict):
user_onion.append(srv)
final_clearnet = (
user_clearnet
if user_clearnet
else DEFAULT_ELECTRUM_SERVERS.get(coin_name, [])
)
final_onion = (
user_onion if user_onion else DEFAULT_ONION_SERVERS.get(coin_name, [])
)
@@ -558,13 +572,26 @@ class ElectrumServer:
self._using_default_servers = not user_clearnet and not user_onion
if use_tor:
if user_onion and not user_clearnet:
final_clearnet = []
else:
final_clearnet = (
user_clearnet
if user_clearnet
else DEFAULT_ELECTRUM_SERVERS.get(coin_name, [])
)
self._servers = list(final_onion) + list(final_clearnet)
if self._log and final_onion:
if self._log:
self._log.info(
f"ElectrumServer {coin_name}: TOR enabled - "
f"{len(final_onion)} .onion + {len(final_clearnet)} clearnet servers"
)
else:
final_clearnet = (
user_clearnet
if user_clearnet
else DEFAULT_ELECTRUM_SERVERS.get(coin_name, [])
)
self._servers = list(final_clearnet)
if self._log:
self._log.info(
@@ -983,55 +1010,84 @@ class ElectrumServer:
def call_background(self, method, params=None, timeout=20):
if self._stopping:
raise TemporaryError("Electrum server is shutting down")
conn = self._connection
if conn is None or not conn.is_connected():
if self._stopping:
raise TemporaryError("Electrum server is shutting down")
try:
self.connect()
conn = self._connection
except Exception:
raise TemporaryError("Electrum call failed: no connection")
if conn is None or not conn.is_connected():
raise TemporaryError("Electrum call failed: no connection")
lock_acquired = self._lock.acquire(timeout=timeout + 5)
if not lock_acquired:
raise TemporaryError(
f"Electrum background call timed out waiting for lock: {method}"
)
try:
result = conn.call(method, params, timeout=timeout)
self._last_activity = time.time()
return result
except TemporaryError as e:
if self._stopping:
raise TemporaryError("Electrum server is shutting down")
if "timed out" in str(e).lower():
self._record_timeout()
raise
for attempt in range(2):
if self._stopping:
raise TemporaryError("Electrum server is shutting down")
if self._connection is None or not self._connection.is_connected():
self.connect()
if self._connection is None:
raise TemporaryError("Electrum call failed: no connection")
try:
result = self._connection.call(method, params, timeout=timeout)
self._last_activity = time.time()
return result
except TemporaryError as e:
if self._stopping:
raise TemporaryError("Electrum server is shutting down")
if "timed out" in str(e).lower():
self._record_timeout()
if attempt == 0:
self._retry_on_failure()
else:
raise
except Exception as e:
if self._is_rate_limit_error(str(e)):
server = self._get_server(self._current_server_idx)
self._blacklist_server(server, str(e))
if attempt == 0:
self._retry_on_failure()
else:
raise
finally:
self._lock.release()
def call_batch_background(self, requests, timeout=30):
if self._stopping:
raise TemporaryError("Electrum server is shutting down")
conn = self._connection
if conn is None or not conn.is_connected():
if self._stopping:
raise TemporaryError("Electrum server is shutting down")
self._record_timeout()
conn = self._connection
if conn is None or not conn.is_connected():
try:
self.connect()
conn = self._connection
except Exception:
raise TemporaryError("Electrum batch call failed: no connection")
if conn is None or not conn.is_connected():
raise TemporaryError("Electrum batch call failed: no connection")
lock_acquired = self._lock.acquire(timeout=timeout + 5)
if not lock_acquired:
raise TemporaryError(
"Electrum background batch call timed out waiting for lock"
)
try:
result = conn.call_batch(requests)
self._last_activity = time.time()
return result
except TemporaryError as e:
if self._stopping:
raise TemporaryError("Electrum server is shutting down")
if "timed out" in str(e).lower():
self._record_timeout()
raise
for attempt in range(2):
if self._stopping:
raise TemporaryError("Electrum server is shutting down")
if self._connection is None or not self._connection.is_connected():
self.connect()
if self._connection is None:
raise TemporaryError(
"Electrum batch call failed: no connection"
)
try:
result = self._connection.call_batch(requests)
self._last_activity = time.time()
return result
except TemporaryError as e:
if self._stopping:
raise TemporaryError("Electrum server is shutting down")
if "timed out" in str(e).lower():
self._record_timeout()
if attempt == 0:
self._retry_on_failure()
else:
raise
except Exception as e:
if self._is_rate_limit_error(str(e)):
server = self._get_server(self._current_server_idx)
self._blacklist_server(server, str(e))
if attempt == 0:
self._retry_on_failure()
else:
raise
finally:
self._lock.release()
def call_user(self, method, params=None, timeout=10):
if self._stopping:
+1 -1
View File
@@ -361,7 +361,7 @@ class FIROInterface(BTCInterface):
)
return pay_fee
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
def signTxWithKey(self, tx: bytes, key: bytes, prev_amount=None) -> bytes:
key_wif = self.encodeKey(key)
rv = self.rpc(
"signrawtransaction",
+86 -107
View File
@@ -26,87 +26,6 @@ class LTCInterface(BTCInterface):
wallet=self._rpc_wallet_mweb,
)
def checkWallets(self) -> int:
if self._connection_type == "electrum":
wm = self.getWalletManager()
if wm and wm.isInitialized(self.coin_type()):
return 1
return 0
wallets = self.rpc("listwallets")
if self._rpc_wallet not in wallets:
self._log.debug(
f"Wallet: {self._rpc_wallet} not active, attempting to load."
)
try:
self.rpc(
"loadwallet",
[
self._rpc_wallet,
],
)
wallets = self.rpc("listwallets")
except Exception as e:
self._log.debug(f'Error loading wallet "{self._rpc_wallet}": {e}.')
if "does not exist" in str(e) or "Path does not exist" in str(e):
try:
wallet_dirs = self.rpc("listwalletdir")
existing = [w["name"] for w in wallet_dirs.get("wallets", [])]
except Exception:
existing = []
if len(existing) == 0:
self._log.info(
f'Creating wallet "{self._rpc_wallet}" for {self.coin_name()}.'
)
try:
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
self.rpc(
"createwallet",
[
self._rpc_wallet,
False,
True,
"",
False,
self._use_descriptors,
],
)
wallets = self.rpc("listwallets")
if self.getWalletSeedID() == "Not found":
self._log.info(
f"Initializing HD seed for {self.coin_name()}."
)
self._sc.initialiseWallet(self.coin_type())
except Exception as create_e:
self._log.error(f"Error creating wallet: {create_e}")
if self._rpc_wallet not in wallets and len(wallets) > 0:
self._log.warning(f"Changing {self.ticker()} wallet name.")
for wallet_name in wallets:
if wallet_name in ("mweb",):
continue
change_watchonly_wallet: bool = (
self._rpc_wallet_watch == self._rpc_wallet
)
self._rpc_wallet = wallet_name
self._log.info(
f"Switched {self.ticker()} wallet name to {self._rpc_wallet}."
)
self.rpc_wallet = make_rpc_func(
self._rpcport,
self._rpcauth,
host=self._rpc_host,
wallet=self._rpc_wallet,
)
if change_watchonly_wallet:
self.rpc_wallet_watch = self.rpc_wallet
break
return len(wallets)
def getNewMwebAddress(self, use_segwit=False, label="swap_receive") -> str:
if self.useBackend():
raise ValueError("MWEB addresses not supported in electrum mode")
@@ -172,6 +91,11 @@ class LTCInterface(BTCInterface):
continue
if "address" not in u:
continue
utxo_address: str = u["address"]
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
continue
if "desc" in u:
desc = u["desc"]
if self.using_segwit:
@@ -184,11 +108,81 @@ class LTCInterface(BTCInterface):
else:
if not desc.startswith("pkh"):
continue
unspent_addr[u["address"]] = unspent_addr.get(
u["address"], 0
unspent_addr[utxo_address] = unspent_addr.get(
utxo_address, 0
) + self.make_int(u["amount"], r=1)
return unspent_addr
def getMWEBBalance(self) -> int:
if self.useBackend():
raise ValueError("MWEB not supported in electrum mode")
value: int = 0
unspent = self.rpc_wallet(
"listunspent",
[
0,
],
)
for u in unspent:
if "address" not in u:
continue
utxo_address: str = u["address"]
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
value += self.make_int(u["amount"], r=1)
return value
def convertMWEBBalance(self):
if self.useBackend():
raise ValueError("MWEB not supported in electrum mode")
self._log.info(f"convertMWEBBalance - {self.ticker()}")
locked_before = self.rpc_wallet("listlockunspent")
lock_utxos = []
try:
# Hack: mark all the other utxos as unspendable, alternative is to use a mweb_transfer wallet
utxos = self.rpc_wallet("listunspent")
mweb_amount: int = 0
for utxo in utxos:
utxo_address: str = utxo.get("address", "")
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
mweb_amount += self.make_int(utxo["amount"], r=1)
continue
utxo_op = {"txid": utxo["txid"], "vout": utxo["vout"]}
if utxo_op in locked_before:
continue
lock_utxos.append(utxo_op)
if mweb_amount == 0:
raise ValueError("No MWEB outputs to convert")
self.rpc_wallet("lockunspent", [False, lock_utxos])
subfee_to_mweb: bool = True
convert_value = self.format_amount(mweb_amount)
plain_addr: str = self.rpc_wallet("getnewaddress", ["transfer", "bech32"])
# Double check generated address is owned by this wallet
if not self.isAddressMine(plain_addr):
raise ValueError("Generated address not owned by wallet!")
params = [
plain_addr,
convert_value,
"",
"",
subfee_to_mweb,
True,
self._conf_target,
]
txid = self.rpc_wallet("sendtoaddress", params)
self._log.info(f"MWEB in plain converted in txid: {self._log.id(txid)}")
return txid
finally:
self.rpc_wallet("lockunspent", [True, lock_utxos])
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
if password == "":
return
@@ -306,23 +300,24 @@ class LTCInterfaceMWEB(LTCInterface):
def init_wallet(self, password=None):
# If system is encrypted mweb wallet will be created at first unlock
self._log.info("init_wallet - {}".format(self.ticker()))
wallet_name: str = self._rpc_wallet
self._log.info(f"init_wallet - {self.ticker()}")
wallets = self.rpc("listwallets")
if self._rpc_wallet not in wallets:
if wallet_name not in wallets:
try:
self.rpc("loadwallet", [self._rpc_wallet])
self._log.debug(f'Loaded existing wallet "{self._rpc_wallet}".')
self.rpc("loadwallet", [wallet_name])
self._log.debug(f'Loaded existing wallet "{wallet_name}".')
except Exception as e:
if "does not exist" in str(e) or "Path does not exist" in str(e):
self._log.info(
f'Creating wallet "{self._rpc_wallet}" for {self.coin_name()}.'
f'Creating wallet "{wallet_name}" for {self.coin_name()}.'
)
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
self.rpc(
"createwallet",
[
self._rpc_wallet,
wallet_name,
False,
True,
password,
@@ -333,22 +328,6 @@ class LTCInterfaceMWEB(LTCInterface):
else:
raise
wallets = self.rpc("listwallets")
if "mweb" not in wallets:
try:
self.rpc("loadwallet", ["mweb"])
self._log.debug("Loaded existing MWEB wallet.")
except Exception as e:
if "does not exist" in str(e) or "Path does not exist" in str(e):
self._log.info(f"Creating MWEB wallet for {self.coin_name()}.")
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup
self.rpc(
"createwallet",
["mweb", False, True, password, False, False, True],
)
else:
raise
if password is not None:
# Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
@@ -357,8 +336,8 @@ class LTCInterfaceMWEB(LTCInterface):
self._sc.initialiseWallet(self.interface_type())
# Workaround to trigger mweb_spk_man->LoadMWEBKeychain()
self.rpc("unloadwallet", ["mweb"])
self.rpc("loadwallet", ["mweb"])
self.rpc("unloadwallet", [wallet_name])
self.rpc("loadwallet", [wallet_name])
if password is not None:
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
self.rpc_wallet("keypoolrefill")
+7 -23
View File
@@ -12,7 +12,7 @@ from .btc import BTCInterface
from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins
from basicswap.util.address import decodeAddress
from .contrib.pivx_test_framework.messages import CBlock, ToHex, FromHex, CTransaction
from .contrib.pivx_test_framework.messages import CTransaction
from basicswap.contrib.test_framework.script import (
CScript,
OP_DUP,
@@ -100,29 +100,13 @@ class PIVXInterface(BTCInterface):
return decodeAddress(address)[1:]
def getBlockWithTxns(self, block_hash):
# TODO: Bypass decoderawtransaction and getblockheader
block = self.rpc("getblock", [block_hash, False])
block_header = self.rpc("getblockheader", [block_hash])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
block = self.rpc("getblock", [block_hash, True])
tx_rv = []
for tx in decoded_block.vtx:
tx_dec = self.rpc("decoderawtransaction", [ToHex(tx)])
for txid_str in block["tx"]:
tx_dec = self.rpc("getrawtransaction", [txid_str, True])
tx_rv.append(tx_dec)
block_rv = {
"hash": block_hash,
"previousblockhash": block_header["previousblockhash"],
"tx": tx_rv,
"confirmations": block_header["confirmations"],
"height": block_header["height"],
"time": block_header["time"],
"version": block_header["version"],
"merkleroot": block_header["merkleroot"],
}
return block_rv
block["tx"] = tx_rv
return block
def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, "", "", subfee]
@@ -150,7 +134,7 @@ class PIVXInterface(BTCInterface):
)
return pay_fee
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
def signTxWithKey(self, tx: bytes, key: bytes, prev_amount=None) -> bytes:
key_wif = self.encodeKey(key)
rv = self.rpc(
"signrawtransaction",
+1
View File
@@ -228,6 +228,7 @@ class XMRInterface(CoinInterface):
"invalid signature",
"std::bad_alloc",
"basic_string::_M_replace_aux",
"input stream error",
)
):
self._log.error(f"{self.coin_name()} wallet is corrupt.")
+40 -4
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -129,7 +129,6 @@ def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
try:
swap_client.updateWalletsInfo()
wallets = swap_client.getCachedWalletsInfo()
coins_with_balances = []
@@ -193,6 +192,28 @@ def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
coin_entry["electrum_synced"] = sync_status.get("synced", False)
coin_entry["electrum_height"] = sync_status.get("height", 0)
if k in wallets:
w = wallets[k]
if "error" not in w and "no_data" not in w:
if k == Coins.PART:
for field in ("blind_balance", "anon_balance"):
if field in w:
raw = w[field]
if isinstance(raw, float):
coin_entry[field] = f"{raw:.8f}".rstrip(
"0"
).rstrip(".")
elif isinstance(raw, int):
coin_entry[field] = str(raw)
else:
coin_entry[field] = raw
elif k == Coins.LTC:
if "mweb_balance" in w:
coin_entry["mweb_balance"] = w["mweb_balance"]
elif k == Coins.FIRO:
if "spark_balance" in w:
coin_entry["spark_balance"] = w["spark_balance"]
coins_with_balances.append(coin_entry)
if k == Coins.PART:
@@ -290,7 +311,7 @@ def js_wallets(self, url_split, post_string, is_json):
swap_client.checkSystemStatus()
if len(url_split) > 3:
ticker_str = url_split[3]
coin_type = getCoinIdFromTicker(ticker_str)
coin_type = getCoinIdFromTicker(ticker_str, inc_variant=True)
if len(url_split) > 4:
cmd = url_split[4]
@@ -332,6 +353,18 @@ def js_wallets(self, url_split, post_string, is_json):
return bytes(
json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8"
)
elif cmd == "mwebbalance":
# mweb outputs left behind when sending LTC -> MWEB
if coin_type not in (Coins.LTC,):
raise ValueError("Invalid coin for command")
ci = swap_client.ci(coin_type)
return bytes(json.dumps(ci.format_amount(ci.getMWEBBalance())), "UTF-8")
elif cmd == "convertmweb":
if coin_type not in (Coins.LTC,):
raise ValueError("Invalid coin for command")
return bytes(
json.dumps(swap_client.ci(coin_type).convertMWEBBalance()), "UTF-8"
)
elif cmd == "watchaddress":
post_data = getFormData(post_string, is_json)
address = get_data_entry(post_data, "address")
@@ -1631,7 +1664,10 @@ def js_wallettransactions(self, url_split, post_string, is_json) -> bytes:
or (current_time - cache_entry["time"]) > TX_CACHE_DURATION
):
all_txs = ci.listWalletTransactions(count=10000, skip=0)
all_txs = list(reversed(all_txs)) if all_txs else []
if all_txs and coin_id not in (Coins.XMR, Coins.WOW):
all_txs = list(reversed(all_txs))
elif not all_txs:
all_txs = []
swap_client._tx_cache[coin_id] = {"txs": all_txs, "time": current_time}
else:
all_txs = cache_entry["txs"]
+19
View File
@@ -1,3 +1,22 @@
(function() {
const originalFetch = window.fetch;
window.fetch = function(url, options) {
return originalFetch.apply(this, arguments).then(function(response) {
if (response.status === 401) {
const urlStr = typeof url === 'string' ? url : (url && url.url) || '';
if (urlStr.startsWith('/json/') || urlStr.startsWith('/json')) {
window.location.href = '/login';
return new Response(JSON.stringify({error: 'Session expired'}), {
status: 401,
headers: {'Content-Type': 'application/json'}
});
}
}
return response;
});
};
})();
document.addEventListener('DOMContentLoaded', function() {
const burger = document.querySelectorAll('.navbar-burger');
const menu = document.querySelectorAll('.navbar-menu');
@@ -40,6 +40,10 @@
});
},
confirmMWEBChangeConvert: function() {
return confirm('Confirm MWEB change conversion: This will create a tx sending all spendable MWEB outputs in the plain LTC wallet to LTC.');
},
confirmReseed: function() {
return confirm('Are you sure you want to reseed the wallet? This will generate new addresses.');
},
@@ -282,6 +286,16 @@
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm-mweb-change-convert]');
if (target) {
if (!this.confirmMWEBChangeConvert()) {
e.preventDefault();
return false;
}
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm-utxo]');
if (target) {
@@ -398,6 +412,7 @@
window.EventHandlers = EventHandlers;
window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers);
window.confirmMWEBChangeConvert = EventHandlers.confirmMWEBChangeConvert.bind(EventHandlers);
window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers);
window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers);
window.confirmRemoveExpired = EventHandlers.confirmRemoveExpired.bind(EventHandlers);
+1 -1
View File
@@ -148,7 +148,7 @@ const BidPage = {
11: { phase: 'locking', order: 10, label: 'Locking' }, // XMR_SWAP_NOSCRIPT_COIN_LOCKED
12: { phase: 'redemption', order: 11, label: 'Redemption' }, // XMR_SWAP_LOCK_RELEASED
13: { phase: 'redemption', order: 12, label: 'Redemption' }, // XMR_SWAP_SCRIPT_TX_REDEEMED
14: { phase: 'failed', order: 90, label: 'Failed' }, // XMR_SWAP_SCRIPT_TX_PREREFUND
14: { phase: 'redemption', order: 11.5, label: 'Refunding' }, // XMR_SWAP_SCRIPT_TX_PREREFUND
15: { phase: 'redemption', order: 13, label: 'Redemption' }, // XMR_SWAP_NOSCRIPT_TX_REDEEMED
16: { phase: 'failed', order: 91, label: 'Recovered' }, // XMR_SWAP_NOSCRIPT_TX_RECOVERED
17: { phase: 'failed', order: 92, label: 'Failed' }, // XMR_SWAP_FAILED_REFUNDED
+10 -7
View File
@@ -351,16 +351,19 @@
);
matchingCoins.forEach(coinData => {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname][data-balance-type]');
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinData.name) {
const currentText = element.textContent;
const ticker = coinData.ticker || coinId.toUpperCase();
const newBalance = `${coinData.balance} ${ticker}`;
if (currentText !== newBalance) {
element.textContent = newBalance;
console.log(`Updated balance: ${coinData.name} -> ${newBalance}`);
const balanceType = element.getAttribute('data-balance-type');
const value = coinData[balanceType];
if (value !== undefined) {
const ticker = coinData.ticker || coinId.toUpperCase();
const newBalance = balanceType === 'est_fee' ? value : `${value} ${ticker}`;
if (element.textContent !== newBalance) {
element.textContent = newBalance;
console.log(`Updated ${balanceType}: ${coinData.name} -> ${newBalance}`);
}
}
}
});
+19
View File
@@ -75,9 +75,28 @@
if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingBalance('Particl', 'Blind Balance:', coinData.pending, coinData.ticker || 'PART', 'Blind Unconfirmed:', coinData);
}
} else if (coinData.name === 'Litecoin MWEB') {
this.updateSpecificBalance('Litecoin', 'MWEB Balance:', coinData.balance, coinData.ticker || 'LTC');
this.removePendingBalance('Litecoin', 'MWEB Balance:');
if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingBalance('Litecoin', 'MWEB Balance:', coinData.pending, coinData.ticker || 'LTC', 'MWEB Pending:', coinData);
}
} else {
this.updateSpecificBalance(coinData.name, 'Balance:', coinData.balance, coinData.ticker || coinData.name);
if (coinData.mweb_balance !== undefined) {
this.updateSpecificBalance(coinData.name, 'MWEB Balance:', coinData.mweb_balance, coinData.ticker || coinData.name);
}
if (coinData.spark_balance !== undefined) {
this.updateSpecificBalance(coinData.name, 'Spark Balance:', coinData.spark_balance, coinData.ticker || coinData.name);
}
if (coinData.blind_balance !== undefined) {
this.updateSpecificBalance(coinData.name, 'Blind Balance:', coinData.blind_balance, coinData.ticker || coinData.name);
}
if (coinData.anon_balance !== undefined) {
this.updateSpecificBalance(coinData.name, 'Anon Balance:', coinData.anon_balance, coinData.ticker || coinData.name);
}
if (coinData.name !== 'Particl Anon' && coinData.name !== 'Particl Blind' && coinData.name !== 'Litecoin MWEB') {
if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingDisplay(coinData);
+1 -1
View File
@@ -26,7 +26,7 @@
<div class="flex items-center">
<p class="text-sm text-gray-90 dark:text-white font-medium">© 2026~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">BSX: v{{ version }}</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">GUI: v3.4.1</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">GUI: v3.5.0</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
{{ love_svg | safe }}
</div>
+26 -14
View File
@@ -138,7 +138,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Balance: </td>
<td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="balance">{{ w.balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.pending }} {{ w.ticker }} </span>
@@ -152,7 +152,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Blind"> </span>Blind Balance: </td>
<td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="blind_balance">{{ w.blind_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.blind_unconfirmed %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Unconfirmed: +{{ w.blind_unconfirmed }} {{ w.ticker }}</span>
@@ -162,7 +162,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Anon"> </span>Anon Balance: </td>
<td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="anon_balance">{{ w.anon_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.anon_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.anon_pending }} {{ w.ticker }}</span>
@@ -177,7 +177,7 @@
{% if is_electrum_mode %}
<span class="text-gray-400 dark:text-gray-300">Not available in light mode</span>
{% else %}
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="mweb_balance">{{ w.mweb_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.mweb_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.mweb_pending }} {{ w.ticker }} </span>
@@ -185,11 +185,22 @@
{% endif %}
</td>
</tr>
{% if w.mweb_in_plain %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} MWEB"> </span>MWEB in Plain Balance: </td>
<td class="py-3 px-6 bold">
<span>{{ w.mweb_in_plain }} {{ w.ticker }}</span>
</td>
<td class="py-3 px-6 bold">
<button type="submit" class="flex justify-center py-2 px-4 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" name="convertmweb_{{ w.cid }}" value="Convert" data-confirm-mweb-change-convert> Convert </button>
</td>
</tr>
{% endif %}
{% elif w.cid == '13' %} {# FIRO #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Spark"> </span>Spark Balance: </td>
<td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="spark_balance">{{ w.spark_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.spark_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.spark_pending }} {{ w.ticker }} </span>
@@ -200,7 +211,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Spark"> </span>Spark Balance: </td>
<td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="spark_balance">{{ w.spark_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.spark_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.spark_pending }} {{ w.ticker }} </span>
@@ -337,6 +348,7 @@
<td class="py-3 px-6">{{ w.expected_seed }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
@@ -463,7 +475,7 @@
</div>
<div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">MWEB Address: </div>
<div class="text-center relative">
<div class="input-like-container hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" id="mweb_address">{{ w.mweb_address }}</div>
<div class="input-like-container hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" id="stealth_address">{{ w.mweb_address }}</div>
<span class="absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer" id="copyIcon"></span>
</div>
<div class="opacity-100 text-gray-500 dark:text-gray-100 flex justify-center items-center">
@@ -535,7 +547,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Balance: </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="balance">{{ w.balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
</td>
</tr>
@@ -547,7 +559,7 @@
{% if is_electrum_mode %}
<span class="text-gray-400 dark:text-gray-300">Not available in light mode</span>
{% else %}
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="mweb_balance">{{ w.mweb_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% endif %}
</td>
@@ -557,7 +569,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold w-1/4"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Spark Balance: </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="spark_balance">{{ w.spark_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
</td>
</tr>
@@ -566,7 +578,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold w-1/4"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Spark Balance: </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="spark_balance">{{ w.spark_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
</td>
</tr>
@@ -575,14 +587,14 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Blind Balance: </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="blind_balance">{{ w.blind_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Anon Balance: </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="anon_balance">{{ w.anon_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
</td>
</tr>
@@ -757,7 +769,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Fee Estimate:</td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.est_fee }}</span>
<span class="coinname-value" data-coinname="{{ w.name }}" data-balance-type="est_fee">{{ w.est_fee }}</span>
(<span class="usd-value fee-estimate-usd" data-decimals="8"></span>)
</td>
</tr>
+5 -5
View File
@@ -65,7 +65,7 @@
<div class="p-6 bg-coolGray-100 dark:bg-gray-600">
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Balance:</h4>
<div class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }}</div>
<div class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}" data-balance-type="balance">{{ w.balance }} {{ w.ticker }}</div>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white ">{{ w.ticker }} USD value:</h4>
@@ -90,7 +90,7 @@
{% if w.cid == '1' %} {# PART #}
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Blind Balance:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }}</span>
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}" data-balance-type="blind_balance">{{ w.blind_balance }} {{ w.ticker }}</span>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Blind USD value:</h4>
@@ -108,7 +108,7 @@
{% endif %}
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Anon Balance:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }}</span>
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}" data-balance-type="anon_balance">{{ w.anon_balance }} {{ w.ticker }}</span>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Anon USD value:</h4>
@@ -129,7 +129,7 @@
{% if w.cid == '3' and w.connection_type != 'electrum' %} {# LTC - MWEB not available in electrum mode #}
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">MWEB Balance:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}" data-balance-type="mweb_balance">{{ w.mweb_balance }} {{ w.ticker }}</span>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">MWEB USD value:</h4>
@@ -151,7 +151,7 @@
{% if w.cid == '13' %} {# FIRO #}
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Spark Balance:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span>
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}" data-balance-type="spark_balance">{{ w.spark_balance }} {{ w.ticker }}</span>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Spark USD value:</h4>
+20 -1
View File
@@ -273,6 +273,9 @@ def page_wallet(self, url_split, post_string):
swap_client.cacheNewAddressForCoin(coin_id)
elif have_data_entry(form_data, "forcerefresh"):
force_refresh = True
elif have_data_entry(form_data, "convertmweb_" + cid):
txid = swap_client.ci(coin_id).convertMWEBBalance()
messages.append(f"Converted MWEB change to LTC in tx: {txid}")
elif have_data_entry(form_data, "newmwebaddr_" + cid):
swap_client.cacheNewStealthAddressForCoin(coin_id)
elif have_data_entry(form_data, "newsparkaddr_" + cid):
@@ -473,6 +476,15 @@ def page_wallet(self, url_split, post_string):
getattr(ci, "_connection_type", "rpc") == "electrum"
)
if hasattr(ci, "getAccountKey") and k not in (Coins.XMR, Coins.WOW):
try:
chain = swap_client.chain
zprv_prefix = 0x04B2430C if chain == "mainnet" else 0x045F18BC
seed_key = swap_client.getWalletKey(k, 1)
wallet_data["account_key"] = ci.getAccountKey(seed_key, zprv_prefix)
except Exception:
pass
fee_rate, fee_src = swap_client.getFeeRateForCoin(k)
est_fee = swap_client.estimateWithdrawFee(k, fee_rate)
wallet_data["fee_rate"] = ci.format_amount(int(fee_rate * ci.COIN()))
@@ -516,6 +528,10 @@ def page_wallet(self, url_split, post_string):
// page_data["fee_estimate"]["sum_weight"]
)
if k == Coins.LTC and ci.useBackend() is False:
mweb_value: int = ci.getMWEBBalance()
if mweb_value > 0:
wallet_data["mweb_in_plain"] = ci.format_amount(mweb_value)
if show_utxo_groups:
utxo_groups = ""
unspent_by_addr = ci.getUnspentsByAddr()
@@ -559,7 +575,10 @@ def page_wallet(self, url_split, post_string):
skip = tx_filters.get("offset", 0)
all_txs = ci.listWalletTransactions(count=10000, skip=0)
all_txs = list(reversed(all_txs)) if all_txs else []
if all_txs and coin_id not in (Coins.XMR, Coins.WOW):
all_txs = list(reversed(all_txs))
elif not all_txs:
all_txs = []
total_transactions = len(all_txs)
raw_txs = all_txs[skip : skip + count] if all_txs else []
+3 -1
View File
@@ -810,7 +810,9 @@ class ElectrumBackend(WalletBackend):
now = time.time()
stale_threshold = 300
is_synced = height > 0 and (now - height_time) < stale_threshold
last_activity = getattr(self._server, "_last_activity", 0)
most_recent = max(height_time, last_activity)
is_synced = height > 0 and (now - most_recent) < stale_threshold
return {
"height": height,
"synced": is_synced,
+35 -49
View File
@@ -4,10 +4,12 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import hashlib
import json
import sqlite3
import threading
import time
from typing import Dict, List, Optional, Tuple
from coincurve import PrivateKey, PublicKey
from .chainparams import Coins
from .contrib.test_framework import segwit_addr
@@ -19,7 +21,7 @@ from .db_wallet import (
WalletTxCache,
WalletWatchOnly,
)
from .util.crypto import hash160
from .util.crypto import hash160, sha256
from .util.extkey import ExtKeyPair
@@ -38,6 +40,7 @@ class WalletManager:
}
GAP_LIMIT = 50
ELECTRUM_GAP_LIMIT = 20
def __init__(self, swap_client, log):
self._gap_limits: Dict[Coins, int] = {}
@@ -111,13 +114,11 @@ class WalletManager:
def _deriveAddress(
self, coin_type: Coins, index: int, internal: bool = False
) -> Tuple[str, str, bytes]:
from coincurve import PublicKey
key = self._deriveKey(coin_type, index, internal)
pubkey = PublicKey.from_secret(key).format()
pkh = hash160(pubkey)
address = segwit_addr.encode(self._getHRP(coin_type), 0, pkh)
scripthash = hashlib.sha256(bytes([0x00, 0x14]) + pkh).digest()[::-1].hex()
scripthash = sha256(bytes([0x00, 0x14]) + pkh)[::-1].hex()
return address, scripthash, pubkey
def _syncStateIndices(self, coin_type: Coins, cursor) -> None:
@@ -149,6 +150,18 @@ class WalletManager:
)
self._swap_client.commitDB()
def _findReusableAddress(self, coin_type: Coins, internal: bool, cursor):
query = (
"SELECT derivation_index, address FROM wallet_addresses"
" WHERE coin_type = ? AND is_internal = ? AND is_funded = 0"
" ORDER BY derivation_index ASC LIMIT 1"
)
cursor.execute(query, (int(coin_type), internal))
row = cursor.fetchone()
if row:
return row[0], row[1]
return None, None
def getNewAddress(
self, coin_type: Coins, internal: bool = False, label: str = "", cursor=None
) -> str:
@@ -157,8 +170,6 @@ class WalletManager:
use_cursor = self._swap_client.openDB(cursor)
try:
self._syncStateIndices(coin_type, use_cursor)
state = self._swap_client.queryOne(
WalletState, use_cursor, {"coin_type": int(coin_type)}
)
@@ -184,6 +195,19 @@ class WalletManager:
else:
next_index = (state.last_external_index or 0) + 1
if next_index >= self.ELECTRUM_GAP_LIMIT:
reuse_index, reuse_addr = self._findReusableAddress(
coin_type, internal, use_cursor
)
if reuse_addr is not None:
self._log.debug(
f"Reusing unfunded address at index {reuse_index}"
f" (next would be {next_index},"
f" electrum gap limit {self.ELECTRUM_GAP_LIMIT})"
)
self._swap_client.commitDB()
return reuse_addr
existing = self._swap_client.queryOne(
WalletAddress,
use_cursor,
@@ -251,8 +275,6 @@ class WalletManager:
def getAddress(
self, coin_type: Coins, index: int, internal: bool = False
) -> Optional[str]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -371,8 +393,6 @@ class WalletManager:
include_watch_only: bool = True,
funded_only: bool = False,
) -> List[str]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -402,8 +422,6 @@ class WalletManager:
return []
def getFundedAddresses(self, coin_type: Coins) -> Dict[str, str]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -428,8 +446,6 @@ class WalletManager:
return {}
def getExistingInternalAddress(self, coin_type: Coins) -> Optional[str]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -493,8 +509,6 @@ class WalletManager:
self._swap_client.closeDB(cursor, commit=False)
def getAddressInfo(self, coin_type: Coins, address: str) -> Optional[dict]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -535,8 +549,6 @@ class WalletManager:
return None
def getCachedTotalBalance(self, coin_type: Coins) -> int:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -676,8 +688,6 @@ class WalletManager:
if not self.isInitialized(coin_type):
return None
import sqlite3
now = int(time.time())
min_cache_time = now - max_cache_age
@@ -720,8 +730,6 @@ class WalletManager:
return None
def hasCachedBalances(self, coin_type: Coins, max_cache_age: int = 120) -> bool:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -738,8 +746,6 @@ class WalletManager:
def getPrivateKey(self, coin_type: Coins, address: str) -> Optional[bytes]:
if not self.isInitialized(coin_type):
return None
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -764,8 +770,6 @@ class WalletManager:
return None
def getSignableAddresses(self, coin_type: Coins) -> Dict[str, str]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -837,10 +841,8 @@ class WalletManager:
label: str = "",
source: str = "import",
) -> bool:
from coincurve import PublicKey as CCPublicKey
try:
pubkey = CCPublicKey.from_secret(private_key).format()
pubkey = PublicKey.from_secret(private_key).format()
if (
segwit_addr.encode(self._getHRP(coin_type), 0, hash160(pubkey))
!= address
@@ -979,7 +981,7 @@ class WalletManager:
def _b58decode_check(self, s: str) -> bytes:
data = self._b58decode(s)
payload, checksum = data[:-4], data[-4:]
expected = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
expected = sha256(sha256(payload))[:4]
if checksum != expected:
raise ValueError("Invalid base58 checksum")
return payload
@@ -1001,7 +1003,7 @@ class WalletManager:
master_key = self._master_keys.get(coin_type)
if master_key is None:
raise ValueError(f"Wallet not initialized for {coin_type}")
return hashlib.sha256(master_key + b"_import_key").digest()
return sha256(master_key + b"_import_key")
def _encryptPrivateKey(self, private_key: bytes, coin_type: Coins) -> bytes:
return bytes(a ^ b for a, b in zip(private_key, self._getXorKey(coin_type)))
@@ -1013,7 +1015,7 @@ class WalletManager:
_, data = segwit_addr.decode(self._getHRP(coin_type), address)
if data is None:
return ""
return hashlib.sha256(bytes([0x00, 0x14]) + bytes(data)).digest()[::-1].hex()
return sha256(bytes([0x00, 0x14]) + bytes(data))[::-1].hex()
def needsMigration(self, coin_type: Coins) -> bool:
cursor = self._swap_client.openDB()
@@ -1169,8 +1171,6 @@ class WalletManager:
self._swap_client.closeDB(cursor, commit=False)
def getSeedID(self, coin_type: Coins) -> Optional[str]:
from basicswap.contrib.test_framework.script import hash160
master_key = self._master_keys.get(coin_type)
if master_key is None:
return None
@@ -1180,16 +1180,12 @@ class WalletManager:
return hash160(ek.encode_p()).hex()
def signMessage(self, coin_type: Coins, address: str, message: str) -> bytes:
from coincurve import PrivateKey
key = self.getPrivateKey(coin_type, address)
if key is None:
raise ValueError(f"Cannot sign: no key for address {address}")
return PrivateKey(key).sign(message.encode("utf-8"))
def signHash(self, coin_type: Coins, address: str, msg_hash: bytes) -> bytes:
from coincurve import PrivateKey
key = self.getPrivateKey(coin_type, address)
if key is None:
raise ValueError(f"Cannot sign: no key for address {address}")
@@ -1198,8 +1194,6 @@ class WalletManager:
def getKeyForAddress(
self, coin_type: Coins, address: str
) -> Optional[Tuple[bytes, bytes]]:
from coincurve import PublicKey
key = self.getPrivateKey(coin_type, address)
if key is None:
return None
@@ -1208,8 +1202,6 @@ class WalletManager:
def findAddressByScripthash(
self, coin_type: Coins, scripthash: str
) -> Optional[str]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -1232,8 +1224,6 @@ class WalletManager:
return None
def getAllScripthashes(self, coin_type: Coins) -> List[str]:
import sqlite3
try:
conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor()
@@ -1871,8 +1861,6 @@ class WalletManager:
for _ in existing:
return False
import json
pending = WalletPendingTx()
pending.coin_type = int(coin_type)
pending.txid = txid
@@ -1921,8 +1909,6 @@ class WalletManager:
) -> List[dict]:
cursor = self._swap_client.openDB()
try:
import json
results = self._swap_client.query(
WalletPendingTx,
cursor,
+7
View File
@@ -0,0 +1,7 @@
# LTC Notes
## MWEB
Sending LTC -> MWEB generates MWEB change outputs in the plain LTC wallet that BSX can't use.
A temporary convenience function is provided to convert those MWEB outputs back to plain LTC.
+6
View File
@@ -140,6 +140,12 @@ Observe progress with
tail -f /tmp/firo.log
Alternatively --extracoinopts can be used with --startonlycoin
docker-compose run --rm swapclient \
basicswap-run --datadir=/coindata --startonlycoin=litecoin --extracoinopts="-reindex"
## Start a subset of the configured coins using docker
docker compose run --rm --service-ports swapclient basicswap-run -datadir=/coindata -withcoins=monero
+3 -3
View File
@@ -135,15 +135,15 @@
(define-public basicswap
(package
(name "basicswap")
(version "0.15.2")
(version "0.16.2")
(source (origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/basicswap/basicswap")
(commit "83807d213fab52c99f69dbc06fa7baedb449d66f")))
(commit "3b76adeedbbf3586308b6bf8ba401cec899f8a61")))
(sha256
(base32
"08ykwn2wbcny5k6kwj3xkfkim40kmzcb988lpcd70r7kcmn8ggp0"))
"0wvy1c6li45ivfnn93iry047fz7w088mcp5rbg07qnnbnb6lgqiz"))
(file-name (git-file-name name version))))
(build-system pyproject-build-system)
+19 -2
View File
@@ -182,6 +182,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
"datadir": node_dir,
"bindir": cfg.PARTICL_BINDIR,
"blocks_confirmed": 2, # Faster testing
"wallet_name": "bsx_wallet",
},
"pivx": {
"connection_type": "rpc",
@@ -191,6 +192,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
"bindir": PIVX_BINDIR,
"use_csv": False,
"use_segwit": False,
"wallet_name": "",
},
"bitcoin": {
"connection_type": "rpc",
@@ -199,6 +201,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
"datadir": btcdatadir,
"bindir": cfg.BITCOIN_BINDIR,
"use_segwit": True,
"wallet_name": "bsx_wallet",
},
},
"check_progress_seconds": 2,
@@ -760,7 +763,17 @@ class Test(unittest.TestCase):
rtx = pivxRpc(f'getrawtransaction "{txid}" true')
assert rtx["version"] == 3
block_hash = pivxRpc(f'generatetoaddress 1 "{generate_addr}"')[0]
block_hash = None
for i in range(15):
rtx = pivxRpc(f'getrawtransaction "{txid}" true')
if "blockhash" in rtx:
block_hash = rtx["blockhash"]
logging.info(f"Shielded tx confirmed in block {block_hash} after {i}s")
break
if i == 5:
pivxRpc(f'generatetoaddress 1 "{generate_addr}"')
delay_event.wait(1)
assert block_hash is not None, "Shielded tx was not confirmed"
ci = self.swap_clients[0].ci(Coins.PIVX)
block = ci.getBlockWithTxns(block_hash)
@@ -837,7 +850,11 @@ class Test(unittest.TestCase):
swap_value = ci_from.make_int(swap_value)
assert swap_value > ci_from.make_int(9)
itx = pi.getFundedInitiateTxTemplate(ci_from, swap_value, True)
addr_to = pi.getMockAddrTo(ci_from)
funded_tx = ci_from.createRawFundedTransaction(
addr_to, swap_value, True, lock_unspents=True
)
itx = bytes.fromhex(funded_tx)
itx_decoded = ci_from.describeTx(itx.hex())
n = pi.findMockVout(ci_from, itx_decoded)
@@ -15,14 +15,15 @@ export XMR_RPC_USER=xmr_user
export XMR_RPC_PWD=xmr_pwd
python tests/basicswap/extended/test_xmr_persistent.py
# Copy coin releases to permanent storage for faster subsequent startups
cp -r ${TEST_PATH}/bin/* ~/tmp/basicswap_bin/
# Continue existing chains with
export RESET_TEST=false
# Set coins started
export TEST_COINS_LIST="bitcoin,monero,litecoin"
"""
import json
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -23,6 +23,25 @@ if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
def wait_for_balance(
port: int,
coin: str,
expect_amount: float,
balance_key: str = "balance",
iterations: int = 30,
delay_time: int = 1,
) -> None:
logger.info(f"Waiting for balance, port: {port}")
for i in range(iterations):
rv_js = read_json_api(port, f"wallets/{coin}")
if float(rv_js[balance_key]) >= expect_amount:
return
time.sleep(delay_time)
logger.warning(f"{port} wallets/{coin} {rv_js}")
raise ValueError(f"Expect {balance_key} {expect_amount}")
def clear_offers(port_list) -> None:
logger.info(f"clear_offers {port_list}")
@@ -60,7 +79,11 @@ def test_swap_dir(driver):
"automation_strat_id": 1,
}
rv = read_json_api(node_1_port, "offers/new", offer_data)
offer_1_id = rv["offer_id"]
try:
offer_1_id = rv["offer_id"]
except Exception as e:
logger.info(f"rv: {rv}")
raise e
offer_data = {
"addr_from": -1,
@@ -72,7 +95,13 @@ def test_swap_dir(driver):
"automation_strat_id": 1,
}
rv = read_json_api(node_1_port, "offers/new", offer_data)
offer_2_id = rv["offer_id"]
try:
offer_2_id = rv["offer_id"]
except Exception as e:
logger.info(f"rv: {rv}")
raise e
wait_for_balance(node_2_port, "xmr", 5.0)
offer_data = {
"addr_from": -1,
@@ -84,7 +113,11 @@ def test_swap_dir(driver):
"automation_strat_id": 1,
}
rv = read_json_api(node_2_port, "offers/new", offer_data)
offer_3_id = rv["offer_id"]
try:
offer_3_id = rv["offer_id"]
except Exception as e:
logger.info(f"rv: {rv}")
raise e
# Wait for offers to propagate
for i in range(1000):
+96 -1
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021-2024 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -184,6 +184,15 @@ class TestLTC(BasicSwapTest):
ci0 = swap_clients[0].ci(self.test_coin_from)
ci1 = swap_clients[1].ci(self.test_coin_from)
# mweb utxos before sending to mweb
num_mweb: int = 0
utxos_0 = ci0.rpc_wallet("listunspent")
for utxo in utxos_0:
addr_info = ci0.rpc_wallet("getaddressinfo", [utxo["address"]])
if addr_info["ismweb"] is True:
num_mweb += 1
assert num_mweb == 1
mweb_addr_0 = ci0.rpc_wallet("getnewaddress", ["mweb addr test 0", "mweb"])
mweb_addr_1 = ci1.rpc_wallet("getnewaddress", ["mweb addr test 1", "mweb"])
@@ -210,6 +219,19 @@ class TestLTC(BasicSwapTest):
< 0.1
)
num_mweb: int = 0
utxos_0 = ci0.rpc_wallet(
"listunspent",
[
0,
],
)
for utxo in utxos_0:
addr_info = ci0.rpc_wallet("getaddressinfo", [utxo["address"]])
if addr_info["ismweb"] is True:
num_mweb += 1
assert num_mweb > 1
try:
pause_event.clear() # Stop mining
ci0.rpc_wallet("sendtoaddress", [mweb_addr_1, 10.0])
@@ -237,6 +259,7 @@ class TestLTC(BasicSwapTest):
for utxo in utxos:
if utxo.get("address", "") == mweb_addr_1:
mweb_tx = utxo
break
assert mweb_tx is not None
unspent_addr = ci1.getUnspentsByAddr()
@@ -245,6 +268,62 @@ class TestLTC(BasicSwapTest):
if "mweb1" in addr:
raise ValueError("getUnspentsByAddr should exclude mweb UTXOs.")
# Test helper functions to convert MWEB change
mweb_change_value = ci0.getMWEBBalance()
assert mweb_change_value > 0
test_lock_utxo = None
for utxo in utxos:
utxo_address: str = utxo.get("address", "")
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
continue
test_lock_utxo = {"txid": utxo["txid"], "vout": utxo["vout"]}
ci0.rpc_wallet(
"lockunspent",
[
False,
[
test_lock_utxo,
],
],
)
break
assert len(ci0.rpc_wallet("listlockunspent")) == 1
txid = ci0.convertMWEBBalance()
# Check utxos locked before conversion are still locked after
assert len(ci0.rpc_wallet("listlockunspent")) == 1
ci0.rpc_wallet(
"lockunspent",
[
True,
[
test_lock_utxo,
],
],
)
assert len(ci0.rpc_wallet("listlockunspent")) == 0
txj = ci0.rpc_wallet(
"gettransaction",
[
txid,
],
)
assert len(txj["details"]) == 2
fee_amt = -ci0.make_int(txj["fee"])
assert txj["details"][0]["category"] == "send"
assert ci0.make_int(txj["details"][0]["amount"]) - fee_amt == -mweb_change_value
assert txj["details"][1]["category"] == "receive"
assert ci0.make_int(txj["details"][1]["amount"]) + fee_amt == mweb_change_value
mweb_change_value = ci0.getMWEBBalance()
assert mweb_change_value == 0
# TODO
def test_22_mweb_balance(self):
@@ -265,7 +344,9 @@ class TestLTC(BasicSwapTest):
ltc_mweb_addr = read_json_api(
TEST_HTTP_PORT + 0, "wallets/ltc_mweb/nextdepositaddr"
)
assert ltc_mweb_addr.startswith("tmweb1")
ltc_mweb_addr2 = read_json_api(TEST_HTTP_PORT + 0, "wallets/ltc/newmwebaddress")
assert ltc_mweb_addr2.startswith("tmweb1")
assert (
ci_mweb.rpc_wallet(
@@ -337,6 +418,20 @@ class TestLTC(BasicSwapTest):
json_rv = read_json_api(TEST_HTTP_PORT + 0, "wallets/ltc", post_json)
assert json_rv["mweb_balance"] <= 20.0
# Test helper functions to convert MWEB change
json_rv = read_json_api(
TEST_HTTP_PORT + 0, "wallets/ltc/mwebbalance", post_json
)
assert float(json_rv) > 0
json_rv = read_json_api(
TEST_HTTP_PORT + 0, "wallets/ltc/convertmweb", post_json
)
assert len(json_rv) == 64
json_rv = read_json_api(
TEST_HTTP_PORT + 0, "wallets/ltc/mwebbalance", post_json
)
assert float(json_rv) == 0
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -222,6 +222,7 @@ def prepare_swapclient_dir(
"datadir": os.path.join(datadir, "ltc_" + str(node_id)),
"bindir": cfg.LITECOIN_BINDIR,
"use_segwit": True,
"wallet_name": "bsx_wallet",
}
if cls: