From 73d486d6f00d1ccfdeca93fa2d5be1d0ca139bea Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Fri, 10 Oct 2025 11:08:23 +0200 Subject: [PATCH 1/7] Refactor + Optimizations --- basicswap/basicswap.py | 338 ++++++- basicswap/config.py | 2 + basicswap/db.py | 24 +- basicswap/http_server.py | 104 +- basicswap/js_server.py | 107 +++ basicswap/rpc.py | 69 ++ basicswap/rpc_pool.py | 131 +++ basicswap/static/js/modules/api-manager.js | 315 +++--- .../static/js/modules/balance-updates.js | 38 +- basicswap/static/js/modules/cache-manager.js | 54 +- .../static/js/modules/cleanup-manager.js | 2 +- basicswap/static/js/modules/coin-manager.js | 14 +- basicswap/static/js/modules/coin-utils.js | 191 ++++ basicswap/static/js/modules/config-manager.js | 70 +- basicswap/static/js/modules/dom-cache.js | 207 ++++ basicswap/static/js/modules/error-handler.js | 215 +++++ basicswap/static/js/modules/event-handlers.js | 342 +++++++ basicswap/static/js/modules/form-validator.js | 225 +++++ .../static/js/modules/identity-manager.js | 62 +- .../static/js/modules/network-manager.js | 19 +- .../static/js/modules/notification-manager.js | 162 ++-- basicswap/static/js/modules/price-manager.js | 19 +- basicswap/static/js/modules/qrcode-manager.js | 79 ++ .../static/js/modules/summary-manager.js | 31 +- .../static/js/modules/tooltips-manager.js | 142 ++- basicswap/static/js/modules/wallet-amount.js | 196 ++++ basicswap/static/js/modules/wallet-manager.js | 38 +- .../static/js/modules/websocket-manager.js | 95 +- basicswap/static/js/pages/amm-config-tabs.js | 294 ++++++ .../{amm_counter.js => pages/amm-counter.js} | 20 +- basicswap/static/js/pages/amm-page.js | 573 +++++++++++ .../js/{amm_tables.js => pages/amm-tables.js} | 103 +- .../bids-available-page.js} | 10 +- .../bids-export.js} | 4 +- .../bids-page.js} | 60 +- .../js/{ui => pages}/bids-tab-navigation.js | 20 +- .../{new_offer.js => pages/offer-new-page.js} | 34 +- basicswap/static/js/pages/offer-page.js | 364 +++++++ .../js/{offers.js => pages/offers-page.js} | 506 +++++----- .../offers-pricechart.js} | 125 +-- basicswap/static/js/pages/settings-page.js | 332 +++++++ .../swaps-page.js} | 29 +- basicswap/static/js/pages/wallet-page.js | 372 ++++++++ basicswap/static/js/pages/wallets-page.js | 344 +++++++ basicswap/static/js/ui/dropdown.js | 4 +- basicswap/templates/404.html | 18 +- basicswap/templates/active.html | 21 +- basicswap/templates/amm.html | 872 +---------------- .../templates/automation_strategies.html | 19 +- basicswap/templates/automation_strategy.html | 22 +- .../templates/automation_strategy_new.html | 22 +- basicswap/templates/bid.html | 27 +- basicswap/templates/bid_xmr.html | 27 +- basicswap/templates/bids.html | 25 +- basicswap/templates/bids_available.html | 21 +- basicswap/templates/changepassword.html | 23 +- basicswap/templates/debug.html | 21 +- basicswap/templates/donation.html | 22 +- basicswap/templates/explorers.html | 19 +- basicswap/templates/header.html | 21 +- basicswap/templates/identity.html | 23 +- basicswap/templates/inc_messages.html | 4 +- basicswap/templates/macros.html | 52 + basicswap/templates/offer.html | 456 +-------- basicswap/templates/offer_confirm.html | 23 +- basicswap/templates/offer_new_1.html | 61 +- basicswap/templates/offer_new_2.html | 25 +- basicswap/templates/offers.html | 31 +- basicswap/templates/rpc.html | 19 +- basicswap/templates/settings.html | 324 +------ basicswap/templates/smsgaddresses.html | 19 +- basicswap/templates/tor.html | 19 +- basicswap/templates/unlock.html | 2 +- basicswap/templates/wallet.html | 903 +----------------- basicswap/templates/wallets.html | 422 +------- basicswap/templates/watched.html | 19 +- basicswap/ui/page_amm.py | 96 -- scripts/createoffers.py | 75 +- .../basicswap/selenium/test_swap_direction.py | 16 +- 79 files changed, 5835 insertions(+), 4419 deletions(-) create mode 100644 basicswap/rpc_pool.py create mode 100644 basicswap/static/js/modules/coin-utils.js create mode 100644 basicswap/static/js/modules/dom-cache.js create mode 100644 basicswap/static/js/modules/error-handler.js create mode 100644 basicswap/static/js/modules/event-handlers.js create mode 100644 basicswap/static/js/modules/form-validator.js create mode 100644 basicswap/static/js/modules/qrcode-manager.js create mode 100644 basicswap/static/js/modules/wallet-amount.js create mode 100644 basicswap/static/js/pages/amm-config-tabs.js rename basicswap/static/js/{amm_counter.js => pages/amm-counter.js} (95%) create mode 100644 basicswap/static/js/pages/amm-page.js rename basicswap/static/js/{amm_tables.js => pages/amm-tables.js} (98%) rename basicswap/static/js/{bids_available.js => pages/bids-available-page.js} (98%) rename basicswap/static/js/{bids_sentreceived_export.js => pages/bids-export.js} (98%) rename basicswap/static/js/{bids_sentreceived.js => pages/bids-page.js} (97%) rename basicswap/static/js/{ui => pages}/bids-tab-navigation.js (93%) rename basicswap/static/js/{new_offer.js => pages/offer-new-page.js} (95%) create mode 100644 basicswap/static/js/pages/offer-page.js rename basicswap/static/js/{offers.js => pages/offers-page.js} (89%) rename basicswap/static/js/{pricechart.js => pages/offers-pricechart.js} (93%) create mode 100644 basicswap/static/js/pages/settings-page.js rename basicswap/static/js/{swaps_in_progress.js => pages/swaps-page.js} (97%) create mode 100644 basicswap/static/js/pages/wallet-page.js create mode 100644 basicswap/static/js/pages/wallets-page.js create mode 100644 basicswap/templates/macros.html diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 6f2c5bb..605bd45 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -562,9 +562,50 @@ 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") + 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 @@ -1143,6 +1184,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( @@ -3867,7 +3910,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 +3960,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 +4047,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 +7951,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 @@ -12329,9 +12382,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") @@ -12400,7 +12452,6 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): if api_key != "": url += f"&api_key={api_key}" - self.log.debug(f"lookupFiatRates: {url}") js = json.loads(self.readURL(url, timeout=10, headers=headers)) for k, v in js.items(): @@ -12414,31 +12465,20 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): default_chart_api_key, escape=True, ) - if len(need_coins) == 1: + coin_ids: str = "" + for coin_id in coins_list: + if len(coin_ids) > 0: + coin_ids += "," 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] + 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}" + ) + js = json.loads(self.readURL(url, timeout=10, headers=headers)) + for k, v in js.items(): + return_rates[int(exchange_name_map[k])] = v[ticker_to] + new_values[exchange_name_map[k]] = v[ticker_to] else: raise ValueError(f"Unknown rate source {rate_source}") @@ -12484,6 +12524,240 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): 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 = {} + + headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"} + + 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: + 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[int(row[0])] = { + "volume_24h": volume_24h, + "price_change_24h": price_change_24h, + } + + need_coins = [] + new_values = {} + exchange_name_map = {} + for coin_id in coins_list: + if coin_id not in return_data: + need_coins.append(coin_id) + + if len(need_coins) < 1: + return return_data + + if rate_source == "coingecko.com": + 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=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=10, headers=headers)) + + for k, v in js.items(): + coin_id = int(exchange_name_map[k]) + volume_24h = v.get("usd_24h_vol") + price_change_24h = v.get("usd_24h_change") + + # Convert to float if value exists, otherwise keep as None + volume_value = float(volume_24h) if volume_24h is not None else None + price_change_value = ( + float(price_change_24h) if price_change_24h is not None else 0.0 + ) + + return_data[coin_id] = { + "volume_24h": volume_value, + "price_change_24h": price_change_value, + } + new_values[coin_id] = { + "volume_24h": volume_value, + "price_change_24h": price_change_value, + } + else: + raise ValueError(f"Unknown rate source {rate_source}") + + if len(new_values) < 1: + return return_data + + for coin_id, data in new_values.items(): + 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(data["volume_24h"]) + if data["volume_24h"] is not None + else "None" + ), + "price_change_24h": str(data["price_change_24h"]), + "rate_source": rate_source, + "last_updated": now, + }, + ) + + self.commitDB() + 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 = {} + for coin_id in coins_list: + if coin_id not in return_data: + need_coins.append(coin_id) + + if len(need_coins) < 1: + return return_data + + if rate_source == "coingecko.com": + api_key: str = get_api_key_setting( + self.settings, + "coingecko_api_key", + default_coingecko_api_key, + escape=True, + ) + + for coin_id in need_coins: + 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}" + + js = json.loads(self.readURL(url, timeout=10, headers=headers)) + + if "prices" in js: + return_data[coin_id] = js["prices"] + new_values[coin_id] = js["prices"] + else: + raise ValueError(f"Unknown rate source {rate_source}") + + if len(new_values) < 1: + return return_data + + for coin_id, price_data in new_values.items(): + cursor.execute( + "INSERT OR REPLACE INTO coinhistory (coin_id, days, price_data, source, last_updated) VALUES (:coin_id, :days, :price_data, :rate_source, :last_updated)", + { + "coin_id": coin_id, + "days": days, + "price_data": json.dumps(price_data), + "rate_source": rate_source, + "last_updated": now, + }, + ) + + self.commitDB() + return return_data + finally: + self.closeDB(cursor, commit=False) + def lookupRates(self, coin_from, coin_to, output_array=False): self.log.debug( "lookupRates {}, {}.".format( 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/http_server.py b/basicswap/http_server.py index 77bbe28..e85c09a 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( @@ -244,15 +246,17 @@ class HttpHandler(BaseHTTPRequestHandler): 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 +270,17 @@ 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 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 +370,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 +635,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 +944,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 +978,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 +996,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..9d77e03 100644 --- a/basicswap/rpc.py +++ b/basicswap/rpc.py @@ -6,6 +6,7 @@ # file LICENSE or http://www.opensource.org/licenses/mit-license.php. import json +import logging import traceback import urllib from xmlrpc.client import ( @@ -15,6 +16,15 @@ 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 Jsonrpc: # __getattr__ complicates extending ServerProxy @@ -91,6 +101,9 @@ class Jsonrpc: 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: @@ -110,6 +123,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) diff --git a/basicswap/rpc_pool.py b/basicswap/rpc_pool.py new file mode 100644 index 0000000..d3c3e23 --- /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=30, 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..c04643e 100644 --- a/basicswap/static/js/modules/api-manager.js +++ b/basicswap/static/js/modules/api-manager.js @@ -4,34 +4,35 @@ 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 }, + cryptocompare: { requestsPerMinute: 30, minInterval: 2000 } } - } - }; + }; + } 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 +42,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 +56,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 +120,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 +133,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 +165,7 @@ const ApiManager = (function() { 'Content-Type': 'application/json', ...headers }, - signal: AbortSignal.timeout(config.requestTimeout) + signal: AbortSignal.timeout(getConfig().requestTimeout || 60000) }; if (body) { @@ -233,11 +248,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 +272,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 +312,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 +364,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 +374,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..050ee4f 100644 --- a/basicswap/static/js/modules/config-manager.js +++ b/basicswap/static/js/modules/config-manager.js @@ -53,20 +53,11 @@ const ConfigManager = (function() { }, 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: { @@ -122,55 +113,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 +185,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( - 'BTC1.00000000 BTC → XMR15.50000000 XMR', + 'New Network Offer', 'new_offer', { offerId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66', - subtitle: 'New offer • Rate: 1 BTC = 15.50000000 XMR', + subtitle: `BTC1.00000000 BTC → XMR15.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( - 'BTC0.50000000 BTC → XMR7.75000000 XMR', + 'New Bid Received', 'new_bid', { bidId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66', - subtitle: 'New bid • Rate: 1 BTC = 15.50000000 XMR', + subtitle: `BTC0.50000000 BTC → XMR7.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 = `${options.coinSymbol}`; } - 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 = `${coinFromName}${amountFrom} ${coinFromName} → ${coinToName}${amountTo} ${coinToName}`; - toastOptions.subtitle = `New offer • Rate: 1 ${coinFromName} = ${(data.amount_to / data.amount_from).toFixed(8)} ${coinToName}`; + toastTitle = `New Network Offer`; + toastOptions.subtitle = `${coinFromName}${amountFrom} ${coinFromName} → ${coinToName}${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 = `${coinFromName}${bidAmountFrom} ${coinFromName} → ${coinToName}${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 = `${coinFromName}${bidAmountFrom} ${coinFromName} → ${coinToName}${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..e971203 100644 --- a/basicswap/static/js/modules/wallet-manager.js +++ b/basicswap/static/js/modules/wallet-manager.js @@ -95,22 +95,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 +164,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 +438,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 +549,6 @@ const WalletManager = (function() { } } - // Public API const publicAPI = { initialize: async function(options) { if (state.initialized) { @@ -579,7 +588,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 +670,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..090bf7d 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; @@ -311,24 +328,37 @@ const WebSocketManager = (function() { 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 +386,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 +403,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 +484,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() {
Balance: ${balance}
`; - if (pendingBalance && parseFloat(pendingBalance) > 0) { html += `
+${pendingBalance} pending
`; } @@ -1061,11 +994,9 @@ const AmmTablesManager = (function() { item.appendChild(itemIcon); item.appendChild(itemText); - item.addEventListener('click', function() { select.value = this.getAttribute('data-value'); - const selectedOption = select.options[select.selectedIndex]; const selectedCoinName = selectedOption.textContent.trim(); const selectedBalance = selectedOption.getAttribute('data-balance') || '0.00000000'; @@ -1079,7 +1010,6 @@ const AmmTablesManager = (function() {
Balance: ${selectedBalance}
`; - if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) { html += `
+${selectedPendingBalance} pending
`; } @@ -1093,7 +1023,6 @@ const AmmTablesManager = (function() { dropdown.classList.add('hidden'); - const event = new Event('change', { bubbles: true }); select.dispatchEvent(event); }); @@ -1101,7 +1030,6 @@ const AmmTablesManager = (function() { dropdown.appendChild(item); }); - const selectedOption = select.options[select.selectedIndex]; if (selectedOption) { const selectedCoinName = selectedOption.textContent.trim(); @@ -1116,7 +1044,6 @@ const AmmTablesManager = (function() {
Balance: ${selectedBalance}
`; - if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) { html += `
+${selectedPendingBalance} pending
`; } @@ -1129,12 +1056,10 @@ const AmmTablesManager = (function() { } } - button.addEventListener('click', function() { dropdown.classList.toggle('hidden'); }); - document.addEventListener('click', function(e) { if (!wrapper.contains(e.target)) { dropdown.classList.add('hidden'); @@ -1267,7 +1192,6 @@ const AmmTablesManager = (function() { modalTitle.textContent = `Add New ${type.charAt(0).toUpperCase() + type.slice(1)}`; } - const modal = document.getElementById('add-amm-modal'); if (modal) { modal.classList.remove('hidden'); @@ -1275,17 +1199,14 @@ const AmmTablesManager = (function() { modal.setAttribute('data-amm-type', type); } - setTimeout(() => { updateDropdownsForModalType('add'); initializeCustomSelects(type); - refreshDropdownBalanceDisplay(type); - if (typeof fetchBalanceData === 'function') { fetchBalanceData() .then(balanceData => { @@ -1721,7 +1642,6 @@ const AmmTablesManager = (function() { modalTitle.textContent = `Edit ${type.charAt(0).toUpperCase() + type.slice(1)}`; } - const modal = document.getElementById('edit-amm-modal'); if (modal) { modal.classList.remove('hidden'); @@ -1729,17 +1649,14 @@ const AmmTablesManager = (function() { modal.setAttribute('data-amm-type', type); } - setTimeout(() => { updateDropdownsForModalType('edit'); initializeCustomSelects(type); - refreshDropdownBalanceDisplay(type); - if (typeof fetchBalanceData === 'function') { fetchBalanceData() .then(balanceData => { @@ -1873,7 +1790,6 @@ const AmmTablesManager = (function() { }); } - function closeEditModal() { const modal = document.getElementById('edit-amm-modal'); if (modal) { @@ -2306,14 +2222,11 @@ const AmmTablesManager = (function() { document.getElementById('edit-offer-swap-type') ]; - - function createSwapTypeDropdown(select) { if (!select) return; - if (select.style.display === 'none' && select.parentNode.querySelector('.relative')) { - return; // Custom dropdown already exists + return; } const wrapper = document.createElement('div'); @@ -2416,9 +2329,9 @@ const AmmTablesManager = (function() { let showBalance = false; if (modalType === 'offer' && select.id.includes('coin-from')) { - showBalance = true; // OFFER: maker shows balance + showBalance = true; } else if (modalType === 'bid' && select.id.includes('coin-to')) { - showBalance = true; // BID: taker shows balance + showBalance = true; } createSimpleDropdown(select, showBalance); @@ -2720,7 +2633,7 @@ const AmmTablesManager = (function() { icon.classList.remove('animate-spin'); } refreshButton.disabled = false; - }, 500); // Reduced from 1000ms to 500ms + }, 500); } }); } diff --git a/basicswap/static/js/bids_available.js b/basicswap/static/js/pages/bids-available-page.js similarity index 98% rename from basicswap/static/js/bids_available.js rename to basicswap/static/js/pages/bids-available-page.js index 1120189..fa49168 100644 --- a/basicswap/static/js/bids_available.js +++ b/basicswap/static/js/pages/bids-available-page.js @@ -53,9 +53,9 @@ const getTimeStrokeColor = (expireTime) => { const now = Math.floor(Date.now() / 1000); const timeLeft = expireTime - now; - if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less - if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less - return '#10B981'; // More than 30 minutes + if (timeLeft <= 300) return '#9CA3AF'; + if (timeLeft <= 1800) return '#3B82F6'; + return '#10B981'; }; const createTimeTooltip = (bid) => { @@ -249,7 +249,7 @@ const updateLoadingState = (isLoading) => { const refreshText = elements.refreshBidsButton.querySelector('#refreshText'); if (refreshIcon) { - // Add CSS transition for smoother animation + refreshIcon.style.transition = 'transform 0.3s ease'; refreshIcon.classList.toggle('animate-spin', isLoading); } @@ -631,7 +631,7 @@ if (elements.refreshBidsButton) { updateLoadingState(true); - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise(resolve => CleanupManager.setTimeout(resolve, 500)); try { await updateBidsTable({ resetPage: true, refreshData: true }); diff --git a/basicswap/static/js/bids_sentreceived_export.js b/basicswap/static/js/pages/bids-export.js similarity index 98% rename from basicswap/static/js/bids_sentreceived_export.js rename to basicswap/static/js/pages/bids-export.js index a916f6e..43ea810 100644 --- a/basicswap/static/js/bids_sentreceived_export.js +++ b/basicswap/static/js/pages/bids-export.js @@ -66,7 +66,7 @@ const BidExporter = { link.click(); document.body.removeChild(link); - setTimeout(() => { + CleanupManager.setTimeout(() => { URL.revokeObjectURL(url); }, 100); } catch (error) { @@ -104,7 +104,7 @@ const BidExporter = { }; document.addEventListener('DOMContentLoaded', function() { - setTimeout(function() { + CleanupManager.setTimeout(function() { if (typeof state !== 'undefined' && typeof EventManager !== 'undefined') { const exportAllButton = document.getElementById('exportAllBids'); if (exportAllButton) { diff --git a/basicswap/static/js/bids_sentreceived.js b/basicswap/static/js/pages/bids-page.js similarity index 97% rename from basicswap/static/js/bids_sentreceived.js rename to basicswap/static/js/pages/bids-page.js index c6f2110..d211b07 100644 --- a/basicswap/static/js/bids_sentreceived.js +++ b/basicswap/static/js/pages/bids-page.js @@ -32,7 +32,7 @@ document.addEventListener('tabactivated', function(event) { if (event.detail && event.detail.tabId) { const tabType = event.detail.type || (event.detail.tabId === '#all' ? 'all' : (event.detail.tabId === '#sent' ? 'sent' : 'received')); - //console.log('Tab activation event received for:', tabType); + state.currentTab = tabType; updateBidsTable(); } @@ -190,8 +190,7 @@ const EventManager = { }; function cleanup() { - //console.log('Starting comprehensive cleanup process for bids table'); - + try { if (searchTimeout) { clearTimeout(searchTimeout); @@ -326,8 +325,7 @@ window.cleanupBidsTable = cleanup; CleanupManager.addListener(document, 'visibilitychange', () => { if (document.hidden) { - //console.log('Page hidden - pausing WebSocket and optimizing memory'); - + if (WebSocketManager && typeof WebSocketManager.pause === 'function') { WebSocketManager.pause(); } else if (WebSocketManager && typeof WebSocketManager.disconnect === 'function') { @@ -351,7 +349,7 @@ CleanupManager.addListener(document, 'visibilitychange', () => { const lastUpdateTime = state.lastRefresh || 0; const now = Date.now(); - const refreshInterval = 5 * 60 * 1000; // 5 minutes + const refreshInterval = 5 * 60 * 1000; if (now - lastUpdateTime > refreshInterval) { setTimeout(() => { @@ -490,13 +488,7 @@ function coinMatches(offerCoin, filterCoin) { if (offerCoin === filterCoin) return true; - if ((offerCoin === 'firo' || offerCoin === 'zcoin') && - (filterCoin === 'firo' || filterCoin === 'zcoin')) { - return true; - } - - if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') || - (offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) { + if (window.CoinUtils && window.CoinUtils.isSameCoin(offerCoin, filterCoin)) { return true; } @@ -1012,7 +1004,7 @@ const forceTooltipDOMCleanup = () => { }); } if (removedCount > 0) { - // console.log(`Tooltip cleanup: found ${foundCount}, removed ${removedCount} detached tooltips`); + } } @@ -1323,8 +1315,6 @@ async function fetchBids(type = state.currentTab) { const withExpiredSelect = document.getElementById('with_expired'); const includeExpired = withExpiredSelect ? withExpiredSelect.value === 'true' : true; - //console.log(`Fetching ${type} bids, include expired:`, includeExpired); - const timeoutId = setTimeout(() => { if (activeFetchController) { activeFetchController.abort(); @@ -1372,8 +1362,6 @@ async function fetchBids(type = state.currentTab) { } } - //console.log(`Received raw ${type} data:`, data.length, 'bids'); - state.filters.with_expired = includeExpired; let processedData; @@ -1405,12 +1393,16 @@ const updateTableContent = async (type) => { const tbody = elements[`${type}BidsBody`]; if (!tbody) return; + tbody.innerHTML = '
Loading bids...
'; + if (window.TooltipManager) { - window.TooltipManager.cleanup(); + requestAnimationFrame(() => window.TooltipManager.cleanup()); } - cleanupTooltips(); - forceTooltipDOMCleanup(); + requestAnimationFrame(() => { + cleanupTooltips(); + forceTooltipDOMCleanup(); + }); tooltipIdsToCleanup.clear(); @@ -1421,14 +1413,6 @@ const updateTableContent = async (type) => { const currentPageData = filteredData.slice(startIndex, endIndex); - //console.log('Updating table content:', { - // type: type, - // totalFilteredBids: filteredData.length, - // currentPageBids: currentPageData.length, - // startIndex: startIndex, - // endIndex: endIndex - //}); - try { if (currentPageData.length > 0) { const BATCH_SIZE = 10; @@ -1440,9 +1424,6 @@ const updateTableContent = async (type) => { const rows = await Promise.all(rowPromises); allRows = allRows.concat(rows); - if (i + BATCH_SIZE < currentPageData.length) { - await new Promise(resolve => setTimeout(resolve, 5)); - } } const scrollPosition = tbody.parentElement?.scrollTop || 0; @@ -1495,7 +1476,7 @@ const initializeTooltips = () => { const tooltipTriggers = document.querySelectorAll(selector); const tooltipCount = tooltipTriggers.length; if (tooltipCount > 50) { - //console.log(`Optimizing ${tooltipCount} tooltips`); + const viewportMargin = 200; const viewportTooltips = Array.from(tooltipTriggers).filter(trigger => { const rect = trigger.getBoundingClientRect(); @@ -1595,13 +1576,6 @@ const updatePaginationControls = (type) => { const currentPageSpan = elements[`currentPage${type.charAt(0).toUpperCase() + type.slice(1)}`]; const bidsCount = elements[`${type}BidsCount`]; - //console.log('Pagination controls update:', { - // type: type, - // totalBids: data.length, - // totalPages: totalPages, - // currentPage: state.currentPage[type] - //}); - if (state.currentPage[type] > totalPages) { state.currentPage[type] = totalPages > 0 ? totalPages : 1; } @@ -2077,7 +2051,7 @@ const setupEventListeners = () => { function setupMemoryMonitoring() { const MEMORY_CHECK_INTERVAL = 2 * 60 * 1000; - const intervalId = setInterval(() => { + const intervalId = CleanupManager.setInterval(() => { if (document.hidden) { console.log('Tab hidden - running memory optimization'); @@ -2110,9 +2084,9 @@ function setupMemoryMonitoring() { } }, MEMORY_CHECK_INTERVAL); - document.addEventListener('beforeunload', () => { + CleanupManager.registerResource('bidsMemoryMonitoring', intervalId, () => { clearInterval(intervalId); - }, { once: true }); + }); } function initialize() { diff --git a/basicswap/static/js/ui/bids-tab-navigation.js b/basicswap/static/js/pages/bids-tab-navigation.js similarity index 93% rename from basicswap/static/js/ui/bids-tab-navigation.js rename to basicswap/static/js/pages/bids-tab-navigation.js index 0e5286f..713e19f 100644 --- a/basicswap/static/js/ui/bids-tab-navigation.js +++ b/basicswap/static/js/pages/bids-tab-navigation.js @@ -7,7 +7,7 @@ originalOnload(); } - setTimeout(function() { + CleanupManager.setTimeout(function() { initBidsTabNavigation(); handleInitialNavigation(); }, 100); @@ -15,6 +15,12 @@ document.addEventListener('DOMContentLoaded', function() { initBidsTabNavigation(); + + if (window.CleanupManager) { + CleanupManager.registerResource('bidsTabHashChange', handleHashChange, () => { + window.removeEventListener('hashchange', handleHashChange); + }); + } }); window.addEventListener('hashchange', handleHashChange); @@ -43,7 +49,7 @@ }); window.bidsTabNavigationInitialized = true; - //console.log('Bids tab navigation initialized'); + } function handleInitialNavigation() { @@ -97,15 +103,13 @@ if (!tabButton) { if (retryCount < 5) { - setTimeout(() => { + CleanupManager.setTimeout(() => { activateTabWithRetry(normalizedTabId, retryCount + 1); }, 100); } return; } - - tabButton.click(); if (window.Tabs) { @@ -160,7 +164,7 @@ } function triggerDataLoad(tabId) { - setTimeout(() => { + CleanupManager.setTimeout(() => { if (window.state) { window.state.currentTab = tabId === '#all' ? 'all' : (tabId === '#sent' ? 'sent' : 'received'); @@ -181,7 +185,7 @@ document.dispatchEvent(event); if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') { - setTimeout(() => { + CleanupManager.setTimeout(() => { window.TooltipManager.cleanup(); if (typeof window.initializeTooltips === 'function') { window.initializeTooltips(); @@ -196,7 +200,7 @@ activateTabWithRetry(tabId); - setTimeout(function() { + CleanupManager.setTimeout(function() { window.scrollTo(0, oldScrollPosition); }, 0); } diff --git a/basicswap/static/js/new_offer.js b/basicswap/static/js/pages/offer-new-page.js similarity index 95% rename from basicswap/static/js/new_offer.js rename to basicswap/static/js/pages/offer-new-page.js index f4b615f..3c61cb9 100644 --- a/basicswap/static/js/new_offer.js +++ b/basicswap/static/js/pages/offer-new-page.js @@ -16,6 +16,30 @@ const DOM = { queryAll: (selector) => document.querySelectorAll(selector) }; +const ErrorModal = { + show: function(title, message) { + const errorTitle = document.getElementById('errorTitle'); + const errorMessage = document.getElementById('errorMessage'); + const modal = document.getElementById('errorModal'); + + if (errorTitle) errorTitle.textContent = title || 'Error'; + if (errorMessage) errorMessage.textContent = message || 'An error occurred'; + if (modal) modal.classList.remove('hidden'); + }, + + hide: function() { + const modal = document.getElementById('errorModal'); + if (modal) modal.classList.add('hidden'); + }, + + init: function() { + const errorOkBtn = document.getElementById('errorOk'); + if (errorOkBtn) { + errorOkBtn.addEventListener('click', this.hide.bind(this)); + } + } +}; + const Storage = { get: (key) => { try { @@ -450,20 +474,17 @@ const UIEnhancer = { const coinName = parts[0]; const balanceInfo = parts[1] || ''; - selectNameElement.innerHTML = ''; selectNameElement.style.display = 'flex'; selectNameElement.style.flexDirection = 'column'; selectNameElement.style.alignItems = 'flex-start'; selectNameElement.style.lineHeight = '1.2'; - const coinNameDiv = document.createElement('div'); coinNameDiv.textContent = coinName; coinNameDiv.style.fontWeight = 'normal'; coinNameDiv.style.color = 'inherit'; - const balanceDiv = document.createElement('div'); balanceDiv.textContent = `Balance: ${balanceInfo}`; balanceDiv.style.fontSize = '0.75rem'; @@ -473,8 +494,6 @@ const UIEnhancer = { selectNameElement.appendChild(coinNameDiv); selectNameElement.appendChild(balanceDiv); - - } else { selectNameElement.textContent = name; @@ -575,6 +594,8 @@ function initializeApp() { UIEnhancer.handleErrorHighlighting(); UIEnhancer.updateDisabledStyles(); UIEnhancer.setupCustomSelects(); + + ErrorModal.init(); } if (document.readyState === 'loading') { @@ -582,3 +603,6 @@ if (document.readyState === 'loading') { } else { initializeApp(); } + +window.showErrorModal = ErrorModal.show.bind(ErrorModal); +window.hideErrorModal = ErrorModal.hide.bind(ErrorModal); diff --git a/basicswap/static/js/pages/offer-page.js b/basicswap/static/js/pages/offer-page.js new file mode 100644 index 0000000..85b4b22 --- /dev/null +++ b/basicswap/static/js/pages/offer-page.js @@ -0,0 +1,364 @@ +(function() { + 'use strict'; + + const OfferPage = { + xhr_rates: null, + xhr_bid_params: null, + + init: function() { + this.xhr_rates = new XMLHttpRequest(); + this.xhr_bid_params = new XMLHttpRequest(); + + this.setupXHRHandlers(); + this.setupEventListeners(); + this.handleBidsPageAddress(); + }, + + setupXHRHandlers: function() { + this.xhr_rates.onload = () => { + if (this.xhr_rates.status == 200) { + const obj = JSON.parse(this.xhr_rates.response); + const inner_html = '

Rates

' + 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 = '

Rates

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..2dee9ad 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]) { @@ -739,6 +749,23 @@ async function fetchOffers() { 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'); @@ -752,14 +779,10 @@ async function fetchOffers() { } const [offersResponse, pricesData] = await Promise.all([ - fetch(isSentOffers ? '/json/sentoffers' : '/json/offers'), + fetchWithRetry(isSentOffers ? '/json/sentoffers' : '/json/offers'), fetchLatestPrices() ]); - if (!offersResponse.ok) { - throw new Error(`HTTP error! status: ${offersResponse.status}`); - } - const data = await offersResponse.json(); if (data.error) { @@ -871,27 +894,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 +1130,13 @@ async function updateOffersTable(options = {}) { return; } + const isIncrementalUpdate = options.incremental === true; + if (!options.skipSkeleton && !isIncrementalUpdate && offersBody) { + offersBody.innerHTML = '
Loading offers...
'; + } + if (window.TooltipManager) { - window.TooltipManager.cleanup(); + requestAnimationFrame(() => window.TooltipManager.cleanup()); } const validOffers = getValidOffers(); @@ -1138,28 +1176,72 @@ async function updateOffersTable(options = {}) { if (row) fragment.appendChild(row); }); - if (i + BATCH_SIZE < itemsToDisplay.length) { - await new Promise(resolve => setTimeout(resolve, 16)); - } } if (offersBody) { - const existingRows = offersBody.querySelectorAll('tr'); - existingRows.forEach(row => cleanupRow(row)); - offersBody.textContent = ''; - offersBody.appendChild(fragment); + if (isIncrementalUpdate && offersBody.children.length > 0) { + + const existingRows = Array.from(offersBody.querySelectorAll('tr[data-offer-id]')); + const newRows = Array.from(fragment.querySelectorAll('tr[data-offer-id]')); + + const existingMap = new Map(existingRows.map(row => [row.getAttribute('data-offer-id'), row])); + const newMap = new Map(newRows.map(row => [row.getAttribute('data-offer-id'), row])); + + existingRows.forEach(row => { + const offerId = row.getAttribute('data-offer-id'); + if (!newMap.has(offerId)) { + cleanupRow(row); + row.remove(); + } + }); + + newRows.forEach((newRow, index) => { + const offerId = newRow.getAttribute('data-offer-id'); + const existingRow = existingMap.get(offerId); + + if (existingRow) { + + const currentIndex = Array.from(offersBody.children).indexOf(existingRow); + if (currentIndex !== index) { + + if (index >= offersBody.children.length) { + offersBody.appendChild(existingRow); + } else { + offersBody.insertBefore(existingRow, offersBody.children[index]); + } + } + + } else { + + if (index >= offersBody.children.length) { + offersBody.appendChild(newRow); + } else { + offersBody.insertBefore(newRow, offersBody.children[index]); + } + } + }); + } else { + + const existingRows = offersBody.querySelectorAll('tr'); + existingRows.forEach(row => cleanupRow(row)); + offersBody.textContent = ''; + offersBody.appendChild(fragment); + } } - initializeTooltips(); + initializeTooltipsInBatches(); - requestAnimationFrame(() => { + CleanupManager.setTimeout(() => { updateRowTimes(); updatePaginationInfo(); updateProfitLossDisplays(); + }, 10); + + CleanupManager.setTimeout(() => { if (tableRateModule?.initializeTable) { tableRateModule.initializeTable(); } - }); + }, 50); lastRefreshTime = Date.now(); updateLastRefreshTime(); @@ -1171,7 +1253,10 @@ async function updateOffersTable(options = {}) { } function updateProfitLossDisplays() { + const rows = document.querySelectorAll('[data-offer-id]'); + const updates = []; + rows.forEach(row => { const offerId = row.getAttribute('data-offer-id'); const offer = jsonData.find(o => o.offer_id === offerId); @@ -1179,6 +1264,17 @@ function updateProfitLossDisplays() { const fromAmount = parseFloat(offer.amount_from) || 0; const toAmount = parseFloat(offer.amount_to) || 0; + + updates.push({ + row, + offerId, + offer, + fromAmount, + toAmount + }); + }); + + updates.forEach(({ row, offerId, offer, fromAmount, toAmount }) => { updateProfitLoss(row, offer.coin_from, offer.coin_to, fromAmount, toAmount, offer.is_own_offer); const rateTooltipId = `tooltip-rate-${offerId}`; @@ -1494,7 +1590,6 @@ function createRateColumn(offer, coinFrom, coinTo) { `; } - function createPercentageColumn(offer) { return ` @@ -1731,45 +1826,10 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou const getPriceKey = (coin) => { if (!coin) return null; - - const lowerCoin = coin.toLowerCase(); - - if (lowerCoin === 'zcoin') return 'firo'; - - if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') { - - if (latestPrices && latestPrices['bitcoin-cash']) { - return 'bitcoin-cash'; - } else if (latestPrices && latestPrices['bch']) { - return 'bch'; - } - return 'bitcoin-cash'; + if (window.CoinUtils) { + return window.CoinUtils.normalizeCoinName(coin, latestPrices); } - - if (lowerCoin === 'part' || lowerCoin === 'particl' || lowerCoin.includes('particl')) { - return 'part'; - } - - if (window.config && window.config.coinMappings && window.config.coinMappings.nameToSymbol) { - const symbol = window.config.coinMappings.nameToSymbol[coin]; - if (symbol) { - if (symbol.toUpperCase() === 'BCH') { - if (latestPrices && latestPrices['bitcoin-cash']) { - return 'bitcoin-cash'; - } else if (latestPrices && latestPrices['bch']) { - return 'bch'; - } - return 'bitcoin-cash'; - } - - if (symbol.toUpperCase() === 'PART') { - return 'part'; - } - - return symbol.toLowerCase(); - } - } - return lowerCoin; + return coin.toLowerCase(); }; const fromSymbol = getPriceKey(coinFrom); @@ -1849,44 +1909,10 @@ function createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer) { const getPriceKey = (coin) => { if (!coin) return null; - - const lowerCoin = coin.toLowerCase(); - - if (lowerCoin === 'zcoin') return 'firo'; - - if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') { - if (latestPrices && latestPrices['bitcoin-cash']) { - return 'bitcoin-cash'; - } else if (latestPrices && latestPrices['bch']) { - return 'bch'; - } - - return 'bitcoin-cash'; + if (window.CoinUtils) { + return window.CoinUtils.normalizeCoinName(coin, latestPrices); } - - if (lowerCoin === 'part' || lowerCoin === 'particl' || lowerCoin.includes('particl')) { - return 'part'; - } - - if (window.config && window.config.coinMappings && window.config.coinMappings.nameToSymbol) { - const symbol = window.config.coinMappings.nameToSymbol[coin]; - if (symbol) { - if (symbol.toUpperCase() === 'BCH') { - - if (latestPrices && latestPrices['bitcoin-cash']) { - return 'bitcoin-cash'; - } else if (latestPrices && latestPrices['bch']) { - return 'bch'; - } - return 'bitcoin-cash'; - } - if (symbol.toUpperCase() === 'PART') { - return 'part'; - } - return symbol.toLowerCase(); - } - } - return lowerCoin; + return coin.toLowerCase(); }; const fromSymbol = getPriceKey(coinFrom); @@ -1958,23 +1984,23 @@ function updateTooltipTargets(row, uniqueId) { }); } -function applyFilters() { - if (filterTimeout) { - clearTimeout(filterTimeout); - filterTimeout = null; +function applyFilters(options = {}) { + if (window.filterTimeout) { + clearTimeout(window.filterTimeout); + window.filterTimeout = null; } try { - filterTimeout = setTimeout(() => { + window.filterTimeout = CleanupManager.setTimeout(() => { currentPage = 1; jsonData = filterAndSortData(); - updateOffersTable(); + updateOffersTable(options); updateClearFiltersButton(); - filterTimeout = null; + window.filterTimeout = null; }, 250); } catch (error) { console.error('Error in filter timeout:', error); - filterTimeout = null; + window.filterTimeout = null; } } @@ -2037,13 +2063,10 @@ function formatTimeLeft(timestamp) { } function getDisplayName(coinName) { - if (window.CoinManager) { + if (window.CoinManager && window.CoinManager.getDisplayName) { return window.CoinManager.getDisplayName(coinName) || coinName; } - if (coinName.toLowerCase() === 'zcoin') { - return 'Firo'; - } - return window.config.coinMappings.nameToDisplayName[coinName] || coinName; + return coinName; } function getCoinSymbolLowercase(coin) { @@ -2085,38 +2108,23 @@ function escapeHtml(unsafe) { } function getPriceKey(coin) { - if (window.CoinManager) { return window.CoinManager.getPriceKey(coin); } - - if (!coin) return null; - - const lowerCoin = coin.toLowerCase(); - - if (lowerCoin === 'zcoin') { - return 'firo'; + if (window.CoinUtils) { + return window.CoinUtils.normalizeCoinName(coin); } - - if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') { - return 'bitcoin-cash'; - } - - if (lowerCoin === 'part' || lowerCoin === 'particl' || - lowerCoin.includes('particl')) { - return 'particl'; - } - - return lowerCoin; + return coin ? coin.toLowerCase() : null; } function getCoinSymbol(fullName) { - if (window.CoinManager) { return window.CoinManager.getSymbol(fullName) || fullName; } - - return window.config.coinMappings.nameToSymbol[fullName] || fullName; + if (window.CoinUtils) { + return window.CoinUtils.getCoinSymbol(fullName); + } + return fullName; } function initializeTableEvents() { @@ -2140,7 +2148,6 @@ function initializeTableEvents() { const statusSelect = document.getElementById('status'); const sentFromSelect = document.getElementById('sent_from'); - if (coinToButton && coinToDropdown) { CleanupManager.addListener(coinToButton, 'click', (e) => { e.stopPropagation(); @@ -2155,7 +2162,6 @@ function initializeTableEvents() { }); } - if (coinFromButton && coinFromDropdown) { CleanupManager.addListener(coinFromButton, 'click', (e) => { e.stopPropagation(); @@ -2220,15 +2226,16 @@ function initializeTableEvents() { refreshButton.classList.remove('bg-blue-600', 'hover:bg-green-600', 'border-blue-500', 'hover:border-green-600'); refreshButton.classList.add('bg-red-600', 'border-red-500', 'cursor-not-allowed'); - if (countdownInterval) clearInterval(countdownInterval); + if (window.countdownInterval) clearInterval(window.countdownInterval); - countdownInterval = setInterval(() => { + window.countdownInterval = CleanupManager.setInterval(() => { const currentTime = Date.now(); const elapsedTime = currentTime - startTime; const remainingTime = Math.ceil((REFRESH_COOLDOWN - elapsedTime) / 1000); if (remainingTime <= 0) { - clearInterval(countdownInterval); + clearInterval(window.countdownInterval); + window.countdownInterval = null; refreshText.textContent = 'Refresh'; refreshButton.classList.remove('bg-red-600', 'border-red-500', 'cursor-not-allowed'); @@ -2240,7 +2247,6 @@ function initializeTableEvents() { return; } - console.log('Manual refresh initiated'); lastRefreshTime = now; const refreshIcon = document.getElementById('refreshIcon'); const refreshText = document.getElementById('refreshText'); @@ -2267,10 +2273,10 @@ function initializeTableEvents() { if (!priceData && previousPrices) { console.log('Using previous price data after failed refresh'); latestPrices = previousPrices; - applyFilters(); + applyFilters({ incremental: false }); } else if (priceData) { latestPrices = priceData; - applyFilters(); + applyFilters({ incremental: false }); } else { throw new Error('Unable to fetch price data'); } @@ -2278,8 +2284,6 @@ function initializeTableEvents() { lastRefreshTime = now; updateLastRefreshTime(); - console.log('Manual refresh completed successfully'); - } catch (error) { console.error('Error during manual refresh:', error); NetworkManager.handleNetworkError(error); @@ -2320,7 +2324,7 @@ function initializeTableEvents() { await updateOffersTable({ fromPaginationClick: true }); updatePaginationInfo(); } finally { - setTimeout(() => { + CleanupManager.setTimeout(() => { isPaginationInProgress = false; }, 100); } @@ -2340,7 +2344,7 @@ function initializeTableEvents() { await updateOffersTable({ fromPaginationClick: true }); updatePaginationInfo(); } finally { - setTimeout(() => { + CleanupManager.setTimeout(() => { isPaginationInProgress = false; }, 100); } @@ -2416,18 +2420,44 @@ function handleTableSort(columnIndex, header) { clearTimeout(window.sortTimeout); } - window.sortTimeout = setTimeout(() => { + window.sortTimeout = CleanupManager.setTimeout(() => { applyFilters(); }, 100); } +function startAutoRefresh() { + const REFRESH_INTERVAL = 2 * 60 * 1000; // 2 minutes + + if (autoRefreshInterval) { + clearInterval(autoRefreshInterval); + } + + autoRefreshInterval = CleanupManager.setInterval(async () => { + try { + + const response = await fetch(isSentOffers ? '/json/sentoffers' : '/json/offers'); + if (response.ok) { + + } + } catch (error) { + console.error('[Auto-refresh] Error during background refresh:', error); + } + }, REFRESH_INTERVAL); +} + +function stopAutoRefresh() { + if (autoRefreshInterval) { + clearInterval(autoRefreshInterval); + autoRefreshInterval = null; + } +} + async function initializeTableAndData() { loadSavedSettings(); updateClearFiltersButton(); initializeTableEvents(); initializeTooltips(); - updateFilterButtonText('coin_to'); updateFilterButtonText('coin_from'); updateCoinBadges('coin_to'); @@ -2527,24 +2557,44 @@ document.addEventListener('DOMContentLoaded', async function() { if (window.WebSocketManager) { WebSocketManager.addMessageHandler('message', async (data) => { if (data.event === 'new_offer' || data.event === 'offer_revoked') { - //console.log('WebSocket event received:', data.event); try { + + 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; + + await new Promise(resolve => CleanupManager.setTimeout(resolve, 100 * Math.pow(2, i))); + } + } + }; - const previousPrices = latestPrices; - - const offersResponse = await fetch(isSentOffers ? '/json/sentoffers' : '/json/offers'); - if (!offersResponse.ok) { - throw new Error(`HTTP error! status: ${offersResponse.status}`); - } - + const offersResponse = await fetchWithRetry(isSentOffers ? '/json/sentoffers' : '/json/offers'); const newData = await offersResponse.json(); const processedNewData = Array.isArray(newData) ? newData : Object.values(newData); - jsonData = formatInitialData(processedNewData); + const newFormattedData = formatInitialData(processedNewData); + + const oldOfferIds = originalJsonData.map(o => o.offer_id).sort().join(','); + const newOfferIds = newFormattedData.map(o => o.offer_id).sort().join(','); + const dataChanged = oldOfferIds !== newOfferIds; + + if (!dataChanged) { + return; + } + + jsonData = newFormattedData; originalJsonData = [...jsonData]; + const previousPrices = latestPrices; let priceData; if (window.PriceManager) { - priceData = await window.PriceManager.getPrices(true); + priceData = await window.PriceManager.getPrices(false); } else { priceData = await fetchLatestPrices(); } @@ -2553,12 +2603,10 @@ document.addEventListener('DOMContentLoaded', async function() { latestPrices = priceData; CacheManager.set('prices_coingecko', priceData, 'prices'); } else if (previousPrices) { - console.log('Using previous price data after failed refresh'); latestPrices = previousPrices; } - applyFilters(); - + applyFilters({ incremental: true, skipSkeleton: true }); updateProfitLossDisplays(); document.querySelectorAll('.usd-value').forEach(usdValue => { @@ -2569,8 +2617,14 @@ document.addEventListener('DOMContentLoaded', async function() { if (price !== undefined && price !== null) { const amount = parseFloat(usdValue.getAttribute('data-amount') || '0'); if (!isNaN(amount) && amount > 0) { - const usdValue = amount * price; - usdValue.textContent = tableRateModule.formatUSD(usdValue); + const calculatedUSD = amount * price; + const formattedUSD = calculatedUSD < 0.01 + ? calculatedUSD.toFixed(8) + ' USD' + : calculatedUSD.toFixed(2) + ' USD'; + + if (usdValue.textContent !== formattedUSD) { + usdValue.textContent = formattedUSD; + } } } } @@ -2578,7 +2632,6 @@ document.addEventListener('DOMContentLoaded', async function() { updatePaginationInfo(); - //console.log('WebSocket-triggered refresh completed successfully'); } catch (error) { console.error('Error during WebSocket-triggered refresh:', error); NetworkManager.handleNetworkError(error); @@ -2613,9 +2666,7 @@ document.addEventListener('DOMContentLoaded', async function() { }); } - if (window.config.autoRefreshEnabled) { - startAutoRefresh(); - } + startAutoRefresh(); const filterForm = document.getElementById('filterForm'); if (filterForm) { @@ -2649,20 +2700,10 @@ document.addEventListener('DOMContentLoaded', async function() { } }); - const rowTimeInterval = setInterval(updateRowTimes, 30000); - if (CleanupManager.registerResource) { - CleanupManager.registerResource('rowTimeInterval', rowTimeInterval, () => { - clearInterval(rowTimeInterval); - }); - } else if (CleanupManager.addResource) { - CleanupManager.addResource('rowTimeInterval', rowTimeInterval, () => { - clearInterval(rowTimeInterval); - }); - } else { - - window._cleanupIntervals = window._cleanupIntervals || []; - window._cleanupIntervals.push(rowTimeInterval); - } + const rowTimeInterval = CleanupManager.setInterval(updateRowTimes, 30000); + CleanupManager.registerResource('rowTimeInterval', rowTimeInterval, () => { + clearInterval(rowTimeInterval); + }); } catch (error) { console.error('Error during initialization:', error); @@ -2694,6 +2735,8 @@ function cleanup() { window.countdownInterval = null; } + stopAutoRefresh(); + if (window._cleanupIntervals && Array.isArray(window._cleanupIntervals)) { window._cleanupIntervals.forEach(interval => { clearInterval(interval); @@ -2739,7 +2782,6 @@ function cleanup() { } } - //console.log('Offers.js cleanup completed'); } catch (error) { console.error('Error during cleanup:', error); } diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pages/offers-pricechart.js similarity index 93% rename from basicswap/static/js/pricechart.js rename to basicswap/static/js/pages/offers-pricechart.js index 96a0555..f1a3c27 100644 --- a/basicswap/static/js/pricechart.js +++ b/basicswap/static/js/pages/offers-pricechart.js @@ -2,46 +2,6 @@ const chartConfig = window.config.chartConfig; const coins = window.config.coins; const apiKeys = window.config.getAPIKeys(); -const utils = { - formatNumber: (number, decimals = 2) => { - if (typeof number !== 'number' || isNaN(number)) { - return '0'; - } - - try { - return new Intl.NumberFormat('en-US', { - minimumFractionDigits: decimals, - maximumFractionDigits: decimals - }).format(number); - } catch (e) { - return '0'; - } - }, - formatDate: (timestamp, resolution) => { - const date = new Date(timestamp); - const options = { - day: { hour: '2-digit', minute: '2-digit', hour12: true }, - week: { month: 'short', day: 'numeric' }, - month: { year: 'numeric', month: 'short', day: 'numeric' } - }; - return date.toLocaleString('en-US', { ...options[resolution], timeZone: 'UTC' }); - }, - debounce: (func, delay) => { - let timeoutId; - return (...args) => { - clearTimeout(timeoutId); - timeoutId = setTimeout(() => func(...args), delay); - }; - } -}; - -class AppError extends Error { - constructor(message, type = 'AppError') { - super(message); - this.name = type; - } -} - const logger = { log: (message) => console.log(`[AppLog] ${new Date().toISOString()}: ${message}`), warn: (message) => console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`), @@ -94,29 +54,6 @@ const api = { } }, - fetchCryptoCompareDataXHR: (coin) => { - try { - if (!NetworkManager.isOnline()) { - throw new Error('Network is offline'); - } - - return Api.fetchCryptoCompareData(coin, { - cryptoCompare: apiKeys.cryptoCompare - }); - } catch (error) { - logger.error(`CryptoCompare request failed for ${coin}:`, error); - - NetworkManager.handleNetworkError(error); - - const cachedData = CacheManager.get(`coinData_${coin}`); - if (cachedData) { - logger.info(`Using cached data for ${coin}`); - return cachedData.value; - } - return { error: error.message }; - } - }, - fetchCoinGeckoDataXHR: async () => { try { const priceData = await window.PriceManager.getPrices(); @@ -242,7 +179,7 @@ const rateLimiter = { 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 { @@ -252,7 +189,7 @@ const rateLimiter = { if (error.message.includes('429') && retryCount < this.retryDelays.length) { const delay = this.retryDelays[retryCount]; console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`); - await new Promise(resolve => setTimeout(resolve, delay)); + await new Promise(resolve => CleanupManager.setTimeout(resolve, delay)); return this.queueRequest(apiName, requestFn, retryCount + 1); } @@ -260,7 +197,7 @@ const rateLimiter = { retryCount < this.retryDelays.length) { const delay = this.retryDelays[retryCount]; logger.warn(`Request failed, retrying in ${delay/1000} seconds...`); - await new Promise(resolve => setTimeout(resolve, delay)); + await new Promise(resolve => CleanupManager.setTimeout(resolve, delay)); return this.queueRequest(apiName, requestFn, retryCount + 1); } @@ -303,7 +240,7 @@ const ui = { if (isError || volume24h === null || volume24h === undefined) { volumeElement.textContent = 'N/A'; } else { - volumeElement.textContent = `${utils.formatNumber(volume24h, 0)} USD`; + volumeElement.textContent = `${window.config.utils.formatNumber(volume24h, 0)} USD`; } volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none'; } @@ -345,7 +282,7 @@ const ui = { } priceChange1d = data.price_change_percentage_24h || 0; - volume24h = data.total_volume || 0; + volume24h = (data.total_volume !== undefined && data.total_volume !== null) ? data.total_volume : null; if (isNaN(priceUSD) || isNaN(priceBTC)) { throw new Error(`Invalid numeric values in data for ${coin}`); } @@ -498,7 +435,7 @@ const ui = { chartContainer.classList.add('blurred'); if (duration > 0) { - setTimeout(() => { + CleanupManager.setTimeout(() => { ui.hideErrorMessage(); }, duration); } @@ -1199,11 +1136,11 @@ const app = { if (coinData) { coinData.displayName = coin.displayName || coin.symbol; - const backendId = getCoinBackendId ? getCoinBackendId(coin.name) : coin.name; - if (volumeData[backendId]) { - coinData.total_volume = volumeData[backendId].total_volume; - if (!coinData.price_change_percentage_24h && volumeData[backendId].price_change_percentage_24h) { - coinData.price_change_percentage_24h = volumeData[backendId].price_change_percentage_24h; + const volumeKey = coin.symbol.toLowerCase(); + if (volumeData[volumeKey]) { + coinData.total_volume = volumeData[volumeKey].total_volume; + if (!coinData.price_change_percentage_24h && volumeData[volumeKey].price_change_percentage_24h) { + coinData.price_change_percentage_24h = volumeData[volumeKey].price_change_percentage_24h; } } @@ -1231,11 +1168,7 @@ const app = { } else { try { ui.showCoinLoader(coin.symbol); - if (coin.usesCoinGecko) { - data = await api.fetchCoinGeckoDataXHR(coin.symbol); - } else { - data = await api.fetchCryptoCompareDataXHR(coin.symbol); - } + data = await api.fetchCoinGeckoDataXHR(coin.symbol); if (data.error) { throw new Error(data.error); } @@ -1382,7 +1315,7 @@ const app = { } const timeUntilRefresh = nextRefreshTime - now; app.nextRefreshTime = nextRefreshTime; - app.autoRefreshInterval = setTimeout(() => { + app.autoRefreshInterval = CleanupManager.setTimeout(() => { if (NetworkManager.isOnline()) { app.refreshAllData(); } else { @@ -1394,8 +1327,7 @@ const app = { }, refreshAllData: async function() { - //console.log('Price refresh started at', new Date().toLocaleTimeString()); - + if (app.isRefreshing) { console.log('Refresh already in progress, skipping...'); return; @@ -1415,7 +1347,7 @@ refreshAllData: async function() { ui.displayErrorMessage(`Rate limit: Please wait ${seconds} seconds before refreshing`); let remainingTime = seconds; - const countdownInterval = setInterval(() => { + const countdownInterval = CleanupManager.setInterval(() => { remainingTime--; if (remainingTime > 0) { ui.displayErrorMessage(`Rate limit: Please wait ${remainingTime} seconds before refreshing`); @@ -1428,7 +1360,6 @@ refreshAllData: async function() { return; } - //console.log('Starting refresh of all data...'); app.isRefreshing = true; app.updateNextRefreshTime(); ui.showLoader(); @@ -1443,7 +1374,7 @@ refreshAllData: async function() { console.warn('BTC price update failed, continuing with cached or default value'); } - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise(resolve => CleanupManager.setTimeout(resolve, 1000)); const allCoinData = await api.fetchCoinGeckoDataXHR(); if (allCoinData.error) { @@ -1468,11 +1399,11 @@ refreshAllData: async function() { coinData.displayName = coin.displayName || coin.symbol; - const backendId = getCoinBackendId ? getCoinBackendId(coin.name) : coin.name; - if (volumeData[backendId]) { - coinData.total_volume = volumeData[backendId].total_volume; - if (!coinData.price_change_percentage_24h && volumeData[backendId].price_change_percentage_24h) { - coinData.price_change_percentage_24h = volumeData[backendId].price_change_percentage_24h; + const volumeKey = coin.symbol.toLowerCase(); + if (volumeData[volumeKey]) { + coinData.total_volume = volumeData[volumeKey].total_volume; + if (!coinData.price_change_percentage_24h && volumeData[volumeKey].price_change_percentage_24h) { + coinData.price_change_percentage_24h = volumeData[volumeKey].price_change_percentage_24h; } } else { try { @@ -1495,15 +1426,13 @@ refreshAllData: async function() { const cacheKey = `coinData_${coin.symbol}`; CacheManager.set(cacheKey, coinData, 'prices'); - //console.log(`Updated price for ${coin.symbol}: $${coinData.current_price}`); - } catch (coinError) { console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`); failedCoins.push(coin.symbol); } } - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise(resolve => CleanupManager.setTimeout(resolve, 1000)); if (chartModule.currentCoin) { try { @@ -1525,7 +1454,7 @@ refreshAllData: async function() { let countdown = 5; ui.displayErrorMessage(`${failureMessage} (${countdown}s)`); - const countdownInterval = setInterval(() => { + const countdownInterval = CleanupManager.setInterval(() => { countdown--; if (countdown > 0) { ui.displayErrorMessage(`${failureMessage} (${countdown}s)`); @@ -1535,8 +1464,7 @@ refreshAllData: async function() { } }, 1000); } - //console.log(`Price refresh completed at ${new Date().toLocaleTimeString()}. Updated ${window.config.coins.length - failedCoins.length}/${window.config.coins.length} coins.`); - + } catch (error) { console.error('Critical error during refresh:', error); NetworkManager.handleNetworkError(error); @@ -1544,7 +1472,7 @@ refreshAllData: async function() { let countdown = 10; ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`); - const countdownInterval = setInterval(() => { + const countdownInterval = CleanupManager.setInterval(() => { countdown--; if (countdown > 0) { ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`); @@ -1566,7 +1494,6 @@ refreshAllData: async function() { app.scheduleNextRefresh(); } - //console.log(`Refresh process finished at ${new Date().toLocaleTimeString()}, next refresh scheduled: ${app.isAutoRefreshEnabled ? 'yes' : 'no'}`); } }, @@ -1590,7 +1517,7 @@ refreshAllData: async function() { const svg = document.querySelector('#toggle-auto-refresh svg'); if (svg) { svg.classList.add('animate-spin'); - setTimeout(() => { + CleanupManager.setTimeout(() => { svg.classList.remove('animate-spin'); }, 2000); } diff --git a/basicswap/static/js/pages/settings-page.js b/basicswap/static/js/pages/settings-page.js new file mode 100644 index 0000000..4ef246e --- /dev/null +++ b/basicswap/static/js/pages/settings-page.js @@ -0,0 +1,332 @@ + +(function() { + 'use strict'; + + const SettingsPage = { + confirmCallback: null, + triggerElement: null, + + init: function() { + this.setupTabs(); + this.setupCoinHeaders(); + this.setupConfirmModal(); + this.setupNotificationSettings(); + }, + + setupTabs: function() { + const tabButtons = document.querySelectorAll('.tab-button'); + const tabContents = document.querySelectorAll('.tab-content'); + + const switchTab = (targetTab) => { + tabButtons.forEach(btn => { + if (btn.dataset.tab === targetTab) { + btn.className = 'tab-button border-b-2 border-blue-500 text-blue-600 dark:text-blue-400 py-4 px-1 text-sm font-medium focus:outline-none focus:ring-0'; + } else { + btn.className = 'tab-button border-b-2 border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600 py-4 px-1 text-sm font-medium focus:outline-none focus:ring-0'; + } + }); + + tabContents.forEach(content => { + if (content.id === targetTab) { + content.classList.remove('hidden'); + } else { + content.classList.add('hidden'); + } + }); + }; + + tabButtons.forEach(btn => { + btn.addEventListener('click', () => { + switchTab(btn.dataset.tab); + }); + }); + }, + + setupCoinHeaders: function() { + const coinHeaders = document.querySelectorAll('.coin-header'); + coinHeaders.forEach(header => { + header.addEventListener('click', function() { + const coinName = this.dataset.coin; + const details = document.getElementById(`details-${coinName}`); + const arrow = this.querySelector('.toggle-arrow'); + + if (details.classList.contains('hidden')) { + details.classList.remove('hidden'); + arrow.style.transform = 'rotate(180deg)'; + } else { + details.classList.add('hidden'); + arrow.style.transform = 'rotate(0deg)'; + } + }); + }); + }, + + setupConfirmModal: function() { + const confirmYesBtn = document.getElementById('confirmYes'); + if (confirmYesBtn) { + confirmYesBtn.addEventListener('click', () => { + if (typeof this.confirmCallback === 'function') { + this.confirmCallback(); + } + this.hideConfirmDialog(); + }); + } + + const confirmNoBtn = document.getElementById('confirmNo'); + if (confirmNoBtn) { + confirmNoBtn.addEventListener('click', () => { + this.hideConfirmDialog(); + }); + } + }, + + showConfirmDialog: function(title, message, callback) { + this.confirmCallback = callback; + document.getElementById('confirmTitle').textContent = title; + document.getElementById('confirmMessage').textContent = message; + const modal = document.getElementById('confirmModal'); + if (modal) { + modal.classList.remove('hidden'); + } + return false; + }, + + hideConfirmDialog: function() { + const modal = document.getElementById('confirmModal'); + if (modal) { + modal.classList.add('hidden'); + } + this.confirmCallback = null; + return false; + }, + + confirmDisableCoin: function() { + this.triggerElement = document.activeElement; + return this.showConfirmDialog( + "Confirm Disable Coin", + "Are you sure you want to disable this coin?", + () => { + if (this.triggerElement) { + const form = this.triggerElement.form; + const hiddenInput = document.createElement('input'); + hiddenInput.type = 'hidden'; + hiddenInput.name = this.triggerElement.name; + hiddenInput.value = this.triggerElement.value; + form.appendChild(hiddenInput); + form.submit(); + } + } + ); + }, + + setupNotificationSettings: function() { + const notificationsTab = document.getElementById('notifications-tab'); + if (notificationsTab) { + notificationsTab.addEventListener('click', () => { + CleanupManager.setTimeout(() => this.syncNotificationSettings(), 100); + }); + } + + document.addEventListener('change', (e) => { + if (e.target.closest('#notifications')) { + this.syncNotificationSettings(); + } + }); + + this.syncNotificationSettings(); + }, + + syncNotificationSettings: function() { + if (window.NotificationManager && typeof window.NotificationManager.updateSettings === 'function') { + const backendSettings = { + showNewOffers: document.getElementById('notifications_new_offers')?.checked || false, + showNewBids: document.getElementById('notifications_new_bids')?.checked || false, + showBidAccepted: document.getElementById('notifications_bid_accepted')?.checked || false, + showBalanceChanges: document.getElementById('notifications_balance_changes')?.checked || false, + showOutgoingTransactions: document.getElementById('notifications_outgoing_transactions')?.checked || false, + showSwapCompleted: document.getElementById('notifications_swap_completed')?.checked || false, + showUpdateNotifications: document.getElementById('check_updates')?.checked || false, + notificationDuration: parseInt(document.getElementById('notifications_duration')?.value || '5') * 1000 + }; + + window.NotificationManager.updateSettings(backendSettings); + } + }, + + testUpdateNotification: function() { + if (window.NotificationManager) { + window.NotificationManager.createToast( + 'Update Available: v0.15.0', + 'update_available', + { + subtitle: 'Current: v0.14.6 • Click to view release (Test/Dummy)', + 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.' + } + ); + } + }, + + testLiveUpdateCheck: function(event) { + const button = event?.target || event?.currentTarget || document.querySelector('[onclick*="testLiveUpdateCheck"]'); + if (!button) return; + + const originalText = button.textContent; + button.textContent = 'Checking...'; + button.disabled = true; + + fetch('/json/checkupdates', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (window.NotificationManager) { + const currentVer = data.current_version || 'Unknown'; + const latestVer = data.latest_version || currentVer; + + if (data.update_available) { + window.NotificationManager.createToast( + `Live Update Available: v${latestVer}`, + 'update_available', + { + latest_version: latestVer, + current_version: currentVer, + subtitle: `Current: v${currentVer} • Click to view release`, + releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${latestVer}`, + releaseNotes: 'This is a real update check from GitHub API.' + } + ); + } else { + window.NotificationManager.createToast( + 'No Updates Available', + 'success', + { + subtitle: `Current version v${currentVer} is up to date` + } + ); + } + } + }) + .catch(error => { + console.error('Update check failed:', error); + if (window.NotificationManager) { + window.NotificationManager.createToast( + 'Update Check Failed', + 'error', + { + subtitle: 'Could not check for updates. See console for details.' + } + ); + } + }) + .finally(() => { + if (button) { + button.textContent = originalText; + button.disabled = false; + } + }); + }, + + checkForUpdatesNow: function(event) { + const button = event?.target || event?.currentTarget || document.querySelector('[data-check-updates]'); + if (!button) return; + + const originalText = button.textContent; + button.textContent = 'Checking...'; + button.disabled = true; + + fetch('/json/checkupdates', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + if (window.NotificationManager) { + window.NotificationManager.createToast( + 'Update Check Failed', + 'error', + { + subtitle: data.error + } + ); + } + return; + } + + if (window.NotificationManager) { + const currentVer = data.current_version || 'Unknown'; + const latestVer = data.latest_version || currentVer; + + if (data.update_available) { + window.NotificationManager.createToast( + `Update Available: v${latestVer}`, + 'update_available', + { + latest_version: latestVer, + current_version: currentVer, + 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.` + } + ); + } else { + window.NotificationManager.createToast( + 'You\'re Up to Date!', + 'success', + { + subtitle: `Current version v${currentVer} is the latest` + } + ); + } + } + }) + .catch(error => { + console.error('Update check failed:', error); + if (window.NotificationManager) { + window.NotificationManager.createToast( + 'Update Check Failed', + 'error', + { + subtitle: 'Network error. Please try again later.' + } + ); + } + }) + .finally(() => { + if (button) { + button.textContent = originalText; + button.disabled = false; + } + }); + } + }; + + SettingsPage.cleanup = function() { + }; + + document.addEventListener('DOMContentLoaded', function() { + SettingsPage.init(); + + if (window.CleanupManager) { + CleanupManager.registerResource('settingsPage', SettingsPage, (page) => { + if (page.cleanup) page.cleanup(); + }); + } + }); + + window.SettingsPage = SettingsPage; + window.syncNotificationSettings = SettingsPage.syncNotificationSettings.bind(SettingsPage); + window.testUpdateNotification = SettingsPage.testUpdateNotification.bind(SettingsPage); + window.testLiveUpdateCheck = SettingsPage.testLiveUpdateCheck.bind(SettingsPage); + window.checkForUpdatesNow = SettingsPage.checkForUpdatesNow.bind(SettingsPage); + window.showConfirmDialog = SettingsPage.showConfirmDialog.bind(SettingsPage); + window.hideConfirmDialog = SettingsPage.hideConfirmDialog.bind(SettingsPage); + window.confirmDisableCoin = SettingsPage.confirmDisableCoin.bind(SettingsPage); + +})(); diff --git a/basicswap/static/js/swaps_in_progress.js b/basicswap/static/js/pages/swaps-page.js similarity index 97% rename from basicswap/static/js/swaps_in_progress.js rename to basicswap/static/js/pages/swaps-page.js index 3637dad..a825331 100644 --- a/basicswap/static/js/swaps_in_progress.js +++ b/basicswap/static/js/pages/swaps-page.js @@ -127,9 +127,9 @@ const getTimeStrokeColor = (expireTime) => { const now = Math.floor(Date.now() / 1000); const timeLeft = expireTime - now; - if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less - if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less - return '#10B981'; // More than 30 minutes + if (timeLeft <= 300) return '#9CA3AF'; + if (timeLeft <= 1800) return '#3B82F6'; + return '#10B981'; }; const updateConnectionStatus = (status) => { @@ -520,8 +520,6 @@ const createSwapTableRow = async (swap) => { async function updateSwapsTable(options = {}) { const { resetPage = false, refreshData = true } = options; - //console.log('Updating swaps table:', { resetPage, refreshData }); - if (state.refreshPromise) { await state.refreshPromise; return; @@ -547,19 +545,17 @@ async function updateSwapsTable(options = {}) { } const data = await response.json(); - //console.log('Received swap data:', data); - + state.swapsData = Array.isArray(data) ? data.filter(swap => { const isActive = isActiveSwap(swap); - //console.log(`Swap ${swap.bid_id}: ${isActive ? 'Active' : 'Inactive'}`, swap.bid_state); + return isActive; }) : []; - //console.log('Filtered active swaps:', state.swapsData); } catch (error) { - //console.error('Error fetching swap data:', error); + state.swapsData = []; } finally { state.refreshPromise = null; @@ -585,8 +581,6 @@ async function updateSwapsTable(options = {}) { const endIndex = startIndex + PAGE_SIZE; const currentPageSwaps = state.swapsData.slice(startIndex, endIndex); - //console.log('Current page swaps:', currentPageSwaps); - if (elements.swapsBody) { if (currentPageSwaps.length > 0) { const rowPromises = currentPageSwaps.map(swap => createSwapTableRow(swap)); @@ -607,7 +601,7 @@ async function updateSwapsTable(options = {}) { }); } } else { - //console.log('No active swaps found, displaying empty state'); + elements.swapsBody.innerHTML = ` @@ -679,7 +673,12 @@ document.addEventListener('DOMContentLoaded', async () => { WebSocketManager.initialize(); setupEventListeners(); await updateSwapsTable({ resetPage: true, refreshData: true }); - const autoRefreshInterval = setInterval(async () => { + + const autoRefreshInterval = CleanupManager.setInterval(async () => { await updateSwapsTable({ resetPage: false, refreshData: true }); - }, 10000); // 30 seconds + }, 10000); + + CleanupManager.registerResource('swapsAutoRefresh', autoRefreshInterval, () => { + clearInterval(autoRefreshInterval); + }); }); diff --git a/basicswap/static/js/pages/wallet-page.js b/basicswap/static/js/pages/wallet-page.js new file mode 100644 index 0000000..4aae02d --- /dev/null +++ b/basicswap/static/js/pages/wallet-page.js @@ -0,0 +1,372 @@ +(function() { + 'use strict'; + + const WalletPage = { + confirmCallback: null, + triggerElement: null, + currentCoinId: '', + activeTooltip: null, + + init: function() { + this.setupAddressCopy(); + this.setupConfirmModal(); + this.setupWithdrawalConfirmation(); + this.setupTransactionDisplay(); + this.setupWebSocketUpdates(); + }, + + setupAddressCopy: function() { + const copyableElements = [ + 'main_deposit_address', + 'monero_main_address', + 'monero_sub_address', + 'stealth_address' + ]; + + copyableElements.forEach(id => { + const element = document.getElementById(id); + if (!element) return; + + element.classList.add('cursor-pointer', 'hover:bg-gray-100', 'dark:hover:bg-gray-600', 'transition-colors'); + + if (!element.querySelector('.copy-icon')) { + const copyIcon = document.createElement('span'); + copyIcon.className = 'copy-icon absolute right-2 inset-y-0 flex items-center text-gray-500 dark:text-gray-300'; + copyIcon.innerHTML = ` + + `; + + element.style.position = 'relative'; + element.style.paddingRight = '2.5rem'; + element.appendChild(copyIcon); + } + + element.addEventListener('click', (e) => { + const textToCopy = element.innerText.trim(); + + this.copyToClipboard(textToCopy); + + element.classList.add('bg-blue-50', 'dark:bg-blue-900'); + + this.showCopyFeedback(element); + + CleanupManager.setTimeout(() => { + element.classList.remove('bg-blue-50', 'dark:bg-blue-900'); + }, 1000); + }); + }); + }, + + copyToClipboard: function(text) { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(() => { + console.log('Address copied to clipboard'); + }).catch(err => { + console.error('Failed to copy address:', err); + this.fallbackCopyToClipboard(text); + }); + } else { + this.fallbackCopyToClipboard(text); + } + }, + + fallbackCopyToClipboard: function(text) { + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + textArea.style.top = '-999999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand('copy'); + console.log('Address copied to clipboard (fallback)'); + } catch (err) { + console.error('Fallback: Failed to copy address', err); + } + + document.body.removeChild(textArea); + }, + + showCopyFeedback: function(element) { + if (this.activeTooltip && this.activeTooltip.parentNode) { + this.activeTooltip.parentNode.removeChild(this.activeTooltip); + } + + const popup = document.createElement('div'); + popup.className = 'copy-feedback-popup fixed z-50 bg-blue-600 text-white text-sm py-2 px-3 rounded-md shadow-lg'; + popup.innerText = 'Copied!'; + document.body.appendChild(popup); + + this.activeTooltip = popup; + + this.updateTooltipPosition(popup, element); + + const scrollHandler = () => { + if (popup.parentNode) { + requestAnimationFrame(() => { + this.updateTooltipPosition(popup, element); + }); + } + }; + + window.addEventListener('scroll', scrollHandler, { passive: true }); + + popup.style.opacity = '0'; + popup.style.transition = 'opacity 0.2s ease-in-out'; + + CleanupManager.setTimeout(() => { + popup.style.opacity = '1'; + }, 10); + + CleanupManager.setTimeout(() => { + window.removeEventListener('scroll', scrollHandler); + popup.style.opacity = '0'; + + CleanupManager.setTimeout(() => { + if (popup.parentNode) { + popup.parentNode.removeChild(popup); + } + if (this.activeTooltip === popup) { + this.activeTooltip = null; + } + }, 200); + }, 1500); + }, + + updateTooltipPosition: function(tooltip, element) { + const rect = element.getBoundingClientRect(); + + let top = rect.top - tooltip.offsetHeight - 8; + const left = rect.left + rect.width / 2; + + if (top < 10) { + top = rect.bottom + 8; + } + + tooltip.style.top = `${top}px`; + tooltip.style.left = `${left}px`; + tooltip.style.transform = 'translateX(-50%)'; + }, + + setupWithdrawalConfirmation: function() { + + const withdrawalClickHandler = (e) => { + const target = e.target.closest('[data-confirm-withdrawal]'); + if (target) { + e.preventDefault(); + + this.triggerElement = target; + + this.confirmWithdrawal().catch(() => { + + }); + } + }; + + document.addEventListener('click', withdrawalClickHandler); + + if (window.CleanupManager) { + CleanupManager.registerResource('walletWithdrawalClick', withdrawalClickHandler, () => { + document.removeEventListener('click', withdrawalClickHandler); + }); + } + }, + + setupConfirmModal: function() { + const confirmYesBtn = document.getElementById('confirmYes'); + if (confirmYesBtn) { + confirmYesBtn.addEventListener('click', () => { + if (this.confirmCallback && typeof this.confirmCallback === 'function') { + this.confirmCallback(); + } + this.hideConfirmDialog(); + }); + } + + const confirmNoBtn = document.getElementById('confirmNo'); + if (confirmNoBtn) { + confirmNoBtn.addEventListener('click', () => { + this.hideConfirmDialog(); + }); + } + + const confirmModal = document.getElementById('confirmModal'); + if (confirmModal) { + confirmModal.addEventListener('click', (e) => { + if (e.target === confirmModal) { + this.hideConfirmDialog(); + } + }); + } + }, + + showConfirmDialog: function(title, message, callback) { + return new Promise((resolve, reject) => { + this.confirmCallback = () => { + if (callback) callback(); + resolve(); + }; + this.confirmReject = reject; + + document.getElementById('confirmTitle').textContent = title; + document.getElementById('confirmMessage').textContent = message; + const modal = document.getElementById('confirmModal'); + if (modal) { + modal.classList.remove('hidden'); + } + }); + }, + + hideConfirmDialog: function() { + const modal = document.getElementById('confirmModal'); + if (modal) { + modal.classList.add('hidden'); + } + if (this.confirmReject) { + this.confirmReject(); + } + this.confirmCallback = null; + this.confirmReject = null; + return false; + }, + + confirmReseed: function() { + this.triggerElement = document.activeElement; + return this.showConfirmDialog( + "Confirm Reseed Wallet", + "Are you sure?\nBackup your wallet before and after.\nWon't detect used keys.\nShould only be used for new wallets.", + () => { + if (this.triggerElement) { + const form = this.triggerElement.form; + const hiddenInput = document.createElement('input'); + hiddenInput.type = 'hidden'; + hiddenInput.name = this.triggerElement.name; + hiddenInput.value = this.triggerElement.value; + form.appendChild(hiddenInput); + form.submit(); + } + } + ); + }, + + confirmWithdrawal: function() { + this.triggerElement = document.activeElement; + return this.showConfirmDialog( + "Confirm Withdrawal", + "Are you sure you want to proceed with this withdrawal?", + () => { + if (this.triggerElement) { + const form = this.triggerElement.form; + const hiddenInput = document.createElement('input'); + hiddenInput.type = 'hidden'; + hiddenInput.name = this.triggerElement.name; + hiddenInput.value = this.triggerElement.value; + form.appendChild(hiddenInput); + form.submit(); + } + } + ); + }, + + confirmCreateUTXO: function() { + this.triggerElement = document.activeElement; + return this.showConfirmDialog( + "Confirm Create UTXO", + "Are you sure you want to create this UTXO?", + () => { + if (this.triggerElement) { + const form = this.triggerElement.form; + const hiddenInput = document.createElement('input'); + hiddenInput.type = 'hidden'; + hiddenInput.name = this.triggerElement.name; + hiddenInput.value = this.triggerElement.value; + form.appendChild(hiddenInput); + form.submit(); + } + } + ); + }, + + confirmUTXOResize: function() { + this.triggerElement = document.activeElement; + return this.showConfirmDialog( + "Confirm UTXO Resize", + "Are you sure you want to resize UTXOs?", + () => { + if (this.triggerElement) { + const form = this.triggerElement.form; + const hiddenInput = document.createElement('input'); + hiddenInput.type = 'hidden'; + hiddenInput.name = this.triggerElement.name; + hiddenInput.value = this.triggerElement.value; + form.appendChild(hiddenInput); + form.submit(); + } + } + ); + }, + + setupTransactionDisplay: function() { + + }, + + setupWebSocketUpdates: function() { + if (window.BalanceUpdatesManager) { + const coinId = this.getCoinIdFromPage(); + if (coinId) { + this.currentCoinId = coinId; + window.BalanceUpdatesManager.setup({ + contextKey: 'wallet_' + coinId, + balanceUpdateCallback: this.handleBalanceUpdate.bind(this), + swapEventCallback: this.handleSwapEvent.bind(this), + errorContext: 'Wallet', + enablePeriodicRefresh: true, + periodicInterval: 60000 + }); + } + } + }, + + getCoinIdFromPage: function() { + const pathParts = window.location.pathname.split('/'); + const walletIndex = pathParts.indexOf('wallet'); + if (walletIndex !== -1 && pathParts[walletIndex + 1]) { + return pathParts[walletIndex + 1]; + } + return null; + }, + + handleBalanceUpdate: function(balanceData) { + + console.log('Balance updated:', balanceData); + }, + + handleSwapEvent: function(eventData) { + + console.log('Swap event:', eventData); + } + }; + + document.addEventListener('DOMContentLoaded', function() { + WalletPage.init(); + + if (window.BalanceUpdatesManager) { + window.BalanceUpdatesManager.initialize(); + } + }); + + window.WalletPage = WalletPage; + window.setupAddressCopy = WalletPage.setupAddressCopy.bind(WalletPage); + window.showConfirmDialog = WalletPage.showConfirmDialog.bind(WalletPage); + window.hideConfirmDialog = WalletPage.hideConfirmDialog.bind(WalletPage); + window.confirmReseed = WalletPage.confirmReseed.bind(WalletPage); + window.confirmWithdrawal = WalletPage.confirmWithdrawal.bind(WalletPage); + window.confirmCreateUTXO = WalletPage.confirmCreateUTXO.bind(WalletPage); + window.confirmUTXOResize = WalletPage.confirmUTXOResize.bind(WalletPage); + window.copyToClipboard = WalletPage.copyToClipboard.bind(WalletPage); + window.showCopyFeedback = WalletPage.showCopyFeedback.bind(WalletPage); + +})(); diff --git a/basicswap/static/js/pages/wallets-page.js b/basicswap/static/js/pages/wallets-page.js new file mode 100644 index 0000000..65fe86d --- /dev/null +++ b/basicswap/static/js/pages/wallets-page.js @@ -0,0 +1,344 @@ +(function() { + 'use strict'; + + const WalletsPage = { + + init: function() { + this.setupWebSocketUpdates(); + }, + + setupWebSocketUpdates: function() { + if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') { + window.WebSocketManager.initialize(); + } + + if (window.BalanceUpdatesManager) { + window.BalanceUpdatesManager.setup({ + contextKey: 'wallets', + balanceUpdateCallback: this.updateWalletBalances.bind(this), + swapEventCallback: this.updateWalletBalances.bind(this), + errorContext: 'Wallets', + enablePeriodicRefresh: true, + periodicInterval: 60000 + }); + + if (window.WebSocketManager && typeof window.WebSocketManager.addMessageHandler === 'function') { + const priceHandlerId = window.WebSocketManager.addMessageHandler('message', (data) => { + if (data && data.event) { + if (data.event === 'price_updated' || data.event === 'prices_updated') { + clearTimeout(window.walletsPriceUpdateTimeout); + window.walletsPriceUpdateTimeout = CleanupManager.setTimeout(() => { + if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') { + window.WalletManager.updatePrices(true); + } + }, 500); + } + } + }); + window.walletsPriceHandlerId = priceHandlerId; + } + } + }, + + updateWalletBalances: function(balanceData) { + if (balanceData) { + balanceData.forEach(coin => { + this.updateWalletDisplay(coin); + }); + + CleanupManager.setTimeout(() => { + if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') { + window.WalletManager.updatePrices(true); + } + }, 250); + } else { + window.BalanceUpdatesManager.fetchBalanceData() + .then(data => this.updateWalletBalances(data)) + .catch(error => { + console.error('Error updating wallet balances:', error); + }); + } + }, + + updateWalletDisplay: function(coinData) { + if (coinData.name === 'Particl') { + this.updateSpecificBalance('Particl', 'Balance:', coinData.balance, coinData.ticker || 'PART'); + } else if (coinData.name === 'Particl Anon') { + this.updateSpecificBalance('Particl', 'Anon Balance:', coinData.balance, coinData.ticker || 'PART'); + this.removePendingBalance('Particl', 'Anon Balance:'); + if (coinData.pending && parseFloat(coinData.pending) > 0) { + this.updatePendingBalance('Particl', 'Anon Balance:', coinData.pending, coinData.ticker || 'PART', 'Anon Pending:', coinData); + } + } else if (coinData.name === 'Particl Blind') { + this.updateSpecificBalance('Particl', 'Blind Balance:', coinData.balance, coinData.ticker || 'PART'); + this.removePendingBalance('Particl', 'Blind Balance:'); + if (coinData.pending && parseFloat(coinData.pending) > 0) { + this.updatePendingBalance('Particl', 'Blind Balance:', coinData.pending, coinData.ticker || 'PART', 'Blind Unconfirmed:', coinData); + } + } else { + this.updateSpecificBalance(coinData.name, 'Balance:', coinData.balance, coinData.ticker || coinData.name); + + if (coinData.name !== 'Particl Anon' && coinData.name !== 'Particl Blind' && coinData.name !== 'Litecoin MWEB') { + if (coinData.pending && parseFloat(coinData.pending) > 0) { + this.updatePendingDisplay(coinData); + } else { + this.removePendingDisplay(coinData.name); + } + } + } + }, + + updateSpecificBalance: function(coinName, labelText, balance, ticker, isPending = false) { + const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]'); + + balanceElements.forEach(element => { + const elementCoinName = element.getAttribute('data-coinname'); + + if (elementCoinName === coinName) { + const parentDiv = element.closest('.flex.mb-2.justify-between.items-center'); + const labelElement = parentDiv ? parentDiv.querySelector('h4') : null; + + if (labelElement) { + const currentLabel = labelElement.textContent.trim(); + + if (currentLabel === labelText) { + if (isPending) { + const cleanBalance = balance.toString().replace(/^\+/, ''); + element.textContent = `+${cleanBalance} ${ticker}`; + } else { + element.textContent = `${balance} ${ticker}`; + } + } + } + } + }); + }, + + updatePendingDisplay: function(coinData) { + const walletContainer = this.findWalletContainer(coinData.name); + if (!walletContainer) return; + + const existingPendingElements = walletContainer.querySelectorAll('.flex.mb-2.justify-between.items-center'); + let staticPendingElement = null; + let staticUsdElement = null; + + existingPendingElements.forEach(element => { + const labelElement = element.querySelector('h4'); + if (labelElement) { + const labelText = labelElement.textContent; + if (labelText.includes('Pending:') && !labelText.includes('USD')) { + staticPendingElement = element; + } else if (labelText.includes('Pending USD value:')) { + staticUsdElement = element; + } + } + }); + + if (staticPendingElement && staticUsdElement) { + const pendingSpan = staticPendingElement.querySelector('.coinname-value'); + if (pendingSpan) { + const cleanPending = coinData.pending.toString().replace(/^\+/, ''); + pendingSpan.textContent = `+${cleanPending} ${coinData.ticker || coinData.name}`; + } + + let initialUSD = '$0.00'; + if (window.WalletManager && window.WalletManager.coinPrices) { + const coinId = coinData.name.toLowerCase().replace(' ', '-'); + const price = window.WalletManager.coinPrices[coinId]; + if (price && price.usd) { + const cleanPending = coinData.pending.toString().replace(/^\+/, ''); + const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2); + initialUSD = `$${usdValue}`; + } + } + + const usdDiv = staticUsdElement.querySelector('.usd-value'); + if (usdDiv) { + usdDiv.textContent = initialUSD; + } + return; + } + + let pendingContainer = walletContainer.querySelector('.pending-container'); + + if (!pendingContainer) { + const balanceContainer = walletContainer.querySelector('.flex.mb-2.justify-between.items-center'); + if (!balanceContainer) return; + + pendingContainer = document.createElement('div'); + pendingContainer.className = 'pending-container'; + balanceContainer.parentNode.insertBefore(pendingContainer, balanceContainer.nextSibling); + } + + pendingContainer.innerHTML = ''; + + const pendingDiv = document.createElement('div'); + pendingDiv.className = 'flex mb-2 justify-between items-center'; + + const cleanPending = coinData.pending.toString().replace(/^\+/, ''); + + pendingDiv.innerHTML = ` +

Pending:

+ +${cleanPending} ${coinData.ticker || coinData.name} + `; + + pendingContainer.appendChild(pendingDiv); + + let initialUSD = '$0.00'; + if (window.WalletManager && window.WalletManager.coinPrices) { + const coinId = coinData.name.toLowerCase().replace(' ', '-'); + const price = window.WalletManager.coinPrices[coinId]; + if (price && price.usd) { + const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2); + initialUSD = `$${usdValue}`; + } + } + + const usdDiv = document.createElement('div'); + usdDiv.className = 'flex mb-2 justify-between items-center'; + usdDiv.innerHTML = ` +

Pending USD value:

+
${initialUSD}
+ `; + + pendingContainer.appendChild(usdDiv); + }, + + removePendingDisplay: function(coinName) { + const walletContainer = this.findWalletContainer(coinName); + if (!walletContainer) return; + + const pendingContainer = walletContainer.querySelector('.pending-container'); + if (pendingContainer) { + pendingContainer.remove(); + } + }, + + findWalletContainer: function(coinName) { + const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]'); + for (const element of balanceElements) { + if (element.getAttribute('data-coinname') === coinName) { + return element.closest('.bg-white, .dark\\:bg-gray-500'); + } + } + return null; + }, + + removePendingBalance: function(coinName, balanceType) { + const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]'); + + balanceElements.forEach(element => { + const elementCoinName = element.getAttribute('data-coinname'); + + if (elementCoinName === coinName) { + const parentDiv = element.closest('.flex.mb-2.justify-between.items-center'); + const labelElement = parentDiv ? parentDiv.querySelector('h4') : null; + + if (labelElement) { + const currentLabel = labelElement.textContent.trim(); + + if (currentLabel.includes('Pending:') || currentLabel.includes('Unconfirmed:')) { + const nextElement = parentDiv.nextElementSibling; + if (nextElement && nextElement.querySelector('h4')?.textContent.includes('USD value:')) { + nextElement.remove(); + } + parentDiv.remove(); + } + } + } + }); + }, + + updatePendingBalance: function(coinName, balanceType, pendingAmount, ticker, pendingLabel, coinData) { + const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]'); + let targetElement = null; + + balanceElements.forEach(element => { + const elementCoinName = element.getAttribute('data-coinname'); + if (elementCoinName === coinName) { + const parentElement = element.closest('.flex.mb-2.justify-between.items-center'); + if (parentElement) { + const labelElement = parentElement.querySelector('h4'); + if (labelElement && labelElement.textContent.includes(balanceType)) { + targetElement = parentElement; + } + } + } + }); + + if (!targetElement) return; + + let insertAfterElement = targetElement; + let nextElement = targetElement.nextElementSibling; + while (nextElement) { + const labelElement = nextElement.querySelector('h4'); + if (labelElement) { + const labelText = labelElement.textContent; + if (labelText.includes('USD value:') && !labelText.includes('Pending') && !labelText.includes('Unconfirmed')) { + insertAfterElement = nextElement; + break; + } + if (labelText.includes('Balance:') || labelText.includes('Pending:') || labelText.includes('Unconfirmed:')) { + break; + } + } + nextElement = nextElement.nextElementSibling; + } + + let pendingElement = insertAfterElement.nextElementSibling; + while (pendingElement && !pendingElement.querySelector('h4')?.textContent.includes(pendingLabel)) { + pendingElement = pendingElement.nextElementSibling; + if (pendingElement && pendingElement.querySelector('h4')?.textContent.includes('Balance:')) { + pendingElement = null; + break; + } + } + + if (!pendingElement) { + const newPendingDiv = document.createElement('div'); + newPendingDiv.className = 'flex mb-2 justify-between items-center'; + + const cleanPending = pendingAmount.toString().replace(/^\+/, ''); + + newPendingDiv.innerHTML = ` +

${pendingLabel}

+ +${cleanPending} ${ticker} + `; + + insertAfterElement.parentNode.insertBefore(newPendingDiv, insertAfterElement.nextSibling); + + let initialUSD = '$0.00'; + if (window.WalletManager && window.WalletManager.coinPrices) { + const coinId = coinName.toLowerCase().replace(' ', '-'); + const price = window.WalletManager.coinPrices[coinId]; + if (price && price.usd) { + const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2); + initialUSD = `$${usdValue}`; + } + } + + const usdDiv = document.createElement('div'); + usdDiv.className = 'flex mb-2 justify-between items-center'; + usdDiv.innerHTML = ` +

${pendingLabel.replace(':', '')} USD value:

+
${initialUSD}
+ `; + + newPendingDiv.parentNode.insertBefore(usdDiv, newPendingDiv.nextSibling); + } else { + const pendingSpan = pendingElement.querySelector('.coinname-value'); + if (pendingSpan) { + const cleanPending = pendingAmount.toString().replace(/^\+/, ''); + pendingSpan.textContent = `+${cleanPending} ${ticker}`; + } + } + } + }; + + document.addEventListener('DOMContentLoaded', function() { + WalletsPage.init(); + }); + + window.WalletsPage = WalletsPage; + +})(); diff --git a/basicswap/static/js/ui/dropdown.js b/basicswap/static/js/ui/dropdown.js index ed0ec86..70276e8 100644 --- a/basicswap/static/js/ui/dropdown.js +++ b/basicswap/static/js/ui/dropdown.js @@ -89,7 +89,7 @@ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') this.hide(); }); - window.addEventListener('scroll', this._handleScroll, true); + window.addEventListener('scroll', this._handleScroll, { passive: true, capture: true }); window.addEventListener('resize', this._handleResize); } @@ -170,7 +170,7 @@ destroy() { document.removeEventListener('click', this._handleOutsideClick); - window.removeEventListener('scroll', this._handleScroll, true); + window.removeEventListener('scroll', this._handleScroll, { passive: true, capture: true }); window.removeEventListener('resize', this._handleResize); const index = dropdownInstances.indexOf(this); diff --git a/basicswap/templates/404.html b/basicswap/templates/404.html index b2529cb..dd4f668 100644 --- a/basicswap/templates/404.html +++ b/basicswap/templates/404.html @@ -1,21 +1,13 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg %} +{% from 'macros.html' import breadcrumb %}
-
    -
  • - -

    Home

    -
    -
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
  • - 404 -
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
+ {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': '404', 'url': '/404'} + ]) }}
diff --git a/basicswap/templates/active.html b/basicswap/templates/active.html index 72fbad5..4ae9acd 100644 --- a/basicswap/templates/active.html +++ b/basicswap/templates/active.html @@ -1,21 +1,8 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %} +{% from 'style.html' import page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %} +{% from 'macros.html' import page_header %} -
-
-
- - - -
-
-

Swaps in Progress

-

Monitor your currently active swap transactions.

-
-
-
-
-
+{{ page_header('Swaps in Progress', 'Monitor your currently active swap transactions.') }} {% include 'inc_messages.html' %} @@ -113,6 +100,6 @@
- + {% include 'footer.html' %} diff --git a/basicswap/templates/amm.html b/basicswap/templates/amm.html index 0eb68d0..0c520ef 100644 --- a/basicswap/templates/amm.html +++ b/basicswap/templates/amm.html @@ -1,5 +1,6 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg, arrow_right_svg, input_time_svg %} +{% from 'style.html' import page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg, arrow_right_svg, input_time_svg %} +{% from 'macros.html' import page_header %} -
-
-
- - -
-
-

Automated Market Maker

- -
-
-
-
-
+{{ page_header('Automated Market Maker', 'Automatically create offers and bids based on your configuration.', dark_bg='dark:bg-gray-500') }}
{% include 'inc_messages.html' %}
-
+
-
+
- - + + +
@@ -1404,9 +1122,9 @@
- - - + + +
@@ -1671,550 +1389,8 @@
- - + + + {% include 'footer.html' %} diff --git a/basicswap/templates/automation_strategies.html b/basicswap/templates/automation_strategies.html index 580164f..a4340e1 100644 --- a/basicswap/templates/automation_strategies.html +++ b/basicswap/templates/automation_strategies.html @@ -1,21 +1,14 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, white_automation_svg, page_forwards_svg, page_back_svg, filter_apply_svg %} +{% from 'style.html' import white_automation_svg, page_forwards_svg, page_back_svg, filter_apply_svg %} +{% from 'macros.html' import breadcrumb %}
- - + {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'Automation Strategies', 'url': '/automation'} + ]) }}
diff --git a/basicswap/templates/automation_strategy.html b/basicswap/templates/automation_strategy.html index dff928b..89cb423 100644 --- a/basicswap/templates/automation_strategy.html +++ b/basicswap/templates/automation_strategy.html @@ -1,24 +1,14 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg %} +{% from 'macros.html' import breadcrumb %}
- - + {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'Automation Strategies', 'url': '/automation'}, + {'text': 'ID:', 'url': '/automation'} + ]) }}
diff --git a/basicswap/templates/automation_strategy_new.html b/basicswap/templates/automation_strategy_new.html index 8e0d916..e85756a 100644 --- a/basicswap/templates/automation_strategy_new.html +++ b/basicswap/templates/automation_strategy_new.html @@ -1,24 +1,14 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg %} +{% from 'macros.html' import breadcrumb %}
- - + {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'Automation Strategies', 'url': '/automation'}, + {'text': 'New', 'url': '/automation'} + ]) }}
diff --git a/basicswap/templates/bid.html b/basicswap/templates/bid.html index 71f967c..66fd2e3 100644 --- a/basicswap/templates/bid.html +++ b/basicswap/templates/bid.html @@ -1,25 +1,16 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg, input_arrow_down_svg, small_arrow_white_right_svg %} +{% from 'style.html' import circular_arrows_svg, input_arrow_down_svg, small_arrow_white_right_svg %} +{% from 'macros.html' import breadcrumb %}
- + {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'Bids', 'url': '#'}, + {'text': 'BID ID: ' ~ bid_id, 'url': bid_id} + ]) }}
@@ -534,14 +525,14 @@
{% if data.can_abandon == true and not edit_bid %}
- +
{% endif %} {% endif %} {% endif %} {% if data.was_received and not edit_bid and data.can_accept_bid %}
- +
{% endif %}
diff --git a/basicswap/templates/bid_xmr.html b/basicswap/templates/bid_xmr.html index f49b7ff..b78848c 100644 --- a/basicswap/templates/bid_xmr.html +++ b/basicswap/templates/bid_xmr.html @@ -1,25 +1,16 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg, input_arrow_down_svg, small_arrow_white_right_svg %} +{% from 'style.html' import circular_arrows_svg, input_arrow_down_svg, small_arrow_white_right_svg %} +{% from 'macros.html' import breadcrumb %}
- + {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'Bids', 'url': '#'}, + {'text': 'BID ID: ' ~ bid_id, 'url': bid_id} + ]) }}
@@ -810,14 +801,14 @@
{% if data.can_abandon == true %}
- +
{% endif %} {% endif %} {% endif %} {% if data.was_received and not edit_bid and data.can_accept_bid %}
- +
{% endif %}
diff --git a/basicswap/templates/bids.html b/basicswap/templates/bids.html index 2bd172a..805ba38 100644 --- a/basicswap/templates/bids.html +++ b/basicswap/templates/bids.html @@ -1,22 +1,8 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, circular_arrows_svg, input_arrow_down_svg, arrow_right_svg %} +{% from 'style.html' import page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, circular_arrows_svg, input_arrow_down_svg, arrow_right_svg %} +{% from 'macros.html' import page_header %} - -
-
-
- - - -
-
-

All Bids / Sent Bids / Received Bids

-

View, and manage bids.

-
-
-
-
-
+{{ page_header('All Bids / Sent Bids / Received Bids', 'View, and manage bids.') }} {% include 'inc_messages.html' %} @@ -467,7 +453,8 @@
- - + + + {% include 'footer.html' %} diff --git a/basicswap/templates/bids_available.html b/basicswap/templates/bids_available.html index 76b9c1e..a8fa28f 100644 --- a/basicswap/templates/bids_available.html +++ b/basicswap/templates/bids_available.html @@ -1,21 +1,8 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %} +{% from 'style.html' import page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %} +{% from 'macros.html' import page_header %} -
-
-
- - - -
-
-

Bid Requests

-

Review and accept bids from other users.

-
-
-
-
-
+{{ page_header('Bid Requests', 'Review and accept bids from other users.') }} {% include 'inc_messages.html' %} @@ -113,6 +100,6 @@
- + {% include 'footer.html' %} diff --git a/basicswap/templates/changepassword.html b/basicswap/templates/changepassword.html index 50af8b5..0648a54 100644 --- a/basicswap/templates/changepassword.html +++ b/basicswap/templates/changepassword.html @@ -1,28 +1,15 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg %} +{% from 'macros.html' import breadcrumb %}
- + {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'Change Password', 'url': '/changepassword'} + ]) }}
diff --git a/basicswap/templates/debug.html b/basicswap/templates/debug.html index 0677713..27445ef 100644 --- a/basicswap/templates/debug.html +++ b/basicswap/templates/debug.html @@ -1,21 +1,14 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, start_process_svg %} +{% from 'style.html' import start_process_svg %} +{% from 'macros.html' import breadcrumb %}
-
    -
  • - -

    Home

    -
    -
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
  • - Debug -
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
+ {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'Debug', 'url': '/debug'} + ]) }}
@@ -63,7 +56,7 @@ Remove expired offers and bids - diff --git a/basicswap/templates/donation.html b/basicswap/templates/donation.html index 5809d16..225a524 100644 --- a/basicswap/templates/donation.html +++ b/basicswap/templates/donation.html @@ -1,24 +1,8 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, love_svg %} +{% from 'style.html' import love_svg %} +{% from 'macros.html' import page_header %} -
-
-
- dots-red - wave -
-
-

- Support BasicSwap Development -

-

- Help keep BasicSwap free and open-source. Your donations directly fund development, security audits, and community growth. -

-
-
-
-
-
+{{ page_header('Support BasicSwap Development', 'Help keep BasicSwap free and open-source. Your donations directly fund development, security audits, and community growth.', title_size='text-3xl', dots_style='all') }}
diff --git a/basicswap/templates/explorers.html b/basicswap/templates/explorers.html index 9a23acc..529d176 100644 --- a/basicswap/templates/explorers.html +++ b/basicswap/templates/explorers.html @@ -1,21 +1,14 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, input_arrow_down_svg %} +{% from 'style.html' import input_arrow_down_svg %} +{% from 'macros.html' import breadcrumb %}
-
    -
  • - -

    Home

    -
    -
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
  • - Explorers -
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
+ {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'Explorers', 'url': '/explores'} + ]) }}
diff --git a/basicswap/templates/header.html b/basicswap/templates/header.html index 2124c54..236827e 100644 --- a/basicswap/templates/header.html +++ b/basicswap/templates/header.html @@ -88,9 +88,13 @@ - + + + + + @@ -104,7 +108,8 @@ - + + {% if current_page == 'wallets' or current_page == 'wallet' %} {% endif %} @@ -817,18 +822,6 @@ + {% if data.amount_negotiable == true %} @@ -626,7 +562,7 @@ if (document.readyState === 'loading') {
-
@@ -652,6 +588,29 @@ if (document.readyState === 'loading') {
+ +
- + {% else %}
@@ -1063,13 +683,13 @@ document.addEventListener('DOMContentLoaded', function() {
{% if data.was_revoked != true %}
- +
{% endif %} {% endif %} @@ -1097,6 +717,10 @@ document.addEventListener('DOMContentLoaded', function() { + + + +

diff --git a/basicswap/templates/offer_confirm.html b/basicswap/templates/offer_confirm.html index 08e2055..476baf4 100644 --- a/basicswap/templates/offer_confirm.html +++ b/basicswap/templates/offer_confirm.html @@ -1,20 +1,15 @@ -{% include 'header.html' %} {% from 'style.html' import breadcrumb_line_svg, input_down_arrow_offer_svg, select_network_svg, select_address_svg, select_swap_type_svg, select_bid_amount_svg, select_rate_svg, step_one_svg, step_two_svg, step_three_svg, select_setup_svg, input_time_svg, select_target_svg , select_auto_strategy_svg %} +{% include 'header.html' %} {% from 'style.html' import input_down_arrow_offer_svg, select_network_svg, select_address_svg, select_swap_type_svg, select_bid_amount_svg, select_rate_svg, step_one_svg, step_two_svg, step_three_svg, select_setup_svg, input_time_svg, select_target_svg , select_auto_strategy_svg %} +{% from 'macros.html' import breadcrumb %}
-
    -
  • -

    Home

    -
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
  • Place
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
  • Setup
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
  • Confirm
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
+ {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'Place', 'url': '/newoffer'}, + {'text': 'Setup', 'url': '#'}, + {'text': 'Confirm', 'url': '#'} + ]) }}
@@ -465,7 +460,7 @@ {% endif %} - +
diff --git a/basicswap/templates/offer_new_1.html b/basicswap/templates/offer_new_1.html index 8ae5c5a..4158196 100644 --- a/basicswap/templates/offer_new_1.html +++ b/basicswap/templates/offer_new_1.html @@ -1,17 +1,13 @@ -{% include 'header.html' %} {% from 'style.html' import breadcrumb_line_svg, input_down_arrow_offer_svg, select_network_svg, select_address_svg, select_swap_type_svg, select_bid_amount_svg, select_rate_svg, step_one_svg, step_two_svg, step_three_svg %} +{% include 'header.html' %} {% from 'style.html' import input_down_arrow_offer_svg, select_network_svg, select_address_svg, select_swap_type_svg, select_bid_amount_svg, select_rate_svg, step_one_svg, step_two_svg, step_three_svg %} +{% from 'macros.html' import breadcrumb %}
-
    -
  • -

    Home

    -
    -
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
  • Place
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
+ {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'Place', 'url': '/newoffer'} + ]) }}
@@ -168,9 +164,9 @@
- - - + + +
@@ -315,7 +311,7 @@
-
@@ -338,6 +334,29 @@
+ + - + {% include 'footer.html' %}
diff --git a/basicswap/templates/offer_new_2.html b/basicswap/templates/offer_new_2.html index b92a8a8..895664d 100644 --- a/basicswap/templates/offer_new_2.html +++ b/basicswap/templates/offer_new_2.html @@ -1,23 +1,14 @@ -{% include 'header.html' %} {% from 'style.html' import breadcrumb_line_svg, input_down_arrow_offer_svg, select_network_svg, select_address_svg, select_swap_type_svg, select_bid_amount_svg, select_rate_svg, step_one_svg, step_two_svg, step_three_svg, select_setup_svg, input_time_svg, select_target_svg , select_auto_strategy_svg %} +{% include 'header.html' %} {% from 'style.html' import input_down_arrow_offer_svg, select_network_svg, select_address_svg, select_swap_type_svg, select_bid_amount_svg, select_rate_svg, step_one_svg, step_two_svg, step_three_svg, select_setup_svg, input_time_svg, select_target_svg , select_auto_strategy_svg %} +{% from 'macros.html' import breadcrumb %}
-
    -
  • -

    Home

    -
    -
  • - {{ breadcrumb_line_svg | safe }} -
  • - Placer -
  • - {{ breadcrumb_line_svg | safe }} -
  • - Setup -
  • - {{ breadcrumb_line_svg | safe }} -
+ {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'Place', 'url': '/newoffer'}, + {'text': 'Setup', 'url': '#'} + ]) }}
@@ -447,7 +438,7 @@ {% endif %} - +
diff --git a/basicswap/templates/offers.html b/basicswap/templates/offers.html index a39475e..40a7c22 100644 --- a/basicswap/templates/offers.html +++ b/basicswap/templates/offers.html @@ -1,5 +1,6 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, place_new_offer_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg, arrow_right_svg %} +{% from 'style.html' import place_new_offer_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg, arrow_right_svg %} +{% from 'macros.html' import page_header %} -
-
-
- - - -
-
-

{{ page_type }}

-

{{ page_type_description }}

-
- -
-
-
-
+{{ page_header(page_type, page_type_description, dark_bg='dark:bg-gray-500') }} {% include 'inc_messages.html' %} @@ -194,7 +173,7 @@ - + {% endif %} @@ -432,6 +411,6 @@ - + {% include 'footer.html' %} diff --git a/basicswap/templates/rpc.html b/basicswap/templates/rpc.html index 8937b6d..a0b7f09 100644 --- a/basicswap/templates/rpc.html +++ b/basicswap/templates/rpc.html @@ -1,21 +1,14 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, input_arrow_down_svg %} +{% from 'style.html' import input_arrow_down_svg %} +{% from 'macros.html' import breadcrumb %}
-
    -
  • - -

    Home

    -
    -
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
  • - RPC Console -
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
+ {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'RPC Console', 'url': '/rpc'} + ]) }}
diff --git a/basicswap/templates/settings.html b/basicswap/templates/settings.html index 8d25a7d..a7d31c5 100644 --- a/basicswap/templates/settings.html +++ b/basicswap/templates/settings.html @@ -1,21 +1,14 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, input_arrow_down_svg %} +{% from 'style.html' import input_arrow_down_svg %} +{% from 'macros.html' import breadcrumb %}
-
    -
  • - -

    Home

    -
    -
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
  • - Settings -
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
+ {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'Settings', 'url': '/settings'} + ]) }}
@@ -268,12 +261,12 @@ Apply Changes {% if c.can_disable == true %} - {% endif %} {% if c.can_reenable == true %} - {% endif %} @@ -507,7 +500,7 @@
-
@@ -549,10 +542,10 @@

Test Notifications

- -
- - - + {% include 'footer.html' %} diff --git a/basicswap/templates/smsgaddresses.html b/basicswap/templates/smsgaddresses.html index e1dd9cd..21c9f4a 100644 --- a/basicswap/templates/smsgaddresses.html +++ b/basicswap/templates/smsgaddresses.html @@ -1,21 +1,14 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, input_arrow_down_svg, filter_apply_svg, circle_plus_svg, page_forwards_svg, page_back_svg %} +{% from 'style.html' import input_arrow_down_svg, filter_apply_svg, circle_plus_svg, page_forwards_svg, page_back_svg %} +{% from 'macros.html' import breadcrumb %}
- + {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'SMSG Addresses', 'url': '/smsgaddresses'} + ]) }}
diff --git a/basicswap/templates/tor.html b/basicswap/templates/tor.html index d9c1fc6..5a866fe 100644 --- a/basicswap/templates/tor.html +++ b/basicswap/templates/tor.html @@ -1,21 +1,14 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg %} +{% from 'style.html' import circular_arrows_svg %} +{% from 'macros.html' import breadcrumb %}
-
    -
  • - -

    Home

    -
    -
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
  • - Tor -
  • -
  • {{ breadcrumb_line_svg | safe }}
  • -
+ {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'Tor', 'url': '/tor'} + ]) }}
diff --git a/basicswap/templates/unlock.html b/basicswap/templates/unlock.html index e81c03d..3790547 100644 --- a/basicswap/templates/unlock.html +++ b/basicswap/templates/unlock.html @@ -129,7 +129,7 @@
Main Address:
@@ -282,7 +269,7 @@
{{ w.main_address }}
{% else %} -
+
Deposit Address:
@@ -309,7 +296,7 @@
{% if w.cid in '6, 9' %} {# XMR | WOW #} -
+
Subaddress:
@@ -323,7 +310,7 @@
{% elif w.cid == '1' %} {# PART #} -
+
Stealth Address:
@@ -333,7 +320,7 @@ {# / PART #} {% elif w.cid == '3' %} {# LTC #} -
+
MWEB Address:
@@ -362,247 +349,9 @@ -{% if w.cid == '1' %} - {# PART #} - - {% elif w.cid == '3' %} - {# LTC #} - - {% endif %} - - {% if w.cid in '6, 9' %} - {# XMR | WOW #} - - - - - {% else %} - -{% endif %} - - +
@@ -682,12 +431,12 @@ function fillDonationAddress(address, type) { {% if donation_info.mweb_address %}
- -
@@ -696,8 +445,8 @@ function fillDonationAddress(address, type) {
{% else %} - {% endif %} @@ -729,64 +478,6 @@ function fillDonationAddress(address, type) { - - {# / PART #} {% elif w.cid == '3' %} @@ -795,80 +486,11 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) { - - {# / LTC #} {% else %} - - {% endif %} @@ -974,7 +596,7 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) { {% else %}
{% endif %} -
+
@@ -1019,7 +641,7 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) { - + @@ -1080,492 +702,7 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) { - + {% include 'footer.html' %} diff --git a/basicswap/templates/wallets.html b/basicswap/templates/wallets.html index 464b5ba..bbda852 100644 --- a/basicswap/templates/wallets.html +++ b/basicswap/templates/wallets.html @@ -1,5 +1,5 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg, withdraw_svg, utxo_groups_svg, create_utxo_svg, lock_svg, eye_show_svg %} +{% from 'style.html' import circular_arrows_svg, withdraw_svg, utxo_groups_svg, create_utxo_svg, lock_svg, eye_show_svg %}
@@ -190,425 +190,7 @@ {% include 'footer.html' %} - + diff --git a/basicswap/templates/watched.html b/basicswap/templates/watched.html index 0dbd6c6..5e73178 100644 --- a/basicswap/templates/watched.html +++ b/basicswap/templates/watched.html @@ -1,21 +1,14 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg %} +{% from 'style.html' import circular_arrows_svg %} +{% from 'macros.html' import breadcrumb %}
- + {{ breadcrumb([ + {'text': 'Home', 'url': '/'}, + {'text': 'Watched Outputs', 'url': '/watched'} + ]) }}
diff --git a/basicswap/ui/page_amm.py b/basicswap/ui/page_amm.py index 1730d94..f1e1e21 100644 --- a/basicswap/ui/page_amm.py +++ b/basicswap/ui/page_amm.py @@ -409,10 +409,6 @@ def get_amm_active_count(swap_client, debug_override=False): state_path = get_amm_state_path(swap_client) if not os.path.exists(state_path): - if debug_enabled: - swap_client.log.info( - f"AMM state file not found at {state_path}, returning count 0" - ) return 0 config_path = get_amm_config_path(swap_client) @@ -432,11 +428,6 @@ def get_amm_active_count(swap_client, debug_override=False): if bid.get("enabled", False): enabled_bids.add(bid.get("name", "")) - if debug_enabled: - swap_client.log.info( - f"Enabled templates: {len(enabled_offers)} offers, {len(enabled_bids)} bids" - ) - except Exception as e: if debug_enabled: swap_client.log.error(f"Error reading config file: {str(e)}") @@ -450,11 +441,6 @@ def get_amm_active_count(swap_client, debug_override=False): with open(state_path, "r") as f: state_data = json.load(f) - if debug_enabled: - swap_client.log.debug( - f"AMM state data loaded with {len(state_data.get('offers', {}))} offer templates" - ) - try: network_offers = swap_client.listOffers() @@ -501,31 +487,17 @@ def get_amm_active_count(swap_client, debug_override=False): swap_client.log.error(traceback.format_exc()) continue - if debug_enabled: - swap_client.log.debug( - f"Found {len(active_network_offers)} active offers in the network" - ) - except Exception as e: if debug_enabled: swap_client.log.error(f"Error getting network offers: {str(e)}") swap_client.log.error(traceback.format_exc()) if len(active_network_offers) == 0: - if debug_enabled: - swap_client.log.info( - "No active network offers found, trying direct API call" - ) - try: global amm_host, amm_port if "amm_host" not in globals() or "amm_port" not in globals(): amm_host = "127.0.0.1" amm_port = 12700 - if debug_enabled: - swap_client.log.info( - f"Using default host {amm_host} and port {amm_port} for API call" - ) api_url = f"http://{amm_host}:{amm_port}/api/v1/offers" @@ -539,11 +511,6 @@ def get_amm_active_count(swap_client, debug_override=False): offer_id = offer["id"] active_network_offers[offer_id] = True - if debug_enabled: - swap_client.log.info( - f"Found {len(active_network_offers)} active offers via API" - ) - except Exception as e: if debug_enabled: swap_client.log.error(f"Error getting offers via API: {str(e)}") @@ -561,21 +528,6 @@ def get_amm_active_count(swap_client, debug_override=False): active_offer_count += 1 amm_count += active_offer_count - if debug_enabled: - total_offers = len(offers) - enabled_status = ( - "enabled" - if enabled_offers is None or template_name in enabled_offers - else "disabled" - ) - if debug_enabled: - swap_client.log.debug( - f"Template '{template_name}' ({enabled_status}): {active_offer_count} active out of {total_offers} total offers" - ) - elif debug_enabled: - swap_client.log.debug( - f"Template '{template_name}' is disabled, skipping {len(offers)} offers" - ) if "bids" in state_data: for template_name, bids in state_data["bids"].items(): @@ -586,36 +538,12 @@ def get_amm_active_count(swap_client, debug_override=False): active_bid_count += 1 amm_count += active_bid_count - if debug_enabled: - total_bids = len(bids) - enabled_status = ( - "enabled" - if enabled_bids is None or template_name in enabled_bids - else "disabled" - ) - - if debug_enabled: - swap_client.log.debug( - f"Template '{template_name}' ({enabled_status}): {active_bid_count} active out of {total_bids} total bids" - ) - elif debug_enabled: - swap_client.log.debug( - f"Template '{template_name}' is disabled, skipping {len(bids)} bids" - ) - - if debug_enabled: - swap_client.log.debug(f"Total active AMM count: {amm_count}") if ( amm_count == 0 and len(active_network_offers) == 0 and "offers" in state_data ): - if debug_enabled: - swap_client.log.info( - "No active network offers found, using most recent offer from state file" - ) - most_recent_time = 0 most_recent_offer = None @@ -631,21 +559,8 @@ def get_amm_active_count(swap_client, debug_override=False): if offer_age < 3600: amm_count = 1 - if debug_enabled: - swap_client.log.info( - f"Using most recent offer as active (age: {offer_age} seconds)" - ) - if "offer_id" in most_recent_offer: - swap_client.log.info( - f"Most recent offer ID: {most_recent_offer['offer_id']}" - ) if amm_count == 0 and "delay_next_offer_before" in state_data: - if debug_enabled: - swap_client.log.info( - "Found delay_next_offer_before in state, AMM is running but waiting to create next offer" - ) - config_path = get_amm_config_path(swap_client) if os.path.exists(config_path): try: @@ -654,10 +569,6 @@ def get_amm_active_count(swap_client, debug_override=False): for offer in config_data.get("offers", []): if offer.get("enabled", False): - if debug_enabled: - swap_client.log.info( - f"Found enabled offer '{offer.get('name')}', but no active offers in network" - ) break except Exception as e: if debug_enabled: @@ -669,10 +580,6 @@ def get_amm_active_count(swap_client, debug_override=False): and "offers" in state_data and len(state_data["offers"]) > 0 ): - if debug_enabled: - swap_client.log.info( - "AMM is running with offers in state file, but none are active. Setting count to 1." - ) amm_count = 1 except Exception as e: if debug_enabled: @@ -680,9 +587,6 @@ def get_amm_active_count(swap_client, debug_override=False): swap_client.log.error(traceback.format_exc()) return 0 - if debug_enabled: - swap_client.log.debug(f"Final AMM active count: {amm_count}") - return amm_count diff --git a/scripts/createoffers.py b/scripts/createoffers.py index e94673c..da6cdc4 100755 --- a/scripts/createoffers.py +++ b/scripts/createoffers.py @@ -711,17 +711,25 @@ def process_offers(args, config, script_state) -> None: print(f"Wallet data: {wallet_from}") continue - for offer in sent_offers: - created_offers = script_state.get("offers", {}) - prev_template_offers = created_offers.get(offer_template["name"], {}) + created_offers = script_state.get("offers", {}) + prev_template_offers = created_offers.get(offer_template["name"], []) - if next( - (x for x in prev_template_offers if x["offer_id"] == offer["offer_id"]), - None, - ): + template_offer_ids = set() + for prev_offer in prev_template_offers: + if "offer_id" in prev_offer: + template_offer_ids.add(prev_offer["offer_id"]) + + matching_sent_offers = [] + for offer in sent_offers: + offer_id = offer.get("offer_id") + if not offer_id: + continue + + if offer_id in template_offer_ids: + matching_sent_offers.append(offer) offers_found += 1 + if wallet_balance <= float(offer_template["min_coin_from_amt"]): - offer_id = offer["offer_id"] print( "Revoking offer {}, wallet from balance below minimum".format( offer_id @@ -732,6 +740,57 @@ def process_offers(args, config, script_state) -> None: print("revokeoffer", result) else: print("Offer revoked successfully") + else: + coin_from_match = offer.get("coin_from") == coin_from_data["id"] + coin_to_match = offer.get("coin_to") == coin_to_data["id"] + + if coin_from_match and coin_to_match: + if args.debug: + print( + f"Found untracked offer {offer_id} matching template {offer_template['name']} coins" + ) + matching_sent_offers.append(offer) + offers_found += 1 + + if len(matching_sent_offers) > 1: + print( + f"WARNING: Found {len(matching_sent_offers)} active offers for template '{offer_template['name']}'" + ) + if args.debug: + print(f"Offer IDs: {[o.get('offer_id') for o in matching_sent_offers]}") + + matching_sent_offers.sort( + key=lambda x: x.get("created_at", 0), reverse=True + ) + newest_offer = matching_sent_offers[0] + + for old_offer in matching_sent_offers[1:]: + old_offer_id = old_offer.get("offer_id") + print(f"Revoking duplicate offer {old_offer_id}") + try: + result = read_json_api(f"revokeoffer/{old_offer_id}") + if args.debug: + print(f"Revoke result: {result}") + + for i, prev_offer in enumerate(prev_template_offers): + if prev_offer.get("offer_id") == old_offer_id: + del prev_template_offers[i] + break + except Exception as e: + print(f"Error revoking duplicate offer {old_offer_id}: {e}") + + offers_found = 1 + + if newest_offer.get("offer_id") not in template_offer_ids: + if "offers" not in script_state: + script_state["offers"] = {} + if offer_template["name"] not in script_state["offers"]: + script_state["offers"][offer_template["name"]] = [] + script_state["offers"][offer_template["name"]].append( + {"offer_id": newest_offer["offer_id"], "time": int(time.time())} + ) + write_state(args.statefile, script_state) + print(f"Added untracked offer {newest_offer['offer_id']} to state") if offers_found > 0: continue diff --git a/tests/basicswap/selenium/test_swap_direction.py b/tests/basicswap/selenium/test_swap_direction.py index c2cd4f8..b2e8163 100644 --- a/tests/basicswap/selenium/test_swap_direction.py +++ b/tests/basicswap/selenium/test_swap_direction.py @@ -293,8 +293,14 @@ def test_swap_dir(driver): try: bid_rows = dict() table = driver.find_element(By.XPATH, "//tbody[@id='active-swaps-body']") - for row in table.find_elements(By.XPATH, ".//tr"): + rows = table.find_elements(By.XPATH, ".//tr") + if len(rows) == 0: + time.sleep(2) + continue + for row in rows: tds = row.find_elements(By.XPATH, ".//td") + if len(tds) < 6: + continue td_details = tds[2] td_send = tds[5] td_recv = tds[3] @@ -336,8 +342,14 @@ def test_swap_dir(driver): try: bid_rows = dict() table = driver.find_element(By.XPATH, "//tbody[@id='active-swaps-body']") - for row in table.find_elements(By.XPATH, ".//tr"): + rows = table.find_elements(By.XPATH, ".//tr") + if len(rows) == 0: + time.sleep(2) + continue + for row in rows: tds = row.find_elements(By.XPATH, ".//td") + if len(tds) < 6: + continue td_details = tds[2] td_send = tds[5] td_recv = tds[3] From eb46a4fcc57296b4bcf23099bb7128dfd9a1254e Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Fri, 10 Oct 2025 12:26:36 +0200 Subject: [PATCH 2/7] Fix pricechart if no price/historical data available. --- basicswap/basicswap.py | 114 ++++++++++-------- .../static/js/pages/offers-pricechart.js | 44 ++++++- 2 files changed, 108 insertions(+), 50 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 605bd45..9cd48b3 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -12592,47 +12592,59 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): return return_data if rate_source == "coingecko.com": - 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 + try: + 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=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=10, headers=headers)) - - for k, v in js.items(): - coin_id = int(exchange_name_map[k]) - volume_24h = v.get("usd_24h_vol") - price_change_24h = v.get("usd_24h_change") - - # Convert to float if value exists, otherwise keep as None - volume_value = float(volume_24h) if volume_24h is not None else None - price_change_value = ( - float(price_change_24h) if price_change_24h is not None else 0.0 + 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=usd&include_24hr_vol=true&include_24hr_change=true" + ) + if api_key != "": + url += f"&api_key={api_key}" - return_data[coin_id] = { - "volume_24h": volume_value, - "price_change_24h": price_change_value, - } - new_values[coin_id] = { - "volume_24h": volume_value, - "price_change_24h": price_change_value, - } + js = json.loads(self.readURL(url, timeout=10, headers=headers)) + + for k, v in js.items(): + coin_id = int(exchange_name_map[k]) + volume_24h = v.get("usd_24h_vol") + price_change_24h = v.get("usd_24h_change") + + # Convert to float if value exists, otherwise keep as None + volume_value = ( + float(volume_24h) if volume_24h is not None else None + ) + price_change_value = ( + float(price_change_24h) + if price_change_24h is not None + else 0.0 + ) + + return_data[coin_id] = { + "volume_24h": volume_value, + "price_change_24h": price_change_value, + } + new_values[coin_id] = { + "volume_24h": volume_value, + "price_change_24h": price_change_value, + } + except Exception as e: + self.log.warning(f"Could not fetch volume data: {e}") + for coin_id in need_coins: + return_data[coin_id] = { + "volume_24h": None, + "price_change_24h": 0.0, + } else: raise ValueError(f"Unknown rate source {rate_source}") @@ -12723,18 +12735,24 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): ) for coin_id in need_coins: - 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}" + 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}" - js = json.loads(self.readURL(url, timeout=10, headers=headers)) + js = json.loads(self.readURL(url, timeout=10, headers=headers)) - if "prices" in js: - return_data[coin_id] = js["prices"] - new_values[coin_id] = js["prices"] + 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}") diff --git a/basicswap/static/js/pages/offers-pricechart.js b/basicswap/static/js/pages/offers-pricechart.js index f1a3c27..c9af205 100644 --- a/basicswap/static/js/pages/offers-pricechart.js +++ b/basicswap/static/js/pages/offers-pricechart.js @@ -866,8 +866,11 @@ destroyChart: function() { const allData = await api.fetchHistoricalDataXHR([coinSymbol]); data = allData[coinSymbol]; - if (!data || Object.keys(data).length === 0) { - throw new Error(`No data returned for ${coinSymbol}`); + if (!data || (Array.isArray(data) && data.length === 0) || Object.keys(data).length === 0) { + console.warn(`No price data available for ${coinSymbol}`); + chartModule.hideChartLoader(); + chartModule.showNoDataMessage(coinSymbol); + return; } CacheManager.set(cacheKey, data, 'chart'); @@ -897,6 +900,8 @@ destroyChart: function() { chartModule.initChart(); } + chartModule.hideNoDataMessage(); + const chartData = chartModule.prepareChartData(coinSymbol, data); if (chartData.length > 0 && chartModule.chart) { chartModule.chart.data.datasets[0].data = chartData; @@ -951,6 +956,41 @@ destroyChart: function() { chart.classList.remove('hidden'); }, + showNoDataMessage: function(coinSymbol) { + const chartCanvas = document.getElementById('coin-chart'); + if (!chartCanvas) { + return; + } + + if (this.chart) { + this.chart.data.datasets[0].data = []; + this.chart.update('none'); + } + + let messageDiv = document.getElementById('chart-no-data-message'); + if (!messageDiv) { + messageDiv = document.createElement('div'); + messageDiv.id = 'chart-no-data-message'; + messageDiv.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #888; font-size: 14px; z-index: 10;'; + chartCanvas.parentElement.style.position = 'relative'; + chartCanvas.parentElement.appendChild(messageDiv); + } + + messageDiv.innerHTML = ` +
+
No Price Data Available
+
+ `; + messageDiv.classList.remove('hidden'); + }, + + hideNoDataMessage: function() { + const messageDiv = document.getElementById('chart-no-data-message'); + if (messageDiv) { + messageDiv.classList.add('hidden'); + } + }, + cleanup: function() { if (this.pendingAnimationFrame) { cancelAnimationFrame(this.pendingAnimationFrame); From 1a9c1533065f941ac40981201025cd7509b38222 Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Mon, 13 Oct 2025 19:37:36 +0200 Subject: [PATCH 3/7] Fix getwalletinfo + various fixes. --- basicswap/basicswap.py | 14 ++++++--- basicswap/http_server.py | 63 +++++++++++++++++++++++--------------- basicswap/rpc.py | 66 +++++++++++++++++++++++++++++++++++----- basicswap/rpc_pool.py | 2 +- 4 files changed, 107 insertions(+), 38 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 9cd48b3..701bc20 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -12354,11 +12354,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: diff --git a/basicswap/http_server.py b/basicswap/http_server.py index e85c09a..ba30795 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -218,31 +218,34 @@ 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 = [] @@ -274,9 +277,19 @@ class HttpHandler(BaseHTTPRequestHandler): 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) with self.server.msg_id_lock: if self.server.msg_id_counter >= 0x7FFFFFFF: diff --git a/basicswap/rpc.py b/basicswap/rpc.py index 9d77e03..9a400c4 100644 --- a/basicswap/rpc.py +++ b/basicswap/rpc.py @@ -9,6 +9,7 @@ import json import logging import traceback import urllib +import http.client from xmlrpc.client import ( Fault, Transport, @@ -26,6 +27,26 @@ def enable_rpc_pooling(settings): _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 def __init__( @@ -39,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, @@ -72,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[:] @@ -81,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, @@ -89,15 +133,23 @@ 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"): @@ -114,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: @@ -211,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 index d3c3e23..3042937 100644 --- a/basicswap/rpc_pool.py +++ b/basicswap/rpc_pool.py @@ -12,7 +12,7 @@ from basicswap.rpc import Jsonrpc class RPCConnectionPool: def __init__( - self, url, max_connections=5, timeout=30, logger=None, max_idle_time=300 + self, url, max_connections=5, timeout=10, logger=None, max_idle_time=300 ): self.url = url self.max_connections = max_connections From 4c1c5cd1a612474bc0539c23bde4785713da9b6e Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Tue, 14 Oct 2025 13:06:59 +0200 Subject: [PATCH 4/7] Fix keep WebSockets alive. --- basicswap/static/js/modules/websocket-manager.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/basicswap/static/js/modules/websocket-manager.js b/basicswap/static/js/modules/websocket-manager.js index 090bf7d..b636085 100644 --- a/basicswap/static/js/modules/websocket-manager.js +++ b/basicswap/static/js/modules/websocket-manager.js @@ -315,24 +315,16 @@ 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; const resumeFn = () => { if (!publicAPI.isConnected()) { publicAPI.connect(); } - startHealthCheck(); }; if (window.CleanupManager) { From de501f4bb5120acde8f0d420cdfa475a46f15ea7 Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Wed, 15 Oct 2025 12:13:08 +0200 Subject: [PATCH 5/7] Removed CryptoCompare + Added background thread for price fetching. --- basicswap/basicswap.py | 518 ++++++++++-------- basicswap/explorers.py | 3 - basicswap/static/js/modules/api-manager.js | 3 +- basicswap/static/js/modules/config-manager.js | 9 - basicswap/static/js/modules/wallet-manager.js | 3 +- .../static/js/pages/offers-pricechart.js | 9 +- basicswap/templates/header.html | 1 - basicswap/templates/settings.html | 6 - basicswap/ui/page_offers.py | 30 +- basicswap/ui/page_settings.py | 9 +- 10 files changed, 302 insertions(+), 289 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 701bc20..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. @@ -599,6 +611,11 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): 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 @@ -1099,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 @@ -1119,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) @@ -1268,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 @@ -11073,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( @@ -11101,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: @@ -12379,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, @@ -12395,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: @@ -12421,109 +12642,10 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): rows = cursor.execute(query, parameters) for row in rows: - return_rates[int(row[0])] = float(row[1]) - - need_coins = [] - new_values = {} - exchange_name_map = {} - for coin_id in coins_list: + coin_id = int(row[0]) if coin_id not in return_rates: - need_coins.append(coin_id) + return_rates[coin_id] = float(row[1]) - if len(need_coins) < 1: - return return_rates - - if rate_source == "coingecko.com": - ticker_to: str = fiatTicker(currency_to).lower() - # Update all requested coins - coin_ids: str = "" - for coin_id in coins_list: - if len(coin_ids) > 0: - coin_ids += "," - exchange_name: str = self.getExchangeName(coin_id, rate_source) - coin_ids += exchange_name - exchange_name_map[exchange_name] = coin_id - - api_key: str = get_api_key_setting( - self.settings, - "coingecko_api_key", - default_coingecko_api_key, - escape=True, - ) - url: str = ( - f"https://api.coingecko.com/api/v3/simple/price?ids={coin_ids}&vs_currencies={ticker_to}" - ) - if api_key != "": - url += f"&api_key={api_key}" - - js = json.loads(self.readURL(url, timeout=10, headers=headers)) - - for k, v in js.items(): - return_rates[int(exchange_name_map[k])] = v[ticker_to] - new_values[exchange_name_map[k]] = v[ticker_to] - elif rate_source == "cryptocompare.com": - ticker_to: str = fiatTicker(currency_to).upper() - api_key: str = get_api_key_setting( - self.settings, - "chart_api_key", - default_chart_api_key, - escape=True, - ) - 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}" - ) - js = json.loads(self.readURL(url, timeout=10, headers=headers)) - for k, v in js.items(): - return_rates[int(exchange_name_map[k])] = v[ticker_to] - new_values[exchange_name_map[k]] = v[ticker_to] - else: - raise ValueError(f"Unknown rate source {rate_source}") - - if len(new_values) < 1: - return return_rates - - # ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint - update_query = """ - UPDATE coinrates SET - rate=:rate, - last_updated=:last_updated - WHERE currency_from = :currency_from AND currency_to = :currency_to AND source = :rate_source - """ - - insert_query = """INSERT INTO coinrates(currency_from, currency_to, rate, source, last_updated) - VALUES(:currency_from, :currency_to, :rate, :rate_source, :last_updated)""" - - for k, v in new_values.items(): - cursor.execute( - update_query, - { - "currency_from": k, - "currency_to": currency_to, - "rate": v, - "rate_source": rate_source, - "last_updated": now, - }, - ) - if cursor.rowcount < 1: - cursor.execute( - insert_query, - { - "currency_from": k, - "currency_to": currency_to, - "rate": v, - "rate_source": rate_source, - "last_updated": now, - }, - ) - - self.commitDB() return return_rates finally: self.closeDB(cursor, commit=False) @@ -12543,7 +12665,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): oldest_time_valid: int = now - saved_ttl return_data = {} - headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"} + 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: @@ -12568,6 +12699,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): 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: @@ -12580,98 +12714,11 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): price_change_24h = float(row[2]) except (ValueError, TypeError): pass - return_data[int(row[0])] = { + return_data[coin_id] = { "volume_24h": volume_24h, "price_change_24h": price_change_24h, } - need_coins = [] - new_values = {} - exchange_name_map = {} - for coin_id in coins_list: - if coin_id not in return_data: - need_coins.append(coin_id) - - if len(need_coins) < 1: - return return_data - - if rate_source == "coingecko.com": - try: - 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=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=10, headers=headers)) - - for k, v in js.items(): - coin_id = int(exchange_name_map[k]) - volume_24h = v.get("usd_24h_vol") - price_change_24h = v.get("usd_24h_change") - - # Convert to float if value exists, otherwise keep as None - volume_value = ( - float(volume_24h) if volume_24h is not None else None - ) - price_change_value = ( - float(price_change_24h) - if price_change_24h is not None - else 0.0 - ) - - return_data[coin_id] = { - "volume_24h": volume_value, - "price_change_24h": price_change_value, - } - new_values[coin_id] = { - "volume_24h": volume_value, - "price_change_24h": price_change_value, - } - except Exception as e: - self.log.warning(f"Could not fetch volume data: {e}") - for coin_id in need_coins: - return_data[coin_id] = { - "volume_24h": None, - "price_change_24h": 0.0, - } - else: - raise ValueError(f"Unknown rate source {rate_source}") - - if len(new_values) < 1: - return return_data - - for coin_id, data in new_values.items(): - 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(data["volume_24h"]) - if data["volume_24h"] is not None - else "None" - ), - "price_change_24h": str(data["price_change_24h"]), - "rate_source": rate_source, - "last_updated": now, - }, - ) - - self.commitDB() return return_data finally: self.closeDB(cursor, commit=False) @@ -12747,7 +12794,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): if api_key != "": url += f"&api_key={api_key}" - js = json.loads(self.readURL(url, timeout=10, headers=headers)) + js = json.loads(self.readURL(url, timeout=5, headers=headers)) if "prices" in js: return_data[coin_id] = js["prices"] @@ -12798,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/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/static/js/modules/api-manager.js b/basicswap/static/js/modules/api-manager.js index c04643e..12b0914 100644 --- a/basicswap/static/js/modules/api-manager.js +++ b/basicswap/static/js/modules/api-manager.js @@ -9,8 +9,7 @@ const ApiManager = (function() { requestTimeout: 60000, retryDelays: [5000, 15000, 30000], rateLimits: { - coingecko: { requestsPerMinute: 50, minInterval: 1200 }, - cryptocompare: { requestsPerMinute: 30, minInterval: 2000 } + coingecko: { requestsPerMinute: 50, minInterval: 1200 } } }; } diff --git a/basicswap/static/js/modules/config-manager.js b/basicswap/static/js/modules/config-manager.js index 050ee4f..63ab310 100644 --- a/basicswap/static/js/modules/config-manager.js +++ b/basicswap/static/js/modules/config-manager.js @@ -35,20 +35,13 @@ 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], @@ -99,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: '' }; }, diff --git a/basicswap/static/js/modules/wallet-manager.js b/basicswap/static/js/modules/wallet-manager.js index e971203..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'] } }; diff --git a/basicswap/static/js/pages/offers-pricechart.js b/basicswap/static/js/pages/offers-pricechart.js index c9af205..a7956e3 100644 --- a/basicswap/static/js/pages/offers-pricechart.js +++ b/basicswap/static/js/pages/offers-pricechart.js @@ -24,7 +24,6 @@ const api = { } const volumeData = await Api.fetchVolumeData({ - cryptoCompare: apiKeys.cryptoCompare, coinGecko: apiKeys.coinGecko }); @@ -109,10 +108,7 @@ const api = { const historicalData = await Api.fetchHistoricalData( coinSymbols, - window.config.currentResolution, - { - cryptoCompare: window.config.getAPIKeys().cryptoCompare - } + window.config.currentResolution ); Object.keys(historicalData).forEach(coin => { @@ -146,8 +142,7 @@ const api = { const rateLimiter = { lastRequestTime: {}, minRequestInterval: { - coingecko: window.config.rateLimits.coingecko.minInterval, - cryptocompare: window.config.rateLimits.cryptocompare.minInterval + coingecko: window.config.rateLimits.coingecko.minInterval }, requestQueue: {}, retryDelays: window.config.retryDelays, diff --git a/basicswap/templates/header.html b/basicswap/templates/header.html index 236827e..cc5e055 100644 --- a/basicswap/templates/header.html +++ b/basicswap/templates/header.html @@ -25,7 +25,6 @@