mirror of
https://github.com/basicswap/basicswap.git
synced 2026-05-06 22:42:13 +02:00
Compare commits
41 Commits
a8d953d8e0
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 680fc7ce35 | |||
| 1f9c85c62f | |||
| 3716a0ab62 | |||
| 2ad8e6f4b3 | |||
| ac084eddf7 | |||
| 262593bd2c | |||
| 9f17ee709a | |||
| e29eb4af76 | |||
| 6ebbd98aec | |||
| bdb7f9bb5a | |||
| f626e400ff | |||
| 2bacbcabd0 | |||
| 2b33ed3d93 | |||
| c4e7de2873 | |||
| 9caae399d2 | |||
| fd2e442839 | |||
| dfa11ed32f | |||
| c4f00dfa5b | |||
| b5226c0e1c | |||
| 842e44e41b | |||
| c298cf3963 | |||
| e06c4638d3 | |||
| 6dcf0df8aa | |||
| 2c13314bdd | |||
| 60eb0b295b | |||
| 2c1d5c60b2 | |||
| 47cd052c9f | |||
| 6a8ab745e1 | |||
| c5e703dfb3 | |||
| ff6d1ad0ba | |||
| 1d80f479c0 | |||
| f2fff7292b | |||
| f84c46376e | |||
| a3e6d0cf17 | |||
| fe0de84054 | |||
| dfd4bb5b65 | |||
| aeff117fdc | |||
| 0dc5284e51 | |||
| b8f41b26c0 | |||
| 0c0fb8360e | |||
| d8d457e283 |
-45
@@ -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'
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
name = "basicswap"
|
||||
|
||||
__version__ = "0.15.3"
|
||||
__version__ = "0.16.1"
|
||||
|
||||
+17
-4
@@ -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
@@ -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
@@ -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}")
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
@@ -3099,7 +3099,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}"
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
+86
-107
@@ -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")
|
||||
|
||||
@@ -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.")
|
||||
|
||||
+18
-4
@@ -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 = []
|
||||
@@ -290,7 +289,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 +331,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 +1642,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"]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -185,6 +185,17 @@
|
||||
{% 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>
|
||||
@@ -337,6 +348,16 @@
|
||||
<td class="py-3 px-6">{{ w.expected_seed }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if w.account_key %}
|
||||
<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">Extended Private Key:</td>
|
||||
<td class="py-3 px-6">
|
||||
<span id="account-key-hidden" class="font-mono text-sm">••••••••••••••••</span>
|
||||
<span id="account-key-value" class="font-mono text-sm hidden break-all">{{ w.account_key }}</span>
|
||||
<button type="button" id="toggle-account-key" onclick="var h=document.getElementById('account-key-hidden'),v=document.getElementById('account-key-value');if(v.classList.contains('hidden')){v.classList.remove('hidden');h.classList.add('hidden');this.textContent='Hide';}else{v.classList.add('hidden');h.classList.remove('hidden');this.textContent='Show';}" class="ml-2 px-2 py-1 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded">Show</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -463,7 +484,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">
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -38,6 +38,7 @@ class WalletManager:
|
||||
}
|
||||
|
||||
GAP_LIMIT = 50
|
||||
ELECTRUM_GAP_LIMIT = 20
|
||||
|
||||
def __init__(self, swap_client, log):
|
||||
self._gap_limits: Dict[Coins, int] = {}
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -135,15 +135,15 @@
|
||||
(define-public basicswap
|
||||
(package
|
||||
(name "basicswap")
|
||||
(version "0.15.2")
|
||||
(version "0.16.0")
|
||||
(source (origin
|
||||
(method git-fetch)
|
||||
(uri (git-reference
|
||||
(url "https://github.com/basicswap/basicswap")
|
||||
(commit "83807d213fab52c99f69dbc06fa7baedb449d66f")))
|
||||
(commit "2c13314bdd29622235c92fd20c237801acb3cb76")))
|
||||
(sha256
|
||||
(base32
|
||||
"08ykwn2wbcny5k6kwj3xkfkim40kmzcb988lpcd70r7kcmn8ggp0"))
|
||||
"0j0id6db3ljdsfag8krjdmd4rzlz2504yk9lzj0p89lqyygi9ilc"))
|
||||
(file-name (git-file-name name version))))
|
||||
(build-system pyproject-build-system)
|
||||
|
||||
|
||||
@@ -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) 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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user