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" interval: "weekly"
open-pull-requests-limit: 20 open-pull-requests-limit: 20
target-branch: "dev" 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: matrix:
python-version: ["3.12"] python-version: ["3.12"]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies
@@ -69,7 +69,7 @@ jobs:
pytest tests/basicswap/test_other.py pytest tests/basicswap/test_other.py
- name: Cache coin cores - name: Cache coin cores
id: cache-cores id: cache-cores
uses: actions/cache@v3 uses: actions/cache@v5
env: env:
cache-name: cache-cores cache-name: cache-cores
with: with:
@@ -101,6 +101,7 @@ jobs:
cp -r $BIN_DIR/* ${TEST_PATH}/bin/ cp -r $BIN_DIR/* ${TEST_PATH}/bin/
pytest tests/basicswap/extended/test_encrypted_xmr_reload.py pytest tests/basicswap/extended/test_encrypted_xmr_reload.py
- name: Run selenium tests - name: Run selenium tests
id: selenium_tests
run: | run: |
export TEST_PATH=/tmp/test_persistent export TEST_PATH=/tmp/test_persistent
mkdir -p ${TEST_PATH}/bin mkdir -p ${TEST_PATH}/bin
@@ -126,3 +127,8 @@ jobs:
echo "Running test_swap_direction.py" echo "Running test_swap_direction.py"
python tests/basicswap/selenium/test_swap_direction.py python tests/basicswap/selenium/test_swap_direction.py
kill $TEST_NETWORK_PID 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" 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() self._initializeElectrumWallets()
is_locked = False
try:
_, is_locked = self.getLockedState()
except Exception:
pass
for c in self.activeCoins(): for c in self.activeCoins():
if self.coin_clients[c]["connection_type"] == "electrum": if self.coin_clients[c]["connection_type"] == "electrum":
if is_locked:
continue
self.checkWalletSeed(c) self.checkWalletSeed(c)
for c in self.activeCoins(): for c in self.activeCoins():
@@ -1651,11 +1659,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
for c in check_coins: for c in check_coins:
ci = self.ci(c) ci = self.ci(c)
if self._restrict_unknown_seed_wallets and not ci.knownWalletSeed(): if self._restrict_unknown_seed_wallets and not ci.knownWalletSeed():
raise ValueError( try:
'{} has an unexpected wallet seed and "restrict_unknown_seed_wallets" is enabled.'.format( self.checkWalletSeed(c)
ci.coin_name() 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"): if self.coin_clients[c]["connection_type"] not in ("rpc", "electrum"):
continue continue
if c in (Coins.XMR, Coins.WOW): if c in (Coins.XMR, Coins.WOW):
+57 -34
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert # 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 # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -29,6 +29,7 @@ import urllib.parse
import zipfile import zipfile
import zmq import zmq
from typing import List
from urllib.request import urlopen from urllib.request import urlopen
import basicswap.config as cfg 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 = os.getenv("BITCOIN_VERSION", "29.3")
BITCOIN_VERSION_TAG = os.getenv("BITCOIN_VERSION_TAG", "") 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", "") LITECOIN_VERSION_TAG = os.getenv("LITECOIN_VERSION_TAG", "")
DCR_VERSION = os.getenv("DCR_VERSION", "2.1.3") DCR_VERSION = os.getenv("DCR_VERSION", "2.1.3")
@@ -185,11 +186,13 @@ else:
BIN_ARCH = os.getenv("BIN_ARCH", BIN_ARCH) BIN_ARCH = os.getenv("BIN_ARCH", BIN_ARCH)
FILE_EXT = os.getenv("FILE_EXT", FILE_EXT) FILE_EXT = os.getenv("FILE_EXT", FILE_EXT)
logger = logging.getLogger() logger = logging.getLogger("prepare")
LOG_LEVEL = logging.DEBUG LOG_LEVEL = logging.DEBUG
logger.propagate = False
logger.level = LOG_LEVEL logger.level = LOG_LEVEL
if not len(logger.handlers): handler = logging.StreamHandler(sys.stdout)
logger.addHandler(logging.StreamHandler(sys.stdout)) handler.setFormatter(logging.Formatter("%(levelname)s : %(message)s"))
logger.addHandler(handler)
logging.getLogger("gnupg").setLevel(logging.INFO) logging.getLogger("gnupg").setLevel(logging.INFO)
BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", False)) BSX_DOCKER_MODE = toBool(os.getenv("BSX_DOCKER_MODE", False))
@@ -458,33 +461,47 @@ def getRemoteFileLength(url: str) -> (int, bool):
popConnectionParameters() popConnectionParameters()
def downloadRelease(url: str, path: str, extra_opts, timeout: int = 10) -> None: def downloadRelease(
"""If file exists at path compare it's size to the content length at the url url_in: str | List[str], path: str, extra_opts, timeout: int = 10
and attempt to resume download if file size is below expected. ) -> None:
""" # If file exists at path compare it's size to the content length at the url
resume_from: int = 0 # and attempt to resume download if file size is below expected.
if os.path.exists(path): release_filename: str = os.path.basename(path)
if extra_opts.get("redownload_releases", False): urls = (
logging.warning(f"Overwriting: {path}") url_in
elif extra_opts.get("verify_release_file_size", True): if isinstance(url_in, list)
file_size = os.stat(path).st_size else [
remote_file_length, can_resume = getRemoteFileLength(url) url_in,
if file_size < remote_file_length: ]
logger.warning( )
f"{path} is an unexpected size, {file_size} < {remote_file_length}. Attempting to resume download." for url in urls:
) try:
if can_resume: resume_from: int = 0
resume_from = file_size 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: else:
logger.warning("Download can not be resumed, restarting.") # File exists and size check is disabled
else: return
return return downloadFile(url, path, timeout, resume_from)
else: except Exception as e:
# File exists and size check is disabled logger.warning(f"Failed to download {release_filename} from {url}")
return logger.debug(f"Download error {e}")
raise RuntimeError(f"Failed to download {release_filename}.")
return downloadFile(url, path, timeout, resume_from)
def downloadFile(url: str, path: str, timeout: int = 5, resume_from: int = 0) -> None: 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, assert_filename,
) )
elif coin == "litecoin": elif coin == "litecoin":
release_url = "https://github.com/litecoin-project/litecoin/releases/download/v{}/{}".format( release_url = [
version + version_tag, release_filename 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( assert_filename = "{}-core-{}-{}-build.assert".format(
coin, os_name, ".".join(version.split(".")[:2]) coin, os_name, ".".join(version.split(".")[:2])
) )
@@ -2106,7 +2124,12 @@ def initialise_wallets(
continue continue
try: try:
ci = swap_client.ci(c) 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) seed_key = swap_client.getWalletKey(c, 1)
account_key = ci.getAccountKey(seed_key, zprv_prefix) account_key = ci.getAccountKey(seed_key, zprv_prefix)
extended_keys[getCoinName(c)] = account_key extended_keys[getCoinName(c)] = account_key
+69 -53
View File
@@ -2,10 +2,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert # 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 # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import copy
import json import json
import logging import logging
import os import os
@@ -22,6 +23,7 @@ from basicswap.chainparams import chainparams, Coins, isKnownCoinName
from basicswap.network.simplex_chat import startSimplexClient from basicswap.network.simplex_chat import startSimplexClient
from basicswap.ui.util import getCoinName from basicswap.ui.util import getCoinName
from basicswap.util.daemon import Daemon from basicswap.util.daemon import Daemon
from typing import Set
initial_logger = logging.getLogger() initial_logger = logging.getLogger()
initial_logger.level = logging.DEBUG initial_logger.level = logging.DEBUG
@@ -347,7 +349,7 @@ def mainLoop(daemons, update: bool = True):
def runClient( def runClient(
data_dir: str, data_dir: str,
chain: str, chain: str,
start_only_coins: bool, start_only_coins: Set[str],
log_prefix: str = "BasicSwap", log_prefix: str = "BasicSwap",
extra_opts=dict(), extra_opts=dict(),
) -> int: ) -> int:
@@ -391,39 +393,46 @@ def runClient(
# Settings may have been modified # Settings may have been modified
settings = swap_client.settings 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:
# Try start daemons # Try start daemons
for network in settings.get("networks", []): if len(start_only_coins) > 0:
if network.get("enabled", True) is False: swap_client.log.warning('Not starting networks as "startonlycoin" is set')
continue else:
network_type: str = network.get("type", "unknown") for network in settings.get("networks", []):
if network_type == "simplex": if network.get("enabled", True) is False:
simplex_dir = os.path.join(data_dir, "simplex") 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" daemons.append(
startSimplexClient(
socks_proxy = None network["client_path"],
if "socks_proxy_override" in network: simplex_dir,
socks_proxy = network["socks_proxy_override"] network["server_address"],
elif swap_client.use_tor_proxy: network["ws_port"],
socks_proxy = ( logger,
f"{swap_client.tor_proxy_host}:{swap_client.tor_proxy_port}" swap_client.delay_event,
socks_proxy=socks_proxy,
log_level=log_level,
)
) )
pid = daemons[-1].handle.pid
daemons.append( swap_client.log.info(f"Started Simplex client {pid}")
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}")
for c, v in settings["chainclients"].items(): for c, v in settings["chainclients"].items():
if len(start_only_coins) > 0 and c not in start_only_coins: 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( trusted_daemon: bool = swap_client.getXMRTrustedDaemon(
coin_id, v["rpchost"] coin_id, v["rpchost"]
) )
opts = [ wallet_opts = [
"--trusted-daemon" if trusted_daemon else "--untrusted-daemon",
"--daemon-address", "--daemon-address",
daemon_addr, 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_log_str = ""
proxy_host, proxy_port = swap_client.getXMRWalletProxy( proxy_host, proxy_port = swap_client.getXMRWalletProxy(
@@ -471,7 +488,7 @@ def runClient(
) )
if proxy_host: if proxy_host:
proxy_log_str = " through proxy" proxy_log_str = " through proxy"
opts += [ wallet_opts += [
"--proxy", "--proxy",
f"{proxy_host}:{proxy_port}", f"{proxy_host}:{proxy_port}",
"--daemon-ssl-allow-any-cert", "--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") filename: str = getWalletBinName(coin_id, v, c + "-wallet-rpc")
daemons.append( daemons.append(
startXmrWalletDaemon(v["datadir"], v["bindir"], filename, opts) startXmrWalletDaemon(
v["datadir"], v["bindir"], filename, wallet_opts
)
) )
pid = daemons[-1].handle.pid pid = daemons[-1].handle.pid
swap_client.log.info(f"Started {filename} {pid}") swap_client.log.info(f"Started {filename} {pid}")
@@ -506,9 +515,8 @@ def runClient(
if c == "decred": if c == "decred":
appdata = v["datadir"] appdata = v["datadir"]
extra_opts = [ coin_opts = copy.deepcopy(base_coin_opts)
f'--appdata="{appdata}"', coin_opts.append(f'--appdata="{appdata}"')
]
use_shell: bool = True if os.name == "nt" else False use_shell: bool = True if os.name == "nt" else False
if v["manage_daemon"] is True: if v["manage_daemon"] is True:
swap_client.log.info(f"Starting {display_name} daemon") swap_client.log.info(f"Starting {display_name} daemon")
@@ -526,7 +534,7 @@ def runClient(
appdata, appdata,
v["bindir"], v["bindir"],
filename, filename,
opts=extra_opts, opts=coin_opts,
extra_config=extra_config, extra_config=extra_config,
) )
) )
@@ -537,12 +545,13 @@ def runClient(
swap_client.log.info(f"Starting {display_name} wallet daemon") swap_client.log.info(f"Starting {display_name} wallet daemon")
filename: str = getWalletBinName(coin_id, v, "dcrwallet") filename: str = getWalletBinName(coin_id, v, "dcrwallet")
wallet_opts = [f'--appdata="{appdata}"']
wallet_pwd = v["wallet_pwd"] wallet_pwd = v["wallet_pwd"]
if wallet_pwd == "": if wallet_pwd == "":
# Only set when in startonlycoin mode # Only set when in startonlycoin mode
wallet_pwd = os.getenv("WALLET_ENCRYPTION_PWD", "") wallet_pwd = os.getenv("WALLET_ENCRYPTION_PWD", "")
if wallet_pwd != "": if wallet_pwd != "":
extra_opts.append(f'--pass="{wallet_pwd}"') wallet_opts.append(f'--pass="{wallet_pwd}"')
extra_config = { extra_config = {
"add_datadir": False, "add_datadir": False,
"stdout_to_file": True, "stdout_to_file": True,
@@ -555,13 +564,12 @@ def runClient(
appdata, appdata,
v["bindir"], v["bindir"],
filename, filename,
opts=extra_opts, opts=wallet_opts,
extra_config=extra_config, extra_config=extra_config,
) )
) )
pid = daemons[-1].handle.pid pid = daemons[-1].handle.pid
swap_client.log.info(f"Started {filename} {pid}") swap_client.log.info(f"Started {filename} {pid}")
continue # /decred continue # /decred
if v["manage_daemon"] is True: if v["manage_daemon"] is True:
@@ -571,7 +579,7 @@ def runClient(
swap_client.log.info(f"Starting {display_name} daemon") swap_client.log.info(f"Starting {display_name} daemon")
filename: str = getCoreBinName(coin_id, v, c + "d") 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 coin_id, v, use_tor_proxy=swap_client.use_tor_proxy
) )
extra_config = {"coin_name": c} extra_config = {"coin_name": c}
@@ -580,7 +588,7 @@ def runClient(
v["datadir"], v["datadir"],
v["bindir"], v["bindir"],
filename, filename,
opts=extra_opts, opts=coin_opts,
extra_config=extra_config, extra_config=extra_config,
) )
) )
@@ -679,6 +687,9 @@ def printHelp():
print( print(
"--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing." "--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("--logprefix Specify log prefix.")
print( print(
"--forcedbupgrade Recheck database against schema regardless of version." "--forcedbupgrade Recheck database against schema regardless of version."
@@ -743,6 +754,11 @@ def main():
ensure_coin_valid(coin) ensure_coin_valid(coin)
start_only_coins.add(coin) start_only_coins.add(coin)
continue 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}") logger.warning(f"Unknown argument {v}")
+12 -2
View File
@@ -552,16 +552,26 @@ chainparams = {
name_map = {} name_map = {}
ticker_map = {} ticker_map = {}
variant_ticker_map = {}
for c, params in chainparams.items(): for c, params in chainparams.items():
name_map[params["name"].lower()] = c name_map[params["name"].lower()] = c
ticker_map[params["ticker"].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: 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: except Exception:
raise ValueError(f"Unknown coin {ticker}") 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): 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 return
self.log.info( 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 # 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 # Wallet name is "" for some LTC and PART installs on older cores
if self._rpc_wallet not in wallets and len(wallets) > 0: if self._rpc_wallet not in wallets and len(wallets) > 0:
if "" in wallets: if "" in wallets:
# Setting wallet= in the coin .conf file should also work
self._log.warning( self._log.warning(
f"Nameless {self.ticker()} wallet found." f"Nameless {self.ticker()} wallet found."
+ '\nPlease set the "wallet_name" coin setting to "" or recreate the wallet' + '\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: if self._rpc_wallet not in wallets:
raise RuntimeError( raise RuntimeError(
@@ -939,32 +939,26 @@ class BTCInterface(Secp256k1Interface):
if wm: if wm:
info = wm.getAddressInfo(self.coin_type(), address) info = wm.getAddressInfo(self.coin_type(), address)
if info: if info:
if or_watch_only: if or_watch_only is False and info["is_watch_only"] is True:
return True return False
return True return True
return False return False
try: try:
addr_info = self.rpc_wallet("getaddressinfo", [address]) addr_info = self.rpc_wallet("getaddressinfo", [address])
if not or_watch_only: if addr_info["ismine"]:
if addr_info["ismine"]: return True
return True if or_watch_only is False:
else: return False
if self._use_descriptors: if addr_info["iswatchonly"]:
addr_info = self.rpc_wallet_watch("getaddressinfo", [address]) return True
if addr_info["ismine"] or addr_info["iswatchonly"]: if self._use_descriptors:
wo_addr_info = self.rpc_wallet_watch("getaddressinfo", [address])
if wo_addr_info["iswatchonly"]:
return True return True
except Exception as e: except Exception as e:
self._log.debug(f"isAddressMine RPC check failed: {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 return False
def checkAddressMine(self, address: str) -> None: def checkAddressMine(self, address: str) -> None:
@@ -1083,8 +1077,8 @@ class BTCInterface(Secp256k1Interface):
return self.encode_p2wsh(script) return self.encode_p2wsh(script)
def getDestForAddress(self, address: str) -> bytes: def getDestForAddress(self, address: str) -> bytes:
bech32_prefix = self.chainparams_network()["hrp"] bech32_prefix: str | None = self.chainparams_network().get("hrp", None)
if address.startswith(bech32_prefix + "1"): if bech32_prefix and address.startswith(bech32_prefix + "1"):
_, witprog = segwit_addr.decode(bech32_prefix, address) _, witprog = segwit_addr.decode(bech32_prefix, address)
return CScript([OP_0, bytes(witprog)]) return CScript([OP_0, bytes(witprog)])
@@ -3099,7 +3093,10 @@ class BTCInterface(Secp256k1Interface):
} }
except Exception as e: except Exception as e:
error_msg = str(e).lower() 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( self._log.debug(
f"checkWatchedOutput exception for {txid_hex}:{vout}: {e}" 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_host = proxy_host
self._proxy_port = proxy_port 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): def connect(self):
try: 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 import socks
sock = socks.socksocket() sock = socks.socksocket()
@@ -101,6 +116,10 @@ class ElectrumConnection:
sock = socket.create_connection( sock = socket.create_connection(
(self._host, self._port), timeout=self._timeout (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: if self._use_ssl:
context = ssl.create_default_context() context = ssl.create_default_context()
context.check_hostname = False context.check_hostname = False
@@ -546,11 +565,6 @@ class ElectrumServer:
elif isinstance(srv, dict): elif isinstance(srv, dict):
user_onion.append(srv) user_onion.append(srv)
final_clearnet = (
user_clearnet
if user_clearnet
else DEFAULT_ELECTRUM_SERVERS.get(coin_name, [])
)
final_onion = ( final_onion = (
user_onion if user_onion else DEFAULT_ONION_SERVERS.get(coin_name, []) 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 self._using_default_servers = not user_clearnet and not user_onion
if use_tor: 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) self._servers = list(final_onion) + list(final_clearnet)
if self._log and final_onion: if self._log:
self._log.info( self._log.info(
f"ElectrumServer {coin_name}: TOR enabled - " f"ElectrumServer {coin_name}: TOR enabled - "
f"{len(final_onion)} .onion + {len(final_clearnet)} clearnet servers" f"{len(final_onion)} .onion + {len(final_clearnet)} clearnet servers"
) )
else: else:
final_clearnet = (
user_clearnet
if user_clearnet
else DEFAULT_ELECTRUM_SERVERS.get(coin_name, [])
)
self._servers = list(final_clearnet) self._servers = list(final_clearnet)
if self._log: if self._log:
self._log.info( self._log.info(
@@ -983,55 +1010,84 @@ class ElectrumServer:
def call_background(self, method, params=None, timeout=20): def call_background(self, method, params=None, timeout=20):
if self._stopping: if self._stopping:
raise TemporaryError("Electrum server is shutting down") raise TemporaryError("Electrum server is shutting down")
conn = self._connection lock_acquired = self._lock.acquire(timeout=timeout + 5)
if conn is None or not conn.is_connected(): if not lock_acquired:
if self._stopping: raise TemporaryError(
raise TemporaryError("Electrum server is shutting down") f"Electrum background call timed out waiting for lock: {method}"
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")
try: try:
result = conn.call(method, params, timeout=timeout) for attempt in range(2):
self._last_activity = time.time() if self._stopping:
return result raise TemporaryError("Electrum server is shutting down")
except TemporaryError as e: if self._connection is None or not self._connection.is_connected():
if self._stopping: self.connect()
raise TemporaryError("Electrum server is shutting down") if self._connection is None:
if "timed out" in str(e).lower(): raise TemporaryError("Electrum call failed: no connection")
self._record_timeout() try:
raise 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): def call_batch_background(self, requests, timeout=30):
if self._stopping: if self._stopping:
raise TemporaryError("Electrum server is shutting down") raise TemporaryError("Electrum server is shutting down")
conn = self._connection lock_acquired = self._lock.acquire(timeout=timeout + 5)
if conn is None or not conn.is_connected(): if not lock_acquired:
if self._stopping: raise TemporaryError(
raise TemporaryError("Electrum server is shutting down") "Electrum background batch call timed out waiting for lock"
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")
try: try:
result = conn.call_batch(requests) for attempt in range(2):
self._last_activity = time.time() if self._stopping:
return result raise TemporaryError("Electrum server is shutting down")
except TemporaryError as e: if self._connection is None or not self._connection.is_connected():
if self._stopping: self.connect()
raise TemporaryError("Electrum server is shutting down") if self._connection is None:
if "timed out" in str(e).lower(): raise TemporaryError(
self._record_timeout() "Electrum batch call failed: no connection"
raise )
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): def call_user(self, method, params=None, timeout=10):
if self._stopping: if self._stopping:
+1 -1
View File
@@ -361,7 +361,7 @@ class FIROInterface(BTCInterface):
) )
return pay_fee 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) key_wif = self.encodeKey(key)
rv = self.rpc( rv = self.rpc(
"signrawtransaction", "signrawtransaction",
+86 -107
View File
@@ -26,87 +26,6 @@ class LTCInterface(BTCInterface):
wallet=self._rpc_wallet_mweb, 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: def getNewMwebAddress(self, use_segwit=False, label="swap_receive") -> str:
if self.useBackend(): if self.useBackend():
raise ValueError("MWEB addresses not supported in electrum mode") raise ValueError("MWEB addresses not supported in electrum mode")
@@ -172,6 +91,11 @@ class LTCInterface(BTCInterface):
continue continue
if "address" not in u: if "address" not in u:
continue continue
utxo_address: str = u["address"]
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
continue
if "desc" in u: if "desc" in u:
desc = u["desc"] desc = u["desc"]
if self.using_segwit: if self.using_segwit:
@@ -184,11 +108,81 @@ class LTCInterface(BTCInterface):
else: else:
if not desc.startswith("pkh"): if not desc.startswith("pkh"):
continue continue
unspent_addr[u["address"]] = unspent_addr.get( unspent_addr[utxo_address] = unspent_addr.get(
u["address"], 0 utxo_address, 0
) + self.make_int(u["amount"], r=1) ) + self.make_int(u["amount"], r=1)
return unspent_addr 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: def unlockWallet(self, password: str, check_seed: bool = True) -> None:
if password == "": if password == "":
return return
@@ -306,23 +300,24 @@ class LTCInterfaceMWEB(LTCInterface):
def init_wallet(self, password=None): def init_wallet(self, password=None):
# If system is encrypted mweb wallet will be created at first unlock # 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") wallets = self.rpc("listwallets")
if self._rpc_wallet not in wallets: if wallet_name not in wallets:
try: try:
self.rpc("loadwallet", [self._rpc_wallet]) self.rpc("loadwallet", [wallet_name])
self._log.debug(f'Loaded existing wallet "{self._rpc_wallet}".') self._log.debug(f'Loaded existing wallet "{wallet_name}".')
except Exception as e: except Exception as e:
if "does not exist" in str(e) or "Path does not exist" in str(e): if "does not exist" in str(e) or "Path does not exist" in str(e):
self._log.info( 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 # wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
self.rpc( self.rpc(
"createwallet", "createwallet",
[ [
self._rpc_wallet, wallet_name,
False, False,
True, True,
password, password,
@@ -333,22 +328,6 @@ class LTCInterfaceMWEB(LTCInterface):
else: else:
raise 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: if password is not None:
# Max timeout value, ~3 years # Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120) self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
@@ -357,8 +336,8 @@ class LTCInterfaceMWEB(LTCInterface):
self._sc.initialiseWallet(self.interface_type()) self._sc.initialiseWallet(self.interface_type())
# Workaround to trigger mweb_spk_man->LoadMWEBKeychain() # Workaround to trigger mweb_spk_man->LoadMWEBKeychain()
self.rpc("unloadwallet", ["mweb"]) self.rpc("unloadwallet", [wallet_name])
self.rpc("loadwallet", ["mweb"]) self.rpc("loadwallet", [wallet_name])
if password is not None: if password is not None:
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120) self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
self.rpc_wallet("keypoolrefill") 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.rpc import make_rpc_func
from basicswap.chainparams import Coins from basicswap.chainparams import Coins
from basicswap.util.address import decodeAddress 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 ( from basicswap.contrib.test_framework.script import (
CScript, CScript,
OP_DUP, OP_DUP,
@@ -100,29 +100,13 @@ class PIVXInterface(BTCInterface):
return decodeAddress(address)[1:] return decodeAddress(address)[1:]
def getBlockWithTxns(self, block_hash): def getBlockWithTxns(self, block_hash):
# TODO: Bypass decoderawtransaction and getblockheader block = self.rpc("getblock", [block_hash, True])
block = self.rpc("getblock", [block_hash, False])
block_header = self.rpc("getblockheader", [block_hash])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
tx_rv = [] tx_rv = []
for tx in decoded_block.vtx: for txid_str in block["tx"]:
tx_dec = self.rpc("decoderawtransaction", [ToHex(tx)]) tx_dec = self.rpc("getrawtransaction", [txid_str, True])
tx_rv.append(tx_dec) tx_rv.append(tx_dec)
block["tx"] = tx_rv
block_rv = { return block
"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
def withdrawCoin(self, value, addr_to, subfee): def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, "", "", subfee] params = [addr_to, value, "", "", subfee]
@@ -150,7 +134,7 @@ class PIVXInterface(BTCInterface):
) )
return pay_fee 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) key_wif = self.encodeKey(key)
rv = self.rpc( rv = self.rpc(
"signrawtransaction", "signrawtransaction",
+1
View File
@@ -228,6 +228,7 @@ class XMRInterface(CoinInterface):
"invalid signature", "invalid signature",
"std::bad_alloc", "std::bad_alloc",
"basic_string::_M_replace_aux", "basic_string::_M_replace_aux",
"input stream error",
) )
): ):
self._log.error(f"{self.coin_name()} wallet is corrupt.") self._log.error(f"{self.coin_name()} wallet is corrupt.")
+40 -4
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # 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 # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # 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 swap_client = self.server.swap_client
try: try:
swap_client.updateWalletsInfo() swap_client.updateWalletsInfo()
wallets = swap_client.getCachedWalletsInfo() wallets = swap_client.getCachedWalletsInfo()
coins_with_balances = [] 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_synced"] = sync_status.get("synced", False)
coin_entry["electrum_height"] = sync_status.get("height", 0) 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) coins_with_balances.append(coin_entry)
if k == Coins.PART: if k == Coins.PART:
@@ -290,7 +311,7 @@ def js_wallets(self, url_split, post_string, is_json):
swap_client.checkSystemStatus() swap_client.checkSystemStatus()
if len(url_split) > 3: if len(url_split) > 3:
ticker_str = 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: if len(url_split) > 4:
cmd = url_split[4] cmd = url_split[4]
@@ -332,6 +353,18 @@ def js_wallets(self, url_split, post_string, is_json):
return bytes( return bytes(
json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8" 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": elif cmd == "watchaddress":
post_data = getFormData(post_string, is_json) post_data = getFormData(post_string, is_json)
address = get_data_entry(post_data, "address") 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 or (current_time - cache_entry["time"]) > TX_CACHE_DURATION
): ):
all_txs = ci.listWalletTransactions(count=10000, skip=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 = []
swap_client._tx_cache[coin_id] = {"txs": all_txs, "time": current_time} swap_client._tx_cache[coin_id] = {"txs": all_txs, "time": current_time}
else: else:
all_txs = cache_entry["txs"] 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() { document.addEventListener('DOMContentLoaded', function() {
const burger = document.querySelectorAll('.navbar-burger'); const burger = document.querySelectorAll('.navbar-burger');
const menu = document.querySelectorAll('.navbar-menu'); const menu = document.querySelectorAll('.navbar-menu');
+17 -2
View File
@@ -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() { confirmReseed: function() {
return confirm('Are you sure you want to reseed the wallet? This will generate new addresses.'); return confirm('Are you sure you want to reseed the wallet? This will generate new addresses.');
}, },
@@ -60,7 +64,7 @@
}, },
fillDonationAddress: function(address, coinType) { fillDonationAddress: function(address, coinType) {
let addressInput = null; let addressInput = null;
addressInput = window.DOMCache addressInput = window.DOMCache
@@ -188,7 +192,7 @@
}, },
lookup_rates: function() { lookup_rates: function() {
if (window.lookup_rates && typeof window.lookup_rates === 'function') { if (window.lookup_rates && typeof window.lookup_rates === 'function') {
window.lookup_rates(); window.lookup_rates();
} else { } else {
@@ -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) => { document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm-utxo]'); const target = e.target.closest('[data-confirm-utxo]');
if (target) { if (target) {
@@ -398,6 +412,7 @@
window.EventHandlers = EventHandlers; window.EventHandlers = EventHandlers;
window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers); window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers);
window.confirmMWEBChangeConvert = EventHandlers.confirmMWEBChangeConvert.bind(EventHandlers);
window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers); window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers);
window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers); window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers);
window.confirmRemoveExpired = EventHandlers.confirmRemoveExpired.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 11: { phase: 'locking', order: 10, label: 'Locking' }, // XMR_SWAP_NOSCRIPT_COIN_LOCKED
12: { phase: 'redemption', order: 11, label: 'Redemption' }, // XMR_SWAP_LOCK_RELEASED 12: { phase: 'redemption', order: 11, label: 'Redemption' }, // XMR_SWAP_LOCK_RELEASED
13: { phase: 'redemption', order: 12, label: 'Redemption' }, // XMR_SWAP_SCRIPT_TX_REDEEMED 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 15: { phase: 'redemption', order: 13, label: 'Redemption' }, // XMR_SWAP_NOSCRIPT_TX_REDEEMED
16: { phase: 'failed', order: 91, label: 'Recovered' }, // XMR_SWAP_NOSCRIPT_TX_RECOVERED 16: { phase: 'failed', order: 91, label: 'Recovered' }, // XMR_SWAP_NOSCRIPT_TX_RECOVERED
17: { phase: 'failed', order: 92, label: 'Failed' }, // XMR_SWAP_FAILED_REFUNDED 17: { phase: 'failed', order: 92, label: 'Failed' }, // XMR_SWAP_FAILED_REFUNDED
+10 -7
View File
@@ -351,16 +351,19 @@
); );
matchingCoins.forEach(coinData => { 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 => { balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname'); const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinData.name) { if (elementCoinName === coinData.name) {
const currentText = element.textContent; const balanceType = element.getAttribute('data-balance-type');
const ticker = coinData.ticker || coinId.toUpperCase(); const value = coinData[balanceType];
const newBalance = `${coinData.balance} ${ticker}`; if (value !== undefined) {
if (currentText !== newBalance) { const ticker = coinData.ticker || coinId.toUpperCase();
element.textContent = newBalance; const newBalance = balanceType === 'est_fee' ? value : `${value} ${ticker}`;
console.log(`Updated balance: ${coinData.name} -> ${newBalance}`); 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) { if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingBalance('Particl', 'Blind Balance:', coinData.pending, coinData.ticker || 'PART', 'Blind Unconfirmed:', coinData); 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 { } else {
this.updateSpecificBalance(coinData.name, 'Balance:', coinData.balance, coinData.ticker || coinData.name); 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.name !== 'Particl Anon' && coinData.name !== 'Particl Blind' && coinData.name !== 'Litecoin MWEB') {
if (coinData.pending && parseFloat(coinData.pending) > 0) { if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingDisplay(coinData); this.updatePendingDisplay(coinData);
+1 -1
View File
@@ -26,7 +26,7 @@
<div class="flex items-center"> <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-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">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> <p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
{{ love_svg | safe }} {{ love_svg | safe }}
</div> </div>
+27 -15
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"> <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="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"> <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>) (<span class="usd-value"></span>)
{% if w.pending %} {% 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> <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"> <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="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"> <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>) (<span class="usd-value"></span>)
{% if w.blind_unconfirmed %} {% 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> <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"> <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="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"> <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>) (<span class="usd-value"></span>)
{% if w.anon_pending %} {% 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> <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 %} {% if is_electrum_mode %}
<span class="text-gray-400 dark:text-gray-300">Not available in light mode</span> <span class="text-gray-400 dark:text-gray-300">Not available in light mode</span>
{% else %} {% 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>) (<span class="usd-value"></span>)
{% if w.mweb_pending %} {% 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> <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 %} {% endif %}
</td> </td>
</tr> </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 #} {% 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"> <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="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"> <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>) (<span class="usd-value"></span>)
{% if w.spark_pending %} {% 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> <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"> <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="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"> <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>) (<span class="usd-value"></span>)
{% if w.spark_pending %} {% 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> <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> <td class="py-3 px-6">{{ w.expected_seed }}</td>
</tr> </tr>
{% endif %} {% endif %}
</table> </table>
</div> </div>
</div> </div>
@@ -463,7 +475,7 @@
</div> </div>
<div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">MWEB Address: </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="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> <span class="absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer" id="copyIcon"></span>
</div> </div>
<div class="opacity-100 text-gray-500 dark:text-gray-100 flex justify-center items-center"> <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"> <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-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"> <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>) (<span class="usd-value"></span>)
</td> </td>
</tr> </tr>
@@ -547,7 +559,7 @@
{% if is_electrum_mode %} {% if is_electrum_mode %}
<span class="text-gray-400 dark:text-gray-300">Not available in light mode</span> <span class="text-gray-400 dark:text-gray-300">Not available in light mode</span>
{% else %} {% 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>) (<span class="usd-value"></span>)
{% endif %} {% endif %}
</td> </td>
@@ -557,7 +569,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <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-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"> <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>) (<span class="usd-value"></span>)
</td> </td>
</tr> </tr>
@@ -566,7 +578,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <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-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"> <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>) (<span class="usd-value"></span>)
</td> </td>
</tr> </tr>
@@ -575,14 +587,14 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <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-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"> <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>) (<span class="usd-value"></span>)
</td> </td>
</tr> </tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <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-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"> <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>) (<span class="usd-value"></span>)
</td> </td>
</tr> </tr>
@@ -757,7 +769,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <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 bold">Fee Estimate:</td>
<td class="py-3 px-6"> <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>) (<span class="usd-value fee-estimate-usd" data-decimals="8"></span>)
</td> </td>
</tr> </tr>
@@ -974,7 +986,7 @@
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4" id="confirmTitle">Confirm Action</h2> <h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4" id="confirmTitle">Confirm Action</h2>
<p class="text-gray-600 dark:text-gray-200 mb-6 whitespace-pre-line" id="confirmMessage">Are you sure?</p> <p class="text-gray-600 dark:text-gray-200 mb-6 whitespace-pre-line" id="confirmMessage">Are you sure?</p>
<div class="flex justify-center gap-4"> <div class="flex justify-center gap-4">
<button type="button" id="confirmYes" <button type="button" id="confirmYes"
class="px-4 py-2.5 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"> class="px-4 py-2.5 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">
Confirm Confirm
</button> </button>
+5 -5
View File
@@ -65,7 +65,7 @@
<div class="p-6 bg-coolGray-100 dark:bg-gray-600"> <div class="p-6 bg-coolGray-100 dark:bg-gray-600">
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Balance:</h4> <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>
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white ">{{ w.ticker }} USD value:</h4> <h4 class="text-xs font-medium dark:text-white ">{{ w.ticker }} USD value:</h4>
@@ -90,7 +90,7 @@
{% if w.cid == '1' %} {# PART #} {% if w.cid == '1' %} {# PART #}
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Blind Balance:</h4> <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>
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Blind USD value:</h4> <h4 class="text-xs font-medium dark:text-white">Blind USD value:</h4>
@@ -108,7 +108,7 @@
{% endif %} {% endif %}
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Anon Balance:</h4> <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>
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Anon USD value:</h4> <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 #} {% 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"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">MWEB Balance:</h4> <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>
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">MWEB USD value:</h4> <h4 class="text-xs font-medium dark:text-white">MWEB USD value:</h4>
@@ -151,7 +151,7 @@
{% if w.cid == '13' %} {# FIRO #} {% if w.cid == '13' %} {# FIRO #}
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Spark Balance:</h4> <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>
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Spark USD value:</h4> <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) swap_client.cacheNewAddressForCoin(coin_id)
elif have_data_entry(form_data, "forcerefresh"): elif have_data_entry(form_data, "forcerefresh"):
force_refresh = True 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): elif have_data_entry(form_data, "newmwebaddr_" + cid):
swap_client.cacheNewStealthAddressForCoin(coin_id) swap_client.cacheNewStealthAddressForCoin(coin_id)
elif have_data_entry(form_data, "newsparkaddr_" + cid): 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" 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) fee_rate, fee_src = swap_client.getFeeRateForCoin(k)
est_fee = swap_client.estimateWithdrawFee(k, fee_rate) est_fee = swap_client.estimateWithdrawFee(k, fee_rate)
wallet_data["fee_rate"] = ci.format_amount(int(fee_rate * ci.COIN())) 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"] // 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: if show_utxo_groups:
utxo_groups = "" utxo_groups = ""
unspent_by_addr = ci.getUnspentsByAddr() unspent_by_addr = ci.getUnspentsByAddr()
@@ -559,7 +575,10 @@ def page_wallet(self, url_split, post_string):
skip = tx_filters.get("offset", 0) skip = tx_filters.get("offset", 0)
all_txs = ci.listWalletTransactions(count=10000, skip=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) total_transactions = len(all_txs)
raw_txs = all_txs[skip : skip + count] if all_txs else [] 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() now = time.time()
stale_threshold = 300 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 { return {
"height": height, "height": height,
"synced": is_synced, "synced": is_synced,
+35 -49
View File
@@ -4,10 +4,12 @@
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import hashlib import json
import sqlite3
import threading import threading
import time import time
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
from coincurve import PrivateKey, PublicKey
from .chainparams import Coins from .chainparams import Coins
from .contrib.test_framework import segwit_addr from .contrib.test_framework import segwit_addr
@@ -19,7 +21,7 @@ from .db_wallet import (
WalletTxCache, WalletTxCache,
WalletWatchOnly, WalletWatchOnly,
) )
from .util.crypto import hash160 from .util.crypto import hash160, sha256
from .util.extkey import ExtKeyPair from .util.extkey import ExtKeyPair
@@ -38,6 +40,7 @@ class WalletManager:
} }
GAP_LIMIT = 50 GAP_LIMIT = 50
ELECTRUM_GAP_LIMIT = 20
def __init__(self, swap_client, log): def __init__(self, swap_client, log):
self._gap_limits: Dict[Coins, int] = {} self._gap_limits: Dict[Coins, int] = {}
@@ -111,13 +114,11 @@ class WalletManager:
def _deriveAddress( def _deriveAddress(
self, coin_type: Coins, index: int, internal: bool = False self, coin_type: Coins, index: int, internal: bool = False
) -> Tuple[str, str, bytes]: ) -> Tuple[str, str, bytes]:
from coincurve import PublicKey
key = self._deriveKey(coin_type, index, internal) key = self._deriveKey(coin_type, index, internal)
pubkey = PublicKey.from_secret(key).format() pubkey = PublicKey.from_secret(key).format()
pkh = hash160(pubkey) pkh = hash160(pubkey)
address = segwit_addr.encode(self._getHRP(coin_type), 0, pkh) 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 return address, scripthash, pubkey
def _syncStateIndices(self, coin_type: Coins, cursor) -> None: def _syncStateIndices(self, coin_type: Coins, cursor) -> None:
@@ -149,6 +150,18 @@ class WalletManager:
) )
self._swap_client.commitDB() 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( def getNewAddress(
self, coin_type: Coins, internal: bool = False, label: str = "", cursor=None self, coin_type: Coins, internal: bool = False, label: str = "", cursor=None
) -> str: ) -> str:
@@ -157,8 +170,6 @@ class WalletManager:
use_cursor = self._swap_client.openDB(cursor) use_cursor = self._swap_client.openDB(cursor)
try: try:
self._syncStateIndices(coin_type, use_cursor)
state = self._swap_client.queryOne( state = self._swap_client.queryOne(
WalletState, use_cursor, {"coin_type": int(coin_type)} WalletState, use_cursor, {"coin_type": int(coin_type)}
) )
@@ -184,6 +195,19 @@ class WalletManager:
else: else:
next_index = (state.last_external_index or 0) + 1 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( existing = self._swap_client.queryOne(
WalletAddress, WalletAddress,
use_cursor, use_cursor,
@@ -251,8 +275,6 @@ class WalletManager:
def getAddress( def getAddress(
self, coin_type: Coins, index: int, internal: bool = False self, coin_type: Coins, index: int, internal: bool = False
) -> Optional[str]: ) -> Optional[str]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -371,8 +393,6 @@ class WalletManager:
include_watch_only: bool = True, include_watch_only: bool = True,
funded_only: bool = False, funded_only: bool = False,
) -> List[str]: ) -> List[str]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -402,8 +422,6 @@ class WalletManager:
return [] return []
def getFundedAddresses(self, coin_type: Coins) -> Dict[str, str]: def getFundedAddresses(self, coin_type: Coins) -> Dict[str, str]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -428,8 +446,6 @@ class WalletManager:
return {} return {}
def getExistingInternalAddress(self, coin_type: Coins) -> Optional[str]: def getExistingInternalAddress(self, coin_type: Coins) -> Optional[str]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -493,8 +509,6 @@ class WalletManager:
self._swap_client.closeDB(cursor, commit=False) self._swap_client.closeDB(cursor, commit=False)
def getAddressInfo(self, coin_type: Coins, address: str) -> Optional[dict]: def getAddressInfo(self, coin_type: Coins, address: str) -> Optional[dict]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -535,8 +549,6 @@ class WalletManager:
return None return None
def getCachedTotalBalance(self, coin_type: Coins) -> int: def getCachedTotalBalance(self, coin_type: Coins) -> int:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -676,8 +688,6 @@ class WalletManager:
if not self.isInitialized(coin_type): if not self.isInitialized(coin_type):
return None return None
import sqlite3
now = int(time.time()) now = int(time.time())
min_cache_time = now - max_cache_age min_cache_time = now - max_cache_age
@@ -720,8 +730,6 @@ class WalletManager:
return None return None
def hasCachedBalances(self, coin_type: Coins, max_cache_age: int = 120) -> bool: def hasCachedBalances(self, coin_type: Coins, max_cache_age: int = 120) -> bool:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -738,8 +746,6 @@ class WalletManager:
def getPrivateKey(self, coin_type: Coins, address: str) -> Optional[bytes]: def getPrivateKey(self, coin_type: Coins, address: str) -> Optional[bytes]:
if not self.isInitialized(coin_type): if not self.isInitialized(coin_type):
return None return None
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -764,8 +770,6 @@ class WalletManager:
return None return None
def getSignableAddresses(self, coin_type: Coins) -> Dict[str, str]: def getSignableAddresses(self, coin_type: Coins) -> Dict[str, str]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -837,10 +841,8 @@ class WalletManager:
label: str = "", label: str = "",
source: str = "import", source: str = "import",
) -> bool: ) -> bool:
from coincurve import PublicKey as CCPublicKey
try: try:
pubkey = CCPublicKey.from_secret(private_key).format() pubkey = PublicKey.from_secret(private_key).format()
if ( if (
segwit_addr.encode(self._getHRP(coin_type), 0, hash160(pubkey)) segwit_addr.encode(self._getHRP(coin_type), 0, hash160(pubkey))
!= address != address
@@ -979,7 +981,7 @@ class WalletManager:
def _b58decode_check(self, s: str) -> bytes: def _b58decode_check(self, s: str) -> bytes:
data = self._b58decode(s) data = self._b58decode(s)
payload, checksum = data[:-4], data[-4:] payload, checksum = data[:-4], data[-4:]
expected = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4] expected = sha256(sha256(payload))[:4]
if checksum != expected: if checksum != expected:
raise ValueError("Invalid base58 checksum") raise ValueError("Invalid base58 checksum")
return payload return payload
@@ -1001,7 +1003,7 @@ class WalletManager:
master_key = self._master_keys.get(coin_type) master_key = self._master_keys.get(coin_type)
if master_key is None: if master_key is None:
raise ValueError(f"Wallet not initialized for {coin_type}") 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: 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))) 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) _, data = segwit_addr.decode(self._getHRP(coin_type), address)
if data is None: if data is None:
return "" 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: def needsMigration(self, coin_type: Coins) -> bool:
cursor = self._swap_client.openDB() cursor = self._swap_client.openDB()
@@ -1169,8 +1171,6 @@ class WalletManager:
self._swap_client.closeDB(cursor, commit=False) self._swap_client.closeDB(cursor, commit=False)
def getSeedID(self, coin_type: Coins) -> Optional[str]: def getSeedID(self, coin_type: Coins) -> Optional[str]:
from basicswap.contrib.test_framework.script import hash160
master_key = self._master_keys.get(coin_type) master_key = self._master_keys.get(coin_type)
if master_key is None: if master_key is None:
return None return None
@@ -1180,16 +1180,12 @@ class WalletManager:
return hash160(ek.encode_p()).hex() return hash160(ek.encode_p()).hex()
def signMessage(self, coin_type: Coins, address: str, message: str) -> bytes: def signMessage(self, coin_type: Coins, address: str, message: str) -> bytes:
from coincurve import PrivateKey
key = self.getPrivateKey(coin_type, address) key = self.getPrivateKey(coin_type, address)
if key is None: if key is None:
raise ValueError(f"Cannot sign: no key for address {address}") raise ValueError(f"Cannot sign: no key for address {address}")
return PrivateKey(key).sign(message.encode("utf-8")) return PrivateKey(key).sign(message.encode("utf-8"))
def signHash(self, coin_type: Coins, address: str, msg_hash: bytes) -> bytes: def signHash(self, coin_type: Coins, address: str, msg_hash: bytes) -> bytes:
from coincurve import PrivateKey
key = self.getPrivateKey(coin_type, address) key = self.getPrivateKey(coin_type, address)
if key is None: if key is None:
raise ValueError(f"Cannot sign: no key for address {address}") raise ValueError(f"Cannot sign: no key for address {address}")
@@ -1198,8 +1194,6 @@ class WalletManager:
def getKeyForAddress( def getKeyForAddress(
self, coin_type: Coins, address: str self, coin_type: Coins, address: str
) -> Optional[Tuple[bytes, bytes]]: ) -> Optional[Tuple[bytes, bytes]]:
from coincurve import PublicKey
key = self.getPrivateKey(coin_type, address) key = self.getPrivateKey(coin_type, address)
if key is None: if key is None:
return None return None
@@ -1208,8 +1202,6 @@ class WalletManager:
def findAddressByScripthash( def findAddressByScripthash(
self, coin_type: Coins, scripthash: str self, coin_type: Coins, scripthash: str
) -> Optional[str]: ) -> Optional[str]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -1232,8 +1224,6 @@ class WalletManager:
return None return None
def getAllScripthashes(self, coin_type: Coins) -> List[str]: def getAllScripthashes(self, coin_type: Coins) -> List[str]:
import sqlite3
try: try:
conn = sqlite3.connect(self._swap_client.sqlite_file) conn = sqlite3.connect(self._swap_client.sqlite_file)
cursor = conn.cursor() cursor = conn.cursor()
@@ -1871,8 +1861,6 @@ class WalletManager:
for _ in existing: for _ in existing:
return False return False
import json
pending = WalletPendingTx() pending = WalletPendingTx()
pending.coin_type = int(coin_type) pending.coin_type = int(coin_type)
pending.txid = txid pending.txid = txid
@@ -1921,8 +1909,6 @@ class WalletManager:
) -> List[dict]: ) -> List[dict]:
cursor = self._swap_client.openDB() cursor = self._swap_client.openDB()
try: try:
import json
results = self._swap_client.query( results = self._swap_client.query(
WalletPendingTx, WalletPendingTx,
cursor, 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 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 ## Start a subset of the configured coins using docker
docker compose run --rm --service-ports swapclient basicswap-run -datadir=/coindata -withcoins=monero docker compose run --rm --service-ports swapclient basicswap-run -datadir=/coindata -withcoins=monero
+3 -3
View File
@@ -135,15 +135,15 @@
(define-public basicswap (define-public basicswap
(package (package
(name "basicswap") (name "basicswap")
(version "0.15.2") (version "0.16.2")
(source (origin (source (origin
(method git-fetch) (method git-fetch)
(uri (git-reference (uri (git-reference
(url "https://github.com/basicswap/basicswap") (url "https://github.com/basicswap/basicswap")
(commit "83807d213fab52c99f69dbc06fa7baedb449d66f"))) (commit "3b76adeedbbf3586308b6bf8ba401cec899f8a61")))
(sha256 (sha256
(base32 (base32
"08ykwn2wbcny5k6kwj3xkfkim40kmzcb988lpcd70r7kcmn8ggp0")) "0wvy1c6li45ivfnn93iry047fz7w088mcp5rbg07qnnbnb6lgqiz"))
(file-name (git-file-name name version)))) (file-name (git-file-name name version))))
(build-system pyproject-build-system) (build-system pyproject-build-system)
+19 -2
View File
@@ -182,6 +182,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
"datadir": node_dir, "datadir": node_dir,
"bindir": cfg.PARTICL_BINDIR, "bindir": cfg.PARTICL_BINDIR,
"blocks_confirmed": 2, # Faster testing "blocks_confirmed": 2, # Faster testing
"wallet_name": "bsx_wallet",
}, },
"pivx": { "pivx": {
"connection_type": "rpc", "connection_type": "rpc",
@@ -191,6 +192,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
"bindir": PIVX_BINDIR, "bindir": PIVX_BINDIR,
"use_csv": False, "use_csv": False,
"use_segwit": False, "use_segwit": False,
"wallet_name": "",
}, },
"bitcoin": { "bitcoin": {
"connection_type": "rpc", "connection_type": "rpc",
@@ -199,6 +201,7 @@ def prepareDir(datadir, nodeId, network_key, network_pubkey):
"datadir": btcdatadir, "datadir": btcdatadir,
"bindir": cfg.BITCOIN_BINDIR, "bindir": cfg.BITCOIN_BINDIR,
"use_segwit": True, "use_segwit": True,
"wallet_name": "bsx_wallet",
}, },
}, },
"check_progress_seconds": 2, "check_progress_seconds": 2,
@@ -760,7 +763,17 @@ class Test(unittest.TestCase):
rtx = pivxRpc(f'getrawtransaction "{txid}" true') rtx = pivxRpc(f'getrawtransaction "{txid}" true')
assert rtx["version"] == 3 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) ci = self.swap_clients[0].ci(Coins.PIVX)
block = ci.getBlockWithTxns(block_hash) block = ci.getBlockWithTxns(block_hash)
@@ -837,7 +850,11 @@ class Test(unittest.TestCase):
swap_value = ci_from.make_int(swap_value) swap_value = ci_from.make_int(swap_value)
assert swap_value > ci_from.make_int(9) 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()) itx_decoded = ci_from.describeTx(itx.hex())
n = pi.findMockVout(ci_from, itx_decoded) n = pi.findMockVout(ci_from, itx_decoded)
@@ -15,14 +15,15 @@ export XMR_RPC_USER=xmr_user
export XMR_RPC_PWD=xmr_pwd export XMR_RPC_PWD=xmr_pwd
python tests/basicswap/extended/test_xmr_persistent.py python tests/basicswap/extended/test_xmr_persistent.py
# Copy coin releases to permanent storage for faster subsequent startups # Copy coin releases to permanent storage for faster subsequent startups
cp -r ${TEST_PATH}/bin/* ~/tmp/basicswap_bin/ cp -r ${TEST_PATH}/bin/* ~/tmp/basicswap_bin/
# Continue existing chains with # Continue existing chains with
export RESET_TEST=false export RESET_TEST=false
# Set coins started
export TEST_COINS_LIST="bitcoin,monero,litecoin"
""" """
import json import json
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert # 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 # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # 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)) 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: def clear_offers(port_list) -> None:
logger.info(f"clear_offers {port_list}") logger.info(f"clear_offers {port_list}")
@@ -60,7 +79,11 @@ def test_swap_dir(driver):
"automation_strat_id": 1, "automation_strat_id": 1,
} }
rv = read_json_api(node_1_port, "offers/new", offer_data) 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 = { offer_data = {
"addr_from": -1, "addr_from": -1,
@@ -72,7 +95,13 @@ def test_swap_dir(driver):
"automation_strat_id": 1, "automation_strat_id": 1,
} }
rv = read_json_api(node_1_port, "offers/new", offer_data) 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 = { offer_data = {
"addr_from": -1, "addr_from": -1,
@@ -84,7 +113,11 @@ def test_swap_dir(driver):
"automation_strat_id": 1, "automation_strat_id": 1,
} }
rv = read_json_api(node_2_port, "offers/new", offer_data) 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 # Wait for offers to propagate
for i in range(1000): for i in range(1000):
+96 -1
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2021-2024 tecnovert # 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 # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # 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) ci0 = swap_clients[0].ci(self.test_coin_from)
ci1 = swap_clients[1].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_0 = ci0.rpc_wallet("getnewaddress", ["mweb addr test 0", "mweb"])
mweb_addr_1 = ci1.rpc_wallet("getnewaddress", ["mweb addr test 1", "mweb"]) mweb_addr_1 = ci1.rpc_wallet("getnewaddress", ["mweb addr test 1", "mweb"])
@@ -210,6 +219,19 @@ class TestLTC(BasicSwapTest):
< 0.1 < 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: try:
pause_event.clear() # Stop mining pause_event.clear() # Stop mining
ci0.rpc_wallet("sendtoaddress", [mweb_addr_1, 10.0]) ci0.rpc_wallet("sendtoaddress", [mweb_addr_1, 10.0])
@@ -237,6 +259,7 @@ class TestLTC(BasicSwapTest):
for utxo in utxos: for utxo in utxos:
if utxo.get("address", "") == mweb_addr_1: if utxo.get("address", "") == mweb_addr_1:
mweb_tx = utxo mweb_tx = utxo
break
assert mweb_tx is not None assert mweb_tx is not None
unspent_addr = ci1.getUnspentsByAddr() unspent_addr = ci1.getUnspentsByAddr()
@@ -245,6 +268,62 @@ class TestLTC(BasicSwapTest):
if "mweb1" in addr: if "mweb1" in addr:
raise ValueError("getUnspentsByAddr should exclude mweb UTXOs.") 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 # TODO
def test_22_mweb_balance(self): def test_22_mweb_balance(self):
@@ -265,7 +344,9 @@ class TestLTC(BasicSwapTest):
ltc_mweb_addr = read_json_api( ltc_mweb_addr = read_json_api(
TEST_HTTP_PORT + 0, "wallets/ltc_mweb/nextdepositaddr" 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") ltc_mweb_addr2 = read_json_api(TEST_HTTP_PORT + 0, "wallets/ltc/newmwebaddress")
assert ltc_mweb_addr2.startswith("tmweb1")
assert ( assert (
ci_mweb.rpc_wallet( ci_mweb.rpc_wallet(
@@ -337,6 +418,20 @@ class TestLTC(BasicSwapTest):
json_rv = read_json_api(TEST_HTTP_PORT + 0, "wallets/ltc", post_json) json_rv = read_json_api(TEST_HTTP_PORT + 0, "wallets/ltc", post_json)
assert json_rv["mweb_balance"] <= 20.0 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__": if __name__ == "__main__":
unittest.main() unittest.main()
+1
View File
@@ -222,6 +222,7 @@ def prepare_swapclient_dir(
"datadir": os.path.join(datadir, "ltc_" + str(node_id)), "datadir": os.path.join(datadir, "ltc_" + str(node_id)),
"bindir": cfg.LITECOIN_BINDIR, "bindir": cfg.LITECOIN_BINDIR,
"use_segwit": True, "use_segwit": True,
"wallet_name": "bsx_wallet",
} }
if cls: if cls: