diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py
index 6f2c5bb..c1569e7 100644
--- a/basicswap/basicswap.py
+++ b/basicswap/basicswap.py
@@ -100,10 +100,7 @@ from .util.network import is_private_ip_address
from .util.smsg import smsgGetID
from .interface.base import Curves
from .interface.part import PARTInterface, PARTInterfaceAnon, PARTInterfaceBlind
-from .explorers import (
- default_chart_api_key,
- default_coingecko_api_key,
-)
+from .explorers import default_coingecko_api_key
from .script import OpCodes
from .messages_npb import (
ADSBidIntentAcceptMessage,
@@ -417,6 +414,21 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self._is_encrypted = None
self._is_locked = None
+ self._price_cache = {}
+ self._volume_cache = {}
+ self._historical_cache = {}
+ self._price_cache_lock = threading.Lock()
+ self._price_fetch_thread = None
+ self._price_fetch_running = False
+ self._last_price_fetch = 0
+ self._last_volume_fetch = 0
+ self.price_fetch_interval = self.get_int_setting(
+ "price_fetch_interval", 5 * 60, 60, 60 * 60
+ )
+ self.volume_fetch_interval = self.get_int_setting(
+ "volume_fetch_interval", 5 * 60, 60, 60 * 60
+ )
+
self._max_transient_errors = self.settings.get(
"max_transient_errors", 100
) # Number of retries before a bid will stop when encountering transient errors.
@@ -562,9 +574,55 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
random.seed(secrets.randbits(128))
+ self._prepare_rpc_pooling()
+
+ def _prepare_rpc_pooling(self):
+ if "rpc_connection_pool" not in self.settings:
+ self.settings["rpc_connection_pool"] = {
+ "enabled": True,
+ "max_connections_per_daemon": 5,
+ }
+ self._save_settings()
+
+ def _enable_rpc_pooling(self):
+ rpc_pool_settings = self.settings.get("rpc_connection_pool", {})
+ if rpc_pool_settings.get("enabled", False):
+ from basicswap import rpc
+ from basicswap import rpc_pool
+
+ rpc.enable_rpc_pooling(rpc_pool_settings)
+ rpc_pool.set_pool_logger(self.log)
+
+ def _save_settings(self):
+ import shutil
+ from basicswap import config as cfg
+
+ settings_path = os.path.join(self.data_dir, cfg.CONFIG_FILENAME)
+ settings_path_new = settings_path + ".new"
+ try:
+ shutil.copyfile(settings_path, settings_path + ".last")
+ with open(settings_path_new, "w") as fp:
+ json.dump(self.settings, fp, indent=4)
+ shutil.move(settings_path_new, settings_path)
+ self.log.debug("Settings saved to basicswap.json")
+ except Exception as e:
+ self.log.error(f"Failed to save settings: {str(e)}")
+
def finalise(self):
self.log.info("Finalising")
+ self._price_fetch_running = False
+ if self._price_fetch_thread and self._price_fetch_thread.is_alive():
+ self._price_fetch_thread.join(timeout=5)
+ self.log.info("Background price fetching stopped")
+
+ try:
+ from basicswap.rpc_pool import close_all_pools
+
+ close_all_pools()
+ except Exception as e:
+ self.log.debug(f"Error closing RPC pools: {e}")
+
try:
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
@@ -1058,6 +1116,19 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
elif self.coin_clients[coin]["connection_type"] == "passthrough":
self.coin_clients[coin]["interface"] = self.createPassthroughInterface(coin)
+ def _cleanupOldSettings(self):
+ settings_changed = False
+ deprecated_keys = ["chart_api_key", "chart_api_key_enc"]
+
+ for key in deprecated_keys:
+ if key in self.settings:
+ self.log.info(f"Removing deprecated setting: {key}")
+ self.settings.pop(key)
+ settings_changed = True
+
+ if settings_changed:
+ self._save_settings()
+
def start(self):
import platform
@@ -1078,6 +1149,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
"SQLite {} or higher required.".format(".".join(MIN_SQLITE_VERSION))
)
+ self._cleanupOldSettings()
+
upgradeDatabase(self, self.db_version)
upgradeDatabaseData(self, self.db_data_version)
@@ -1143,6 +1216,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.checkWalletSeed(c)
+ self._enable_rpc_pooling()
+
if "p2p_host" in self.settings:
network_key = self.getNetworkKey(1)
self._network = bsn.Network(
@@ -1225,6 +1300,13 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
else:
self.log.info("AMM autostart is disabled")
+ self._price_fetch_running = True
+ self._price_fetch_thread = threading.Thread(
+ target=self._backgroundPriceFetchLoop, daemon=True
+ )
+ self._price_fetch_thread.start()
+ self.log.info("Background price fetching started")
+
def stopDaemon(self, coin) -> None:
if coin in (Coins.XMR, Coins.DCR, Coins.WOW):
return
@@ -3867,7 +3949,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
# Set msg_buf.message_nets to let the remote node know what networks to respond on.
# bid.message_nets is a local field denoting the network/s to send to
- if offer.smsg_payload_version > 1:
+ if offer.smsg_payload_version is not None and offer.smsg_payload_version > 1:
msg_buf.message_nets = self.getMessageNetsString()
return msg_buf
@@ -3917,7 +3999,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
# Set msg_buf.message_nets to let the remote node know what networks to respond on.
# bid.message_nets is a local field denoting the network/s to send to
- if offer.smsg_payload_version > 1:
+ if offer.smsg_payload_version is not None and offer.smsg_payload_version > 1:
msg_buf.message_nets = self.getMessageNetsString()
return msg_buf
@@ -4004,7 +4086,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
# Set msg_buf.message_nets to let the remote node know what networks to respond on.
# bid.message_nets is a local field denoting the network/s to send to
- if offer.smsg_payload_version > 1:
+ if offer.smsg_payload_version is not None and offer.smsg_payload_version > 1:
msg_buf.message_nets = self.getMessageNetsString()
return msg_buf
@@ -7908,7 +7990,17 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.add(xmr_offer, cursor)
- self.notify(NT.OFFER_RECEIVED, {"offer_id": offer_id.hex()}, cursor)
+ self.notify(
+ NT.OFFER_RECEIVED,
+ {
+ "offer_id": offer_id.hex(),
+ "coin_from": offer_data.coin_from,
+ "coin_to": offer_data.coin_to,
+ "amount_from": offer_data.amount_from,
+ "amount_to": offer_data.amount_to,
+ },
+ cursor,
+ )
else:
existing_offer.setState(OfferStates.OFFER_RECEIVED)
existing_offer.pk_from = pk_from
@@ -11020,27 +11112,6 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
settings_copy["show_chart"] = new_value
settings_changed = True
- if "chart_api_key" in data:
- new_value = data["chart_api_key"]
- ensure(
- isinstance(new_value, str), "New chart_api_key value not a string"
- )
- ensure(len(new_value) <= 128, "New chart_api_key value too long")
- if all(c in string.hexdigits for c in new_value):
- if settings_copy.get("chart_api_key", "") != new_value:
- settings_copy["chart_api_key"] = new_value
- if "chart_api_key_enc" in settings_copy:
- settings_copy.pop("chart_api_key_enc")
- settings_changed = True
- else:
- # Encode value as hex to avoid escaping
- new_value = new_value.encode("UTF-8").hex()
- if settings_copy.get("chart_api_key_enc", "") != new_value:
- settings_copy["chart_api_key_enc"] = new_value
- if "chart_api_key" in settings_copy:
- settings_copy.pop("chart_api_key")
- settings_changed = True
-
if "coingecko_api_key" in data:
new_value = data["coingecko_api_key"]
ensure(
@@ -11048,7 +11119,15 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
"New coingecko_api_key value not a string",
)
ensure(len(new_value) <= 128, "New coingecko_api_keyvalue too long")
- if all(c in string.hexdigits for c in new_value):
+ if new_value == "":
+ if (
+ "coingecko_api_key" in settings_copy
+ or "coingecko_api_key_enc" in settings_copy
+ ):
+ settings_copy.pop("coingecko_api_key", None)
+ settings_copy.pop("coingecko_api_key_enc", None)
+ settings_changed = True
+ elif all(c in string.hexdigits for c in new_value):
if settings_copy.get("coingecko_api_key", "") != new_value:
settings_copy["coingecko_api_key"] = new_value
if "coingecko_api_key_enc" in settings_copy:
@@ -12301,11 +12380,15 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.closeDB(cursor, commit=False)
def getLockedState(self):
- if self._is_encrypted is None or self._is_locked is None:
- self._is_encrypted, self._is_locked = self.ci(
- Coins.PART
- ).isWalletEncryptedLocked()
- return self._is_encrypted, self._is_locked
+ try:
+ if self._is_encrypted is None or self._is_locked is None:
+ self._is_encrypted, self._is_locked = self.ci(
+ Coins.PART
+ ).isWalletEncryptedLocked()
+ return self._is_encrypted, self._is_locked
+ except Exception as e:
+ self.log.warning(f"getLockedState failed: {e}")
+ return False, False
def getExchangeName(self, coin_id: int, exchange_name: str) -> str:
if coin_id == Coins.BCH:
@@ -12322,6 +12405,195 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
return chainparams[use_coinid]["name"]
+ def _backgroundPriceFetchLoop(self):
+ while self._price_fetch_running:
+ try:
+ now = int(time.time())
+ if now - self._last_price_fetch >= self.price_fetch_interval:
+ self._fetchPricesBackground()
+ self._last_price_fetch = now
+ if now - self._last_volume_fetch >= self.volume_fetch_interval:
+ self._fetchVolumeBackground()
+ self._last_volume_fetch = now
+ except Exception as e:
+ self.log.error(f"Background price/volume fetch error: {e}")
+
+ for _ in range(60):
+ if not self._price_fetch_running:
+ break
+ time.sleep(1)
+
+ def _fetchPricesBackground(self):
+ all_coins = [c for c in Coins if c in chainparams]
+ if not all_coins:
+ return
+
+ for rate_source in ["coingecko.com"]:
+ try:
+ self._fetchPricesForSource(all_coins, rate_source, Fiat.USD)
+ except Exception as e:
+ self.log.warning(
+ f"Background price fetch from {rate_source} failed: {e}"
+ )
+
+ def _fetchVolumeBackground(self):
+ all_coins = [c for c in Coins if c in chainparams]
+ if not all_coins:
+ return
+
+ for rate_source in ["coingecko.com"]:
+ try:
+ self._fetchVolumeForSource(all_coins, rate_source)
+ except Exception as e:
+ self.log.warning(
+ f"Background volume fetch from {rate_source} failed: {e}"
+ )
+
+ def _fetchPricesForSource(self, coins_list, rate_source, currency_to):
+ now = int(time.time())
+ headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"}
+
+ exchange_name_map = {}
+ coin_ids = ""
+ for coin_id in coins_list:
+ if len(coin_ids) > 0:
+ coin_ids += ","
+ exchange_name = self.getExchangeName(coin_id, rate_source)
+ coin_ids += exchange_name
+ exchange_name_map[exchange_name] = coin_id
+
+ if rate_source == "coingecko.com":
+ ticker_to = fiatTicker(currency_to).lower()
+ api_key = get_api_key_setting(
+ self.settings,
+ "coingecko_api_key",
+ default_coingecko_api_key,
+ escape=True,
+ )
+ url = 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}"
+
+ js = json.loads(self.readURL(url, timeout=3, headers=headers))
+
+ with self._price_cache_lock:
+ for k, v in js.items():
+ coin_id = exchange_name_map[k]
+ cache_key = (coin_id, currency_to, rate_source)
+ self._price_cache[cache_key] = {
+ "rate": v[ticker_to],
+ "timestamp": now,
+ }
+
+ cursor = self.openDB()
+ try:
+ 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 js.items():
+ coin_id = exchange_name_map[k]
+ cursor.execute(
+ update_query,
+ {
+ "currency_from": coin_id,
+ "currency_to": currency_to,
+ "rate": v[ticker_to],
+ "rate_source": rate_source,
+ "last_updated": now,
+ },
+ )
+ if cursor.rowcount < 1:
+ cursor.execute(
+ insert_query,
+ {
+ "currency_from": coin_id,
+ "currency_to": currency_to,
+ "rate": v[ticker_to],
+ "rate_source": rate_source,
+ "last_updated": now,
+ },
+ )
+ self.commitDB()
+ finally:
+ self.closeDB(cursor, commit=False)
+
+ def _fetchVolumeForSource(self, coins_list, rate_source):
+ now = int(time.time())
+ headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"}
+
+ exchange_name_map = {}
+ coin_ids = ""
+ for coin_id in coins_list:
+ if len(coin_ids) > 0:
+ coin_ids += ","
+ exchange_name = self.getExchangeName(coin_id, rate_source)
+ coin_ids += exchange_name
+ exchange_name_map[exchange_name] = coin_id
+
+ if rate_source == "coingecko.com":
+ api_key = get_api_key_setting(
+ self.settings,
+ "coingecko_api_key",
+ default_coingecko_api_key,
+ escape=True,
+ )
+ url = f"https://api.coingecko.com/api/v3/simple/price?ids={coin_ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true"
+ if api_key != "":
+ url += f"&api_key={api_key}"
+
+ js = json.loads(self.readURL(url, timeout=3, headers=headers))
+
+ with self._price_cache_lock:
+ for k, v in js.items():
+ coin_id = exchange_name_map[k]
+ cache_key = (coin_id, rate_source)
+ volume_24h = v.get("usd_24h_vol")
+ price_change_24h = v.get("usd_24h_change")
+ self._volume_cache[cache_key] = {
+ "volume_24h": (
+ float(volume_24h) if volume_24h is not None else None
+ ),
+ "price_change_24h": (
+ float(price_change_24h)
+ if price_change_24h is not None
+ else 0.0
+ ),
+ "timestamp": now,
+ }
+
+ cursor = self.openDB()
+ try:
+ for k, v in js.items():
+ coin_id = exchange_name_map[k]
+ volume_24h = v.get("usd_24h_vol")
+ price_change_24h = v.get("usd_24h_change")
+ cursor.execute(
+ "INSERT OR REPLACE INTO coinvolume (coin_id, volume_24h, price_change_24h, source, last_updated) VALUES (:coin_id, :volume_24h, :price_change_24h, :rate_source, :last_updated)",
+ {
+ "coin_id": coin_id,
+ "volume_24h": (
+ str(volume_24h) if volume_24h is not None else "None"
+ ),
+ "price_change_24h": (
+ str(price_change_24h)
+ if price_change_24h is not None
+ else "0.0"
+ ),
+ "rate_source": rate_source,
+ "last_updated": now,
+ },
+ )
+ self.commitDB()
+ finally:
+ self.closeDB(cursor, commit=False)
+
def lookupFiatRates(
self,
coins_list,
@@ -12329,9 +12601,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
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}.")
+ 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")
@@ -12339,7 +12610,13 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
oldest_time_valid: int = now - saved_ttl
return_rates = {}
- headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"}
+ with self._price_cache_lock:
+ for coin_id in coins_list:
+ cache_key = (coin_id, currency_to, rate_source)
+ if cache_key in self._price_cache:
+ cached = self._price_cache[cache_key]
+ if cached["timestamp"] >= oldest_time_valid:
+ return_rates[coin_id] = cached["rate"]
cursor = self.openDB()
try:
@@ -12365,122 +12642,188 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
rows = cursor.execute(query, parameters)
for row in rows:
- return_rates[int(row[0])] = float(row[1])
+ coin_id = int(row[0])
+ if coin_id not in return_rates:
+ return_rates[coin_id] = float(row[1])
+
+ return return_rates
+ finally:
+ self.closeDB(cursor, commit=False)
+
+ def lookupVolume(
+ self,
+ coins_list,
+ rate_source: str = "coingecko.com",
+ saved_ttl: int = 300,
+ ):
+ coins_list_display = ", ".join([Coins(c).name for c in coins_list])
+ self.log.debug(f"lookupVolume {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_data = {}
+
+ with self._price_cache_lock:
+ for coin_id in coins_list:
+ cache_key = (coin_id, rate_source)
+ if cache_key in self._volume_cache:
+ cached = self._volume_cache[cache_key]
+ if cached["timestamp"] >= oldest_time_valid:
+ return_data[coin_id] = {
+ "volume_24h": cached["volume_24h"],
+ "price_change_24h": cached["price_change_24h"],
+ }
+
+ cursor = self.openDB()
+ try:
+ parameters = {
+ "rate_source": rate_source,
+ "oldest_time_valid": oldest_time_valid,
+ }
+ 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 coin_id, volume_24h, price_change_24h FROM coinvolume WHERE coin_id IN ({coins_list_query}) AND source = :rate_source AND last_updated >= :oldest_time_valid"
+ rows = cursor.execute(query, parameters)
+
+ for row in rows:
+ coin_id = int(row[0])
+ if coin_id in return_data:
+ continue
+ volume_24h = None
+ price_change_24h = 0.0
+ try:
+ if row[1] is not None and row[1] != "None":
+ volume_24h = float(row[1])
+ except (ValueError, TypeError):
+ pass
+ try:
+ if row[2] is not None and row[2] != "None":
+ price_change_24h = float(row[2])
+ except (ValueError, TypeError):
+ pass
+ return_data[coin_id] = {
+ "volume_24h": volume_24h,
+ "price_change_24h": price_change_24h,
+ }
+
+ return return_data
+ finally:
+ self.closeDB(cursor, commit=False)
+
+ def lookupHistoricalData(
+ self,
+ coins_list,
+ days: int = 1,
+ rate_source: str = "coingecko.com",
+ saved_ttl: int = 3600,
+ ):
+ coins_list_display = ", ".join([Coins(c).name for c in coins_list])
+ self.log.debug(f"lookupHistoricalData {coins_list_display}, days={days}.")
+ ensure(len(coins_list) > 0, "Must specify coin/s")
+ ensure(saved_ttl >= 0, "Invalid saved time")
+ ensure(days > 0, "Days must be positive")
+
+ now: int = int(time.time())
+ oldest_time_valid: int = now - saved_ttl
+ return_data = {}
+
+ headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"}
+
+ cursor = self.openDB()
+ try:
+ parameters = {
+ "rate_source": rate_source,
+ "oldest_time_valid": oldest_time_valid,
+ "days": days,
+ }
+ 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 coin_id, price_data FROM coinhistory WHERE coin_id IN ({coins_list_query}) AND days = :days AND source = :rate_source AND last_updated >= :oldest_time_valid"
+ rows = cursor.execute(query, parameters)
+
+ for row in rows:
+ return_data[int(row[0])] = json.loads(row[1])
need_coins = []
new_values = {}
- exchange_name_map = {}
for coin_id in coins_list:
- if coin_id not in return_rates:
+ if coin_id not in return_data:
need_coins.append(coin_id)
if len(need_coins) < 1:
- return return_rates
+ return return_data
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 coin_id in need_coins:
+ try:
+ exchange_name: str = self.getExchangeName(coin_id, rate_source)
+ url: str = (
+ f"https://api.coingecko.com/api/v3/coins/{exchange_name}/market_chart?vs_currency=usd&days={days}"
+ )
+ if api_key != "":
+ url += f"&api_key={api_key}"
- 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]
+ js = json.loads(self.readURL(url, timeout=5, headers=headers))
+
+ if "prices" in js:
+ return_data[coin_id] = js["prices"]
+ new_values[coin_id] = js["prices"]
+ except Exception as e:
+ self.log.warning(
+ f"Could not fetch historical data for {Coins(coin_id).name}: {e}"
+ )
+ return_data[coin_id] = []
else:
raise ValueError(f"Unknown rate source {rate_source}")
if len(new_values) < 1:
- return return_rates
+ return return_data
- # 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():
+ for coin_id, price_data in new_values.items():
cursor.execute(
- update_query,
+ "INSERT OR REPLACE INTO coinhistory (coin_id, days, price_data, source, last_updated) VALUES (:coin_id, :days, :price_data, :rate_source, :last_updated)",
{
- "currency_from": k,
- "currency_to": currency_to,
- "rate": v,
+ "coin_id": coin_id,
+ "days": days,
+ "price_data": json.dumps(price_data),
"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
+ return return_data
finally:
self.closeDB(cursor, commit=False)
@@ -12502,14 +12845,35 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if rate_sources.get("coingecko.com", True):
try:
- js = self.lookupFiatRates([int(coin_from), int(coin_to)])
- rate = float(js[int(coin_from)]) / float(js[int(coin_to)])
- js["rate_inferred"] = ci_to.format_amount(rate, conv_int=True, r=1)
+ price_coin_from = int(coin_from)
+ price_coin_to = int(coin_to)
- js[name_from] = {"usd": js[int(coin_from)]}
- js.pop(int(coin_from))
- js[name_to] = {"usd": js[int(coin_to)]}
- js.pop(int(coin_to))
+ if price_coin_from in (Coins.PART_BLIND, Coins.PART_ANON):
+ price_coin_from = Coins.PART
+ elif price_coin_from == Coins.LTC_MWEB:
+ price_coin_from = Coins.LTC
+
+ if price_coin_to in (Coins.PART_BLIND, Coins.PART_ANON):
+ price_coin_to = Coins.PART
+ elif price_coin_to == Coins.LTC_MWEB:
+ price_coin_to = Coins.LTC
+
+ if price_coin_from == price_coin_to:
+ rate = 1.0
+ js = self.lookupFiatRates([price_coin_from])
+ usd_price = js[price_coin_from]
+ js["rate_inferred"] = ci_to.format_amount(rate, conv_int=True, r=1)
+ js[name_from] = {"usd": usd_price}
+ js[name_to] = {"usd": usd_price}
+ js.pop(price_coin_from)
+ else:
+ js = self.lookupFiatRates([price_coin_from, price_coin_to])
+ rate = float(js[price_coin_from]) / float(js[price_coin_to])
+ js["rate_inferred"] = ci_to.format_amount(rate, conv_int=True, r=1)
+ js[name_from] = {"usd": js[price_coin_from]}
+ js.pop(price_coin_from)
+ js[name_to] = {"usd": js[price_coin_to]}
+ js.pop(price_coin_to)
rv["coingecko"] = js
except Exception as e:
diff --git a/basicswap/config.py b/basicswap/config.py
index cc010a0..ea76b54 100644
--- a/basicswap/config.py
+++ b/basicswap/config.py
@@ -9,6 +9,8 @@ import os
CONFIG_FILENAME = "basicswap.json"
BASICSWAP_DATADIR = os.getenv("BASICSWAP_DATADIR", os.path.join("~", ".basicswap"))
DEFAULT_ALLOW_CORS = False
+DEFAULT_RPC_POOL_ENABLED = True
+DEFAULT_RPC_POOL_MAX_CONNECTIONS = 5
TEST_DATADIRS = os.path.expanduser(os.getenv("DATADIRS", "/tmp/basicswap"))
DEFAULT_TEST_BINDIR = os.path.expanduser(
os.getenv("DEFAULT_TEST_BINDIR", os.path.join("~", ".basicswap", "bin"))
diff --git a/basicswap/db.py b/basicswap/db.py
index 2c3286d..a0a15d5 100644
--- a/basicswap/db.py
+++ b/basicswap/db.py
@@ -13,7 +13,7 @@ from enum import IntEnum, auto
from typing import Optional
-CURRENT_DB_VERSION = 31
+CURRENT_DB_VERSION = 32
CURRENT_DB_DATA_VERSION = 7
@@ -674,6 +674,28 @@ class CoinRates(Table):
last_updated = Column("integer")
+class CoinVolume(Table):
+ __tablename__ = "coinvolume"
+
+ record_id = Column("integer", primary_key=True, autoincrement=True)
+ coin_id = Column("integer")
+ volume_24h = Column("string")
+ price_change_24h = Column("string")
+ source = Column("string")
+ last_updated = Column("integer")
+
+
+class CoinHistory(Table):
+ __tablename__ = "coinhistory"
+
+ record_id = Column("integer", primary_key=True, autoincrement=True)
+ coin_id = Column("integer")
+ days = Column("integer")
+ price_data = Column("blob")
+ source = Column("string")
+ last_updated = Column("integer")
+
+
class MessageNetworks(Table):
__tablename__ = "message_networks"
diff --git a/basicswap/explorers.py b/basicswap/explorers.py
index 8d10eeb..b34f254 100644
--- a/basicswap/explorers.py
+++ b/basicswap/explorers.py
@@ -8,9 +8,6 @@
import json
-default_chart_api_key = (
- "95dd900af910656e0e17c41f2ddc5dba77d01bf8b0e7d2787634a16bd976c553"
-)
default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj"
diff --git a/basicswap/http_server.py b/basicswap/http_server.py
index 77bbe28..ba30795 100644
--- a/basicswap/http_server.py
+++ b/basicswap/http_server.py
@@ -14,7 +14,7 @@ import threading
import http.client
import base64
-from http.server import BaseHTTPRequestHandler, HTTPServer
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from jinja2 import Environment, PackageLoader
from socket import error as SocketError
from urllib import parse
@@ -169,15 +169,16 @@ class HttpHandler(BaseHTTPRequestHandler):
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
+ with self.server.session_lock:
+ 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]
+ if session_id in self.server.active_sessions:
+ del self.server.active_sessions[session_id]
return False
def log_error(self, format, *args):
@@ -195,10 +196,11 @@ class HttpHandler(BaseHTTPRequestHandler):
return None
form_data = parse.parse_qs(post_string)
form_id = form_data[b"formid"][0].decode("utf-8")
- if self.server.last_form_id.get(name, None) == form_id:
- messages.append("Prevented double submit for form {}.".format(form_id))
- return None
- self.server.last_form_id[name] = form_id
+ with self.server.form_id_lock:
+ if self.server.last_form_id.get(name, None) == form_id:
+ messages.append("Prevented double submit for form {}.".format(form_id))
+ return None
+ self.server.last_form_id[name] = form_id
return form_data
def render_template(
@@ -216,43 +218,48 @@ class HttpHandler(BaseHTTPRequestHandler):
args_dict["debug_mode"] = True
if swap_client.debug_ui:
args_dict["debug_ui_mode"] = True
- if swap_client.use_tor_proxy:
- args_dict["use_tor_proxy"] = True
+
+ is_authenticated = self.is_authenticated() or not swap_client.settings.get(
+ "client_auth_hash"
+ )
+
+ if is_authenticated:
+ if swap_client.use_tor_proxy:
+ args_dict["use_tor_proxy"] = True
+ try:
+ tor_state = get_tor_established_state(swap_client)
+ args_dict["tor_established"] = True if tor_state == "1" else False
+ except Exception:
+ args_dict["tor_established"] = False
+
+ from .ui.page_amm import get_amm_status, get_amm_active_count
+
try:
- tor_state = get_tor_established_state(swap_client)
- args_dict["tor_established"] = True if tor_state == "1" else False
- except Exception as e:
- args_dict["tor_established"] = False
- if swap_client.debug:
- swap_client.log.error(f"Error getting Tor state: {str(e)}")
- swap_client.log.error(traceback.format_exc())
+ args_dict["current_status"] = get_amm_status()
+ args_dict["amm_active_count"] = get_amm_active_count(swap_client)
+ except Exception:
+ args_dict["current_status"] = "stopped"
+ args_dict["amm_active_count"] = 0
- from .ui.page_amm import get_amm_status, get_amm_active_count
-
- try:
- args_dict["current_status"] = get_amm_status()
- args_dict["amm_active_count"] = get_amm_active_count(swap_client)
- except Exception as e:
- args_dict["current_status"] = "stopped"
+ if swap_client._show_notifications:
+ args_dict["notifications"] = swap_client.getNotifications()
+ else:
+ args_dict["current_status"] = "unknown"
args_dict["amm_active_count"] = 0
- if swap_client.debug:
- swap_client.log.error(f"Error getting AMM state: {str(e)}")
- swap_client.log.error(traceback.format_exc())
-
- if swap_client._show_notifications:
- args_dict["notifications"] = swap_client.getNotifications()
if "messages" in args_dict:
messages_with_ids = []
- for msg in args_dict["messages"]:
- messages_with_ids.append((self.server.msg_id_counter, msg))
- self.server.msg_id_counter += 1
+ with self.server.msg_id_lock:
+ for msg in args_dict["messages"]:
+ messages_with_ids.append((self.server.msg_id_counter, msg))
+ self.server.msg_id_counter += 1
args_dict["messages"] = messages_with_ids
if "err_messages" in args_dict:
err_messages_with_ids = []
- for msg in args_dict["err_messages"]:
- err_messages_with_ids.append((self.server.msg_id_counter, msg))
- self.server.msg_id_counter += 1
+ with self.server.msg_id_lock:
+ for msg in args_dict["err_messages"]:
+ err_messages_with_ids.append((self.server.msg_id_counter, msg))
+ self.server.msg_id_counter += 1
args_dict["err_messages"] = err_messages_with_ids
if self.path:
@@ -266,15 +273,27 @@ class HttpHandler(BaseHTTPRequestHandler):
args_dict["current_page"] = "index"
shutdown_token = os.urandom(8).hex()
- self.server.session_tokens["shutdown"] = shutdown_token
+ with self.server.session_lock:
+ self.server.session_tokens["shutdown"] = shutdown_token
args_dict["shutdown_token"] = shutdown_token
- encrypted, locked = swap_client.getLockedState()
- args_dict["encrypted"] = encrypted
- args_dict["locked"] = locked
+ if is_authenticated:
+ try:
+ encrypted, locked = swap_client.getLockedState()
+ args_dict["encrypted"] = encrypted
+ args_dict["locked"] = locked
+ except Exception as e:
+ args_dict["encrypted"] = False
+ args_dict["locked"] = False
+ if swap_client.debug:
+ swap_client.log.warning(f"Could not get wallet locked state: {e}")
+ else:
+ args_dict["encrypted"] = args_dict.get("encrypted", False)
+ args_dict["locked"] = args_dict.get("locked", False)
- if self.server.msg_id_counter >= 0x7FFFFFFF:
- self.server.msg_id_counter = 0
+ with self.server.msg_id_lock:
+ if self.server.msg_id_counter >= 0x7FFFFFFF:
+ self.server.msg_id_counter = 0
args_dict["version"] = version
@@ -364,7 +383,8 @@ class HttpHandler(BaseHTTPRequestHandler):
expires = datetime.now(timezone.utc) + timedelta(
minutes=SESSION_DURATION_MINUTES
)
- self.server.active_sessions[session_id] = {"expires": expires}
+ with self.server.session_lock:
+ self.server.active_sessions[session_id] = {"expires": expires}
cookie_header = self._set_session_cookie(session_id)
if is_json_request:
@@ -628,13 +648,15 @@ class HttpHandler(BaseHTTPRequestHandler):
if len(url_split) > 2:
token = url_split[2]
- expect_token = self.server.session_tokens.get("shutdown", None)
+ with self.server.session_lock:
+ expect_token = self.server.session_tokens.get("shutdown", None)
if token != expect_token:
return self.page_info("Unexpected token, still running.")
session_id = self._get_session_cookie()
- if session_id and session_id in self.server.active_sessions:
- del self.server.active_sessions[session_id]
+ with self.server.session_lock:
+ 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)
@@ -935,22 +957,28 @@ class HttpHandler(BaseHTTPRequestHandler):
return self.page_error(str(ex))
def do_GET(self):
- response = self.handle_http(200, self.path)
try:
- self.wfile.write(response)
- except SocketError as e:
- self.server.swap_client.log.debug(f"do_GET SocketError {e}")
+ response = self.handle_http(200, self.path)
+ try:
+ self.wfile.write(response)
+ except SocketError as e:
+ self.server.swap_client.log.debug(f"do_GET SocketError {e}")
+ finally:
+ pass
def do_POST(self):
- content_length = int(self.headers.get("Content-Length", 0))
- post_string = self.rfile.read(content_length)
-
- is_json = True if "json" in self.headers.get("Content-Type", "") else False
- response = self.handle_http(200, self.path, post_string, is_json)
try:
- self.wfile.write(response)
- except SocketError as e:
- self.server.swap_client.log.debug(f"do_POST SocketError {e}")
+ content_length = int(self.headers.get("Content-Length", 0))
+ post_string = self.rfile.read(content_length)
+
+ is_json = True if "json" in self.headers.get("Content-Type", "") else False
+ response = self.handle_http(200, self.path, post_string, is_json)
+ try:
+ self.wfile.write(response)
+ except SocketError as e:
+ self.server.swap_client.log.debug(f"do_POST SocketError {e}")
+ finally:
+ pass
def do_HEAD(self):
self.putHeaders(200, "text/html")
@@ -963,7 +991,9 @@ class HttpHandler(BaseHTTPRequestHandler):
self.end_headers()
-class HttpThread(threading.Thread, HTTPServer):
+class HttpThread(threading.Thread, ThreadingHTTPServer):
+ daemon_threads = True
+
def __init__(self, host_name, port_no, allow_cors, swap_client):
threading.Thread.__init__(self)
@@ -979,8 +1009,15 @@ class HttpThread(threading.Thread, HTTPServer):
self.env = env
self.msg_id_counter = 0
+ self.session_lock = threading.Lock()
+ self.form_id_lock = threading.Lock()
+ self.msg_id_lock = threading.Lock()
+
self.timeout = 60
- HTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
+ ThreadingHTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
+
+ if swap_client.debug:
+ swap_client.log.info("HTTP server initialized with threading support")
def stop(self):
self.stop_event.set()
diff --git a/basicswap/js_server.py b/basicswap/js_server.py
index 4b37d6f..a29d64e 100644
--- a/basicswap/js_server.py
+++ b/basicswap/js_server.py
@@ -1395,6 +1395,111 @@ def js_coinprices(self, url_split, post_string, is_json) -> bytes:
)
+def js_coinvolume(self, url_split, post_string, is_json) -> bytes:
+ swap_client = self.server.swap_client
+ post_data = {} if post_string == "" else getFormData(post_string, is_json)
+ if not have_data_entry(post_data, "coins"):
+ raise ValueError("Requires coins list.")
+
+ 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
+
+ volume_data = swap_client.lookupVolume(
+ coin_ids, rate_source=rate_source, saved_ttl=ttl
+ )
+
+ rv = {}
+ for k, v in volume_data.items():
+ if match_input_key:
+ rv[input_id_map[k]] = v
+ else:
+ rv[int(k)] = v
+ return bytes(
+ json.dumps({"source": rate_source, "data": rv}),
+ "UTF-8",
+ )
+
+
+def js_coinhistory(self, url_split, post_string, is_json) -> bytes:
+ swap_client = self.server.swap_client
+ post_data = {} if post_string == "" else getFormData(post_string, is_json)
+ if not have_data_entry(post_data, "coins"):
+ raise ValueError("Requires coins list.")
+
+ 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", 3600))
+ days: int = int(get_data_entry_or(post_data, "days", 1))
+
+ 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
+
+ historical_data = swap_client.lookupHistoricalData(
+ coin_ids, days=days, rate_source=rate_source, saved_ttl=ttl
+ )
+
+ rv = {}
+ for k, v in historical_data.items():
+ if match_input_key:
+ rv[input_id_map[k]] = v
+ else:
+ rv[int(k)] = v
+ return bytes(
+ json.dumps({"source": rate_source, "days": days, "data": rv}),
+ "UTF-8",
+ )
+
+
def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json)
@@ -1467,6 +1572,8 @@ endpoints = {
"readurl": js_readurl,
"active": js_active,
"coinprices": js_coinprices,
+ "coinvolume": js_coinvolume,
+ "coinhistory": js_coinhistory,
"messageroutes": js_messageroutes,
}
diff --git a/basicswap/rpc.py b/basicswap/rpc.py
index 49eff25..9a400c4 100644
--- a/basicswap/rpc.py
+++ b/basicswap/rpc.py
@@ -6,8 +6,10 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
+import logging
import traceback
import urllib
+import http.client
from xmlrpc.client import (
Fault,
Transport,
@@ -15,6 +17,35 @@ from xmlrpc.client import (
)
from .util import jsonDecimal
+_use_rpc_pooling = False
+_rpc_pool_settings = {}
+
+
+def enable_rpc_pooling(settings):
+ global _use_rpc_pooling, _rpc_pool_settings
+ _use_rpc_pooling = settings.get("enabled", False)
+ _rpc_pool_settings = settings
+
+
+class TimeoutTransport(Transport):
+ def __init__(self, timeout=10, *args, **kwargs):
+ self.timeout = timeout
+ super().__init__(*args, **kwargs)
+
+ def make_connection(self, host):
+ conn = http.client.HTTPConnection(host, timeout=self.timeout)
+ return conn
+
+
+class TimeoutSafeTransport(SafeTransport):
+ def __init__(self, timeout=10, *args, **kwargs):
+ self.timeout = timeout
+ super().__init__(*args, **kwargs)
+
+ def make_connection(self, host):
+ conn = http.client.HTTPSConnection(host, timeout=self.timeout)
+ return conn
+
class Jsonrpc:
# __getattr__ complicates extending ServerProxy
@@ -29,22 +60,40 @@ class Jsonrpc:
use_builtin_types=False,
*,
context=None,
+ timeout=10,
):
# establish a "logical" server connection
- # get the url
parsed = urllib.parse.urlparse(uri)
if parsed.scheme not in ("http", "https"):
raise OSError("unsupported XML-RPC protocol")
- self.__host = parsed.netloc
+
+ self.__auth = None
+ if "@" in parsed.netloc:
+ auth_part, host_port = parsed.netloc.rsplit("@", 1)
+ self.__host = host_port
+ if ":" in auth_part:
+ import base64
+
+ auth_bytes = auth_part.encode("utf-8")
+ auth_b64 = base64.b64encode(auth_bytes).decode("ascii")
+ self.__auth = f"Basic {auth_b64}"
+ else:
+ self.__host = parsed.netloc
+
+ if not self.__host:
+ raise ValueError(f"Invalid or empty hostname in URI: {uri}")
self.__handler = parsed.path
if not self.__handler:
self.__handler = "/RPC2"
if transport is None:
- handler = SafeTransport if parsed.scheme == "https" else Transport
+ handler = (
+ TimeoutSafeTransport if parsed.scheme == "https" else TimeoutTransport
+ )
extra_kwargs = {}
transport = handler(
+ timeout=timeout,
use_datetime=use_datetime,
use_builtin_types=use_builtin_types,
**extra_kwargs,
@@ -62,6 +111,7 @@ class Jsonrpc:
self.__transport.close()
def json_request(self, method, params):
+ connection = None
try:
connection = self.__transport.make_connection(self.__host)
headers = self.__transport._extra_headers[:]
@@ -71,6 +121,10 @@ class Jsonrpc:
connection.putrequest("POST", self.__handler)
headers.append(("Content-Type", "application/json"))
headers.append(("User-Agent", "jsonrpc"))
+
+ if self.__auth:
+ headers.append(("Authorization", self.__auth))
+
self.__transport.send_headers(connection, headers)
self.__transport.send_content(
connection,
@@ -79,18 +133,29 @@ class Jsonrpc:
self.__request_id += 1
resp = connection.getresponse()
- return resp.read()
+ result = resp.read()
+
+ connection.close()
+
+ return result
except Fault:
raise
except Exception:
- # All unexpected errors leave connection in
- # a strange state, so we clear it.
self.__transport.close()
raise
+ finally:
+ if connection is not None:
+ try:
+ connection.close()
+ except Exception:
+ pass
def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
+ if _use_rpc_pooling:
+ return callrpc_pooled(rpc_port, auth, method, params, wallet, host)
+
try:
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
if wallet is not None:
@@ -101,7 +166,6 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
x.close()
r = json.loads(v.decode("utf-8"))
except Exception as ex:
- traceback.print_exc()
raise ValueError(f"RPC server error: {ex}, method: {method}")
if "error" in r and r["error"] is not None:
@@ -110,6 +174,62 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
return r["result"]
+def callrpc_pooled(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
+ from .rpc_pool import get_rpc_pool
+ import http.client
+ import socket
+
+ url = "http://{}@{}:{}/".format(auth, host, rpc_port)
+ if wallet is not None:
+ url += "wallet/" + urllib.parse.quote(wallet)
+
+ max_connections = _rpc_pool_settings.get("max_connections_per_daemon", 5)
+ pool = get_rpc_pool(url, max_connections)
+
+ max_retries = 2
+
+ for attempt in range(max_retries):
+ conn = pool.get_connection()
+
+ try:
+ v = conn.json_request(method, params)
+ r = json.loads(v.decode("utf-8"))
+
+ if "error" in r and r["error"] is not None:
+ pool.discard_connection(conn)
+ raise ValueError("RPC error " + str(r["error"]))
+
+ pool.return_connection(conn)
+ return r["result"]
+
+ except (
+ http.client.RemoteDisconnected,
+ http.client.IncompleteRead,
+ http.client.BadStatusLine,
+ ConnectionError,
+ ConnectionResetError,
+ ConnectionAbortedError,
+ BrokenPipeError,
+ TimeoutError,
+ socket.timeout,
+ socket.error,
+ OSError,
+ ) as ex:
+ pool.discard_connection(conn)
+ if attempt < max_retries - 1:
+ continue
+ logging.warning(
+ f"RPC server error after {max_retries} attempts: {ex}, method: {method}"
+ )
+ raise ValueError(f"RPC server error: {ex}, method: {method}")
+ except ValueError:
+ raise
+ except Exception as ex:
+ pool.discard_connection(conn)
+ logging.error(f"Unexpected RPC error: {ex}, method: {method}")
+ raise ValueError(f"RPC server error: {ex}, method: {method}")
+
+
def openrpc(rpc_port, auth, wallet=None, host="127.0.0.1"):
try:
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
@@ -142,5 +262,6 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
def escape_rpcauth(auth_str: str) -> str:
username, password = auth_str.split(":", 1)
+ username = urllib.parse.quote(username, safe="")
password = urllib.parse.quote(password, safe="")
return f"{username}:{password}"
diff --git a/basicswap/rpc_pool.py b/basicswap/rpc_pool.py
new file mode 100644
index 0000000..3042937
--- /dev/null
+++ b/basicswap/rpc_pool.py
@@ -0,0 +1,131 @@
+# -*- 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 queue
+import threading
+import time
+from basicswap.rpc import Jsonrpc
+
+
+class RPCConnectionPool:
+ def __init__(
+ self, url, max_connections=5, timeout=10, logger=None, max_idle_time=300
+ ):
+ self.url = url
+ self.max_connections = max_connections
+ self.timeout = timeout
+ self.logger = logger
+ self.max_idle_time = max_idle_time
+ self._pool = queue.Queue(maxsize=max_connections)
+ self._lock = threading.Lock()
+ self._created_connections = 0
+ self._connection_timestamps = {}
+
+ def get_connection(self):
+ try:
+ conn_data = self._pool.get(block=False)
+ conn, timestamp = (
+ conn_data if isinstance(conn_data, tuple) else (conn_data, time.time())
+ )
+
+ if time.time() - timestamp > self.max_idle_time:
+ if self.logger:
+ self.logger.debug(
+ f"RPC pool: discarding stale connection (idle for {time.time() - timestamp:.1f}s)"
+ )
+ conn.close()
+ with self._lock:
+ if self._created_connections > 0:
+ self._created_connections -= 1
+ return self._create_new_connection()
+
+ return conn
+ except queue.Empty:
+ return self._create_new_connection()
+
+ def _create_new_connection(self):
+ with self._lock:
+ if self._created_connections < self.max_connections:
+ self._created_connections += 1
+ return Jsonrpc(self.url)
+
+ try:
+ conn_data = self._pool.get(block=True, timeout=self.timeout)
+ conn, timestamp = (
+ conn_data if isinstance(conn_data, tuple) else (conn_data, time.time())
+ )
+
+ if time.time() - timestamp > self.max_idle_time:
+ if self.logger:
+ self.logger.debug(
+ f"RPC pool: discarding stale connection (idle for {time.time() - timestamp:.1f}s)"
+ )
+ conn.close()
+ with self._lock:
+ if self._created_connections > 0:
+ self._created_connections -= 1
+ return Jsonrpc(self.url)
+
+ return conn
+ except queue.Empty:
+ if self.logger:
+ self.logger.warning(
+ f"RPC pool: timeout waiting for connection, creating temporary connection for {self.url}"
+ )
+ return Jsonrpc(self.url)
+
+ def return_connection(self, conn):
+ try:
+ self._pool.put((conn, time.time()), block=False)
+ except queue.Full:
+ conn.close()
+ with self._lock:
+ if self._created_connections > 0:
+ self._created_connections -= 1
+
+ def discard_connection(self, conn):
+ conn.close()
+ with self._lock:
+ if self._created_connections > 0:
+ self._created_connections -= 1
+
+ def close_all(self):
+ while not self._pool.empty():
+ try:
+ conn_data = self._pool.get(block=False)
+ conn = conn_data[0] if isinstance(conn_data, tuple) else conn_data
+ conn.close()
+ except queue.Empty:
+ break
+ with self._lock:
+ self._created_connections = 0
+ self._connection_timestamps.clear()
+
+
+_rpc_pools = {}
+_pool_lock = threading.Lock()
+_pool_logger = None
+
+
+def set_pool_logger(logger):
+ global _pool_logger
+ _pool_logger = logger
+
+
+def get_rpc_pool(url, max_connections=5):
+ with _pool_lock:
+ if url not in _rpc_pools:
+ _rpc_pools[url] = RPCConnectionPool(
+ url, max_connections, logger=_pool_logger
+ )
+ return _rpc_pools[url]
+
+
+def close_all_pools():
+ with _pool_lock:
+ for pool in _rpc_pools.values():
+ pool.close_all()
+ _rpc_pools.clear()
diff --git a/basicswap/static/js/modules/api-manager.js b/basicswap/static/js/modules/api-manager.js
index 0a332fe..12b0914 100644
--- a/basicswap/static/js/modules/api-manager.js
+++ b/basicswap/static/js/modules/api-manager.js
@@ -4,34 +4,34 @@ const ApiManager = (function() {
isInitialized: false
};
- const config = {
- requestTimeout: 60000,
- retryDelays: [5000, 15000, 30000],
- rateLimits: {
- coingecko: {
- requestsPerMinute: 50,
- minInterval: 1200
- },
- cryptocompare: {
- requestsPerMinute: 30,
- minInterval: 2000
+ function getConfig() {
+ return window.config || window.ConfigManager || {
+ requestTimeout: 60000,
+ retryDelays: [5000, 15000, 30000],
+ rateLimits: {
+ coingecko: { requestsPerMinute: 50, minInterval: 1200 }
}
- }
- };
+ };
+ }
const rateLimiter = {
lastRequestTime: {},
- minRequestInterval: {
- coingecko: 1200,
- cryptocompare: 2000
- },
requestQueue: {},
- retryDelays: [5000, 15000, 30000],
+
+ getMinInterval: function(apiName) {
+ const config = getConfig();
+ return config.rateLimits?.[apiName]?.minInterval || 1200;
+ },
+
+ getRetryDelays: function() {
+ const config = getConfig();
+ return config.retryDelays || [5000, 15000, 30000];
+ },
canMakeRequest: function(apiName) {
const now = Date.now();
const lastRequest = this.lastRequestTime[apiName] || 0;
- return (now - lastRequest) >= this.minRequestInterval[apiName];
+ return (now - lastRequest) >= this.getMinInterval(apiName);
},
updateLastRequestTime: function(apiName) {
@@ -41,7 +41,7 @@ const ApiManager = (function() {
getWaitTime: function(apiName) {
const now = Date.now();
const lastRequest = this.lastRequestTime[apiName] || 0;
- return Math.max(0, this.minRequestInterval[apiName] - (now - lastRequest));
+ return Math.max(0, this.getMinInterval(apiName) - (now - lastRequest));
},
queueRequest: async function(apiName, requestFn, retryCount = 0) {
@@ -55,29 +55,30 @@ const ApiManager = (function() {
const executeRequest = async () => {
const waitTime = this.getWaitTime(apiName);
if (waitTime > 0) {
- await new Promise(resolve => setTimeout(resolve, waitTime));
+ await new Promise(resolve => CleanupManager.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];
+ const retryDelays = this.getRetryDelays();
+ if (error.message.includes('429') && retryCount < retryDelays.length) {
+ const delay = retryDelays[retryCount];
console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`);
- await new Promise(resolve => setTimeout(resolve, delay));
+ await new Promise(resolve => CleanupManager.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];
+ retryCount < retryDelays.length) {
+ const delay = retryDelays[retryCount];
console.warn(`Request failed, retrying in ${delay/1000} seconds...`, {
apiName,
retryCount,
error: error.message
});
- await new Promise(resolve => setTimeout(resolve, delay));
+ await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
}
@@ -118,19 +119,7 @@ const ApiManager = (function() {
}
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];
+ console.log('[ApiManager] Config options provided, but using ConfigManager instead');
}
if (window.CleanupManager) {
@@ -143,6 +132,31 @@ const ApiManager = (function() {
},
makeRequest: async function(url, method = 'GET', headers = {}, body = null) {
+ if (window.ErrorHandler) {
+ return window.ErrorHandler.safeExecuteAsync(async () => {
+ const options = {
+ method: method,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...headers
+ },
+ signal: AbortSignal.timeout(getConfig().requestTimeout || 60000)
+ };
+
+ 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();
+ }, `ApiManager.makeRequest(${url})`, null);
+ }
+
try {
const options = {
method: method,
@@ -150,7 +164,7 @@ const ApiManager = (function() {
'Content-Type': 'application/json',
...headers
},
- signal: AbortSignal.timeout(config.requestTimeout)
+ signal: AbortSignal.timeout(getConfig().requestTimeout || 60000)
};
if (body) {
@@ -233,11 +247,8 @@ const ApiManager = (function() {
.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');
}
@@ -260,80 +271,38 @@ const ApiManager = (function() {
fetchVolumeData: async function() {
return this.rateLimiter.queueRequest('coingecko', async () => {
try {
- let coinList = (window.config && window.config.coins) ?
- window.config.coins
- .filter(coin => coin.usesCoinGecko)
- .map(coin => {
- return window.config.getCoinBackendId ?
- window.config.getCoinBackendId(coin.name) :
- (typeof getCoinBackendId === 'function' ?
- getCoinBackendId(coin.name) : coin.name.toLowerCase());
- })
- .join(',') :
- 'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin,wownero';
+ 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']);
- if (!coinList.includes('zcoin') && coinList.includes('firo')) {
- coinList = coinList + ',zcoin';
- }
-
- const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coinList}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`;
-
- const response = await this.makePostRequest(url, {
- 'User-Agent': 'Mozilla/5.0',
- 'Accept': 'application/json'
+ const response = await this.makeRequest('/json/coinvolume', 'POST', {}, {
+ coins: coinSymbols.join(','),
+ source: 'coingecko.com',
+ ttl: 300
});
- if (!response || typeof response !== 'object') {
- throw new Error('Invalid response from CoinGecko API');
+ if (!response) {
+ console.error('No response from backend');
+ throw new Error('Invalid response from backend');
+ }
+
+ if (!response.data) {
+ console.error('Response missing data field:', response);
+ throw new Error('Invalid response from backend');
}
const volumeData = {};
- Object.entries(response).forEach(([coinId, data]) => {
- if (data && data.usd_24h_vol !== undefined) {
- volumeData[coinId] = {
- total_volume: data.usd_24h_vol || 0,
- price_change_percentage_24h: data.usd_24h_change || 0
- };
- }
+ Object.entries(response.data).forEach(([coinSymbol, data]) => {
+ const coinKey = coinSymbol.toLowerCase();
+ volumeData[coinKey] = {
+ total_volume: (data.volume_24h !== undefined && data.volume_24h !== null) ? data.volume_24h : null,
+ price_change_percentage_24h: data.price_change_24h || 0
+ };
});
- const coinMappings = {
- 'firo': ['firo', 'zcoin'],
- 'zcoin': ['zcoin', 'firo'],
- 'bitcoin-cash': ['bitcoin-cash', 'bitcoincash', 'bch'],
- 'bitcoincash': ['bitcoincash', 'bitcoin-cash', 'bch'],
- 'particl': ['particl', 'part']
- };
-
- if (response['zcoin'] && (!volumeData['firo'] || volumeData['firo'].total_volume === 0)) {
- volumeData['firo'] = {
- total_volume: response['zcoin'].usd_24h_vol || 0,
- price_change_percentage_24h: response['zcoin'].usd_24h_change || 0
- };
- }
-
- if (response['bitcoin-cash'] && (!volumeData['bitcoincash'] || volumeData['bitcoincash'].total_volume === 0)) {
- volumeData['bitcoincash'] = {
- total_volume: response['bitcoin-cash'].usd_24h_vol || 0,
- price_change_percentage_24h: response['bitcoin-cash'].usd_24h_change || 0
- };
- }
-
- for (const [mainCoin, alternativeIds] of Object.entries(coinMappings)) {
- if (!volumeData[mainCoin] || volumeData[mainCoin].total_volume === 0) {
- for (const altId of alternativeIds) {
- if (response[altId] && response[altId].usd_24h_vol) {
- volumeData[mainCoin] = {
- total_volume: response[altId].usd_24h_vol,
- price_change_percentage_24h: response[altId].usd_24h_change || 0
- };
- break;
- }
- }
- }
- }
-
return volumeData;
} catch (error) {
console.error("Error fetching volume data:", error);
@@ -342,104 +311,45 @@ const ApiManager = (function() {
});
},
- 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 => {
- let useCoinGecko = false;
- let coingeckoId = null;
-
- if (window.CoinManager) {
- const coinConfig = window.CoinManager.getCoinByAnyIdentifier(coin);
- if (coinConfig) {
- useCoinGecko = !coinConfig.usesCryptoCompare || coin === 'PART';
- coingeckoId = coinConfig.coingeckoId;
+ return this.rateLimiter.queueRequest('coingecko', async () => {
+ try {
+ let days;
+ if (resolution === 'day') {
+ days = 1;
+ } else if (resolution === 'year') {
+ days = 365;
+ } else {
+ days = 180;
}
- } else {
- const coinGeckoCoins = {
- 'WOW': 'wownero',
- 'PART': 'particl',
- 'BTC': 'bitcoin'
- };
- if (coinGeckoCoins[coin]) {
- useCoinGecko = true;
- coingeckoId = coinGeckoCoins[coin];
+
+ const response = await this.makeRequest('/json/coinhistory', 'POST', {}, {
+ coins: coinSymbols.join(','),
+ days: days,
+ source: 'coingecko.com',
+ ttl: 3600
+ });
+
+ if (!response) {
+ console.error('No response from backend');
+ throw new Error('Invalid response from backend');
}
- }
- if (useCoinGecko && coingeckoId) {
- return this.rateLimiter.queueRequest('coingecko', async () => {
- let days;
- if (resolution === 'day') {
- days = 1;
- } else if (resolution === 'year') {
- days = 365;
- } else {
- days = 180;
- }
- const url = `https://api.coingecko.com/api/v3/coins/${coingeckoId}/market_chart?vs_currency=usd&days=${days}`;
- try {
- const response = await this.makePostRequest(url);
- if (response && response.prices) {
- results[coin] = response.prices;
- }
- } catch (error) {
- console.error(`Error fetching CoinGecko data for ${coin}:`, error);
- throw error;
- }
- });
- } else {
- return this.rateLimiter.queueRequest('cryptocompare', async () => {
- try {
- const apiKey = window.config?.apiKeys?.cryptoCompare || '';
- let url;
+ if (!response.data) {
+ console.error('Response missing data field:', response);
+ throw new Error('Invalid response from backend');
+ }
- 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;
- }
- });
+ return response.data;
+ } catch (error) {
+ console.error('Error fetching historical data:', error);
+ throw error;
}
});
-
- await Promise.all(fetchPromises);
- return results;
},
dispose: function() {
@@ -453,17 +363,6 @@ const ApiManager = (function() {
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;
@@ -474,5 +373,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
-//console.log('ApiManager initialized with methods:', Object.keys(ApiManager));
console.log('ApiManager initialized');
diff --git a/basicswap/static/js/modules/balance-updates.js b/basicswap/static/js/modules/balance-updates.js
index 92f1735..465cc49 100644
--- a/basicswap/static/js/modules/balance-updates.js
+++ b/basicswap/static/js/modules/balance-updates.js
@@ -15,7 +15,21 @@ const BalanceUpdatesManager = (function() {
initialized: false
};
- function fetchBalanceData() {
+ async function fetchBalanceData() {
+ if (window.ApiManager) {
+ const data = await window.ApiManager.makeRequest('/json/walletbalances', 'GET');
+
+ if (data && data.error) {
+ throw new Error(data.error);
+ }
+
+ if (!Array.isArray(data)) {
+ throw new Error('Invalid response format');
+ }
+
+ return data;
+ }
+
return fetch('/json/walletbalances', {
headers: {
'Accept': 'application/json',
@@ -43,27 +57,41 @@ const BalanceUpdatesManager = (function() {
function clearTimeoutByKey(key) {
if (state.timeouts.has(key)) {
- clearTimeout(state.timeouts.get(key));
+ const timeoutId = state.timeouts.get(key);
+ if (window.CleanupManager) {
+ window.CleanupManager.clearTimeout(timeoutId);
+ } else {
+ clearTimeout(timeoutId);
+ }
state.timeouts.delete(key);
}
}
function setTimeoutByKey(key, callback, delay) {
clearTimeoutByKey(key);
- const timeoutId = setTimeout(callback, delay);
+ const timeoutId = window.CleanupManager
+ ? window.CleanupManager.setTimeout(callback, delay)
+ : setTimeout(callback, delay);
state.timeouts.set(key, timeoutId);
}
function clearIntervalByKey(key) {
if (state.intervals.has(key)) {
- clearInterval(state.intervals.get(key));
+ const intervalId = state.intervals.get(key);
+ if (window.CleanupManager) {
+ window.CleanupManager.clearInterval(intervalId);
+ } else {
+ clearInterval(intervalId);
+ }
state.intervals.delete(key);
}
}
function setIntervalByKey(key, callback, interval) {
clearIntervalByKey(key);
- const intervalId = setInterval(callback, interval);
+ const intervalId = window.CleanupManager
+ ? window.CleanupManager.setInterval(callback, interval)
+ : setInterval(callback, interval);
state.intervals.set(key, intervalId);
}
diff --git a/basicswap/static/js/modules/cache-manager.js b/basicswap/static/js/modules/cache-manager.js
index 8211a89..9807995 100644
--- a/basicswap/static/js/modules/cache-manager.js
+++ b/basicswap/static/js/modules/cache-manager.js
@@ -1,9 +1,19 @@
const CacheManager = (function() {
- const defaults = window.config?.cacheConfig?.storage || {
- maxSizeBytes: 10 * 1024 * 1024,
- maxItems: 200,
- defaultTTL: 5 * 60 * 1000
- };
+ function getDefaults() {
+ if (window.config?.cacheConfig?.storage) {
+ return window.config.cacheConfig.storage;
+ }
+ if (window.ConfigManager?.cacheConfig?.storage) {
+ return window.ConfigManager.cacheConfig.storage;
+ }
+ return {
+ maxSizeBytes: 10 * 1024 * 1024,
+ maxItems: 200,
+ defaultTTL: 5 * 60 * 1000
+ };
+ }
+
+ const defaults = getDefaults();
const PRICES_CACHE_KEY = 'crypto_prices_unified';
@@ -45,8 +55,12 @@ const CacheManager = (function() {
const cacheAPI = {
getTTL: function(resourceType) {
- const ttlConfig = window.config?.cacheConfig?.ttlSettings || {};
- return ttlConfig[resourceType] || window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL;
+ const ttlConfig = window.config?.cacheConfig?.ttlSettings ||
+ window.ConfigManager?.cacheConfig?.ttlSettings || {};
+ const defaultTTL = window.config?.cacheConfig?.defaultTTL ||
+ window.ConfigManager?.cacheConfig?.defaultTTL ||
+ defaults.defaultTTL;
+ return ttlConfig[resourceType] || defaultTTL;
},
set: function(key, value, resourceTypeOrCustomTtl = null) {
@@ -73,13 +87,18 @@ const CacheManager = (function() {
expiresAt: Date.now() + ttl
};
- let serializedItem;
- try {
- serializedItem = JSON.stringify(item);
- } catch (e) {
- console.error('Failed to serialize cache item:', e);
- return false;
- }
+ const serializedItem = window.ErrorHandler
+ ? window.ErrorHandler.safeExecute(() => JSON.stringify(item), 'CacheManager.set.serialize', null)
+ : (() => {
+ try {
+ return JSON.stringify(item);
+ } catch (e) {
+ console.error('Failed to serialize cache item:', e);
+ return null;
+ }
+ })();
+
+ if (!serializedItem) return false;
const itemSize = new Blob([serializedItem]).size;
if (itemSize > defaults.maxSizeBytes) {
@@ -118,7 +137,7 @@ const CacheManager = (function() {
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%
+ .slice(0, Math.floor(memoryCache.size * 0.2));
keysToDelete.forEach(k => memoryCache.delete(k));
}
@@ -285,7 +304,7 @@ const CacheManager = (function() {
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
+ .slice(0, Math.floor(memoryCache.size * 0.3));
keysToDelete.forEach(key => memoryCache.delete(key));
}
@@ -328,7 +347,6 @@ const CacheManager = (function() {
.filter(key => isCacheKey(key))
.forEach(key => memoryCache.delete(key));
- //console.log("Cache cleared successfully");
return true;
},
@@ -531,6 +549,4 @@ const CacheManager = (function() {
window.CacheManager = CacheManager;
-
-//console.log('CacheManager initialized with methods:', Object.keys(CacheManager));
console.log('CacheManager initialized');
diff --git a/basicswap/static/js/modules/cleanup-manager.js b/basicswap/static/js/modules/cleanup-manager.js
index c50a93c..d813747 100644
--- a/basicswap/static/js/modules/cleanup-manager.js
+++ b/basicswap/static/js/modules/cleanup-manager.js
@@ -233,7 +233,7 @@ const CleanupManager = (function() {
},
setupMemoryOptimization: function(options = {}) {
- const memoryCheckInterval = options.interval || 2 * 60 * 1000; // Default: 2 minutes
+ const memoryCheckInterval = options.interval || 2 * 60 * 1000;
const maxCacheSize = options.maxCacheSize || 100;
const maxDataSize = options.maxDataSize || 1000;
diff --git a/basicswap/static/js/modules/coin-manager.js b/basicswap/static/js/modules/coin-manager.js
index a84db04..9ebc24e 100644
--- a/basicswap/static/js/modules/coin-manager.js
+++ b/basicswap/static/js/modules/coin-manager.js
@@ -178,19 +178,7 @@ const CoinManager = (function() {
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 coinAliasesMap[normalizedId] || null;
}
return {
diff --git a/basicswap/static/js/modules/coin-utils.js b/basicswap/static/js/modules/coin-utils.js
new file mode 100644
index 0000000..cb337a5
--- /dev/null
+++ b/basicswap/static/js/modules/coin-utils.js
@@ -0,0 +1,191 @@
+const CoinUtils = (function() {
+ function buildAliasesFromCoinManager() {
+ const aliases = {};
+ const symbolMap = {};
+
+ if (window.CoinManager) {
+ const coins = window.CoinManager.getAllCoins();
+ coins.forEach(coin => {
+ const canonical = coin.name.toLowerCase();
+ aliases[canonical] = coin.aliases || [coin.name.toLowerCase()];
+ symbolMap[canonical] = coin.symbol;
+ });
+ }
+
+ return { aliases, symbolMap };
+ }
+
+ let COIN_ALIASES = {};
+ let CANONICAL_TO_SYMBOL = {};
+
+ function initializeAliases() {
+ const { aliases, symbolMap } = buildAliasesFromCoinManager();
+ COIN_ALIASES = aliases;
+ CANONICAL_TO_SYMBOL = symbolMap;
+ }
+
+ if (window.CoinManager) {
+ initializeAliases();
+ } else {
+ document.addEventListener('DOMContentLoaded', () => {
+ if (window.CoinManager) {
+ initializeAliases();
+ }
+ });
+ }
+
+ function getCanonicalName(coin) {
+ if (!coin) return null;
+ const lower = coin.toString().toLowerCase().trim();
+
+ for (const [canonical, aliases] of Object.entries(COIN_ALIASES)) {
+ if (aliases.includes(lower)) {
+ return canonical;
+ }
+ }
+ return lower;
+ }
+
+ return {
+ normalizeCoinName: function(coin, priceData = null) {
+ const canonical = getCanonicalName(coin);
+ if (!canonical) return null;
+
+ if (priceData) {
+ if (canonical === 'bitcoin-cash') {
+ if (priceData['bitcoin-cash']) return 'bitcoin-cash';
+ if (priceData['bch']) return 'bch';
+ if (priceData['bitcoincash']) return 'bitcoincash';
+ return 'bitcoin-cash';
+ }
+
+ if (canonical === 'particl') {
+ if (priceData['part']) return 'part';
+ if (priceData['particl']) return 'particl';
+ return 'part';
+ }
+ }
+
+ return canonical;
+ },
+
+ isSameCoin: function(coin1, coin2) {
+ if (!coin1 || !coin2) return false;
+
+ if (window.CoinManager) {
+ return window.CoinManager.coinMatches(coin1, coin2);
+ }
+
+ const canonical1 = getCanonicalName(coin1);
+ const canonical2 = getCanonicalName(coin2);
+ if (canonical1 === canonical2) return true;
+
+ const lower1 = coin1.toString().toLowerCase().trim();
+ const lower2 = coin2.toString().toLowerCase().trim();
+
+ const particlVariants = ['particl', 'particl anon', 'particl blind', 'part', 'part_anon', 'part_blind'];
+ if (particlVariants.includes(lower1) && particlVariants.includes(lower2)) {
+ return true;
+ }
+
+ if (lower1.includes(' ') || lower2.includes(' ')) {
+ const word1 = lower1.split(' ')[0];
+ const word2 = lower2.split(' ')[0];
+ if (word1 === word2 && word1.length > 4) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ getCoinSymbol: function(identifier) {
+ if (!identifier) return null;
+
+ if (window.CoinManager) {
+ const coin = window.CoinManager.getCoinByAnyIdentifier(identifier);
+ if (coin) return coin.symbol;
+ }
+
+ const canonical = getCanonicalName(identifier);
+ if (canonical && CANONICAL_TO_SYMBOL[canonical]) {
+ return CANONICAL_TO_SYMBOL[canonical];
+ }
+
+ return identifier.toString().toUpperCase();
+ },
+
+ getDisplayName: function(identifier) {
+ if (!identifier) return null;
+
+ if (window.CoinManager) {
+ const coin = window.CoinManager.getCoinByAnyIdentifier(identifier);
+ if (coin) return coin.displayName || coin.name;
+ }
+
+ const symbol = this.getCoinSymbol(identifier);
+ return symbol || identifier;
+ },
+
+ getCoinImage: function(coinName) {
+ if (!coinName) return null;
+
+ const canonical = getCanonicalName(coinName);
+ const symbol = this.getCoinSymbol(canonical);
+
+ if (!symbol) return null;
+
+ const imagePath = `/static/images/coins/${symbol.toLowerCase()}.png`;
+ return imagePath;
+ },
+
+ getPriceKey: function(coin, priceData = null) {
+ return this.normalizeCoinName(coin, priceData);
+ },
+
+ getCoingeckoId: function(coinName) {
+ if (!coinName) return null;
+
+ if (window.CoinManager) {
+ const coin = window.CoinManager.getCoinByAnyIdentifier(coinName);
+ if (coin && coin.coingeckoId) {
+ return coin.coingeckoId;
+ }
+ }
+
+ const canonical = getCanonicalName(coinName);
+ return canonical;
+ },
+
+ formatCoinAmount: function(amount, decimals = 8) {
+ if (amount === null || amount === undefined) return '0';
+
+ const numAmount = parseFloat(amount);
+ if (isNaN(numAmount)) return '0';
+
+ return numAmount.toFixed(decimals).replace(/\.?0+$/, '');
+ },
+
+ getAllAliases: function(coin) {
+ const canonical = getCanonicalName(coin);
+ return COIN_ALIASES[canonical] || [canonical];
+ },
+
+ isValidCoin: function(coin) {
+ if (!coin) return false;
+ const canonical = getCanonicalName(coin);
+ return canonical !== null && COIN_ALIASES.hasOwnProperty(canonical);
+ },
+
+ refreshAliases: function() {
+ initializeAliases();
+ return Object.keys(COIN_ALIASES).length;
+ }
+ };
+})();
+
+if (typeof window !== 'undefined') {
+ window.CoinUtils = CoinUtils;
+}
+
+console.log('CoinUtils module loaded');
diff --git a/basicswap/static/js/modules/config-manager.js b/basicswap/static/js/modules/config-manager.js
index 7cd1bd3..63ab310 100644
--- a/basicswap/static/js/modules/config-manager.js
+++ b/basicswap/static/js/modules/config-manager.js
@@ -35,38 +35,22 @@ const ConfigManager = (function() {
},
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 }
- ];
+ if (window.CoinManager) {
+ return window.CoinManager.getAllCoins();
+ }
+ console.warn('[ConfigManager] CoinManager not available, returning empty array');
+ return [];
},
chartConfig: {
colors: {
@@ -108,12 +92,10 @@ const ConfigManager = (function() {
if (typeof window.getAPIKeys === 'function') {
const apiKeys = window.getAPIKeys();
return {
- cryptoCompare: apiKeys.cryptoCompare || '',
coinGecko: apiKeys.coinGecko || ''
};
}
return {
- cryptoCompare: '',
coinGecko: ''
};
},
@@ -122,55 +104,20 @@ const ConfigManager = (function() {
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;
+ if (window.CoinUtils) {
+ return window.CoinUtils.normalizeCoinName(coinName);
+ }
+ return typeof coinName === 'string' ? coinName.toLowerCase() : '';
},
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 (window.CoinUtils) {
+ return window.CoinUtils.isSameCoin(offerCoin, filterCoin);
}
- if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
- (offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
- return true;
- }
- const particlVariants = ['particl', 'particl anon', 'particl blind'];
- if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) {
- return true;
- }
-
- if (filterCoin.includes(' ') || offerCoin.includes(' ')) {
- const filterFirstWord = filterCoin.split(' ')[0];
- const offerFirstWord = offerCoin.split(' ')[0];
-
- if (filterFirstWord === 'bitcoin' && offerFirstWord === 'bitcoin') {
- const filterHasCash = filterCoin.includes('cash');
- const offerHasCash = offerCoin.includes('cash');
- return filterHasCash === offerHasCash;
- }
-
- if (filterFirstWord === offerFirstWord && filterFirstWord.length > 4) {
- return true;
- }
- }
- if (particlVariants.includes(filterCoin)) {
- return offerCoin === filterCoin;
- }
- return false;
+ return offerCoin.toLowerCase() === filterCoin.toLowerCase();
},
update: function(path, value) {
const parts = path.split('.');
@@ -229,7 +176,7 @@ const ConfigManager = (function() {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
- timeoutId = setTimeout(() => func(...args), delay);
+ timeoutId = CleanupManager.setTimeout(() => func(...args), delay);
};
},
formatTimeLeft: function(timestamp) {
diff --git a/basicswap/static/js/modules/dom-cache.js b/basicswap/static/js/modules/dom-cache.js
new file mode 100644
index 0000000..f3d075d
--- /dev/null
+++ b/basicswap/static/js/modules/dom-cache.js
@@ -0,0 +1,207 @@
+(function() {
+ 'use strict';
+
+ const originalGetElementById = document.getElementById.bind(document);
+
+ const DOMCache = {
+
+ cache: {},
+
+ get: function(id, forceRefresh = false) {
+ if (!id) {
+ console.warn('DOMCache: No ID provided');
+ return null;
+ }
+
+ if (!forceRefresh && this.cache[id]) {
+
+ if (document.body.contains(this.cache[id])) {
+ return this.cache[id];
+ } else {
+
+ delete this.cache[id];
+ }
+ }
+
+ const element = originalGetElementById(id);
+ if (element) {
+ this.cache[id] = element;
+ }
+
+ return element;
+ },
+
+ getMultiple: function(ids) {
+ const elements = {};
+ ids.forEach(id => {
+ elements[id] = this.get(id);
+ });
+ return elements;
+ },
+
+ setValue: function(id, value) {
+ const element = this.get(id);
+ if (element) {
+ element.value = value;
+ return true;
+ }
+ console.warn(`DOMCache: Element not found: ${id}`);
+ return false;
+ },
+
+ getValue: function(id, defaultValue = '') {
+ const element = this.get(id);
+ return element ? element.value : defaultValue;
+ },
+
+ setText: function(id, text) {
+ const element = this.get(id);
+ if (element) {
+ element.textContent = text;
+ return true;
+ }
+ console.warn(`DOMCache: Element not found: ${id}`);
+ return false;
+ },
+
+ getText: function(id, defaultValue = '') {
+ const element = this.get(id);
+ return element ? element.textContent : defaultValue;
+ },
+
+ addClass: function(id, className) {
+ const element = this.get(id);
+ if (element) {
+ element.classList.add(className);
+ return true;
+ }
+ return false;
+ },
+
+ removeClass: function(id, className) {
+ const element = this.get(id);
+ if (element) {
+ element.classList.remove(className);
+ return true;
+ }
+ return false;
+ },
+
+ toggleClass: function(id, className) {
+ const element = this.get(id);
+ if (element) {
+ element.classList.toggle(className);
+ return true;
+ }
+ return false;
+ },
+
+ show: function(id) {
+ const element = this.get(id);
+ if (element) {
+ element.style.display = '';
+ return true;
+ }
+ return false;
+ },
+
+ hide: function(id) {
+ const element = this.get(id);
+ if (element) {
+ element.style.display = 'none';
+ return true;
+ }
+ return false;
+ },
+
+ exists: function(id) {
+ return this.get(id) !== null;
+ },
+
+ clear: function(id) {
+ if (id) {
+ delete this.cache[id];
+ } else {
+ this.cache = {};
+ }
+ },
+
+ size: function() {
+ return Object.keys(this.cache).length;
+ },
+
+ validate: function() {
+ const ids = Object.keys(this.cache);
+ let removed = 0;
+
+ ids.forEach(id => {
+ const element = this.cache[id];
+ if (!document.body.contains(element)) {
+ delete this.cache[id];
+ removed++;
+ }
+ });
+
+ return removed;
+ },
+
+ createScope: function(elementIds) {
+ const scope = {};
+
+ elementIds.forEach(id => {
+ Object.defineProperty(scope, id, {
+ get: () => this.get(id),
+ enumerable: true
+ });
+ });
+
+ return scope;
+ },
+
+ batch: function(operations) {
+ Object.keys(operations).forEach(id => {
+ const ops = operations[id];
+ const element = this.get(id);
+
+ if (!element) {
+ console.warn(`DOMCache: Element not found in batch operation: ${id}`);
+ return;
+ }
+
+ if (ops.value !== undefined) element.value = ops.value;
+ if (ops.text !== undefined) element.textContent = ops.text;
+ if (ops.html !== undefined) element.innerHTML = ops.html;
+ if (ops.class) element.classList.add(ops.class);
+ if (ops.removeClass) element.classList.remove(ops.removeClass);
+ if (ops.hide) element.style.display = 'none';
+ if (ops.show) element.style.display = '';
+ if (ops.disabled !== undefined) element.disabled = ops.disabled;
+ });
+ }
+ };
+
+ window.DOMCache = DOMCache;
+
+ if (!window.$) {
+ window.$ = function(id) {
+ return DOMCache.get(id);
+ };
+ }
+
+ document.getElementById = function(id) {
+ return DOMCache.get(id);
+ };
+
+ document.getElementByIdOriginal = originalGetElementById;
+
+ if (window.CleanupManager) {
+ const validationInterval = CleanupManager.setInterval(() => {
+ DOMCache.validate();
+ }, 30000);
+
+ CleanupManager.registerResource('domCacheValidation', validationInterval, () => {
+ clearInterval(validationInterval);
+ });
+ }
+
+})();
diff --git a/basicswap/static/js/modules/error-handler.js b/basicswap/static/js/modules/error-handler.js
new file mode 100644
index 0000000..b17359b
--- /dev/null
+++ b/basicswap/static/js/modules/error-handler.js
@@ -0,0 +1,215 @@
+const ErrorHandler = (function() {
+ const config = {
+ logErrors: true,
+ throwErrors: false,
+ errorCallbacks: []
+ };
+
+ function formatError(error, context) {
+ const timestamp = new Date().toISOString();
+ const contextStr = context ? ` [${context}]` : '';
+
+ if (error instanceof Error) {
+ return `${timestamp}${contextStr} ${error.name}: ${error.message}`;
+ }
+
+ return `${timestamp}${contextStr} ${String(error)}`;
+ }
+
+ function notifyCallbacks(error, context) {
+ config.errorCallbacks.forEach(callback => {
+ try {
+ callback(error, context);
+ } catch (e) {
+ console.error('[ErrorHandler] Error in callback:', e);
+ }
+ });
+ }
+
+ return {
+ configure: function(options = {}) {
+ Object.assign(config, options);
+ return this;
+ },
+
+ addCallback: function(callback) {
+ if (typeof callback === 'function') {
+ config.errorCallbacks.push(callback);
+ }
+ return this;
+ },
+
+ removeCallback: function(callback) {
+ const index = config.errorCallbacks.indexOf(callback);
+ if (index > -1) {
+ config.errorCallbacks.splice(index, 1);
+ }
+ return this;
+ },
+
+ safeExecute: function(fn, context = null, fallbackValue = null) {
+ try {
+ return fn();
+ } catch (error) {
+ if (config.logErrors) {
+ console.error(formatError(error, context));
+ }
+
+ notifyCallbacks(error, context);
+
+ if (config.throwErrors) {
+ throw error;
+ }
+
+ return fallbackValue;
+ }
+ },
+
+ safeExecuteAsync: async function(fn, context = null, fallbackValue = null) {
+ try {
+ return await fn();
+ } catch (error) {
+ if (config.logErrors) {
+ console.error(formatError(error, context));
+ }
+
+ notifyCallbacks(error, context);
+
+ if (config.throwErrors) {
+ throw error;
+ }
+
+ return fallbackValue;
+ }
+ },
+
+ wrap: function(fn, context = null, fallbackValue = null) {
+ return (...args) => {
+ try {
+ return fn(...args);
+ } catch (error) {
+ if (config.logErrors) {
+ console.error(formatError(error, context));
+ }
+
+ notifyCallbacks(error, context);
+
+ if (config.throwErrors) {
+ throw error;
+ }
+
+ return fallbackValue;
+ }
+ };
+ },
+
+ wrapAsync: function(fn, context = null, fallbackValue = null) {
+ return async (...args) => {
+ try {
+ return await fn(...args);
+ } catch (error) {
+ if (config.logErrors) {
+ console.error(formatError(error, context));
+ }
+
+ notifyCallbacks(error, context);
+
+ if (config.throwErrors) {
+ throw error;
+ }
+
+ return fallbackValue;
+ }
+ };
+ },
+
+ handleError: function(error, context = null, fallbackValue = null) {
+ if (config.logErrors) {
+ console.error(formatError(error, context));
+ }
+
+ notifyCallbacks(error, context);
+
+ if (config.throwErrors) {
+ throw error;
+ }
+
+ return fallbackValue;
+ },
+
+ try: function(fn, catchFn = null, finallyFn = null) {
+ try {
+ return fn();
+ } catch (error) {
+ if (config.logErrors) {
+ console.error(formatError(error, 'ErrorHandler.try'));
+ }
+
+ notifyCallbacks(error, 'ErrorHandler.try');
+
+ if (catchFn) {
+ return catchFn(error);
+ }
+
+ if (config.throwErrors) {
+ throw error;
+ }
+
+ return null;
+ } finally {
+ if (finallyFn) {
+ finallyFn();
+ }
+ }
+ },
+
+ tryAsync: async function(fn, catchFn = null, finallyFn = null) {
+ try {
+ return await fn();
+ } catch (error) {
+ if (config.logErrors) {
+ console.error(formatError(error, 'ErrorHandler.tryAsync'));
+ }
+
+ notifyCallbacks(error, 'ErrorHandler.tryAsync');
+
+ if (catchFn) {
+ return await catchFn(error);
+ }
+
+ if (config.throwErrors) {
+ throw error;
+ }
+
+ return null;
+ } finally {
+ if (finallyFn) {
+ await finallyFn();
+ }
+ }
+ },
+
+ createBoundary: function(context) {
+ return {
+ execute: (fn, fallbackValue = null) => {
+ return ErrorHandler.safeExecute(fn, context, fallbackValue);
+ },
+ executeAsync: (fn, fallbackValue = null) => {
+ return ErrorHandler.safeExecuteAsync(fn, context, fallbackValue);
+ },
+ wrap: (fn, fallbackValue = null) => {
+ return ErrorHandler.wrap(fn, context, fallbackValue);
+ },
+ wrapAsync: (fn, fallbackValue = null) => {
+ return ErrorHandler.wrapAsync(fn, context, fallbackValue);
+ }
+ };
+ }
+ };
+})();
+
+if (typeof window !== 'undefined') {
+ window.ErrorHandler = ErrorHandler;
+}
+
+console.log('ErrorHandler module loaded');
diff --git a/basicswap/static/js/modules/event-handlers.js b/basicswap/static/js/modules/event-handlers.js
new file mode 100644
index 0000000..592effe
--- /dev/null
+++ b/basicswap/static/js/modules/event-handlers.js
@@ -0,0 +1,342 @@
+(function() {
+ 'use strict';
+
+ const EventHandlers = {
+
+ confirmPopup: function(action = 'proceed', coinName = '') {
+ const message = action === 'Accept'
+ ? 'Are you sure you want to accept this bid?'
+ : coinName
+ ? `Are you sure you want to ${action} ${coinName}?`
+ : 'Are you sure you want to proceed?';
+
+ return confirm(message);
+ },
+
+ confirmReseed: function() {
+ return confirm('Are you sure you want to reseed the wallet? This will generate new addresses.');
+ },
+
+ confirmWithdrawal: function() {
+
+ if (window.WalletPage && typeof window.WalletPage.confirmWithdrawal === 'function') {
+ return window.WalletPage.confirmWithdrawal();
+ }
+ return confirm('Are you sure you want to withdraw? Please verify the address and amount.');
+ },
+
+ confirmUTXOResize: function() {
+ return confirm('Are you sure you want to create a UTXO? This will split your balance.');
+ },
+
+ confirmRemoveExpired: function() {
+ return confirm('Are you sure you want to remove all expired offers and bids?');
+ },
+
+ fillDonationAddress: function(address, coinType) {
+
+ let addressInput = null;
+
+ addressInput = window.DOMCache
+ ? window.DOMCache.get('address_to')
+ : document.getElementById('address_to');
+
+ if (!addressInput) {
+ addressInput = document.querySelector('input[name^="to_"]');
+ }
+
+ if (!addressInput) {
+ addressInput = document.querySelector('input[placeholder*="Address"]');
+ }
+
+ if (addressInput) {
+ addressInput.value = address;
+ console.log(`Filled donation address for ${coinType}: ${address}`);
+ } else {
+ console.error('EventHandlers: Address input not found');
+ }
+ },
+
+ setAmmAmount: function(percent, inputId) {
+ const amountInput = window.DOMCache
+ ? window.DOMCache.get(inputId)
+ : document.getElementById(inputId);
+
+ if (!amountInput) {
+ console.error('EventHandlers: AMM amount input not found:', inputId);
+ return;
+ }
+
+ const balanceElement = amountInput.closest('form')?.querySelector('[data-balance]');
+ const balance = balanceElement ? parseFloat(balanceElement.getAttribute('data-balance')) : 0;
+
+ if (balance > 0) {
+ const calculatedAmount = balance * percent;
+ amountInput.value = calculatedAmount.toFixed(8);
+ } else {
+ console.warn('EventHandlers: No balance found for AMM amount calculation');
+ }
+ },
+
+ setOfferAmount: function(percent, inputId) {
+ const amountInput = window.DOMCache
+ ? window.DOMCache.get(inputId)
+ : document.getElementById(inputId);
+
+ if (!amountInput) {
+ console.error('EventHandlers: Offer amount input not found:', inputId);
+ return;
+ }
+
+ const coinFromSelect = document.getElementById('coin_from');
+ if (!coinFromSelect) {
+ console.error('EventHandlers: coin_from select not found');
+ return;
+ }
+
+ const selectedOption = coinFromSelect.options[coinFromSelect.selectedIndex];
+ if (!selectedOption || selectedOption.value === '-1') {
+ if (window.showErrorModal) {
+ window.showErrorModal('Validation Error', 'Please select a coin first');
+ } else {
+ alert('Please select a coin first');
+ }
+ return;
+ }
+
+ const balance = selectedOption.getAttribute('data-balance');
+ if (!balance) {
+ console.error('EventHandlers: Balance not found for selected coin');
+ return;
+ }
+
+ const floatBalance = parseFloat(balance);
+ if (isNaN(floatBalance) || floatBalance <= 0) {
+ if (window.showErrorModal) {
+ window.showErrorModal('Invalid Balance', 'The selected coin has no available balance. Please select a coin with a positive balance.');
+ } else {
+ alert('Invalid balance for selected coin');
+ }
+ return;
+ }
+
+ const calculatedAmount = floatBalance * percent;
+ amountInput.value = calculatedAmount.toFixed(8);
+ },
+
+ resetForm: function() {
+ const form = document.querySelector('form[name="offer_form"]') || document.querySelector('form');
+ if (form) {
+ form.reset();
+ }
+ },
+
+ hideConfirmModal: function() {
+ if (window.DOMCache) {
+ window.DOMCache.hide('confirmModal');
+ } else {
+ const modal = document.getElementById('confirmModal');
+ if (modal) {
+ modal.style.display = 'none';
+ }
+ }
+ },
+
+ lookup_rates: function() {
+
+ if (window.lookup_rates && typeof window.lookup_rates === 'function') {
+ window.lookup_rates();
+ } else {
+ console.error('EventHandlers: lookup_rates function not found');
+ }
+ },
+
+ checkForUpdatesNow: function() {
+ if (window.checkForUpdatesNow && typeof window.checkForUpdatesNow === 'function') {
+ window.checkForUpdatesNow();
+ } else {
+ console.error('EventHandlers: checkForUpdatesNow function not found');
+ }
+ },
+
+ testUpdateNotification: function() {
+ if (window.testUpdateNotification && typeof window.testUpdateNotification === 'function') {
+ window.testUpdateNotification();
+ } else {
+ console.error('EventHandlers: testUpdateNotification function not found');
+ }
+ },
+
+ toggleNotificationDropdown: function(event) {
+ if (window.toggleNotificationDropdown && typeof window.toggleNotificationDropdown === 'function') {
+ window.toggleNotificationDropdown(event);
+ } else {
+ console.error('EventHandlers: toggleNotificationDropdown function not found');
+ }
+ },
+
+ closeMessage: function(messageId) {
+ if (window.DOMCache) {
+ window.DOMCache.hide(messageId);
+ } else {
+ const messageElement = document.getElementById(messageId);
+ if (messageElement) {
+ messageElement.style.display = 'none';
+ }
+ }
+ },
+
+ initialize: function() {
+
+ document.addEventListener('click', (e) => {
+ const target = e.target.closest('[data-confirm]');
+ if (target) {
+ const action = target.getAttribute('data-confirm-action') || 'proceed';
+ const coinName = target.getAttribute('data-confirm-coin') || '';
+
+ if (!this.confirmPopup(action, coinName)) {
+ e.preventDefault();
+ return false;
+ }
+ }
+ });
+
+ document.addEventListener('click', (e) => {
+ const target = e.target.closest('[data-confirm-reseed]');
+ if (target) {
+ if (!this.confirmReseed()) {
+ e.preventDefault();
+ return false;
+ }
+ }
+ });
+
+ document.addEventListener('click', (e) => {
+ const target = e.target.closest('[data-confirm-utxo]');
+ if (target) {
+ if (!this.confirmUTXOResize()) {
+ e.preventDefault();
+ return false;
+ }
+ }
+ });
+
+ document.addEventListener('click', (e) => {
+ const target = e.target.closest('[data-confirm-remove-expired]');
+ if (target) {
+ if (!this.confirmRemoveExpired()) {
+ e.preventDefault();
+ return false;
+ }
+ }
+ });
+
+ document.addEventListener('click', (e) => {
+ const target = e.target.closest('[data-fill-donation]');
+ if (target) {
+ e.preventDefault();
+ const address = target.getAttribute('data-address');
+ const coinType = target.getAttribute('data-coin-type');
+ this.fillDonationAddress(address, coinType);
+ }
+ });
+
+ document.addEventListener('click', (e) => {
+ const target = e.target.closest('[data-set-amm-amount]');
+ if (target) {
+ e.preventDefault();
+ const percent = parseFloat(target.getAttribute('data-set-amm-amount'));
+ const inputId = target.getAttribute('data-input-id');
+ this.setAmmAmount(percent, inputId);
+ }
+ });
+
+ document.addEventListener('click', (e) => {
+ const target = e.target.closest('[data-set-offer-amount]');
+ if (target) {
+ e.preventDefault();
+ const percent = parseFloat(target.getAttribute('data-set-offer-amount'));
+ const inputId = target.getAttribute('data-input-id');
+ this.setOfferAmount(percent, inputId);
+ }
+ });
+
+ document.addEventListener('click', (e) => {
+ const target = e.target.closest('[data-reset-form]');
+ if (target) {
+ e.preventDefault();
+ this.resetForm();
+ }
+ });
+
+ document.addEventListener('click', (e) => {
+ const target = e.target.closest('[data-hide-modal]');
+ if (target) {
+ e.preventDefault();
+ this.hideConfirmModal();
+ }
+ });
+
+ document.addEventListener('click', (e) => {
+ const target = e.target.closest('[data-lookup-rates]');
+ if (target) {
+ e.preventDefault();
+ this.lookup_rates();
+ }
+ });
+
+ document.addEventListener('click', (e) => {
+ const target = e.target.closest('[data-check-updates]');
+ if (target) {
+ e.preventDefault();
+ this.checkForUpdatesNow();
+ }
+ });
+
+ document.addEventListener('click', (e) => {
+ const target = e.target.closest('[data-test-notification]');
+ if (target) {
+ e.preventDefault();
+ const type = target.getAttribute('data-test-notification');
+ if (type === 'update') {
+ this.testUpdateNotification();
+ } else {
+ window.NotificationManager && window.NotificationManager.testToasts();
+ }
+ }
+ });
+
+ document.addEventListener('click', (e) => {
+ const target = e.target.closest('[data-close-message]');
+ if (target) {
+ e.preventDefault();
+ const messageId = target.getAttribute('data-close-message');
+ this.closeMessage(messageId);
+ }
+ });
+ }
+ };
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', function() {
+ EventHandlers.initialize();
+ });
+ } else {
+ EventHandlers.initialize();
+ }
+
+ window.EventHandlers = EventHandlers;
+
+ window.confirmPopup = EventHandlers.confirmPopup.bind(EventHandlers);
+ window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers);
+ window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers);
+ window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers);
+ window.confirmRemoveExpired = EventHandlers.confirmRemoveExpired.bind(EventHandlers);
+ window.fillDonationAddress = EventHandlers.fillDonationAddress.bind(EventHandlers);
+ window.setAmmAmount = EventHandlers.setAmmAmount.bind(EventHandlers);
+ window.setOfferAmount = EventHandlers.setOfferAmount.bind(EventHandlers);
+ window.resetForm = EventHandlers.resetForm.bind(EventHandlers);
+ window.hideConfirmModal = EventHandlers.hideConfirmModal.bind(EventHandlers);
+ window.toggleNotificationDropdown = EventHandlers.toggleNotificationDropdown.bind(EventHandlers);
+
+})();
diff --git a/basicswap/static/js/modules/form-validator.js b/basicswap/static/js/modules/form-validator.js
new file mode 100644
index 0000000..81efa81
--- /dev/null
+++ b/basicswap/static/js/modules/form-validator.js
@@ -0,0 +1,225 @@
+(function() {
+ 'use strict';
+
+ const FormValidator = {
+
+ checkPasswordStrength: function(password) {
+ const requirements = {
+ length: password.length >= 8,
+ uppercase: /[A-Z]/.test(password),
+ lowercase: /[a-z]/.test(password),
+ number: /[0-9]/.test(password)
+ };
+
+ let score = 0;
+ if (requirements.length) score += 25;
+ if (requirements.uppercase) score += 25;
+ if (requirements.lowercase) score += 25;
+ if (requirements.number) score += 25;
+
+ return {
+ score: score,
+ requirements: requirements,
+ isStrong: score >= 60
+ };
+ },
+
+ updatePasswordStrengthUI: function(password, elements) {
+ const result = this.checkPasswordStrength(password);
+ const { score, requirements } = result;
+
+ if (!elements.bar || !elements.text) {
+ console.warn('FormValidator: Missing strength UI elements');
+ return result.isStrong;
+ }
+
+ elements.bar.style.width = `${score}%`;
+
+ if (score === 0) {
+ elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-gray-300 dark:bg-gray-500';
+ elements.text.textContent = 'Enter password';
+ elements.text.className = 'text-sm font-medium text-gray-500 dark:text-gray-400';
+ } else if (score < 40) {
+ elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-red-500';
+ elements.text.textContent = 'Weak';
+ elements.text.className = 'text-sm font-medium text-red-600 dark:text-red-400';
+ } else if (score < 70) {
+ elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-yellow-500';
+ elements.text.textContent = 'Fair';
+ elements.text.className = 'text-sm font-medium text-yellow-600 dark:text-yellow-400';
+ } else if (score < 90) {
+ elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-blue-500';
+ elements.text.textContent = 'Good';
+ elements.text.className = 'text-sm font-medium text-blue-600 dark:text-blue-400';
+ } else {
+ elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-green-500';
+ elements.text.textContent = 'Strong';
+ elements.text.className = 'text-sm font-medium text-green-600 dark:text-green-400';
+ }
+
+ if (elements.requirements) {
+ this.updateRequirement(elements.requirements.length, requirements.length);
+ this.updateRequirement(elements.requirements.uppercase, requirements.uppercase);
+ this.updateRequirement(elements.requirements.lowercase, requirements.lowercase);
+ this.updateRequirement(elements.requirements.number, requirements.number);
+ }
+
+ return result.isStrong;
+ },
+
+ updateRequirement: function(element, met) {
+ if (!element) return;
+
+ if (met) {
+ element.className = 'flex items-center text-green-600 dark:text-green-400';
+ } else {
+ element.className = 'flex items-center text-gray-500 dark:text-gray-400';
+ }
+ },
+
+ checkPasswordMatch: function(password1, password2, elements) {
+ if (!elements) {
+ return password1 === password2;
+ }
+
+ const { container, success, error } = elements;
+
+ if (password2.length === 0) {
+ if (container) container.classList.add('hidden');
+ return false;
+ }
+
+ if (container) container.classList.remove('hidden');
+
+ if (password1 === password2) {
+ if (success) success.classList.remove('hidden');
+ if (error) error.classList.add('hidden');
+ return true;
+ } else {
+ if (success) success.classList.add('hidden');
+ if (error) error.classList.remove('hidden');
+ return false;
+ }
+ },
+
+ validateEmail: function(email) {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+ },
+
+ validateRequired: function(value) {
+ return value && value.trim().length > 0;
+ },
+
+ validateMinLength: function(value, minLength) {
+ return value && value.length >= minLength;
+ },
+
+ validateMaxLength: function(value, maxLength) {
+ return value && value.length <= maxLength;
+ },
+
+ validateNumeric: function(value) {
+ return !isNaN(value) && !isNaN(parseFloat(value));
+ },
+
+ validateRange: function(value, min, max) {
+ const num = parseFloat(value);
+ return !isNaN(num) && num >= min && num <= max;
+ },
+
+ showError: function(element, message) {
+ if (!element) return;
+
+ element.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
+ element.classList.remove('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500');
+
+ let errorElement = element.parentElement.querySelector('.validation-error');
+ if (!errorElement) {
+ errorElement = document.createElement('p');
+ errorElement.className = 'validation-error text-red-600 dark:text-red-400 text-sm mt-1';
+ element.parentElement.appendChild(errorElement);
+ }
+
+ errorElement.textContent = message;
+ errorElement.classList.remove('hidden');
+ },
+
+ clearError: function(element) {
+ if (!element) return;
+
+ element.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
+ element.classList.add('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500');
+
+ const errorElement = element.parentElement.querySelector('.validation-error');
+ if (errorElement) {
+ errorElement.classList.add('hidden');
+ }
+ },
+
+ validateForm: function(form, rules) {
+ if (!form || !rules) return false;
+
+ let isValid = true;
+
+ Object.keys(rules).forEach(fieldName => {
+ const field = form.querySelector(`[name="${fieldName}"]`);
+ if (!field) return;
+
+ const fieldRules = rules[fieldName];
+ let fieldValid = true;
+ let errorMessage = '';
+
+ if (fieldRules.required && !this.validateRequired(field.value)) {
+ fieldValid = false;
+ errorMessage = fieldRules.requiredMessage || 'This field is required';
+ }
+
+ if (fieldValid && fieldRules.minLength && !this.validateMinLength(field.value, fieldRules.minLength)) {
+ fieldValid = false;
+ errorMessage = fieldRules.minLengthMessage || `Minimum ${fieldRules.minLength} characters required`;
+ }
+
+ if (fieldValid && fieldRules.maxLength && !this.validateMaxLength(field.value, fieldRules.maxLength)) {
+ fieldValid = false;
+ errorMessage = fieldRules.maxLengthMessage || `Maximum ${fieldRules.maxLength} characters allowed`;
+ }
+
+ if (fieldValid && fieldRules.email && !this.validateEmail(field.value)) {
+ fieldValid = false;
+ errorMessage = fieldRules.emailMessage || 'Invalid email format';
+ }
+
+ if (fieldValid && fieldRules.numeric && !this.validateNumeric(field.value)) {
+ fieldValid = false;
+ errorMessage = fieldRules.numericMessage || 'Must be a number';
+ }
+
+ if (fieldValid && fieldRules.range && !this.validateRange(field.value, fieldRules.range.min, fieldRules.range.max)) {
+ fieldValid = false;
+ errorMessage = fieldRules.rangeMessage || `Must be between ${fieldRules.range.min} and ${fieldRules.range.max}`;
+ }
+
+ if (fieldValid && fieldRules.custom) {
+ const customResult = fieldRules.custom(field.value, form);
+ if (!customResult.valid) {
+ fieldValid = false;
+ errorMessage = customResult.message || 'Invalid value';
+ }
+ }
+
+ if (fieldValid) {
+ this.clearError(field);
+ } else {
+ this.showError(field, errorMessage);
+ isValid = false;
+ }
+ });
+
+ return isValid;
+ }
+ };
+
+ window.FormValidator = FormValidator;
+
+})();
diff --git a/basicswap/static/js/modules/identity-manager.js b/basicswap/static/js/modules/identity-manager.js
index 68359f7..bebc49d 100644
--- a/basicswap/static/js/modules/identity-manager.js
+++ b/basicswap/static/js/modules/identity-manager.js
@@ -23,10 +23,24 @@ const IdentityManager = (function() {
return null;
}
- const cachedData = this.getCachedIdentity(address);
- if (cachedData) {
- log(`Cache hit for ${address}`);
- return cachedData;
+ const cached = state.cache.get(address);
+ const now = Date.now();
+
+ if (cached && (now - cached.timestamp) < state.config.cacheTimeout) {
+ log(`Cache hit (fresh) for ${address}`);
+ return cached.data;
+ }
+
+ if (cached && (now - cached.timestamp) < state.config.cacheTimeout * 2) {
+ log(`Cache hit (stale) for ${address}, refreshing in background`);
+
+ const staleData = cached.data;
+
+ if (!state.pendingRequests.has(address)) {
+ this.refreshIdentityInBackground(address);
+ }
+
+ return staleData;
}
if (state.pendingRequests.has(address)) {
@@ -47,6 +61,20 @@ const IdentityManager = (function() {
}
},
+ refreshIdentityInBackground: function(address) {
+ const request = fetchWithRetry(address);
+ state.pendingRequests.set(address, request);
+
+ request.then(data => {
+ this.setCachedIdentity(address, data);
+ log(`Background refresh completed for ${address}`);
+ }).catch(error => {
+ log(`Background refresh failed for ${address}:`, error);
+ }).finally(() => {
+ state.pendingRequests.delete(address);
+ });
+ },
+
getCachedIdentity: function(address) {
const cached = state.cache.get(address);
if (cached && (Date.now() - cached.timestamp) < state.config.cacheTimeout) {
@@ -155,15 +183,23 @@ const IdentityManager = (function() {
async function fetchWithRetry(address, attempt = 1) {
try {
- const response = await fetch(`/json/identities/${address}`, {
- signal: AbortSignal.timeout(5000)
- });
+ let data;
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
+ if (window.ApiManager) {
+ data = await window.ApiManager.makeRequest(`/json/identities/${address}`, 'GET');
+ } else {
+ const response = await fetch(`/json/identities/${address}`, {
+ signal: AbortSignal.timeout(5000)
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ data = await response.json();
}
- return await response.json();
+ return data;
} catch (error) {
if (attempt >= state.config.maxRetries) {
console.error(`[IdentityManager] Error:`, error.message);
@@ -171,7 +207,10 @@ const IdentityManager = (function() {
return null;
}
- await new Promise(resolve => setTimeout(resolve, state.config.retryDelay * attempt));
+ const delay = state.config.retryDelay * attempt;
+ await new Promise(resolve => {
+ CleanupManager.setTimeout(resolve, delay);
+ });
return fetchWithRetry(address, attempt + 1);
}
}
@@ -188,5 +227,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
-//console.log('IdentityManager initialized with methods:', Object.keys(IdentityManager));
console.log('IdentityManager initialized');
diff --git a/basicswap/static/js/modules/network-manager.js b/basicswap/static/js/modules/network-manager.js
index 6a8a932..dcf6f34 100644
--- a/basicswap/static/js/modules/network-manager.js
+++ b/basicswap/static/js/modules/network-manager.js
@@ -108,7 +108,7 @@ const NetworkManager = (function() {
log(`Scheduling reconnection attempt in ${delay/1000} seconds`);
- state.reconnectTimer = setTimeout(() => {
+ state.reconnectTimer = CleanupManager.setTimeout(() => {
state.reconnectTimer = null;
this.attemptReconnect();
}, delay);
@@ -167,7 +167,20 @@ const NetworkManager = (function() {
});
},
- testBackendConnection: function() {
+ testBackendConnection: async function() {
+ if (window.ApiManager) {
+ try {
+ await window.ApiManager.makeRequest(config.connectionTestEndpoint, 'HEAD', {
+ 'Cache-Control': 'no-cache',
+ 'Pragma': 'no-cache'
+ });
+ return true;
+ } catch (error) {
+ log('Backend connection test failed:', error.message);
+ return false;
+ }
+ }
+
return fetch(config.connectionTestEndpoint, {
method: 'HEAD',
headers: {
@@ -275,6 +288,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
-//console.log('NetworkManager initialized with methods:', Object.keys(NetworkManager));
console.log('NetworkManager initialized');
-
diff --git a/basicswap/static/js/modules/notification-manager.js b/basicswap/static/js/modules/notification-manager.js
index 751faa1..fcea181 100644
--- a/basicswap/static/js/modules/notification-manager.js
+++ b/basicswap/static/js/modules/notification-manager.js
@@ -1,6 +1,5 @@
const NotificationManager = (function() {
-
const defaultConfig = {
showNewOffers: false,
showNewBids: true,
@@ -12,7 +11,6 @@ const NotificationManager = (function() {
notificationDuration: 20000
};
-
function loadConfig() {
const saved = localStorage.getItem('notification_settings');
if (saved) {
@@ -25,7 +23,6 @@ const NotificationManager = (function() {
return { ...defaultConfig };
}
-
function saveConfig(newConfig) {
try {
localStorage.setItem('notification_settings', JSON.stringify(newConfig));
@@ -269,7 +266,6 @@ function ensureToastContainer() {
return colors[type] || colors['success'];
}
-// Todo: Remove later and use global.
function getCoinDisplayName(coinId) {
const coinMap = {
1: 'PART',
@@ -292,25 +288,24 @@ function ensureToastContainer() {
return coinMap[coinId] || `Coin ${coinId}`;
}
-// Todo: Remove later.
function formatCoinAmount(amount, coinId) {
const divisors = {
- 1: 100000000, // PART - 8 decimals
- 2: 100000000, // BTC - 8 decimals
- 3: 100000000, // LTC - 8 decimals
- 4: 100000000, // DCR - 8 decimals
- 5: 100000000, // NMC - 8 decimals
+ 1: 100000000, // PART - 8 decimals
+ 2: 100000000, // BTC - 8 decimals
+ 3: 100000000, // LTC - 8 decimals
+ 4: 100000000, // DCR - 8 decimals
+ 5: 100000000, // NMC - 8 decimals
6: 1000000000000, // XMR - 12 decimals
- 7: 100000000, // PART (Blind) - 8 decimals
- 8: 100000000, // PART (Anon) - 8 decimals
- 9: 100000000000, // WOW - 11 decimals
- 11: 100000000, // PIVX - 8 decimals
- 12: 100000000, // DASH - 8 decimals
- 13: 100000000, // FIRO - 8 decimals
- 14: 100000000, // NAV - 8 decimals
- 15: 100000000, // LTC (MWEB) - 8 decimals
- 17: 100000000, // BCH - 8 decimals
- 18: 100000000 // DOGE - 8 decimals
+ 7: 100000000, // PART (Blind) - 8 decimals
+ 8: 100000000, // PART (Anon) - 8 decimals
+ 9: 100000000000, // WOW - 11 decimals
+ 11: 100000000, // PIVX - 8 decimals
+ 12: 100000000, // DASH - 8 decimals
+ 13: 100000000, // FIRO - 8 decimals
+ 14: 100000000, // NAV - 8 decimals
+ 15: 100000000, // LTC (MWEB) - 8 decimals
+ 17: 100000000, // BCH - 8 decimals
+ 18: 100000000 // DOGE - 8 decimals
};
const divisor = divisors[coinId] || 100000000;
@@ -358,7 +353,7 @@ function ensureToastContainer() {
testToasts: function() {
if (!this.createToast) return;
- setTimeout(() => {
+ CleanupManager.setTimeout(() => {
this.createToast(
'+0.05000000 PART',
'balance_change',
@@ -366,7 +361,7 @@ function ensureToastContainer() {
);
}, 500);
- setTimeout(() => {
+ CleanupManager.setTimeout(() => {
this.createToast(
'+0.00123456 XMR',
'balance_change',
@@ -374,7 +369,7 @@ function ensureToastContainer() {
);
}, 1000);
- setTimeout(() => {
+ CleanupManager.setTimeout(() => {
this.createToast(
'-29.86277595 PART',
'balance_change',
@@ -382,7 +377,7 @@ function ensureToastContainer() {
);
}, 1500);
- setTimeout(() => {
+ CleanupManager.setTimeout(() => {
this.createToast(
'-0.05000000 PART (Anon)',
'balance_change',
@@ -390,7 +385,7 @@ function ensureToastContainer() {
);
}, 2000);
- setTimeout(() => {
+ CleanupManager.setTimeout(() => {
this.createToast(
'+1.23456789 PART (Anon)',
'balance_change',
@@ -398,33 +393,37 @@ function ensureToastContainer() {
);
}, 2500);
- setTimeout(() => {
+ CleanupManager.setTimeout(() => {
+ const btcIcon = getCoinIcon('BTC');
+ const xmrIcon = getCoinIcon('XMR');
this.createToast(
- '1.00000000 BTC →
15.50000000 XMR',
+ 'New Network Offer',
'new_offer',
{
offerId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66',
- subtitle: 'New offer • Rate: 1 BTC = 15.50000000 XMR',
+ subtitle: `
1.00000000 BTC →
15.50000000 XMR
Rate: 1 BTC = 15.50000000 XMR`,
coinFrom: 2,
coinTo: 6
}
);
}, 3000);
- setTimeout(() => {
+ CleanupManager.setTimeout(() => {
+ const btcIcon = getCoinIcon('BTC');
+ const xmrIcon = getCoinIcon('XMR');
this.createToast(
- '0.50000000 BTC →
7.75000000 XMR',
+ 'New Bid Received',
'new_bid',
{
bidId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66',
- subtitle: 'New bid • Rate: 1 BTC = 15.50000000 XMR',
+ subtitle: `
0.50000000 BTC →
7.75000000 XMR
Rate: 1 BTC = 15.50000000 XMR`,
coinFrom: 2,
coinTo: 6
}
);
}, 3500);
- setTimeout(() => {
+ CleanupManager.setTimeout(() => {
this.createToast(
'Swap completed successfully',
'swap_completed',
@@ -435,25 +434,68 @@ function ensureToastContainer() {
);
}, 4000);
- setTimeout(() => {
- this.createToast(
- 'Update Available: v0.15.0',
- 'update_available',
- {
- subtitle: 'Current: v0.14.6 • Click to view release',
- releaseUrl: 'https://github.com/basicswap/basicswap/releases/tag/v0.15.0',
- releaseNotes: 'New version v0.15.0 is available. Click to view details on GitHub.'
+ CleanupManager.setTimeout(async () => {
+ try {
+ const response = await fetch('/json/checkupdates', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' }
+ });
+ const data = await response.json();
+
+ if (data.error) {
+ console.warn('Test notification - API returned error, using fallback:', data.error);
+ this.createToast(
+ 'Update Available: v0.15.0',
+ 'update_available',
+ {
+ subtitle: 'Current: v0.14.6 • Click to view release',
+ releaseUrl: 'https://github.com/basicswap/basicswap/releases/tag/v0.15.0',
+ releaseNotes: 'New version v0.15.0 is available. Click to view details on GitHub.'
+ }
+ );
+ return;
}
- );
+
+ const currentVer = (data.current_version && String(data.current_version) !== 'null' && String(data.current_version) !== 'None')
+ ? String(data.current_version)
+ : '0.14.6';
+ const latestVer = (data.latest_version && String(data.latest_version) !== 'null' && String(data.latest_version) !== 'None')
+ ? String(data.latest_version)
+ : currentVer;
+
+ this.createToast(
+ `Update Available: v${latestVer}`,
+ 'update_available',
+ {
+ subtitle: `Current: v${currentVer} • Click to view release`,
+ releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${latestVer}`,
+ releaseNotes: `New version v${latestVer} is available. Click to view details on GitHub.`
+ }
+ );
+ } catch (error) {
+ console.error('Test notification - API error:', error);
+ this.createToast(
+ 'Update Available: v0.15.0',
+ 'update_available',
+ {
+ subtitle: 'Current: v0.14.6 • Click to view release',
+ releaseUrl: 'https://github.com/basicswap/basicswap/releases/tag/v0.15.0',
+ releaseNotes: 'New version v0.15.0 is available. Click to view details on GitHub.'
+ }
+ );
+ }
}, 4500);
},
- initializeBalanceTracking: function() {
+ initializeBalanceTracking: async function() {
this.checkAndResetStaleBalanceTracking();
- fetch('/json/walletbalances')
- .then(response => response.json())
+ const fetchBalances = window.ApiManager
+ ? window.ApiManager.makeRequest('/json/walletbalances', 'GET')
+ : fetch('/json/walletbalances').then(response => response.json());
+
+ fetchBalances
.then(balanceData => {
if (Array.isArray(balanceData)) {
balanceData.forEach(coin => {
@@ -533,7 +575,6 @@ function ensureToastContainer() {
coinIconHtml = ``;
}
-
let clickAction = '';
let cursorStyle = 'cursor-default';
@@ -585,10 +626,10 @@ function ensureToastContainer() {
messages.appendChild(message);
if (!isPersistent) {
- setTimeout(() => {
+ CleanupManager.setTimeout(() => {
if (message.parentNode) {
message.classList.add('toast-slide-out');
- setTimeout(() => {
+ CleanupManager.setTimeout(() => {
if (message.parentNode) {
message.parentNode.removeChild(message);
}
@@ -613,12 +654,12 @@ function ensureToastContainer() {
const amountTo = formatCoinAmount(data.amount_to, data.coin_to);
const coinFromIcon = getCoinIcon(coinFromName);
const coinToIcon = getCoinIcon(coinToName);
- toastTitle = `
${amountFrom} ${coinFromName} →
${amountTo} ${coinToName}`;
- toastOptions.subtitle = `New offer • Rate: 1 ${coinFromName} = ${(data.amount_to / data.amount_from).toFixed(8)} ${coinToName}`;
+ toastTitle = `New Network Offer`;
+ toastOptions.subtitle = `
${amountFrom} ${coinFromName} →
${amountTo} ${coinToName}
Rate: 1 ${coinFromName} = ${(data.amount_to / data.amount_from).toFixed(8)} ${coinToName}`;
toastOptions.coinFrom = data.coin_from;
toastOptions.coinTo = data.coin_to;
} else {
- toastTitle = `New network offer`;
+ toastTitle = `New Network Offer`;
toastOptions.subtitle = 'Click to view offer';
}
toastType = 'new_offer';
@@ -633,12 +674,12 @@ function ensureToastContainer() {
const bidAmountTo = formatCoinAmount(data.bid_amount_to, data.coin_to);
const coinFromIcon = getCoinIcon(coinFromName);
const coinToIcon = getCoinIcon(coinToName);
- toastTitle = `${bidAmountFrom} ${coinFromName} →
${bidAmountTo} ${coinToName}`;
- toastOptions.subtitle = `New bid • Rate: 1 ${coinFromName} = ${(data.bid_amount_to / data.bid_amount).toFixed(8)} ${coinToName}`;
+ toastTitle = `New Bid Received`;
+ toastOptions.subtitle = `
${bidAmountFrom} ${coinFromName} →
${bidAmountTo} ${coinToName}
Rate: 1 ${coinFromName} = ${(data.bid_amount_to / data.bid_amount).toFixed(8)} ${coinToName}`;
toastOptions.coinFrom = data.coin_from;
toastOptions.coinTo = data.coin_to;
} else {
- toastTitle = `New bid received`;
+ toastTitle = `New Bid Received`;
toastOptions.subtitle = 'Click to view bid';
}
toastOptions.bidId = data.bid_id;
@@ -696,14 +737,17 @@ function ensureToastContainer() {
this.balanceTimeouts = {};
}
- this.balanceTimeouts[balanceKey] = setTimeout(() => {
+ this.balanceTimeouts[balanceKey] = CleanupManager.setTimeout(() => {
this.fetchAndShowBalanceChange(data.coin);
}, 2000);
},
fetchAndShowBalanceChange: function(coinSymbol) {
- fetch('/json/walletbalances')
- .then(response => response.json())
+ const fetchBalances = window.ApiManager
+ ? window.ApiManager.makeRequest('/json/walletbalances', 'GET')
+ : fetch('/json/walletbalances').then(response => response.json());
+
+ fetchBalances
.then(balanceData => {
if (Array.isArray(balanceData)) {
@@ -748,13 +792,10 @@ function ensureToastContainer() {
const pendingIncrease = currentPending - prevPending;
const pendingDecrease = prevPending - currentPending;
-
-
const totalChange = Math.abs(balanceIncrease) + Math.abs(pendingIncrease);
const maxReasonableChange = Math.max(currentBalance, prevBalance) * 0.5;
if (totalChange > maxReasonableChange && totalChange > 1.0) {
- console.log(`Detected potentially stale balance data for ${coinData.name}, resetting tracking`);
localStorage.setItem(storageKey, currentBalance.toString());
localStorage.setItem(pendingStorageKey, currentPending.toString());
return;
@@ -782,7 +823,6 @@ function ensureToastContainer() {
const isPendingToConfirmed = pendingDecrease > 0.00000001 && balanceIncrease > 0.00000001;
-
const displaySymbol = originalCoinSymbol;
let variantInfo = '';
@@ -871,8 +911,6 @@ function ensureToastContainer() {
}
},
-
-
updateConfig: function(newConfig) {
Object.assign(config, newConfig);
return this;
diff --git a/basicswap/static/js/modules/price-manager.js b/basicswap/static/js/modules/price-manager.js
index 5ef672b..a24ef20 100644
--- a/basicswap/static/js/modules/price-manager.js
+++ b/basicswap/static/js/modules/price-manager.js
@@ -42,7 +42,7 @@ const PriceManager = (function() {
});
}
- setTimeout(() => this.getPrices(), 1500);
+ CleanupManager.setTimeout(() => this.getPrices(), 1500);
isInitialized = true;
console.log('PriceManager initialized');
return this;
@@ -60,7 +60,6 @@ const PriceManager = (function() {
return fetchPromise;
}
-
lastFetchTime = Date.now();
fetchPromise = this.fetchPrices()
.then(prices => {
@@ -90,8 +89,6 @@ const PriceManager = (function() {
? 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');
}
@@ -133,15 +130,15 @@ const PriceManager = (function() {
const coin = window.CoinManager.getCoinByAnyIdentifier(coinId);
if (coin) {
normalizedCoinId = window.CoinManager.getPriceKey(coin.name);
+ } else if (window.CoinUtils) {
+ normalizedCoinId = window.CoinUtils.normalizeCoinName(coinId);
} else {
- normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
+ normalizedCoinId = coinId.toLowerCase();
}
+ } else if (window.CoinUtils) {
+ normalizedCoinId = window.CoinUtils.normalizeCoinName(coinId);
} else {
- normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
- }
-
- if (coinId.toLowerCase() === 'zcoin') {
- normalizedCoinId = 'firo';
+ normalizedCoinId = coinId.toLowerCase();
}
processedData[normalizedCoinId] = {
@@ -230,5 +227,3 @@ document.addEventListener('DOMContentLoaded', function() {
window.priceManagerInitialized = true;
}
});
-
-
diff --git a/basicswap/static/js/modules/qrcode-manager.js b/basicswap/static/js/modules/qrcode-manager.js
new file mode 100644
index 0000000..9a002f8
--- /dev/null
+++ b/basicswap/static/js/modules/qrcode-manager.js
@@ -0,0 +1,79 @@
+
+(function() {
+ 'use strict';
+
+ const QRCodeManager = {
+
+ defaultOptions: {
+ width: 200,
+ height: 200,
+ colorDark: "#000000",
+ colorLight: "#ffffff",
+ correctLevel: QRCode.CorrectLevel.L
+ },
+
+ initialize: function() {
+ const qrElements = document.querySelectorAll('[data-qrcode]');
+
+ qrElements.forEach(element => {
+ this.generateQRCode(element);
+ });
+ },
+
+ generateQRCode: function(element) {
+ const address = element.getAttribute('data-address');
+ const width = parseInt(element.getAttribute('data-width')) || this.defaultOptions.width;
+ const height = parseInt(element.getAttribute('data-height')) || this.defaultOptions.height;
+
+ if (!address) {
+ console.error('QRCodeManager: No address provided for element', element);
+ return;
+ }
+
+ element.innerHTML = '';
+
+ try {
+ new QRCode(element, {
+ text: address,
+ width: width,
+ height: height,
+ colorDark: this.defaultOptions.colorDark,
+ colorLight: this.defaultOptions.colorLight,
+ correctLevel: this.defaultOptions.correctLevel
+ });
+ } catch (error) {
+ console.error('QRCodeManager: Failed to generate QR code', error);
+ }
+ },
+
+ generateById: function(elementId, address, options = {}) {
+
+ const element = window.DOMCache
+ ? window.DOMCache.get(elementId)
+ : document.getElementById(elementId);
+
+ if (!element) {
+ console.error('QRCodeManager: Element not found:', elementId);
+ return;
+ }
+
+ element.setAttribute('data-address', address);
+
+ if (options.width) element.setAttribute('data-width', options.width);
+ if (options.height) element.setAttribute('data-height', options.height);
+
+ this.generateQRCode(element);
+ }
+ };
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', function() {
+ QRCodeManager.initialize();
+ });
+ } else {
+ QRCodeManager.initialize();
+ }
+
+ window.QRCodeManager = QRCodeManager;
+
+})();
diff --git a/basicswap/static/js/modules/summary-manager.js b/basicswap/static/js/modules/summary-manager.js
index 251ebf2..99efe2f 100644
--- a/basicswap/static/js/modules/summary-manager.js
+++ b/basicswap/static/js/modules/summary-manager.js
@@ -166,7 +166,7 @@ const SummaryManager = (function() {
}
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
- setTimeout(() => {
+ CleanupManager.setTimeout(() => {
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
debugLog(`Re-initialized tooltips for ${tooltipId}`);
}, 50);
@@ -205,8 +205,16 @@ const SummaryManager = (function() {
}
function fetchSummaryDataWithTimeout() {
+ if (window.ApiManager) {
+ return window.ApiManager.makeRequest(config.summaryEndpoint, 'GET', {
+ 'Accept': 'application/json',
+ 'Cache-Control': 'no-cache',
+ 'Pragma': 'no-cache'
+ });
+ }
+
const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), config.requestTimeout);
+ const timeoutId = CleanupManager.setTimeout(() => controller.abort(), config.requestTimeout);
return fetch(config.summaryEndpoint, {
signal: controller.signal,
@@ -217,7 +225,11 @@ const SummaryManager = (function() {
}
})
.then(response => {
- clearTimeout(timeoutId);
+ if (window.CleanupManager) {
+ window.CleanupManager.clearTimeout(timeoutId);
+ } else {
+ clearTimeout(timeoutId);
+ }
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
@@ -226,7 +238,11 @@ const SummaryManager = (function() {
return response.json();
})
.catch(error => {
- clearTimeout(timeoutId);
+ if (window.CleanupManager) {
+ window.CleanupManager.clearTimeout(timeoutId);
+ } else {
+ clearTimeout(timeoutId);
+ }
throw error;
});
}
@@ -275,7 +291,7 @@ const SummaryManager = (function() {
};
webSocket.onclose = () => {
- setTimeout(setupWebSocket, 5000);
+ CleanupManager.setTimeout(setupWebSocket, 5000);
};
}
@@ -303,7 +319,7 @@ const SummaryManager = (function() {
.then(() => {})
.catch(() => {});
- refreshTimer = setInterval(() => {
+ refreshTimer = CleanupManager.setInterval(() => {
publicAPI.fetchSummaryData()
.then(() => {})
.catch(() => {});
@@ -386,7 +402,7 @@ const SummaryManager = (function() {
}
return new Promise(resolve => {
- setTimeout(() => {
+ CleanupManager.setTimeout(() => {
resolve(this.fetchSummaryData());
}, config.retryDelay);
});
@@ -446,5 +462,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
-//console.log('SummaryManager initialized with methods:', Object.keys(SummaryManager));
console.log('SummaryManager initialized');
diff --git a/basicswap/static/js/modules/tooltips-manager.js b/basicswap/static/js/modules/tooltips-manager.js
index 8fa7613..b43c007 100644
--- a/basicswap/static/js/modules/tooltips-manager.js
+++ b/basicswap/static/js/modules/tooltips-manager.js
@@ -14,6 +14,9 @@ const TooltipManager = (function() {
this.debug = false;
this.tooltipData = new WeakMap();
this.resources = {};
+ this.creationQueue = [];
+ this.batchSize = 5;
+ this.isProcessingQueue = false;
if (window.CleanupManager) {
CleanupManager.registerResource(
@@ -48,40 +51,69 @@ const TooltipManager = (function() {
this.performPeriodicCleanup(true);
}
- const createTooltip = () => {
- if (!document.body.contains(element)) return;
+ this.creationQueue.push({ element, content, options });
- const rect = element.getBoundingClientRect();
- if (rect.width > 0 && rect.height > 0) {
- this.createTooltipInstance(element, content, options);
- } else {
- let retryCount = 0;
- const maxRetries = 3;
+ if (!this.isProcessingQueue) {
+ this.processCreationQueue();
+ }
- const retryCreate = () => {
- const newRect = element.getBoundingClientRect();
- if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) {
- if (newRect.width > 0 && newRect.height > 0) {
- this.createTooltipInstance(element, content, options);
- }
- } else {
- retryCount++;
- CleanupManager.setTimeout(() => {
- CleanupManager.requestAnimationFrame(retryCreate);
- }, 100);
- }
- };
-
- CleanupManager.setTimeout(() => {
- CleanupManager.requestAnimationFrame(retryCreate);
- }, 100);
- }
- };
-
- CleanupManager.requestAnimationFrame(createTooltip);
return null;
}
+ processCreationQueue() {
+ if (this.creationQueue.length === 0) {
+ this.isProcessingQueue = false;
+ return;
+ }
+
+ this.isProcessingQueue = true;
+ const batch = this.creationQueue.splice(0, this.batchSize);
+
+ CleanupManager.requestAnimationFrame(() => {
+ batch.forEach(({ element, content, options }) => {
+ this.createTooltipSync(element, content, options);
+ });
+
+ if (this.creationQueue.length > 0) {
+ CleanupManager.setTimeout(() => {
+ this.processCreationQueue();
+ }, 0);
+ } else {
+ this.isProcessingQueue = false;
+ }
+ });
+ }
+
+ createTooltipSync(element, content, options) {
+ if (!document.body.contains(element)) return;
+
+ const rect = element.getBoundingClientRect();
+ if (rect.width > 0 && rect.height > 0) {
+ this.createTooltipInstance(element, content, options);
+ } else {
+ let retryCount = 0;
+ const maxRetries = 3;
+
+ const retryCreate = () => {
+ const newRect = element.getBoundingClientRect();
+ if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) {
+ if (newRect.width > 0 && newRect.height > 0) {
+ this.createTooltipInstance(element, content, options);
+ }
+ } else {
+ retryCount++;
+ CleanupManager.setTimeout(() => {
+ CleanupManager.requestAnimationFrame(retryCreate);
+ }, 100);
+ }
+ };
+
+ CleanupManager.setTimeout(() => {
+ CleanupManager.requestAnimationFrame(retryCreate);
+ }, 100);
+ }
+ }
+
createTooltipInstance(element, content, options = {}) {
if (!element || !document.body.contains(element)) {
return null;
@@ -191,6 +223,9 @@ const TooltipManager = (function() {
return null;
} catch (error) {
+ if (window.ErrorHandler) {
+ return window.ErrorHandler.handleError(error, 'TooltipManager.createTooltipInstance', null);
+ }
console.error('Error creating tooltip:', error);
return null;
}
@@ -199,7 +234,7 @@ const TooltipManager = (function() {
destroy(element) {
if (!element) return;
- try {
+ const destroyFn = () => {
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
if (!tooltipId) return;
@@ -224,8 +259,16 @@ const TooltipManager = (function() {
this.tooltipData.delete(element);
tooltipInstanceMap.delete(element);
- } catch (error) {
- console.error('Error destroying tooltip:', error);
+ };
+
+ if (window.ErrorHandler) {
+ window.ErrorHandler.safeExecute(destroyFn, 'TooltipManager.destroy', null);
+ } else {
+ try {
+ destroyFn();
+ } catch (error) {
+ console.error('Error destroying tooltip:', error);
+ }
}
}
@@ -738,10 +781,40 @@ const TooltipManager = (function() {
}
}
+ initializeLazyTooltips(selector = '[data-tooltip-target]') {
+
+ const initializedTooltips = new Set();
+
+ const initializeTooltip = (element) => {
+ const targetId = element.getAttribute('data-tooltip-target');
+ if (!targetId || initializedTooltips.has(targetId)) return;
+
+ const tooltipContent = document.getElementById(targetId);
+ if (tooltipContent) {
+ this.create(element, tooltipContent.innerHTML, {
+ placement: element.getAttribute('data-tooltip-placement') || 'top'
+ });
+ initializedTooltips.add(targetId);
+ }
+ };
+
+ document.addEventListener('mouseover', (e) => {
+ const target = e.target.closest(selector);
+ if (target) {
+ initializeTooltip(target);
+ }
+ }, { passive: true, capture: true });
+
+ this.log('Lazy tooltip initialization enabled');
+ }
+
dispose() {
this.log('Disposing TooltipManager');
try {
+ this.creationQueue = [];
+ this.isProcessingQueue = false;
+
this.cleanup();
Object.values(this.resources).forEach(resourceId => {
@@ -830,6 +903,11 @@ const TooltipManager = (function() {
return manager.initializeTooltips(...args);
},
+ initializeLazyTooltips: function(...args) {
+ const manager = this.getInstance();
+ return manager.initializeLazyTooltips(...args);
+ },
+
setDebugMode: function(enabled) {
const manager = this.getInstance();
return manager.setDebugMode(enabled);
diff --git a/basicswap/static/js/modules/wallet-amount.js b/basicswap/static/js/modules/wallet-amount.js
new file mode 100644
index 0000000..3e14cb5
--- /dev/null
+++ b/basicswap/static/js/modules/wallet-amount.js
@@ -0,0 +1,196 @@
+(function() {
+ 'use strict';
+
+ const WalletAmountManager = {
+
+ coinConfigs: {
+ 1: {
+ types: ['plain', 'blind', 'anon'],
+ hasSubfee: true,
+ hasSweepAll: false
+ },
+ 3: {
+ types: ['plain', 'mweb'],
+ hasSubfee: true,
+ hasSweepAll: false
+ },
+ 6: {
+ types: ['default'],
+ hasSubfee: false,
+ hasSweepAll: true
+ },
+ 9: {
+ types: ['default'],
+ hasSubfee: false,
+ hasSweepAll: true
+ }
+ },
+
+ safeParseFloat: function(value) {
+ const numValue = Number(value);
+
+ if (!isNaN(numValue) && numValue > 0) {
+ return numValue;
+ }
+
+ console.warn('WalletAmountManager: Invalid balance value:', value);
+ return 0;
+ },
+
+ getBalance: function(coinId, balances, selectedType) {
+ const cid = parseInt(coinId);
+
+ if (cid === 1) {
+ switch(selectedType) {
+ case 'plain':
+ return this.safeParseFloat(balances.main || balances.balance);
+ case 'blind':
+ return this.safeParseFloat(balances.blind);
+ case 'anon':
+ return this.safeParseFloat(balances.anon);
+ default:
+ return this.safeParseFloat(balances.main || balances.balance);
+ }
+ }
+
+ if (cid === 3) {
+ switch(selectedType) {
+ case 'plain':
+ return this.safeParseFloat(balances.main || balances.balance);
+ case 'mweb':
+ return this.safeParseFloat(balances.mweb);
+ default:
+ return this.safeParseFloat(balances.main || balances.balance);
+ }
+ }
+
+ return this.safeParseFloat(balances.main || balances.balance);
+ },
+
+ calculateAmount: function(balance, percent, coinId) {
+ const cid = parseInt(coinId);
+
+ if (percent === 1) {
+ return balance;
+ }
+
+ if (cid === 1) {
+ return Math.max(0, Math.floor(balance * percent * 100000000) / 100000000);
+ }
+
+ const calculatedAmount = balance * percent;
+
+ if (calculatedAmount < 0.00000001) {
+ console.warn('WalletAmountManager: Calculated amount too small, setting to zero');
+ return 0;
+ }
+
+ return calculatedAmount;
+ },
+
+ setAmount: function(percent, balances, coinId) {
+
+ const amountInput = window.DOMCache
+ ? window.DOMCache.get('amount')
+ : document.getElementById('amount');
+ const typeSelect = window.DOMCache
+ ? window.DOMCache.get('withdraw_type')
+ : document.getElementById('withdraw_type');
+
+ if (!amountInput) {
+ console.error('WalletAmountManager: Amount input not found');
+ return;
+ }
+
+ const cid = parseInt(coinId);
+ const selectedType = typeSelect ? typeSelect.value : 'plain';
+
+ const balance = this.getBalance(cid, balances, selectedType);
+
+ const calculatedAmount = this.calculateAmount(balance, percent, cid);
+
+ const specialCids = [6, 9];
+ if (specialCids.includes(cid) && percent === 1) {
+ amountInput.setAttribute('data-hidden', 'true');
+ amountInput.placeholder = 'Sweep All';
+ amountInput.value = '';
+ amountInput.disabled = true;
+
+ const sweepAllCheckbox = window.DOMCache
+ ? window.DOMCache.get('sweepall')
+ : document.getElementById('sweepall');
+ if (sweepAllCheckbox) {
+ sweepAllCheckbox.checked = true;
+ }
+ } else {
+
+ amountInput.value = calculatedAmount.toFixed(8);
+ amountInput.setAttribute('data-hidden', 'false');
+ amountInput.placeholder = '';
+ amountInput.disabled = false;
+
+ const sweepAllCheckbox = window.DOMCache
+ ? window.DOMCache.get('sweepall')
+ : document.getElementById('sweepall');
+ if (sweepAllCheckbox) {
+ sweepAllCheckbox.checked = false;
+ }
+ }
+
+ const subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
+ if (subfeeCheckbox) {
+ subfeeCheckbox.checked = (percent === 1);
+ }
+ },
+
+ initialize: function() {
+
+ const amountButtons = document.querySelectorAll('[data-set-amount]');
+
+ amountButtons.forEach(button => {
+ button.addEventListener('click', (e) => {
+ e.preventDefault();
+
+ const percent = parseFloat(button.getAttribute('data-set-amount'));
+ const balancesJson = button.getAttribute('data-balances');
+ const coinId = button.getAttribute('data-coin-id');
+
+ if (!balancesJson || !coinId) {
+ console.error('WalletAmountManager: Missing data attributes on button', button);
+ return;
+ }
+
+ try {
+ const balances = JSON.parse(balancesJson);
+ this.setAmount(percent, balances, coinId);
+ } catch (error) {
+ console.error('WalletAmountManager: Failed to parse balances', error);
+ }
+ });
+ });
+ }
+ };
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', function() {
+ WalletAmountManager.initialize();
+ });
+ } else {
+ WalletAmountManager.initialize();
+ }
+
+ window.WalletAmountManager = WalletAmountManager;
+
+ window.setAmount = function(percent, balance, coinId, balance2, balance3) {
+
+ const balances = {
+ main: balance || balance,
+ balance: balance,
+ blind: balance2,
+ anon: balance3,
+ mweb: balance2
+ };
+ WalletAmountManager.setAmount(percent, balances, coinId);
+ };
+
+})();
diff --git a/basicswap/static/js/modules/wallet-manager.js b/basicswap/static/js/modules/wallet-manager.js
index dc4a1ee..4cd039a 100644
--- a/basicswap/static/js/modules/wallet-manager.js
+++ b/basicswap/static/js/modules/wallet-manager.js
@@ -11,8 +11,7 @@ const WalletManager = (function() {
defaultTTL: 300,
priceSource: {
primary: 'coingecko.com',
- fallback: 'cryptocompare.com',
- enabledSources: ['coingecko.com', 'cryptocompare.com']
+ enabledSources: ['coingecko.com']
}
};
@@ -95,22 +94,32 @@ const WalletManager = (function() {
const fetchCoinsString = coinsToFetch.join(',');
- const mainResponse = await fetch("/json/coinprices", {
- method: "POST",
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({
+ let mainData;
+
+ if (window.ApiManager) {
+ mainData = await window.ApiManager.makeRequest("/json/coinprices", "POST", {}, {
coins: fetchCoinsString,
source: currentSource,
ttl: config.defaultTTL
- })
- });
+ });
+ } else {
+ 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}`);
+ if (!mainResponse.ok) {
+ throw new Error(`HTTP error: ${mainResponse.status}`);
+ }
+
+ mainData = await mainResponse.json();
}
- const mainData = await mainResponse.json();
-
if (mainData && mainData.rates) {
document.querySelectorAll('.coinname-value').forEach(el => {
const coinName = el.getAttribute('data-coinname');
@@ -154,7 +163,7 @@ const WalletManager = (function() {
if (attempt < config.maxRetries - 1) {
const delay = Math.min(config.baseDelay * Math.pow(2, attempt), 10000);
- await new Promise(resolve => setTimeout(resolve, delay));
+ await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
}
}
}
@@ -428,7 +437,7 @@ const WalletManager = (function() {
clearTimeout(state.toggleDebounceTimer);
}
- state.toggleDebounceTimer = window.setTimeout(async () => {
+ state.toggleDebounceTimer = CleanupManager.setTimeout(async () => {
state.toggleInProgress = false;
if (newVisibility) {
await updatePrices(true);
@@ -539,7 +548,6 @@ const WalletManager = (function() {
}
}
- // Public API
const publicAPI = {
initialize: async function(options) {
if (state.initialized) {
@@ -579,7 +587,7 @@ const WalletManager = (function() {
clearInterval(state.priceUpdateInterval);
}
- state.priceUpdateInterval = setInterval(() => {
+ state.priceUpdateInterval = CleanupManager.setInterval(() => {
if (localStorage.getItem('balancesVisible') === 'true' && !state.toggleInProgress) {
updatePrices(false);
}
@@ -661,5 +669,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
-//console.log('WalletManager initialized with methods:', Object.keys(WalletManager));
console.log('WalletManager initialized');
diff --git a/basicswap/static/js/modules/websocket-manager.js b/basicswap/static/js/modules/websocket-manager.js
index d85996a..b636085 100644
--- a/basicswap/static/js/modules/websocket-manager.js
+++ b/basicswap/static/js/modules/websocket-manager.js
@@ -32,26 +32,24 @@ const WebSocketManager = (function() {
}
function determineWebSocketPort() {
- let wsPort;
+ if (window.ConfigManager && window.ConfigManager.wsPort) {
+ return window.ConfigManager.wsPort.toString();
+ }
if (window.config && window.config.wsPort) {
- wsPort = window.config.wsPort;
- return wsPort;
+ return window.config.wsPort.toString();
}
if (window.ws_port) {
- wsPort = window.ws_port.toString();
- return wsPort;
+ return window.ws_port.toString();
}
if (typeof getWebSocketConfig === 'function') {
const wsConfig = getWebSocketConfig();
- wsPort = (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
- return wsPort;
+ return (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
}
- wsPort = '11700';
- return wsPort;
+ return '11700';
}
const publicAPI = {
@@ -77,7 +75,11 @@ const WebSocketManager = (function() {
}
if (state.reconnectTimeout) {
- clearTimeout(state.reconnectTimeout);
+ if (window.CleanupManager) {
+ window.CleanupManager.clearTimeout(state.reconnectTimeout);
+ } else {
+ clearTimeout(state.reconnectTimeout);
+ }
state.reconnectTimeout = null;
}
@@ -96,13 +98,17 @@ const WebSocketManager = (function() {
ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
setupEventHandlers();
- state.connectTimeout = setTimeout(() => {
+ const timeoutFn = () => {
if (state.isConnecting) {
log('Connection timeout, cleaning up');
cleanup();
handleReconnect();
}
- }, 5000);
+ };
+
+ state.connectTimeout = window.CleanupManager
+ ? window.CleanupManager.setTimeout(timeoutFn, 5000)
+ : setTimeout(timeoutFn, 5000);
return true;
} catch (error) {
@@ -159,18 +165,25 @@ const WebSocketManager = (function() {
cleanup: function() {
log('Cleaning up WebSocket resources');
- clearTimeout(state.connectTimeout);
+ if (window.CleanupManager) {
+ window.CleanupManager.clearTimeout(state.connectTimeout);
+ } else {
+ clearTimeout(state.connectTimeout);
+ }
stopHealthCheck();
if (state.reconnectTimeout) {
- clearTimeout(state.reconnectTimeout);
+ if (window.CleanupManager) {
+ window.CleanupManager.clearTimeout(state.reconnectTimeout);
+ } else {
+ clearTimeout(state.reconnectTimeout);
+ }
state.reconnectTimeout = null;
}
state.isConnecting = false;
state.messageHandlers = {};
-
if (ws) {
ws.onopen = null;
ws.onmessage = null;
@@ -228,7 +241,11 @@ const WebSocketManager = (function() {
ws.onopen = () => {
state.isConnecting = false;
config.reconnectAttempts = 0;
- clearTimeout(state.connectTimeout);
+ if (window.CleanupManager) {
+ window.CleanupManager.clearTimeout(state.connectTimeout);
+ } else {
+ clearTimeout(state.connectTimeout);
+ }
state.lastHealthCheck = Date.now();
window.ws = ws;
@@ -298,37 +315,42 @@ const WebSocketManager = (function() {
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(() => {
+ const resumeFn = () => {
if (!publicAPI.isConnected()) {
publicAPI.connect();
}
- startHealthCheck();
- }, 0);
+ };
+
+ if (window.CleanupManager) {
+ window.CleanupManager.setTimeout(resumeFn, 0);
+ } else {
+ setTimeout(resumeFn, 0);
+ }
}
function startHealthCheck() {
stopHealthCheck();
- state.healthCheckInterval = setInterval(() => {
+ const healthCheckFn = () => {
performHealthCheck();
- }, 30000);
+ };
+ state.healthCheckInterval = window.CleanupManager
+ ? window.CleanupManager.setInterval(healthCheckFn, 30000)
+ : setInterval(healthCheckFn, 30000);
}
function stopHealthCheck() {
if (state.healthCheckInterval) {
- clearInterval(state.healthCheckInterval);
+ if (window.CleanupManager) {
+ window.CleanupManager.clearInterval(state.healthCheckInterval);
+ } else {
+ clearInterval(state.healthCheckInterval);
+ }
state.healthCheckInterval = null;
}
}
@@ -356,7 +378,11 @@ const WebSocketManager = (function() {
function handleReconnect() {
if (state.reconnectTimeout) {
- clearTimeout(state.reconnectTimeout);
+ if (window.CleanupManager) {
+ window.CleanupManager.clearTimeout(state.reconnectTimeout);
+ } else {
+ clearTimeout(state.reconnectTimeout);
+ }
state.reconnectTimeout = null;
}
@@ -369,23 +395,31 @@ const WebSocketManager = (function() {
log(`Scheduling reconnect in ${delay}ms (attempt ${config.reconnectAttempts})`);
- state.reconnectTimeout = setTimeout(() => {
+ const reconnectFn = () => {
state.reconnectTimeout = null;
if (!state.isIntentionallyClosed) {
publicAPI.connect();
}
- }, delay);
+ };
+
+ state.reconnectTimeout = window.CleanupManager
+ ? window.CleanupManager.setTimeout(reconnectFn, delay)
+ : setTimeout(reconnectFn, delay);
} else {
log('Max reconnect attempts reached');
if (typeof updateConnectionStatus === 'function') {
updateConnectionStatus('error');
}
- state.reconnectTimeout = setTimeout(() => {
+ const resetFn = () => {
state.reconnectTimeout = null;
config.reconnectAttempts = 0;
publicAPI.connect();
- }, 60000);
+ };
+
+ state.reconnectTimeout = window.CleanupManager
+ ? window.CleanupManager.setTimeout(resetFn, 60000)
+ : setTimeout(resetFn, 60000);
}
}
@@ -442,5 +476,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
-//console.log('WebSocketManager initialized with methods:', Object.keys(WebSocketManager));
console.log('WebSocketManager initialized');
diff --git a/basicswap/static/js/pages/amm-config-tabs.js b/basicswap/static/js/pages/amm-config-tabs.js
new file mode 100644
index 0000000..52ed3b8
--- /dev/null
+++ b/basicswap/static/js/pages/amm-config-tabs.js
@@ -0,0 +1,294 @@
+(function() {
+ 'use strict';
+
+ const AMMConfigTabs = {
+
+ init: function() {
+ const jsonTab = document.getElementById('json-tab');
+ const settingsTab = document.getElementById('settings-tab');
+ const overviewTab = document.getElementById('overview-tab');
+ const jsonContent = document.getElementById('json-content');
+ const settingsContent = document.getElementById('settings-content');
+ const overviewContent = document.getElementById('overview-content');
+
+ if (!jsonTab || !settingsTab || !overviewTab || !jsonContent || !settingsContent || !overviewContent) {
+ return;
+ }
+
+ const activeConfigTab = localStorage.getItem('amm_active_config_tab');
+
+ const switchConfigTab = (tabId) => {
+ jsonContent.classList.add('hidden');
+ jsonContent.classList.remove('block');
+ settingsContent.classList.add('hidden');
+ settingsContent.classList.remove('block');
+ overviewContent.classList.add('hidden');
+ overviewContent.classList.remove('block');
+
+ jsonTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
+ settingsTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
+ overviewTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
+
+ if (tabId === 'json-tab') {
+ jsonContent.classList.remove('hidden');
+ jsonContent.classList.add('block');
+ jsonTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
+ localStorage.setItem('amm_active_config_tab', 'json-tab');
+ } else if (tabId === 'settings-tab') {
+ settingsContent.classList.remove('hidden');
+ settingsContent.classList.add('block');
+ settingsTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
+ localStorage.setItem('amm_active_config_tab', 'settings-tab');
+
+ this.loadSettingsFromJson();
+ } else if (tabId === 'overview-tab') {
+ overviewContent.classList.remove('hidden');
+ overviewContent.classList.add('block');
+ overviewTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
+ localStorage.setItem('amm_active_config_tab', 'overview-tab');
+ }
+ };
+
+ jsonTab.addEventListener('click', () => switchConfigTab('json-tab'));
+ settingsTab.addEventListener('click', () => switchConfigTab('settings-tab'));
+ overviewTab.addEventListener('click', () => switchConfigTab('overview-tab'));
+
+ const returnToTab = localStorage.getItem('amm_return_to_tab');
+ if (returnToTab && (returnToTab === 'json-tab' || returnToTab === 'settings-tab' || returnToTab === 'overview-tab')) {
+ localStorage.removeItem('amm_return_to_tab');
+ switchConfigTab(returnToTab);
+ } else if (activeConfigTab === 'settings-tab') {
+ switchConfigTab('settings-tab');
+ } else if (activeConfigTab === 'overview-tab') {
+ switchConfigTab('overview-tab');
+ } else {
+ switchConfigTab('json-tab');
+ }
+
+ const globalSettingsForm = document.getElementById('global-settings-form');
+ if (globalSettingsForm) {
+ globalSettingsForm.addEventListener('submit', () => {
+ this.updateJsonFromSettings();
+ });
+ }
+
+ this.setupCollapsibles();
+
+ this.setupConfigForm();
+
+ this.setupCreateDefaultButton();
+
+ this.handleCreateDefaultRefresh();
+ },
+
+ loadSettingsFromJson: function() {
+ const configTextarea = document.querySelector('textarea[name="config_content"]');
+ if (!configTextarea) return;
+
+ try {
+ const configText = configTextarea.value.trim();
+ if (!configText) return;
+
+ const config = JSON.parse(configText);
+
+ document.getElementById('min_seconds_between_offers').value = config.min_seconds_between_offers || 15;
+ document.getElementById('max_seconds_between_offers').value = config.max_seconds_between_offers || 60;
+ document.getElementById('main_loop_delay').value = config.main_loop_delay || 60;
+
+ const minSecondsBetweenBidsEl = document.getElementById('min_seconds_between_bids');
+ const maxSecondsBetweenBidsEl = document.getElementById('max_seconds_between_bids');
+ const pruneStateDelayEl = document.getElementById('prune_state_delay');
+ const pruneStateAfterSecondsEl = document.getElementById('prune_state_after_seconds');
+
+ if (minSecondsBetweenBidsEl) minSecondsBetweenBidsEl.value = config.min_seconds_between_bids || 15;
+ if (maxSecondsBetweenBidsEl) maxSecondsBetweenBidsEl.value = config.max_seconds_between_bids || 60;
+ if (pruneStateDelayEl) pruneStateDelayEl.value = config.prune_state_delay || 120;
+ if (pruneStateAfterSecondsEl) pruneStateAfterSecondsEl.value = config.prune_state_after_seconds || 604800;
+ document.getElementById('auth').value = config.auth || '';
+ } catch (error) {
+ console.error('Error loading settings from JSON:', error);
+ }
+ },
+
+ updateJsonFromSettings: function() {
+ const configTextarea = document.querySelector('textarea[name="config_content"]');
+ if (!configTextarea) return;
+
+ try {
+ const configText = configTextarea.value.trim();
+ let config = {};
+
+ if (configText) {
+ config = JSON.parse(configText);
+ }
+
+ config.min_seconds_between_offers = parseInt(document.getElementById('min_seconds_between_offers').value) || 15;
+ config.max_seconds_between_offers = parseInt(document.getElementById('max_seconds_between_offers').value) || 60;
+ config.main_loop_delay = parseInt(document.getElementById('main_loop_delay').value) || 60;
+
+ const minSecondsBetweenBidsEl = document.getElementById('min_seconds_between_bids');
+ const maxSecondsBetweenBidsEl = document.getElementById('max_seconds_between_bids');
+ const pruneStateDelayEl = document.getElementById('prune_state_delay');
+ const pruneStateAfterSecondsEl = document.getElementById('prune_state_after_seconds');
+
+ if (minSecondsBetweenBidsEl) config.min_seconds_between_bids = parseInt(minSecondsBetweenBidsEl.value) || 15;
+ if (maxSecondsBetweenBidsEl) config.max_seconds_between_bids = parseInt(maxSecondsBetweenBidsEl.value) || 60;
+ if (pruneStateDelayEl) config.prune_state_delay = parseInt(pruneStateDelayEl.value) || 120;
+ if (pruneStateAfterSecondsEl) config.prune_state_after_seconds = parseInt(pruneStateAfterSecondsEl.value) || 604800;
+ config.auth = document.getElementById('auth').value || '';
+
+ configTextarea.value = JSON.stringify(config, null, 2);
+
+ localStorage.setItem('amm_return_to_tab', 'settings-tab');
+ } catch (error) {
+ console.error('Error updating JSON from settings:', error);
+ alert('Error updating configuration: ' + error.message);
+ }
+ },
+
+ setupCollapsibles: function() {
+ const collapsibleHeaders = document.querySelectorAll('.collapsible-header');
+
+ if (collapsibleHeaders.length === 0) return;
+
+ let collapsibleStates = {};
+ try {
+ const storedStates = localStorage.getItem('amm_collapsible_states');
+ if (storedStates) {
+ collapsibleStates = JSON.parse(storedStates);
+ }
+ } catch (e) {
+ console.error('Error parsing stored collapsible states:', e);
+ collapsibleStates = {};
+ }
+
+ const toggleCollapsible = (header) => {
+ const targetId = header.getAttribute('data-target');
+ const content = document.getElementById(targetId);
+ const arrow = header.querySelector('svg');
+
+ if (content) {
+ if (content.classList.contains('hidden')) {
+ content.classList.remove('hidden');
+ arrow.classList.add('rotate-180');
+ collapsibleStates[targetId] = 'open';
+ } else {
+ content.classList.add('hidden');
+ arrow.classList.remove('rotate-180');
+ collapsibleStates[targetId] = 'closed';
+ }
+
+ localStorage.setItem('amm_collapsible_states', JSON.stringify(collapsibleStates));
+ }
+ };
+
+ collapsibleHeaders.forEach(header => {
+ const targetId = header.getAttribute('data-target');
+ const content = document.getElementById(targetId);
+ const arrow = header.querySelector('svg');
+
+ if (content) {
+ if (collapsibleStates[targetId] === 'open') {
+ content.classList.remove('hidden');
+ arrow.classList.add('rotate-180');
+ } else {
+ content.classList.add('hidden');
+ arrow.classList.remove('rotate-180');
+ collapsibleStates[targetId] = 'closed';
+ }
+ }
+
+ header.addEventListener('click', () => toggleCollapsible(header));
+ });
+
+ localStorage.setItem('amm_collapsible_states', JSON.stringify(collapsibleStates));
+ },
+
+ setupConfigForm: function() {
+ const configForm = document.querySelector('form[method="post"]');
+ const saveConfigBtn = document.getElementById('save_config_btn');
+
+ if (configForm && saveConfigBtn) {
+ configForm.addEventListener('submit', (e) => {
+ if (e.submitter && e.submitter.name === 'save_config') {
+ localStorage.setItem('amm_update_tables', 'true');
+ }
+ });
+
+ if (localStorage.getItem('amm_update_tables') === 'true') {
+ localStorage.removeItem('amm_update_tables');
+ CleanupManager.setTimeout(() => {
+ if (window.ammTablesManager && window.ammTablesManager.updateTables) {
+ window.ammTablesManager.updateTables();
+ }
+ }, 500);
+ }
+ }
+ },
+
+ setupCreateDefaultButton: function() {
+ const createDefaultBtn = document.getElementById('create_default_btn');
+ const configForm = document.querySelector('form[method="post"]');
+
+ if (createDefaultBtn && configForm) {
+ createDefaultBtn.addEventListener('click', (e) => {
+ e.preventDefault();
+
+ const title = 'Create Default Configuration';
+ const message = 'This will overwrite your current configuration with a default template.\n\nAre you sure you want to continue?';
+
+ if (window.showConfirmModal) {
+ window.showConfirmModal(title, message, () => {
+ const hiddenInput = document.createElement('input');
+ hiddenInput.type = 'hidden';
+ hiddenInput.name = 'create_default';
+ hiddenInput.value = 'true';
+ configForm.appendChild(hiddenInput);
+
+ localStorage.setItem('amm_create_default_refresh', 'true');
+
+ configForm.submit();
+ });
+ } else {
+ if (confirm('This will overwrite your current configuration with a default template.\n\nAre you sure you want to continue?')) {
+ const hiddenInput = document.createElement('input');
+ hiddenInput.type = 'hidden';
+ hiddenInput.name = 'create_default';
+ hiddenInput.value = 'true';
+ configForm.appendChild(hiddenInput);
+
+ localStorage.setItem('amm_create_default_refresh', 'true');
+ configForm.submit();
+ }
+ }
+ });
+ }
+ },
+
+ handleCreateDefaultRefresh: function() {
+ if (localStorage.getItem('amm_create_default_refresh') === 'true') {
+ localStorage.removeItem('amm_create_default_refresh');
+
+ CleanupManager.setTimeout(() => {
+ window.location.href = window.location.pathname + window.location.search;
+ }, 500);
+ }
+ },
+
+ cleanup: function() {
+ }
+ };
+
+ document.addEventListener('DOMContentLoaded', function() {
+ AMMConfigTabs.init();
+
+ if (window.CleanupManager) {
+ CleanupManager.registerResource('ammConfigTabs', AMMConfigTabs, (tabs) => {
+ if (tabs.cleanup) tabs.cleanup();
+ });
+ }
+ });
+
+ window.AMMConfigTabs = AMMConfigTabs;
+
+})();
diff --git a/basicswap/static/js/amm_counter.js b/basicswap/static/js/pages/amm-counter.js
similarity index 95%
rename from basicswap/static/js/amm_counter.js
rename to basicswap/static/js/pages/amm-counter.js
index ec665c6..f1ca0f1 100644
--- a/basicswap/static/js/amm_counter.js
+++ b/basicswap/static/js/pages/amm-counter.js
@@ -16,13 +16,7 @@ const AmmCounterManager = (function() {
}
function debugLog(message, data) {
- // if (isDebugEnabled()) {
- // if (data) {
- // console.log(`[AmmCounter] ${message}`, data);
- // } else {
- // console.log(`[AmmCounter] ${message}`);
- // }
- // }
+
}
function updateAmmCounter(count, status) {
@@ -103,7 +97,7 @@ const AmmCounterManager = (function() {
}
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
- setTimeout(() => {
+ CleanupManager.setTimeout(() => {
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
debugLog(`Re-initialized tooltips for ${tooltipId}`);
}, 50);
@@ -148,7 +142,7 @@ const AmmCounterManager = (function() {
debugLog(`Retrying AMM status fetch (${fetchRetryCount}/${config.maxRetries}) in ${config.retryDelay/1000}s`);
return new Promise(resolve => {
- setTimeout(() => {
+ CleanupManager.setTimeout(() => {
resolve(fetchAmmStatus());
}, config.retryDelay);
});
@@ -168,7 +162,7 @@ const AmmCounterManager = (function() {
.then(() => {})
.catch(() => {});
- refreshTimer = setInterval(() => {
+ refreshTimer = CleanupManager.setInterval(() => {
fetchAmmStatus()
.then(() => {})
.catch(() => {});
@@ -251,5 +245,11 @@ document.addEventListener('DOMContentLoaded', function() {
if (!window.ammCounterManagerInitialized) {
window.AmmCounterManager = AmmCounterManager.initialize();
window.ammCounterManagerInitialized = true;
+
+ if (window.CleanupManager) {
+ CleanupManager.registerResource('ammCounter', window.AmmCounterManager, (mgr) => {
+ if (mgr && mgr.dispose) mgr.dispose();
+ });
+ }
}
});
diff --git a/basicswap/static/js/pages/amm-page.js b/basicswap/static/js/pages/amm-page.js
new file mode 100644
index 0000000..b9242c4
--- /dev/null
+++ b/basicswap/static/js/pages/amm-page.js
@@ -0,0 +1,573 @@
+(function() {
+ 'use strict';
+
+ const AMMPage = {
+
+ init: function() {
+ this.loadDebugSetting();
+ this.setupAutostartCheckbox();
+ this.setupStartupValidation();
+ this.setupDebugCheckbox();
+ this.setupModals();
+ this.setupClearStateButton();
+ this.setupWebSocketBalanceUpdates();
+ this.setupCleanup();
+ },
+
+ saveDebugSetting: function() {
+ const debugCheckbox = document.getElementById('debug-mode');
+ if (debugCheckbox) {
+ localStorage.setItem('amm_debug_enabled', debugCheckbox.checked);
+ }
+ },
+
+ loadDebugSetting: function() {
+ const debugCheckbox = document.getElementById('debug-mode');
+ if (debugCheckbox) {
+ const savedSetting = localStorage.getItem('amm_debug_enabled');
+ if (savedSetting !== null) {
+ debugCheckbox.checked = savedSetting === 'true';
+ }
+ }
+ },
+
+ setupDebugCheckbox: function() {
+ const debugCheckbox = document.getElementById('debug-mode');
+ if (debugCheckbox) {
+ debugCheckbox.addEventListener('change', this.saveDebugSetting.bind(this));
+ }
+ },
+
+ saveAutostartSetting: function(checked) {
+ const bodyData = `autostart=${checked ? 'true' : 'false'}`;
+
+ fetch('/amm/autostart', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: bodyData
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ localStorage.setItem('amm_autostart_enabled', checked);
+
+ if (data.autostart !== checked) {
+ console.warn('WARNING: API returned different autostart value than expected!', {
+ sent: checked,
+ received: data.autostart
+ });
+ }
+ } else {
+ console.error('Failed to save autostart setting:', data.error);
+ const autostartCheckbox = document.getElementById('autostart-amm');
+ if (autostartCheckbox) {
+ autostartCheckbox.checked = !checked;
+ }
+ }
+ })
+ .catch(error => {
+ console.error('Error saving autostart setting:', error);
+ const autostartCheckbox = document.getElementById('autostart-amm');
+ if (autostartCheckbox) {
+ autostartCheckbox.checked = !checked;
+ }
+ });
+ },
+
+ setupAutostartCheckbox: function() {
+ const autostartCheckbox = document.getElementById('autostart-amm');
+ if (autostartCheckbox) {
+ autostartCheckbox.addEventListener('change', () => {
+ this.saveAutostartSetting(autostartCheckbox.checked);
+ });
+ }
+ },
+
+ showErrorModal: function(title, message) {
+ document.getElementById('errorTitle').textContent = title || 'Error';
+ document.getElementById('errorMessage').textContent = message || 'An error occurred';
+ const modal = document.getElementById('errorModal');
+ if (modal) {
+ modal.classList.remove('hidden');
+ }
+ },
+
+ hideErrorModal: function() {
+ const modal = document.getElementById('errorModal');
+ if (modal) {
+ modal.classList.add('hidden');
+ }
+ },
+
+ showConfirmModal: function(title, message, callback) {
+ document.getElementById('confirmTitle').textContent = title || 'Confirm Action';
+ document.getElementById('confirmMessage').textContent = message || 'Are you sure?';
+ const modal = document.getElementById('confirmModal');
+ if (modal) {
+ modal.classList.remove('hidden');
+ }
+
+ window.confirmCallback = callback;
+ },
+
+ hideConfirmModal: function() {
+ const modal = document.getElementById('confirmModal');
+ if (modal) {
+ modal.classList.add('hidden');
+ }
+ window.confirmCallback = null;
+ },
+
+ setupModals: function() {
+ const errorOkBtn = document.getElementById('errorOk');
+ if (errorOkBtn) {
+ errorOkBtn.addEventListener('click', this.hideErrorModal.bind(this));
+ }
+
+ const errorModal = document.getElementById('errorModal');
+ if (errorModal) {
+ errorModal.addEventListener('click', (e) => {
+ if (e.target === errorModal) {
+ this.hideErrorModal();
+ }
+ });
+ }
+
+ const confirmYesBtn = document.getElementById('confirmYes');
+ if (confirmYesBtn) {
+ confirmYesBtn.addEventListener('click', () => {
+ if (window.confirmCallback && typeof window.confirmCallback === 'function') {
+ window.confirmCallback();
+ }
+ this.hideConfirmModal();
+ });
+ }
+
+ const confirmNoBtn = document.getElementById('confirmNo');
+ if (confirmNoBtn) {
+ confirmNoBtn.addEventListener('click', this.hideConfirmModal.bind(this));
+ }
+
+ const confirmModal = document.getElementById('confirmModal');
+ if (confirmModal) {
+ confirmModal.addEventListener('click', (e) => {
+ if (e.target === confirmModal) {
+ this.hideConfirmModal();
+ }
+ });
+ }
+ },
+
+ setupStartupValidation: function() {
+ const controlForm = document.querySelector('form[method="post"]');
+ if (!controlForm) return;
+
+ const startButton = controlForm.querySelector('input[name="start"]');
+ if (!startButton) return;
+
+ startButton.addEventListener('click', (e) => {
+ e.preventDefault();
+ this.performStartupValidation();
+ });
+ },
+
+ performStartupValidation: function() {
+ const feedbackDiv = document.getElementById('startup-feedback');
+ const titleEl = document.getElementById('startup-title');
+ const messageEl = document.getElementById('startup-message');
+ const progressBar = document.getElementById('startup-progress-bar');
+
+ feedbackDiv.classList.remove('hidden');
+
+ const steps = [
+ { message: 'Checking configuration...', progress: 20 },
+ { message: 'Validating offers and bids...', progress: 40 },
+ { message: 'Checking wallet balances...', progress: 60 },
+ { message: 'Verifying API connection...', progress: 80 },
+ { message: 'Starting AMM process...', progress: 100 }
+ ];
+
+ let currentStep = 0;
+
+ const runNextStep = () => {
+ if (currentStep >= steps.length) {
+ this.submitStartForm();
+ return;
+ }
+
+ const step = steps[currentStep];
+ messageEl.textContent = step.message;
+ progressBar.style.width = step.progress + '%';
+
+ CleanupManager.setTimeout(() => {
+ this.validateStep(currentStep).then(result => {
+ if (result.success) {
+ currentStep++;
+ runNextStep();
+ } else {
+ this.showStartupError(result.error);
+ }
+ }).catch(error => {
+ this.showStartupError('Validation failed: ' + error.message);
+ });
+ }, 500);
+ };
+
+ runNextStep();
+ },
+
+ validateStep: async function(stepIndex) {
+ try {
+ switch (stepIndex) {
+ case 0:
+ return await this.validateConfiguration();
+ case 1:
+ return await this.validateOffersAndBids();
+ case 2:
+ return await this.validateWalletBalances();
+ case 3:
+ return await this.validateApiConnection();
+ case 4:
+ return { success: true };
+ default:
+ return { success: true };
+ }
+ } catch (error) {
+ return { success: false, error: error.message };
+ }
+ },
+
+ validateConfiguration: async function() {
+ const configData = window.ammTablesConfig?.configData;
+ if (!configData) {
+ return { success: false, error: 'No configuration found. Please save a configuration first.' };
+ }
+
+ if (!configData.min_seconds_between_offers || !configData.max_seconds_between_offers) {
+ return { success: false, error: 'Missing timing configuration. Please check your settings.' };
+ }
+
+ return { success: true };
+ },
+
+ validateOffersAndBids: async function() {
+ const configData = window.ammTablesConfig?.configData;
+ if (!configData) {
+ return { success: false, error: 'Configuration not available for validation.' };
+ }
+
+ const offers = configData.offers || [];
+ const bids = configData.bids || [];
+ const enabledOffers = offers.filter(o => o.enabled);
+ const enabledBids = bids.filter(b => b.enabled);
+
+ if (enabledOffers.length === 0 && enabledBids.length === 0) {
+ return { success: false, error: 'No enabled offers or bids found. Please enable at least one offer or bid before starting.' };
+ }
+
+ for (const offer of enabledOffers) {
+ if (!offer.amount_step) {
+ return { success: false, error: `Offer "${offer.name}" is missing required Amount Step (privacy feature).` };
+ }
+
+ const amountStep = parseFloat(offer.amount_step);
+ const amount = parseFloat(offer.amount);
+
+ if (amountStep <= 0 || amountStep < 0.001) {
+ return { success: false, error: `Offer "${offer.name}" has invalid Amount Step. Must be >= 0.001.` };
+ }
+
+ if (amountStep > amount) {
+ return { success: false, error: `Offer "${offer.name}" Amount Step (${amountStep}) cannot be greater than offer amount (${amount}).` };
+ }
+ }
+
+ return { success: true };
+ },
+
+ validateWalletBalances: async function() {
+ const configData = window.ammTablesConfig?.configData;
+ if (!configData) return { success: true };
+
+ const offers = configData.offers || [];
+ const enabledOffers = offers.filter(o => o.enabled);
+
+ for (const offer of enabledOffers) {
+ if (!offer.min_coin_from_amt || parseFloat(offer.min_coin_from_amt) <= 0) {
+ return { success: false, error: `Offer "${offer.name}" needs a minimum coin amount to protect your wallet balance.` };
+ }
+ }
+
+ return { success: true };
+ },
+
+ validateApiConnection: async function() {
+ return { success: true };
+ },
+
+ showStartupError: function(errorMessage) {
+ const feedbackDiv = document.getElementById('startup-feedback');
+ feedbackDiv.classList.add('hidden');
+
+ if (window.showErrorModal) {
+ window.showErrorModal('AMM Startup Failed', errorMessage);
+ } else {
+ alert('AMM Startup Failed: ' + errorMessage);
+ }
+ },
+
+ submitStartForm: function() {
+ const feedbackDiv = document.getElementById('startup-feedback');
+ const titleEl = document.getElementById('startup-title');
+ const messageEl = document.getElementById('startup-message');
+
+ titleEl.textContent = 'Starting AMM...';
+ messageEl.textContent = 'AMM process is starting. Please wait...';
+
+ const controlForm = document.querySelector('form[method="post"]');
+ if (controlForm) {
+ const formData = new FormData(controlForm);
+ formData.append('start', 'Start');
+
+ fetch(window.location.pathname, {
+ method: 'POST',
+ body: formData
+ }).then(response => {
+ if (response.ok) {
+ window.location.reload();
+ } else {
+ throw new Error('Failed to start AMM');
+ }
+ }).catch(error => {
+ this.showStartupError('Failed to start AMM: ' + error.message);
+ });
+ }
+ },
+
+ setupClearStateButton: function() {
+ const clearStateBtn = document.getElementById('clearStateBtn');
+ if (clearStateBtn) {
+ clearStateBtn.addEventListener('click', () => {
+ this.showConfirmModal(
+ 'Clear AMM State',
+ 'This will clear the AMM state file. All running offers/bids will be lost. Are you sure?',
+ () => {
+ const form = clearStateBtn.closest('form');
+ if (form) {
+ const hiddenInput = document.createElement('input');
+ hiddenInput.type = 'hidden';
+ hiddenInput.name = 'prune_state';
+ hiddenInput.value = 'true';
+ form.appendChild(hiddenInput);
+ form.submit();
+ }
+ }
+ );
+ });
+ }
+ },
+
+ setAmmAmount: function(percent, fieldId) {
+ const amountInput = document.getElementById(fieldId);
+ let coinSelect;
+
+ let modalType = null;
+ if (fieldId.includes('add-amm')) {
+ const addModal = document.getElementById('add-amm-modal');
+ modalType = addModal ? addModal.getAttribute('data-amm-type') : null;
+ } else if (fieldId.includes('edit-amm')) {
+ const editModal = document.getElementById('edit-amm-modal');
+ modalType = editModal ? editModal.getAttribute('data-amm-type') : null;
+ }
+
+ if (fieldId.includes('add-amm')) {
+ const isBidModal = modalType === 'bid';
+ coinSelect = document.getElementById(isBidModal ? 'add-amm-coin-to' : 'add-amm-coin-from');
+ } else if (fieldId.includes('edit-amm')) {
+ const isBidModal = modalType === 'bid';
+ coinSelect = document.getElementById(isBidModal ? 'edit-amm-coin-to' : 'edit-amm-coin-from');
+ }
+
+ if (!amountInput || !coinSelect) {
+ console.error('Required elements not found');
+ return;
+ }
+
+ const selectedOption = coinSelect.options[coinSelect.selectedIndex];
+ if (!selectedOption) {
+ if (window.showErrorModal) {
+ window.showErrorModal('Validation Error', 'Please select a coin first');
+ } else {
+ alert('Please select a coin first');
+ }
+ return;
+ }
+
+ const balance = selectedOption.getAttribute('data-balance');
+ if (!balance) {
+ console.error('Balance not found for selected coin');
+ return;
+ }
+
+ const floatBalance = parseFloat(balance);
+ if (isNaN(floatBalance) || floatBalance <= 0) {
+ if (window.showErrorModal) {
+ window.showErrorModal('Invalid Balance', 'The selected coin has no available balance. Please select a coin with a positive balance.');
+ } else {
+ alert('Invalid balance for selected coin');
+ }
+ return;
+ }
+
+ const calculatedAmount = floatBalance * percent;
+ amountInput.value = calculatedAmount.toFixed(8);
+
+ const event = new Event('input', { bubbles: true });
+ amountInput.dispatchEvent(event);
+ },
+
+ updateAmmModalBalances: function(balanceData) {
+ const addModal = document.getElementById('add-amm-modal');
+ const editModal = document.getElementById('edit-amm-modal');
+ const addModalVisible = addModal && !addModal.classList.contains('hidden');
+ const editModalVisible = editModal && !editModal.classList.contains('hidden');
+
+ let modalType = null;
+ if (addModalVisible) {
+ modalType = addModal.getAttribute('data-amm-type');
+ } else if (editModalVisible) {
+ modalType = editModal.getAttribute('data-amm-type');
+ }
+
+ if (modalType === 'offer') {
+ this.updateOfferDropdownBalances(balanceData);
+ } else if (modalType === 'bid') {
+ this.updateBidDropdownBalances(balanceData);
+ }
+ },
+
+ setupWebSocketBalanceUpdates: function() {
+ window.BalanceUpdatesManager.setup({
+ contextKey: 'amm',
+ balanceUpdateCallback: this.updateAmmModalBalances.bind(this),
+ swapEventCallback: this.updateAmmModalBalances.bind(this),
+ errorContext: 'AMM',
+ enablePeriodicRefresh: true,
+ periodicInterval: 120000
+ });
+ },
+
+ updateAmmDropdownBalances: function(balanceData) {
+ const balanceMap = {};
+ const pendingMap = {};
+ balanceData.forEach(coin => {
+ balanceMap[coin.name] = coin.balance;
+ pendingMap[coin.name] = coin.pending || '0.0';
+ });
+
+ const dropdownIds = ['add-amm-coin-from', 'edit-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-to'];
+
+ dropdownIds.forEach(dropdownId => {
+ const select = document.getElementById(dropdownId);
+ if (!select) {
+ return;
+ }
+
+ Array.from(select.options).forEach(option => {
+ const coinName = option.value;
+ const balance = balanceMap[coinName] || '0.0';
+ const pending = pendingMap[coinName] || '0.0';
+
+ option.setAttribute('data-balance', balance);
+ option.setAttribute('data-pending-balance', pending);
+ });
+ });
+
+ const addModal = document.getElementById('add-amm-modal');
+ const editModal = document.getElementById('edit-amm-modal');
+ const addModalVisible = addModal && !addModal.classList.contains('hidden');
+ const editModalVisible = editModal && !editModal.classList.contains('hidden');
+
+ let currentModalType = null;
+ if (addModalVisible) {
+ currentModalType = addModal.getAttribute('data-amm-type');
+ } else if (editModalVisible) {
+ currentModalType = editModal.getAttribute('data-amm-type');
+ }
+
+ if (currentModalType && window.ammTablesManager) {
+ if (currentModalType === 'offer' && typeof window.ammTablesManager.refreshOfferDropdownBalanceDisplay === 'function') {
+ window.ammTablesManager.refreshOfferDropdownBalanceDisplay();
+ } else if (currentModalType === 'bid' && typeof window.ammTablesManager.refreshBidDropdownBalanceDisplay === 'function') {
+ window.ammTablesManager.refreshBidDropdownBalanceDisplay();
+ }
+ }
+ },
+
+ updateOfferDropdownBalances: function(balanceData) {
+ this.updateAmmDropdownBalances(balanceData);
+ },
+
+ updateBidDropdownBalances: function(balanceData) {
+ this.updateAmmDropdownBalances(balanceData);
+ },
+
+ cleanupAmmBalanceUpdates: function() {
+ window.BalanceUpdatesManager.cleanup('amm');
+
+ if (window.ammDropdowns) {
+ window.ammDropdowns.forEach(dropdown => {
+ if (dropdown.parentNode) {
+ dropdown.parentNode.removeChild(dropdown);
+ }
+ });
+ window.ammDropdowns = [];
+ }
+ },
+
+ setupCleanup: function() {
+ if (window.CleanupManager) {
+ window.CleanupManager.registerResource('ammBalanceUpdates', null, this.cleanupAmmBalanceUpdates.bind(this));
+ }
+
+ const beforeUnloadHandler = this.cleanupAmmBalanceUpdates.bind(this);
+ window.addEventListener('beforeunload', beforeUnloadHandler);
+
+ if (window.CleanupManager) {
+ CleanupManager.registerResource('ammBeforeUnload', beforeUnloadHandler, () => {
+ window.removeEventListener('beforeunload', beforeUnloadHandler);
+ });
+ }
+ },
+
+ cleanup: function() {
+ const debugCheckbox = document.getElementById('amm_debug');
+ const autostartCheckbox = document.getElementById('amm_autostart');
+ const errorOkBtn = document.getElementById('errorOk');
+ const confirmYesBtn = document.getElementById('confirmYes');
+ const confirmNoBtn = document.getElementById('confirmNo');
+ const startButton = document.getElementById('startAMM');
+ const clearStateBtn = document.getElementById('clearAmmState');
+
+ this.cleanupAmmBalanceUpdates();
+ }
+ };
+
+ document.addEventListener('DOMContentLoaded', function() {
+ AMMPage.init();
+
+ if (window.BalanceUpdatesManager) {
+ window.BalanceUpdatesManager.initialize();
+ }
+ });
+
+ window.AMMPage = AMMPage;
+ window.showErrorModal = AMMPage.showErrorModal.bind(AMMPage);
+ window.hideErrorModal = AMMPage.hideErrorModal.bind(AMMPage);
+ window.showConfirmModal = AMMPage.showConfirmModal.bind(AMMPage);
+ window.hideConfirmModal = AMMPage.hideConfirmModal.bind(AMMPage);
+ window.setAmmAmount = AMMPage.setAmmAmount.bind(AMMPage);
+
+})();
diff --git a/basicswap/static/js/amm_tables.js b/basicswap/static/js/pages/amm-tables.js
similarity index 98%
rename from basicswap/static/js/amm_tables.js
rename to basicswap/static/js/pages/amm-tables.js
index ad8f074..8d4d17e 100644
--- a/basicswap/static/js/amm_tables.js
+++ b/basicswap/static/js/pages/amm-tables.js
@@ -61,53 +61,8 @@ const AmmTablesManager = (function() {
}
function getCoinDisplayName(coinId) {
- if (config.debug) {
- console.log('[AMM Tables] getCoinDisplayName called with:', coinId, typeof coinId);
- }
-
- if (typeof coinId === 'string') {
- const lowerCoinId = coinId.toLowerCase();
-
- if (lowerCoinId === 'part_anon' ||
- lowerCoinId === 'particl_anon' ||
- lowerCoinId === 'particl anon') {
- if (config.debug) {
- console.log('[AMM Tables] Matched Particl Anon variant:', coinId);
- }
- return 'Particl Anon';
- }
-
- if (lowerCoinId === 'part_blind' ||
- lowerCoinId === 'particl_blind' ||
- lowerCoinId === 'particl blind') {
- if (config.debug) {
- console.log('[AMM Tables] Matched Particl Blind variant:', coinId);
- }
- return 'Particl Blind';
- }
-
- if (lowerCoinId === 'ltc_mweb' ||
- lowerCoinId === 'litecoin_mweb' ||
- lowerCoinId === 'litecoin mweb') {
- if (config.debug) {
- console.log('[AMM Tables] Matched Litecoin MWEB variant:', coinId);
- }
- return 'Litecoin MWEB';
- }
- }
-
if (window.CoinManager && window.CoinManager.getDisplayName) {
- const displayName = window.CoinManager.getDisplayName(coinId);
- if (displayName) {
- if (config.debug) {
- console.log('[AMM Tables] CoinManager returned:', displayName);
- }
- return displayName;
- }
- }
-
- if (config.debug) {
- console.log('[AMM Tables] Returning coin name as-is:', coinId);
+ return window.CoinManager.getDisplayName(coinId) || coinId;
}
return coinId;
}
@@ -303,7 +258,6 @@ const AmmTablesManager = (function() {
`;
});
-
if (offersBody.innerHTML.trim() !== tableHtml.trim()) {
offersBody.innerHTML = tableHtml;
}
@@ -438,7 +392,6 @@ const AmmTablesManager = (function() {
`;
});
-
if (bidsBody.innerHTML.trim() !== tableHtml.trim()) {
bidsBody.innerHTML = tableHtml;
}
@@ -540,7 +493,6 @@ const AmmTablesManager = (function() {
coinPrice = window.latestPrices[coinName.toUpperCase()];
}
-
if (!coinPrice || isNaN(coinPrice)) {
return null;
}
@@ -550,6 +502,9 @@ const AmmTablesManager = (function() {
function formatUSDPrice(usdValue) {
if (!usdValue || isNaN(usdValue)) return '';
+ if (window.config && window.config.utils && window.config.utils.formatPrice) {
+ return `($${window.config.utils.formatPrice('USD', usdValue)} USD)`;
+ }
return `($${usdValue.toFixed(2)} USD)`;
}
@@ -728,7 +683,6 @@ const AmmTablesManager = (function() {
const isMakerDropdown = select.id.includes('coin-from');
const isTakerDropdown = select.id.includes('coin-to');
-
const addModal = document.getElementById('add-amm-modal');
const editModal = document.getElementById('edit-amm-modal');
const addModalVisible = addModal && !addModal.classList.contains('hidden');
@@ -755,7 +709,6 @@ const AmmTablesManager = (function() {
}
}
-
const result = isBidModal ? isTakerDropdown : isMakerDropdown;
console.log(`[DEBUG] shouldDropdownOptionsShowBalance: ${select.id}, isBidModal=${isBidModal}, isMaker=${isMakerDropdown}, isTaker=${isTakerDropdown}, result=${result}`);
@@ -773,22 +726,18 @@ const AmmTablesManager = (function() {
const wrapper = select.parentNode.querySelector('.relative');
if (!wrapper) return;
-
const dropdown = wrapper.querySelector('[role="listbox"]');
if (!dropdown) return;
-
const options = dropdown.querySelectorAll('[data-value]');
options.forEach(optionElement => {
const coinValue = optionElement.getAttribute('data-value');
const originalOption = Array.from(select.options).find(opt => opt.value === coinValue);
if (!originalOption) return;
-
const textContainer = optionElement.querySelector('div.flex.flex-col, div.flex.items-center');
if (!textContainer) return;
-
textContainer.innerHTML = '';
const shouldShowBalance = shouldDropdownOptionsShowBalance(select);
@@ -828,7 +777,6 @@ const AmmTablesManager = (function() {
});
}
-
function refreshDropdownBalances() {
const dropdownIds = ['add-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-from', 'edit-amm-coin-to'];
@@ -839,7 +787,6 @@ const AmmTablesManager = (function() {
const wrapper = select.parentNode.querySelector('.relative');
if (!wrapper) return;
-
const dropdownItems = wrapper.querySelectorAll('[data-value]');
dropdownItems.forEach(item => {
const value = item.getAttribute('data-value');
@@ -852,7 +799,6 @@ const AmmTablesManager = (function() {
if (balanceDiv) {
balanceDiv.textContent = `Balance: ${balance}`;
-
let pendingDiv = item.querySelector('.text-green-500');
if (pendingBalance && parseFloat(pendingBalance) > 0) {
if (!pendingDiv) {
@@ -880,7 +826,6 @@ const AmmTablesManager = (function() {
balanceDiv.textContent = `Balance: ${balance}`;
-
let pendingDiv = textContainer.querySelector('.text-green-500');
if (pendingBalance && parseFloat(pendingBalance) > 0) {
if (!pendingDiv) {
@@ -940,10 +885,8 @@ const AmmTablesManager = (function() {
if (!coinFromSelect || !coinToSelect) return;
-
const balanceData = {};
-
Array.from(coinFromSelect.options).forEach(option => {
const balance = option.getAttribute('data-balance');
if (balance) {
@@ -951,7 +894,6 @@ const AmmTablesManager = (function() {
}
});
-
Array.from(coinToSelect.options).forEach(option => {
const balance = option.getAttribute('data-balance');
if (balance) {
@@ -959,7 +901,6 @@ const AmmTablesManager = (function() {
}
});
-
updateDropdownOptions(coinFromSelect, balanceData);
updateDropdownOptions(coinToSelect, balanceData);
}
@@ -970,11 +911,9 @@ const AmmTablesManager = (function() {
const balance = balanceData[coinName] || '0.00000000';
const pending = pendingData[coinName] || '0.0';
-
option.setAttribute('data-balance', balance);
option.setAttribute('data-pending-balance', pending);
-
option.textContent = coinName;
});
}
@@ -982,7 +921,6 @@ const AmmTablesManager = (function() {
function createSimpleDropdown(select, showBalance = false) {
if (!select) return;
-
const existingWrapper = select.parentNode.querySelector('.relative');
if (existingWrapper) {
existingWrapper.remove();
@@ -994,13 +932,11 @@ const AmmTablesManager = (function() {
const wrapper = document.createElement('div');
wrapper.className = 'relative';
-
const button = document.createElement('button');
button.type = 'button';
button.className = 'flex items-center justify-between w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:text-white';
button.style.minHeight = '60px';
-
const displayContent = document.createElement('div');
displayContent.className = 'flex items-center';
@@ -1019,11 +955,9 @@ const AmmTablesManager = (function() {
button.appendChild(displayContent);
button.appendChild(arrow);
-
const dropdown = document.createElement('div');
dropdown.className = 'absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden dark:bg-gray-700 dark:border-gray-600 max-h-60 overflow-y-auto';
-
Array.from(select.options).forEach(option => {
const item = document.createElement('div');
item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer';
@@ -1047,7 +981,6 @@ const AmmTablesManager = (function() {
Transaction Status
@@ -1256,6 +1257,12 @@ const createTableRow = async (bid) => {' + JSON.stringify(obj, null, ' ') + '';
+ const ratesDisplay = document.getElementById('rates_display');
+ if (ratesDisplay) {
+ ratesDisplay.innerHTML = inner_html;
+ }
+ }
+ };
+
+ this.xhr_bid_params.onload = () => {
+ if (this.xhr_bid_params.status == 200) {
+ const obj = JSON.parse(this.xhr_bid_params.response);
+ const bidAmountSendInput = document.getElementById('bid_amount_send');
+ if (bidAmountSendInput) {
+ bidAmountSendInput.value = obj['amount_to'];
+ }
+ this.updateModalValues();
+ }
+ };
+ },
+
+ setupEventListeners: function() {
+ const sendBidBtn = document.querySelector('button[name="sendbid"][value="Send Bid"]');
+ if (sendBidBtn) {
+ sendBidBtn.onclick = this.showConfirmModal.bind(this);
+ }
+
+ const modalCancelBtn = document.querySelector('#confirmModal .flex button:last-child');
+ if (modalCancelBtn) {
+ modalCancelBtn.onclick = this.hideConfirmModal.bind(this);
+ }
+
+ const mainCancelBtn = document.querySelector('button[name="cancel"]');
+ if (mainCancelBtn) {
+ mainCancelBtn.onclick = this.handleCancelClick.bind(this);
+ }
+
+ const validMinsInput = document.querySelector('input[name="validmins"]');
+ if (validMinsInput) {
+ validMinsInput.addEventListener('input', this.updateModalValues.bind(this));
+ }
+
+ const addrFromSelect = document.querySelector('select[name="addr_from"]');
+ if (addrFromSelect) {
+ addrFromSelect.addEventListener('change', this.updateModalValues.bind(this));
+ }
+
+ const errorOkBtn = document.getElementById('errorOk');
+ if (errorOkBtn) {
+ errorOkBtn.addEventListener('click', this.hideErrorModal.bind(this));
+ }
+ },
+
+ lookup_rates: function() {
+ const coin_from = document.getElementById('coin_from')?.value;
+ const coin_to = document.getElementById('coin_to')?.value;
+
+ if (!coin_from || !coin_to || coin_from === '-1' || coin_to === '-1') {
+ alert('Coins from and to must be set first.');
+ return;
+ }
+
+ const ratesDisplay = document.getElementById('rates_display');
+ if (ratesDisplay) {
+ ratesDisplay.innerHTML = 'Updating...
'; + } + + this.xhr_rates.open('POST', '/json/rates'); + this.xhr_rates.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + this.xhr_rates.send(`coin_from=${coin_from}&coin_to=${coin_to}`); + }, + + resetForm: function() { + const bidAmountSendInput = document.getElementById('bid_amount_send'); + const bidAmountInput = document.getElementById('bid_amount'); + const bidRateInput = document.getElementById('bid_rate'); + const validMinsInput = document.querySelector('input[name="validmins"]'); + const amtVar = document.getElementById('amt_var')?.value === 'True'; + + if (bidAmountSendInput) { + bidAmountSendInput.value = amtVar ? '' : bidAmountSendInput.getAttribute('max'); + } + if (bidAmountInput) { + bidAmountInput.value = amtVar ? '' : bidAmountInput.getAttribute('max'); + } + if (bidRateInput && !bidRateInput.disabled) { + const defaultRate = document.getElementById('offer_rate')?.value || ''; + bidRateInput.value = defaultRate; + } + if (validMinsInput) { + validMinsInput.value = "60"; + } + if (!amtVar) { + this.updateBidParams('rate'); + } + this.updateModalValues(); + + const errorMessages = document.querySelectorAll('.error-message'); + errorMessages.forEach(msg => msg.remove()); + + const inputs = document.querySelectorAll('input'); + inputs.forEach(input => { + input.classList.remove('border-red-500', 'focus:border-red-500'); + }); + }, + + roundUpToDecimals: function(value, decimals) { + const factor = Math.pow(10, decimals); + return Math.ceil(value * factor) / factor; + }, + + updateBidParams: function(value_changed) { + const coin_from = document.getElementById('coin_from')?.value; + const coin_to = document.getElementById('coin_to')?.value; + const coin_from_exp = parseInt(document.getElementById('coin_from_exp')?.value || '8'); + const coin_to_exp = parseInt(document.getElementById('coin_to_exp')?.value || '8'); + const amt_var = document.getElementById('amt_var')?.value; + const rate_var = document.getElementById('rate_var')?.value; + const bidAmountInput = document.getElementById('bid_amount'); + const bidAmountSendInput = document.getElementById('bid_amount_send'); + const bidRateInput = document.getElementById('bid_rate'); + const offerRateInput = document.getElementById('offer_rate'); + + if (!coin_from || !coin_to || !amt_var || !rate_var) return; + + const rate = rate_var === 'True' && bidRateInput ? + parseFloat(bidRateInput.value) || 0 : + parseFloat(offerRateInput?.value || '0'); + + if (!rate) return; + + if (value_changed === 'rate') { + if (bidAmountSendInput && bidAmountInput) { + const sendAmount = parseFloat(bidAmountSendInput.value) || 0; + const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp); + bidAmountInput.value = receiveAmount; + } + } else if (value_changed === 'sending') { + if (bidAmountSendInput && bidAmountInput) { + const sendAmount = parseFloat(bidAmountSendInput.value) || 0; + const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp); + bidAmountInput.value = receiveAmount; + } + } else if (value_changed === 'receiving') { + if (bidAmountInput && bidAmountSendInput) { + const receiveAmount = parseFloat(bidAmountInput.value) || 0; + const sendAmount = this.roundUpToDecimals(receiveAmount * rate, coin_to_exp).toFixed(coin_to_exp); + bidAmountSendInput.value = sendAmount; + } + } + + this.validateAmountsAfterChange(); + + this.xhr_bid_params.open('POST', '/json/rate'); + this.xhr_bid_params.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + this.xhr_bid_params.send(`coin_from=${coin_from}&coin_to=${coin_to}&rate=${rate}&amt_from=${bidAmountInput?.value || '0'}`); + + this.updateModalValues(); + }, + + validateAmountsAfterChange: function() { + const bidAmountSendInput = document.getElementById('bid_amount_send'); + const bidAmountInput = document.getElementById('bid_amount'); + + if (bidAmountSendInput) { + const maxSend = parseFloat(bidAmountSendInput.getAttribute('max')); + this.validateMaxAmount(bidAmountSendInput, maxSend); + } + if (bidAmountInput) { + const maxReceive = parseFloat(bidAmountInput.getAttribute('max')); + this.validateMaxAmount(bidAmountInput, maxReceive); + } + }, + + validateMaxAmount: function(input, maxAmount) { + if (!input) return; + const value = parseFloat(input.value) || 0; + if (value > maxAmount) { + input.value = maxAmount; + } + }, + + showErrorModal: function(title, message) { + document.getElementById('errorTitle').textContent = title || 'Error'; + document.getElementById('errorMessage').textContent = message || 'An error occurred'; + const modal = document.getElementById('errorModal'); + if (modal) { + modal.classList.remove('hidden'); + } + }, + + hideErrorModal: function() { + const modal = document.getElementById('errorModal'); + if (modal) { + modal.classList.add('hidden'); + } + }, + + showConfirmModal: function() { + const bidAmountSendInput = document.getElementById('bid_amount_send'); + const bidAmountInput = document.getElementById('bid_amount'); + const validMinsInput = document.querySelector('input[name="validmins"]'); + const addrFromSelect = document.querySelector('select[name="addr_from"]'); + + let sendAmount = 0; + let receiveAmount = 0; + + if (bidAmountSendInput && bidAmountSendInput.value) { + sendAmount = parseFloat(bidAmountSendInput.value) || 0; + } + + if (bidAmountInput && bidAmountInput.value) { + receiveAmount = parseFloat(bidAmountInput.value) || 0; + } + + if (sendAmount <= 0 || receiveAmount <= 0) { + this.showErrorModal('Validation Error', 'Please enter valid amounts for both sending and receiving.'); + return false; + } + + const coinFrom = document.getElementById('coin_from_name')?.value || ''; + const coinTo = document.getElementById('coin_to_name')?.value || ''; + const tlaFrom = document.getElementById('tla_from')?.value || ''; + const tlaTo = document.getElementById('tla_to')?.value || ''; + + const validMins = validMinsInput ? validMinsInput.value : '60'; + + const addrFrom = addrFromSelect ? addrFromSelect.value : ''; + + const modalAmtReceive = document.getElementById('modal-amt-receive'); + const modalReceiveCurrency = document.getElementById('modal-receive-currency'); + const modalAmtSend = document.getElementById('modal-amt-send'); + const modalSendCurrency = document.getElementById('modal-send-currency'); + const modalAddrFrom = document.getElementById('modal-addr-from'); + const modalValidMins = document.getElementById('modal-valid-mins'); + + if (modalAmtReceive) modalAmtReceive.textContent = receiveAmount.toFixed(8); + if (modalReceiveCurrency) modalReceiveCurrency.textContent = ` ${tlaFrom}`; + if (modalAmtSend) modalAmtSend.textContent = sendAmount.toFixed(8); + if (modalSendCurrency) modalSendCurrency.textContent = ` ${tlaTo}`; + if (modalAddrFrom) modalAddrFrom.textContent = addrFrom || 'Default'; + if (modalValidMins) modalValidMins.textContent = validMins; + + const modal = document.getElementById('confirmModal'); + if (modal) { + modal.classList.remove('hidden'); + } + return false; + }, + + hideConfirmModal: function() { + const modal = document.getElementById('confirmModal'); + if (modal) { + modal.classList.add('hidden'); + } + return false; + }, + + updateModalValues: function() { + + }, + + handleBidsPageAddress: function() { + const selectElement = document.querySelector('select[name="addr_from"]'); + const STORAGE_KEY = 'lastUsedAddressBids'; + + if (!selectElement) return; + + const loadInitialAddress = () => { + const savedAddressJSON = localStorage.getItem(STORAGE_KEY); + if (savedAddressJSON) { + try { + const savedAddress = JSON.parse(savedAddressJSON); + selectElement.value = savedAddress.value; + } catch (e) { + selectFirstAddress(); + } + } else { + selectFirstAddress(); + } + }; + + const selectFirstAddress = () => { + if (selectElement.options.length > 1) { + const firstOption = selectElement.options[1]; + if (firstOption) { + selectElement.value = firstOption.value; + this.saveAddress(firstOption.value, firstOption.text); + } + } + }; + + selectElement.addEventListener('change', (event) => { + this.saveAddress(event.target.value, event.target.selectedOptions[0].text); + }); + + loadInitialAddress(); + }, + + saveAddress: function(value, text) { + const addressData = { + value: value, + text: text + }; + localStorage.setItem('lastUsedAddressBids', JSON.stringify(addressData)); + }, + + confirmPopup: function() { + return confirm("Are you sure?"); + }, + + handleCancelClick: function(event) { + if (event) event.preventDefault(); + const pathParts = window.location.pathname.split('/'); + const offerId = pathParts[pathParts.indexOf('offer') + 1]; + window.location.href = `/offer/${offerId}`; + }, + + cleanup: function() { + } + }; + + document.addEventListener('DOMContentLoaded', function() { + OfferPage.init(); + + if (window.CleanupManager) { + CleanupManager.registerResource('offerPage', OfferPage, (page) => { + if (page.cleanup) page.cleanup(); + }); + } + }); + + window.OfferPage = OfferPage; + window.lookup_rates = OfferPage.lookup_rates.bind(OfferPage); + window.resetForm = OfferPage.resetForm.bind(OfferPage); + window.updateBidParams = OfferPage.updateBidParams.bind(OfferPage); + window.validateMaxAmount = OfferPage.validateMaxAmount.bind(OfferPage); + window.showConfirmModal = OfferPage.showConfirmModal.bind(OfferPage); + window.hideConfirmModal = OfferPage.hideConfirmModal.bind(OfferPage); + window.showErrorModal = OfferPage.showErrorModal.bind(OfferPage); + window.hideErrorModal = OfferPage.hideErrorModal.bind(OfferPage); + window.confirmPopup = OfferPage.confirmPopup.bind(OfferPage); + window.handleBidsPageAddress = OfferPage.handleBidsPageAddress.bind(OfferPage); + +})(); diff --git a/basicswap/static/js/offers.js b/basicswap/static/js/pages/offers-page.js similarity index 89% rename from basicswap/static/js/offers.js rename to basicswap/static/js/pages/offers-page.js index ffedd66..6d86d31 100644 --- a/basicswap/static/js/offers.js +++ b/basicswap/static/js/pages/offers-page.js @@ -5,8 +5,8 @@ let jsonData = []; let originalJsonData = []; let currentSortColumn = 0; let currentSortDirection = 'desc'; -let filterTimeout = null; let isPaginationInProgress = false; +let autoRefreshInterval = null; const isSentOffers = window.offersTableConfig.isSentOffers; const CACHE_DURATION = window.config.cacheConfig.defaultTTL; @@ -28,6 +28,9 @@ window.tableRateModule = { processedOffers: new Set(), getCachedValue(key) { + if (window.CacheManager) { + return window.CacheManager.get(key); + } const cachedItem = localStorage.getItem(key); if (cachedItem) { const parsedItem = JSON.parse(cachedItem); @@ -41,6 +44,14 @@ window.tableRateModule = { }, setCachedValue(key, value, resourceType = null) { + if (window.CacheManager) { + const ttl = resourceType ? + window.config.cacheConfig.ttlSettings[resourceType] || + window.config.cacheConfig.defaultTTL : + 900000; + window.CacheManager.set(key, value, ttl); + return; + } const ttl = resourceType ? window.config.cacheConfig.ttlSettings[resourceType] || window.config.cacheConfig.defaultTTL : @@ -65,26 +76,6 @@ window.tableRateModule = { return true; }, - formatUSD(value) { - if (Math.abs(value) < 0.000001) { - return value.toExponential(8) + ' USD'; - } else if (Math.abs(value) < 0.01) { - return value.toFixed(8) + ' USD'; - } else { - return value.toFixed(2) + ' USD'; - } - }, - - formatNumber(value, decimals) { - if (Math.abs(value) < 0.000001) { - return value.toExponential(decimals); - } else if (Math.abs(value) < 0.01) { - return value.toFixed(decimals); - } else { - return value.toFixed(Math.min(2, decimals)); - } - }, - getFallbackValue(coinSymbol) { if (!coinSymbol) return null; const normalizedSymbol = coinSymbol.toLowerCase() === 'part' ? 'particl' : coinSymbol.toLowerCase(); @@ -151,6 +142,41 @@ function initializeTooltips() { } } +function initializeTooltipsInBatches() { + if (!window.TooltipManager) return; + + const tooltipElements = document.querySelectorAll('[data-tooltip-target]'); + const BATCH_SIZE = 5; + let currentIndex = 0; + + function processBatch() { + const endIndex = Math.min(currentIndex + BATCH_SIZE, tooltipElements.length); + + for (let i = currentIndex; i < endIndex; i++) { + const element = tooltipElements[i]; + const targetId = element.getAttribute('data-tooltip-target'); + if (!targetId) continue; + + const tooltipContent = document.getElementById(targetId); + if (tooltipContent) { + window.TooltipManager.create(element, tooltipContent.innerHTML, { + placement: element.getAttribute('data-tooltip-placement') || 'top' + }); + } + } + + currentIndex = endIndex; + + if (currentIndex < tooltipElements.length) { + CleanupManager.setTimeout(processBatch, 0); + } + } + + if (tooltipElements.length > 0) { + CleanupManager.setTimeout(processBatch, 0); + } +} + function getValidOffers() { if (!jsonData) { return []; @@ -180,7 +206,6 @@ function saveFilterSettings() { })); } - function getSelectedCoins(filterType) { const dropdown = document.getElementById(`${filterType}_dropdown`); @@ -188,7 +213,6 @@ function getSelectedCoins(filterType) { return ['any']; } - const allCheckboxes = dropdown.querySelectorAll('input[type="checkbox"]'); const selected = []; @@ -252,7 +276,6 @@ function updateFilterButtonText(filterType) { textSpan.textContent = `Filter ${filterLabel} (${selected.length} selected)`; } - button.style.width = '210px'; } @@ -270,7 +293,6 @@ function updateCoinBadges(filterType) { const coinName = getCoinNameFromValue(coinValue, filterType); const badge = document.createElement('span'); - const isBidsFilter = filterType === 'coin_to' && !isSentOffers; const isOffersFilter = filterType === 'coin_from' && !isSentOffers; const isReceivingFilter = filterType === 'coin_to' && isSentOffers; @@ -285,7 +307,6 @@ function updateCoinBadges(filterType) { badge.className = badgeClass + ' cursor-pointer hover:opacity-80'; - const coinImage = getCoinImage(coinName); badge.innerHTML = ` @@ -350,13 +371,7 @@ function coinMatches(offerCoin, filterCoins) { return true; } - if ((normalizedOfferCoin === 'firo' || normalizedOfferCoin === 'zcoin') && - (normalizedFilterCoin === 'firo' || normalizedFilterCoin === 'zcoin')) { - return true; - } - - if ((normalizedOfferCoin === 'bitcoincash' && normalizedFilterCoin === 'bitcoin cash') || - (normalizedOfferCoin === 'bitcoin cash' && normalizedFilterCoin === 'bitcoincash')) { + if (window.CoinUtils && window.CoinUtils.isSameCoin(normalizedOfferCoin, normalizedFilterCoin)) { return true; } @@ -467,7 +482,6 @@ function removeCoinFilter(filterType, coinValue) { if (checkbox) { checkbox.checked = false; - updateFilterButtonText(filterType); updateCoinBadges(filterType); applyFilters(); @@ -475,7 +489,6 @@ function removeCoinFilter(filterType, coinValue) { } } - window.removeCoinFilter = removeCoinFilter; function filterAndSortData() { @@ -510,7 +523,6 @@ function filterAndSortData() { return false; } - if (selectedCoinTo.length > 0 && !(selectedCoinTo.length === 1 && selectedCoinTo[0] === 'any')) { const coinNames = selectedCoinTo.map(value => getCoinNameFromValue(value, 'coin_to')); const matches = coinMatches(offer.coin_to, coinNames); @@ -519,7 +531,6 @@ function filterAndSortData() { } } - if (selectedCoinFrom.length > 0 && !(selectedCoinFrom.length === 1 && selectedCoinFrom[0] === 'any')) { const coinNames = selectedCoinFrom.map(value => getCoinNameFromValue(value, 'coin_from')); const matches = coinMatches(offer.coin_from, coinNames); @@ -674,10 +685,9 @@ async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwn if (window.CoinManager) { normalizedCoin = window.CoinManager.getPriceKey(coin) || normalizedCoin; } else { - if (normalizedCoin === 'zcoin') normalizedCoin = 'firo'; - if (normalizedCoin === 'bitcoincash' || normalizedCoin === 'bitcoin cash') - normalizedCoin = 'bitcoin-cash'; - if (normalizedCoin.includes('particl')) normalizedCoin = 'particl'; + if (window.CoinUtils) { + normalizedCoin = window.CoinUtils.normalizeCoinName(normalizedCoin, latestPrices); + } } let price = null; if (latestPrices && latestPrices[normalizedCoin]) { @@ -734,11 +744,38 @@ async function fetchLatestPrices() { } } +async function fetchPricesAsync() { + try { + const prices = await window.PriceManager.getPrices(false); + return prices; + } catch (error) { + console.error('Error fetching prices asynchronously:', error); + return null; + } +} + async function fetchOffers() { const refreshButton = document.getElementById('refreshOffers'); const refreshIcon = document.getElementById('refreshIcon'); const refreshText = document.getElementById('refreshText'); + const fetchWithRetry = async (url, maxRetries = 3) => { + for (let i = 0; i < maxRetries; i++) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response; + } catch (error) { + if (i === maxRetries - 1) throw error; + console.log(`Fetch retry ${i + 1}/${maxRetries} for ${url}`); + + await new Promise(resolve => CleanupManager.setTimeout(resolve, 100 * Math.pow(2, i))); + } + } + }; + try { if (!NetworkManager.isOnline()) { throw new Error('Network is offline'); @@ -751,15 +788,7 @@ async function fetchOffers() { refreshButton.classList.add('opacity-75', 'cursor-wait'); } - const [offersResponse, pricesData] = await Promise.all([ - fetch(isSentOffers ? '/json/sentoffers' : '/json/offers'), - fetchLatestPrices() - ]); - - if (!offersResponse.ok) { - throw new Error(`HTTP error! status: ${offersResponse.status}`); - } - + const offersResponse = await fetchWithRetry(isSentOffers ? '/json/sentoffers' : '/json/offers'); const data = await offersResponse.json(); if (data.error) { @@ -789,13 +818,20 @@ async function fetchOffers() { jsonData = formatInitialData(processedData); originalJsonData = [...jsonData]; - latestPrices = pricesData || getEmptyPriceData(); - CacheManager.set('offers_cached', jsonData, 'offers'); applyFilters(); updatePaginationInfo(); + fetchPricesAsync().then(prices => { + if (prices) { + latestPrices = prices; + updateProfitLossDisplays(); + } + }).catch(error => { + console.error('Error fetching prices after offers refresh:', error); + }); + } catch (error) { console.error('[Debug] Error fetching offers:', error); NetworkManager.handleNetworkError(error); @@ -871,27 +907,37 @@ function updateConnectionStatus(status) { } function updateRowTimes() { - requestAnimationFrame(() => { - const rows = document.querySelectorAll('[data-offer-id]'); - rows.forEach(row => { - const offerId = row.getAttribute('data-offer-id'); - const offer = jsonData.find(o => o.offer_id === offerId); - if (!offer) return; + + const rows = document.querySelectorAll('[data-offer-id]'); + const updates = []; - const newPostedTime = formatTime(offer.created_at, true); - const newExpiresIn = formatTimeLeft(offer.expire_at); + rows.forEach(row => { + const offerId = row.getAttribute('data-offer-id'); + const offer = jsonData.find(o => o.offer_id === offerId); + if (!offer) return; - const postedElement = row.querySelector('.text-xs:first-child'); - const expiresElement = row.querySelector('.text-xs:last-child'); + const newPostedTime = formatTime(offer.created_at, true); + const newExpiresIn = formatTimeLeft(offer.expire_at); - if (postedElement && postedElement.textContent !== `Posted: ${newPostedTime}`) { - postedElement.textContent = `Posted: ${newPostedTime}`; - } - if (expiresElement && expiresElement.textContent !== `Expires in: ${newExpiresIn}`) { - expiresElement.textContent = `Expires in: ${newExpiresIn}`; - } + const postedElement = row.querySelector('.text-xs:first-child'); + const expiresElement = row.querySelector('.text-xs:last-child'); + + updates.push({ + postedElement, + expiresElement, + newPostedTime, + newExpiresIn }); }); + + updates.forEach(({ postedElement, expiresElement, newPostedTime, newExpiresIn }) => { + if (postedElement && postedElement.textContent !== `Posted: ${newPostedTime}`) { + postedElement.textContent = `Posted: ${newPostedTime}`; + } + if (expiresElement && expiresElement.textContent !== `Expires in: ${newExpiresIn}`) { + expiresElement.textContent = `Expires in: ${newExpiresIn}`; + } + }); } function updateLastRefreshTime() { @@ -1097,8 +1143,13 @@ async function updateOffersTable(options = {}) { return; } + const isIncrementalUpdate = options.incremental === true; + if (!options.skipSkeleton && !isIncrementalUpdate && offersBody) { + offersBody.innerHTML = 'Monitor your currently active swap transactions.
-- Automatically create offers and bids based on your configuration. -
-