1 Commits

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

View File

@@ -9,9 +9,6 @@ concurrency:
env:
BIN_DIR: /tmp/cached_bin
TEST_RELOAD_PATH: /tmp/test_basicswap
BSX_SELENIUM_DRIVER: firefox-ci
XMR_RPC_USER: xmr_user
XMR_RPC_PWD: xmr_pwd
jobs:
ci:
@@ -27,18 +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 -e .[dev]
pip install flake8 codespell pytest
pip install -r requirements.txt --require-hashes
- name: Install
run: |
@@ -46,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
@@ -71,41 +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"
until curl -s -f -o /dev/null "http://localhost:12701/json/coins"
do
tail -n 1 /tmp/log.txt
sleep 2
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 -9 $TEST_NETWORK_PID

4
.gitignore vendored
View File

@@ -8,8 +8,6 @@ __pycache__
/*.eggs
.tox
.eggs
.ruff_cache
.pytest_cache
*~
# geckodriver.log
@@ -17,4 +15,4 @@ __pycache__
docker/.env
# vscode dev container settings
compose-dev.yaml
compose-dev.yaml

View File

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

View File

@@ -5,18 +5,17 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import logging
import os
import random
import shlex
import socket
import socks
import subprocess
import sys
import threading
import time
import traceback
import shlex
import socks
import random
import socket
import urllib
import logging
import threading
import traceback
import subprocess
from sockshandler import SocksiPyHandler
@@ -43,9 +42,9 @@ def getaddrinfo_tor(*args):
class BaseApp(DBMethods):
def __init__(self, data_dir, settings, chain, log_name="BasicSwap"):
self.fp = None
def __init__(self, fp, data_dir, settings, chain, log_name="BasicSwap"):
self.log_name = log_name
self.fp = fp
self.fail_code = 0
self.mock_time_offset = 0
@@ -72,33 +71,24 @@ class BaseApp(DBMethods):
self.default_socket_timeout = socket.getdefaulttimeout()
self.default_socket_getaddrinfo = socket.getaddrinfo
def __del__(self):
if self.fp:
self.fp.close()
def stopRunning(self, with_code=0):
self.fail_code = with_code
with self.mxDB:
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(
@@ -108,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)

View File

@@ -11,7 +11,6 @@ import concurrent.futures
import copy
import datetime as dt
import json
import logging
import os
import random
import secrets
@@ -33,7 +32,7 @@ from .interface.part import PARTInterface, PARTInterfaceAnon, PARTInterfaceBlind
from . import __version__
from .rpc import escape_rpcauth
from .rpc_xmr import make_xmr_rpc2_func
from .ui.util import getCoinName
from .ui.util import getCoinName, known_chart_coins
from .util import (
AutomationConstraint,
AutomationConstraintTemporary,
@@ -66,12 +65,6 @@ from basicswap.util.network import is_private_ip_address
from .chainparams import (
Coins,
chainparams,
Fiat,
ticker_map,
)
from .explorers import (
default_chart_api_key,
default_coingecko_api_key,
)
from .script import (
OpCodes,
@@ -122,16 +115,8 @@ from .explorers import (
ExplorerBitAps,
ExplorerChainz,
)
from .network.simplex import (
initialiseSimplexNetwork,
sendSimplexMsg,
readSimplexMsgs,
)
from .network.util import (
getMsgPubkey,
)
import basicswap.config as cfg
import basicswap.network.network as bsn
import basicswap.network as bsn
import basicswap.protocols.atomic_swap_1 as atomic_swap_1
import basicswap.protocols.xmr_swap_1 as xmr_swap_1
from .basicswap_util import (
@@ -141,8 +126,6 @@ from .basicswap_util import (
BidStates,
DebugTypes,
EventLogTypes,
fiatTicker,
get_api_key_setting,
KeyTypes,
MessageTypes,
NotificationTypes as NT,
@@ -290,13 +273,14 @@ class BasicSwap(BaseApp):
def __init__(
self,
fp,
data_dir,
settings,
chain,
log_name="BasicSwap",
transient_instance=False,
):
super().__init__(data_dir, settings, chain, log_name)
super().__init__(fp, data_dir, settings, chain, log_name)
v = __version__.split(".")
self._version = struct.pack(">HHH", int(v[0]), int(v[1]), int(v[2]))
@@ -360,13 +344,6 @@ class BasicSwap(BaseApp):
self._expire_db_records_after = self.get_int_setting(
"expire_db_records_after", 7 * 86400, 0, 31 * 86400
) # Seconds
self._max_logfile_bytes = self.settings.get(
"max_logfile_size", 100
) # In MB 0 to disable truncation
if self._max_logfile_bytes > 0:
self._max_logfile_bytes *= 1024 * 1024
self._max_logfiles = self.get_int_setting("max_logfiles", 10, 1, 100)
self._notifications_cache = {}
self._is_encrypted = None
self._is_locked = None
@@ -399,7 +376,7 @@ class BasicSwap(BaseApp):
Coins.PART_BLIND,
Coins.BCH,
)
self.coins_without_segwit = (Coins.PIVX, Coins.DASH)
self.coins_without_segwit = (Coins.PIVX, Coins.DASH, Coins.NMC)
# TODO: Adjust ranges
self.min_delay_event = self.get_int_setting("min_delay_event", 10, 0, 20 * 60)
@@ -436,9 +413,6 @@ class BasicSwap(BaseApp):
self.swaps_in_progress = dict()
self.dleag_split_size_init = 16000
self.dleag_split_size = 17000
self.SMSG_SECONDS_IN_HOUR = (
60 * 60
) # Note: Set smsgsregtestadjust=0 for regtest
@@ -537,8 +511,6 @@ class BasicSwap(BaseApp):
self._network = None
for t in self.threads:
if hasattr(t, "stop") and callable(t.stop):
t.stop()
t.join()
if sys.version_info[1] >= 9:
@@ -720,7 +692,7 @@ class BasicSwap(BaseApp):
def getXMRTrustedDaemon(self, coin, node_host: str) -> bool:
coin = Coins(coin) # Errors for invalid coin value
chain_client_settings = self.getChainClientSettings(coin)
trusted_daemon_setting = chain_client_settings.get("trusted_daemon", True)
trusted_daemon_setting = chain_client_settings.get("trusted_daemon", "auto")
self.log.debug(
f"'trusted_daemon' setting for {getCoinName(coin)}: {trusted_daemon_setting}."
)
@@ -1067,9 +1039,7 @@ class BasicSwap(BaseApp):
elif c in (Coins.XMR, Coins.WOW):
try:
ci.ensureWalletExists()
except Exception as e:
if "invalid signature" in str(e): # wallet is corrupt
raise
except Exception as e: # noqa: F841
self.log.warning(
f"Can't open {ci.coin_name()} wallet, could be locked."
)
@@ -1093,17 +1063,6 @@ class BasicSwap(BaseApp):
f"network_key {self.network_key}\nnetwork_pubkey {self.network_pubkey}\nnetwork_addr {self.network_addr}"
)
self.active_networks = []
network_config_list = self.settings.get("networks", [])
if len(network_config_list) < 1:
network_config_list = [{"type": "smsg", "enabled": True}]
for network in network_config_list:
if network["type"] == "smsg":
self.active_networks.append({"type": "smsg"})
elif network["type"] == "simplex":
initialiseSimplexNetwork(self, network)
ro = self.callrpc("smsglocalkeys")
found = False
for k in ro["smsg_keys"]:
@@ -1358,9 +1317,7 @@ class BasicSwap(BaseApp):
legacy_root_hash = ci.getSeedHash(root_key, 20)
self.setStringKV(key_str, legacy_root_hash.hex(), cursor)
def initialiseWallet(
self, interface_type, raise_errors: bool = False, restore_time: int = -1
) -> None:
def initialiseWallet(self, interface_type, raise_errors: bool = False) -> None:
if interface_type == Coins.PART:
return
ci = self.ci(interface_type)
@@ -1379,7 +1336,7 @@ class BasicSwap(BaseApp):
root_key = self.getWalletKey(interface_type, 1)
try:
ci.initialiseWallet(root_key, restore_time)
ci.initialiseWallet(root_key)
except Exception as e:
# < 0.21: sethdseed cannot set a new HD seed while still in Initial Block Download.
self.log.error(f"initialiseWallet failed: {e}")
@@ -1683,33 +1640,6 @@ class BasicSwap(BaseApp):
bid_valid = (bid.expire_at - now) + 10 * 60 # Add 10 minute buffer
return max(smsg_min_valid, min(smsg_max_valid, bid_valid))
def sendMessage(
self, addr_from: str, addr_to: str, payload_hex: bytes, msg_valid: int, cursor
) -> bytes:
message_id: bytes = None
# First network in list will set message_id
for network in self.active_networks:
net_message_id = None
if network["type"] == "smsg":
net_message_id = self.sendSmsg(
addr_from, addr_to, payload_hex, msg_valid
)
elif network["type"] == "simplex":
net_message_id = sendSimplexMsg(
self,
network,
addr_from,
addr_to,
bytes.fromhex(payload_hex),
msg_valid,
cursor,
)
else:
raise ValueError("Unknown network: {}".format(network["type"]))
if not message_id:
message_id = net_message_id
return message_id
def sendSmsg(
self, addr_from: str, addr_to: str, payload_hex: bytes, msg_valid: int
) -> bytes:
@@ -2220,24 +2150,6 @@ class BasicSwap(BaseApp):
msg_buf.fee_rate_to
) # Unused: TODO - Set priority?
# Set auto-accept type
automation_id = extra_options.get("automation_id", -1)
if automation_id == -1 and auto_accept_bids:
automation_id = 1 # Default strategy
if automation_id != -1:
strategy = self.queryOne(
AutomationStrategy,
cursor,
{"active_ind": 1, "record_id": automation_id},
)
if strategy:
msg_buf.auto_accept_type = (
2 if strategy.only_known_identities else 1
)
else:
msg_buf.auto_accept_type = 0
# If a prefunded txn is not used, check that the wallet balance can cover the tx fee.
if "prefunded_itx" not in extra_options:
# TODO: Better tx size estimate, xmr_swap_b_lock_tx_vsize could be larger than xmr_swap_b_lock_spend_tx_vsize
@@ -2255,9 +2167,7 @@ class BasicSwap(BaseApp):
offer_bytes = msg_buf.to_bytes()
payload_hex = str.format("{:02x}", MessageTypes.OFFER) + offer_bytes.hex()
msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, valid_for_seconds)
offer_id = self.sendMessage(
offer_addr, offer_addr_to, payload_hex, msg_valid, cursor
)
offer_id = self.sendSmsg(offer_addr, offer_addr_to, payload_hex, msg_valid)
security_token = extra_options.get("security_token", None)
if security_token is not None and len(security_token) != 20:
@@ -2292,7 +2202,6 @@ class BasicSwap(BaseApp):
security_token=security_token,
from_feerate=msg_buf.fee_rate_from,
to_feerate=msg_buf.fee_rate_to,
auto_accept_type=msg_buf.auto_accept_type,
)
offer.setState(OfferStates.OFFER_SENT)
@@ -2362,8 +2271,8 @@ class BasicSwap(BaseApp):
)
msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, offer.time_valid)
msg_id = self.sendMessage(
offer.addr_from, self.network_addr, payload_hex, msg_valid, cursor
msg_id = self.sendSmsg(
offer.addr_from, self.network_addr, payload_hex, msg_valid
)
self.log.debug(
f"Revoked offer {self.log.id(offer_id)} in msg {self.log.id(msg_id)}"
@@ -2741,8 +2650,7 @@ class BasicSwap(BaseApp):
self.log.warning(msg)
return False
seed_key: str = "main_wallet_seedid_" + ci.coin_name().lower()
expect_seedid: str = self.getStringKV(seed_key)
expect_seedid = self.getStringKV("main_wallet_seedid_" + ci.coin_name().lower())
if expect_seedid is None:
self.log.warning(
f"Can't find expected wallet seed id for coin {ci.coin_name()}."
@@ -2754,7 +2662,6 @@ class BasicSwap(BaseApp):
)
root_key = self.getWalletKey(c, 1)
self.storeSeedIDForCoin(root_key, c)
expect_seedid: str = self.getStringKV(seed_key)
else:
self.log.warning("Node is locked.")
return False
@@ -3209,9 +3116,7 @@ class BasicSwap(BaseApp):
bid_addr = self.prepareSMSGAddress(addr_send_from, AddressTypes.BID, cursor)
msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, valid_for_seconds)
bid_id = self.sendMessage(
bid_addr, offer.addr_from, payload_hex, msg_valid, cursor
)
bid_id = self.sendSmsg(bid_addr, offer.addr_from, payload_hex, msg_valid)
bid = Bid(
protocol_version=msg_buf.protocol_version,
@@ -3547,8 +3452,8 @@ class BasicSwap(BaseApp):
)
msg_valid: int = self.getAcceptBidMsgValidTime(bid)
accept_msg_id = self.sendMessage(
offer.addr_from, bid.bid_addr, payload_hex, msg_valid, cursor
accept_msg_id = self.sendSmsg(
offer.addr_from, bid.bid_addr, payload_hex, msg_valid
)
self.addMessageLink(
@@ -3578,29 +3483,20 @@ class BasicSwap(BaseApp):
dleag: bytes,
msg_valid: int,
bid_msg_ids,
cursor,
) -> None:
msg_buf2 = XmrSplitMessage(
msg_id=bid_id, msg_type=msg_type, sequence=1, dleag=dleag[16000:32000]
)
msg_bytes = msg_buf2.to_bytes()
payload_hex = str.format("{:02x}", MessageTypes.XMR_BID_SPLIT) + msg_bytes.hex()
bid_msg_ids[1] = self.sendSmsg(addr_from, addr_to, payload_hex, msg_valid)
sent_bytes = self.dleag_split_size_init
num_sent = 1
while sent_bytes < len(dleag):
size_to_send: int = min(self.dleag_split_size, len(dleag) - sent_bytes)
msg_buf = XmrSplitMessage(
msg_id=bid_id,
msg_type=msg_type,
sequence=num_sent,
dleag=dleag[sent_bytes : sent_bytes + size_to_send],
)
msg_bytes = msg_buf.to_bytes()
payload_hex = (
str.format("{:02x}", MessageTypes.XMR_BID_SPLIT) + msg_bytes.hex()
)
bid_msg_ids[num_sent] = self.sendMessage(
addr_from, addr_to, payload_hex, msg_valid, cursor
)
num_sent += 1
sent_bytes += size_to_send
msg_buf3 = XmrSplitMessage(
msg_id=bid_id, msg_type=msg_type, sequence=2, dleag=dleag[32000:]
)
msg_bytes = msg_buf3.to_bytes()
payload_hex = str.format("{:02x}", MessageTypes.XMR_BID_SPLIT) + msg_bytes.hex()
bid_msg_ids[2] = self.sendSmsg(addr_from, addr_to, payload_hex, msg_valid)
def postXmrBid(
self, offer_id: bytes, amount: int, addr_send_from: str = None, extra_options={}
@@ -3676,8 +3572,8 @@ class BasicSwap(BaseApp):
)
msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, valid_for_seconds)
xmr_swap.bid_id = self.sendMessage(
bid_addr, offer.addr_from, payload_hex, msg_valid, cursor
xmr_swap.bid_id = self.sendSmsg(
bid_addr, offer.addr_from, payload_hex, msg_valid
)
bid = Bid(
@@ -3759,7 +3655,7 @@ class BasicSwap(BaseApp):
if ci_to.curve_type() == Curves.ed25519:
xmr_swap.kbsf_dleag = ci_to.proveDLEAG(kbsf)
xmr_swap.pkasf = xmr_swap.kbsf_dleag[0:33]
msg_buf.kbsf_dleag = xmr_swap.kbsf_dleag[: self.dleag_split_size_init]
msg_buf.kbsf_dleag = xmr_swap.kbsf_dleag[:16000]
elif ci_to.curve_type() == Curves.secp256k1:
for i in range(10):
xmr_swap.kbsf_dleag = ci_to.signRecoverable(
@@ -3789,8 +3685,8 @@ class BasicSwap(BaseApp):
bid_addr = self.prepareSMSGAddress(addr_send_from, AddressTypes.BID, cursor)
msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, valid_for_seconds)
xmr_swap.bid_id = self.sendMessage(
bid_addr, offer.addr_from, payload_hex, msg_valid, cursor
xmr_swap.bid_id = self.sendSmsg(
bid_addr, offer.addr_from, payload_hex, msg_valid
)
bid_msg_ids = {}
@@ -3803,7 +3699,6 @@ class BasicSwap(BaseApp):
xmr_swap.kbsf_dleag,
msg_valid,
bid_msg_ids,
cursor,
)
bid = Bid(
@@ -4082,7 +3977,7 @@ class BasicSwap(BaseApp):
if ci_to.curve_type() == Curves.ed25519:
xmr_swap.kbsl_dleag = ci_to.proveDLEAG(kbsl)
msg_buf.kbsl_dleag = xmr_swap.kbsl_dleag[: self.dleag_split_size_init]
msg_buf.kbsl_dleag = xmr_swap.kbsl_dleag[:16000]
elif ci_to.curve_type() == Curves.secp256k1:
for i in range(10):
xmr_swap.kbsl_dleag = ci_to.signRecoverable(
@@ -4117,9 +4012,7 @@ class BasicSwap(BaseApp):
msg_valid: int = self.getAcceptBidMsgValidTime(bid)
bid_msg_ids = {}
bid_msg_ids[0] = self.sendMessage(
addr_from, addr_to, payload_hex, msg_valid, use_cursor
)
bid_msg_ids[0] = self.sendSmsg(addr_from, addr_to, payload_hex, msg_valid)
if ci_to.curve_type() == Curves.ed25519:
self.sendXmrSplitMessages(
@@ -4130,7 +4023,6 @@ class BasicSwap(BaseApp):
xmr_swap.kbsl_dleag,
msg_valid,
bid_msg_ids,
use_cursor,
)
bid.setState(BidStates.BID_ACCEPTED) # ADS
@@ -4252,8 +4144,8 @@ class BasicSwap(BaseApp):
msg_buf.kbvf = kbvf
msg_buf.kbsf_dleag = (
xmr_swap.kbsf_dleag
if len(xmr_swap.kbsf_dleag) < self.dleag_split_size_init
else xmr_swap.kbsf_dleag[: self.dleag_split_size_init]
if len(xmr_swap.kbsf_dleag) < 16000
else xmr_swap.kbsf_dleag[:16000]
)
bid_bytes = msg_buf.to_bytes()
@@ -4265,9 +4157,7 @@ class BasicSwap(BaseApp):
addr_to: str = bid.bid_addr
msg_valid: int = self.getAcceptBidMsgValidTime(bid)
bid_msg_ids = {}
bid_msg_ids[0] = self.sendMessage(
addr_from, addr_to, payload_hex, msg_valid, use_cursor
)
bid_msg_ids[0] = self.sendSmsg(addr_from, addr_to, payload_hex, msg_valid)
if ci_to.curve_type() == Curves.ed25519:
self.sendXmrSplitMessages(
@@ -4278,7 +4168,6 @@ class BasicSwap(BaseApp):
xmr_swap.kbsf_dleag,
msg_valid,
bid_msg_ids,
use_cursor,
)
bid.setState(BidStates.BID_REQUEST_ACCEPTED)
@@ -4764,7 +4653,7 @@ class BasicSwap(BaseApp):
+ (len(txn_script)).to_bytes(1, "big")
+ txn_script
)
refund_txn = ci.setTxScriptSig(bytes.fromhex(refund_txn), 0, script).hex()
refund_txn = ci.setTxScriptSig(bytes.fromhex(refund_txn), 0, script)
if coin_type in (Coins.NAV, Coins.DCR):
# Only checks signature
@@ -6883,61 +6772,29 @@ class BasicSwap(BaseApp):
now: int = self.getTime()
ttl_xmr_split_messages = 60 * 60
bid_cursor = None
dleag_proof_len: int = 48893 # coincurve.dleag.dleag_proof_len()
try:
cursor = self.openDB()
bid_cursor = self.getNewDBCursor()
q_bids = self.query(
Bid,
bid_cursor,
{
"state": (
int(BidStates.BID_RECEIVING),
int(BidStates.BID_RECEIVING_ACC),
)
},
Bid, bid_cursor, {"state": int(BidStates.BID_RECEIVING)}
)
for bid in q_bids:
q = cursor.execute(
"SELECT LENGTH(kbsl_dleag), LENGTH(kbsf_dleag) FROM xmr_swaps WHERE bid_id = :bid_id",
{
"bid_id": bid.bid_id,
},
"SELECT COUNT(*) FROM xmr_split_data WHERE bid_id = :bid_id AND msg_type = :msg_type",
{"bid_id": bid.bid_id, "msg_type": int(XmrSplitMsgTypes.BID)},
).fetchone()
kbsl_dleag_len: int = q[0]
kbsf_dleag_len: int = q[1]
if bid.state == int(BidStates.BID_RECEIVING_ACC):
bid_type: str = "bid accept"
msg_type: int = int(XmrSplitMsgTypes.BID_ACCEPT)
total_dleag_size: int = kbsl_dleag_len
else:
bid_type: str = "bid"
msg_type: int = int(XmrSplitMsgTypes.BID)
total_dleag_size: int = kbsf_dleag_len
q = cursor.execute(
"SELECT COUNT(*), SUM(LENGTH(dleag)) AS total_dleag_size FROM xmr_split_data WHERE bid_id = :bid_id AND msg_type = :msg_type",
{"bid_id": bid.bid_id, "msg_type": msg_type},
).fetchone()
total_dleag_size += 0 if q[1] is None else q[1]
if total_dleag_size >= dleag_proof_len:
num_segments = q[0]
if num_segments > 1:
try:
if bid.state == int(BidStates.BID_RECEIVING):
self.receiveXmrBid(bid, cursor)
elif bid.state == int(BidStates.BID_RECEIVING_ACC):
self.receiveXmrBidAccept(bid, cursor)
else:
raise ValueError("Unexpected bid state")
self.receiveXmrBid(bid, cursor)
except Exception as ex:
self.log.info(
f"Verify adaptor-sig {bid_type} {self.log.id(bid.bid_id)} failed: {ex}"
f"Verify adaptor-sig bid {self.log.id(bid.bid_id)} failed: {ex}"
)
if self.debug:
self.log.error(traceback.format_exc())
bid.setState(
BidStates.BID_ERROR, f"Failed {bid_type} validation: {ex}"
BidStates.BID_ERROR, "Failed validation: " + str(ex)
)
self.updateDB(
bid,
@@ -6950,7 +6807,7 @@ class BasicSwap(BaseApp):
continue
if bid.created_at + ttl_xmr_split_messages < now:
self.log.debug(
f"Expiring partially received {bid_type}: {self.log.id(bid.bid_id)}."
f"Expiring partially received bid: {self.log.id(bid.bid_id)}."
)
bid.setState(BidStates.BID_ERROR, "Timed out")
self.updateDB(
@@ -6960,6 +6817,53 @@ class BasicSwap(BaseApp):
"bid_id",
],
)
q_bids = self.query(
Bid, bid_cursor, {"state": int(BidStates.BID_RECEIVING_ACC)}
)
for bid in q_bids:
q = cursor.execute(
"SELECT COUNT(*) FROM xmr_split_data WHERE bid_id = :bid_id AND msg_type = :msg_type",
{
"bid_id": bid.bid_id,
"msg_type": int(XmrSplitMsgTypes.BID_ACCEPT),
},
).fetchone()
num_segments = q[0]
if num_segments > 1:
try:
self.receiveXmrBidAccept(bid, cursor)
except Exception as ex:
if self.debug:
self.log.error(traceback.format_exc())
self.log.info(
f"Verify adaptor-sig bid accept {self.log.id(bid.bid_id)} failed: {ex}."
)
bid.setState(
BidStates.BID_ERROR, "Failed accept validation: " + str(ex)
)
self.updateDB(
bid,
cursor,
[
"bid_id",
],
)
self.updateBidInProgress(bid)
continue
if bid.created_at + ttl_xmr_split_messages < now:
self.log.debug(
f"Expiring partially received bid accept: {self.log.id(bid.bid_id)}."
)
bid.setState(BidStates.BID_ERROR, "Timed out")
self.updateDB(
bid,
cursor,
[
"bid_id",
],
)
# Expire old records
cursor.execute(
"DELETE FROM xmr_split_data WHERE created_at + :ttl < :now",
@@ -7089,7 +6993,6 @@ class BasicSwap(BaseApp):
if self.isOfferRevoked(offer_id, msg["from"]):
raise ValueError("Offer has been revoked {}.".format(offer_id.hex()))
pk_from: bytes = getMsgPubkey(self, msg)
try:
cursor = self.openDB()
# Offers must be received on the public network_addr or manually created addresses
@@ -7130,16 +7033,10 @@ class BasicSwap(BaseApp):
rate_negotiable=offer_data.rate_negotiable,
addr_to=msg["to"],
addr_from=msg["from"],
pk_from=pk_from,
created_at=msg["sent"],
expire_at=msg["sent"] + offer_data.time_valid,
was_sent=False,
bid_reversed=bid_reversed,
auto_accept_type=(
offer_data.auto_accept_type
if b"\xa0\x01" in offer_bytes
else None
),
)
offer.setState(OfferStates.OFFER_RECEIVED)
self.add(offer, cursor)
@@ -7479,7 +7376,6 @@ class BasicSwap(BaseApp):
bid = self.getBid(bid_id)
if bid is None:
pk_from: bytes = getMsgPubkey(self, msg)
bid = Bid(
active_ind=1,
bid_id=bid_id,
@@ -7494,7 +7390,6 @@ class BasicSwap(BaseApp):
created_at=msg["sent"],
expire_at=msg["sent"] + bid_data.time_valid,
bid_addr=msg["from"],
pk_bid_addr=pk_from,
was_received=True,
chain_a_height_start=ci_from.getChainHeight(),
chain_b_height_start=ci_to.getChainHeight(),
@@ -7893,13 +7788,12 @@ class BasicSwap(BaseApp):
)
if ci_to.curve_type() == Curves.ed25519:
ensure(len(bid_data.kbsf_dleag) <= 16000, "Invalid kbsf_dleag size")
ensure(len(bid_data.kbsf_dleag) == 16000, "Invalid kbsf_dleag size")
bid_id = bytes.fromhex(msg["msgid"])
bid, xmr_swap = self.getXmrBid(bid_id)
if bid is None:
pk_from: bytes = getMsgPubkey(self, msg)
bid = Bid(
active_ind=1,
bid_id=bid_id,
@@ -7911,7 +7805,6 @@ class BasicSwap(BaseApp):
created_at=msg["sent"],
expire_at=msg["sent"] + bid_data.time_valid,
bid_addr=msg["from"],
pk_bid_addr=pk_from,
was_received=True,
chain_a_height_start=ci_from.getChainHeight(),
chain_b_height_start=ci_to.getChainHeight(),
@@ -8241,8 +8134,8 @@ class BasicSwap(BaseApp):
msg_valid: int = self.getActiveBidMsgValidTime()
addr_send_from: str = offer.addr_from if reverse_bid else bid.bid_addr
addr_send_to: str = bid.bid_addr if reverse_bid else offer.addr_from
coin_a_lock_tx_sigs_l_msg_id = self.sendMessage(
addr_send_from, addr_send_to, payload_hex, msg_valid, cursor
coin_a_lock_tx_sigs_l_msg_id = self.sendSmsg(
addr_send_from, addr_send_to, payload_hex, msg_valid
)
self.addMessageLink(
Concepts.BID,
@@ -8610,8 +8503,8 @@ class BasicSwap(BaseApp):
addr_send_from: str = bid.bid_addr if reverse_bid else offer.addr_from
addr_send_to: str = offer.addr_from if reverse_bid else bid.bid_addr
msg_valid: int = self.getActiveBidMsgValidTime()
coin_a_lock_release_msg_id = self.sendMessage(
addr_send_from, addr_send_to, payload_hex, msg_valid, cursor
coin_a_lock_release_msg_id = self.sendSmsg(
addr_send_from, addr_send_to, payload_hex, msg_valid
)
self.addMessageLink(
Concepts.BID,
@@ -9030,8 +8923,8 @@ class BasicSwap(BaseApp):
)
msg_valid: int = self.getActiveBidMsgValidTime()
xmr_swap.coin_a_lock_refund_spend_tx_msg_id = self.sendMessage(
addr_send_from, addr_send_to, payload_hex, msg_valid, cursor
xmr_swap.coin_a_lock_refund_spend_tx_msg_id = self.sendSmsg(
addr_send_from, addr_send_to, payload_hex, msg_valid
)
bid.setState(BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX)
@@ -9413,7 +9306,6 @@ class BasicSwap(BaseApp):
bid, xmr_swap = self.getXmrBid(bid_id)
if bid is None:
pk_from: bytes = getMsgPubkey(self, msg)
bid = Bid(
active_ind=1,
bid_id=bid_id,
@@ -9425,7 +9317,6 @@ class BasicSwap(BaseApp):
created_at=msg["sent"],
expire_at=msg["sent"] + bid_data.time_valid,
bid_addr=msg["from"],
pk_bid_addr=pk_from,
was_sent=False,
was_received=True,
chain_a_height_start=ci_from.getChainHeight(),
@@ -9528,7 +9419,7 @@ class BasicSwap(BaseApp):
"Invalid destination address",
)
if ci_to.curve_type() == Curves.ed25519:
ensure(len(msg_data.kbsf_dleag) <= 16000, "Invalid kbsf_dleag size")
ensure(len(msg_data.kbsf_dleag) == 16000, "Invalid kbsf_dleag size")
xmr_swap.dest_af = msg_data.dest_af
xmr_swap.pkaf = msg_data.pkaf
@@ -9563,14 +9454,6 @@ class BasicSwap(BaseApp):
def processMsg(self, msg) -> None:
try:
if "hex" not in msg:
if self.debug:
if "error" in msg:
self.log.debug(
"Message error {}: {}.".format(msg["msgid"], msg["error"])
)
raise ValueError("Invalid msg received {}.".format(msg["msgid"]))
return
msg_type = int(msg["hex"][:2], 16)
if msg_type == MessageTypes.OFFER:
@@ -9784,10 +9667,6 @@ class BasicSwap(BaseApp):
self.processMsg(msg)
try:
for network in self.active_networks:
if network["type"] == "simplex":
readSimplexMsgs(self, network)
# TODO: Wait for blocks / txns, would need to check multiple coins
now: int = self.getTime()
self.expireBidsAndOffers(now)
@@ -9837,51 +9716,6 @@ class BasicSwap(BaseApp):
self.checkAcceptedBids()
self._last_checked_expired = now
if self._max_logfile_bytes > 0:
logfile_size: int = self.fp.tell()
self.log.debug(f"Log file bytes: {logfile_size}.")
if logfile_size > self._max_logfile_bytes:
for i, log_handler in enumerate(self.log.handlers):
stream_name = getattr(log_handler.stream, "name", "")
if stream_name.endswith(".log"):
del self.log.handlers[i]
break
self.fp.close()
log_path = os.path.join(self.data_dir, "basicswap.log")
if self._max_logfiles == 1:
os.remove(log_path)
else:
last_log = os.path.join(
self.data_dir,
f"basicswap_{self._max_logfiles - 1:0>2}.log",
)
if os.path.exists(last_log):
os.remove(last_log)
for i in range(self._max_logfiles - 2, 0, -1):
path_from = os.path.join(
self.data_dir, f"basicswap_{i:0>2}.log"
)
path_to = os.path.join(
self.data_dir, f"basicswap_{i + 1:0>2}.log"
)
if os.path.exists(path_from):
os.rename(path_from, path_to)
log_path = os.path.join(self.data_dir, "basicswap.log")
os.rename(
log_path,
os.path.join(self.data_dir, "basicswap_01.log"),
)
self.openLogFile()
stream_fp = logging.StreamHandler(self.fp)
stream_fp.setFormatter(self.log_formatter)
self.log.addHandler(stream_fp)
self.log.info("Log file rotated.")
if now - self._last_checked_actions >= self.check_actions_seconds:
self.checkQueuedActions()
self._last_checked_actions = now
@@ -10060,7 +9894,7 @@ class BasicSwap(BaseApp):
seen_tickers = []
for ticker in tickers:
upcased_ticker = ticker.strip().upper()
if upcased_ticker.lower() not in ticker_map:
if upcased_ticker not in known_chart_coins:
raise ValueError(f"Unknown coin: {ticker}")
if upcased_ticker in seen_tickers:
raise ValueError(f"Duplicate coin: {ticker}")
@@ -11224,175 +11058,6 @@ class BasicSwap(BaseApp):
).isWalletEncryptedLocked()
return self._is_encrypted, self._is_locked
def getExchangeName(self, coin_id: int, exchange_name: str) -> str:
if coin_id == Coins.BCH:
return "bitcoin-cash"
if coin_id == Coins.FIRO:
return "zcoin"
return chainparams[coin_id]["name"]
def lookupFiatRates(
self,
coins_list,
currency_to: int = Fiat.USD,
rate_source: str = "coingecko.com",
saved_ttl: int = 300,
):
if self.debug:
coins_list_display = ", ".join([Coins(c).name for c in coins_list])
self.log.debug(f"lookupFiatRates {coins_list_display}.")
ensure(len(coins_list) > 0, "Must specify coin/s")
ensure(saved_ttl >= 0, "Invalid saved time")
now: int = int(time.time())
oldest_time_valid: int = now - saved_ttl
return_rates = {}
headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"}
cursor = self.openDB()
try:
parameters = {
"rate_source": rate_source,
"oldest_time_valid": oldest_time_valid,
"currency_to": currency_to,
}
coins_list_query = ""
for i, coin_id in enumerate(coins_list):
try:
_ = Coins(coin_id)
except Exception:
raise ValueError(f"Unknown coin type {coin_id}")
param_name = f"coin_{i}"
if i > 0:
coins_list_query += ","
coins_list_query += f":{param_name}"
parameters[param_name] = coin_id
query = f"SELECT currency_from, rate FROM coinrates WHERE currency_from IN ({coins_list_query}) AND currency_to = :currency_to AND source = :rate_source AND last_updated >= :oldest_time_valid"
rows = cursor.execute(query, parameters)
for row in rows:
return_rates[int(row[0])] = float(row[1])
need_coins = []
new_values = {}
exchange_name_map = {}
for coin_id in coins_list:
if coin_id not in return_rates:
need_coins.append(coin_id)
if len(need_coins) < 1:
return return_rates
if rate_source == "coingecko.com":
ticker_to: str = fiatTicker(currency_to).lower()
# Update all requested coins
coin_ids: str = ""
for coin_id in coins_list:
if len(coin_ids) > 0:
coin_ids += ","
exchange_name: str = self.getExchangeName(coin_id, rate_source)
coin_ids += exchange_name
exchange_name_map[exchange_name] = coin_id
api_key: str = get_api_key_setting(
self.settings,
"coingecko_api_key",
default_coingecko_api_key,
escape=True,
)
url: str = (
f"https://api.coingecko.com/api/v3/simple/price?ids={coin_ids}&vs_currencies={ticker_to}"
)
if api_key != "":
url += f"&api_key={api_key}"
self.log.debug(f"lookupFiatRates: {url}")
js = json.loads(self.readURL(url, timeout=10, headers=headers))
for k, v in js.items():
return_rates[int(exchange_name_map[k])] = v[ticker_to]
new_values[exchange_name_map[k]] = v[ticker_to]
elif rate_source == "cryptocompare.com":
ticker_to: str = fiatTicker(currency_to).upper()
api_key: str = get_api_key_setting(
self.settings,
"chart_api_key",
default_chart_api_key,
escape=True,
)
if len(need_coins) == 1:
coin_ticker: str = chainparams[coin_id]["ticker"]
url: str = (
f"https://min-api.cryptocompare.com/data/price?fsym={coin_ticker}&tsyms={ticker_to}"
)
self.log.debug(f"lookupFiatRates: {url}")
js = json.loads(self.readURL(url, timeout=10, headers=headers))
return_rates[int(coin_id)] = js[ticker_to]
new_values[coin_id] = js[ticker_to]
else:
coin_ids: str = ""
for coin_id in coins_list:
if len(coin_ids) > 0:
coin_ids += ","
coin_ticker: str = chainparams[coin_id]["ticker"]
coin_ids += coin_ticker
exchange_name_map[coin_ticker] = coin_id
url: str = (
f"https://min-api.cryptocompare.com/data/pricemulti?fsyms={coin_ids}&tsyms={ticker_to}"
)
self.log.debug(f"lookupFiatRates: {url}")
js = json.loads(self.readURL(url, timeout=10, headers=headers))
for k, v in js.items():
return_rates[int(exchange_name_map[k])] = v[ticker_to]
new_values[exchange_name_map[k]] = v[ticker_to]
else:
raise ValueError(f"Unknown rate source {rate_source}")
if len(new_values) < 1:
return return_rates
# ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint
update_query = """
UPDATE coinrates SET
rate=:rate,
last_updated=:last_updated
WHERE currency_from = :currency_from AND currency_to = :currency_to AND source = :rate_source
"""
insert_query = """INSERT INTO coinrates(currency_from, currency_to, rate, source, last_updated)
VALUES(:currency_from, :currency_to, :rate, :rate_source, :last_updated)"""
for k, v in new_values.items():
cursor.execute(
update_query,
{
"currency_from": k,
"currency_to": currency_to,
"rate": v,
"rate_source": rate_source,
"last_updated": now,
},
)
if cursor.rowcount < 1:
cursor.execute(
insert_query,
{
"currency_from": k,
"currency_to": currency_to,
"rate": v,
"rate_source": rate_source,
"last_updated": now,
},
)
self.commitDB()
return return_rates
finally:
self.closeDB(cursor, commit=False)
def lookupRates(self, coin_from, coin_to, output_array=False):
self.log.debug(
"lookupRates {}, {}.".format(
@@ -11405,14 +11070,25 @@ class BasicSwap(BaseApp):
ci_to = self.ci(int(coin_to))
name_from = ci_from.chainparams()["name"]
name_to = ci_to.chainparams()["name"]
exchange_name_from = ci_from.getExchangeName("coingecko.com")
exchange_name_to = ci_to.getExchangeName("coingecko.com")
ticker_from = ci_from.chainparams()["ticker"]
ticker_to = ci_to.chainparams()["ticker"]
headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"}
rv = {}
if rate_sources.get("coingecko.com", True):
try:
js = self.lookupFiatRates([int(coin_from), int(coin_to)])
rate = float(js[int(coin_from)]) / float(js[int(coin_to)])
url = "https://api.coingecko.com/api/v3/simple/price?ids={},{}&vs_currencies=usd,btc".format(
exchange_name_from, exchange_name_to
)
self.log.debug(f"lookupRates: {url}")
start = time.time()
js = json.loads(self.readURL(url, timeout=10, headers=headers))
js["time_taken"] = time.time() - start
rate = float(js[exchange_name_from]["usd"]) / float(
js[exchange_name_to]["usd"]
)
js["rate_inferred"] = ci_to.format_amount(rate, conv_int=True, r=1)
rv["coingecko"] = js
except Exception as e:
@@ -11420,10 +11096,12 @@ class BasicSwap(BaseApp):
if self.debug:
self.log.error(traceback.format_exc())
js[name_from] = {"usd": js[int(coin_from)]}
js.pop(int(coin_from))
js[name_to] = {"usd": js[int(coin_to)]}
js.pop(int(coin_to))
if exchange_name_from != name_from:
js[name_from] = js[exchange_name_from]
js.pop(exchange_name_from)
if exchange_name_to != name_to:
js[name_to] = js[exchange_name_to]
js.pop(exchange_name_to)
if output_array:
@@ -11442,6 +11120,8 @@ class BasicSwap(BaseApp):
ticker_to,
format_float(float(js[name_from]["usd"])),
format_float(float(js[name_to]["usd"])),
format_float(float(js[name_from]["btc"])),
format_float(float(js[name_to]["btc"])),
format_float(float(js["rate_inferred"])),
)
)

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -9,14 +9,12 @@
import struct
import hashlib
from enum import IntEnum, auto
from html import escape as html_escape
from .util.address import (
encodeAddress,
decodeAddress,
)
from .chainparams import (
chainparams,
Fiat,
)
@@ -522,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:
@@ -530,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:
@@ -538,31 +536,6 @@ def strSwapDesc(swap_type) -> str:
return None
def fiatTicker(fiat_ind: int) -> str:
try:
return Fiat(fiat_ind).name
except Exception as e: # noqa: F841
raise ValueError(f"Unknown fiat ind {fiat_ind}")
def fiatFromTicker(ticker: str) -> int:
ticker_uc = ticker.upper()
for entry in Fiat:
if entry.name == ticker_uc:
return entry
raise ValueError(f"Unknown fiat {ticker}")
def get_api_key_setting(
settings, setting_name: str, default_value: str = "", escape: bool = False
):
setting_name_enc: str = setting_name + "_enc"
if setting_name_enc in settings:
rv = bytes.fromhex(settings[setting_name_enc]).decode("utf-8")
return html_escape(rv) if escape else rv
return settings.get(setting_name, default_value)
inactive_states = [
BidStates.SWAP_COMPLETED,
BidStates.BID_ERROR,

File diff suppressed because it is too large Load Diff

View File

@@ -265,19 +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 runClient(
data_dir: str, chain: str, start_only_coins: bool, log_prefix: str = "BasicSwap"
) -> int:
def runClient(fp, data_dir, chain, start_only_coins):
global swap_client, logger
daemons = []
pids = []
@@ -302,7 +296,7 @@ def runClient(
with open(settings_path) as fs:
settings = json.load(fs)
swap_client = BasicSwap(data_dir, settings, chain, log_name=log_prefix)
swap_client = BasicSwap(fp, data_dir, settings, chain)
logger = swap_client.log
if os.path.exists(pids_path):
@@ -440,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
@@ -486,6 +480,7 @@ def runClient(
else cfg.DEFAULT_ALLOW_CORS
)
thread_http = HttpThread(
fp,
settings["htmlhost"],
settings["htmlport"],
allow_cors,
@@ -534,7 +529,7 @@ def runClient(
closed_pids = []
for d in daemons:
swap_client.log.info(f"Interrupting {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
@@ -551,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")
@@ -567,13 +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__}",
)
logger.info("Basicswap version: %s", __version__)
def printHelp():
@@ -581,7 +569,9 @@ 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.")
@@ -589,18 +579,16 @@ def printHelp():
print(
"--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing."
)
print("--logprefix Specify log prefix.")
def main():
data_dir = None
chain = "mainnet"
start_only_coins = set()
log_prefix: str = "BasicSwap"
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("=")
@@ -625,9 +613,6 @@ def main():
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(",")]:
if is_known_coin(coin) is False:
@@ -635,7 +620,7 @@ def main():
start_only_coins.add(coin)
continue
logger.warning(f"Unknown argument {v}")
logger.warning("Unknown argument %s", v)
if os.name == "nt":
logger.warning(
@@ -644,17 +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)
logger.info(os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n")
fail_code = runClient(data_dir, chain, start_only_coins, log_prefix)
with open(os.path.join(data_dir, "basicswap.log"), "a") as fp:
logger.info(
os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n"
)
runClient(fp, data_dir, chain, start_only_coins)
print("Done.")
return fail_code
return swap_client.fail_code if swap_client is not None else 0
if __name__ == "__main__":

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,22 +5,20 @@
"""Helpful routines for regression testing."""
from base64 import b64encode
from binascii import unhexlify
from decimal import Decimal, ROUND_DOWN
from subprocess import CalledProcessError
import hashlib
import inspect
import json
import logging
import os
import pathlib
import platform
import random
import re
import time
from . import coverage
from .authproxy import AuthServiceProxy, JSONRPCException
from collections.abc import Callable
from typing import Optional
from io import BytesIO
logger = logging.getLogger("TestFramework.utils")
@@ -30,46 +28,23 @@ logger = logging.getLogger("TestFramework.utils")
def assert_approx(v, vexp, vspan=0.00001):
"""Assert that `v` is within `vspan` of `vexp`"""
if isinstance(v, Decimal) or isinstance(vexp, Decimal):
v=Decimal(v)
vexp=Decimal(vexp)
vspan=Decimal(vspan)
if v < vexp - vspan:
raise AssertionError("%s < [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan)))
if v > vexp + vspan:
raise AssertionError("%s > [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan)))
def assert_fee_amount(fee, tx_size, feerate_BTC_kvB):
"""Assert the fee is in range."""
assert isinstance(tx_size, int)
target_fee = get_fee(tx_size, feerate_BTC_kvB)
def assert_fee_amount(fee, tx_size, fee_per_kB):
"""Assert the fee was in range"""
target_fee = round(tx_size * fee_per_kB / 1000, 8)
if fee < target_fee:
raise AssertionError("Fee of %s BTC too low! (Should be %s BTC)" % (str(fee), str(target_fee)))
# allow the wallet's estimation to be at most 2 bytes off
high_fee = get_fee(tx_size + 2, feerate_BTC_kvB)
if fee > high_fee:
if fee > (tx_size + 2) * fee_per_kB / 1000:
raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)" % (str(fee), str(target_fee)))
def summarise_dict_differences(thing1, thing2):
if not isinstance(thing1, dict) or not isinstance(thing2, dict):
return thing1, thing2
d1, d2 = {}, {}
for k in sorted(thing1.keys()):
if k not in thing2:
d1[k] = thing1[k]
elif thing1[k] != thing2[k]:
d1[k], d2[k] = summarise_dict_differences(thing1[k], thing2[k])
for k in sorted(thing2.keys()):
if k not in thing1:
d2[k] = thing2[k]
return d1, d2
def assert_equal(thing1, thing2, *args):
if thing1 != thing2 and not args and isinstance(thing1, dict) and isinstance(thing2, dict):
d1,d2 = summarise_dict_differences(thing1, thing2)
raise AssertionError("not(%s == %s)\n in particular not(%s == %s)" % (thing1, thing2, d1, d2))
if thing1 != thing2 or any(thing1 != arg for arg in args):
raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
@@ -104,7 +79,7 @@ def assert_raises_message(exc, message, fun, *args, **kwds):
raise AssertionError("No exception raised")
def assert_raises_process_error(returncode: int, output: str, fun: Callable, *args, **kwds):
def assert_raises_process_error(returncode, output, fun, *args, **kwds):
"""Execute a process and asserts the process return code and output.
Calls function `fun` with arguments `args` and `kwds`. Catches a CalledProcessError
@@ -112,9 +87,9 @@ def assert_raises_process_error(returncode: int, output: str, fun: Callable, *ar
no CalledProcessError was raised or if the return code and output are not as expected.
Args:
returncode: the process return code.
output: [a substring of] the process output.
fun: the function to call. This should execute a process.
returncode (int): the process return code.
output (string): [a substring of] the process output.
fun (function): the function to call. This should execute a process.
args*: positional arguments for the function.
kwds**: named arguments for the function.
"""
@@ -129,7 +104,7 @@ def assert_raises_process_error(returncode: int, output: str, fun: Callable, *ar
raise AssertionError("No exception raised")
def assert_raises_rpc_error(code: Optional[int], message: Optional[str], fun: Callable, *args, **kwds):
def assert_raises_rpc_error(code, message, fun, *args, **kwds):
"""Run an RPC and verify that a specific JSONRPC exception code and message is raised.
Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException
@@ -137,11 +112,11 @@ def assert_raises_rpc_error(code: Optional[int], message: Optional[str], fun: Ca
no JSONRPCException was raised or if the error code/message are not as expected.
Args:
code: the error code returned by the RPC call (defined in src/rpc/protocol.h).
Set to None if checking the error code is not required.
message: [a substring of] the error string returned by the RPC call.
Set to None if checking the error string is not required.
fun: the function to call. This should be the name of an RPC.
code (int), optional: the error code returned by the RPC call (defined
in src/rpc/protocol.h). Set to None if checking the error code is not required.
message (string), optional: [a substring of] the error string returned by the
RPC call. Set to None if checking the error string is not required.
fun (function): the function to call. This should be the name of an RPC.
args*: positional arguments for the function.
kwds**: named arguments for the function.
"""
@@ -228,45 +203,29 @@ def check_json_precision():
raise RuntimeError("JSON encode/decode loses precision")
def EncodeDecimal(o):
if isinstance(o, Decimal):
return str(o)
raise TypeError(repr(o) + " is not JSON serializable")
def count_bytes(hex_string):
return len(bytearray.fromhex(hex_string))
def hex_str_to_bytes(hex_str):
return unhexlify(hex_str.encode('ascii'))
def str_to_b64str(string):
return b64encode(string.encode('utf-8')).decode('ascii')
def ceildiv(a, b):
"""
Divide 2 ints and round up to next int rather than round down
Implementation requires python integers, which have a // operator that does floor division.
Other types like decimal.Decimal whose // operator truncates towards 0 will not work.
"""
assert isinstance(a, int)
assert isinstance(b, int)
return -(-a // b)
def get_fee(tx_size, feerate_btc_kvb):
"""Calculate the fee in BTC given a feerate is BTC/kvB. Reflects CFeeRate::GetFee"""
feerate_sat_kvb = int(feerate_btc_kvb * Decimal(1e8)) # Fee in sat/kvb as an int to avoid float precision errors
target_fee_sat = ceildiv(feerate_sat_kvb * tx_size, 1000) # Round calculated fee up to nearest sat
return target_fee_sat / Decimal(1e8) # Return result in BTC
def satoshi_round(amount):
return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=None, timeout_factor=1.0):
"""Sleep until the predicate resolves to be True.
Warning: Note that this method is not recommended to be used in tests as it is
not aware of the context of the test framework. Using the `wait_until()` members
from `BitcoinTestFramework` or `P2PInterface` class ensures the timeout is
properly scaled. Furthermore, `wait_until()` from `P2PInterface` class in
`p2p.py` has a preset lock.
"""
def wait_until(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=None, timeout_factor=1.0):
if attempts == float('inf') and timeout == float('inf'):
timeout = 60
timeout = timeout * timeout_factor
@@ -294,16 +253,6 @@ def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=floa
raise RuntimeError('Unreachable')
def sha256sum_file(filename):
h = hashlib.sha256()
with open(filename, 'rb') as f:
d = f.read(4096)
while len(d) > 0:
h.update(d)
d = f.read(4096)
return h.digest()
# RPC/P2P connection constants and functions
############################################
@@ -320,15 +269,15 @@ class PortSeed:
n = None
def get_rpc_proxy(url: str, node_number: int, *, timeout: Optional[int]=None, coveragedir: Optional[str]=None) -> coverage.AuthServiceProxyWrapper:
def get_rpc_proxy(url, node_number, *, timeout=None, coveragedir=None):
"""
Args:
url: URL of the RPC server to call
node_number: the node number (or id) that this calls to
url (str): URL of the RPC server to call
node_number (int): the node number (or id) that this calls to
Kwargs:
timeout: HTTP timeout in seconds
coveragedir: Directory
timeout (int): HTTP timeout in seconds
coveragedir (str): Directory
Returns:
AuthServiceProxy. convenience object for making RPC calls.
@@ -339,10 +288,11 @@ def get_rpc_proxy(url: str, node_number: int, *, timeout: Optional[int]=None, co
proxy_kwargs['timeout'] = int(timeout)
proxy = AuthServiceProxy(url, **proxy_kwargs)
proxy.url = url # store URL on proxy for info
coverage_logfile = coverage.get_filename(coveragedir, node_number) if coveragedir else None
return coverage.AuthServiceProxyWrapper(proxy, url, coverage_logfile)
return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile)
def p2p_port(n):
@@ -371,76 +321,38 @@ def rpc_url(datadir, i, chain, rpchost):
################
def initialize_datadir(dirname, n, chain, disable_autoconnect=True):
def initialize_datadir(dirname, n, chain):
datadir = get_datadir_path(dirname, n)
if not os.path.isdir(datadir):
os.makedirs(datadir)
write_config(os.path.join(datadir, "particl.conf"), n=n, chain=chain, disable_autoconnect=disable_autoconnect)
os.makedirs(os.path.join(datadir, 'stderr'), exist_ok=True)
os.makedirs(os.path.join(datadir, 'stdout'), exist_ok=True)
return datadir
def write_config(config_path, *, n, chain, extra_config="", disable_autoconnect=True):
# Translate chain subdirectory name to config name
if chain == 'testnet':
# Translate chain name to config name
if chain == 'testnet3':
chain_name_conf_arg = 'testnet'
chain_name_conf_section = 'test'
else:
chain_name_conf_arg = chain
chain_name_conf_section = chain
with open(config_path, 'w', encoding='utf8') as f:
if chain_name_conf_arg:
f.write("{}=1\n".format(chain_name_conf_arg))
if chain_name_conf_section:
f.write("[{}]\n".format(chain_name_conf_section))
with open(os.path.join(datadir, "particl.conf"), 'w', encoding='utf8') as f:
f.write("{}=1\n".format(chain_name_conf_arg))
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"]:
return i
scriptPubKey = tx["vout"][i]["scriptPubKey"]
if "addresses" in scriptPubKey:
if any([addr == a for a in scriptPubKey["addresses"]]):
return i
elif "address" in scriptPubKey:
if addr == scriptPubKey["address"]:
return i
raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr))

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -13,8 +13,8 @@ from enum import IntEnum, auto
from typing import Optional
CURRENT_DB_VERSION = 28
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,7 +215,6 @@ 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_utxos = Column("blob")
# Address to spend lock tx to - address from wallet if empty TODO
@@ -647,17 +644,6 @@ class CheckedBlock(Table):
block_time = Column("integer")
class CoinRates(Table):
__tablename__ = "coinrates"
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")
def create_db(db_path: str, log) -> None:
con = None
try:
@@ -929,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
@@ -942,29 +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:
query += f" AND {ck} = :{ck} "
query_data[ck] = constraint_value
query += f" AND {ck} = :{ck} "
for order_col, order_dir in order_by.items():
if validColumnName(order_col) is False:
@@ -977,6 +947,7 @@ class DBMethods:
if query_suffix:
query += query_suffix
query_data = constraints.copy()
query_data.update(extra_query_data)
rows = cursor.execute(query, query_data)
for row in rows:

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -104,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),
},
)
@@ -412,27 +410,6 @@ def upgradeDatabase(self, db_version):
elif current_version == 24:
db_version += 1
cursor.execute("ALTER TABLE bidstates ADD COLUMN can_accept INTEGER")
elif current_version == 25:
db_version += 1
cursor.execute(
"""
CREATE TABLE coinrates (
record_id INTEGER NOT NULL,
currency_from INTEGER,
currency_to INTEGER,
rate VARCHAR,
source VARCHAR,
last_updated INTEGER,
PRIMARY KEY (record_id))"""
)
elif current_version == 26:
db_version += 1
cursor.execute("ALTER TABLE offers ADD COLUMN auto_accept_type INTEGER")
elif current_version == 27:
db_version += 1
cursor.execute("ALTER TABLE offers ADD COLUMN pk_from BLOB")
cursor.execute("ALTER TABLE bids ADD COLUMN pk_bid_addr BLOB")
if current_version != db_version:
self.db_version = db_version
self.setIntKV("db_version", db_version, cursor)

View File

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

View File

@@ -1,25 +1,19 @@
# -*- 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 (
@@ -36,7 +30,6 @@ from .basicswap_util import (
strTxState,
strBidState,
)
from .util.rfc2440 import verify_rfc2440_password
from .js_server import (
js_error,
@@ -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
@@ -243,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
@@ -266,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,
@@ -278,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,
@@ -287,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()
@@ -298,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):
@@ -314,93 +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_explorers(self, url_split, post_string):
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
@@ -414,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)]
@@ -570,6 +410,7 @@ class HttpHandler(BaseHTTPRequestHandler):
return self.render_template(
template,
{
"refresh": 30,
"active_swaps": [
(
s[0].hex(),
@@ -606,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]
@@ -614,15 +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)
swap_client.stopRunning()
return self.page_info("Shutting down", extra_headers=extra_headers)
return self.page_info("Shutting down")
def page_index(self, url_split):
swap_client = self.server.swap_client
@@ -643,81 +477,22 @@ 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):
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:
@@ -725,20 +500,18 @@ class HttpHandler(BaseHTTPRequestHandler):
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",
@@ -747,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:
@@ -777,8 +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 == "active":
return self.page_active(url_split, post_string)
if page == "wallets":
@@ -845,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}")
self.wfile.write(response)
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}")
self.wfile.write(response)
def do_HEAD(self):
self.putHeaders(200, "text/html")
@@ -873,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
@@ -884,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
@@ -894,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")
conn.close()
except Exception:
pass
# 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()
def serve_forever(self):
self.timeout = 1
while not self.stop_event.is_set():
self.handle_request()
self.socket.close()
self.swap_client.log.info("HTTP server stopped.")
def run(self):
self.serve_forever()

View File

@@ -106,31 +106,6 @@ class BCHInterface(BTCInterface):
) + self.make_int(u["amount"], r=1)
return unspent_addr
def createWallet(self, wallet_name: str, password: str = ""):
self.rpc("createwallet", [wallet_name, False])
if password != "":
self.rpc(
"encryptwallet",
[
password,
],
override_wallet=wallet_name,
)
def newKeypool(self) -> None:
self._log.debug("Refreshing keypool.")
# Use up current keypool
wi = self.rpc_wallet("getwalletinfo")
keypool_size: int = wi["keypoolsize"]
for i in range(keypool_size):
_ = self.rpc_wallet("getnewaddress")
keypoolsize_hd_internal: int = wi["keypoolsize_hd_internal"]
for i in range(keypoolsize_hd_internal):
_ = self.rpc_wallet("getrawchangeaddress")
self.rpc_wallet("keypoolrefill")
# returns pkh
def decodeAddress(self, address: str) -> bytes:
return bytes(Address.from_string(address).payload)

View File

@@ -10,13 +10,8 @@ import base64
import hashlib
import json
import logging
import mmap
import os
import shutil
import sqlite3
import traceback
from io import BytesIO
from basicswap.basicswap_util import (
@@ -382,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:
@@ -392,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",
[
@@ -400,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,
},
@@ -417,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)
self.rpc_wallet("sethdseed", [True, key_wif])
def getWalletInfo(self):
rv = self.rpc_wallet("getwalletinfo")
@@ -472,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:
@@ -483,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
@@ -1878,70 +1862,20 @@ 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(),
],
)
signature = self.rpc_wallet(
"signmessage",
[sign_for_addr, sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex()],
)
prove_utxos = [] # TODO: Send specific utxos
return (sign_for_addr, signature, prove_utxos)
@@ -1994,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()}")
@@ -2239,21 +1951,13 @@ 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())
self._sc.checkWalletSeed(self.coin_type())
def lockWallet(self):
self._log.info(f"lockWallet - {self.ticker()}")

View File

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

View File

@@ -332,14 +332,14 @@ class DCRInterface(Secp256k1Interface):
def testDaemonRPC(self, with_wallet=True) -> None:
if with_wallet:
self.rpc_wallet("walletislocked")
self.rpc_wallet("getinfo")
else:
self.rpc("getblockchaininfo")
def getChainHeight(self) -> int:
return self.rpc("getblockcount")
def initialiseWallet(self, key: bytes, restore_time: int = -1) -> None:
def initialiseWallet(self, key: bytes) -> None:
# Load with --create
pass
@@ -354,9 +354,7 @@ class DCRInterface(Secp256k1Interface):
walletislocked = self.rpc_wallet("walletislocked")
return True, walletislocked
def changeWalletPassword(
self, old_password: str, new_password: str, check_seed_if_encrypt: bool = True
):
def changeWalletPassword(self, old_password: str, new_password: str):
self._log.info("changeWalletPassword - {}".format(self.ticker()))
if old_password == "":
# Read initial pwd from settings
@@ -370,15 +368,14 @@ 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())
self._sc.checkWalletSeed(self.coin_type())
def lockWallet(self):
self._log.info("lockWallet - {}".format(self.ticker()))

View File

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

View File

@@ -51,32 +51,13 @@ class FIROInterface(BTCInterface):
def getExchangeName(self, exchange_name: str) -> str:
return "zcoin"
def initialiseWallet(self, key, restore_time: int = -1):
def initialiseWallet(self, key):
# load with -hdseed= parameter
pass
def checkWallets(self) -> int:
return 1
def encryptWallet(self, password: str, check_seed: bool = True):
# Watchonly wallets are not encrypted
# Firo shuts down after encryptwallet
seed_id_before: str = self.getWalletSeedID() if check_seed else "Not found"
self.rpc_wallet("encryptwallet", [password])
if check_seed is False or seed_id_before == "Not found":
return
seed_id_after: str = self.getWalletSeedID()
if seed_id_before == seed_id_after:
return
self._log.warning(f"{self.ticker()} wallet seed changed after encryption.")
self._log.debug(
f"seed_id_before: {seed_id_before} seed_id_after: {seed_id_after}."
)
self.setWalletSeedWarning(True)
def getNewAddress(self, use_segwit, label="swap_receive"):
return self.rpc("getnewaddress", [label])
# addr_plain = self.rpc('getnewaddress', [label])

View File

@@ -146,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()))
@@ -156,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())
self._sc.checkWalletSeed(self.coin_type())

View File

@@ -87,7 +87,7 @@ class NAVInterface(BTCInterface):
# p2sh-p2wsh
return True
def initialiseWallet(self, key, restore_time: int = -1):
def initialiseWallet(self, key):
# Load with -importmnemonic= parameter
pass

View File

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

View File

@@ -110,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):

View File

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

View File

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

View File

@@ -7,7 +7,6 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import logging
import os
import basicswap.contrib.ed25519_fast as edf
import basicswap.ed25519_fast_util as edu
@@ -202,57 +201,25 @@ class XMRInterface(CoinInterface):
try:
self.rpc_wallet("open_wallet", params)
# TODO Remove `refresh` after upstream fix to refresh on open_wallet
self.rpc_wallet("refresh")
except Exception as e:
if "no connection to daemon" in str(e):
self._log.debug(f"{self.coin_name()} {e}")
return # Bypass refresh error to allow startup with a busy daemon
if any(
x in str(e)
for x in (
"invalid signature",
"std::bad_alloc",
"basic_string::_M_replace_aux",
)
):
self._log.error(f"{self.coin_name()} wallet is corrupt.")
chain_client_settings = self._sc.getChainClientSettings(
self.coin_type()
) # basicswap.json
if chain_client_settings.get("manage_wallet_daemon", False):
self._log.info(f"Renaming {self.coin_name()} wallet cache file.")
walletpath = os.path.join(
chain_client_settings.get("datadir", "none"),
"wallets",
filename,
)
if not os.path.isfile(walletpath):
self._log.warning(
f"Could not find {self.coin_name()} wallet cache file."
)
raise
bkp_path = walletpath + ".corrupt"
for i in range(100):
if not os.path.exists(bkp_path):
break
bkp_path = walletpath + f".corrupt{i}"
if os.path.exists(bkp_path):
self._log.error(
f"Could not find backup path for {self.coin_name()} wallet."
)
raise
os.rename(walletpath, bkp_path)
# Drop through to open_wallet
else:
raise
else:
try:
self.rpc_wallet("close_wallet")
self._log.debug(f"Closing {self.coin_name()} wallet")
except Exception as e: # noqa: F841
pass
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"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)
@@ -484,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:
@@ -538,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)[
@@ -608,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.")
@@ -664,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")
@@ -745,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},
@@ -760,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 != "":
@@ -783,11 +730,11 @@ class XMRInterface(CoinInterface):
self._wallet_password = orig_password
raise e
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
def unlockWallet(self, password: str) -> None:
self._log.info("unlockWallet - {}".format(self.ticker()))
self._wallet_password = password
if check_seed and not self._have_checked_seed:
if not self._have_checked_seed:
self._sc.checkWalletSeed(self.coin_type())
def lockWallet(self) -> None:

View File

@@ -14,7 +14,6 @@ from .util import (
toBool,
)
from .basicswap_util import (
fiatFromTicker,
strBidState,
strTxState,
SwapTypes,
@@ -23,9 +22,6 @@ from .basicswap_util import (
from .chainparams import (
Coins,
chainparams,
Fiat,
getCoinIdFromTicker,
getCoinIdFromName,
)
from .ui.util import (
PAGE_LIMIT,
@@ -37,6 +33,7 @@ from .ui.util import (
get_data_entry,
get_data_entry_or,
have_data_entry,
tickerToCoinId,
listOldBidStates,
checkAddressesOwned,
)
@@ -127,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]
@@ -328,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(),
@@ -342,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")
@@ -824,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
@@ -850,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.")
@@ -883,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,
}
)
@@ -965,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")
@@ -973,134 +962,86 @@ 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.")
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",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
}
response = swap_client.readURL(url, headers=default_headers)
try:
error = json.loads(response.decode())
if "Error" in error:
return json.dumps({"Error": error["Error"]}).encode()
except json.JSONDecodeError:
pass
return response
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",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
}
response = swap_client.readURL(url, headers=default_headers)
try:
error = json.loads(response.decode())
if "Error" in error:
return json.dumps({"Error": error["Error"]}).encode()
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()
filters = {
"sort_by": "created_at",
"sort_dir": "desc"
}
EXCLUDED_STATES = [
'Completed',
'Expired',
'Timed-out',
'Abandoned',
'Failed, refunded',
'Failed, swiped',
'Failed',
'Error',
'received'
]
all_bids = []
try:
for bid_id, (bid, offer) in list(swap_client.swaps_in_progress.items()):
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_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": swap_client.ci(offer.coin_from).coin_name(),
"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(offer.coin_from).format_amount(
bid.amount
),
"amount_from": swap_client.ci(bid[9]).format_amount(bid[4]),
"amount_to": swap_client.ci(offer.coin_to).format_amount(
bid.amount_to
(bid[4] * bid[10]) // swap_client.ci(bid[9]).COIN()
),
"addr_from": bid.bid_addr if bid.was_received else offer.addr_from,
"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'
}
}
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()
if hasattr(bid, "rate"):
swap_data["rate"] = bid.rate
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",
)
endpoints = {
pages = {
"coins": js_coins,
"wallets": js_wallets,
"offers": js_offers,
@@ -1126,11 +1067,10 @@ endpoints = {
"help": js_help,
"readurl": js_readurl,
"active": js_active,
"coinprices": js_coinprices,
}
def js_url_to_function(url_split):
if len(url_split) > 2:
return endpoints.get(url_split[2], js_404)
return pages.get(url_split[2], js_404)
return js_index

View File

@@ -136,7 +136,6 @@ class OfferMessage(NonProtobufClass):
17: ("amount_negotiable", 0, 2),
18: ("rate_negotiable", 0, 2),
19: ("proof_utxos", 2, 0),
20: ("auto_accept_type", 0, 0),
}

View File

@@ -6,17 +6,17 @@
# 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:
node0 connecting to node1
node0 send_handshake
node1 process_handshake
node1 send_ping - With a version field
node0 recv_ping
Both nodes are initialised
Handshake procedure:
node0 connecting to node1
node0 send_handshake
node1 process_handshake
node1 send_ping - With a version field
node0 recv_ping
Both nodes are initialised
XChaCha20_Poly1305 mac is 16bytes
XChaCha20_Poly1305 mac is 16bytes
"""
import time

View File

@@ -1,350 +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 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,
)
from basicswap.basicswap_util import (
BidStates,
)
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()
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.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 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(100):
message = ws_thread.cmd_queue_get()
if message is not None:
data = json.loads(message)
# print(f"json: {json.dumps(data, indent=4)}")
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 sendSimplexMsg(
self, network, addr_from: str, addr_to: str, payload: bytes, msg_valid: int, cursor
) -> bytes:
self.log.debug("sendSimplexMsg")
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)
smsg_id = smsgGetID(smsg_msg)
ws_thread = network["ws_thread"]
sent_id = ws_thread.send_command("#bsx " + encode_base64(smsg_msg))
response = waitForResponse(ws_thread, sent_id, self.delay_event)
if response["resp"]["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 bid_addr AS address FROM bids WHERE active_ind = 1
AND (in_progress = 1 OR (state > :bid_received AND state < :bid_completed) OR (state IN (:bid_received, :bid_sent) AND 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,
{
"bid_received": int(BidStates.BID_RECEIVED),
"bid_completed": int(BidStates.SWAP_COMPLETED),
"bid_sent": int(BidStates.BID_SENT),
"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 readSimplexMsgs(self, network):
ws_thread = network["ws_thread"]
for i in range(100):
message = ws_thread.queue_get()
if message is None:
break
data = json.loads(message)
# self.log.debug(f"message 1: {json.dumps(data, indent=4)}")
try:
if data["resp"]["type"] in ("chatItemsStatusesUpdated", "newChatItems"):
for chat_item in data["resp"]["chatItems"]:
item_status = chat_item["chatItem"]["meta"]["itemStatus"]
if item_status["type"] in ("sndRcvd", "rcvNew"):
snd_progress = item_status.get("sndProgress", None)
if snd_progress:
if snd_progress != "complete":
item_id = chat_item["chatItem"]["meta"]["itemId"]
self.log.debug(
f"simplex chat item {item_id} {snd_progress}"
)
continue
try:
msg_data: bytes = decode_base64(
chat_item["chatItem"]["content"]["msgContent"]["text"]
)
decrypted_msg = decryptSimplexMsg(self, msg_data)
if decrypted_msg is None:
continue
self.processMsg(decrypted_msg)
except Exception as e: # noqa: F841
# self.log.debug(f"decryptSimplexMsg error: {e}")
pass
except Exception as e:
self.log.debug(f"readSimplexMsgs error: {e}")
self.delay_event.wait(0.05)
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(response["resp"]["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 response["resp"]["connection"]
network = {
"type": "simplex",
"ws_thread": ws_thread,
}
self.active_networks.append(network)

View File

@@ -1,107 +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 subprocess
import time
from basicswap.bin.run 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
# logging.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 ValueError("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,
) -> Daemon:
logger.info("Starting Simplex client")
if not os.path.exists(data_path):
os.makedirs(data_path)
db_path = os.path.join(data_path, "simplex_client_data")
args = [bin_path, "-d", db_path, "-s", server_address, "-p", str(websocket_port)]
if not os.path.exists(db_path):
# Need to set initial profile through CLI
# TODO: Must be a better way?
init_args = args + ["-e", "/help"] # Run command ro exit client
initSimplexClient(init_args, logger, delay_event)
args += ["-l", "debug"]
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,
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 743 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,5 +1,22 @@
// Constants and State
const PAGE_SIZE = 50;
const COIN_NAME_TO_SYMBOL = {
'Bitcoin': 'BTC',
'Litecoin': 'LTC',
'Monero': 'XMR',
'Particl': 'PART',
'Particl Blind': 'PART',
'Particl Anon': 'PART',
'PIVX': 'PIVX',
'Firo': 'FIRO',
'Dash': 'DASH',
'Decred': 'DCR',
'Wownero': 'WOW',
'Bitcoin Cash': 'BCH',
'Dogecoin': 'DOGE'
};
// Global state
const state = {
identities: new Map(),
currentPage: 1,
@@ -10,6 +27,7 @@ const state = {
refreshPromise: null
};
// DOM
const elements = {
swapsBody: document.getElementById('active-swaps-body'),
prevPageButton: document.getElementById('prevPage'),
@@ -22,6 +40,105 @@ const elements = {
statusText: document.getElementById('status-text')
};
// Identity Manager
const IdentityManager = {
cache: new Map(),
pendingRequests: new Map(),
retryDelay: 2000,
maxRetries: 3,
cacheTimeout: 5 * 60 * 1000, // 5 minutes
async getIdentityData(address) {
if (!address) {
return { address: '' };
}
const cachedData = this.getCachedIdentity(address);
if (cachedData) {
return { ...cachedData, address };
}
if (this.pendingRequests.has(address)) {
const pendingData = await this.pendingRequests.get(address);
return { ...pendingData, address };
}
const request = this.fetchWithRetry(address);
this.pendingRequests.set(address, request);
try {
const data = await request;
this.cache.set(address, {
data,
timestamp: Date.now()
});
return { ...data, address };
} catch (error) {
console.warn(`Error fetching identity for ${address}:`, error);
return { address };
} finally {
this.pendingRequests.delete(address);
}
},
getCachedIdentity(address) {
const cached = this.cache.get(address);
if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
return cached.data;
}
if (cached) {
this.cache.delete(address);
}
return null;
},
async fetchWithRetry(address, attempt = 1) {
try {
const response = await fetch(`/json/identities/${address}`, {
signal: AbortSignal.timeout(5000)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
...data,
address,
num_sent_bids_successful: safeParseInt(data.num_sent_bids_successful),
num_recv_bids_successful: safeParseInt(data.num_recv_bids_successful),
num_sent_bids_failed: safeParseInt(data.num_sent_bids_failed),
num_recv_bids_failed: safeParseInt(data.num_recv_bids_failed),
num_sent_bids_rejected: safeParseInt(data.num_sent_bids_rejected),
num_recv_bids_rejected: safeParseInt(data.num_recv_bids_rejected),
label: data.label || '',
note: data.note || '',
automation_override: safeParseInt(data.automation_override)
};
} catch (error) {
if (attempt >= this.maxRetries) {
console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`);
return {
address,
num_sent_bids_successful: 0,
num_recv_bids_successful: 0,
num_sent_bids_failed: 0,
num_recv_bids_failed: 0,
num_sent_bids_rejected: 0,
num_recv_bids_rejected: 0,
label: '',
note: '',
automation_override: 0
};
}
await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
return this.fetchWithRetry(address, attempt + 1);
}
}
};
const safeParseInt = (value) => {
const parsed = parseInt(value);
return isNaN(parsed) ? 0 : parsed;
@@ -70,7 +187,7 @@ const getStatusClass = (status, tx_a, tx_b) => {
const getTxStatusClass = (status) => {
if (!status || status === 'None') return 'text-gray-400';
if (status.includes('Complete') || status.includes('Confirmed')) {
return 'text-green-500';
}
@@ -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,8 +502,8 @@ const createSwapTableRow = async (swap) => {
const identity = await IdentityManager.getIdentityData(swap.addr_from);
const uniqueId = `${swap.bid_id}_${swap.created_at}`;
const fromSymbol = window.CoinManager.getSymbol(swap.coin_from) || swap.coin_from;
const toSymbol = window.CoinManager.getSymbol(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;
@@ -304,7 +513,7 @@ const createSwapTableRow = async (swap) => {
<td class="relative w-0 p-0 m-0">
<div class="absolute top-0 bottom-0 left-0 w-1"></div>
</td>
<!-- Time Column -->
<td class="py-3 pl-1 pr-2 text-xs whitespace-nowrap">
<div class="flex items-center">
@@ -368,8 +577,8 @@ const createSwapTableRow = async (swap) => {
<div class="py-3 px-4 text-center">
<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/${swap.coin_from.replace(' ', '-')}.png"
<img class="h-12"
src="/static/images/coins/${swap.coin_from.replace(' ', '-')}.png"
alt="${swap.coin_from}"
onerror="this.src='/static/images/coins/default.png'">
</span>
@@ -377,8 +586,8 @@ const createSwapTableRow = async (swap) => {
<path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"></path>
</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/${swap.coin_to.replace(' ', '-')}.png"
<img class="h-12"
src="/static/images/coins/${swap.coin_to.replace(' ', '-')}.png"
alt="${swap.coin_to}"
onerror="this.src='/static/images/coins/default.png'">
</span>
@@ -395,10 +604,11 @@ const createSwapTableRow = async (swap) => {
</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>
@@ -406,7 +616,7 @@ const createSwapTableRow = async (swap) => {
<!-- Actions Column -->
<td class="py-3 px-4 text-center">
<a href="/bid/${swap.bid_id}"
<a href="/bid/${swap.bid_id}"
class="inline-block w-20 py-1 px-2 font-medium text-center text-sm rounded-md bg-blue-500 text-white border border-blue-500 hover:bg-blue-600 transition duration-200">
Details
</a>
@@ -501,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;
@@ -528,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;
@@ -555,7 +753,7 @@ async function updateSwapsTable(options = {}) {
}
const totalPages = Math.ceil(state.swapsData.length / PAGE_SIZE);
if (resetPage && state.swapsData.length > 0) {
state.currentPage = 1;
}
@@ -566,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]');
@@ -588,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">
@@ -598,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) {
@@ -613,10 +825,7 @@ async function updateSwapsTable(options = {}) {
}
}
function isActiveSwap(swap) {
return true;
}
// Event
const setupEventListeners = () => {
if (elements.refreshSwapsButton) {
elements.refreshSwapsButton.addEventListener('click', async (e) => {
@@ -656,11 +865,8 @@ const setupEventListeners = () => {
}
};
document.addEventListener('DOMContentLoaded', async () => {
// Init
document.addEventListener('DOMContentLoaded', () => {
WebSocketManager.initialize();
setupEventListeners();
await updateSwapsTable({ resetPage: true, refreshData: true });
const autoRefreshInterval = setInterval(async () => {
await updateSwapsTable({ resetPage: false, refreshData: true });
}, 10000); // 30 seconds
});

View File

@@ -1,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}`;
@@ -320,13 +547,13 @@ const createBidTableRow = async (bid) => {
</a>
</div>
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
<span class="font-semibold">Offer ID:</span>
<span class="font-semibold">Offer ID:</span>
<a href="/offer/${bid.offer_id}" data-tooltip-target="tooltip-offer-${uniqueId}" class="hover:underline">
${formatAddress(bid.offer_id)}
</a>
</div>
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
<span class="font-semibold">Bid ID:</span>
<span class="font-semibold">Bid ID:</span>
<a href="/bid/${bid.bid_id}" data-tooltip-target="tooltip-bid-${uniqueId}" class="hover:underline">
${formatAddress(bid.bid_id)}
</a>
@@ -351,8 +578,8 @@ const createBidTableRow = async (bid) => {
<div class="py-3 px-4 text-center">
<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/${bid.coin_from.replace(' ', '-')}.png"
<img class="h-12"
src="/static/images/coins/${bid.coin_from.replace(' ', '-')}.png"
alt="${bid.coin_from}"
onerror="this.src='/static/images/coins/default.png'">
</span>
@@ -360,14 +587,15 @@ const createBidTableRow = async (bid) => {
<path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"></path>
</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/${bid.coin_to.replace(' ', '-')}.png"
<img class="h-12"
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">
@@ -394,7 +622,7 @@ const createBidTableRow = async (bid) => {
<!-- Actions Column -->
<td class="py-3 px-4 text-center">
<a href="/bid/${bid.bid_id}/accept"
<a href="/bid/${bid.bid_id}/accept"
class="inline-block w-20 py-1 px-2 font-medium text-center text-sm rounded-md bg-blue-500 text-white border border-blue-500 hover:bg-blue-600 transition duration-200">
Accept
</a>
@@ -490,13 +718,13 @@ const createDetailsColumn = (bid, identity, uniqueId) => `
</a>
</div>
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
<span class="font-semibold">Offer ID:</span>
<span class="font-semibold">Offer ID:</span>
<a href="/offer/${bid.offer_id}" data-tooltip-target="tooltip-offer-${uniqueId}" class="hover:underline">
${formatAddress(bid.offer_id)}
</a>
</div>
<div class="monospace text-xs text-gray-500 dark:text-gray-300">
<span class="font-semibold">Bid ID:</span>
<span class="font-semibold">Bid ID:</span>
<a href="/bid/${bid.bid_id}" data-tooltip-target="tooltip-bid-${uniqueId}" class="hover:underline">
${formatAddress(bid.bid_id)}
</a>
@@ -624,6 +852,7 @@ async function updateBidsTable(options = {}) {
}
}
// Event
const setupEventListeners = () => {
if (elements.refreshBidsButton) {
elements.refreshBidsButton.addEventListener('click', async () => {
@@ -663,8 +892,8 @@ if (elements.refreshBidsButton) {
}
};
document.addEventListener('DOMContentLoaded', async () => {
// Init
document.addEventListener('DOMContentLoaded', () => {
WebSocketManager.initialize();
setupEventListeners();
await updateBidsTable({ resetPage: true, refreshData: true });
});

View File

@@ -5,7 +5,7 @@ const BidExporter = {
}
const isSent = type === 'sent';
const headers = [
'Date/Time',
'Bid ID',
@@ -19,9 +19,9 @@ const BidExporter = {
'Created At',
'Expires At'
];
let csvContent = headers.join(',') + '\n';
bids.forEach(bid => {
const row = [
`"${formatTime(bid.created_at)}"`,
@@ -36,17 +36,17 @@ const BidExporter = {
bid.created_at,
bid.expire_at
];
csvContent += row.join(',') + '\n';
});
return csvContent;
},
download(content, filename) {
try {
const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(blob, filename);
return;
@@ -54,48 +54,48 @@ const BidExporter = {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 100);
} catch (error) {
console.error('Error downloading CSV:', error);
const csvData = 'data:text/csv;charset=utf-8,' + encodeURIComponent(content);
const link = document.createElement('a');
link.setAttribute('href', csvData);
link.setAttribute('download', filename);
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
},
exportCurrentView() {
const type = state.currentTab;
const data = state.data[type];
if (!data || !data.length) {
alert('No data to export');
return;
}
const csvContent = this.toCSV(data, type);
const now = new Date();
const dateStr = now.toISOString().split('T')[0];
const filename = `bsx_${type}_bids_${dateStr}.csv`;
this.download(csvContent, filename);
}
};
@@ -111,7 +111,7 @@ document.addEventListener('DOMContentLoaded', function() {
BidExporter.exportCurrentView();
});
}
const exportReceivedButton = document.getElementById('exportReceivedBids');
if (exportReceivedButton) {
EventManager.add(exportReceivedButton, 'click', (e) => {
@@ -127,14 +127,14 @@ document.addEventListener('DOMContentLoaded', function() {
const originalCleanup = window.cleanup || function(){};
window.cleanup = function() {
originalCleanup();
const exportSentButton = document.getElementById('exportSentBids');
const exportReceivedButton = document.getElementById('exportReceivedBids');
if (exportSentButton && typeof EventManager !== 'undefined') {
EventManager.remove(exportSentButton, 'click');
}
if (exportReceivedButton && typeof EventManager !== 'undefined') {
EventManager.remove(exportReceivedButton, 'click');
}

View File

@@ -1,3 +1,4 @@
// Constants and State
const PAGE_SIZE = 50;
const state = {
currentPage: {
@@ -97,7 +98,7 @@ const EventManager = {
add(element, type, handler, options = false) {
if (!element) return null;
if (!this.listeners.has(element)) {
this.listeners.set(element, new Map());
}
@@ -116,7 +117,7 @@ const EventManager = {
remove(element, type, handler, options = false) {
if (!element) return;
const elementListeners = this.listeners.get(element);
if (!elementListeners) return;
@@ -140,7 +141,7 @@ const EventManager = {
removeAll(element) {
if (!element) return;
const elementListeners = this.listeners.get(element);
if (!elementListeners) return;
@@ -166,225 +167,246 @@ const EventManager = {
};
function cleanup() {
//console.log('Starting comprehensive cleanup process for bids table');
console.log('Starting cleanup process');
EventManager.clearAll();
try {
if (searchTimeout) {
clearTimeout(searchTimeout);
searchTimeout = null;
}
const exportSentButton = document.getElementById('exportSentBids');
const exportReceivedButton = document.getElementById('exportReceivedBids');
if (state.refreshPromise) {
state.isRefreshing = false;
}
if (exportSentButton) {
exportSentButton.remove();
}
if (window.WebSocketManager) {
WebSocketManager.disconnect();
}
if (exportReceivedButton) {
exportReceivedButton.remove();
}
cleanupTooltips();
forceTooltipDOMCleanup();
if (window.TooltipManager) {
const originalCleanup = window.TooltipManager.cleanup;
window.TooltipManager.cleanup = function() {
originalCleanup.call(window.TooltipManager);
if (window.TooltipManager) {
window.TooltipManager.cleanup();
}
setTimeout(() => {
forceTooltipDOMCleanup();
tooltipIdsToCleanup.clear();
const cleanupTableBody = (tableId) => {
const tbody = document.getElementById(tableId);
if (!tbody) return;
const rows = tbody.querySelectorAll('tr');
rows.forEach(row => {
if (window.CleanupManager) {
CleanupManager.removeListenersByElement(row);
} else {
EventManager.removeAll(row);
const detachedTooltips = document.querySelectorAll('[id^="tooltip-"]');
detachedTooltips.forEach(tooltip => {
const tooltipId = tooltip.id;
const trigger = document.querySelector(`[data-tooltip-target="${tooltipId}"]`);
if (!trigger || !document.body.contains(trigger)) {
tooltip.remove();
}
Array.from(row.attributes).forEach(attr => {
if (attr.name.startsWith('data-')) {
row.removeAttribute(attr.name);
}
});
});
while (tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
};
}, 10);
};
}
cleanupTableBody('sent-tbody');
cleanupTableBody('received-tbody');
WebSocketManager.cleanup();
if (searchTimeout) {
clearTimeout(searchTimeout);
searchTimeout = null;
}
state.data = {
sent: [],
received: []
};
IdentityManager.clearCache();
Object.keys(elements).forEach(key => {
elements[key] = null;
});
console.log('Cleanup completed');
}
if (window.CleanupManager) {
CleanupManager.clearAll();
} else {
EventManager.clearAll();
document.addEventListener('beforeunload', cleanup);
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
WebSocketManager.pause();
} else {
WebSocketManager.resume();
}
});
// WebSocket Management
const WebSocketManager = {
ws: null,
processingQueue: false,
reconnectTimeout: null,
maxReconnectAttempts: 5,
reconnectAttempts: 0,
reconnectDelay: 5000,
healthCheckInterval: null,
isPaused: false,
lastMessageTime: Date.now(),
initialize() {
this.connect();
this.startHealthCheck();
},
isConnected() {
return this.ws?.readyState === WebSocket.OPEN;
},
connect() {
if (this.isConnected() || this.isPaused) return;
if (this.ws) {
this.cleanupConnection();
}
const clearAllAnimationFrames = () => {
const rafList = window.requestAnimationFrameList;
if (Array.isArray(rafList)) {
rafList.forEach(id => {
cancelAnimationFrame(id);
});
window.requestAnimationFrameList = [];
}
};
clearAllAnimationFrames();
state.data = {
sent: [],
received: []
};
state.currentPage = {
sent: 1,
received: 1
};
state.isLoading = false;
state.isRefreshing = false;
state.wsConnected = false;
state.refreshPromise = null;
state.filters = {
state: -1,
sort_by: 'created_at',
sort_dir: 'desc',
with_expired: true,
searchQuery: '',
coin_from: 'any',
coin_to: 'any'
};
if (window.IdentityManager) {
IdentityManager.clearCache();
}
if (window.CacheManager) {
CacheManager.cleanup(true);
}
if (window.MemoryManager) {
MemoryManager.forceCleanup();
}
Object.keys(elements).forEach(key => {
elements[key] = null;
});
console.log('Comprehensive cleanup completed');
} catch (error) {
console.error('Error during cleanup process:', error);
try {
if (window.EventManager) EventManager.clearAll();
if (window.CleanupManager) CleanupManager.clearAll();
if (window.WebSocketManager) WebSocketManager.disconnect();
state.data = { sent: [], received: [] };
state.isLoading = false;
Object.keys(elements).forEach(key => {
elements[key] = null;
});
} catch (e) {
console.error('Failsafe cleanup also failed:', e);
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();
}
}
}
},
window.cleanupBidsTable = cleanup;
setupEventHandlers() {
if (!this.ws) return;
this.ws.onopen = () => {
state.wsConnected = true;
this.reconnectAttempts = 0;
this.lastMessageTime = Date.now();
updateConnectionStatus('connected');
console.log('🟢 WebSocket connection established for Sent Bids / Received Bids');
updateBidsTable();
};
CleanupManager.addListener(document, 'visibilitychange', () => {
if (document.hidden) {
//console.log('Page hidden - pausing WebSocket and optimizing memory');
this.ws.onmessage = () => {
this.lastMessageTime = Date.now();
if (this.isPaused) return;
if (!this.processingQueue) {
this.processingQueue = true;
setTimeout(async () => {
try {
if (!state.isRefreshing) {
await updateBidsTable();
}
} finally {
this.processingQueue = false;
}
}, 200);
}
};
if (WebSocketManager && typeof WebSocketManager.pause === 'function') {
WebSocketManager.pause();
} else if (WebSocketManager && typeof WebSocketManager.disconnect === 'function') {
WebSocketManager.disconnect();
this.ws.onclose = () => {
state.wsConnected = false;
updateConnectionStatus('disconnected');
if (!this.isPaused) {
this.handleReconnect();
}
};
this.ws.onerror = () => {
updateConnectionStatus('error');
};
},
startHealthCheck() {
this.stopHealthCheck();
this.healthCheckInterval = setInterval(() => {
if (this.isPaused) return;
const timeSinceLastMessage = Date.now() - this.lastMessageTime;
if (timeSinceLastMessage > 120000) {
console.log('WebSocket connection appears stale. Reconnecting...');
this.cleanupConnection();
this.connect();
return;
}
if (!this.isConnected()) {
this.handleReconnect();
}
}, 30000);
},
stopHealthCheck() {
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
this.healthCheckInterval = null;
}
},
handleReconnect() {
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
window.TooltipManager.cleanup();
}
if (this.isPaused) return;
// Run memory optimization
if (window.MemoryManager) {
MemoryManager.forceCleanup();
}
} else {
if (WebSocketManager && typeof WebSocketManager.resume === 'function') {
WebSocketManager.resume();
} else if (WebSocketManager && typeof WebSocketManager.connect === 'function') {
WebSocketManager.connect();
}
const lastUpdateTime = state.lastRefresh || 0;
const now = Date.now();
const refreshInterval = 5 * 60 * 1000; // 5 minutes
if (now - lastUpdateTime > refreshInterval) {
this.reconnectAttempts++;
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1);
//console.log(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
this.reconnectTimeout = setTimeout(() => this.connect(), delay);
} else {
updateConnectionStatus('error');
//console.log('Maximum reconnection attempts reached. Will try again in 60 seconds.');
setTimeout(() => {
updateBidsTable();
}, 500);
this.reconnectAttempts = 0;
this.connect();
}, 60000);
}
}
});
},
CleanupManager.addListener(window, 'beforeunload', () => {
cleanup();
});
function cleanupRow(row) {
if (!row) return;
const tooltipTriggers = row.querySelectorAll('[data-tooltip-target]');
tooltipTriggers.forEach(trigger => {
if (window.TooltipManager) {
window.TooltipManager.destroy(trigger);
cleanupConnection() {
if (this.ws) {
this.ws.onopen = null;
this.ws.onmessage = null;
this.ws.onclose = null;
this.ws.onerror = null;
if (this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.close(1000, 'Cleanup');
} catch (e) {
console.warn('Error closing WebSocket:', e);
}
}
this.ws = null;
}
});
},
if (window.CleanupManager) {
CleanupManager.removeListenersByElement(row);
} else {
EventManager.removeAll(row);
}
row.removeAttribute('data-offer-id');
row.removeAttribute('data-bid-id');
while (row.firstChild) {
const child = row.firstChild;
row.removeChild(child);
}
}
function optimizeMemoryUsage() {
const MAX_BIDS_IN_MEMORY = 500;
['sent', 'received'].forEach(type => {
if (state.data[type] && state.data[type].length > MAX_BIDS_IN_MEMORY) {
console.log(`Trimming ${type} bids data from ${state.data[type].length} to ${MAX_BIDS_IN_MEMORY}`);
state.data[type] = state.data[type].slice(0, MAX_BIDS_IN_MEMORY);
pause() {
this.isPaused = true;
//console.log('WebSocket operations paused');
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
});
},
cleanupOffscreenTooltips();
resume() {
if (!this.isPaused) return;
this.isPaused = false;
//console.log('WebSocket operations resumed');
this.lastMessageTime = Date.now();
if (!this.isConnected()) {
this.reconnectAttempts = 0;
this.connect();
}
},
if (window.IdentityManager && typeof IdentityManager.limitCacheSize === 'function') {
IdentityManager.limitCacheSize(100);
cleanup() {
this.isPaused = true;
this.stopHealthCheck();
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
this.cleanupConnection();
}
};
if (window.MemoryManager) {
MemoryManager.forceCleanup();
}
}
// Core
const safeParseInt = (value) => {
const parsed = parseInt(value);
return isNaN(parsed) ? 0 : parsed;
@@ -490,6 +512,7 @@ function coinMatches(offerCoin, filterCoin) {
return false;
}
// State
function hasActiveFilters() {
const coinFromSelect = document.getElementById('coin_from');
const coinToSelect = document.getElementById('coin_to');
@@ -557,58 +580,11 @@ function filterAndSortData(bids) {
const searchStr = state.filters.searchQuery.toLowerCase();
const matchesBidId = bid.bid_id.toLowerCase().includes(searchStr);
const matchesIdentity = bid.addr_from?.toLowerCase().includes(searchStr);
let label = '';
try {
if (window.IdentityManager) {
let identity = null;
if (IdentityManager.cache && typeof IdentityManager.cache.get === 'function') {
identity = IdentityManager.cache.get(bid.addr_from);
}
if (identity && identity.label) {
label = identity.label;
} else if (identity && identity.data && identity.data.label) {
label = identity.data.label;
}
if (!label && bid.identity) {
label = bid.identity.label || '';
}
}
} catch (e) {
console.warn('Error accessing identity for search:', e);
}
const identity = IdentityManager.cache.get(bid.addr_from);
const label = identity?.data?.label || '';
const matchesLabel = label.toLowerCase().includes(searchStr);
let matchesDisplayedLabel = false;
if (!matchesLabel && document) {
try {
const tableId = state.currentTab === 'sent' ? 'sent' : 'received';
const cells = document.querySelectorAll(`#${tableId} a[href^="/identity/"]`);
for (const cell of cells) {
const href = cell.getAttribute('href');
const cellAddress = href ? href.split('/').pop() : '';
if (cellAddress === bid.addr_from) {
const cellText = cell.textContent.trim().toLowerCase();
if (cellText.includes(searchStr)) {
matchesDisplayedLabel = true;
break;
}
}
}
} catch (e) {
console.warn('Error checking displayed labels:', e);
}
}
if (!(matchesBidId || matchesIdentity || matchesLabel || matchesDisplayedLabel)) {
if (!(matchesBidId || matchesIdentity || matchesLabel)) {
return false;
}
}
@@ -623,37 +599,6 @@ function filterAndSortData(bids) {
});
}
async function preloadIdentitiesForSearch(bids) {
if (!window.IdentityManager || typeof IdentityManager.getIdentityData !== 'function') {
return;
}
try {
const addresses = new Set();
bids.forEach(bid => {
if (bid.addr_from) {
addresses.add(bid.addr_from);
}
});
const BATCH_SIZE = 20;
const addressArray = Array.from(addresses);
for (let i = 0; i < addressArray.length; i += BATCH_SIZE) {
const batch = addressArray.slice(i, i + BATCH_SIZE);
await Promise.all(batch.map(addr => IdentityManager.getIdentityData(addr)));
if (i + BATCH_SIZE < addressArray.length) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
console.log(`Preloaded ${addressArray.length} identities for search`);
} catch (error) {
console.error('Error preloading identities:', error);
}
}
function updateCoinFilterImages() {
const coinToSelect = document.getElementById('coin_to');
const coinFromSelect = document.getElementById('coin_from');
@@ -748,6 +693,108 @@ const updateConnectionStatus = (status) => {
});
};
// Identity
const IdentityManager = {
cache: new Map(),
pendingRequests: new Map(),
retryDelay: 2000,
maxRetries: 3,
cacheTimeout: 5 * 60 * 1000,
maxCacheSize: 500,
async getIdentityData(address) {
if (!address) return { address: '' };
const cachedData = this.getCachedIdentity(address);
if (cachedData) return { ...cachedData, address };
if (this.pendingRequests.has(address)) {
try {
const pendingData = await this.pendingRequests.get(address);
return { ...pendingData, address };
} catch (error) {
this.pendingRequests.delete(address);
}
}
const request = this.fetchWithRetry(address);
this.pendingRequests.set(address, request);
try {
const data = await request;
this.trimCacheIfNeeded();
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) {
cached.timestamp = Date.now();
return cached.data;
}
if (cached) {
this.cache.delete(address);
}
return null;
},
trimCacheIfNeeded() {
if (this.cache.size > this.maxCacheSize) {
const entries = Array.from(this.cache.entries());
const sortedByAge = entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
const toRemove = Math.ceil(this.maxCacheSize * 0.2);
for (let i = 0; i < toRemove && i < sortedByAge.length; i++) {
this.cache.delete(sortedByAge[i][0]);
}
console.log(`Trimmed identity cache: removed ${toRemove} oldest entries`);
}
},
clearCache() {
this.cache.clear();
this.pendingRequests.clear();
},
async fetchWithRetry(address, attempt = 1) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(`/json/identities/${address}`, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return await response.json();
} catch (error) {
if (attempt >= this.maxRetries) {
console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`);
return { address };
}
await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
return this.fetchWithRetry(address, attempt + 1);
}
}
};
// Stats
const processIdentityStats = (identity) => {
if (!identity) return null;
@@ -847,7 +894,8 @@ const createIdentityTooltipContent = (identity) => {
`;
};
const tooltipIdsToCleanup = new Set();
// Table
let tooltipIdsToCleanup = new Set();
const cleanupTooltips = () => {
if (window.TooltipManager) {
@@ -869,7 +917,8 @@ const forceTooltipDOMCleanup = () => {
foundCount += allTooltipElements.length;
allTooltipElements.forEach(element => {
const isDetached = !document.body.contains(element) ||
const isDetached = !document.body.contains(element) ||
element.classList.contains('hidden') ||
element.style.display === 'none';
@@ -877,7 +926,7 @@ const forceTooltipDOMCleanup = () => {
const triggerId = element.id;
const triggerElement = document.querySelector(`[data-tooltip-target="${triggerId}"]`);
if (!triggerElement ||
if (!triggerElement ||
!document.body.contains(triggerElement) ||
triggerElement.classList.contains('hidden')) {
element.remove();
@@ -898,8 +947,9 @@ const forceTooltipDOMCleanup = () => {
const tippyRoots = document.querySelectorAll('[data-tippy-root]');
foundCount += tippyRoots.length;
tippyRoots.forEach(element => {
const isOrphan = !element.children.length ||
const isOrphan = !element.children.length ||
element.children[0].classList.contains('hidden') ||
!document.body.contains(element);
@@ -925,10 +975,13 @@ const forceTooltipDOMCleanup = () => {
}
}
});
// Handle legacy tooltip elements
document.querySelectorAll('.tooltip').forEach(element => {
const isTrulyDetached = !element.parentElement ||
const isTrulyDetached = !element.parentElement ||
!document.body.contains(element.parentElement) ||
element.classList.contains('hidden');
if (isTrulyDetached) {
try {
element.remove();
@@ -939,11 +992,14 @@ const forceTooltipDOMCleanup = () => {
}
});
if (window.TooltipManager && typeof window.TooltipManager.getActiveTooltipInstances === 'function') {
const activeTooltips = window.TooltipManager.getActiveTooltipInstances();
activeTooltips.forEach(([element, instance]) => {
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
if (!document.body.contains(element)) {
if (window.TooltipManager && window.TooltipManager.activeTooltips) {
window.TooltipManager.activeTooltips.forEach((instance, id) => {
const tooltipElement = document.getElementById(id.split('tooltip-trigger-')[1]);
const triggerElement = document.querySelector(`[data-tooltip-trigger-id="${id}"]`);
if (!tooltipElement || !triggerElement ||
!document.body.contains(tooltipElement) ||
!document.body.contains(triggerElement)) {
if (instance?.[0]) {
try {
instance[0].destroy();
@@ -951,13 +1007,14 @@ const forceTooltipDOMCleanup = () => {
console.warn('Error destroying tooltip instance:', e);
}
}
window.TooltipManager.activeTooltips.delete(id);
}
});
}
if (removedCount > 0) {
// console.log(`Tooltip cleanup: found ${foundCount}, removed ${removedCount} detached tooltips`);
}
}
};
const createTableRow = async (bid) => {
const identity = await IdentityManager.getIdentityData(bid.addr_from);
@@ -1033,14 +1090,14 @@ const createTableRow = async (bid) => {
<!-- Status Column -->
<td class="py-3 px-6">
<div class="relative flex justify-center" data-tooltip-target="tooltip-status-${uniqueId}">
<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(bid.bid_state)}">
<span class="w-full lg:w-7/8 xl:w-2/3 px-2.5 py-1 inline-flex items-center justify-center rounded-full text-xs font-medium bold ${getStatusClass(bid.bid_state)}">
${bid.bid_state}
</span>
</div>
</td>
<!-- Actions Column -->
<td class="py-3 pr-4">
<td class="py-3 pr-4 pl-3">
<div class="flex justify-center">
<a href="/bid/${bid.bid_id}"
class="inline-block w-20 py-1 px-2 font-medium text-center text-sm rounded-md bg-blue-500 text-white border border-blue-500 hover:bg-blue-600 transition duration-200">
@@ -1108,7 +1165,7 @@ const updateTableContent = async (type) => {
if (currentPageData.length > 0) {
const BATCH_SIZE = 10;
let allRows = [];
for (let i = 0; i < currentPageData.length; i += BATCH_SIZE) {
const batch = currentPageData.slice(i, i + BATCH_SIZE);
const rowPromises = batch.map(bid => createTableRow(bid));
@@ -1166,7 +1223,7 @@ const initializeTooltips = () => {
window.TooltipManager.cleanup();
const selector = '#' + state.currentTab + ' [data-tooltip-target]';
let selector = '#' + state.currentTab + ' [data-tooltip-target]';
const tooltipTriggers = document.querySelectorAll(selector);
const tooltipCount = tooltipTriggers.length;
if (tooltipCount > 50) {
@@ -1187,7 +1244,7 @@ const initializeTooltips = () => {
});
const offscreenTooltips = Array.from(tooltipTriggers).filter(t => !viewportTooltips.includes(t));
offscreenTooltips.forEach(trigger => {
const createTooltipOnHover = () => {
createTooltipForTrigger(trigger);
@@ -1206,7 +1263,7 @@ const initializeTooltips = () => {
const createTooltipForTrigger = (trigger) => {
if (!trigger || !window.TooltipManager) return;
const targetId = trigger.getAttribute('data-tooltip-target');
const tooltipContent = document.getElementById(targetId);
@@ -1250,7 +1307,7 @@ function cleanupOffscreenTooltips() {
const farOffscreenTriggers = Array.from(tooltipTriggers).filter(trigger => {
const rect = trigger.getBoundingClientRect();
return (rect.bottom < -window.innerHeight * 2 ||
return (rect.bottom < -window.innerHeight * 2 ||
rect.top > window.innerHeight * 3);
});
@@ -1293,6 +1350,7 @@ function implementVirtualizedRows() {
});
}
// Fetching
let activeFetchController = null;
const fetchBids = async () => {
@@ -1312,7 +1370,7 @@ const fetchBids = async () => {
activeFetchController.abort();
}
}, 30000);
const response = await fetch(endpoint, {
method: 'POST',
headers: {
@@ -1328,14 +1386,14 @@ const fetchBids = async () => {
}),
signal: activeFetchController.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
let data = await response.json();
//console.log('Received raw data:', data.length, 'bids');
state.filters.with_expired = includeExpired;
@@ -1351,7 +1409,7 @@ const fetchBids = async () => {
} else {
processedData = filterAndSortData(data);
}
return processedData;
} catch (error) {
if (error.name === 'AbortError') {
@@ -1367,19 +1425,20 @@ const fetchBids = async () => {
const updateBidsTable = async () => {
if (state.isLoading) {
//console.log('Already loading, skipping update');
return;
}
try {
//console.log('Starting updateBidsTable for tab:', state.currentTab);
//console.log('Current filters:', state.filters);
state.isLoading = true;
updateLoadingState(true);
const bids = await fetchBids();
// Add identity preloading if we're searching
if (state.filters.searchQuery && state.filters.searchQuery.length > 0) {
await preloadIdentitiesForSearch(bids);
}
//console.log('Fetched bids:', bids.length);
state.data[state.currentTab] = bids;
state.currentPage[state.currentTab] = 1;
@@ -1437,6 +1496,7 @@ const updatePaginationControls = (type) => {
}
};
// Filter
let searchTimeout;
function handleSearch(event) {
if (searchTimeout) {
@@ -1641,6 +1701,7 @@ const setupRefreshButtons = () => {
});
};
// Tabs
const switchTab = (tabId) => {
if (state.isLoading) return;
@@ -1857,22 +1918,15 @@ function setupMemoryMonitoring() {
const intervalId = setInterval(() => {
if (document.hidden) {
console.log('Tab hidden - running memory optimization');
if (window.IdentityManager) {
if (typeof IdentityManager.limitCacheSize === 'function') {
IdentityManager.limitCacheSize(100);
}
}
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
IdentityManager.trimCacheIfNeeded();
if (window.TooltipManager) {
window.TooltipManager.cleanup();
}
if (state.data.sent.length > 1000) {
console.log('Trimming sent bids data');
state.data.sent = state.data.sent.slice(0, 1000);
}
if (state.data.received.length > 1000) {
console.log('Trimming received bids data');
state.data.received = state.data.received.slice(0, 1000);
@@ -1881,7 +1935,6 @@ function setupMemoryMonitoring() {
cleanupTooltips();
}
}, MEMORY_CHECK_INTERVAL);
document.addEventListener('beforeunload', () => {
clearInterval(intervalId);
}, { once: true });
@@ -1911,13 +1964,13 @@ function initialize() {
WebSocketManager.initialize();
setupEventListeners();
}, 10);
setTimeout(() => {
setupRefreshButtons();
setupFilterEventListeners();
updateCoinFilterImages();
}, 50);
setTimeout(() => {
updateClearFiltersButton();
state.currentTab = 'sent';
@@ -1925,12 +1978,6 @@ function initialize() {
updateBidsTable();
}, 100);
setInterval(() => {
if ((state.data.sent.length + state.data.received.length) > 1000) {
optimizeMemoryUsage();
}
}, 5 * 60 * 1000); // Check every 5 minutes
window.cleanupBidsTable = cleanup;
}

View File

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

View File

@@ -1,17 +1,15 @@
(function(window) {
'use strict';
const dropdownInstances = [];
function positionElement(targetEl, triggerEl, placement = 'bottom', offsetDistance = 8) {
targetEl.style.visibility = 'hidden';
targetEl.style.display = 'block';
const triggerRect = triggerEl.getBoundingClientRect();
const targetRect = targetEl.getBoundingClientRect();
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
let top, left;
top = triggerRect.bottom + offsetDistance;
@@ -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,9 +66,8 @@
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;
}
@@ -120,8 +114,8 @@
}
_handleOutsideClick(e) {
if (this._visible &&
!this._targetEl.contains(e.target) &&
if (this._visible &&
!this._targetEl.contains(e.target) &&
!this._triggerEl.contains(e.target)) {
this.hide();
}
@@ -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';
@@ -145,7 +133,7 @@
this._options.placement,
this._options.offset
);
this._visible = true;
this._options.onShow();
});
@@ -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;
}
}
@@ -186,7 +168,7 @@
document.querySelectorAll('[data-dropdown-toggle]').forEach(triggerEl => {
const targetId = triggerEl.getAttribute('data-dropdown-toggle');
const targetEl = document.getElementById(targetId);
if (targetEl) {
const placement = triggerEl.getAttribute('data-dropdown-placement');
new Dropdown(targetEl, triggerEl, {
@@ -202,8 +184,6 @@
initDropdowns();
}
Dropdown.instances = dropdownInstances;
window.Dropdown = Dropdown;
window.initDropdowns = initDropdowns;

View File

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

View File

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

View File

@@ -1,400 +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 {
const coins = (window.config && window.config.coins) ?
window.config.coins
.filter(coin => coin.usesCoinGecko)
.map(coin => getCoinBackendId ? getCoinBackendId(coin.name) : coin.name)
.join(',') :
'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin';
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coins}&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'
});
const volumeData = {};
Object.entries(response).forEach(([coinId, data]) => {
if (data && data.usd_24h_vol) {
volumeData[coinId] = {
total_volume: data.usd_24h_vol,
price_change_percentage_24h: data.usd_24h_change || 0
};
}
});
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() {
// Clear any pending requests or resources
rateLimiter.requestQueue = {};
rateLimiter.lastRequestTime = {};
state.isInitialized = false;
console.log('ApiManager disposed');
}
};
return publicAPI;
})();
function getCoinBackendId(coinName) {
const nameMap = {
'bitcoin-cash': 'bitcoincash',
'bitcoin cash': 'bitcoincash',
'firo': 'zcoin',
'zcoin': 'zcoin',
'bitcoincash': 'bitcoin-cash'
};
return nameMap[coinName.toLowerCase()] || coinName.toLowerCase();
}
window.Api = ApiManager;
window.ApiManager = ApiManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.apiManagerInitialized) {
ApiManager.initialize();
window.apiManagerInitialized = true;
}
});
//console.log('ApiManager initialized with methods:', Object.keys(ApiManager));
console.log('ApiManager initialized');

View File

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

View File

@@ -1,270 +0,0 @@
const CleanupManager = (function() {
const state = {
eventListeners: [],
timeouts: [],
intervals: [],
animationFrames: [],
resources: new Map(),
debug: false
};
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
};
},
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);
}
log('CleanupManager initialized');
return this;
}
};
return publicAPI;
})();
window.CleanupManager = CleanupManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.cleanupManagerInitialized) {
CleanupManager.initialize();
window.cleanupManagerInitialized = true;
}
});
//console.log('CleanupManager initialized with methods:', Object.keys(CleanupManager));
console.log('CleanupManager initialized');

View File

@@ -1,230 +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-Cash.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) {
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;
}
};
})();
window.CoinManager = CoinManager;
console.log('CoinManager initialized');

View File

@@ -1,321 +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 (particlVariants.includes(filterCoin)) {
return offerCoin === filterCoin;
}
return false;
},
update: function(path, value) {
const parts = path.split('.');
let current = this;
for (let i = 0; i < parts.length - 1; i++) {
if (!current[parts[i]]) {
current[parts[i]] = {};
}
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
return this;
},
get: function(path, defaultValue = null) {
const parts = path.split('.');
let current = this;
for (let i = 0; i < parts.length; i++) {
if (current === undefined || current === null) {
return defaultValue;
}
current = current[parts[i]];
}
return current !== undefined ? current : defaultValue;
},
dispose: function() {
state.isInitialized = false;
console.log('ConfigManager disposed');
}
};
const utils = {
formatNumber: function(number, decimals = 2) {
if (typeof number !== 'number' || isNaN(number)) {
console.warn('formatNumber received a non-number value:', number);
return '0';
}
try {
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
}).format(number);
} catch (e) {
return '0';
}
},
formatDate: function(timestamp, resolution) {
const date = new Date(timestamp);
const options = {
day: { hour: '2-digit', minute: '2-digit', hour12: true },
week: { month: 'short', day: 'numeric' },
month: { year: 'numeric', month: 'short', day: 'numeric' }
};
return date.toLocaleString('en-US', { ...options[resolution], timeZone: 'UTC' });
},
debounce: function(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
},
formatTimeLeft: function(timestamp) {
const now = Math.floor(Date.now() / 1000);
if (timestamp <= now) return "Expired";
return this.formatTime(timestamp);
},
formatTime: function(timestamp, addAgoSuffix = false) {
const now = Math.floor(Date.now() / 1000);
const diff = Math.abs(now - timestamp);
let timeString;
if (diff < 60) {
timeString = `${diff} seconds`;
} else if (diff < 3600) {
timeString = `${Math.floor(diff / 60)} minutes`;
} else if (diff < 86400) {
timeString = `${Math.floor(diff / 3600)} hours`;
} else if (diff < 2592000) {
timeString = `${Math.floor(diff / 86400)} days`;
} else if (diff < 31536000) {
timeString = `${Math.floor(diff / 2592000)} months`;
} else {
timeString = `${Math.floor(diff / 31536000)} years`;
}
return addAgoSuffix ? `${timeString} ago` : timeString;
},
escapeHtml: function(unsafe) {
if (typeof unsafe !== 'string') {
console.warn('escapeHtml received a non-string value:', unsafe);
return '';
}
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
},
formatPrice: function(coin, price) {
if (typeof price !== 'number' || isNaN(price)) {
console.warn(`Invalid price for ${coin}:`, price);
return 'N/A';
}
if (price < 0.000001) return price.toExponential(2);
if (price < 0.001) return price.toFixed(8);
if (price < 1) return price.toFixed(4);
if (price < 10) return price.toFixed(3);
if (price < 1000) return price.toFixed(2);
if (price < 100000) return price.toFixed(1);
return price.toFixed(0);
},
getEmptyPriceData: function() {
return {
'bitcoin': { usd: null, btc: null },
'bitcoin-cash': { usd: null, btc: null },
'dash': { usd: null, btc: null },
'dogecoin': { usd: null, btc: null },
'decred': { usd: null, btc: null },
'namecoin': { usd: null, btc: null },
'litecoin': { usd: null, btc: null },
'particl': { usd: null, btc: null },
'pivx': { usd: null, btc: null },
'monero': { usd: null, btc: null },
'zano': { usd: null, btc: null },
'wownero': { usd: null, btc: null },
'firo': { usd: null, btc: null }
};
},
getCoinSymbol: function(fullName) {
if (window.CoinManager) {
return window.CoinManager.getSymbol(fullName) || fullName;
}
return fullName;
}
};
return publicAPI;
})();
window.logger = {
log: function(message) {
console.log(`[AppLog] ${new Date().toISOString()}: ${message}`);
},
warn: function(message) {
console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`);
},
error: function(message) {
console.error(`[AppError] ${new Date().toISOString()}: ${message}`);
}
};
window.config = ConfigManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.configManagerInitialized) {
ConfigManager.initialize();
window.configManagerInitialized = true;
}
});
if (typeof module !== 'undefined') {
module.exports = ConfigManager;
}
console.log('ConfigManager initialized');

View File

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

View File

@@ -1,219 +0,0 @@
const MemoryManager = (function() {
const state = {
isMonitoringEnabled: false,
monitorInterval: null,
cleanupInterval: null
};
const config = {
monitorInterval: 30000,
cleanupInterval: 60000,
debug: false
};
function log(message, ...args) {
if (config.debug) {
console.log(`[MemoryManager] ${message}`, ...args);
}
}
const publicAPI = {
enableMonitoring: function(interval = config.monitorInterval) {
if (state.monitorInterval) {
clearInterval(state.monitorInterval);
}
state.isMonitoringEnabled = true;
config.monitorInterval = interval;
this.logMemoryUsage();
state.monitorInterval = setInterval(() => {
this.logMemoryUsage();
}, interval);
console.log(`Memory monitoring enabled - reporting every ${interval/1000} seconds`);
return true;
},
disableMonitoring: function() {
if (state.monitorInterval) {
clearInterval(state.monitorInterval);
state.monitorInterval = null;
}
state.isMonitoringEnabled = false;
console.log('Memory monitoring disabled');
return true;
},
logMemoryUsage: function() {
const timestamp = new Date().toLocaleTimeString();
console.log(`=== Memory Monitor [${timestamp}] ===`);
if (window.performance && window.performance.memory) {
console.log('Memory usage:', {
usedJSHeapSize: (window.performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
totalJSHeapSize: (window.performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB'
});
}
if (navigator.deviceMemory) {
console.log('Device memory:', navigator.deviceMemory, 'GB');
}
const nodeCount = document.querySelectorAll('*').length;
console.log('DOM node count:', nodeCount);
if (window.CleanupManager) {
const counts = CleanupManager.getResourceCounts();
console.log('Managed resources:', counts);
}
if (window.TooltipManager) {
const tooltipInstances = document.querySelectorAll('[data-tippy-root]').length;
const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]').length;
console.log('Tooltip instances:', tooltipInstances, '- Tooltip triggers:', tooltipTriggers);
}
if (window.CacheManager && window.CacheManager.getStats) {
const cacheStats = CacheManager.getStats();
console.log('Cache stats:', cacheStats);
}
if (window.IdentityManager && window.IdentityManager.getStats) {
const identityStats = window.IdentityManager.getStats();
console.log('Identity cache stats:', identityStats);
}
console.log('==============================');
},
enableAutoCleanup: function(interval = config.cleanupInterval) {
if (state.cleanupInterval) {
clearInterval(state.cleanupInterval);
}
config.cleanupInterval = interval;
this.forceCleanup();
state.cleanupInterval = setInterval(() => {
this.forceCleanup();
}, interval);
log('Auto-cleanup enabled every', interval/1000, 'seconds');
return true;
},
disableAutoCleanup: function() {
if (state.cleanupInterval) {
clearInterval(state.cleanupInterval);
state.cleanupInterval = null;
}
console.log('Memory auto-cleanup disabled');
return true;
},
forceCleanup: function() {
if (config.debug) {
console.log('Running memory cleanup...', new Date().toLocaleTimeString());
}
if (window.CacheManager && CacheManager.cleanup) {
CacheManager.cleanup(true);
}
if (window.TooltipManager && TooltipManager.cleanup) {
window.TooltipManager.cleanup();
}
document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => {
if (window.TooltipManager && TooltipManager.destroy) {
window.TooltipManager.destroy(element);
}
});
if (window.chartModule && chartModule.cleanup) {
chartModule.cleanup();
}
if (window.gc) {
window.gc();
} else {
const arr = new Array(1000);
for (let i = 0; i < 1000; i++) {
arr[i] = new Array(10000).join('x');
}
}
if (config.debug) {
console.log('Memory cleanup completed');
}
return true;
},
setDebugMode: function(enabled) {
config.debug = Boolean(enabled);
return `Debug mode ${config.debug ? 'enabled' : 'disabled'}`;
},
getStatus: function() {
return {
monitoring: {
enabled: Boolean(state.monitorInterval),
interval: config.monitorInterval
},
autoCleanup: {
enabled: Boolean(state.cleanupInterval),
interval: config.cleanupInterval
},
debug: config.debug
};
},
initialize: function(options = {}) {
if (options.debug !== undefined) {
this.setDebugMode(options.debug);
}
if (options.enableMonitoring) {
this.enableMonitoring(options.monitorInterval || config.monitorInterval);
}
if (options.enableAutoCleanup) {
this.enableAutoCleanup(options.cleanupInterval || config.cleanupInterval);
}
if (window.CleanupManager) {
window.CleanupManager.registerResource('memoryManager', this, (mgr) => mgr.dispose());
}
log('MemoryManager initialized');
return this;
},
dispose: function() {
this.disableMonitoring();
this.disableAutoCleanup();
log('MemoryManager disposed');
}
};
return publicAPI;
})();
window.MemoryManager = MemoryManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.memoryManagerInitialized) {
MemoryManager.initialize();
window.memoryManagerInitialized = true;
}
});
//console.log('MemoryManager initialized with methods:', Object.keys(MemoryManager));
console.log('MemoryManager initialized');

View File

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

View File

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

View File

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

View File

@@ -1,338 +0,0 @@
const SummaryManager = (function() {
const config = {
refreshInterval: window.config?.cacheDuration || 30000,
summaryEndpoint: '/json',
retryDelay: 5000,
maxRetries: 3,
requestTimeout: 15000
};
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('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);
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 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;
}
});
},
startRefreshTimer: function() {
startRefreshTimer();
},
stopRefreshTimer: function() {
stopRefreshTimer();
},
dispose: function() {
stopRefreshTimer();
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
webSocket.close();
}
webSocket = null;
}
};
return publicAPI;
})();
window.SummaryManager = SummaryManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.summaryManagerInitialized) {
window.SummaryManager = SummaryManager.initialize();
window.summaryManagerInitialized = true;
}
});
//console.log('SummaryManager initialized with methods:', Object.keys(SummaryManager));
console.log('SummaryManager initialized');

View File

@@ -1,588 +0,0 @@
const TooltipManager = (function() {
let instance = null;
class TooltipManagerImpl {
constructor() {
if (instance) {
return instance;
}
this.activeTooltips = new WeakMap();
this.tooltipIdCounter = 0;
this.pendingAnimationFrames = new Set();
this.tooltipElementsMap = new Map();
this.maxTooltips = 300;
this.cleanupThreshold = 1.3;
this.disconnectedCheckInterval = null;
this.setupStyles();
this.setupCleanupEvents();
this.initializeMutationObserver();
this.startDisconnectedElementsCheck();
instance = this;
}
create(element, content, options = {}) {
if (!element) return null;
this.destroy(element);
if (this.tooltipElementsMap.size > this.maxTooltips * this.cleanupThreshold) {
const oldestEntries = Array.from(this.tooltipElementsMap.entries())
.sort((a, b) => a[1].timestamp - b[1].timestamp)
.slice(0, 20);
oldestEntries.forEach(([el]) => {
this.destroy(el);
});
}
const originalContent = content;
const rafId = requestAnimationFrame(() => {
this.pendingAnimationFrames.delete(rafId);
if (!document.body.contains(element)) return;
const rect = element.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
this.createTooltip(element, originalContent, options, rect);
} else {
let retryCount = 0;
const retryCreate = () => {
const newRect = element.getBoundingClientRect();
if ((newRect.width > 0 && newRect.height > 0) || retryCount >= 3) {
if (newRect.width > 0 && newRect.height > 0) {
this.createTooltip(element, originalContent, options, newRect);
}
} else {
retryCount++;
const newRafId = requestAnimationFrame(retryCreate);
this.pendingAnimationFrames.add(newRafId);
}
};
const initialRetryId = requestAnimationFrame(retryCreate);
this.pendingAnimationFrames.add(initialRetryId);
}
});
this.pendingAnimationFrames.add(rafId);
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-') && window.jsonData) {
try {
const offerId = targetId.split('tooltip-offer-')[1];
let actualOfferId = offerId;
if (offerId.includes('_')) {
[actualOfferId] = offerId.split('_');
}
let offer = null;
if (Array.isArray(window.jsonData)) {
for (let i = 0; i < window.jsonData.length; i++) {
const o = window.jsonData[i];
if (o && (o.unique_id === offerId || o.offer_id === actualOfferId)) {
offer = o;
break;
}
}
}
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)';
}
}
} catch (e) {
console.warn('Error finding offer for tooltip:', e);
}
}
const tooltipId = `tooltip-${++this.tooltipIdCounter}`;
try {
if (typeof tippy !== 'function') {
console.error('Tippy.js is not loaded. Cannot create tooltip.');
return null;
}
const instance = tippy(element, {
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) {
instance.popper.firstElementChild.classList.add(bgClass);
instance.popper.setAttribute('data-for-tooltip-id', tooltipId);
}
const arrow = instance.popper.querySelector('.tippy-arrow');
if (arrow) {
arrow.style.setProperty('color', arrowColor, 'important');
}
},
popperOptions: {
strategy: 'fixed',
modifiers: [
{
name: 'preventOverflow',
options: {
boundary: 'viewport',
padding: 10
}
},
{
name: 'flip',
options: {
padding: 10,
fallbackPlacements: ['top', 'bottom', 'right', 'left']
}
}
]
}
});
element.setAttribute('data-tooltip-trigger-id', tooltipId);
this.activeTooltips.set(element, instance);
this.tooltipElementsMap.set(element, {
timestamp: Date.now(),
id: tooltipId
});
return instance;
} catch (e) {
console.error('Error creating tooltip:', e);
return null;
}
}
destroy(element) {
if (!element) return;
const id = element.getAttribute('data-tooltip-trigger-id');
if (!id) return;
const instance = this.activeTooltips.get(element);
if (instance?.[0]) {
try {
instance[0].destroy();
} catch (e) {
console.warn('Error destroying tooltip:', e);
const tippyRoot = document.querySelector(`[data-for-tooltip-id="${id}"]`);
if (tippyRoot && tippyRoot.parentNode) {
tippyRoot.parentNode.removeChild(tippyRoot);
}
}
}
this.activeTooltips.delete(element);
this.tooltipElementsMap.delete(element);
element.removeAttribute('data-tooltip-trigger-id');
}
cleanup() {
this.pendingAnimationFrames.forEach(id => {
cancelAnimationFrame(id);
});
this.pendingAnimationFrames.clear();
const elements = document.querySelectorAll('[data-tooltip-trigger-id]');
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) {
const rafId = requestAnimationFrame(() => {
this.pendingAnimationFrames.delete(rafId);
processElementsBatch(endIdx);
});
this.pendingAnimationFrames.add(rafId);
} else {
this.cleanupOrphanedTippyElements();
}
};
if (elements.length > 0) {
processElementsBatch(0);
} else {
this.cleanupOrphanedTippyElements();
}
this.tooltipElementsMap.clear();
}
cleanupOrphanedTippyElements() {
const tippyElements = document.querySelectorAll('[data-tippy-root]');
tippyElements.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() {
this.boundCleanup = this.cleanup.bind(this);
this.handleVisibilityChange = () => {
if (document.hidden) {
this.cleanup();
if (window.MemoryManager) {
window.MemoryManager.forceCleanup();
}
}
};
window.addEventListener('beforeunload', this.boundCleanup);
window.addEventListener('unload', this.boundCleanup);
document.addEventListener('visibilitychange', this.handleVisibilityChange);
if (window.CleanupManager) {
window.CleanupManager.registerResource('tooltipManager', this, (tm) => tm.dispose());
}
this.cleanupInterval = setInterval(() => {
this.performPeriodicCleanup();
}, 120000);
}
startDisconnectedElementsCheck() {
if (this.disconnectedCheckInterval) {
clearInterval(this.disconnectedCheckInterval);
}
this.disconnectedCheckInterval = setInterval(() => {
this.checkForDisconnectedElements();
}, 60000);
}
checkForDisconnectedElements() {
if (this.tooltipElementsMap.size === 0) return;
const elementsToCheck = Array.from(this.tooltipElementsMap.keys());
let removedCount = 0;
elementsToCheck.forEach(element => {
if (!document.body.contains(element)) {
this.destroy(element);
removedCount++;
}
});
if (removedCount > 0) {
this.cleanupOrphanedTippyElements();
}
}
performPeriodicCleanup() {
this.cleanupOrphanedTippyElements();
this.checkForDisconnectedElements();
if (this.tooltipElementsMap.size > this.maxTooltips * this.cleanupThreshold) {
const sortedTooltips = Array.from(this.tooltipElementsMap.entries())
.sort((a, b) => a[1].timestamp - b[1].timestamp);
const tooltipsToRemove = sortedTooltips.slice(0, sortedTooltips.length - this.maxTooltips);
tooltipsToRemove.forEach(([element]) => {
this.destroy(element);
});
}
}
removeCleanupEvents() {
window.removeEventListener('beforeunload', this.boundCleanup);
window.removeEventListener('unload', this.boundCleanup);
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
if (this.disconnectedCheckInterval) {
clearInterval(this.disconnectedCheckInterval);
this.disconnectedCheckInterval = null;
}
}
initializeMutationObserver() {
if (this.mutationObserver) return;
this.mutationObserver = new MutationObserver(mutations => {
let needsCleanup = false;
mutations.forEach(mutation => {
if (mutation.removedNodes.length) {
Array.from(mutation.removedNodes).forEach(node => {
if (node.nodeType === 1) {
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(el => {
this.destroy(el);
});
needsCleanup = true;
}
}
}
});
}
});
if (needsCleanup) {
this.cleanupOrphanedTippyElements();
}
});
this.mutationObserver.observe(document.body, {
childList: true,
subtree: true
});
}
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'
});
}
});
}
dispose() {
this.cleanup();
this.pendingAnimationFrames.forEach(id => {
cancelAnimationFrame(id);
});
this.pendingAnimationFrames.clear();
if (this.mutationObserver) {
this.mutationObserver.disconnect();
this.mutationObserver = null;
}
this.removeCleanupEvents();
const styleElement = document.getElementById('tooltip-styles');
if (styleElement && styleElement.parentNode) {
styleElement.parentNode.removeChild(styleElement);
}
this.activeTooltips = new WeakMap();
this.tooltipElementsMap.clear();
instance = null;
}
initialize(options = {}) {
if (options.maxTooltips) {
this.maxTooltips = options.maxTooltips;
}
console.log('TooltipManager initialized');
return this;
}
}
return {
initialize: function(options = {}) {
if (!instance) {
const manager = new TooltipManagerImpl();
manager.initialize(options);
}
return instance;
},
getInstance: function() {
if (!instance) {
const manager = new TooltipManagerImpl();
}
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);
},
initializeTooltips: function(...args) {
const manager = this.getInstance();
return manager.initializeTooltips(...args);
},
dispose: function(...args) {
const manager = this.getInstance();
return manager.dispose(...args);
}
};
})();
window.TooltipManager = TooltipManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.tooltipManagerInitialized) {
TooltipManager.initialize();
TooltipManager.initializeTooltips();
window.tooltipManagerInitialized = true;
}
});
if (typeof module !== 'undefined' && module.exports) {
module.exports = TooltipManager;
}
//console.log('TooltipManager initialized with methods:', Object.keys(TooltipManager));
console.log('TooltipManager initialized');

View File

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

View File

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

View File

@@ -1,548 +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;
}
},
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;
}
},
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;
}
},
getRaw: (key) => {
try {
return localStorage.getItem(key);
} catch(e) {
console.warn(`Failed to retrieve raw 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');
}
};
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;
if (err_msg.indexOf('Coin From') >= 0) {
e = document.getElementById('coin_from');
e.classList.add('error');
}
};
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('Amount From') >= 0) {
e = document.getElementById('amt_from');
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);
}
}
if (err_msg.indexOf('Amount To') >= 0) {
e = document.getElementById('amt_to');
e.classList.add('error');
}
function saveAddress(value, text) {
Storage.set(STORAGE_KEY, { value, text });
if (err_msg.indexOf('Minimum Bid Amount') >= 0) {
e = document.getElementById('amt_bid_min');
e.classList.add('error');
}
if (err_msg.indexOf('Select coin you send') >= 0) {
e = document.getElementById('coin_from').parentNode;
e.classList.add('error');
}
}
form.addEventListener('submit', () => {
saveAddress(selectElement.value, selectElement.selectedOptions[0].text);
// 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');
});
});
});
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);
}
);
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');
}
};
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');
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 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');
}
};
function set_swap_type_enabled(coinFrom, coinTo, swapTypeElement) {
SwapTypeManager.setSwapTypeEnabled(coinFrom, coinTo, swapTypeElement);
}
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 => {
event.target.classList.remove('error');
});
});
},
updateDisabledStyles: () => {
document.querySelectorAll('select.disabled-select').forEach(select => {
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 => {
if (input.readOnly) {
input.classList.add('disabled-input-enabled');
} else {
input.classList.remove('disabled-input-enabled');
}
});
},
setupCustomSelects: () => {
const selectCache = {};
function updateSelectCache(select) {
if (!select || !select.options || select.selectedIndex === undefined) return;
const selectedOption = select.options[select.selectedIndex];
if (!selectedOption) return;
const image = selectedOption.getAttribute('data-image');
const name = selectedOption.textContent.trim();
selectCache[select.id] = { image, name };
}
function setSelectData(select) {
if (!select || !select.options || select.selectedIndex === undefined) return;
const selectedOption = select.options[select.selectedIndex];
if (!selectedOption) return;
const image = selectedOption.getAttribute('data-image') || '';
const name = selectedOption.textContent.trim();
select.style.backgroundImage = image ? `url(${image}?${new Date().getTime()})` : '';
const selectImage = select.nextElementSibling?.querySelector('.select-image');
if (selectImage) {
selectImage.src = image;
}
const selectNameElement = select.nextElementSibling?.querySelector('.select-name');
if (selectNameElement) {
selectNameElement.textContent = name;
}
updateSelectCache(select);
}
function setupCustomSelect(select) {
if (!select) return;
const options = select.querySelectorAll('option');
const selectIcon = select.parentElement?.querySelector('.select-icon');
const selectImage = select.parentElement?.querySelector('.select-image');
if (!options || !selectIcon || !selectImage) return;
options.forEach(option => {
const image = option.getAttribute('data-image');
if (image) {
option.style.backgroundImage = `url(${image})`;
}
});
const storedValue = Storage.getRaw(select.name);
if (storedValue && select.value == '-1') {
select.value = storedValue;
}
select.addEventListener('change', () => {
setSelectData(select);
Storage.setRaw(select.name, select.value);
});
setSelectData(select);
selectIcon.style.display = 'none';
selectImage.style.display = 'none';
}
const selectIcons = document.querySelectorAll('.custom-select .select-icon');
const selectImages = document.querySelectorAll('.custom-select .select-image');
const selectNames = document.querySelectorAll('.custom-select .select-name');
selectIcons.forEach(icon => icon.style.display = 'none');
selectImages.forEach(image => image.style.display = 'none');
selectNames.forEach(name => name.style.display = 'none');
const customSelects = document.querySelectorAll('.custom-select select');
customSelects.forEach(setupCustomSelect);
}
};
function initializeApp() {
handleNewOfferAddress();
DOM.addEvent('get_rate_inferred_button', 'click', RateManager.getRateInferred);
const coinFrom = DOM.get('coin_from');
const coinTo = DOM.get('coin_to');
const swapType = DOM.get('swap_type');
if (coinFrom && coinTo && swapType) {
SwapTypeManager.setSwapTypeEnabled(coinFrom.value, coinTo.value, swapType);
coinFrom.addEventListener('change', function() {
SwapTypeManager.setSwapTypeEnabled(this.value, coinTo.value, swapType);
RateManager.setRate('coin_from');
});
coinTo.addEventListener('change', function() {
SwapTypeManager.setSwapTypeEnabled(coinFrom.value, this.value, swapType);
RateManager.setRate('coin_to');
});
}
['amt_from', 'amt_to', 'rate'].forEach(id => {
DOM.addEvent(id, 'change', function() {
RateManager.setRate(id);
});
DOM.addEvent(id, 'input', function() {
RateManager.setRate(id);
});
});
DOM.addEvent('rate_lock', 'change', function() {
if (DOM.getValue('rate')) {
RateManager.setRate('rate');
}
});
DOM.addEvent('lookup_rates_button', 'click', RateManager.lookupRates);
UIEnhancer.handleErrorHighlighting();
UIEnhancer.updateDisabledStyles();
UIEnhancer.setupCustomSelects();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeApp);
} else {
initializeApp();
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,7 @@
show(tabId, force = false) {
const tab = this.getTab(tabId);
if ((tab !== this._activeTab) || force) {
this._items.forEach(t => {
if (t !== tab) {

View File

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

View File

@@ -42,21 +42,21 @@
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Details</span>
</div>
</th>
<th class="p-0">
<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 Send</span>
</div>
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-center">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Swap</span>
</div>
</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>
</div>
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-center">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Swap</span>
</div>
</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>
</div>
</th>
</th>
<th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-center">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Status</span>
@@ -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' %}

View File

@@ -532,12 +532,10 @@
<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 %}
{% 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>
@@ -557,27 +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-md 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-4" id="confirmTitle">Confirm Action</h2>
<p class="text-gray-600 dark:text-gray-200 mb-6 whitespace-pre-line" id="confirmMessage">Are you sure?</p>
<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>
@@ -585,76 +562,11 @@
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
let confirmCallback = null;
let triggerElement = null;
document.getElementById('confirmYes').addEventListener('click', function() {
if (typeof confirmCallback === 'function') {
confirmCallback();
}
hideConfirmDialog();
});
document.getElementById('confirmNo').addEventListener('click', hideConfirmDialog);
function showConfirmDialog(title, message, callback) {
confirmCallback = callback;
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
}
return false;
function confirmPopup(name) {
return confirm(name + " Bid - Are you sure?");
}
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 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();
}
});
};
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' %}
</body>
</html>
</html>

View File

@@ -808,12 +808,10 @@
<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 %}
{% 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>

View File

@@ -363,6 +363,6 @@
</div>
<script src="/static/js/bids_sentreceived.js"></script>
<script src="/static/js/bids_sentreceived_export.js"></script>
<script src="/static/js/bids_export.js"></script>
{% include 'footer.html' %}

View File

@@ -25,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.1</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">GUI: v3.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>
@@ -43,3 +43,68 @@
</div>
</div>
</section>
<script>
var toggleImages = function() {
var html = document.querySelector('html');
var darkImages = document.querySelectorAll('.dark-image');
var lightImages = document.querySelectorAll('.light-image');
if (html && html.classList.contains('dark')) {
toggleImageDisplay(darkImages, 'block');
toggleImageDisplay(lightImages, 'none');
} else {
toggleImageDisplay(darkImages, 'none');
toggleImageDisplay(lightImages, 'block');
}
};
var toggleImageDisplay = function(images, display) {
images.forEach(function(img) {
img.style.display = display;
});
};
document.addEventListener('DOMContentLoaded', function() {
var themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', function() {
toggleImages();
});
}
toggleImages();
});
</script>
<script>
var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
themeToggleLightIcon.classList.remove('hidden');
} else {
themeToggleDarkIcon.classList.remove('hidden');
}
function setTheme(theme) {
if (theme === 'light') {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
}
document.getElementById('theme-toggle').addEventListener('click', () => {
if (localStorage.getItem('color-theme') === 'dark') {
setTheme('light');
} else {
setTheme('dark');
}
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
toggleImages();
});
</script>

View File

@@ -9,83 +9,121 @@
swap_in_progress_green_svg, available_bids_svg, your_offers_svg, bids_received_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">
<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">
<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
});
window.getWebSocketConfig = window.getWebSocketConfig || function() {
return {
port: window.ws_port || '11700',
fallbackPort: '11700'
};
};
})();
(function() {
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');
}
document.documentElement.classList.toggle('dark', isDarkMode);
})();
</script>
<!-- Third-party Libraries -->
<!-- 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>
<!-- UI Components -->
<script src="/static/js/ui/tabs.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>
{% 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>
<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">
<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>
document.addEventListener('DOMContentLoaded', () => {
const tooltipManager = TooltipManager.initialize();
tooltipManager.initializeTooltips();
});
</script>
<!-- Dark mode initialization -->
<script>
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');
}
document.documentElement.classList.toggle('dark', isDarkMode);
</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">
@@ -686,3 +724,194 @@
</div>
</div>
</section>
<!-- WebSocket -->
{% if ws_port %}
<script>
(function() {
window.notificationConfig = {
showNewOffers: false,
showNewBids: true,
showBidAccepted: true
};
function ensureToastContainer() {
let container = document.getElementById('ul_updates');
if (!container) {
const floating_div = document.createElement('div');
floating_div.classList.add('floatright');
container = document.createElement('ul');
container.setAttribute('id', 'ul_updates');
floating_div.appendChild(container);
document.body.appendChild(floating_div);
}
return container;
}
function createToast(title, type = 'success') {
const messages = ensureToastContainer();
const message = document.createElement('li');
message.innerHTML = `
<div id="hide">
<div id="toast-${type}" class="flex items-center p-4 mb-4 w-full max-w-xs text-gray-500
bg-white rounded-lg shadow" role="alert">
<div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10
bg-blue-500 rounded-lg">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="18" width="18"
viewBox="0 0 24 24">
<g fill="#ffffff">
<path d="M8.5,20a1.5,1.5,0,0,1-1.061-.439L.379,12.5,2.5,10.379l6,6,13-13L23.621,
5.5,9.561,19.561A1.5,1.5,0,0,1,8.5,20Z"></path>
</g>
</svg>
</div>
<div class="uppercase w-40 ml-3 text-sm font-semibold text-gray-900">${title}</div>
<button type="button" onclick="closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5
bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-0 focus:outline-none
focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8">
<span class="sr-only">Close</span>
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1
1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293
4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</button>
</div>
</div>
`;
messages.appendChild(message);
}
function updateElement(elementId, value, options = {}) {
const element = document.getElementById(elementId);
if (!element) return false;
const safeValue = (value !== undefined && value !== null)
? value
: (element.dataset.lastValue || 0);
element.dataset.lastValue = safeValue;
if (elementId === 'sent-bids-counter' || elementId === 'recv-bids-counter') {
const svg = element.querySelector('svg');
element.textContent = safeValue;
if (svg) {
element.insertBefore(svg, element.firstChild);
}
} else {
element.textContent = safeValue;
}
if (['offers-counter', 'bid-requests-counter', 'sent-bids-counter',
'recv-bids-counter', 'swaps-counter', 'network-offers-counter',
'watched-outputs-counter'].includes(elementId)) {
element.classList.remove('bg-blue-500', 'bg-gray-400');
element.classList.add(safeValue > 0 ? 'bg-blue-500' : 'bg-gray-400');
}
if (elementId === 'swaps-counter') {
const swapContainer = document.getElementById('swapContainer');
if (swapContainer) {
const isSwapping = safeValue > 0;
if (isSwapping) {
swapContainer.innerHTML = `{{ swap_in_progress_green_svg | safe }}`;
swapContainer.style.animation = 'spin 2s linear infinite';
} else {
swapContainer.innerHTML = `{{ swap_in_progress_svg | safe }}`;
swapContainer.style.animation = 'none';
}
}
}
return true;
}
function fetchSummaryData() {
fetch('/json')
.then(response => response.json())
.then(data => {
updateElement('network-offers-counter', data.num_network_offers);
updateElement('offers-counter', data.num_sent_active_offers);
updateElement('sent-bids-counter', data.num_sent_active_bids);
updateElement('recv-bids-counter', data.num_recv_active_bids);
updateElement('bid-requests-counter', data.num_available_bids);
updateElement('swaps-counter', data.num_swapping);
updateElement('watched-outputs-counter', data.num_watched_outputs);
})
.catch(error => console.error('Summary data fetch error:', error));
}
function initWebSocket() {
const wsUrl = "ws://" + window.location.hostname + ":{{ ws_port }}";
const ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('🟢 WebSocket connection established for Dynamic Counters');
fetchSummaryData();
setInterval(fetchSummaryData, 30000); // Refresh every 30 seconds
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.event) {
let toastTitle;
let shouldShowToast = false;
switch (data.event) {
case 'new_offer':
toastTitle = `New network <a class="underline" href=/offer/${data.offer_id}>offer</a>`;
shouldShowToast = window.notificationConfig.showNewOffers;
break;
case 'new_bid':
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>New bid</a> on
<a class="underline" href=/offer/${data.offer_id}>offer</a>`;
shouldShowToast = window.notificationConfig.showNewBids;
break;
case 'bid_accepted':
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>Bid</a> accepted`;
shouldShowToast = window.notificationConfig.showBidAccepted;
break;
}
if (toastTitle && shouldShowToast) {
createToast(toastTitle);
}
}
fetchSummaryData();
} catch (error) {
console.error('WebSocket message processing error:', error);
}
};
ws.onerror = (error) => {
console.error('WebSocket Error:', error);
};
ws.onclose = (event) => {
console.log('WebSocket connection closed', event);
setTimeout(initWebSocket, 5000);
};
}
window.closeAlert = function(event) {
let element = event.target;
while (element.nodeName !== "BUTTON") {
element = element.parentNode;
}
element.parentNode.parentNode.removeChild(element.parentNode);
};
function init() {
initWebSocket();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
{% endif %}

View File

@@ -1,8 +1,8 @@
{% from 'style.html' import circular_info_messages_svg, green_cross_close_svg, red_cross_close_svg, circular_error_messages_svg %}
{% for m in messages %}
<section class="py-4 px-6" id="messages_{{ m[0] }}" role="alert">
<div class="lg:container mx-auto">
<section class="py-4" id="messages_{{ m[0] }}" role="alert">
<div class="container px-4 mx-auto">
<div class="p-6 text-green-800 rounded-lg bg-green-50 border border-green-500 dark:bg-gray-500 dark:text-green-400 rounded-md">
<div class="flex flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2">
@@ -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">

View File

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

View File

@@ -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>

View File

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

View File

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

View File

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

View File

@@ -8,12 +8,28 @@
};
</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">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover"
<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">
@@ -21,7 +37,7 @@
<p class="font-normal text-coolGray-200 dark:text-white">{{ page_type_description }}</p>
</div>
<div class="w-full md:w-1/2 p-3 flex justify-end items-center hidden">
<a id="refresh" href="/newoffer"
<a id="refresh" href="/newoffer"
class="rounded-full flex items-center justify-center px-4 py-2 bg-blue-500 hover:bg-green-600 hover:border-green-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">
{{ place_new_offer_svg | safe }}
<span>Place new Offer</span>
@@ -136,13 +152,12 @@
'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 == "" %}
{% set display_coins = coin_data.keys()|list %}
@@ -194,9 +209,9 @@
</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 mt-5 h-full overflow-hidden">
@@ -343,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">
@@ -402,5 +417,4 @@
<input type="hidden" name="formid" value="{{ form_id }}">
<script src="/static/js/offers.js"></script>
{% include 'footer.html' %}

View File

@@ -422,7 +422,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Enabled Coins</td>
<td class="py-3 px-6">
<label for="enabledchartcoins" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Coins to show data for: Blank for active coins, "all" for all known coins or comma separated<br/> list of coin tickers to show
<label for="enabledchartcoins" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Coins to show data for: Blank for active coins, "all" for all known coins or comma separated list of coin tickers to show
<br />
</label>
<input name="enabledchartcoins" type="text" 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" value="{{chart_settings.enabled_chart_coins}}">

View File

@@ -1,92 +1,27 @@
{% 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>
<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">
<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">
<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
});
window.getWebSocketConfig = window.getWebSocketConfig || function() {
return {
port: window.ws_port || '11700',
fallbackPort: '11700'
};
};
})();
(function() {
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');
}
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/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>
{% 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>
</head>
<head>
<meta charset="UTF-8">
{% if refresh %}
<meta http-equiv="refresh" content="{{ refresh }}">
{% endif %}
<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);
if (!localStorage.getItem('color-theme')) {
localStorage.setItem('color-theme', 'dark');
}
document.documentElement.classList.toggle('dark', isDarkMode);
})();
</script>
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', isDarkMode ? 'dark' : 'light');
}
document.documentElement.classList.toggle('dark', isDarkMode);
</script>
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
<title>(BSX) BasicSwap - v{{ version }}</title>
</head>
@@ -172,6 +107,7 @@
<script>
document.addEventListener('DOMContentLoaded', () => {
// Password toggle functionality
const passwordToggle = document.querySelector('.js-password-toggle');
if (passwordToggle) {
passwordToggle.addEventListener('change', function() {
@@ -190,6 +126,7 @@
});
}
// Image toggling function
function toggleImages() {
const html = document.querySelector('html');
const darkImages = document.querySelectorAll('.dark-image');
@@ -210,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>

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,29 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg, withdraw_svg, utxo_groups_svg, create_utxo_svg, lock_svg, eye_show_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">
<div class="container mx-auto">
<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">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/"><p>Home</p></a></li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/wallets">Wallets</a></li>
<li>{{ breadcrumb_line_svg | safe }}</li>
</ul>
</div>
</div>
</section>
<section class="py-3">
<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">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="dots-red">
<img class="absolute z-10 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 -m-3">
<div class="w-full md:w-1/2 p-3 h-48">
<h2 class="text-4xl font-bold text-white tracking-tighter">Wallets</h2>
<h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">Wallets</h2>
<div class="flex items-center">
<h2 class="text-lg font-bold text-white tracking-tighter mr-2">Total Assets:</h2>
<button id="hide-usd-amount-toggle" class="flex items-center justify-center p-1 focus:ring-0 focus:outline-none">{{ eye_show_svg | safe }}</button>
@@ -28,11 +42,11 @@
</div>
</div>
</section>
{% include 'inc_messages.html' %}
<section class="py-4">
<div class="container mx-auto">
<div class="container px-4 mx-auto">
<div class="flex flex-wrap -m-4">
{% for w in wallets %}
{% if w.havedata %}
@@ -59,7 +73,7 @@
<div class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }}</div>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white ">{{ w.ticker }} USD value:</h4>
<h4 class="text-xs font-medium dark:text-white usd-text">{{ w.ticker }} USD value:</h4>
<div class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 usd-value" data-coinname="{{ w.name }}"></div>
</div>
{% if w.pending %}
@@ -188,7 +202,448 @@
{% endfor %}
</div>
</section>
</div>
{% include 'footer.html' %}
<script>
const CONFIG = {
MAX_RETRIES: 3,
BASE_DELAY: 1000,
CACHE_EXPIRATION: 5 * 60 * 1000,
PRICE_UPDATE_INTERVAL: 5 * 60 * 1000,
API_TIMEOUT: 30000,
DEBOUNCE_DELAY: 500,
CACHE_MIN_INTERVAL: 60 * 1000
};
const STATE_KEYS = {
LAST_UPDATE: 'last-update-time',
PREVIOUS_TOTAL: 'previous-total-usd',
CURRENT_TOTAL: 'current-total-usd',
BALANCES_VISIBLE: 'balancesVisible'
};
const COIN_SYMBOLS = {
'Bitcoin': 'bitcoin',
'Particl': 'particl',
'Monero': 'monero',
'Wownero': 'wownero',
'Litecoin': 'litecoin',
'Dogecoin': 'dogecoin',
'Firo': 'zcoin',
'Dash': 'dash',
'PIVX': 'pivx',
'Decred': 'decred',
'Zano': 'zano',
'Bitcoin Cash': 'bitcoin-cash'
};
const SHORT_NAMES = {
'Bitcoin': 'BTC',
'Particl': 'PART',
'Monero': 'XMR',
'Wownero': 'WOW',
'Litecoin': 'LTC',
'Litecoin MWEB': 'LTC MWEB',
'Firo': 'FIRO',
'Dash': 'DASH',
'PIVX': 'PIVX',
'Decred': 'DCR',
'Zano': 'ZANO',
'Bitcoin Cash': 'BCH'
};
class Cache {
constructor(expirationTime) {
this.data = null;
this.timestamp = null;
this.expirationTime = expirationTime;
}
isValid() {
return Boolean(
this.data &&
this.timestamp &&
(Date.now() - this.timestamp < this.expirationTime)
);
}
set(data) {
this.data = data;
this.timestamp = Date.now();
}
get() {
if (this.isValid()) {
return this.data;
}
return null;
}
clear() {
this.data = null;
this.timestamp = null;
}
}
class ApiClient {
constructor() {
this.cache = new Cache(CONFIG.CACHE_EXPIRATION);
this.lastFetchTime = 0;
}
makeRequest(url, headers = {}) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/json/readurl');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.timeout = CONFIG.API_TIMEOUT;
xhr.ontimeout = () => {
reject(new Error('Request timed out'));
};
xhr.onload = () => {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.Error) {
reject(new Error(response.Error));
} else {
resolve(response);
}
} catch (error) {
reject(new Error(`Invalid JSON response: ${error.message}`));
}
} else {
reject(new Error(`HTTP Error: ${xhr.status} ${xhr.statusText}`));
}
};
xhr.onerror = () => {
reject(new Error('Network error occurred'));
};
xhr.send(JSON.stringify({ url, headers }));
});
}
async fetchPrices(forceUpdate = false) {
const now = Date.now();
const timeSinceLastFetch = now - this.lastFetchTime;
if (!forceUpdate && timeSinceLastFetch < CONFIG.CACHE_MIN_INTERVAL) {
const cachedData = this.cache.get();
if (cachedData) {
return cachedData;
}
}
let lastError = null;
for (let attempt = 0; attempt < CONFIG.MAX_RETRIES; attempt++) {
try {
const prices = await this.makeRequest(
'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC'
);
this.cache.set(prices);
this.lastFetchTime = now;
return prices;
} catch (error) {
lastError = error;
if (attempt < CONFIG.MAX_RETRIES - 1) {
const delay = Math.min(CONFIG.BASE_DELAY * Math.pow(2, attempt), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
const cachedData = this.cache.get();
if (cachedData) {
return cachedData;
}
throw lastError || new Error('Failed to fetch prices');
}
}
class UiManager {
constructor() {
this.api = new ApiClient();
this.toggleInProgress = false;
this.toggleDebounceTimer = null;
this.priceUpdateInterval = null;
this.lastUpdateTime = parseInt(localStorage.getItem(STATE_KEYS.LAST_UPDATE) || '0');
}
getShortName(fullName) {
return SHORT_NAMES[fullName] || fullName;
}
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 coinId = COIN_SYMBOLS[coinName];
const shortName = this.getShortName(coinName);
if (coinId) {
if (coinId === '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 (coinId === '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(`${coinId}-amount`, amount.toString());
}
el.setAttribute('data-original-value', `${amount} ${shortName}`);
}
}
});
}
async updatePrices(forceUpdate = false) {
try {
const prices = await this.api.fetchPrices(forceUpdate);
let newTotal = 0;
const currentTime = Date.now();
localStorage.setItem(STATE_KEYS.LAST_UPDATE, currentTime.toString());
this.lastUpdateTime = currentTime;
if (prices) {
Object.entries(COIN_SYMBOLS).forEach(([coinName, coinId]) => {
if (prices[coinId]?.usd) {
localStorage.setItem(`${coinId}-price`, prices[coinId].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;
const amount = amountStr ? parseFloat(amountStr.replace(/[^0-9.-]+/g, '')) : 0;
const coinId = COIN_SYMBOLS[coinName];
if (!coinId) return;
const price = prices?.[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
const usdValue = (amount * price).toFixed(2);
if (coinId === '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 (coinId === '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());
}
newTotal += parseFloat(usdValue);
const usdEl = el.closest('.flex')?.nextElementSibling?.querySelector('.usd-value');
if (usdEl) {
usdEl.textContent = `$${usdValue} USD`;
}
});
this.updateTotalValues(newTotal, prices?.bitcoin?.usd);
localStorage.setItem(STATE_KEYS.PREVIOUS_TOTAL, localStorage.getItem(STATE_KEYS.CURRENT_TOTAL) || '0');
localStorage.setItem(STATE_KEYS.CURRENT_TOTAL, newTotal.toString());
return true;
} catch (error) {
console.error('Price update failed:', error);
return false;
}
}
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 toggleBalances() {
if (this.toggleInProgress) return;
try {
this.toggleInProgress = true;
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
const newVisibility = !balancesVisible;
localStorage.setItem('balancesVisible', newVisibility.toString());
this.updateVisibility(newVisibility);
if (this.toggleDebounceTimer) {
clearTimeout(this.toggleDebounceTimer);
}
this.toggleDebounceTimer = window.setTimeout(async () => {
this.toggleInProgress = false;
if (newVisibility) {
await this.updatePrices(true);
}
}, CONFIG.DEBOUNCE_DELAY);
} catch (error) {
console.error('Failed to toggle balances:', error);
this.toggleInProgress = false;
}
}
updateVisibility(isVisible) {
if (isVisible) {
this.showBalances();
} else {
this.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>';
}
}
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) {
el.textContent = `$${parseFloat(storedValue).toFixed(2)} USD`;
el.style.color = 'white';
}
});
['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`;
}
}
});
}
hideBalances() {
const usdText = document.getElementById('usd-text');
if (usdText) {
usdText.style.display = 'none';
}
document.querySelectorAll('.coinname-value, .usd-value').forEach(el => {
el.textContent = '****';
});
['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 initialize() {
this.storeOriginalValues();
if (localStorage.getItem('balancesVisible') === null) {
localStorage.setItem('balancesVisible', 'true');
}
const hideBalancesToggle = document.getElementById('hide-usd-amount-toggle');
if (hideBalancesToggle) {
hideBalancesToggle.addEventListener('click', () => this.toggleBalances());
}
await this.loadBalanceVisibility();
if (this.priceUpdateInterval) {
clearInterval(this.priceUpdateInterval);
}
this.priceUpdateInterval = setInterval(() => {
if (localStorage.getItem('balancesVisible') === 'true' && !this.toggleInProgress) {
this.updatePrices(false);
}
}, CONFIG.PRICE_UPDATE_INTERVAL);
}
async loadBalanceVisibility() {
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
this.updateVisibility(balancesVisible);
if (balancesVisible) {
await this.updatePrices(true);
}
}
}
window.addEventListener('beforeunload', () => {
const uiManager = window.uiManager;
if (uiManager?.priceUpdateInterval) {
clearInterval(uiManager.priceUpdateInterval);
}
});
window.addEventListener('load', () => {
const uiManager = new UiManager();
window.uiManager = uiManager;
uiManager.initialize().catch(error => {
console.error('Failed to initialize application:', error);
});
});
</script>
</body>
</html>

View File

@@ -151,9 +151,7 @@ def page_bid(self, url_split, post_string):
)
def page_bids(
self, url_split, post_string, sent=False, available=False, received=False
):
def page_bids(self, url_split, post_string, sent=False, available=False, received=False):
server = self.server
swap_client = server.swap_client
swap_client.checkSystemStatus()
@@ -223,6 +221,8 @@ def page_bids(
return self.render_template(
template,
{
"page_type_available": "Bids Available",
"page_type_available_description": "Bids available for you to accept.",
"messages": messages,
"filters": filters,
"data": page_data,

View File

@@ -15,6 +15,7 @@ from .util import (
get_data_entry_or,
have_data_entry,
inputAmount,
known_chart_coins,
listAvailableCoins,
PAGE_LIMIT,
setCoinFilter,
@@ -31,7 +32,6 @@ from basicswap.basicswap_util import (
SwapTypes,
DebugTypes,
getLockName,
get_api_key_setting,
strBidState,
strSwapDesc,
strSwapType,
@@ -40,12 +40,12 @@ from basicswap.basicswap_util import (
)
from basicswap.chainparams import (
Coins,
ticker_map,
)
from basicswap.explorers import (
default_chart_api_key,
default_coingecko_api_key,
default_chart_api_key = (
"95dd900af910656e0e17c41f2ddc5dba77d01bf8b0e7d2787634a16bd976c553"
)
default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj"
def value_or_none(v):
@@ -733,9 +733,6 @@ def page_offer(self, url_split, post_string):
"swap_type": strSwapDesc(offer.swap_type),
"reverse": reverse_bid,
"form_id": get_data_entry_or(form_data, "formid", "") if form_data else "",
"auto_accept_type": (
offer.auto_accept_type if hasattr(offer, "auto_accept_type") else 0
),
}
data.update(extend_data)
@@ -976,20 +973,30 @@ def page_offers(self, url_split, post_string, sent=False):
coins_from, coins_to = listAvailableCoins(swap_client, split_from=True)
chart_api_key = get_api_key_setting(
swap_client.settings, "chart_api_key", default_chart_api_key
)
coingecko_api_key = get_api_key_setting(
swap_client.settings, "coingecko_api_key", default_coingecko_api_key
)
chart_api_key = swap_client.settings.get("chart_api_key", "")
if chart_api_key == "":
chart_api_key_enc = swap_client.settings.get("chart_api_key_enc", "")
chart_api_key = (
default_chart_api_key
if chart_api_key_enc == ""
else bytes.fromhex(chart_api_key_enc).decode("utf-8")
)
coingecko_api_key = swap_client.settings.get("coingecko_api_key", "")
if coingecko_api_key == "":
coingecko_api_key_enc = swap_client.settings.get("coingecko_api_key_enc", "")
coingecko_api_key = (
default_coingecko_api_key
if coingecko_api_key_enc == ""
else bytes.fromhex(coingecko_api_key_enc).decode("utf-8")
)
offers_count = len(formatted_offers)
enabled_chart_coins = []
enabled_chart_coins_setting = swap_client.settings.get("enabled_chart_coins", "")
if enabled_chart_coins_setting.lower() == "all":
for coin_ticker in ticker_map:
enabled_chart_coins.append(coin_ticker.upper())
enabled_chart_coins = known_chart_coins
elif enabled_chart_coins_setting.strip() == "":
for coin_id in swap_client.coin_clients:
if not swap_client.isCoinActive(coin_id):
@@ -1000,7 +1007,7 @@ def page_offers(self, url_split, post_string, sent=False):
continue
if (
enabled_ticker not in enabled_chart_coins
and enabled_ticker.lower() in ticker_map
and enabled_ticker in known_chart_coins
):
enabled_chart_coins.append(enabled_ticker)
else:
@@ -1009,7 +1016,7 @@ def page_offers(self, url_split, post_string, sent=False):
if (
upcased_ticker not in enabled_chart_coins
and upcased_ticker.lower() in ticker_map
and upcased_ticker in known_chart_coins
):
enabled_chart_coins.append(upcased_ticker)

View File

@@ -16,9 +16,6 @@ from basicswap.util import (
toBool,
InactiveCoin,
)
from basicswap.basicswap_util import (
get_api_key_setting,
)
from basicswap.chainparams import (
Coins,
)
@@ -171,13 +168,23 @@ def page_settings(self, url_split, post_string):
"debug_ui": swap_client.debug_ui,
"expire_db_records": swap_client._expire_db_records,
}
if "chart_api_key_enc" in swap_client.settings:
chart_api_key = html.escape(
bytes.fromhex(swap_client.settings.get("chart_api_key_enc", "")).decode(
"utf-8"
)
)
else:
chart_api_key = swap_client.settings.get("chart_api_key", "")
chart_api_key = get_api_key_setting(
swap_client.settings, "chart_api_key", escape=True
)
coingecko_api_key = get_api_key_setting(
swap_client.settings, "coingecko_api_key", escape=True
)
if "coingecko_api_key_enc" in swap_client.settings:
coingecko_api_key = html.escape(
bytes.fromhex(swap_client.settings.get("coingecko_api_key_enc", "")).decode(
"utf-8"
)
)
else:
coingecko_api_key = swap_client.settings.get("coingecko_api_key", "")
chart_settings = {
"show_chart": swap_client.settings.get("show_chart", True),

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-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.
@@ -15,7 +15,6 @@ from basicswap.util import (
from basicswap.chainparams import (
Coins,
chainparams,
getCoinIdFromTicker,
)
from basicswap.basicswap_util import (
ActionTypes,
@@ -35,6 +34,30 @@ from basicswap.basicswap_util import (
from basicswap.protocols.xmr_swap_1 import getChainBSplitKey, getChainBRemoteSplitKey
PAGE_LIMIT = 1000
invalid_coins_from = []
known_chart_coins = [
"BTC",
"PART",
"XMR",
"LTC",
"FIRO",
"DASH",
"PIVX",
"DOGE",
"ETH",
"DCR",
"ZANO",
"WOW",
"BCH",
]
def tickerToCoinId(ticker):
search_str = ticker.upper()
for c in Coins:
if c.name == search_str:
return c.value
raise ValueError("Unknown coin")
def getCoinType(coin_type_ind):
@@ -42,7 +65,7 @@ def getCoinType(coin_type_ind):
try:
return int(coin_type_ind)
except Exception:
return getCoinIdFromTicker(coin_type_ind)
return tickerToCoinId(coin_type_ind)
def validateAmountString(amount, ci):
@@ -122,7 +145,7 @@ def get_data_with_pagination(data, filters):
offset = filters.get("offset", 0)
limit = filters.get("limit", PAGE_LIMIT)
return data[offset : offset + limit]
return data[offset:offset + limit]
def getTxIdHex(bid, tx_type, suffix):
@@ -644,12 +667,12 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
continue
if v["connection_type"] == "rpc":
coins.append((int(k), getCoinName(k)))
if split_from:
if split_from and k not in invalid_coins_from:
coins_from.append(coins[-1])
if with_variants and k == Coins.PART:
for v in (Coins.PART_ANON, Coins.PART_BLIND):
coins.append((int(v), getCoinName(v)))
if split_from:
if split_from and v not in invalid_coins_from:
coins_from.append(coins[-1])
if with_variants and k == Coins.LTC:
for v in (Coins.LTC_MWEB,):

View File

@@ -38,7 +38,8 @@ def make_reporthook(read_start: int, logger):
logger.info(f"Attempting to resume from byte {read_start}")
def reporthook(blocknum, blocksize, totalsize):
nonlocal read, last_percent_str, time_last, read_last, display_last, abo
nonlocal read, last_percent_str, time_last, read_last, display_last, read_start
nonlocal average_buffer, abo, logger
read += blocksize
# totalsize excludes read_start

View File

@@ -29,51 +29,3 @@ def rfc2440_hash_password(password, salt=None):
break
rv = "16:" + salt.hex() + "60" + h.hexdigest()
return rv.upper()
def verify_rfc2440_password(stored_hash, provided_password):
"""
Verifies a password against a hash generated by rfc2440_hash_password.
Args:
stored_hash (str): The hash string stored (e.g., "16:<salt>60<hash>").
provided_password (str): The password attempt to verify.
Returns:
bool: True if the password matches the hash, False otherwise.
"""
try:
parts = stored_hash.upper().split(":")
if len(parts) != 2 or parts[0] != "16":
return False
salt_hex_plus_hash_hex = parts[1]
separator_index = salt_hex_plus_hash_hex.find("60")
if separator_index != 16:
return False
salt_hex = salt_hex_plus_hash_hex[:separator_index]
expected_hash_hex = salt_hex_plus_hash_hex[separator_index + 2 :]
salt = bytes.fromhex(salt_hex)
except (ValueError, IndexError):
return False
EXPBIAS = 6
c = 96
count = (16 + (c & 15)) << ((c >> 4) + EXPBIAS)
hashbytes = salt + provided_password.encode("utf-8")
len_hashbytes = len(hashbytes)
h = hashlib.sha1()
while count > 0:
if count >= len_hashbytes:
h.update(hashbytes)
count -= len_hashbytes
continue
h.update(hashbytes[:count])
break
calculated_hash_hex = h.hexdigest().upper()
return secrets.compare_digest(calculated_hash_hex, expected_hash_hex)

View File

@@ -1,229 +0,0 @@
#!/usr/bin/env python3
# -*- 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 hashlib
import hmac
import secrets
import time
from typing import Union, Dict
from coincurve.keys import (
PublicKey,
PrivateKey,
)
from Crypto.Cipher import AES
from basicswap.util.crypto import hash160, sha256, ripemd160
from basicswap.util.ecc import getSecretInt
from basicswap.contrib.test_framework.messages import (
uint256_from_compact,
uint256_from_str,
)
AES_BLOCK_SIZE = 16
def aes_pad(s: bytes):
c = AES_BLOCK_SIZE - len(s) % AES_BLOCK_SIZE
return s + (bytes((c,)) * c)
def aes_unpad(s: bytes):
return s[: -(s[len(s) - 1])]
def aes_encrypt(raw: bytes, pass_data: bytes, iv: bytes):
assert len(pass_data) == 32
assert len(iv) == 16
raw = aes_pad(raw)
cipher = AES.new(pass_data, AES.MODE_CBC, iv)
return cipher.encrypt(raw)
def aes_decrypt(enc, pass_data: bytes, iv: bytes):
assert len(pass_data) == 32
assert len(iv) == 16
cipher = AES.new(pass_data, AES.MODE_CBC, iv)
return aes_unpad(cipher.decrypt(enc))
SMSG_MIN_TTL = 60 * 60
SMSG_BUCKET_LEN = 60 * 60
SMSG_HDR_LEN = (
108 # Length of unencrypted header, 4 + 4 + 2 + 1 + 8 + 4 + 16 + 33 + 32 + 4
)
SMSG_PL_HDR_LEN = 1 + 20 + 65 + 4 # Length of encrypted header in payload
def smsgGetTimestamp(smsg_message: bytes) -> int:
assert len(smsg_message) > SMSG_HDR_LEN
return int.from_bytes(smsg_message[11 : 11 + 8], byteorder="little")
def smsgGetPOWHash(smsg_message: bytes) -> bytes:
assert len(smsg_message) > SMSG_HDR_LEN
ofs: int = 4
nonce: bytes = smsg_message[ofs : ofs + 4]
iv: bytes = nonce * 8
m = hmac.new(iv, digestmod="SHA256")
m.update(smsg_message[4:])
return m.digest()
def smsgGetID(smsg_message: bytes) -> bytes:
assert len(smsg_message) > SMSG_HDR_LEN
smsg_timestamp = int.from_bytes(smsg_message[11 : 11 + 8], byteorder="little")
return smsg_timestamp.to_bytes(8, byteorder="big") + ripemd160(smsg_message[8:])
def smsgEncrypt(privkey_from: bytes, pubkey_to: bytes, payload: bytes) -> bytes:
# assert len(payload) < 128 # Requires lz4 if payload > 128 bytes
# TODO: Add lz4 to match core smsg
smsg_timestamp = int(time.time())
r = getSecretInt().to_bytes(32, byteorder="big")
R = PublicKey.from_secret(r).format()
p = PrivateKey(r).ecdh(pubkey_to)
H = hashlib.sha512(p).digest()
key_e: bytes = H[:32]
key_m: bytes = H[32:]
smsg_iv: bytes = secrets.token_bytes(16)
payload_hash: bytes = sha256(sha256(payload))
signature: bytes = PrivateKey(privkey_from).sign_recoverable(
payload_hash, hasher=None
)
# Convert format to BTC, add 4 to mark as compressed key
recid = signature[64]
signature = bytes((27 + recid + 4,)) + signature[:64]
pubkey_from: bytes = PublicKey.from_secret(privkey_from).format()
pkh_from: bytes = hash160(pubkey_from)
len_payload = len(payload)
address_version = 0
plaintext_data: bytes = (
bytes((address_version,))
+ pkh_from
+ signature
+ len_payload.to_bytes(4, byteorder="little")
+ payload
)
ciphertext: bytes = aes_encrypt(plaintext_data, key_e, smsg_iv)
m = hmac.new(key_m, digestmod="SHA256")
m.update(smsg_timestamp.to_bytes(8, byteorder="little"))
m.update(smsg_iv)
m.update(ciphertext)
mac: bytes = m.digest()
smsg_hash = bytes((0,)) * 4
smsg_nonce = bytes((0,)) * 4
smsg_version = bytes((2, 1))
smsg_flags = bytes((0,))
smsg_ttl = SMSG_MIN_TTL
assert len(R) == 33
assert len(mac) == 32
smsg_message: bytes = (
smsg_hash
+ smsg_nonce
+ smsg_version
+ smsg_flags
+ smsg_timestamp.to_bytes(8, byteorder="little")
+ smsg_ttl.to_bytes(4, byteorder="little")
+ smsg_iv
+ R
+ mac
+ len(ciphertext).to_bytes(4, byteorder="little")
+ ciphertext
)
target: int = uint256_from_compact(0x1EFFFFFF)
for i in range(1000000):
pow_hash = smsgGetPOWHash(smsg_message)
if uint256_from_str(pow_hash) > target:
smsg_nonce = (int.from_bytes(smsg_nonce, byteorder="little") + 1).to_bytes(
4, byteorder="little"
)
smsg_message = pow_hash[:4] + smsg_nonce + smsg_message[8:]
continue
smsg_message = pow_hash[:4] + smsg_message[4:]
return smsg_message
raise ValueError("Failed to set POW hash.")
def smsgDecrypt(
privkey_to: bytes, encrypted_message: bytes, output_dict: bool = False
) -> Union[bytes, Dict]:
# Without lz4
assert len(encrypted_message) > SMSG_HDR_LEN
smsg_timestamp = int.from_bytes(encrypted_message[11 : 11 + 8], byteorder="little")
ofs: int = 23
smsg_iv = encrypted_message[ofs : ofs + 16]
ofs += 16
R = encrypted_message[ofs : ofs + 33]
ofs += 33
mac = encrypted_message[ofs : ofs + 32]
ofs += 32
ciphertextlen = int.from_bytes(encrypted_message[ofs : ofs + 4], byteorder="little")
ofs += 4
ciphertext = encrypted_message[ofs:]
assert len(ciphertext) == ciphertextlen
p = PrivateKey(privkey_to).ecdh(R)
H = hashlib.sha512(p).digest()
key_e: bytes = H[:32]
key_m: bytes = H[32:]
m = hmac.new(key_m, digestmod="SHA256")
m.update(smsg_timestamp.to_bytes(8, byteorder="little"))
m.update(smsg_iv)
m.update(ciphertext)
mac_calculated: bytes = m.digest()
assert mac == mac_calculated
plaintext = aes_decrypt(ciphertext, key_e, smsg_iv)
ofs = 1
pkh_from = plaintext[ofs : ofs + 20]
ofs += 20
signature = plaintext[ofs : ofs + 65]
ofs += 65
ofs += 4
payload = plaintext[ofs:]
payload_hash: bytes = sha256(sha256(payload))
# Convert format from BTC
recid = (signature[0] - 27) & 3
signature = signature[1:] + bytes((recid,))
pubkey_signer = PublicKey.from_signature_and_message(
signature, payload_hash, hasher=None
).format()
pkh_from_recovered: bytes = hash160(pubkey_signer)
assert pkh_from == pkh_from_recovered
if output_dict:
return {
"msgid": smsgGetID(encrypted_message).hex(),
"sent": smsg_timestamp,
"hex": payload.hex(),
"pk_from": pubkey_signer.hex(),
}
return payload

View File

@@ -66,10 +66,6 @@ Adjust `--withcoins` and `--withoutcoins` as desired, eg: `--withcoins=monero,bi
Append `--usebtcfastsync` to the below command to optionally initialise the Bitcoin datadir with a chain snapshot from btcpayserver FastSync.<br>
[FastSync README.md](https://github.com/btcpayserver/btcpayserver-docker/blob/master/contrib/FastSync/README.md)
##### FastSync
Append `--client-auth-password=<YOUR_PASSWORD>` to the below command to optionally enable client authentication to protect your web UI from unauthorized access.<br>
Setup with a local Monero daemon (recommended):
@@ -86,16 +82,6 @@ To instead use Monero public nodes and not run a local Monero daemon<br>(it can
**Mnemonics should be stored encrypted and/or air-gapped.**
And the output of `echo $CURRENT_XMR_HEIGHT` for use if you need to later restore your wallet.
##### Restore
To restore an existing install use --particl_mnemonic to input the existing mnemonic:
docker-compose run --rm swapclient basicswap-prepare --datadir=/coindata --htmlhost="0.0.0.0" --wshost="0.0.0.0" \
--withcoins=monero --xmrrestoreheight=$ORIGINAL_XMR_HEIGHT \
--particl_mnemonic="existing mnemonic here"
#### Set the timezone (optional)
Edit the `.env` file in the docker directory, set TZ to your local timezone.
@@ -204,7 +190,7 @@ Prepare the datadir:
OR using a remote/public XMR daemon (not recommended):
XMR_RPC_HOST="node.xmr.to" XMR_RPC_PORT=18081 basicswap-prepare --datadir=$SWAP_DATADIR --withcoins=monero --xmrrestoreheight=$CURRENT_XMR_HEIGHT
Append `--client-auth-password=<YOUR_PASSWORD>` to the above command to optionally enable client authentication to protect your web UI from unauthorized access.<br>
Record the mnemonic from the output of the above command.
Start Basicswap:
@@ -223,7 +209,7 @@ Add a coin (Stop basicswap first):
Start after installed:
export SWAP_DATADIR=$HOME/coinswaps
. $SWAP_DATADIR/venv/bin/activate && python -V
basicswap-run --datadir=$SWAP_DATADIR

View File

@@ -27,11 +27,11 @@ If the dependencies have changed the container must be built with `--no-cache`:
After updating the code and rebuilding the container run:
basicswap/docker]$ docker-compose run --rm swapclient \
basicswap-prepare --datadir=/coindata --upgradecores
basicswap-prepare --datadir=/coindata --preparebinonly --withcoins=monero,bitcoin
Specify all required coins after `--withcoins=`, separated by commas.
If updating from versions below 0.21, you may need to add `wallet=wallet.dat` to the core config files.
@@ -46,4 +46,4 @@ If updating from versions below 0.21, you may need to add `wallet=wallet.dat` to
#### Update core versions
basicswap-prepare --datadir=$SWAP_DATADIR --upgradecores
basicswap-prepare --datadir=$SWAP_DATADIR -preparebinonly --withcoins=monero,bitcoin

View File

@@ -21,7 +21,6 @@
#:use-module (gnu packages python-check)
#:use-module (gnu packages python-crypto)
#:use-module (gnu packages python-science)
#:use-module (gnu packages python-web)
#:use-module (gnu packages python-xyz)
#:use-module (gnu packages libffi)
#:use-module (gnu packages license))
@@ -115,15 +114,15 @@
(define-public basicswap
(package
(name "basicswap")
(version "0.14.4")
(version "0.14.3")
(source (origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/basicswap/basicswap")
(commit "3c18a3ed26222bac22a9c15795bd8c6fae0b01ba")))
(commit "3b60472c04a58f26e33665f0eb0e88a558050c74")))
(sha256
(base32
"02mwyklcw9320crcm8laiw4ba24xrazbg48whvdxnbmarcbipkd3"))
"0xrli8mzigm0ryn28y28xvy4gc0358ck2036ncx5f1sj5s8dwfkh"))
(file-name (git-file-name name version))))
(build-system pyproject-build-system)
@@ -146,8 +145,7 @@
python-pyzmq
python-gnupg
python-jinja2
python-pysocks
python-websocket-client))
python-pysocks))
(native-inputs
(list
python-hatchling

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