mirror of
https://github.com/basicswap/basicswap.git
synced 2025-12-31 09:31:39 +01:00
Compare commits
1 Commits
v0.14.6
...
cryptoguar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
631ccea626 |
61
.github/workflows/ci.yml
vendored
61
.github/workflows/ci.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -8,8 +8,6 @@ __pycache__
|
||||
/*.eggs
|
||||
.tox
|
||||
.eggs
|
||||
.ruff_cache
|
||||
.pytest_cache
|
||||
*~
|
||||
|
||||
# geckodriver.log
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
name = "basicswap"
|
||||
|
||||
__version__ = "0.14.6"
|
||||
__version__ = "0.14.3"
|
||||
|
||||
@@ -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
@@ -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
@@ -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,76 +296,30 @@ 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):
|
||||
with open(pids_path) as fd:
|
||||
for ln in fd:
|
||||
# TODO: try close
|
||||
logger.warning("Found pid for daemon {}".format(ln.strip()))
|
||||
logger.warning("Found pid for daemon {} ".format(ln.strip()))
|
||||
|
||||
# Ensure daemons are stopped
|
||||
swap_client.stopDaemons()
|
||||
|
||||
# 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__":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
214
basicswap/db.py
214
basicswap/db.py
@@ -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:
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()])
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
"""
|
||||
Message 2 bytes msg_class, 4 bytes length, [ 2 bytes msg_type, payload ]
|
||||
Message 2 bytes msg_class, 4 bytes length, [ 2 bytes msg_type, payload ]
|
||||
|
||||
Handshake procedure:
|
||||
Handshake procedure:
|
||||
node0 connecting to node1
|
||||
node0 send_handshake
|
||||
node1 process_handshake
|
||||
@@ -16,7 +16,7 @@ Handshake procedure:
|
||||
node0 recv_ping
|
||||
Both nodes are initialised
|
||||
|
||||
XChaCha20_Poly1305 mac is 16bytes
|
||||
XChaCha20_Poly1305 mac is 16bytes
|
||||
"""
|
||||
|
||||
import time
|
||||
@@ -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
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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"])
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
14
basicswap/pgp/keys/namecoin_JeremyRand.pgp
Normal file
14
basicswap/pgp/keys/namecoin_JeremyRand.pgp
Normal 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-----
|
||||
@@ -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-----
|
||||
@@ -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-----
|
||||
|
||||
@@ -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-----
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
});
|
||||
@@ -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
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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
68
basicswap/static/js/coin_icons.js
Normal file
68
basicswap/static/js/coin_icons.js
Normal 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);
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
40
basicswap/static/js/main.js
Normal file
40
basicswap/static/js/main.js
Normal 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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
},
|
||||
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');
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -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
306
basicswap/static/js/tooltips.js
Normal file
306
basicswap/static/js/tooltips.js
Normal 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();
|
||||
});
|
||||
@@ -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';
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -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
@@ -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' %}
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</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)
|
||||
</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>
|
||||
|
||||
<input type="hidden" name="formid" value="{{ form_id }}">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</section>
|
||||
<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');
|
||||
} else {
|
||||
eyeOpen.classList.remove('hidden');
|
||||
eyeClosed.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
toggleBtn.addEventListener('mousedown', function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</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' %}
|
||||
<script>
|
||||
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 {
|
||||
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();
|
||||
}
|
||||
|
||||
const toggles = ["toggle-old", "toggle-new", "toggle-conf"]
|
||||
toggles.forEach(function (toggle_id, index) {
|
||||
document.getElementById(toggle_id).addEventListener('change', togglePassword, false);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 you’ve 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' %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -1,231 +1,132 @@
|
||||
{% from 'style.html' import circular_info_messages_svg, green_cross_close_svg, red_cross_close_svg, circular_error_messages_svg %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
|
||||
<title>(BSX) BasicSwap - 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-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
Reference in New Issue
Block a user