1 Commits

Author SHA1 Message Date
Cryptoguard
631ccea626 Added NMC to README 2025-04-07 16:35:51 -04:00
156 changed files with 10506 additions and 32972 deletions

View File

@@ -9,9 +9,6 @@ concurrency:
env:
BIN_DIR: /tmp/cached_bin
TEST_RELOAD_PATH: /tmp/test_basicswap
BSX_SELENIUM_DRIVER: firefox-ci
XMR_RPC_USER: xmr_user
XMR_RPC_PWD: xmr_pwd
jobs:
ci:
@@ -27,19 +24,8 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
if [ $(dpkg-query -W -f='${Status}' firefox 2>/dev/null | grep -c "ok installed") -eq 0 ]; then
install -d -m 0755 /etc/apt/keyrings
wget -q https://packages.mozilla.org/apt/repo-signing-key.gpg -O- | sudo tee /etc/apt/keyrings/packages.mozilla.org.asc > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main" | sudo tee -a /etc/apt/sources.list.d/mozilla.list > /dev/null
echo "Package: *" | sudo tee /etc/apt/preferences.d/mozilla
echo "Pin: origin packages.mozilla.org" | sudo tee -a /etc/apt/preferences.d/mozilla
echo "Pin-Priority: 1000" | sudo tee -a /etc/apt/preferences.d/mozilla
sudo apt-get update
sudo apt-get install -y firefox
fi
python -m pip install --upgrade pip
pip install python-gnupg
pip install -e .[dev]
pip install flake8 codespell pytest
pip install -r requirements.txt --require-hashes
- name: Install
run: |
@@ -47,16 +33,13 @@ jobs:
# Print the core versions to a file for caching
basicswap-prepare --version --withcoins=bitcoin | tail -n +2 > core_versions.txt
cat core_versions.txt
- name: Run flake8
- name: Running flake8
run: |
flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
- name: Run codespell
- name: Running codespell
run: |
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
- name: Run black
run: |
black --check --diff --exclude="contrib" .
- name: Run test_other
- name: Running test_other
run: |
pytest tests/basicswap/test_other.py
- name: Cache coin cores
@@ -72,49 +55,17 @@ jobs:
name: Running basicswap-prepare
run: |
basicswap-prepare --bindir="$BIN_DIR" --preparebinonly --withcoins=particl,bitcoin,monero
- name: Run test_prepare
run: |
export PYTHONPATH=$(pwd)
export TEST_BIN_PATH="$BIN_DIR"
export TEST_PATH=/tmp/test_prepare
pytest tests/basicswap/extended/test_prepare.py
- name: Run test_xmr
- name: Running test_xmr
run: |
export PYTHONPATH=$(pwd)
export PARTICL_BINDIR="$BIN_DIR/particl"
export BITCOIN_BINDIR="$BIN_DIR/bitcoin"
export XMR_BINDIR="$BIN_DIR/monero"
pytest tests/basicswap/test_btc_xmr.py::TestBTC -k "test_003_api or test_02_a_leader_recover_a_lock_tx"
- name: Run test_encrypted_xmr_reload
- name: Running test_encrypted_xmr_reload
run: |
export PYTHONPATH=$(pwd)
export TEST_PATH=${TEST_RELOAD_PATH}
mkdir -p ${TEST_PATH}/bin
cp -r $BIN_DIR/* ${TEST_PATH}/bin/
pytest tests/basicswap/extended/test_encrypted_xmr_reload.py
- name: Run selenium tests
run: |
export TEST_PATH=/tmp/test_persistent
mkdir -p ${TEST_PATH}/bin
cp -r $BIN_DIR/* ${TEST_PATH}/bin/
export PYTHONPATH=$(pwd)
python tests/basicswap/extended/test_xmr_persistent.py > /tmp/log.txt 2>&1 & TEST_NETWORK_PID=$!
echo "Starting test_xmr_persistent, PID $TEST_NETWORK_PID"
i=0
until curl -s -f -o /dev/null "http://localhost:12701/json/coins"
do
tail -n 1 /tmp/log.txt
sleep 2
((++i))
if [ $i -ge 60 ]; then
echo "Timed out waiting for test_xmr_persistent, PID $TEST_NETWORK_PID"
kill $TEST_NETWORK_PID
(exit 1) # Fail test
break
fi
done
echo "Running test_settings.py"
python tests/basicswap/selenium/test_settings.py
echo "Running test_swap_direction.py"
python tests/basicswap/selenium/test_swap_direction.py
kill $TEST_NETWORK_PID

2
.gitignore vendored
View File

@@ -8,8 +8,6 @@ __pycache__
/*.eggs
.tox
.eggs
.ruff_cache
.pytest_cache
*~
# geckodriver.log

View File

@@ -1,3 +1,3 @@
name = "basicswap"
__version__ = "0.14.6"
__version__ = "0.14.3"

View File

@@ -5,18 +5,17 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import logging
import os
import random
import shlex
import socket
import socks
import subprocess
import sys
import threading
import time
import traceback
import shlex
import socks
import random
import socket
import urllib
import logging
import threading
import traceback
import subprocess
from sockshandler import SocksiPyHandler
@@ -43,9 +42,9 @@ def getaddrinfo_tor(*args):
class BaseApp(DBMethods):
def __init__(self, data_dir, settings, chain, log_name="BasicSwap"):
self.fp = None
def __init__(self, fp, data_dir, settings, chain, log_name="BasicSwap"):
self.log_name = log_name
self.fp = fp
self.fail_code = 0
self.mock_time_offset = 0
@@ -61,7 +60,7 @@ class BaseApp(DBMethods):
self._network = None
self.prepareLogging()
self.log.info(f"Network: {self.chain}")
self.log.info("Network: {}".format(self.chain))
self.use_tor_proxy = self.settings.get("use_tor", False)
self.tor_proxy_host = self.settings.get("tor_proxy_host", "127.0.0.1")
@@ -71,42 +70,25 @@ class BaseApp(DBMethods):
self.default_socket = socket.socket
self.default_socket_timeout = socket.getdefaulttimeout()
self.default_socket_getaddrinfo = socket.getaddrinfo
self._force_db_upgrade = False
def __del__(self):
if self.fp:
self.fp.close()
def stopRunning(self, with_code=0):
self.fail_code = with_code
# Wait for lock to shutdown gracefully.
if self.mxDB.acquire(timeout=5):
with self.mxDB:
self.chainstate_delay_event.set()
self.delay_event.set()
self.mxDB.release()
else:
# Waiting for lock timed out, stop anyway
self.chainstate_delay_event.set()
self.delay_event.set()
def openLogFile(self):
self.fp = open(os.path.join(self.data_dir, "basicswap.log"), "a")
def prepareLogging(self):
logging.setLoggerClass(BSXLogger)
self.log = logging.getLogger(self.log_name)
self.log.propagate = False
self.openLogFile()
# Remove any existing handlers
self.log.handlers = []
formatter = logging.Formatter(
"%(asctime)s %(levelname)s : %(message)s", "%Y-%m-%d %H:%M:%S"
)
stream_stdout = logging.StreamHandler(sys.stdout)
stream_stdout = logging.StreamHandler()
if self.log_name != "BasicSwap":
stream_stdout.setFormatter(
logging.Formatter(
@@ -116,7 +98,6 @@ class BaseApp(DBMethods):
)
else:
stream_stdout.setFormatter(formatter)
self.log_formatter = formatter
stream_fp = logging.StreamHandler(self.fp)
stream_fp.setFormatter(formatter)
@@ -151,7 +132,7 @@ class BaseApp(DBMethods):
for c, params in chainparams.items():
if coin_name.lower() == params["name"].lower():
return c
raise ValueError(f"Unknown coin: {coin_name}")
raise ValueError("Unknown coin: {}".format(coin_name))
def callrpc(self, method, params=[], wallet=None):
cc = self.coin_clients[Coins.PART]

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -9,14 +9,12 @@
import struct
import hashlib
from enum import IntEnum, auto
from html import escape as html_escape
from .util.address import (
encodeAddress,
decodeAddress,
)
from .chainparams import (
chainparams,
Fiat,
)
@@ -36,11 +34,6 @@ class KeyTypes(IntEnum):
KAF = 6
class MessageNetworks(IntEnum):
SMSG = auto()
SIMPLEX = auto()
class MessageTypes(IntEnum):
OFFER = auto()
BID = auto()
@@ -58,8 +51,6 @@ class MessageTypes(IntEnum):
ADS_BID_LF = auto()
ADS_BID_ACCEPT_FL = auto()
CONNECT_REQ = auto()
class AddressTypes(IntEnum):
OFFER = auto()
@@ -118,7 +109,6 @@ class BidStates(IntEnum):
BID_EXPIRED = 31
BID_AACCEPT_DELAY = 32
BID_AACCEPT_FAIL = 33
CONNECT_REQ_SENT = 34
class TxStates(IntEnum):
@@ -236,10 +226,6 @@ class NotificationTypes(IntEnum):
BID_ACCEPTED = auto()
class ConnectionRequestTypes(IntEnum):
BID = 1
class AutomationOverrideOptions(IntEnum):
DEFAULT = 0
ALWAYS_ACCEPT = 1
@@ -351,8 +337,6 @@ def strBidState(state):
return "Auto accept delay"
if state == BidStates.BID_AACCEPT_FAIL:
return "Auto accept failed"
if state == BidStates.CONNECT_REQ_SENT:
return "Connect request sent"
return "Unknown" + " " + str(state)
@@ -536,7 +520,7 @@ def getLastBidState(packed_states):
return BidStates.BID_STATE_UNKNOWN
def strSwapType(swap_type) -> str:
def strSwapType(swap_type):
if swap_type == SwapTypes.SELLER_FIRST:
return "seller_first"
if swap_type == SwapTypes.XMR_SWAP:
@@ -544,7 +528,7 @@ def strSwapType(swap_type) -> str:
return None
def strSwapDesc(swap_type) -> str:
def strSwapDesc(swap_type):
if swap_type == SwapTypes.SELLER_FIRST:
return "Secret Hash"
if swap_type == SwapTypes.XMR_SWAP:
@@ -552,31 +536,6 @@ def strSwapDesc(swap_type) -> str:
return None
def fiatTicker(fiat_ind: int) -> str:
try:
return Fiat(fiat_ind).name
except Exception as e: # noqa: F841
raise ValueError(f"Unknown fiat ind {fiat_ind}")
def fiatFromTicker(ticker: str) -> int:
ticker_uc = ticker.upper()
for entry in Fiat:
if entry.name == ticker_uc:
return entry
raise ValueError(f"Unknown fiat {ticker}")
def get_api_key_setting(
settings, setting_name: str, default_value: str = "", escape: bool = False
):
setting_name_enc: str = setting_name + "_enc"
if setting_name_enc in settings:
rv = bytes.fromhex(settings[setting_name_enc]).decode("utf-8")
return html_escape(rv) if escape else rv
return settings.get(setting_name, default_value)
inactive_states = [
BidStates.SWAP_COMPLETED,
BidStates.BID_ERROR,

File diff suppressed because it is too large Load Diff

View File

@@ -17,13 +17,12 @@ import traceback
import basicswap.config as cfg
from basicswap import __version__
from basicswap.basicswap import BasicSwap
from basicswap.chainparams import chainparams, Coins, isKnownCoinName
from basicswap.contrib.websocket_server import WebsocketServer
from basicswap.http_server import HttpThread
from basicswap.network.simplex_chat import startSimplexClient
from basicswap.ui.util import getCoinName
from basicswap.util.daemon import Daemon
from basicswap.basicswap import BasicSwap
from basicswap.chainparams import chainparams, Coins
from basicswap.http_server import HttpThread
from basicswap.contrib.websocket_server import WebsocketServer
initial_logger = logging.getLogger()
initial_logger.level = logging.DEBUG
@@ -34,45 +33,41 @@ logger = initial_logger
swap_client = None
class Daemon:
__slots__ = ("handle", "files")
def __init__(self, handle, files):
self.handle = handle
self.files = files
def is_known_coin(coin_name: str) -> bool:
for k, v in chainparams.items():
if coin_name == v["name"]:
return True
return False
def signal_handler(sig, frame):
os.write(
sys.stdout.fileno(), f"Signal {sig} detected, ending program.\n".encode("utf-8")
)
if swap_client is not None and not swap_client.chainstate_delay_event.is_set():
try:
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
amm_status = get_amm_status()
if amm_status == "running":
logger.info("Signal handler stopping AMM process...")
success, msg = stop_amm_process(swap_client)
if success:
logger.info(f"AMM signal shutdown: {msg}")
else:
logger.warning(f"AMM signal shutdown warning: {msg}")
except Exception as e:
logger.error(f"Error stopping AMM in signal handler: {e}")
swap_client.stopRunning()
def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
datadir_path = os.path.expanduser(node_dir)
coin_name = extra_config.get("coin_name", "")
# Rewrite litecoin.conf
# TODO: Remove
needs_rewrite: bool = False
ltc_conf_path = os.path.join(datadir_path, "litecoin.conf")
if os.path.exists(ltc_conf_path):
needs_rewrite: bool = False
add_changetype: bool = True
with open(ltc_conf_path) as fp:
for line in fp:
line = line.strip()
if line.startswith("changetype="):
add_changetype = False
break
if line.endswith("=onion"):
needs_rewrite = True
break
@@ -88,29 +83,6 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
fp_to.write(line.strip()[:-6] + "\n")
else:
fp_to.write(line)
if add_changetype:
fp_to.write("changetype=bech32\n")
add_changetype = False
if add_changetype:
logger.info("Adding changetype to litecoin.conf")
with open(ltc_conf_path, "a") as fp:
fp.write("changetype=bech32\n")
# Rewrite bitcoin.conf
# TODO: Remove
btc_conf_path = os.path.join(datadir_path, "bitcoin.conf")
if coin_name == "bitcoin" and os.path.exists(btc_conf_path):
add_changetype: bool = True
with open(btc_conf_path) as fp:
for line in fp:
line = line.strip()
if line.startswith("changetype="):
add_changetype = False
break
if add_changetype:
logger.info("Adding changetype to bitcoin.conf")
with open(btc_conf_path, "a") as fp:
fp.write("changetype=bech32\n")
args = [
daemon_bin,
@@ -119,7 +91,7 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
if add_datadir:
args.append("-datadir=" + datadir_path)
args += opts
logger.info(f"Starting node {daemon_bin}")
logger.info("Starting node {}".format(daemon_bin))
logger.debug("Arguments {}".format(" ".join(args)))
opened_files = []
@@ -150,7 +122,6 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
cwd=datadir_path,
),
opened_files,
os.path.basename(daemon_bin),
)
@@ -166,7 +137,7 @@ def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
"--non-interactive",
"--config-file=" + os.path.join(datadir_path, config_filename),
] + opts
logger.info(f"Starting node {daemon_bin}")
logger.info("Starting node {}".format(daemon_bin))
logger.debug("Arguments {}".format(" ".join(args)))
# return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@@ -181,7 +152,6 @@ def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
cwd=datadir_path,
),
[file_stdout, file_stderr],
os.path.basename(daemon_bin),
)
@@ -230,7 +200,7 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
):
fp_to.write(line)
logger.info(f"Starting wallet daemon {wallet_bin}")
logger.info("Starting wallet daemon {}".format(wallet_bin))
logger.debug("Arguments {}".format(" ".join(args)))
# TODO: return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=data_dir)
@@ -245,7 +215,6 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
cwd=data_dir,
),
[wallet_stdout, wallet_stderr],
os.path.basename(wallet_bin),
)
@@ -296,43 +265,13 @@ def getCoreBinArgs(coin_id: int, coin_settings, prepare=False, use_tor_proxy=Fal
# As BCH may use port 8334, disable it here.
# When tor is enabled a bind option for the onionport will be added to bitcoin.conf.
# https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-28.0.md?plain=1#L84
if (
prepare is False
and use_tor_proxy is False
and coin_id in (Coins.BTC, Coins.NMC)
):
if prepare is False and use_tor_proxy is False and coin_id == Coins.BTC:
port: int = coin_settings.get("port", 8333)
extra_args.append(f"--bind=0.0.0.0:{port}")
return extra_args
def mainLoop(daemons, update: bool = True):
while not swap_client.delay_event.wait(0.5):
if update:
swap_client.update()
else:
pass
for daemon in daemons:
if daemon.running is False:
continue
poll = daemon.handle.poll()
if poll is None:
pass # Process is running
else:
daemon.running = False
swap_client.log.error(
f"Process {daemon.handle.pid} for {daemon.name} terminated unexpectedly returning {poll}."
)
def runClient(
data_dir: str,
chain: str,
start_only_coins: bool,
log_prefix: str = "BasicSwap",
extra_opts=dict(),
) -> int:
def runClient(fp, data_dir, chain, start_only_coins):
global swap_client, logger
daemons = []
pids = []
@@ -357,9 +296,7 @@ def runClient(
with open(settings_path) as fs:
settings = json.load(fs)
swap_client = BasicSwap(
data_dir, settings, chain, log_name=log_prefix, extra_opts=extra_opts
)
swap_client = BasicSwap(fp, data_dir, settings, chain)
logger = swap_client.log
if os.path.exists(pids_path):
@@ -373,60 +310,16 @@ def runClient(
# Settings may have been modified
settings = swap_client.settings
try:
# Try start daemons
for network in settings.get("networks", []):
if network.get("enabled", True) is False:
continue
network_type = 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}"
)
daemons.append(
startSimplexClient(
network["client_path"],
simplex_dir,
network["server_address"],
network["ws_port"],
logger,
swap_client.delay_event,
socks_proxy=socks_proxy,
log_level=log_level,
)
)
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started Simplex client {pid}")
for c, v in settings["chainclients"].items():
if len(start_only_coins) > 0 and c not in start_only_coins:
continue
if (
len(swap_client.with_coins_override) > 0
and c not in swap_client.with_coins_override
) or c in swap_client.without_coins_override:
if v.get("manage_daemon", False) or v.get(
"manage_wallet_daemon", False
):
logger.warning(
f"Not starting coin {c.capitalize()}, disabled by arguments."
)
continue
try:
coin_id = swap_client.getCoinIdFromName(c)
display_name = getCoinName(coin_id)
except Exception as e: # noqa: F841
logger.warning(f"Not starting unknown coin: {c}")
logger.warning("Not starting unknown coin: {}".format(c))
continue
if c in ("monero", "wownero"):
if v["manage_daemon"] is True:
@@ -435,7 +328,7 @@ def runClient(
daemons.append(startXmrDaemon(v["datadir"], v["bindir"], filename))
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started {filename} {pid}")
swap_client.log.info("Started {} {}".format(filename, pid))
if v["manage_wallet_daemon"] is True:
swap_client.log.info(f"Starting {display_name} wallet daemon")
@@ -483,7 +376,7 @@ def runClient(
startXmrWalletDaemon(v["datadir"], v["bindir"], filename, opts)
)
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started {filename} {pid}")
swap_client.log.info("Started {} {}".format(filename, pid))
continue # /monero
@@ -502,7 +395,6 @@ def runClient(
"stdout_to_file": True,
"stdout_filename": "dcrd_stdout.log",
"use_shell": use_shell,
"coin_name": "decred",
}
daemons.append(
startDaemon(
@@ -514,7 +406,7 @@ def runClient(
)
)
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started {filename} {pid}")
swap_client.log.info("Started {} {}".format(filename, pid))
if v["manage_wallet_daemon"] is True:
swap_client.log.info(f"Starting {display_name} wallet daemon")
@@ -531,7 +423,6 @@ def runClient(
"stdout_to_file": True,
"stdout_filename": "dcrwallet_stdout.log",
"use_shell": use_shell,
"coin_name": "decred",
}
daemons.append(
startDaemon(
@@ -543,7 +434,7 @@ def runClient(
)
)
pid = daemons[-1].handle.pid
swap_client.log.info(f"Started {filename} {pid}")
swap_client.log.info("Started {} {}".format(filename, pid))
continue # /decred
@@ -554,20 +445,13 @@ def runClient(
extra_opts = getCoreBinArgs(
coin_id, v, use_tor_proxy=swap_client.use_tor_proxy
)
extra_config = {"coin_name": c}
daemons.append(
startDaemon(
v["datadir"],
v["bindir"],
filename,
opts=extra_opts,
extra_config=extra_config,
)
startDaemon(v["datadir"], v["bindir"], filename, opts=extra_opts)
)
pid = daemons[-1].handle.pid
pids.append((c, pid))
swap_client.setDaemonPID(c, pid)
swap_client.log.info(f"Started {filename} {pid}")
swap_client.log.info("Started {} {}".format(filename, pid))
if len(pids) > 0:
with open(pids_path, "w") as fd:
for p in pids:
@@ -581,7 +465,8 @@ def runClient(
logger.info(
f"Only running {start_only_coins}. Manually exit with Ctrl + c when ready."
)
mainLoop(daemons, update=False)
while not swap_client.delay_event.wait(0.5):
pass
else:
swap_client.start()
if "htmlhost" in settings:
@@ -595,6 +480,7 @@ def runClient(
else cfg.DEFAULT_ALLOW_CORS
)
thread_http = HttpThread(
fp,
settings["htmlhost"],
settings["htmlport"],
allow_cors,
@@ -619,7 +505,8 @@ def runClient(
swap_client.ws_server.run_forever(threaded=True)
logger.info("Exit with Ctrl + c.")
mainLoop(daemons)
while not swap_client.delay_event.wait(0.5):
swap_client.update()
except Exception as e: # noqa: F841
traceback.print_exc()
@@ -642,13 +529,13 @@ def runClient(
closed_pids = []
for d in daemons:
swap_client.log.info(f"Interrupting {d.name} {d.handle.pid}")
swap_client.log.info("Interrupting {}".format(d.handle.pid))
try:
d.handle.send_signal(
signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
)
except Exception as e:
swap_client.log.info(f"Interrupting {d.name} {d.handle.pid}, error {e}")
swap_client.log.info(f"Interrupting {d.handle.pid}, error {e}")
for d in daemons:
try:
d.handle.wait(timeout=120)
@@ -659,9 +546,6 @@ def runClient(
except Exception as e:
swap_client.log.error(f"Error: {e}")
fail_code: int = swap_client.fail_code
del swap_client
if os.path.exists(pids_path):
with open(pids_path) as fd:
lines = fd.read().split("\n")
@@ -675,18 +559,9 @@ def runClient(
with open(pids_path, "w") as fd:
fd.write(still_running)
return fail_code
def printVersion():
logger.info(
f"Basicswap version: {__version__}",
)
def ensure_coin_valid(coin: str) -> bool:
if isKnownCoinName(coin) is False:
raise ValueError(f"Unknown coin: {coin}")
logger.info("Basicswap version: %s", __version__)
def printHelp():
@@ -694,34 +569,26 @@ def printHelp():
print("\n--help, -h Print help.")
print("--version, -v Print version.")
print(
f"--datadir=PATH Path to basicswap data directory, default:{cfg.BASICSWAP_DATADIR}."
"--datadir=PATH Path to basicswap data directory, default:{}.".format(
cfg.BASICSWAP_DATADIR
)
)
print("--mainnet Run in mainnet mode.")
print("--testnet Run in testnet mode.")
print("--regtest Run in regtest mode.")
print("--withcoin= Run only with coin/s.")
print("--withoutcoin= Run without coin/s.")
print(
"--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing."
)
print("--logprefix Specify log prefix.")
print(
"--forcedbupgrade Recheck database against schema regardless of version."
)
def main():
data_dir = None
chain = "mainnet"
start_only_coins = set()
log_prefix: str = "BasicSwap"
options = dict()
with_coins = set()
without_coins = set()
for v in sys.argv[1:]:
if len(v) < 2 or v[0] != "-":
logger.warning(f"Unknown argument {v}")
logger.warning("Unknown argument %s", v)
continue
s = v.split("=")
@@ -741,35 +608,19 @@ def main():
if name in ("mainnet", "testnet", "regtest"):
chain = name
continue
if name in ("withcoin", "withcoins"):
for coin in [s.strip().lower() for s in s[1].split(",")]:
ensure_coin_valid(coin)
with_coins.add(coin)
continue
if name in ("withoutcoin", "withoutcoins"):
for coin in [s.strip().lower() for s in s[1].split(",")]:
if coin == "particl":
raise ValueError("Particl is required.")
ensure_coin_valid(coin)
without_coins.add(coin)
continue
if name == "forcedbupgrade":
options["force_db_upgrade"] = True
continue
if len(s) == 2:
if name == "datadir":
data_dir = os.path.expanduser(s[1])
continue
if name == "logprefix":
log_prefix = s[1]
continue
if name == "startonlycoin":
for coin in [s.lower() for s in s[1].split(",")]:
ensure_coin_valid(coin)
if is_known_coin(coin) is False:
raise ValueError(f"Unknown coin: {coin}")
start_only_coins.add(coin)
continue
logger.warning(f"Unknown argument {v}")
logger.warning("Unknown argument %s", v)
if os.name == "nt":
logger.warning(
@@ -778,23 +629,20 @@ def main():
if data_dir is None:
data_dir = os.path.join(os.path.expanduser(cfg.BASICSWAP_DATADIR))
logger.info(f"Using datadir: {data_dir}")
logger.info(f"Chain: {chain}")
logger.info("Using datadir: %s", data_dir)
logger.info("Chain: %s", chain)
if not os.path.exists(data_dir):
os.makedirs(data_dir)
if len(with_coins) > 0:
with_coins.add("particl")
options["with_coins"] = with_coins
if len(without_coins) > 0:
options["without_coins"] = without_coins
logger.info(os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n")
fail_code = runClient(data_dir, chain, start_only_coins, log_prefix, options)
with open(os.path.join(data_dir, "basicswap.log"), "a") as fp:
logger.info(
os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n"
)
runClient(fp, data_dir, chain, start_only_coins)
print("Done.")
return fail_code
return swap_client.fail_code if swap_client is not None else 0
if __name__ == "__main__":

View File

@@ -35,12 +35,6 @@ class Coins(IntEnum):
DOGE = 18
class Fiat(IntEnum):
USD = -1
GBP = -2
EUR = -3
chainparams = {
Coins.PART: {
"name": "particl",
@@ -58,8 +52,6 @@ chainparams = {
"bip44": 44,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0x696E82D1,
"ext_secret_key_prefix": 0x8F1DAEB8,
},
"testnet": {
"rpcport": 51935,
@@ -71,8 +63,6 @@ chainparams = {
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0xE1427800,
"ext_secret_key_prefix": 0x04889478,
},
"regtest": {
"rpcport": 51936,
@@ -84,8 +74,6 @@ chainparams = {
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0xE1427800,
"ext_secret_key_prefix": 0x04889478,
},
},
Coins.BTC: {
@@ -257,38 +245,29 @@ chainparams = {
"rpcport": 8336,
"pubkey_address": 52,
"script_address": 13,
"key_prefix": 180,
"hrp": "nc",
"bip44": 7,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0x0488B21E, # base58Prefixes[EXT_PUBLIC_KEY]
"ext_secret_key_prefix": 0x0488ADE4,
},
"testnet": {
"rpcport": 18336,
"pubkey_address": 111,
"script_address": 196,
"key_prefix": 239,
"hrp": "tn",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"name": "testnet3",
"ext_public_key_prefix": 0x043587CF,
"ext_secret_key_prefix": 0x04358394,
},
"regtest": {
"rpcport": 18443,
"pubkey_address": 111,
"script_address": 196,
"key_prefix": 239,
"hrp": "ncrt",
"bip44": 1,
"min_amount": 100000,
"max_amount": 10000000 * COIN,
"ext_public_key_prefix": 0x043587CF,
"ext_secret_key_prefix": 0x04358394,
},
},
Coins.XMR: {
@@ -571,7 +550,3 @@ def getCoinIdFromName(name: str) -> str:
return name_map[name.lower()]
except Exception:
raise ValueError(f"Unknown coin {name}")
def isKnownCoinName(name: str) -> bool:
return params["name"].lower() in name_map

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2025 The Basicswap developers
# Copyright (c) 2019-2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -40,6 +40,13 @@ DOGECOIND = os.getenv("DOGECOIND", "dogecoind" + bin_suffix)
DOGECOIN_CLI = os.getenv("DOGECOIN_CLI", "dogecoin-cli" + bin_suffix)
DOGECOIN_TX = os.getenv("DOGECOIN_TX", "dogecoin-tx" + bin_suffix)
NAMECOIN_BINDIR = os.path.expanduser(
os.getenv("NAMECOIN_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "namecoin"))
)
NAMECOIND = os.getenv("NAMECOIND", "namecoind" + bin_suffix)
NAMECOIN_CLI = os.getenv("NAMECOIN_CLI", "namecoin-cli" + bin_suffix)
NAMECOIN_TX = os.getenv("NAMECOIN_TX", "namecoin-tx" + bin_suffix)
XMR_BINDIR = os.path.expanduser(
os.getenv("XMR_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "monero"))
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,22 +5,20 @@
"""Helpful routines for regression testing."""
from base64 import b64encode
from binascii import unhexlify
from decimal import Decimal, ROUND_DOWN
from subprocess import CalledProcessError
import hashlib
import inspect
import json
import logging
import os
import pathlib
import platform
import random
import re
import time
from . import coverage
from .authproxy import AuthServiceProxy, JSONRPCException
from collections.abc import Callable
from typing import Optional
from io import BytesIO
logger = logging.getLogger("TestFramework.utils")
@@ -30,46 +28,23 @@ logger = logging.getLogger("TestFramework.utils")
def assert_approx(v, vexp, vspan=0.00001):
"""Assert that `v` is within `vspan` of `vexp`"""
if isinstance(v, Decimal) or isinstance(vexp, Decimal):
v=Decimal(v)
vexp=Decimal(vexp)
vspan=Decimal(vspan)
if v < vexp - vspan:
raise AssertionError("%s < [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan)))
if v > vexp + vspan:
raise AssertionError("%s > [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan)))
def assert_fee_amount(fee, tx_size, feerate_BTC_kvB):
"""Assert the fee is in range."""
assert isinstance(tx_size, int)
target_fee = get_fee(tx_size, feerate_BTC_kvB)
def assert_fee_amount(fee, tx_size, fee_per_kB):
"""Assert the fee was in range"""
target_fee = round(tx_size * fee_per_kB / 1000, 8)
if fee < target_fee:
raise AssertionError("Fee of %s BTC too low! (Should be %s BTC)" % (str(fee), str(target_fee)))
# allow the wallet's estimation to be at most 2 bytes off
high_fee = get_fee(tx_size + 2, feerate_BTC_kvB)
if fee > high_fee:
if fee > (tx_size + 2) * fee_per_kB / 1000:
raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)" % (str(fee), str(target_fee)))
def summarise_dict_differences(thing1, thing2):
if not isinstance(thing1, dict) or not isinstance(thing2, dict):
return thing1, thing2
d1, d2 = {}, {}
for k in sorted(thing1.keys()):
if k not in thing2:
d1[k] = thing1[k]
elif thing1[k] != thing2[k]:
d1[k], d2[k] = summarise_dict_differences(thing1[k], thing2[k])
for k in sorted(thing2.keys()):
if k not in thing1:
d2[k] = thing2[k]
return d1, d2
def assert_equal(thing1, thing2, *args):
if thing1 != thing2 and not args and isinstance(thing1, dict) and isinstance(thing2, dict):
d1,d2 = summarise_dict_differences(thing1, thing2)
raise AssertionError("not(%s == %s)\n in particular not(%s == %s)" % (thing1, thing2, d1, d2))
if thing1 != thing2 or any(thing1 != arg for arg in args):
raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
@@ -104,7 +79,7 @@ def assert_raises_message(exc, message, fun, *args, **kwds):
raise AssertionError("No exception raised")
def assert_raises_process_error(returncode: int, output: str, fun: Callable, *args, **kwds):
def assert_raises_process_error(returncode, output, fun, *args, **kwds):
"""Execute a process and asserts the process return code and output.
Calls function `fun` with arguments `args` and `kwds`. Catches a CalledProcessError
@@ -112,9 +87,9 @@ def assert_raises_process_error(returncode: int, output: str, fun: Callable, *ar
no CalledProcessError was raised or if the return code and output are not as expected.
Args:
returncode: the process return code.
output: [a substring of] the process output.
fun: the function to call. This should execute a process.
returncode (int): the process return code.
output (string): [a substring of] the process output.
fun (function): the function to call. This should execute a process.
args*: positional arguments for the function.
kwds**: named arguments for the function.
"""
@@ -129,7 +104,7 @@ def assert_raises_process_error(returncode: int, output: str, fun: Callable, *ar
raise AssertionError("No exception raised")
def assert_raises_rpc_error(code: Optional[int], message: Optional[str], fun: Callable, *args, **kwds):
def assert_raises_rpc_error(code, message, fun, *args, **kwds):
"""Run an RPC and verify that a specific JSONRPC exception code and message is raised.
Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException
@@ -137,11 +112,11 @@ def assert_raises_rpc_error(code: Optional[int], message: Optional[str], fun: Ca
no JSONRPCException was raised or if the error code/message are not as expected.
Args:
code: the error code returned by the RPC call (defined in src/rpc/protocol.h).
Set to None if checking the error code is not required.
message: [a substring of] the error string returned by the RPC call.
Set to None if checking the error string is not required.
fun: the function to call. This should be the name of an RPC.
code (int), optional: the error code returned by the RPC call (defined
in src/rpc/protocol.h). Set to None if checking the error code is not required.
message (string), optional: [a substring of] the error string returned by the
RPC call. Set to None if checking the error string is not required.
fun (function): the function to call. This should be the name of an RPC.
args*: positional arguments for the function.
kwds**: named arguments for the function.
"""
@@ -228,45 +203,29 @@ def check_json_precision():
raise RuntimeError("JSON encode/decode loses precision")
def EncodeDecimal(o):
if isinstance(o, Decimal):
return str(o)
raise TypeError(repr(o) + " is not JSON serializable")
def count_bytes(hex_string):
return len(bytearray.fromhex(hex_string))
def hex_str_to_bytes(hex_str):
return unhexlify(hex_str.encode('ascii'))
def str_to_b64str(string):
return b64encode(string.encode('utf-8')).decode('ascii')
def ceildiv(a, b):
"""
Divide 2 ints and round up to next int rather than round down
Implementation requires python integers, which have a // operator that does floor division.
Other types like decimal.Decimal whose // operator truncates towards 0 will not work.
"""
assert isinstance(a, int)
assert isinstance(b, int)
return -(-a // b)
def get_fee(tx_size, feerate_btc_kvb):
"""Calculate the fee in BTC given a feerate is BTC/kvB. Reflects CFeeRate::GetFee"""
feerate_sat_kvb = int(feerate_btc_kvb * Decimal(1e8)) # Fee in sat/kvb as an int to avoid float precision errors
target_fee_sat = ceildiv(feerate_sat_kvb * tx_size, 1000) # Round calculated fee up to nearest sat
return target_fee_sat / Decimal(1e8) # Return result in BTC
def satoshi_round(amount):
return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=None, timeout_factor=1.0):
"""Sleep until the predicate resolves to be True.
Warning: Note that this method is not recommended to be used in tests as it is
not aware of the context of the test framework. Using the `wait_until()` members
from `BitcoinTestFramework` or `P2PInterface` class ensures the timeout is
properly scaled. Furthermore, `wait_until()` from `P2PInterface` class in
`p2p.py` has a preset lock.
"""
def wait_until(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=None, timeout_factor=1.0):
if attempts == float('inf') and timeout == float('inf'):
timeout = 60
timeout = timeout * timeout_factor
@@ -294,16 +253,6 @@ def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=floa
raise RuntimeError('Unreachable')
def sha256sum_file(filename):
h = hashlib.sha256()
with open(filename, 'rb') as f:
d = f.read(4096)
while len(d) > 0:
h.update(d)
d = f.read(4096)
return h.digest()
# RPC/P2P connection constants and functions
############################################
@@ -320,15 +269,15 @@ class PortSeed:
n = None
def get_rpc_proxy(url: str, node_number: int, *, timeout: Optional[int]=None, coveragedir: Optional[str]=None) -> coverage.AuthServiceProxyWrapper:
def get_rpc_proxy(url, node_number, *, timeout=None, coveragedir=None):
"""
Args:
url: URL of the RPC server to call
node_number: the node number (or id) that this calls to
url (str): URL of the RPC server to call
node_number (int): the node number (or id) that this calls to
Kwargs:
timeout: HTTP timeout in seconds
coveragedir: Directory
timeout (int): HTTP timeout in seconds
coveragedir (str): Directory
Returns:
AuthServiceProxy. convenience object for making RPC calls.
@@ -339,10 +288,11 @@ def get_rpc_proxy(url: str, node_number: int, *, timeout: Optional[int]=None, co
proxy_kwargs['timeout'] = int(timeout)
proxy = AuthServiceProxy(url, **proxy_kwargs)
proxy.url = url # store URL on proxy for info
coverage_logfile = coverage.get_filename(coveragedir, node_number) if coveragedir else None
return coverage.AuthServiceProxyWrapper(proxy, url, coverage_logfile)
return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile)
def p2p_port(n):
@@ -371,76 +321,38 @@ def rpc_url(datadir, i, chain, rpchost):
################
def initialize_datadir(dirname, n, chain, disable_autoconnect=True):
def initialize_datadir(dirname, n, chain):
datadir = get_datadir_path(dirname, n)
if not os.path.isdir(datadir):
os.makedirs(datadir)
write_config(os.path.join(datadir, "particl.conf"), n=n, chain=chain, disable_autoconnect=disable_autoconnect)
os.makedirs(os.path.join(datadir, 'stderr'), exist_ok=True)
os.makedirs(os.path.join(datadir, 'stdout'), exist_ok=True)
return datadir
def write_config(config_path, *, n, chain, extra_config="", disable_autoconnect=True):
# Translate chain subdirectory name to config name
if chain == 'testnet':
# Translate chain name to config name
if chain == 'testnet3':
chain_name_conf_arg = 'testnet'
chain_name_conf_section = 'test'
else:
chain_name_conf_arg = chain
chain_name_conf_section = chain
with open(config_path, 'w', encoding='utf8') as f:
if chain_name_conf_arg:
with open(os.path.join(datadir, "particl.conf"), 'w', encoding='utf8') as f:
f.write("{}=1\n".format(chain_name_conf_arg))
if chain_name_conf_section:
f.write("[{}]\n".format(chain_name_conf_section))
f.write("port=" + str(p2p_port(n)) + "\n")
f.write("rpcport=" + str(rpc_port(n)) + "\n")
# Disable server-side timeouts to avoid intermittent issues
f.write("rpcservertimeout=99000\n")
f.write("rpcdoccheck=1\n")
f.write("fallbackfee=0.0002\n")
f.write("server=1\n")
f.write("keypool=1\n")
f.write("discover=0\n")
f.write("dnsseed=0\n")
f.write("fixedseeds=0\n")
f.write("listenonion=0\n")
# Increase peertimeout to avoid disconnects while using mocktime.
# peertimeout is measured in mock time, so setting it large enough to
# cover any duration in mock time is sufficient. It can be overridden
# in tests.
f.write("peertimeout=999999999\n")
f.write("printtoconsole=0\n")
f.write("upnp=0\n")
f.write("natpmp=0\n")
f.write("shrinkdebugfile=0\n")
f.write("deprecatedrpc=create_bdb\n") # Required to run the tests
# To improve SQLite wallet performance so that the tests don't timeout, use -unsafesqlitesync
f.write("unsafesqlitesync=1\n")
if disable_autoconnect:
f.write("connect=0\n")
f.write(extra_config)
os.makedirs(os.path.join(datadir, 'stderr'), exist_ok=True)
os.makedirs(os.path.join(datadir, 'stdout'), exist_ok=True)
return datadir
def get_datadir_path(dirname, n):
return pathlib.Path(dirname) / f"node{n}"
def get_temp_default_datadir(temp_dir: pathlib.Path) -> tuple[dict, pathlib.Path]:
"""Return os-specific environment variables that can be set to make the
GetDefaultDataDir() function return a datadir path under the provided
temp_dir, as well as the complete path it would return."""
if platform.system() == "Windows":
env = dict(APPDATA=str(temp_dir))
datadir = temp_dir / "Particl"
else:
env = dict(HOME=str(temp_dir))
if platform.system() == "Darwin":
datadir = temp_dir / "Library/Application Support/Particl"
else:
datadir = temp_dir / ".particl"
return env, datadir
return os.path.join(dirname, "node" + str(n))
def append_config(datadir, options):
@@ -483,7 +395,7 @@ def delete_cookie_file(datadir, chain):
def softfork_active(node, key):
"""Return whether a softfork is active."""
return node.getdeploymentinfo()['deployments'][key]['active']
return node.getblockchaininfo()['softforks'][key]['active']
def set_node_times(nodes, t):
@@ -491,51 +403,208 @@ def set_node_times(nodes, t):
node.setmocktime(t)
def check_node_connections(*, node, num_in, num_out):
info = node.getnetworkinfo()
assert_equal(info["connections_in"], num_in)
assert_equal(info["connections_out"], num_out)
def disconnect_nodes(from_connection, node_num):
def get_peer_ids():
result = []
for peer in from_connection.getpeerinfo():
if "testnode{}".format(node_num) in peer['subver']:
result.append(peer['id'])
return result
peer_ids = get_peer_ids()
if not peer_ids:
logger.warning("disconnect_nodes: {} and {} were not connected".format(
from_connection.index,
node_num,
))
return
for peer_id in peer_ids:
try:
from_connection.disconnectnode(nodeid=peer_id)
except JSONRPCException as e:
# If this node is disconnected between calculating the peer id
# and issuing the disconnect, don't worry about it.
# This avoids a race condition if we're mass-disconnecting peers.
if e.error['code'] != -29: # RPC_CLIENT_NODE_NOT_CONNECTED
raise
# wait to disconnect
wait_until(lambda: not get_peer_ids(), timeout=5)
def connect_nodes(from_connection, node_num):
ip_port = "127.0.0.1:" + str(p2p_port(node_num))
from_connection.addnode(ip_port, "onetry")
# poll until version handshake complete to avoid race conditions
# with transaction relaying
# See comments in net_processing:
# * Must have a version message before anything else
# * Must have a verack message before anything else
wait_until(lambda: all(peer['version'] != 0 for peer in from_connection.getpeerinfo()))
wait_until(lambda: all(peer['bytesrecv_per_msg'].pop('verack', 0) == 24 for peer in from_connection.getpeerinfo()))
# Transaction/Block functions
#############################
def find_output(node, txid, amount, *, blockhash=None):
"""
Return index to output of txid with value amount
Raises exception if there is none.
"""
txdata = node.getrawtransaction(txid, 1, blockhash)
for i in range(len(txdata["vout"])):
if txdata["vout"][i]["value"] == amount:
return i
raise RuntimeError("find_output txid %s : %s not found" % (txid, str(amount)))
def gather_inputs(from_node, amount_needed, confirmations_required=1):
"""
Return a random set of unspent txouts that are enough to pay amount_needed
"""
assert confirmations_required >= 0
utxo = from_node.listunspent(confirmations_required)
random.shuffle(utxo)
inputs = []
total_in = Decimal("0.00000000")
while total_in < amount_needed and len(utxo) > 0:
t = utxo.pop()
total_in += t["amount"]
inputs.append({"txid": t["txid"], "vout": t["vout"], "address": t["address"]})
if total_in < amount_needed:
raise RuntimeError("Insufficient funds: need %d, have %d" % (amount_needed, total_in))
return (total_in, inputs)
def make_change(from_node, amount_in, amount_out, fee):
"""
Create change output(s), return them
"""
outputs = {}
amount = amount_out + fee
change = amount_in - amount
if change > amount * 2:
# Create an extra change output to break up big inputs
change_address = from_node.getnewaddress()
# Split change in two, being careful of rounding:
outputs[change_address] = Decimal(change / 2).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
change = amount_in - amount - outputs[change_address]
if change > 0:
outputs[from_node.getnewaddress()] = change
return outputs
def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
"""
Create a random transaction.
Returns (txid, hex-encoded-transaction-data, fee)
"""
from_node = random.choice(nodes)
to_node = random.choice(nodes)
fee = min_fee + fee_increment * random.randint(0, fee_variants)
(total_in, inputs) = gather_inputs(from_node, amount + fee)
outputs = make_change(from_node, total_in, amount, fee)
outputs[to_node.getnewaddress()] = float(amount)
rawtx = from_node.createrawtransaction(inputs, outputs)
signresult = from_node.signrawtransactionwithwallet(rawtx)
txid = from_node.sendrawtransaction(signresult["hex"], 0)
return (txid, signresult["hex"], fee)
# Helper to create at least "count" utxos
# Pass in a fee that is sufficient for relay and mining new transactions.
def create_confirmed_utxos(fee, node, count):
to_generate = int(0.5 * count) + 101
while to_generate > 0:
node.generate(min(25, to_generate))
to_generate -= 25
utxos = node.listunspent()
iterations = count - len(utxos)
addr1 = node.getnewaddress()
addr2 = node.getnewaddress()
if iterations <= 0:
return utxos
for i in range(iterations):
t = utxos.pop()
inputs = []
inputs.append({"txid": t["txid"], "vout": t["vout"]})
outputs = {}
send_value = t['amount'] - fee
outputs[addr1] = satoshi_round(send_value / 2)
outputs[addr2] = satoshi_round(send_value / 2)
raw_tx = node.createrawtransaction(inputs, outputs)
signed_tx = node.signrawtransactionwithwallet(raw_tx)["hex"]
node.sendrawtransaction(signed_tx)
while (node.getmempoolinfo()['size'] > 0):
node.generate(1)
utxos = node.listunspent()
assert len(utxos) >= count
return utxos
# Create large OP_RETURN txouts that can be appended to a transaction
# to make it large (helper for constructing large transactions). The
# total serialized size of the txouts is about 66k vbytes.
# to make it large (helper for constructing large transactions).
def gen_return_txouts():
# Some pre-processing to create a bunch of OP_RETURN txouts to insert into transactions we create
# So we have big transactions (and therefore can't fit very many into each block)
# create one script_pubkey
script_pubkey = "6a4d0200" # OP_RETURN OP_PUSH2 512 bytes
for i in range(512):
script_pubkey = script_pubkey + "01"
# concatenate 128 txouts of above script_pubkey which we'll insert before the txout for change
txouts = []
from .messages import CTxOut
from .script import CScript, OP_RETURN
txouts = [CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN, b'\x01'*67437]))]
assert_equal(sum([len(txout.serialize()) for txout in txouts]), 67456)
txout = CTxOut()
txout.nValue = 0
txout.scriptPubKey = hex_str_to_bytes(script_pubkey)
for k in range(128):
txouts.append(txout)
return txouts
# Create a spend of each passed-in utxo, splicing in "txouts" to each raw
# transaction to make it large. See gen_return_txouts() above.
def create_lots_of_big_transactions(mini_wallet, node, fee, tx_batch_size, txouts, utxos=None):
def create_lots_of_big_transactions(node, txouts, utxos, num, fee):
addr = node.getnewaddress()
txids = []
use_internal_utxos = utxos is None
for _ in range(tx_batch_size):
tx = mini_wallet.create_self_transfer(
utxo_to_spend=None if use_internal_utxos else utxos.pop(),
fee=fee,
)["tx"]
tx.vout.extend(txouts)
res = node.testmempoolaccept([tx.serialize().hex()])[0]
assert_equal(res['fees']['base'], fee)
txids.append(node.sendrawtransaction(tx.serialize().hex()))
from .messages import CTransaction
for _ in range(num):
t = utxos.pop()
inputs = [{"txid": t["txid"], "vout": t["vout"]}]
outputs = {}
change = t['amount'] - fee
outputs[addr] = satoshi_round(change)
rawtx = node.createrawtransaction(inputs, outputs)
tx = CTransaction()
tx.deserialize(BytesIO(hex_str_to_bytes(rawtx)))
for txout in txouts:
tx.vout.append(txout)
newtx = tx.serialize().hex()
signresult = node.signrawtransactionwithwallet(newtx, None, "NONE")
txid = node.sendrawtransaction(signresult["hex"], 0)
txids.append(txid)
return txids
def mine_large_block(test_framework, mini_wallet, node):
def mine_large_block(node, utxos=None):
# generate a 66k transaction,
# and 14 of them is close to the 1MB block limit
num = 14
txouts = gen_return_txouts()
utxos = utxos if utxos is not None else []
if len(utxos) < num:
utxos.clear()
utxos.extend(node.listunspent())
fee = 100 * node.getnetworkinfo()["relayfee"]
create_lots_of_big_transactions(mini_wallet, node, fee, 14, txouts)
test_framework.generate(node, 1)
create_lots_of_big_transactions(node, txouts, utxos, num, fee=fee)
node.generate(1)
def find_vout_for_address(node, txid, addr):
@@ -545,6 +614,11 @@ def find_vout_for_address(node, txid, addr):
"""
tx = node.getrawtransaction(txid, True)
for i in range(len(tx["vout"])):
if addr == tx["vout"][i]["scriptPubKey"]["address"]:
scriptPubKey = tx["vout"][i]["scriptPubKey"]
if "addresses" in scriptPubKey:
if any([addr == a for a in scriptPubKey["addresses"]]):
return i
elif "address" in scriptPubKey:
if addr == scriptPubKey["address"]:
return i
raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr))

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -13,8 +13,8 @@ from enum import IntEnum, auto
from typing import Optional
CURRENT_DB_VERSION = 29
CURRENT_DB_DATA_VERSION = 6
CURRENT_DB_VERSION = 25
CURRENT_DB_DATA_VERSION = 5
class Concepts(IntEnum):
@@ -174,7 +174,6 @@ class Offer(Table):
secret_hash = Column("blob")
addr_from = Column("string")
pk_from = Column("blob")
addr_to = Column("string")
created_at = Column("integer")
expire_at = Column("integer")
@@ -184,7 +183,6 @@ class Offer(Table):
amount_negotiable = Column("bool")
rate_negotiable = Column("bool")
auto_accept_type = Column("integer")
# Local fields
auto_accept_bids = Column("bool")
@@ -217,9 +215,7 @@ class Bid(Table):
created_at = Column("integer")
expire_at = Column("integer")
bid_addr = Column("string")
pk_bid_addr = Column("blob")
proof_address = Column("string")
proof_signature = Column("blob")
proof_utxos = Column("blob")
# Address to spend lock tx to - address from wallet if empty TODO
withdraw_to_addr = Column("string")
@@ -486,14 +482,6 @@ class XmrSwap(Table):
b_lock_tx_id = Column("blob")
msg_split_info = Column("string")
def getMsgSplitInfo(self):
if self.msg_split_info is None:
return 16000, 17000
msg_split_info = self.msg_split_info.split(":")
return int(msg_split_info[0]), int(msg_split_info[1])
class XmrSplitData(Table):
__tablename__ = "xmr_split_data"
@@ -656,55 +644,13 @@ class CheckedBlock(Table):
block_time = Column("integer")
class CoinRates(Table):
__tablename__ = "coinrates"
def create_db(db_path: str, log) -> None:
con = None
try:
con = sqlite3.connect(db_path)
c = con.cursor()
record_id = Column("integer", primary_key=True, autoincrement=True)
currency_from = Column("integer")
currency_to = Column("integer")
rate = Column("string")
source = Column("string")
last_updated = Column("integer")
class MessageNetworks(Table):
__tablename__ = "message_networks"
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
name = Column("string")
created_at = Column("integer")
class DirectMessageRoute(Table):
__tablename__ = "direct_message_routes"
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
network_id = Column("integer")
linked_type = Column("integer")
linked_id = Column("blob")
smsg_addr_local = Column("string")
smsg_addr_remote = Column("string")
# smsg_addr_id_local = Column("integer") # SmsgAddress
# smsg_addr_id_remote = Column("integer") # KnownIdentity
route_data = Column("blob")
created_at = Column("integer")
class DirectMessageRouteLink(Table):
__tablename__ = "direct_message_route_links"
record_id = Column("integer", primary_key=True, autoincrement=True)
active_ind = Column("integer")
direct_message_route_id = Column("integer")
linked_type = Column("integer")
linked_id = Column("blob")
created_at = Column("integer")
def extract_schema() -> dict:
g = globals().copy()
tables = {}
for name, obj in g.items():
if not inspect.isclass(obj):
continue
@@ -714,13 +660,15 @@ def extract_schema() -> dict:
continue
table_name: str = obj.__tablename__
table = {}
columns = {}
query: str = f"CREATE TABLE {table_name} ("
primary_key = None
constraints = []
indices = []
num_columns: int = 0
for m in inspect.getmembers(obj):
m_name, m_obj = m
if hasattr(m_obj, "__sqlite3_primary_key__"):
primary_key = m_obj
continue
@@ -731,110 +679,47 @@ def extract_schema() -> dict:
indices.append(m_obj)
continue
if hasattr(m_obj, "__sqlite3_column__"):
if num_columns > 0:
query += ","
col_type: str = m_obj.column_type.upper()
if col_type == "BOOL":
col_type = "INTEGER"
columns[m_name] = {
"type": col_type,
"primary_key": m_obj.primary_key,
"unique": m_obj.unique,
}
table["columns"] = columns
if primary_key is not None:
table["primary_key"] = {"column_1": primary_key.column_1}
if primary_key.column_2:
table["primary_key"]["column_2"] = primary_key.column_2
if primary_key.column_3:
table["primary_key"]["column_3"] = primary_key.column_3
query += f" {m_name} {col_type} "
for constraint in constraints:
if "constraints" not in table:
table["constraints"] = []
table_constraint = {"column_1": constraint.column_1}
if constraint.column_2:
table_constraint["column_2"] = constraint.column_2
if constraint.column_3:
table_constraint["column_3"] = constraint.column_3
table["constraints"].append(table_constraint)
for i in indices:
if "indices" not in table:
table["indices"] = []
table_index = {"index_name": i.name, "column_1": i.column_1}
if i.column_2 is not None:
table_index["column_2"] = i.column_2
if i.column_3 is not None:
table_index["column_3"] = i.column_3
table["indices"].append(table_index)
tables[table_name] = table
return tables
def create_table(c, table_name, table) -> None:
query: str = f"CREATE TABLE {table_name} ("
for i, (colname, column) in enumerate(table["columns"].items()):
col_type = column["type"]
query += ("," if i > 0 else "") + f" {colname} {col_type} "
if column["primary_key"]:
if m_obj.primary_key:
query += "PRIMARY KEY ASC "
if column["unique"]:
if m_obj.unique:
query += "UNIQUE "
num_columns += 1
if "primary_key" in table:
column_1 = table["primary_key"]["column_1"]
column_2 = table["primary_key"].get("column_2", None)
column_3 = table["primary_key"].get("column_3", None)
query += f", PRIMARY KEY ({column_1}"
if column_2:
query += f", {column_2}"
if column_3:
query += f", {column_3}"
if primary_key is not None:
query += f", PRIMARY KEY ({primary_key.column_1}"
if primary_key.column_2:
query += f", {primary_key.column_2}"
if primary_key.column_3:
query += f", {primary_key.column_3}"
query += ") "
constraints = table.get("constraints", [])
for constraint in constraints:
column_1 = constraint["column_1"]
column_2 = constraint.get("column_2", None)
column_3 = constraint.get("column_3", None)
query += f", UNIQUE ({column_1}"
if column_2:
query += f", {column_2}"
if column_3:
query += f", {column_3}"
query += f", UNIQUE ({constraint.column_1}"
if constraint.column_2:
query += f", {constraint.column_2}"
if constraint.column_3:
query += f", {constraint.column_3}"
query += ") "
query += ")"
c.execute(query)
indices = table.get("indices", [])
for index in indices:
index_name = index["index_name"]
column_1 = index["column_1"]
column_2 = index.get("column_2", None)
column_3 = index.get("column_3", None)
query: str = f"CREATE INDEX {index_name} ON {table_name} ({column_1}"
if column_2:
query += f", {column_2}"
if column_3:
query += f", {column_3}"
for i in indices:
query: str = f"CREATE INDEX {i.name} ON {table_name} ({i.column_1}"
if i.column_2 is not None:
query += f", {i.column_2}"
if i.column_3 is not None:
query += f", {i.column_3}"
query += ")"
c.execute(query)
def create_db_(con, log) -> None:
db_schema = extract_schema()
c = con.cursor()
for table_name, table in db_schema.items():
create_table(c, table_name, table)
def create_db(db_path: str, log) -> None:
con = None
try:
con = sqlite3.connect(db_path)
create_db_(con, log)
con.commit()
finally:
if con:
@@ -1013,7 +898,6 @@ class DBMethods:
query += f"{key}=:{key}"
cursor.execute(query, values)
return cursor.lastrowid
def query(
self,
@@ -1031,12 +915,15 @@ class DBMethods:
table_name: str = table_class.__tablename__
query: str = "SELECT "
columns = []
for mc in inspect.getmembers(table_class):
mc_name, mc_obj = mc
if not hasattr(mc_obj, "__sqlite3_column__"):
continue
if len(columns) > 0:
query += ", "
query += mc_name
@@ -1044,32 +931,10 @@ class DBMethods:
query += f" FROM {table_name} WHERE 1=1 "
query_data = {}
for ck in constraints:
if not validColumnName(ck):
raise ValueError(f"Invalid constraint column: {ck}")
constraint_value = constraints[ck]
if isinstance(constraint_value, tuple) or isinstance(
constraint_value, list
):
if len(constraint_value) < 2:
raise ValueError(f"Too few constraint values for list: {ck}")
query += f" AND {ck} IN ("
for i, cv in enumerate(constraint_value):
cv_name: str = f"{ck}_{i}"
if i > 0:
query += ","
query += ":" + cv_name
query_data[cv_name] = cv
query += ") "
else:
if constraint_value is None:
query += f" AND {ck} IS NULL "
else:
query += f" AND {ck} = :{ck} "
query_data[ck] = constraint_value
for order_col, order_dir in order_by.items():
if validColumnName(order_col) is False:
@@ -1082,6 +947,7 @@ class DBMethods:
if query_suffix:
query += query_suffix
query_data = constraints.copy()
query_data.update(extra_query_data)
rows = cursor.execute(query, query_data)
for row in rows:

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -12,10 +12,8 @@ from .db import (
AutomationStrategy,
BidState,
Concepts,
create_table,
CURRENT_DB_DATA_VERSION,
CURRENT_DB_VERSION,
extract_schema,
)
from .basicswap_util import (
@@ -51,9 +49,10 @@ def upgradeDatabaseData(self, data_version):
return
self.log.info(
f"Upgrading database records from version {data_version} to {CURRENT_DB_DATA_VERSION}."
"Upgrading database records from version %d to %d.",
data_version,
CURRENT_DB_DATA_VERSION,
)
cursor = self.openDB()
try:
now = int(time.time())
@@ -105,19 +104,17 @@ def upgradeDatabaseData(self, data_version):
),
cursor,
)
if data_version > 0 and data_version < 6:
if data_version > 0 and data_version < 3:
for state in BidStates:
in_error = isErrorBidState(state)
swap_failed = isFailingBidState(state)
swap_ended = isFinalBidState(state)
can_accept = canAcceptBidState(state)
cursor.execute(
"UPDATE bidstates SET can_accept = :can_accept, in_error = :in_error, swap_failed = :swap_failed, swap_ended = :swap_ended WHERE state_id = :state_id",
"UPDATE bidstates SET in_error = :in_error, swap_failed = :swap_failed, swap_ended = :swap_ended WHERE state_id = :state_id",
{
"in_error": in_error,
"swap_failed": swap_failed,
"swap_ended": swap_ended,
"can_accept": can_accept,
"state_id": int(state),
},
)
@@ -139,137 +136,292 @@ def upgradeDatabaseData(self, data_version):
self.db_data_version = CURRENT_DB_DATA_VERSION
self.setIntKV("db_data_version", self.db_data_version, cursor)
self.commitDB()
self.log.info(f"Upgraded database records to version {self.db_data_version}")
self.log.info(
"Upgraded database records to version {}".format(self.db_data_version)
)
finally:
self.closeDB(cursor, commit=False)
def upgradeDatabase(self, db_version):
if self._force_db_upgrade is False and db_version >= CURRENT_DB_VERSION:
if db_version >= CURRENT_DB_VERSION:
return
self.log.info(
f"Upgrading database from version {db_version} to {CURRENT_DB_VERSION}."
)
# db_version, tablename, oldcolumnname, newcolumnname
rename_columns = [
(13, "actions", "event_id", "action_id"),
(13, "actions", "event_type", "action_type"),
(13, "actions", "event_data", "action_data"),
(
14,
"xmr_swaps",
"coin_a_lock_refund_spend_tx_msg_id",
"coin_a_lock_spend_tx_msg_id",
),
]
expect_schema = extract_schema()
have_tables = {}
while True:
try:
cursor = self.openDB()
for rename_column in rename_columns:
dbv, table_name, colname_from, colname_to = rename_column
if db_version < dbv:
current_version = db_version
if current_version == 6:
cursor.execute("ALTER TABLE bids ADD COLUMN security_token BLOB")
cursor.execute("ALTER TABLE offers ADD COLUMN security_token BLOB")
db_version += 1
elif current_version == 7:
cursor.execute("ALTER TABLE transactions ADD COLUMN block_hash BLOB")
cursor.execute(
f"ALTER TABLE {table_name} RENAME COLUMN {colname_from} TO {colname_to}"
"ALTER TABLE transactions ADD COLUMN block_height INTEGER"
)
query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
tables = cursor.execute(query).fetchall()
for table in tables:
table_name = table[0]
if table_name in ("sqlite_sequence",):
continue
have_table = {}
have_columns = {}
query = "SELECT * FROM PRAGMA_TABLE_INFO(:table_name) ORDER BY cid DESC;"
columns = cursor.execute(query, {"table_name": table_name}).fetchall()
for column in columns:
cid, name, data_type, notnull, default_value, primary_key = column
have_columns[name] = {"type": data_type, "primary_key": primary_key}
have_table["columns"] = have_columns
cursor.execute(f"PRAGMA INDEX_LIST('{table_name}');")
indices = cursor.fetchall()
for index in indices:
seq, index_name, unique, origin, partial = index
if origin == "pk": # Created by a PRIMARY KEY constraint
continue
cursor.execute(f"PRAGMA INDEX_INFO('{index_name}');")
index_info = cursor.fetchall()
add_index = {"index_name": index_name}
for index_columns in index_info:
seqno, cid, name = index_columns
if origin == "u": # Created by a UNIQUE constraint
have_columns[name]["unique"] = 1
else:
if "column_1" not in add_index:
add_index["column_1"] = name
elif "column_2" not in add_index:
add_index["column_2"] = name
elif "column_3" not in add_index:
add_index["column_3"] = name
else:
raise RuntimeError("Add more index columns.")
if origin == "c":
if "indices" not in table:
have_table["indices"] = []
have_table["indices"].append(add_index)
have_tables[table_name] = have_table
for table_name, table in expect_schema.items():
if table_name not in have_tables:
self.log.info(f"Creating table {table_name}.")
create_table(cursor, table_name, table)
continue
have_table = have_tables[table_name]
have_columns = have_table["columns"]
for colname, column in table["columns"].items():
if colname not in have_columns:
col_type = column["type"]
self.log.info(f"Adding column {colname} to table {table_name}.")
cursor.execute("ALTER TABLE transactions ADD COLUMN block_time INTEGER")
db_version += 1
elif current_version == 8:
cursor.execute(
f"ALTER TABLE {table_name} ADD COLUMN {colname} {col_type}"
"""
CREATE TABLE wallets (
record_id INTEGER NOT NULL,
coin_id INTEGER,
wallet_name VARCHAR,
wallet_data VARCHAR,
balance_type INTEGER,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
indices = table.get("indices", [])
have_indices = have_table.get("indices", [])
for index in indices:
index_name = index["index_name"]
if not any(
have_idx.get("index_name") == index_name
for have_idx in have_indices
):
self.log.info(f"Adding index {index_name} to table {table_name}.")
column_1 = index["column_1"]
column_2 = index.get("column_2", None)
column_3 = index.get("column_3", None)
query: str = (
f"CREATE INDEX {index_name} ON {table_name} ({column_1}"
db_version += 1
elif current_version == 9:
cursor.execute("ALTER TABLE wallets ADD COLUMN wallet_data VARCHAR")
db_version += 1
elif current_version == 10:
cursor.execute(
"ALTER TABLE smsgaddresses ADD COLUMN active_ind INTEGER"
)
cursor.execute(
"ALTER TABLE smsgaddresses ADD COLUMN created_at INTEGER"
)
cursor.execute("ALTER TABLE smsgaddresses ADD COLUMN note VARCHAR")
cursor.execute("ALTER TABLE smsgaddresses ADD COLUMN pubkey VARCHAR")
cursor.execute(
"UPDATE smsgaddresses SET active_ind = 1, created_at = 1"
)
if column_2:
query += f", {column_2}"
if column_3:
query += f", {column_3}"
query += ")"
cursor.execute(query)
if CURRENT_DB_VERSION != db_version:
self.db_version = CURRENT_DB_VERSION
self.setIntKV("db_version", CURRENT_DB_VERSION, cursor)
self.log.info(f"Upgraded database to version {self.db_version}")
cursor.execute("ALTER TABLE offers ADD COLUMN addr_to VARCHAR")
cursor.execute(f'UPDATE offers SET addr_to = "{self.network_addr}"')
db_version += 1
elif current_version == 11:
cursor.execute(
"ALTER TABLE bids ADD COLUMN chain_a_height_start INTEGER"
)
cursor.execute(
"ALTER TABLE bids ADD COLUMN chain_b_height_start INTEGER"
)
cursor.execute("ALTER TABLE bids ADD COLUMN protocol_version INTEGER")
cursor.execute("ALTER TABLE offers ADD COLUMN protocol_version INTEGER")
cursor.execute("ALTER TABLE transactions ADD COLUMN tx_data BLOB")
db_version += 1
elif current_version == 12:
cursor.execute(
"""
CREATE TABLE knownidentities (
record_id INTEGER NOT NULL,
address VARCHAR,
label VARCHAR,
publickey BLOB,
num_sent_bids_successful INTEGER,
num_recv_bids_successful INTEGER,
num_sent_bids_rejected INTEGER,
num_recv_bids_rejected INTEGER,
num_sent_bids_failed INTEGER,
num_recv_bids_failed INTEGER,
note VARCHAR,
updated_at BIGINT,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
cursor.execute("ALTER TABLE bids ADD COLUMN reject_code INTEGER")
cursor.execute("ALTER TABLE bids ADD COLUMN rate INTEGER")
cursor.execute(
"ALTER TABLE offers ADD COLUMN amount_negotiable INTEGER"
)
cursor.execute("ALTER TABLE offers ADD COLUMN rate_negotiable INTEGER")
db_version += 1
elif current_version == 13:
db_version += 1
cursor.execute(
"""
CREATE TABLE automationstrategies (
record_id INTEGER NOT NULL,
active_ind INTEGER,
label VARCHAR,
type_ind INTEGER,
only_known_identities INTEGER,
num_concurrent INTEGER,
data BLOB,
note VARCHAR,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
cursor.execute(
"""
CREATE TABLE automationlinks (
record_id INTEGER NOT NULL,
active_ind INTEGER,
linked_type INTEGER,
linked_id BLOB,
strategy_id INTEGER,
data BLOB,
repeat_limit INTEGER,
repeat_count INTEGER,
note VARCHAR,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
cursor.execute(
"""
CREATE TABLE history (
record_id INTEGER NOT NULL,
concept_type INTEGER,
concept_id INTEGER,
changed_data BLOB,
note VARCHAR,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
cursor.execute(
"""
CREATE TABLE bidstates (
record_id INTEGER NOT NULL,
active_ind INTEGER,
state_id INTEGER,
label VARCHAR,
in_progress INTEGER,
note VARCHAR,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
cursor.execute("ALTER TABLE wallets ADD COLUMN active_ind INTEGER")
cursor.execute(
"ALTER TABLE knownidentities ADD COLUMN active_ind INTEGER"
)
cursor.execute("ALTER TABLE eventqueue RENAME TO actions")
cursor.execute(
"ALTER TABLE actions RENAME COLUMN event_id TO action_id"
)
cursor.execute(
"ALTER TABLE actions RENAME COLUMN event_type TO action_type"
)
cursor.execute(
"ALTER TABLE actions RENAME COLUMN event_data TO action_data"
)
elif current_version == 14:
db_version += 1
cursor.execute(
"ALTER TABLE xmr_swaps ADD COLUMN coin_a_lock_release_msg_id BLOB"
)
cursor.execute(
"ALTER TABLE xmr_swaps RENAME COLUMN coin_a_lock_refund_spend_tx_msg_id TO coin_a_lock_spend_tx_msg_id"
)
elif current_version == 15:
db_version += 1
cursor.execute(
"""
CREATE TABLE notifications (
record_id INTEGER NOT NULL,
active_ind INTEGER,
event_type INTEGER,
event_data BLOB,
created_at BIGINT,
PRIMARY KEY (record_id))"""
)
elif current_version == 16:
db_version += 1
cursor.execute(
"""
CREATE TABLE prefunded_transactions (
record_id INTEGER NOT NULL,
active_ind INTEGER,
created_at BIGINT,
linked_type INTEGER,
linked_id BLOB,
tx_type INTEGER,
tx_data BLOB,
used_by BLOB,
PRIMARY KEY (record_id))"""
)
elif current_version == 17:
db_version += 1
cursor.execute(
"ALTER TABLE knownidentities ADD COLUMN automation_override INTEGER"
)
cursor.execute(
"ALTER TABLE knownidentities ADD COLUMN visibility_override INTEGER"
)
cursor.execute("ALTER TABLE knownidentities ADD COLUMN data BLOB")
cursor.execute("UPDATE knownidentities SET active_ind = 1")
elif current_version == 18:
db_version += 1
cursor.execute("ALTER TABLE xmr_split_data ADD COLUMN addr_from STRING")
cursor.execute("ALTER TABLE xmr_split_data ADD COLUMN addr_to STRING")
elif current_version == 19:
db_version += 1
cursor.execute("ALTER TABLE bidstates ADD COLUMN in_error INTEGER")
cursor.execute("ALTER TABLE bidstates ADD COLUMN swap_failed INTEGER")
cursor.execute("ALTER TABLE bidstates ADD COLUMN swap_ended INTEGER")
elif current_version == 20:
db_version += 1
cursor.execute(
"""
CREATE TABLE message_links (
record_id INTEGER NOT NULL,
active_ind INTEGER,
created_at BIGINT,
linked_type INTEGER,
linked_id BLOB,
msg_type INTEGER,
msg_sequence INTEGER,
msg_id BLOB,
PRIMARY KEY (record_id))"""
)
cursor.execute("ALTER TABLE offers ADD COLUMN bid_reversed INTEGER")
elif current_version == 21:
db_version += 1
cursor.execute("ALTER TABLE offers ADD COLUMN proof_utxos BLOB")
cursor.execute("ALTER TABLE bids ADD COLUMN proof_utxos BLOB")
elif current_version == 22:
db_version += 1
cursor.execute("ALTER TABLE offers ADD COLUMN amount_to INTEGER")
elif current_version == 23:
db_version += 1
cursor.execute(
"""
CREATE TABLE checkedblocks (
record_id INTEGER NOT NULL,
created_at BIGINT,
coin_type INTEGER,
block_height INTEGER,
block_hash BLOB,
block_time INTEGER,
PRIMARY KEY (record_id))"""
)
cursor.execute("ALTER TABLE bids ADD COLUMN pkhash_buyer_to BLOB")
elif current_version == 24:
db_version += 1
cursor.execute("ALTER TABLE bidstates ADD COLUMN can_accept INTEGER")
if current_version != db_version:
self.db_version = db_version
self.setIntKV("db_version", db_version, cursor)
self.commitDB()
self.log.info("Upgraded database to version {}".format(self.db_version))
continue
except Exception as e:
self.log.error(f"Upgrade failed {e}")
self.log.error("Upgrade failed {}".format(e))
self.rollbackDB()
finally:
self.closeDB(cursor, commit=False)
break
if db_version != CURRENT_DB_VERSION:
raise ValueError("Unable to upgrade database.")

View File

@@ -76,10 +76,6 @@ def remove_expired_data(self, time_offset: int = 0):
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :linked_id",
{"type_ind": int(Concepts.BID), "linked_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM direct_message_route_links WHERE linked_type = :type_ind AND linked_id = :linked_id",
{"type_ind": int(Concepts.BID), "linked_id": bid_row[0]},
)
cursor.execute(
"DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :offer_id",

View File

@@ -1,19 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2023 tecnovert
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
default_chart_api_key = (
"95dd900af910656e0e17c41f2ddc5dba77d01bf8b0e7d2787634a16bd976c553"
)
default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj"
class Explorer:
def __init__(self, swapclient, coin_type, base_url):
self.swapclient = swapclient

View File

@@ -1,30 +1,25 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import json
import shlex
import secrets
import traceback
import threading
import http.client
import base64
from urllib import parse
from http.server import BaseHTTPRequestHandler, HTTPServer
from jinja2 import Environment, PackageLoader
from socket import error as SocketError
from urllib import parse
from datetime import datetime, timedelta, timezone
from http.cookies import SimpleCookie
from . import __version__
from .util import (
dumpj,
toBool,
LockedCoinError,
format_timestamp,
)
from .chainparams import (
@@ -35,7 +30,6 @@ from .basicswap_util import (
strTxState,
strBidState,
)
from .util.rfc2440 import verify_rfc2440_password
from .js_server import (
js_error,
@@ -53,7 +47,6 @@ from .ui.page_automation import (
page_automation_strategy_new,
)
from .ui.page_amm import page_amm, amm_status_api, amm_autostart_api, amm_debug_api
from .ui.page_bids import page_bids, page_bid
from .ui.page_offers import page_offers, page_offer, page_newoffer
from .ui.page_tor import page_tor, get_tor_established_state
@@ -64,9 +57,6 @@ from .ui.page_identity import page_identity
from .ui.page_smsgaddresses import page_smsgaddresses
from .ui.page_debug import page_debug
SESSION_COOKIE_NAME = "basicswap_session_id"
SESSION_DURATION_MINUTES = 60
env = Environment(loader=PackageLoader("basicswap", "templates"))
env.filters["formatts"] = format_timestamp
@@ -129,57 +119,6 @@ def parse_cmd(cmd: str, type_map: str):
class HttpHandler(BaseHTTPRequestHandler):
def _get_session_cookie(self):
if "Cookie" in self.headers:
cookie = SimpleCookie(self.headers["Cookie"])
if SESSION_COOKIE_NAME in cookie:
return cookie[SESSION_COOKIE_NAME].value
return None
def _set_session_cookie(self, session_id):
cookie = SimpleCookie()
cookie[SESSION_COOKIE_NAME] = session_id
cookie[SESSION_COOKIE_NAME]["path"] = "/"
cookie[SESSION_COOKIE_NAME]["httponly"] = True
cookie[SESSION_COOKIE_NAME]["samesite"] = "Lax"
expires = datetime.now(timezone.utc) + timedelta(
minutes=SESSION_DURATION_MINUTES
)
cookie[SESSION_COOKIE_NAME]["expires"] = expires.strftime(
"%a, %d %b %Y %H:%M:%S GMT"
)
return ("Set-Cookie", cookie.output(header="").strip())
def _clear_session_cookie(self):
cookie = SimpleCookie()
cookie[SESSION_COOKIE_NAME] = ""
cookie[SESSION_COOKIE_NAME]["path"] = "/"
cookie[SESSION_COOKIE_NAME]["httponly"] = True
cookie[SESSION_COOKIE_NAME]["expires"] = "Thu, 01 Jan 1970 00:00:00 GMT"
return ("Set-Cookie", cookie.output(header="").strip())
def is_authenticated(self):
swap_client = self.server.swap_client
client_auth_hash = swap_client.settings.get("client_auth_hash")
if not client_auth_hash:
return True
session_id = self._get_session_cookie()
if not session_id:
return False
session_data = self.server.active_sessions.get(session_id)
if session_data and session_data["expires"] > datetime.now(timezone.utc):
session_data["expires"] = datetime.now(timezone.utc) + timedelta(
minutes=SESSION_DURATION_MINUTES
)
return True
if session_id in self.server.active_sessions:
del self.server.active_sessions[session_id]
return False
def log_error(self, format, *args):
super().log_message(format, *args)
@@ -202,12 +141,7 @@ class HttpHandler(BaseHTTPRequestHandler):
return form_data
def render_template(
self,
template,
args_dict,
status_code=200,
version=__version__,
extra_headers=None,
self, template, args_dict, status_code=200, version=__version__
):
swap_client = self.server.swap_client
if swap_client.ws_server:
@@ -218,6 +152,7 @@ class HttpHandler(BaseHTTPRequestHandler):
args_dict["debug_ui_mode"] = True
if swap_client.use_tor_proxy:
args_dict["use_tor_proxy"] = True
# TODO: Cache value?
try:
tor_state = get_tor_established_state(swap_client)
args_dict["tor_established"] = True if tor_state == "1" else False
@@ -227,18 +162,6 @@ class HttpHandler(BaseHTTPRequestHandler):
swap_client.log.error(f"Error getting Tor state: {str(e)}")
swap_client.log.error(traceback.format_exc())
from .ui.page_amm import get_amm_status, get_amm_active_count
try:
args_dict["current_status"] = get_amm_status()
args_dict["amm_active_count"] = get_amm_active_count(swap_client)
except Exception as e:
args_dict["current_status"] = "stopped"
args_dict["amm_active_count"] = 0
if swap_client.debug:
swap_client.log.error(f"Error getting AMM state: {str(e)}")
swap_client.log.error(traceback.format_exc())
if swap_client._show_notifications:
args_dict["notifications"] = swap_client.getNotifications()
@@ -255,16 +178,6 @@ class HttpHandler(BaseHTTPRequestHandler):
self.server.msg_id_counter += 1
args_dict["err_messages"] = err_messages_with_ids
if self.path:
parsed = parse.urlparse(self.path)
url_split = parsed.path.split("/")
if len(url_split) > 1 and url_split[1]:
args_dict["current_page"] = url_split[1]
else:
args_dict["current_page"] = "index"
else:
args_dict["current_page"] = "index"
shutdown_token = os.urandom(8).hex()
self.server.session_tokens["shutdown"] = shutdown_token
args_dict["shutdown_token"] = shutdown_token
@@ -278,7 +191,7 @@ class HttpHandler(BaseHTTPRequestHandler):
args_dict["version"] = version
self.putHeaders(status_code, "text/html", extra_headers=extra_headers)
self.putHeaders(status_code, "text/html")
return bytes(
template.render(
title=self.server.title,
@@ -290,7 +203,6 @@ class HttpHandler(BaseHTTPRequestHandler):
)
def render_simple_template(self, template, args_dict):
self.putHeaders(200, "text/html")
return bytes(
template.render(
title=self.server.title,
@@ -299,7 +211,7 @@ class HttpHandler(BaseHTTPRequestHandler):
"UTF-8",
)
def page_info(self, info_str, post_string=None, extra_headers=None):
def page_info(self, info_str, post_string=None):
template = env.get_template("info.html")
swap_client = self.server.swap_client
summary = swap_client.getSummary()
@@ -310,7 +222,6 @@ class HttpHandler(BaseHTTPRequestHandler):
"message_str": info_str,
"summary": summary,
},
extra_headers=extra_headers,
)
def page_error(self, error_str, post_string=None):
@@ -326,99 +237,6 @@ class HttpHandler(BaseHTTPRequestHandler):
},
)
def page_login(self, url_split, post_string):
swap_client = self.server.swap_client
template = env.get_template("login.html")
err_messages = []
extra_headers = []
is_json_request = "application/json" in self.headers.get("Content-Type", "")
security_warning = None
if self.server.host_name not in ("127.0.0.1", "localhost"):
security_warning = "WARNING: Server is accessible on the network. Sending password over plain HTTP is insecure. Use HTTPS (e.g., via reverse proxy) for non-local access."
if not is_json_request:
err_messages.append(security_warning)
if post_string:
password = None
if is_json_request:
try:
json_data = json.loads(post_string.decode("utf-8"))
password = json_data.get("password")
except Exception as e:
swap_client.log.error(f"Error parsing JSON login data: {e}")
else:
try:
form_data = parse.parse_qs(post_string.decode("utf-8"))
password = form_data.get("password", [None])[0]
except Exception as e:
swap_client.log.error(f"Error parsing form login data: {e}")
client_auth_hash = swap_client.settings.get("client_auth_hash")
if (
client_auth_hash
and password is not None
and verify_rfc2440_password(client_auth_hash, password)
):
session_id = secrets.token_urlsafe(32)
expires = datetime.now(timezone.utc) + timedelta(
minutes=SESSION_DURATION_MINUTES
)
self.server.active_sessions[session_id] = {"expires": expires}
cookie_header = self._set_session_cookie(session_id)
if is_json_request:
response_data = {"success": True, "session_id": session_id}
if security_warning:
response_data["warning"] = security_warning
self.putHeaders(
200, "application/json", extra_headers=[cookie_header]
)
return json.dumps(response_data).encode("utf-8")
else:
self.send_response(302)
self.send_header("Location", "/offers")
self.send_header(cookie_header[0], cookie_header[1])
self.end_headers()
return b""
else:
if is_json_request:
self.putHeaders(401, "application/json")
return json.dumps({"error": "Invalid password"}).encode("utf-8")
else:
err_messages.append("Invalid password.")
clear_cookie_header = self._clear_session_cookie()
extra_headers.append(clear_cookie_header)
if (
not is_json_request
and swap_client.settings.get("client_auth_hash")
and self.is_authenticated()
):
self.send_response(302)
self.send_header("Location", "/offers")
self.end_headers()
return b""
return self.render_template(
template,
{
"title_str": "Login",
"err_messages": err_messages,
"summary": {},
"encrypted": False,
"locked": False,
},
status_code=401 if post_string and not is_json_request else 200,
extra_headers=extra_headers,
)
def page_shutdown_ping(self, url_split, post_string):
if not self.server.stop_event.is_set():
raise ValueError("Unexpected shutdown ping.")
self.putHeaders(401, "application/json")
return json.dumps({"ack": True}).encode("utf-8")
def page_explorers(self, url_split, post_string):
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
@@ -432,10 +250,14 @@ class HttpHandler(BaseHTTPRequestHandler):
form_data = self.checkForm(post_string, "explorers", err_messages)
if form_data:
explorer = get_data_entry(form_data, "explorer")
action = get_data_entry(form_data, "action")
args = get_data_entry_or(form_data, "args", "")
explorer = form_data[b"explorer"][0].decode("utf-8")
action = form_data[b"action"][0].decode("utf-8")
args = (
""
if b"args" not in form_data
else form_data[b"args"][0].decode("utf-8")
)
try:
c, e = explorer.split("_")
exp = swap_client.coin_clients[Coins(int(c))]["explorers"][int(e)]
@@ -588,6 +410,7 @@ class HttpHandler(BaseHTTPRequestHandler):
return self.render_template(
template,
{
"refresh": 30,
"active_swaps": [
(
s[0].hex(),
@@ -624,7 +447,6 @@ class HttpHandler(BaseHTTPRequestHandler):
def page_shutdown(self, url_split, post_string):
swap_client = self.server.swap_client
extra_headers = []
if len(url_split) > 2:
token = url_split[2]
@@ -632,42 +454,9 @@ class HttpHandler(BaseHTTPRequestHandler):
if token != expect_token:
return self.page_info("Unexpected token, still running.")
session_id = self._get_session_cookie()
if session_id and session_id in self.server.active_sessions:
del self.server.active_sessions[session_id]
clear_cookie_header = self._clear_session_cookie()
extra_headers.append(clear_cookie_header)
try:
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
amm_status = get_amm_status()
if amm_status == "running":
swap_client.log.info("Web shutdown stopping AMM process...")
success, msg = stop_amm_process(swap_client)
if success:
swap_client.log.info(f"AMM web shutdown: {msg}")
else:
swap_client.log.warning(f"AMM web shutdown warning: {msg}")
except Exception as e:
swap_client.log.error(f"Error stopping AMM in web shutdown: {e}")
swap_client.stopRunning()
return self.page_info("Shutting down", extra_headers=extra_headers)
def page_donation(self, url_split, post_string):
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
summary = swap_client.getSummary()
template = env.get_template("donation.html")
return self.render_template(
template,
{
"summary": summary,
},
)
return self.page_info("Shutting down")
def page_index(self, url_split):
swap_client = self.server.swap_client
@@ -688,109 +477,41 @@ class HttpHandler(BaseHTTPRequestHandler):
},
)
def putHeaders(self, status_code, content_type, extra_headers=None):
def putHeaders(self, status_code, content_type):
self.send_response(status_code)
if self.server.allow_cors:
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Type", content_type)
if extra_headers:
for header_tuple in extra_headers:
self.send_header(header_tuple[0], header_tuple[1])
self.end_headers()
def handle_http(self, status_code, path, post_string="", is_json=False):
from basicswap.util import LockedCoinError
from basicswap.ui.util import getCoinName
swap_client = self.server.swap_client
parsed = parse.urlparse(self.path)
url_split = parsed.path.split("/")
page = url_split[1] if len(url_split) > 1 else ""
exempt_pages = ["login", "static", "error", "info"]
auth_header = self.headers.get("Authorization")
basic_auth_ok = False
if auth_header and auth_header.startswith("Basic "):
try:
encoded_creds = auth_header.split(" ", 1)[1]
decoded_creds = base64.b64decode(encoded_creds).decode("utf-8")
_, password = decoded_creds.split(":", 1)
client_auth_hash = swap_client.settings.get("client_auth_hash")
if client_auth_hash and verify_rfc2440_password(
client_auth_hash, password
):
basic_auth_ok = True
else:
self.send_response(401)
self.send_header("WWW-Authenticate", 'Basic realm="Basicswap"')
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(
json.dumps({"error": "Invalid Basic Auth credentials"}).encode(
"utf-8"
)
)
return b""
except Exception as e:
swap_client.log.error(f"Error processing Basic Auth header: {e}")
self.send_response(401)
self.send_header("WWW-Authenticate", 'Basic realm="Basicswap"')
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(
json.dumps({"error": "Malformed Basic Auth header"}).encode("utf-8")
)
return b""
if not basic_auth_ok and page not in exempt_pages:
if not self.is_authenticated():
if page == "json":
self.putHeaders(401, "application/json")
self.wfile.write(
json.dumps({"error": "Unauthorized"}).encode("utf-8")
)
return b""
else:
self.send_response(302)
self.send_header("Location", "/login")
clear_cookie_header = self._clear_session_cookie()
self.send_header(clear_cookie_header[0], clear_cookie_header[1])
self.end_headers()
return b""
if not post_string and len(parsed.query) > 0:
if post_string == "" and len(parsed.query) > 0:
post_string = parsed.query
if page == "json":
if len(url_split) > 1 and url_split[1] == "json":
try:
self.putHeaders(status_code, "json")
self.putHeaders(status_code, "text/plain")
func = js_url_to_function(url_split)
return func(self, url_split, post_string, is_json)
except Exception as ex:
if isinstance(ex, LockedCoinError):
clean_msg = f"Wallet locked: {getCoinName(ex.coinid)} wallet must be unlocked"
swap_client.log.warning(clean_msg)
return js_error(self, clean_msg)
elif swap_client.debug is True:
if swap_client.debug is True:
swap_client.log.error(traceback.format_exc())
return js_error(self, str(ex))
if page == "static":
if len(url_split) > 1 and url_split[1] == "static":
try:
static_path = os.path.join(os.path.dirname(__file__), "static")
content = None
mime_type = ""
filepath = ""
if len(url_split) > 3 and url_split[2] == "sequence_diagrams":
filepath = os.path.join(
static_path, "sequence_diagrams", url_split[3]
)
mime_type = "image/svg+xml"
with open(
os.path.join(static_path, "sequence_diagrams", url_split[3]),
"rb",
) as fp:
self.putHeaders(status_code, "image/svg+xml")
return fp.read()
elif len(url_split) > 3 and url_split[2] == "images":
filename = os.path.join(*url_split[3:])
filepath = os.path.join(static_path, "images", filename)
_, extension = os.path.splitext(filename)
mime_type = {
".svg": "image/svg+xml",
@@ -799,25 +520,25 @@ class HttpHandler(BaseHTTPRequestHandler):
".gif": "image/gif",
".ico": "image/x-icon",
}.get(extension, "")
if mime_type == "":
raise ValueError("Unknown file type " + filename)
with open(
os.path.join(static_path, "images", filename), "rb"
) as fp:
self.putHeaders(status_code, mime_type)
return fp.read()
elif len(url_split) > 3 and url_split[2] == "css":
filename = os.path.join(*url_split[3:])
filepath = os.path.join(static_path, "css", filename)
mime_type = "text/css; charset=utf-8"
with open(os.path.join(static_path, "css", filename), "rb") as fp:
self.putHeaders(status_code, "text/css; charset=utf-8")
return fp.read()
elif len(url_split) > 3 and url_split[2] == "js":
filename = os.path.join(*url_split[3:])
filepath = os.path.join(static_path, "js", filename)
mime_type = "application/javascript"
with open(os.path.join(static_path, "js", filename), "rb") as fp:
self.putHeaders(status_code, "application/javascript")
return fp.read()
else:
return self.page_404(url_split)
if mime_type == "" or not filepath:
raise ValueError("Unknown file type or path")
with open(filepath, "rb") as fp:
content = fp.read()
self.putHeaders(status_code, mime_type)
return content
except FileNotFoundError:
return self.page_404(url_split)
except Exception as ex:
@@ -829,10 +550,6 @@ class HttpHandler(BaseHTTPRequestHandler):
if len(url_split) > 1:
page = url_split[1]
if page == "login":
return self.page_login(url_split, post_string)
if page == "shutdown_ping":
return self.page_shutdown_ping(url_split, post_string)
if page == "active":
return self.page_active(url_split, post_string)
if page == "wallets":
@@ -867,8 +584,6 @@ class HttpHandler(BaseHTTPRequestHandler):
return page_bids(self, url_split, post_string, available=True)
if page == "watched":
return self.page_watched(url_split, post_string)
if page == "donation":
return self.page_donation(url_split, post_string)
if page == "smsgaddresses":
return page_smsgaddresses(self, url_split, post_string)
if page == "identity":
@@ -881,41 +596,6 @@ class HttpHandler(BaseHTTPRequestHandler):
return page_automation_strategy(self, url_split, post_string)
if page == "newautomationstrategy":
return page_automation_strategy_new(self, url_split, post_string)
if page == "amm":
if len(url_split) > 2 and url_split[2] == "status":
query_params = {}
if parsed.query:
query_params = {
k: v[0] for k, v in parse.parse_qs(parsed.query).items()
}
status_data = amm_status_api(
swap_client, self.path, query_params
)
self.putHeaders(200, "application/json")
return json.dumps(status_data).encode("utf-8")
elif len(url_split) > 2 and url_split[2] == "autostart":
query_params = {}
if parsed.query:
query_params = {
k: v[0] for k, v in parse.parse_qs(parsed.query).items()
}
autostart_data = amm_autostart_api(
swap_client, post_string, query_params
)
self.putHeaders(200, "application/json")
return json.dumps(autostart_data).encode("utf-8")
elif len(url_split) > 2 and url_split[2] == "debug":
query_params = {}
if parsed.query:
query_params = {
k: v[0] for k, v in parse.parse_qs(parsed.query).items()
}
debug_data = amm_debug_api(
swap_client, post_string, query_params
)
self.putHeaders(200, "application/json")
return json.dumps(debug_data).encode("utf-8")
return page_amm(self, url_split, post_string)
if page == "shutdown":
return self.page_shutdown(url_split, post_string)
if page == "changepassword":
@@ -936,21 +616,14 @@ class HttpHandler(BaseHTTPRequestHandler):
def do_GET(self):
response = self.handle_http(200, self.path)
try:
self.wfile.write(response)
except SocketError as e:
self.server.swap_client.log.debug(f"do_GET SocketError {e}")
def do_POST(self):
content_length = int(self.headers.get("Content-Length", 0))
post_string = self.rfile.read(content_length)
post_string = self.rfile.read(int(self.headers.get("Content-Length")))
is_json = True if "json" in self.headers.get("Content-Type", "") else False
response = self.handle_http(200, self.path, post_string, is_json)
try:
self.wfile.write(response)
except SocketError as e:
self.server.swap_client.log.debug(f"do_POST SocketError {e}")
def do_HEAD(self):
self.putHeaders(200, "text/html")
@@ -964,10 +637,11 @@ class HttpHandler(BaseHTTPRequestHandler):
class HttpThread(threading.Thread, HTTPServer):
def __init__(self, host_name, port_no, allow_cors, swap_client):
def __init__(self, fp, host_name, port_no, allow_cors, swap_client):
threading.Thread.__init__(self)
self.stop_event = threading.Event()
self.fp = fp
self.host_name = host_name
self.port_no = port_no
self.allow_cors = allow_cors
@@ -975,7 +649,6 @@ class HttpThread(threading.Thread, HTTPServer):
self.title = "BasicSwap - " + __version__
self.last_form_id = dict()
self.session_tokens = dict()
self.active_sessions = {}
self.env = env
self.msg_id_counter = 0
@@ -985,19 +658,18 @@ class HttpThread(threading.Thread, HTTPServer):
def stop(self):
self.stop_event.set()
try:
conn = http.client.HTTPConnection(self.host_name, self.port_no, timeout=0.5)
conn.request("GET", "/shutdown_ping")
# Send fake request
conn = http.client.HTTPConnection(self.host_name, self.port_no)
conn.connect()
conn.request("GET", "/none")
response = conn.getresponse()
_ = response.read()
conn.close()
except Exception:
pass
def serve_forever(self):
self.timeout = 1
while not self.stop_event.is_set():
self.handle_request()
self.socket.close()
self.swap_client.log.info("HTTP server stopped.")
def run(self):
self.serve_forever()

View File

@@ -79,7 +79,6 @@ class BCHInterface(BTCInterface):
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
)
self.rpc_wallet_watch = self.rpc_wallet
def has_segwit(self) -> bool:
# bch does not have segwit, but we return true here to avoid extra checks in basicswap.py
@@ -107,31 +106,6 @@ class BCHInterface(BTCInterface):
) + self.make_int(u["amount"], r=1)
return unspent_addr
def createWallet(self, wallet_name: str, password: str = ""):
self.rpc("createwallet", [wallet_name, False])
if password != "":
self.rpc(
"encryptwallet",
[
password,
],
override_wallet=wallet_name,
)
def newKeypool(self) -> None:
self._log.debug("Refreshing keypool.")
# Use up current keypool
wi = self.rpc_wallet("getwalletinfo")
keypool_size: int = wi["keypoolsize"]
for i in range(keypool_size):
_ = self.rpc_wallet("getnewaddress")
keypoolsize_hd_internal: int = wi["keypoolsize_hd_internal"]
for i in range(keypoolsize_hd_internal):
_ = self.rpc_wallet("getrawchangeaddress")
self.rpc_wallet("keypoolrefill")
# returns pkh
def decodeAddress(self, address: str) -> bytes:
return bytes(Address.from_string(address).payload)

View File

@@ -10,13 +10,8 @@ import base64
import hashlib
import json
import logging
import mmap
import os
import shutil
import sqlite3
import traceback
from io import BytesIO
from basicswap.basicswap_util import (
@@ -315,21 +310,6 @@ class BTCInterface(Secp256k1Interface):
def checkWallets(self) -> int:
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_wallet(
"loadwallet",
[
self._rpc_wallet,
],
)
wallets = self.rpc("listwallets")
except Exception as e:
self._log.debug(f'Error loading wallet "self._rpc_wallet": {e}.')
# Wallet name is "" for some LTC and PART installs on older cores
if self._rpc_wallet not in wallets and len(wallets) > 0:
self._log.warning(f"Changing {self.ticker()} wallet name.")
@@ -397,7 +377,7 @@ class BTCInterface(Secp256k1Interface):
last_block_header = prev_block_header
raise ValueError(f"Block header not found at time: {time}")
def initialiseWallet(self, key_bytes: bytes, restore_time: int = -1) -> None:
def initialiseWallet(self, key_bytes: bytes) -> None:
assert len(key_bytes) == 32
self._have_checked_seed = False
if self._use_descriptors:
@@ -407,7 +387,6 @@ class BTCInterface(Secp256k1Interface):
ek_encoded: str = self.encode_secret_extkey(ek.encode_v())
desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)")
desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)")
rv = self.rpc_wallet(
"importdescriptors",
[
@@ -415,7 +394,7 @@ class BTCInterface(Secp256k1Interface):
{"desc": desc_external, "timestamp": "now", "active": True},
{
"desc": desc_internal,
"timestamp": "now" if restore_time == -1 else restore_time,
"timestamp": "now",
"active": True,
"internal": True,
},
@@ -432,18 +411,7 @@ class BTCInterface(Secp256k1Interface):
raise ValueError("Failed to import descriptors.")
else:
key_wif = self.encodeKey(key_bytes)
try:
self.rpc_wallet("sethdseed", [True, key_wif])
except Exception as e:
self._log.debug(f"sethdseed failed: {e}")
"""
# TODO: Find derived key counts
if "Already have this key" in str(e):
key_id: bytes = self.getSeedHash(key_bytes)
self.setActiveKeyChain(key_id)
else:
"""
raise (e)
def getWalletInfo(self):
rv = self.rpc_wallet("getwalletinfo")
@@ -487,6 +455,10 @@ class BTCInterface(Secp256k1Interface):
self.close_rpc(rpc_conn)
raise ValueError(f"{self.coin_name()} wallet restore height not found.")
def getWalletSeedID(self) -> str:
wi = self.rpc_wallet("getwalletinfo")
return "Not found" if "hdseedid" not in wi else wi["hdseedid"]
def getActiveDescriptor(self):
descriptors = self.rpc_wallet("listdescriptors")["descriptors"]
for descriptor in descriptors:
@@ -498,24 +470,21 @@ class BTCInterface(Secp256k1Interface):
return descriptor
return None
def getWalletSeedID(self) -> str:
def checkExpectedSeed(self, expect_seedid: str) -> bool:
if self._use_descriptors:
descriptor = self.getActiveDescriptor()
if descriptor is None:
self._log.debug("Could not find active descriptor.")
return "Not found"
return False
end = descriptor["desc"].find("/")
if end < 10:
return "Not found"
return False
extkey = descriptor["desc"][5:end]
extkey_data = b58decode(extkey)[4:-4]
extkey_data_hash: bytes = hash160(extkey_data)
return extkey_data_hash.hex()
return True if extkey_data_hash.hex() == expect_seedid else False
wi = self.rpc_wallet("getwalletinfo")
return "Not found" if "hdseedid" not in wi else wi["hdseedid"]
def checkExpectedSeed(self, expect_seedid: str) -> bool:
wallet_seed_id = self.getWalletSeedID()
self._expect_seedid_hex = expect_seedid
self._have_checked_seed = True
@@ -561,7 +530,7 @@ class BTCInterface(Secp256k1Interface):
override_feerate = chain_client_settings.get("override_feerate", None)
if override_feerate:
self._log.debug(
f"Fee rate override used for {self.coin_name()}: {override_feerate}"
"Fee rate override used for %s: %f", self.coin_name(), override_feerate
)
return override_feerate, "override_feerate"
@@ -1318,37 +1287,22 @@ class BTCInterface(Secp256k1Interface):
rv = self.rpc_wallet("fundrawtransaction", [tx.hex(), options])
return bytes.fromhex(rv["hex"])
def getNonSegwitOutputs(self):
unspents = self.rpc_wallet("listunspent", [0, 99999999])
nonsegwit_unspents = []
for u in unspents:
def lockNonSegwitPrevouts(self) -> None:
# For tests
unspent = self.rpc_wallet("listunspent")
to_lock = []
for u in unspent:
if u.get("spendable", False) is False:
continue
if "desc" in u:
desc = u["desc"]
if self.use_p2shp2wsh():
if not desc.startswith("sh(wpkh"):
nonsegwit_unspents.append(
{
"txid": u["txid"],
"vout": u["vout"],
"amount": u["amount"],
}
)
to_lock.append({"txid": u["txid"], "vout": u["vout"]})
else:
if not desc.startswith("wpkh"):
nonsegwit_unspents.append(
{
"txid": u["txid"],
"vout": u["vout"],
"amount": u["amount"],
}
)
return nonsegwit_unspents
def lockNonSegwitPrevouts(self) -> None:
# For tests
to_lock = self.getNonSegwitOutputs()
to_lock.append({"txid": u["txid"], "vout": u["vout"]})
if len(to_lock) > 0:
self._log.debug(f"Locking {len(to_lock)} non segwit prevouts")
@@ -1423,9 +1377,6 @@ class BTCInterface(Secp256k1Interface):
def getScriptDest(self, script):
return CScript([OP_0, sha256(script)])
def getP2WSHScriptDest(self, script):
return CScript([OP_0, sha256(script)])
def getScriptScriptSig(self, script: bytes) -> bytes:
return bytes()
@@ -1525,14 +1476,7 @@ class BTCInterface(Secp256k1Interface):
return (weight + wsf - 1) // wsf
def findTxB(
self,
kbv,
Kbs,
cb_swap_value: int,
cb_block_confirmed: int,
restore_height: int,
bid_sender: bool,
check_amount: bool = True,
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
):
dest_address = (
self.pubkey_to_segwit_address(Kbs)
@@ -1676,7 +1620,7 @@ class BTCInterface(Secp256k1Interface):
"listunspent",
[
0,
99999999,
9999999,
[
dest_address,
],
@@ -1918,69 +1862,19 @@ class BTCInterface(Secp256k1Interface):
"Could not find address with enough funds for proof",
)
self._log.debug(f"sign_for_addr {sign_for_addr}")
self._log.debug("sign_for_addr %s", sign_for_addr)
funds_addr: str = sign_for_addr
if (
self.using_segwit()
): # TODO: Use isSegwitAddress when scantxoutset can use combo
# 'Address does not refer to key' for non p2pkh
pkh = self.decodeAddress(sign_for_addr)
sign_for_addr = self.pkh_to_address(pkh)
self._log.debug(f"sign_for_addr converted {sign_for_addr}")
self._log.debug("sign_for_addr converted %s", sign_for_addr)
if self._use_descriptors:
# https://github.com/bitcoin/bitcoin/issues/10542
# https://github.com/bitcoin/bitcoin/issues/26046
priv_keys = self.rpc_wallet(
"listdescriptors",
[
True,
],
)
addr_info = self.rpc_wallet(
"getaddressinfo",
[
funds_addr,
],
)
hdkeypath = addr_info["hdkeypath"]
sign_for_address_key = None
for descriptor in priv_keys["descriptors"]:
if descriptor["active"] is False or descriptor["internal"] is True:
continue
desc = descriptor["desc"]
assert desc.startswith("wpkh(")
ext_key = desc[5:].split(")")[0].split("/", 1)[0]
ext_key_data = decodeAddress(ext_key)[4:]
ci_part = self._sc.ci(Coins.PART)
ext_key_data_part = ci_part.encode_secret_extkey(ext_key_data)
rv = ci_part.rpc_wallet(
"extkey", ["info", ext_key_data_part, hdkeypath]
)
extkey_derived = rv["key_info"]["result"]
ext_key_data = decodeAddress(extkey_derived)[4:]
ek = ExtKeyPair()
ek.decode(ext_key_data)
sign_for_address_key = self.encodeKey(ek._key)
break
assert sign_for_address_key is not None
signature = self.rpc(
"signmessagewithprivkey",
[
sign_for_address_key,
sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex(),
],
)
del priv_keys
else:
signature = self.rpc_wallet(
"signmessage",
[
sign_for_addr,
sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex(),
],
[sign_for_addr, sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex()],
)
prove_utxos = [] # TODO: Send specific utxos
@@ -2034,237 +1928,15 @@ class BTCInterface(Secp256k1Interface):
locked = encrypted and wallet_info["unlocked_until"] <= 0
return encrypted, locked
def createWallet(self, wallet_name: str, password: str = "") -> None:
self.rpc(
"createwallet",
[wallet_name, False, True, password, False, self._use_descriptors],
)
def setActiveWallet(self, wallet_name: str) -> None:
# For debugging
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host, wallet=wallet_name
)
self._rpc_wallet = wallet_name
def newKeypool(self) -> None:
self._log.debug("Running newkeypool.")
self.rpc_wallet("newkeypool")
def encryptWallet(self, password: str, check_seed: bool = True):
# Watchonly wallets are not encrypted
# Workaround for https://github.com/bitcoin/bitcoin/issues/26607
seed_id_before: str = self.getWalletSeedID()
orig_active_descriptors = []
orig_hdchain_bytes = None
walletpath = None
max_hdchain_key_count: int = 4000000 # Arbitrary
chain_client_settings = self._sc.getChainClientSettings(
self.coin_type()
) # basicswap.json
if (
chain_client_settings.get("manage_daemon", False)
and check_seed is True
and seed_id_before != "Not found"
):
# Store active keys
self.rpc("unloadwallet", [self._rpc_wallet])
datadir = chain_client_settings["datadir"]
if self._network != "mainnet":
datadir = os.path.join(datadir, self._network)
try_wallet_path = os.path.join(datadir, self._rpc_wallet)
if os.path.exists(try_wallet_path):
walletpath = try_wallet_path
else:
try_wallet_path = os.path.join(datadir, "wallets", self._rpc_wallet)
if os.path.exists(try_wallet_path):
walletpath = try_wallet_path
walletfilepath = walletpath
if os.path.isdir(walletpath):
walletfilepath = os.path.join(walletpath, "wallet.dat")
if walletpath is None:
self._log.warning(f"Unable to find {self.ticker()} wallet path.")
else:
if self._use_descriptors:
orig_active_descriptors = []
with sqlite3.connect(walletfilepath) as conn:
c = conn.cursor()
rows = c.execute(
"SELECT * FROM main WHERE key in (:kext, :kint)",
{
"kext": bytes.fromhex(
"1161637469766565787465726e616c73706b02"
),
"kint": bytes.fromhex(
"11616374697665696e7465726e616c73706b02"
),
},
)
for row in rows:
k, v = row
orig_active_descriptors.append({"k": k, "v": v})
else:
seedid_bytes: bytes = bytes.fromhex(seed_id_before)[::-1]
with open(walletfilepath, "rb") as fp:
with mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) as mm:
pos = mm.find(seedid_bytes)
while pos != -1:
mm.seek(pos - 8)
hdchain_bytes = mm.read(12 + 20)
version = int.from_bytes(hdchain_bytes[:4], "little")
if version == 2:
external_counter = int.from_bytes(
hdchain_bytes[4:8], "little"
)
internal_counter = int.from_bytes(
hdchain_bytes[-4:], "little"
)
if (
external_counter > 0
and external_counter <= max_hdchain_key_count
and internal_counter > 0
and internal_counter <= max_hdchain_key_count
):
orig_hdchain_bytes = hdchain_bytes
self._log.debug(
f"Found hdchain for: {seed_id_before} external_counter: {external_counter}, internal_counter: {internal_counter}."
)
break
pos = mm.find(seedid_bytes, pos + 1)
self.rpc("loadwallet", [self._rpc_wallet])
self.rpc_wallet("encryptwallet", [password])
if check_seed is False or seed_id_before == "Not found" or walletpath is None:
return
seed_id_after: str = self.getWalletSeedID()
if seed_id_before == seed_id_after:
return
self._log.warning(f"{self.ticker()} wallet seed changed after encryption.")
self._log.debug(
f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}."
)
self.setWalletSeedWarning(True)
if chain_client_settings.get("manage_daemon", False) is False:
self._log.warning(
f"{self.ticker()} manage_daemon is false. Can't attempt to fix."
)
return
if self._use_descriptors:
if len(orig_active_descriptors) < 2:
self._log.error(
"Could not find original active descriptors for wallet."
)
return
self._log.info("Attempting to revert to last descriptors.")
else:
if orig_hdchain_bytes is None:
self._log.error("Could not find hdchain for wallet.")
return
self._log.info("Attempting to revert to last hdchain.")
try:
# Make a copy of the encrypted wallet before modifying it
bkp_path = walletpath + ".bkp"
for i in range(100):
if not os.path.exists(bkp_path):
break
bkp_path = walletpath + f".bkp{i}"
if os.path.exists(bkp_path):
self._log.error("Could not find backup path for wallet.")
return
self.rpc("unloadwallet", [self._rpc_wallet])
if os.path.isfile(walletpath):
shutil.copy(walletpath, bkp_path)
else:
shutil.copytree(walletpath, bkp_path)
hdchain_replaced: bool = False
if self._use_descriptors:
with sqlite3.connect(walletfilepath) as conn:
c = conn.cursor()
c.executemany(
"UPDATE main SET value = :v WHERE key = :k",
orig_active_descriptors,
)
conn.commit()
else:
seedid_after_bytes: bytes = bytes.fromhex(seed_id_after)[::-1]
with open(walletfilepath, "r+b") as fp:
with mmap.mmap(fp.fileno(), 0) as mm:
pos = mm.find(seedid_after_bytes)
while pos != -1:
mm.seek(pos - 8)
hdchain_bytes = mm.read(12 + 20)
version = int.from_bytes(hdchain_bytes[:4], "little")
if version == 2:
external_counter = int.from_bytes(
hdchain_bytes[4:8], "little"
)
internal_counter = int.from_bytes(
hdchain_bytes[-4:], "little"
)
if (
external_counter > 0
and external_counter <= max_hdchain_key_count
and internal_counter > 0
and internal_counter <= max_hdchain_key_count
):
self._log.debug(
f"Replacing hdchain for: {seed_id_after} external_counter: {external_counter}, internal_counter: {internal_counter}."
)
offset: int = pos - 8
mm.seek(offset)
mm.write(orig_hdchain_bytes)
self._log.debug(
f"hdchain replaced at offset: {offset}."
)
hdchain_replaced = True
# Can appear multiple times in file, replace all.
pos = mm.find(seedid_after_bytes, pos + 1)
if hdchain_replaced is False:
self._log.error("Could not find new hdchain in wallet.")
self.rpc("loadwallet", [self._rpc_wallet])
if hdchain_replaced:
self.unlockWallet(password, check_seed=False)
seed_id_after_restore: str = self.getWalletSeedID()
if seed_id_after_restore == seed_id_before:
self.newKeypool()
else:
self._log.warning(
f"Expected seed id not found: {seed_id_before}, have {seed_id_after_restore}."
)
self.lockWallet()
except Exception as e:
self._log.error(f"{self.ticker()} recreating wallet failed: {e}.")
if self._sc.debug:
self._log.error(traceback.format_exc())
def changeWalletPassword(
self, old_password: str, new_password: str, check_seed_if_encrypt: bool = True
):
def changeWalletPassword(self, old_password: str, new_password: str):
self._log.info("changeWalletPassword - {}".format(self.ticker()))
if old_password == "":
if self.isWalletEncrypted():
raise ValueError("Old password must be set")
return self.encryptWallet(new_password, check_seed=check_seed_if_encrypt)
return self.rpc_wallet("encryptwallet", [new_password])
self.rpc_wallet("walletpassphrasechange", [old_password, new_password])
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
def unlockWallet(self, password: str):
if password == "":
return
self._log.info(f"unlockWallet - {self.ticker()}")
@@ -2279,20 +1951,12 @@ class BTCInterface(Secp256k1Interface):
)
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
self.rpc(
"createwallet",
[
self._rpc_wallet,
False,
True,
password,
False,
self._use_descriptors,
],
"createwallet", [self._rpc_wallet, False, True, "", False, False]
)
self.rpc_wallet("encryptwallet", [password])
# Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000])
if check_seed:
self._sc.checkWalletSeed(self.coin_type())
def lockWallet(self):
@@ -2407,59 +2071,6 @@ class BTCInterface(Secp256k1Interface):
def isTxNonFinalError(self, err_str: str) -> bool:
return "non-BIP68-final" in err_str or "non-final" in err_str
def combine_non_segwit_prevouts(self):
self._log.info("Combining non-segwit prevouts")
if self._use_segwit is False:
raise RuntimeError("Not configured to use segwit outputs.")
prevouts_to_spend = self.getNonSegwitOutputs()
if len(prevouts_to_spend) < 1:
raise RuntimeError("No non-segwit outputs found.")
total_amount: int = 0
for n, prevout in enumerate(prevouts_to_spend):
total_amount += self.make_int(prevout["amount"])
addr_to: str = self.getNewAddress(
self._use_segwit, "combine_non_segwit_prevouts"
)
txn = self.rpc(
"createrawtransaction",
[prevouts_to_spend, {addr_to: self.format_amount(total_amount)}],
)
fee_rate, rate_src = self.get_fee_rate(self._conf_target)
fee_rate_str: str = self.format_amount(fee_rate, True, 1)
self._log.debug(
f"Using fee rate: {fee_rate_str}, src: {rate_src}, confirms target: {self._conf_target}"
)
options = {
"add_inputs": False,
"subtractFeeFromOutputs": [
0,
],
"feeRate": fee_rate_str,
}
tx_fee_set = self.rpc_wallet("fundrawtransaction", [txn, options])["hex"]
tx_signed = self.rpc_wallet("signrawtransactionwithwallet", [tx_fee_set])["hex"]
tx = self.rpc(
"decoderawtransaction",
[
tx_signed,
],
)
self._log.info(
"Submitting tx to combine non-segwit prevouts: {}".format(
self._log.id(bytes.fromhex(tx["txid"]))
)
)
self.rpc(
"sendrawtransaction",
[
tx_signed,
],
)
return tx["txid"]
def testBTCInterface():
print("TODO: testBTCInterface")

View File

@@ -47,7 +47,7 @@ class DASHInterface(BTCInterface):
def entropyToMnemonic(self, key: bytes) -> None:
return Mnemonic("english").to_mnemonic(key)
def initialiseWallet(self, key_bytes: bytes, restore_time: int = -1) -> None:
def initialiseWallet(self, key_bytes: bytes) -> None:
self._have_checked_seed = False
if self._wallet_v20_compatible:
self._log.warning("Generating wallet compatible with v20 seed.")
@@ -66,11 +66,7 @@ class DASHInterface(BTCInterface):
def checkExpectedSeed(self, expect_seedid: str) -> bool:
self._expect_seedid_hex = expect_seedid
try:
rv = self.rpc_wallet("dumphdinfo")
except Exception as e:
self._log.debug(f"DASH dumphdinfo failed {e}.")
return False
if rv["mnemonic"] != "":
entropy = Mnemonic("english").to_entropy(rv["mnemonic"].split(" "))
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
@@ -115,45 +111,18 @@ class DASHInterface(BTCInterface):
return None
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
super().unlockWallet(password, check_seed)
def unlockWallet(self, password: str):
super().unlockWallet(password)
if self._wallet_v20_compatible:
# Store password for initialiseWallet
self._wallet_passphrase = password
if not self._have_checked_seed:
try:
self._sc.checkWalletSeed(self.coin_type())
except Exception as ex:
# dumphdinfo can fail if the wallet is not initialised
self._log.debug(f"DASH checkWalletSeed failed: {ex}.")
def lockWallet(self):
super().lockWallet()
self._wallet_passphrase = ""
def encryptWallet(
self, old_password: str, new_password: str, check_seed: bool = True
):
if old_password != "":
self.unlockWallet(old_password, check_seed=False)
seed_id_before: str = self.getWalletSeedID()
self.rpc_wallet("encryptwallet", [new_password])
if check_seed is False or seed_id_before == "Not found":
return
self.unlockWallet(new_password, check_seed=False)
seed_id_after: str = self.getWalletSeedID()
self.lockWallet()
if seed_id_before == seed_id_after:
return
self._log.warning(f"{self.ticker()} wallet seed changed after encryption.")
self._log.debug(
f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}."
)
self.setWalletSeedWarning(True)
def changeWalletPassword(
self, old_password: str, new_password: str, check_seed_if_encrypt: bool = True
):
self._log.info("changeWalletPassword - {}".format(self.ticker()))
if old_password == "":
if self.isWalletEncrypted():
raise ValueError("Old password must be set")
return self.encryptWallet(old_password, new_password, check_seed_if_encrypt)
self.rpc_wallet("walletpassphrasechange", [old_password, new_password])

View File

@@ -332,14 +332,14 @@ class DCRInterface(Secp256k1Interface):
def testDaemonRPC(self, with_wallet=True) -> None:
if with_wallet:
self.rpc_wallet("walletislocked")
self.rpc_wallet("getinfo")
else:
self.rpc("getblockchaininfo")
def getChainHeight(self) -> int:
return self.rpc("getblockcount")
def initialiseWallet(self, key: bytes, restore_time: int = -1) -> None:
def initialiseWallet(self, key: bytes) -> None:
# Load with --create
pass
@@ -354,9 +354,7 @@ class DCRInterface(Secp256k1Interface):
walletislocked = self.rpc_wallet("walletislocked")
return True, walletislocked
def changeWalletPassword(
self, old_password: str, new_password: str, check_seed_if_encrypt: bool = True
):
def changeWalletPassword(self, old_password: str, new_password: str):
self._log.info("changeWalletPassword - {}".format(self.ticker()))
if old_password == "":
# Read initial pwd from settings
@@ -370,14 +368,13 @@ class DCRInterface(Secp256k1Interface):
# Clear initial password
self._sc.editSettings(self.coin_name().lower(), {"wallet_pwd": ""})
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
def unlockWallet(self, password: str):
if password == "":
return
self._log.info("unlockWallet - {}".format(self.ticker()))
# Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000])
if check_seed:
self._sc.checkWalletSeed(self.coin_type())
def lockWallet(self):

View File

@@ -42,6 +42,7 @@ def make_rpc_func(port, auth, host="127.0.0.1"):
host = host
def rpc_func(method, params=None):
nonlocal port, auth, host
return callrpc(port, auth, method, params, host)
return rpc_func

View File

@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -44,7 +44,6 @@ class FIROInterface(BTCInterface):
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
)
self.rpc_wallet_watch = self.rpc_wallet
if "wallet_name" in coin_settings:
raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name")
@@ -52,32 +51,13 @@ class FIROInterface(BTCInterface):
def getExchangeName(self, exchange_name: str) -> str:
return "zcoin"
def initialiseWallet(self, key, restore_time: int = -1):
def initialiseWallet(self, key):
# load with -hdseed= parameter
pass
def checkWallets(self) -> int:
return 1
def encryptWallet(self, password: str, check_seed: bool = True):
# Watchonly wallets are not encrypted
# Firo shuts down after encryptwallet
seed_id_before: str = self.getWalletSeedID() if check_seed else "Not found"
self.rpc_wallet("encryptwallet", [password])
if check_seed is False or seed_id_before == "Not found":
return
seed_id_after: str = self.getWalletSeedID()
if seed_id_before == seed_id_after:
return
self._log.warning(f"{self.ticker()} wallet seed changed after encryption.")
self._log.debug(
f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}."
)
self.setWalletSeedWarning(True)
def getNewAddress(self, use_segwit, label="swap_receive"):
return self.rpc("getnewaddress", [label])
# addr_plain = self.rpc('getnewaddress', [label])

View File

@@ -98,7 +98,6 @@ class LTCInterfaceMWEB(LTCInterface):
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet
)
self.rpc_wallet_watch = self.rpc_wallet
def chainparams(self):
return chainparams[Coins.LTC]
@@ -147,7 +146,7 @@ class LTCInterfaceMWEB(LTCInterface):
self.rpc_wallet("walletpassphrase", [password, 100000000])
self.rpc_wallet("keypoolrefill")
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
def unlockWallet(self, password: str):
if password == "":
return
self._log.info("unlockWallet - {}".format(self.ticker()))
@@ -157,5 +156,5 @@ class LTCInterfaceMWEB(LTCInterface):
else:
# Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000])
if check_seed:
self._sc.checkWalletSeed(self.coin_type())

View File

@@ -79,7 +79,6 @@ class NAVInterface(BTCInterface):
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
)
self.rpc_wallet_watch = self.rpc_wallet
if "wallet_name" in coin_settings:
raise ValueError(f"Invalid setting for {self.coin_name()}: wallet_name")
@@ -88,7 +87,7 @@ class NAVInterface(BTCInterface):
# p2sh-p2wsh
return True
def initialiseWallet(self, key, restore_time: int = -1):
def initialiseWallet(self, key):
# Load with -importmnemonic= parameter
pass

View File

@@ -2,7 +2,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2022 tecnovert
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -14,3 +13,39 @@ class NMCInterface(BTCInterface):
@staticmethod
def coin_type():
return Coins.NMC
def getLockTxHeight(
self,
txid,
dest_address,
bid_amount,
rescan_from,
find_index: bool = False,
vout: int = -1,
):
self._log.debug("[rm] scantxoutset start") # scantxoutset is slow
ro = self.rpc(
"scantxoutset", ["start", ["addr({})".format(dest_address)]]
) # TODO: Use combo(address) where possible
self._log.debug("[rm] scantxoutset end")
return_txid = True if txid is None else False
for o in ro["unspents"]:
if txid and o["txid"] != txid.hex():
continue
# Verify amount
if self.make_int(o["amount"]) != int(bid_amount):
self._log.warning(
"Found output to lock tx address of incorrect value: %s, %s",
str(o["amount"]),
o["txid"],
)
continue
rv = {"depth": 0, "height": o["height"]}
if o["height"] > 0:
rv["depth"] = ro["height"] - o["height"]
if find_index:
rv["index"] = o["vout"]
if return_txid:
rv["txid"] = o["txid"]
return rv

View File

@@ -14,6 +14,7 @@ from basicswap.contrib.test_framework.messages import (
)
from basicswap.contrib.test_framework.script import (
CScript,
OP_0,
OP_DUP,
OP_HASH160,
OP_EQUALVERIFY,
@@ -24,6 +25,7 @@ from basicswap.util import (
TemporaryError,
)
from basicswap.util.script import (
getP2WSH,
getCompactSizeLen,
getWitnessElementLen,
)
@@ -108,7 +110,7 @@ class PARTInterface(BTCInterface):
)
return index_info["spentindex"]
def initialiseWallet(self, key: bytes, restore_time: int = -1) -> None:
def initialiseWallet(self, key: bytes) -> None:
raise ValueError("TODO")
def withdrawCoin(self, value, addr_to, subfee):
@@ -187,9 +189,6 @@ class PARTInterface(BTCInterface):
) + self.make_int(u["amount"], r=1)
return unspent_addr
def combine_non_segwit_prevouts(self):
raise RuntimeError("No non-segwit outputs found.")
class PARTInterfaceBlind(PARTInterface):
@@ -212,15 +211,6 @@ class PARTInterfaceBlind(PARTInterface):
def xmr_swap_b_lock_spend_tx_vsize() -> int:
return 980
@staticmethod
def compareFeeRates(actual: int, expected: int) -> bool:
# Allow the fee to be up to 10% larger than expected
if actual < expected - 20:
return False
if actual > expected + expected * 0.1:
return False
return True
def coin_name(self) -> str:
return super().coin_name() + " Blind"
@@ -266,7 +256,7 @@ class PARTInterfaceBlind(PARTInterface):
ephemeral_pubkey = self.getPubkey(ephemeral_key)
assert len(ephemeral_pubkey) == 33
nonce = self.getScriptLockTxNonce(vkbv)
p2wsh_addr = self.encode_p2wsh(self.getP2WSHScriptDest(script))
p2wsh_addr = self.encode_p2wsh(getP2WSH(script))
inputs = []
outputs = [
{
@@ -340,7 +330,7 @@ class PARTInterfaceBlind(PARTInterface):
locked_coin = input_blinded_info["amount"]
tx_lock_id = lock_tx_obj["txid"]
refund_script = self.genScriptLockRefundTxScript(Kal, Kaf, csv_val)
p2wsh_addr = self.encode_p2wsh(self.getP2WSHScriptDest(refund_script))
p2wsh_addr = self.encode_p2wsh(getP2WSH(refund_script))
inputs = [
{
@@ -505,7 +495,7 @@ class PARTInterfaceBlind(PARTInterface):
lock_txo_scriptpk = bytes.fromhex(
lock_tx_obj["vout"][lock_output_n]["scriptPubKey"]["hex"]
)
script_pk = self.getP2WSHScriptDest(script_out)
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
ensure(lock_txo_scriptpk == script_pk, "Bad output script")
A, B = extractScriptLockScriptValues(script_out)
ensure(A == Kal, "Bad script leader pubkey")
@@ -582,7 +572,7 @@ class PARTInterfaceBlind(PARTInterface):
lock_refund_txo_scriptpk = bytes.fromhex(
lock_refund_tx_obj["vout"][lock_refund_output_n]["scriptPubKey"]["hex"]
)
script_pk = self.getP2WSHScriptDest(script_out)
script_pk = CScript([OP_0, hashlib.sha256(script_out).digest()])
ensure(lock_refund_txo_scriptpk == script_pk, "Bad output script")
A, B, csv_val, C = extractScriptLockRefundScriptValues(script_out)
ensure(A == Kal, "Bad script pubkey")
@@ -690,7 +680,6 @@ class PARTInterfaceBlind(PARTInterface):
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(self.loadTx(tx_bytes), add_witness_bytes=witness_bytes)
fee_paid = self.make_int(lock_refund_spend_tx_obj["vout"][0]["ct_fee"])
fee_rate_paid = fee_paid * 1000 // vsize
ensure(
self.compareFeeRates(fee_rate_paid, feerate),
@@ -1042,11 +1031,10 @@ class PARTInterfaceBlind(PARTInterface):
self,
kbv,
Kbs,
cb_swap_value: int,
cb_block_confirmed: int,
cb_swap_value,
cb_block_confirmed,
restore_height: int,
bid_sender: bool,
check_amount: bool = True,
):
Kbv = self.getPubkey(kbv)
sx_addr = self.formatStealthAddress(Kbv, Kbs)
@@ -1077,10 +1065,7 @@ class PARTInterfaceBlind(PARTInterface):
) # Should not be possible
ensure(tx["outputs"][0]["type"] == "blind", "Output is not anon")
if (
self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value
or check_amount is False
):
if self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value:
height = 0
if tx["confirmations"] > 0:
chain_height = self.rpc("getblockcount")
@@ -1302,14 +1287,7 @@ class PARTInterfaceAnon(PARTInterface):
return bytes.fromhex(txid)
def findTxB(
self,
kbv,
Kbs,
cb_swap_value,
cb_block_confirmed,
restore_height,
bid_sender,
check_amount: bool = True,
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
):
Kbv = self.getPubkey(kbv)
sx_addr = self.formatStealthAddress(Kbv, Kbs)
@@ -1341,10 +1319,7 @@ class PARTInterfaceAnon(PARTInterface):
) # Should not be possible
ensure(tx["outputs"][0]["type"] == "anon", "Output is not anon")
if (
self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value
or check_amount is False
):
if self.make_int(tx["outputs"][0]["amount"]) == cb_swap_value:
height = 0
if tx["confirmations"] > 0:
chain_height = self.rpc("getblockcount")

View File

@@ -33,36 +33,9 @@ class PIVXInterface(BTCInterface):
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
)
self.rpc_wallet_watch = self.rpc_wallet
def encryptWallet(self, password: str, check_seed: bool = True):
# Watchonly wallets are not encrypted
seed_id_before: str = self.getWalletSeedID()
self.rpc_wallet("encryptwallet", [password])
if check_seed is False or seed_id_before == "Not found":
return
seed_id_after: str = self.getWalletSeedID()
if seed_id_before == seed_id_after:
return
self._log.warning(f"{self.ticker()} wallet seed changed after encryption.")
self._log.debug(
f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}."
)
self.setWalletSeedWarning(True)
# Workaround for https://github.com/bitcoin/bitcoin/issues/26607
chain_client_settings = self._sc.getChainClientSettings(
self.coin_type()
) # basicswap.json
if chain_client_settings.get("manage_daemon", False) is False:
self._log.warning(
f"{self.ticker()} manage_daemon is false. Can't attempt to fix."
)
return
def checkWallets(self) -> int:
return 1
def signTxWithWallet(self, tx):
rv = self.rpc("signrawtransaction", [tx.hex()])

View File

@@ -30,27 +30,3 @@ class WOWInterface(XMRInterface):
@staticmethod
def depth_spendable() -> int:
return 3
# below only needed until wow is rebased to monero v0.18.4.0+
def openWallet(self, filename):
params = {"filename": filename}
if self._wallet_password is not None:
params["password"] = self._wallet_password
try:
self.rpc_wallet("open_wallet", params)
except Exception as e:
if "no connection to daemon" in str(e):
self._log.debug(f"{self.coin_name()} {e}")
return # bypass refresh error to allow startup with a busy daemon
try:
# TODO Remove `store` after upstream fix to autosave on close_wallet
self.rpc_wallet("store")
self.rpc_wallet("close_wallet")
self._log.debug(f"Attempt to save and close {self.coin_name()} wallet")
except Exception as e: # noqa: F841
pass
self.rpc_wallet("open_wallet", params)
self._log.debug(f"Reattempt to open {self.coin_name()} wallet")

View File

@@ -7,7 +7,6 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import logging
import os
import basicswap.contrib.ed25519_fast as edf
import basicswap.ed25519_fast_util as edu
@@ -202,57 +201,25 @@ class XMRInterface(CoinInterface):
try:
self.rpc_wallet("open_wallet", params)
# TODO Remove `refresh` after upstream fix to refresh on open_wallet
self.rpc_wallet("refresh")
except Exception as e:
if "no connection to daemon" in str(e):
self._log.debug(f"{self.coin_name()} {e}")
return # Bypass refresh error to allow startup with a busy daemon
if any(
x in str(e)
for x in (
"invalid signature",
"std::bad_alloc",
"basic_string::_M_replace_aux",
)
):
self._log.error(f"{self.coin_name()} wallet is corrupt.")
chain_client_settings = self._sc.getChainClientSettings(
self.coin_type()
) # basicswap.json
if chain_client_settings.get("manage_wallet_daemon", False):
self._log.info(f"Renaming {self.coin_name()} wallet cache file.")
walletpath = os.path.join(
chain_client_settings.get("datadir", "none"),
"wallets",
filename,
)
if not os.path.isfile(walletpath):
self._log.warning(
f"Could not find {self.coin_name()} wallet cache file."
)
raise
bkp_path = walletpath + ".corrupt"
for i in range(100):
if not os.path.exists(bkp_path):
break
bkp_path = walletpath + f".corrupt{i}"
if os.path.exists(bkp_path):
self._log.error(
f"Could not find backup path for {self.coin_name()} wallet."
)
raise
os.rename(walletpath, bkp_path)
# Drop through to open_wallet
else:
raise
else:
return # bypass refresh error to allow startup with a busy daemon
try:
# TODO Remove `store` after upstream fix to autosave on close_wallet
self.rpc_wallet("store")
self.rpc_wallet("close_wallet")
self._log.debug(f"Closing {self.coin_name()} wallet")
self._log.debug(f"Attempt to save and close {self.coin_name()} wallet")
except Exception as e: # noqa: F841
pass
self.rpc_wallet("open_wallet", params)
self._log.debug(f"Attempting to open {self.coin_name()} wallet")
# TODO Remove `refresh` after upstream fix to refresh on open_wallet
self.rpc_wallet("refresh")
self._log.debug(f"Reattempt to open {self.coin_name()} wallet")
def initialiseWallet(
self, key_view: bytes, key_spend: bytes, restore_height=None
@@ -338,8 +305,6 @@ class XMRInterface(CoinInterface):
raise e
rv = {}
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
balance_info = self.rpc_wallet("get_balance")
rv["wallet_blocks"] = self.rpc_wallet("get_height")["height"]
@@ -441,8 +406,6 @@ class XMRInterface(CoinInterface):
) -> bytes:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
Kbv = self.getPubkey(kbv)
shared_addr = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
@@ -455,24 +418,16 @@ class XMRInterface(CoinInterface):
params["priority"] = self._fee_priority
rv = self.rpc_wallet("transfer", params)
self._log.info(
"publishBLockTx {} to address_b58 {}".format(
"publishBLockTx %s to address_b58 %s",
self._log.id(rv["tx_hash"]),
self._log.addr(shared_addr),
)
)
tx_hash = bytes.fromhex(rv["tx_hash"])
return tx_hash
def findTxB(
self,
kbv,
Kbs,
cb_swap_value: int,
cb_block_confirmed: int,
restore_height: int,
bid_sender: bool,
check_amount: bool = True,
self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender
):
with self._mx_wallet:
Kbv = self.getPubkey(kbv)
@@ -492,9 +447,6 @@ class XMRInterface(CoinInterface):
self.createWallet(params)
self.openWallet(address_b58)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
"""
# Debug
try:
@@ -524,7 +476,7 @@ class XMRInterface(CoinInterface):
)
rv = -1
continue
if transfer["amount"] == cb_swap_value or check_amount is False:
if transfer["amount"] == cb_swap_value:
return {
"txid": transfer["tx_hash"],
"amount": transfer["amount"],
@@ -546,8 +498,6 @@ class XMRInterface(CoinInterface):
def findTxnByHash(self, txid):
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
try:
current_height = self.rpc2("get_height", timeout=self._rpctimeout)[
@@ -616,8 +566,6 @@ class XMRInterface(CoinInterface):
self.createWallet(params)
self.openWallet(wallet_filename)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
rv = self.rpc_wallet("get_balance")
if rv["balance"] < cb_swap_value:
self._log.warning("Balance is too low, checking for existing spend.")
@@ -672,8 +620,6 @@ class XMRInterface(CoinInterface):
) -> str:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
if sweepall:
balance = self.rpc_wallet("get_balance")
@@ -753,9 +699,6 @@ class XMRInterface(CoinInterface):
self.createWallet(params)
self.openWallet(address_b58)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
rv = self.rpc_wallet(
"get_transfers",
{"in": True, "out": True, "pending": True, "failed": True},
@@ -768,15 +711,11 @@ class XMRInterface(CoinInterface):
def getSpendableBalance(self) -> int:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
balance_info = self.rpc_wallet("get_balance")
return balance_info["unlocked_balance"]
def changeWalletPassword(
self, old_password, new_password, check_seed_if_encrypt: bool = True
):
def changeWalletPassword(self, old_password, new_password):
self._log.info("changeWalletPassword - {}".format(self.ticker()))
orig_password = self._wallet_password
if old_password != "":
@@ -791,11 +730,11 @@ class XMRInterface(CoinInterface):
self._wallet_password = orig_password
raise e
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
def unlockWallet(self, password: str) -> None:
self._log.info("unlockWallet - {}".format(self.ticker()))
self._wallet_password = password
if check_seed and not self._have_checked_seed:
if not self._have_checked_seed:
self._sc.checkWalletSeed(self.coin_type())
def lockWallet(self) -> None:

View File

@@ -14,7 +14,6 @@ from .util import (
toBool,
)
from .basicswap_util import (
fiatFromTicker,
strBidState,
strTxState,
SwapTypes,
@@ -23,9 +22,6 @@ from .basicswap_util import (
from .chainparams import (
Coins,
chainparams,
Fiat,
getCoinIdFromTicker,
getCoinIdFromName,
)
from .ui.util import (
PAGE_LIMIT,
@@ -37,12 +33,12 @@ from .ui.util import (
get_data_entry,
get_data_entry_or,
have_data_entry,
tickerToCoinId,
listOldBidStates,
checkAddressesOwned,
)
from .ui.page_offers import postNewOffer
from .protocols.xmr_swap_1 import recoverNoScriptTxnWithKey, getChainBSplitKey
from .db import Concepts
def getFormData(post_string: str, is_json: bool):
@@ -128,7 +124,7 @@ def js_wallets(self, url_split, post_string, is_json):
swap_client.checkSystemStatus()
if len(url_split) > 3:
ticker_str = url_split[3]
coin_type = getCoinIdFromTicker(ticker_str)
coin_type = tickerToCoinId(ticker_str)
if len(url_split) > 4:
cmd = url_split[4]
@@ -167,13 +163,12 @@ def js_wallets(self, url_split, post_string, is_json):
return bytes(
json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8"
)
raise ValueError("Unknown command")
if coin_type == Coins.LTC_MWEB:
coin_type = Coins.LTC
rv = swap_client.getWalletInfo(coin_type)
if not rv:
raise ValueError(f"getWalletInfo failed for coin: {coin_type}")
rv.update(swap_client.getBlockchainInfo(coin_type))
ci = swap_client.ci(coin_type)
checkAddressesOwned(swap_client, ci, rv)
@@ -184,19 +179,7 @@ def js_wallets(self, url_split, post_string, is_json):
def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
swap_client = self.server.swap_client
try:
swap_client.checkSystemStatus()
except Exception as e:
from basicswap.util import LockedCoinError
from basicswap.ui.util import getCoinName
if isinstance(e, LockedCoinError):
error_msg = f"Wallet must be unlocked to view offers. Please unlock your {getCoinName(e.coinid)} wallet."
return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8")
else:
return bytes(json.dumps({"error": str(e)}), "UTF-8")
offer_id = None
if len(url_split) > 3:
if url_split[3] == "new":
@@ -219,12 +202,6 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
if offer_id:
filters["offer_id"] = offer_id
parsed_url = urllib.parse.urlparse(self.path)
query_params = urllib.parse.parse_qs(parsed_url.query) if parsed_url.query else {}
if "with_extra_info" in query_params:
with_extra_info = toBool(query_params["with_extra_info"][0])
if post_string != "":
post_data = getFormData(post_string, is_json)
filters["coin_from"] = setCoinFilter(post_data, "coin_from")
@@ -278,7 +255,6 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
"is_public": o.addr_to == swap_client.network_addr
or o.addr_to.strip() == "",
}
offer_data["auto_accept_type"] = getattr(o, "auto_accept_type", 0)
if with_extra_info:
offer_data["amount_negotiable"] = o.amount_negotiable
offer_data["rate_negotiable"] = o.rate_negotiable
@@ -293,25 +269,6 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
offer_data["feerate_from"] = o.from_feerate
offer_data["feerate_to"] = o.to_feerate
offer_data["automation_strat_id"] = getattr(o, "auto_accept_type", 0)
offer_data["auto_accept_type"] = getattr(o, "auto_accept_type", 0)
if o.was_sent:
try:
strategy = swap_client.getLinkedStrategy(Concepts.OFFER, o.offer_id)
if strategy:
offer_data["local_automation_strat_id"] = strategy[0]
swap_client.log.debug(
f"Found local automation strategy for own offer {o.offer_id.hex()}: {strategy[0]}"
)
else:
offer_data["local_automation_strat_id"] = 0
except Exception as e:
swap_client.log.debug(
f"Error getting local automation strategy for offer {o.offer_id.hex()}: {e}"
)
offer_data["local_automation_strat_id"] = 0
rv.append(offer_data)
return bytes(json.dumps(rv), "UTF-8")
@@ -368,10 +325,11 @@ def formatBids(swap_client, bids, filters) -> bytes:
offer = swap_client.getOffer(b[3])
ci_to = swap_client.ci(offer.coin_to) if offer else None
bid_rate: int = 0 if b[10] is None else b[10]
amount_to = None
if ci_to:
amount_to = ci_to.format_amount((b[4] * bid_rate) // ci_from.COIN())
amount_to = ci_to.format_amount(
(b[4] * b[10]) // ci_from.COIN()
)
bid_data = {
"bid_id": b[2].hex(),
@@ -382,16 +340,17 @@ def formatBids(swap_client, bids, filters) -> bytes:
"coin_to": ci_to.coin_name() if ci_to else "Unknown",
"amount_from": ci_from.format_amount(b[4]),
"amount_to": amount_to,
"bid_rate": swap_client.ci(b[14]).format_amount(bid_rate),
"bid_rate": swap_client.ci(b[14]).format_amount(b[10]),
"bid_state": strBidState(b[5]),
"addr_from": b[11],
"addr_to": offer.addr_to if offer else None,
"addr_to": offer.addr_to if offer else None
}
if with_extra_info:
bid_data.update(
{"tx_state_a": strTxState(b[7]), "tx_state_b": strTxState(b[8])}
)
bid_data.update({
"tx_state_a": strTxState(b[7]),
"tx_state_b": strTxState(b[8])
})
rv.append(bid_data)
return bytes(json.dumps(rv), "UTF-8")
@@ -553,19 +512,7 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
def js_sentbids(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
try:
swap_client.checkSystemStatus()
except Exception as e:
from basicswap.util import LockedCoinError
from basicswap.ui.util import getCoinName
if isinstance(e, LockedCoinError):
error_msg = f"Wallet must be unlocked to view bids. Please unlock your {getCoinName(e.coinid)} wallet."
return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8")
else:
return bytes(json.dumps({"error": str(e)}), "UTF-8")
post_data = getFormData(post_string, is_json)
offer_id, filters = parseBidFilters(post_data)
@@ -682,19 +629,7 @@ def js_rate(self, url_split, post_string, is_json) -> bytes:
def js_index(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
try:
swap_client.checkSystemStatus()
except Exception as e:
from basicswap.util import LockedCoinError
from basicswap.ui.util import getCoinName
if isinstance(e, LockedCoinError):
error_msg = f"Wallet must be unlocked to view summary. Please unlock your {getCoinName(e.coinid)} wallet."
return bytes(json.dumps({"error": error_msg, "locked": True}), "UTF-8")
else:
return bytes(json.dumps({"error": str(e)}), "UTF-8")
return bytes(json.dumps(swap_client.getSummary()), "UTF-8")
@@ -860,7 +795,7 @@ def js_automationstrategies(self, url_split, post_string: str, is_json: bool) ->
"label": strat_data.label,
"type_ind": strat_data.type_ind,
"only_known_identities": strat_data.only_known_identities,
"data": json.loads(strat_data.data.decode("UTF-8")),
"data": json.loads(strat_data.data.decode("utf-8")),
"note": "" if strat_data.note is None else strat_data.note,
}
return bytes(json.dumps(rv), "UTF-8")
@@ -888,7 +823,7 @@ def js_validateamount(self, url_split, post_string: str, is_json: bool) -> bytes
f"Unknown rounding method, must be one of {valid_round_methods}"
)
coin_type = getCoinIdFromTicker(ticker_str)
coin_type = tickerToCoinId(ticker_str)
ci = swap_client.ci(coin_type)
r = 0
@@ -914,12 +849,7 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
swap_client.checkSystemStatus()
post_data = getFormData(post_string, is_json)
coin_in = get_data_entry(post_data, "coin")
try:
coin = getCoinIdFromName(coin_in)
except Exception:
coin = getCoinType(coin_in)
coin = getCoinType(get_data_entry(post_data, "coin"))
if coin in (Coins.PART, Coins.PART_ANON, Coins.PART_BLIND):
raise ValueError("Particl wallet seed is set from the Basicswap mnemonic.")
@@ -947,17 +877,12 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
expect_seedid = swap_client.getStringKV(
"main_wallet_seedid_" + ci.coin_name().lower()
)
try:
wallet_seed_id = ci.getWalletSeedID()
except Exception as e:
wallet_seed_id = f"Error: {e}"
rv.update(
{
"seed": seed_key.hex(),
"seed_id": seed_id.hex(),
"expected_seed_id": "Unset" if expect_seedid is None else expect_seedid,
"current_seed_id": wallet_seed_id,
}
)
@@ -994,7 +919,7 @@ def js_unlock(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = getFormData(post_string, is_json)
password: str = get_data_entry(post_data, "password")
password = get_data_entry(post_data, "password")
if have_data_entry(post_data, "coin"):
coin = getCoinType(str(get_data_entry(post_data, "coin")))
@@ -1029,7 +954,7 @@ def js_404(self, url_split, post_string, is_json) -> bytes:
def js_help(self, url_split, post_string, is_json) -> bytes:
# TODO: Add details and examples
commands = []
for k in endpoints:
for k in pages:
commands.append(k)
return bytes(json.dumps({"commands": commands}), "UTF-8")
@@ -1037,8 +962,7 @@ def js_help(self, url_split, post_string, is_json) -> bytes:
def js_readurl(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json)
if not have_data_entry(post_data, "url"):
raise ValueError("Requires URL.")
if have_data_entry(post_data, "url"):
url = get_data_entry(post_data, "url")
default_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
@@ -1053,166 +977,71 @@ def js_readurl(self, url_split, post_string, is_json) -> bytes:
except json.JSONDecodeError:
pass
return response
raise ValueError("Requires URL.")
def js_active(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
all_bids = []
try:
for bid_id, (bid, offer) in list(swap_client.swaps_in_progress.items()):
try:
ci_from = swap_client.ci(offer.coin_from)
ci_to = swap_client.ci(offer.coin_to)
if offer.bid_reversed:
amount_from: int = bid.amount_to
amount_to: int = bid.amount
bid_rate: int = ci_from.make_int(amount_to / amount_from, r=1)
else:
amount_from: int = bid.amount
amount_to: int = bid.amount_to
bid_rate: int = bid.rate
swap_data = {
"bid_id": bid_id.hex(),
"offer_id": offer.offer_id.hex(),
"created_at": bid.created_at,
"expire_at": bid.expire_at,
"bid_state": strBidState(bid.state),
"tx_state_a": None,
"tx_state_b": None,
"coin_from": ci_from.coin_name(),
"coin_to": ci_to.coin_name(),
"amount_from": ci_from.format_amount(amount_from),
"amount_to": ci_to.format_amount(amount_to),
"rate": bid_rate,
"addr_from": bid.bid_addr if bid.was_received else offer.addr_from,
"was_sent": bid.was_sent,
filters = {
"sort_by": "created_at",
"sort_dir": "desc"
}
EXCLUDED_STATES = [
'Completed',
'Expired',
'Timed-out',
'Abandoned',
'Failed, refunded',
'Failed, swiped',
'Failed',
'Error',
'received'
]
all_bids = []
if offer.swap_type == SwapTypes.XMR_SWAP:
swap_data["tx_state_a"] = (
strTxState(bid.xmr_a_lock_tx.state)
if bid.xmr_a_lock_tx
else None
)
swap_data["tx_state_b"] = (
strTxState(bid.xmr_b_lock_tx.state)
if bid.xmr_b_lock_tx
else None
)
else:
swap_data["tx_state_a"] = bid.getITxState()
swap_data["tx_state_b"] = bid.getPTxState()
try:
received_bids = swap_client.listBids(filters=filters)
sent_bids = swap_client.listBids(sent=True, filters=filters)
for bid in received_bids + sent_bids:
try:
bid_state = strBidState(bid[5])
tx_state_a = strTxState(bid[7])
tx_state_b = strTxState(bid[8])
if bid_state in EXCLUDED_STATES:
continue
offer = swap_client.getOffer(bid[3])
if not offer:
continue
swap_data = {
"bid_id": bid[2].hex(),
"offer_id": bid[3].hex(),
"created_at": bid[0],
"bid_state": bid_state,
"tx_state_a": tx_state_a if tx_state_a else 'None',
"tx_state_b": tx_state_b if tx_state_b else 'None',
"coin_from": swap_client.ci(bid[9]).coin_name(),
"coin_to": swap_client.ci(offer.coin_to).coin_name(),
"amount_from": swap_client.ci(bid[9]).format_amount(bid[4]),
"amount_to": swap_client.ci(offer.coin_to).format_amount(
(bid[4] * bid[10]) // swap_client.ci(bid[9]).COIN()
),
"addr_from": bid[11],
"status": {
"main": bid_state,
"initial_tx": tx_state_a if tx_state_a else 'None',
"payment_tx": tx_state_b if tx_state_b else 'None'
}
}
all_bids.append(swap_data)
except Exception:
pass
continue
except Exception:
return bytes(json.dumps([]), "UTF-8")
return bytes(json.dumps(all_bids), "UTF-8")
def js_coinprices(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json)
if not have_data_entry(post_data, "coins"):
raise ValueError("Requires coins list.")
currency_to = Fiat.USD
if have_data_entry(post_data, "currency_to"):
currency_to = fiatFromTicker(get_data_entry(post_data, "currency_to"))
rate_source: str = "coingecko.com"
if have_data_entry(post_data, "source"):
rate_source = get_data_entry(post_data, "source")
match_input_key: bool = toBool(
get_data_entry_or(post_data, "match_input_key", "true")
)
ttl: int = int(get_data_entry_or(post_data, "ttl", 300))
coins = get_data_entry(post_data, "coins")
coins_list = coins.split(",")
coin_ids = []
input_id_map = {}
for coin in coins_list:
if coin.isdigit():
try:
coin_id = Coins(int(coin))
except Exception:
raise ValueError(f"Unknown coin type {coin}")
else:
try:
coin_id = getCoinIdFromTicker(coin)
except Exception:
try:
coin_id = getCoinIdFromName(coin)
except Exception:
raise ValueError(f"Unknown coin type {coin}")
coin_ids.append(coin_id)
input_id_map[coin_id] = coin
coinprices = swap_client.lookupFiatRates(
coin_ids, currency_to=currency_to, rate_source=rate_source, saved_ttl=ttl
)
rv = {}
for k, v in coinprices.items():
if match_input_key:
rv[input_id_map[k]] = v
else:
rv[int(k)] = v
return bytes(
json.dumps({"currency": currency_to.name, "source": rate_source, "rates": rv}),
"UTF-8",
)
def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json)
filters = {
"page_no": 1,
"limit": PAGE_LIMIT,
"sort_by": "created_at",
"sort_dir": "desc",
}
if have_data_entry(post_data, "sort_by"):
sort_by = get_data_entry(post_data, "sort_by")
ensure(
sort_by
in [
"created_at",
],
"Invalid sort by",
)
filters["sort_by"] = sort_by
if have_data_entry(post_data, "sort_dir"):
sort_dir = get_data_entry(post_data, "sort_dir")
ensure(sort_dir in ["asc", "desc"], "Invalid sort dir")
filters["sort_dir"] = sort_dir
if have_data_entry(post_data, "offset"):
filters["offset"] = int(get_data_entry(post_data, "offset"))
if have_data_entry(post_data, "limit"):
filters["limit"] = int(get_data_entry(post_data, "limit"))
ensure(filters["limit"] > 0, "Invalid limit")
if have_data_entry(post_data, "address_from"):
filters["address_from"] = get_data_entry(post_data, "address_from")
if have_data_entry(post_data, "address_to"):
filters["address_to"] = get_data_entry(post_data, "address_to")
action = get_data_entry_or(post_data, "action", None)
message_routes = swap_client.listMessageRoutes(filters, action)
return bytes(json.dumps(message_routes), "UTF-8")
endpoints = {
pages = {
"coins": js_coins,
"wallets": js_wallets,
"offers": js_offers,
@@ -1238,12 +1067,10 @@ endpoints = {
"help": js_help,
"readurl": js_readurl,
"active": js_active,
"coinprices": js_coinprices,
"messageroutes": js_messageroutes,
}
def js_url_to_function(url_split):
if len(url_split) > 2:
return endpoints.get(url_split[2], js_404)
return pages.get(url_split[2], js_404)
return js_index

View File

@@ -2,7 +2,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -24,13 +23,6 @@ protobuf ParseFromString would reset the whole object, from_bytes won't.
from basicswap.util.integer import encode_varint, decode_varint
NPBW_INT = 0
NPBW_BYTES = 2
NPBF_STR = 1
NPBF_BOOL = 2
class NonProtobufClass:
def __init__(self, init_all: bool = True, **kwargs):
for key, value in kwargs.items():
@@ -42,7 +34,7 @@ class NonProtobufClass:
found_field = True
break
if found_field is False:
raise ValueError(f"Got an unexpected keyword argument '{key}'")
raise ValueError(f"got an unexpected keyword argument '{key}'")
if init_all:
self.init_fields()
@@ -125,160 +117,150 @@ class NonProtobufClass:
class OfferMessage(NonProtobufClass):
_map = {
1: ("protocol_version", NPBW_INT, 0),
2: ("coin_from", NPBW_INT, 0),
3: ("coin_to", NPBW_INT, 0),
4: ("amount_from", NPBW_INT, 0),
5: ("amount_to", NPBW_INT, 0),
6: ("min_bid_amount", NPBW_INT, 0),
7: ("time_valid", NPBW_INT, 0),
8: ("lock_type", NPBW_INT, 0),
9: ("lock_value", NPBW_INT, 0),
10: ("swap_type", NPBW_INT, 0),
11: ("proof_address", NPBW_BYTES, NPBF_STR),
12: ("proof_signature", NPBW_BYTES, NPBF_STR),
13: ("pkhash_seller", NPBW_BYTES, 0),
14: ("secret_hash", NPBW_BYTES, 0),
15: ("fee_rate_from", NPBW_INT, 0),
16: ("fee_rate_to", NPBW_INT, 0),
17: ("amount_negotiable", NPBW_INT, NPBF_BOOL),
18: ("rate_negotiable", NPBW_INT, NPBF_BOOL),
19: ("proof_utxos", NPBW_BYTES, 0),
20: ("auto_accept_type", 0, 0),
1: ("protocol_version", 0, 0),
2: ("coin_from", 0, 0),
3: ("coin_to", 0, 0),
4: ("amount_from", 0, 0),
5: ("amount_to", 0, 0),
6: ("min_bid_amount", 0, 0),
7: ("time_valid", 0, 0),
8: ("lock_type", 0, 0),
9: ("lock_value", 0, 0),
10: ("swap_type", 0, 0),
11: ("proof_address", 2, 1),
12: ("proof_signature", 2, 1),
13: ("pkhash_seller", 2, 0),
14: ("secret_hash", 2, 0),
15: ("fee_rate_from", 0, 0),
16: ("fee_rate_to", 0, 0),
17: ("amount_negotiable", 0, 2),
18: ("rate_negotiable", 0, 2),
19: ("proof_utxos", 2, 0),
}
class BidMessage(NonProtobufClass):
_map = {
1: ("protocol_version", NPBW_INT, 0),
2: ("offer_msg_id", NPBW_BYTES, 0),
3: ("time_valid", NPBW_INT, 0),
4: ("amount", NPBW_INT, 0),
5: ("amount_to", NPBW_INT, 0),
6: ("pkhash_buyer", NPBW_BYTES, 0),
7: ("proof_address", NPBW_BYTES, NPBF_STR),
8: ("proof_signature", NPBW_BYTES, NPBF_STR),
9: ("proof_utxos", NPBW_BYTES, 0),
10: ("pkhash_buyer_to", NPBW_BYTES, 0),
1: ("protocol_version", 0, 0),
2: ("offer_msg_id", 2, 0),
3: ("time_valid", 0, 0),
4: ("amount", 0, 0),
5: ("amount_to", 0, 0),
6: ("pkhash_buyer", 2, 0),
7: ("proof_address", 2, 1),
8: ("proof_signature", 2, 1),
9: ("proof_utxos", 2, 0),
10: ("pkhash_buyer_to", 2, 0),
}
class BidAcceptMessage(NonProtobufClass):
# Step 3, seller -> buyer
_map = {
1: ("bid_msg_id", NPBW_BYTES, 0),
2: ("initiate_txid", NPBW_BYTES, 0),
3: ("contract_script", NPBW_BYTES, 0),
4: ("pkhash_seller", NPBW_BYTES, 0),
1: ("bid_msg_id", 2, 0),
2: ("initiate_txid", 2, 0),
3: ("contract_script", 2, 0),
4: ("pkhash_seller", 2, 0),
}
class OfferRevokeMessage(NonProtobufClass):
_map = {
1: ("offer_msg_id", NPBW_BYTES, 0),
2: ("signature", NPBW_BYTES, 0),
1: ("offer_msg_id", 2, 0),
2: ("signature", 2, 0),
}
class BidRejectMessage(NonProtobufClass):
_map = {
1: ("bid_msg_id", NPBW_BYTES, 0),
2: ("reject_code", NPBW_INT, 0),
1: ("bid_msg_id", 2, 0),
2: ("reject_code", 0, 0),
}
class XmrBidMessage(NonProtobufClass):
# MSG1L, F -> L
_map = {
1: ("protocol_version", NPBW_INT, 0),
2: ("offer_msg_id", NPBW_BYTES, 0),
3: ("time_valid", NPBW_INT, 0),
4: ("amount", NPBW_INT, 0),
5: ("amount_to", NPBW_INT, 0),
6: ("pkaf", NPBW_BYTES, 0),
7: ("kbvf", NPBW_BYTES, 0),
8: ("kbsf_dleag", NPBW_BYTES, 0),
9: ("dest_af", NPBW_BYTES, 0),
1: ("protocol_version", 0, 0),
2: ("offer_msg_id", 2, 0),
3: ("time_valid", 0, 0),
4: ("amount", 0, 0),
5: ("amount_to", 0, 0),
6: ("pkaf", 2, 0),
7: ("kbvf", 2, 0),
8: ("kbsf_dleag", 2, 0),
9: ("dest_af", 2, 0),
}
class XmrSplitMessage(NonProtobufClass):
_map = {
1: ("msg_id", NPBW_BYTES, 0),
2: ("msg_type", NPBW_INT, 0),
3: ("sequence", NPBW_INT, 0),
4: ("dleag", NPBW_BYTES, 0),
1: ("msg_id", 2, 0),
2: ("msg_type", 0, 0),
3: ("sequence", 0, 0),
4: ("dleag", 2, 0),
}
class XmrBidAcceptMessage(NonProtobufClass):
_map = {
1: ("bid_msg_id", NPBW_BYTES, 0),
2: ("pkal", NPBW_BYTES, 0),
3: ("kbvl", NPBW_BYTES, 0),
4: ("kbsl_dleag", NPBW_BYTES, 0),
1: ("bid_msg_id", 2, 0),
2: ("pkal", 2, 0),
3: ("kbvl", 2, 0),
4: ("kbsl_dleag", 2, 0),
# MSG2F
5: ("a_lock_tx", NPBW_BYTES, 0),
6: ("a_lock_tx_script", NPBW_BYTES, 0),
7: ("a_lock_refund_tx", NPBW_BYTES, 0),
8: ("a_lock_refund_tx_script", NPBW_BYTES, 0),
9: ("a_lock_refund_spend_tx", NPBW_BYTES, 0),
10: ("al_lock_refund_tx_sig", NPBW_BYTES, 0),
5: ("a_lock_tx", 2, 0),
6: ("a_lock_tx_script", 2, 0),
7: ("a_lock_refund_tx", 2, 0),
8: ("a_lock_refund_tx_script", 2, 0),
9: ("a_lock_refund_spend_tx", 2, 0),
10: ("al_lock_refund_tx_sig", 2, 0),
}
class XmrBidLockTxSigsMessage(NonProtobufClass):
# MSG3L
_map = {
1: ("bid_msg_id", NPBW_BYTES, 0),
2: ("af_lock_refund_spend_tx_esig", NPBW_BYTES, 0),
3: ("af_lock_refund_tx_sig", NPBW_BYTES, 0),
1: ("bid_msg_id", 2, 0),
2: ("af_lock_refund_spend_tx_esig", 2, 0),
3: ("af_lock_refund_tx_sig", 2, 0),
}
class XmrBidLockSpendTxMessage(NonProtobufClass):
# MSG4F
_map = {
1: ("bid_msg_id", NPBW_BYTES, 0),
2: ("a_lock_spend_tx", NPBW_BYTES, 0),
3: ("kal_sig", NPBW_BYTES, 0),
1: ("bid_msg_id", 2, 0),
2: ("a_lock_spend_tx", 2, 0),
3: ("kal_sig", 2, 0),
}
class XmrBidLockReleaseMessage(NonProtobufClass):
# MSG5F
_map = {
1: ("bid_msg_id", NPBW_BYTES, 0),
2: ("al_lock_spend_tx_esig", NPBW_BYTES, 0),
1: ("bid_msg_id", 2, 0),
2: ("al_lock_spend_tx_esig", 2, 0),
}
class ADSBidIntentMessage(NonProtobufClass):
# L -> F Sent from bidder, construct a reverse bid
_map = {
1: ("protocol_version", NPBW_INT, 0),
2: ("offer_msg_id", NPBW_BYTES, 0),
3: ("time_valid", NPBW_INT, 0),
4: ("amount_from", NPBW_INT, 0),
5: ("amount_to", NPBW_INT, 0),
1: ("protocol_version", 0, 0),
2: ("offer_msg_id", 2, 0),
3: ("time_valid", 0, 0),
4: ("amount_from", 0, 0),
5: ("amount_to", 0, 0),
}
class ADSBidIntentAcceptMessage(NonProtobufClass):
# F -> L Sent from offerer, construct a reverse bid
_map = {
1: ("bid_msg_id", NPBW_BYTES, 0),
2: ("pkaf", NPBW_BYTES, 0),
3: ("kbvf", NPBW_BYTES, 0),
4: ("kbsf_dleag", NPBW_BYTES, 0),
5: ("dest_af", NPBW_BYTES, 0),
}
class ConnectReqMessage(NonProtobufClass):
_map = {
1: ("network_type", NPBW_INT, 0),
2: ("network_data", NPBW_BYTES, 0),
3: ("request_type", NPBW_INT, 0),
4: ("request_data", NPBW_BYTES, 0),
1: ("bid_msg_id", 2, 0),
2: ("pkaf", 2, 0),
3: ("kbvf", 2, 0),
4: ("kbsf_dleag", 2, 0),
5: ("dest_af", 2, 0),
}

View File

@@ -1,523 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import base64
import json
import threading
import traceback
import websocket
from queue import Queue, Empty
from basicswap.util.smsg import (
smsgEncrypt,
smsgDecrypt,
smsgGetID,
)
from basicswap.chainparams import (
Coins,
)
from basicswap.util.address import (
b58decode,
decodeWif,
)
def encode_base64(data: bytes) -> str:
return base64.b64encode(data).decode("utf-8")
def decode_base64(encoded_data: str) -> bytes:
return base64.b64decode(encoded_data)
class WebSocketThread(threading.Thread):
def __init__(self, url: str, tag: str = None, logger=None):
super().__init__()
self.url: str = url
self.tag = tag
self.logger = logger
self.ws = None
self.mutex = threading.Lock()
self.corrId: int = 0
self.connected: bool = False
self.delay_event = threading.Event()
self.recv_queue = Queue()
self.cmd_recv_queue = Queue()
self.delayed_events_queue = Queue()
self.ignore_events: bool = False
self.num_messages_received: int = 0
def disable_debug_mode(self):
self.ignore_events = False
for i in range(100):
try:
message = self.delayed_events_queue.get(block=False)
except Empty:
break
self.recv_queue.put(message)
def on_message(self, ws, message):
if self.logger:
self.logger.debug("Simplex received msg")
else:
print(f"{self.tag} - Received msg")
if message.startswith('{"corrId"'):
self.cmd_recv_queue.put(message)
else:
self.num_messages_received += 1
self.recv_queue.put(message)
def queue_get(self):
try:
return self.recv_queue.get(block=False)
except Empty:
return None
def cmd_queue_get(self):
try:
return self.cmd_recv_queue.get(block=False)
except Empty:
return None
def on_error(self, ws, error):
if self.logger:
self.logger.error(f"Simplex ws - {error}")
else:
print(f"{self.tag} - Error: {error}")
def on_close(self, ws, close_status_code, close_msg):
if self.logger:
self.logger.info(f"Simplex ws - Closed: {close_status_code}, {close_msg}")
else:
print(f"{self.tag} - Closed: {close_status_code}, {close_msg}")
def on_open(self, ws):
if self.logger:
self.logger.info("Simplex ws - Connection opened")
else:
print(f"{self.tag}: WebSocket connection opened")
self.connected = True
def send_command(self, cmd_str: str):
with self.mutex:
self.corrId += 1
if self.logger:
self.logger.debug(f"Simplex sent command {self.corrId}")
else:
print(f"{self.tag}: sent command {self.corrId}")
cmd = json.dumps({"corrId": str(self.corrId), "cmd": cmd_str})
self.ws.send(cmd)
return self.corrId
def wait_for_command_response(self, cmd_id, num_tries: int = 200):
cmd_id = str(cmd_id)
for i in range(num_tries):
message = self.cmd_queue_get()
if message is not None:
data = json.loads(message)
if "corrId" in data:
if data["corrId"] == cmd_id:
return data
self.delay_event.wait(0.5)
raise ValueError(
f"wait_for_command_response timed-out waiting for ID: {cmd_id}"
)
def run(self):
self.ws = websocket.WebSocketApp(
self.url,
on_message=self.on_message,
on_error=self.on_error,
on_open=self.on_open,
on_close=self.on_close,
)
while not self.delay_event.is_set():
self.ws.run_forever()
self.delay_event.wait(0.5)
def stop(self):
self.delay_event.set()
if self.ws:
self.ws.close()
def waitForResponse(ws_thread, sent_id, delay_event):
sent_id = str(sent_id)
for i in range(200):
message = ws_thread.cmd_queue_get()
if message is not None:
data = json.loads(message)
if "corrId" in data:
if data["corrId"] == sent_id:
return data
delay_event.wait(0.5)
raise ValueError(f"waitForResponse timed-out waiting for ID: {sent_id}")
def waitForConnected(ws_thread, delay_event):
for i in range(100):
if ws_thread.connected:
return True
delay_event.wait(0.5)
raise ValueError("waitForConnected timed-out.")
def getPrivkeyForAddress(self, addr) -> bytes:
ci_part = self.ci(Coins.PART)
try:
return ci_part.decodeKey(
self.callrpc(
"smsgdumpprivkey",
[
addr,
],
)
)
except Exception as e: # noqa: F841
pass
try:
return ci_part.decodeKey(
ci_part.rpc_wallet(
"dumpprivkey",
[
addr,
],
)
)
except Exception as e: # noqa: F841
pass
raise ValueError("key not found")
def encryptMsg(
self,
addr_from: str,
addr_to: str,
payload: bytes,
msg_valid: int,
cursor,
timestamp=None,
deterministic=False,
) -> bytes:
self.log.debug("encryptMsg")
try:
rv = self.callrpc(
"smsggetpubkey",
[
addr_to,
],
)
pubkey_to: bytes = b58decode(rv["publickey"])
except Exception as e: # noqa: F841
use_cursor = self.openDB(cursor)
try:
query: str = "SELECT pk_from FROM offers WHERE addr_from = :addr_to LIMIT 1"
rows = use_cursor.execute(query, {"addr_to": addr_to}).fetchall()
if len(rows) > 0:
pubkey_to = rows[0][0]
else:
query: str = (
"SELECT pk_bid_addr FROM bids WHERE bid_addr = :addr_to LIMIT 1"
)
rows = use_cursor.execute(query, {"addr_to": addr_to}).fetchall()
if len(rows) > 0:
pubkey_to = rows[0][0]
else:
raise ValueError(f"Could not get public key for address {addr_to}")
finally:
if cursor is None:
self.closeDB(use_cursor, commit=False)
privkey_from = getPrivkeyForAddress(self, addr_from)
payload += bytes((0,)) # Include null byte to match smsg
smsg_msg: bytes = smsgEncrypt(
privkey_from, pubkey_to, payload, timestamp, deterministic
)
return smsg_msg
def sendSimplexMsg(
self,
network,
addr_from: str,
addr_to: str,
payload: bytes,
msg_valid: int,
cursor,
timestamp: int = None,
deterministic: bool = False,
to_user_name: str = None,
) -> bytes:
self.log.debug("sendSimplexMsg")
smsg_msg: bytes = encryptMsg(
self, addr_from, addr_to, payload, msg_valid, cursor, timestamp, deterministic
)
smsg_id = smsgGetID(smsg_msg)
ws_thread = network["ws_thread"]
if to_user_name is not None:
to = "@" + to_user_name + " "
else:
to = "#bsx "
sent_id = ws_thread.send_command(to + encode_base64(smsg_msg))
response = waitForResponse(ws_thread, sent_id, self.delay_event)
if getResponseData(response, "type") != "newChatItems":
json_str = json.dumps(response, indent=4)
self.log.debug(f"Response {json_str}")
raise ValueError("Send failed")
return smsg_id
def decryptSimplexMsg(self, msg_data):
ci_part = self.ci(Coins.PART)
# Try with the network key first
network_key: bytes = decodeWif(self.network_key)
try:
decrypted = smsgDecrypt(network_key, msg_data, output_dict=True)
decrypted["from"] = ci_part.pubkey_to_address(
bytes.fromhex(decrypted["pk_from"])
)
decrypted["to"] = self.network_addr
decrypted["msg_net"] = "simplex"
return decrypted
except Exception as e: # noqa: F841
pass
# Try with all active bid/offer addresses
query: str = """SELECT DISTINCT address FROM (
SELECT b.bid_addr AS address FROM bids b
JOIN bidstates s ON b.state = s.state_id
WHERE b.active_ind = 1
AND (s.in_progress OR (s.swap_ended = 0 AND b.expire_at > :now))
UNION
SELECT addr_from AS address FROM offers WHERE active_ind = 1 AND expire_at > :now
)"""
now: int = self.getTime()
try:
cursor = self.openDB()
addr_rows = cursor.execute(query, {"now": now}).fetchall()
finally:
self.closeDB(cursor, commit=False)
decrypted = None
for row in addr_rows:
addr = row[0]
try:
vk_addr = getPrivkeyForAddress(self, addr)
decrypted = smsgDecrypt(vk_addr, msg_data, output_dict=True)
decrypted["from"] = ci_part.pubkey_to_address(
bytes.fromhex(decrypted["pk_from"])
)
decrypted["to"] = addr
decrypted["msg_net"] = "simplex"
return decrypted
except Exception as e: # noqa: F841
pass
return decrypted
def parseSimplexMsg(self, chat_item):
item_status = chat_item["chatItem"]["meta"]["itemStatus"]
dir_type = item_status["type"]
if dir_type not in ("sndRcvd", "rcvNew"):
return None
snd_progress = item_status.get("sndProgress", None)
if snd_progress and snd_progress != "complete":
item_id = chat_item["chatItem"]["meta"]["itemId"]
self.log.debug(f"simplex chat item {item_id} {snd_progress}")
return None
conn_id = None
msg_dir: str = "recv" if dir_type == "rcvNew" else "sent"
chat_type: str = chat_item["chatInfo"]["type"]
if chat_type == "group":
chat_name = chat_item["chatInfo"]["groupInfo"]["localDisplayName"]
conn_id = chat_item["chatInfo"]["groupInfo"]["groupId"]
self.num_group_simplex_messages_received += 1
elif chat_type == "direct":
chat_name = chat_item["chatInfo"]["contact"]["localDisplayName"]
conn_id = chat_item["chatInfo"]["contact"]["activeConn"]["connId"]
self.num_direct_simplex_messages_received += 1
else:
return None
msg_content = chat_item["chatItem"]["content"]["msgContent"]["text"]
try:
msg_data: bytes = decode_base64(msg_content)
decrypted_msg = decryptSimplexMsg(self, msg_data)
if decrypted_msg is None:
return None
decrypted_msg["chat_type"] = chat_type
decrypted_msg["chat_name"] = chat_name
decrypted_msg["conn_id"] = conn_id
decrypted_msg["msg_dir"] = msg_dir
return decrypted_msg
except Exception as e: # noqa: F841
# self.log.debug(f"decryptSimplexMsg error: {e}")
self.log.debug(f"decryptSimplexMsg error: {e}")
pass
return None
def processEvent(self, ws_thread, msg_type: str, data) -> bool:
if ws_thread.ignore_events:
if msg_type not in ("contactConnected", "contactDeletedByContact"):
return False
ws_thread.delayed_events_queue.put(json.dumps(data))
return True
if msg_type == "contactConnected":
self.processContactConnected(data)
elif msg_type == "contactDeletedByContact":
self.processContactDisconnected(data)
else:
return False
return True
def readSimplexMsgs(self, network):
ws_thread = network["ws_thread"]
for i in range(100):
message = ws_thread.queue_get()
if message is None:
break
if self.delay_event.is_set():
break
data = json.loads(message)
# self.log.debug(f"Message: {json.dumps(data, indent=4)}")
try:
msg_type: str = getResponseData(data, "type")
if msg_type in ("chatItemsStatusesUpdated", "newChatItems"):
for chat_item in getResponseData(data, "chatItems"):
decrypted_msg = parseSimplexMsg(self, chat_item)
if decrypted_msg is None:
continue
self.processMsg(decrypted_msg)
elif msg_type == "chatError":
# self.log.debug(f"chatError Message: {json.dumps(data, indent=4)}")
pass
elif processEvent(self, ws_thread, msg_type, data):
pass
else:
self.log.debug(f"Unknown msg_type: {msg_type}")
# self.log.debug(f"Message: {json.dumps(data, indent=4)}")
except Exception as e:
self.log.debug(f"readSimplexMsgs error: {e}")
if self.debug:
self.log.error(traceback.format_exc())
self.delay_event.wait(0.05)
def getResponseData(data, tag=None):
if "Right" in data["resp"]:
if tag:
return data["resp"]["Right"][tag]
return data["resp"]["Right"]
if tag:
return data["resp"][tag]
return data["resp"]
def getNewSimplexLink(data):
response_data = getResponseData(data)
if "connLinkContact" in response_data:
return response_data["connLinkContact"]["connFullLink"]
return response_data["connReqContact"]
def getJoinedSimplexLink(data):
response_data = getResponseData(data)
if "connLinkInvitation" in response_data:
return response_data["connLinkInvitation"]["connFullLink"]
return response_data["connReqInvitation"]
def initialiseSimplexNetwork(self, network_config) -> None:
self.log.debug("initialiseSimplexNetwork")
client_host: str = network_config.get("client_host", "127.0.0.1")
ws_port: str = network_config.get("ws_port")
ws_thread = WebSocketThread(f"ws://{client_host}:{ws_port}", logger=self.log)
self.threads.append(ws_thread)
ws_thread.start()
waitForConnected(ws_thread, self.delay_event)
sent_id = ws_thread.send_command("/groups")
response = waitForResponse(ws_thread, sent_id, self.delay_event)
if len(getResponseData(response, "groups")) < 1:
sent_id = ws_thread.send_command("/c " + network_config["group_link"])
response = waitForResponse(ws_thread, sent_id, self.delay_event)
assert "groupLinkId" in getResponseData(response, "connection")
network = {
"type": "simplex",
"ws_thread": ws_thread,
}
self.active_networks.append(network)
def closeSimplexChat(self, net_i, connId) -> bool:
try:
cmd_id = net_i.send_command("/chats")
response = net_i.wait_for_command_response(cmd_id, num_tries=500)
remote_name = None
for chat in getResponseData(response, "chats"):
if (
"chatInfo" not in chat
or "type" not in chat["chatInfo"]
or chat["chatInfo"]["type"] != "direct"
):
continue
try:
if chat["chatInfo"]["contact"]["activeConn"]["connId"] == connId:
remote_name = chat["chatInfo"]["contact"]["localDisplayName"]
break
except Exception as e:
self.log.debug(f"Error parsing chat: {e}")
if remote_name is None:
self.log.warning(
f"Unable to find remote name for simplex direct chat, ID: {connId}"
)
return False
self.log.debug(f"Deleting simplex chat @{remote_name}, connID {connId}")
cmd_id = net_i.send_command(f"/delete @{remote_name}")
cmd_response = net_i.wait_for_command_response(cmd_id)
if getResponseData(cmd_response, "type") != "contactDeleted":
self.log.warning(f"Failed to delete simplex chat, ID: {connId}")
self.log.debug(
"cmd_response: {}".format(json.dumps(cmd_response, indent=4))
)
return False
except Exception as e:
self.log.warning(f"Error deleting simplex chat, ID: {connId} - {e}")
return False
return True

View File

@@ -1,132 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import select
import sqlite3
import subprocess
import time
from basicswap.util.daemon import Daemon
def initSimplexClient(args, logger, delay_event):
logger.info("Initialising Simplex client")
(pipe_r, pipe_w) = os.pipe() # subprocess.PIPE is buffered, blocks when read
if os.name == "nt":
str_args = " ".join(args)
p = subprocess.Popen(
str_args, shell=True, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w
)
else:
p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=pipe_w, stderr=pipe_w)
def readOutput():
buf = os.read(pipe_r, 1024).decode("utf-8")
response = None
# logger.debug(f"simplex-chat output: {buf}")
if "display name:" in buf:
logger.debug("Setting display name")
response = b"user\n"
else:
logger.debug(f"Unexpected output: {buf}")
return
if response is not None:
p.stdin.write(response)
p.stdin.flush()
try:
start_time: int = time.time()
max_wait_seconds: int = 60
while p.poll() is None:
if time.time() > start_time + max_wait_seconds:
raise RuntimeError("Timed out")
if os.name == "nt":
readOutput()
delay_event.wait(0.1)
continue
while len(select.select([pipe_r], [], [], 0)[0]) == 1:
readOutput()
delay_event.wait(0.1)
except Exception as e:
logger.error(f"initSimplexClient: {e}")
finally:
if p.poll() is None:
p.terminate()
os.close(pipe_r)
os.close(pipe_w)
p.stdin.close()
def startSimplexClient(
bin_path: str,
data_path: str,
server_address: str,
websocket_port: int,
logger,
delay_event,
socks_proxy=None,
log_level: str = "debug",
) -> Daemon:
logger.info("Starting Simplex client")
if not os.path.exists(data_path):
os.makedirs(data_path)
simplex_data_prefix = os.path.join(data_path, "simplex_client_data")
simplex_db_path = simplex_data_prefix + "_chat.db"
args = [bin_path, "-d", simplex_data_prefix, "-p", str(websocket_port)]
if socks_proxy:
args += ["--socks-proxy", socks_proxy]
if not os.path.exists(simplex_db_path):
# Need to set initial profile through CLI
# TODO: Must be a better way?
init_args = args + ["-e", "/help"] # Run command to exit client
init_args += ["-s", server_address]
initSimplexClient(init_args, logger, delay_event)
else:
# Workaround to avoid error:
# SQLite3 returned ErrorConstraint while attempting to perform step: UNIQUE constraint failed: protocol_servers.user_id, protocol_servers.host, protocol_servers.port
# TODO: Remove?
with sqlite3.connect(simplex_db_path) as con:
c = con.cursor()
if ":" in server_address:
host, port = server_address.split(":")
else:
host = server_address
port = ""
query: str = (
"SELECT COUNT(*) FROM protocol_servers WHERE host = :host and port = :port"
)
q = c.execute(query, {"host": host, "port": port}).fetchone()
if q[0] < 1:
args += ["-s", server_address]
args += ["-l", log_level]
opened_files = []
stdout_dest = open(
os.path.join(data_path, "simplex_stdout.log"),
"w",
)
opened_files.append(stdout_dest)
stderr_dest = stdout_dest
return Daemon(
subprocess.Popen(
args,
shell=False,
stdin=subprocess.PIPE,
stdout=stdout_dest,
stderr=stderr_dest,
cwd=data_path,
),
opened_files,
"simplex-chat",
)

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from basicswap.util.address import b58decode
def getMsgPubkey(self, msg) -> bytes:
if "pk_from" in msg:
return bytes.fromhex(msg["pk_from"])
rv = self.callrpc(
"smsggetpubkey",
[
msg["from"],
],
)
return b58decode(rv["publickey"])

View File

@@ -1,54 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: FB44 AF81 A45B DE32 7319 797C 8510 7E35 7D4A 17FC
Comment: SimpleX Chat <chat@simplex.chat>
xsFNBGRDvZkBEACsxFENFWj5hMS1dCPCOXIJTNnWClVarltfUOESy5q0Ar84WJaj
hmAcc8j1Qw7uiLxVq/j+tMxcZOy79jnmhWpV5KrYA6H/E3I5NNlZOyT23rvah9mg
KtxfMHnhz/jJSwSXifYN2mmAYetQ1TQBSdLZayC7aW6BFhUaaQsaFABGli5abRUW
KArmnSfVEHI0f7TthLerPZ0hCoK06ZOPxEKCWt5CSqrC3J2d+8Cyb6j2jxkkB3GN
JXr9kI4JebivqrFNwvGw15xEDbSXIZf9I/+B/t9EA4Ebs+qrbLFRH5Drha50RIhu
LNYCkVnpKbrO6Y90KkJibm4ZtdUeNTFXjfXxT81Gi5lDmsvIyIMkFC78ePK68knM
dnESnIzEEwDtniV+ZvY0L9t/Ig1tGYggqPGVTVp9672bHKTGdiL3eXEzwv0FROD2
0HaZORXj2UZkAJTQO2ia7aS3hWdJL/iVBf4yIYARr+6NjPxv/sUMCaeuPYXTqCOB
Ykl6Lv3SPoSkEyPfVJY+12STtHH1ZofxJKYwo6Xe7EvmCiC9DK0KKVbeakZZ6wfd
5LO/tArDkqT2YjT3DUsfGqxQOoQvGCmk9yUuCm0s0vLwTHdJhSVgn9dxrEuK4FYL
IM3tGENAPAcK3e1VEbncgBMRikxvECKIz+YZyQVtoYzX2HDlT4D2HrbgXQARAQAB
zSBTaW1wbGVYIENoYXQgPGNoYXRAc2ltcGxleC5jaGF0PsLBlAQTAQgAPhYhBPtE
r4GkW94ycxl5fIUQfjV9Shf8BQJkQ72ZAhsDBQkHhh9CBQsJCAcCBhUKCQgLAgQW
AgMBAh4BAheAAAoJEIUQfjV9Shf8GekP/jpZYGJrna7467Qe82KV+qtwu+p2cRIy
IsoOmCje2p0D9DmmmDQH1IdxlJhvHZ8uEu21QwDK03r5y4iaXhz9bx4CDSDB5JPp
fMIDfOdc1V1GDT8Q2f/sYd5DX9kwpW6LdWOQZf6hwRDAeWDa+BQVhwo3E0WsPvRK
o5fqrbJzfWj8pz+JMlT8RGGt0ZxEyUjnD9C6XfqGckLdubBycs9CipPKV+3X4cY/
ix0zM2Nb3oSJ27VWMIFxi7lnBGtyUY69bE248Xhj0nJ79twPwzvk94+3e5tLQvyt
NIZcWEZEu+eYthyKcGDo/aA6lIvt1Bqp8eeFMogRxs5GJI4L/wQGwIDckemtLb45
gUdjpufEfPEfxuYWuuHuQ8W7Yvd2/ndiRkir4k+r8ypXx8yeCgocxnuUm1+4s+Wv
h64Op+M+l56cTjVCaEn25kv/T+4ll/RBplzKdNe4ClcH4NXppwXFkAHXq/j3RX++
64gRzIEC33TGheTo3btowUW+0/6iOi7Jy1RDsNvigzWwpm0p+pje54+d7hTxDmLR
bHxOZ9QiauO/HnlqNw/MezZLYL18hyEsghD3ns6QIHcUsHf17u/tRfLgN11x9tiE
ADqORsNgQ8FIRGdJcxGIt8lUlSe5vKPArsjpiomoA9CeAqepU27haIesl2QGe/jI
5OuS7CsRVDlOzsFNBGRDvZkBEADGYf7E+bzYgORnlSY3TZgS5UvkMIGswlw6GW7j
Vx6hAsMbiCwoKCVdzl/J4BImbJIJg2Pxvn/k7tYS2Jqb1q/EcpBmOZU9BRiTw49A
TiK8UfeH9aIPNFwuiatmA29dGxPH2RgSCwa3f4l2RsnQl301UdNlXj6mmWngD6mj
ae5/COUgH6CbKptfLp0Xw0WpPfKV1GK9+/X8Hv7W6RDA6xoWFlgzTyuy96rMmXJ1
3E7P/50ebIOundVzCni10dZyn7+W13cJGOyzxQnbR6PEMVHsgi4uZB/Gt6PxF0dC
s56IUi05hr9uH++p7ps2G8iIwvqXDu8VOvwN9hvt1fpxnRC2+Zv0lHwpDrnSBvjY
8er2tJxXlybXwEpk1nzctmDDWrgbBgQugOxTu4rkqIvAGwq7U98aLUb3vEqlyWSp
YDufsiLbGYC5owCli36yDzjfm48W0DwaOA5Ne5yVCih1f4ocF3RXVU6o1TEW1pfL
DEDZOXDT9sj0qef9NW0Nz+x/EiCT2k2Bkwt0ETf4TralsJ7smCcbhqfJbu1NG22g
oLNXZcZgTUxmOWmU+nlrFk8Hk7EK2KDeMKSgiX6jrAGpwbphrYYBZ3NLpvJ311l2
d56ZgmUt8gb1O5tLNiD2ySCvWKnpG0A5WoKZ2329nlnX2R30otYdpP1vcAEvA3GU
7fw5lQARAQABwsF8BBgBCAAmFiEE+0SvgaRb3jJzGXl8hRB+NX1KF/wFAmRDvZkC
GwwFCQeGH0IACgkQhRB+NX1KF/wNbw//bi4RcxEOVJpT37pyx6wSlq6urHopuZA5
duy0fGYxRXt4w/WR0UMH9i7iSU8J2E/UKgE7OMZg3oJqVt7g70zQDiT8ez+ep9d0
YvPAqgRnT1VDmAyMO8FOTPQPIrPMsQTnmtmxf9qrdoxW8HVqiyK+7mCGqd9ldcer
XGplALTugRWABY7iYyRyfpDSid+xMKV7KLHabv/0WdcT41HpZuUt0gmH0sMDDiJt
XrWW01LDqEZTdfaZ1xXPPp7oXUYGY6U7cH5CdLS6D38tPKR9x0ttgM83/SOx/hOO
XApcA+g113eMOyh4udowGYEkpxT26V3u8cLzCBOPDNSFx/H8ggFbfMsCWNBYV2Nx
EmAmciHvPMNLR7Hjfvn018/Q+lo1J6snoEhT9zFwpL15Lwurkqy5Z4n1D9BUyZ7m
hS/Wg7LDpaEeJCkSkOvQEPKz8YsnMpsbPc44ZZf0yuTUsWwJkZCVEqN8qByKXRdI
28zGBBJr5/rjaSJJ7+VGbh/FGUzaEkLONybzKcxazwjSASBNZXmasgStngOGWGpM
GKDnIuXs/Z7vljkKF2YoNT9bvGr7yoY74PCKrMkWdVSA1cQBj+cJ4OOojVvOGJaR
Gdpp/2r7me5UKImmUw2dhHf0KdM1iYwjzztCO72hi5Fw7vFlNS7QoadmYDzAgWkk
0oXYKNS+x2w=
=68E9
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -1,25 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQMuBFmZ6L4RCACuqDDCIe2bzKznyKVN1aInzRQnSxdGTXuw0mcDz5HYudAhBjR8
gY6sxCRPNxvZCJVDZDpCygXMhWZlJtWLR8KMTCXxC4HLXXOY4RxQ5KGnYWxEAcKY
deq1ymmuOuMUp7ltRTSyWcBKbR9xTd2vW/+0W7GQIOxUW/aiT1V0x3cky+6kqaec
BorP3+uxJcx0Q8WdlS/6N4x3pBv/lfsdrZSaDD8fU/29pQGMDUEnupKoWJVVei6r
G+vxLHEtIFYYO8VWjZntymw3dl+aogrjyuxqWzl8mfPi9M/DgiRb4pJnH2yOGDI6
Lvg+oo9E79Vwi98UjYSicsB1dtcptKiA96UXAQD/hDB+dil7/SX/SDTlaw/+uTdd
Xg0No63dbN++iY4k3Qf/Xk1ZzbuDviLhe+zEhlJOw6TaMlxfwwQOtxEJXILS5uIL
jYlGcDbBtJh3p4qUoUduDOgjumJ9m47XqIq81rQ0pqzzGMbK1Y82NQjX5Sn8yTm9
p1hmOZ/uX9vCrUSbYBjxJXyQ1OXlerlLRLfBf5WQ0+LO+0cmgtCyX0zV4oGK7vph
XEm7lar7AezOOXaSrWAB+CTPUdJF1E7lcJiUuMVcqMx8pphrH+rfcsqPtN6tkyUD
TmPDpc5ViqFFelEEQnKSlmAY+3iCNZ3y/VdPPhuJ2lAsL3tm9MMh2JGV378LG45a
6SOkQrC977Qq1dhgJA+PGJxQvL2RJWsYlJwp79+Npgf9EfFaJVNzbdjGVq1XmNie
MZYqHRfABkyK0ooDxSyzJrq4vvuhWKInS4JhpKSabgNSsNiiaoDR+YYMHb0H8GRR
Za6JCmfU8w97R41UTI32N7dhul4xCDs5OV6maOIoNts20oigNGb7TKH9b5N7sDJB
zh3Of/fHCChO9Y2chbzU0bERfcn+evrWBf/9XdQGQ3ggoLbOtGpcUQuB/7ofTcBZ
awL6K4VJ2Qlb8DPlRgju6uU9AR/KTYeAlVFC8FX7R0FGgPRcJ3GNkNHGqrbuQ72q
AOhYOPx9nRrU5u+E2J325vOabLnLbOazze3j6LFPSFV4vfmTO9exYlwhz3g+lFAd
CrQ2Q2FsaW4gQ3VsaWFudSAoTmlsYWNUaGVHcmltKSA8Y2FsaW4uY3VsaWFudUBn
bWFpbC5jb20+iHoEExEIACIFAlmZ6L4CGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B
AheAAAoJECGBClQgMcAsU5cBAO/ngONpHsxny2uTV4ge2f+5V2ajTjcIfN2jUZtg
31jJAQCl1NcrwcIu98+aM2IyjB1UFXkoaMANpr8L9jBopivRGQ==
=cf8I
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -0,0 +1,14 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEXPNfCBYJKwYBBAHaRw8BAQdAWGFiEJYnlV2TDTesLIO/eoQ3IPduzcG97GpA
6K+Gj+K0K0BKZXJlbXlSYW5kIG9uIEdpdEh1YiA8amVyZW15QG5hbWVjb2luLm9y
Zz6IlgQTFggAPhYhBJza8EpykDv+wJWdvi2+M54p9ilMBQJc88q7AhsDBQkB4TOA
BQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEC2+M54p9ilMMUoA/1oZn8AtwQ7D
wXgNKq++zqHaiEcHGgsyFeDRbQARsYRVAQDxa36p181id1YuMjeV1KhC5vaDS4nY
GB4FHPsQ4bbqDLRESmVyZW15IFJhbmQgKE5hbWVjb2luIENvcmUgR2l0aWFuIFNp
Z25pbmcgS2V5KSA8amVyZW15QG5hbWVjb2luLm9yZz6ImQQTFggAQQIbAwUJAeEz
gAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgBYhBJza8EpykDv+wJWdvi2+M54p9ilM
BQJc9WDrAhkBAAoJEC2+M54p9ilMz3IA/3mCKeFYcEJFlwP43mdIMGOV2zt/R4Fn
z/rBJpv5hFoHAQDXAY8+mbY/9N+5Rn8Iy51tXEaTq3khdruuFFdty+bXAg==
=EpnJ
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -1,52 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGdeBqoBEADuBizUBhm1m34OQ0rnqUONvkfL3tGsriWuX0n3Bq9rhf3I3kZk
5fi+R0Jj6jmz+sbUYRULU35S6eeIY77eYiveWl81H+3JAu8kmo/S6gegINnsPM/g
F7X2P757gQdHMAE0olME3iGfXNpLg/e0OBv3j45eimjvUejgE7eI0e4gjajq8hyf
bizMrGT+5I2PE0g3h07NqN3OuI5xVYujuZp41EgxY99QgYm5qEoU0wMGy8+F7gXV
0htjhvUZcSGGpixP5+kaJJXFAP1TkZ/jqya6vy7LLeEEEuU8eMWhViOmzIjqoOFW
Mq+2rJUrzNEk43tXW5LU+DdGl90HQcXPmQP3aWL27Dx/4AcTMYPDB/0bJrU9qF9Y
9zfJV2HcNMnkhEb9XKDwkA6m3Jx2gfYG6HoMKp6bWSWsODItEgL1taoy35OnaVSM
NWb857DC6p6n+eQUXUNx/1ct4LWmf4lN4Uf61i4mD+hkc4cWmRLAh7vTqMGG4xmb
8Tb3wss8mEXzJvWVP4+bE6EkNPMCVAQleD4ePItaDg3lSJH/cIueIz6NDl5ik07r
AZOZTxhhGU1CD8NkxQKoZLZ6GgjHDEwiUbxaCoD0FAzqtG5/at+jiwyDmCsJ96aE
f0tPLXKOOc62BbqsAUuEOIooGwX/swXrhS4Xvfh8GxBYFBlRponoWXG7XQARAQAB
tEhSb3NlIFR1cmluZyAoUm9zZSBUdXJpbmcncyBzaWduaW5nIGtleSBmb3IgZGV2
IHdvcmsuKSA8cm9zZXR1cmluZ0BwbS5tZT6JAk4EEwEKADgWIQT9g2aoB6mfon/Z
zOqf47/dpsU0lQUCZ14GqgIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCf
47/dpsU0lTjfD/9WkMBWlbYhJwRU6JrdZdIPsj2jlMIDYEHXxFo+h1lNn1SLKKrE
4c/+9+H0YGM03pL5ZTtydsxdPMTbAP5l24hBFpokySds3abOcKaPuNcct5BDWiiL
UxsnV3SxCAsN3QcBt+0tYFYP9yIMkko9BRwsY7pSpjZOSCx26jeTKj7M4XQGdcpT
4KMtzXe2s8ss1jLyuaDP2B5ikrFI+IZ5dHVBhohK3ug1y0SzHjfSYeskOEYSgJ/4
uRUJCItWxrkSh16qRz+NFxwsewqIKz8Q0EmpHx4WpAii8z29IFPYKJEqdwcuPyF3
7SiqAow4tY+CtnLAUYEbSiL52e8W/U8KSnrxqhkpMd5wZ28z+k682A5uEQn5YjOy
7dBRjytSC2S87FJ+3zp4OtToDio8Wi0zpZWj/BD5K9raE2ct6Uiw3NG6JI8A7yaJ
pEENfMpxMgKc8G5t8NfiZdDFDw+P+bd6sMAk3q7ZFe/o0zJcsbhtYacBFvwBpeIp
HZnLdUQlKrZoASku7biTZyt7BBJZuNdVv6Q/K+pigJxTYCZNbbx9s/lzS6KGUKuD
yi7n/1qYFXVFktomR+Cm045btVNeAQpnfIKiJS77FNeB5saSWEAOcCMtUkoR74lA
9MGYdeWrPjvdeBu+Muvo/y1h57sVMwvStrXjGrJNs6KBcmvITXrek0osbrkCDQRn
XgaqARAAu8bgP9AbeNatYshdG1xoYv20FeC0MUz0oYu+FvVuhvaAePl/VFFBlh3O
CsCzJ+a+/hyeW22ZGZl62yblvlZcSTw1/WOv5zboFVVLD58/iiz3dCYAUUTQ2OaI
+oMLTCmZ/+GIcuVM1ZZMEohvR9eLcyzY89CgOi8R9+agqTXxNg7Uj43tPkgY2vc0
v66od1SrOAisduXVDAiqTbc6nax9d9aYt27zQlGfuVo5J//rnteHiGA7VphDLlCR
+dra1ZGjbdOieSyhxiEAkBPY2js6UqO/CoRn9uHaTSv4MJqzzMOzLfPni+6y3FqH
qaUoe3vr07Ehf85gBEL4IBiux/WL3Vi1WceqvNkS9aC0MVnnEgHbyAy2R6pWrtN5
vlxdrkqQcnnnYHvOupG5KPsgT/CFK0jGfA23I/dBPuI372EcqFLFpAB4q14cSLQE
ZER81pK7Q445vTv9qQIPu34oq0mg7GWlunduI4v7uGN+oSYIW0kfNLRnM4QjNhTP
07LJZLZoCRW2MyPqTbk8cM0UQDGFOozcjlSgSZSABLdHpnudArl6fzkMi4VH8WNS
JNXvtL2yX8cnOWXuOgK5pFuhr6zeRaHsjlMXgR5ZPSCiq0aMR4upk5n/Mn64qGVm
EnxDEBiGfgL1sl+GGl+rYxvH8vYEEX3fjTtlsaImUzKByfLaY60AEQEAAYkCNgQY
AQoAIBYhBP2DZqgHqZ+if9nM6p/jv92mxTSVBQJnXgaqAhsMAAoJEJ/jv92mxTSV
+0wP/itANwrdF+9kolUUVJg8Vkx7IgIGlcdIiUTxPAu9c8JdTKpziy9q7oVVpzLf
zo+4qgzXGUGuGtcHdM8XSFYQ8CAuuOdvPUvtKbNQiZ1DVjoS/wk4vrzIvLTS1VVd
f4jTgOImx3Tk75/8KX3EpCk26orMMBCHk7nWWia1KF8X2K2Hu1DZ9GqsWlE/uAPN
tS/+ONlbn6tlk1XWDvFC8DkDkRWNRPva++GP5ACylybOHy2rqWKNEtetYflDuMIc
5tkrXZ/rdZgzASKzSrNlEjN2DEBjl15WjUppOPkSc4QPK+SVza6UZJaE7oOrIOqs
tQRchspkyDFreCuK/WZLZC8SUwZ5rzbOsFMLUHeZtFtNkJGxwF1ZUNHbNPPCEaCN
oqNu/nkjxFqeydJfqDM8K8An9dQE2GkUm1nACpuLNgpILXebdG7ItVbbkjosx7HI
0i3BXHeQzT+xY1gmuFFGEVCf9bZVmYspXJaiRGFRfGVyc6mMtdow7urb/A9g5Jqb
Dkc+p29y9hCeOAVZfTY2C/GlWu9X/E64WJ2mQ3ujhtJmSgLM4ieYJU+lxosOC6BW
EjFrTOeLa+myW7qm+/R6Mo/545s1qXvXnDL5Z4aVkSHtUu+fiWBa4f4WaH3mxAAg
XLVwKhulQ3wPaCehbbMPbsQ+091iAOo+hn9s2BPfehM0ltgI
=atlH
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -1,29 +1,78 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFlLyYIBEADW3oJnVMDC94g+7OB1/IBUYNevCqJIOAtC0eeFS5g0XGvvqRLx
2NLUqn5te+R7deoGElkZFJLLxFUwEnhqGCRH50Iou5aanUzvgI5fVAbK3k0fp9vc
LKCR0fQVIidcLyqMpkLZo8BSE3+BWxFp/r2OHvh2dYtJC+BZVwblkDS3cqwKvUZx
IocvDs47Wo3tzZfEsqUavbbiGx+Dm0fCV7TVHdVLU7q3bZsHSRiyTUZ2EAApoAmT
ir9csVxv2IM8yf6/HwWi6/Lp7dgSG1+qBZ1lUPPTY+dFLPZyt/kul+vuOj6GLZaU
s3D66d7TaPCHKWAOnP9RHpic/iXODXVXo1KHJfa0x8fW7I+y7/Gb+5x/m4O0Bz2T
BivdrSAuFpXkPqwawlw4CPgI9fc801g83+ZFzD2jJ6qxkEgfnlmf+zGNn5tC4N5j
NRTQ+GyHo1w4824SXcSN590wgz8goGJC3QPJxbifvOA8GzQIVzpxHckofOVyqIEq
qSnkP2xn4mELqD7HcFnoojZBqFbF2cN+oWQ+niLN+v4qrUncpQI9SVWlyp66S+1T
BhBQj2QuX+3B+K27EiDbhNV7EX6xEbGsnB1poMc2aMiz6veybW3GnoM+2ppr8Ko/
12Ij7l+ZA44t6PWUfQQbNSbUk/0Yhd9QJ8VQVck+TaS6gtarTbURlSdmHwARAQAB
tCl0ZWNub3ZlcnQgKGdpdGlhbikgPHRlY25vdmVydEBwYXJ0aWNsLmlvPokCVAQT
AQgAPhYhBI5RfcEuwcw39kI6ihPxNlHJzw1rBQJZS8mCAhsDBQkSzAMABQsJCAcC
BhUICQoLAgQWAgMBAh4BAheAAAoJEBPxNlHJzw1rtdYP/22iRX8O2Q++MayPHNx5
XHAlMk9mfi5FB1qJwshtlhda7P9U/hOTi227wH+Mzh5dBje4t2DkoHzxlz/Wr4cQ
QUJMOYd0OEZY6kpAQkvtyYobIb6zlRQK1koAfNMxewmfZZGTlr16IUVCovGSFvZ+
hdYRDEjuHqXjpwBfrxFAy/HCnfY10qSRkJc5w1ypj1IkzlanS+xeRJSDvRTQDAEr
zv3xKcMGjCCHaaCP+tyAaViBaUOlvmZdWwg0gwQCuPLqIh0cfDbcg0quciRIpnyp
zINmfwngZCwXdIYfAzmCzMHw1J3iOiqfqK0EcpHMsL689VyQSPgsoEHtcOGHYjRL
pMPGvRFHtICrnCHENK3IcwFWDGXW+i3zgOlA7g48yYWWvSup+t8I6YT+FeeFlxSO
dj1GdeMA0O7gXZ7znLVduokL2Ef4dZjc+3NwBlFov52vwCZwQMAGsMriwEDB2rDZ
B7YOvAxlUB/kavtx/oE8fV7mZcuwYg02lq4bozF9xlhjOFaRit6xXnLVi0TuI76c
uz67nB9VkWczSLIzCyjNyFpWbx1BMxTYfehZX3+YNajXwG6HdEp9CAYK0u46Guz/
Pth67bbNVYyP/cuOIrv/hqQ6xo4mOBMDDxcCEAXx3rwxfbxNM8vlwrMcpITrtNON
r41bcxUIfMEDefPm5wnXep8W
=szpX
mQENBFlLyDYBCADqup3EHjFCMELf4I0smf4hDl48qDn/Hue08JLmSToMc7z9ylLk
6Uzx6S1m7RiDO63A7yW4qyRkb54VNj+6rUSPNt2uVy1vT8OEQJAZLf2c4qpaKHAQ
QV3utu8pYxYOJfLHh4zNEGXrbSrjDv/FTPuri+SkIABhjf70ZSocm4l49rtBanK5
AIAp8DoXWcUdbwmAfl6qrLfzrDu75kq+bspd8p4CVy4fzdOtr6LvXW38z1t3XtLP
+EGVMAzZQWr2WbN762rK7skH+ZfhaMjAwr8gPYymYnFGLdS1nBmhksnulQNGQOro
WojsvQKgBJoGUnp/OrVpi3gn7UNfDo99CxMRABEBAAG0IHRlY25vdmVydCA8dGVj
bm92ZXJ0QHBhcnRpY2wuaW8+iQFUBBMBCAA+FiEEOQGTZk5wi3vnahADcJ5tyVzr
Ac8FAllLyDYCGwMFCQPCZwAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQcJ5t
yVzrAc+0LAf/SvBJFJGq1yT9pdLT+7lv7BrshfSYQBLNqPmPrxRuxzH3q/EaEk6D
oQh/Jk4vmSXR1y+bsKtS55ekGsPZZWlUFMbXDuU0II3YkWewHXTnqxLtqzcWODoK
6vPonjiVuhYC57d4TWw5ebzHy8wICunyVeaL/cvYQM1TfaI2fN5v0Ep+XiRpH/15
HQzRaynKq58w7gH79mPIRA2WFz4eMIMWS3rSa+cSoJ0MhpimgnKUDlh2DebVP1eH
keSW1JlPZHhca/XB93ghFlbO6wOrbg+gsKtB45OkpsoOzUMFIKVJLBAjK751dTcc
Pb4xTzABaBXxk+IUxgGB1h+g3i6wzksfgLkBDQRZS8g2AQgAw7Db3G5J21jsty9S
pMmqp93dgZFm8E4VTcsL4KVvZybhwHngNHnhG8G/DWQ53o07/BKorfRBmFD3x2Eq
RqfOn4ytmZVw/sOjbZPi4m/tF8z+O9qR8I0CzedYip21rwz2j4UgnpDQ+BnOpyXB
H0gDBlPFq8ih9kkm413QRTTKnkRM/U8SfyFU8vIFdH7T0Ae07m0LxePDaTyxLPg3
x1+RvEjVkruc3/9Z4kzexoUv654wirRdxPX8GsWI1WNDQrj4GqmpF/e0WDM97+Lk
DGzbcXy7TGMIHQx8QFlFwdSZv9x70574as9Od4jOWTk90sopSMr8t6H6wTdn+2MD
qsZKUwARAQABiQE8BBgBCAAmFiEEOQGTZk5wi3vnahADcJ5tyVzrAc8FAllLyDYC
GwwFCQPCZwAACgkQcJ5tyVzrAc/QFgf8CQydF/VqJtujQC/rjB1YYNQcljzoeQWA
2F2O5cF5skTNYy+xas3PTgxfOpn5iTpixpkB+I7X8LwoPmRjZvg2MFirDVXUypcx
HwMbQqYCuAaK1EhtVUVYbFGjM67nClmBApLdenbqEP/BhyR9kgDCBt7ZvSLe5N/6
MKYJF1FlCgGc5OJPJrMIl0slU5QtzRy5J+l75WflkgxFUKJPotJ5Z+yduxOff//e
qSEXqlkaebWT0ZFiAqHhExJCRJ5HBqQEdW4JHrB7j3bNh8Qdf8epiYtcXXSsE9+K
XEP7UJRk5bFFKdn0wMONgmQLMjjspU5byMQDJ0hFNMmmrbKX2AXqRpkCDQRZS8mC
ARAA1t6CZ1TAwveIPuzgdfyAVGDXrwqiSDgLQtHnhUuYNFxr76kS8djS1Kp+bXvk
e3XqBhJZGRSSy8RVMBJ4ahgkR+dCKLuWmp1M74COX1QGyt5NH6fb3CygkdH0FSIn
XC8qjKZC2aPAUhN/gVsRaf69jh74dnWLSQvgWVcG5ZA0t3KsCr1GcSKHLw7OO1qN
7c2XxLKlGr224hsfg5tHwle01R3VS1O6t22bB0kYsk1GdhAAKaAJk4q/XLFcb9iD
PMn+vx8Fouvy6e3YEhtfqgWdZVDz02PnRSz2crf5Lpfr7jo+hi2WlLNw+une02jw
hylgDpz/UR6YnP4lzg11V6NShyX2tMfH1uyPsu/xm/ucf5uDtAc9kwYr3a0gLhaV
5D6sGsJcOAj4CPX3PNNYPN/mRcw9oyeqsZBIH55Zn/sxjZ+bQuDeYzUU0Phsh6Nc
OPNuEl3EjefdMIM/IKBiQt0DycW4n7zgPBs0CFc6cR3JKHzlcqiBKqkp5D9sZ+Jh
C6g+x3BZ6KI2QahWxdnDfqFkPp4izfr+Kq1J3KUCPUlVpcqeukvtUwYQUI9kLl/t
wfituxIg24TVexF+sRGxrJwdaaDHNmjIs+r3sm1txp6DPtqaa/CqP9diI+5fmQOO
Lej1lH0EGzUm1JP9GIXfUCfFUFXJPk2kuoLWq021EZUnZh8AEQEAAbQpdGVjbm92
ZXJ0IChnaXRpYW4pIDx0ZWNub3ZlcnRAcGFydGljbC5pbz6JAlQEEwEIAD4WIQSO
UX3BLsHMN/ZCOooT8TZRyc8NawUCWUvJggIbAwUJEswDAAULCQgHAgYVCAkKCwIE
FgIDAQIeAQIXgAAKCRAT8TZRyc8Na7XWD/9tokV/DtkPvjGsjxzceVxwJTJPZn4u
RQdaicLIbZYXWuz/VP4Tk4ttu8B/jM4eXQY3uLdg5KB88Zc/1q+HEEFCTDmHdDhG
WOpKQEJL7cmKGyG+s5UUCtZKAHzTMXsJn2WRk5a9eiFFQqLxkhb2foXWEQxI7h6l
46cAX68RQMvxwp32NdKkkZCXOcNcqY9SJM5Wp0vsXkSUg70U0AwBK8798SnDBowg
h2mgj/rcgGlYgWlDpb5mXVsINIMEArjy6iIdHHw23INKrnIkSKZ8qcyDZn8J4GQs
F3SGHwM5gszB8NSd4joqn6itBHKRzLC+vPVckEj4LKBB7XDhh2I0S6TDxr0RR7SA
q5whxDStyHMBVgxl1vot84DpQO4OPMmFlr0rqfrfCOmE/hXnhZcUjnY9RnXjANDu
4F2e85y1XbqJC9hH+HWY3PtzcAZRaL+dr8AmcEDABrDK4sBAwdqw2Qe2DrwMZVAf
5Gr7cf6BPH1e5mXLsGINNpauG6MxfcZYYzhWkYresV5y1YtE7iO+nLs+u5wfVZFn
M0iyMwsozchaVm8dQTMU2H3oWV9/mDWo18Buh3RKfQgGCtLuOhrs/z7Yeu22zVWM
j/3LjiK7/4akOsaOJjgTAw8XAhAF8d68MX28TTPL5cKzHKSE67TTja+NW3MVCHzB
A3nz5ucJ13qfFrkCDQRZS8mCARAA7QMvR0fFA1FZKzcS6/W5Jcm0g6FQ1xHaMeEh
LECOQpM3wSOL1A8trbpC2VgMLjRFq+h3YQRlF8Y4oIaIz2UzziqK6mGZxhtEN6y3
IIXrVC5CTpcDXxlvJyHeHQONvMnEbmnbHfZAtxJq2wFOr7BWiLVzfioyNSND/JOP
VlgezL6YRAocQbHU7mQKY7gCqU4jDZIxru01e2hoIHSbAFXjmEcFBFoErWXAMf5w
HaK7dGGMpJXgNCK2weatNCBxD/krv1gA7nheT665K7HUQxu/NhUIk8XnOPD5iDoJ
zeQXHY3SM8jrhhabRubm27c/Oads9lgk9EGZhxLhIMQ9jUu7TsX1sPZpfnoE/JAq
ofY3WwimOXYb+p0jetg4FQaqul6FpgesSI4Nl5nHHB8/4CWUv2oV2YjUJlBpazyc
ullt8a7GdwzQMbiw23Jgz1frrMuq/zQc4wLGUFchhnYMrva+6t0ewjxD7bCL/7N7
3UDdNpVi+ZcBVQPVididC4iRcCLDqmr+WtTfVKw58Rnb7Qt9Z+2MqVZa1/numTG1
DastjRg6KGkN6eYaxKcXHf7t/lYZ5ejGFVUh+wtwlb1tTpOvWKq130tuO/aDWTa2
jViwy2UUpbyg5UbBvd0PHTJ+8TTdxEoC5wQCYHZ5Ueg9wwLhs0VQ44GI7vnXJZ8b
aXUe/mEAEQEAAYkCPAQYAQgAJhYhBI5RfcEuwcw39kI6ihPxNlHJzw1rBQJZS8mC
AhsMBQkSzAMAAAoJEBPxNlHJzw1r+3YQAM5648S/oQLnK5WO0/w3gIUI5g7BrdJO
kRINe8SNYs6PvCFjKij/3p9YMxrc/TojTQfhxew7bNxkhDU7sudxIr6TcKW5SK9f
g9zz2Ib5heR+orjPSX9hgSLX66t4DvJfdph+O1O3l83g0bsDUPCivTSnQ5XtdiVK
ytOoM26/GaQHwzKbk1Qzn1nrZeLaeDAsJ30GdmteNRMof1G2H9kg/33xbcyRCMaT
xjKS0ssa8RUmxuYsR+fjc7t5FvXwnfoXapkqUWcddFCCgAiTc0NZjzcDSXVB/++2
KxLZ0Q86kuJwdb7KEq0SwPQAM6ikmIaoke9fJAZzhyyWX7AeSQx1ime31Xrjh0CC
MHW+PdQMpLSNTAHEZDuybGKaShVMiHASXs7XsnJr6lOObMYzSGr0+B5fQWU7aHlM
u+4YNHUwQldx/EqkL/DjIpocVC5ozaW+dV1zSMLBHdk24soWI+gLrL3FG0NMyNZ+
O95X/bB/X+dqOBYpitR3xpYZes4Jl4Kechi60+mdDktFKfKfiRxyJlg2LNd7/OLB
hpxg2zsXlHhqhSJAo9IGih2rOgcMwtCXKmHCGG5KGsNF8x3H9bPOwynAUMqUJ2cR
7BCjzmUxUnsLcJnokUnHMbECZ+pee9YcaRNrlbVAIvED3ZHEhFJxIMaArxSLmRwE
XHovfCfpcB/C
=0Wkp
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -1,29 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGROehcBEACXWWc6dHqCos1PmKI32iHi0jP3mYM3jU57YxbjwT78QEtEwSqf
YklpXkgTYq7jexx2JElfegM6w1sPYarq1y051RjnCgzl32da5I506SMvcJTmXumV
Rw6erPeDxAO74PflDSlALgtGOgbKhwwWRudbWgT5hKGkl62qy0mI6GStul0rbT+3
gq77DCGyURfe1PG1pymhO5XVz3WGtOa12NvRA+3wGIcqIji2MbtXuOhGMg//kVI5
m2vcfHyMMuQ01xUXRu57WxRujYaJ1RB4p86JCbDX3YU2XlzTxGAhqChDLuJGqo54
AZMUWDceftXsAoOqH8Hwmm5gFkYSpMt86ZT+umvWygmxohD5k85MuRj4AGagFj/u
CMcQjI/SN1UU/Qozg6VL/5FO8aH9IybDzX7eE3j0V/jTweStw1CIUajYgfemWOWl
whLPBDflRz/8EEqTN0CaSSaiYiULZUiawBO/bRIiCO2Q6QrAi3KpPUhCwiw/Yecd
rAMLH7bytpECDdbNonQ/VMxWwtWJQ87qBtWvHFQxXBKjyuANsKL9X7v3KcYOUdd2
fSt7eqE9GDT4DbK6sTmuTpq2TgHXET0cA39+N2zxTh5xFupI/pi2iAHJ6hgIiQnn
662TngjGOSFvrTV/51Ua0Vx8OCMJJOcRdOVaYzuzg9DsjVcJin3aRqUh4wARAQAB
tCBXb3dhcmlvIDx3b3dhcmlvQHByb3Rvbm1haWwuY29tPokCVAQTAQgAPhYhBKs6
L3JYGPz/J5SEHHk1BLRJxpIgBQJkTnoXAhsDBQkHhh77BQsJCAcCBhUKCQgLAgQW
AgMBAh4BAheAAAoJEHk1BLRJxpIgwgEP/109vw1WXRh9EaRr3Y1+sBi+/PRQ5TCx
UEcP9ru5sQPJ0BsZK8RYw0BNIfDQX9OB1k/AoiBelL+0EoDKvjXmwz9fPUmSVk5r
3RzfClXTnxn4HXPKkSGMt4WBUnvohTexK7CPkb9xy+K0Jtx8XF1XiQLDFg2a9lBj
IIX2H6aHn4VjdUBv7TrTCAI2Vg0cQUpeJUwyHH+lk0r2WM3zAxzS3Iy2yDDstNT8
audXEX4BtJhyEU1m57jwgscrbTtgwYOAsaRLcnUaAFWhbov3IiGInk7N1fkMsuW5
HE5RcegSZRS3X4o6O/nmwdSjCEB9weydOCPrtfdbvfvuTiMg/jZBikOk/Sj7FM/D
eZKghSHpLbT/V3S76FyIcc/xFkUmR+2fGvCNjJ1Qn2lXTS8xcbyzqR4LZPeUGppV
hvriilLnXSjyc60wuD3kmCCo1Zw4tNL8pr09BtVmScUy6eiwca8LLzvbbivqxF1g
Mrkkv8yQE0ZwO1kgNSn+PSzUPbwAoklcyN5Rhr5DxZh0UudiH5Jt5WWYeE8O2Uc1
si13X575kymGkkeiUcp9WtBkh2uial+RVmTrUTDUTIR2HzT6MAR84/DHlC5dsW8a
h4uDUhzeG2cTxuIfZC881UHKL+xT/I3PPuFdLbU5uoWJpXYpxKYulYWd7LA/k4bi
JWBrQo7VDvvP
=H3wS
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import json
import traceback
import shlex
import urllib
import traceback
import subprocess
from xmlrpc.client import (
Fault,
Transport,
@@ -102,7 +104,7 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
r = json.loads(v.decode("utf-8"))
except Exception as ex:
traceback.print_exc()
raise ValueError(f"RPC server error: {ex}, method: {method}")
raise ValueError("RPC server error " + str(ex) + ", method: " + method)
if "error" in r and r["error"] is not None:
raise ValueError("RPC error " + str(r["error"]))
@@ -118,7 +120,36 @@ def openrpc(rpc_port, auth, wallet=None, host="127.0.0.1"):
return Jsonrpc(url)
except Exception as ex:
traceback.print_exc()
raise ValueError(f"RPC error: {ex}")
raise ValueError("RPC error " + str(ex))
def callrpc_cli(bindir, datadir, chain, cmd, cli_bin="particl-cli", wallet=None):
cli_bin = os.path.join(bindir, cli_bin)
args = [
cli_bin,
]
if chain != "mainnet":
args.append("-" + chain)
args.append("-datadir=" + datadir)
if wallet is not None:
args.append("-rpcwallet=" + wallet)
args += shlex.split(cmd)
p = subprocess.Popen(
args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
out = p.communicate()
if len(out[1]) > 0:
raise ValueError("RPC error " + str(out[1]))
r = out[0].decode("utf-8").strip()
try:
r = json.loads(r)
except Exception:
pass
return r
def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
@@ -128,6 +159,7 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
host = host
def rpc_func(method, params=None, wallet_override=None):
nonlocal port, auth, wallet, host
return callrpc(
port,
auth,

View File

@@ -309,6 +309,7 @@ def make_xmr_rpc2_func(
transport.set_proxy(proxy_host, proxy_port)
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
nonlocal port, auth, host, transport, tag
return callrpc_xmr2(
port,
method,
@@ -344,6 +345,7 @@ def make_xmr_rpc_func(
transport.set_proxy(proxy_host, proxy_port)
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
nonlocal port, auth, host, transport, tag
return callrpc_xmr(
port,
method,

View File

@@ -365,147 +365,3 @@ select.disabled-select-enabled {
#toggle-auto-refresh[data-enabled="true"] {
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
}
/* Multi-select dropdown styles */
.multi-select-dropdown::-webkit-scrollbar {
width: 12px;
}
.multi-select-dropdown::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 6px;
}
.multi-select-dropdown::-webkit-scrollbar-thumb {
background: #888;
border-radius: 6px;
}
.multi-select-dropdown::-webkit-scrollbar-thumb:hover {
background: #555;
}
.dark .multi-select-dropdown::-webkit-scrollbar-track {
background: #374151;
}
.dark .multi-select-dropdown::-webkit-scrollbar-thumb {
background: #6b7280;
}
.dark .multi-select-dropdown::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
.multi-select-dropdown input[type="checkbox"]:focus {
outline: none !important;
box-shadow: none !important;
border-color: inherit !important;
}
.multi-select-dropdown label:focus-within {
outline: none !important;
box-shadow: none !important;
}
#coin_to_button:focus,
#coin_from_button:focus {
outline: none !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
border-color: #3b82f6 !important;
}
.coin-badge {
background: #3b82f6;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
display: inline-flex;
align-items: center;
gap: 4px;
margin: 2px;
}
.coin-badge .remove {
cursor: pointer;
font-weight: bold;
opacity: 0.7;
margin-left: 4px;
}
.coin-badge .remove:hover {
opacity: 1;
}
.multi-select-dropdown {
max-height: 300px;
overflow-y: auto;
z-index: 9999 !important;
position: fixed !important;
min-width: 200px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
}
.multi-select-dropdown::-webkit-scrollbar {
width: 6px;
}
.multi-select-dropdown::-webkit-scrollbar-track {
background: #f1f1f1;
}
.multi-select-dropdown::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
.multi-select-dropdown::-webkit-scrollbar-thumb:hover {
background: #555;
}
.dark .multi-select-dropdown::-webkit-scrollbar-track {
background: #374151;
}
.dark .multi-select-dropdown::-webkit-scrollbar-thumb {
background: #6b7280;
}
.dropdown-container {
position: relative;
z-index: 1;
}
.dropdown-container.open {
z-index: 9999;
}
.filter-button-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.multi-select-dropdown input[type="checkbox"] {
outline: none !important;
box-shadow: none !important;
border: 1px solid #d1d5db;
border-radius: 3px;
}
.multi-select-dropdown input[type="checkbox"]:focus {
outline: none !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
border-color: #3b82f6 !important;
}
.multi-select-dropdown input[type="checkbox"]:checked {
background-color: #3b82f6 !important;
border-color: #3b82f6 !important;
}
.dark .multi-select-dropdown input[type="checkbox"] {
border-color: #6b7280;
background-color: #374151;
}
.dark .multi-select-dropdown input[type="checkbox"]:focus {
border-color: #3b82f6 !important;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
}
.dark .multi-select-dropdown input[type="checkbox"]:checked {
background-color: #3b82f6 !important;
border-color: #3b82f6 !important;
}
.multi-select-dropdown label {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 743 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,5 +1,22 @@
// Constants and State
const PAGE_SIZE = 50;
const COIN_NAME_TO_SYMBOL = {
'Bitcoin': 'BTC',
'Litecoin': 'LTC',
'Monero': 'XMR',
'Particl': 'PART',
'Particl Blind': 'PART',
'Particl Anon': 'PART',
'PIVX': 'PIVX',
'Firo': 'FIRO',
'Dash': 'DASH',
'Decred': 'DCR',
'Wownero': 'WOW',
'Bitcoin Cash': 'BCH',
'Dogecoin': 'DOGE'
};
// Global state
const state = {
identities: new Map(),
currentPage: 1,
@@ -10,6 +27,7 @@ const state = {
refreshPromise: null
};
// DOM
const elements = {
swapsBody: document.getElementById('active-swaps-body'),
prevPageButton: document.getElementById('prevPage'),
@@ -22,6 +40,105 @@ const elements = {
statusText: document.getElementById('status-text')
};
// Identity Manager
const IdentityManager = {
cache: new Map(),
pendingRequests: new Map(),
retryDelay: 2000,
maxRetries: 3,
cacheTimeout: 5 * 60 * 1000, // 5 minutes
async getIdentityData(address) {
if (!address) {
return { address: '' };
}
const cachedData = this.getCachedIdentity(address);
if (cachedData) {
return { ...cachedData, address };
}
if (this.pendingRequests.has(address)) {
const pendingData = await this.pendingRequests.get(address);
return { ...pendingData, address };
}
const request = this.fetchWithRetry(address);
this.pendingRequests.set(address, request);
try {
const data = await request;
this.cache.set(address, {
data,
timestamp: Date.now()
});
return { ...data, address };
} catch (error) {
console.warn(`Error fetching identity for ${address}:`, error);
return { address };
} finally {
this.pendingRequests.delete(address);
}
},
getCachedIdentity(address) {
const cached = this.cache.get(address);
if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
return cached.data;
}
if (cached) {
this.cache.delete(address);
}
return null;
},
async fetchWithRetry(address, attempt = 1) {
try {
const response = await fetch(`/json/identities/${address}`, {
signal: AbortSignal.timeout(5000)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
...data,
address,
num_sent_bids_successful: safeParseInt(data.num_sent_bids_successful),
num_recv_bids_successful: safeParseInt(data.num_recv_bids_successful),
num_sent_bids_failed: safeParseInt(data.num_sent_bids_failed),
num_recv_bids_failed: safeParseInt(data.num_recv_bids_failed),
num_sent_bids_rejected: safeParseInt(data.num_sent_bids_rejected),
num_recv_bids_rejected: safeParseInt(data.num_recv_bids_rejected),
label: data.label || '',
note: data.note || '',
automation_override: safeParseInt(data.automation_override)
};
} catch (error) {
if (attempt >= this.maxRetries) {
console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`);
return {
address,
num_sent_bids_successful: 0,
num_recv_bids_successful: 0,
num_sent_bids_failed: 0,
num_recv_bids_failed: 0,
num_sent_bids_rejected: 0,
num_recv_bids_rejected: 0,
label: '',
note: '',
automation_override: 0
};
}
await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
return this.fetchWithRetry(address, attempt + 1);
}
}
};
const safeParseInt = (value) => {
const parsed = parseInt(value);
return isNaN(parsed) ? 0 : parsed;
@@ -83,6 +200,7 @@ const getTxStatusClass = (status) => {
return 'text-blue-500';
};
// Util
const formatTimeAgo = (timestamp) => {
const now = Math.floor(Date.now() / 1000);
const diff = now - timestamp;
@@ -93,6 +211,7 @@ const formatTimeAgo = (timestamp) => {
return `${Math.floor(diff / 86400)} days ago`;
};
const formatTime = (timestamp) => {
if (!timestamp) return '';
const date = new Date(timestamp * 1000);
@@ -132,6 +251,96 @@ const getTimeStrokeColor = (expireTime) => {
return '#10B981'; // More than 30 minutes
};
// WebSocket Manager
const WebSocketManager = {
ws: null,
processingQueue: false,
reconnectTimeout: null,
maxReconnectAttempts: 5,
reconnectAttempts: 0,
reconnectDelay: 5000,
initialize() {
this.connect();
this.startHealthCheck();
},
connect() {
if (this.ws?.readyState === WebSocket.OPEN) return;
try {
const wsPort = window.ws_port || '11700';
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
this.setupEventHandlers();
} catch (error) {
console.error('WebSocket connection error:', error);
this.handleReconnect();
}
},
setupEventHandlers() {
this.ws.onopen = () => {
state.wsConnected = true;
this.reconnectAttempts = 0;
updateConnectionStatus('connected');
console.log('🟢 WebSocket connection established for Swaps in Progress');
updateSwapsTable({ resetPage: true, refreshData: true });
};
this.ws.onmessage = () => {
if (!this.processingQueue) {
this.processingQueue = true;
setTimeout(async () => {
try {
if (!state.isRefreshing) {
await updateSwapsTable({ resetPage: false, refreshData: true });
}
} finally {
this.processingQueue = false;
}
}, 200);
}
};
this.ws.onclose = () => {
state.wsConnected = false;
updateConnectionStatus('disconnected');
this.handleReconnect();
};
this.ws.onerror = () => {
updateConnectionStatus('error');
};
},
startHealthCheck() {
setInterval(() => {
if (this.ws?.readyState !== WebSocket.OPEN) {
this.handleReconnect();
}
}, 30000);
},
handleReconnect() {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
}
this.reconnectAttempts++;
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1);
this.reconnectTimeout = setTimeout(() => this.connect(), delay);
} else {
updateConnectionStatus('error');
setTimeout(() => {
this.reconnectAttempts = 0;
this.connect();
}, 60000);
}
}
};
// UI
const updateConnectionStatus = (status) => {
const { statusDot, statusText } = elements;
if (!statusDot || !statusText) return;
@@ -293,33 +502,12 @@ const createSwapTableRow = async (swap) => {
const identity = await IdentityManager.getIdentityData(swap.addr_from);
const uniqueId = `${swap.bid_id}_${swap.created_at}`;
const fromSymbol = window.CoinManager.getDisplayName(swap.coin_from) || swap.coin_from;
const toSymbol = window.CoinManager.getDisplayName(swap.coin_to) || swap.coin_to;
const fromSymbol = COIN_NAME_TO_SYMBOL[swap.coin_from] || swap.coin_from;
const toSymbol = COIN_NAME_TO_SYMBOL[swap.coin_to] || swap.coin_to;
const timeColor = getTimeStrokeColor(swap.expire_at);
const fromAmount = parseFloat(swap.amount_from) || 0;
const toAmount = parseFloat(swap.amount_to) || 0;
let send_column = "";
let recv_column = "";
if (swap.was_sent) {
send_column = `
<div class="text-sm font-semibold">${fromAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${fromSymbol}</div>
`
recv_column = `
<div class="text-sm font-semibold">${toAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${toSymbol}</div>
`
} else {
send_column = `
<div class="text-sm font-semibold">${toAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${toSymbol}</div>
`
recv_column = `
<div class="text-sm font-semibold">${fromAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${fromSymbol}</div>
`
}
return `
<tr class="relative opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600" data-bid-id="${swap.bid_id}">
<td class="relative w-0 p-0 m-0">
@@ -377,7 +565,8 @@ const createSwapTableRow = async (swap) => {
<div class="py-3 px-4 text-left">
<div class="items-center monospace">
<div class="pr-2">
${send_column}
<div class="text-sm font-semibold">${fromAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${fromSymbol}</div>
</div>
</div>
</div>
@@ -389,7 +578,7 @@ const createSwapTableRow = async (swap) => {
<div class="flex items-center justify-center">
<span class="inline-flex mr-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${window.CoinManager.getCoinIcon(swap.coin_from)}"
src="/static/images/coins/${swap.coin_from.replace(' ', '-')}.png"
alt="${swap.coin_from}"
onerror="this.src='/static/images/coins/default.png'">
</span>
@@ -398,7 +587,7 @@ const createSwapTableRow = async (swap) => {
</svg>
<span class="inline-flex ml-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${window.CoinManager.getCoinIcon(swap.coin_to)}"
src="/static/images/coins/${swap.coin_to.replace(' ', '-')}.png"
alt="${swap.coin_to}"
onerror="this.src='/static/images/coins/default.png'">
</span>
@@ -410,14 +599,16 @@ const createSwapTableRow = async (swap) => {
<td class="py-0">
<div class="py-3 px-4 text-right">
<div class="items-center monospace">
${recv_column}
<div class="text-sm font-semibold">${toAmount.toFixed(8)}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">${toSymbol}</div>
</div>
</div>
</td>
<!-- Status Column -->
<td class="py-3 px-4 text-center">
<div data-tooltip-target="tooltip-status-${uniqueId}" class="flex justify-center">
<span class="w-full lg:w-7/8 xl:w-2/3 px-2.5 py-1 inline-flex items-center justify-center text-center rounded-full text-xs font-medium bold ${getStatusClass(swap.bid_state, swap.tx_state_a, swap.tx_state_b)}">
<span class="px-2.5 py-1 text-xs font-medium rounded-full ${getStatusClass(swap.bid_state, swap.tx_state_a, swap.tx_state_b)}">
${swap.bid_state}
</span>
</div>
@@ -520,8 +711,6 @@ const createSwapTableRow = async (swap) => {
async function updateSwapsTable(options = {}) {
const { resetPage = false, refreshData = true } = options;
//console.log('Updating swaps table:', { resetPage, refreshData });
if (state.refreshPromise) {
await state.refreshPromise;
return;
@@ -547,19 +736,9 @@ async function updateSwapsTable(options = {}) {
}
const data = await response.json();
//console.log('Received swap data:', data);
state.swapsData = Array.isArray(data)
? data.filter(swap => {
const isActive = isActiveSwap(swap);
//console.log(`Swap ${swap.bid_id}: ${isActive ? 'Active' : 'Inactive'}`, swap.bid_state);
return isActive;
})
: [];
//console.log('Filtered active swaps:', state.swapsData);
state.swapsData = Array.isArray(data) ? data : [];
} catch (error) {
//console.error('Error fetching swap data:', error);
console.error('Error fetching swap data:', error);
state.swapsData = [];
} finally {
state.refreshPromise = null;
@@ -585,14 +764,13 @@ async function updateSwapsTable(options = {}) {
const endIndex = startIndex + PAGE_SIZE;
const currentPageSwaps = state.swapsData.slice(startIndex, endIndex);
//console.log('Current page swaps:', currentPageSwaps);
if (elements.swapsBody) {
if (currentPageSwaps.length > 0) {
const rowPromises = currentPageSwaps.map(swap => createSwapTableRow(swap));
const rows = await Promise.all(rowPromises);
elements.swapsBody.innerHTML = rows.join('');
// Initialize tooltips
if (window.TooltipManager) {
window.TooltipManager.cleanup();
const tooltipTriggers = document.querySelectorAll('[data-tooltip-target]');
@@ -607,7 +785,6 @@ async function updateSwapsTable(options = {}) {
});
}
} else {
//console.log('No active swaps found, displaying empty state');
elements.swapsBody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white">
@@ -617,6 +794,22 @@ async function updateSwapsTable(options = {}) {
}
}
if (elements.paginationControls) {
elements.paginationControls.style.display = totalPages > 1 ? 'flex' : 'none';
}
if (elements.currentPageSpan) {
elements.currentPageSpan.textContent = state.currentPage;
}
if (elements.prevPageButton) {
elements.prevPageButton.style.display = state.currentPage > 1 ? 'inline-flex' : 'none';
}
if (elements.nextPageButton) {
elements.nextPageButton.style.display = state.currentPage < totalPages ? 'inline-flex' : 'none';
}
} catch (error) {
console.error('Error updating swaps table:', error);
if (elements.swapsBody) {
@@ -632,10 +825,7 @@ async function updateSwapsTable(options = {}) {
}
}
function isActiveSwap(swap) {
return true;
}
// Event
const setupEventListeners = () => {
if (elements.refreshSwapsButton) {
elements.refreshSwapsButton.addEventListener('click', async (e) => {
@@ -675,11 +865,8 @@ const setupEventListeners = () => {
}
};
document.addEventListener('DOMContentLoaded', async () => {
// Init
document.addEventListener('DOMContentLoaded', () => {
WebSocketManager.initialize();
setupEventListeners();
await updateSwapsTable({ resetPage: true, refreshData: true });
const autoRefreshInterval = setInterval(async () => {
await updateSwapsTable({ resetPage: false, refreshData: true });
}, 10000); // 30 seconds
});

View File

@@ -1,255 +0,0 @@
const AmmCounterManager = (function() {
const config = {
refreshInterval: 10000,
ammStatusEndpoint: '/amm/status',
retryDelay: 5000,
maxRetries: 3,
debug: false
};
let refreshTimer = null;
let fetchRetryCount = 0;
let lastAmmStatus = null;
function isDebugEnabled() {
return localStorage.getItem('amm_debug_enabled') === 'true' || config.debug;
}
function debugLog(message, data) {
// if (isDebugEnabled()) {
// if (data) {
// console.log(`[AmmCounter] ${message}`, data);
// } else {
// console.log(`[AmmCounter] ${message}`);
// }
// }
}
function updateAmmCounter(count, status) {
const ammCounter = document.getElementById('amm-counter');
const ammCounterMobile = document.getElementById('amm-counter-mobile');
debugLog(`Updating AMM counter: count=${count}, status=${status}`);
if (ammCounter) {
ammCounter.textContent = count;
ammCounter.classList.remove('bg-blue-500', 'bg-gray-400');
ammCounter.classList.add(status === 'running' && count > 0 ? 'bg-blue-500' : 'bg-gray-400');
}
if (ammCounterMobile) {
ammCounterMobile.textContent = count;
ammCounterMobile.classList.remove('bg-blue-500', 'bg-gray-400');
ammCounterMobile.classList.add(status === 'running' && count > 0 ? 'bg-blue-500' : 'bg-gray-400');
}
updateAmmTooltips(count, status);
}
function updateAmmTooltips(count, status) {
debugLog(`updateAmmTooltips called with count=${count}, status=${status}`);
const subheaderTooltip = document.getElementById('tooltip-amm-subheader');
debugLog('Looking for tooltip-amm-subheader element:', subheaderTooltip);
if (subheaderTooltip) {
const statusText = status === 'running' ? 'Active' : 'Inactive';
const newContent = `
<p><b>Status:</b> ${statusText}</p>
<p><b>Currently active offers/bids:</b> ${count}</p>
`;
const statusParagraph = subheaderTooltip.querySelector('p:first-child');
const countParagraph = subheaderTooltip.querySelector('p:last-child');
if (statusParagraph && countParagraph) {
statusParagraph.innerHTML = `<b>Status:</b> ${statusText}`;
countParagraph.innerHTML = `<b>Currently active offers/bids:</b> ${count}`;
debugLog(`Updated AMM subheader tooltip paragraphs: status=${statusText}, count=${count}`);
} else {
subheaderTooltip.innerHTML = newContent;
debugLog(`Replaced AMM subheader tooltip content: status=${statusText}, count=${count}`);
}
refreshTooltipInstances('tooltip-amm-subheader', newContent);
} else {
debugLog('AMM subheader tooltip element not found - checking all tooltip elements');
const allTooltips = document.querySelectorAll('[id*="tooltip"]');
debugLog('All tooltip elements found:', Array.from(allTooltips).map(el => el.id));
}
}
function refreshTooltipInstances(tooltipId, newContent) {
const triggers = document.querySelectorAll(`[data-tooltip-target="${tooltipId}"]`);
triggers.forEach(trigger => {
if (trigger._tippy) {
trigger._tippy.setContent(newContent);
debugLog(`Updated Tippy instance content for ${tooltipId}`);
} else {
if (window.TooltipManager && typeof window.TooltipManager.create === 'function') {
window.TooltipManager.create(trigger, newContent, {
placement: trigger.getAttribute('data-tooltip-placement') || 'top'
});
debugLog(`Created new Tippy instance for ${tooltipId}`);
}
}
});
if (window.TooltipManager && typeof window.TooltipManager.refreshTooltip === 'function') {
window.TooltipManager.refreshTooltip(tooltipId, newContent);
debugLog(`Refreshed tooltip via TooltipManager for ${tooltipId}`);
}
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
setTimeout(() => {
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
debugLog(`Re-initialized tooltips for ${tooltipId}`);
}, 50);
}
}
function fetchAmmStatus() {
debugLog('Fetching AMM status...');
let url = config.ammStatusEndpoint;
if (isDebugEnabled()) {
url += '?debug=true';
}
return fetch(url, {
headers: {
'Accept': 'application/json',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
lastAmmStatus = data;
debugLog('AMM status data received:', data);
updateAmmCounter(data.amm_active_count, data.status);
fetchRetryCount = 0;
return data;
})
.catch(error => {
if (isDebugEnabled()) {
console.error('[AmmCounter] AMM status fetch error:', error);
}
if (fetchRetryCount < config.maxRetries) {
fetchRetryCount++;
debugLog(`Retrying AMM status fetch (${fetchRetryCount}/${config.maxRetries}) in ${config.retryDelay/1000}s`);
return new Promise(resolve => {
setTimeout(() => {
resolve(fetchAmmStatus());
}, config.retryDelay);
});
} else {
fetchRetryCount = 0;
throw error;
}
});
}
function startRefreshTimer() {
stopRefreshTimer();
debugLog('Starting AMM status refresh timer');
fetchAmmStatus()
.then(() => {})
.catch(() => {});
refreshTimer = setInterval(() => {
fetchAmmStatus()
.then(() => {})
.catch(() => {});
}, config.refreshInterval);
}
function stopRefreshTimer() {
if (refreshTimer) {
debugLog('Stopping AMM status refresh timer');
clearInterval(refreshTimer);
refreshTimer = null;
}
}
function setupWebSocketHandler() {
if (window.WebSocketManager && typeof window.WebSocketManager.addMessageHandler === 'function') {
debugLog('Setting up WebSocket handler for AMM status updates');
window.WebSocketManager.addMessageHandler('message', (data) => {
if (data && data.event) {
debugLog('WebSocket event received, refreshing AMM status');
fetchAmmStatus()
.then(() => {})
.catch(() => {});
}
});
}
}
function setupDebugListener() {
const debugCheckbox = document.getElementById('amm_debug');
if (debugCheckbox) {
debugLog('Found AMM debug checkbox, setting up listener');
localStorage.setItem('amm_debug_enabled', debugCheckbox.checked ? 'true' : 'false');
debugCheckbox.addEventListener('change', function() {
localStorage.setItem('amm_debug_enabled', this.checked ? 'true' : 'false');
debugLog(`Debug mode ${this.checked ? 'enabled' : 'disabled'}`);
});
}
}
const publicAPI = {
initialize: function(options = {}) {
Object.assign(config, options);
setupWebSocketHandler();
setupDebugListener();
startRefreshTimer();
debugLog('AMM Counter Manager initialized');
if (window.CleanupManager) {
window.CleanupManager.registerResource('ammCounterManager', this, (mgr) => mgr.dispose());
}
return this;
},
fetchAmmStatus: fetchAmmStatus,
updateCounter: updateAmmCounter,
updateTooltips: updateAmmTooltips,
startRefreshTimer: startRefreshTimer,
stopRefreshTimer: stopRefreshTimer,
dispose: function() {
debugLog('Disposing AMM Counter Manager');
stopRefreshTimer();
}
};
return publicAPI;
})();
document.addEventListener('DOMContentLoaded', function() {
if (!window.ammCounterManagerInitialized) {
window.AmmCounterManager = AmmCounterManager.initialize();
window.ammCounterManagerInitialized = true;
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,22 @@
// Constants and State
const PAGE_SIZE = 50;
const COIN_NAME_TO_SYMBOL = {
'Bitcoin': 'BTC',
'Litecoin': 'LTC',
'Monero': 'XMR',
'Particl': 'PART',
'Particl Blind': 'PART',
'Particl Anon': 'PART',
'PIVX': 'PIVX',
'Firo': 'FIRO',
'Dash': 'DASH',
'Decred': 'DCR',
'Wownero': 'WOW',
'Bitcoin Cash': 'BCH',
'Dogecoin': 'DOGE'
};
// Global state
const state = {
dentities: new Map(),
currentPage: 1,
@@ -10,6 +27,7 @@ const state = {
refreshPromise: null
};
// DOM
const elements = {
bidsBody: document.getElementById('bids-body'),
prevPageButton: document.getElementById('prevPage'),
@@ -22,6 +40,125 @@ const elements = {
statusText: document.getElementById('status-text')
};
// Identity Manager
const IdentityManager = {
cache: new Map(),
pendingRequests: new Map(),
retryDelay: 2000,
maxRetries: 3,
cacheTimeout: 5 * 60 * 1000, // 5 minutes
async getIdentityData(address) {
if (!address) {
return { address: '' };
}
const cachedData = this.getCachedIdentity(address);
if (cachedData) {
return { ...cachedData, address };
}
if (this.pendingRequests.has(address)) {
const pendingData = await this.pendingRequests.get(address);
return { ...pendingData, address };
}
const request = this.fetchWithRetry(address);
this.pendingRequests.set(address, request);
try {
const data = await request;
this.cache.set(address, {
data,
timestamp: Date.now()
});
return { ...data, address };
} catch (error) {
console.warn(`Error fetching identity for ${address}:`, error);
return { address };
} finally {
this.pendingRequests.delete(address);
}
},
getCachedIdentity(address) {
const cached = this.cache.get(address);
if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
return cached.data;
}
if (cached) {
this.cache.delete(address);
}
return null;
},
async fetchWithRetry(address, attempt = 1) {
try {
const response = await fetch(`/json/identities/${address}`, {
signal: AbortSignal.timeout(5000)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
...data,
address,
num_sent_bids_successful: safeParseInt(data.num_sent_bids_successful),
num_recv_bids_successful: safeParseInt(data.num_recv_bids_successful),
num_sent_bids_failed: safeParseInt(data.num_sent_bids_failed),
num_recv_bids_failed: safeParseInt(data.num_recv_bids_failed),
num_sent_bids_rejected: safeParseInt(data.num_sent_bids_rejected),
num_recv_bids_rejected: safeParseInt(data.num_recv_bids_rejected),
label: data.label || '',
note: data.note || '',
automation_override: safeParseInt(data.automation_override)
};
} catch (error) {
if (attempt >= this.maxRetries) {
console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`);
return {
address,
num_sent_bids_successful: 0,
num_recv_bids_successful: 0,
num_sent_bids_failed: 0,
num_recv_bids_failed: 0,
num_sent_bids_rejected: 0,
num_recv_bids_rejected: 0,
label: '',
note: '',
automation_override: 0
};
}
await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
return this.fetchWithRetry(address, attempt + 1);
}
},
clearCache() {
this.cache.clear();
this.pendingRequests.clear();
},
removeFromCache(address) {
this.cache.delete(address);
this.pendingRequests.delete(address);
},
cleanup() {
const now = Date.now();
for (const [address, cached] of this.cache.entries()) {
if (now - cached.timestamp >= this.cacheTimeout) {
this.cache.delete(address);
}
}
}
};
// Util
const formatTimeAgo = (timestamp) => {
const now = Math.floor(Date.now() / 1000);
const diff = now - timestamp;
@@ -205,6 +342,96 @@ const createIdentityTooltip = (identity) => {
`;
};
// WebSocket Manager
const WebSocketManager = {
ws: null,
processingQueue: false,
reconnectTimeout: null,
maxReconnectAttempts: 5,
reconnectAttempts: 0,
reconnectDelay: 5000,
initialize() {
this.connect();
this.startHealthCheck();
},
connect() {
if (this.ws?.readyState === WebSocket.OPEN) return;
try {
const wsPort = window.ws_port || '11700';
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
this.setupEventHandlers();
} catch (error) {
console.error('WebSocket connection error:', error);
this.handleReconnect();
}
},
setupEventHandlers() {
this.ws.onopen = () => {
state.wsConnected = true;
this.reconnectAttempts = 0;
updateConnectionStatus('connected');
console.log('🟢 WebSocket connection established for Bid Requests');
updateBidsTable({ resetPage: true, refreshData: true });
};
this.ws.onmessage = () => {
if (!this.processingQueue) {
this.processingQueue = true;
setTimeout(async () => {
try {
if (!state.isRefreshing) {
await updateBidsTable({ resetPage: false, refreshData: true });
}
} finally {
this.processingQueue = false;
}
}, 200);
}
};
this.ws.onclose = () => {
state.wsConnected = false;
updateConnectionStatus('disconnected');
this.handleReconnect();
};
this.ws.onerror = () => {
updateConnectionStatus('error');
};
},
startHealthCheck() {
setInterval(() => {
if (this.ws?.readyState !== WebSocket.OPEN) {
this.handleReconnect();
}
}, 30000);
},
handleReconnect() {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
}
this.reconnectAttempts++;
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1);
this.reconnectTimeout = setTimeout(() => this.connect(), delay);
} else {
updateConnectionStatus('error');
setTimeout(() => {
this.reconnectAttempts = 0;
this.connect();
}, 60000);
}
}
};
// UI
const updateConnectionStatus = (status) => {
const { statusDot, statusText } = elements;
if (!statusDot || !statusText) return;
@@ -272,8 +499,8 @@ const createBidTableRow = async (bid) => {
const rate = toAmount > 0 ? toAmount / fromAmount : 0;
const inverseRate = fromAmount > 0 ? fromAmount / toAmount : 0;
const fromSymbol = window.CoinManager.getSymbol(bid.coin_from) || bid.coin_from;
const toSymbol = window.CoinManager.getSymbol(bid.coin_to) || bid.coin_to;
const fromSymbol = COIN_NAME_TO_SYMBOL[bid.coin_from] || bid.coin_from;
const toSymbol = COIN_NAME_TO_SYMBOL[bid.coin_to] || bid.coin_to;
const timeColor = getTimeStrokeColor(bid.expire_at);
const uniqueId = `${bid.bid_id}_${bid.created_at}`;
@@ -352,7 +579,7 @@ const createBidTableRow = async (bid) => {
<div class="flex items-center justify-center">
<span class="inline-flex mr-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${window.CoinManager.getCoinIcon(bid.coin_from)}"
src="/static/images/coins/${bid.coin_from.replace(' ', '-')}.png"
alt="${bid.coin_from}"
onerror="this.src='/static/images/coins/default.png'">
</span>
@@ -361,13 +588,14 @@ const createBidTableRow = async (bid) => {
</svg>
<span class="inline-flex ml-3 align-middle items-center justify-center w-18 h-20 rounded">
<img class="h-12"
src="/static/images/coins/${window.CoinManager.getCoinIcon(bid.coin_to)}"
src="/static/images/coins/${bid.coin_to.replace(' ', '-')}.png"
alt="${bid.coin_to}"
onerror="this.src='/static/images/coins/default.png'">
</span>
</div>
</div>
</td>
<!-- You Get Column -->
<td class="py-0">
<div class="py-3 px-4 text-right">
@@ -624,6 +852,7 @@ async function updateBidsTable(options = {}) {
}
}
// Event
const setupEventListeners = () => {
if (elements.refreshBidsButton) {
elements.refreshBidsButton.addEventListener('click', async () => {
@@ -663,8 +892,8 @@ if (elements.refreshBidsButton) {
}
};
document.addEventListener('DOMContentLoaded', async () => {
// Init
document.addEventListener('DOMContentLoaded', () => {
WebSocketManager.initialize();
setupEventListeners();
await updateBidsTable({ resetPage: true, refreshData: true });
});

View File

@@ -4,18 +4,17 @@ const BidExporter = {
return 'No data to export';
}
const isAllTab = type === 'all';
const isSent = type === 'sent';
const headers = [
'Date/Time',
'Bid ID',
'Offer ID',
'From Address',
...(isAllTab ? ['Type'] : []),
'You Send Amount',
'You Send Coin',
'You Receive Amount',
'You Receive Coin',
isSent ? 'You Send Amount' : 'You Receive Amount',
isSent ? 'You Send Coin' : 'You Receive Coin',
isSent ? 'You Receive Amount' : 'You Send Amount',
isSent ? 'You Receive Coin' : 'You Send Coin',
'Status',
'Created At',
'Expires At'
@@ -24,13 +23,11 @@ const BidExporter = {
let csvContent = headers.join(',') + '\n';
bids.forEach(bid => {
const isSent = isAllTab ? (bid.source === 'sent') : (type === 'sent');
const row = [
`"${formatTime(bid.created_at)}"`,
`"${bid.bid_id}"`,
`"${bid.offer_id}"`,
`"${bid.addr_from}"`,
...(isAllTab ? [`"${bid.source}"`] : []),
isSent ? bid.amount_from : bid.amount_to,
`"${isSent ? bid.coin_from : bid.coin_to}"`,
isSent ? bid.amount_to : bid.amount_from,
@@ -106,15 +103,6 @@ const BidExporter = {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
if (typeof state !== 'undefined' && typeof EventManager !== 'undefined') {
const exportAllButton = document.getElementById('exportAllBids');
if (exportAllButton) {
EventManager.add(exportAllButton, 'click', (e) => {
e.preventDefault();
state.currentTab = 'all';
BidExporter.exportCurrentView();
});
}
const exportSentButton = document.getElementById('exportSentBids');
if (exportSentButton) {
EventManager.add(exportSentButton, 'click', (e) => {
@@ -140,14 +128,9 @@ const originalCleanup = window.cleanup || function(){};
window.cleanup = function() {
originalCleanup();
const exportAllButton = document.getElementById('exportAllBids');
const exportSentButton = document.getElementById('exportSentBids');
const exportReceivedButton = document.getElementById('exportReceivedBids');
if (exportAllButton && typeof EventManager !== 'undefined') {
EventManager.remove(exportAllButton, 'click');
}
if (exportSentButton && typeof EventManager !== 'undefined') {
EventManager.remove(exportSentButton, 'click');
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
document.addEventListener('DOMContentLoaded', () => {
const selectCache = {};
function updateSelectCache(select) {
const selectedOption = select.options[select.selectedIndex];
const image = selectedOption.getAttribute('data-image');
const name = selectedOption.textContent.trim();
selectCache[select.id] = { image, name };
}
function setSelectData(select) {
const selectedOption = select.options[select.selectedIndex];
const image = selectedOption.getAttribute('data-image') || '';
const name = selectedOption.textContent.trim();
select.style.backgroundImage = image ? `url(${image}?${new Date().getTime()})` : '';
const selectImage = select.nextElementSibling.querySelector('.select-image');
if (selectImage) {
selectImage.src = image;
}
const selectNameElement = select.nextElementSibling.querySelector('.select-name');
if (selectNameElement) {
selectNameElement.textContent = name;
}
updateSelectCache(select);
}
const selectIcons = document.querySelectorAll('.custom-select .select-icon');
const selectImages = document.querySelectorAll('.custom-select .select-image');
const selectNames = document.querySelectorAll('.custom-select .select-name');
selectIcons.forEach(icon => icon.style.display = 'none');
selectImages.forEach(image => image.style.display = 'none');
selectNames.forEach(name => name.style.display = 'none');
function setupCustomSelect(select) {
const options = select.querySelectorAll('option');
const selectIcon = select.parentElement.querySelector('.select-icon');
const selectImage = select.parentElement.querySelector('.select-image');
options.forEach(option => {
const image = option.getAttribute('data-image');
if (image) {
option.style.backgroundImage = `url(${image})`;
}
});
const storedValue = localStorage.getItem(select.name);
if (storedValue && select.value == '-1') {
select.value = storedValue;
}
select.addEventListener('change', () => {
setSelectData(select);
localStorage.setItem(select.name, select.value);
});
setSelectData(select);
selectIcon.style.display = 'none';
selectImage.style.display = 'none';
}
const customSelects = document.querySelectorAll('.custom-select select');
customSelects.forEach(setupCustomSelect);
});

View File

@@ -1,8 +1,6 @@
(function(window) {
'use strict';
const dropdownInstances = [];
function positionElement(targetEl, triggerEl, placement = 'bottom', offsetDistance = 8) {
targetEl.style.visibility = 'hidden';
targetEl.style.display = 'block';
@@ -60,9 +58,6 @@
this._handleScroll = this._handleScroll.bind(this);
this._handleResize = this._handleResize.bind(this);
this._handleOutsideClick = this._handleOutsideClick.bind(this);
dropdownInstances.push(this);
this.init();
}
@@ -71,8 +66,7 @@
this._targetEl.style.margin = '0';
this._targetEl.style.display = 'none';
this._targetEl.style.position = 'fixed';
this._targetEl.style.zIndex = '40';
this._targetEl.classList.add('dropdown-menu');
this._targetEl.style.zIndex = '50';
this._setupEventListeners();
this._initialized = true;
@@ -129,12 +123,6 @@
show() {
if (!this._visible) {
dropdownInstances.forEach(instance => {
if (instance !== this && instance._visible) {
instance.hide();
}
});
this._targetEl.style.display = 'block';
this._targetEl.style.visibility = 'hidden';
@@ -172,12 +160,6 @@
document.removeEventListener('click', this._handleOutsideClick);
window.removeEventListener('scroll', this._handleScroll, true);
window.removeEventListener('resize', this._handleResize);
const index = dropdownInstances.indexOf(this);
if (index > -1) {
dropdownInstances.splice(index, 1);
}
this._initialized = false;
}
}
@@ -202,8 +184,6 @@
initDropdowns();
}
Dropdown.instances = dropdownInstances;
window.Dropdown = Dropdown;
window.initDropdowns = initDropdowns;

View File

@@ -1,199 +0,0 @@
document.addEventListener('DOMContentLoaded', function() {
const burger = document.querySelectorAll('.navbar-burger');
const menu = document.querySelectorAll('.navbar-menu');
if (burger.length && menu.length) {
for (var i = 0; i < burger.length; i++) {
burger[i].addEventListener('click', function() {
for (var j = 0; j < menu.length; j++) {
menu[j].classList.toggle('hidden');
}
});
}
}
const close = document.querySelectorAll('.navbar-close');
const backdrop = document.querySelectorAll('.navbar-backdrop');
if (close.length) {
for (var k = 0; k < close.length; k++) {
close[k].addEventListener('click', function() {
for (var j = 0; j < menu.length; j++) {
menu[j].classList.toggle('hidden');
}
});
}
}
if (backdrop.length) {
for (var l = 0; l < backdrop.length; l++) {
backdrop[l].addEventListener('click', function() {
for (var j = 0; j < menu.length; j++) {
menu[j].classList.toggle('hidden');
}
});
}
}
const tooltipManager = TooltipManager.initialize();
tooltipManager.initializeTooltips();
setupShutdownModal();
setupDarkMode();
toggleImages();
});
function setupShutdownModal() {
const shutdownButtons = document.querySelectorAll('.shutdown-button');
const shutdownModal = document.getElementById('shutdownModal');
const closeModalButton = document.getElementById('closeShutdownModal');
const confirmShutdownButton = document.getElementById('confirmShutdown');
const shutdownWarning = document.getElementById('shutdownWarning');
function updateShutdownButtons() {
const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
shutdownButtons.forEach(button => {
if (activeSwaps > 0) {
button.classList.add('shutdown-disabled');
button.setAttribute('data-disabled', 'true');
button.setAttribute('title', 'Caution: Swaps in progress');
} else {
button.classList.remove('shutdown-disabled');
button.removeAttribute('data-disabled');
button.removeAttribute('title');
}
});
}
function closeAllDropdowns() {
const openDropdowns = document.querySelectorAll('.dropdown-menu:not(.hidden)');
openDropdowns.forEach(dropdown => {
if (dropdown.style.display !== 'none') {
dropdown.style.display = 'none';
}
});
if (window.Dropdown && window.Dropdown.instances) {
window.Dropdown.instances.forEach(instance => {
if (instance._visible) {
instance.hide();
}
});
}
}
function showShutdownModal() {
closeAllDropdowns();
const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
if (activeSwaps > 0) {
shutdownWarning.classList.remove('hidden');
confirmShutdownButton.textContent = 'Yes, Shut Down Anyway';
} else {
shutdownWarning.classList.add('hidden');
confirmShutdownButton.textContent = 'Yes, Shut Down';
}
shutdownModal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
function hideShutdownModal() {
shutdownModal.classList.add('hidden');
document.body.style.overflow = '';
}
if (shutdownButtons.length) {
shutdownButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
showShutdownModal();
});
});
}
if (closeModalButton) {
closeModalButton.addEventListener('click', hideShutdownModal);
}
if (confirmShutdownButton) {
confirmShutdownButton.addEventListener('click', function() {
const shutdownToken = document.querySelector('.shutdown-button')
.getAttribute('href').split('/').pop();
window.location.href = '/shutdown/' + shutdownToken;
});
}
if (shutdownModal) {
shutdownModal.addEventListener('click', function(e) {
if (e.target === this) {
hideShutdownModal();
}
});
}
if (shutdownButtons.length) {
updateShutdownButtons();
}
}
function setupDarkMode() {
const themeToggle = document.getElementById('theme-toggle');
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
if (themeToggleDarkIcon && themeToggleLightIcon) {
if (localStorage.getItem('color-theme') === 'dark' ||
(!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
}
}
function setTheme(theme) {
if (theme === 'light') {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
}
if (themeToggle) {
themeToggle.addEventListener('click', () => {
if (localStorage.getItem('color-theme') === 'dark') {
setTheme('light');
} else {
setTheme('dark');
}
if (themeToggleDarkIcon && themeToggleLightIcon) {
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
}
toggleImages();
});
}
}
function toggleImages() {
var html = document.querySelector('html');
var darkImages = document.querySelectorAll('.dark-image');
var lightImages = document.querySelectorAll('.light-image');
if (html && html.classList.contains('dark')) {
toggleImageDisplay(darkImages, 'block');
toggleImageDisplay(lightImages, 'none');
} else {
toggleImageDisplay(darkImages, 'none');
toggleImageDisplay(lightImages, 'block');
}
}
function toggleImageDisplay(images, display) {
images.forEach(function(img) {
img.style.display = display;
});
}

View File

@@ -0,0 +1,40 @@
// Burger menus
document.addEventListener('DOMContentLoaded', function() {
// open
const burger = document.querySelectorAll('.navbar-burger');
const menu = document.querySelectorAll('.navbar-menu');
if (burger.length && menu.length) {
for (var i = 0; i < burger.length; i++) {
burger[i].addEventListener('click', function() {
for (var j = 0; j < menu.length; j++) {
menu[j].classList.toggle('hidden');
}
});
}
}
// close
const close = document.querySelectorAll('.navbar-close');
const backdrop = document.querySelectorAll('.navbar-backdrop');
if (close.length) {
for (var k = 0; k < close.length; k++) {
close[k].addEventListener('click', function() {
for (var j = 0; j < menu.length; j++) {
menu[j].classList.toggle('hidden');
}
});
}
}
if (backdrop.length) {
for (var l = 0; l < backdrop.length; l++) {
backdrop[l].addEventListener('click', function() {
for (var j = 0; j < menu.length; j++) {
menu[j].classList.toggle('hidden');
}
});
}
}
});

View File

@@ -1,449 +0,0 @@
const ApiManager = (function() {
const state = {
isInitialized: false
};
const config = {
requestTimeout: 60000,
retryDelays: [5000, 15000, 30000],
rateLimits: {
coingecko: {
requestsPerMinute: 50,
minInterval: 1200
},
cryptocompare: {
requestsPerMinute: 30,
minInterval: 2000
}
}
};
const rateLimiter = {
lastRequestTime: {},
minRequestInterval: {
coingecko: 1200,
cryptocompare: 2000
},
requestQueue: {},
retryDelays: [5000, 15000, 30000],
canMakeRequest: function(apiName) {
const now = Date.now();
const lastRequest = this.lastRequestTime[apiName] || 0;
return (now - lastRequest) >= this.minRequestInterval[apiName];
},
updateLastRequestTime: function(apiName) {
this.lastRequestTime[apiName] = Date.now();
},
getWaitTime: function(apiName) {
const now = Date.now();
const lastRequest = this.lastRequestTime[apiName] || 0;
return Math.max(0, this.minRequestInterval[apiName] - (now - lastRequest));
},
queueRequest: async function(apiName, requestFn, retryCount = 0) {
if (!this.requestQueue[apiName]) {
this.requestQueue[apiName] = Promise.resolve();
}
try {
await this.requestQueue[apiName];
const executeRequest = async () => {
const waitTime = this.getWaitTime(apiName);
if (waitTime > 0) {
await new Promise(resolve => setTimeout(resolve, waitTime));
}
try {
this.updateLastRequestTime(apiName);
return await requestFn();
} catch (error) {
if (error.message.includes('429') && retryCount < this.retryDelays.length) {
const delay = this.retryDelays[retryCount];
console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, delay));
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
}
if ((error.message.includes('timeout') || error.name === 'NetworkError') &&
retryCount < this.retryDelays.length) {
const delay = this.retryDelays[retryCount];
console.warn(`Request failed, retrying in ${delay/1000} seconds...`, {
apiName,
retryCount,
error: error.message
});
await new Promise(resolve => setTimeout(resolve, delay));
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
}
throw error;
}
};
this.requestQueue[apiName] = executeRequest();
return await this.requestQueue[apiName];
} catch (error) {
if (error.message.includes('429') ||
error.message.includes('timeout') ||
error.name === 'NetworkError') {
const cacheKey = `coinData_${apiName}`;
try {
const cachedData = JSON.parse(localStorage.getItem(cacheKey));
if (cachedData && cachedData.value) {
return cachedData.value;
}
} catch (e) {
console.warn('Error accessing cached data:', e);
}
}
throw error;
}
}
};
const publicAPI = {
config,
rateLimiter,
initialize: function(options = {}) {
if (state.isInitialized) {
console.warn('[ApiManager] Already initialized');
return this;
}
if (options.config) {
Object.assign(config, options.config);
}
if (config.rateLimits) {
Object.keys(config.rateLimits).forEach(api => {
if (config.rateLimits[api].minInterval) {
rateLimiter.minRequestInterval[api] = config.rateLimits[api].minInterval;
}
});
}
if (config.retryDelays) {
rateLimiter.retryDelays = [...config.retryDelays];
}
if (window.CleanupManager) {
window.CleanupManager.registerResource('apiManager', this, (mgr) => mgr.dispose());
}
state.isInitialized = true;
console.log('ApiManager initialized');
return this;
},
makeRequest: async function(url, method = 'GET', headers = {}, body = null) {
try {
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
...headers
},
signal: AbortSignal.timeout(config.requestTimeout)
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Request failed for ${url}:`, error);
throw error;
}
},
makePostRequest: async function(url, headers = {}) {
return new Promise((resolve, reject) => {
fetch('/json/readurl', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
url: url,
headers: headers
})
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.Error) {
reject(new Error(data.Error));
} else {
resolve(data);
}
})
.catch(error => {
console.error(`Request failed for ${url}:`, error);
reject(error);
});
});
},
fetchCoinPrices: async function(coins, source = "coingecko.com", ttl = 300) {
if (!coins) {
throw new Error('No coins specified for price lookup');
}
let coinsParam;
if (Array.isArray(coins)) {
coinsParam = coins.filter(c => c && c.trim() !== '').join(',');
} else if (typeof coins === 'object' && coins.coins) {
coinsParam = coins.coins;
} else {
coinsParam = coins;
}
if (!coinsParam || coinsParam.trim() === '') {
throw new Error('No valid coins to fetch prices for');
}
return this.makeRequest('/json/coinprices', 'POST', {}, {
coins: coinsParam,
source: source,
ttl: ttl
});
},
fetchCoinGeckoData: async function() {
return this.rateLimiter.queueRequest('coingecko', async () => {
try {
const coins = (window.config && window.config.coins) ?
window.config.coins
.filter(coin => coin.usesCoinGecko)
.map(coin => coin.name)
.join(',') :
'bitcoin,monero,particl,bitcoincash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin';
//console.log('Fetching coin prices for:', coins);
const response = await this.fetchCoinPrices(coins);
//console.log('Full API response:', response);
if (!response || typeof response !== 'object') {
throw new Error('Invalid response type');
}
if (!response.rates || typeof response.rates !== 'object' || Object.keys(response.rates).length === 0) {
throw new Error('No valid rates found in response');
}
return response;
} catch (error) {
console.error('Error in fetchCoinGeckoData:', {
message: error.message,
stack: error.stack
});
throw error;
}
});
},
fetchVolumeData: async function() {
return this.rateLimiter.queueRequest('coingecko', async () => {
try {
let coinList = (window.config && window.config.coins) ?
window.config.coins
.filter(coin => coin.usesCoinGecko)
.map(coin => {
return window.config.getCoinBackendId ?
window.config.getCoinBackendId(coin.name) :
(typeof getCoinBackendId === 'function' ?
getCoinBackendId(coin.name) : coin.name.toLowerCase());
})
.join(',') :
'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin,wownero';
if (!coinList.includes('zcoin') && coinList.includes('firo')) {
coinList = coinList + ',zcoin';
}
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coinList}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`;
const response = await this.makePostRequest(url, {
'User-Agent': 'Mozilla/5.0',
'Accept': 'application/json'
});
if (!response || typeof response !== 'object') {
throw new Error('Invalid response from CoinGecko API');
}
const volumeData = {};
Object.entries(response).forEach(([coinId, data]) => {
if (data && data.usd_24h_vol !== undefined) {
volumeData[coinId] = {
total_volume: data.usd_24h_vol || 0,
price_change_percentage_24h: data.usd_24h_change || 0
};
}
});
const coinMappings = {
'firo': ['firo', 'zcoin'],
'zcoin': ['zcoin', 'firo'],
'bitcoin-cash': ['bitcoin-cash', 'bitcoincash', 'bch'],
'bitcoincash': ['bitcoincash', 'bitcoin-cash', 'bch'],
'particl': ['particl', 'part']
};
if (response['zcoin'] && (!volumeData['firo'] || volumeData['firo'].total_volume === 0)) {
volumeData['firo'] = {
total_volume: response['zcoin'].usd_24h_vol || 0,
price_change_percentage_24h: response['zcoin'].usd_24h_change || 0
};
}
if (response['bitcoin-cash'] && (!volumeData['bitcoincash'] || volumeData['bitcoincash'].total_volume === 0)) {
volumeData['bitcoincash'] = {
total_volume: response['bitcoin-cash'].usd_24h_vol || 0,
price_change_percentage_24h: response['bitcoin-cash'].usd_24h_change || 0
};
}
for (const [mainCoin, alternativeIds] of Object.entries(coinMappings)) {
if (!volumeData[mainCoin] || volumeData[mainCoin].total_volume === 0) {
for (const altId of alternativeIds) {
if (response[altId] && response[altId].usd_24h_vol) {
volumeData[mainCoin] = {
total_volume: response[altId].usd_24h_vol,
price_change_percentage_24h: response[altId].usd_24h_change || 0
};
break;
}
}
}
}
return volumeData;
} catch (error) {
console.error("Error fetching volume data:", error);
throw error;
}
});
},
fetchCryptoCompareData: function(coin) {
return this.rateLimiter.queueRequest('cryptocompare', async () => {
try {
const apiKey = window.config?.apiKeys?.cryptoCompare || '';
const url = `https://min-api.cryptocompare.com/data/pricemultifull?fsyms=${coin}&tsyms=USD,BTC&api_key=${apiKey}`;
const headers = {
'User-Agent': 'Mozilla/5.0',
'Accept': 'application/json'
};
return await this.makePostRequest(url, headers);
} catch (error) {
console.error(`CryptoCompare request failed for ${coin}:`, error);
throw error;
}
});
},
fetchHistoricalData: async function(coinSymbols, resolution = 'day') {
if (!Array.isArray(coinSymbols)) {
coinSymbols = [coinSymbols];
}
const results = {};
const fetchPromises = coinSymbols.map(async coin => {
if (coin === 'WOW') {
return this.rateLimiter.queueRequest('coingecko', async () => {
const url = `https://api.coingecko.com/api/v3/coins/wownero/market_chart?vs_currency=usd&days=1`;
try {
const response = await this.makePostRequest(url);
if (response && response.prices) {
results[coin] = response.prices;
}
} catch (error) {
console.error(`Error fetching CoinGecko data for WOW:`, error);
throw error;
}
});
} else {
return this.rateLimiter.queueRequest('cryptocompare', async () => {
try {
const apiKey = window.config?.apiKeys?.cryptoCompare || '';
let url;
if (resolution === 'day') {
url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coin}&tsym=USD&limit=24&api_key=${apiKey}`;
} else if (resolution === 'year') {
url = `https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coin}&tsym=USD&limit=365&api_key=${apiKey}`;
} else {
url = `https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coin}&tsym=USD&limit=180&api_key=${apiKey}`;
}
const response = await this.makePostRequest(url);
if (response.Response === "Error") {
console.error(`API Error for ${coin}:`, response.Message);
throw new Error(response.Message);
} else if (response.Data && response.Data.Data) {
results[coin] = response.Data;
}
} catch (error) {
console.error(`Error fetching CryptoCompare data for ${coin}:`, error);
throw error;
}
});
}
});
await Promise.all(fetchPromises);
return results;
},
dispose: function() {
rateLimiter.requestQueue = {};
rateLimiter.lastRequestTime = {};
state.isInitialized = false;
console.log('ApiManager disposed');
}
};
return publicAPI;
})();
function getCoinBackendId(coinName) {
const nameMap = {
'bitcoin-cash': 'bitcoincash',
'bitcoin cash': 'bitcoincash',
'firo': 'zcoin',
'zcoin': 'zcoin',
'bitcoincash': 'bitcoin-cash'
};
return nameMap[coinName.toLowerCase()] || coinName.toLowerCase();
}
window.Api = ApiManager;
window.ApiManager = ApiManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.apiManagerInitialized) {
ApiManager.initialize();
window.apiManagerInitialized = true;
}
});
//console.log('ApiManager initialized with methods:', Object.keys(ApiManager));
console.log('ApiManager initialized');

View File

@@ -1,536 +0,0 @@
const CacheManager = (function() {
const defaults = window.config?.cacheConfig?.storage || {
maxSizeBytes: 10 * 1024 * 1024,
maxItems: 200,
defaultTTL: 5 * 60 * 1000
};
const PRICES_CACHE_KEY = 'crypto_prices_unified';
const CACHE_KEY_PATTERNS = [
'coinData_',
'chartData_',
'historical_',
'rates_',
'prices_',
'offers_',
'fallback_',
'volumeData'
];
const isCacheKey = (key) => {
return CACHE_KEY_PATTERNS.some(pattern => key.startsWith(pattern)) ||
key === 'coinGeckoOneLiner' ||
key === PRICES_CACHE_KEY;
};
const isLocalStorageAvailable = () => {
try {
const testKey = '__storage_test__';
localStorage.setItem(testKey, testKey);
localStorage.removeItem(testKey);
return true;
} catch (e) {
return false;
}
};
let storageAvailable = isLocalStorageAvailable();
const memoryCache = new Map();
if (!storageAvailable) {
console.warn('localStorage is not available. Using in-memory cache instead.');
}
const cacheAPI = {
getTTL: function(resourceType) {
const ttlConfig = window.config?.cacheConfig?.ttlSettings || {};
return ttlConfig[resourceType] || window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL;
},
set: function(key, value, resourceTypeOrCustomTtl = null) {
try {
this.cleanup();
if (!value) {
console.warn('Attempted to cache null/undefined value for key:', key);
return false;
}
let ttl;
if (typeof resourceTypeOrCustomTtl === 'string') {
ttl = this.getTTL(resourceTypeOrCustomTtl);
} else if (typeof resourceTypeOrCustomTtl === 'number') {
ttl = resourceTypeOrCustomTtl;
} else {
ttl = window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL;
}
const item = {
value: value,
timestamp: Date.now(),
expiresAt: Date.now() + ttl
};
let serializedItem;
try {
serializedItem = JSON.stringify(item);
} catch (e) {
console.error('Failed to serialize cache item:', e);
return false;
}
const itemSize = new Blob([serializedItem]).size;
if (itemSize > defaults.maxSizeBytes) {
console.warn(`Cache item exceeds maximum size (${(itemSize/1024/1024).toFixed(2)}MB)`);
return false;
}
if (storageAvailable) {
try {
localStorage.setItem(key, serializedItem);
return true;
} catch (storageError) {
if (storageError.name === 'QuotaExceededError') {
this.cleanup(true);
try {
localStorage.setItem(key, serializedItem);
return true;
} catch (retryError) {
console.error('Storage quota exceeded even after cleanup:', retryError);
storageAvailable = false;
console.warn('Switching to in-memory cache due to quota issues');
memoryCache.set(key, item);
return true;
}
} else {
console.error('localStorage error:', storageError);
storageAvailable = false;
console.warn('Switching to in-memory cache due to localStorage error');
memoryCache.set(key, item);
return true;
}
}
} else {
memoryCache.set(key, item);
if (memoryCache.size > defaults.maxItems) {
const keysToDelete = Array.from(memoryCache.keys())
.filter(k => isCacheKey(k))
.sort((a, b) => memoryCache.get(a).timestamp - memoryCache.get(b).timestamp)
.slice(0, Math.floor(memoryCache.size * 0.2)); // Remove oldest 20%
keysToDelete.forEach(k => memoryCache.delete(k));
}
return true;
}
} catch (error) {
console.error('Cache set error:', error);
try {
memoryCache.set(key, {
value: value,
timestamp: Date.now(),
expiresAt: Date.now() + (window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL)
});
return true;
} catch (e) {
console.error('Memory cache set error:', e);
return false;
}
}
},
get: function(key) {
try {
if (storageAvailable) {
try {
const itemStr = localStorage.getItem(key);
if (itemStr) {
let item;
try {
item = JSON.parse(itemStr);
} catch (parseError) {
console.error('Failed to parse cached item:', parseError);
localStorage.removeItem(key);
return null;
}
if (!item || typeof item.expiresAt !== 'number' || !Object.prototype.hasOwnProperty.call(item, 'value')) {
console.warn('Invalid cache item structure for key:', key);
localStorage.removeItem(key);
return null;
}
const now = Date.now();
if (now < item.expiresAt) {
return {
value: item.value,
remainingTime: item.expiresAt - now
};
}
localStorage.removeItem(key);
return null;
}
} catch (error) {
console.error("localStorage access error:", error);
storageAvailable = false;
console.warn('Switching to in-memory cache due to localStorage error');
}
}
if (memoryCache.has(key)) {
const item = memoryCache.get(key);
const now = Date.now();
if (now < item.expiresAt) {
return {
value: item.value,
remainingTime: item.expiresAt - now
};
} else {
memoryCache.delete(key);
}
}
return null;
} catch (error) {
console.error("Cache retrieval error:", error);
try {
if (storageAvailable) {
localStorage.removeItem(key);
}
memoryCache.delete(key);
} catch (removeError) {
console.error("Failed to remove invalid cache entry:", removeError);
}
return null;
}
},
isValid: function(key) {
return this.get(key) !== null;
},
cleanup: function(aggressive = false) {
const now = Date.now();
let totalSize = 0;
let itemCount = 0;
const items = [];
if (storageAvailable) {
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!isCacheKey(key)) continue;
try {
const itemStr = localStorage.getItem(key);
const size = new Blob([itemStr]).size;
const item = JSON.parse(itemStr);
if (now >= item.expiresAt) {
localStorage.removeItem(key);
continue;
}
items.push({
key,
size,
expiresAt: item.expiresAt,
timestamp: item.timestamp
});
totalSize += size;
itemCount++;
} catch (error) {
console.error("Error processing cache item:", error);
localStorage.removeItem(key);
}
}
if (aggressive || totalSize > defaults.maxSizeBytes || itemCount > defaults.maxItems) {
items.sort((a, b) => b.timestamp - a.timestamp);
while ((totalSize > defaults.maxSizeBytes || itemCount > defaults.maxItems) && items.length > 0) {
const item = items.pop();
try {
localStorage.removeItem(item.key);
totalSize -= item.size;
itemCount--;
} catch (error) {
console.error("Error removing cache item:", error);
}
}
}
} catch (error) {
console.error("Error during localStorage cleanup:", error);
storageAvailable = false;
console.warn('Switching to in-memory cache due to localStorage error');
}
}
const expiredKeys = [];
memoryCache.forEach((item, key) => {
if (now >= item.expiresAt) {
expiredKeys.push(key);
}
});
expiredKeys.forEach(key => memoryCache.delete(key));
if (aggressive && memoryCache.size > defaults.maxItems / 2) {
const keysToDelete = Array.from(memoryCache.keys())
.filter(key => isCacheKey(key))
.sort((a, b) => memoryCache.get(a).timestamp - memoryCache.get(b).timestamp)
.slice(0, Math.floor(memoryCache.size * 0.3)); // Remove oldest 30% during aggressive cleanup
keysToDelete.forEach(key => memoryCache.delete(key));
}
return {
totalSize,
itemCount,
memoryCacheSize: memoryCache.size,
cleaned: items.length,
storageAvailable
};
},
clear: function() {
if (storageAvailable) {
try {
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (isCacheKey(key)) {
keys.push(key);
}
}
keys.forEach(key => {
try {
localStorage.removeItem(key);
} catch (error) {
console.error("Error clearing cache item:", error);
}
});
} catch (error) {
console.error("Error clearing localStorage cache:", error);
storageAvailable = false;
}
}
Array.from(memoryCache.keys())
.filter(key => isCacheKey(key))
.forEach(key => memoryCache.delete(key));
//console.log("Cache cleared successfully");
return true;
},
getStats: function() {
let totalSize = 0;
let itemCount = 0;
let expiredCount = 0;
const now = Date.now();
if (storageAvailable) {
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!isCacheKey(key)) continue;
try {
const itemStr = localStorage.getItem(key);
const size = new Blob([itemStr]).size;
const item = JSON.parse(itemStr);
totalSize += size;
itemCount++;
if (now >= item.expiresAt) {
expiredCount++;
}
} catch (error) {
console.error("Error getting cache stats:", error);
}
}
} catch (error) {
console.error("Error getting localStorage stats:", error);
storageAvailable = false;
}
}
let memoryCacheSize = 0;
let memoryCacheItems = 0;
let memoryCacheExpired = 0;
memoryCache.forEach((item, key) => {
if (isCacheKey(key)) {
memoryCacheItems++;
if (now >= item.expiresAt) {
memoryCacheExpired++;
}
try {
memoryCacheSize += new Blob([JSON.stringify(item)]).size;
} catch (e) {
}
}
});
return {
totalSizeMB: (totalSize / 1024 / 1024).toFixed(2),
itemCount,
expiredCount,
utilization: ((totalSize / defaults.maxSizeBytes) * 100).toFixed(1) + '%',
memoryCacheItems,
memoryCacheExpired,
memoryCacheSizeKB: (memoryCacheSize / 1024).toFixed(2),
storageType: storageAvailable ? 'localStorage' : 'memory'
};
},
checkStorage: function() {
const wasAvailable = storageAvailable;
storageAvailable = isLocalStorageAvailable();
if (storageAvailable && !wasAvailable && memoryCache.size > 0) {
console.log('localStorage is now available. Migrating memory cache...');
let migratedCount = 0;
memoryCache.forEach((item, key) => {
if (isCacheKey(key)) {
try {
localStorage.setItem(key, JSON.stringify(item));
memoryCache.delete(key);
migratedCount++;
} catch (e) {
if (e.name === 'QuotaExceededError') {
console.warn('Storage quota exceeded during migration. Keeping items in memory cache.');
return false;
}
}
}
});
console.log(`Migrated ${migratedCount} items from memory cache to localStorage.`);
}
return {
available: storageAvailable,
type: storageAvailable ? 'localStorage' : 'memory'
};
}
};
const publicAPI = {
...cacheAPI,
setPrices: function(priceData, customTtl = null) {
return this.set(PRICES_CACHE_KEY, priceData,
customTtl || (typeof customTtl === 'undefined' ? 'prices' : null));
},
getPrices: function() {
return this.get(PRICES_CACHE_KEY);
},
getCoinPrice: function(symbol) {
const prices = this.getPrices();
if (!prices || !prices.value) {
return null;
}
const normalizedSymbol = symbol.toLowerCase();
return prices.value[normalizedSymbol] || null;
},
getCompatiblePrices: function(format) {
const prices = this.getPrices();
if (!prices || !prices.value) {
return null;
}
switch(format) {
case 'rates':
const ratesFormat = {};
Object.entries(prices.value).forEach(([coin, data]) => {
const coinKey = coin.replace(/-/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
.toLowerCase()
.replace(' ', '-');
ratesFormat[coinKey] = {
usd: data.price || data.usd,
btc: data.price_btc || data.btc
};
});
return {
value: ratesFormat,
remainingTime: prices.remainingTime
};
case 'coinGecko':
const geckoFormat = {};
Object.entries(prices.value).forEach(([coin, data]) => {
const symbol = this.getSymbolFromCoinId(coin);
if (symbol) {
geckoFormat[symbol.toLowerCase()] = {
current_price: data.price || data.usd,
price_btc: data.price_btc || data.btc,
total_volume: data.total_volume,
price_change_percentage_24h: data.price_change_percentage_24h,
displayName: symbol
};
}
});
return {
value: geckoFormat,
remainingTime: prices.remainingTime
};
default:
return prices;
}
},
getSymbolFromCoinId: function(coinId) {
const symbolMap = {
'bitcoin': 'BTC',
'litecoin': 'LTC',
'monero': 'XMR',
'wownero': 'WOW',
'particl': 'PART',
'pivx': 'PIVX',
'firo': 'FIRO',
'zcoin': 'FIRO',
'dash': 'DASH',
'decred': 'DCR',
'namecoin': 'NMR',
'bitcoin-cash': 'BCH',
'dogecoin': 'DOGE'
};
return symbolMap[coinId] || null;
}
};
if (window.CleanupManager) {
window.CleanupManager.registerResource('cacheManager', publicAPI, (cm) => {
cm.clear();
});
}
return publicAPI;
})();
window.CacheManager = CacheManager;
//console.log('CacheManager initialized with methods:', Object.keys(CacheManager));
console.log('CacheManager initialized');

View File

@@ -1,508 +0,0 @@
const CleanupManager = (function() {
const state = {
eventListeners: [],
timeouts: [],
intervals: [],
animationFrames: [],
resources: new Map(),
debug: false,
memoryOptimizationInterval: null
};
function log(message, ...args) {
if (state.debug) {
console.log(`[CleanupManager] ${message}`, ...args);
}
}
const publicAPI = {
addListener: function(element, type, handler, options = false) {
if (!element) {
log('Warning: Attempted to add listener to null/undefined element');
return handler;
}
element.addEventListener(type, handler, options);
state.eventListeners.push({ element, type, handler, options });
log(`Added ${type} listener to`, element);
return handler;
},
setTimeout: function(callback, delay) {
const id = window.setTimeout(callback, delay);
state.timeouts.push(id);
log(`Created timeout ${id} with ${delay}ms delay`);
return id;
},
setInterval: function(callback, delay) {
const id = window.setInterval(callback, delay);
state.intervals.push(id);
log(`Created interval ${id} with ${delay}ms delay`);
return id;
},
requestAnimationFrame: function(callback) {
const id = window.requestAnimationFrame(callback);
state.animationFrames.push(id);
log(`Requested animation frame ${id}`);
return id;
},
registerResource: function(type, resource, cleanupFn) {
const id = `${type}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
state.resources.set(id, { resource, cleanupFn });
log(`Registered custom resource ${id} of type ${type}`);
return id;
},
unregisterResource: function(id) {
const resourceInfo = state.resources.get(id);
if (resourceInfo) {
try {
resourceInfo.cleanupFn(resourceInfo.resource);
state.resources.delete(id);
log(`Unregistered and cleaned up resource ${id}`);
return true;
} catch (error) {
console.error(`[CleanupManager] Error cleaning up resource ${id}:`, error);
return false;
}
}
log(`Resource ${id} not found`);
return false;
},
clearTimeout: function(id) {
const index = state.timeouts.indexOf(id);
if (index !== -1) {
window.clearTimeout(id);
state.timeouts.splice(index, 1);
log(`Cleared timeout ${id}`);
}
},
clearInterval: function(id) {
const index = state.intervals.indexOf(id);
if (index !== -1) {
window.clearInterval(id);
state.intervals.splice(index, 1);
log(`Cleared interval ${id}`);
}
},
cancelAnimationFrame: function(id) {
const index = state.animationFrames.indexOf(id);
if (index !== -1) {
window.cancelAnimationFrame(id);
state.animationFrames.splice(index, 1);
log(`Cancelled animation frame ${id}`);
}
},
removeListener: function(element, type, handler, options = false) {
if (!element) return;
try {
element.removeEventListener(type, handler, options);
log(`Removed ${type} listener from`, element);
} catch (error) {
console.error(`[CleanupManager] Error removing event listener:`, error);
}
state.eventListeners = state.eventListeners.filter(
listener => !(listener.element === element &&
listener.type === type &&
listener.handler === handler)
);
},
removeListenersByElement: function(element) {
if (!element) return;
const listenersToRemove = state.eventListeners.filter(
listener => listener.element === element
);
listenersToRemove.forEach(({ element, type, handler, options }) => {
try {
element.removeEventListener(type, handler, options);
log(`Removed ${type} listener from`, element);
} catch (error) {
console.error(`[CleanupManager] Error removing event listener:`, error);
}
});
state.eventListeners = state.eventListeners.filter(
listener => listener.element !== element
);
},
clearAllTimeouts: function() {
state.timeouts.forEach(id => {
window.clearTimeout(id);
});
const count = state.timeouts.length;
state.timeouts = [];
log(`Cleared all timeouts (${count})`);
},
clearAllIntervals: function() {
state.intervals.forEach(id => {
window.clearInterval(id);
});
const count = state.intervals.length;
state.intervals = [];
log(`Cleared all intervals (${count})`);
},
clearAllAnimationFrames: function() {
state.animationFrames.forEach(id => {
window.cancelAnimationFrame(id);
});
const count = state.animationFrames.length;
state.animationFrames = [];
log(`Cancelled all animation frames (${count})`);
},
clearAllResources: function() {
let successCount = 0;
let errorCount = 0;
state.resources.forEach((resourceInfo, id) => {
try {
resourceInfo.cleanupFn(resourceInfo.resource);
successCount++;
} catch (error) {
console.error(`[CleanupManager] Error cleaning up resource ${id}:`, error);
errorCount++;
}
});
state.resources.clear();
log(`Cleared all custom resources (${successCount} success, ${errorCount} errors)`);
},
clearAllListeners: function() {
state.eventListeners.forEach(({ element, type, handler, options }) => {
if (element) {
try {
element.removeEventListener(type, handler, options);
} catch (error) {
console.error(`[CleanupManager] Error removing event listener:`, error);
}
}
});
const count = state.eventListeners.length;
state.eventListeners = [];
log(`Removed all event listeners (${count})`);
},
clearAll: function() {
const counts = {
listeners: state.eventListeners.length,
timeouts: state.timeouts.length,
intervals: state.intervals.length,
animationFrames: state.animationFrames.length,
resources: state.resources.size
};
this.clearAllListeners();
this.clearAllTimeouts();
this.clearAllIntervals();
this.clearAllAnimationFrames();
this.clearAllResources();
log(`All resources cleaned up:`, counts);
return counts;
},
getResourceCounts: function() {
return {
listeners: state.eventListeners.length,
timeouts: state.timeouts.length,
intervals: state.intervals.length,
animationFrames: state.animationFrames.length,
resources: state.resources.size,
total: state.eventListeners.length +
state.timeouts.length +
state.intervals.length +
state.animationFrames.length +
state.resources.size
};
},
setupMemoryOptimization: function(options = {}) {
const memoryCheckInterval = options.interval || 2 * 60 * 1000; // Default: 2 minutes
const maxCacheSize = options.maxCacheSize || 100;
const maxDataSize = options.maxDataSize || 1000;
if (state.memoryOptimizationInterval) {
this.clearInterval(state.memoryOptimizationInterval);
}
this.addListener(document, 'visibilitychange', () => {
if (document.hidden) {
log('Tab hidden - running memory optimization');
this.optimizeMemory({
maxCacheSize: maxCacheSize,
maxDataSize: maxDataSize
});
} else if (window.TooltipManager) {
window.TooltipManager.cleanup();
}
});
state.memoryOptimizationInterval = this.setInterval(() => {
if (document.hidden) {
log('Periodic memory optimization');
this.optimizeMemory({
maxCacheSize: maxCacheSize,
maxDataSize: maxDataSize
});
}
}, memoryCheckInterval);
log('Memory optimization setup complete');
return state.memoryOptimizationInterval;
},
optimizeMemory: function(options = {}) {
log('Running memory optimization');
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
window.TooltipManager.cleanup();
}
if (window.IdentityManager && typeof window.IdentityManager.limitCacheSize === 'function') {
window.IdentityManager.limitCacheSize(options.maxCacheSize || 100);
}
this.cleanupOrphanedResources();
if (window.gc) {
try {
window.gc();
log('Forced garbage collection');
} catch (e) {
}
}
document.dispatchEvent(new CustomEvent('memoryOptimized', {
detail: {
timestamp: Date.now(),
maxDataSize: options.maxDataSize || 1000
}
}));
log('Memory optimization complete');
},
cleanupOrphanedResources: function() {
let removedListeners = 0;
const validListeners = [];
for (let i = 0; i < state.eventListeners.length; i++) {
const listener = state.eventListeners[i];
if (!listener.element) {
removedListeners++;
continue;
}
try {
const isDetached = !(listener.element instanceof Node) ||
!document.body.contains(listener.element) ||
(listener.element.classList && listener.element.classList.contains('hidden')) ||
(listener.element.style && listener.element.style.display === 'none');
if (isDetached) {
try {
if (listener.element instanceof Node) {
listener.element.removeEventListener(listener.type, listener.handler, listener.options);
}
removedListeners++;
} catch (e) {
}
} else {
validListeners.push(listener);
}
} catch (e) {
log(`Error checking listener (removing): ${e.message}`);
removedListeners++;
}
}
if (removedListeners > 0) {
state.eventListeners = validListeners;
log(`Removed ${removedListeners} event listeners for detached/hidden elements`);
}
let removedResources = 0;
const resourcesForRemoval = [];
state.resources.forEach((info, id) => {
const resource = info.resource;
try {
if (resource instanceof Element && !document.body.contains(resource)) {
resourcesForRemoval.push(id);
}
if (resource && resource.element) {
if (resource.element instanceof Node && !document.body.contains(resource.element)) {
resourcesForRemoval.push(id);
}
}
} catch (e) {
log(`Error checking resource ${id}: ${e.message}`);
}
});
resourcesForRemoval.forEach(id => {
this.unregisterResource(id);
removedResources++;
});
if (removedResources > 0) {
log(`Removed ${removedResources} orphaned resources`);
}
if (window.TooltipManager) {
if (typeof window.TooltipManager.cleanupOrphanedTooltips === 'function') {
try {
window.TooltipManager.cleanupOrphanedTooltips();
} catch (e) {
if (typeof window.TooltipManager.cleanup === 'function') {
try {
window.TooltipManager.cleanup();
} catch (err) {
log(`Error cleaning up tooltips: ${err.message}`);
}
}
}
} else if (typeof window.TooltipManager.cleanup === 'function') {
try {
window.TooltipManager.cleanup();
} catch (e) {
log(`Error cleaning up tooltips: ${e.message}`);
}
}
}
try {
this.cleanupTooltipDOM();
} catch (e) {
log(`Error in cleanupTooltipDOM: ${e.message}`);
}
},
cleanupTooltipDOM: function() {
let removedElements = 0;
try {
const tooltipSelectors = [
'[role="tooltip"]',
'[id^="tooltip-"]',
'.tippy-box',
'[data-tippy-root]'
];
tooltipSelectors.forEach(selector => {
try {
const elements = document.querySelectorAll(selector);
elements.forEach(element => {
try {
if (!(element instanceof Element)) return;
const isDetached = !element.parentElement ||
!document.body.contains(element.parentElement) ||
element.classList.contains('hidden') ||
element.style.display === 'none' ||
element.style.visibility === 'hidden';
if (isDetached) {
try {
element.remove();
removedElements++;
} catch (e) {
}
}
} catch (err) {
}
});
} catch (err) {
log(`Error querying for ${selector}: ${err.message}`);
}
});
} catch (e) {
log(`Error in tooltip DOM cleanup: ${e.message}`);
}
if (removedElements > 0) {
log(`Removed ${removedElements} detached tooltip elements`);
}
},
setDebugMode: function(enabled) {
state.debug = Boolean(enabled);
log(`Debug mode ${state.debug ? 'enabled' : 'disabled'}`);
return state.debug;
},
dispose: function() {
this.clearAll();
log('CleanupManager disposed');
},
initialize: function(options = {}) {
if (options.debug !== undefined) {
this.setDebugMode(options.debug);
}
if (typeof window !== 'undefined' && !options.noAutoCleanup) {
this.addListener(window, 'beforeunload', () => {
this.clearAll();
});
}
if (typeof window !== 'undefined' && !options.noMemoryOptimization) {
this.setupMemoryOptimization(options.memoryOptions || {});
}
log('CleanupManager initialized');
return this;
}
};
return publicAPI;
})();
if (typeof module !== 'undefined' && module.exports) {
module.exports = CleanupManager;
}
if (typeof window !== 'undefined') {
window.CleanupManager = CleanupManager;
}
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
if (document.readyState === 'complete' || document.readyState === 'interactive') {
CleanupManager.initialize({ debug: false });
} else {
document.addEventListener('DOMContentLoaded', () => {
CleanupManager.initialize({ debug: false });
}, { once: true });
}
}

View File

@@ -1,268 +0,0 @@
const CoinManager = (function() {
const coinRegistry = [
{
symbol: 'BTC',
name: 'bitcoin',
displayName: 'Bitcoin',
aliases: ['btc', 'bitcoin'],
coingeckoId: 'bitcoin',
cryptocompareId: 'BTC',
usesCryptoCompare: false,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Bitcoin.png'
},
{
symbol: 'XMR',
name: 'monero',
displayName: 'Monero',
aliases: ['xmr', 'monero'],
coingeckoId: 'monero',
cryptocompareId: 'XMR',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Monero.png'
},
{
symbol: 'PART',
name: 'particl',
displayName: 'Particl',
aliases: ['part', 'particl', 'particl anon', 'particl blind'],
variants: ['Particl', 'Particl Blind', 'Particl Anon'],
coingeckoId: 'particl',
cryptocompareId: 'PART',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Particl.png'
},
{
symbol: 'BCH',
name: 'bitcoin-cash',
displayName: 'Bitcoin Cash',
aliases: ['bch', 'bitcoincash', 'bitcoin cash'],
coingeckoId: 'bitcoin-cash',
cryptocompareId: 'BCH',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Bitcoin%20Cash.png'
},
{
symbol: 'PIVX',
name: 'pivx',
displayName: 'PIVX',
aliases: ['pivx'],
coingeckoId: 'pivx',
cryptocompareId: 'PIVX',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'PIVX.png'
},
{
symbol: 'FIRO',
name: 'firo',
displayName: 'Firo',
aliases: ['firo', 'zcoin'],
coingeckoId: 'firo',
cryptocompareId: 'FIRO',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Firo.png'
},
{
symbol: 'DASH',
name: 'dash',
displayName: 'Dash',
aliases: ['dash'],
coingeckoId: 'dash',
cryptocompareId: 'DASH',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Dash.png'
},
{
symbol: 'LTC',
name: 'litecoin',
displayName: 'Litecoin',
aliases: ['ltc', 'litecoin'],
variants: ['Litecoin', 'Litecoin MWEB'],
coingeckoId: 'litecoin',
cryptocompareId: 'LTC',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Litecoin.png'
},
{
symbol: 'DOGE',
name: 'dogecoin',
displayName: 'Dogecoin',
aliases: ['doge', 'dogecoin'],
coingeckoId: 'dogecoin',
cryptocompareId: 'DOGE',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Dogecoin.png'
},
{
symbol: 'DCR',
name: 'decred',
displayName: 'Decred',
aliases: ['dcr', 'decred'],
coingeckoId: 'decred',
cryptocompareId: 'DCR',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Decred.png'
},
{
symbol: 'NMC',
name: 'namecoin',
displayName: 'Namecoin',
aliases: ['nmc', 'namecoin'],
coingeckoId: 'namecoin',
cryptocompareId: 'NMC',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Namecoin.png'
},
{
symbol: 'WOW',
name: 'wownero',
displayName: 'Wownero',
aliases: ['wow', 'wownero'],
coingeckoId: 'wownero',
cryptocompareId: 'WOW',
usesCryptoCompare: false,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Wownero.png'
}
];
const symbolToInfo = {};
const nameToInfo = {};
const displayNameToInfo = {};
const coinAliasesMap = {};
function buildLookupMaps() {
coinRegistry.forEach(coin => {
symbolToInfo[coin.symbol.toLowerCase()] = coin;
nameToInfo[coin.name.toLowerCase()] = coin;
displayNameToInfo[coin.displayName.toLowerCase()] = coin;
if (coin.aliases && Array.isArray(coin.aliases)) {
coin.aliases.forEach(alias => {
coinAliasesMap[alias.toLowerCase()] = coin;
});
}
coinAliasesMap[coin.symbol.toLowerCase()] = coin;
coinAliasesMap[coin.name.toLowerCase()] = coin;
coinAliasesMap[coin.displayName.toLowerCase()] = coin;
if (coin.variants && Array.isArray(coin.variants)) {
coin.variants.forEach(variant => {
coinAliasesMap[variant.toLowerCase()] = coin;
});
}
});
}
buildLookupMaps();
function getCoinByAnyIdentifier(identifier) {
if (!identifier) return null;
const normalizedId = identifier.toString().toLowerCase().trim();
const coin = coinAliasesMap[normalizedId];
if (coin) return coin;
if (normalizedId.includes('bitcoin') && normalizedId.includes('cash') ||
normalizedId === 'bch') {
return symbolToInfo['bch'];
}
if (normalizedId === 'zcoin' || normalizedId.includes('firo')) {
return symbolToInfo['firo'];
}
if (normalizedId.includes('particl')) {
return symbolToInfo['part'];
}
return null;
}
return {
getAllCoins: function() {
return [...coinRegistry];
},
getCoinByAnyIdentifier: getCoinByAnyIdentifier,
getSymbol: function(identifier) {
const coin = getCoinByAnyIdentifier(identifier);
return coin ? coin.symbol : null;
},
getDisplayName: function(identifier) {
if (!identifier) return null;
const normalizedId = identifier.toString().toLowerCase().trim();
if (normalizedId === 'particl anon' || normalizedId === 'part_anon' || normalizedId === 'particl_anon') {
return 'Particl Anon';
}
if (normalizedId === 'particl blind' || normalizedId === 'part_blind' || normalizedId === 'particl_blind') {
return 'Particl Blind';
}
if (normalizedId === 'litecoin mweb' || normalizedId === 'ltc_mweb' || normalizedId === 'litecoin_mweb') {
return 'Litecoin MWEB';
}
const coin = getCoinByAnyIdentifier(identifier);
return coin ? coin.displayName : null;
},
getCoingeckoId: function(identifier) {
const coin = getCoinByAnyIdentifier(identifier);
return coin ? coin.coingeckoId : null;
},
coinMatches: function(coinId1, coinId2) {
if (!coinId1 || !coinId2) return false;
const coin1 = getCoinByAnyIdentifier(coinId1);
const coin2 = getCoinByAnyIdentifier(coinId2);
if (!coin1 || !coin2) return false;
return coin1.symbol === coin2.symbol;
},
getPriceKey: function(coinIdentifier) {
if (!coinIdentifier) return null;
const coin = getCoinByAnyIdentifier(coinIdentifier);
if (!coin) return coinIdentifier.toLowerCase();
return coin.coingeckoId;
},
getCoinIcon: function(identifier) {
if (!identifier) return null;
const normalizedId = identifier.toString().toLowerCase().trim();
if (normalizedId === 'particl anon' || normalizedId === 'part_anon' || normalizedId === 'particl_anon') {
return 'Particl.png';
}
if (normalizedId === 'particl blind' || normalizedId === 'part_blind' || normalizedId === 'particl_blind') {
return 'Particl.png';
}
if (normalizedId === 'litecoin mweb' || normalizedId === 'ltc_mweb' || normalizedId === 'litecoin_mweb') {
return 'Litecoin.png';
}
const coin = getCoinByAnyIdentifier(identifier);
if (coin && coin.icon) {
return coin.icon;
}
const capitalizedName = identifier.toString().split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('%20');
return `${capitalizedName}.png`;
}
};
})();
window.CoinManager = CoinManager;
console.log('CoinManager initialized');

View File

@@ -1,336 +0,0 @@
const ConfigManager = (function() {
const state = {
isInitialized: false
};
function determineWebSocketPort() {
const wsPort =
window.ws_port ||
(typeof getWebSocketConfig === 'function' ? getWebSocketConfig().port : null) ||
'11700';
return wsPort;
}
const selectedWsPort = determineWebSocketPort();
const defaultConfig = {
cacheDuration: 10 * 60 * 1000,
requestTimeout: 60000,
wsPort: selectedWsPort,
cacheConfig: {
defaultTTL: 10 * 60 * 1000,
ttlSettings: {
prices: 5 * 60 * 1000,
chart: 5 * 60 * 1000,
historical: 60 * 60 * 1000,
volume: 30 * 60 * 1000,
offers: 2 * 60 * 1000,
identity: 15 * 60 * 1000
},
storage: {
maxSizeBytes: 10 * 1024 * 1024,
maxItems: 200
},
fallbackTTL: 24 * 60 * 60 * 1000
},
itemsPerPage: 50,
apiEndpoints: {
cryptoCompare: 'https://min-api.cryptocompare.com/data/pricemultifull',
coinGecko: 'https://api.coingecko.com/api/v3',
cryptoCompareHistorical: 'https://min-api.cryptocompare.com/data/v2/histoday',
cryptoCompareHourly: 'https://min-api.cryptocompare.com/data/v2/histohour',
volumeEndpoint: 'https://api.coingecko.com/api/v3/simple/price'
},
rateLimits: {
coingecko: {
requestsPerMinute: 50,
minInterval: 1200
},
cryptocompare: {
requestsPerMinute: 30,
minInterval: 2000
}
},
retryDelays: [5000, 15000, 30000],
get coins() {
return window.CoinManager ? window.CoinManager.getAllCoins() : [
{ symbol: 'BTC', name: 'bitcoin', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'XMR', name: 'monero', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'PART', name: 'particl', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'BCH', name: 'bitcoincash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'PIVX', name: 'pivx', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'FIRO', name: 'firo', displayName: 'Firo', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'DASH', name: 'dash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'LTC', name: 'litecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'DOGE', name: 'dogecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'DCR', name: 'decred', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'NMC', name: 'namecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'WOW', name: 'wownero', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 }
];
},
chartConfig: {
colors: {
default: {
lineColor: 'rgba(77, 132, 240, 1)',
backgroundColor: 'rgba(77, 132, 240, 0.1)'
}
},
showVolume: false,
specialCoins: [''],
resolutions: {
year: { days: 365, interval: 'month' },
sixMonths: { days: 180, interval: 'daily' },
day: { days: 1, interval: 'hourly' }
},
currentResolution: 'year'
}
};
const publicAPI = {
...defaultConfig,
initialize: function(options = {}) {
if (state.isInitialized) {
console.warn('[ConfigManager] Already initialized');
return this;
}
if (options) {
Object.assign(this, options);
}
if (window.CleanupManager) {
window.CleanupManager.registerResource('configManager', this, (mgr) => mgr.dispose());
}
this.utils = utils;
state.isInitialized = true;
console.log('ConfigManager initialized');
return this;
},
getAPIKeys: function() {
if (typeof window.getAPIKeys === 'function') {
const apiKeys = window.getAPIKeys();
return {
cryptoCompare: apiKeys.cryptoCompare || '',
coinGecko: apiKeys.coinGecko || ''
};
}
return {
cryptoCompare: '',
coinGecko: ''
};
},
getCoinBackendId: function(coinName) {
if (!coinName) return null;
if (window.CoinManager) {
return window.CoinManager.getPriceKey(coinName);
}
const nameMap = {
'bitcoin-cash': 'bitcoincash',
'bitcoin cash': 'bitcoincash',
'firo': 'firo',
'zcoin': 'firo',
'bitcoincash': 'bitcoin-cash'
};
const lowerCoinName = typeof coinName === 'string' ? coinName.toLowerCase() : '';
return nameMap[lowerCoinName] || lowerCoinName;
},
coinMatches: function(offerCoin, filterCoin) {
if (!offerCoin || !filterCoin) return false;
if (window.CoinManager) {
return window.CoinManager.coinMatches(offerCoin, filterCoin);
}
offerCoin = offerCoin.toLowerCase();
filterCoin = filterCoin.toLowerCase();
if (offerCoin === filterCoin) return true;
if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
(filterCoin === 'firo' || filterCoin === 'zcoin')) {
return true;
}
if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
(offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
return true;
}
const particlVariants = ['particl', 'particl anon', 'particl blind'];
if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) {
return true;
}
if (filterCoin.includes(' ') || offerCoin.includes(' ')) {
const filterFirstWord = filterCoin.split(' ')[0];
const offerFirstWord = offerCoin.split(' ')[0];
if (filterFirstWord === 'bitcoin' && offerFirstWord === 'bitcoin') {
const filterHasCash = filterCoin.includes('cash');
const offerHasCash = offerCoin.includes('cash');
return filterHasCash === offerHasCash;
}
if (filterFirstWord === offerFirstWord && filterFirstWord.length > 4) {
return true;
}
}
if (particlVariants.includes(filterCoin)) {
return offerCoin === filterCoin;
}
return false;
},
update: function(path, value) {
const parts = path.split('.');
let current = this;
for (let i = 0; i < parts.length - 1; i++) {
if (!current[parts[i]]) {
current[parts[i]] = {};
}
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
return this;
},
get: function(path, defaultValue = null) {
const parts = path.split('.');
let current = this;
for (let i = 0; i < parts.length; i++) {
if (current === undefined || current === null) {
return defaultValue;
}
current = current[parts[i]];
}
return current !== undefined ? current : defaultValue;
},
dispose: function() {
state.isInitialized = false;
console.log('ConfigManager disposed');
}
};
const utils = {
formatNumber: function(number, decimals = 2) {
if (typeof number !== 'number' || isNaN(number)) {
console.warn('formatNumber received a non-number value:', number);
return '0';
}
try {
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
}).format(number);
} catch (e) {
return '0';
}
},
formatDate: function(timestamp, resolution) {
const date = new Date(timestamp);
const options = {
day: { hour: '2-digit', minute: '2-digit', hour12: true },
week: { month: 'short', day: 'numeric' },
month: { year: 'numeric', month: 'short', day: 'numeric' }
};
return date.toLocaleString('en-US', { ...options[resolution], timeZone: 'UTC' });
},
debounce: function(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
},
formatTimeLeft: function(timestamp) {
const now = Math.floor(Date.now() / 1000);
if (timestamp <= now) return "Expired";
return this.formatTime(timestamp);
},
formatTime: function(timestamp, addAgoSuffix = false) {
const now = Math.floor(Date.now() / 1000);
const diff = Math.abs(now - timestamp);
let timeString;
if (diff < 60) {
timeString = `${diff} seconds`;
} else if (diff < 3600) {
timeString = `${Math.floor(diff / 60)} minutes`;
} else if (diff < 86400) {
timeString = `${Math.floor(diff / 3600)} hours`;
} else if (diff < 2592000) {
timeString = `${Math.floor(diff / 86400)} days`;
} else if (diff < 31536000) {
timeString = `${Math.floor(diff / 2592000)} months`;
} else {
timeString = `${Math.floor(diff / 31536000)} years`;
}
return addAgoSuffix ? `${timeString} ago` : timeString;
},
escapeHtml: function(unsafe) {
if (typeof unsafe !== 'string') {
console.warn('escapeHtml received a non-string value:', unsafe);
return '';
}
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
},
formatPrice: function(coin, price) {
if (typeof price !== 'number' || isNaN(price)) {
console.warn(`Invalid price for ${coin}:`, price);
return 'N/A';
}
if (price < 0.000001) return price.toExponential(2);
if (price < 0.001) return price.toFixed(8);
if (price < 1) return price.toFixed(4);
if (price < 10) return price.toFixed(3);
if (price < 1000) return price.toFixed(2);
if (price < 100000) return price.toFixed(1);
return price.toFixed(0);
},
getEmptyPriceData: function() {
return {
'bitcoin': { usd: null, btc: null },
'bitcoin-cash': { usd: null, btc: null },
'dash': { usd: null, btc: null },
'dogecoin': { usd: null, btc: null },
'decred': { usd: null, btc: null },
'namecoin': { usd: null, btc: null },
'litecoin': { usd: null, btc: null },
'particl': { usd: null, btc: null },
'pivx': { usd: null, btc: null },
'monero': { usd: null, btc: null },
'zano': { usd: null, btc: null },
'wownero': { usd: null, btc: null },
'firo': { usd: null, btc: null }
};
},
getCoinSymbol: function(fullName) {
if (window.CoinManager) {
return window.CoinManager.getSymbol(fullName) || fullName;
}
return fullName;
}
};
return publicAPI;
})();
window.logger = {
log: function(message) {
console.log(`[AppLog] ${new Date().toISOString()}: ${message}`);
},
warn: function(message) {
console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`);
},
error: function(message) {
console.error(`[AppError] ${new Date().toISOString()}: ${message}`);
}
};
window.config = ConfigManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.configManagerInitialized) {
ConfigManager.initialize();
window.configManagerInitialized = true;
}
});
if (typeof module !== 'undefined') {
module.exports = ConfigManager;
}
console.log('ConfigManager initialized');

View File

@@ -1,192 +0,0 @@
const IdentityManager = (function() {
const state = {
cache: new Map(),
pendingRequests: new Map(),
config: {
retryDelay: 2000,
maxRetries: 3,
maxCacheSize: 100,
cacheTimeout: window.config?.cacheConfig?.ttlSettings?.identity || 15 * 60 * 1000,
debug: false
}
};
function log(message, ...args) {
if (state.config.debug) {
console.log(`[IdentityManager] ${message}`, ...args);
}
}
const publicAPI = {
getIdentityData: async function(address) {
if (!address) {
return null;
}
const cachedData = this.getCachedIdentity(address);
if (cachedData) {
log(`Cache hit for ${address}`);
return cachedData;
}
if (state.pendingRequests.has(address)) {
log(`Using pending request for ${address}`);
return state.pendingRequests.get(address);
}
log(`Fetching identity for ${address}`);
const request = fetchWithRetry(address);
state.pendingRequests.set(address, request);
try {
const data = await request;
this.setCachedIdentity(address, data);
return data;
} finally {
state.pendingRequests.delete(address);
}
},
getCachedIdentity: function(address) {
const cached = state.cache.get(address);
if (cached && (Date.now() - cached.timestamp) < state.config.cacheTimeout) {
return cached.data;
}
return null;
},
setCachedIdentity: function(address, data) {
if (state.cache.size >= state.config.maxCacheSize) {
const oldestEntries = [...state.cache.entries()]
.sort((a, b) => a[1].timestamp - b[1].timestamp)
.slice(0, Math.floor(state.config.maxCacheSize * 0.2));
oldestEntries.forEach(([key]) => {
state.cache.delete(key);
log(`Pruned cache entry for ${key}`);
});
}
state.cache.set(address, {
data,
timestamp: Date.now()
});
log(`Cached identity for ${address}`);
},
clearCache: function() {
log(`Clearing identity cache (${state.cache.size} entries)`);
state.cache.clear();
state.pendingRequests.clear();
},
limitCacheSize: function(maxSize = state.config.maxCacheSize) {
if (state.cache.size <= maxSize) {
return 0;
}
const entriesToRemove = [...state.cache.entries()]
.sort((a, b) => a[1].timestamp - b[1].timestamp)
.slice(0, state.cache.size - maxSize);
entriesToRemove.forEach(([key]) => state.cache.delete(key));
log(`Limited cache size, removed ${entriesToRemove.length} entries`);
return entriesToRemove.length;
},
getCacheSize: function() {
return state.cache.size;
},
configure: function(options = {}) {
Object.assign(state.config, options);
log(`Configuration updated:`, state.config);
return state.config;
},
getStats: function() {
const now = Date.now();
let expiredCount = 0;
let totalSize = 0;
state.cache.forEach((value, key) => {
if (now - value.timestamp > state.config.cacheTimeout) {
expiredCount++;
}
const keySize = key.length * 2;
const dataSize = JSON.stringify(value.data).length * 2;
totalSize += keySize + dataSize;
});
return {
cacheEntries: state.cache.size,
pendingRequests: state.pendingRequests.size,
expiredEntries: expiredCount,
estimatedSizeKB: Math.round(totalSize / 1024),
config: { ...state.config }
};
},
setDebugMode: function(enabled) {
state.config.debug = Boolean(enabled);
return `Debug mode ${state.config.debug ? 'enabled' : 'disabled'}`;
},
initialize: function(options = {}) {
if (options) {
this.configure(options);
}
if (window.CleanupManager) {
window.CleanupManager.registerResource('identityManager', this, (mgr) => mgr.dispose());
}
log('IdentityManager initialized');
return this;
},
dispose: function() {
this.clearCache();
log('IdentityManager disposed');
}
};
async function fetchWithRetry(address, attempt = 1) {
try {
const response = await fetch(`/json/identities/${address}`, {
signal: AbortSignal.timeout(5000)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (attempt >= state.config.maxRetries) {
console.error(`[IdentityManager] Error:`, error.message);
console.warn(`[IdentityManager] Failed to fetch identity for ${address} after ${attempt} attempts`);
return null;
}
await new Promise(resolve => setTimeout(resolve, state.config.retryDelay * attempt));
return fetchWithRetry(address, attempt + 1);
}
}
return publicAPI;
})();
window.IdentityManager = IdentityManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.identityManagerInitialized) {
IdentityManager.initialize();
window.identityManagerInitialized = true;
}
});
//console.log('IdentityManager initialized with methods:', Object.keys(IdentityManager));
console.log('IdentityManager initialized');

View File

@@ -1,582 +0,0 @@
const MemoryManager = (function() {
const config = {
tooltipCleanupInterval: 300000,
diagnosticsInterval: 600000,
elementVerificationInterval: 300000,
maxTooltipsThreshold: 100,
maxTooltips: 300,
cleanupThreshold: 1.5,
minTimeBetweenCleanups: 180000,
memoryGrowthThresholdMB: 100,
debug: false,
protectedWebSockets: ['wsPort', 'ws_port'],
interactiveSelectors: [
'tr:hover',
'[data-tippy-root]:hover',
'.tooltip:hover',
'[data-tooltip-trigger-id]:hover',
'[data-tooltip-target]:hover'
],
protectedContainers: [
'#sent-tbody',
'#received-tbody',
'#offers-body'
]
};
const state = {
pendingAnimationFrames: new Set(),
pendingTimeouts: new Set(),
cleanupInterval: null,
diagnosticsInterval: null,
elementVerificationInterval: null,
mutationObserver: null,
lastCleanupTime: Date.now(),
startTime: Date.now(),
isCleanupRunning: false,
metrics: {
tooltipsRemoved: 0,
cleanupRuns: 0,
lastMemoryUsage: null,
lastCleanupDetails: {},
history: []
},
originalTooltipFunctions: {}
};
function log(message, ...args) {
if (config.debug) {
console.log(`[MemoryManager] ${message}`, ...args);
}
}
function preserveTooltipFunctions() {
if (window.TooltipManager && !state.originalTooltipFunctions.destroy) {
state.originalTooltipFunctions = {
destroy: window.TooltipManager.destroy,
cleanup: window.TooltipManager.cleanup,
create: window.TooltipManager.create
};
}
}
function isInProtectedContainer(element) {
if (!element) return false;
for (const selector of config.protectedContainers) {
if (element.closest && element.closest(selector)) {
return true;
}
}
return false;
}
function shouldSkipCleanup() {
if (state.isCleanupRunning) return true;
const selector = config.interactiveSelectors.join(', ');
const hoveredElements = document.querySelectorAll(selector);
return hoveredElements.length > 0;
}
function performCleanup(force = false) {
if (shouldSkipCleanup() && !force) {
return false;
}
if (state.isCleanupRunning) {
return false;
}
const now = Date.now();
if (!force && now - state.lastCleanupTime < config.minTimeBetweenCleanups) {
return false;
}
try {
state.isCleanupRunning = true;
state.lastCleanupTime = now;
state.metrics.cleanupRuns++;
const startTime = performance.now();
const startMemory = checkMemoryUsage();
state.pendingAnimationFrames.forEach(id => {
cancelAnimationFrame(id);
});
state.pendingAnimationFrames.clear();
state.pendingTimeouts.forEach(id => {
clearTimeout(id);
});
state.pendingTimeouts.clear();
const tooltipsResult = removeOrphanedTooltips();
state.metrics.tooltipsRemoved += tooltipsResult;
const disconnectedResult = checkForDisconnectedElements();
tryRunGarbageCollection(false);
const endTime = performance.now();
const endMemory = checkMemoryUsage();
const runStats = {
timestamp: new Date().toISOString(),
duration: endTime - startTime,
tooltipsRemoved: tooltipsResult,
disconnectedRemoved: disconnectedResult,
memoryBefore: startMemory ? startMemory.usedMB : null,
memoryAfter: endMemory ? endMemory.usedMB : null,
memorySaved: startMemory && endMemory ?
(startMemory.usedMB - endMemory.usedMB).toFixed(2) : null
};
state.metrics.history.unshift(runStats);
if (state.metrics.history.length > 10) {
state.metrics.history.pop();
}
state.metrics.lastCleanupDetails = runStats;
if (config.debug) {
log(`Cleanup completed in ${runStats.duration.toFixed(2)}ms, removed ${tooltipsResult} tooltips`);
}
return true;
} catch (error) {
console.error("Error during cleanup:", error);
return false;
} finally {
state.isCleanupRunning = false;
}
}
function removeOrphanedTooltips() {
try {
const tippyRoots = document.querySelectorAll('[data-tippy-root]:not(:hover)');
let removed = 0;
tippyRoots.forEach(root => {
const tooltipId = root.getAttribute('data-for-tooltip-id');
const trigger = tooltipId ?
document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : null;
if (!trigger || !document.body.contains(trigger)) {
if (root.parentNode) {
root.parentNode.removeChild(root);
removed++;
}
}
});
return removed;
} catch (error) {
console.error("Error removing orphaned tooltips:", error);
return 0;
}
}
function checkForDisconnectedElements() {
try {
const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]:not(:hover)');
const disconnectedElements = new Set();
tooltipTriggers.forEach(el => {
if (!document.body.contains(el)) {
const tooltipId = el.getAttribute('data-tooltip-trigger-id');
disconnectedElements.add(tooltipId);
}
});
const tooltipRoots = document.querySelectorAll('[data-for-tooltip-id]');
let removed = 0;
disconnectedElements.forEach(id => {
for (const root of tooltipRoots) {
if (root.getAttribute('data-for-tooltip-id') === id && root.parentNode) {
root.parentNode.removeChild(root);
removed++;
break;
}
}
});
return disconnectedElements.size;
} catch (error) {
console.error("Error checking for disconnected elements:", error);
return 0;
}
}
function tryRunGarbageCollection(aggressive = false) {
setTimeout(() => {
const cache = {};
for (let i = 0; i < 100; i++) {
cache[`key${i}`] = {};
}
for (const key in cache) {
delete cache[key];
}
}, 100);
return true;
}
function checkMemoryUsage() {
const result = {
usedJSHeapSize: 0,
totalJSHeapSize: 0,
jsHeapSizeLimit: 0,
percentUsed: "0",
usedMB: "0",
totalMB: "0",
limitMB: "0"
};
if (window.performance && window.performance.memory) {
result.usedJSHeapSize = window.performance.memory.usedJSHeapSize;
result.totalJSHeapSize = window.performance.memory.totalJSHeapSize;
result.jsHeapSizeLimit = window.performance.memory.jsHeapSizeLimit;
result.percentUsed = (result.usedJSHeapSize / result.jsHeapSizeLimit * 100).toFixed(2);
result.usedMB = (result.usedJSHeapSize / (1024 * 1024)).toFixed(2);
result.totalMB = (result.totalJSHeapSize / (1024 * 1024)).toFixed(2);
result.limitMB = (result.jsHeapSizeLimit / (1024 * 1024)).toFixed(2);
} else {
result.usedMB = "Unknown";
result.totalMB = "Unknown";
result.limitMB = "Unknown";
result.percentUsed = "Unknown";
}
state.metrics.lastMemoryUsage = result;
return result;
}
function handleVisibilityChange() {
if (document.hidden) {
removeOrphanedTooltips();
checkForDisconnectedElements();
}
}
function setupMutationObserver() {
if (state.mutationObserver) {
state.mutationObserver.disconnect();
state.mutationObserver = null;
}
let processingScheduled = false;
let lastProcessTime = 0;
const MIN_PROCESS_INTERVAL = 10000;
const processMutations = (mutations) => {
const now = Date.now();
if (now - lastProcessTime < MIN_PROCESS_INTERVAL || processingScheduled) {
return;
}
processingScheduled = true;
setTimeout(() => {
processingScheduled = false;
lastProcessTime = Date.now();
if (state.isCleanupRunning) {
return;
}
const tooltipSelectors = ['[data-tippy-root]', '[data-tooltip-trigger-id]', '.tooltip'];
let tooltipCount = 0;
tooltipCount = document.querySelectorAll(tooltipSelectors.join(', ')).length;
if (tooltipCount > config.maxTooltipsThreshold &&
(Date.now() - state.lastCleanupTime > config.minTimeBetweenCleanups)) {
removeOrphanedTooltips();
checkForDisconnectedElements();
state.lastCleanupTime = Date.now();
}
}, 5000);
};
state.mutationObserver = new MutationObserver(processMutations);
state.mutationObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
return state.mutationObserver;
}
function enhanceTooltipManager() {
if (!window.TooltipManager || window.TooltipManager._memoryManagerEnhanced) return false;
preserveTooltipFunctions();
const originalDestroy = window.TooltipManager.destroy;
const originalCleanup = window.TooltipManager.cleanup;
window.TooltipManager.destroy = function(element) {
if (!element) return;
try {
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
if (isInProtectedContainer(element)) {
if (originalDestroy) {
return originalDestroy.call(window.TooltipManager, element);
}
return;
}
if (tooltipId) {
if (originalDestroy) {
originalDestroy.call(window.TooltipManager, element);
}
const tooltipRoot = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`);
if (tooltipRoot && tooltipRoot.parentNode) {
tooltipRoot.parentNode.removeChild(tooltipRoot);
}
element.removeAttribute('data-tooltip-trigger-id');
element.removeAttribute('aria-describedby');
if (element._tippy) {
try {
element._tippy.destroy();
element._tippy = null;
} catch (e) {}
}
}
} catch (error) {
console.error('Error in enhanced tooltip destroy:', error);
if (originalDestroy) {
originalDestroy.call(window.TooltipManager, element);
}
}
};
window.TooltipManager.cleanup = function() {
try {
if (originalCleanup) {
originalCleanup.call(window.TooltipManager);
}
removeOrphanedTooltips();
} catch (error) {
console.error('Error in enhanced tooltip cleanup:', error);
if (originalCleanup) {
originalCleanup.call(window.TooltipManager);
}
}
};
window.TooltipManager._memoryManagerEnhanced = true;
window.TooltipManager._originalDestroy = originalDestroy;
window.TooltipManager._originalCleanup = originalCleanup;
return true;
}
function initializeScheduledCleanups() {
if (state.cleanupInterval) {
clearInterval(state.cleanupInterval);
state.cleanupInterval = null;
}
if (state.diagnosticsInterval) {
clearInterval(state.diagnosticsInterval);
state.diagnosticsInterval = null;
}
if (state.elementVerificationInterval) {
clearInterval(state.elementVerificationInterval);
state.elementVerificationInterval = null;
}
state.cleanupInterval = setInterval(() => {
removeOrphanedTooltips();
checkForDisconnectedElements();
}, config.tooltipCleanupInterval);
state.diagnosticsInterval = setInterval(() => {
checkMemoryUsage();
}, config.diagnosticsInterval);
state.elementVerificationInterval = setInterval(() => {
checkForDisconnectedElements();
}, config.elementVerificationInterval);
document.removeEventListener('visibilitychange', handleVisibilityChange);
document.addEventListener('visibilitychange', handleVisibilityChange);
setupMutationObserver();
return true;
}
function initialize(options = {}) {
preserveTooltipFunctions();
if (options) {
Object.assign(config, options);
}
enhanceTooltipManager();
if (window.WebSocketManager && !window.WebSocketManager.cleanupOrphanedSockets) {
window.WebSocketManager.cleanupOrphanedSockets = function() {
return 0;
};
}
const manager = window.ApiManager || window.Api;
if (manager && !manager.abortPendingRequests) {
manager.abortPendingRequests = function() {
return 0;
};
}
initializeScheduledCleanups();
setTimeout(() => {
removeOrphanedTooltips();
checkForDisconnectedElements();
}, 5000);
return this;
}
function dispose() {
if (state.cleanupInterval) {
clearInterval(state.cleanupInterval);
state.cleanupInterval = null;
}
if (state.diagnosticsInterval) {
clearInterval(state.diagnosticsInterval);
state.diagnosticsInterval = null;
}
if (state.elementVerificationInterval) {
clearInterval(state.elementVerificationInterval);
state.elementVerificationInterval = null;
}
if (state.mutationObserver) {
state.mutationObserver.disconnect();
state.mutationObserver = null;
}
document.removeEventListener('visibilitychange', handleVisibilityChange);
return true;
}
function displayStats() {
const stats = getDetailedStats();
console.group('Memory Manager Stats');
console.log('Memory Usage:', stats.memory ?
`${stats.memory.usedMB}MB / ${stats.memory.limitMB}MB (${stats.memory.percentUsed}%)` :
'Not available');
console.log('Total Cleanups:', stats.metrics.cleanupRuns);
console.log('Total Tooltips Removed:', stats.metrics.tooltipsRemoved);
console.log('Current Tooltips:', stats.tooltips.total);
console.log('Last Cleanup:', stats.metrics.lastCleanupDetails);
console.log('Cleanup History:', stats.metrics.history);
console.groupEnd();
return stats;
}
function getDetailedStats() {
const allTooltipElements = document.querySelectorAll('[data-tippy-root], [data-tooltip-trigger-id], .tooltip');
const tooltips = {
roots: document.querySelectorAll('[data-tippy-root]').length,
triggers: document.querySelectorAll('[data-tooltip-trigger-id]').length,
tooltipElements: document.querySelectorAll('.tooltip').length,
total: allTooltipElements.length,
protectedContainers: {}
};
config.protectedContainers.forEach(selector => {
const container = document.querySelector(selector);
if (container) {
tooltips.protectedContainers[selector] = {
tooltips: container.querySelectorAll('.tooltip').length,
triggers: container.querySelectorAll('[data-tooltip-trigger-id]').length,
roots: document.querySelectorAll(`[data-tippy-root][data-for-tooltip-id]`).length
};
}
});
return {
memory: checkMemoryUsage(),
metrics: { ...state.metrics },
tooltips,
config: { ...config }
};
}
return {
initialize,
cleanup: performCleanup,
forceCleanup: function() {
return performCleanup(true);
},
fullCleanup: function() {
return performCleanup(true);
},
getStats: getDetailedStats,
displayStats,
setDebugMode: function(enabled) {
config.debug = Boolean(enabled);
return config.debug;
},
addProtectedContainer: function(selector) {
if (!config.protectedContainers.includes(selector)) {
config.protectedContainers.push(selector);
}
return config.protectedContainers;
},
removeProtectedContainer: function(selector) {
const index = config.protectedContainers.indexOf(selector);
if (index !== -1) {
config.protectedContainers.splice(index, 1);
}
return config.protectedContainers;
},
dispose
};
})();
document.addEventListener('DOMContentLoaded', function() {
const isDevMode = window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1';
MemoryManager.initialize({
debug: isDevMode
});
console.log('Memory Manager initialized');
});
window.MemoryManager = MemoryManager;

View File

@@ -1,280 +0,0 @@
const NetworkManager = (function() {
const state = {
isOnline: navigator.onLine,
reconnectAttempts: 0,
reconnectTimer: null,
lastNetworkError: null,
eventHandlers: {},
connectionTestInProgress: false
};
const config = {
maxReconnectAttempts: 5,
reconnectDelay: 5000,
reconnectBackoff: 1.5,
connectionTestEndpoint: '/json',
connectionTestTimeout: 3000,
debug: false
};
function log(message, ...args) {
if (config.debug) {
console.log(`[NetworkManager] ${message}`, ...args);
}
}
function generateHandlerId() {
return `handler_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
}
const publicAPI = {
initialize: function(options = {}) {
Object.assign(config, options);
window.addEventListener('online', this.handleOnlineStatus.bind(this));
window.addEventListener('offline', this.handleOfflineStatus.bind(this));
state.isOnline = navigator.onLine;
log(`Network status initialized: ${state.isOnline ? 'online' : 'offline'}`);
if (window.CleanupManager) {
window.CleanupManager.registerResource('networkManager', this, (mgr) => mgr.dispose());
}
return this;
},
isOnline: function() {
return state.isOnline;
},
getReconnectAttempts: function() {
return state.reconnectAttempts;
},
resetReconnectAttempts: function() {
state.reconnectAttempts = 0;
return this;
},
handleOnlineStatus: function() {
log('Browser reports online status');
state.isOnline = true;
this.notifyHandlers('online');
if (state.reconnectTimer) {
this.scheduleReconnectRefresh();
}
},
handleOfflineStatus: function() {
log('Browser reports offline status');
state.isOnline = false;
this.notifyHandlers('offline');
},
handleNetworkError: function(error) {
if (error && (
(error.name === 'TypeError' && error.message.includes('NetworkError')) ||
(error.name === 'AbortError') ||
(error.message && error.message.includes('network')) ||
(error.message && error.message.includes('timeout'))
)) {
log('Network error detected:', error.message);
if (state.isOnline) {
state.isOnline = false;
state.lastNetworkError = error;
this.notifyHandlers('error', error);
}
if (!state.reconnectTimer) {
this.scheduleReconnectRefresh();
}
return true;
}
return false;
},
scheduleReconnectRefresh: function() {
if (state.reconnectTimer) {
clearTimeout(state.reconnectTimer);
state.reconnectTimer = null;
}
const delay = config.reconnectDelay * Math.pow(config.reconnectBackoff,
Math.min(state.reconnectAttempts, 5));
log(`Scheduling reconnection attempt in ${delay/1000} seconds`);
state.reconnectTimer = setTimeout(() => {
state.reconnectTimer = null;
this.attemptReconnect();
}, delay);
return this;
},
attemptReconnect: function() {
if (!navigator.onLine) {
log('Browser still reports offline, delaying reconnection attempt');
this.scheduleReconnectRefresh();
return;
}
if (state.connectionTestInProgress) {
log('Connection test already in progress');
return;
}
state.reconnectAttempts++;
state.connectionTestInProgress = true;
log(`Attempting reconnect #${state.reconnectAttempts}`);
this.testBackendConnection()
.then(isAvailable => {
state.connectionTestInProgress = false;
if (isAvailable) {
log('Backend connection confirmed');
state.isOnline = true;
state.reconnectAttempts = 0;
state.lastNetworkError = null;
this.notifyHandlers('reconnected');
} else {
log('Backend still unavailable');
if (state.reconnectAttempts < config.maxReconnectAttempts) {
this.scheduleReconnectRefresh();
} else {
log('Maximum reconnect attempts reached');
this.notifyHandlers('maxAttemptsReached');
}
}
})
.catch(error => {
state.connectionTestInProgress = false;
log('Error during connection test:', error);
if (state.reconnectAttempts < config.maxReconnectAttempts) {
this.scheduleReconnectRefresh();
} else {
log('Maximum reconnect attempts reached');
this.notifyHandlers('maxAttemptsReached');
}
});
},
testBackendConnection: function() {
return fetch(config.connectionTestEndpoint, {
method: 'HEAD',
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
},
timeout: config.connectionTestTimeout,
signal: AbortSignal.timeout(config.connectionTestTimeout)
})
.then(response => {
return response.ok;
})
.catch(error => {
log('Backend connection test failed:', error.message);
return false;
});
},
manualReconnect: function() {
log('Manual reconnection requested');
state.isOnline = navigator.onLine;
state.reconnectAttempts = 0;
this.notifyHandlers('manualReconnect');
if (state.isOnline) {
return this.attemptReconnect();
} else {
log('Cannot attempt manual reconnect while browser reports offline');
this.notifyHandlers('offlineWarning');
return false;
}
},
addHandler: function(event, handler) {
if (!state.eventHandlers[event]) {
state.eventHandlers[event] = {};
}
const handlerId = generateHandlerId();
state.eventHandlers[event][handlerId] = handler;
return handlerId;
},
removeHandler: function(event, handlerId) {
if (state.eventHandlers[event] && state.eventHandlers[event][handlerId]) {
delete state.eventHandlers[event][handlerId];
return true;
}
return false;
},
notifyHandlers: function(event, data) {
if (state.eventHandlers[event]) {
Object.values(state.eventHandlers[event]).forEach(handler => {
try {
handler(data);
} catch (error) {
log(`Error in ${event} handler:`, error);
}
});
}
},
setDebugMode: function(enabled) {
config.debug = Boolean(enabled);
return `Debug mode ${config.debug ? 'enabled' : 'disabled'}`;
},
getState: function() {
return {
isOnline: state.isOnline,
reconnectAttempts: state.reconnectAttempts,
hasReconnectTimer: Boolean(state.reconnectTimer),
connectionTestInProgress: state.connectionTestInProgress
};
},
dispose: function() {
if (state.reconnectTimer) {
clearTimeout(state.reconnectTimer);
state.reconnectTimer = null;
}
window.removeEventListener('online', this.handleOnlineStatus);
window.removeEventListener('offline', this.handleOfflineStatus);
state.eventHandlers = {};
log('NetworkManager disposed');
}
};
return publicAPI;
})();
window.NetworkManager = NetworkManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.networkManagerInitialized) {
NetworkManager.initialize();
window.networkManagerInitialized = true;
}
});
//console.log('NetworkManager initialized with methods:', Object.keys(NetworkManager));
console.log('NetworkManager initialized');

View File

@@ -1,126 +0,0 @@
const NotificationManager = (function() {
const config = {
showNewOffers: false,
showNewBids: true,
showBidAccepted: true
};
function ensureToastContainer() {
let container = document.getElementById('ul_updates');
if (!container) {
const floating_div = document.createElement('div');
floating_div.classList.add('floatright');
container = document.createElement('ul');
container.setAttribute('id', 'ul_updates');
floating_div.appendChild(container);
document.body.appendChild(floating_div);
}
return container;
}
const publicAPI = {
initialize: function(options = {}) {
Object.assign(config, options);
if (window.CleanupManager) {
window.CleanupManager.registerResource('notificationManager', this, (mgr) => {
console.log('NotificationManager disposed');
});
}
return this;
},
createToast: function(title, type = 'success') {
const messages = ensureToastContainer();
const message = document.createElement('li');
message.innerHTML = `
<div id="hide">
<div id="toast-${type}" class="flex items-center p-4 mb-4 w-full max-w-xs text-gray-500
bg-white rounded-lg shadow" role="alert">
<div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10
bg-blue-500 rounded-lg">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="18" width="18"
viewBox="0 0 24 24">
<g fill="#ffffff">
<path d="M8.5,20a1.5,1.5,0,0,1-1.061-.439L.379,12.5,2.5,10.379l6,6,13-13L23.621,
5.5,9.561,19.561A1.5,1.5,0,0,1,8.5,20Z"></path>
</g>
</svg>
</div>
<div class="uppercase w-40 ml-3 text-sm font-semibold text-gray-900">${title}</div>
<button type="button" onclick="closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5
bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-0 focus:outline-none
focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8">
<span class="sr-only">Close</span>
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1
1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293
4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</button>
</div>
</div>
`;
messages.appendChild(message);
},
handleWebSocketEvent: function(data) {
if (!data || !data.event) return;
let toastTitle;
let shouldShowToast = false;
switch (data.event) {
case 'new_offer':
toastTitle = `New network <a class="underline" href=/offer/${data.offer_id}>offer</a>`;
shouldShowToast = config.showNewOffers;
break;
case 'new_bid':
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>New bid</a> on
<a class="underline" href=/offer/${data.offer_id}>offer</a>`;
shouldShowToast = config.showNewBids;
break;
case 'bid_accepted':
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>Bid</a> accepted`;
shouldShowToast = config.showBidAccepted;
break;
}
if (toastTitle && shouldShowToast) {
this.createToast(toastTitle);
}
},
updateConfig: function(newConfig) {
Object.assign(config, newConfig);
return this;
}
};
window.closeAlert = function(event) {
let element = event.target;
while (element.nodeName !== "BUTTON") {
element = element.parentNode;
}
element.parentNode.parentNode.removeChild(element.parentNode);
};
return publicAPI;
})();
window.NotificationManager = NotificationManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.notificationManagerInitialized) {
window.NotificationManager.initialize(window.notificationConfig || {});
window.notificationManagerInitialized = true;
}
});
//console.log('NotificationManager initialized with methods:', Object.keys(NotificationManager));
console.log('NotificationManager initialized');

View File

@@ -1,233 +0,0 @@
const PriceManager = (function() {
const PRICES_CACHE_KEY = 'prices_unified';
let fetchPromise = null;
let lastFetchTime = 0;
const MIN_FETCH_INTERVAL = 60000;
let isInitialized = false;
const eventListeners = {
'priceUpdate': [],
'error': []
};
return {
addEventListener: function(event, callback) {
if (eventListeners[event]) {
eventListeners[event].push(callback);
}
},
removeEventListener: function(event, callback) {
if (eventListeners[event]) {
eventListeners[event] = eventListeners[event].filter(cb => cb !== callback);
}
},
triggerEvent: function(event, data) {
if (eventListeners[event]) {
eventListeners[event].forEach(callback => callback(data));
}
},
initialize: function() {
if (isInitialized) {
console.warn('PriceManager: Already initialized');
return this;
}
if (window.CleanupManager) {
window.CleanupManager.registerResource('priceManager', this, (mgr) => {
Object.keys(eventListeners).forEach(event => {
eventListeners[event] = [];
});
});
}
setTimeout(() => this.getPrices(), 1500);
isInitialized = true;
return this;
},
getPrices: async function(forceRefresh = false) {
if (!forceRefresh) {
const cachedData = CacheManager.get(PRICES_CACHE_KEY);
if (cachedData) {
return cachedData.value;
}
}
if (fetchPromise && Date.now() - lastFetchTime < MIN_FETCH_INTERVAL) {
return fetchPromise;
}
//console.log('PriceManager: Fetching latest prices.');
lastFetchTime = Date.now();
fetchPromise = this.fetchPrices()
.then(prices => {
this.triggerEvent('priceUpdate', prices);
return prices;
})
.catch(error => {
this.triggerEvent('error', error);
throw error;
})
.finally(() => {
fetchPromise = null;
});
return fetchPromise;
},
fetchPrices: async function() {
try {
if (!NetworkManager.isOnline()) {
throw new Error('Network is offline');
}
const coinSymbols = window.CoinManager
? window.CoinManager.getAllCoins().map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
: (window.config.coins
? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
: ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']);
//console.log('PriceManager: lookupFiatRates ' + coinSymbols.join(', '));
if (!coinSymbols.length) {
throw new Error('No valid coins configured');
}
let apiResponse;
try {
apiResponse = await Api.fetchCoinPrices(
coinSymbols,
"coingecko.com",
300
);
if (!apiResponse) {
throw new Error('Empty response received from API');
}
if (apiResponse.error) {
throw new Error(`API error: ${apiResponse.error}`);
}
if (!apiResponse.rates) {
throw new Error('No rates found in API response');
}
if (typeof apiResponse.rates !== 'object' || Object.keys(apiResponse.rates).length === 0) {
throw new Error('Empty rates object in API response');
}
} catch (apiError) {
console.error('API call error:', apiError);
throw new Error(`API error: ${apiError.message}`);
}
const processedData = {};
Object.entries(apiResponse.rates).forEach(([coinId, price]) => {
let normalizedCoinId;
if (window.CoinManager) {
const coin = window.CoinManager.getCoinByAnyIdentifier(coinId);
if (coin) {
normalizedCoinId = window.CoinManager.getPriceKey(coin.name);
} else {
normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
}
} else {
normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
}
if (coinId.toLowerCase() === 'zcoin') {
normalizedCoinId = 'firo';
}
processedData[normalizedCoinId] = {
usd: price,
btc: normalizedCoinId === 'bitcoin' ? 1 : price / (apiResponse.rates.bitcoin || 1)
};
});
CacheManager.set(PRICES_CACHE_KEY, processedData, 'prices');
Object.entries(processedData).forEach(([coin, prices]) => {
if (prices.usd) {
if (window.tableRateModule) {
window.tableRateModule.setFallbackValue(coin, prices.usd);
}
}
});
return processedData;
} catch (error) {
console.error('Error fetching prices:', error);
NetworkManager.handleNetworkError(error);
const cachedData = CacheManager.get(PRICES_CACHE_KEY);
if (cachedData) {
console.log('Using cached price data');
return cachedData.value;
}
try {
const existingCache = localStorage.getItem(PRICES_CACHE_KEY);
if (existingCache) {
console.log('Using localStorage cached price data');
return JSON.parse(existingCache).value;
}
} catch (e) {
console.warn('Failed to parse existing cache:', e);
}
const emptyData = {};
const coinNames = window.CoinManager
? window.CoinManager.getAllCoins().map(c => c.name.toLowerCase())
: ['bitcoin', 'bitcoin-cash', 'dash', 'dogecoin', 'decred', 'namecoin', 'litecoin', 'particl', 'pivx', 'monero', 'wownero', 'firo'];
coinNames.forEach(coin => {
emptyData[coin] = { usd: null, btc: null };
});
return emptyData;
}
},
getCoinPrice: function(coinSymbol) {
if (!coinSymbol) return null;
const prices = this.getPrices();
if (!prices) return null;
let normalizedSymbol;
if (window.CoinManager) {
normalizedSymbol = window.CoinManager.getPriceKey(coinSymbol);
} else {
normalizedSymbol = coinSymbol.toLowerCase();
}
return prices[normalizedSymbol] || null;
},
formatPrice: function(coin, price) {
if (window.config && window.config.utils && window.config.utils.formatPrice) {
return window.config.utils.formatPrice(coin, price);
}
if (typeof price !== 'number' || isNaN(price)) return 'N/A';
if (price < 0.01) return price.toFixed(8);
if (price < 1) return price.toFixed(4);
if (price < 1000) return price.toFixed(2);
return price.toFixed(0);
}
};
})();
window.PriceManager = PriceManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.priceManagerInitialized) {
window.PriceManager = PriceManager.initialize();
window.priceManagerInitialized = true;
}
});
console.log('PriceManager initialized');

View File

@@ -1,444 +0,0 @@
const SummaryManager = (function() {
const config = {
refreshInterval: window.config?.cacheDuration || 30000,
summaryEndpoint: '/json',
retryDelay: 5000,
maxRetries: 3,
requestTimeout: 15000,
debug: false
};
let refreshTimer = null;
let webSocket = null;
let fetchRetryCount = 0;
let lastSuccessfulData = null;
function updateElement(elementId, value) {
const element = document.getElementById(elementId);
if (!element) return false;
const safeValue = (value !== undefined && value !== null)
? value
: (element.dataset.lastValue || 0);
element.dataset.lastValue = safeValue;
if (elementId === 'sent-bids-counter' || elementId === 'recv-bids-counter') {
const svg = element.querySelector('svg');
element.textContent = safeValue;
if (svg) {
element.insertBefore(svg, element.firstChild);
}
} else {
element.textContent = safeValue;
}
if (['offers-counter', 'bid-requests-counter', 'sent-bids-counter',
'recv-bids-counter', 'swaps-counter', 'network-offers-counter',
'watched-outputs-counter'].includes(elementId)) {
element.classList.remove('bg-blue-500', 'bg-gray-400');
element.classList.add(safeValue > 0 ? 'bg-blue-500' : 'bg-gray-400');
}
if (elementId === 'swaps-counter') {
const swapContainer = document.getElementById('swapContainer');
if (swapContainer) {
const isSwapping = safeValue > 0;
if (isSwapping) {
swapContainer.innerHTML = document.querySelector('#swap-in-progress-green-template').innerHTML || '';
swapContainer.style.animation = 'spin 2s linear infinite';
} else {
swapContainer.innerHTML = document.querySelector('#swap-in-progress-template').innerHTML || '';
swapContainer.style.animation = 'none';
}
}
}
return true;
}
function updateUIFromData(data) {
if (!data) return;
updateElement('network-offers-counter', data.num_network_offers);
updateElement('offers-counter', data.num_sent_active_offers);
updateElement('offers-counter-mobile', data.num_sent_active_offers);
updateElement('sent-bids-counter', data.num_sent_active_bids);
updateElement('recv-bids-counter', data.num_recv_active_bids);
updateElement('bid-requests-counter', data.num_available_bids);
updateElement('swaps-counter', data.num_swapping);
updateElement('watched-outputs-counter', data.num_watched_outputs);
updateTooltips(data);
const shutdownButtons = document.querySelectorAll('.shutdown-button');
shutdownButtons.forEach(button => {
button.setAttribute('data-active-swaps', data.num_swapping);
if (data.num_swapping > 0) {
button.classList.add('shutdown-disabled');
button.setAttribute('data-disabled', 'true');
button.setAttribute('title', 'Caution: Swaps in progress');
} else {
button.classList.remove('shutdown-disabled');
button.removeAttribute('data-disabled');
button.removeAttribute('title');
}
});
}
function updateTooltips(data) {
debugLog(`updateTooltips called with data:`, data);
const yourOffersTooltip = document.getElementById('tooltip-your-offers');
debugLog('Looking for tooltip-your-offers element:', yourOffersTooltip);
if (yourOffersTooltip) {
const newContent = `
<p><b>Total offers:</b> ${data.num_sent_offers || 0}</p>
<p><b>Active offers:</b> ${data.num_sent_active_offers || 0}</p>
`;
const totalParagraph = yourOffersTooltip.querySelector('p:first-child');
const activeParagraph = yourOffersTooltip.querySelector('p:last-child');
debugLog('Found paragraphs:', { totalParagraph, activeParagraph });
if (totalParagraph && activeParagraph) {
totalParagraph.innerHTML = `<b>Total offers:</b> ${data.num_sent_offers || 0}`;
activeParagraph.innerHTML = `<b>Active offers:</b> ${data.num_sent_active_offers || 0}`;
debugLog(`Updated Your Offers tooltip paragraphs: total=${data.num_sent_offers}, active=${data.num_sent_active_offers}`);
} else {
yourOffersTooltip.innerHTML = newContent;
debugLog(`Replaced Your Offers tooltip content: total=${data.num_sent_offers}, active=${data.num_sent_active_offers}`);
}
refreshTooltipInstances('tooltip-your-offers', newContent);
} else {
debugLog('Your Offers tooltip element not found - checking all tooltip elements');
const allTooltips = document.querySelectorAll('[id*="tooltip"]');
debugLog('All tooltip elements found:', Array.from(allTooltips).map(el => el.id));
}
const bidsTooltip = document.getElementById('tooltip-bids');
if (bidsTooltip) {
const newBidsContent = `
<p><b>Sent bids:</b> ${data.num_sent_bids || 0} (${data.num_sent_active_bids || 0} active)</p>
<p><b>Received bids:</b> ${data.num_recv_bids || 0} (${data.num_recv_active_bids || 0} active)</p>
`;
const sentParagraph = bidsTooltip.querySelector('p:first-child');
const recvParagraph = bidsTooltip.querySelector('p:last-child');
if (sentParagraph && recvParagraph) {
sentParagraph.innerHTML = `<b>Sent bids:</b> ${data.num_sent_bids || 0} (${data.num_sent_active_bids || 0} active)`;
recvParagraph.innerHTML = `<b>Received bids:</b> ${data.num_recv_bids || 0} (${data.num_recv_active_bids || 0} active)`;
debugLog(`Updated Bids tooltip: sent=${data.num_sent_bids}(${data.num_sent_active_bids}), recv=${data.num_recv_bids}(${data.num_recv_active_bids})`);
} else {
bidsTooltip.innerHTML = newBidsContent;
debugLog(`Replaced Bids tooltip content: sent=${data.num_sent_bids}(${data.num_sent_active_bids}), recv=${data.num_recv_bids}(${data.num_recv_active_bids})`);
}
refreshTooltipInstances('tooltip-bids', newBidsContent);
} else {
debugLog('Bids tooltip element not found');
}
}
function refreshTooltipInstances(tooltipId, newContent) {
const triggers = document.querySelectorAll(`[data-tooltip-target="${tooltipId}"]`);
triggers.forEach(trigger => {
if (trigger._tippy) {
trigger._tippy.setContent(newContent);
debugLog(`Updated Tippy instance content for ${tooltipId}`);
} else {
if (window.TooltipManager && typeof window.TooltipManager.create === 'function') {
window.TooltipManager.create(trigger, newContent, {
placement: trigger.getAttribute('data-tooltip-placement') || 'top'
});
debugLog(`Created new Tippy instance for ${tooltipId}`);
}
}
});
if (window.TooltipManager && typeof window.TooltipManager.refreshTooltip === 'function') {
window.TooltipManager.refreshTooltip(tooltipId, newContent);
debugLog(`Refreshed tooltip via TooltipManager for ${tooltipId}`);
}
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
setTimeout(() => {
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
debugLog(`Re-initialized tooltips for ${tooltipId}`);
}, 50);
}
}
function debugLog(message) {
if (config.debug && console && console.log) {
console.log(`[SummaryManager] ${message}`);
}
}
function cacheSummaryData(data) {
if (!data) return;
localStorage.setItem('summary_data_cache', JSON.stringify({
timestamp: Date.now(),
data: data
}));
}
function getCachedSummaryData() {
let cachedData = null;
cachedData = localStorage.getItem('summary_data_cache');
if (!cachedData) return null;
const parsedCache = JSON.parse(cachedData);
const maxAge = 24 * 60 * 60 * 1000;
if (Date.now() - parsedCache.timestamp < maxAge) {
return parsedCache.data;
}
return null;
}
function fetchSummaryDataWithTimeout() {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), config.requestTimeout);
return fetch(config.summaryEndpoint, {
signal: controller.signal,
headers: {
'Accept': 'application/json',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
}
})
.then(response => {
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.catch(error => {
clearTimeout(timeoutId);
throw error;
});
}
function setupWebSocket() {
if (webSocket) {
webSocket.close();
}
const wsPort = window.config?.wsPort ||
(typeof determineWebSocketPort === 'function' ? determineWebSocketPort() : '11700');
const wsUrl = "ws://" + window.location.hostname + ":" + wsPort;
webSocket = new WebSocket(wsUrl);
webSocket.onopen = () => {
publicAPI.fetchSummaryData()
.then(() => {})
.catch(() => {});
};
webSocket.onmessage = (event) => {
let data;
try {
data = JSON.parse(event.data);
} catch (error) {
if (window.logger && window.logger.error) {
window.logger.error('WebSocket message processing error: ' + error.message);
}
return;
}
if (data.event) {
publicAPI.fetchSummaryData()
.then(() => {})
.catch(() => {});
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
window.NotificationManager.handleWebSocketEvent(data);
}
}
};
webSocket.onclose = () => {
setTimeout(setupWebSocket, 5000);
};
}
function ensureSwapTemplates() {
if (!document.getElementById('swap-in-progress-template')) {
const template = document.createElement('template');
template.id = 'swap-in-progress-template';
template.innerHTML = document.querySelector('[id^="swapContainer"]')?.innerHTML || '';
document.body.appendChild(template);
}
if (!document.getElementById('swap-in-progress-green-template') &&
document.querySelector('[id^="swapContainer"]')?.innerHTML) {
const greenTemplate = document.createElement('template');
greenTemplate.id = 'swap-in-progress-green-template';
greenTemplate.innerHTML = document.querySelector('[id^="swapContainer"]')?.innerHTML || '';
document.body.appendChild(greenTemplate);
}
}
function startRefreshTimer() {
stopRefreshTimer();
publicAPI.fetchSummaryData()
.then(() => {})
.catch(() => {});
refreshTimer = setInterval(() => {
publicAPI.fetchSummaryData()
.then(() => {})
.catch(() => {});
}, config.refreshInterval);
}
function stopRefreshTimer() {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
}
const publicAPI = {
initialize: function(options = {}) {
Object.assign(config, options);
ensureSwapTemplates();
const cachedData = getCachedSummaryData();
if (cachedData) {
updateUIFromData(cachedData);
}
if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') {
const wsManager = window.WebSocketManager;
if (!wsManager.isConnected()) {
wsManager.connect();
}
wsManager.addMessageHandler('message', (data) => {
if (data.event) {
this.fetchSummaryData()
.then(() => {})
.catch(() => {});
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
window.NotificationManager.handleWebSocketEvent(data);
}
}
});
} else {
setupWebSocket();
}
startRefreshTimer();
if (window.CleanupManager) {
window.CleanupManager.registerResource('summaryManager', this, (mgr) => mgr.dispose());
}
return this;
},
fetchSummaryData: function() {
return fetchSummaryDataWithTimeout()
.then(data => {
lastSuccessfulData = data;
cacheSummaryData(data);
fetchRetryCount = 0;
updateUIFromData(data);
return data;
})
.catch(error => {
if (window.logger && window.logger.error) {
window.logger.error('Summary data fetch error: ' + error.message);
}
if (fetchRetryCount < config.maxRetries) {
fetchRetryCount++;
if (window.logger && window.logger.warn) {
window.logger.warn(`Retrying summary data fetch (${fetchRetryCount}/${config.maxRetries}) in ${config.retryDelay/1000}s`);
}
return new Promise(resolve => {
setTimeout(() => {
resolve(this.fetchSummaryData());
}, config.retryDelay);
});
} else {
const cachedData = lastSuccessfulData || getCachedSummaryData();
if (cachedData) {
if (window.logger && window.logger.warn) {
window.logger.warn('Using cached summary data after fetch failures');
}
updateUIFromData(cachedData);
}
fetchRetryCount = 0;
throw error;
}
});
},
updateTooltips: function(data) {
updateTooltips(data || lastSuccessfulData);
},
updateUI: function(data) {
updateUIFromData(data || lastSuccessfulData);
},
startRefreshTimer: function() {
startRefreshTimer();
},
stopRefreshTimer: function() {
stopRefreshTimer();
},
dispose: function() {
stopRefreshTimer();
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
webSocket.close();
}
webSocket = null;
}
};
return publicAPI;
})();
window.SummaryManager = SummaryManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.summaryManagerInitialized) {
window.SummaryManager = SummaryManager.initialize();
window.summaryManagerInitialized = true;
}
});
//console.log('SummaryManager initialized with methods:', Object.keys(SummaryManager));
console.log('SummaryManager initialized');

View File

@@ -1,899 +0,0 @@
const TooltipManager = (function() {
let instance = null;
const tooltipInstanceMap = new WeakMap();
class TooltipManagerImpl {
constructor() {
if (instance) {
return instance;
}
this.tooltipIdCounter = 0;
this.maxTooltips = 200;
this.cleanupThreshold = 1.2;
this.debug = false;
this.tooltipData = new WeakMap();
this.resources = {};
if (window.CleanupManager) {
CleanupManager.registerResource(
'tooltipManager',
this,
(manager) => manager.dispose()
);
}
instance = this;
}
log(message, ...args) {
if (this.debug) {
console.log(`[TooltipManager] ${message}`, ...args);
}
}
create(element, content, options = {}) {
if (!element || !document.body.contains(element)) return null;
if (!document.contains(element)) {
this.log('Tried to create tooltip for detached element');
return null;
}
this.destroy(element);
const currentTooltipCount = document.querySelectorAll('[data-tooltip-trigger-id]').length;
if (currentTooltipCount > this.maxTooltips * this.cleanupThreshold) {
this.cleanupOrphanedTooltips();
this.performPeriodicCleanup(true);
}
const createTooltip = () => {
if (!document.body.contains(element)) return;
const rect = element.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
this.createTooltipInstance(element, content, options);
} else {
let retryCount = 0;
const maxRetries = 3;
const retryCreate = () => {
const newRect = element.getBoundingClientRect();
if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) {
if (newRect.width > 0 && newRect.height > 0) {
this.createTooltipInstance(element, content, options);
}
} else {
retryCount++;
CleanupManager.setTimeout(() => {
CleanupManager.requestAnimationFrame(retryCreate);
}, 100);
}
};
CleanupManager.setTimeout(() => {
CleanupManager.requestAnimationFrame(retryCreate);
}, 100);
}
};
CleanupManager.requestAnimationFrame(createTooltip);
return null;
}
createTooltipInstance(element, content, options = {}) {
if (!element || !document.body.contains(element)) {
return null;
}
if (typeof window.tippy !== 'function') {
console.error('Tippy.js is not available.');
return null;
}
try {
const tooltipId = `tooltip-${++this.tooltipIdCounter}`;
const tooltipOptions = {
content: content,
allowHTML: true,
placement: options.placement || 'top',
appendTo: document.body,
animation: false,
duration: 0,
delay: 0,
interactive: true,
arrow: false,
theme: '',
moveTransition: 'none',
offset: [0, 10],
onShow(instance) {
if (!document.body.contains(element)) {
return false;
}
return true;
},
onMount(instance) {
if (instance.popper && instance.popper.firstElementChild) {
const bgClass = options.bgClass || 'bg-gray-400';
instance.popper.firstElementChild.classList.add(bgClass);
instance.popper.setAttribute('data-for-tooltip-id', tooltipId);
}
const arrow = instance.popper.querySelector('.tippy-arrow');
if (arrow) {
const arrowColor = options.arrowColor || 'rgb(156 163 175)';
arrow.style.setProperty('color', arrowColor, 'important');
}
},
onHidden(instance) {
if (!document.body.contains(element)) {
CleanupManager.setTimeout(() => {
if (instance && instance.destroy) {
instance.destroy();
}
}, 100);
}
},
popperOptions: {
strategy: 'fixed',
modifiers: [
{
name: 'preventOverflow',
options: {
boundary: 'viewport',
padding: 10
}
},
{
name: 'flip',
options: {
padding: 10,
fallbackPlacements: ['top', 'bottom', 'right', 'left']
}
}
]
}
};
const tippyInstance = window.tippy(element, tooltipOptions);
if (tippyInstance && Array.isArray(tippyInstance) && tippyInstance[0]) {
this.tooltipData.set(element, {
id: tooltipId,
instance: tippyInstance[0],
timestamp: Date.now()
});
element.setAttribute('data-tooltip-trigger-id', tooltipId);
tooltipInstanceMap.set(element, tippyInstance[0]);
const resourceId = CleanupManager.registerResource(
'tooltip',
{ element, instance: tippyInstance[0] },
(resource) => {
try {
if (resource.instance && resource.instance.destroy) {
resource.instance.destroy();
}
if (resource.element) {
resource.element.removeAttribute('data-tooltip-trigger-id');
resource.element.removeAttribute('aria-describedby');
}
} catch (e) {
console.warn('Error destroying tooltip during cleanup:', e);
}
}
);
return tippyInstance[0];
}
return null;
} catch (error) {
console.error('Error creating tooltip:', error);
return null;
}
}
destroy(element) {
if (!element) return;
try {
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
if (!tooltipId) return;
const tooltipData = this.tooltipData.get(element);
const instance = tooltipData?.instance || tooltipInstanceMap.get(element);
if (instance) {
try {
instance.destroy();
} catch (e) {
console.warn('Error destroying tooltip instance:', e);
}
}
element.removeAttribute('data-tooltip-trigger-id');
element.removeAttribute('aria-describedby');
const tippyRoot = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`);
if (tippyRoot && tippyRoot.parentNode) {
tippyRoot.parentNode.removeChild(tippyRoot);
}
this.tooltipData.delete(element);
tooltipInstanceMap.delete(element);
} catch (error) {
console.error('Error destroying tooltip:', error);
}
}
getActiveTooltipInstances() {
const result = [];
try {
document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => {
const instance = element._tippy ? [element._tippy] : null;
if (instance) {
result.push([element, instance]);
}
});
} catch (error) {
console.error('Error getting active tooltip instances:', error);
}
return result;
}
cleanup() {
this.log('Running tooltip cleanup');
try {
if ((window.location.pathname.includes('/offers') || window.location.pathname.includes('/bids')) &&
(document.querySelector('[data-tippy-root]:hover') || document.querySelector('[data-tooltip-trigger-id]:hover'))) {
console.log('Skipping tooltip cleanup - tooltip is being hovered');
return;
}
const elements = document.querySelectorAll('[data-tooltip-trigger-id]:not(:hover)');
const batchSize = 20;
const processElementsBatch = (startIdx) => {
const endIdx = Math.min(startIdx + batchSize, elements.length);
for (let i = startIdx; i < endIdx; i++) {
this.destroy(elements[i]);
}
if (endIdx < elements.length) {
CleanupManager.requestAnimationFrame(() => {
processElementsBatch(endIdx);
});
} else {
this.cleanupOrphanedTooltips();
}
};
if (elements.length > 0) {
processElementsBatch(0);
} else {
this.cleanupOrphanedTooltips();
}
} catch (error) {
console.error('Error during cleanup:', error);
}
}
thoroughCleanup() {
this.log('Running thorough tooltip cleanup');
try {
this.cleanup();
this.cleanupAllTooltips();
this.log('Thorough tooltip cleanup completed');
} catch (error) {
console.error('Error in thorough tooltip cleanup:', error);
}
}
cleanupAllTooltips() {
this.log('Cleaning up all tooltips');
try {
if ((window.location.pathname.includes('/offers') || window.location.pathname.includes('/bids')) &&
document.querySelector('#offers-body tr:hover')) {
this.log('Skipping all tooltips cleanup on offers/bids page with row hover');
return;
}
const tooltipRoots = document.querySelectorAll('[data-tippy-root]');
const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]');
const tooltipElements = document.querySelectorAll('.tooltip');
const isHovered = element => {
try {
return element.matches && element.matches(':hover');
} catch (e) {
return false;
}
};
tooltipRoots.forEach(root => {
if (!isHovered(root) && root.parentNode) {
root.parentNode.removeChild(root);
}
});
tooltipTriggers.forEach(trigger => {
if (!isHovered(trigger)) {
trigger.removeAttribute('data-tooltip-trigger-id');
trigger.removeAttribute('aria-describedby');
if (trigger._tippy) {
try {
trigger._tippy.destroy();
trigger._tippy = null;
} catch (e) {}
}
}
});
tooltipElements.forEach(tooltip => {
if (!isHovered(tooltip) && tooltip.parentNode) {
let closestHoveredRow = false;
try {
if (tooltip.closest && tooltip.closest('tr') && isHovered(tooltip.closest('tr'))) {
closestHoveredRow = true;
}
} catch (e) {}
if (!closestHoveredRow) {
const style = window.getComputedStyle(tooltip);
const isVisible = style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0';
if (!isVisible) {
tooltip.parentNode.removeChild(tooltip);
}
}
}
});
} catch (error) {
console.error('Error cleaning up all tooltips:', error);
}
}
cleanupOrphanedTooltips() {
try {
const tippyElements = document.querySelectorAll('[data-tippy-root]');
let removed = 0;
tippyElements.forEach(element => {
const tooltipId = element.getAttribute('data-for-tooltip-id');
const trigger = tooltipId ?
document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) :
null;
if (!trigger || !document.body.contains(trigger)) {
if (element.parentNode) {
element.parentNode.removeChild(element);
removed++;
}
}
});
if (removed > 0) {
this.log(`Removed ${removed} orphaned tooltip elements`);
}
return removed;
} catch (error) {
console.error('Error cleaning up orphaned tooltips:', error);
return 0;
}
}
setupMutationObserver() {
try {
const mutationObserver = new MutationObserver(mutations => {
let needsCleanup = false;
mutations.forEach(mutation => {
if (mutation.removedNodes.length) {
Array.from(mutation.removedNodes).forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) {
this.destroy(node);
needsCleanup = true;
}
if (node.querySelectorAll) {
const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]');
if (tooltipTriggers.length > 0) {
tooltipTriggers.forEach(trigger => {
this.destroy(trigger);
});
needsCleanup = true;
}
}
}
});
}
});
if (needsCleanup) {
this.cleanupOrphanedTooltips();
}
});
mutationObserver.observe(document.body, {
childList: true,
subtree: true
});
this.resources.mutationObserver = CleanupManager.registerResource(
'mutationObserver',
mutationObserver,
(observer) => observer.disconnect()
);
return mutationObserver;
} catch (error) {
console.error('Error setting up mutation observer:', error);
return null;
}
}
startDisconnectedElementsCheck() {
try {
this.resources.disconnectedCheckInterval = CleanupManager.setInterval(() => {
this.checkForDisconnectedElements();
}, 60000);
} catch (error) {
console.error('Error starting disconnected elements check:', error);
}
}
checkForDisconnectedElements() {
try {
const elements = document.querySelectorAll('[data-tooltip-trigger-id]');
let removedCount = 0;
elements.forEach(element => {
if (!document.body.contains(element)) {
this.destroy(element);
removedCount++;
}
});
if (removedCount > 0) {
this.log(`Removed ${removedCount} tooltips for disconnected elements`);
this.cleanupOrphanedTooltips();
}
} catch (error) {
console.error('Error checking for disconnected elements:', error);
}
}
startPeriodicCleanup() {
try {
this.resources.cleanupInterval = CleanupManager.setInterval(() => {
this.performPeriodicCleanup();
}, 120000);
} catch (error) {
console.error('Error starting periodic cleanup:', error);
}
}
performPeriodicCleanup(force = false) {
try {
if ((window.location.pathname.includes('/offers') || window.location.pathname.includes('/bids')) &&
!force) {
return;
}
this.cleanupOrphanedTooltips();
this.checkForDisconnectedElements();
const tooltipCount = document.querySelectorAll('[data-tippy-root]').length;
if (force || tooltipCount > this.maxTooltips) {
this.log(`Performing aggressive cleanup (${tooltipCount} tooltips)`);
this.cleanup();
}
} catch (error) {
console.error('Error performing periodic cleanup:', error);
}
}
setupStyles() {
if (document.getElementById('tooltip-styles')) return;
try {
const style = document.createElement('style');
style.id = 'tooltip-styles';
style.textContent = `
[data-tippy-root] {
position: fixed !important;
z-index: 9999 !important;
pointer-events: none !important;
}
.tippy-box {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
border-radius: 0.5rem;
color: white;
position: relative !important;
pointer-events: auto !important;
}
.tippy-content {
padding: 0.5rem 0.75rem !important;
}
.tippy-box .bg-gray-400 {
background-color: rgb(156 163 175);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-gray-400) .tippy-arrow {
color: rgb(156 163 175);
}
.tippy-box .bg-red-500 {
background-color: rgb(239 68 68);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-red-500) .tippy-arrow {
color: rgb(239 68 68);
}
.tippy-box .bg-gray-300 {
background-color: rgb(209 213 219);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-gray-300) .tippy-arrow {
color: rgb(209 213 219);
}
.tippy-box .bg-green-700 {
background-color: rgb(21 128 61);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-green-700) .tippy-arrow {
color: rgb(21 128 61);
}
.tippy-box[data-placement^='top'] > .tippy-arrow::before {
border-top-color: currentColor;
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: currentColor;
}
.tippy-box[data-placement^='left'] > .tippy-arrow::before {
border-left-color: currentColor;
}
.tippy-box[data-placement^='right'] > .tippy-arrow::before {
border-right-color: currentColor;
}
.tippy-box[data-placement^='top'] > .tippy-arrow {
bottom: 0;
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow {
top: 0;
}
.tippy-box[data-placement^='left'] > .tippy-arrow {
right: 0;
}
.tippy-box[data-placement^='right'] > .tippy-arrow {
left: 0;
}
`;
document.head.appendChild(style);
this.resources.tooltipStyles = CleanupManager.registerResource(
'tooltipStyles',
style,
(styleElement) => {
if (styleElement && styleElement.parentNode) {
styleElement.parentNode.removeChild(styleElement);
}
}
);
} catch (error) {
console.error('Error setting up styles:', error);
try {
document.head.insertAdjacentHTML('beforeend', `
<style id="tooltip-styles">
[data-tippy-root] {
position: fixed !important;
z-index: 9999 !important;
pointer-events: none !important;
}
.tippy-box {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
border-radius: 0.5rem;
color: white;
position: relative !important;
pointer-events: auto !important;
}
.tippy-content {
padding: 0.5rem 0.75rem !important;
}
.tippy-box .bg-gray-400 {
background-color: rgb(156 163 175);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-gray-400) .tippy-arrow {
color: rgb(156 163 175);
}
.tippy-box .bg-red-500 {
background-color: rgb(239 68 68);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-red-500) .tippy-arrow {
color: rgb(239 68 68);
}
.tippy-box .bg-gray-300 {
background-color: rgb(209 213 219);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-gray-300) .tippy-arrow {
color: rgb(209 213 219);
}
.tippy-box .bg-green-700 {
background-color: rgb(21 128 61);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-green-700) .tippy-arrow {
color: rgb(21 128 61);
}
.tippy-box[data-placement^='top'] > .tippy-arrow::before {
border-top-color: currentColor;
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: currentColor;
}
.tippy-box[data-placement^='left'] > .tippy-arrow::before {
border-left-color: currentColor;
}
.tippy-box[data-placement^='right'] > .tippy-arrow::before {
border-right-color: currentColor;
}
.tippy-box[data-placement^='top'] > .tippy-arrow {
bottom: 0;
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow {
top: 0;
}
.tippy-box[data-placement^='left'] > .tippy-arrow {
right: 0;
}
.tippy-box[data-placement^='right'] > .tippy-arrow {
left: 0;
}
</style>
`);
const styleElement = document.getElementById('tooltip-styles');
if (styleElement) {
this.resources.tooltipStyles = CleanupManager.registerResource(
'tooltipStyles',
styleElement,
(elem) => {
if (elem && elem.parentNode) {
elem.parentNode.removeChild(elem);
}
}
);
}
} catch (e) {
console.error('Failed to add tooltip styles:', e);
}
}
}
initializeTooltips(selector = '[data-tooltip-target]') {
try {
document.querySelectorAll(selector).forEach(element => {
const targetId = element.getAttribute('data-tooltip-target');
if (!targetId) return;
const tooltipContent = document.getElementById(targetId);
if (tooltipContent) {
this.create(element, tooltipContent.innerHTML, {
placement: element.getAttribute('data-tooltip-placement') || 'top'
});
}
});
} catch (error) {
console.error('Error initializing tooltips:', error);
}
}
dispose() {
this.log('Disposing TooltipManager');
try {
this.cleanup();
Object.values(this.resources).forEach(resourceId => {
if (resourceId) {
CleanupManager.unregisterResource(resourceId);
}
});
this.resources = {};
instance = null;
return true;
} catch (error) {
console.error('Error disposing TooltipManager:', error);
return false;
}
}
setDebugMode(enabled) {
this.debug = Boolean(enabled);
return this.debug;
}
initialize(options = {}) {
try {
if (options.maxTooltips) {
this.maxTooltips = options.maxTooltips;
}
if (options.debug !== undefined) {
this.setDebugMode(options.debug);
}
this.setupStyles();
this.setupMutationObserver();
this.startPeriodicCleanup();
this.startDisconnectedElementsCheck();
this.log('TooltipManager initialized');
return this;
} catch (error) {
console.error('Error initializing TooltipManager:', error);
return this;
}
}
}
return {
initialize: function(options = {}) {
if (!instance) {
const manager = new TooltipManagerImpl();
manager.initialize(options);
}
return instance;
},
getInstance: function() {
if (!instance) {
this.initialize();
}
return instance;
},
create: function(...args) {
const manager = this.getInstance();
return manager.create(...args);
},
destroy: function(...args) {
const manager = this.getInstance();
return manager.destroy(...args);
},
cleanup: function(...args) {
const manager = this.getInstance();
return manager.cleanup(...args);
},
thoroughCleanup: function() {
const manager = this.getInstance();
return manager.thoroughCleanup();
},
initializeTooltips: function(...args) {
const manager = this.getInstance();
return manager.initializeTooltips(...args);
},
setDebugMode: function(enabled) {
const manager = this.getInstance();
return manager.setDebugMode(enabled);
},
getActiveTooltipInstances: function() {
const manager = this.getInstance();
return manager.getActiveTooltipInstances();
},
dispose: function(...args) {
const manager = this.getInstance();
return manager.dispose(...args);
}
};
})();
if (typeof module !== 'undefined' && module.exports) {
module.exports = TooltipManager;
}
if (typeof window !== 'undefined') {
window.TooltipManager = TooltipManager;
}
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
function initializeTooltipManager() {
if (!window.tooltipManagerInitialized) {
if (!window.CleanupManager) {
console.warn('CleanupManager not found. TooltipManager will run with limited functionality.');
window.CleanupManager = window.CleanupManager || {
registerResource: (type, resource, cleanup) => {
return Math.random().toString(36).substring(2, 9);
},
unregisterResource: () => {},
setTimeout: (callback, delay) => setTimeout(callback, delay),
setInterval: (callback, delay) => setInterval(callback, delay),
requestAnimationFrame: (callback) => requestAnimationFrame(callback),
addListener: (element, type, handler, options) => {
element.addEventListener(type, handler, options);
return handler;
}
};
}
window.TooltipManager.initialize({
maxTooltips: 200,
debug: false
});
window.TooltipManager.initializeTooltips();
window.tooltipManagerInitialized = true;
}
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
initializeTooltipManager();
} else {
document.addEventListener('DOMContentLoaded', initializeTooltipManager, { once: true });
}
}
if (typeof window !== 'undefined' && typeof console !== 'undefined') {
console.log('TooltipManager initialized');
}

View File

@@ -1,623 +0,0 @@
const WalletManager = (function() {
const config = {
maxRetries: 5,
baseDelay: 500,
cacheExpiration: 5 * 60 * 1000,
priceUpdateInterval: 5 * 60 * 1000,
apiTimeout: 30000,
debounceDelay: 300,
cacheMinInterval: 60 * 1000,
defaultTTL: 300,
priceSource: {
primary: 'coingecko.com',
fallback: 'cryptocompare.com',
enabledSources: ['coingecko.com', 'cryptocompare.com']
}
};
const stateKeys = {
lastUpdate: 'last-update-time',
previousTotal: 'previous-total-usd',
currentTotal: 'current-total-usd',
balancesVisible: 'balancesVisible'
};
const state = {
lastFetchTime: 0,
toggleInProgress: false,
toggleDebounceTimer: null,
priceUpdateInterval: null,
lastUpdateTime: 0,
isWalletsPage: false,
initialized: false,
cacheKey: 'rates_crypto_prices'
};
function getShortName(fullName) {
return window.CoinManager.getSymbol(fullName) || fullName;
}
function getCoingeckoId(coinName) {
if (!window.CoinManager) {
console.warn('[WalletManager] CoinManager not available');
return coinName;
}
const coin = window.CoinManager.getCoinByAnyIdentifier(coinName);
if (!coin) {
console.warn(`[WalletManager] No coin found for: ${coinName}`);
return coinName;
}
return coin.symbol;
}
async function fetchPrices(forceUpdate = false) {
const now = Date.now();
const timeSinceLastFetch = now - state.lastFetchTime;
if (!forceUpdate && timeSinceLastFetch < config.cacheMinInterval) {
const cachedData = CacheManager.get(state.cacheKey);
if (cachedData) {
return cachedData.value;
}
}
let lastError = null;
for (let attempt = 0; attempt < config.maxRetries; attempt++) {
try {
const processedData = {};
const currentSource = config.priceSource.primary;
const shouldIncludeWow = currentSource === 'coingecko.com';
const coinsToFetch = [];
const processedCoins = new Set();
document.querySelectorAll('.coinname-value').forEach(el => {
const coinName = el.getAttribute('data-coinname');
if (!coinName || processedCoins.has(coinName)) return;
const adjustedName = coinName === 'Zcoin' ? 'Firo' :
coinName.includes('Particl') ? 'Particl' :
coinName;
const coinId = getCoingeckoId(adjustedName);
if (coinId && (shouldIncludeWow || coinId !== 'WOW')) {
coinsToFetch.push(coinId);
processedCoins.add(coinName);
}
});
const fetchCoinsString = coinsToFetch.join(',');
const mainResponse = await fetch("/json/coinprices", {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
coins: fetchCoinsString,
source: currentSource,
ttl: config.defaultTTL
})
});
if (!mainResponse.ok) {
throw new Error(`HTTP error: ${mainResponse.status}`);
}
const mainData = await mainResponse.json();
if (mainData && mainData.rates) {
document.querySelectorAll('.coinname-value').forEach(el => {
const coinName = el.getAttribute('data-coinname');
if (!coinName) return;
const adjustedName = coinName === 'Zcoin' ? 'Firo' :
coinName.includes('Particl') ? 'Particl' :
coinName;
const coinId = getCoingeckoId(adjustedName);
const price = mainData.rates[coinId];
if (price) {
const coinKey = coinName.toLowerCase().replace(' ', '-');
processedData[coinKey] = {
usd: price,
btc: coinId === 'BTC' ? 1 : price / (mainData.rates.BTC || 1)
};
}
});
}
CacheManager.set(state.cacheKey, processedData, config.cacheExpiration);
state.lastFetchTime = now;
return processedData;
} catch (error) {
lastError = error;
console.error(`Price fetch attempt ${attempt + 1} failed:`, error);
if (attempt === config.maxRetries - 1 &&
config.priceSource.fallback &&
config.priceSource.fallback !== config.priceSource.primary) {
const temp = config.priceSource.primary;
config.priceSource.primary = config.priceSource.fallback;
config.priceSource.fallback = temp;
console.warn(`Switching to fallback source: ${config.priceSource.primary}`);
attempt = -1;
continue;
}
if (attempt < config.maxRetries - 1) {
const delay = Math.min(config.baseDelay * Math.pow(2, attempt), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
const cachedData = CacheManager.get(state.cacheKey);
if (cachedData) {
console.warn('Using cached data after fetch failures');
return cachedData.value;
}
throw lastError || new Error('Failed to fetch prices');
}
function storeOriginalValues() {
document.querySelectorAll('.coinname-value').forEach(el => {
const coinName = el.getAttribute('data-coinname');
const value = el.textContent?.trim() || '';
if (coinName) {
const amount = value ? parseFloat(value.replace(/[^0-9.-]+/g, '')) : 0;
const coinSymbol = window.CoinManager.getSymbol(coinName);
const shortName = getShortName(coinName);
if (coinSymbol) {
if (coinName === 'Particl') {
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
} else if (coinName === 'Litecoin') {
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('MWEB');
const balanceType = isMWEB ? 'mweb' : 'public';
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
} else {
localStorage.setItem(`${coinSymbol.toLowerCase()}-amount`, amount.toString());
}
el.setAttribute('data-original-value', `${amount} ${shortName}`);
}
}
});
document.querySelectorAll('.usd-value').forEach(el => {
const text = el.textContent?.trim() || '';
if (text === 'Loading...') {
el.textContent = '';
}
});
}
async function updatePrices(forceUpdate = false) {
try {
const prices = await fetchPrices(forceUpdate);
let newTotal = 0;
const currentTime = Date.now();
localStorage.setItem(stateKeys.lastUpdate, currentTime.toString());
state.lastUpdateTime = currentTime;
if (prices) {
Object.entries(prices).forEach(([coinId, priceData]) => {
if (priceData?.usd) {
localStorage.setItem(`${coinId}-price`, priceData.usd.toString());
}
});
}
document.querySelectorAll('.coinname-value').forEach(el => {
const coinName = el.getAttribute('data-coinname');
const amountStr = el.getAttribute('data-original-value') || el.textContent?.trim() || '';
if (!coinName) return;
let amount = 0;
if (amountStr) {
const matches = amountStr.match(/([0-9]*[.])?[0-9]+/);
if (matches && matches.length > 0) {
amount = parseFloat(matches[0]);
}
}
const coinId = coinName.toLowerCase().replace(' ', '-');
if (!prices[coinId]) {
return;
}
const price = prices[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
if (!price) return;
const usdValue = (amount * price).toFixed(2);
if (coinName === 'Particl') {
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
localStorage.setItem(`particl-${balanceType}-last-value`, usdValue);
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
} else if (coinName === 'Litecoin') {
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('MWEB');
const balanceType = isMWEB ? 'mweb' : 'public';
localStorage.setItem(`litecoin-${balanceType}-last-value`, usdValue);
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
} else {
localStorage.setItem(`${coinId}-last-value`, usdValue);
localStorage.setItem(`${coinId}-amount`, amount.toString());
}
if (amount > 0) {
newTotal += parseFloat(usdValue);
}
let usdEl = null;
const flexContainer = el.closest('.flex');
if (flexContainer) {
const nextFlex = flexContainer.nextElementSibling;
if (nextFlex) {
const usdInNextFlex = nextFlex.querySelector('.usd-value');
if (usdInNextFlex) {
usdEl = usdInNextFlex;
}
}
}
if (!usdEl) {
const parentCell = el.closest('td');
if (parentCell) {
const usdInSameCell = parentCell.querySelector('.usd-value');
if (usdInSameCell) {
usdEl = usdInSameCell;
}
}
}
if (!usdEl) {
const sibling = el.nextElementSibling;
if (sibling && sibling.classList.contains('usd-value')) {
usdEl = sibling;
}
}
if (!usdEl) {
const parentElement = el.parentElement;
if (parentElement) {
const usdElNearby = parentElement.querySelector('.usd-value');
if (usdElNearby) {
usdEl = usdElNearby;
}
}
}
if (usdEl) {
usdEl.textContent = `$${usdValue}`;
usdEl.setAttribute('data-original-value', usdValue);
}
});
document.querySelectorAll('.usd-value').forEach(el => {
if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
const parentCell = el.closest('td');
if (!parentCell) return;
const coinValueEl = parentCell.querySelector('.coinname-value');
if (!coinValueEl) return;
const coinName = coinValueEl.getAttribute('data-coinname');
if (!coinName) return;
const amountStr = coinValueEl.textContent?.trim() || '0';
const amount = parseFloat(amountStr) || 0;
const coinId = coinName.toLowerCase().replace(' ', '-');
if (!prices[coinId]) return;
const price = prices[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
if (!price) return;
const usdValue = (amount * price).toFixed(8);
el.textContent = `$${usdValue}`;
el.setAttribute('data-original-value', usdValue);
}
});
if (state.isWalletsPage) {
updateTotalValues(newTotal, prices?.bitcoin?.usd);
}
localStorage.setItem(stateKeys.previousTotal, localStorage.getItem(stateKeys.currentTotal) || '0');
localStorage.setItem(stateKeys.currentTotal, newTotal.toString());
return true;
} catch (error) {
console.error('Price update failed:', error);
return false;
}
}
function updateTotalValues(totalUsd, btcPrice) {
const totalUsdEl = document.getElementById('total-usd-value');
if (totalUsdEl) {
totalUsdEl.textContent = `$${totalUsd.toFixed(2)}`;
totalUsdEl.setAttribute('data-original-value', totalUsd.toString());
localStorage.setItem('total-usd', totalUsd.toString());
}
if (btcPrice) {
const btcTotal = btcPrice ? totalUsd / btcPrice : 0;
const totalBtcEl = document.getElementById('total-btc-value');
if (totalBtcEl) {
totalBtcEl.textContent = `~ ${btcTotal.toFixed(8)} BTC`;
totalBtcEl.setAttribute('data-original-value', btcTotal.toString());
}
}
}
async function toggleBalances() {
if (state.toggleInProgress) return;
try {
state.toggleInProgress = true;
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
const newVisibility = !balancesVisible;
localStorage.setItem('balancesVisible', newVisibility.toString());
updateVisibility(newVisibility);
if (state.toggleDebounceTimer) {
clearTimeout(state.toggleDebounceTimer);
}
state.toggleDebounceTimer = window.setTimeout(async () => {
state.toggleInProgress = false;
if (newVisibility) {
await updatePrices(true);
}
}, config.debounceDelay);
} catch (error) {
console.error('Failed to toggle balances:', error);
state.toggleInProgress = false;
}
}
function updateVisibility(isVisible) {
if (isVisible) {
showBalances();
} else {
hideBalances();
}
const eyeIcon = document.querySelector("#hide-usd-amount-toggle svg");
if (eyeIcon) {
eyeIcon.innerHTML = isVisible ?
'<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z"></path>' :
'<path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z"></path><path d="M12,3C6.292,3,2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z"></path><path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path>';
}
}
function showBalances() {
const usdText = document.getElementById('usd-text');
if (usdText) {
usdText.style.display = 'inline';
}
document.querySelectorAll('.coinname-value').forEach(el => {
const originalValue = el.getAttribute('data-original-value');
if (originalValue) {
el.textContent = originalValue;
}
});
document.querySelectorAll('.usd-value').forEach(el => {
const storedValue = el.getAttribute('data-original-value');
if (storedValue !== null && storedValue !== undefined) {
if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
el.textContent = `$${parseFloat(storedValue).toFixed(8)}`;
} else {
el.textContent = `$${parseFloat(storedValue).toFixed(2)}`;
}
} else {
if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
el.textContent = '$0.00000000';
} else {
el.textContent = '$0.00';
}
}
});
if (state.isWalletsPage) {
['total-usd-value', 'total-btc-value'].forEach(id => {
const el = document.getElementById(id);
const originalValue = el?.getAttribute('data-original-value');
if (el && originalValue) {
if (id === 'total-usd-value') {
el.textContent = `$${parseFloat(originalValue).toFixed(2)}`;
el.classList.add('font-extrabold');
} else {
el.textContent = `~ ${parseFloat(originalValue).toFixed(8)} BTC`;
}
}
});
}
}
function hideBalances() {
const usdText = document.getElementById('usd-text');
if (usdText) {
usdText.style.display = 'none';
}
document.querySelectorAll('.coinname-value').forEach(el => {
el.textContent = '****';
});
document.querySelectorAll('.usd-value').forEach(el => {
el.textContent = '****';
});
if (state.isWalletsPage) {
['total-usd-value', 'total-btc-value'].forEach(id => {
const el = document.getElementById(id);
if (el) {
el.textContent = '****';
}
});
const totalUsdEl = document.getElementById('total-usd-value');
if (totalUsdEl) {
totalUsdEl.classList.remove('font-extrabold');
}
}
}
async function loadBalanceVisibility() {
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
updateVisibility(balancesVisible);
if (balancesVisible) {
await updatePrices(true);
}
}
// Public API
const publicAPI = {
initialize: async function(options) {
if (state.initialized) {
console.warn('[WalletManager] Already initialized');
return this;
}
if (options) {
Object.assign(config, options);
}
state.lastUpdateTime = parseInt(localStorage.getItem(stateKeys.lastUpdate) || '0');
state.isWalletsPage = document.querySelector('.wallet-list') !== null ||
window.location.pathname.includes('/wallets');
document.querySelectorAll('.usd-value').forEach(el => {
const text = el.textContent?.trim() || '';
if (text === 'Loading...') {
el.textContent = '';
}
});
storeOriginalValues();
if (localStorage.getItem('balancesVisible') === null) {
localStorage.setItem('balancesVisible', 'true');
}
const hideBalancesToggle = document.getElementById('hide-usd-amount-toggle');
if (hideBalancesToggle) {
hideBalancesToggle.addEventListener('click', toggleBalances);
}
await loadBalanceVisibility();
if (state.priceUpdateInterval) {
clearInterval(state.priceUpdateInterval);
}
state.priceUpdateInterval = setInterval(() => {
if (localStorage.getItem('balancesVisible') === 'true' && !state.toggleInProgress) {
updatePrices(false);
}
}, config.priceUpdateInterval);
if (window.CleanupManager) {
window.CleanupManager.registerResource('walletManager', this, (mgr) => mgr.dispose());
}
state.initialized = true;
console.log('WalletManager initialized');
return this;
},
updatePrices: function(forceUpdate = false) {
return updatePrices(forceUpdate);
},
toggleBalances: function() {
return toggleBalances();
},
setPriceSource: function(primarySource, fallbackSource = null) {
if (!config.priceSource.enabledSources.includes(primarySource)) {
throw new Error(`Invalid primary source: ${primarySource}`);
}
if (fallbackSource && !config.priceSource.enabledSources.includes(fallbackSource)) {
throw new Error(`Invalid fallback source: ${fallbackSource}`);
}
config.priceSource.primary = primarySource;
if (fallbackSource) {
config.priceSource.fallback = fallbackSource;
}
return this;
},
getConfig: function() {
return { ...config };
},
getState: function() {
return {
initialized: state.initialized,
lastUpdateTime: state.lastUpdateTime,
isWalletsPage: state.isWalletsPage,
balancesVisible: localStorage.getItem('balancesVisible') === 'true'
};
},
dispose: function() {
if (state.priceUpdateInterval) {
clearInterval(state.priceUpdateInterval);
state.priceUpdateInterval = null;
}
if (state.toggleDebounceTimer) {
clearTimeout(state.toggleDebounceTimer);
state.toggleDebounceTimer = null;
}
state.initialized = false;
console.log('WalletManager disposed');
}
};
return publicAPI;
})();
window.WalletManager = WalletManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.walletManagerInitialized) {
WalletManager.initialize();
window.walletManagerInitialized = true;
}
});
//console.log('WalletManager initialized with methods:', Object.keys(WalletManager));
console.log('WalletManager initialized');

View File

@@ -1,446 +0,0 @@
const WebSocketManager = (function() {
let ws = null;
const config = {
reconnectAttempts: 0,
maxReconnectAttempts: 5,
reconnectDelay: 5000,
debug: false
};
const state = {
isConnecting: false,
isIntentionallyClosed: false,
lastConnectAttempt: null,
connectTimeout: null,
lastHealthCheck: null,
healthCheckInterval: null,
isPageHidden: document.hidden,
messageHandlers: {},
listeners: {},
reconnectTimeout: null
};
function log(message, ...args) {
if (config.debug) {
console.log(`[WebSocketManager] ${message}`, ...args);
}
}
function generateHandlerId() {
return `handler_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
}
function determineWebSocketPort() {
let wsPort;
if (window.config && window.config.wsPort) {
wsPort = window.config.wsPort;
return wsPort;
}
if (window.ws_port) {
wsPort = window.ws_port.toString();
return wsPort;
}
if (typeof getWebSocketConfig === 'function') {
const wsConfig = getWebSocketConfig();
wsPort = (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
return wsPort;
}
wsPort = '11700';
return wsPort;
}
const publicAPI = {
initialize: function(options = {}) {
Object.assign(config, options);
setupPageVisibilityHandler();
this.connect();
startHealthCheck();
log('WebSocketManager initialized with options:', options);
if (window.CleanupManager) {
window.CleanupManager.registerResource('webSocketManager', this, (mgr) => mgr.dispose());
}
return this;
},
connect: function() {
if (state.isConnecting || state.isIntentionallyClosed) {
log('Connection attempt blocked - already connecting or intentionally closed');
return false;
}
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
state.reconnectTimeout = null;
}
cleanup();
state.isConnecting = true;
state.lastConnectAttempt = Date.now();
try {
const wsPort = determineWebSocketPort();
if (!wsPort) {
state.isConnecting = false;
return false;
}
ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
setupEventHandlers();
state.connectTimeout = setTimeout(() => {
if (state.isConnecting) {
log('Connection timeout, cleaning up');
cleanup();
handleReconnect();
}
}, 5000);
return true;
} catch (error) {
log('Error during connection attempt:', error);
state.isConnecting = false;
handleReconnect();
return false;
}
},
disconnect: function() {
log('Disconnecting WebSocket');
state.isIntentionallyClosed = true;
cleanup();
stopHealthCheck();
},
isConnected: function() {
return ws && ws.readyState === WebSocket.OPEN;
},
sendMessage: function(message) {
if (!this.isConnected()) {
log('Cannot send message - not connected');
return false;
}
try {
ws.send(JSON.stringify(message));
return true;
} catch (error) {
log('Error sending message:', error);
return false;
}
},
addMessageHandler: function(type, handler) {
if (!state.messageHandlers[type]) {
state.messageHandlers[type] = {};
}
const handlerId = generateHandlerId();
state.messageHandlers[type][handlerId] = handler;
return handlerId;
},
removeMessageHandler: function(type, handlerId) {
if (state.messageHandlers[type] && state.messageHandlers[type][handlerId]) {
delete state.messageHandlers[type][handlerId];
}
},
cleanup: function() {
log('Cleaning up WebSocket resources');
clearTimeout(state.connectTimeout);
stopHealthCheck();
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
state.reconnectTimeout = null;
}
state.isConnecting = false;
state.messageHandlers = {};
if (ws) {
ws.onopen = null;
ws.onmessage = null;
ws.onerror = null;
ws.onclose = null;
if (ws.readyState === WebSocket.OPEN) {
ws.close(1000, 'Cleanup');
}
ws = null;
window.ws = null;
}
},
dispose: function() {
log('Disposing WebSocketManager');
this.disconnect();
if (state.listeners.visibilityChange) {
document.removeEventListener('visibilitychange', state.listeners.visibilityChange);
}
state.messageHandlers = {};
state.listeners = {};
},
pause: function() {
log('WebSocketManager paused');
state.isIntentionallyClosed = true;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close(1000, 'WebSocketManager paused');
}
stopHealthCheck();
},
resume: function() {
log('WebSocketManager resumed');
state.isIntentionallyClosed = false;
if (!this.isConnected()) {
this.connect();
}
startHealthCheck();
}
};
function setupEventHandlers() {
if (!ws) return;
ws.onopen = () => {
state.isConnecting = false;
config.reconnectAttempts = 0;
clearTimeout(state.connectTimeout);
state.lastHealthCheck = Date.now();
window.ws = ws;
log('WebSocket connection established');
notifyHandlers('connect', { isConnected: true });
if (typeof updateConnectionStatus === 'function') {
updateConnectionStatus('connected');
}
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
log('WebSocket message received:', message);
notifyHandlers('message', message);
} catch (error) {
log('Error processing message:', error);
if (typeof updateConnectionStatus === 'function') {
updateConnectionStatus('error');
}
}
};
ws.onerror = (error) => {
log('WebSocket error:', error);
if (typeof updateConnectionStatus === 'function') {
updateConnectionStatus('error');
}
notifyHandlers('error', error);
};
ws.onclose = (event) => {
log('WebSocket closed:', event);
state.isConnecting = false;
window.ws = null;
if (typeof updateConnectionStatus === 'function') {
updateConnectionStatus('disconnected');
}
notifyHandlers('disconnect', {
code: event.code,
reason: event.reason
});
if (!state.isIntentionallyClosed) {
handleReconnect();
}
};
}
function setupPageVisibilityHandler() {
const visibilityChangeHandler = () => {
if (document.hidden) {
handlePageHidden();
} else {
handlePageVisible();
}
};
document.addEventListener('visibilitychange', visibilityChangeHandler);
state.listeners.visibilityChange = visibilityChangeHandler;
}
function handlePageHidden() {
log('Page hidden');
state.isPageHidden = true;
stopHealthCheck();
if (ws && ws.readyState === WebSocket.OPEN) {
state.isIntentionallyClosed = true;
ws.close(1000, 'Page hidden');
}
}
function handlePageVisible() {
log('Page visible');
state.isPageHidden = false;
state.isIntentionallyClosed = false;
setTimeout(() => {
if (!publicAPI.isConnected()) {
publicAPI.connect();
}
startHealthCheck();
}, 0);
}
function startHealthCheck() {
stopHealthCheck();
state.healthCheckInterval = setInterval(() => {
performHealthCheck();
}, 30000);
}
function stopHealthCheck() {
if (state.healthCheckInterval) {
clearInterval(state.healthCheckInterval);
state.healthCheckInterval = null;
}
}
function performHealthCheck() {
if (!publicAPI.isConnected()) {
log('Health check failed - not connected');
handleReconnect();
return;
}
const now = Date.now();
const lastCheck = state.lastHealthCheck;
if (lastCheck && (now - lastCheck) > 60000) {
log('Health check failed - too long since last check');
handleReconnect();
return;
}
state.lastHealthCheck = now;
log('Health check passed');
}
function handleReconnect() {
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
state.reconnectTimeout = null;
}
config.reconnectAttempts++;
if (config.reconnectAttempts <= config.maxReconnectAttempts) {
const delay = Math.min(
config.reconnectDelay * Math.pow(1.5, config.reconnectAttempts - 1),
30000
);
log(`Scheduling reconnect in ${delay}ms (attempt ${config.reconnectAttempts})`);
state.reconnectTimeout = setTimeout(() => {
state.reconnectTimeout = null;
if (!state.isIntentionallyClosed) {
publicAPI.connect();
}
}, delay);
} else {
log('Max reconnect attempts reached');
if (typeof updateConnectionStatus === 'function') {
updateConnectionStatus('error');
}
state.reconnectTimeout = setTimeout(() => {
state.reconnectTimeout = null;
config.reconnectAttempts = 0;
publicAPI.connect();
}, 60000);
}
}
function notifyHandlers(type, data) {
if (state.messageHandlers[type]) {
Object.values(state.messageHandlers[type]).forEach(handler => {
try {
handler(data);
} catch (error) {
log(`Error in ${type} handler:`, error);
}
});
}
}
function cleanup() {
log('Cleaning up WebSocket resources');
clearTimeout(state.connectTimeout);
stopHealthCheck();
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
state.reconnectTimeout = null;
}
state.isConnecting = false;
if (ws) {
ws.onopen = null;
ws.onmessage = null;
ws.onerror = null;
ws.onclose = null;
if (ws.readyState === WebSocket.OPEN) {
ws.close(1000, 'Cleanup');
}
ws = null;
window.ws = null;
}
}
return publicAPI;
})();
window.WebSocketManager = WebSocketManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.webSocketManagerInitialized) {
window.WebSocketManager.initialize();
window.webSocketManagerInitialized = true;
}
});
//console.log('WebSocketManager initialized with methods:', Object.keys(WebSocketManager));
console.log('WebSocketManager initialized');

View File

@@ -1,546 +1,59 @@
const DOM = {
get: (id) => document.getElementById(id),
getValue: (id) => {
const el = document.getElementById(id);
return el ? el.value : '';
},
setValue: (id, value) => {
const el = document.getElementById(id);
if (el) el.value = value;
},
addEvent: (id, event, handler) => {
const el = document.getElementById(id);
if (el) el.addEventListener(event, handler);
},
query: (selector) => document.querySelector(selector),
queryAll: (selector) => document.querySelectorAll(selector)
};
const Storage = {
get: (key) => {
try {
return JSON.parse(localStorage.getItem(key));
} catch(e) {
console.warn(`Failed to retrieve item from storage: ${key}`, e);
return null;
window.addEventListener('DOMContentLoaded', () => {
const err_msgs = document.querySelectorAll('p.error_msg');
for (let i = 0; i < err_msgs.length; i++) {
err_msg = err_msgs[i].innerText;
if (err_msg.indexOf('coin_to') >= 0 || err_msg.indexOf('Coin To') >= 0) {
e = document.getElementById('coin_to');
e.classList.add('error');
}
},
set: (key, value) => {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch(e) {
console.error(`Failed to save item to storage: ${key}`, e);
return false;
if (err_msg.indexOf('Coin From') >= 0) {
e = document.getElementById('coin_from');
e.classList.add('error');
}
},
setRaw: (key, value) => {
try {
localStorage.setItem(key, value);
return true;
} catch(e) {
console.error(`Failed to save raw item to storage: ${key}`, e);
return false;
if (err_msg.indexOf('Amount From') >= 0) {
e = document.getElementById('amt_from');
e.classList.add('error');
}
},
getRaw: (key) => {
try {
return localStorage.getItem(key);
} catch(e) {
console.warn(`Failed to retrieve raw item from storage: ${key}`, e);
return null;
if (err_msg.indexOf('Amount To') >= 0) {
e = document.getElementById('amt_to');
e.classList.add('error');
}
if (err_msg.indexOf('Minimum Bid Amount') >= 0) {
e = document.getElementById('amt_bid_min');
e.classList.add('error');
}
};
const Ajax = {
post: (url, data, onSuccess, onError) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState !== XMLHttpRequest.DONE) return;
if (xhr.status === 200) {
if (onSuccess) {
try {
const response = xhr.responseText.startsWith('{') ?
JSON.parse(xhr.responseText) : xhr.responseText;
onSuccess(response);
} catch (e) {
console.error('Failed to parse response:', e);
if (onError) onError('Invalid response format');
}
}
} else {
console.error('Request failed:', xhr.statusText);
if (onError) onError(xhr.statusText);
}
};
xhr.open('POST', url);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send(data);
return xhr;
}
};
function handleNewOfferAddress() {
const STORAGE_KEY = 'lastUsedAddressNewOffer';
const selectElement = DOM.query('select[name="addr_from"]');
const form = selectElement?.closest('form');
if (!selectElement || !form) return;
function loadInitialAddress() {
const savedAddress = Storage.get(STORAGE_KEY);
if (savedAddress) {
try {
selectElement.value = savedAddress.value;
} catch (e) {
selectFirstAddress();
}
} else {
selectFirstAddress();
if (err_msg.indexOf('Select coin you send') >= 0) {
e = document.getElementById('coin_from').parentNode;
e.classList.add('error');
}
}
function selectFirstAddress() {
if (selectElement.options.length > 1) {
const firstOption = selectElement.options[1];
if (firstOption) {
selectElement.value = firstOption.value;
saveAddress(firstOption.value, firstOption.text);
}
}
}
function saveAddress(value, text) {
Storage.set(STORAGE_KEY, { value, text });
}
form.addEventListener('submit', () => {
saveAddress(selectElement.value, selectElement.selectedOptions[0].text);
});
selectElement.addEventListener('change', (event) => {
saveAddress(event.target.value, event.target.selectedOptions[0].text);
});
loadInitialAddress();
}
const RateManager = {
lookupRates: () => {
const coinFrom = DOM.getValue('coin_from');
const coinTo = DOM.getValue('coin_to');
const ratesDisplay = DOM.get('rates_display');
if (!coinFrom || !coinTo || !ratesDisplay) {
console.log('Required elements for lookup_rates not found');
return;
}
if (coinFrom === '-1' || coinTo === '-1') {
alert('Coins from and to must be set first.');
return;
}
const selectedCoin = (coinFrom === '15') ? '3' : coinFrom;
ratesDisplay.innerHTML = '<p>Updating...</p>';
const priceJsonElement = DOM.query(".pricejsonhidden");
if (priceJsonElement) {
priceJsonElement.classList.remove("hidden");
}
const params = 'coin_from=' + selectedCoin + '&coin_to=' + coinTo;
Ajax.post('/json/rates', params,
(response) => {
if (ratesDisplay) {
ratesDisplay.innerHTML = typeof response === 'string' ?
response : '<pre><code>' + JSON.stringify(response, null, ' ') + '</code></pre>';
}
},
(error) => {
if (ratesDisplay) {
ratesDisplay.innerHTML = '<p>Error loading rates: ' + error + '</p>';
}
}
);
},
getRateInferred: (event) => {
if (event) event.preventDefault();
const coinFrom = DOM.getValue('coin_from');
const coinTo = DOM.getValue('coin_to');
const rateElement = DOM.get('rate');
if (!coinFrom || !coinTo || !rateElement) {
console.log('Required elements for getRateInferred not found');
return;
}
const params = 'coin_from=' + encodeURIComponent(coinFrom) +
'&coin_to=' + encodeURIComponent(coinTo);
DOM.setValue('rate', 'Loading...');
Ajax.post('/json/rates', params,
(response) => {
if (response.coingecko && response.coingecko.rate_inferred) {
DOM.setValue('rate', response.coingecko.rate_inferred);
RateManager.setRate('rate');
} else {
DOM.setValue('rate', 'Error: No rate available');
console.error('Rate not available in response');
}
},
(error) => {
DOM.setValue('rate', 'Error: Rate lookup failed');
console.error('Error fetching rate data:', error);
}
);
},
setRate: (valueChanged) => {
const elements = {
coinFrom: DOM.get('coin_from'),
coinTo: DOM.get('coin_to'),
amtFrom: DOM.get('amt_from'),
amtTo: DOM.get('amt_to'),
rate: DOM.get('rate'),
rateLock: DOM.get('rate_lock'),
swapType: DOM.get('swap_type')
};
if (!elements.coinFrom || !elements.coinTo ||
!elements.amtFrom || !elements.amtTo || !elements.rate) {
console.log('Required elements for setRate not found');
return;
}
const values = {
coinFrom: elements.coinFrom.value,
coinTo: elements.coinTo.value,
amtFrom: elements.amtFrom.value,
amtTo: elements.amtTo.value,
rate: elements.rate.value,
lockRate: elements.rate.value == '' ? false :
(elements.rateLock ? elements.rateLock.checked : false)
};
if (valueChanged === 'coin_from' || valueChanged === 'coin_to') {
DOM.setValue('rate', '');
return;
}
if (elements.swapType) {
SwapTypeManager.setSwapTypeEnabled(
values.coinFrom,
values.coinTo,
elements.swapType
);
}
if (values.coinFrom == '-1' || values.coinTo == '-1') {
return;
}
let params = 'coin_from=' + values.coinFrom + '&coin_to=' + values.coinTo;
if (valueChanged == 'rate' ||
(values.lockRate && valueChanged == 'amt_from') ||
(values.amtTo == '' && valueChanged == 'amt_from')) {
if (values.rate == '' || (values.amtFrom == '' && values.amtTo == '')) {
return;
} else if (values.amtFrom == '' && values.amtTo != '') {
if (valueChanged == 'amt_from') {
return;
}
params += '&rate=' + values.rate + '&amt_to=' + values.amtTo;
} else {
params += '&rate=' + values.rate + '&amt_from=' + values.amtFrom;
}
} else if (values.lockRate && valueChanged == 'amt_to') {
if (values.amtTo == '' || values.rate == '') {
return;
}
params += '&amt_to=' + values.amtTo + '&rate=' + values.rate;
} else {
if (values.amtFrom == '' || values.amtTo == '') {
return;
}
params += '&amt_from=' + values.amtFrom + '&amt_to=' + values.amtTo;
}
Ajax.post('/json/rate', params,
(response) => {
if (response.hasOwnProperty('rate')) {
DOM.setValue('rate', response.rate);
} else if (response.hasOwnProperty('amount_to')) {
DOM.setValue('amt_to', response.amount_to);
} else if (response.hasOwnProperty('amount_from')) {
DOM.setValue('amt_from', response.amount_from);
}
},
(error) => {
console.error('Rate calculation failed:', error);
}
);
}
};
function set_rate(valueChanged) {
RateManager.setRate(valueChanged);
}
function lookup_rates() {
RateManager.lookupRates();
}
function getRateInferred(event) {
RateManager.getRateInferred(event);
}
const SwapTypeManager = {
adaptor_sig_only_coins: ['6', '9', '8', '7', '13', '18', '17'],
secret_hash_only_coins: ['11', '12'],
setSwapTypeEnabled: (coinFrom, coinTo, swapTypeElement) => {
if (!swapTypeElement) return;
let makeHidden = false;
coinFrom = String(coinFrom);
coinTo = String(coinTo);
if (SwapTypeManager.adaptor_sig_only_coins.includes(coinFrom) ||
SwapTypeManager.adaptor_sig_only_coins.includes(coinTo)) {
swapTypeElement.disabled = true;
swapTypeElement.value = 'xmr_swap';
makeHidden = true;
swapTypeElement.classList.add('select-disabled');
} else if (SwapTypeManager.secret_hash_only_coins.includes(coinFrom) ||
SwapTypeManager.secret_hash_only_coins.includes(coinTo)) {
swapTypeElement.disabled = true;
swapTypeElement.value = 'seller_first';
makeHidden = true;
swapTypeElement.classList.add('select-disabled');
} else {
swapTypeElement.disabled = false;
swapTypeElement.classList.remove('select-disabled');
if (['xmr_swap', 'seller_first'].includes(swapTypeElement.value) == false) {
swapTypeElement.value = 'xmr_swap';
}
}
let swapTypeHidden = DOM.get('swap_type_hidden');
if (makeHidden) {
if (!swapTypeHidden) {
const form = DOM.get('form');
if (form) {
swapTypeHidden = document.createElement('input');
swapTypeHidden.setAttribute('id', 'swap_type_hidden');
swapTypeHidden.setAttribute('type', 'hidden');
swapTypeHidden.setAttribute('name', 'swap_type');
form.appendChild(swapTypeHidden);
}
}
if (swapTypeHidden) {
swapTypeHidden.setAttribute('value', swapTypeElement.value);
}
} else if (swapTypeHidden) {
swapTypeHidden.parentNode.removeChild(swapTypeHidden);
}
}
};
const UIEnhancer = {
handleErrorHighlighting: () => {
const errMsgs = document.querySelectorAll('p.error_msg');
const errorFieldMap = {
'coin_to': ['coin_to', 'Coin To'],
'coin_from': ['Coin From'],
'amt_from': ['Amount From'],
'amt_to': ['Amount To'],
'amt_bid_min': ['Minimum Bid Amount'],
'Select coin you send': ['coin_from', 'parentNode']
};
errMsgs.forEach(errMsg => {
const text = errMsg.innerText;
Object.entries(errorFieldMap).forEach(([field, keywords]) => {
if (keywords.some(keyword => text.includes(keyword))) {
let element = DOM.get(field);
if (field === 'Select coin you send' && element) {
element = element.parentNode;
}
if (element) {
element.classList.add('error');
}
}
});
});
document.querySelectorAll('input.error, select.error').forEach(element => {
element.addEventListener('focus', event => {
// remove error class on input or select focus
const inputs = document.querySelectorAll('input.error');
const selects = document.querySelectorAll('select.error');
const elements = [...inputs, ...selects];
elements.forEach((element) => {
element.addEventListener('focus', (event) => {
event.target.classList.remove('error');
});
});
},
});
updateDisabledStyles: () => {
document.querySelectorAll('select.disabled-select').forEach(select => {
const selects = document.querySelectorAll('select.disabled-select');
for (const select of selects) {
if (select.disabled) {
select.classList.add('disabled-select-enabled');
} else {
select.classList.remove('disabled-select-enabled');
}
});
}
document.querySelectorAll('input.disabled-input, input[type="checkbox"].disabled-input').forEach(input => {
const inputs = document.querySelectorAll('input.disabled-input, input[type="checkbox"].disabled-input');
for (const input of inputs) {
if (input.readOnly) {
input.classList.add('disabled-input-enabled');
} else {
input.classList.remove('disabled-input-enabled');
}
});
},
setupCustomSelects: () => {
const selectCache = {};
function updateSelectCache(select) {
if (!select || !select.options || select.selectedIndex === undefined) return;
const selectedOption = select.options[select.selectedIndex];
if (!selectedOption) return;
const image = selectedOption.getAttribute('data-image');
const name = selectedOption.textContent.trim();
selectCache[select.id] = { image, name };
}
function setSelectData(select) {
if (!select || !select.options || select.selectedIndex === undefined) return;
const selectedOption = select.options[select.selectedIndex];
if (!selectedOption) return;
const image = selectedOption.getAttribute('data-image') || '';
const name = selectedOption.textContent.trim();
select.style.backgroundImage = image ? `url(${image}?${new Date().getTime()})` : '';
const selectImage = select.nextElementSibling?.querySelector('.select-image');
if (selectImage) {
selectImage.src = image;
}
const selectNameElement = select.nextElementSibling?.querySelector('.select-name');
if (selectNameElement) {
selectNameElement.textContent = name;
}
updateSelectCache(select);
}
function setupCustomSelect(select) {
if (!select) return;
const options = select.querySelectorAll('option');
const selectIcon = select.parentElement?.querySelector('.select-icon');
const selectImage = select.parentElement?.querySelector('.select-image');
if (!options || !selectIcon || !selectImage) return;
options.forEach(option => {
const image = option.getAttribute('data-image');
if (image) {
option.style.backgroundImage = `url(${image})`;
}
});
const storedValue = Storage.getRaw(select.name);
if (storedValue && select.value == '-1') {
select.value = storedValue;
}
select.addEventListener('change', () => {
setSelectData(select);
Storage.setRaw(select.name, select.value);
});
setSelectData(select);
selectIcon.style.display = 'none';
selectImage.style.display = 'none';
}
const selectIcons = document.querySelectorAll('.custom-select .select-icon');
const selectImages = document.querySelectorAll('.custom-select .select-image');
const selectNames = document.querySelectorAll('.custom-select .select-name');
selectIcons.forEach(icon => icon.style.display = 'none');
selectImages.forEach(image => image.style.display = 'none');
selectNames.forEach(name => name.style.display = 'none');
const customSelects = document.querySelectorAll('.custom-select select');
customSelects.forEach(setupCustomSelect);
}
};
function initializeApp() {
handleNewOfferAddress();
DOM.addEvent('get_rate_inferred_button', 'click', RateManager.getRateInferred);
const coinFrom = DOM.get('coin_from');
const coinTo = DOM.get('coin_to');
const swapType = DOM.get('swap_type');
if (coinFrom && coinTo && swapType) {
SwapTypeManager.setSwapTypeEnabled(coinFrom.value, coinTo.value, swapType);
coinFrom.addEventListener('change', function() {
SwapTypeManager.setSwapTypeEnabled(this.value, coinTo.value, swapType);
RateManager.setRate('coin_from');
});
coinTo.addEventListener('change', function() {
SwapTypeManager.setSwapTypeEnabled(coinFrom.value, this.value, swapType);
RateManager.setRate('coin_to');
});
}
['amt_from', 'amt_to', 'rate'].forEach(id => {
DOM.addEvent(id, 'change', function() {
RateManager.setRate(id);
});
DOM.addEvent(id, 'input', function() {
RateManager.setRate(id);
});
});
DOM.addEvent('rate_lock', 'change', function() {
if (DOM.getValue('rate')) {
RateManager.setRate('rate');
}
});
DOM.addEvent('lookup_rates_button', 'click', RateManager.lookupRates);
UIEnhancer.handleErrorHighlighting();
UIEnhancer.updateDisabledStyles();
UIEnhancer.setupCustomSelects();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeApp);
} else {
initializeApp();
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,306 @@
class TooltipManager {
constructor() {
this.activeTooltips = new Map();
this.sizeCheckIntervals = new Map();
this.setupStyles();
this.setupCleanupEvents();
}
static initialize() {
if (!window.TooltipManager) {
window.TooltipManager = new TooltipManager();
}
return window.TooltipManager;
}
create(element, content, options = {}) {
if (!element) return null;
this.destroy(element);
const checkSize = () => {
const rect = element.getBoundingClientRect();
if (rect.width && rect.height) {
clearInterval(this.sizeCheckIntervals.get(element));
this.sizeCheckIntervals.delete(element);
this.createTooltip(element, content, options, rect);
}
};
this.sizeCheckIntervals.set(element, setInterval(checkSize, 50));
checkSize();
return null;
}
createTooltip(element, content, options, rect) {
const targetId = element.getAttribute('data-tooltip-target');
let bgClass = 'bg-gray-400';
let arrowColor = 'rgb(156 163 175)';
if (targetId?.includes('tooltip-offer-')) {
const offerId = targetId.split('tooltip-offer-')[1];
const [actualOfferId] = offerId.split('_');
if (window.jsonData) {
const offer = window.jsonData.find(o =>
o.unique_id === offerId ||
o.offer_id === actualOfferId
);
if (offer) {
if (offer.is_revoked) {
bgClass = 'bg-red-500';
arrowColor = 'rgb(239 68 68)';
} else if (offer.is_own_offer) {
bgClass = 'bg-gray-300';
arrowColor = 'rgb(209 213 219)';
} else {
bgClass = 'bg-green-700';
arrowColor = 'rgb(21 128 61)';
}
}
}
}
const instance = tippy(element, {
content,
allowHTML: true,
placement: options.placement || 'top',
appendTo: document.body,
animation: false,
duration: 0,
delay: 0,
interactive: true,
arrow: false,
theme: '',
moveTransition: 'none',
offset: [0, 10],
popperOptions: {
strategy: 'fixed',
modifiers: [
{
name: 'preventOverflow',
options: {
boundary: 'viewport',
padding: 10
}
},
{
name: 'flip',
options: {
padding: 10,
fallbackPlacements: ['top', 'bottom', 'right', 'left']
}
}
]
},
onCreate(instance) {
instance._originalPlacement = instance.props.placement;
},
onShow(instance) {
if (!document.body.contains(element)) {
return false;
}
const rect = element.getBoundingClientRect();
if (!rect.width || !rect.height) {
return false;
}
instance.setProps({
placement: instance._originalPlacement
});
if (instance.popper.firstElementChild) {
instance.popper.firstElementChild.classList.add(bgClass);
}
return true;
},
onMount(instance) {
if (instance.popper.firstElementChild) {
instance.popper.firstElementChild.classList.add(bgClass);
}
const arrow = instance.popper.querySelector('.tippy-arrow');
if (arrow) {
arrow.style.setProperty('color', arrowColor, 'important');
}
}
});
const id = element.getAttribute('data-tooltip-trigger-id') ||
`tooltip-${Math.random().toString(36).substring(7)}`;
element.setAttribute('data-tooltip-trigger-id', id);
this.activeTooltips.set(id, instance);
return instance;
}
destroy(element) {
if (!element) return;
if (this.sizeCheckIntervals.has(element)) {
clearInterval(this.sizeCheckIntervals.get(element));
this.sizeCheckIntervals.delete(element);
}
const id = element.getAttribute('data-tooltip-trigger-id');
if (!id) return;
const instance = this.activeTooltips.get(id);
if (instance?.[0]) {
try {
instance[0].destroy();
} catch (e) {
console.warn('Error destroying tooltip:', e);
}
}
this.activeTooltips.delete(id);
element.removeAttribute('data-tooltip-trigger-id');
}
cleanup() {
this.sizeCheckIntervals.forEach((interval) => clearInterval(interval));
this.sizeCheckIntervals.clear();
this.activeTooltips.forEach((instance, id) => {
if (instance?.[0]) {
try {
instance[0].destroy();
} catch (e) {
console.warn('Error cleaning up tooltip:', e);
}
}
});
this.activeTooltips.clear();
document.querySelectorAll('[data-tippy-root]').forEach(element => {
if (element.parentNode) {
element.parentNode.removeChild(element);
}
});
}
setupStyles() {
if (document.getElementById('tooltip-styles')) return;
document.head.insertAdjacentHTML('beforeend', `
<style id="tooltip-styles">
[data-tippy-root] {
position: fixed !important;
z-index: 9999 !important;
pointer-events: none !important;
}
.tippy-box {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 500;
border-radius: 0.5rem;
color: white;
position: relative !important;
pointer-events: auto !important;
}
.tippy-content {
padding: 0.5rem 0.75rem !important;
}
.tippy-box .bg-gray-400 {
background-color: rgb(156 163 175);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-gray-400) .tippy-arrow {
color: rgb(156 163 175);
}
.tippy-box .bg-red-500 {
background-color: rgb(239 68 68);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-red-500) .tippy-arrow {
color: rgb(239 68 68);
}
.tippy-box .bg-gray-300 {
background-color: rgb(209 213 219);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-gray-300) .tippy-arrow {
color: rgb(209 213 219);
}
.tippy-box .bg-green-700 {
background-color: rgb(21 128 61);
padding: 0.5rem 0.75rem;
}
.tippy-box:has(.bg-green-700) .tippy-arrow {
color: rgb(21 128 61);
}
.tippy-box[data-placement^='top'] > .tippy-arrow::before {
border-top-color: currentColor;
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: currentColor;
}
.tippy-box[data-placement^='left'] > .tippy-arrow::before {
border-left-color: currentColor;
}
.tippy-box[data-placement^='right'] > .tippy-arrow::before {
border-right-color: currentColor;
}
.tippy-box[data-placement^='top'] > .tippy-arrow {
bottom: 0;
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow {
top: 0;
}
.tippy-box[data-placement^='left'] > .tippy-arrow {
right: 0;
}
.tippy-box[data-placement^='right'] > .tippy-arrow {
left: 0;
}
</style>
`);
}
setupCleanupEvents() {
window.addEventListener('beforeunload', () => this.cleanup());
window.addEventListener('unload', () => this.cleanup());
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.cleanup();
}
});
}
initializeTooltips(selector = '[data-tooltip-target]') {
document.querySelectorAll(selector).forEach(element => {
const targetId = element.getAttribute('data-tooltip-target');
const tooltipContent = document.getElementById(targetId);
if (tooltipContent) {
this.create(element, tooltipContent.innerHTML, {
placement: element.getAttribute('data-tooltip-placement') || 'top'
});
}
});
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = TooltipManager;
}
document.addEventListener('DOMContentLoaded', () => {
TooltipManager.initialize();
});

View File

@@ -1,214 +0,0 @@
(function() {
'use strict';
const originalOnload = window.onload;
window.onload = function() {
if (typeof originalOnload === 'function') {
originalOnload();
}
setTimeout(function() {
initBidsTabNavigation();
handleInitialNavigation();
}, 100);
};
document.addEventListener('DOMContentLoaded', function() {
initBidsTabNavigation();
});
window.addEventListener('hashchange', handleHashChange);
window.bidsTabNavigationInitialized = false;
function initBidsTabNavigation() {
if (window.bidsTabNavigationInitialized) {
return;
}
document.querySelectorAll('.bids-tab-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const targetTabId = this.getAttribute('data-tab-target');
if (targetTabId) {
if (window.location.pathname === '/bids') {
navigateToTabDirectly(targetTabId);
} else {
localStorage.setItem('bidsTabToActivate', targetTabId.replace('#', ''));
window.location.href = '/bids';
}
}
});
});
window.bidsTabNavigationInitialized = true;
console.log('Bids tab navigation initialized');
}
function handleInitialNavigation() {
if (window.location.pathname !== '/bids') {
return;
}
const tabToActivate = localStorage.getItem('bidsTabToActivate');
if (tabToActivate) {
//console.log('Activating tab from localStorage:', tabToActivate);
localStorage.removeItem('bidsTabToActivate');
activateTabWithRetry('#' + tabToActivate);
} else if (window.location.hash) {
//console.log('Activating tab from hash:', window.location.hash);
activateTabWithRetry(window.location.hash);
} else {
//console.log('Activating default tab: #all');
activateTabWithRetry('#all');
}
}
function handleHashChange() {
if (window.location.pathname !== '/bids') {
return;
}
const hash = window.location.hash;
if (hash) {
//console.log('Hash changed, activating tab:', hash);
activateTabWithRetry(hash);
} else {
//console.log('Hash cleared, activating default tab: #all');
activateTabWithRetry('#all');
}
}
function activateTabWithRetry(tabId, retryCount = 0) {
const normalizedTabId = tabId.startsWith('#') ? tabId : '#' + tabId;
if (normalizedTabId !== '#all' && normalizedTabId !== '#sent' && normalizedTabId !== '#received') {
//console.log('Invalid tab ID, defaulting to #all');
activateTabWithRetry('#all');
return;
}
const tabButtonId = normalizedTabId === '#all' ? 'all-tab' :
(normalizedTabId === '#sent' ? 'sent-tab' : 'received-tab');
const tabButton = document.getElementById(tabButtonId);
if (!tabButton) {
if (retryCount < 5) {
//console.log('Tab button not found, retrying...', retryCount + 1);
setTimeout(() => {
activateTabWithRetry(normalizedTabId, retryCount + 1);
}, 100);
} else {
//console.error('Failed to find tab button after retries');
}
return;
}
//console.log('Activating tab:', normalizedTabId);
tabButton.click();
if (window.Tabs) {
const tabsEl = document.querySelector('[data-tabs-toggle="#bidstab"]');
if (tabsEl) {
const allTabs = Array.from(tabsEl.querySelectorAll('[role="tab"]'));
const targetTab = allTabs.find(tab => tab.getAttribute('data-tabs-target') === normalizedTabId);
if (targetTab) {
allTabs.forEach(tab => {
tab.setAttribute('aria-selected', tab === targetTab ? 'true' : 'false');
if (tab === targetTab) {
tab.classList.add('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
tab.classList.remove('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500');
} else {
tab.classList.remove('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
tab.classList.add('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500');
}
});
const allContent = document.getElementById('all');
const sentContent = document.getElementById('sent');
const receivedContent = document.getElementById('received');
if (allContent && sentContent && receivedContent) {
allContent.classList.toggle('hidden', normalizedTabId !== '#all');
sentContent.classList.toggle('hidden', normalizedTabId !== '#sent');
receivedContent.classList.toggle('hidden', normalizedTabId !== '#received');
}
}
}
}
const allPanel = document.getElementById('all');
const sentPanel = document.getElementById('sent');
const receivedPanel = document.getElementById('received');
if (allPanel && sentPanel && receivedPanel) {
allPanel.classList.toggle('hidden', normalizedTabId !== '#all');
sentPanel.classList.toggle('hidden', normalizedTabId !== '#sent');
receivedPanel.classList.toggle('hidden', normalizedTabId !== '#received');
}
const newHash = normalizedTabId.replace('#', '');
if (window.location.hash !== '#' + newHash) {
history.replaceState(null, null, '#' + newHash);
}
triggerDataLoad(normalizedTabId);
}
function triggerDataLoad(tabId) {
setTimeout(() => {
if (window.state) {
window.state.currentTab = tabId === '#all' ? 'all' :
(tabId === '#sent' ? 'sent' : 'received');
if (typeof window.updateBidsTable === 'function') {
//console.log('Triggering data load for', tabId);
window.updateBidsTable();
}
}
const event = new CustomEvent('tabactivated', {
detail: {
tabId: tabId,
type: tabId === '#all' ? 'all' :
(tabId === '#sent' ? 'sent' : 'received')
}
});
document.dispatchEvent(event);
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
setTimeout(() => {
window.TooltipManager.cleanup();
if (typeof window.initializeTooltips === 'function') {
window.initializeTooltips();
}
}, 200);
}
}, 100);
}
function navigateToTabDirectly(tabId) {
const oldScrollPosition = window.scrollY;
activateTabWithRetry(tabId);
setTimeout(function() {
window.scrollTo(0, oldScrollPosition);
}, 0);
}
window.navigateToBidsTab = function(tabId) {
if (window.location.pathname === '/bids') {
navigateToTabDirectly('#' + tabId);
} else {
localStorage.setItem('bidsTabToActivate', tabId);
window.location.href = '/bids';
}
};
})();

View File

@@ -44,7 +44,7 @@
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-left">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Receive</span>
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Send</span>
</div>
</th>
<th class="p-0">
@@ -54,7 +54,7 @@
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-right">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Send</span>
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Receive</span>
</div>
</th>
<th class="p-0">
@@ -113,6 +113,6 @@
</div>
</section>
<script src="/static/js/swaps_in_progress.js"></script>
<script src="/static/js/active.js"></script>
{% include 'footer.html' %}

File diff suppressed because it is too large Load Diff

View File

@@ -76,7 +76,7 @@
</th>
</tr>
</thead>
{% if data.was_sent %}
{% if data.was_sent == 'True' %}
<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">Swap</td>
<td class="py-3 px-6">
@@ -103,7 +103,7 @@
<td class="py-3 px-6 bold">Bid Rate</td>
<td class="py-3 px-6">{{ data.bid_rate }}</td>
</tr>
{% if data.was_sent %}
{% if data.was_sent == 'True' %}
<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">You Send</td>
<td class="py-3 px-6">
@@ -183,8 +183,12 @@
<td class="py-3 px-6">{{ data.expired_at }}</td>
</tr>
<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">Bid Type</td>
<td class="py-3 px-6" id="bidtype">{% if data.was_sent and data.was_received %}Self Bid{% elif data.was_sent %}Sent{% elif data.was_received %}Received{% endif %}</td>
<td class="py-3 px-6 bold">Sent</td>
<td class="py-3 px-6">{{ data.was_sent }}</td>
</tr>
<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">Received</td>
<td class="py-3 px-6">{{ data.was_received }}</td>
</tr>
<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">Initiate Tx</td>
@@ -528,18 +532,16 @@
<button name="show_txns" type="submit" value="Show More Info" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md shadow-button focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Show More Info </button>
</div>
{% endif %}
{% if debug_ui_mode == true %}
<div class="w-full md:w-auto p-1.5">
<button name="edit_bid" type="submit" value="Edit Bid" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md shadow-button focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Edit Bid</button>
</div>
{% endif %}
{% if data.can_abandon == true and not edit_bid %}
<div class="w-full md:w-auto p-1.5">
<button name="abandon_bid" type="submit" value="Abandon Bid" onclick="return confirmPopup();" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Abandon Bid</button>
</div>
{% endif %}
{% endif %}
{% endif %}
{% if data.was_received and not edit_bid and data.can_accept_bid %}
{% if data.was_received == 'True' and not edit_bid and data.can_accept_bid %}
<div class="w-full md:w-auto p-1.5">
<button name="accept_bid" value="Accept Bid" type="submit" onclick='return confirmPopup("Accept");' class="flex flex-wrap justify-center w-full 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">Accept Bid</button>
</div>
@@ -553,56 +555,6 @@
</div>
</div>
</section>
<div id="confirmModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
<div class="bg-white dark:bg-gray-500 rounded-lg max-w-2xl w-full p-6 shadow-lg transition-opacity duration-300 ease-out">
<div class="text-center">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6" id="confirmTitle">Confirm Action</h2>
<div id="bidDetailsSection" class="hidden space-y-4 text-left mb-8">
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">You will send:</div>
<div class="font-medium text-gray-900 dark:text-white text-lg" id="confirmSendAmount">
{% if data.was_sent %}
{{ data.amt_to }} {{ data.ticker_to }}
{% else %}
{{ data.amt_from }} {{ data.ticker_from }}
{% endif %}
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">You will receive:</div>
<div class="font-medium text-gray-900 dark:text-white text-lg" id="confirmReceiveAmount">
{% if data.was_sent %}
{{ data.amt_from }} {{ data.ticker_from }}
{% else %}
{{ data.amt_to }} {{ data.ticker_to }}
{% endif %}
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Exchange rate:</div>
<div class="font-medium text-gray-900 dark:text-white text-lg">{{ data.bid_rate }}</div>
</div>
</div>
<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">
<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">
Confirm
</button>
<button type="button" id="confirmNo"
class="px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
<input type="hidden" name="formid" value="{{ form_id }}">
</div>
</div>
@@ -610,93 +562,9 @@
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
let confirmCallback = null;
let triggerElement = null;
document.getElementById('confirmYes').addEventListener('click', function() {
if (typeof confirmCallback === 'function') {
confirmCallback();
function confirmPopup(name) {
return confirm(name + " Bid - Are you sure?");
}
hideConfirmDialog();
});
document.getElementById('confirmNo').addEventListener('click', hideConfirmDialog);
function showConfirmDialog(title, message, callback, showBidDetails = false) {
confirmCallback = callback;
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
const bidDetailsSection = document.getElementById('bidDetailsSection');
const confirmMessage = document.getElementById('confirmMessage');
if (showBidDetails && bidDetailsSection) {
bidDetailsSection.classList.remove('hidden');
confirmMessage.classList.add('hidden');
} else {
if (bidDetailsSection) bidDetailsSection.classList.add('hidden');
confirmMessage.classList.remove('hidden');
}
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
}
return false;
}
function hideConfirmDialog() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
}
confirmCallback = null;
return false;
}
window.confirmPopup = function(action = 'Abandon') {
triggerElement = document.activeElement;
const title = `Confirm ${action} Bid`;
const showBidDetails = action.toLowerCase() === 'accept';
let message;
if (showBidDetails) {
message = 'Please review the bid details below and confirm if you want to proceed with this exchange.';
} else {
message = `Are you sure you want to ${action.toLowerCase()} this bid?`;
}
return showConfirmDialog(title, message, function() {
if (triggerElement) {
const form = triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = triggerElement.name;
hiddenInput.value = triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}, showBidDetails);
};
const overrideButtonConfirm = function(button, action) {
if (button) {
button.removeAttribute('onclick');
button.addEventListener('click', function(e) {
e.preventDefault();
triggerElement = this;
return confirmPopup(action);
});
}
};
const abandonBidBtn = document.querySelector('button[name="abandon_bid"]');
overrideButtonConfirm(abandonBidBtn, 'Abandon');
const acceptBidBtn = document.querySelector('button[name="accept_bid"]');
overrideButtonConfirm(acceptBidBtn, 'Accept');
});
</script>
</div>
{% include 'footer.html' %}

View File

@@ -76,7 +76,7 @@
</th>
</tr>
</thead>
{% if data.was_sent %}
{% if data.was_sent == 'True' %}
<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">Swap</td>
<td class="py-3 px-6">
@@ -103,7 +103,7 @@
<td class="py-3 px-6 bold">Bid Rate</td>
<td class="py-3 px-6">{{ data.bid_rate }}</td>
</tr>
{% if data.was_sent %}
{% if data.was_sent == 'True' %}
<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">You Send</td>
<td class="py-3 px-6">
@@ -188,8 +188,12 @@
<td class="py-3 px-6">{{ data.expired_at }}</td>
</tr>
<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">Bid Type</td>
<td class="py-3 px-6" id="bidtype">{% if data.was_sent and data.was_received %}Self Bid{% elif data.was_sent %}Sent{% elif data.was_received %}Received{% endif %}{% if data.reverse_bid %} (Transposed){% endif %}</td>
<td class="py-3 px-6 bold">Sent</td>
<td class="py-3 px-6">{{ data.was_sent }}</td>
</tr>
<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">Received</td>
<td class="py-3 px-6">{{ data.was_received }}</td>
</tr>
{% if data.coin_a_lock_refund_tx_est_final != 'None' %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
@@ -804,18 +808,16 @@
<button name="show_txns" type="submit" value="Show More Info" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Show More Info </button>
</div>
{% endif %}
{% if debug_ui_mode == true %}
<div class="w-full md:w-auto p-1.5">
<button name="edit_bid" type="submit" value="Edit Bid" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">Edit Bid</button>
</div>
{% endif %}
{% if data.can_abandon == true %}
<div class="w-full md:w-auto p-1.5">
<button name="abandon_bid" type="submit" value="Abandon Bid" onclick="return confirmPopup();" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md focus:ring-0 focus:outline-none">Abandon Bid</button>
</div>
{% endif %}
{% endif %}
{% endif %}
{% if data.was_received and not edit_bid and data.can_accept_bid %}
{% if data.was_received == 'True' and not edit_bid and data.can_accept_bid %}
<div class="w-full md:w-auto p-1.5">
<button name="accept_bid" value="Accept Bid" type="submit" onclick='return confirmPopup("Accept");' class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">Accept Bid</button>
</div>
@@ -829,56 +831,6 @@
</div>
</div>
</section>
<div id="confirmModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
<div class="bg-white dark:bg-gray-500 rounded-lg max-w-2xl w-full p-6 shadow-lg transition-opacity duration-300 ease-out">
<div class="text-center">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6" id="confirmTitle">Confirm Action</h2>
<div id="bidDetailsSection" class="hidden space-y-4 text-left mb-8">
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">You will send:</div>
<div class="font-medium text-gray-900 dark:text-white text-lg" id="confirmSendAmount">
{% if data.was_sent %}
{{ data.amt_to }} {{ data.ticker_to }}
{% else %}
{{ data.amt_from }} {{ data.ticker_from }}
{% endif %}
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">You will receive:</div>
<div class="font-medium text-gray-900 dark:text-white text-lg" id="confirmReceiveAmount">
{% if data.was_sent %}
{{ data.amt_from }} {{ data.ticker_from }}
{% else %}
{{ data.amt_to }} {{ data.ticker_to }}
{% endif %}
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-4">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Exchange rate:</div>
<div class="font-medium text-gray-900 dark:text-white text-lg">{{ data.bid_rate }}</div>
</div>
</div>
<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">
<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">
Confirm
</button>
<button type="button" id="confirmNo"
class="px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
<input type="hidden" name="formid" value="{{ form_id }}">
</div>
</div>
@@ -886,93 +838,9 @@
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
let confirmCallback = null;
let triggerElement = null;
document.getElementById('confirmYes').addEventListener('click', function() {
if (typeof confirmCallback === 'function') {
confirmCallback();
function confirmPopup(name) {
return confirm(name + " Bid - Are you sure?");
}
hideConfirmDialog();
});
document.getElementById('confirmNo').addEventListener('click', hideConfirmDialog);
function showConfirmDialog(title, message, callback, showBidDetails = false) {
confirmCallback = callback;
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
const bidDetailsSection = document.getElementById('bidDetailsSection');
const confirmMessage = document.getElementById('confirmMessage');
if (showBidDetails && bidDetailsSection) {
bidDetailsSection.classList.remove('hidden');
confirmMessage.classList.add('hidden');
} else {
if (bidDetailsSection) bidDetailsSection.classList.add('hidden');
confirmMessage.classList.remove('hidden');
}
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
}
return false;
}
function hideConfirmDialog() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
}
confirmCallback = null;
return false;
}
window.confirmPopup = function(action = 'Abandon') {
triggerElement = document.activeElement;
const title = `Confirm ${action} Bid`;
const showBidDetails = action.toLowerCase() === 'accept';
let message;
if (showBidDetails) {
message = 'Please review the bid details below and confirm if you want to proceed with this exchange.';
} else {
message = `Are you sure you want to ${action.toLowerCase()} this bid?`;
}
return showConfirmDialog(title, message, function() {
if (triggerElement) {
const form = triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = triggerElement.name;
hiddenInput.value = triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}, showBidDetails);
};
const overrideButtonConfirm = function(button, action) {
if (button) {
button.removeAttribute('onclick');
button.addEventListener('click', function(e) {
e.preventDefault();
triggerElement = this;
return confirmPopup(action);
});
}
};
const abandonBidBtn = document.querySelector('button[name="abandon_bid"]');
overrideButtonConfirm(abandonBidBtn, 'Abandon');
const acceptBidBtn = document.querySelector('button[name="accept_bid"]');
overrideButtonConfirm(acceptBidBtn, 'Accept');
});
</script>
</div>
{% include 'footer.html' %}

View File

@@ -10,7 +10,7 @@
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">All Bids / Sent Bids / Received Bids</h2>
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">Sent Bids / Received Bids</h2>
<p class="font-normal text-coolGray-200 dark:text-white">View, and manage bids.</p>
</div>
</div>
@@ -28,12 +28,7 @@
<div class="mb-4 border-b pb-5 border-gray-200 dark:border-gray-500">
<ul class="flex flex-wrap text-sm font-medium text-center text-gray-500 dark:text-gray-400" id="myTab" data-tabs-toggle="#bidstab" role="tablist">
<li class="mr-2">
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="all-tab" data-tabs-target="#all" type="button" role="tab" aria-controls="all" aria-selected="true">
All Bids <span class="text-gray-500 dark:text-gray-400">({{ sent_bids_count + received_bids_count }})</span>
</button>
</li>
<li class="mr-2">
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="sent-tab" data-tabs-target="#sent" type="button" role="tab" aria-controls="sent" aria-selected="false">
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="sent-tab" data-tabs-target="#sent" type="button" role="tab" aria-controls="sent" aria-selected="true">
Sent Bids <span class="text-gray-500 dark:text-gray-400">({{ sent_bids_count }})</span>
</button>
</li>
@@ -172,106 +167,7 @@
</section>
<div id="bidstab">
<!-- All Bids Tab -->
<div class="rounded-lg lg:px-6" id="all" role="tabpanel" aria-labelledby="all-tab">
<div id="all-content">
<div class="xl:container mx-auto lg:px-0">
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-0">
<div class="w-auto overflow-auto lg:overflow-hidden">
<table class="w-full lg:min-w-max">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 pl-16 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Date/Time</span>
</div>
</th>
<th class="p-0 hidden lg:block">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Details</span>
</div>
</th>
<th class="p-0">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Send</span>
</div>
</th>
<th class="p-0">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Receive</span>
</div>
</th>
<th class="p-0">
<div class="p-3 text-center bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Status</span>
</div>
</th>
<th class="p-0">
<div class="p-3 pr-6 text-center rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Actions</span>
</div>
</th>
</tr>
</thead>
<tbody id="all-tbody">
</tbody>
</table>
</div>
<div class="rounded-b-md">
<div class="w-full">
<div class="flex flex-wrap justify-between items-center pl-6 pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
<div class="flex items-center">
<div class="flex items-center mr-4">
<span id="status-dot-all" class="w-2.5 h-2.5 rounded-full bg-gray-500 mr-2"></span>
<span id="status-text-all" class="text-sm text-gray-500">Connecting...</span>
</div>
<p class="text-sm font-heading dark:text-gray-400">
All Bids: <span id="allBidsCount">0</span>
</p>
{% if debug_ui_mode == true %}
<button id="refreshAllBids" class="ml-4 inline-flex items-center px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<span id="refreshAllText">Refresh</span>
</button>
{% endif %}
<button id="exportAllBids" class="ml-4 inline-flex items-center px-4 py-2.5 font-medium text-sm text-white bg-green-600 hover:bg-green-700 hover:border-green-700 rounded-lg transition duration-200 border border-green-600 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<span>Export CSV</span>
</button>
</div>
<div id="pagination-controls-all" class="flex items-center space-x-2" style="display: none;">
<button id="prevPageAll" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
Previous
</button>
<p class="text-sm font-heading dark:text-white">Page <span id="currentPageAll">1</span></p>
<button id="nextPageAll" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
Next
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Sent Bids Tab -->
<div class="hidden rounded-lg lg:px-6" id="sent" role="tabpanel" aria-labelledby="sent-tab">
<div class="rounded-lg lg:px-6" id="sent" role="tabpanel" aria-labelledby="sent-tab">
<div id="sent-content">
<div class="xl:container mx-auto lg:px-0">
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
@@ -312,7 +208,7 @@
</th>
</tr>
</thead>
<tbody id="sent-tbody">
<tbody>
</tbody>
</table>
</div>
@@ -368,7 +264,6 @@
</div>
</div>
<!-- Received Bids Tab -->
<div class="hidden rounded-lg lg:px-6" id="received" role="tabpanel" aria-labelledby="received-tab">
<div id="received-content">
<div class="xl:container mx-auto lg:px-0">
@@ -383,7 +278,7 @@
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Date/Time</span>
</div>
</th>
<th class="p-0 hidden lg:block">
<th class="p-0">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Details</span>
</div>
@@ -410,7 +305,7 @@
</th>
</tr>
</thead>
<tbody id="received-tbody">
<tbody>
</tbody>
</table>
</div>
@@ -468,6 +363,6 @@
</div>
<script src="/static/js/bids_sentreceived.js"></script>
<script src="/static/js/bids_sentreceived_export.js"></script>
<script src="/static/js/bids_export.js"></script>
{% include 'footer.html' %}

View File

@@ -54,7 +54,7 @@
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-right">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Receive</span>
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Get</span>
</div>
</th>
<th class="p-0">

View File

@@ -1,8 +1,6 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg %}
<div class="container mx-auto">
<!-- Breadcrumb -->
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
@@ -26,7 +24,6 @@
</div>
</div>
</section>
<section class="py-4">
<div class="container px-4 mx-auto">
<div class="relative py-11 px-16 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
@@ -42,50 +39,7 @@
</div>
</div>
</section>
{% include 'inc_messages.html' %}
{% set disabled_coins = [] %}
{% for c in chains_formatted %}
{% if c.connection_type == "none" %}
{% set _ = disabled_coins.append(c.display_name) %}
{% endif %}
{% endfor %}
{% if disabled_coins|length > 0 %}
<section class="py-4 px-6" role="alert">
<div class="lg:container mx-auto">
<div class="p-6 text-red-800 rounded-lg bg-red-50 border border-red-400 dark:bg-gray-500 dark:text-red-400 rounded-md">
<div class="flex flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2">
<div class="flex flex-wrap -m-1">
<ul class="ml-4 mt-1">
<li class="font-semibold text-sm text-red-500 error_msg"><span class="bold">WARNING:</span></li>
<li class="font-medium text-sm text-red-500 error_msg mb-2">Password Change Blocked - Disabled Coins Detected</li>
<li class="font-medium text-sm text-red-500 error_msg mb-2">
<strong>Changing your password now will break your installation!</strong>
</li>
<li class="font-medium text-sm text-red-500 error_msg mb-2">
The following coins are currently disabled and will NOT have their passwords updated:
</li>
{% for coin_name in disabled_coins %}
<li class="font-medium text-sm text-red-500 error_msg ml-4">• {{ coin_name }}</li>
{% endfor %}
<li class="font-medium text-sm text-red-500 error_msg mb-2 mt-2">
<strong>What this means:</strong> When you re-enable these coins later, they will still have the old password while your other coins have the new password, causing authentication failures.
</li>
<li class="font-medium text-sm text-red-500 error_msg">
<strong>Solution:</strong> Please <a href="/settings" class="underline font-medium">enable all coins</a> before changing your password, or wait until all coins are enabled.
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
{% endif %}
<section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden">
<div class="border-coolGray-100">
@@ -94,429 +48,138 @@
<div class="container mt-5 mx-auto">
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<form method="post" autocomplete="off" id="change-password-form" {% if disabled_coins|length > 0 %}class="form-disabled"{% endif %}>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div class="space-y-6">
<div>
<label for="oldpassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Current Password
<div class="w-full mt-6 pb-6 overflow-x-auto">
<table class="w-full min-w-max text-sm">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold">Password</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-6 bg-coolGray-200 dark:bg-gray-600">
<span class="text-xs text-gray-600 dark:text-gray-300 font-semibold py-3 px-6"></span>
</div>
</th>
</tr>
</thead>
<form method="post" autocomplete="off">
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Old Password</td>
<td td class="py-3 px-6">
<div class="relative w-full">
<div class="absolute inset-y-0 right-0 flex items-center px-2">
<input class="hidden js-password-toggle" id="toggle-old" type="checkbox" />
<label class="px-2 py-1 text-sm text-gray-600 font-mono cursor-pointer js-password-label" for="toggle-old" id="input-old-label">
<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g fill="#8896ab">
<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z" fill="#8896ab"></path>
</g>
</svg>
</label>
<div class="relative">
<input
type="password"
id="oldpassword"
name="oldpassword"
class="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-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0"
placeholder="Enter your current password"
{% if disabled_coins|length > 0 %}disabled{% endif %}
required
/>
<button
type="button"
id="toggle-old-password"
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
aria-label="Toggle password visibility"
>
<svg id="eye-open-old" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
<svg id="eye-closed-old" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"></path>
</svg>
</button>
</div>
<input class="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-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="password" name="oldpassword" id="input-old">
</div>
<div>
<label for="newpassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
New Password
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">New Password</td>
<td td class="py-3 px-6">
<div class="relative w-full">
<div class="absolute inset-y-0 right-0 flex items-center px-2">
<input class="hidden js-password-toggle" id="toggle-new" type="checkbox" />
<label class="px-2 py-1 text-sm text-gray-600 font-mono cursor-pointer js-password-label" for="toggle-new" id="input-new-label">
<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g fill="#8896ab">
<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z" fill="#8896ab"></path>
</g>
</svg>
</label>
<div class="relative">
<input
type="password"
id="newpassword"
name="newpassword"
class="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-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0"
placeholder="Enter your new password"
{% if disabled_coins|length > 0 %}disabled{% endif %}
required
/>
<button
type="button"
id="toggle-new-password"
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
aria-label="Toggle password visibility"
>
<svg id="eye-open-new" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</div>
<input class="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-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="password" name="newpassword" id="input-new">
</div>
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Confirm Password</td>
<td td class="py-3 px-6">
<div class="relative w-full">
<div class="absolute inset-y-0 right-0 flex items-center px-2">
<input class="hidden js-password-toggle" id="toggle-conf" type="checkbox" />
<label class="px-2 py-1 text-sm text-gray-600 font-mono cursor-pointer js-password-label" for="toggle-conf" id="input-confirm-label">
<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g fill="#8896ab">
<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z" fill="#8896ab"></path>
</g>
</svg>
<svg id="eye-closed-new" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"></path>
</svg>
</button>
</div>
<div id="caps-warning-new" class="hidden mt-2 text-sm text-amber-700 dark:text-amber-300 flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg>
Caps Lock is on
</div>
<div class="mt-3">
<div class="flex items-center justify-between mb-2">
<span class="text-sm text-gray-600 dark:text-gray-400">Password Strength:</span>
<span id="strength-text" class="text-sm font-medium text-gray-500 dark:text-gray-400">Enter password</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-600 rounded-full h-2">
<div id="strength-bar" class="h-2 rounded-full transition-all duration-300 bg-gray-300 dark:bg-gray-500" style="width: 0%"></div>
</div>
</div>
</div>
<div>
<label for="confirmpassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Confirm New Password
</label>
<div class="relative">
<input
type="password"
id="confirmpassword"
name="confirmpassword"
class="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-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0"
placeholder="Confirm your new password"
{% if disabled_coins|length > 0 %}disabled{% endif %}
required
/>
<button
type="button"
id="toggle-confirm-password"
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
aria-label="Toggle password visibility"
>
<svg id="eye-open-confirm" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
<svg id="eye-closed-confirm" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"></path>
</svg>
</button>
</div>
<div id="password-match" class="mt-2 text-sm hidden">
<div id="match-success" class="text-green-600 dark:text-green-400 flex items-center hidden">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
Passwords match
<input class="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-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="password" name="confirmpassword" id="input-confirm">
</div>
<div id="match-error" class="text-red-600 dark:text-red-400 flex items-center hidden">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L10 10.586l1.293-1.293a1 1 0 001.414 1.414L10 13.414l-1.293-1.293a1 1 0 00-1.414-1.414z" clip-rule="evenodd"></path>
</svg>
Passwords do not match
</div>
</div>
</div>
</div>
<div class="space-y-6">
<div class="bg-gray-50 dark:bg-gray-600 rounded-lg p-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Password Suggestions</h3>
<div class="space-y-3">
<div id="req-length" class="flex items-center text-gray-500 dark:text-gray-400">
<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
At least 8 characters
</div>
<div id="req-uppercase" class="flex items-center text-gray-500 dark:text-gray-400">
<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
Uppercase letter (A-Z)
</div>
<div id="req-lowercase" class="flex items-center text-gray-500 dark:text-gray-400">
<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
Lowercase letter (a-z)
</div>
<div id="req-number" class="flex items-center text-gray-500 dark:text-gray-400">
<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
Number (0-9)
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="mt-8 flex justify-end">
<button
type="submit"
id="submit-btn"
class="bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-3 px-8 rounded-lg transition-colors focus:outline-none disabled:cursor-not-allowed"
{% if disabled_coins|length > 0 %}disabled{% endif %}
>
<span id="submit-text">{% if disabled_coins|length > 0 %}Disabled - Enable All Coins First{% else %}Change Password{% endif %}</span>
<svg id="submit-spinner" class="hidden animate-spin ml-2 h-5 w-5 text-white inline" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</button>
</div>
</div>
</div>
</section>
<section>
<div class="pl-6 pr-6 pt-0 pb-0 h-full overflow-hidden ">
<div class="pb-6 ">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="container mx-auto">
<div class="pt-6 pb-6 bg-coolGray-100 border-t border-gray-100 dark:border-gray-400 dark:bg-gray-500 rounded-bl-xl rounded-br-xl">
<div class="px-6">
<div class="flex flex-wrap justify-end">
<div class="w-full md:w-auto p-1.5 ml-2">
<button type="submit" name="unlock" value="Unlock" class="flex flex-wrap justify-center w-full 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">Apply</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<input type="hidden" name="formid" value="{{ form_id }}">
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</section>
{% include 'footer.html' %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const oldPasswordInput = document.getElementById('oldpassword');
const newPasswordInput = document.getElementById('newpassword');
const confirmPasswordInput = document.getElementById('confirmpassword');
const form = document.getElementById('change-password-form');
const submitBtn = document.getElementById('submit-btn');
const submitText = document.getElementById('submit-text');
const submitSpinner = document.getElementById('submit-spinner');
setupPasswordToggle('old');
setupPasswordToggle('new');
setupPasswordToggle('confirm');
function setupPasswordToggle(type) {
const toggleBtn = document.getElementById(`toggle-${type}-password`);
const passwordInput = document.getElementById(`${type}password`);
const eyeOpen = document.getElementById(`eye-open-${type}`);
const eyeClosed = document.getElementById(`eye-closed-${type}`);
if (toggleBtn && passwordInput) {
toggleBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const isPassword = passwordInput.type === 'password';
const cursorPosition = passwordInput.selectionStart;
const inputValue = passwordInput.value;
passwordInput.type = isPassword ? 'text' : 'password';
passwordInput.value = inputValue;
setTimeout(() => {
passwordInput.setSelectionRange(cursorPosition, cursorPosition);
}, 0);
if (isPassword) {
eyeOpen.classList.add('hidden');
eyeClosed.classList.remove('hidden');
function togglePassword(event) {
let input_name = 'input-new';
if (event.target.id == 'toggle-old') {
input_name = 'input-old';
} else
if (event.target.id == 'toggle-conf') {
input_name = 'input-confirm';
}
const password = document.getElementById(input_name),
passwordLabel = document.getElementById(input_name + '-label');
if (password.type === 'password') {
password.type = 'text';
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab"><path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z" fill="#8896ab"></path><path d="M12,3C6.292,3,2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z" fill="#8896ab"></path><path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path></g></svg>';
} else {
eyeOpen.classList.remove('hidden');
eyeClosed.classList.add('hidden');
}
});
toggleBtn.addEventListener('mousedown', function(e) {
e.preventDefault();
});
password.type = 'password';
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab" ><path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z" fill="#8896ab"></path></g></svg>';
}
password.focus();
}
if (newPasswordInput) {
const capsWarning = document.getElementById('caps-warning-new');
newPasswordInput.addEventListener('keydown', function(e) {
const capsLockOn = e.getModifierState && e.getModifierState('CapsLock');
if (capsLockOn && capsWarning) {
capsWarning.classList.remove('hidden');
} else if (capsWarning) {
capsWarning.classList.add('hidden');
}
});
newPasswordInput.addEventListener('keyup', function(e) {
const capsLockOn = e.getModifierState && e.getModifierState('CapsLock');
if (!capsLockOn && capsWarning) {
capsWarning.classList.add('hidden');
}
});
}
function calculatePasswordStrength(password) {
let score = 0;
const requirements = {
length: password.length >= 8,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
number: /\d/.test(password)
};
if (requirements.length) score += 25;
if (requirements.uppercase) score += 25;
if (requirements.lowercase) score += 25;
if (requirements.number) score += 25;
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score += 10;
if (password.length >= 12) score += 10;
return { score: Math.min(score, 100), requirements };
}
function updatePasswordStrength(password) {
const { score, requirements } = calculatePasswordStrength(password);
const strengthBar = document.getElementById('strength-bar');
const strengthText = document.getElementById('strength-text');
if (strengthBar) {
strengthBar.style.width = `${score}%`;
if (score === 0) {
strengthBar.className = 'h-2 rounded-full transition-all duration-300 bg-gray-300 dark:bg-gray-500';
strengthText.textContent = 'Enter password';
strengthText.className = 'text-sm font-medium text-gray-500 dark:text-gray-400';
} else if (score < 40) {
strengthBar.className = 'h-2 rounded-full transition-all duration-300 bg-red-500';
strengthText.textContent = 'Weak';
strengthText.className = 'text-sm font-medium text-red-600 dark:text-red-400';
} else if (score < 70) {
strengthBar.className = 'h-2 rounded-full transition-all duration-300 bg-yellow-500';
strengthText.textContent = 'Fair';
strengthText.className = 'text-sm font-medium text-yellow-600 dark:text-yellow-400';
} else if (score < 90) {
strengthBar.className = 'h-2 rounded-full transition-all duration-300 bg-blue-500';
strengthText.textContent = 'Good';
strengthText.className = 'text-sm font-medium text-blue-600 dark:text-blue-400';
} else {
strengthBar.className = 'h-2 rounded-full transition-all duration-300 bg-green-500';
strengthText.textContent = 'Strong';
strengthText.className = 'text-sm font-medium text-green-600 dark:text-green-400';
}
}
updateRequirement('length', requirements.length);
updateRequirement('uppercase', requirements.uppercase);
updateRequirement('lowercase', requirements.lowercase);
updateRequirement('number', requirements.number);
return score >= 60;
}
function updateRequirement(type, met) {
const element = document.getElementById(`req-${type}`);
if (element) {
if (met) {
element.className = 'flex items-center text-green-600 dark:text-green-400';
} else {
element.className = 'flex items-center text-gray-500 dark:text-gray-400';
}
}
}
function checkPasswordMatch() {
const newPassword = newPasswordInput.value;
const confirmPassword = confirmPasswordInput.value;
const matchContainer = document.getElementById('password-match');
const matchSuccess = document.getElementById('match-success');
const matchError = document.getElementById('match-error');
if (confirmPassword.length === 0) {
matchContainer.classList.add('hidden');
return false;
}
matchContainer.classList.remove('hidden');
if (newPassword === confirmPassword) {
matchSuccess.classList.remove('hidden');
matchError.classList.add('hidden');
return true;
} else {
matchSuccess.classList.add('hidden');
matchError.classList.remove('hidden');
return false;
}
}
if (newPasswordInput) {
newPasswordInput.addEventListener('input', function() {
updatePasswordStrength(this.value);
if (confirmPasswordInput.value) {
checkPasswordMatch();
}
});
}
if (confirmPasswordInput) {
confirmPasswordInput.addEventListener('input', checkPasswordMatch);
}
if (form) {
form.addEventListener('submit', function(e) {
if (form.classList.contains('form-disabled')) {
e.preventDefault();
alert('Cannot change password while coins are disabled. Please enable all coins first.');
return;
}
const newPassword = newPasswordInput.value;
const isStrongEnough = updatePasswordStrength(newPassword);
const passwordsMatch = checkPasswordMatch();
if (!isStrongEnough) {
e.preventDefault();
alert('Please choose a stronger password.');
return;
}
if (!passwordsMatch) {
e.preventDefault();
alert('Passwords do not match.');
return;
}
if (submitBtn && submitText && submitSpinner) {
submitBtn.disabled = true;
submitText.textContent = 'Changing Password...';
submitSpinner.classList.remove('hidden');
}
});
}
const toggles = ["toggle-old", "toggle-new", "toggle-conf"]
toggles.forEach(function (toggle_id, index) {
document.getElementById(toggle_id).addEventListener('change', togglePassword, false);
});
</script>
<style>
.form-disabled {
opacity: 0.6;
pointer-events: none;
}
.form-disabled input[disabled] {
background-color: #f3f4f6 !important;
color: #9ca3af !important;
cursor: not-allowed !important;
}
.form-disabled button[disabled] {
background-color: #9ca3af !important;
cursor: not-allowed !important;
}
.dark .form-disabled input[disabled] {
background-color: #374151 !important;
color: #6b7280 !important;
}
.dark .form-disabled button[disabled] {
background-color: #6b7280 !important;
}
</style>
{% include 'footer.html' %}
</body>
</html>

View File

@@ -74,27 +74,6 @@
{{ start_process_svg| safe }} Start Process</button>
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">List non-segwit UTXOs</td>
<td td class="py-3 px-6 ">
<button name="list_non_segwit_prevouts" type="submit" value="Yes" class="w-60 flex flex-wrap 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">
List</button>
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Combine non-segwit BTC UTXOs</td>
<td td class="py-3 px-6 ">
<button name="combine_non_segwit_prevouts_btc" type="submit" value="Yes" class="w-60 flex flex-wrap 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">
Combine</button>
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Combine non-segwit LTC UTXOs</td>
<td td class="py-3 px-6 ">
<button name="combine_non_segwit_prevouts_ltc" type="submit" value="Yes" class="w-60 flex flex-wrap 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">
Combine</button>
</td>
</tr>
<input type="hidden" name="formid" value="{{ form_id }}">
</form>
</table>

View File

@@ -1,334 +0,0 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, love_svg %}
<section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="dots-red">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="wave">
<div class="relative z-20 flex flex-wrap items-center justify-center text-center">
<div class="w-full">
<h2 class="text-3xl font-bold text-white mb-4">
Support BasicSwap Development
</h2>
<p class="text-lg text-white max-w-3xl mx-auto">
Help keep BasicSwap free and open-source. Your donations directly fund development, security audits, and community growth.
</p>
</div>
</div>
</div>
</div>
</section>
<div class="xl:container mx-auto">
<section class="p-6">
<div class="lg:container mx-auto">
<div class="text-center mb-8">
<h3 class="font-semibold text-2xl text-black dark:text-white mb-4">Why Your Support Matters</h3>
<div class="flex justify-center mb-6">
{{ love_svg | safe }}
</div>
<div class="max-w-4xl mx-auto">
<p class="text-lg text-coolGray-500 dark:text-gray-300 mb-6">
BasicSwap is completely free and open-source software that charges no fees for its use. The project is entirely funded by generous community donations from users who believe in decentralized, censorship-resistant trading.
</p>
<p class="text-lg text-coolGray-500 dark:text-gray-300 mb-8">
Your donations are vital to keeping this project alive, accelerating development, and expanding our reach to more users who value financial freedom and privacy.
</p>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="bg-coolGray-100 dark:bg-gray-500 rounded-lg p-6">
<div class="text-green-500 mb-3">
<svg class="w-8 h-8 mx-auto" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</div>
<h4 class="font-semibold text-coolGray-900 dark:text-white mb-2">Core Development</h4>
<p class="text-sm text-coolGray-500 dark:text-gray-300">New features and improvements</p>
</div>
<div class="bg-coolGray-100 dark:bg-gray-500 rounded-lg p-6">
<div class="text-green-500 mb-3">
<svg class="w-8 h-8 mx-auto" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</div>
<h4 class="font-semibold text-coolGray-900 dark:text-white mb-2">Security Audits</h4>
<p class="text-sm text-coolGray-500 dark:text-gray-300">Testing and security infrastructure</p>
</div>
<div class="bg-coolGray-100 dark:bg-gray-500 rounded-lg p-6">
<div class="text-green-500 mb-3">
<svg class="w-8 h-8 mx-auto" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</div>
<h4 class="font-semibold text-coolGray-900 dark:text-white mb-2">Documentation</h4>
<p class="text-sm text-coolGray-500 dark:text-gray-300">Educational resources and guides</p>
</div>
<div class="bg-coolGray-100 dark:bg-gray-500 rounded-lg p-6">
<div class="text-green-500 mb-3">
<svg class="w-8 h-8 mx-auto" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</div>
<h4 class="font-semibold text-coolGray-900 dark:text-white mb-2">Community Growth</h4>
<p class="text-sm text-coolGray-500 dark:text-gray-300">Outreach and adoption initiatives</p>
</div>
</div>
<p class="text-lg text-coolGray-500 dark:text-gray-300">
Together, we're building financial tools that empower individuals and resist censorship. Thank you for being part of this movement toward true financial freedom.
</p>
</div>
</div>
</div>
</section>
<section>
<div class="px-6 py-0 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
<div class="lg:container mt-5 mx-auto">
<div class="pt-6 pb-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6">
<h3 class="mb-6 text-2xl text-coolGray-900 dark:text-white font-bold text-center">
Donation Addresses
</h3>
<div class="w-full pb-6 overflow-x-auto">
<table class="w-full text-lg lg:text-sm">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0 w-1/6">
<div class="py-3 px-6 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-md lg:text-xs text-gray-600 dark:text-gray-300 font-semibold">Cryptocurrency</span>
</div>
</th>
<th class="p-0 w-5/6">
<div class="py-3 px-6 rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-md lg:text-xs text-gray-600 dark:text-gray-300 font-semibold">Donation Address</span>
</div>
</th>
</tr>
</thead>
<!-- Monero -->
<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/Monero.png" alt="Monero">
</span>
Monero (XMR)
</td>
<td class="py-3 px-6">
<input type="text" readonly
class="donation-address cursor-pointer hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0 font-mono"
value="8BuQsYBNdfhfoWsvVR1unE7YuZEoTkC4hANaPm2fD6VR5VM2DzQoJhq2CHHXUN1UCWQfH3dctJgorSRxksVa5U4RNTJkcAc"
data-address="8BuQsYBNdfhfoWsvVR1unE7YuZEoTkC4hANaPm2fD6VR5VM2DzQoJhq2CHHXUN1UCWQfH3dctJgorSRxksVa5U4RNTJkcAc">
</td>
</tr>
<!-- Litecoin -->
<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/Litecoin.png" alt="Litecoin">
</span>
Litecoin (LTC)
</td>
<td class="py-3 px-6">
<input type="text" readonly
class="donation-address cursor-pointer hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0 font-mono"
value="ltc1qevlumv48nz2afl0re9ml4tdewc56svxq3egkyt"
data-address="ltc1qevlumv48nz2afl0re9ml4tdewc56svxq3egkyt">
</td>
</tr>
<!-- Litecoin MWEB -->
<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/Litecoin-MWEB.png" alt="Litecoin MWEB">
</span>
Litecoin MWEB
</td>
<td class="py-3 px-6">
<input type="text" readonly
class="donation-address cursor-pointer hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0 font-mono"
value="ltcmweb1qqt9rwznnxzkghv4s5wgtwxs0m0ry6n3atp95f47slppapxljde3xyqmdlnrc8ag7y2k354jzdc4pc4ks0kr43jehr77lngdecgh6689nn5mgv5yn"
data-address="ltcmweb1qqt9rwznnxzkghv4s5wgtwxs0m0ry6n3atp95f47slppapxljde3xyqmdlnrc8ag7y2k354jzdc4pc4ks0kr43jehr77lngdecgh6689nn5mgv5yn">
</td>
</tr>
<!-- Bitcoin -->
<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/Bitcoin.png" alt="Bitcoin">
</span>
Bitcoin (BTC)
</td>
<td class="py-3 px-6">
<input type="text" readonly
class="donation-address cursor-pointer hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0 font-mono"
value="bc1q72j07vkn059xnmsrkk8x9up9lgvd9h9xjf8cq8"
data-address="bc1q72j07vkn059xnmsrkk8x9up9lgvd9h9xjf8cq8">
</td>
</tr>
<!-- Particl -->
<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/Particl.png" alt="Particl">
</span>
Particl (PART)
</td>
<td class="py-3 px-6">
<input type="text" readonly
class="donation-address cursor-pointer hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0 font-mono"
value="pw1qf59ef0zjdckldjs8smfhv4j04gsjv302w7pdpz"
data-address="pw1qf59ef0zjdckldjs8smfhv4j04gsjv302w7pdpz">
</td>
</tr>
</table>
</div>
<div class="text-center mt-6">
<p class="text-lg text-coolGray-500 dark:text-gray-300">
Every contribution helps make decentralized trading more accessible to everyone.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<script>
document.addEventListener('DOMContentLoaded', function() {
setupDonationAddressCopy();
});
function setupDonationAddressCopy() {
const donationAddresses = document.querySelectorAll('.donation-address');
donationAddresses.forEach(element => {
element.addEventListener('click', function(e) {
const address = this.value;
copyToClipboard(address);
this.select();
this.setSelectionRange(0, 99999);
this.classList.add('bg-blue-50', 'dark:bg-blue-900');
showCopyFeedback(this);
setTimeout(() => {
this.classList.remove('bg-blue-50', 'dark:bg-blue-900');
this.blur(); // Remove focus
}, 1000);
});
});
}
let activeTooltip = null;
function showCopyFeedback(element) {
if (activeTooltip && activeTooltip.parentNode) {
activeTooltip.parentNode.removeChild(activeTooltip);
}
const popup = document.createElement('div');
popup.className = 'copy-feedback-popup fixed z-50 bg-blue-600 text-white text-sm py-2 px-3 rounded-md shadow-lg';
popup.innerText = 'Address copied!';
document.body.appendChild(popup);
activeTooltip = popup;
updateTooltipPosition(popup, element);
const scrollHandler = () => {
if (popup.parentNode) {
updateTooltipPosition(popup, element);
}
};
window.addEventListener('scroll', scrollHandler, { passive: true });
popup.style.opacity = '0';
popup.style.transition = 'opacity 0.2s ease-in-out';
setTimeout(() => {
popup.style.opacity = '1';
}, 10);
setTimeout(() => {
window.removeEventListener('scroll', scrollHandler);
popup.style.opacity = '0';
setTimeout(() => {
if (popup.parentNode) {
popup.parentNode.removeChild(popup);
}
if (activeTooltip === popup) {
activeTooltip = null;
}
}, 200);
}, 1500);
}
function updateTooltipPosition(tooltip, element) {
const rect = element.getBoundingClientRect();
let top = rect.top - tooltip.offsetHeight - 8;
const left = rect.left + rect.width / 2;
if (top < 10) {
top = rect.bottom + 8;
}
tooltip.style.top = `${top}px`;
tooltip.style.left = `${left}px`;
tooltip.style.transform = 'translateX(-50%)';
}
function copyToClipboard(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(err => {
console.error('Failed to copy: ', err);
copyToClipboardFallback(text);
});
} else {
copyToClipboardFallback(text);
}
}
function copyToClipboardFallback(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
} catch (err) {
console.error('Failed to copy text: ', err);
}
document.body.removeChild(textArea);
}
</script>
{% include 'footer.html' %}

View File

@@ -8,7 +8,6 @@
<img src="/static/images/logos/basicswap-logo-dark.svg" class="h-8 imageshow light-image">
</a>
<div class="mb-12 md:mb-0 flex flex-wrap -mx-3 md:-mx-6">
<div class="w-full md:w-auto p-3 md:py-0 md:px-6"><a class="inline-block text-coolGray-500 dark:text-gray-300 font-medium" href="/donation">Donate</a></div>
<div class="w-full md:w-auto p-3 md:py-0 md:px-6"><a class="inline-block text-coolGray-500 dark:text-gray-300 font-medium" href="https://academy.particl.io/en/latest/basicswap-dex/basicswap_explained.html" target="_blank">BasicSwap Explained</a></div>
<div class="w-full md:w-auto p-3 md:py-0 md:px-6"><a class="inline-block text-coolGray-500 dark:text-gray-300 font-medium" href="https://academy.particl.io/en/latest/basicswap-guides/basicswapguides_installation.html" target="_blank">Tutorials and Guides</a></div>
<div class="w-full md:w-auto p-3 md:py-0 md:px-6"><a class="inline-block text-coolGray-500 dark:text-gray-300 font-medium" href="https://academy.particl.io/en/latest/faq/get_support.html" target="_blank">Get Support</a></div>
@@ -26,7 +25,7 @@
<div class="flex items-center">
<p class="text-sm text-gray-90 dark:text-white font-medium">© 2025~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">BSX: v{{ version }}</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">GUI: v3.2.2</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.2.0</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
{{ love_svg | safe }}
</div>
@@ -44,3 +43,68 @@
</div>
</div>
</section>
<script>
var toggleImages = function() {
var html = document.querySelector('html');
var darkImages = document.querySelectorAll('.dark-image');
var lightImages = document.querySelectorAll('.light-image');
if (html && html.classList.contains('dark')) {
toggleImageDisplay(darkImages, 'block');
toggleImageDisplay(lightImages, 'none');
} else {
toggleImageDisplay(darkImages, 'none');
toggleImageDisplay(lightImages, 'block');
}
};
var toggleImageDisplay = function(images, display) {
images.forEach(function(img) {
img.style.display = display;
});
};
document.addEventListener('DOMContentLoaded', function() {
var themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', function() {
toggleImages();
});
}
toggleImages();
});
</script>
<script>
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
}
function setTheme(theme) {
if (theme === 'light') {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
}
document.getElementById('theme-toggle').addEventListener('click', () => {
if (localStorage.getItem('color-theme') === 'dark') {
setTheme('light');
} else {
setTheme('dark');
}
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
toggleImages();
});
</script>

View File

@@ -1,3 +1,4 @@
<!DOCTYPE html>
{% from 'style.html' import change_password_svg, notifications_network_offer_svg,
notifications_bid_accepted_svg, notifications_unknow_event_svg,
notifications_new_bid_on_offer_svg, notifications_close_svg, swap_in_progress_mobile_svg,
@@ -6,46 +7,44 @@
shutdown_svg, notifications_svg, debug_nerd_svg, wallet_locked_svg, mobile_menu_svg,
wallet_unlocked_svg, tor_purple_svg, sun_svg, moon_svg, swap_in_progress_svg,
swap_in_progress_green_svg, available_bids_svg, your_offers_svg, bids_received_svg,
bids_sent_svg, header_arrow_down_svg, love_svg, mobile_love_svg, amm_active_svg, amm_inactive_svg %}
bids_sent_svg, header_arrow_down_svg, love_svg %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
{% if refresh %}
<meta http-equiv="refresh" content="{{ refresh }}">
{% endif %}
<title>(BSX) BasicSwap - v{{ version }}</title>
<link rel="icon" sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
<!-- CSS Stylesheets -->
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet">
<!-- Scripts -->
<script src="/static/js/libs/chart.js"></script>
<script src="/static/js/libs/chartjs-adapter-date-fns.bundle.min.js"></script>
<script src="/static/js/main.js"></script>
<script src="/static/js/tabs.js"></script>
<script src="/static/js/dropdown.js"></script>
<script src="/static/js/libs/popper.js"></script>
<script src="/static/js/libs/tippy.js"></script>
<script src="/static/js/tooltips.js"></script>
<!-- Styles -->
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
<!-- Custom styles -->
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
<link rel="icon" sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
<title>(BSX) BasicSwap - v{{ version }}</title>
<!-- Initialize tooltips -->
<script>
function getAPIKeys() {
return {
cryptoCompare: "{{ chart_api_key|safe }}",
coinGecko: "{{ coingecko_api_key|safe }}"
};
}
(function() {
Object.defineProperty(window, 'ws_port', {
value: "{{ ws_port|safe }}",
writable: false,
configurable: false,
enumerable: true
document.addEventListener('DOMContentLoaded', () => {
const tooltipManager = TooltipManager.initialize();
tooltipManager.initializeTooltips();
});
window.getWebSocketConfig = window.getWebSocketConfig || function() {
return {
port: window.ws_port || '11700',
fallbackPort: '11700'
};
};
})();
</script>
(function() {
<!-- Dark mode initialization -->
<script>
const isDarkMode = localStorage.getItem('color-theme') === 'dark' ||
(!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
@@ -53,41 +52,78 @@
localStorage.setItem('color-theme', 'dark');
}
document.documentElement.classList.toggle('dark', isDarkMode);
})();
</script>
<!-- Third-party Libraries -->
<script src="/static/js/libs/chart.js"></script>
<script src="/static/js/libs/chartjs-adapter-date-fns.bundle.min.js"></script>
<script src="/static/js/libs/popper.js"></script>
<script src="/static/js/libs/tippy.js"></script>
<!-- UI Components -->
<script src="/static/js/ui/tabs.js"></script>
<script src="/static/js/ui/bids-tab-navigation.js"></script>
<script src="/static/js/ui/dropdown.js"></script>
<!-- Core functionality -->
<script src="/static/js/modules/coin-manager.js"></script>
<script src="/static/js/modules/config-manager.js"></script>
<script src="/static/js/modules/cache-manager.js"></script>
<script src="/static/js/modules/cleanup-manager.js"></script>
<script src="/static/js/modules/websocket-manager.js"></script>
<script src="/static/js/modules/network-manager.js"></script>
<script src="/static/js/modules/api-manager.js"></script>
<script src="/static/js/modules/price-manager.js"></script>
<script src="/static/js/modules/tooltips-manager.js"></script>
<script src="/static/js/modules/notification-manager.js"></script>
<script src="/static/js/modules/identity-manager.js"></script>
<script src="/static/js/modules/summary-manager.js"></script>
<script src="/static/js/amm_counter.js"></script>
{% if current_page == 'wallets' or current_page == 'wallet' %}
<script src="/static/js/modules/wallet-manager.js"></script>
{% endif %}
<!-- Memory management -->
<script src="/static/js/modules/memory-manager.js"></script>
<!-- Main application script -->
<script src="/static/js/global.js"></script>
<!-- Shutdown modal functionality -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const shutdownButtons = document.querySelectorAll('.shutdown-button');
const shutdownModal = document.getElementById('shutdownModal');
const closeModalButton = document.getElementById('closeShutdownModal');
const confirmShutdownButton = document.getElementById('confirmShutdown');
const shutdownWarning = document.getElementById('shutdownWarning');
function updateShutdownButtons() {
const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
shutdownButtons.forEach(button => {
if (activeSwaps > 0) {
button.classList.add('shutdown-disabled');
button.setAttribute('data-disabled', 'true');
button.setAttribute('title', 'Caution: Swaps in progress');
} else {
button.classList.remove('shutdown-disabled');
button.removeAttribute('data-disabled');
button.removeAttribute('title');
}
});
}
function showShutdownModal() {
const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
if (activeSwaps > 0) {
shutdownWarning.classList.remove('hidden');
confirmShutdownButton.textContent = 'Yes, Shut Down Anyway';
} else {
shutdownWarning.classList.add('hidden');
confirmShutdownButton.textContent = 'Yes, Shut Down';
}
shutdownModal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
function hideShutdownModal() {
shutdownModal.classList.add('hidden');
document.body.style.overflow = '';
}
shutdownButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
showShutdownModal();
});
});
closeModalButton.addEventListener('click', hideShutdownModal);
confirmShutdownButton.addEventListener('click', function() {
const shutdownToken = document.querySelector('.shutdown-button')
.getAttribute('href').split('/').pop();
window.location.href = '/shutdown/' + shutdownToken;
});
shutdownModal.addEventListener('click', function(e) {
if (e.target === this) {
hideShutdownModal();
}
});
updateShutdownButtons();
});
</script>
</head>
<body class="dark:bg-gray-700">
<!-- Shutdown Modal -->
<div id="shutdownModal" tabindex="-1" class="hidden fixed inset-0 z-50 overflow-y-auto overflow-x-hidden">
<div class="fixed inset-0 bg-black bg-opacity-60 transition-opacity"></div>
<div class="flex items-center justify-center min-h-screen p-4 relative z-10">
@@ -167,18 +203,8 @@
</li>
</ul>
<!-- Donation Link -->
<ul class="hidden xl:flex lg:justify-end lg:items-center lg:space-x-6 ml-auto">
<li>
<a href="/donation" class="flex items-center py-2 pr-4 pl-3 text-gray-50 text-sm hover:text-gray-100">
{{ love_svg | safe }}
<span class="ml-2">Donate</span>
</a>
</li>
</ul>
<!-- Settings Dropdown -->
<ul class="hidden xl:flex lg:justify-end lg:items-center lg:space-x-6">
<ul class="hidden xl:flex lg:justify-end lg:items-center lg:space-x-6 ml-auto">
<div id="dropdownNavbarLink" data-dropdown-toggle="dropdownNavbar" class="flex justify-between
items-center py-2 pr-4 pl-3 w-full text-gray-50 text-sm md:border-0 md:p-0 md:w-auto
text-gray-50 hover:text-gray-100">
@@ -266,7 +292,6 @@
</span>
</a>
</li>
{% if debug_mode == true %}
<li>
<a href="/automation" class="flex items-center block py-4 px-4 hover:bg-gray-100
@@ -312,38 +337,6 @@
</ul>
{% endif %}
<!-- AMM Status -->
<ul class="xl:flex">
<li>
{% if current_status == 'running' %}
<a href='/amm'>
<div data-tooltip-target="tooltip-amm-active" class="ml-5 flex items-center text-gray-50
hover:text-gray-100 text-sm">
{{ amm_active_svg | safe }}
</div>
<div id="tooltip-amm-active" role="tooltip" class="inline-block absolute invisible z-10
py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0
transition-opacity duration-300 tooltip">
<p><b>AMM:</b> Active</p>
<p><b>Currently offers/bids:</b> {{ amm_active_count }}</p>
</div>
</a>
{% else %}
<a href='/amm'>
<div data-tooltip-target="tooltip-amm-inactive" class="ml-5 flex items-center
text-gray-50 hover:text-gray-100 text-sm">
{{ amm_inactive_svg | safe }}
</div>
<div id="tooltip-amm-inactive" role="tooltip" class="inline-block absolute invisible
z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0
transition-opacity duration-300 tooltip">
<p><b>AMM:</b> Inactive</p>
</div>
</a>
{% endif %}
</li>
</ul>
<!-- Wallet Status -->
{% if encrypted == true %}
<ul class="xl:flex">
@@ -423,30 +416,6 @@
dark:bg-body border-b dark:border-b-2">
<div class="flex items-center justify-center container mx-auto">
<ul class="flex items-center space-x-8">
<!-- AMM -->
<li>
<a data-tooltip-target="tooltip-amm-subheader" class="flex items-center text-sm text-gray-400
hover:text-gray-600 dark:text-gray-100 dark:hover:text-gray-100" href="/amm">
{{ automation_svg | safe }}
<span>AMM</span>
<span id="amm-counter" class="inline-flex justify-center items-center text-xs
font-semibold ml-3 px-2.5 py-1 text-white {% if current_status == 'running' and amm_active_count > 0 %}
bg-blue-500{% else %}bg-gray-400{% endif %} rounded-full">
{{ amm_active_count }}
</span>
</a>
<div id="tooltip-amm-subheader" role="tooltip" class="inline-block absolute invisible z-10
py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0
transition-opacity duration-300 tooltip">
<p><b>Status:</b> {% if current_status == 'running' %}Active{% else %}Inactive{% endif %}</p>
<p><b>Currently active offers/bids:</b> {{ amm_active_count }}</p>
</div>
</li>
<li>
<span class="text-gray-300">|</span>
</li>
<!-- Your Offers -->
<li>
<a data-tooltip-target="tooltip-your-offers" class="flex items-center text-sm text-gray-400
@@ -491,17 +460,26 @@
<!-- Bids -->
<li>
<a href="/bids" data-tooltip-target="tooltip-bids" class="flex items-center text-sm text-gray-400 hover:text-gray-600 dark:text-gray-100 dark:hover:text-gray-100">
<a href="/bids" data-tooltip-target="tooltip-bids" class="flex items-center text-sm text-gray-400
hover:text-gray-600 dark:text-gray-100 dark:hover:text-gray-100">
<span class="inline-block mr-2">{{ bids_sent_svg | safe }}</span>
<span class="my-auto">Bids</span>
<span class="flex items-center ml-2 my-auto">
<span id="sent-bids-counter" class="inline-flex items-center text-xs font-semibold px-2.5 py-1 text-white {% if summary.num_sent_active_bids > 0 %}bg-blue-500{% else %}bg-gray-400{% endif %} rounded-full bids-tab-link cursor-pointer" data-tab-target="#sent">
<span>Bids</span>
<span class="flex items-center ml-2">
<!-- Outgoing bids counter arrow -->
<span id="sent-bids-counter" class="inline-flex items-center text-xs font-semibold px-2.5 py-1
text-white {% if summary.num_sent_active_bids > 0 %}bg-blue-500{% else %}bg-gray-400{% endif %}
rounded-full">
<svg class="w-3 h-3 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 19V5L19 12L5 19Z" fill="currentColor" transform="rotate(-90 12 12)"/>
</svg>
{{ summary.num_sent_active_bids }}
</span>
<span id="recv-bids-counter" class="inline-flex items-center text-xs font-semibold ml-2 px-2.5 py-1 text-white {% if summary.num_recv_active_bids > 0 %}bg-blue-500{% else %}bg-gray-400{% endif %} rounded-full bids-tab-link cursor-pointer" data-tab-target="#received">
<!-- Incoming bids counter arrow -->
<span id="recv-bids-counter" class="inline-flex items-center text-xs font-semibold ml-2 px-2.5
py-1 text-white {% if summary.num_recv_active_bids > 0 %}bg-blue-500{% else %}bg-gray-400
{% endif %} rounded-full">
<svg class="w-3 h-3 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 19V5L19 12L5 19Z" fill="currentColor" transform="rotate(90 12 12)"/>
</svg>
@@ -509,11 +487,14 @@
</span>
</span>
</a>
<div id="tooltip-bids" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
<p><b>Sent bids:</b> {{ sent_bids_count }} ({{ summary.num_sent_active_bids }} active)</p>
<p><b>Received bids:</b> {{ received_bids_count }} ({{ summary.num_recv_active_bids }} active)</p>
<div id="tooltip-bids" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm
font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300
tooltip">
<p><b>Sent bids:</b> {{ summary.num_sent_bids }} ({{ summary.num_sent_active_bids }} active)</p>
<p><b>Received bids:</b> {{ summary.num_recv_bids }} ({{ summary.num_recv_active_bids }} active)</p>
</div>
</li>
<li>
<span class="text-gray-300">|</span>
</li>
@@ -580,37 +561,17 @@
<span>Wallets</span>
</a>
</li>
<li>
<a class="flex items-center pl-3 py-3 pr-4 text-gray-50 hover:bg-gray-900 rounded"
href="/donation">
{{ mobile_love_svg | safe }}
<span>Donate</span>
</a>
</li>
</ul>
<!-- Trading Section -->
<h3 class="mb-2 text-xs uppercase text-gray-300 font-medium">Trading</h3>
<ul class="mb-8 text-sm font-medium">
<li>
<a class="flex items-center pl-3 py-3 pr-4 text-gray-50 hover:bg-gray-900 rounded"
href="/amm">
{{ automation_svg | safe }}
<span>AMM</span>
<span id="amm-counter-mobile" class="inline-flex justify-center items-center text-xs font-semibold
ml-auto px-2.5 py-1 text-white {% if current_status == 'running' and amm_active_count > 0 %}
bg-blue-500{% else %}bg-gray-400{% endif %} rounded-full">
{{ amm_active_count }}
</span>
</a>
</li>
<li>
<a class="flex items-center pl-3 py-3 pr-4 text-gray-50 hover:bg-gray-900 rounded"
href="/sentoffers">
{{ your_offers_svg | safe }}
<span>Your Offers</span>
<span id="offers-counter-mobile" class="inline-flex justify-center items-center text-xs font-semibold
<span id="offers-counter" class="inline-flex justify-center items-center text-xs font-semibold
ml-auto px-2.5 py-1 text-white {% if summary.num_sent_active_offers and
summary.num_sent_active_offers > 0 %}bg-blue-500{% else %}bg-gray-400{% endif %}
rounded-full">
@@ -633,12 +594,11 @@
<li>
<a class="flex items-center pl-3 py-3 pr-4 text-gray-50 hover:bg-gray-900 rounded" href="/bids">
{{ bids_received_svg | safe }}
<span class="my-auto">Bids</span>
<div class="flex items-center ml-auto my-auto">
<span>Bids</span>
<div class="flex ml-auto">
<span id="sent-bids-counter" class="inline-flex items-center text-xs font-semibold px-2.5
py-1 text-white {% if summary.num_sent_active_bids and summary.num_sent_active_bids > 0 %}
bg-blue-500{% else %}bg-gray-400{% endif %} rounded-full mr-2 bids-tab-link cursor-pointer"
data-tab-target="#sent">
bg-blue-500{% else %}bg-gray-400{% endif %} rounded-full mr-2">
<svg class="w-3 h-3 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 19V5L19 12L5 19Z" fill="currentColor" transform="rotate(-90 12 12)"/>
</svg>
@@ -646,8 +606,7 @@
</span>
<span id="recv-bids-counter" class="inline-flex items-center text-xs font-semibold px-2.5
py-1 text-white {% if summary.num_recv_active_bids and summary.num_recv_active_bids > 0 %}
bg-blue-500{% else %}bg-gray-400{% endif %} rounded-full bids-tab-link cursor-pointer"
data-tab-target="#received">
bg-blue-500{% else %}bg-gray-400{% endif %} rounded-full">
<svg class="w-3 h-3 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 19V5L19 12L5 19Z" fill="currentColor" transform="rotate(90 12 12)"/>
</svg>
@@ -733,7 +692,6 @@
<span>SMSG Addresses</span>
</a>
</li>
{% if debug_mode == true %}
<li>
<a class="flex items-center pl-3 py-3 pr-4 text-gray-50 hover:bg-gray-900 rounded"
@@ -766,3 +724,194 @@
</div>
</div>
</section>
<!-- WebSocket -->
{% if ws_port %}
<script>
(function() {
window.notificationConfig = {
showNewOffers: false,
showNewBids: true,
showBidAccepted: true
};
function ensureToastContainer() {
let container = document.getElementById('ul_updates');
if (!container) {
const floating_div = document.createElement('div');
floating_div.classList.add('floatright');
container = document.createElement('ul');
container.setAttribute('id', 'ul_updates');
floating_div.appendChild(container);
document.body.appendChild(floating_div);
}
return container;
}
function createToast(title, type = 'success') {
const messages = ensureToastContainer();
const message = document.createElement('li');
message.innerHTML = `
<div id="hide">
<div id="toast-${type}" class="flex items-center p-4 mb-4 w-full max-w-xs text-gray-500
bg-white rounded-lg shadow" role="alert">
<div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10
bg-blue-500 rounded-lg">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="18" width="18"
viewBox="0 0 24 24">
<g fill="#ffffff">
<path d="M8.5,20a1.5,1.5,0,0,1-1.061-.439L.379,12.5,2.5,10.379l6,6,13-13L23.621,
5.5,9.561,19.561A1.5,1.5,0,0,1,8.5,20Z"></path>
</g>
</svg>
</div>
<div class="uppercase w-40 ml-3 text-sm font-semibold text-gray-900">${title}</div>
<button type="button" onclick="closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5
bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-0 focus:outline-none
focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8">
<span class="sr-only">Close</span>
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1
1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293
4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</button>
</div>
</div>
`;
messages.appendChild(message);
}
function updateElement(elementId, value, options = {}) {
const element = document.getElementById(elementId);
if (!element) return false;
const safeValue = (value !== undefined && value !== null)
? value
: (element.dataset.lastValue || 0);
element.dataset.lastValue = safeValue;
if (elementId === 'sent-bids-counter' || elementId === 'recv-bids-counter') {
const svg = element.querySelector('svg');
element.textContent = safeValue;
if (svg) {
element.insertBefore(svg, element.firstChild);
}
} else {
element.textContent = safeValue;
}
if (['offers-counter', 'bid-requests-counter', 'sent-bids-counter',
'recv-bids-counter', 'swaps-counter', 'network-offers-counter',
'watched-outputs-counter'].includes(elementId)) {
element.classList.remove('bg-blue-500', 'bg-gray-400');
element.classList.add(safeValue > 0 ? 'bg-blue-500' : 'bg-gray-400');
}
if (elementId === 'swaps-counter') {
const swapContainer = document.getElementById('swapContainer');
if (swapContainer) {
const isSwapping = safeValue > 0;
if (isSwapping) {
swapContainer.innerHTML = `{{ swap_in_progress_green_svg | safe }}`;
swapContainer.style.animation = 'spin 2s linear infinite';
} else {
swapContainer.innerHTML = `{{ swap_in_progress_svg | safe }}`;
swapContainer.style.animation = 'none';
}
}
}
return true;
}
function fetchSummaryData() {
fetch('/json')
.then(response => response.json())
.then(data => {
updateElement('network-offers-counter', data.num_network_offers);
updateElement('offers-counter', data.num_sent_active_offers);
updateElement('sent-bids-counter', data.num_sent_active_bids);
updateElement('recv-bids-counter', data.num_recv_active_bids);
updateElement('bid-requests-counter', data.num_available_bids);
updateElement('swaps-counter', data.num_swapping);
updateElement('watched-outputs-counter', data.num_watched_outputs);
})
.catch(error => console.error('Summary data fetch error:', error));
}
function initWebSocket() {
const wsUrl = "ws://" + window.location.hostname + ":{{ ws_port }}";
const ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('🟢 WebSocket connection established for Dynamic Counters');
fetchSummaryData();
setInterval(fetchSummaryData, 30000); // Refresh every 30 seconds
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.event) {
let toastTitle;
let shouldShowToast = false;
switch (data.event) {
case 'new_offer':
toastTitle = `New network <a class="underline" href=/offer/${data.offer_id}>offer</a>`;
shouldShowToast = window.notificationConfig.showNewOffers;
break;
case 'new_bid':
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>New bid</a> on
<a class="underline" href=/offer/${data.offer_id}>offer</a>`;
shouldShowToast = window.notificationConfig.showNewBids;
break;
case 'bid_accepted':
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>Bid</a> accepted`;
shouldShowToast = window.notificationConfig.showBidAccepted;
break;
}
if (toastTitle && shouldShowToast) {
createToast(toastTitle);
}
}
fetchSummaryData();
} catch (error) {
console.error('WebSocket message processing error:', error);
}
};
ws.onerror = (error) => {
console.error('WebSocket Error:', error);
};
ws.onclose = (event) => {
console.log('WebSocket connection closed', event);
setTimeout(initWebSocket, 5000);
};
}
window.closeAlert = function(event) {
let element = event.target;
while (element.nodeName !== "BUTTON") {
element = element.parentNode;
}
element.parentNode.parentNode.removeChild(element.parentNode);
};
function init() {
initWebSocket();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
{% endif %}

View File

@@ -1,8 +1,8 @@
{% from 'style.html' import circular_info_messages_svg, green_cross_close_svg, red_cross_close_svg, circular_error_messages_svg %}
{% for m in messages %}
<section class="py-4 px-6" id="messages_{{ m[0] }}" role="alert">
<div class="lg:container mx-auto">
<section class="py-4" id="messages_{{ m[0] }}" role="alert">
<div class="container px-4 mx-auto">
<div class="p-6 text-green-800 rounded-lg bg-green-50 border border-green-500 dark:bg-gray-500 dark:text-green-400 rounded-md">
<div class="flex flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2">
@@ -17,7 +17,7 @@
</div>
</div>
<div class="w-auto p-2">
<button type="button" class="ms-auto bg-green-50 text-green-500 rounded-lg focus:ring-0 focus:ring-green-400 p-1.5 hover:bg-green-200 inline-flex items-center justify-center h-8 w-8 focus:outline-none dark:bg-gray-800 dark:text-green-400 dark:hover:bg-gray-700" onclick="document.getElementById('messages_{{ m[0] }}').style.display='none';" aria-label="Close"><span class="sr-only">Close</span>
<button type="button" class="ms-auto bg-green-50 text-green-500 rounded-lg focus:ring-0 focus:ring-green-400 p-1.5 hover:bg-green-200 inline-flex items-center justify-center h-8 w-8 focus:outline-none dark:bg-gray-800 dark:text-green-400 dark:hover:bg-gray-700" data-dismiss-target="#messages_{{ m[0] }}" aria-label="Close"><span class="sr-only">Close</span>
{{ green_cross_close_svg | safe }}
</button>
</div>
@@ -27,8 +27,8 @@
</section>
{% endfor %}
{% if err_messages %}
<section class="py-4 px-6" id="err_messages_{{ err_messages[0][0] }}" role="alert">
<div class="lg:container mx-auto">
<section class="py-4" id="err_messages_{{ err_messages[0][0] }}" role="alert">
<div class="container px-4 mx-auto">
<div class="p-6 text-green-800 rounded-lg bg-red-50 border border-red-400 dark:bg-gray-500 dark:text-red-400 rounded-md">
<div class="flex flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2">
@@ -47,7 +47,7 @@
</div>
</div>
<div class="w-auto p-2">
<button type="button" class="ml-auto bg-red-100 text-red-500 rounded-lg focus:ring-0 focus:ring-red-400 p-1.5 hover:bg-red-200 inline-flex h-8 w-8 focus:outline-none inline-flex items-center justify-center h-8 w-8 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700" onclick="document.getElementById('err_messages_{{ err_messages[0][0] }}').style.display='none';" aria-label="Close">
<button type="button" class="ml-auto bg-red-100 text-red-500 rounded-lg focus:ring-0 focus:ring-red-400 p-1.5 hover:bg-red-200 inline-flex h-8 w-8 focus:outline-none inline-flex items-center justify-center h-8 w-8 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700" data-dismiss-target="#err_messages_{{ err_messages[0][0] }}" aria-label="Close">
<span class="sr-only">Close</span>
{{ red_cross_close_svg | safe }}
</button>

View File

@@ -1,72 +0,0 @@
{% from 'style.html' import circular_error_messages_svg %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
<script>
const isDarkMode = localStorage.getItem('color-theme') === 'dark' || (!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDarkMode) {
document.documentElement.classList.add('dark');
}
</script>
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
<title>(BSX) BasicSwap - Login - v{{ version }}</title>
</head>
<body class="dark:bg-gray-700">
<section class="py-24 md:py-32">
<div class="container px-4 mx-auto">
<div class="max-w-sm mx-auto">
<div class="mb-6 text-center">
<a class="inline-block mb-6" href="#">
<img src="/static/images/logos/basicswap-logo.svg" class="h-20 imageshow dark-image" style="display: none;">
<img src="/static/images/logos/basicswap-logo-dark.svg" class="h-20 imageshow light-image" style="display: block;">
</a>
<h3 class="mb-4 text-2xl md:text-3xl font-bold dark:text-white">Login Required</h3>
<p class="text-lg text-coolGray-500 font-medium dark:text-gray-300">Please enter the password to access BasicSwap.</p>
</div>
{% for m in err_messages %}
<section class="py-4" id="err_messages_{{ m[0] }}" role="alert">
<div class="container px-4 mx-auto">
<div class="p-4 text-red-800 rounded-lg bg-red-50 border border-red-400 dark:bg-gray-600 dark:text-red-300 rounded-md">
<div class="flex flex-wrap items-center -m-1">
<div class="w-auto p-1"> {{ circular_error_messages_svg | safe }} </div>
<p class="ml-2 font-medium text-sm">{{ m[1] }}</p>
</div>
</div>
</div>
</section>
{% endfor %}
<form method="post" action="/login" autocomplete="off">
<div class="mb-6">
<label class="block mb-2 text-coolGray-800 font-medium dark:text-white" for="password">Password</label>
<input class="appearance-none block w-full p-3 leading-5 text-coolGray-900 border border-coolGray-200 rounded-lg shadow-md placeholder-coolGray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 dark:bg-gray-600 dark:border-gray-500 dark:text-white"
type="password" name="password" id="password" required autocomplete="current-password">
</div>
<button class="inline-block py-3 px-7 mb-6 w-full text-base text-blue-50 font-medium text-center leading-6 bg-blue-500 hover:bg-blue-600 rounded-md shadow-sm"
type="submit">Login</button>
<p class="text-center">
<span class="text-xs font-medium text-coolGray-500 dark:text-gray-500">{{ title }}</span>
</p>
</form>
</div>
</div>
</section>
<script>
document.addEventListener('DOMContentLoaded', () => {
function toggleImages() {
const isDark = document.documentElement.classList.contains('dark');
const darkImages = document.querySelectorAll('.dark-image');
const lightImages = document.querySelectorAll('.light-image');
darkImages.forEach(img => img.style.display = isDark ? 'block' : 'none');
lightImages.forEach(img => img.style.display = isDark ? 'none' : 'block');
}
toggleImages();
});
</script>
</body>
</html>

View File

@@ -110,7 +110,7 @@
</tr>
<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">Swap Type</td>
<td class="py-3 px-6">{{ data.swap_type }}{% if data.reverse == true %} (Transposed){% endif %}</td>
<td class="py-3 px-6">{{ data.swap_type }}{% if data.reverse == true %} (Reversed){% endif %}</td>
</tr>
<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">{% if data.sent %}You Send{% else %}You Get{% endif %}</td>
@@ -219,17 +219,16 @@
<td class="py-3 px-6 bold">Revoked</td>
<td class="py-3 px-6">{{ data.was_revoked }}</td>
</tr>
{% if data.sent %}
<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">Auto Accept Type</td>
<td class="py-3 px-6 bold">Auto Accept Strategy</td>
<td class="py-3 px-6">
{% if data.auto_accept_type is none %} Unknown
{% elif data.auto_accept_type == 0 %} Bids are accepted manually
{% elif data.auto_accept_type == 1 %} Bids are accepted automatically
{% elif data.auto_accept_type == 2 %} Bids are accepted automatically from known identities
{% else %} Unknown ({{ data.auto_accept_type }})
{% if data.automation_strat_id == -1 %} None {% else %}
<a href="/automationstrategy/{{ data.automation_strat_id }}">{{ data.automation_strat_label }}</a>
{% endif %}
</td>
</tr>
{% endif %}
{% if data.xmr_type == true %}
<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">Chain A offer fee rate</td>
@@ -483,7 +482,7 @@ if (document.readyState === 'loading') {
name="bid_amount_send"
value=""
max="{{ data.amt_to }}"
onchange="validateMaxAmount(this, parseFloat('{{ data.amt_to }}')); updateBidParams('sending');">
onchange="validateMaxAmount(this, {{ data.amt_to }}); updateBidParams('sending');">
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none text-gray-400 dark:text-gray-300 text-sm">
max {{ data.amt_to }} ({{ data.tla_to }})
</div>
@@ -506,7 +505,7 @@ if (document.readyState === 'loading') {
name="bid_amount"
value=""
max="{{ data.amt_from }}"
onchange="validateMaxAmount(this, parseFloat('{{ data.amt_from }}')); updateBidParams('receiving');">
onchange="validateMaxAmount(this, {{ data.amt_from }}); updateBidParams('receiving');">
<div class="absolute inset-y-0 right-3 flex items-center pointer-events-none text-gray-400 dark:text-gray-300 text-sm">
max {{ data.amt_from }} ({{ data.tla_from }})
</div>
@@ -846,27 +845,6 @@ function validateMaxAmount(input, maxAmount) {
}
function showConfirmModal() {
const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidAmountInput = document.getElementById('bid_amount');
let sendAmount = 0;
let receiveAmount = 0;
if (bidAmountSendInput && bidAmountSendInput.value) {
sendAmount = parseFloat(bidAmountSendInput.value) || 0;
}
if (bidAmountInput && bidAmountInput.value) {
receiveAmount = parseFloat(bidAmountInput.value) || 0;
}
if (sendAmount <= 0 && bidAmountSendInput && !bidAmountSendInput.disabled) {
return false;
}
if (receiveAmount <= 0 && bidAmountInput && !bidAmountInput.disabled) {
return false;
}
updateModalValues();
const modal = document.getElementById('confirmModal');
if (modal) {

View File

@@ -1,4 +1,5 @@
{% include 'header.html' %} {% from 'style.html' import breadcrumb_line_svg, input_down_arrow_offer_svg, select_network_svg, select_address_svg, select_swap_type_svg, select_bid_amount_svg, select_rate_svg, step_one_svg, step_two_svg, step_three_svg, select_setup_svg, input_time_svg, select_target_svg , select_auto_strategy_svg %}
<script src="static/js/coin_icons.js"></script>
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">

View File

@@ -1,4 +1,5 @@
{% include 'header.html' %} {% from 'style.html' import breadcrumb_line_svg, input_down_arrow_offer_svg, select_network_svg, select_address_svg, select_swap_type_svg, select_bid_amount_svg, select_rate_svg, step_one_svg, step_two_svg, step_three_svg %}
<script src="static/js/coin_icons.js"></script>
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
@@ -69,7 +70,7 @@
<div class="container px-4 mx-auto">
<div class="p-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="flex flex-wrap items-center justify-between -mx-4 pb-6 border-gray-400 border-opacity-20"> </div>
<form method="post" autocomplete="off" id="form">
<form method="post" autocomplete="off" id='form'>
<div class="py-3 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
<div class="w-full md:w-10/12">
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
@@ -116,6 +117,63 @@
</div>
</div>
</div>
<script>
function handleNewOfferAddress() {
const selectElement = document.querySelector('select[name="addr_from"]');
const STORAGE_KEY = 'lastUsedAddressNewOffer';
const form = selectElement?.closest('form');
if (!selectElement || !form) return;
function loadInitialAddress() {
const savedAddressJSON = localStorage.getItem(STORAGE_KEY);
if (savedAddressJSON) {
try {
const savedAddress = JSON.parse(savedAddressJSON);
selectElement.value = savedAddress.value;
} catch (e) {
selectFirstAddress();
}
} else {
selectFirstAddress();
}
}
function selectFirstAddress() {
if (selectElement.options.length > 1) {
const firstOption = selectElement.options[1];
if (firstOption) {
selectElement.value = firstOption.value;
saveAddress(firstOption.value, firstOption.text);
}
}
}
function saveAddress(value, text) {
const addressData = {
value: value,
text: text
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(addressData));
}
form.addEventListener('submit', async (e) => {
saveAddress(selectElement.value, selectElement.selectedOptions[0].text);
});
selectElement.addEventListener('change', (event) => {
saveAddress(event.target.value, event.target.selectedOptions[0].text);
});
loadInitialAddress();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', handleNewOfferAddress);
} else {
handleNewOfferAddress();
}
</script>
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
<div class="w-full md:w-10/12">
@@ -236,7 +294,7 @@
<button type="button" id="get_rate_inferred_button" class="px-4 py-2.5 text-sm font-medium text-white bg-blue-500 hover:bg-blue-600 rounded-md shadow-sm focus:outline-none">Get Rate Inferred</button>
</div>
<div class="flex form-check form-check-inline mt-5">
<div class="flex items-center h-5"> <input class="form-check-input hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_lock" name="rate_lock" value="rl" checked="checked"> </div>
<div class="flex items-center h-5"> <input class="form-check-input hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_lock" name="rate_lock" value="rl" checked=checked> </div>
<div class="ml-2 text-sm">
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox1">Lock Rate</label>
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300">Automatically adjusts the <b>You Get</b> value based on the rate youve entered. Without it, the rate value is automatically adjusted based on the number of coins you put in <b>You Get.</b></p>
@@ -248,6 +306,7 @@
</div>
</div>
</div>
{% if debug_mode == true %}
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
<div class="w-full md:w-10/12">
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
@@ -257,11 +316,7 @@
<div class="w-full md:flex-1 p-3">
<div class="flex form-check form-check-inline">
<div class="flex items-center h-5">
{% if debug_ui_mode == true %}
<input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="amt_var" name="amt_var" value="av">
{% else %}
<input class="cursor-not-allowed hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="amt_var" name="amt_var" value="av" checked disabled>
{% endif %}
</div>
<div class="ml-2 text-sm">
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox2">Amount Variable</label>
@@ -270,11 +325,7 @@
</div>
<div class="flex mt-2 form-check form-check-inline">
<div class="flex items-center h-5">
{% if debug_ui_mode == true %}
<input class="hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_var" name="rate_var" value="rv">
{% else %}
<input class="cursor-not-allowed hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_var" name="rate_var" value="rv" disabled>
{% endif %}
</div>
<div class="ml-2 text-sm">
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox3">Rate Variable</label>
@@ -285,6 +336,37 @@
</div>
</div>
</div>
{% else %}
<div class="py-0 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
<div class="w-full md:w-10/12">
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
<div class="w-full md:w-1/3 p-6">
<p class="text-sm text-coolGray-800 dark:text-white font-semibold">Options</p>
</div>
<div class="w-full md:flex-1 p-3">
<div class="flex form-check form-check-inline">
<div class="flex items-center h-5">
<input class="cursor-not-allowed hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="amt_var" name="amt_var" value="av" checked disabled>
</div>
<div class="ml-2 text-sm">
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox2" style="opacity: 0.40;">Amount Variable</label>
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300" style="opacity: 0.40;">Allow bids with a different amount to the offer.</p>
</div>
</div>
<div class="flex mt-2 form-check form-check-inline">
<div class="flex items-center h-5">
<input class="cursor-not-allowed hover:border-blue-500 w-5 h-5 form-check-input text-blue-600 bg-gray-50 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-1 dark:bg-gray-500 dark:border-gray-400" type="checkbox" id="rate_var" name="rate_var" value="rv" disabled>
</div>
<div class="ml-2 text-sm">
<label class="form-check-label text-sm font-medium text-gray-800 dark:text-white" for="inlineCheckbox3" style="opacity: 0.40;">Rate Variable</label>
<p id="helper-checkbox-text" class="text-xs font-normal text-gray-500 dark:text-gray-300" style="opacity: 0.40;">Allow bids with a different rate to the offer.</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<div class="pricejsonhidden hidden py-3 border-b items-center justify-between -mx-4 mb-6 pb-3 border-gray-400 border-opacity-20">
<div class="w-full md:w-10/12">
<div class="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
@@ -331,6 +413,225 @@
</div>
</div>
</section>
<script>
const xhr_rates = new XMLHttpRequest();
xhr_rates.onload = () => {
if (xhr_rates.status == 200) {
const obj = JSON.parse(xhr_rates.response);
inner_html = '<pre><code>' + JSON.stringify(obj, null, ' ') + '</code></pre>';
document.getElementById('rates_display').innerHTML = inner_html;
}
};
const xhr_rate = new XMLHttpRequest();
xhr_rate.onload = () => {
if (xhr_rate.status == 200) {
const obj = JSON.parse(xhr_rate.response);
if (obj.hasOwnProperty('rate')) {
document.getElementById('rate').value = obj['rate'];
} else if (obj.hasOwnProperty('amount_to')) {
document.getElementById('amt_to').value = obj['amount_to'];
} else if (obj.hasOwnProperty('amount_from')) {
document.getElementById('amt_from').value = obj['amount_from'];
}
}
};
function lookup_rates() {
const coin_from = document.getElementById('coin_from').value;
const coin_to = document.getElementById('coin_to').value;
if (coin_from === '-1' || coin_to === '-1') {
alert('Coins from and to must be set first.');
return;
}
const selectedCoin = (coin_from === '15') ? '3' : coin_from;
inner_html = '<p>Updating...</p>';
document.getElementById('rates_display').innerHTML = inner_html;
document.querySelector(".pricejsonhidden").classList.remove("hidden");
const xhr_rates = new XMLHttpRequest();
xhr_rates.onreadystatechange = function() {
if (xhr_rates.readyState === XMLHttpRequest.DONE) {
if (xhr_rates.status === 200) {
document.getElementById('rates_display').innerHTML = xhr_rates.responseText;
} else {
console.error('Error fetching data:', xhr_rates.statusText);
}
}
};
xhr_rates.open('POST', '/json/rates');
xhr_rates.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr_rates.send('coin_from=' + selectedCoin + '&coin_to=' + coin_to);
}
function getRateInferred(event) {
event.preventDefault();
const coin_from = document.getElementById('coin_from').value;
const coin_to = document.getElementById('coin_to').value;
const params = 'coin_from=' + encodeURIComponent(coin_from) + '&coin_to=' + encodeURIComponent(coin_to);
const xhr_rates = new XMLHttpRequest();
xhr_rates.onreadystatechange = function() {
if (xhr_rates.readyState === XMLHttpRequest.DONE) {
if (xhr_rates.status === 200) {
try {
const responseData = JSON.parse(xhr_rates.responseText);
if (responseData.coingecko && responseData.coingecko.rate_inferred) {
const rateInferred = responseData.coingecko.rate_inferred;
document.getElementById('rate').value = rateInferred;
set_rate('rate');
} else {
document.getElementById('rate').value = 'Error: Rate limit';
console.error('Rate limit reached or invalid response format');
}
} catch (error) {
document.getElementById('rate').value = 'Error: Rate limit';
console.error('Error parsing response:', error);
}
} else {
document.getElementById('rate').value = 'Error: Rate limit';
console.error('Error fetching data:', xhr_rates.statusText);
}
}
};
xhr_rates.open('POST', '/json/rates');
xhr_rates.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr_rates.send(params);
}
document.getElementById('get_rate_inferred_button').addEventListener('click', getRateInferred);
function set_swap_type_enabled(coin_from, coin_to, swap_type) {
const adaptor_sig_only_coins = [
'6', /* XMR */
'9', /* WOW */
'8', /* PART_ANON */
'7', /* PART_BLIND */
'13', /* FIRO */
'18', /* DOGE */
'17' /* BCH */
];
const secret_hash_only_coins = [
'11', /* PIVX */
'12' /* DASH */
];
let make_hidden = false;
coin_from = String(coin_from);
coin_to = String(coin_to);
if (adaptor_sig_only_coins.indexOf(coin_from) !== -1 || adaptor_sig_only_coins.indexOf(coin_to) !== -1) {
swap_type.disabled = true;
swap_type.value = 'xmr_swap';
make_hidden = true;
swap_type.classList.add('select-disabled');
} else if (secret_hash_only_coins.indexOf(coin_from) !== -1 || secret_hash_only_coins.indexOf(coin_to) !== -1) {
swap_type.disabled = true;
swap_type.value = 'seller_first';
make_hidden = true;
swap_type.classList.add('select-disabled');
} else {
swap_type.disabled = false;
swap_type.classList.remove('select-disabled');
swap_type.value = 'xmr_swap';
}
let swap_type_hidden = document.getElementById('swap_type_hidden');
if (make_hidden) {
if (!swap_type_hidden) {
swap_type_hidden = document.createElement('input');
swap_type_hidden.setAttribute('id', 'swap_type_hidden');
swap_type_hidden.setAttribute('type', 'hidden');
swap_type_hidden.setAttribute('name', 'swap_type');
document.getElementById('form').appendChild(swap_type_hidden);
}
swap_type_hidden.setAttribute('value', swap_type.value);
} else if (swap_type_hidden) {
swap_type_hidden.parentNode.removeChild(swap_type_hidden);
}
}
document.addEventListener('DOMContentLoaded', function() {
const coin_from = document.getElementById('coin_from');
const coin_to = document.getElementById('coin_to');
if (coin_from && coin_to) {
coin_from.addEventListener('change', function() {
const swap_type = document.getElementById('swap_type');
set_swap_type_enabled(this.value, coin_to.value, swap_type);
});
coin_to.addEventListener('change', function() {
const swap_type = document.getElementById('swap_type');
set_swap_type_enabled(coin_from.value, this.value, swap_type);
});
}
});
function set_rate(value_changed) {
const coin_from = document.getElementById('coin_from').value;
const coin_to = document.getElementById('coin_to').value;
const amt_from = document.getElementById('amt_from').value;
const amt_to = document.getElementById('amt_to').value;
const rate = document.getElementById('rate').value;
const lock_rate = rate == '' ? false : document.getElementById('rate_lock').checked;
if (value_changed === 'coin_from' || value_changed === 'coin_to') {
document.getElementById('rate').value = '';
return;
}
const swap_type = document.getElementById('swap_type');
set_swap_type_enabled(coin_from, coin_to, swap_type);
if (coin_from == '-1' || coin_to == '-1') {
return;
}
let params = 'coin_from=' + coin_from + '&coin_to=' + coin_to;
if (value_changed == 'rate' || (lock_rate && value_changed == 'amt_from') || (amt_to == '' && value_changed == 'amt_from')) {
if (rate == '' || (amt_from == '' && amt_to == '')) {
return;
} else if (amt_from == '' && amt_to != '') {
if (value_changed == 'amt_from') {
return;
}
params += '&rate=' + rate + '&amt_to=' + amt_to;
} else {
params += '&rate=' + rate + '&amt_from=' + amt_from;
}
} else if (lock_rate && value_changed == 'amt_to') {
if (amt_to == '' || rate == '') {
return;
}
params += '&amt_to=' + amt_to + '&rate=' + rate;
} else {
if (amt_from == '' || amt_to == '') {
return;
}
params += '&amt_from=' + amt_from + '&amt_to=' + amt_to;
}
xhr_rate.open('POST', '/json/rate');
xhr_rate.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr_rate.send(params);
}
document.addEventListener("DOMContentLoaded", function() {
const coin_from = document.getElementById('coin_from').value;
const coin_to = document.getElementById('coin_to').value;
const swap_type = document.getElementById('swap_type');
set_swap_type_enabled(coin_from, coin_to, swap_type);
});
</script>
</div>
<script src="static/js/new_offer.js"></script>
{% include 'footer.html' %}

View File

@@ -1,4 +1,5 @@
{% include 'header.html' %} {% from 'style.html' import breadcrumb_line_svg, input_down_arrow_offer_svg, select_network_svg, select_address_svg, select_swap_type_svg, select_bid_amount_svg, select_rate_svg, step_one_svg, step_two_svg, step_three_svg, select_setup_svg, input_time_svg, select_target_svg , select_auto_strategy_svg %}
<script src="static/js/coin_icons.js"></script>
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">

View File

@@ -8,6 +8,22 @@
};
</script>
<script>
function getAPIKeys() {
return {
cryptoCompare: "{{ chart_api_key|safe }}",
coinGecko: "{{ coingecko_api_key|safe }}"
};
}
function getWebSocketConfig() {
return {
port: "{{ ws_port|safe }}",
fallbackPort: "11700"
};
}
</script>
<section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-gray-500 rounded-md overflow-hidden">
@@ -136,12 +152,11 @@
'ETH': {'name': 'Ethereum', 'symbol': 'ETH', 'image': 'Ethereum.png', 'show': false},
'DOGE': {'name': 'Dogecoin', 'symbol': 'DOGE', 'image': 'Dogecoin.png', 'show': true},
'DCR': {'name': 'Decred', 'symbol': 'DCR', 'image': 'Decred.png', 'show': true},
'NMC': {'name': 'Namecoin', 'symbol': 'NMC', 'image': 'Namecoin.png', 'show': true},
'ZANO': {'name': 'Zano', 'symbol': 'ZANO', 'image': 'Zano.png', 'show': false},
'WOW': {'name': 'Wownero', 'symbol': 'WOW', 'image': 'Wownero.png', 'show': true}
}
%}
{% set custom_order = ['BTC', 'ETH', 'XMR', 'PART', 'LTC', 'BCH', 'FIRO', 'PIVX', 'DASH', 'DOGE', 'DCR', 'NMC', 'ZANO', 'WOW'] %}
{% set custom_order = ['BTC', 'ETH', 'XMR', 'PART', 'LTC', 'BCH', 'FIRO', 'PIVX', 'DASH', 'DOGE', 'DCR', 'ZANO', 'WOW'] %}
{% if enabled_chart_coins is string %}
{% if enabled_chart_coins == "" %}
@@ -194,72 +209,54 @@
</div>
</div>
</section>
<script src="/static/js/pricechart.js"></script>
{% endif %}
{% endif %}
<script src="/static/js/pricechart.js"></script>
<section>
<div class="px-6 py-0 h-full overflow-hidden">
<div class="px-6 py-0 mt-5 h-full overflow-hidden">
<div class="border-coolGray-100">
<div class="flex flex-wrap items-center justify-between">
<div class="w-full mx-auto pt-2">
<form method="post" id="filterForm">
<div class="pb-6 mt-6 border-coolGray-100">
<div class="flex flex-wrap justify-center -m-1.5">
<div class="w-full md:w-auto p-1.5 hover-container">
<div class="flex justify-center md:justify-start">
<div class="flex items-center justify-center pb-4 dark:text-white">
<div class="rounded-b-md">
<div class="w-full md:w-0/12">
<div class="lg:container flex flex-wrap justify-center">
<div class="md:w-auto hover-container">
<div class="flex flex-wrap">
<div class="pt-3 px-3 md:w-auto hover-container">
<div class="flex">
<button id="coin_to_button" class="bg-gray-50 text-gray-900 appearance-none w-10 dark:bg-gray-500 dark:text-white border-l border-t border-b border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-l-lg flex items-center" disabled></button>
<div class="relative">
{{ input_arrow_down_svg | safe }}
<button type="button" id="coin_to_button" class="bg-gray-50 text-gray-900 appearance-none pr-10 pl-5 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none block w-full p-2.5 text-left focus:ring-0 whitespace-nowrap">
<span id="coin_to_text" class="filter-button-text">Filter {% if sent_offers %}Receiving{% else %}Bids{% endif %}</span>
</button>
<div id="coin_to_dropdown" class="multi-select-dropdown bg-gray-50 dark:bg-gray-500 border border-gray-300 dark:border-gray-400 rounded-lg shadow-lg max-h-64 overflow-y-auto" style="display: none; position: absolute; z-index: 1000; min-width: 200px; top: 100%; left: 0;">
<div class="p-2">
<label class="flex items-center p-2 hover:bg-gray-200 dark:hover:bg-gray-600 rounded cursor-pointer">
<input type="checkbox" name="coin_to" value="any" class="mr-3 coin-to-checkbox" {% if filters.coin_to==-1 %}checked{% endif %}>
<span class="text-sm text-gray-900 dark:text-white">Any (clear all)</span>
</label>
<select name="coin_to" id="coin_to" class="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-50 text-sm rounded-none rounded-r-lg outline-none block w-full p-2.5 focus:ring-0">
<option value="any" {% if filters.coin_to==-1 %} selected{% endif %}>Filter {% if sent_offers %}Receiving{% else %}Bids{% endif %}</option>
{% for c in coins %}
<label class="flex items-center p-2 hover:bg-gray-200 dark:hover:bg-gray-600 rounded cursor-pointer">
<input type="checkbox" name="coin_to" value="{{ c[0] }}" class="mr-3 coin-to-checkbox" {% if filters.coin_to==c[0] %}checked{% endif %}>
<img src="/static/images/coins/{{ c[1]|replace(" ", "%20") }}.png" class="w-4 h-4 mr-2" alt="{{ c[1] }}">
<span class="text-sm text-gray-900 dark:text-white">{{ c[1] }}</span>
</label>
<option class="text-sm" value="{{ c[0] }}" {% if filters.coin_to==c[0] %} selected{% endif %} data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}.png">{{ c[1] }}</option>
{% endfor %}
</div>
</div>
</select>
</div>
<div class="flex items-center">
<div class="w-full md:w-auto p-1.5">
<p class="text-sm font-heading text-gray-500 dark:text-white">{{ arrow_right_svg | safe }}</p>
<p class="text-sm font-heading">{{ arrow_right_svg | safe }}</p>
</div>
</div>
<button id="coin_from_button" class="bg-gray-50 text-gray-900 appearance-none w-10 dark:bg-gray-500 dark:text-white border-l border-t border-b border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-l-lg flex items-center" disabled></button>
<div class="relative">
{{ input_arrow_down_svg | safe }}
<button type="button" id="coin_from_button" class="bg-gray-50 text-gray-900 appearance-none pr-10 pl-5 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none block w-full p-2.5 text-left focus:ring-0 whitespace-nowrap">
<span id="coin_from_text" class="filter-button-text">Filter {% if sent_offers %}Sending{% else %}Offers{% endif %}</span>
</button>
<div id="coin_from_dropdown" class="multi-select-dropdown bg-gray-50 dark:bg-gray-500 border border-gray-300 dark:border-gray-400 rounded-lg shadow-lg max-h-64 overflow-y-auto" style="display: none; position: absolute; z-index: 1000; min-width: 200px; top: 100%; left: 0;">
<div class="p-2">
<label class="flex items-center p-2 hover:bg-gray-200 dark:hover:bg-gray-600 rounded cursor-pointer">
<input type="checkbox" name="coin_from" value="any" class="mr-3 coin-from-checkbox" {% if filters.coin_from==-1 %}checked{% endif %}>
<span class="text-sm text-gray-900 dark:text-white">Any (clear all)</span>
</label>
<select name="coin_from" id="coin_from" class="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-50 text-sm rounded-none rounded-r-lg outline-none block w-full p-2.5 focus:ring-0">
<option value="any" {% if filters.coin_from==-1 %} selected{% endif %}>Filter {% if sent_offers %}Sending{% else %}Offers{% endif %}</option>
{% for c in coins_from %}
<label class="flex items-center p-2 hover:bg-gray-200 dark:hover:bg-gray-600 rounded cursor-pointer">
<input type="checkbox" name="coin_from" value="{{ c[0] }}" class="mr-3 coin-from-checkbox" {% if filters.coin_from==c[0] %}checked{% endif %}>
<img src="/static/images/coins/{{ c[1]|replace(" ", "%20") }}.png" class="w-4 h-4 mr-2" alt="{{ c[1] }}">
<span class="text-sm text-gray-900 dark:text-white">{{ c[1] }}</span>
</label>
<option class="text-sm" value="{{ c[0] }}" {% if filters.coin_from==c[0] %} selected{% endif %} data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}.png">{{ c[1] }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
</div>
</div>
{% if sent_offers %}
<div class="w-full md:w-auto p-1.5">
<div class="pt-3 px-3 md:w-auto hover-container">
<div class="flex">
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select name="status" id="status" class="bg-gray-50 text-gray-900 appearance-none pr-10 pl-5 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none block w-full p-2.5 focus:ring-0">
@@ -270,21 +267,12 @@
</select>
</div>
</div>
</div>
{% endif %}
<div class="w-full md:w-auto p-1.5">
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select name="auto_accept_type" id="auto_accept_type" class="bg-gray-50 text-gray-900 appearance-none pr-10 pl-5 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none block w-full p-2.5 focus:ring-0">
<option value="any" {% if filters.auto_accept_type == 'any' %} selected{% endif %}>Auto Accept Type</option>
<option value="0" {% if filters.auto_accept_type == 0 %} selected{% endif %}>Manual</option>
<option value="1" {% if filters.auto_accept_type == 1 %} selected{% endif %}>Automatic</option>
<option value="2" {% if filters.auto_accept_type == 2 %} selected{% endif %}>Known Identities</option>
</select>
</div>
</div>
<div class="w-full md:w-auto p-1.5">
<div class="pt-3 px-3 md:w-auto hover-container">
<div class="flex">
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select name="sent_from" id="sent_from" class="bg-gray-50 text-gray-900 appearance-none pr-10 pl-5 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none block w-full p-2.5 focus:ring-0">
@@ -294,17 +282,17 @@
</select>
</div>
</div>
<div class="w-full md:w-auto p-1.5">
</div>
<div class="w-full lg:w-auto pt-3 px-3">
<div class="relative">
<button type="button" id="clearFilters" class="transition-opacity duration-200 flex justify-center w-full px-4 py-2.5 font-medium text-sm hover:text-white dark:text-white dark:bg-gray-500 bg-coolGray-200 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-coolGray-200 dark:border-gray-400 rounded-md shadow-button focus:ring-0 focus:outline-none" disabled>
<button type="button" id="clearFilters" class="transition-opacity duration-200 flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm hover:text-white dark:text-white dark:bg-gray-500 bg-coolGray-200 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-coolGray-200 dark:border-gray-400 rounded-md shadow-button focus:ring-0 focus:outline-none" disabled>
<span>Clear Filters</span>
</button>
</div>
</div>
<div class="w-full md:w-auto p-1.5">
<div class="w-full lg:w-auto pt-3 px-3">
<div class="relative">
<button type="button" id="refreshOffers" class="flex justify-center w-full px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
<button type="button" id="refreshOffers" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg id="refreshIcon" class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
@@ -314,13 +302,9 @@
</div>
</div>
</div>
</div>
</div>
</form>
<div id="selected_coins_container" class="w-full py-3" style="display: none;">
<div class="flex flex-wrap justify-center gap-2">
<div id="coin_to_badges" class="flex flex-wrap gap-1"></div>
<div id="coin_from_badges" class="flex flex-wrap gap-1"></div>
</div>
</div>
</div>
</div>
</div>
@@ -328,7 +312,7 @@
</section>
<section>
<div class="lg:container mx-auto lg:px-0 px-6">
<div class="mt-5 lg:container mx-auto lg:px-0 px-6">
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-0">
<div class="w-auto mt-6 overflow-auto lg:overflow-hidden">
@@ -374,16 +358,16 @@
{% endif %}
</div>
</th>
<th class="p-0" data-sortable="true" data-column-index="6">
<th class="p-0" data-sortable="true" data-column-index="5">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-right flex items-center justify-end">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Rate</span>
<span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-6"></span>
<span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-5"></span>
</div>
</th>
<th class="p-0" data-sortable="true" data-column-index="7">
<th class="p-0" data-sortable="true" data-column-index="6">
<div class="py-3 bg-coolGray-200 dark:bg-gray-600 text-center flex items-center justify-center">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Market +/-</span>
<span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-7"></span>
<span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-6"></span>
</div>
</th>
<th class="p-0">
@@ -433,5 +417,4 @@
<input type="hidden" name="formid" value="{{ form_id }}">
<script src="/static/js/offers.js"></script>
{% include 'footer.html' %}

File diff suppressed because it is too large Load Diff

View File

@@ -471,48 +471,14 @@
<svg class="text-gray-500 w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#ffffff" stroke-linejoin="round">
<line data-cap="butt" x1="5" y1="1" x2="5" y2="6" stroke="#ffffff"></line>
<line x1="3" y1="1" x2="7" y2="1" stroke="#ffffff"></line>
<line x1="3" y1="1" x2="7" y2="1" stroke="#fffffwww"></line>
<line data-cap="butt" x1="19" y1="1" x2="19" y2="6" stroke="#ffffff"></line>
<line x1="17" y1="1" x2="21" y2="1" stroke="#ffffff"></line>
<rect x="6" y="15" width="12" height="4" stroke="#ffffff"></rect>
<line data-cap="butt" x1="10" y1="19" x2="10" y2="15" stroke="#ffffff"></line>
<line data-cap="butt" x1="14" y1="19" x2="14" y2="15" stroke="#ffffff"></line>
<line x1="6" y1="11" x2="8" y2="11" stroke="#ffffff"></line>
<line x1="16" y1="11" x2="18" y2="11" stroke="#ffffff"></line>
<polygon points="23 6 5 6 1 6 1 23 23 23 23 6"></polygon>
</g>
</svg>
' %}
{% set amm_active_svg = '
<svg class="text-gray-500 w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#2ad167" stroke-linejoin="round">
<line data-cap="butt" x1="5" y1="1" x2="5" y2="6" stroke="#2ad167"></line>
<line x1="3" y1="1" x2="7" y2="1" stroke="#2ad167"></line>
<line data-cap="butt" x1="19" y1="1" x2="19" y2="6" stroke="#2ad167"></line>
<line x1="17" y1="1" x2="21" y2="1" stroke="#2ad167"></line>
<rect x="6" y="15" width="12" height="4" stroke="#2ad167"></rect>
<line data-cap="butt" x1="10" y1="19" x2="10" y2="15" stroke="#2ad167"></line>
<line data-cap="butt" x1="14" y1="19" x2="14" y2="15" stroke="#2ad167"></line>
<line x1="6" y1="11" x2="8" y2="11" stroke="#2ad167"></line>
<line x1="16" y1="11" x2="18" y2="11" stroke="#2ad167"></line>
<polygon points="23 6 5 6 1 6 1 23 23 23 23 6"></polygon>
</g>
</svg>
' %}
{% set amm_inactive_svg = '
<svg class="text-gray-500 w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#f80b0b" stroke-linejoin="round">
<line data-cap="butt" x1="5" y1="1" x2="5" y2="6" stroke="#f80b0b"></line>
<line x1="3" y1="1" x2="7" y2="1" stroke="#f80b0b"></line>
<line data-cap="butt" x1="19" y1="1" x2="19" y2="6" stroke="#f80b0b"></line>
<line x1="17" y1="1" x2="21" y2="1" stroke="#f80b0b"></line>
<rect x="6" y="15" width="12" height="4" stroke="#f80b0b"></rect>
<line data-cap="butt" x1="10" y1="19" x2="10" y2="15" stroke="#f80b0b"></line>
<line data-cap="butt" x1="14" y1="19" x2="14" y2="15" stroke="#f80b0b"></line>
<line x1="6" y1="11" x2="8" y2="11" stroke="#f80b0b"></line>
<line x1="16" y1="11" x2="18" y2="11" stroke="#f80b0b"></line>
<line x1="16" y1="11" x2="18" y2="11" stroke="#fff"></line>
<polygon points="23 6 5 6 1 6 1 23 23 23 23 6"></polygon>
</g>
</svg>
@@ -536,20 +502,6 @@
</g>
</svg>
' %}
{% set mobile_love_svg = '
<svg class="w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#f80b0b" stroke-linejoin="round">
<path d="M21.243,3.757 c-2.343-2.343-6.142-2.343-8.485,0c-0.289,0.289-0.54,0.6-0.757,0.927c-0.217-0.327-0.469-0.639-0.757-0.927 c-2.343-2.343-6.142-2.343-8.485,0c-2.343,2.343-2.343,6.142,0,8.485L12,21.485l9.243-9.243C23.586,9.899,23.586,6.1,21.243,3.757z"></path>
</g>
</svg>
' %}
{% set donation_svg = '
<svg class="text-gray-500 w-5 h-5 mr-3" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#6b7280" stroke-linejoin="round">
<path d="M21.243,3.757 c-2.343-2.343-6.142-2.343-8.485,0c-0.289,0.289-0.54,0.6-0.757,0.927c-0.217-0.327-0.469-0.639-0.757-0.927 c-2.343-2.343-6.142-2.343-8.485,0c-2.343,2.343-2.343,6.142,0,8.485L12,21.485l9.243-9.243C23.586,9.899,23.586,6.1,21.243,3.757z"></path>
</g>
</svg>
' %}
{% set github_svg = '
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 0C4.0275 0 0 4.13211 0 9.22838C0 13.3065 2.5785 16.7648 6.15375 17.9841C6.60375 18.0709 6.76875 17.7853 6.76875 17.5403C6.76875 17.3212 6.76125 16.7405 6.7575 15.9712C4.254 16.5277 3.726 14.7332 3.726 14.7332C3.3165 13.6681 2.72475 13.3832 2.72475 13.3832C1.9095 12.8111 2.78775 12.8229 2.78775 12.8229C3.6915 12.887 4.16625 13.7737 4.16625 13.7737C4.96875 15.1847 6.273 14.777 6.7875 14.5414C6.8685 13.9443 7.10025 13.5381 7.3575 13.3073C5.35875 13.0764 3.258 12.2829 3.258 8.74709C3.258 7.73988 3.60675 6.91659 4.18425 6.27095C4.083 6.03774 3.77925 5.0994 4.263 3.82846C4.263 3.82846 5.01675 3.58116 6.738 4.77462C7.458 4.56958 8.223 4.46785 8.988 4.46315C9.753 4.46785 10.518 4.56958 11.238 4.77462C12.948 3.58116 13.7017 3.82846 13.7017 3.82846C14.1855 5.0994 13.8818 6.03774 13.7917 6.27095C14.3655 6.91659 14.7142 7.73988 14.7142 8.74709C14.7142 12.2923 12.6105 13.0725 10.608 13.2995C10.923 13.5765 11.2155 14.1423 11.2155 15.0071C11.2155 16.242 11.2043 17.2344 11.2043 17.5341C11.2043 17.7759 11.3617 18.0647 11.823 17.9723C15.4237 16.7609 18 13.3002 18 9.22838C18 4.13211 13.9703 0 9 0Z" fill="currentColor"></path>

View File

@@ -3,229 +3,130 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% if refresh %}
<meta http-equiv="refresh" content="{{ refresh }}">
{% endif %}
<title>(BSX) BasicSwap - v{{ version }}</title>
<link rel="icon" sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet">
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
<script src="/static/js/main.js"></script>
<script>
(function() {
const isDarkMode = localStorage.getItem('color-theme') === 'dark' ||
(!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
const isDarkMode =
localStorage.getItem('color-theme') === 'dark' ||
(!localStorage.getItem('color-theme') &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
if (!localStorage.getItem('color-theme')) {
localStorage.setItem('color-theme', 'dark');
localStorage.setItem('color-theme', isDarkMode ? 'dark' : 'light');
}
document.documentElement.classList.toggle('dark', isDarkMode);
})();
</script>
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
<title>(BSX) BasicSwap - v{{ version }}</title>
</head>
<body class="bg-gray-50 dark:bg-gray-900 min-h-screen flex items-center justify-center">
<div class="w-full max-w-md px-6">
<div class="bg-white dark:bg-gray-800 rounded-xl dark:shadow-lg p-8">
<div class="text-center mb-8">
<div class="mb-6">
<img src="/static/images/logos/basicswap-logo.svg" class="h-16 mx-auto imageshow dark-image">
<img src="/static/images/logos/basicswap-logo-dark.svg" class="h-16 mx-auto imageshow light-image">
</div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">Unlock BasicSwap</h1>
<p class="text-gray-600 dark:text-gray-400">Enter your password to access your wallets</p>
</div>
<body class="dark:bg-gray-700">
<section class="py-24 md:py-32">
<div class="container px-4 mx-auto">
<div class="max-w-sm mx-auto">
<div class="mb-3 text-center">
<a class="inline-block mb-6" href="#">
<img src="/static/images/logos/basicswap-logo.svg" class="h-20 imageshow dark-image">
<img src="/static/images/logos/basicswap-logo-dark.svg" class="h-20 imageshow light-image">
</a>
<p class="text-lg text-coolGray-500 font-medium mb-6 dark:text-white">Unlock your wallets</p>
{% for m in messages %}
<div class="mb-4 p-4 bg-green-50 border border-green-500 rounded-lg dark:bg-gray-500 dark:text-green-400" role="alert">
<div class="flex items-start">
<div class="flex-shrink-0">
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-green-800 dark:text-green-400">
This will unlock the system for all users!
</p>
<section class="py-4" id="messages_{{ m[0] }}" role="alert">
<div class="container px-4 mx-auto">
<div class="p-6 text-green-800 rounded-lg bg-green-50 border border-green-500 dark:bg-gray-500 dark:text-green-400 rounded-md">
<div class="flex flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2">
<div class="flex flex-wrap -m-1">
<div class="w-auto p-1"> {{ circular_info_messages_svg | safe }} </div>
<ul class="ml-4 mt-1">
<li class="font-semibold text-sm text-green-500 error_msg text-left"><span class="bold">ALERT:</span></li>
<li class="font-medium text-sm text-green-500 infomsg">This will unlock the system for all users!</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
{% endfor %}
{% for m in err_messages %}
<div class="mb-4 p-4 bg-red-50 border border-red-400 rounded-lg dark:bg-gray-500 dark:text-red-400" role="alert">
<div class="flex items-start">
<div class="flex-shrink-0">
<svg class="w-5 h-5 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-red-800 dark:text-red-400">
{{ m[1] }}
</p>
<section class="py-4" id="err_messages_{{ m[0] }}" role="alert">
<div class="container px-4 mx-auto">
<div class="p-6 text-green-800 rounded-lg bg-red-50 border border-red-400 dark:bg-gray-500 dark:text-red-400 rounded-md">
<div class="flex flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2">
<div class="flex flex-wrap -m-1">
<div class="w-auto p-1"> {{ circular_error_messages_svg | safe }} </div>
<ul class="ml-4 mt-1">
<li class="font-semibold text-sm text-red-500 error_msg text-left"><span class="bold">ERROR:</span></li>
<li class="font-medium text-sm text-red-500 error_msg">{{ m[1] }}</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
{% endfor %}
<form method="post" autocomplete="off" id="unlock-form">
<div class="mb-6">
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Password
</div>
<form method="post" autocomplete="off">
<div class="mb-4">
<label class="block mb-2 text-coolGray-800 font-medium dark:text-white" for="">Your Password</label>
<div class="relative w-full">
<div class="absolute inset-y-0 right-0 flex items-center px-2">
<input class="hidden js-password-toggle" id="toggle" type="checkbox" />
<label class="px-2 py-1 text-sm text-gray-600 font-mono cursor-pointer js-password-label" for="toggle">
<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g fill="#8896ab">
<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z" fill="#8896ab"></path>
</g>
</svg>
</label>
<div class="relative">
<input
type="password"
id="password"
name="password"
class="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-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0"
placeholder="Enter your password"
autocomplete="current-password"
required
autofocus
/>
<button
type="button"
id="toggle-password"
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
aria-label="Toggle password visibility"
>
<svg id="eye-open" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
<svg id="eye-closed" class="w-5 h-5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"></path>
</svg>
</button>
</div>
<div id="caps-warning" class="hidden mt-2 text-sm text-amber-700 dark:text-amber-300 flex items-center">
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg>
Caps Lock is on
<input class="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-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0 js-password" name="password" id="password" type="password" autocomplete="off" />
</div>
</div>
<button
type="submit"
name="unlock"
value="Unlock"
id="unlock-btn"
class="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-medium py-3 px-4 rounded-lg transition-colors focus:outline-none disabled:cursor-not-allowed"
>
<span id="unlock-text">Unlock</span>
<svg id="unlock-spinner" class="hidden animate-spin ml-2 h-5 w-5 text-white inline" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</button>
<button type="submit" name="unlock" value="Unlock" class="appearance-none focus:outline-none inline-block py-3 px-7 mb-6 w-full text-base text-blue-50 font-medium text-center leading-6 bg-blue-500 hover:bg-blue-600 focus:ring-0 rounded-md shadow-sm">Unlock</button>
<p class="text-center">
<span class="text-xs font-medium dark:text-white">Need help?</span>
<a class="inline-block text-xs font-medium text-blue-500 hover:text-blue-600 hover:underline" href="https://academy.particl.io/en/latest/faq/get_support.html" target="_blank">Help / Tutorials</a>
</p>
<p class="text-center">
<span class="text-xs font-medium text-coolGray-500 dark:text-gray-500">{{ title }}</span>
</p>
<input type="hidden" name="formid" value="{{ form_id }}">
</form>
<div class="mt-8 text-center space-y-2">
<p class="text-sm text-gray-600 dark:text-gray-400">
Need help?
<a href="https://academy.particl.io/en/latest/faq/get_support.html"
target="_blank"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline">
View tutorials
</a>
</p>
<p class="text-xs text-gray-500 dark:text-gray-500">
{{ title }}
</p>
</div>
</div>
</div>
</section>
<script>
document.addEventListener('DOMContentLoaded', function() {
const passwordInput = document.getElementById('password');
const toggleButton = document.getElementById('toggle-password');
const eyeOpen = document.getElementById('eye-open');
const eyeClosed = document.getElementById('eye-closed');
const capsWarning = document.getElementById('caps-warning');
const unlockForm = document.getElementById('unlock-form');
const unlockBtn = document.getElementById('unlock-btn');
const unlockText = document.getElementById('unlock-text');
const unlockSpinner = document.getElementById('unlock-spinner');
if (toggleButton && passwordInput) {
toggleButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const isPassword = passwordInput.type === 'password';
const cursorPosition = passwordInput.selectionStart;
const inputValue = passwordInput.value;
passwordInput.type = isPassword ? 'text' : 'password';
passwordInput.value = inputValue;
setTimeout(() => {
passwordInput.setSelectionRange(cursorPosition, cursorPosition);
}, 0);
if (isPassword) {
eyeOpen.classList.add('hidden');
eyeClosed.classList.remove('hidden');
document.addEventListener('DOMContentLoaded', () => {
// Password toggle functionality
const passwordToggle = document.querySelector('.js-password-toggle');
if (passwordToggle) {
passwordToggle.addEventListener('change', function() {
const password = document.querySelector('.js-password');
const passwordLabel = document.querySelector('.js-password-label');
if (password && passwordLabel) {
if (password.type === 'password') {
password.type = 'text';
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab"><path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z" fill="#8896ab"></path><path d="M12,3C6.292,3,2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z" fill="#8896ab"></path><path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path></g></svg>';
} else {
eyeOpen.classList.remove('hidden');
eyeClosed.classList.add('hidden');
password.type = 'password';
passwordLabel.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#8896ab" ><path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z" fill="#8896ab"></path></g></svg>';
}
});
toggleButton.addEventListener('mousedown', function(e) {
e.preventDefault();
});
}
if (passwordInput && capsWarning) {
passwordInput.addEventListener('keydown', function(e) {
const capsLockOn = e.getModifierState && e.getModifierState('CapsLock');
if (capsLockOn) {
capsWarning.classList.remove('hidden');
} else {
capsWarning.classList.add('hidden');
}
});
passwordInput.addEventListener('keyup', function(e) {
const capsLockOn = e.getModifierState && e.getModifierState('CapsLock');
if (!capsLockOn) {
capsWarning.classList.add('hidden');
}
});
}
if (unlockForm) {
unlockForm.addEventListener('submit', function(e) {
if (unlockBtn && unlockText && unlockSpinner) {
unlockBtn.disabled = true;
unlockText.textContent = 'Unlocking...';
unlockSpinner.classList.remove('hidden');
}
});
}
const errorMessages = document.querySelectorAll('[role="alert"]');
if (errorMessages.length > 0 && passwordInput) {
passwordInput.value = '';
passwordInput.focus();
}
if (passwordInput) {
passwordInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && unlockForm) {
unlockForm.submit();
password.focus();
}
});
}
// Image toggling function
function toggleImages() {
const html = document.querySelector('html');
const darkImages = document.querySelectorAll('.dark-image');
@@ -246,6 +147,42 @@
});
}
// Theme toggle functionality
function setTheme(theme) {
if (theme === 'light') {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
}
// Initialize theme
const themeToggle = document.getElementById('theme-toggle');
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
if (themeToggle && themeToggleDarkIcon && themeToggleLightIcon) {
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
}
themeToggle.addEventListener('click', () => {
if (localStorage.getItem('color-theme') === 'dark') {
setTheme('light');
} else {
setTheme('dark');
}
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
toggleImages();
});
}
// Call toggleImages on load
toggleImages();
});
</script>

Some files were not shown because too many files have changed in this diff Show More