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: env:
BIN_DIR: /tmp/cached_bin BIN_DIR: /tmp/cached_bin
TEST_RELOAD_PATH: /tmp/test_basicswap TEST_RELOAD_PATH: /tmp/test_basicswap
BSX_SELENIUM_DRIVER: firefox-ci
XMR_RPC_USER: xmr_user
XMR_RPC_PWD: xmr_pwd
jobs: jobs:
ci: ci:
@@ -27,18 +24,8 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install dependencies
run: | 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 python -m pip install --upgrade pip
pip install -e .[dev] pip install flake8 codespell pytest
pip install -r requirements.txt --require-hashes pip install -r requirements.txt --require-hashes
- name: Install - name: Install
run: | run: |
@@ -46,16 +33,13 @@ jobs:
# Print the core versions to a file for caching # Print the core versions to a file for caching
basicswap-prepare --version --withcoins=bitcoin | tail -n +2 > core_versions.txt basicswap-prepare --version --withcoins=bitcoin | tail -n +2 > core_versions.txt
cat core_versions.txt cat core_versions.txt
- name: Run flake8 - name: Running flake8
run: | run: |
flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py flake8 --ignore=E203,E501,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,.eggs,.tox,bin/install_certifi.py
- name: Run codespell - name: Running codespell
run: | 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 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 - name: Running test_other
run: |
black --check --diff --exclude="contrib" .
- name: Run test_other
run: | run: |
pytest tests/basicswap/test_other.py pytest tests/basicswap/test_other.py
- name: Cache coin cores - name: Cache coin cores
@@ -71,41 +55,17 @@ jobs:
name: Running basicswap-prepare name: Running basicswap-prepare
run: | run: |
basicswap-prepare --bindir="$BIN_DIR" --preparebinonly --withcoins=particl,bitcoin,monero basicswap-prepare --bindir="$BIN_DIR" --preparebinonly --withcoins=particl,bitcoin,monero
- name: Run test_prepare - name: Running test_xmr
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
run: | run: |
export PYTHONPATH=$(pwd) export PYTHONPATH=$(pwd)
export PARTICL_BINDIR="$BIN_DIR/particl" export PARTICL_BINDIR="$BIN_DIR/particl"
export BITCOIN_BINDIR="$BIN_DIR/bitcoin" export BITCOIN_BINDIR="$BIN_DIR/bitcoin"
export XMR_BINDIR="$BIN_DIR/monero" 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" 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: | run: |
export PYTHONPATH=$(pwd) export PYTHONPATH=$(pwd)
export TEST_PATH=${TEST_RELOAD_PATH} export TEST_PATH=${TEST_RELOAD_PATH}
mkdir -p ${TEST_PATH}/bin mkdir -p ${TEST_PATH}/bin
cp -r $BIN_DIR/* ${TEST_PATH}/bin/ cp -r $BIN_DIR/* ${TEST_PATH}/bin/
pytest tests/basicswap/extended/test_encrypted_xmr_reload.py pytest tests/basicswap/extended/test_encrypted_xmr_reload.py
- name: Run selenium tests
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

2
.gitignore vendored
View File

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

View File

@@ -1,3 +1,3 @@
name = "basicswap" 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 # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import logging
import os import os
import random
import shlex
import socket
import socks
import subprocess
import sys
import threading
import time import time
import traceback import shlex
import socks
import random
import socket
import urllib import urllib
import logging
import threading
import traceback
import subprocess
from sockshandler import SocksiPyHandler from sockshandler import SocksiPyHandler
@@ -43,9 +42,9 @@ def getaddrinfo_tor(*args):
class BaseApp(DBMethods): class BaseApp(DBMethods):
def __init__(self, data_dir, settings, chain, log_name="BasicSwap"): def __init__(self, fp, data_dir, settings, chain, log_name="BasicSwap"):
self.fp = None
self.log_name = log_name self.log_name = log_name
self.fp = fp
self.fail_code = 0 self.fail_code = 0
self.mock_time_offset = 0 self.mock_time_offset = 0
@@ -72,33 +71,24 @@ class BaseApp(DBMethods):
self.default_socket_timeout = socket.getdefaulttimeout() self.default_socket_timeout = socket.getdefaulttimeout()
self.default_socket_getaddrinfo = socket.getaddrinfo self.default_socket_getaddrinfo = socket.getaddrinfo
def __del__(self):
if self.fp:
self.fp.close()
def stopRunning(self, with_code=0): def stopRunning(self, with_code=0):
self.fail_code = with_code self.fail_code = with_code
with self.mxDB: with self.mxDB:
self.chainstate_delay_event.set() self.chainstate_delay_event.set()
self.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): def prepareLogging(self):
logging.setLoggerClass(BSXLogger) logging.setLoggerClass(BSXLogger)
self.log = logging.getLogger(self.log_name) self.log = logging.getLogger(self.log_name)
self.log.propagate = False self.log.propagate = False
self.openLogFile()
# Remove any existing handlers # Remove any existing handlers
self.log.handlers = [] self.log.handlers = []
formatter = logging.Formatter( formatter = logging.Formatter(
"%(asctime)s %(levelname)s : %(message)s", "%Y-%m-%d %H:%M:%S" "%(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": if self.log_name != "BasicSwap":
stream_stdout.setFormatter( stream_stdout.setFormatter(
logging.Formatter( logging.Formatter(
@@ -108,7 +98,6 @@ class BaseApp(DBMethods):
) )
else: else:
stream_stdout.setFormatter(formatter) stream_stdout.setFormatter(formatter)
self.log_formatter = formatter
stream_fp = logging.StreamHandler(self.fp) stream_fp = logging.StreamHandler(self.fp)
stream_fp.setFormatter(formatter) stream_fp.setFormatter(formatter)

View File

@@ -11,7 +11,6 @@ import concurrent.futures
import copy import copy
import datetime as dt import datetime as dt
import json import json
import logging
import os import os
import random import random
import secrets import secrets
@@ -33,7 +32,7 @@ from .interface.part import PARTInterface, PARTInterfaceAnon, PARTInterfaceBlind
from . import __version__ from . import __version__
from .rpc import escape_rpcauth from .rpc import escape_rpcauth
from .rpc_xmr import make_xmr_rpc2_func from .rpc_xmr import make_xmr_rpc2_func
from .ui.util import getCoinName from .ui.util import getCoinName, known_chart_coins
from .util import ( from .util import (
AutomationConstraint, AutomationConstraint,
AutomationConstraintTemporary, AutomationConstraintTemporary,
@@ -66,12 +65,6 @@ from basicswap.util.network import is_private_ip_address
from .chainparams import ( from .chainparams import (
Coins, Coins,
chainparams, chainparams,
Fiat,
ticker_map,
)
from .explorers import (
default_chart_api_key,
default_coingecko_api_key,
) )
from .script import ( from .script import (
OpCodes, OpCodes,
@@ -122,16 +115,8 @@ from .explorers import (
ExplorerBitAps, ExplorerBitAps,
ExplorerChainz, ExplorerChainz,
) )
from .network.simplex import (
initialiseSimplexNetwork,
sendSimplexMsg,
readSimplexMsgs,
)
from .network.util import (
getMsgPubkey,
)
import basicswap.config as cfg 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.atomic_swap_1 as atomic_swap_1
import basicswap.protocols.xmr_swap_1 as xmr_swap_1 import basicswap.protocols.xmr_swap_1 as xmr_swap_1
from .basicswap_util import ( from .basicswap_util import (
@@ -141,8 +126,6 @@ from .basicswap_util import (
BidStates, BidStates,
DebugTypes, DebugTypes,
EventLogTypes, EventLogTypes,
fiatTicker,
get_api_key_setting,
KeyTypes, KeyTypes,
MessageTypes, MessageTypes,
NotificationTypes as NT, NotificationTypes as NT,
@@ -290,13 +273,14 @@ class BasicSwap(BaseApp):
def __init__( def __init__(
self, self,
fp,
data_dir, data_dir,
settings, settings,
chain, chain,
log_name="BasicSwap", log_name="BasicSwap",
transient_instance=False, transient_instance=False,
): ):
super().__init__(data_dir, settings, chain, log_name) super().__init__(fp, data_dir, settings, chain, log_name)
v = __version__.split(".") v = __version__.split(".")
self._version = struct.pack(">HHH", int(v[0]), int(v[1]), int(v[2])) 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( self._expire_db_records_after = self.get_int_setting(
"expire_db_records_after", 7 * 86400, 0, 31 * 86400 "expire_db_records_after", 7 * 86400, 0, 31 * 86400
) # Seconds ) # 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._notifications_cache = {}
self._is_encrypted = None self._is_encrypted = None
self._is_locked = None self._is_locked = None
@@ -399,7 +376,7 @@ class BasicSwap(BaseApp):
Coins.PART_BLIND, Coins.PART_BLIND,
Coins.BCH, Coins.BCH,
) )
self.coins_without_segwit = (Coins.PIVX, Coins.DASH) self.coins_without_segwit = (Coins.PIVX, Coins.DASH, Coins.NMC)
# TODO: Adjust ranges # TODO: Adjust ranges
self.min_delay_event = self.get_int_setting("min_delay_event", 10, 0, 20 * 60) 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.swaps_in_progress = dict()
self.dleag_split_size_init = 16000
self.dleag_split_size = 17000
self.SMSG_SECONDS_IN_HOUR = ( self.SMSG_SECONDS_IN_HOUR = (
60 * 60 60 * 60
) # Note: Set smsgsregtestadjust=0 for regtest ) # Note: Set smsgsregtestadjust=0 for regtest
@@ -537,8 +511,6 @@ class BasicSwap(BaseApp):
self._network = None self._network = None
for t in self.threads: for t in self.threads:
if hasattr(t, "stop") and callable(t.stop):
t.stop()
t.join() t.join()
if sys.version_info[1] >= 9: if sys.version_info[1] >= 9:
@@ -720,7 +692,7 @@ class BasicSwap(BaseApp):
def getXMRTrustedDaemon(self, coin, node_host: str) -> bool: def getXMRTrustedDaemon(self, coin, node_host: str) -> bool:
coin = Coins(coin) # Errors for invalid coin value coin = Coins(coin) # Errors for invalid coin value
chain_client_settings = self.getChainClientSettings(coin) 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( self.log.debug(
f"'trusted_daemon' setting for {getCoinName(coin)}: {trusted_daemon_setting}." f"'trusted_daemon' setting for {getCoinName(coin)}: {trusted_daemon_setting}."
) )
@@ -1067,9 +1039,7 @@ class BasicSwap(BaseApp):
elif c in (Coins.XMR, Coins.WOW): elif c in (Coins.XMR, Coins.WOW):
try: try:
ci.ensureWalletExists() ci.ensureWalletExists()
except Exception as e: except Exception as e: # noqa: F841
if "invalid signature" in str(e): # wallet is corrupt
raise
self.log.warning( self.log.warning(
f"Can't open {ci.coin_name()} wallet, could be locked." 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}" 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") ro = self.callrpc("smsglocalkeys")
found = False found = False
for k in ro["smsg_keys"]: for k in ro["smsg_keys"]:
@@ -1358,9 +1317,7 @@ class BasicSwap(BaseApp):
legacy_root_hash = ci.getSeedHash(root_key, 20) legacy_root_hash = ci.getSeedHash(root_key, 20)
self.setStringKV(key_str, legacy_root_hash.hex(), cursor) self.setStringKV(key_str, legacy_root_hash.hex(), cursor)
def initialiseWallet( def initialiseWallet(self, interface_type, raise_errors: bool = False) -> None:
self, interface_type, raise_errors: bool = False, restore_time: int = -1
) -> None:
if interface_type == Coins.PART: if interface_type == Coins.PART:
return return
ci = self.ci(interface_type) ci = self.ci(interface_type)
@@ -1379,7 +1336,7 @@ class BasicSwap(BaseApp):
root_key = self.getWalletKey(interface_type, 1) root_key = self.getWalletKey(interface_type, 1)
try: try:
ci.initialiseWallet(root_key, restore_time) ci.initialiseWallet(root_key)
except Exception as e: except Exception as e:
# < 0.21: sethdseed cannot set a new HD seed while still in Initial Block Download. # < 0.21: sethdseed cannot set a new HD seed while still in Initial Block Download.
self.log.error(f"initialiseWallet failed: {e}") 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 bid_valid = (bid.expire_at - now) + 10 * 60 # Add 10 minute buffer
return max(smsg_min_valid, min(smsg_max_valid, bid_valid)) 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( def sendSmsg(
self, addr_from: str, addr_to: str, payload_hex: bytes, msg_valid: int self, addr_from: str, addr_to: str, payload_hex: bytes, msg_valid: int
) -> bytes: ) -> bytes:
@@ -2220,24 +2150,6 @@ class BasicSwap(BaseApp):
msg_buf.fee_rate_to msg_buf.fee_rate_to
) # Unused: TODO - Set priority? ) # 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 a prefunded txn is not used, check that the wallet balance can cover the tx fee.
if "prefunded_itx" not in extra_options: 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 # 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() offer_bytes = msg_buf.to_bytes()
payload_hex = str.format("{:02x}", MessageTypes.OFFER) + offer_bytes.hex() payload_hex = str.format("{:02x}", MessageTypes.OFFER) + offer_bytes.hex()
msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, valid_for_seconds) msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, valid_for_seconds)
offer_id = self.sendMessage( offer_id = self.sendSmsg(offer_addr, offer_addr_to, payload_hex, msg_valid)
offer_addr, offer_addr_to, payload_hex, msg_valid, cursor
)
security_token = extra_options.get("security_token", None) security_token = extra_options.get("security_token", None)
if security_token is not None and len(security_token) != 20: if security_token is not None and len(security_token) != 20:
@@ -2292,7 +2202,6 @@ class BasicSwap(BaseApp):
security_token=security_token, security_token=security_token,
from_feerate=msg_buf.fee_rate_from, from_feerate=msg_buf.fee_rate_from,
to_feerate=msg_buf.fee_rate_to, to_feerate=msg_buf.fee_rate_to,
auto_accept_type=msg_buf.auto_accept_type,
) )
offer.setState(OfferStates.OFFER_SENT) 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_valid: int = max(self.SMSG_SECONDS_IN_HOUR, offer.time_valid)
msg_id = self.sendMessage( msg_id = self.sendSmsg(
offer.addr_from, self.network_addr, payload_hex, msg_valid, cursor offer.addr_from, self.network_addr, payload_hex, msg_valid
) )
self.log.debug( self.log.debug(
f"Revoked offer {self.log.id(offer_id)} in msg {self.log.id(msg_id)}" 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) self.log.warning(msg)
return False return False
seed_key: str = "main_wallet_seedid_" + ci.coin_name().lower() expect_seedid = self.getStringKV("main_wallet_seedid_" + ci.coin_name().lower())
expect_seedid: str = self.getStringKV(seed_key)
if expect_seedid is None: if expect_seedid is None:
self.log.warning( self.log.warning(
f"Can't find expected wallet seed id for coin {ci.coin_name()}." 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) root_key = self.getWalletKey(c, 1)
self.storeSeedIDForCoin(root_key, c) self.storeSeedIDForCoin(root_key, c)
expect_seedid: str = self.getStringKV(seed_key)
else: else:
self.log.warning("Node is locked.") self.log.warning("Node is locked.")
return False return False
@@ -3209,9 +3116,7 @@ class BasicSwap(BaseApp):
bid_addr = self.prepareSMSGAddress(addr_send_from, AddressTypes.BID, cursor) bid_addr = self.prepareSMSGAddress(addr_send_from, AddressTypes.BID, cursor)
msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, valid_for_seconds) msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, valid_for_seconds)
bid_id = self.sendMessage( bid_id = self.sendSmsg(bid_addr, offer.addr_from, payload_hex, msg_valid)
bid_addr, offer.addr_from, payload_hex, msg_valid, cursor
)
bid = Bid( bid = Bid(
protocol_version=msg_buf.protocol_version, protocol_version=msg_buf.protocol_version,
@@ -3547,8 +3452,8 @@ class BasicSwap(BaseApp):
) )
msg_valid: int = self.getAcceptBidMsgValidTime(bid) msg_valid: int = self.getAcceptBidMsgValidTime(bid)
accept_msg_id = self.sendMessage( accept_msg_id = self.sendSmsg(
offer.addr_from, bid.bid_addr, payload_hex, msg_valid, cursor offer.addr_from, bid.bid_addr, payload_hex, msg_valid
) )
self.addMessageLink( self.addMessageLink(
@@ -3578,29 +3483,20 @@ class BasicSwap(BaseApp):
dleag: bytes, dleag: bytes,
msg_valid: int, msg_valid: int,
bid_msg_ids, bid_msg_ids,
cursor,
) -> None: ) -> 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 msg_buf3 = XmrSplitMessage(
msg_id=bid_id, msg_type=msg_type, sequence=2, dleag=dleag[32000:]
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() msg_bytes = msg_buf3.to_bytes()
payload_hex = ( payload_hex = str.format("{:02x}", MessageTypes.XMR_BID_SPLIT) + msg_bytes.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)
)
bid_msg_ids[num_sent] = self.sendMessage(
addr_from, addr_to, payload_hex, msg_valid, cursor
)
num_sent += 1
sent_bytes += size_to_send
def postXmrBid( def postXmrBid(
self, offer_id: bytes, amount: int, addr_send_from: str = None, extra_options={} 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) msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, valid_for_seconds)
xmr_swap.bid_id = self.sendMessage( xmr_swap.bid_id = self.sendSmsg(
bid_addr, offer.addr_from, payload_hex, msg_valid, cursor bid_addr, offer.addr_from, payload_hex, msg_valid
) )
bid = Bid( bid = Bid(
@@ -3759,7 +3655,7 @@ class BasicSwap(BaseApp):
if ci_to.curve_type() == Curves.ed25519: if ci_to.curve_type() == Curves.ed25519:
xmr_swap.kbsf_dleag = ci_to.proveDLEAG(kbsf) xmr_swap.kbsf_dleag = ci_to.proveDLEAG(kbsf)
xmr_swap.pkasf = xmr_swap.kbsf_dleag[0:33] 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: elif ci_to.curve_type() == Curves.secp256k1:
for i in range(10): for i in range(10):
xmr_swap.kbsf_dleag = ci_to.signRecoverable( xmr_swap.kbsf_dleag = ci_to.signRecoverable(
@@ -3789,8 +3685,8 @@ class BasicSwap(BaseApp):
bid_addr = self.prepareSMSGAddress(addr_send_from, AddressTypes.BID, cursor) bid_addr = self.prepareSMSGAddress(addr_send_from, AddressTypes.BID, cursor)
msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, valid_for_seconds) msg_valid: int = max(self.SMSG_SECONDS_IN_HOUR, valid_for_seconds)
xmr_swap.bid_id = self.sendMessage( xmr_swap.bid_id = self.sendSmsg(
bid_addr, offer.addr_from, payload_hex, msg_valid, cursor bid_addr, offer.addr_from, payload_hex, msg_valid
) )
bid_msg_ids = {} bid_msg_ids = {}
@@ -3803,7 +3699,6 @@ class BasicSwap(BaseApp):
xmr_swap.kbsf_dleag, xmr_swap.kbsf_dleag,
msg_valid, msg_valid,
bid_msg_ids, bid_msg_ids,
cursor,
) )
bid = Bid( bid = Bid(
@@ -4082,7 +3977,7 @@ class BasicSwap(BaseApp):
if ci_to.curve_type() == Curves.ed25519: if ci_to.curve_type() == Curves.ed25519:
xmr_swap.kbsl_dleag = ci_to.proveDLEAG(kbsl) 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: elif ci_to.curve_type() == Curves.secp256k1:
for i in range(10): for i in range(10):
xmr_swap.kbsl_dleag = ci_to.signRecoverable( xmr_swap.kbsl_dleag = ci_to.signRecoverable(
@@ -4117,9 +4012,7 @@ class BasicSwap(BaseApp):
msg_valid: int = self.getAcceptBidMsgValidTime(bid) msg_valid: int = self.getAcceptBidMsgValidTime(bid)
bid_msg_ids = {} bid_msg_ids = {}
bid_msg_ids[0] = self.sendMessage( bid_msg_ids[0] = self.sendSmsg(addr_from, addr_to, payload_hex, msg_valid)
addr_from, addr_to, payload_hex, msg_valid, use_cursor
)
if ci_to.curve_type() == Curves.ed25519: if ci_to.curve_type() == Curves.ed25519:
self.sendXmrSplitMessages( self.sendXmrSplitMessages(
@@ -4130,7 +4023,6 @@ class BasicSwap(BaseApp):
xmr_swap.kbsl_dleag, xmr_swap.kbsl_dleag,
msg_valid, msg_valid,
bid_msg_ids, bid_msg_ids,
use_cursor,
) )
bid.setState(BidStates.BID_ACCEPTED) # ADS bid.setState(BidStates.BID_ACCEPTED) # ADS
@@ -4252,8 +4144,8 @@ class BasicSwap(BaseApp):
msg_buf.kbvf = kbvf msg_buf.kbvf = kbvf
msg_buf.kbsf_dleag = ( msg_buf.kbsf_dleag = (
xmr_swap.kbsf_dleag xmr_swap.kbsf_dleag
if len(xmr_swap.kbsf_dleag) < self.dleag_split_size_init if len(xmr_swap.kbsf_dleag) < 16000
else xmr_swap.kbsf_dleag[: self.dleag_split_size_init] else xmr_swap.kbsf_dleag[:16000]
) )
bid_bytes = msg_buf.to_bytes() bid_bytes = msg_buf.to_bytes()
@@ -4265,9 +4157,7 @@ class BasicSwap(BaseApp):
addr_to: str = bid.bid_addr addr_to: str = bid.bid_addr
msg_valid: int = self.getAcceptBidMsgValidTime(bid) msg_valid: int = self.getAcceptBidMsgValidTime(bid)
bid_msg_ids = {} bid_msg_ids = {}
bid_msg_ids[0] = self.sendMessage( bid_msg_ids[0] = self.sendSmsg(addr_from, addr_to, payload_hex, msg_valid)
addr_from, addr_to, payload_hex, msg_valid, use_cursor
)
if ci_to.curve_type() == Curves.ed25519: if ci_to.curve_type() == Curves.ed25519:
self.sendXmrSplitMessages( self.sendXmrSplitMessages(
@@ -4278,7 +4168,6 @@ class BasicSwap(BaseApp):
xmr_swap.kbsf_dleag, xmr_swap.kbsf_dleag,
msg_valid, msg_valid,
bid_msg_ids, bid_msg_ids,
use_cursor,
) )
bid.setState(BidStates.BID_REQUEST_ACCEPTED) bid.setState(BidStates.BID_REQUEST_ACCEPTED)
@@ -4764,7 +4653,7 @@ class BasicSwap(BaseApp):
+ (len(txn_script)).to_bytes(1, "big") + (len(txn_script)).to_bytes(1, "big")
+ txn_script + 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): if coin_type in (Coins.NAV, Coins.DCR):
# Only checks signature # Only checks signature
@@ -6883,61 +6772,29 @@ class BasicSwap(BaseApp):
now: int = self.getTime() now: int = self.getTime()
ttl_xmr_split_messages = 60 * 60 ttl_xmr_split_messages = 60 * 60
bid_cursor = None bid_cursor = None
dleag_proof_len: int = 48893 # coincurve.dleag.dleag_proof_len()
try: try:
cursor = self.openDB() cursor = self.openDB()
bid_cursor = self.getNewDBCursor() bid_cursor = self.getNewDBCursor()
q_bids = self.query( q_bids = self.query(
Bid, Bid, bid_cursor, {"state": int(BidStates.BID_RECEIVING)}
bid_cursor,
{
"state": (
int(BidStates.BID_RECEIVING),
int(BidStates.BID_RECEIVING_ACC),
)
},
) )
for bid in q_bids: for bid in q_bids:
q = cursor.execute( q = cursor.execute(
"SELECT LENGTH(kbsl_dleag), LENGTH(kbsf_dleag) FROM xmr_swaps WHERE bid_id = :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)},
"bid_id": bid.bid_id,
},
).fetchone() ).fetchone()
kbsl_dleag_len: int = q[0] num_segments = q[0]
kbsf_dleag_len: int = q[1] if num_segments > 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:
try: try:
if bid.state == int(BidStates.BID_RECEIVING):
self.receiveXmrBid(bid, cursor) self.receiveXmrBid(bid, cursor)
elif bid.state == int(BidStates.BID_RECEIVING_ACC):
self.receiveXmrBidAccept(bid, cursor)
else:
raise ValueError("Unexpected bid state")
except Exception as ex: except Exception as ex:
self.log.info( 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: if self.debug:
self.log.error(traceback.format_exc()) self.log.error(traceback.format_exc())
bid.setState( bid.setState(
BidStates.BID_ERROR, f"Failed {bid_type} validation: {ex}" BidStates.BID_ERROR, "Failed validation: " + str(ex)
) )
self.updateDB( self.updateDB(
bid, bid,
@@ -6950,7 +6807,7 @@ class BasicSwap(BaseApp):
continue continue
if bid.created_at + ttl_xmr_split_messages < now: if bid.created_at + ttl_xmr_split_messages < now:
self.log.debug( 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") bid.setState(BidStates.BID_ERROR, "Timed out")
self.updateDB( self.updateDB(
@@ -6960,6 +6817,53 @@ class BasicSwap(BaseApp):
"bid_id", "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 # Expire old records
cursor.execute( cursor.execute(
"DELETE FROM xmr_split_data WHERE created_at + :ttl < :now", "DELETE FROM xmr_split_data WHERE created_at + :ttl < :now",
@@ -7089,7 +6993,6 @@ class BasicSwap(BaseApp):
if self.isOfferRevoked(offer_id, msg["from"]): if self.isOfferRevoked(offer_id, msg["from"]):
raise ValueError("Offer has been revoked {}.".format(offer_id.hex())) raise ValueError("Offer has been revoked {}.".format(offer_id.hex()))
pk_from: bytes = getMsgPubkey(self, msg)
try: try:
cursor = self.openDB() cursor = self.openDB()
# Offers must be received on the public network_addr or manually created addresses # 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, rate_negotiable=offer_data.rate_negotiable,
addr_to=msg["to"], addr_to=msg["to"],
addr_from=msg["from"], addr_from=msg["from"],
pk_from=pk_from,
created_at=msg["sent"], created_at=msg["sent"],
expire_at=msg["sent"] + offer_data.time_valid, expire_at=msg["sent"] + offer_data.time_valid,
was_sent=False, was_sent=False,
bid_reversed=bid_reversed, 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) offer.setState(OfferStates.OFFER_RECEIVED)
self.add(offer, cursor) self.add(offer, cursor)
@@ -7479,7 +7376,6 @@ class BasicSwap(BaseApp):
bid = self.getBid(bid_id) bid = self.getBid(bid_id)
if bid is None: if bid is None:
pk_from: bytes = getMsgPubkey(self, msg)
bid = Bid( bid = Bid(
active_ind=1, active_ind=1,
bid_id=bid_id, bid_id=bid_id,
@@ -7494,7 +7390,6 @@ class BasicSwap(BaseApp):
created_at=msg["sent"], created_at=msg["sent"],
expire_at=msg["sent"] + bid_data.time_valid, expire_at=msg["sent"] + bid_data.time_valid,
bid_addr=msg["from"], bid_addr=msg["from"],
pk_bid_addr=pk_from,
was_received=True, was_received=True,
chain_a_height_start=ci_from.getChainHeight(), chain_a_height_start=ci_from.getChainHeight(),
chain_b_height_start=ci_to.getChainHeight(), chain_b_height_start=ci_to.getChainHeight(),
@@ -7893,13 +7788,12 @@ class BasicSwap(BaseApp):
) )
if ci_to.curve_type() == Curves.ed25519: 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_id = bytes.fromhex(msg["msgid"])
bid, xmr_swap = self.getXmrBid(bid_id) bid, xmr_swap = self.getXmrBid(bid_id)
if bid is None: if bid is None:
pk_from: bytes = getMsgPubkey(self, msg)
bid = Bid( bid = Bid(
active_ind=1, active_ind=1,
bid_id=bid_id, bid_id=bid_id,
@@ -7911,7 +7805,6 @@ class BasicSwap(BaseApp):
created_at=msg["sent"], created_at=msg["sent"],
expire_at=msg["sent"] + bid_data.time_valid, expire_at=msg["sent"] + bid_data.time_valid,
bid_addr=msg["from"], bid_addr=msg["from"],
pk_bid_addr=pk_from,
was_received=True, was_received=True,
chain_a_height_start=ci_from.getChainHeight(), chain_a_height_start=ci_from.getChainHeight(),
chain_b_height_start=ci_to.getChainHeight(), chain_b_height_start=ci_to.getChainHeight(),
@@ -8241,8 +8134,8 @@ class BasicSwap(BaseApp):
msg_valid: int = self.getActiveBidMsgValidTime() msg_valid: int = self.getActiveBidMsgValidTime()
addr_send_from: str = offer.addr_from if reverse_bid else bid.bid_addr 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 addr_send_to: str = bid.bid_addr if reverse_bid else offer.addr_from
coin_a_lock_tx_sigs_l_msg_id = self.sendMessage( coin_a_lock_tx_sigs_l_msg_id = self.sendSmsg(
addr_send_from, addr_send_to, payload_hex, msg_valid, cursor addr_send_from, addr_send_to, payload_hex, msg_valid
) )
self.addMessageLink( self.addMessageLink(
Concepts.BID, 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_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 addr_send_to: str = offer.addr_from if reverse_bid else bid.bid_addr
msg_valid: int = self.getActiveBidMsgValidTime() msg_valid: int = self.getActiveBidMsgValidTime()
coin_a_lock_release_msg_id = self.sendMessage( coin_a_lock_release_msg_id = self.sendSmsg(
addr_send_from, addr_send_to, payload_hex, msg_valid, cursor addr_send_from, addr_send_to, payload_hex, msg_valid
) )
self.addMessageLink( self.addMessageLink(
Concepts.BID, Concepts.BID,
@@ -9030,8 +8923,8 @@ class BasicSwap(BaseApp):
) )
msg_valid: int = self.getActiveBidMsgValidTime() msg_valid: int = self.getActiveBidMsgValidTime()
xmr_swap.coin_a_lock_refund_spend_tx_msg_id = self.sendMessage( xmr_swap.coin_a_lock_refund_spend_tx_msg_id = self.sendSmsg(
addr_send_from, addr_send_to, payload_hex, msg_valid, cursor addr_send_from, addr_send_to, payload_hex, msg_valid
) )
bid.setState(BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX) bid.setState(BidStates.XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX)
@@ -9413,7 +9306,6 @@ class BasicSwap(BaseApp):
bid, xmr_swap = self.getXmrBid(bid_id) bid, xmr_swap = self.getXmrBid(bid_id)
if bid is None: if bid is None:
pk_from: bytes = getMsgPubkey(self, msg)
bid = Bid( bid = Bid(
active_ind=1, active_ind=1,
bid_id=bid_id, bid_id=bid_id,
@@ -9425,7 +9317,6 @@ class BasicSwap(BaseApp):
created_at=msg["sent"], created_at=msg["sent"],
expire_at=msg["sent"] + bid_data.time_valid, expire_at=msg["sent"] + bid_data.time_valid,
bid_addr=msg["from"], bid_addr=msg["from"],
pk_bid_addr=pk_from,
was_sent=False, was_sent=False,
was_received=True, was_received=True,
chain_a_height_start=ci_from.getChainHeight(), chain_a_height_start=ci_from.getChainHeight(),
@@ -9528,7 +9419,7 @@ class BasicSwap(BaseApp):
"Invalid destination address", "Invalid destination address",
) )
if ci_to.curve_type() == Curves.ed25519: 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.dest_af = msg_data.dest_af
xmr_swap.pkaf = msg_data.pkaf xmr_swap.pkaf = msg_data.pkaf
@@ -9563,14 +9454,6 @@ class BasicSwap(BaseApp):
def processMsg(self, msg) -> None: def processMsg(self, msg) -> None:
try: 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) msg_type = int(msg["hex"][:2], 16)
if msg_type == MessageTypes.OFFER: if msg_type == MessageTypes.OFFER:
@@ -9784,10 +9667,6 @@ class BasicSwap(BaseApp):
self.processMsg(msg) self.processMsg(msg)
try: 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 # TODO: Wait for blocks / txns, would need to check multiple coins
now: int = self.getTime() now: int = self.getTime()
self.expireBidsAndOffers(now) self.expireBidsAndOffers(now)
@@ -9837,51 +9716,6 @@ class BasicSwap(BaseApp):
self.checkAcceptedBids() self.checkAcceptedBids()
self._last_checked_expired = now 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: if now - self._last_checked_actions >= self.check_actions_seconds:
self.checkQueuedActions() self.checkQueuedActions()
self._last_checked_actions = now self._last_checked_actions = now
@@ -10060,7 +9894,7 @@ class BasicSwap(BaseApp):
seen_tickers = [] seen_tickers = []
for ticker in tickers: for ticker in tickers:
upcased_ticker = ticker.strip().upper() 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}") raise ValueError(f"Unknown coin: {ticker}")
if upcased_ticker in seen_tickers: if upcased_ticker in seen_tickers:
raise ValueError(f"Duplicate coin: {ticker}") raise ValueError(f"Duplicate coin: {ticker}")
@@ -11224,175 +11058,6 @@ class BasicSwap(BaseApp):
).isWalletEncryptedLocked() ).isWalletEncryptedLocked()
return self._is_encrypted, self._is_locked 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): def lookupRates(self, coin_from, coin_to, output_array=False):
self.log.debug( self.log.debug(
"lookupRates {}, {}.".format( "lookupRates {}, {}.".format(
@@ -11405,14 +11070,25 @@ class BasicSwap(BaseApp):
ci_to = self.ci(int(coin_to)) ci_to = self.ci(int(coin_to))
name_from = ci_from.chainparams()["name"] name_from = ci_from.chainparams()["name"]
name_to = ci_to.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_from = ci_from.chainparams()["ticker"]
ticker_to = ci_to.chainparams()["ticker"] ticker_to = ci_to.chainparams()["ticker"]
headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"}
rv = {} rv = {}
if rate_sources.get("coingecko.com", True): if rate_sources.get("coingecko.com", True):
try: try:
js = self.lookupFiatRates([int(coin_from), int(coin_to)]) url = "https://api.coingecko.com/api/v3/simple/price?ids={},{}&vs_currencies=usd,btc".format(
rate = float(js[int(coin_from)]) / float(js[int(coin_to)]) 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) js["rate_inferred"] = ci_to.format_amount(rate, conv_int=True, r=1)
rv["coingecko"] = js rv["coingecko"] = js
except Exception as e: except Exception as e:
@@ -11420,10 +11096,12 @@ class BasicSwap(BaseApp):
if self.debug: if self.debug:
self.log.error(traceback.format_exc()) self.log.error(traceback.format_exc())
js[name_from] = {"usd": js[int(coin_from)]} if exchange_name_from != name_from:
js.pop(int(coin_from)) js[name_from] = js[exchange_name_from]
js[name_to] = {"usd": js[int(coin_to)]} js.pop(exchange_name_from)
js.pop(int(coin_to)) if exchange_name_to != name_to:
js[name_to] = js[exchange_name_to]
js.pop(exchange_name_to)
if output_array: if output_array:
@@ -11442,6 +11120,8 @@ class BasicSwap(BaseApp):
ticker_to, ticker_to,
format_float(float(js[name_from]["usd"])), format_float(float(js[name_from]["usd"])),
format_float(float(js[name_to]["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"])), format_float(float(js["rate_inferred"])),
) )
) )

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2021-2024 tecnovert # 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 # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -9,14 +9,12 @@
import struct import struct
import hashlib import hashlib
from enum import IntEnum, auto from enum import IntEnum, auto
from html import escape as html_escape
from .util.address import ( from .util.address import (
encodeAddress, encodeAddress,
decodeAddress, decodeAddress,
) )
from .chainparams import ( from .chainparams import (
chainparams, chainparams,
Fiat,
) )
@@ -522,7 +520,7 @@ def getLastBidState(packed_states):
return BidStates.BID_STATE_UNKNOWN return BidStates.BID_STATE_UNKNOWN
def strSwapType(swap_type) -> str: def strSwapType(swap_type):
if swap_type == SwapTypes.SELLER_FIRST: if swap_type == SwapTypes.SELLER_FIRST:
return "seller_first" return "seller_first"
if swap_type == SwapTypes.XMR_SWAP: if swap_type == SwapTypes.XMR_SWAP:
@@ -530,7 +528,7 @@ def strSwapType(swap_type) -> str:
return None return None
def strSwapDesc(swap_type) -> str: def strSwapDesc(swap_type):
if swap_type == SwapTypes.SELLER_FIRST: if swap_type == SwapTypes.SELLER_FIRST:
return "Secret Hash" return "Secret Hash"
if swap_type == SwapTypes.XMR_SWAP: if swap_type == SwapTypes.XMR_SWAP:
@@ -538,31 +536,6 @@ def strSwapDesc(swap_type) -> str:
return None 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 = [ inactive_states = [
BidStates.SWAP_COMPLETED, BidStates.SWAP_COMPLETED,
BidStates.BID_ERROR, BidStates.BID_ERROR,

View File

@@ -52,22 +52,16 @@ PARTICL_VERSION = os.getenv("PARTICL_VERSION", "23.2.7.0")
PARTICL_VERSION_TAG = os.getenv("PARTICL_VERSION_TAG", "") PARTICL_VERSION_TAG = os.getenv("PARTICL_VERSION_TAG", "")
PARTICL_LINUX_EXTRA = os.getenv("PARTICL_LINUX_EXTRA", "nousb") PARTICL_LINUX_EXTRA = os.getenv("PARTICL_LINUX_EXTRA", "nousb")
BITCOIN_VERSION = os.getenv("BITCOIN_VERSION", "28.0")
BITCOIN_VERSION_TAG = os.getenv("BITCOIN_VERSION_TAG", "")
LITECOIN_VERSION = os.getenv("LITECOIN_VERSION", "0.21.4") LITECOIN_VERSION = os.getenv("LITECOIN_VERSION", "0.21.4")
LITECOIN_VERSION_TAG = os.getenv("LITECOIN_VERSION_TAG", "") LITECOIN_VERSION_TAG = os.getenv("LITECOIN_VERSION_TAG", "")
DCR_VERSION = os.getenv("DCR_VERSION", "1.8.1") BITCOIN_VERSION = os.getenv("BITCOIN_VERSION", "28.0")
DCR_VERSION_TAG = os.getenv("DCR_VERSION_TAG", "") BITCOIN_VERSION_TAG = os.getenv("BITCOIN_VERSION_TAG", "")
NMC_VERSION = os.getenv("NMC_VERSION", "28.0") MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.3.4")
NMC_VERSION_TAG = os.getenv("NMC_VERSION_TAG", "")
MONERO_VERSION = os.getenv("MONERO_VERSION", "0.18.4.0")
MONERO_VERSION_TAG = os.getenv("MONERO_VERSION_TAG", "") MONERO_VERSION_TAG = os.getenv("MONERO_VERSION_TAG", "")
XMR_SITE_COMMIT = ( XMR_SITE_COMMIT = (
"375fe249c22af0b7cf5794179638b1842427b129" # Lock hashes.txt to monero version "3751c0d7987a9e78324a718c32c008e2ec91b339" # Lock hashes.txt to monero version
) )
WOWNERO_VERSION = os.getenv("WOWNERO_VERSION", "0.11.3.0") WOWNERO_VERSION = os.getenv("WOWNERO_VERSION", "0.11.3.0")
@@ -88,6 +82,9 @@ FIRO_VERSION_TAG = os.getenv("FIRO_VERSION_TAG", "")
NAV_VERSION = os.getenv("NAV_VERSION", "7.0.3") NAV_VERSION = os.getenv("NAV_VERSION", "7.0.3")
NAV_VERSION_TAG = os.getenv("NAV_VERSION_TAG", "") NAV_VERSION_TAG = os.getenv("NAV_VERSION_TAG", "")
DCR_VERSION = os.getenv("DCR_VERSION", "1.8.1")
DCR_VERSION_TAG = os.getenv("DCR_VERSION_TAG", "")
BITCOINCASH_VERSION = os.getenv("BITCOINCASH_VERSION", "28.0.1") BITCOINCASH_VERSION = os.getenv("BITCOINCASH_VERSION", "28.0.1")
BITCOINCASH_VERSION_TAG = os.getenv("BITCOINCASH_VERSION_TAG", "") BITCOINCASH_VERSION_TAG = os.getenv("BITCOINCASH_VERSION_TAG", "")
@@ -106,7 +103,7 @@ known_coins = {
"bitcoin": (BITCOIN_VERSION, BITCOIN_VERSION_TAG, ("laanwj",)), "bitcoin": (BITCOIN_VERSION, BITCOIN_VERSION_TAG, ("laanwj",)),
"litecoin": (LITECOIN_VERSION, LITECOIN_VERSION_TAG, ("davidburkett38",)), "litecoin": (LITECOIN_VERSION, LITECOIN_VERSION_TAG, ("davidburkett38",)),
"decred": (DCR_VERSION, DCR_VERSION_TAG, ("decred_release",)), "decred": (DCR_VERSION, DCR_VERSION_TAG, ("decred_release",)),
"namecoin": (NMC_VERSION, NMC_VERSION_TAG, ("RoseTuring",)), "namecoin": ("0.18.0", "", ("JeremyRand",)),
"monero": (MONERO_VERSION, MONERO_VERSION_TAG, ("binaryfate",)), "monero": (MONERO_VERSION, MONERO_VERSION_TAG, ("binaryfate",)),
"wownero": (WOWNERO_VERSION, WOWNERO_VERSION_TAG, ("wowario",)), "wownero": (WOWNERO_VERSION, WOWNERO_VERSION_TAG, ("wowario",)),
"pivx": (PIVX_VERSION, PIVX_VERSION_TAG, ("fuzzbawls",)), "pivx": (PIVX_VERSION, PIVX_VERSION_TAG, ("fuzzbawls",)),
@@ -119,32 +116,26 @@ known_coins = {
disabled_coins = [ disabled_coins = [
"navcoin", "navcoin",
"namecoin", # Needs update
] ]
expected_key_ids = { expected_key_ids = {
"tecnovert": ("8E517DC12EC1CC37F6423A8A13F13651C9CF0D6B",), "tecnovert": ("13F13651C9CF0D6B",),
"thrasher": ("59CAF0E96F23F53747945FD4FE3348877809386C",), "thrasher": ("FE3348877809386C",),
"laanwj": ("9DEAE0DC7063249FB05474681E4AED62986CD25D",), "laanwj": ("1E4AED62986CD25D",),
"RoseTuring": ("FD8366A807A99FA27FD9CCEA9FE3BFDDA6C53495",), "JeremyRand": ("2DBE339E29F6294C",),
"binaryfate": ("81AC591FE9C4B65C5806AFC3F0AF4D462A0BDF92",), "binaryfate": ("F0AF4D462A0BDF92",),
"wowario": ("AB3A2F725818FCFF2794841C793504B449C69220",), "wowario": ("793504B449C69220",),
"davidburkett38": ("D35621D53A1CC6A3456758D03620E9D387E55666",), "davidburkett38": ("3620E9D387E55666",),
"xanimo": ("2EAA8B1021C71AD5186CA07F6E8F17C1B1BCDCBE",), "xanimo": ("6E8F17C1B1BCDCBE",),
"patricklodder": ("DC6EF4A8BF9F1B1E4DE1EE522D3A345B98D0DC1F",), "patricklodder": ("2D3A345B98D0DC1F",),
"fuzzbawls": ("0CFBDA9F60D661BA31EB5D50C1ABA64407731FD9",), "fuzzbawls": ("C1ABA64407731FD9",),
"pasta": ( "pasta": ("52527BEDABE87984", "E2F3D7916E722D38"),
"29590362EC878A81FD3C202B52527BEDABE87984", "reuben": ("1290A1D0FA7EE109",),
"02B8E7D002167C8B451AF05FE2F3D7916E722D38", "nav_builder": ("2782262BF6E7FADB",),
), "nicolasdorier": ("6618763EF09186FE", "223FDA69DEBEA82D", "62FE85647DEDDA2E"),
"reuben": ("0186454D63E83D85EF91DE4E1290A1D0FA7EE109",), "decred_release": ("6D897EDF518A031D",),
"nav_builder": ("1BF9B51BAED51BA0B3A174EE2782262BF6E7FADB",), "Calin_Culianu": ("21810A542031C02C",),
"nicolasdorier": (
"AB4CFA9895ACA0DBE27F6B346618763EF09186FE",
"015B4C837B245509E4AC8995223FDA69DEBEA82D",
"7121BDE3555D9BE06BDDC68162FE85647DEDDA2E",
),
"decred_release": ("F516ADB7A069852C7C28A02D6D897EDF518A031D",),
"Calin_Culianu": ("D465135F97D0047E18E99DC321810A542031C02C",),
} }
USE_PLATFORM = os.getenv("USE_PLATFORM", platform.system()) USE_PLATFORM = os.getenv("USE_PLATFORM", platform.system())
@@ -182,7 +173,6 @@ BSX_UPDATE_UNMANAGED = toBool(
UI_HTML_PORT = int(os.getenv("UI_HTML_PORT", 12700)) UI_HTML_PORT = int(os.getenv("UI_HTML_PORT", 12700))
UI_WS_PORT = int(os.getenv("UI_WS_PORT", 11700)) UI_WS_PORT = int(os.getenv("UI_WS_PORT", 11700))
COINS_RPCBIND_IP = os.getenv("COINS_RPCBIND_IP", "127.0.0.1") COINS_RPCBIND_IP = os.getenv("COINS_RPCBIND_IP", "127.0.0.1")
DEFAULT_RESTORE_TIME = int(os.getenv("DEFAULT_RESTORE_TIME", 1577833261)) # 2020
PART_ZMQ_PORT = int(os.getenv("PART_ZMQ_PORT", 20792)) PART_ZMQ_PORT = int(os.getenv("PART_ZMQ_PORT", 20792))
PART_RPC_HOST = os.getenv("PART_RPC_HOST", "127.0.0.1") PART_RPC_HOST = os.getenv("PART_RPC_HOST", "127.0.0.1")
@@ -191,36 +181,6 @@ PART_ONION_PORT = int(os.getenv("PART_ONION_PORT", 51734))
PART_RPC_USER = os.getenv("PART_RPC_USER", "") PART_RPC_USER = os.getenv("PART_RPC_USER", "")
PART_RPC_PWD = os.getenv("PART_RPC_PWD", "") PART_RPC_PWD = os.getenv("PART_RPC_PWD", "")
BTC_RPC_HOST = os.getenv("BTC_RPC_HOST", "127.0.0.1")
BTC_RPC_PORT = int(os.getenv("BTC_RPC_PORT", 19996))
BTC_PORT = int(os.getenv("BTC_PORT", 8333))
BTC_ONION_PORT = int(os.getenv("BTC_ONION_PORT", 8334))
BTC_RPC_USER = os.getenv("BTC_RPC_USER", "")
BTC_RPC_PWD = os.getenv("BTC_RPC_PWD", "")
LTC_RPC_HOST = os.getenv("LTC_RPC_HOST", "127.0.0.1")
LTC_RPC_PORT = int(os.getenv("LTC_RPC_PORT", 19895))
LTC_ONION_PORT = int(os.getenv("LTC_ONION_PORT", 9333))
LTC_RPC_USER = os.getenv("LTC_RPC_USER", "")
LTC_RPC_PWD = os.getenv("LTC_RPC_PWD", "")
DCR_RPC_HOST = os.getenv("DCR_RPC_HOST", "127.0.0.1")
DCR_RPC_PORT = int(os.getenv("DCR_RPC_PORT", 9109))
DCR_WALLET_RPC_HOST = os.getenv("DCR_WALLET_RPC_HOST", "127.0.0.1")
DCR_WALLET_RPC_PORT = int(os.getenv("DCR_WALLET_RPC_PORT", 9209))
DCR_WALLET_PWD = os.getenv(
"DCR_WALLET_PWD", random.randbytes(random.randint(14, 18)).hex()
)
DCR_RPC_USER = os.getenv("DCR_RPC_USER", "user")
DCR_RPC_PWD = os.getenv("DCR_RPC_PWD", random.randbytes(random.randint(14, 18)).hex())
NMC_RPC_HOST = os.getenv("NMC_RPC_HOST", "127.0.0.1")
NMC_RPC_PORT = int(os.getenv("NMC_RPC_PORT", 19698))
NMC_PORT = int(os.getenv("NMC_PORT", 8134))
NMC_ONION_PORT = int(os.getenv("NMC_ONION_PORT", 9698))
NMC_RPC_USER = os.getenv("NMC_RPC_USER", "")
NMC_RPC_PWD = os.getenv("NMC_RPC_PWD", "")
XMR_RPC_HOST = os.getenv("XMR_RPC_HOST", "127.0.0.1") XMR_RPC_HOST = os.getenv("XMR_RPC_HOST", "127.0.0.1")
XMR_RPC_PORT = int(os.getenv("XMR_RPC_PORT", 29798)) XMR_RPC_PORT = int(os.getenv("XMR_RPC_PORT", 29798))
XMR_ZMQ_PORT = int(os.getenv("XMR_ZMQ_PORT", 30898)) XMR_ZMQ_PORT = int(os.getenv("XMR_ZMQ_PORT", 30898))
@@ -243,6 +203,32 @@ WOW_RPC_USER = os.getenv("WOW_RPC_USER", "")
WOW_RPC_PWD = os.getenv("WOW_RPC_PWD", "") WOW_RPC_PWD = os.getenv("WOW_RPC_PWD", "")
DEFAULT_WOW_RESTORE_HEIGHT = int(os.getenv("DEFAULT_WOW_RESTORE_HEIGHT", 450000)) DEFAULT_WOW_RESTORE_HEIGHT = int(os.getenv("DEFAULT_WOW_RESTORE_HEIGHT", 450000))
LTC_RPC_HOST = os.getenv("LTC_RPC_HOST", "127.0.0.1")
LTC_RPC_PORT = int(os.getenv("LTC_RPC_PORT", 19895))
LTC_ONION_PORT = int(os.getenv("LTC_ONION_PORT", 9333))
LTC_RPC_USER = os.getenv("LTC_RPC_USER", "")
LTC_RPC_PWD = os.getenv("LTC_RPC_PWD", "")
BTC_RPC_HOST = os.getenv("BTC_RPC_HOST", "127.0.0.1")
BTC_RPC_PORT = int(os.getenv("BTC_RPC_PORT", 19996))
BTC_PORT = int(os.getenv("BTC_PORT", 8333))
BTC_ONION_PORT = int(os.getenv("BTC_ONION_PORT", 8334))
BTC_RPC_USER = os.getenv("BTC_RPC_USER", "")
BTC_RPC_PWD = os.getenv("BTC_RPC_PWD", "")
DCR_RPC_HOST = os.getenv("DCR_RPC_HOST", "127.0.0.1")
DCR_RPC_PORT = int(os.getenv("DCR_RPC_PORT", 9109))
DCR_WALLET_RPC_HOST = os.getenv("DCR_WALLET_RPC_HOST", "127.0.0.1")
DCR_WALLET_RPC_PORT = int(os.getenv("DCR_WALLET_RPC_PORT", 9209))
DCR_WALLET_PWD = os.getenv(
"DCR_WALLET_PWD", random.randbytes(random.randint(14, 18)).hex()
)
DCR_RPC_USER = os.getenv("DCR_RPC_USER", "user")
DCR_RPC_PWD = os.getenv("DCR_RPC_PWD", random.randbytes(random.randint(14, 18)).hex())
NMC_RPC_HOST = os.getenv("NMC_RPC_HOST", "127.0.0.1")
NMC_RPC_PORT = int(os.getenv("NMC_RPC_PORT", 19698))
PIVX_RPC_HOST = os.getenv("PIVX_RPC_HOST", "127.0.0.1") PIVX_RPC_HOST = os.getenv("PIVX_RPC_HOST", "127.0.0.1")
PIVX_RPC_PORT = int(os.getenv("PIVX_RPC_PORT", 51473)) PIVX_RPC_PORT = int(os.getenv("PIVX_RPC_PORT", 51473))
PIVX_ONION_PORT = int(os.getenv("PIVX_ONION_PORT", 51472)) # nDefaultPort PIVX_ONION_PORT = int(os.getenv("PIVX_ONION_PORT", 51472)) # nDefaultPort
@@ -284,23 +270,18 @@ TOR_PROXY_HOST = os.getenv("TOR_PROXY_HOST", "127.0.0.1")
TOR_PROXY_PORT = int(os.getenv("TOR_PROXY_PORT", 9050)) TOR_PROXY_PORT = int(os.getenv("TOR_PROXY_PORT", 9050))
TOR_CONTROL_PORT = int(os.getenv("TOR_CONTROL_PORT", 9051)) TOR_CONTROL_PORT = int(os.getenv("TOR_CONTROL_PORT", 9051))
TOR_DNS_PORT = int(os.getenv("TOR_DNS_PORT", 5353)) TOR_DNS_PORT = int(os.getenv("TOR_DNS_PORT", 5353))
TOR_CONTROL_LISTEN_INTERFACE = os.getenv(
def setTorrcVars():
global TOR_CONTROL_LISTEN_INTERFACE, TORRC_PROXY_HOST, TORRC_CONTROL_HOST, TORRC_DNS_HOST
TOR_CONTROL_LISTEN_INTERFACE = os.getenv(
"TOR_CONTROL_LISTEN_INTERFACE", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0" "TOR_CONTROL_LISTEN_INTERFACE", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0"
) )
TORRC_PROXY_HOST = os.getenv( TORRC_PROXY_HOST = os.getenv(
"TORRC_PROXY_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0" "TORRC_PROXY_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0"
) )
TORRC_CONTROL_HOST = os.getenv( TORRC_CONTROL_HOST = os.getenv(
"TORRC_CONTROL_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0" "TORRC_CONTROL_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0"
) )
TORRC_DNS_HOST = os.getenv( TORRC_DNS_HOST = os.getenv(
"TORRC_DNS_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0" "TORRC_DNS_HOST", "127.0.0.1" if BSX_LOCAL_TOR else "0.0.0.0"
) )
TEST_TOR_PROXY = toBool( TEST_TOR_PROXY = toBool(
os.getenv("TEST_TOR_PROXY", "true") os.getenv("TEST_TOR_PROXY", "true")
@@ -394,12 +375,6 @@ def getWalletName(coin_params: str, default_name: str, prefix_override=None) ->
return wallet_name return wallet_name
def getDescriptorWalletOption(coin_params):
ticker: str = coin_params["ticker"]
default_option: bool = True if ticker in ("NMC",) else False
return toBool(os.getenv(ticker + "_USE_DESCRIPTORS", default_option))
def getKnownVersion(coin_name: str) -> str: def getKnownVersion(coin_name: str) -> str:
version, version_tag, _ = known_coins[coin_name] version, version_tag, _ = known_coins[coin_name]
return version + version_tag return version + version_tag
@@ -518,8 +493,6 @@ def importPubkey(gpg, pubkey_filename, pubkeyurls):
return return
except Exception as e: except Exception as e:
logging.warning(f"Import from file failed: {e}") logging.warning(f"Import from file failed: {e}")
else:
logger.warning(f"Public key file {pubkey_filename} not found locally.")
for url in pubkeyurls: for url in pubkeyurls:
try: try:
@@ -553,7 +526,7 @@ def testOnionLink():
def havePubkey(gpg, key_id): def havePubkey(gpg, key_id):
for key in gpg.list_keys(): for key in gpg.list_keys():
if key["fingerprint"] == key_id: if key["keyid"] == key_id:
return True return True
return False return False
@@ -616,10 +589,8 @@ def ensureValidSignatureBy(result, signing_key_name):
if not isValidSignature(result): if not isValidSignature(result):
raise ValueError("Signature verification failed.") raise ValueError("Signature verification failed.")
if result.fingerprint not in expected_key_ids[signing_key_name]: if result.key_id not in expected_key_ids[signing_key_name]:
raise ValueError( raise ValueError("Signature made by unexpected keyid: " + result.key_id)
"Signature made by unexpected key fingerprint: " + result.fingerprint
)
logger.debug(f"Found valid signature by {signing_key_name} ({result.key_id}).") logger.debug(f"Found valid signature by {signing_key_name} ({result.key_id}).")
@@ -815,8 +786,6 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
else: else:
architecture = "x86_64-apple-darwin11" architecture = "x86_64-apple-darwin11"
elif USE_PLATFORM == "Windows": elif USE_PLATFORM == "Windows":
if machine == "AMD64":
machine = "x86_64"
architecture = machine + "-w64-mingw32" architecture = machine + "-w64-mingw32"
release_url = "https://codeberg.org/wownero/wownero/releases/download/v{}/wownero-{}-v{}.{}".format( release_url = "https://codeberg.org/wownero/wownero/releases/download/v{}/wownero-{}-v{}.{}".format(
@@ -960,10 +929,16 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
% (version, assert_filename) % (version, assert_filename)
) )
elif coin == "namecoin": elif coin == "namecoin":
release_url = f"https://www.namecoin.org/files/namecoin-core/namecoin-core-{version}/{release_filename}" release_url = "https://beta.namecoin.org/files/namecoin-core/namecoin-core-{}/{}".format(
signing_key = "Rose%20Turing" version, release_filename
assert_filename = "noncodesigned.SHA256SUMS" )
assert_url = f"https://raw.githubusercontent.com/namecoin/guix.sigs/main/{version}/{signing_key}/{assert_filename}" assert_filename = "{}-{}-{}-build.assert".format(
coin, os_name, version.rsplit(".", 1)[0]
)
assert_url = (
"https://raw.githubusercontent.com/namecoin/gitian.sigs/master/%s-%s/%s/%s"
% (version, os_dir_name, signing_key_name, assert_filename)
)
elif coin == "pivx": elif coin == "pivx":
release_filename = "{}-{}-{}.{}".format(coin, version, BIN_ARCH, FILE_EXT) release_filename = "{}-{}-{}.{}".format(coin, version, BIN_ARCH, FILE_EXT)
release_url = ( release_url = (
@@ -1052,7 +1027,7 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
if not os.path.exists(assert_sig_path): if not os.path.exists(assert_sig_path):
downloadFile(assert_sig_url, assert_sig_path) downloadFile(assert_sig_url, assert_sig_path)
release_hash: str = getFileHash(release_path) release_hash = getFileHash(release_path)
logger.info(f"{release_filename} hash: {release_hash}") logger.info(f"{release_filename} hash: {release_hash}")
with ( with (
open(assert_path, "rb", 0) as fp, open(assert_path, "rb", 0) as fp,
@@ -1150,7 +1125,7 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
logger.warning("Double checking Navcoin release hash.") logger.warning("Double checking Navcoin release hash.")
with open(assert_sig_path, "rb") as fp: with open(assert_sig_path, "rb") as fp:
decrypted = gpg.decrypt_file(fp) decrypted = gpg.decrypt_file(fp)
assert release_hash in str(decrypted) assert release_hash.hex() in str(decrypted)
else: else:
with open(assert_sig_path, "rb") as fp: with open(assert_sig_path, "rb") as fp:
verified = gpg.verify_file(fp, assert_path) verified = gpg.verify_file(fp, assert_path)
@@ -1368,8 +1343,6 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
fp.write("printtoconsole=0\n") fp.write("printtoconsole=0\n")
fp.write("daemon=0\n") fp.write("daemon=0\n")
fp.write(f"wallet={wallet_name}\n") fp.write(f"wallet={wallet_name}\n")
if "watch_wallet_name" in core_settings:
fp.write("wallet={}\n".format(core_settings["watch_wallet_name"]))
if tor_control_password is not None: if tor_control_password is not None:
writeTorSettings(fp, coin, core_settings, tor_control_password) writeTorSettings(fp, coin, core_settings, tor_control_password)
@@ -1426,10 +1399,6 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
) )
elif coin == "namecoin": elif coin == "namecoin":
fp.write("prune=2000\n") fp.write("prune=2000\n")
fp.write("deprecatedrpc=create_bdb\n")
fp.write("addresstype=bech32\n")
fp.write("changetype=bech32\n")
fp.write("fallbackfee=0.001\n") # minrelaytxfee
elif coin == "pivx": elif coin == "pivx":
params_dir = os.path.join(data_dir, "pivx-params") params_dir = os.path.join(data_dir, "pivx-params")
downloadPIVXParams(params_dir) downloadPIVXParams(params_dir)
@@ -1708,11 +1677,6 @@ def printHelp():
DEFAULT_WOW_RESTORE_HEIGHT DEFAULT_WOW_RESTORE_HEIGHT
) )
) )
print(
"--walletrestoretime=n Time to restore wallets from, default:{}, -1 for now.".format(
DEFAULT_RESTORE_TIME
)
)
print( print(
"--trustremotenode Set trusted-daemon for XMR, defaults to auto: true when daemon rpchost value is a private ip address else false" "--trustremotenode Set trusted-daemon for XMR, defaults to auto: true when daemon rpchost value is a private ip address else false"
) )
@@ -1745,8 +1709,6 @@ def printHelp():
print( print(
"--dashv20compatible Generate the same DASH wallet seed as for DASH v20 - Use only when importing an existing seed." "--dashv20compatible Generate the same DASH wallet seed as for DASH v20 - Use only when importing an existing seed."
) )
print("--client-auth-password= Set or update the password to protect the web UI.")
print("--disable-client-auth Remove password protection from the web UI.")
active_coins = [] active_coins = []
for coin_name in known_coins.keys(): for coin_name in known_coins.keys():
@@ -1771,8 +1733,11 @@ def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
swap_client = None swap_client = None
daemons = [] daemons = []
daemon_args = ["-noconnect", "-nodnsseed", "-nofindpeers", "-nostaking"] daemon_args = ["-noconnect", "-nodnsseed", "-nofindpeers", "-nostaking"]
with open(os.path.join(data_dir, "basicswap.log"), "a") as fp:
try: try:
swap_client = BasicSwap(data_dir, settings, chain, transient_instance=True) swap_client = BasicSwap(
fp, data_dir, settings, chain, transient_instance=True
)
if not swap_client.use_tor_proxy: if not swap_client.use_tor_proxy:
# Cannot set -bind or -whitebind together with -listen=0 # Cannot set -bind or -whitebind together with -listen=0
daemon_args.append("-nolisten") daemon_args.append("-nolisten")
@@ -1814,18 +1779,12 @@ def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
def encrypt_wallet(swap_client, coin_type) -> None: def encrypt_wallet(swap_client, coin_type) -> None:
ci = swap_client.ci(coin_type) ci = swap_client.ci(coin_type)
ci.changeWalletPassword("", WALLET_ENCRYPTION_PWD, check_seed_if_encrypt=False) ci.changeWalletPassword("", WALLET_ENCRYPTION_PWD)
ci.unlockWallet(WALLET_ENCRYPTION_PWD) ci.unlockWallet(WALLET_ENCRYPTION_PWD)
def initialise_wallets( def initialise_wallets(
particl_wallet_mnemonic, particl_wallet_mnemonic, with_coins, data_dir, settings, chain, use_tor_proxy
with_coins,
data_dir,
settings,
chain,
use_tor_proxy,
extra_opts={},
): ):
swap_client = None swap_client = None
daemons = [] daemons = []
@@ -1834,8 +1793,11 @@ def initialise_wallets(
coins_failed_to_initialise = [] coins_failed_to_initialise = []
with open(os.path.join(data_dir, "basicswap.log"), "a") as fp:
try: try:
swap_client = BasicSwap(data_dir, settings, chain, transient_instance=True) swap_client = BasicSwap(
fp, data_dir, settings, chain, transient_instance=True
)
if not swap_client.use_tor_proxy: if not swap_client.use_tor_proxy:
# Cannot set -bind or -whitebind together with -listen=0 # Cannot set -bind or -whitebind together with -listen=0
daemon_args.append("-nolisten") daemon_args.append("-nolisten")
@@ -1846,7 +1808,6 @@ def initialise_wallets(
Coins.DOGE, Coins.DOGE,
Coins.DCR, Coins.DCR,
Coins.DASH, Coins.DASH,
Coins.NMC,
) )
# Always start Particl, it must be running to initialise a wallet in addcoin mode # Always start Particl, it must be running to initialise a wallet in addcoin mode
# Particl must be loaded first as subsequent coins are initialised from the Particl mnemonic # Particl must be loaded first as subsequent coins are initialised from the Particl mnemonic
@@ -1861,7 +1822,9 @@ def initialise_wallets(
if c == Coins.XMR: if c == Coins.XMR:
if coin_settings["manage_wallet_daemon"]: if coin_settings["manage_wallet_daemon"]:
filename = ( filename = (
coin_name + "-wallet-rpc" + (".exe" if os.name == "nt" else "") coin_name
+ "-wallet-rpc"
+ (".exe" if os.name == "nt" else "")
) )
filename: str = getWalletBinName( filename: str = getWalletBinName(
c, coin_settings, coin_name + "-wallet-rpc" c, coin_settings, coin_name + "-wallet-rpc"
@@ -1889,7 +1852,9 @@ def initialise_wallets(
pass pass
else: else:
if coin_settings["manage_daemon"]: if coin_settings["manage_daemon"]:
filename: str = getCoreBinName(c, coin_settings, coin_name + "d") filename: str = getCoreBinName(
c, coin_settings, coin_name + "d"
)
coin_args = ( coin_args = (
["-nofindpeers", "-nostaking"] if c == Coins.PART else [] ["-nofindpeers", "-nostaking"] if c == Coins.PART else []
) )
@@ -1917,13 +1882,6 @@ def initialise_wallets(
swap_client.createCoinInterface(c) swap_client.createCoinInterface(c)
if c in coins_to_create_wallets_for: if c in coins_to_create_wallets_for:
if c == Coins.PART and "particl" not in with_coins:
# Running addcoin with an existing particl wallet
swap_client.waitForDaemonRPC(c, with_wallet=True)
# Particl wallet must be unlocked to call getWalletKey
if WALLET_ENCRYPTION_PWD != "":
swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD)
continue
if c == Coins.DCR: if c == Coins.DCR:
if coin_settings["manage_wallet_daemon"] is False: if coin_settings["manage_wallet_daemon"] is False:
continue continue
@@ -1934,7 +1892,7 @@ def initialise_wallets(
if WALLET_ENCRYPTION_PWD == "" if WALLET_ENCRYPTION_PWD == ""
else WALLET_ENCRYPTION_PWD else WALLET_ENCRYPTION_PWD
) )
extra_args = [ extra_opts = [
'--appdata="{}"'.format(coin_settings["datadir"]), '--appdata="{}"'.format(coin_settings["datadir"]),
"--pass={}".format(dcr_password), "--pass={}".format(dcr_password),
] ]
@@ -1943,7 +1901,7 @@ def initialise_wallets(
args = [ args = [
os.path.join(coin_settings["bindir"], filename), os.path.join(coin_settings["bindir"], filename),
"--create", "--create",
] + extra_args ] + extra_opts
hex_seed = swap_client.getWalletKey(Coins.DCR, 1).hex() hex_seed = swap_client.getWalletKey(Coins.DCR, 1).hex()
createDCRWallet(args, hex_seed, logger, threading.Event()) createDCRWallet(args, hex_seed, logger, threading.Event())
continue continue
@@ -1954,28 +1912,13 @@ def initialise_wallets(
logger.info( logger.info(
f'Creating wallet "{wallet_name}" for {getCoinName(c)}.' f'Creating wallet "{wallet_name}" for {getCoinName(c)}.'
) )
use_descriptors = coin_settings.get("use_descriptors", False)
if c in (Coins.DASH,): if c in (Coins.BTC, Coins.LTC, Coins.DOGE, Coins.DASH):
# TODO: Remove when fixed
if WALLET_ENCRYPTION_PWD != "":
logger.warning(
"Workaround for Dash sethdseed error if wallet is encrypted."
) # Errors with "AddHDChainSingle failed"
assert use_descriptors is False
swap_client.callcoinrpc(
c,
"createwallet",
[
wallet_name,
False,
True,
"",
False,
use_descriptors,
],
)
elif c in (Coins.BTC, Coins.LTC, Coins.NMC, Coins.DOGE):
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors # wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
use_descriptors = coin_settings.get(
"use_descriptors", False
)
swap_client.callcoinrpc( swap_client.callcoinrpc(
c, c,
"createwallet", "createwallet",
@@ -1989,15 +1932,11 @@ def initialise_wallets(
], ],
) )
if use_descriptors: if use_descriptors:
watch_wallet_name = coin_settings["watch_wallet_name"]
logger.info(
f'Creating wallet "{watch_wallet_name}" for {getCoinName(c)}.'
)
swap_client.callcoinrpc( swap_client.callcoinrpc(
c, c,
"createwallet", "createwallet",
[ [
watch_wallet_name, coin_settings["watch_wallet_name"],
True, True,
True, True,
"", "",
@@ -2005,9 +1944,7 @@ def initialise_wallets(
use_descriptors, use_descriptors,
], ],
) )
swap_client.ci(c).unlockWallet( swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD)
WALLET_ENCRYPTION_PWD, check_seed=False
)
else: else:
swap_client.callcoinrpc( swap_client.callcoinrpc(
c, c,
@@ -2038,9 +1975,11 @@ def initialise_wallets(
swap_client.callcoinrpc( swap_client.callcoinrpc(
Coins.PART, "extkeyimportmaster", [particl_wallet_mnemonic] Coins.PART, "extkeyimportmaster", [particl_wallet_mnemonic]
) )
# Particl wallet must be unlocked to call getWalletKey
if WALLET_ENCRYPTION_PWD != "":
swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD)
for coin_name in with_coins: for coin_name in with_coins:
coin_settings = settings["chainclients"][coin_name]
c = swap_client.getCoinIdFromName(coin_name) c = swap_client.getCoinIdFromName(coin_name)
if c in (Coins.PART,): if c in (Coins.PART,):
continue continue
@@ -2048,28 +1987,13 @@ def initialise_wallets(
# initialiseWallet only sets main_wallet_seedid_ # initialiseWallet only sets main_wallet_seedid_
swap_client.waitForDaemonRPC(c) swap_client.waitForDaemonRPC(c)
try: try:
default_restore_time = ( swap_client.initialiseWallet(c, raise_errors=True)
-1 if generated_mnemonic else DEFAULT_RESTORE_TIME
) # Set to -1 (now) if key is newly generated
restore_time: int = extra_opts.get(
"walletrestoretime", default_restore_time
)
swap_client.initialiseWallet(
c, raise_errors=True, restore_time=restore_time
)
if c not in (Coins.XMR, Coins.WOW):
if restore_time == -1:
restore_time = int(time.time())
coin_settings["restore_time"] = restore_time
except Exception as e: except Exception as e:
coins_failed_to_initialise.append((c, e)) coins_failed_to_initialise.append((c, e))
if WALLET_ENCRYPTION_PWD != "" and ( if WALLET_ENCRYPTION_PWD != "" and c not in coins_to_create_wallets_for:
c not in coins_to_create_wallets_for or c in (Coins.DASH,)
): # TODO: Remove DASH workaround
try: try:
swap_client.ci(c).changeWalletPassword( swap_client.ci(c).changeWalletPassword(
"", WALLET_ENCRYPTION_PWD, check_seed_if_encrypt=False "", WALLET_ENCRYPTION_PWD
) )
except Exception as e: # noqa: F841 except Exception as e: # noqa: F841
logger.warning(f"changeWalletPassword failed for {coin_name}.") logger.warning(f"changeWalletPassword failed for {coin_name}.")
@@ -2084,12 +2008,12 @@ def initialise_wallets(
print("") print("")
for pair in coins_failed_to_initialise: for pair in coins_failed_to_initialise:
c, e = pair c, e = pair
if c in (Coins.PIVX, Coins.BCH): if c in (Coins.PIVX,):
print( print(
f"NOTE - Unable to initialise wallet for {getCoinName(c)}. To complete setup click 'Reseed Wallet' from the ui page once chain is synced." f"NOTE - Unable to initialise wallet for {getCoinName(c)}. To complete setup click 'Reseed Wallet' from the ui page once chain is synced."
) )
else: else:
raise ValueError(f"Failed to initialise wallet for {getCoinName(c)}: {e}") print(f"WARNING - Failed to initialise wallet for {getCoinName(c)}: {e}")
if "decred" in with_coins and WALLET_ENCRYPTION_PWD != "": if "decred" in with_coins and WALLET_ENCRYPTION_PWD != "":
print( print(
@@ -2110,68 +2034,7 @@ def load_config(config_path):
if not os.path.exists(config_path): if not os.path.exists(config_path):
exitWithError("{} does not exist".format(config_path)) exitWithError("{} does not exist".format(config_path))
with open(config_path) as fs: with open(config_path) as fs:
settings = json.load(fs) return json.load(fs)
BSX_ALLOW_ENV_OVERRIDE = toBool(os.getenv("BSX_ALLOW_ENV_OVERRIDE", "false"))
saved_env_var_settings = [
("setup_docker_mode", "BSX_DOCKER_MODE"),
("setup_local_tor", "BSX_LOCAL_TOR"),
("setup_tor_control_listen_interface", "TOR_CONTROL_LISTEN_INTERFACE"),
("setup_torrc_proxy_host", "TORRC_PROXY_HOST"),
("setup_torrc_control_host", "TORRC_CONTROL_HOST"),
("setup_torrc_dns_host", "TORRC_DNS_HOST"),
("tor_proxy_host", "TOR_PROXY_HOST"),
("tor_proxy_port", "TOR_PROXY_PORT"),
("tor_control_port", "TOR_CONTROL_PORT"),
]
for setting in saved_env_var_settings:
config_name, env_name = setting
env_value = globals()[env_name]
saved_config_value = settings.get(config_name, env_value)
if saved_config_value != env_value:
if os.getenv(env_name):
# If the env var was manually set override the saved config if allowed else fail.
if BSX_ALLOW_ENV_OVERRIDE:
logger.warning(
f"Env var {env_name} differs from saved config '{config_name}', overriding."
)
else:
print(
f"Env var {env_name} differs from saved config '{config_name}', set 'BSX_ALLOW_ENV_OVERRIDE' to override.",
file=sys.stderr,
)
sys.exit(1)
else:
logger.info(f"Setting {env_name} from saved config '{config_name}'.")
globals()[env_name] = saved_config_value
# Recalculate env vars that depend on the changed var
if env_name == "BSX_LOCAL_TOR":
setTorrcVars()
return settings
def save_config(config_path, settings, add_options: bool = True) -> None:
if add_options is True:
# Add to config file only if manually set
if os.getenv("BSX_DOCKER_MODE"):
settings["setup_docker_mode"] = BSX_DOCKER_MODE
if os.getenv("BSX_LOCAL_TOR"):
settings["setup_local_tor"] = BSX_LOCAL_TOR
if os.getenv("TOR_CONTROL_LISTEN_INTERFACE"):
settings["setup_tor_control_listen_interface"] = (
TOR_CONTROL_LISTEN_INTERFACE
)
if os.getenv("TORRC_PROXY_HOST"):
settings["setup_torrc_proxy_host"] = TORRC_PROXY_HOST
if os.getenv("TORRC_CONTROL_HOST"):
settings["setup_torrc_control_host"] = TORRC_CONTROL_HOST
if os.getenv("TORRC_DNS_HOST"):
settings["setup_torrc_dns_host"] = TORRC_DNS_HOST
with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
def signal_handler(sig, frame): def signal_handler(sig, frame):
@@ -2218,10 +2081,7 @@ def check_btc_fastsync_data(base_dir, sync_filename):
importPubkey(gpg, pubkey_filename, pubkeyurls) importPubkey(gpg, pubkey_filename, pubkeyurls)
with open(asc_file_path, "rb") as fp: with open(asc_file_path, "rb") as fp:
verified = gpg.verify_file(fp) verified = gpg.verify_file(fp)
if ( if isValidSignature(verified) and verified.key_id in expected_key_ids["tecnovert"]:
isValidSignature(verified)
and verified.fingerprint in expected_key_ids["tecnovert"]
):
ensureValidSignatureBy(verified, "tecnovert") ensureValidSignatureBy(verified, "tecnovert")
else: else:
pubkey_filename = "nicolasdorier.asc" pubkey_filename = "nicolasdorier.asc"
@@ -2241,7 +2101,6 @@ def ensure_coin_valid(coin: str, test_disabled: bool = True) -> None:
def main(): def main():
global use_tor_proxy, with_coins_changed global use_tor_proxy, with_coins_changed
setTorrcVars()
data_dir = None data_dir = None
bin_dir = None bin_dir = None
port_offset = None port_offset = None
@@ -2263,8 +2122,6 @@ def main():
disable_tor = False disable_tor = False
initwalletsonly = False initwalletsonly = False
tor_control_password = None tor_control_password = None
client_auth_pwd_value = None
disable_client_auth_flag = False
extra_opts = {} extra_opts = {}
if os.getenv("SSL_CERT_DIR", "") == "" and GUIX_SSL_CERT_DIR is not None: if os.getenv("SSL_CERT_DIR", "") == "" and GUIX_SSL_CERT_DIR is not None:
@@ -2391,24 +2248,13 @@ def main():
if name == "wowrestoreheight": if name == "wowrestoreheight":
wow_restore_height = int(s[1]) wow_restore_height = int(s[1])
continue continue
if name == "walletrestoretime":
extra_opts["walletrestoretime"] = int(s[1])
continue
if name == "keysdirpath": if name == "keysdirpath":
extra_opts["keysdirpath"] = os.path.expanduser(s[1].strip('"')) extra_opts["keysdirpath"] = os.path.expanduser(s[1].strip('"'))
continue continue
if name == "trustremotenode": if name == "trustremotenode":
extra_opts["trust_remote_node"] = toBool(s[1]) extra_opts["trust_remote_node"] = toBool(s[1])
continue continue
if name == "client-auth-password":
client_auth_pwd_value = s[1].strip('"')
continue
if name == "disable-client-auth":
disable_client_auth_flag = True
continue
if len(s) != 2:
exitWithError("Unknown argument {}".format(v))
exitWithError("Unknown argument {}".format(v)) exitWithError("Unknown argument {}".format(v))
if print_versions: if print_versions:
@@ -2438,34 +2284,6 @@ def main():
os.makedirs(data_dir) os.makedirs(data_dir)
config_path = os.path.join(data_dir, cfg.CONFIG_FILENAME) config_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)
config_exists = os.path.exists(config_path)
if config_exists and (
client_auth_pwd_value is not None or disable_client_auth_flag
):
try:
settings = load_config(config_path)
modified = False
if client_auth_pwd_value is not None:
settings["client_auth_hash"] = rfc2440_hash_password(
client_auth_pwd_value
)
logger.info("Client authentication password updated.")
modified = True
elif disable_client_auth_flag:
if "client_auth_hash" in settings:
del settings["client_auth_hash"]
logger.info("Client authentication disabled.")
modified = True
else:
logger.info("Client authentication is already disabled.")
if modified:
with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
return 0
except Exception as e:
exitWithError(f"Failed to update client auth settings: {e}")
if use_tor_proxy and extra_opts.get("no_tor_proxy", False): if use_tor_proxy and extra_opts.get("no_tor_proxy", False):
exitWithError("Can't use --usetorproxy and --notorproxy together") exitWithError("Can't use --usetorproxy and --notorproxy together")
@@ -2569,6 +2387,22 @@ def main():
"core_version_no": getKnownVersion("bitcoin"), "core_version_no": getKnownVersion("bitcoin"),
"core_version_group": 28, "core_version_group": 28,
}, },
"bitcoincash": {
"connection_type": "rpc",
"manage_daemon": shouldManageDaemon("BCH"),
"rpchost": BCH_RPC_HOST,
"rpcport": BCH_RPC_PORT + port_offset,
"onionport": BCH_ONION_PORT + port_offset,
"datadir": os.getenv("BCH_DATA_DIR", os.path.join(data_dir, "bitcoincash")),
"bindir": os.path.join(bin_dir, "bitcoincash"),
"port": BCH_PORT + port_offset,
"config_filename": "bitcoin.conf",
"use_segwit": False,
"blocks_confirmed": 1,
"conf_target": 2,
"core_version_no": getKnownVersion("bitcoincash"),
"core_version_group": 22,
},
"litecoin": { "litecoin": {
"connection_type": "rpc", "connection_type": "rpc",
"manage_daemon": shouldManageDaemon("LTC"), "manage_daemon": shouldManageDaemon("LTC"),
@@ -2584,6 +2418,22 @@ def main():
"core_version_group": 20, "core_version_group": 20,
"min_relay_fee": 0.00001, "min_relay_fee": 0.00001,
}, },
"dogecoin": {
"connection_type": "rpc",
"manage_daemon": shouldManageDaemon("DOGE"),
"rpchost": DOGE_RPC_HOST,
"rpcport": DOGE_RPC_PORT + port_offset,
"onionport": DOGE_ONION_PORT + port_offset,
"datadir": os.getenv("DOGE_DATA_DIR", os.path.join(data_dir, "dogecoin")),
"bindir": os.path.join(bin_dir, "dogecoin"),
"use_segwit": False,
"use_csv": False,
"blocks_confirmed": 2,
"conf_target": 2,
"core_version_no": getKnownVersion("dogecoin"),
"core_version_group": 23,
"min_relay_fee": 0.01, # RECOMMENDED_MIN_TX_FEE
},
"decred": { "decred": {
"connection_type": "rpc", "connection_type": "rpc",
"manage_daemon": shouldManageDaemon("DCR"), "manage_daemon": shouldManageDaemon("DCR"),
@@ -2611,16 +2461,14 @@ def main():
"manage_daemon": shouldManageDaemon("NMC"), "manage_daemon": shouldManageDaemon("NMC"),
"rpchost": NMC_RPC_HOST, "rpchost": NMC_RPC_HOST,
"rpcport": NMC_RPC_PORT + port_offset, "rpcport": NMC_RPC_PORT + port_offset,
"onionport": NMC_ONION_PORT + port_offset,
"datadir": os.getenv("NMC_DATA_DIR", os.path.join(data_dir, "namecoin")), "datadir": os.getenv("NMC_DATA_DIR", os.path.join(data_dir, "namecoin")),
"bindir": os.path.join(bin_dir, "namecoin"), "bindir": os.path.join(bin_dir, "namecoin"),
"port": NMC_PORT + port_offset, "use_segwit": False,
"use_segwit": True, "use_csv": False,
"use_csv": True,
"blocks_confirmed": 1, "blocks_confirmed": 1,
"conf_target": 2, "conf_target": 2,
"core_version_no": getKnownVersion("namecoin"), "core_version_no": getKnownVersion("namecoin"),
"core_version_group": 28, "core_version_group": 18,
"chain_lookups": "local", "chain_lookups": "local",
}, },
"monero": { "monero": {
@@ -2631,7 +2479,7 @@ def main():
"zmqport": XMR_ZMQ_PORT + port_offset, "zmqport": XMR_ZMQ_PORT + port_offset,
"walletrpcport": XMR_WALLET_RPC_PORT + port_offset, "walletrpcport": XMR_WALLET_RPC_PORT + port_offset,
"rpchost": XMR_RPC_HOST, "rpchost": XMR_RPC_HOST,
"trusted_daemon": extra_opts.get("trust_remote_node", True), "trusted_daemon": extra_opts.get("trust_remote_node", "auto"),
"walletrpchost": XMR_WALLET_RPC_HOST, "walletrpchost": XMR_WALLET_RPC_HOST,
"walletrpcuser": XMR_WALLET_RPC_USER, "walletrpcuser": XMR_WALLET_RPC_USER,
"walletrpcpassword": XMR_WALLET_RPC_PWD, "walletrpcpassword": XMR_WALLET_RPC_PWD,
@@ -2646,28 +2494,6 @@ def main():
"core_version_no": getKnownVersion("monero"), "core_version_no": getKnownVersion("monero"),
"core_type_group": "xmr", "core_type_group": "xmr",
}, },
"wownero": {
"connection_type": "rpc",
"manage_daemon": shouldManageDaemon("WOW"),
"manage_wallet_daemon": shouldManageDaemon("WOW_WALLET"),
"rpcport": WOW_RPC_PORT + port_offset,
"zmqport": WOW_ZMQ_PORT + port_offset,
"walletrpcport": WOW_WALLET_RPC_PORT + port_offset,
"rpchost": WOW_RPC_HOST,
"trusted_daemon": extra_opts.get("trust_remote_node", True),
"walletrpchost": WOW_WALLET_RPC_HOST,
"walletrpcuser": WOW_WALLET_RPC_USER,
"walletrpcpassword": WOW_WALLET_RPC_PWD,
"datadir": os.getenv("WOW_DATA_DIR", os.path.join(data_dir, "wownero")),
"bindir": os.path.join(bin_dir, "wownero"),
"restore_height": wow_restore_height,
"blocks_confirmed": 2,
"rpctimeout": 60,
"walletrpctimeout": 120,
"walletrpctimeoutlong": 300,
"core_version_no": getKnownVersion("wownero"),
"core_type_group": "xmr",
},
"pivx": { "pivx": {
"connection_type": "rpc", "connection_type": "rpc",
"manage_daemon": shouldManageDaemon("PIVX"), "manage_daemon": shouldManageDaemon("PIVX"),
@@ -2731,37 +2557,27 @@ def main():
"chain_lookups": "local", "chain_lookups": "local",
"startup_tries": 40, "startup_tries": 40,
}, },
"bitcoincash": { "wownero": {
"connection_type": "rpc", "connection_type": "rpc",
"manage_daemon": shouldManageDaemon("BCH"), "manage_daemon": shouldManageDaemon("WOW"),
"rpchost": BCH_RPC_HOST, "manage_wallet_daemon": shouldManageDaemon("WOW_WALLET"),
"rpcport": BCH_RPC_PORT + port_offset, "rpcport": WOW_RPC_PORT + port_offset,
"onionport": BCH_ONION_PORT + port_offset, "zmqport": WOW_ZMQ_PORT + port_offset,
"datadir": os.getenv("BCH_DATA_DIR", os.path.join(data_dir, "bitcoincash")), "walletrpcport": WOW_WALLET_RPC_PORT + port_offset,
"bindir": os.path.join(bin_dir, "bitcoincash"), "rpchost": WOW_RPC_HOST,
"port": BCH_PORT + port_offset, "trusted_daemon": extra_opts.get("trust_remote_node", "auto"),
"config_filename": "bitcoin.conf", "walletrpchost": WOW_WALLET_RPC_HOST,
"use_segwit": False, "walletrpcuser": WOW_WALLET_RPC_USER,
"blocks_confirmed": 1, "walletrpcpassword": WOW_WALLET_RPC_PWD,
"conf_target": 2, "datadir": os.getenv("WOW_DATA_DIR", os.path.join(data_dir, "wownero")),
"core_version_no": getKnownVersion("bitcoincash"), "bindir": os.path.join(bin_dir, "wownero"),
"core_version_group": 22, "restore_height": wow_restore_height,
},
"dogecoin": {
"connection_type": "rpc",
"manage_daemon": shouldManageDaemon("DOGE"),
"rpchost": DOGE_RPC_HOST,
"rpcport": DOGE_RPC_PORT + port_offset,
"onionport": DOGE_ONION_PORT + port_offset,
"datadir": os.getenv("DOGE_DATA_DIR", os.path.join(data_dir, "dogecoin")),
"bindir": os.path.join(bin_dir, "dogecoin"),
"use_segwit": False,
"use_csv": False,
"blocks_confirmed": 2, "blocks_confirmed": 2,
"conf_target": 2, "rpctimeout": 60,
"core_version_no": getKnownVersion("dogecoin"), "walletrpctimeout": 120,
"core_version_group": 23, "walletrpctimeoutlong": 300,
"min_relay_fee": 0.01, # RECOMMENDED_MIN_TX_FEE "core_version_no": getKnownVersion("wownero"),
"core_type_group": "xmr",
}, },
} }
@@ -2785,8 +2601,9 @@ def main():
coin_settings["wallet_name"] = set_name coin_settings["wallet_name"] = set_name
ticker: str = coin_params["ticker"] ticker: str = coin_params["ticker"]
if getDescriptorWalletOption(coin_params): if toBool(os.getenv(ticker + "_USE_DESCRIPTORS", False)):
if coin_id not in (Coins.BTC, Coins.NMC):
if coin_id not in (Coins.BTC,):
raise ValueError(f"Descriptor wallet unavailable for {coin_name}") raise ValueError(f"Descriptor wallet unavailable for {coin_name}")
coin_settings["use_descriptors"] = True coin_settings["use_descriptors"] = True
@@ -2854,7 +2671,6 @@ def main():
settings, settings,
chain, chain,
use_tor_proxy, use_tor_proxy,
extra_opts=extra_opts,
) )
print("Done.") print("Done.")
@@ -2876,15 +2692,15 @@ def main():
settings, coin, tor_control_password, enable=True, extra_opts=extra_opts settings, coin, tor_control_password, enable=True, extra_opts=extra_opts
) )
save_config(config_path, settings) with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
logger.info("Done.") logger.info("Done.")
return 0 return 0
if disable_tor: if disable_tor:
logger.info("Disabling TOR") logger.info("Disabling TOR")
settings = load_config(config_path) settings = load_config(config_path)
if not settings.get("use_tor", False):
logger.info("TOR is not enabled.") # Continue anyway to clear any config
settings["use_tor"] = False settings["use_tor"] = False
for coin in settings["chainclients"]: for coin in settings["chainclients"]:
modify_tor_config( modify_tor_config(
@@ -2895,7 +2711,9 @@ def main():
extra_opts=extra_opts, extra_opts=extra_opts,
) )
save_config(config_path, settings) with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
logger.info("Done.") logger.info("Done.")
return 0 return 0
@@ -2920,7 +2738,9 @@ def main():
if "manage_wallet_daemon" in coin_settings: if "manage_wallet_daemon" in coin_settings:
coin_settings["manage_wallet_daemon"] = False coin_settings["manage_wallet_daemon"] = False
save_config(config_path, settings) with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
logger.info("Done.") logger.info("Done.")
return 0 return 0
@@ -2942,7 +2762,8 @@ def main():
coin_settings["manage_daemon"] = True coin_settings["manage_daemon"] = True
if "manage_wallet_daemon" in coin_settings: if "manage_wallet_daemon" in coin_settings:
coin_settings["manage_wallet_daemon"] = True coin_settings["manage_wallet_daemon"] = True
save_config(config_path, settings) with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
logger.info("Done.") logger.info("Done.")
return 0 return 0
exitWithError("{} is already in the settings file".format(add_coin)) exitWithError("{} is already in the settings file".format(add_coin))
@@ -2957,6 +2778,7 @@ def main():
test_particl_encryption(data_dir, settings, chain, use_tor_proxy) test_particl_encryption(data_dir, settings, chain, use_tor_proxy)
settings["chainclients"][add_coin] = chainclients[add_coin] settings["chainclients"][add_coin] = chainclients[add_coin]
settings["use_tor_proxy"] = use_tor_proxy
if not no_cores: if not no_cores:
prepareCore(add_coin, known_coins[add_coin], settings, data_dir, extra_opts) prepareCore(add_coin, known_coins[add_coin], settings, data_dir, extra_opts)
@@ -2976,10 +2798,10 @@ def main():
settings, settings,
chain, chain,
use_tor_proxy, use_tor_proxy,
extra_opts=extra_opts,
) )
save_config(config_path, settings) with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
logger.info(f"Done. Coin {add_coin} successfully added.") logger.info(f"Done. Coin {add_coin} successfully added.")
return 0 return 0
@@ -2998,7 +2820,8 @@ def main():
if c not in settings["chainclients"]: if c not in settings["chainclients"]:
settings["chainclients"][c] = chainclients[c] settings["chainclients"][c] = chainclients[c]
elif upgrade_cores: elif upgrade_cores:
settings = load_config(config_path) with open(config_path) as fs:
settings = json.load(fs)
with_coins_start = with_coins with_coins_start = with_coins
if not with_coins_changed: if not with_coins_changed:
@@ -3047,8 +2870,8 @@ def main():
# Run second loop to update, so all versions are logged together. # Run second loop to update, so all versions are logged together.
# Backup settings # Backup settings
old_config_path = config_path[:-5] + "_" + str(int(time.time())) + ".json" old_config_path = config_path[:-5] + "_" + str(int(time.time())) + ".json"
save_config(old_config_path, settings, add_options=False) with open(old_config_path, "w") as fp:
json.dump(settings, fp, indent=4)
for c in with_coins: for c in with_coins:
prepareCore(c, known_coins[c], settings, data_dir, extra_opts) prepareCore(c, known_coins[c], settings, data_dir, extra_opts)
current_coin_settings = chainclients[c] current_coin_settings = chainclients[c]
@@ -3061,7 +2884,8 @@ def main():
settings["chainclients"][c][ settings["chainclients"][c][
"core_version_group" "core_version_group"
] = current_version_group ] = current_version_group
save_config(config_path, settings) with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
logger.info("Done.") logger.info("Done.")
return 0 return 0
@@ -3100,10 +2924,6 @@ def main():
tor_control_password = generate_salt(24) tor_control_password = generate_salt(24)
addTorSettings(settings, tor_control_password) addTorSettings(settings, tor_control_password)
if client_auth_pwd_value is not None:
settings["client_auth_hash"] = rfc2440_hash_password(client_auth_pwd_value)
logger.info("Client authentication password set.")
if not no_cores: if not no_cores:
for c in with_coins: for c in with_coins:
prepareCore(c, known_coins[c], settings, data_dir, extra_opts) prepareCore(c, known_coins[c], settings, data_dir, extra_opts)
@@ -3115,21 +2935,16 @@ def main():
for c in with_coins: for c in with_coins:
prepareDataDir(c, settings, chain, particl_wallet_mnemonic, extra_opts) prepareDataDir(c, settings, chain, particl_wallet_mnemonic, extra_opts)
with open(config_path, "w") as fp:
json.dump(settings, fp, indent=4)
if particl_wallet_mnemonic == "none": if particl_wallet_mnemonic == "none":
save_config(config_path, settings)
logger.info("Done.") logger.info("Done.")
return 0 return 0
initialise_wallets( initialise_wallets(
particl_wallet_mnemonic, particl_wallet_mnemonic, with_coins, data_dir, settings, chain, use_tor_proxy
with_coins,
data_dir,
settings,
chain,
use_tor_proxy,
extra_opts=extra_opts,
) )
save_config(config_path, settings)
print("Done.") print("Done.")

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. # 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. # 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 # https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-28.0.md?plain=1#L84
if ( if prepare is False and use_tor_proxy is False and coin_id == Coins.BTC:
prepare is False
and use_tor_proxy is False
and coin_id in (Coins.BTC, Coins.NMC)
):
port: int = coin_settings.get("port", 8333) port: int = coin_settings.get("port", 8333)
extra_args.append(f"--bind=0.0.0.0:{port}") extra_args.append(f"--bind=0.0.0.0:{port}")
return extra_args return extra_args
def runClient( def runClient(fp, data_dir, chain, start_only_coins):
data_dir: str, chain: str, start_only_coins: bool, log_prefix: str = "BasicSwap"
) -> int:
global swap_client, logger global swap_client, logger
daemons = [] daemons = []
pids = [] pids = []
@@ -302,7 +296,7 @@ def runClient(
with open(settings_path) as fs: with open(settings_path) as fs:
settings = json.load(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 logger = swap_client.log
if os.path.exists(pids_path): if os.path.exists(pids_path):
@@ -440,7 +434,7 @@ def runClient(
) )
) )
pid = daemons[-1].handle.pid pid = daemons[-1].handle.pid
swap_client.log.info(f"Started {filename} {pid}") swap_client.log.info("Started {} {}".format(filename, pid))
continue # /decred continue # /decred
@@ -486,6 +480,7 @@ def runClient(
else cfg.DEFAULT_ALLOW_CORS else cfg.DEFAULT_ALLOW_CORS
) )
thread_http = HttpThread( thread_http = HttpThread(
fp,
settings["htmlhost"], settings["htmlhost"],
settings["htmlport"], settings["htmlport"],
allow_cors, allow_cors,
@@ -534,7 +529,7 @@ def runClient(
closed_pids = [] closed_pids = []
for d in daemons: for d in daemons:
swap_client.log.info(f"Interrupting {d.handle.pid}") swap_client.log.info("Interrupting {}".format(d.handle.pid))
try: try:
d.handle.send_signal( d.handle.send_signal(
signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
@@ -551,9 +546,6 @@ def runClient(
except Exception as e: except Exception as e:
swap_client.log.error(f"Error: {e}") swap_client.log.error(f"Error: {e}")
fail_code: int = swap_client.fail_code
del swap_client
if os.path.exists(pids_path): if os.path.exists(pids_path):
with open(pids_path) as fd: with open(pids_path) as fd:
lines = fd.read().split("\n") lines = fd.read().split("\n")
@@ -567,13 +559,9 @@ def runClient(
with open(pids_path, "w") as fd: with open(pids_path, "w") as fd:
fd.write(still_running) fd.write(still_running)
return fail_code
def printVersion(): def printVersion():
logger.info( logger.info("Basicswap version: %s", __version__)
f"Basicswap version: {__version__}",
)
def printHelp(): def printHelp():
@@ -581,7 +569,9 @@ def printHelp():
print("\n--help, -h Print help.") print("\n--help, -h Print help.")
print("--version, -v Print version.") print("--version, -v Print version.")
print( 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("--mainnet Run in mainnet mode.")
print("--testnet Run in testnet mode.") print("--testnet Run in testnet mode.")
@@ -589,18 +579,16 @@ def printHelp():
print( print(
"--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing." "--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing."
) )
print("--logprefix Specify log prefix.")
def main(): def main():
data_dir = None data_dir = None
chain = "mainnet" chain = "mainnet"
start_only_coins = set() start_only_coins = set()
log_prefix: str = "BasicSwap"
for v in sys.argv[1:]: for v in sys.argv[1:]:
if len(v) < 2 or v[0] != "-": if len(v) < 2 or v[0] != "-":
logger.warning(f"Unknown argument {v}") logger.warning("Unknown argument %s", v)
continue continue
s = v.split("=") s = v.split("=")
@@ -625,9 +613,6 @@ def main():
if name == "datadir": if name == "datadir":
data_dir = os.path.expanduser(s[1]) data_dir = os.path.expanduser(s[1])
continue continue
if name == "logprefix":
log_prefix = s[1]
continue
if name == "startonlycoin": if name == "startonlycoin":
for coin in [s.lower() for s in s[1].split(",")]: for coin in [s.lower() for s in s[1].split(",")]:
if is_known_coin(coin) is False: if is_known_coin(coin) is False:
@@ -635,7 +620,7 @@ def main():
start_only_coins.add(coin) start_only_coins.add(coin)
continue continue
logger.warning(f"Unknown argument {v}") logger.warning("Unknown argument %s", v)
if os.name == "nt": if os.name == "nt":
logger.warning( logger.warning(
@@ -644,17 +629,20 @@ def main():
if data_dir is None: if data_dir is None:
data_dir = os.path.join(os.path.expanduser(cfg.BASICSWAP_DATADIR)) data_dir = os.path.join(os.path.expanduser(cfg.BASICSWAP_DATADIR))
logger.info(f"Using datadir: {data_dir}") logger.info("Using datadir: %s", data_dir)
logger.info(f"Chain: {chain}") logger.info("Chain: %s", chain)
if not os.path.exists(data_dir): if not os.path.exists(data_dir):
os.makedirs(data_dir) os.makedirs(data_dir)
logger.info(os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n") with open(os.path.join(data_dir, "basicswap.log"), "a") as fp:
fail_code = runClient(data_dir, chain, start_only_coins, log_prefix) logger.info(
os.path.basename(sys.argv[0]) + ", version: " + __version__ + "\n\n"
)
runClient(fp, data_dir, chain, start_only_coins)
print("Done.") print("Done.")
return fail_code return swap_client.fail_code if swap_client is not None else 0
if __name__ == "__main__": if __name__ == "__main__":

View File

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

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- 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 # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -40,6 +40,13 @@ DOGECOIND = os.getenv("DOGECOIND", "dogecoind" + bin_suffix)
DOGECOIN_CLI = os.getenv("DOGECOIN_CLI", "dogecoin-cli" + bin_suffix) DOGECOIN_CLI = os.getenv("DOGECOIN_CLI", "dogecoin-cli" + bin_suffix)
DOGECOIN_TX = os.getenv("DOGECOIN_TX", "dogecoin-tx" + 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( XMR_BINDIR = os.path.expanduser(
os.getenv("XMR_BINDIR", os.path.join(DEFAULT_TEST_BINDIR, "monero")) 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.""" """Helpful routines for regression testing."""
from base64 import b64encode from base64 import b64encode
from binascii import unhexlify
from decimal import Decimal, ROUND_DOWN from decimal import Decimal, ROUND_DOWN
from subprocess import CalledProcessError from subprocess import CalledProcessError
import hashlib
import inspect import inspect
import json import json
import logging import logging
import os import os
import pathlib import random
import platform
import re import re
import time import time
from . import coverage from . import coverage
from .authproxy import AuthServiceProxy, JSONRPCException from .authproxy import AuthServiceProxy, JSONRPCException
from collections.abc import Callable from io import BytesIO
from typing import Optional
logger = logging.getLogger("TestFramework.utils") logger = logging.getLogger("TestFramework.utils")
@@ -30,46 +28,23 @@ logger = logging.getLogger("TestFramework.utils")
def assert_approx(v, vexp, vspan=0.00001): def assert_approx(v, vexp, vspan=0.00001):
"""Assert that `v` is within `vspan` of `vexp`""" """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: if v < vexp - vspan:
raise AssertionError("%s < [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan))) raise AssertionError("%s < [%s..%s]" % (str(v), str(vexp - vspan), str(vexp + vspan)))
if v > vexp + vspan: if v > vexp + vspan:
raise AssertionError("%s > [%s..%s]" % (str(v), str(vexp - vspan), str(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): def assert_fee_amount(fee, tx_size, fee_per_kB):
"""Assert the fee is in range.""" """Assert the fee was in range"""
assert isinstance(tx_size, int) target_fee = round(tx_size * fee_per_kB / 1000, 8)
target_fee = get_fee(tx_size, feerate_BTC_kvB)
if fee < target_fee: if fee < target_fee:
raise AssertionError("Fee of %s BTC too low! (Should be %s BTC)" % (str(fee), str(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 # allow the wallet's estimation to be at most 2 bytes off
high_fee = get_fee(tx_size + 2, feerate_BTC_kvB) if fee > (tx_size + 2) * fee_per_kB / 1000:
if fee > high_fee:
raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)" % (str(fee), str(target_fee))) 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): 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): if thing1 != thing2 or any(thing1 != arg for arg in args):
raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + 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") 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. """Execute a process and asserts the process return code and output.
Calls function `fun` with arguments `args` and `kwds`. Catches a CalledProcessError 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. no CalledProcessError was raised or if the return code and output are not as expected.
Args: Args:
returncode: the process return code. returncode (int): the process return code.
output: [a substring of] the process output. output (string): [a substring of] the process output.
fun: the function to call. This should execute a process. fun (function): the function to call. This should execute a process.
args*: positional arguments for the function. args*: positional arguments for the function.
kwds**: named 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") 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. """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 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. no JSONRPCException was raised or if the error code/message are not as expected.
Args: Args:
code: the error code returned by the RPC call (defined in src/rpc/protocol.h). code (int), optional: the error code returned by the RPC call (defined
Set to None if checking the error code is not required. 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. message (string), optional: [a substring of] the error string returned by the
Set to None if checking the error string is not required. 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. fun (function): the function to call. This should be the name of an RPC.
args*: positional arguments for the function. args*: positional arguments for the function.
kwds**: named 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") 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): def count_bytes(hex_string):
return len(bytearray.fromhex(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): def str_to_b64str(string):
return b64encode(string.encode('utf-8')).decode('ascii') 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): def satoshi_round(amount):
return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) 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): def wait_until(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.
"""
if attempts == float('inf') and timeout == float('inf'): if attempts == float('inf') and timeout == float('inf'):
timeout = 60 timeout = 60
timeout = timeout * timeout_factor timeout = timeout * timeout_factor
@@ -294,16 +253,6 @@ def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=floa
raise RuntimeError('Unreachable') 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 # RPC/P2P connection constants and functions
############################################ ############################################
@@ -320,15 +269,15 @@ class PortSeed:
n = None 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: Args:
url: URL of the RPC server to call url (str): URL of the RPC server to call
node_number: the node number (or id) that this calls to node_number (int): the node number (or id) that this calls to
Kwargs: Kwargs:
timeout: HTTP timeout in seconds timeout (int): HTTP timeout in seconds
coveragedir: Directory coveragedir (str): Directory
Returns: Returns:
AuthServiceProxy. convenience object for making RPC calls. 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_kwargs['timeout'] = int(timeout)
proxy = AuthServiceProxy(url, **proxy_kwargs) 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 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): 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) datadir = get_datadir_path(dirname, n)
if not os.path.isdir(datadir): if not os.path.isdir(datadir):
os.makedirs(datadir) os.makedirs(datadir)
write_config(os.path.join(datadir, "particl.conf"), n=n, chain=chain, disable_autoconnect=disable_autoconnect) # Translate chain name to config name
os.makedirs(os.path.join(datadir, 'stderr'), exist_ok=True) if chain == 'testnet3':
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':
chain_name_conf_arg = 'testnet' chain_name_conf_arg = 'testnet'
chain_name_conf_section = 'test' chain_name_conf_section = 'test'
else: else:
chain_name_conf_arg = chain chain_name_conf_arg = chain
chain_name_conf_section = chain chain_name_conf_section = chain
with open(config_path, 'w', encoding='utf8') as f: with open(os.path.join(datadir, "particl.conf"), 'w', encoding='utf8') as f:
if chain_name_conf_arg:
f.write("{}=1\n".format(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)) f.write("[{}]\n".format(chain_name_conf_section))
f.write("port=" + str(p2p_port(n)) + "\n") f.write("port=" + str(p2p_port(n)) + "\n")
f.write("rpcport=" + str(rpc_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("fallbackfee=0.0002\n")
f.write("server=1\n") f.write("server=1\n")
f.write("keypool=1\n") f.write("keypool=1\n")
f.write("discover=0\n") f.write("discover=0\n")
f.write("dnsseed=0\n") f.write("dnsseed=0\n")
f.write("fixedseeds=0\n")
f.write("listenonion=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("printtoconsole=0\n")
f.write("upnp=0\n") f.write("upnp=0\n")
f.write("natpmp=0\n")
f.write("shrinkdebugfile=0\n") f.write("shrinkdebugfile=0\n")
f.write("deprecatedrpc=create_bdb\n") # Required to run the tests os.makedirs(os.path.join(datadir, 'stderr'), exist_ok=True)
# To improve SQLite wallet performance so that the tests don't timeout, use -unsafesqlitesync os.makedirs(os.path.join(datadir, 'stdout'), exist_ok=True)
f.write("unsafesqlitesync=1\n") return datadir
if disable_autoconnect:
f.write("connect=0\n")
f.write(extra_config)
def get_datadir_path(dirname, n): def get_datadir_path(dirname, n):
return pathlib.Path(dirname) / f"node{n}" return os.path.join(dirname, "node" + str(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
def append_config(datadir, options): def append_config(datadir, options):
@@ -483,7 +395,7 @@ def delete_cookie_file(datadir, chain):
def softfork_active(node, key): def softfork_active(node, key):
"""Return whether a softfork is active.""" """Return whether a softfork is active."""
return node.getdeploymentinfo()['deployments'][key]['active'] return node.getblockchaininfo()['softforks'][key]['active']
def set_node_times(nodes, t): def set_node_times(nodes, t):
@@ -491,51 +403,208 @@ def set_node_times(nodes, t):
node.setmocktime(t) node.setmocktime(t)
def check_node_connections(*, node, num_in, num_out): def disconnect_nodes(from_connection, node_num):
info = node.getnetworkinfo() def get_peer_ids():
assert_equal(info["connections_in"], num_in) result = []
assert_equal(info["connections_out"], num_out) 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 # 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 # Create large OP_RETURN txouts that can be appended to a transaction
# to make it large (helper for constructing large transactions). The # to make it large (helper for constructing large transactions).
# total serialized size of the txouts is about 66k vbytes.
def gen_return_txouts(): 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 .messages import CTxOut
from .script import CScript, OP_RETURN txout = CTxOut()
txouts = [CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN, b'\x01'*67437]))] txout.nValue = 0
assert_equal(sum([len(txout.serialize()) for txout in txouts]), 67456) txout.scriptPubKey = hex_str_to_bytes(script_pubkey)
for k in range(128):
txouts.append(txout)
return txouts return txouts
# Create a spend of each passed-in utxo, splicing in "txouts" to each raw # Create a spend of each passed-in utxo, splicing in "txouts" to each raw
# transaction to make it large. See gen_return_txouts() above. # 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 = [] txids = []
use_internal_utxos = utxos is None from .messages import CTransaction
for _ in range(tx_batch_size): for _ in range(num):
tx = mini_wallet.create_self_transfer( t = utxos.pop()
utxo_to_spend=None if use_internal_utxos else utxos.pop(), inputs = [{"txid": t["txid"], "vout": t["vout"]}]
fee=fee, outputs = {}
)["tx"] change = t['amount'] - fee
tx.vout.extend(txouts) outputs[addr] = satoshi_round(change)
res = node.testmempoolaccept([tx.serialize().hex()])[0] rawtx = node.createrawtransaction(inputs, outputs)
assert_equal(res['fees']['base'], fee) tx = CTransaction()
txids.append(node.sendrawtransaction(tx.serialize().hex())) 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 return txids
def mine_large_block(test_framework, mini_wallet, node): def mine_large_block(node, utxos=None):
# generate a 66k transaction, # generate a 66k transaction,
# and 14 of them is close to the 1MB block limit # and 14 of them is close to the 1MB block limit
num = 14
txouts = gen_return_txouts() 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"] fee = 100 * node.getnetworkinfo()["relayfee"]
create_lots_of_big_transactions(mini_wallet, node, fee, 14, txouts) create_lots_of_big_transactions(node, txouts, utxos, num, fee=fee)
test_framework.generate(node, 1) node.generate(1)
def find_vout_for_address(node, txid, addr): 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) tx = node.getrawtransaction(txid, True)
for i in range(len(tx["vout"])): for i in range(len(tx["vout"])):
if addr == tx["vout"][i]["scriptPubKey"]["address"]: scriptPubKey = tx["vout"][i]["scriptPubKey"]
if "addresses" in scriptPubKey:
if any([addr == a for a in scriptPubKey["addresses"]]):
return i
elif "address" in scriptPubKey:
if addr == scriptPubKey["address"]:
return i return i
raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr)) raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr))

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert # Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -13,8 +13,8 @@ from enum import IntEnum, auto
from typing import Optional from typing import Optional
CURRENT_DB_VERSION = 28 CURRENT_DB_VERSION = 25
CURRENT_DB_DATA_VERSION = 6 CURRENT_DB_DATA_VERSION = 5
class Concepts(IntEnum): class Concepts(IntEnum):
@@ -174,7 +174,6 @@ class Offer(Table):
secret_hash = Column("blob") secret_hash = Column("blob")
addr_from = Column("string") addr_from = Column("string")
pk_from = Column("blob")
addr_to = Column("string") addr_to = Column("string")
created_at = Column("integer") created_at = Column("integer")
expire_at = Column("integer") expire_at = Column("integer")
@@ -184,7 +183,6 @@ class Offer(Table):
amount_negotiable = Column("bool") amount_negotiable = Column("bool")
rate_negotiable = Column("bool") rate_negotiable = Column("bool")
auto_accept_type = Column("integer")
# Local fields # Local fields
auto_accept_bids = Column("bool") auto_accept_bids = Column("bool")
@@ -217,7 +215,6 @@ class Bid(Table):
created_at = Column("integer") created_at = Column("integer")
expire_at = Column("integer") expire_at = Column("integer")
bid_addr = Column("string") bid_addr = Column("string")
pk_bid_addr = Column("blob")
proof_address = Column("string") proof_address = Column("string")
proof_utxos = Column("blob") proof_utxos = Column("blob")
# Address to spend lock tx to - address from wallet if empty TODO # Address to spend lock tx to - address from wallet if empty TODO
@@ -647,17 +644,6 @@ class CheckedBlock(Table):
block_time = Column("integer") 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: def create_db(db_path: str, log) -> None:
con = None con = None
try: try:
@@ -929,12 +915,15 @@ class DBMethods:
table_name: str = table_class.__tablename__ table_name: str = table_class.__tablename__
query: str = "SELECT " query: str = "SELECT "
columns = [] columns = []
for mc in inspect.getmembers(table_class): for mc in inspect.getmembers(table_class):
mc_name, mc_obj = mc mc_name, mc_obj = mc
if not hasattr(mc_obj, "__sqlite3_column__"): if not hasattr(mc_obj, "__sqlite3_column__"):
continue continue
if len(columns) > 0: if len(columns) > 0:
query += ", " query += ", "
query += mc_name query += mc_name
@@ -942,29 +931,10 @@ class DBMethods:
query += f" FROM {table_name} WHERE 1=1 " query += f" FROM {table_name} WHERE 1=1 "
query_data = {}
for ck in constraints: for ck in constraints:
if not validColumnName(ck): if not validColumnName(ck):
raise ValueError(f"Invalid constraint column: {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 += f" AND {ck} = :{ck} "
query_data[ck] = constraint_value
for order_col, order_dir in order_by.items(): for order_col, order_dir in order_by.items():
if validColumnName(order_col) is False: if validColumnName(order_col) is False:
@@ -977,6 +947,7 @@ class DBMethods:
if query_suffix: if query_suffix:
query += query_suffix query += query_suffix
query_data = constraints.copy()
query_data.update(extra_query_data) query_data.update(extra_query_data)
rows = cursor.execute(query, query_data) rows = cursor.execute(query, query_data)
for row in rows: for row in rows:

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2022-2024 tecnovert # 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 # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -104,19 +104,17 @@ def upgradeDatabaseData(self, data_version):
), ),
cursor, cursor,
) )
if data_version > 0 and data_version < 6: if data_version > 0 and data_version < 3:
for state in BidStates: for state in BidStates:
in_error = isErrorBidState(state) in_error = isErrorBidState(state)
swap_failed = isFailingBidState(state) swap_failed = isFailingBidState(state)
swap_ended = isFinalBidState(state) swap_ended = isFinalBidState(state)
can_accept = canAcceptBidState(state)
cursor.execute( 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, "in_error": in_error,
"swap_failed": swap_failed, "swap_failed": swap_failed,
"swap_ended": swap_ended, "swap_ended": swap_ended,
"can_accept": can_accept,
"state_id": int(state), "state_id": int(state),
}, },
) )
@@ -412,27 +410,6 @@ def upgradeDatabase(self, db_version):
elif current_version == 24: elif current_version == 24:
db_version += 1 db_version += 1
cursor.execute("ALTER TABLE bidstates ADD COLUMN can_accept INTEGER") 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: if current_version != db_version:
self.db_version = db_version self.db_version = db_version
self.setIntKV("db_version", db_version, cursor) self.setIntKV("db_version", db_version, cursor)

View File

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

View File

@@ -1,25 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2019-2024 tecnovert # Copyright (c) 2019-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os import os
import json import json
import shlex import shlex
import secrets
import traceback import traceback
import threading import threading
import http.client import http.client
import base64 from urllib import parse
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
from jinja2 import Environment, PackageLoader 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 . import __version__
from .util import ( from .util import (
@@ -36,7 +30,6 @@ from .basicswap_util import (
strTxState, strTxState,
strBidState, strBidState,
) )
from .util.rfc2440 import verify_rfc2440_password
from .js_server import ( from .js_server import (
js_error, js_error,
@@ -64,9 +57,6 @@ from .ui.page_identity import page_identity
from .ui.page_smsgaddresses import page_smsgaddresses from .ui.page_smsgaddresses import page_smsgaddresses
from .ui.page_debug import page_debug from .ui.page_debug import page_debug
SESSION_COOKIE_NAME = "basicswap_session_id"
SESSION_DURATION_MINUTES = 60
env = Environment(loader=PackageLoader("basicswap", "templates")) env = Environment(loader=PackageLoader("basicswap", "templates"))
env.filters["formatts"] = format_timestamp env.filters["formatts"] = format_timestamp
@@ -129,57 +119,6 @@ def parse_cmd(cmd: str, type_map: str):
class HttpHandler(BaseHTTPRequestHandler): 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): def log_error(self, format, *args):
super().log_message(format, *args) super().log_message(format, *args)
@@ -202,12 +141,7 @@ class HttpHandler(BaseHTTPRequestHandler):
return form_data return form_data
def render_template( def render_template(
self, self, template, args_dict, status_code=200, version=__version__
template,
args_dict,
status_code=200,
version=__version__,
extra_headers=None,
): ):
swap_client = self.server.swap_client swap_client = self.server.swap_client
if swap_client.ws_server: if swap_client.ws_server:
@@ -218,6 +152,7 @@ class HttpHandler(BaseHTTPRequestHandler):
args_dict["debug_ui_mode"] = True args_dict["debug_ui_mode"] = True
if swap_client.use_tor_proxy: if swap_client.use_tor_proxy:
args_dict["use_tor_proxy"] = True args_dict["use_tor_proxy"] = True
# TODO: Cache value?
try: try:
tor_state = get_tor_established_state(swap_client) tor_state = get_tor_established_state(swap_client)
args_dict["tor_established"] = True if tor_state == "1" else False args_dict["tor_established"] = True if tor_state == "1" else False
@@ -243,16 +178,6 @@ class HttpHandler(BaseHTTPRequestHandler):
self.server.msg_id_counter += 1 self.server.msg_id_counter += 1
args_dict["err_messages"] = err_messages_with_ids 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() shutdown_token = os.urandom(8).hex()
self.server.session_tokens["shutdown"] = shutdown_token self.server.session_tokens["shutdown"] = shutdown_token
args_dict["shutdown_token"] = shutdown_token args_dict["shutdown_token"] = shutdown_token
@@ -266,7 +191,7 @@ class HttpHandler(BaseHTTPRequestHandler):
args_dict["version"] = version args_dict["version"] = version
self.putHeaders(status_code, "text/html", extra_headers=extra_headers) self.putHeaders(status_code, "text/html")
return bytes( return bytes(
template.render( template.render(
title=self.server.title, title=self.server.title,
@@ -278,7 +203,6 @@ class HttpHandler(BaseHTTPRequestHandler):
) )
def render_simple_template(self, template, args_dict): def render_simple_template(self, template, args_dict):
self.putHeaders(200, "text/html")
return bytes( return bytes(
template.render( template.render(
title=self.server.title, title=self.server.title,
@@ -287,7 +211,7 @@ class HttpHandler(BaseHTTPRequestHandler):
"UTF-8", "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") template = env.get_template("info.html")
swap_client = self.server.swap_client swap_client = self.server.swap_client
summary = swap_client.getSummary() summary = swap_client.getSummary()
@@ -298,7 +222,6 @@ class HttpHandler(BaseHTTPRequestHandler):
"message_str": info_str, "message_str": info_str,
"summary": summary, "summary": summary,
}, },
extra_headers=extra_headers,
) )
def page_error(self, error_str, post_string=None): 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): def page_explorers(self, url_split, post_string):
swap_client = self.server.swap_client swap_client = self.server.swap_client
swap_client.checkSystemStatus() swap_client.checkSystemStatus()
@@ -414,10 +250,14 @@ class HttpHandler(BaseHTTPRequestHandler):
form_data = self.checkForm(post_string, "explorers", err_messages) form_data = self.checkForm(post_string, "explorers", err_messages)
if form_data: if form_data:
explorer = get_data_entry(form_data, "explorer") explorer = form_data[b"explorer"][0].decode("utf-8")
action = get_data_entry(form_data, "action") action = form_data[b"action"][0].decode("utf-8")
args = get_data_entry_or(form_data, "args", "")
args = (
""
if b"args" not in form_data
else form_data[b"args"][0].decode("utf-8")
)
try: try:
c, e = explorer.split("_") c, e = explorer.split("_")
exp = swap_client.coin_clients[Coins(int(c))]["explorers"][int(e)] exp = swap_client.coin_clients[Coins(int(c))]["explorers"][int(e)]
@@ -570,6 +410,7 @@ class HttpHandler(BaseHTTPRequestHandler):
return self.render_template( return self.render_template(
template, template,
{ {
"refresh": 30,
"active_swaps": [ "active_swaps": [
( (
s[0].hex(), s[0].hex(),
@@ -606,7 +447,6 @@ class HttpHandler(BaseHTTPRequestHandler):
def page_shutdown(self, url_split, post_string): def page_shutdown(self, url_split, post_string):
swap_client = self.server.swap_client swap_client = self.server.swap_client
extra_headers = []
if len(url_split) > 2: if len(url_split) > 2:
token = url_split[2] token = url_split[2]
@@ -614,15 +454,9 @@ class HttpHandler(BaseHTTPRequestHandler):
if token != expect_token: if token != expect_token:
return self.page_info("Unexpected token, still running.") 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() 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): def page_index(self, url_split):
swap_client = self.server.swap_client 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) self.send_response(status_code)
if self.server.allow_cors: if self.server.allow_cors:
self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Content-Type", content_type) 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() self.end_headers()
def handle_http(self, status_code, path, post_string="", is_json=False): def handle_http(self, status_code, path, post_string="", is_json=False):
swap_client = self.server.swap_client swap_client = self.server.swap_client
parsed = parse.urlparse(self.path) parsed = parse.urlparse(self.path)
url_split = parsed.path.split("/") url_split = parsed.path.split("/")
page = url_split[1] if len(url_split) > 1 else "" if post_string == "" and len(parsed.query) > 0:
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:
post_string = parsed.query post_string = parsed.query
if len(url_split) > 1 and url_split[1] == "json":
if page == "json":
try: try:
self.putHeaders(status_code, "json") self.putHeaders(status_code, "text/plain")
func = js_url_to_function(url_split) func = js_url_to_function(url_split)
return func(self, url_split, post_string, is_json) return func(self, url_split, post_string, is_json)
except Exception as ex: except Exception as ex:
@@ -725,20 +500,18 @@ class HttpHandler(BaseHTTPRequestHandler):
swap_client.log.error(traceback.format_exc()) swap_client.log.error(traceback.format_exc())
return js_error(self, str(ex)) return js_error(self, str(ex))
if page == "static": if len(url_split) > 1 and url_split[1] == "static":
try: try:
static_path = os.path.join(os.path.dirname(__file__), "static") 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": if len(url_split) > 3 and url_split[2] == "sequence_diagrams":
filepath = os.path.join( with open(
static_path, "sequence_diagrams", url_split[3] os.path.join(static_path, "sequence_diagrams", url_split[3]),
) "rb",
mime_type = "image/svg+xml" ) as fp:
self.putHeaders(status_code, "image/svg+xml")
return fp.read()
elif len(url_split) > 3 and url_split[2] == "images": elif len(url_split) > 3 and url_split[2] == "images":
filename = os.path.join(*url_split[3:]) filename = os.path.join(*url_split[3:])
filepath = os.path.join(static_path, "images", filename)
_, extension = os.path.splitext(filename) _, extension = os.path.splitext(filename)
mime_type = { mime_type = {
".svg": "image/svg+xml", ".svg": "image/svg+xml",
@@ -747,25 +520,25 @@ class HttpHandler(BaseHTTPRequestHandler):
".gif": "image/gif", ".gif": "image/gif",
".ico": "image/x-icon", ".ico": "image/x-icon",
}.get(extension, "") }.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": elif len(url_split) > 3 and url_split[2] == "css":
filename = os.path.join(*url_split[3:]) filename = os.path.join(*url_split[3:])
filepath = os.path.join(static_path, "css", filename) with open(os.path.join(static_path, "css", filename), "rb") as fp:
mime_type = "text/css; charset=utf-8" self.putHeaders(status_code, "text/css; charset=utf-8")
return fp.read()
elif len(url_split) > 3 and url_split[2] == "js": elif len(url_split) > 3 and url_split[2] == "js":
filename = os.path.join(*url_split[3:]) filename = os.path.join(*url_split[3:])
filepath = os.path.join(static_path, "js", filename) with open(os.path.join(static_path, "js", filename), "rb") as fp:
mime_type = "application/javascript" self.putHeaders(status_code, "application/javascript")
return fp.read()
else: else:
return self.page_404(url_split) 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: except FileNotFoundError:
return self.page_404(url_split) return self.page_404(url_split)
except Exception as ex: except Exception as ex:
@@ -777,8 +550,6 @@ class HttpHandler(BaseHTTPRequestHandler):
if len(url_split) > 1: if len(url_split) > 1:
page = url_split[1] page = url_split[1]
if page == "login":
return self.page_login(url_split, post_string)
if page == "active": if page == "active":
return self.page_active(url_split, post_string) return self.page_active(url_split, post_string)
if page == "wallets": if page == "wallets":
@@ -845,21 +616,14 @@ class HttpHandler(BaseHTTPRequestHandler):
def do_GET(self): def do_GET(self):
response = self.handle_http(200, self.path) response = self.handle_http(200, self.path)
try:
self.wfile.write(response) self.wfile.write(response)
except SocketError as e:
self.server.swap_client.log.debug(f"do_GET SocketError {e}")
def do_POST(self): def do_POST(self):
content_length = int(self.headers.get("Content-Length", 0)) post_string = self.rfile.read(int(self.headers.get("Content-Length")))
post_string = self.rfile.read(content_length)
is_json = True if "json" in self.headers.get("Content-Type", "") else False is_json = True if "json" in self.headers.get("Content-Type", "") else False
response = self.handle_http(200, self.path, post_string, is_json) response = self.handle_http(200, self.path, post_string, is_json)
try:
self.wfile.write(response) self.wfile.write(response)
except SocketError as e:
self.server.swap_client.log.debug(f"do_POST SocketError {e}")
def do_HEAD(self): def do_HEAD(self):
self.putHeaders(200, "text/html") self.putHeaders(200, "text/html")
@@ -873,10 +637,11 @@ class HttpHandler(BaseHTTPRequestHandler):
class HttpThread(threading.Thread, HTTPServer): 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) threading.Thread.__init__(self)
self.stop_event = threading.Event() self.stop_event = threading.Event()
self.fp = fp
self.host_name = host_name self.host_name = host_name
self.port_no = port_no self.port_no = port_no
self.allow_cors = allow_cors self.allow_cors = allow_cors
@@ -884,7 +649,6 @@ class HttpThread(threading.Thread, HTTPServer):
self.title = "BasicSwap - " + __version__ self.title = "BasicSwap - " + __version__
self.last_form_id = dict() self.last_form_id = dict()
self.session_tokens = dict() self.session_tokens = dict()
self.active_sessions = {}
self.env = env self.env = env
self.msg_id_counter = 0 self.msg_id_counter = 0
@@ -894,19 +658,18 @@ class HttpThread(threading.Thread, HTTPServer):
def stop(self): def stop(self):
self.stop_event.set() self.stop_event.set()
try: # Send fake request
conn = http.client.HTTPConnection(self.host_name, self.port_no, timeout=0.5) conn = http.client.HTTPConnection(self.host_name, self.port_no)
conn.request("GET", "/shutdown_ping") conn.connect()
conn.request("GET", "/none")
response = conn.getresponse()
_ = response.read()
conn.close() conn.close()
except Exception:
pass
def serve_forever(self): def serve_forever(self):
self.timeout = 1
while not self.stop_event.is_set(): while not self.stop_event.is_set():
self.handle_request() self.handle_request()
self.socket.close() self.socket.close()
self.swap_client.log.info("HTTP server stopped.")
def run(self): def run(self):
self.serve_forever() self.serve_forever()

View File

@@ -106,31 +106,6 @@ class BCHInterface(BTCInterface):
) + self.make_int(u["amount"], r=1) ) + self.make_int(u["amount"], r=1)
return unspent_addr 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 # returns pkh
def decodeAddress(self, address: str) -> bytes: def decodeAddress(self, address: str) -> bytes:
return bytes(Address.from_string(address).payload) return bytes(Address.from_string(address).payload)

View File

@@ -10,13 +10,8 @@ import base64
import hashlib import hashlib
import json import json
import logging import logging
import mmap
import os
import shutil
import sqlite3
import traceback import traceback
from io import BytesIO from io import BytesIO
from basicswap.basicswap_util import ( from basicswap.basicswap_util import (
@@ -382,7 +377,7 @@ class BTCInterface(Secp256k1Interface):
last_block_header = prev_block_header last_block_header = prev_block_header
raise ValueError(f"Block header not found at time: {time}") 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 assert len(key_bytes) == 32
self._have_checked_seed = False self._have_checked_seed = False
if self._use_descriptors: if self._use_descriptors:
@@ -392,7 +387,6 @@ class BTCInterface(Secp256k1Interface):
ek_encoded: str = self.encode_secret_extkey(ek.encode_v()) ek_encoded: str = self.encode_secret_extkey(ek.encode_v())
desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)") desc_external = descsum_create(f"wpkh({ek_encoded}/0h/0h/*h)")
desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)") desc_internal = descsum_create(f"wpkh({ek_encoded}/0h/1h/*h)")
rv = self.rpc_wallet( rv = self.rpc_wallet(
"importdescriptors", "importdescriptors",
[ [
@@ -400,7 +394,7 @@ class BTCInterface(Secp256k1Interface):
{"desc": desc_external, "timestamp": "now", "active": True}, {"desc": desc_external, "timestamp": "now", "active": True},
{ {
"desc": desc_internal, "desc": desc_internal,
"timestamp": "now" if restore_time == -1 else restore_time, "timestamp": "now",
"active": True, "active": True,
"internal": True, "internal": True,
}, },
@@ -417,18 +411,7 @@ class BTCInterface(Secp256k1Interface):
raise ValueError("Failed to import descriptors.") raise ValueError("Failed to import descriptors.")
else: else:
key_wif = self.encodeKey(key_bytes) key_wif = self.encodeKey(key_bytes)
try:
self.rpc_wallet("sethdseed", [True, key_wif]) self.rpc_wallet("sethdseed", [True, key_wif])
except Exception as e:
self._log.debug(f"sethdseed failed: {e}")
"""
# TODO: Find derived key counts
if "Already have this key" in str(e):
key_id: bytes = self.getSeedHash(key_bytes)
self.setActiveKeyChain(key_id)
else:
"""
raise (e)
def getWalletInfo(self): def getWalletInfo(self):
rv = self.rpc_wallet("getwalletinfo") rv = self.rpc_wallet("getwalletinfo")
@@ -472,6 +455,10 @@ class BTCInterface(Secp256k1Interface):
self.close_rpc(rpc_conn) self.close_rpc(rpc_conn)
raise ValueError(f"{self.coin_name()} wallet restore height not found.") 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): def getActiveDescriptor(self):
descriptors = self.rpc_wallet("listdescriptors")["descriptors"] descriptors = self.rpc_wallet("listdescriptors")["descriptors"]
for descriptor in descriptors: for descriptor in descriptors:
@@ -483,24 +470,21 @@ class BTCInterface(Secp256k1Interface):
return descriptor return descriptor
return None return None
def getWalletSeedID(self) -> str: def checkExpectedSeed(self, expect_seedid: str) -> bool:
if self._use_descriptors: if self._use_descriptors:
descriptor = self.getActiveDescriptor() descriptor = self.getActiveDescriptor()
if descriptor is None: if descriptor is None:
self._log.debug("Could not find active descriptor.") self._log.debug("Could not find active descriptor.")
return "Not found" return False
end = descriptor["desc"].find("/") end = descriptor["desc"].find("/")
if end < 10: if end < 10:
return "Not found" return False
extkey = descriptor["desc"][5:end] extkey = descriptor["desc"][5:end]
extkey_data = b58decode(extkey)[4:-4] extkey_data = b58decode(extkey)[4:-4]
extkey_data_hash: bytes = hash160(extkey_data) 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() wallet_seed_id = self.getWalletSeedID()
self._expect_seedid_hex = expect_seedid self._expect_seedid_hex = expect_seedid
self._have_checked_seed = True self._have_checked_seed = True
@@ -1878,69 +1862,19 @@ class BTCInterface(Secp256k1Interface):
"Could not find address with enough funds for proof", "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 ( if (
self.using_segwit() self.using_segwit()
): # TODO: Use isSegwitAddress when scantxoutset can use combo ): # TODO: Use isSegwitAddress when scantxoutset can use combo
# 'Address does not refer to key' for non p2pkh # 'Address does not refer to key' for non p2pkh
pkh = self.decodeAddress(sign_for_addr) pkh = self.decodeAddress(sign_for_addr)
sign_for_addr = self.pkh_to_address(pkh) 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( signature = self.rpc_wallet(
"signmessage", "signmessage",
[ [sign_for_addr, sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex()],
sign_for_addr,
sign_for_addr + "_swap_proof_" + extra_commit_bytes.hex(),
],
) )
prove_utxos = [] # TODO: Send specific utxos prove_utxos = [] # TODO: Send specific utxos
@@ -1994,237 +1928,15 @@ class BTCInterface(Secp256k1Interface):
locked = encrypted and wallet_info["unlocked_until"] <= 0 locked = encrypted and wallet_info["unlocked_until"] <= 0
return encrypted, locked return encrypted, locked
def createWallet(self, wallet_name: str, password: str = "") -> None: def changeWalletPassword(self, old_password: str, new_password: str):
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
):
self._log.info("changeWalletPassword - {}".format(self.ticker())) self._log.info("changeWalletPassword - {}".format(self.ticker()))
if old_password == "": if old_password == "":
if self.isWalletEncrypted(): if self.isWalletEncrypted():
raise ValueError("Old password must be set") 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]) 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 == "": if password == "":
return return
self._log.info(f"unlockWallet - {self.ticker()}") self._log.info(f"unlockWallet - {self.ticker()}")
@@ -2239,20 +1951,12 @@ class BTCInterface(Secp256k1Interface):
) )
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors # wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
self.rpc( self.rpc(
"createwallet", "createwallet", [self._rpc_wallet, False, True, "", False, False]
[
self._rpc_wallet,
False,
True,
password,
False,
self._use_descriptors,
],
) )
self.rpc_wallet("encryptwallet", [password])
# Max timeout value, ~3 years # Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000]) self.rpc_wallet("walletpassphrase", [password, 100000000])
if check_seed:
self._sc.checkWalletSeed(self.coin_type()) self._sc.checkWalletSeed(self.coin_type())
def lockWallet(self): def lockWallet(self):

View File

@@ -47,7 +47,7 @@ class DASHInterface(BTCInterface):
def entropyToMnemonic(self, key: bytes) -> None: def entropyToMnemonic(self, key: bytes) -> None:
return Mnemonic("english").to_mnemonic(key) 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 self._have_checked_seed = False
if self._wallet_v20_compatible: if self._wallet_v20_compatible:
self._log.warning("Generating wallet compatible with v20 seed.") self._log.warning("Generating wallet compatible with v20 seed.")
@@ -66,11 +66,7 @@ class DASHInterface(BTCInterface):
def checkExpectedSeed(self, expect_seedid: str) -> bool: def checkExpectedSeed(self, expect_seedid: str) -> bool:
self._expect_seedid_hex = expect_seedid self._expect_seedid_hex = expect_seedid
try:
rv = self.rpc_wallet("dumphdinfo") rv = self.rpc_wallet("dumphdinfo")
except Exception as e:
self._log.debug(f"DASH dumphdinfo failed {e}.")
return False
if rv["mnemonic"] != "": if rv["mnemonic"] != "":
entropy = Mnemonic("english").to_entropy(rv["mnemonic"].split(" ")) entropy = Mnemonic("english").to_entropy(rv["mnemonic"].split(" "))
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex() entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
@@ -115,45 +111,18 @@ class DASHInterface(BTCInterface):
return None return None
def unlockWallet(self, password: str, check_seed: bool = True) -> None: def unlockWallet(self, password: str):
super().unlockWallet(password, check_seed) super().unlockWallet(password)
if self._wallet_v20_compatible: if self._wallet_v20_compatible:
# Store password for initialiseWallet # Store password for initialiseWallet
self._wallet_passphrase = password 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): def lockWallet(self):
super().lockWallet() super().lockWallet()
self._wallet_passphrase = "" 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: def testDaemonRPC(self, with_wallet=True) -> None:
if with_wallet: if with_wallet:
self.rpc_wallet("walletislocked") self.rpc_wallet("getinfo")
else: else:
self.rpc("getblockchaininfo") self.rpc("getblockchaininfo")
def getChainHeight(self) -> int: def getChainHeight(self) -> int:
return self.rpc("getblockcount") return self.rpc("getblockcount")
def initialiseWallet(self, key: bytes, restore_time: int = -1) -> None: def initialiseWallet(self, key: bytes) -> None:
# Load with --create # Load with --create
pass pass
@@ -354,9 +354,7 @@ class DCRInterface(Secp256k1Interface):
walletislocked = self.rpc_wallet("walletislocked") walletislocked = self.rpc_wallet("walletislocked")
return True, walletislocked return True, walletislocked
def changeWalletPassword( def changeWalletPassword(self, old_password: str, new_password: str):
self, old_password: str, new_password: str, check_seed_if_encrypt: bool = True
):
self._log.info("changeWalletPassword - {}".format(self.ticker())) self._log.info("changeWalletPassword - {}".format(self.ticker()))
if old_password == "": if old_password == "":
# Read initial pwd from settings # Read initial pwd from settings
@@ -370,14 +368,13 @@ class DCRInterface(Secp256k1Interface):
# Clear initial password # Clear initial password
self._sc.editSettings(self.coin_name().lower(), {"wallet_pwd": ""}) 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 == "": if password == "":
return return
self._log.info("unlockWallet - {}".format(self.ticker())) self._log.info("unlockWallet - {}".format(self.ticker()))
# Max timeout value, ~3 years # Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000]) self.rpc_wallet("walletpassphrase", [password, 100000000])
if check_seed:
self._sc.checkWalletSeed(self.coin_type()) self._sc.checkWalletSeed(self.coin_type())
def lockWallet(self): def lockWallet(self):

View File

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

View File

@@ -51,32 +51,13 @@ class FIROInterface(BTCInterface):
def getExchangeName(self, exchange_name: str) -> str: def getExchangeName(self, exchange_name: str) -> str:
return "zcoin" return "zcoin"
def initialiseWallet(self, key, restore_time: int = -1): def initialiseWallet(self, key):
# load with -hdseed= parameter # load with -hdseed= parameter
pass pass
def checkWallets(self) -> int: def checkWallets(self) -> int:
return 1 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"): def getNewAddress(self, use_segwit, label="swap_receive"):
return self.rpc("getnewaddress", [label]) return self.rpc("getnewaddress", [label])
# addr_plain = 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("walletpassphrase", [password, 100000000])
self.rpc_wallet("keypoolrefill") self.rpc_wallet("keypoolrefill")
def unlockWallet(self, password: str, check_seed: bool = True) -> None: def unlockWallet(self, password: str):
if password == "": if password == "":
return return
self._log.info("unlockWallet - {}".format(self.ticker())) self._log.info("unlockWallet - {}".format(self.ticker()))
@@ -156,5 +156,5 @@ class LTCInterfaceMWEB(LTCInterface):
else: else:
# Max timeout value, ~3 years # Max timeout value, ~3 years
self.rpc_wallet("walletpassphrase", [password, 100000000]) 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 # p2sh-p2wsh
return True return True
def initialiseWallet(self, key, restore_time: int = -1): def initialiseWallet(self, key):
# Load with -importmnemonic= parameter # Load with -importmnemonic= parameter
pass pass

View File

@@ -2,7 +2,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2022 tecnovert # Copyright (c) 2020-2022 tecnovert
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -14,3 +13,39 @@ class NMCInterface(BTCInterface):
@staticmethod @staticmethod
def coin_type(): def coin_type():
return Coins.NMC 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"] return index_info["spentindex"]
def initialiseWallet(self, key: bytes, restore_time: int = -1) -> None: def initialiseWallet(self, key: bytes) -> None:
raise ValueError("TODO") raise ValueError("TODO")
def withdrawCoin(self, value, addr_to, subfee): def withdrawCoin(self, value, addr_to, subfee):

View File

@@ -34,34 +34,8 @@ class PIVXInterface(BTCInterface):
self._rpcport, self._rpcauth, host=self._rpc_host self._rpcport, self._rpcauth, host=self._rpc_host
) )
def encryptWallet(self, password: str, check_seed: bool = True): def checkWallets(self) -> int:
# Watchonly wallets are not encrypted return 1
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 signTxWithWallet(self, tx): def signTxWithWallet(self, tx):
rv = self.rpc("signrawtransaction", [tx.hex()]) rv = self.rpc("signrawtransaction", [tx.hex()])

View File

@@ -30,27 +30,3 @@ class WOWInterface(XMRInterface):
@staticmethod @staticmethod
def depth_spendable() -> int: def depth_spendable() -> int:
return 3 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. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import logging import logging
import os
import basicswap.contrib.ed25519_fast as edf import basicswap.contrib.ed25519_fast as edf
import basicswap.ed25519_fast_util as edu import basicswap.ed25519_fast_util as edu
@@ -202,57 +201,25 @@ class XMRInterface(CoinInterface):
try: try:
self.rpc_wallet("open_wallet", params) 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: except Exception as e:
if "no connection to daemon" in str(e): if "no connection to daemon" in str(e):
self._log.debug(f"{self.coin_name()} {e}") self._log.debug(f"{self.coin_name()} {e}")
return # Bypass refresh error to allow startup with a busy daemon 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: try:
# TODO Remove `store` after upstream fix to autosave on close_wallet
self.rpc_wallet("store")
self.rpc_wallet("close_wallet") self.rpc_wallet("close_wallet")
self._log.debug(f"Closing {self.coin_name()} wallet") self._log.debug(f"Attempt to save and close {self.coin_name()} wallet")
except Exception as e: # noqa: F841 except Exception as e: # noqa: F841
pass pass
self.rpc_wallet("open_wallet", params) 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( def initialiseWallet(
self, key_view: bytes, key_spend: bytes, restore_height=None self, key_view: bytes, key_spend: bytes, restore_height=None
@@ -338,8 +305,6 @@ class XMRInterface(CoinInterface):
raise e raise e
rv = {} rv = {}
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
balance_info = self.rpc_wallet("get_balance") balance_info = self.rpc_wallet("get_balance")
rv["wallet_blocks"] = self.rpc_wallet("get_height")["height"] rv["wallet_blocks"] = self.rpc_wallet("get_height")["height"]
@@ -441,8 +406,6 @@ class XMRInterface(CoinInterface):
) -> bytes: ) -> bytes:
with self._mx_wallet: with self._mx_wallet:
self.openWallet(self._wallet_filename) self.openWallet(self._wallet_filename)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
Kbv = self.getPubkey(kbv) Kbv = self.getPubkey(kbv)
shared_addr = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix) shared_addr = xmr_util.encode_address(Kbv, Kbs, self._addr_prefix)
@@ -484,9 +447,6 @@ class XMRInterface(CoinInterface):
self.createWallet(params) self.createWallet(params)
self.openWallet(address_b58) self.openWallet(address_b58)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
""" """
# Debug # Debug
try: try:
@@ -538,8 +498,6 @@ class XMRInterface(CoinInterface):
def findTxnByHash(self, txid): def findTxnByHash(self, txid):
with self._mx_wallet: with self._mx_wallet:
self.openWallet(self._wallet_filename) self.openWallet(self._wallet_filename)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
try: try:
current_height = self.rpc2("get_height", timeout=self._rpctimeout)[ current_height = self.rpc2("get_height", timeout=self._rpctimeout)[
@@ -608,8 +566,6 @@ class XMRInterface(CoinInterface):
self.createWallet(params) self.createWallet(params)
self.openWallet(wallet_filename) self.openWallet(wallet_filename)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
rv = self.rpc_wallet("get_balance") rv = self.rpc_wallet("get_balance")
if rv["balance"] < cb_swap_value: if rv["balance"] < cb_swap_value:
self._log.warning("Balance is too low, checking for existing spend.") self._log.warning("Balance is too low, checking for existing spend.")
@@ -664,8 +620,6 @@ class XMRInterface(CoinInterface):
) -> str: ) -> str:
with self._mx_wallet: with self._mx_wallet:
self.openWallet(self._wallet_filename) self.openWallet(self._wallet_filename)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
if sweepall: if sweepall:
balance = self.rpc_wallet("get_balance") balance = self.rpc_wallet("get_balance")
@@ -745,9 +699,6 @@ class XMRInterface(CoinInterface):
self.createWallet(params) self.createWallet(params)
self.openWallet(address_b58) self.openWallet(address_b58)
self.rpc_wallet("refresh")
self._log.debug(f"Refreshing {self.coin_name()} wallet")
rv = self.rpc_wallet( rv = self.rpc_wallet(
"get_transfers", "get_transfers",
{"in": True, "out": True, "pending": True, "failed": True}, {"in": True, "out": True, "pending": True, "failed": True},
@@ -760,15 +711,11 @@ class XMRInterface(CoinInterface):
def getSpendableBalance(self) -> int: def getSpendableBalance(self) -> int:
with self._mx_wallet: with self._mx_wallet:
self.openWallet(self._wallet_filename) 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") balance_info = self.rpc_wallet("get_balance")
return balance_info["unlocked_balance"] return balance_info["unlocked_balance"]
def changeWalletPassword( def changeWalletPassword(self, old_password, new_password):
self, old_password, new_password, check_seed_if_encrypt: bool = True
):
self._log.info("changeWalletPassword - {}".format(self.ticker())) self._log.info("changeWalletPassword - {}".format(self.ticker()))
orig_password = self._wallet_password orig_password = self._wallet_password
if old_password != "": if old_password != "":
@@ -783,11 +730,11 @@ class XMRInterface(CoinInterface):
self._wallet_password = orig_password self._wallet_password = orig_password
raise e 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._log.info("unlockWallet - {}".format(self.ticker()))
self._wallet_password = password 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()) self._sc.checkWalletSeed(self.coin_type())
def lockWallet(self) -> None: def lockWallet(self) -> None:

View File

@@ -14,7 +14,6 @@ from .util import (
toBool, toBool,
) )
from .basicswap_util import ( from .basicswap_util import (
fiatFromTicker,
strBidState, strBidState,
strTxState, strTxState,
SwapTypes, SwapTypes,
@@ -23,9 +22,6 @@ from .basicswap_util import (
from .chainparams import ( from .chainparams import (
Coins, Coins,
chainparams, chainparams,
Fiat,
getCoinIdFromTicker,
getCoinIdFromName,
) )
from .ui.util import ( from .ui.util import (
PAGE_LIMIT, PAGE_LIMIT,
@@ -37,6 +33,7 @@ from .ui.util import (
get_data_entry, get_data_entry,
get_data_entry_or, get_data_entry_or,
have_data_entry, have_data_entry,
tickerToCoinId,
listOldBidStates, listOldBidStates,
checkAddressesOwned, checkAddressesOwned,
) )
@@ -127,7 +124,7 @@ def js_wallets(self, url_split, post_string, is_json):
swap_client.checkSystemStatus() swap_client.checkSystemStatus()
if len(url_split) > 3: if len(url_split) > 3:
ticker_str = url_split[3] ticker_str = url_split[3]
coin_type = getCoinIdFromTicker(ticker_str) coin_type = tickerToCoinId(ticker_str)
if len(url_split) > 4: if len(url_split) > 4:
cmd = url_split[4] cmd = url_split[4]
@@ -328,10 +325,11 @@ def formatBids(swap_client, bids, filters) -> bytes:
offer = swap_client.getOffer(b[3]) offer = swap_client.getOffer(b[3])
ci_to = swap_client.ci(offer.coin_to) if offer else None 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 amount_to = None
if ci_to: 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_data = {
"bid_id": b[2].hex(), "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", "coin_to": ci_to.coin_name() if ci_to else "Unknown",
"amount_from": ci_from.format_amount(b[4]), "amount_from": ci_from.format_amount(b[4]),
"amount_to": amount_to, "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]), "bid_state": strBidState(b[5]),
"addr_from": b[11], "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: if with_extra_info:
bid_data.update( bid_data.update({
{"tx_state_a": strTxState(b[7]), "tx_state_b": strTxState(b[8])} "tx_state_a": strTxState(b[7]),
) "tx_state_b": strTxState(b[8])
})
rv.append(bid_data) rv.append(bid_data)
return bytes(json.dumps(rv), "UTF-8") 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}" 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) ci = swap_client.ci(coin_type)
r = 0 r = 0
@@ -850,12 +849,7 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
swap_client.checkSystemStatus() swap_client.checkSystemStatus()
post_data = getFormData(post_string, is_json) post_data = getFormData(post_string, is_json)
coin_in = get_data_entry(post_data, "coin") coin = getCoinType(get_data_entry(post_data, "coin"))
try:
coin = getCoinIdFromName(coin_in)
except Exception:
coin = getCoinType(coin_in)
if coin in (Coins.PART, Coins.PART_ANON, Coins.PART_BLIND): if coin in (Coins.PART, Coins.PART_ANON, Coins.PART_BLIND):
raise ValueError("Particl wallet seed is set from the Basicswap mnemonic.") 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( expect_seedid = swap_client.getStringKV(
"main_wallet_seedid_" + ci.coin_name().lower() "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( rv.update(
{ {
"seed": seed_key.hex(), "seed": seed_key.hex(),
"seed_id": seed_id.hex(), "seed_id": seed_id.hex(),
"expected_seed_id": "Unset" if expect_seedid is None else expect_seedid, "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: def js_help(self, url_split, post_string, is_json) -> bytes:
# TODO: Add details and examples # TODO: Add details and examples
commands = [] commands = []
for k in endpoints: for k in pages:
commands.append(k) commands.append(k)
return bytes(json.dumps({"commands": commands}), "UTF-8") return bytes(json.dumps({"commands": commands}), "UTF-8")
@@ -973,8 +962,7 @@ def js_help(self, url_split, post_string, is_json) -> bytes:
def js_readurl(self, url_split, post_string, is_json) -> bytes: def js_readurl(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json) post_data = {} if post_string == "" else getFormData(post_string, is_json)
if not have_data_entry(post_data, "url"): if have_data_entry(post_data, "url"):
raise ValueError("Requires URL.")
url = get_data_entry(post_data, "url") url = get_data_entry(post_data, "url")
default_headers = { 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", "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",
@@ -989,118 +977,71 @@ def js_readurl(self, url_split, post_string, is_json) -> bytes:
except json.JSONDecodeError: except json.JSONDecodeError:
pass pass
return response return response
raise ValueError("Requires URL.")
def js_active(self, url_split, post_string, is_json) -> bytes: def js_active(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client swap_client = self.server.swap_client
swap_client.checkSystemStatus() swap_client.checkSystemStatus()
all_bids = [] filters = {
try: "sort_by": "created_at",
for bid_id, (bid, offer) in list(swap_client.swaps_in_progress.items()): "sort_dir": "desc"
try:
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(),
"coin_to": swap_client.ci(offer.coin_to).coin_name(),
"amount_from": swap_client.ci(offer.coin_from).format_amount(
bid.amount
),
"amount_to": swap_client.ci(offer.coin_to).format_amount(
bid.amount_to
),
"addr_from": bid.bid_addr if bid.was_received else offer.addr_from,
} }
EXCLUDED_STATES = [
'Completed',
'Expired',
'Timed-out',
'Abandoned',
'Failed, refunded',
'Failed, swiped',
'Failed',
'Error',
'received'
]
all_bids = []
if offer.swap_type == SwapTypes.XMR_SWAP: try:
swap_data["tx_state_a"] = ( received_bids = swap_client.listBids(filters=filters)
strTxState(bid.xmr_a_lock_tx.state) sent_bids = swap_client.listBids(sent=True, filters=filters)
if bid.xmr_a_lock_tx for bid in received_bids + sent_bids:
else None try:
) bid_state = strBidState(bid[5])
swap_data["tx_state_b"] = ( tx_state_a = strTxState(bid[7])
strTxState(bid.xmr_b_lock_tx.state) tx_state_b = strTxState(bid[8])
if bid.xmr_b_lock_tx if bid_state in EXCLUDED_STATES:
else None continue
) offer = swap_client.getOffer(bid[3])
else: if not offer:
swap_data["tx_state_a"] = bid.getITxState() continue
swap_data["tx_state_b"] = bid.getPTxState() swap_data = {
"bid_id": bid[2].hex(),
if hasattr(bid, "rate"): "offer_id": bid[3].hex(),
swap_data["rate"] = bid.rate "created_at": bid[0],
"bid_state": bid_state,
"tx_state_a": tx_state_a if tx_state_a else 'None',
"tx_state_b": tx_state_b if tx_state_b else 'None',
"coin_from": swap_client.ci(bid[9]).coin_name(),
"coin_to": swap_client.ci(offer.coin_to).coin_name(),
"amount_from": swap_client.ci(bid[9]).format_amount(bid[4]),
"amount_to": swap_client.ci(offer.coin_to).format_amount(
(bid[4] * bid[10]) // swap_client.ci(bid[9]).COIN()
),
"addr_from": bid[11],
"status": {
"main": bid_state,
"initial_tx": tx_state_a if tx_state_a else 'None',
"payment_tx": tx_state_b if tx_state_b else 'None'
}
}
all_bids.append(swap_data) all_bids.append(swap_data)
except Exception: except Exception:
pass continue
except Exception: except Exception:
return bytes(json.dumps([]), "UTF-8") return bytes(json.dumps([]), "UTF-8")
return bytes(json.dumps(all_bids), "UTF-8") return bytes(json.dumps(all_bids), "UTF-8")
def js_coinprices(self, url_split, post_string, is_json) -> bytes: pages = {
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 = {
"coins": js_coins, "coins": js_coins,
"wallets": js_wallets, "wallets": js_wallets,
"offers": js_offers, "offers": js_offers,
@@ -1126,11 +1067,10 @@ endpoints = {
"help": js_help, "help": js_help,
"readurl": js_readurl, "readurl": js_readurl,
"active": js_active, "active": js_active,
"coinprices": js_coinprices,
} }
def js_url_to_function(url_split): def js_url_to_function(url_split):
if len(url_split) > 2: if len(url_split) > 2:
return endpoints.get(url_split[2], js_404) return pages.get(url_split[2], js_404)
return js_index return js_index

View File

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

View File

@@ -6,9 +6,9 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
""" """
Message 2 bytes msg_class, 4 bytes length, [ 2 bytes msg_type, payload ] Message 2 bytes msg_class, 4 bytes length, [ 2 bytes msg_type, payload ]
Handshake procedure: Handshake procedure:
node0 connecting to node1 node0 connecting to node1
node0 send_handshake node0 send_handshake
node1 process_handshake node1 process_handshake
@@ -16,7 +16,7 @@ Handshake procedure:
node0 recv_ping node0 recv_ping
Both nodes are initialised Both nodes are initialised
XChaCha20_Poly1305 mac is 16bytes XChaCha20_Poly1305 mac is 16bytes
""" """
import time 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----- -----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBFlLyYIBEADW3oJnVMDC94g+7OB1/IBUYNevCqJIOAtC0eeFS5g0XGvvqRLx mQENBFlLyDYBCADqup3EHjFCMELf4I0smf4hDl48qDn/Hue08JLmSToMc7z9ylLk
2NLUqn5te+R7deoGElkZFJLLxFUwEnhqGCRH50Iou5aanUzvgI5fVAbK3k0fp9vc 6Uzx6S1m7RiDO63A7yW4qyRkb54VNj+6rUSPNt2uVy1vT8OEQJAZLf2c4qpaKHAQ
LKCR0fQVIidcLyqMpkLZo8BSE3+BWxFp/r2OHvh2dYtJC+BZVwblkDS3cqwKvUZx QV3utu8pYxYOJfLHh4zNEGXrbSrjDv/FTPuri+SkIABhjf70ZSocm4l49rtBanK5
IocvDs47Wo3tzZfEsqUavbbiGx+Dm0fCV7TVHdVLU7q3bZsHSRiyTUZ2EAApoAmT AIAp8DoXWcUdbwmAfl6qrLfzrDu75kq+bspd8p4CVy4fzdOtr6LvXW38z1t3XtLP
ir9csVxv2IM8yf6/HwWi6/Lp7dgSG1+qBZ1lUPPTY+dFLPZyt/kul+vuOj6GLZaU +EGVMAzZQWr2WbN762rK7skH+ZfhaMjAwr8gPYymYnFGLdS1nBmhksnulQNGQOro
s3D66d7TaPCHKWAOnP9RHpic/iXODXVXo1KHJfa0x8fW7I+y7/Gb+5x/m4O0Bz2T WojsvQKgBJoGUnp/OrVpi3gn7UNfDo99CxMRABEBAAG0IHRlY25vdmVydCA8dGVj
BivdrSAuFpXkPqwawlw4CPgI9fc801g83+ZFzD2jJ6qxkEgfnlmf+zGNn5tC4N5j bm92ZXJ0QHBhcnRpY2wuaW8+iQFUBBMBCAA+FiEEOQGTZk5wi3vnahADcJ5tyVzr
NRTQ+GyHo1w4824SXcSN590wgz8goGJC3QPJxbifvOA8GzQIVzpxHckofOVyqIEq Ac8FAllLyDYCGwMFCQPCZwAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQcJ5t
qSnkP2xn4mELqD7HcFnoojZBqFbF2cN+oWQ+niLN+v4qrUncpQI9SVWlyp66S+1T yVzrAc+0LAf/SvBJFJGq1yT9pdLT+7lv7BrshfSYQBLNqPmPrxRuxzH3q/EaEk6D
BhBQj2QuX+3B+K27EiDbhNV7EX6xEbGsnB1poMc2aMiz6veybW3GnoM+2ppr8Ko/ oQh/Jk4vmSXR1y+bsKtS55ekGsPZZWlUFMbXDuU0II3YkWewHXTnqxLtqzcWODoK
12Ij7l+ZA44t6PWUfQQbNSbUk/0Yhd9QJ8VQVck+TaS6gtarTbURlSdmHwARAQAB 6vPonjiVuhYC57d4TWw5ebzHy8wICunyVeaL/cvYQM1TfaI2fN5v0Ep+XiRpH/15
tCl0ZWNub3ZlcnQgKGdpdGlhbikgPHRlY25vdmVydEBwYXJ0aWNsLmlvPokCVAQT HQzRaynKq58w7gH79mPIRA2WFz4eMIMWS3rSa+cSoJ0MhpimgnKUDlh2DebVP1eH
AQgAPhYhBI5RfcEuwcw39kI6ihPxNlHJzw1rBQJZS8mCAhsDBQkSzAMABQsJCAcC keSW1JlPZHhca/XB93ghFlbO6wOrbg+gsKtB45OkpsoOzUMFIKVJLBAjK751dTcc
BhUICQoLAgQWAgMBAh4BAheAAAoJEBPxNlHJzw1rtdYP/22iRX8O2Q++MayPHNx5 Pb4xTzABaBXxk+IUxgGB1h+g3i6wzksfgLkBDQRZS8g2AQgAw7Db3G5J21jsty9S
XHAlMk9mfi5FB1qJwshtlhda7P9U/hOTi227wH+Mzh5dBje4t2DkoHzxlz/Wr4cQ pMmqp93dgZFm8E4VTcsL4KVvZybhwHngNHnhG8G/DWQ53o07/BKorfRBmFD3x2Eq
QUJMOYd0OEZY6kpAQkvtyYobIb6zlRQK1koAfNMxewmfZZGTlr16IUVCovGSFvZ+ RqfOn4ytmZVw/sOjbZPi4m/tF8z+O9qR8I0CzedYip21rwz2j4UgnpDQ+BnOpyXB
hdYRDEjuHqXjpwBfrxFAy/HCnfY10qSRkJc5w1ypj1IkzlanS+xeRJSDvRTQDAEr H0gDBlPFq8ih9kkm413QRTTKnkRM/U8SfyFU8vIFdH7T0Ae07m0LxePDaTyxLPg3
zv3xKcMGjCCHaaCP+tyAaViBaUOlvmZdWwg0gwQCuPLqIh0cfDbcg0quciRIpnyp x1+RvEjVkruc3/9Z4kzexoUv654wirRdxPX8GsWI1WNDQrj4GqmpF/e0WDM97+Lk
zINmfwngZCwXdIYfAzmCzMHw1J3iOiqfqK0EcpHMsL689VyQSPgsoEHtcOGHYjRL DGzbcXy7TGMIHQx8QFlFwdSZv9x70574as9Od4jOWTk90sopSMr8t6H6wTdn+2MD
pMPGvRFHtICrnCHENK3IcwFWDGXW+i3zgOlA7g48yYWWvSup+t8I6YT+FeeFlxSO qsZKUwARAQABiQE8BBgBCAAmFiEEOQGTZk5wi3vnahADcJ5tyVzrAc8FAllLyDYC
dj1GdeMA0O7gXZ7znLVduokL2Ef4dZjc+3NwBlFov52vwCZwQMAGsMriwEDB2rDZ GwwFCQPCZwAACgkQcJ5tyVzrAc/QFgf8CQydF/VqJtujQC/rjB1YYNQcljzoeQWA
B7YOvAxlUB/kavtx/oE8fV7mZcuwYg02lq4bozF9xlhjOFaRit6xXnLVi0TuI76c 2F2O5cF5skTNYy+xas3PTgxfOpn5iTpixpkB+I7X8LwoPmRjZvg2MFirDVXUypcx
uz67nB9VkWczSLIzCyjNyFpWbx1BMxTYfehZX3+YNajXwG6HdEp9CAYK0u46Guz/ HwMbQqYCuAaK1EhtVUVYbFGjM67nClmBApLdenbqEP/BhyR9kgDCBt7ZvSLe5N/6
Pth67bbNVYyP/cuOIrv/hqQ6xo4mOBMDDxcCEAXx3rwxfbxNM8vlwrMcpITrtNON MKYJF1FlCgGc5OJPJrMIl0slU5QtzRy5J+l75WflkgxFUKJPotJ5Z+yduxOff//e
r41bcxUIfMEDefPm5wnXep8W qSEXqlkaebWT0ZFiAqHhExJCRJ5HBqQEdW4JHrB7j3bNh8Qdf8epiYtcXXSsE9+K
=szpX 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----- -----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 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import os
import json import json
import traceback import shlex
import urllib import urllib
import traceback
import subprocess
from xmlrpc.client import ( from xmlrpc.client import (
Fault, Fault,
Transport, 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")) r = json.loads(v.decode("utf-8"))
except Exception as ex: except Exception as ex:
traceback.print_exc() 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: if "error" in r and r["error"] is not None:
raise ValueError("RPC error " + str(r["error"])) 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) return Jsonrpc(url)
except Exception as ex: except Exception as ex:
traceback.print_exc() 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"): 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 host = host
def rpc_func(method, params=None, wallet_override=None): def rpc_func(method, params=None, wallet_override=None):
nonlocal port, auth, wallet, host
return callrpc( return callrpc(
port, port,
auth, auth,

View File

@@ -309,6 +309,7 @@ def make_xmr_rpc2_func(
transport.set_proxy(proxy_host, proxy_port) transport.set_proxy(proxy_host, proxy_port)
def rpc_func(method, params=None, wallet=None, timeout=default_timeout): def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
nonlocal port, auth, host, transport, tag
return callrpc_xmr2( return callrpc_xmr2(
port, port,
method, method,
@@ -344,6 +345,7 @@ def make_xmr_rpc_func(
transport.set_proxy(proxy_host, proxy_port) transport.set_proxy(proxy_host, proxy_port)
def rpc_func(method, params=None, wallet=None, timeout=default_timeout): def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
nonlocal port, auth, host, transport, tag
return callrpc_xmr( return callrpc_xmr(
port, port,
method, 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 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 = { const state = {
identities: new Map(), identities: new Map(),
currentPage: 1, currentPage: 1,
@@ -10,6 +27,7 @@ const state = {
refreshPromise: null refreshPromise: null
}; };
// DOM
const elements = { const elements = {
swapsBody: document.getElementById('active-swaps-body'), swapsBody: document.getElementById('active-swaps-body'),
prevPageButton: document.getElementById('prevPage'), prevPageButton: document.getElementById('prevPage'),
@@ -22,6 +40,105 @@ const elements = {
statusText: document.getElementById('status-text') 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 safeParseInt = (value) => {
const parsed = parseInt(value); const parsed = parseInt(value);
return isNaN(parsed) ? 0 : parsed; return isNaN(parsed) ? 0 : parsed;
@@ -83,6 +200,7 @@ const getTxStatusClass = (status) => {
return 'text-blue-500'; return 'text-blue-500';
}; };
// Util
const formatTimeAgo = (timestamp) => { const formatTimeAgo = (timestamp) => {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
const diff = now - timestamp; const diff = now - timestamp;
@@ -93,6 +211,7 @@ const formatTimeAgo = (timestamp) => {
return `${Math.floor(diff / 86400)} days ago`; return `${Math.floor(diff / 86400)} days ago`;
}; };
const formatTime = (timestamp) => { const formatTime = (timestamp) => {
if (!timestamp) return ''; if (!timestamp) return '';
const date = new Date(timestamp * 1000); const date = new Date(timestamp * 1000);
@@ -132,6 +251,96 @@ const getTimeStrokeColor = (expireTime) => {
return '#10B981'; // More than 30 minutes 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 updateConnectionStatus = (status) => {
const { statusDot, statusText } = elements; const { statusDot, statusText } = elements;
if (!statusDot || !statusText) return; if (!statusDot || !statusText) return;
@@ -293,8 +502,8 @@ const createSwapTableRow = async (swap) => {
const identity = await IdentityManager.getIdentityData(swap.addr_from); const identity = await IdentityManager.getIdentityData(swap.addr_from);
const uniqueId = `${swap.bid_id}_${swap.created_at}`; const uniqueId = `${swap.bid_id}_${swap.created_at}`;
const fromSymbol = window.CoinManager.getSymbol(swap.coin_from) || swap.coin_from; const fromSymbol = COIN_NAME_TO_SYMBOL[swap.coin_from] || swap.coin_from;
const toSymbol = window.CoinManager.getSymbol(swap.coin_to) || swap.coin_to; const toSymbol = COIN_NAME_TO_SYMBOL[swap.coin_to] || swap.coin_to;
const timeColor = getTimeStrokeColor(swap.expire_at); const timeColor = getTimeStrokeColor(swap.expire_at);
const fromAmount = parseFloat(swap.amount_from) || 0; const fromAmount = parseFloat(swap.amount_from) || 0;
const toAmount = parseFloat(swap.amount_to) || 0; const toAmount = parseFloat(swap.amount_to) || 0;
@@ -395,10 +604,11 @@ const createSwapTableRow = async (swap) => {
</div> </div>
</div> </div>
</td> </td>
<!-- Status Column --> <!-- Status Column -->
<td class="py-3 px-4 text-center"> <td class="py-3 px-4 text-center">
<div data-tooltip-target="tooltip-status-${uniqueId}" class="flex justify-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} ${swap.bid_state}
</span> </span>
</div> </div>
@@ -501,8 +711,6 @@ const createSwapTableRow = async (swap) => {
async function updateSwapsTable(options = {}) { async function updateSwapsTable(options = {}) {
const { resetPage = false, refreshData = true } = options; const { resetPage = false, refreshData = true } = options;
//console.log('Updating swaps table:', { resetPage, refreshData });
if (state.refreshPromise) { if (state.refreshPromise) {
await state.refreshPromise; await state.refreshPromise;
return; return;
@@ -528,19 +736,9 @@ async function updateSwapsTable(options = {}) {
} }
const data = await response.json(); const data = await response.json();
//console.log('Received swap data:', data); state.swapsData = Array.isArray(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);
} catch (error) { } catch (error) {
//console.error('Error fetching swap data:', error); console.error('Error fetching swap data:', error);
state.swapsData = []; state.swapsData = [];
} finally { } finally {
state.refreshPromise = null; state.refreshPromise = null;
@@ -566,14 +764,13 @@ async function updateSwapsTable(options = {}) {
const endIndex = startIndex + PAGE_SIZE; const endIndex = startIndex + PAGE_SIZE;
const currentPageSwaps = state.swapsData.slice(startIndex, endIndex); const currentPageSwaps = state.swapsData.slice(startIndex, endIndex);
//console.log('Current page swaps:', currentPageSwaps);
if (elements.swapsBody) { if (elements.swapsBody) {
if (currentPageSwaps.length > 0) { if (currentPageSwaps.length > 0) {
const rowPromises = currentPageSwaps.map(swap => createSwapTableRow(swap)); const rowPromises = currentPageSwaps.map(swap => createSwapTableRow(swap));
const rows = await Promise.all(rowPromises); const rows = await Promise.all(rowPromises);
elements.swapsBody.innerHTML = rows.join(''); elements.swapsBody.innerHTML = rows.join('');
// Initialize tooltips
if (window.TooltipManager) { if (window.TooltipManager) {
window.TooltipManager.cleanup(); window.TooltipManager.cleanup();
const tooltipTriggers = document.querySelectorAll('[data-tooltip-target]'); const tooltipTriggers = document.querySelectorAll('[data-tooltip-target]');
@@ -588,7 +785,6 @@ async function updateSwapsTable(options = {}) {
}); });
} }
} else { } else {
//console.log('No active swaps found, displaying empty state');
elements.swapsBody.innerHTML = ` elements.swapsBody.innerHTML = `
<tr> <tr>
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white"> <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) { } catch (error) {
console.error('Error updating swaps table:', error); console.error('Error updating swaps table:', error);
if (elements.swapsBody) { if (elements.swapsBody) {
@@ -613,10 +825,7 @@ async function updateSwapsTable(options = {}) {
} }
} }
function isActiveSwap(swap) { // Event
return true;
}
const setupEventListeners = () => { const setupEventListeners = () => {
if (elements.refreshSwapsButton) { if (elements.refreshSwapsButton) {
elements.refreshSwapsButton.addEventListener('click', async (e) => { elements.refreshSwapsButton.addEventListener('click', async (e) => {
@@ -656,11 +865,8 @@ const setupEventListeners = () => {
} }
}; };
document.addEventListener('DOMContentLoaded', async () => { // Init
document.addEventListener('DOMContentLoaded', () => {
WebSocketManager.initialize(); WebSocketManager.initialize();
setupEventListeners(); 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 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 = { const state = {
dentities: new Map(), dentities: new Map(),
currentPage: 1, currentPage: 1,
@@ -10,6 +27,7 @@ const state = {
refreshPromise: null refreshPromise: null
}; };
// DOM
const elements = { const elements = {
bidsBody: document.getElementById('bids-body'), bidsBody: document.getElementById('bids-body'),
prevPageButton: document.getElementById('prevPage'), prevPageButton: document.getElementById('prevPage'),
@@ -22,6 +40,125 @@ const elements = {
statusText: document.getElementById('status-text') 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 formatTimeAgo = (timestamp) => {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
const diff = now - timestamp; 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 updateConnectionStatus = (status) => {
const { statusDot, statusText } = elements; const { statusDot, statusText } = elements;
if (!statusDot || !statusText) return; if (!statusDot || !statusText) return;
@@ -272,8 +499,8 @@ const createBidTableRow = async (bid) => {
const rate = toAmount > 0 ? toAmount / fromAmount : 0; const rate = toAmount > 0 ? toAmount / fromAmount : 0;
const inverseRate = fromAmount > 0 ? fromAmount / toAmount : 0; const inverseRate = fromAmount > 0 ? fromAmount / toAmount : 0;
const fromSymbol = window.CoinManager.getSymbol(bid.coin_from) || bid.coin_from; const fromSymbol = COIN_NAME_TO_SYMBOL[bid.coin_from] || bid.coin_from;
const toSymbol = window.CoinManager.getSymbol(bid.coin_to) || bid.coin_to; const toSymbol = COIN_NAME_TO_SYMBOL[bid.coin_to] || bid.coin_to;
const timeColor = getTimeStrokeColor(bid.expire_at); const timeColor = getTimeStrokeColor(bid.expire_at);
const uniqueId = `${bid.bid_id}_${bid.created_at}`; const uniqueId = `${bid.bid_id}_${bid.created_at}`;
@@ -368,6 +595,7 @@ const createBidTableRow = async (bid) => {
</div> </div>
</div> </div>
</td> </td>
<!-- You Get Column --> <!-- You Get Column -->
<td class="py-0"> <td class="py-0">
<div class="py-3 px-4 text-right"> <div class="py-3 px-4 text-right">
@@ -624,6 +852,7 @@ async function updateBidsTable(options = {}) {
} }
} }
// Event
const setupEventListeners = () => { const setupEventListeners = () => {
if (elements.refreshBidsButton) { if (elements.refreshBidsButton) {
elements.refreshBidsButton.addEventListener('click', async () => { elements.refreshBidsButton.addEventListener('click', async () => {
@@ -663,8 +892,8 @@ if (elements.refreshBidsButton) {
} }
}; };
document.addEventListener('DOMContentLoaded', async () => { // Init
document.addEventListener('DOMContentLoaded', () => {
WebSocketManager.initialize(); WebSocketManager.initialize();
setupEventListeners(); setupEventListeners();
await updateBidsTable({ resetPage: true, refreshData: true });
}); });

View File

@@ -1,3 +1,4 @@
// Constants and State
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
const state = { const state = {
currentPage: { currentPage: {
@@ -166,225 +167,246 @@ const EventManager = {
}; };
function cleanup() { function cleanup() {
//console.log('Starting comprehensive cleanup process for bids table'); console.log('Starting cleanup process');
EventManager.clearAll();
try { const exportSentButton = document.getElementById('exportSentBids');
const exportReceivedButton = document.getElementById('exportReceivedBids');
if (exportSentButton) {
exportSentButton.remove();
}
if (exportReceivedButton) {
exportReceivedButton.remove();
}
if (window.TooltipManager) {
const originalCleanup = window.TooltipManager.cleanup;
window.TooltipManager.cleanup = function() {
originalCleanup.call(window.TooltipManager);
setTimeout(() => {
forceTooltipDOMCleanup();
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();
}
});
}, 10);
};
}
WebSocketManager.cleanup();
if (searchTimeout) { if (searchTimeout) {
clearTimeout(searchTimeout); clearTimeout(searchTimeout);
searchTimeout = null; searchTimeout = null;
} }
if (state.refreshPromise) {
state.isRefreshing = false;
}
if (window.WebSocketManager) {
WebSocketManager.disconnect();
}
cleanupTooltips();
forceTooltipDOMCleanup();
if (window.TooltipManager) {
window.TooltipManager.cleanup();
}
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);
}
Array.from(row.attributes).forEach(attr => {
if (attr.name.startsWith('data-')) {
row.removeAttribute(attr.name);
}
});
});
while (tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
};
cleanupTableBody('sent-tbody');
cleanupTableBody('received-tbody');
if (window.CleanupManager) {
CleanupManager.clearAll();
} else {
EventManager.clearAll();
}
const clearAllAnimationFrames = () => {
const rafList = window.requestAnimationFrameList;
if (Array.isArray(rafList)) {
rafList.forEach(id => {
cancelAnimationFrame(id);
});
window.requestAnimationFrameList = [];
}
};
clearAllAnimationFrames();
state.data = { state.data = {
sent: [], sent: [],
received: [] 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(); IdentityManager.clearCache();
}
if (window.CacheManager) {
CacheManager.cleanup(true);
}
if (window.MemoryManager) {
MemoryManager.forceCleanup();
}
Object.keys(elements).forEach(key => { Object.keys(elements).forEach(key => {
elements[key] = null; elements[key] = null;
}); });
console.log('Comprehensive cleanup completed'); console.log('Cleanup completed');
} catch (error) { }
console.error('Error during cleanup process:', error);
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();
}
try { try {
if (window.EventManager) EventManager.clearAll(); const wsPort = window.ws_port || '11700';
if (window.CleanupManager) CleanupManager.clearAll(); this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
if (window.WebSocketManager) WebSocketManager.disconnect(); this.setupEventHandlers();
} catch (error) {
state.data = { sent: [], received: [] }; console.error('WebSocket connection error:', error);
state.isLoading = false; this.handleReconnect();
Object.keys(elements).forEach(key => {
elements[key] = null;
});
} catch (e) {
console.error('Failsafe cleanup also failed:', e);
} }
} },
}
window.cleanupBidsTable = cleanup; setupEventHandlers() {
if (!this.ws) return;
CleanupManager.addListener(document, 'visibilitychange', () => { this.ws.onopen = () => {
if (document.hidden) { state.wsConnected = true;
//console.log('Page hidden - pausing WebSocket and optimizing memory'); this.reconnectAttempts = 0;
this.lastMessageTime = Date.now();
if (WebSocketManager && typeof WebSocketManager.pause === 'function') { updateConnectionStatus('connected');
WebSocketManager.pause(); console.log('🟢 WebSocket connection established for Sent Bids / Received Bids');
} else if (WebSocketManager && typeof WebSocketManager.disconnect === 'function') {
WebSocketManager.disconnect();
}
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
window.TooltipManager.cleanup();
}
// 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) {
setTimeout(() => {
updateBidsTable(); updateBidsTable();
}, 500); };
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);
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);
} }
}); };
if (window.CleanupManager) { this.ws.onclose = () => {
CleanupManager.removeListenersByElement(row); 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 (this.isPaused) return;
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 { } else {
EventManager.removeAll(row); updateConnectionStatus('error');
//console.log('Maximum reconnection attempts reached. Will try again in 60 seconds.');
setTimeout(() => {
this.reconnectAttempts = 0;
this.connect();
}, 60000);
} }
},
row.removeAttribute('data-offer-id'); cleanupConnection() {
row.removeAttribute('data-bid-id'); if (this.ws) {
this.ws.onopen = null;
while (row.firstChild) { this.ws.onmessage = null;
const child = row.firstChild; this.ws.onclose = null;
row.removeChild(child); this.ws.onerror = null;
if (this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.close(1000, 'Cleanup');
} catch (e) {
console.warn('Error closing WebSocket:', e);
} }
}
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);
} }
}); this.ws = null;
cleanupOffscreenTooltips();
if (window.IdentityManager && typeof IdentityManager.limitCacheSize === 'function') {
IdentityManager.limitCacheSize(100);
} }
},
if (window.MemoryManager) { pause() {
MemoryManager.forceCleanup(); this.isPaused = true;
//console.log('WebSocket operations paused');
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
} }
} },
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();
}
},
cleanup() {
this.isPaused = true;
this.stopHealthCheck();
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
this.cleanupConnection();
}
};
// Core
const safeParseInt = (value) => { const safeParseInt = (value) => {
const parsed = parseInt(value); const parsed = parseInt(value);
return isNaN(parsed) ? 0 : parsed; return isNaN(parsed) ? 0 : parsed;
@@ -490,6 +512,7 @@ function coinMatches(offerCoin, filterCoin) {
return false; return false;
} }
// State
function hasActiveFilters() { function hasActiveFilters() {
const coinFromSelect = document.getElementById('coin_from'); const coinFromSelect = document.getElementById('coin_from');
const coinToSelect = document.getElementById('coin_to'); const coinToSelect = document.getElementById('coin_to');
@@ -557,58 +580,11 @@ function filterAndSortData(bids) {
const searchStr = state.filters.searchQuery.toLowerCase(); const searchStr = state.filters.searchQuery.toLowerCase();
const matchesBidId = bid.bid_id.toLowerCase().includes(searchStr); const matchesBidId = bid.bid_id.toLowerCase().includes(searchStr);
const matchesIdentity = bid.addr_from?.toLowerCase().includes(searchStr); const matchesIdentity = bid.addr_from?.toLowerCase().includes(searchStr);
const identity = IdentityManager.cache.get(bid.addr_from);
let label = ''; const label = identity?.data?.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 matchesLabel = label.toLowerCase().includes(searchStr); const matchesLabel = label.toLowerCase().includes(searchStr);
let matchesDisplayedLabel = false; if (!(matchesBidId || matchesIdentity || matchesLabel)) {
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)) {
return false; 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() { function updateCoinFilterImages() {
const coinToSelect = document.getElementById('coin_to'); const coinToSelect = document.getElementById('coin_to');
const coinFromSelect = document.getElementById('coin_from'); 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) => { const processIdentityStats = (identity) => {
if (!identity) return null; if (!identity) return null;
@@ -847,7 +894,8 @@ const createIdentityTooltipContent = (identity) => {
`; `;
}; };
const tooltipIdsToCleanup = new Set(); // Table
let tooltipIdsToCleanup = new Set();
const cleanupTooltips = () => { const cleanupTooltips = () => {
if (window.TooltipManager) { if (window.TooltipManager) {
@@ -869,6 +917,7 @@ const forceTooltipDOMCleanup = () => {
foundCount += allTooltipElements.length; foundCount += allTooltipElements.length;
allTooltipElements.forEach(element => { allTooltipElements.forEach(element => {
const isDetached = !document.body.contains(element) || const isDetached = !document.body.contains(element) ||
element.classList.contains('hidden') || element.classList.contains('hidden') ||
element.style.display === 'none'; element.style.display === 'none';
@@ -898,6 +947,7 @@ const forceTooltipDOMCleanup = () => {
const tippyRoots = document.querySelectorAll('[data-tippy-root]'); const tippyRoots = document.querySelectorAll('[data-tippy-root]');
foundCount += tippyRoots.length; foundCount += tippyRoots.length;
tippyRoots.forEach(element => { tippyRoots.forEach(element => {
const isOrphan = !element.children.length || const isOrphan = !element.children.length ||
element.children[0].classList.contains('hidden') || element.children[0].classList.contains('hidden') ||
@@ -925,10 +975,13 @@ const forceTooltipDOMCleanup = () => {
} }
} }
}); });
// Handle legacy tooltip elements
document.querySelectorAll('.tooltip').forEach(element => { document.querySelectorAll('.tooltip').forEach(element => {
const isTrulyDetached = !element.parentElement || const isTrulyDetached = !element.parentElement ||
!document.body.contains(element.parentElement) || !document.body.contains(element.parentElement) ||
element.classList.contains('hidden'); element.classList.contains('hidden');
if (isTrulyDetached) { if (isTrulyDetached) {
try { try {
element.remove(); element.remove();
@@ -939,11 +992,14 @@ const forceTooltipDOMCleanup = () => {
} }
}); });
if (window.TooltipManager && typeof window.TooltipManager.getActiveTooltipInstances === 'function') { if (window.TooltipManager && window.TooltipManager.activeTooltips) {
const activeTooltips = window.TooltipManager.getActiveTooltipInstances(); window.TooltipManager.activeTooltips.forEach((instance, id) => {
activeTooltips.forEach(([element, instance]) => { const tooltipElement = document.getElementById(id.split('tooltip-trigger-')[1]);
const tooltipId = element.getAttribute('data-tooltip-trigger-id'); const triggerElement = document.querySelector(`[data-tooltip-trigger-id="${id}"]`);
if (!document.body.contains(element)) {
if (!tooltipElement || !triggerElement ||
!document.body.contains(tooltipElement) ||
!document.body.contains(triggerElement)) {
if (instance?.[0]) { if (instance?.[0]) {
try { try {
instance[0].destroy(); instance[0].destroy();
@@ -951,13 +1007,14 @@ const forceTooltipDOMCleanup = () => {
console.warn('Error destroying tooltip instance:', e); console.warn('Error destroying tooltip instance:', e);
} }
} }
window.TooltipManager.activeTooltips.delete(id);
} }
}); });
} }
if (removedCount > 0) { if (removedCount > 0) {
// console.log(`Tooltip cleanup: found ${foundCount}, removed ${removedCount} detached tooltips`); // console.log(`Tooltip cleanup: found ${foundCount}, removed ${removedCount} detached tooltips`);
} }
} };
const createTableRow = async (bid) => { const createTableRow = async (bid) => {
const identity = await IdentityManager.getIdentityData(bid.addr_from); const identity = await IdentityManager.getIdentityData(bid.addr_from);
@@ -1033,14 +1090,14 @@ const createTableRow = async (bid) => {
<!-- Status Column --> <!-- Status Column -->
<td class="py-3 px-6"> <td class="py-3 px-6">
<div class="relative flex justify-center" data-tooltip-target="tooltip-status-${uniqueId}"> <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} ${bid.bid_state}
</span> </span>
</div> </div>
</td> </td>
<!-- Actions Column --> <!-- Actions Column -->
<td class="py-3 pr-4"> <td class="py-3 pr-4 pl-3">
<div class="flex justify-center"> <div class="flex justify-center">
<a href="/bid/${bid.bid_id}" <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"> 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">
@@ -1166,7 +1223,7 @@ const initializeTooltips = () => {
window.TooltipManager.cleanup(); window.TooltipManager.cleanup();
const selector = '#' + state.currentTab + ' [data-tooltip-target]'; let selector = '#' + state.currentTab + ' [data-tooltip-target]';
const tooltipTriggers = document.querySelectorAll(selector); const tooltipTriggers = document.querySelectorAll(selector);
const tooltipCount = tooltipTriggers.length; const tooltipCount = tooltipTriggers.length;
if (tooltipCount > 50) { if (tooltipCount > 50) {
@@ -1293,6 +1350,7 @@ function implementVirtualizedRows() {
}); });
} }
// Fetching
let activeFetchController = null; let activeFetchController = null;
const fetchBids = async () => { const fetchBids = async () => {
@@ -1335,7 +1393,7 @@ const fetchBids = async () => {
throw new Error(`HTTP error! status: ${response.status}`); 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'); //console.log('Received raw data:', data.length, 'bids');
state.filters.with_expired = includeExpired; state.filters.with_expired = includeExpired;
@@ -1367,19 +1425,20 @@ const fetchBids = async () => {
const updateBidsTable = async () => { const updateBidsTable = async () => {
if (state.isLoading) { if (state.isLoading) {
//console.log('Already loading, skipping update');
return; return;
} }
try { try {
//console.log('Starting updateBidsTable for tab:', state.currentTab);
//console.log('Current filters:', state.filters);
state.isLoading = true; state.isLoading = true;
updateLoadingState(true); updateLoadingState(true);
const bids = await fetchBids(); const bids = await fetchBids();
// Add identity preloading if we're searching //console.log('Fetched bids:', bids.length);
if (state.filters.searchQuery && state.filters.searchQuery.length > 0) {
await preloadIdentitiesForSearch(bids);
}
state.data[state.currentTab] = bids; state.data[state.currentTab] = bids;
state.currentPage[state.currentTab] = 1; state.currentPage[state.currentTab] = 1;
@@ -1437,6 +1496,7 @@ const updatePaginationControls = (type) => {
} }
}; };
// Filter
let searchTimeout; let searchTimeout;
function handleSearch(event) { function handleSearch(event) {
if (searchTimeout) { if (searchTimeout) {
@@ -1641,6 +1701,7 @@ const setupRefreshButtons = () => {
}); });
}; };
// Tabs
const switchTab = (tabId) => { const switchTab = (tabId) => {
if (state.isLoading) return; if (state.isLoading) return;
@@ -1857,17 +1918,10 @@ function setupMemoryMonitoring() {
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
if (document.hidden) { if (document.hidden) {
console.log('Tab hidden - running memory optimization'); console.log('Tab hidden - running memory optimization');
IdentityManager.trimCacheIfNeeded();
if (window.IdentityManager) { if (window.TooltipManager) {
if (typeof IdentityManager.limitCacheSize === 'function') {
IdentityManager.limitCacheSize(100);
}
}
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
window.TooltipManager.cleanup(); window.TooltipManager.cleanup();
} }
if (state.data.sent.length > 1000) { if (state.data.sent.length > 1000) {
console.log('Trimming sent bids data'); console.log('Trimming sent bids data');
state.data.sent = state.data.sent.slice(0, 1000); state.data.sent = state.data.sent.slice(0, 1000);
@@ -1881,7 +1935,6 @@ function setupMemoryMonitoring() {
cleanupTooltips(); cleanupTooltips();
} }
}, MEMORY_CHECK_INTERVAL); }, MEMORY_CHECK_INTERVAL);
document.addEventListener('beforeunload', () => { document.addEventListener('beforeunload', () => {
clearInterval(intervalId); clearInterval(intervalId);
}, { once: true }); }, { once: true });
@@ -1925,12 +1978,6 @@ function initialize() {
updateBidsTable(); updateBidsTable();
}, 100); }, 100);
setInterval(() => {
if ((state.data.sent.length + state.data.received.length) > 1000) {
optimizeMemoryUsage();
}
}, 5 * 60 * 1000); // Check every 5 minutes
window.cleanupBidsTable = cleanup; 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,8 +1,6 @@
(function(window) { (function(window) {
'use strict'; 'use strict';
const dropdownInstances = [];
function positionElement(targetEl, triggerEl, placement = 'bottom', offsetDistance = 8) { function positionElement(targetEl, triggerEl, placement = 'bottom', offsetDistance = 8) {
targetEl.style.visibility = 'hidden'; targetEl.style.visibility = 'hidden';
targetEl.style.display = 'block'; targetEl.style.display = 'block';
@@ -60,9 +58,6 @@
this._handleScroll = this._handleScroll.bind(this); this._handleScroll = this._handleScroll.bind(this);
this._handleResize = this._handleResize.bind(this); this._handleResize = this._handleResize.bind(this);
this._handleOutsideClick = this._handleOutsideClick.bind(this); this._handleOutsideClick = this._handleOutsideClick.bind(this);
dropdownInstances.push(this);
this.init(); this.init();
} }
@@ -71,8 +66,7 @@
this._targetEl.style.margin = '0'; this._targetEl.style.margin = '0';
this._targetEl.style.display = 'none'; this._targetEl.style.display = 'none';
this._targetEl.style.position = 'fixed'; this._targetEl.style.position = 'fixed';
this._targetEl.style.zIndex = '40'; this._targetEl.style.zIndex = '50';
this._targetEl.classList.add('dropdown-menu');
this._setupEventListeners(); this._setupEventListeners();
this._initialized = true; this._initialized = true;
@@ -129,12 +123,6 @@
show() { show() {
if (!this._visible) { if (!this._visible) {
dropdownInstances.forEach(instance => {
if (instance !== this && instance._visible) {
instance.hide();
}
});
this._targetEl.style.display = 'block'; this._targetEl.style.display = 'block';
this._targetEl.style.visibility = 'hidden'; this._targetEl.style.visibility = 'hidden';
@@ -172,12 +160,6 @@
document.removeEventListener('click', this._handleOutsideClick); document.removeEventListener('click', this._handleOutsideClick);
window.removeEventListener('scroll', this._handleScroll, true); window.removeEventListener('scroll', this._handleScroll, true);
window.removeEventListener('resize', this._handleResize); window.removeEventListener('resize', this._handleResize);
const index = dropdownInstances.indexOf(this);
if (index > -1) {
dropdownInstances.splice(index, 1);
}
this._initialized = false; this._initialized = false;
} }
} }
@@ -202,8 +184,6 @@
initDropdowns(); initDropdowns();
} }
Dropdown.instances = dropdownInstances;
window.Dropdown = Dropdown; window.Dropdown = Dropdown;
window.initDropdowns = initDropdowns; 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 = { window.addEventListener('DOMContentLoaded', () => {
get: (id) => document.getElementById(id), const err_msgs = document.querySelectorAll('p.error_msg');
getValue: (id) => { for (let i = 0; i < err_msgs.length; i++) {
const el = document.getElementById(id); err_msg = err_msgs[i].innerText;
return el ? el.value : ''; if (err_msg.indexOf('coin_to') >= 0 || err_msg.indexOf('Coin To') >= 0) {
}, e = document.getElementById('coin_to');
setValue: (id, value) => { e.classList.add('error');
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;
} }
}, if (err_msg.indexOf('Coin From') >= 0) {
set: (key, value) => { e = document.getElementById('coin_from');
try { e.classList.add('error');
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch(e) {
console.error(`Failed to save item to storage: ${key}`, e);
return false;
} }
}, if (err_msg.indexOf('Amount From') >= 0) {
setRaw: (key, value) => { e = document.getElementById('amt_from');
try { e.classList.add('error');
localStorage.setItem(key, value);
return true;
} catch(e) {
console.error(`Failed to save raw item to storage: ${key}`, e);
return false;
} }
}, if (err_msg.indexOf('Amount To') >= 0) {
getRaw: (key) => { e = document.getElementById('amt_to');
try { e.classList.add('error');
return localStorage.getItem(key);
} catch(e) {
console.warn(`Failed to retrieve raw item from storage: ${key}`, e);
return null;
} }
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;
const Ajax = { e.classList.add('error');
post: (url, data, onSuccess, onError) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState !== XMLHttpRequest.DONE) return;
if (xhr.status === 200) {
if (onSuccess) {
try {
const response = xhr.responseText.startsWith('{') ?
JSON.parse(xhr.responseText) : xhr.responseText;
onSuccess(response);
} catch (e) {
console.error('Failed to parse response:', e);
if (onError) onError('Invalid response format');
}
}
} else {
console.error('Request failed:', xhr.statusText);
if (onError) onError(xhr.statusText);
}
};
xhr.open('POST', url);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send(data);
return xhr;
}
};
function handleNewOfferAddress() {
const STORAGE_KEY = 'lastUsedAddressNewOffer';
const selectElement = DOM.query('select[name="addr_from"]');
const form = selectElement?.closest('form');
if (!selectElement || !form) return;
function loadInitialAddress() {
const savedAddress = Storage.get(STORAGE_KEY);
if (savedAddress) {
try {
selectElement.value = savedAddress.value;
} catch (e) {
selectFirstAddress();
}
} else {
selectFirstAddress();
} }
} }
function selectFirstAddress() { // remove error class on input or select focus
if (selectElement.options.length > 1) { const inputs = document.querySelectorAll('input.error');
const firstOption = selectElement.options[1]; const selects = document.querySelectorAll('select.error');
if (firstOption) { const elements = [...inputs, ...selects];
selectElement.value = firstOption.value; elements.forEach((element) => {
saveAddress(firstOption.value, firstOption.text); element.addEventListener('focus', (event) => {
}
}
}
function saveAddress(value, text) {
Storage.set(STORAGE_KEY, { value, text });
}
form.addEventListener('submit', () => {
saveAddress(selectElement.value, selectElement.selectedOptions[0].text);
});
selectElement.addEventListener('change', (event) => {
saveAddress(event.target.value, event.target.selectedOptions[0].text);
});
loadInitialAddress();
}
const RateManager = {
lookupRates: () => {
const coinFrom = DOM.getValue('coin_from');
const coinTo = DOM.getValue('coin_to');
const ratesDisplay = DOM.get('rates_display');
if (!coinFrom || !coinTo || !ratesDisplay) {
console.log('Required elements for lookup_rates not found');
return;
}
if (coinFrom === '-1' || coinTo === '-1') {
alert('Coins from and to must be set first.');
return;
}
const selectedCoin = (coinFrom === '15') ? '3' : coinFrom;
ratesDisplay.innerHTML = '<p>Updating...</p>';
const priceJsonElement = DOM.query(".pricejsonhidden");
if (priceJsonElement) {
priceJsonElement.classList.remove("hidden");
}
const params = 'coin_from=' + selectedCoin + '&coin_to=' + coinTo;
Ajax.post('/json/rates', params,
(response) => {
if (ratesDisplay) {
ratesDisplay.innerHTML = typeof response === 'string' ?
response : '<pre><code>' + JSON.stringify(response, null, ' ') + '</code></pre>';
}
},
(error) => {
if (ratesDisplay) {
ratesDisplay.innerHTML = '<p>Error loading rates: ' + error + '</p>';
}
}
);
},
getRateInferred: (event) => {
if (event) event.preventDefault();
const coinFrom = DOM.getValue('coin_from');
const coinTo = DOM.getValue('coin_to');
const rateElement = DOM.get('rate');
if (!coinFrom || !coinTo || !rateElement) {
console.log('Required elements for getRateInferred not found');
return;
}
const params = 'coin_from=' + encodeURIComponent(coinFrom) +
'&coin_to=' + encodeURIComponent(coinTo);
DOM.setValue('rate', 'Loading...');
Ajax.post('/json/rates', params,
(response) => {
if (response.coingecko && response.coingecko.rate_inferred) {
DOM.setValue('rate', response.coingecko.rate_inferred);
RateManager.setRate('rate');
} else {
DOM.setValue('rate', 'Error: No rate available');
console.error('Rate not available in response');
}
},
(error) => {
DOM.setValue('rate', 'Error: Rate lookup failed');
console.error('Error fetching rate data:', error);
}
);
},
setRate: (valueChanged) => {
const elements = {
coinFrom: DOM.get('coin_from'),
coinTo: DOM.get('coin_to'),
amtFrom: DOM.get('amt_from'),
amtTo: DOM.get('amt_to'),
rate: DOM.get('rate'),
rateLock: DOM.get('rate_lock'),
swapType: DOM.get('swap_type')
};
if (!elements.coinFrom || !elements.coinTo ||
!elements.amtFrom || !elements.amtTo || !elements.rate) {
console.log('Required elements for setRate not found');
return;
}
const values = {
coinFrom: elements.coinFrom.value,
coinTo: elements.coinTo.value,
amtFrom: elements.amtFrom.value,
amtTo: elements.amtTo.value,
rate: elements.rate.value,
lockRate: elements.rate.value == '' ? false :
(elements.rateLock ? elements.rateLock.checked : false)
};
if (valueChanged === 'coin_from' || valueChanged === 'coin_to') {
DOM.setValue('rate', '');
return;
}
if (elements.swapType) {
SwapTypeManager.setSwapTypeEnabled(
values.coinFrom,
values.coinTo,
elements.swapType
);
}
if (values.coinFrom == '-1' || values.coinTo == '-1') {
return;
}
let params = 'coin_from=' + values.coinFrom + '&coin_to=' + values.coinTo;
if (valueChanged == 'rate' ||
(values.lockRate && valueChanged == 'amt_from') ||
(values.amtTo == '' && valueChanged == 'amt_from')) {
if (values.rate == '' || (values.amtFrom == '' && values.amtTo == '')) {
return;
} else if (values.amtFrom == '' && values.amtTo != '') {
if (valueChanged == 'amt_from') {
return;
}
params += '&rate=' + values.rate + '&amt_to=' + values.amtTo;
} else {
params += '&rate=' + values.rate + '&amt_from=' + values.amtFrom;
}
} else if (values.lockRate && valueChanged == 'amt_to') {
if (values.amtTo == '' || values.rate == '') {
return;
}
params += '&amt_to=' + values.amtTo + '&rate=' + values.rate;
} else {
if (values.amtFrom == '' || values.amtTo == '') {
return;
}
params += '&amt_from=' + values.amtFrom + '&amt_to=' + values.amtTo;
}
Ajax.post('/json/rate', params,
(response) => {
if (response.hasOwnProperty('rate')) {
DOM.setValue('rate', response.rate);
} else if (response.hasOwnProperty('amount_to')) {
DOM.setValue('amt_to', response.amount_to);
} else if (response.hasOwnProperty('amount_from')) {
DOM.setValue('amt_from', response.amount_from);
}
},
(error) => {
console.error('Rate calculation failed:', error);
}
);
}
};
function set_rate(valueChanged) {
RateManager.setRate(valueChanged);
}
function lookup_rates() {
RateManager.lookupRates();
}
function getRateInferred(event) {
RateManager.getRateInferred(event);
}
const SwapTypeManager = {
adaptor_sig_only_coins: ['6', '9', '8', '7', '13', '18', '17'],
secret_hash_only_coins: ['11', '12'],
setSwapTypeEnabled: (coinFrom, coinTo, swapTypeElement) => {
if (!swapTypeElement) return;
let makeHidden = false;
coinFrom = String(coinFrom);
coinTo = String(coinTo);
if (SwapTypeManager.adaptor_sig_only_coins.includes(coinFrom) ||
SwapTypeManager.adaptor_sig_only_coins.includes(coinTo)) {
swapTypeElement.disabled = true;
swapTypeElement.value = 'xmr_swap';
makeHidden = true;
swapTypeElement.classList.add('select-disabled');
} else if (SwapTypeManager.secret_hash_only_coins.includes(coinFrom) ||
SwapTypeManager.secret_hash_only_coins.includes(coinTo)) {
swapTypeElement.disabled = true;
swapTypeElement.value = 'seller_first';
makeHidden = true;
swapTypeElement.classList.add('select-disabled');
} else {
swapTypeElement.disabled = false;
swapTypeElement.classList.remove('select-disabled');
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);
}
}
};
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'); event.target.classList.remove('error');
}); });
}); });
}, });
updateDisabledStyles: () => { const selects = document.querySelectorAll('select.disabled-select');
document.querySelectorAll('select.disabled-select').forEach(select => { for (const select of selects) {
if (select.disabled) { if (select.disabled) {
select.classList.add('disabled-select-enabled'); select.classList.add('disabled-select-enabled');
} else { } else {
select.classList.remove('disabled-select-enabled'); select.classList.remove('disabled-select-enabled');
} }
}); }
document.querySelectorAll('input.disabled-input, input[type="checkbox"].disabled-input').forEach(input => {
const inputs = document.querySelectorAll('input.disabled-input, input[type="checkbox"].disabled-input');
for (const input of inputs) {
if (input.readOnly) { if (input.readOnly) {
input.classList.add('disabled-input-enabled'); input.classList.add('disabled-input-enabled');
} else { } else {
input.classList.remove('disabled-input-enabled'); input.classList.remove('disabled-input-enabled');
} }
});
},
setupCustomSelects: () => {
const selectCache = {};
function updateSelectCache(select) {
if (!select || !select.options || select.selectedIndex === undefined) return;
const selectedOption = select.options[select.selectedIndex];
if (!selectedOption) return;
const image = selectedOption.getAttribute('data-image');
const name = selectedOption.textContent.trim();
selectCache[select.id] = { image, name };
}
function setSelectData(select) {
if (!select || !select.options || select.selectedIndex === undefined) return;
const selectedOption = select.options[select.selectedIndex];
if (!selectedOption) return;
const image = selectedOption.getAttribute('data-image') || '';
const name = selectedOption.textContent.trim();
select.style.backgroundImage = image ? `url(${image}?${new Date().getTime()})` : '';
const selectImage = select.nextElementSibling?.querySelector('.select-image');
if (selectImage) {
selectImage.src = image;
}
const selectNameElement = select.nextElementSibling?.querySelector('.select-name');
if (selectNameElement) {
selectNameElement.textContent = name;
}
updateSelectCache(select);
}
function setupCustomSelect(select) {
if (!select) return;
const options = select.querySelectorAll('option');
const selectIcon = select.parentElement?.querySelector('.select-icon');
const selectImage = select.parentElement?.querySelector('.select-image');
if (!options || !selectIcon || !selectImage) return;
options.forEach(option => {
const image = option.getAttribute('data-image');
if (image) {
option.style.backgroundImage = `url(${image})`;
}
});
const storedValue = Storage.getRaw(select.name);
if (storedValue && select.value == '-1') {
select.value = storedValue;
}
select.addEventListener('change', () => {
setSelectData(select);
Storage.setRaw(select.name, select.value);
});
setSelectData(select);
selectIcon.style.display = 'none';
selectImage.style.display = 'none';
}
const selectIcons = document.querySelectorAll('.custom-select .select-icon');
const selectImages = document.querySelectorAll('.custom-select .select-image');
const selectNames = document.querySelectorAll('.custom-select .select-name');
selectIcons.forEach(icon => icon.style.display = 'none');
selectImages.forEach(image => image.style.display = 'none');
selectNames.forEach(name => name.style.display = 'none');
const customSelects = document.querySelectorAll('.custom-select select');
customSelects.forEach(setupCustomSelect);
}
};
function initializeApp() {
handleNewOfferAddress();
DOM.addEvent('get_rate_inferred_button', 'click', RateManager.getRateInferred);
const coinFrom = DOM.get('coin_from');
const coinTo = DOM.get('coin_to');
const swapType = DOM.get('swap_type');
if (coinFrom && coinTo && swapType) {
SwapTypeManager.setSwapTypeEnabled(coinFrom.value, coinTo.value, swapType);
coinFrom.addEventListener('change', function() {
SwapTypeManager.setSwapTypeEnabled(this.value, coinTo.value, swapType);
RateManager.setRate('coin_from');
});
coinTo.addEventListener('change', function() {
SwapTypeManager.setSwapTypeEnabled(coinFrom.value, this.value, swapType);
RateManager.setRate('coin_to');
});
}
['amt_from', 'amt_to', 'rate'].forEach(id => {
DOM.addEvent(id, 'change', function() {
RateManager.setRate(id);
});
DOM.addEvent(id, 'input', function() {
RateManager.setRate(id);
});
});
DOM.addEvent('rate_lock', 'change', function() {
if (DOM.getValue('rate')) {
RateManager.setRate('rate');
}
});
DOM.addEvent('lookup_rates_button', 'click', RateManager.lookupRates);
UIEnhancer.handleErrorHighlighting();
UIEnhancer.updateDisabledStyles();
UIEnhancer.setupCustomSelects();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeApp);
} else {
initializeApp();
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -44,7 +44,7 @@
</th> </th>
<th class="p-0"> <th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-left"> <div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-left">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Receive</span> <span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Send</span>
</div> </div>
</th> </th>
<th class="p-0"> <th class="p-0">
@@ -54,7 +54,7 @@
</th> </th>
<th class="p-0"> <th class="p-0">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-right"> <div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-right">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Send</span> <span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Receive</span>
</div> </div>
</th> </th>
<th class="p-0"> <th class="p-0">
@@ -113,6 +113,6 @@
</div> </div>
</section> </section>
<script src="/static/js/swaps_in_progress.js"></script> <script src="/static/js/active.js"></script>
{% include 'footer.html' %} {% 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> <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> </div>
{% endif %} {% endif %}
{% if debug_ui_mode == true %}
<div class="w-full md:w-auto p-1.5"> <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> <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> </div>
{% endif %} {% endif %}
{% endif %}
{% if data.can_abandon == true and not edit_bid %} {% if data.can_abandon == true and not edit_bid %}
<div class="w-full md:w-auto p-1.5"> <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> <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>
</div> </div>
</section> </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 }}"> <input type="hidden" name="formid" value="{{ form_id }}">
</div> </div>
</div> </div>
@@ -585,74 +562,9 @@
</div> </div>
</form> </form>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { function confirmPopup(name) {
let confirmCallback = null; return confirm(name + " Bid - Are you sure?");
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 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> </script>
</div> </div>
{% include 'footer.html' %} {% include 'footer.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> <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> </div>
{% endif %} {% endif %}
{% if debug_ui_mode == true %}
<div class="w-full md:w-auto p-1.5"> <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> <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> </div>
{% endif %} {% endif %}
{% endif %}
{% if data.can_abandon == true %} {% if data.can_abandon == true %}
<div class="w-full md:w-auto p-1.5"> <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> <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> </div>
<script src="/static/js/bids_sentreceived.js"></script> <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' %} {% include 'footer.html' %}

View File

@@ -25,7 +25,7 @@
<div class="flex items-center"> <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-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">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> <p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
{{ love_svg | safe }} {{ love_svg | safe }}
</div> </div>
@@ -43,3 +43,68 @@
</div> </div>
</div> </div>
</section> </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,44 +9,42 @@
swap_in_progress_green_svg, available_bids_svg, your_offers_svg, bids_received_svg, swap_in_progress_green_svg, available_bids_svg, your_offers_svg, bids_received_svg,
bids_sent_svg, header_arrow_down_svg, love_svg %} bids_sent_svg, header_arrow_down_svg, love_svg %}
<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
{% if refresh %} {% if refresh %}
<meta http-equiv="refresh" content="{{ refresh }}"> <meta http-equiv="refresh" content="{{ refresh }}">
{% endif %} {% endif %}
<title>(BSX) BasicSwap - v{{ version }}</title>
<link rel="icon" sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png"> <!-- Scripts -->
<!-- CSS Stylesheets -->> <script src="/static/js/libs/chart.js"></script>
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet"> <script src="/static/js/libs/chartjs-adapter-date-fns.bundle.min.js"></script>
<script src="/static/js/main.js"></script>
<script src="/static/js/tabs.js"></script>
<script src="/static/js/dropdown.js"></script>
<script src="/static/js/libs/popper.js"></script>
<script src="/static/js/libs/tippy.js"></script>
<script src="/static/js/tooltips.js"></script>
<!-- Styles -->
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet"> <link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
<!-- Custom styles -->
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet"> <link 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> <script>
function getAPIKeys() { document.addEventListener('DOMContentLoaded', () => {
return { const tooltipManager = TooltipManager.initialize();
cryptoCompare: "{{ chart_api_key|safe }}", tooltipManager.initializeTooltips();
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() { </script>
return {
port: window.ws_port || '11700',
fallbackPort: '11700'
};
};
})();
(function() { <!-- Dark mode initialization -->
<script>
const isDarkMode = localStorage.getItem('color-theme') === 'dark' || const isDarkMode = localStorage.getItem('color-theme') === 'dark' ||
(!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches); (!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
@@ -54,38 +52,78 @@
localStorage.setItem('color-theme', 'dark'); localStorage.setItem('color-theme', 'dark');
} }
document.documentElement.classList.toggle('dark', isDarkMode); document.documentElement.classList.toggle('dark', isDarkMode);
})();
</script> </script>
<!-- Third-party Libraries -->
<script src="/static/js/libs/chart.js"></script> <!-- Shutdown modal functionality -->
<script src="/static/js/libs/chartjs-adapter-date-fns.bundle.min.js"></script> <script>
<script src="/static/js/libs/popper.js"></script> document.addEventListener('DOMContentLoaded', function() {
<script src="/static/js/libs/tippy.js"></script> const shutdownButtons = document.querySelectorAll('.shutdown-button');
<!-- UI Components --> const shutdownModal = document.getElementById('shutdownModal');
<script src="/static/js/ui/tabs.js"></script> const closeModalButton = document.getElementById('closeShutdownModal');
<script src="/static/js/ui/dropdown.js"></script> const confirmShutdownButton = document.getElementById('confirmShutdown');
<!-- Core functionality --> const shutdownWarning = document.getElementById('shutdownWarning');
<script src="/static/js/modules/coin-manager.js"></script>
<script src="/static/js/modules/config-manager.js"></script> function updateShutdownButtons() {
<script src="/static/js/modules/cache-manager.js"></script> const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
<script src="/static/js/modules/cleanup-manager.js"></script> shutdownButtons.forEach(button => {
<script src="/static/js/modules/websocket-manager.js"></script> if (activeSwaps > 0) {
<script src="/static/js/modules/network-manager.js"></script> button.classList.add('shutdown-disabled');
<script src="/static/js/modules/api-manager.js"></script> button.setAttribute('data-disabled', 'true');
<script src="/static/js/modules/price-manager.js"></script> button.setAttribute('title', 'Caution: Swaps in progress');
<script src="/static/js/modules/tooltips-manager.js"></script> } else {
<script src="/static/js/modules/notification-manager.js"></script> button.classList.remove('shutdown-disabled');
<script src="/static/js/modules/identity-manager.js"></script> button.removeAttribute('data-disabled');
<script src="/static/js/modules/summary-manager.js"></script> button.removeAttribute('title');
{% 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> function showShutdownModal() {
<!-- Main application script --> const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
<script src="/static/js/global.js"></script> 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> </head>
<body class="dark:bg-gray-700"> <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 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="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"> <div class="flex items-center justify-center min-h-screen p-4 relative z-10">
@@ -686,3 +724,194 @@
</div> </div>
</div> </div>
</section> </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 %} {% from 'style.html' import circular_info_messages_svg, green_cross_close_svg, red_cross_close_svg, circular_error_messages_svg %}
{% for m in messages %} {% for m in messages %}
<section class="py-4 px-6" id="messages_{{ m[0] }}" role="alert"> <section class="py-4" id="messages_{{ m[0] }}" role="alert">
<div class="lg:container mx-auto"> <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="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 flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2"> <div class="flex-1 p-2">
@@ -27,8 +27,8 @@
</section> </section>
{% endfor %} {% endfor %}
{% if err_messages %} {% if err_messages %}
<section class="py-4 px-6" id="err_messages_{{ err_messages[0][0] }}" role="alert"> <section class="py-4" id="err_messages_{{ err_messages[0][0] }}" role="alert">
<div class="lg:container mx-auto"> <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="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 flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-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 bold">Revoked</td>
<td class="py-3 px-6">{{ data.was_revoked }}</td> <td class="py-3 px-6">{{ data.was_revoked }}</td>
</tr> </tr>
{% if data.sent %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Auto Accept Type</td> <td class="py-3 px-6 bold">Auto Accept Strategy</td>
<td class="py-3 px-6"> <td class="py-3 px-6">
{% if data.auto_accept_type is none %} Unknown {% if data.automation_strat_id == -1 %} None {% else %}
{% elif data.auto_accept_type == 0 %} Bids are accepted manually <a href="/automationstrategy/{{ data.automation_strat_id }}">{{ data.automation_strat_label }}</a>
{% 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 }})
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% endif %}
{% if data.xmr_type == true %} {% 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"> <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> <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" name="bid_amount_send"
value="" value=""
max="{{ data.amt_to }}" 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"> <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 }}) max {{ data.amt_to }} ({{ data.tla_to }})
</div> </div>
@@ -506,7 +505,7 @@ if (document.readyState === 'loading') {
name="bid_amount" name="bid_amount"
value="" value=""
max="{{ data.amt_from }}" 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"> <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 }}) max {{ data.amt_from }} ({{ data.tla_from }})
</div> </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 %} {% 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"> <div class="container mx-auto">
<section class="p-5 mt-5"> <section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2"> <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 %} {% 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"> <div class="container mx-auto">
<section class="p-5 mt-5"> <section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2"> <div class="flex flex-wrap items-center -m-2">
@@ -69,7 +70,7 @@
<div class="container px-4 mx-auto"> <div class="container px-4 mx-auto">
<div class="p-8 bg-coolGray-100 dark:bg-gray-500 rounded-xl"> <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> <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="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="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="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
@@ -116,6 +117,63 @@
</div> </div>
</div> </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="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="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> <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>
<div class="flex form-check form-check-inline mt-5"> <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"> <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> <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> <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> </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="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="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="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="w-full md:flex-1 p-3">
<div class="flex form-check form-check-inline"> <div class="flex form-check form-check-inline">
<div class="flex items-center h-5"> <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"> <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>
<div class="ml-2 text-sm"> <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> <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>
<div class="flex mt-2 form-check form-check-inline"> <div class="flex mt-2 form-check form-check-inline">
<div class="flex items-center h-5"> <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"> <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>
<div class="ml-2 text-sm"> <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> <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> </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="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="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="flex flex-wrap -m-3 w-full sm:w-auto px-4 mb-6 sm:mb-0">
@@ -331,6 +413,225 @@
</div> </div>
</div> </div>
</section> </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> </div>
<script src="static/js/new_offer.js"></script> <script src="static/js/new_offer.js"></script>
{% include 'footer.html' %} {% 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 %} {% 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"> <div class="container mx-auto">
<section class="p-5 mt-5"> <section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2"> <div class="flex flex-wrap items-center -m-2">

View File

@@ -8,6 +8,22 @@
}; };
</script> </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"> <section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto"> <div class="lg:container mx-auto">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-gray-500 rounded-md overflow-hidden"> <div class="relative py-8 px-8 bg-coolGray-900 dark:bg-gray-500 rounded-md overflow-hidden">
@@ -136,12 +152,11 @@
'ETH': {'name': 'Ethereum', 'symbol': 'ETH', 'image': 'Ethereum.png', 'show': false}, 'ETH': {'name': 'Ethereum', 'symbol': 'ETH', 'image': 'Ethereum.png', 'show': false},
'DOGE': {'name': 'Dogecoin', 'symbol': 'DOGE', 'image': 'Dogecoin.png', 'show': true}, 'DOGE': {'name': 'Dogecoin', 'symbol': 'DOGE', 'image': 'Dogecoin.png', 'show': true},
'DCR': {'name': 'Decred', 'symbol': 'DCR', 'image': 'Decred.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}, 'ZANO': {'name': 'Zano', 'symbol': 'ZANO', 'image': 'Zano.png', 'show': false},
'WOW': {'name': 'Wownero', 'symbol': 'WOW', 'image': 'Wownero.png', 'show': true} '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 is string %}
{% if enabled_chart_coins == "" %} {% if enabled_chart_coins == "" %}
@@ -194,9 +209,9 @@
</div> </div>
</div> </div>
</section> </section>
<script src="/static/js/pricechart.js"></script>
{% endif %}
{% endif %}
<script src="/static/js/pricechart.js"></script>
<section> <section>
<div class="px-6 py-0 mt-5 h-full overflow-hidden"> <div class="px-6 py-0 mt-5 h-full overflow-hidden">
@@ -343,16 +358,16 @@
{% endif %} {% endif %}
</div> </div>
</th> </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"> <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="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> </div>
</th> </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"> <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="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> </div>
</th> </th>
<th class="p-0"> <th class="p-0">
@@ -402,5 +417,4 @@
<input type="hidden" name="formid" value="{{ form_id }}"> <input type="hidden" name="formid" value="{{ form_id }}">
<script src="/static/js/offers.js"></script> <script src="/static/js/offers.js"></script>
{% include 'footer.html' %} {% include 'footer.html' %}

View File

@@ -422,7 +422,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100"> <tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Enabled Coins</td> <td class="py-3 px-6 bold">Enabled Coins</td>
<td class="py-3 px-6"> <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 /> <br />
</label> </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}}"> <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 %} {% from 'style.html' import circular_info_messages_svg, green_cross_close_svg, red_cross_close_svg, circular_error_messages_svg %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
{% if refresh %} {% if refresh %}
<meta http-equiv="refresh" content="{{ refresh }}"> <meta http-equiv="refresh" content="{{ refresh }}">
{% endif %} {% endif %}
<title>(BSX) BasicSwap - v{{ version }}</title> <link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" />
<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"> <link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
<!-- Custom styles -->
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet"> <link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
<script src="/static/js/main.js"></script>
<script> <script>
function getAPIKeys() { const isDarkMode =
return { localStorage.getItem('color-theme') === 'dark' ||
cryptoCompare: "{{ chart_api_key|safe }}", (!localStorage.getItem('color-theme') &&
coinGecko: "{{ coingecko_api_key|safe }}" window.matchMedia('(prefers-color-scheme: dark)').matches);
};
}
(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')) { if (!localStorage.getItem('color-theme')) {
localStorage.setItem('color-theme', 'dark'); localStorage.setItem('color-theme', isDarkMode ? 'dark' : 'light');
} }
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>
<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); document.documentElement.classList.toggle('dark', isDarkMode);
})();
</script> </script>
<link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png"> <link rel=icon sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
<title>(BSX) BasicSwap - v{{ version }}</title> <title>(BSX) BasicSwap - v{{ version }}</title>
</head> </head>
@@ -172,6 +107,7 @@
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Password toggle functionality
const passwordToggle = document.querySelector('.js-password-toggle'); const passwordToggle = document.querySelector('.js-password-toggle');
if (passwordToggle) { if (passwordToggle) {
passwordToggle.addEventListener('change', function() { passwordToggle.addEventListener('change', function() {
@@ -190,6 +126,7 @@
}); });
} }
// Image toggling function
function toggleImages() { function toggleImages() {
const html = document.querySelector('html'); const html = document.querySelector('html');
const darkImages = document.querySelectorAll('.dark-image'); 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(); toggleImages();
}); });
</script> </script>

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,29 @@
{% include 'header.html' %} {% 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 %} {% 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="container mx-auto">
<div class="lg:container mx-auto"> <section class="p-5 mt-5">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden"> <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 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 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"> <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="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3 h-48"> <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"> <div class="flex items-center">
<h2 class="text-lg font-bold text-white tracking-tighter mr-2">Total Assets:</h2> <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> <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>
@@ -32,7 +46,7 @@
{% include 'inc_messages.html' %} {% include 'inc_messages.html' %}
<section class="py-4"> <section class="py-4">
<div class="container mx-auto"> <div class="container px-4 mx-auto">
<div class="flex flex-wrap -m-4"> <div class="flex flex-wrap -m-4">
{% for w in wallets %} {% for w in wallets %}
{% if w.havedata %} {% 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 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>
<div class="flex mb-2 justify-between items-center"> <div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white ">{{ w.ticker }} USD value:</h4> <h4 class="text-xs font-medium dark:text-white 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 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> </div>
{% if w.pending %} {% if w.pending %}
@@ -188,7 +202,448 @@
{% endfor %} {% endfor %}
</div> </div>
</section> </section>
</div>
{% include 'footer.html' %} {% 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> </body>
</html> </html>

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers # Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php. # file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -15,7 +15,6 @@ from basicswap.util import (
from basicswap.chainparams import ( from basicswap.chainparams import (
Coins, Coins,
chainparams, chainparams,
getCoinIdFromTicker,
) )
from basicswap.basicswap_util import ( from basicswap.basicswap_util import (
ActionTypes, ActionTypes,
@@ -35,6 +34,30 @@ from basicswap.basicswap_util import (
from basicswap.protocols.xmr_swap_1 import getChainBSplitKey, getChainBRemoteSplitKey from basicswap.protocols.xmr_swap_1 import getChainBSplitKey, getChainBRemoteSplitKey
PAGE_LIMIT = 1000 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): def getCoinType(coin_type_ind):
@@ -42,7 +65,7 @@ def getCoinType(coin_type_ind):
try: try:
return int(coin_type_ind) return int(coin_type_ind)
except Exception: except Exception:
return getCoinIdFromTicker(coin_type_ind) return tickerToCoinId(coin_type_ind)
def validateAmountString(amount, ci): def validateAmountString(amount, ci):
@@ -122,7 +145,7 @@ def get_data_with_pagination(data, filters):
offset = filters.get("offset", 0) offset = filters.get("offset", 0)
limit = filters.get("limit", PAGE_LIMIT) limit = filters.get("limit", PAGE_LIMIT)
return data[offset : offset + limit] return data[offset:offset + limit]
def getTxIdHex(bid, tx_type, suffix): def getTxIdHex(bid, tx_type, suffix):
@@ -644,12 +667,12 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
continue continue
if v["connection_type"] == "rpc": if v["connection_type"] == "rpc":
coins.append((int(k), getCoinName(k))) coins.append((int(k), getCoinName(k)))
if split_from: if split_from and k not in invalid_coins_from:
coins_from.append(coins[-1]) coins_from.append(coins[-1])
if with_variants and k == Coins.PART: if with_variants and k == Coins.PART:
for v in (Coins.PART_ANON, Coins.PART_BLIND): for v in (Coins.PART_ANON, Coins.PART_BLIND):
coins.append((int(v), getCoinName(v))) coins.append((int(v), getCoinName(v)))
if split_from: if split_from and v not in invalid_coins_from:
coins_from.append(coins[-1]) coins_from.append(coins[-1])
if with_variants and k == Coins.LTC: if with_variants and k == Coins.LTC:
for v in (Coins.LTC_MWEB,): 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}") logger.info(f"Attempting to resume from byte {read_start}")
def reporthook(blocknum, blocksize, totalsize): 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 read += blocksize
# totalsize excludes read_start # totalsize excludes read_start

View File

@@ -29,51 +29,3 @@ def rfc2440_hash_password(password, salt=None):
break break
rv = "16:" + salt.hex() + "60" + h.hexdigest() rv = "16:" + salt.hex() + "60" + h.hexdigest()
return rv.upper() 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> 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 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): 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.** **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. 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) #### Set the timezone (optional)
Edit the `.env` file in the docker directory, set TZ to your local timezone. 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): 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 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. Record the mnemonic from the output of the above command.
Start Basicswap: Start Basicswap:

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: After updating the code and rebuilding the container run:
basicswap/docker]$ docker-compose run --rm swapclient \ 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. 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 #### 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-check)
#:use-module (gnu packages python-crypto) #:use-module (gnu packages python-crypto)
#:use-module (gnu packages python-science) #:use-module (gnu packages python-science)
#:use-module (gnu packages python-web)
#:use-module (gnu packages python-xyz) #:use-module (gnu packages python-xyz)
#:use-module (gnu packages libffi) #:use-module (gnu packages libffi)
#:use-module (gnu packages license)) #:use-module (gnu packages license))
@@ -115,15 +114,15 @@
(define-public basicswap (define-public basicswap
(package (package
(name "basicswap") (name "basicswap")
(version "0.14.4") (version "0.14.3")
(source (origin (source (origin
(method git-fetch) (method git-fetch)
(uri (git-reference (uri (git-reference
(url "https://github.com/basicswap/basicswap") (url "https://github.com/basicswap/basicswap")
(commit "3c18a3ed26222bac22a9c15795bd8c6fae0b01ba"))) (commit "3b60472c04a58f26e33665f0eb0e88a558050c74")))
(sha256 (sha256
(base32 (base32
"02mwyklcw9320crcm8laiw4ba24xrazbg48whvdxnbmarcbipkd3")) "0xrli8mzigm0ryn28y28xvy4gc0358ck2036ncx5f1sj5s8dwfkh"))
(file-name (git-file-name name version)))) (file-name (git-file-name name version))))
(build-system pyproject-build-system) (build-system pyproject-build-system)
@@ -146,8 +145,7 @@
python-pyzmq python-pyzmq
python-gnupg python-gnupg
python-jinja2 python-jinja2
python-pysocks python-pysocks))
python-websocket-client))
(native-inputs (native-inputs
(list (list
python-hatchling python-hatchling

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