Merge pull request #378 from gerlofvanek/refactor-2

Refactor + Optimizations
This commit is contained in:
tecnovert
2025-10-15 18:37:32 +00:00
committed by GitHub
83 changed files with 6238 additions and 4646 deletions

View File

@@ -100,10 +100,7 @@ from .util.network import is_private_ip_address
from .util.smsg import smsgGetID
from .interface.base import Curves
from .interface.part import PARTInterface, PARTInterfaceAnon, PARTInterfaceBlind
from .explorers import (
default_chart_api_key,
default_coingecko_api_key,
)
from .explorers import default_coingecko_api_key
from .script import OpCodes
from .messages_npb import (
ADSBidIntentAcceptMessage,
@@ -417,6 +414,21 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self._is_encrypted = None
self._is_locked = None
self._price_cache = {}
self._volume_cache = {}
self._historical_cache = {}
self._price_cache_lock = threading.Lock()
self._price_fetch_thread = None
self._price_fetch_running = False
self._last_price_fetch = 0
self._last_volume_fetch = 0
self.price_fetch_interval = self.get_int_setting(
"price_fetch_interval", 5 * 60, 60, 60 * 60
)
self.volume_fetch_interval = self.get_int_setting(
"volume_fetch_interval", 5 * 60, 60, 60 * 60
)
self._max_transient_errors = self.settings.get(
"max_transient_errors", 100
) # Number of retries before a bid will stop when encountering transient errors.
@@ -562,9 +574,55 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
random.seed(secrets.randbits(128))
self._prepare_rpc_pooling()
def _prepare_rpc_pooling(self):
if "rpc_connection_pool" not in self.settings:
self.settings["rpc_connection_pool"] = {
"enabled": True,
"max_connections_per_daemon": 5,
}
self._save_settings()
def _enable_rpc_pooling(self):
rpc_pool_settings = self.settings.get("rpc_connection_pool", {})
if rpc_pool_settings.get("enabled", False):
from basicswap import rpc
from basicswap import rpc_pool
rpc.enable_rpc_pooling(rpc_pool_settings)
rpc_pool.set_pool_logger(self.log)
def _save_settings(self):
import shutil
from basicswap import config as cfg
settings_path = os.path.join(self.data_dir, cfg.CONFIG_FILENAME)
settings_path_new = settings_path + ".new"
try:
shutil.copyfile(settings_path, settings_path + ".last")
with open(settings_path_new, "w") as fp:
json.dump(self.settings, fp, indent=4)
shutil.move(settings_path_new, settings_path)
self.log.debug("Settings saved to basicswap.json")
except Exception as e:
self.log.error(f"Failed to save settings: {str(e)}")
def finalise(self):
self.log.info("Finalising")
self._price_fetch_running = False
if self._price_fetch_thread and self._price_fetch_thread.is_alive():
self._price_fetch_thread.join(timeout=5)
self.log.info("Background price fetching stopped")
try:
from basicswap.rpc_pool import close_all_pools
close_all_pools()
except Exception as e:
self.log.debug(f"Error closing RPC pools: {e}")
try:
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
@@ -1058,6 +1116,19 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
elif self.coin_clients[coin]["connection_type"] == "passthrough":
self.coin_clients[coin]["interface"] = self.createPassthroughInterface(coin)
def _cleanupOldSettings(self):
settings_changed = False
deprecated_keys = ["chart_api_key", "chart_api_key_enc"]
for key in deprecated_keys:
if key in self.settings:
self.log.info(f"Removing deprecated setting: {key}")
self.settings.pop(key)
settings_changed = True
if settings_changed:
self._save_settings()
def start(self):
import platform
@@ -1078,6 +1149,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
"SQLite {} or higher required.".format(".".join(MIN_SQLITE_VERSION))
)
self._cleanupOldSettings()
upgradeDatabase(self, self.db_version)
upgradeDatabaseData(self, self.db_data_version)
@@ -1143,6 +1216,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.checkWalletSeed(c)
self._enable_rpc_pooling()
if "p2p_host" in self.settings:
network_key = self.getNetworkKey(1)
self._network = bsn.Network(
@@ -1225,6 +1300,13 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
else:
self.log.info("AMM autostart is disabled")
self._price_fetch_running = True
self._price_fetch_thread = threading.Thread(
target=self._backgroundPriceFetchLoop, daemon=True
)
self._price_fetch_thread.start()
self.log.info("Background price fetching started")
def stopDaemon(self, coin) -> None:
if coin in (Coins.XMR, Coins.DCR, Coins.WOW):
return
@@ -3867,7 +3949,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
# Set msg_buf.message_nets to let the remote node know what networks to respond on.
# bid.message_nets is a local field denoting the network/s to send to
if offer.smsg_payload_version > 1:
if offer.smsg_payload_version is not None and offer.smsg_payload_version > 1:
msg_buf.message_nets = self.getMessageNetsString()
return msg_buf
@@ -3917,7 +3999,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
# Set msg_buf.message_nets to let the remote node know what networks to respond on.
# bid.message_nets is a local field denoting the network/s to send to
if offer.smsg_payload_version > 1:
if offer.smsg_payload_version is not None and offer.smsg_payload_version > 1:
msg_buf.message_nets = self.getMessageNetsString()
return msg_buf
@@ -4004,7 +4086,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
# Set msg_buf.message_nets to let the remote node know what networks to respond on.
# bid.message_nets is a local field denoting the network/s to send to
if offer.smsg_payload_version > 1:
if offer.smsg_payload_version is not None and offer.smsg_payload_version > 1:
msg_buf.message_nets = self.getMessageNetsString()
return msg_buf
@@ -7908,7 +7990,17 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.add(xmr_offer, cursor)
self.notify(NT.OFFER_RECEIVED, {"offer_id": offer_id.hex()}, cursor)
self.notify(
NT.OFFER_RECEIVED,
{
"offer_id": offer_id.hex(),
"coin_from": offer_data.coin_from,
"coin_to": offer_data.coin_to,
"amount_from": offer_data.amount_from,
"amount_to": offer_data.amount_to,
},
cursor,
)
else:
existing_offer.setState(OfferStates.OFFER_RECEIVED)
existing_offer.pk_from = pk_from
@@ -11020,27 +11112,6 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
settings_copy["show_chart"] = new_value
settings_changed = True
if "chart_api_key" in data:
new_value = data["chart_api_key"]
ensure(
isinstance(new_value, str), "New chart_api_key value not a string"
)
ensure(len(new_value) <= 128, "New chart_api_key value too long")
if all(c in string.hexdigits for c in new_value):
if settings_copy.get("chart_api_key", "") != new_value:
settings_copy["chart_api_key"] = new_value
if "chart_api_key_enc" in settings_copy:
settings_copy.pop("chart_api_key_enc")
settings_changed = True
else:
# Encode value as hex to avoid escaping
new_value = new_value.encode("UTF-8").hex()
if settings_copy.get("chart_api_key_enc", "") != new_value:
settings_copy["chart_api_key_enc"] = new_value
if "chart_api_key" in settings_copy:
settings_copy.pop("chart_api_key")
settings_changed = True
if "coingecko_api_key" in data:
new_value = data["coingecko_api_key"]
ensure(
@@ -11048,7 +11119,15 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
"New coingecko_api_key value not a string",
)
ensure(len(new_value) <= 128, "New coingecko_api_keyvalue too long")
if all(c in string.hexdigits for c in new_value):
if new_value == "":
if (
"coingecko_api_key" in settings_copy
or "coingecko_api_key_enc" in settings_copy
):
settings_copy.pop("coingecko_api_key", None)
settings_copy.pop("coingecko_api_key_enc", None)
settings_changed = True
elif all(c in string.hexdigits for c in new_value):
if settings_copy.get("coingecko_api_key", "") != new_value:
settings_copy["coingecko_api_key"] = new_value
if "coingecko_api_key_enc" in settings_copy:
@@ -12301,11 +12380,15 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.closeDB(cursor, commit=False)
def getLockedState(self):
if self._is_encrypted is None or self._is_locked is None:
self._is_encrypted, self._is_locked = self.ci(
Coins.PART
).isWalletEncryptedLocked()
return self._is_encrypted, self._is_locked
try:
if self._is_encrypted is None or self._is_locked is None:
self._is_encrypted, self._is_locked = self.ci(
Coins.PART
).isWalletEncryptedLocked()
return self._is_encrypted, self._is_locked
except Exception as e:
self.log.warning(f"getLockedState failed: {e}")
return False, False
def getExchangeName(self, coin_id: int, exchange_name: str) -> str:
if coin_id == Coins.BCH:
@@ -12322,6 +12405,195 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
return chainparams[use_coinid]["name"]
def _backgroundPriceFetchLoop(self):
while self._price_fetch_running:
try:
now = int(time.time())
if now - self._last_price_fetch >= self.price_fetch_interval:
self._fetchPricesBackground()
self._last_price_fetch = now
if now - self._last_volume_fetch >= self.volume_fetch_interval:
self._fetchVolumeBackground()
self._last_volume_fetch = now
except Exception as e:
self.log.error(f"Background price/volume fetch error: {e}")
for _ in range(60):
if not self._price_fetch_running:
break
time.sleep(1)
def _fetchPricesBackground(self):
all_coins = [c for c in Coins if c in chainparams]
if not all_coins:
return
for rate_source in ["coingecko.com"]:
try:
self._fetchPricesForSource(all_coins, rate_source, Fiat.USD)
except Exception as e:
self.log.warning(
f"Background price fetch from {rate_source} failed: {e}"
)
def _fetchVolumeBackground(self):
all_coins = [c for c in Coins if c in chainparams]
if not all_coins:
return
for rate_source in ["coingecko.com"]:
try:
self._fetchVolumeForSource(all_coins, rate_source)
except Exception as e:
self.log.warning(
f"Background volume fetch from {rate_source} failed: {e}"
)
def _fetchPricesForSource(self, coins_list, rate_source, currency_to):
now = int(time.time())
headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"}
exchange_name_map = {}
coin_ids = ""
for coin_id in coins_list:
if len(coin_ids) > 0:
coin_ids += ","
exchange_name = self.getExchangeName(coin_id, rate_source)
coin_ids += exchange_name
exchange_name_map[exchange_name] = coin_id
if rate_source == "coingecko.com":
ticker_to = fiatTicker(currency_to).lower()
api_key = get_api_key_setting(
self.settings,
"coingecko_api_key",
default_coingecko_api_key,
escape=True,
)
url = f"https://api.coingecko.com/api/v3/simple/price?ids={coin_ids}&vs_currencies={ticker_to}"
if api_key != "":
url += f"&api_key={api_key}"
js = json.loads(self.readURL(url, timeout=3, headers=headers))
with self._price_cache_lock:
for k, v in js.items():
coin_id = exchange_name_map[k]
cache_key = (coin_id, currency_to, rate_source)
self._price_cache[cache_key] = {
"rate": v[ticker_to],
"timestamp": now,
}
cursor = self.openDB()
try:
update_query = """
UPDATE coinrates SET
rate=:rate,
last_updated=:last_updated
WHERE currency_from = :currency_from AND currency_to = :currency_to AND source = :rate_source
"""
insert_query = """INSERT INTO coinrates(currency_from, currency_to, rate, source, last_updated)
VALUES(:currency_from, :currency_to, :rate, :rate_source, :last_updated)"""
for k, v in js.items():
coin_id = exchange_name_map[k]
cursor.execute(
update_query,
{
"currency_from": coin_id,
"currency_to": currency_to,
"rate": v[ticker_to],
"rate_source": rate_source,
"last_updated": now,
},
)
if cursor.rowcount < 1:
cursor.execute(
insert_query,
{
"currency_from": coin_id,
"currency_to": currency_to,
"rate": v[ticker_to],
"rate_source": rate_source,
"last_updated": now,
},
)
self.commitDB()
finally:
self.closeDB(cursor, commit=False)
def _fetchVolumeForSource(self, coins_list, rate_source):
now = int(time.time())
headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"}
exchange_name_map = {}
coin_ids = ""
for coin_id in coins_list:
if len(coin_ids) > 0:
coin_ids += ","
exchange_name = self.getExchangeName(coin_id, rate_source)
coin_ids += exchange_name
exchange_name_map[exchange_name] = coin_id
if rate_source == "coingecko.com":
api_key = get_api_key_setting(
self.settings,
"coingecko_api_key",
default_coingecko_api_key,
escape=True,
)
url = f"https://api.coingecko.com/api/v3/simple/price?ids={coin_ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true"
if api_key != "":
url += f"&api_key={api_key}"
js = json.loads(self.readURL(url, timeout=3, headers=headers))
with self._price_cache_lock:
for k, v in js.items():
coin_id = exchange_name_map[k]
cache_key = (coin_id, rate_source)
volume_24h = v.get("usd_24h_vol")
price_change_24h = v.get("usd_24h_change")
self._volume_cache[cache_key] = {
"volume_24h": (
float(volume_24h) if volume_24h is not None else None
),
"price_change_24h": (
float(price_change_24h)
if price_change_24h is not None
else 0.0
),
"timestamp": now,
}
cursor = self.openDB()
try:
for k, v in js.items():
coin_id = exchange_name_map[k]
volume_24h = v.get("usd_24h_vol")
price_change_24h = v.get("usd_24h_change")
cursor.execute(
"INSERT OR REPLACE INTO coinvolume (coin_id, volume_24h, price_change_24h, source, last_updated) VALUES (:coin_id, :volume_24h, :price_change_24h, :rate_source, :last_updated)",
{
"coin_id": coin_id,
"volume_24h": (
str(volume_24h) if volume_24h is not None else "None"
),
"price_change_24h": (
str(price_change_24h)
if price_change_24h is not None
else "0.0"
),
"rate_source": rate_source,
"last_updated": now,
},
)
self.commitDB()
finally:
self.closeDB(cursor, commit=False)
def lookupFiatRates(
self,
coins_list,
@@ -12329,9 +12601,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
rate_source: str = "coingecko.com",
saved_ttl: int = 300,
):
if self.debug:
coins_list_display = ", ".join([Coins(c).name for c in coins_list])
self.log.debug(f"lookupFiatRates {coins_list_display}.")
coins_list_display = ", ".join([Coins(c).name for c in coins_list])
self.log.debug(f"lookupFiatRates {coins_list_display}.")
ensure(len(coins_list) > 0, "Must specify coin/s")
ensure(saved_ttl >= 0, "Invalid saved time")
@@ -12339,7 +12610,13 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
oldest_time_valid: int = now - saved_ttl
return_rates = {}
headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"}
with self._price_cache_lock:
for coin_id in coins_list:
cache_key = (coin_id, currency_to, rate_source)
if cache_key in self._price_cache:
cached = self._price_cache[cache_key]
if cached["timestamp"] >= oldest_time_valid:
return_rates[coin_id] = cached["rate"]
cursor = self.openDB()
try:
@@ -12365,122 +12642,188 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
rows = cursor.execute(query, parameters)
for row in rows:
return_rates[int(row[0])] = float(row[1])
coin_id = int(row[0])
if coin_id not in return_rates:
return_rates[coin_id] = float(row[1])
return return_rates
finally:
self.closeDB(cursor, commit=False)
def lookupVolume(
self,
coins_list,
rate_source: str = "coingecko.com",
saved_ttl: int = 300,
):
coins_list_display = ", ".join([Coins(c).name for c in coins_list])
self.log.debug(f"lookupVolume {coins_list_display}.")
ensure(len(coins_list) > 0, "Must specify coin/s")
ensure(saved_ttl >= 0, "Invalid saved time")
now: int = int(time.time())
oldest_time_valid: int = now - saved_ttl
return_data = {}
with self._price_cache_lock:
for coin_id in coins_list:
cache_key = (coin_id, rate_source)
if cache_key in self._volume_cache:
cached = self._volume_cache[cache_key]
if cached["timestamp"] >= oldest_time_valid:
return_data[coin_id] = {
"volume_24h": cached["volume_24h"],
"price_change_24h": cached["price_change_24h"],
}
cursor = self.openDB()
try:
parameters = {
"rate_source": rate_source,
"oldest_time_valid": oldest_time_valid,
}
coins_list_query = ""
for i, coin_id in enumerate(coins_list):
try:
_ = Coins(coin_id)
except Exception:
raise ValueError(f"Unknown coin type {coin_id}")
param_name = f"coin_{i}"
if i > 0:
coins_list_query += ","
coins_list_query += f":{param_name}"
parameters[param_name] = coin_id
query = f"SELECT coin_id, volume_24h, price_change_24h FROM coinvolume WHERE coin_id IN ({coins_list_query}) AND source = :rate_source AND last_updated >= :oldest_time_valid"
rows = cursor.execute(query, parameters)
for row in rows:
coin_id = int(row[0])
if coin_id in return_data:
continue
volume_24h = None
price_change_24h = 0.0
try:
if row[1] is not None and row[1] != "None":
volume_24h = float(row[1])
except (ValueError, TypeError):
pass
try:
if row[2] is not None and row[2] != "None":
price_change_24h = float(row[2])
except (ValueError, TypeError):
pass
return_data[coin_id] = {
"volume_24h": volume_24h,
"price_change_24h": price_change_24h,
}
return return_data
finally:
self.closeDB(cursor, commit=False)
def lookupHistoricalData(
self,
coins_list,
days: int = 1,
rate_source: str = "coingecko.com",
saved_ttl: int = 3600,
):
coins_list_display = ", ".join([Coins(c).name for c in coins_list])
self.log.debug(f"lookupHistoricalData {coins_list_display}, days={days}.")
ensure(len(coins_list) > 0, "Must specify coin/s")
ensure(saved_ttl >= 0, "Invalid saved time")
ensure(days > 0, "Days must be positive")
now: int = int(time.time())
oldest_time_valid: int = now - saved_ttl
return_data = {}
headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"}
cursor = self.openDB()
try:
parameters = {
"rate_source": rate_source,
"oldest_time_valid": oldest_time_valid,
"days": days,
}
coins_list_query = ""
for i, coin_id in enumerate(coins_list):
try:
_ = Coins(coin_id)
except Exception:
raise ValueError(f"Unknown coin type {coin_id}")
param_name = f"coin_{i}"
if i > 0:
coins_list_query += ","
coins_list_query += f":{param_name}"
parameters[param_name] = coin_id
query = f"SELECT coin_id, price_data FROM coinhistory WHERE coin_id IN ({coins_list_query}) AND days = :days AND source = :rate_source AND last_updated >= :oldest_time_valid"
rows = cursor.execute(query, parameters)
for row in rows:
return_data[int(row[0])] = json.loads(row[1])
need_coins = []
new_values = {}
exchange_name_map = {}
for coin_id in coins_list:
if coin_id not in return_rates:
if coin_id not in return_data:
need_coins.append(coin_id)
if len(need_coins) < 1:
return return_rates
return return_data
if rate_source == "coingecko.com":
ticker_to: str = fiatTicker(currency_to).lower()
# Update all requested coins
coin_ids: str = ""
for coin_id in coins_list:
if len(coin_ids) > 0:
coin_ids += ","
exchange_name: str = self.getExchangeName(coin_id, rate_source)
coin_ids += exchange_name
exchange_name_map[exchange_name] = coin_id
api_key: str = get_api_key_setting(
self.settings,
"coingecko_api_key",
default_coingecko_api_key,
escape=True,
)
url: str = (
f"https://api.coingecko.com/api/v3/simple/price?ids={coin_ids}&vs_currencies={ticker_to}"
)
if api_key != "":
url += f"&api_key={api_key}"
self.log.debug(f"lookupFiatRates: {url}")
js = json.loads(self.readURL(url, timeout=10, headers=headers))
for coin_id in need_coins:
try:
exchange_name: str = self.getExchangeName(coin_id, rate_source)
url: str = (
f"https://api.coingecko.com/api/v3/coins/{exchange_name}/market_chart?vs_currency=usd&days={days}"
)
if api_key != "":
url += f"&api_key={api_key}"
for k, v in js.items():
return_rates[int(exchange_name_map[k])] = v[ticker_to]
new_values[exchange_name_map[k]] = v[ticker_to]
elif rate_source == "cryptocompare.com":
ticker_to: str = fiatTicker(currency_to).upper()
api_key: str = get_api_key_setting(
self.settings,
"chart_api_key",
default_chart_api_key,
escape=True,
)
if len(need_coins) == 1:
coin_ticker: str = chainparams[coin_id]["ticker"]
url: str = (
f"https://min-api.cryptocompare.com/data/price?fsym={coin_ticker}&tsyms={ticker_to}"
)
self.log.debug(f"lookupFiatRates: {url}")
js = json.loads(self.readURL(url, timeout=10, headers=headers))
return_rates[int(coin_id)] = js[ticker_to]
new_values[coin_id] = js[ticker_to]
else:
coin_ids: str = ""
for coin_id in coins_list:
if len(coin_ids) > 0:
coin_ids += ","
coin_ticker: str = chainparams[coin_id]["ticker"]
coin_ids += coin_ticker
exchange_name_map[coin_ticker] = coin_id
url: str = (
f"https://min-api.cryptocompare.com/data/pricemulti?fsyms={coin_ids}&tsyms={ticker_to}"
)
self.log.debug(f"lookupFiatRates: {url}")
js = json.loads(self.readURL(url, timeout=10, headers=headers))
for k, v in js.items():
return_rates[int(exchange_name_map[k])] = v[ticker_to]
new_values[exchange_name_map[k]] = v[ticker_to]
js = json.loads(self.readURL(url, timeout=5, headers=headers))
if "prices" in js:
return_data[coin_id] = js["prices"]
new_values[coin_id] = js["prices"]
except Exception as e:
self.log.warning(
f"Could not fetch historical data for {Coins(coin_id).name}: {e}"
)
return_data[coin_id] = []
else:
raise ValueError(f"Unknown rate source {rate_source}")
if len(new_values) < 1:
return return_rates
return return_data
# ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint
update_query = """
UPDATE coinrates SET
rate=:rate,
last_updated=:last_updated
WHERE currency_from = :currency_from AND currency_to = :currency_to AND source = :rate_source
"""
insert_query = """INSERT INTO coinrates(currency_from, currency_to, rate, source, last_updated)
VALUES(:currency_from, :currency_to, :rate, :rate_source, :last_updated)"""
for k, v in new_values.items():
for coin_id, price_data in new_values.items():
cursor.execute(
update_query,
"INSERT OR REPLACE INTO coinhistory (coin_id, days, price_data, source, last_updated) VALUES (:coin_id, :days, :price_data, :rate_source, :last_updated)",
{
"currency_from": k,
"currency_to": currency_to,
"rate": v,
"coin_id": coin_id,
"days": days,
"price_data": json.dumps(price_data),
"rate_source": rate_source,
"last_updated": now,
},
)
if cursor.rowcount < 1:
cursor.execute(
insert_query,
{
"currency_from": k,
"currency_to": currency_to,
"rate": v,
"rate_source": rate_source,
"last_updated": now,
},
)
self.commitDB()
return return_rates
return return_data
finally:
self.closeDB(cursor, commit=False)
@@ -12502,14 +12845,35 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if rate_sources.get("coingecko.com", True):
try:
js = self.lookupFiatRates([int(coin_from), int(coin_to)])
rate = float(js[int(coin_from)]) / float(js[int(coin_to)])
js["rate_inferred"] = ci_to.format_amount(rate, conv_int=True, r=1)
price_coin_from = int(coin_from)
price_coin_to = int(coin_to)
js[name_from] = {"usd": js[int(coin_from)]}
js.pop(int(coin_from))
js[name_to] = {"usd": js[int(coin_to)]}
js.pop(int(coin_to))
if price_coin_from in (Coins.PART_BLIND, Coins.PART_ANON):
price_coin_from = Coins.PART
elif price_coin_from == Coins.LTC_MWEB:
price_coin_from = Coins.LTC
if price_coin_to in (Coins.PART_BLIND, Coins.PART_ANON):
price_coin_to = Coins.PART
elif price_coin_to == Coins.LTC_MWEB:
price_coin_to = Coins.LTC
if price_coin_from == price_coin_to:
rate = 1.0
js = self.lookupFiatRates([price_coin_from])
usd_price = js[price_coin_from]
js["rate_inferred"] = ci_to.format_amount(rate, conv_int=True, r=1)
js[name_from] = {"usd": usd_price}
js[name_to] = {"usd": usd_price}
js.pop(price_coin_from)
else:
js = self.lookupFiatRates([price_coin_from, price_coin_to])
rate = float(js[price_coin_from]) / float(js[price_coin_to])
js["rate_inferred"] = ci_to.format_amount(rate, conv_int=True, r=1)
js[name_from] = {"usd": js[price_coin_from]}
js.pop(price_coin_from)
js[name_to] = {"usd": js[price_coin_to]}
js.pop(price_coin_to)
rv["coingecko"] = js
except Exception as e:

View File

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

View File

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

View File

@@ -8,9 +8,6 @@
import json
default_chart_api_key = (
"95dd900af910656e0e17c41f2ddc5dba77d01bf8b0e7d2787634a16bd976c553"
)
default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj"

View File

@@ -14,7 +14,7 @@ import threading
import http.client
import base64
from http.server import BaseHTTPRequestHandler, HTTPServer
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from jinja2 import Environment, PackageLoader
from socket import error as SocketError
from urllib import parse
@@ -169,15 +169,16 @@ class HttpHandler(BaseHTTPRequestHandler):
if not session_id:
return False
session_data = self.server.active_sessions.get(session_id)
if session_data and session_data["expires"] > datetime.now(timezone.utc):
session_data["expires"] = datetime.now(timezone.utc) + timedelta(
minutes=SESSION_DURATION_MINUTES
)
return True
with self.server.session_lock:
session_data = self.server.active_sessions.get(session_id)
if session_data and session_data["expires"] > datetime.now(timezone.utc):
session_data["expires"] = datetime.now(timezone.utc) + timedelta(
minutes=SESSION_DURATION_MINUTES
)
return True
if session_id in self.server.active_sessions:
del self.server.active_sessions[session_id]
if session_id in self.server.active_sessions:
del self.server.active_sessions[session_id]
return False
def log_error(self, format, *args):
@@ -195,10 +196,11 @@ class HttpHandler(BaseHTTPRequestHandler):
return None
form_data = parse.parse_qs(post_string)
form_id = form_data[b"formid"][0].decode("utf-8")
if self.server.last_form_id.get(name, None) == form_id:
messages.append("Prevented double submit for form {}.".format(form_id))
return None
self.server.last_form_id[name] = form_id
with self.server.form_id_lock:
if self.server.last_form_id.get(name, None) == form_id:
messages.append("Prevented double submit for form {}.".format(form_id))
return None
self.server.last_form_id[name] = form_id
return form_data
def render_template(
@@ -216,43 +218,48 @@ class HttpHandler(BaseHTTPRequestHandler):
args_dict["debug_mode"] = True
if swap_client.debug_ui:
args_dict["debug_ui_mode"] = True
if swap_client.use_tor_proxy:
args_dict["use_tor_proxy"] = True
is_authenticated = self.is_authenticated() or not swap_client.settings.get(
"client_auth_hash"
)
if is_authenticated:
if swap_client.use_tor_proxy:
args_dict["use_tor_proxy"] = True
try:
tor_state = get_tor_established_state(swap_client)
args_dict["tor_established"] = True if tor_state == "1" else False
except Exception:
args_dict["tor_established"] = False
from .ui.page_amm import get_amm_status, get_amm_active_count
try:
tor_state = get_tor_established_state(swap_client)
args_dict["tor_established"] = True if tor_state == "1" else False
except Exception as e:
args_dict["tor_established"] = False
if swap_client.debug:
swap_client.log.error(f"Error getting Tor state: {str(e)}")
swap_client.log.error(traceback.format_exc())
args_dict["current_status"] = get_amm_status()
args_dict["amm_active_count"] = get_amm_active_count(swap_client)
except Exception:
args_dict["current_status"] = "stopped"
args_dict["amm_active_count"] = 0
from .ui.page_amm import get_amm_status, get_amm_active_count
try:
args_dict["current_status"] = get_amm_status()
args_dict["amm_active_count"] = get_amm_active_count(swap_client)
except Exception as e:
args_dict["current_status"] = "stopped"
if swap_client._show_notifications:
args_dict["notifications"] = swap_client.getNotifications()
else:
args_dict["current_status"] = "unknown"
args_dict["amm_active_count"] = 0
if swap_client.debug:
swap_client.log.error(f"Error getting AMM state: {str(e)}")
swap_client.log.error(traceback.format_exc())
if swap_client._show_notifications:
args_dict["notifications"] = swap_client.getNotifications()
if "messages" in args_dict:
messages_with_ids = []
for msg in args_dict["messages"]:
messages_with_ids.append((self.server.msg_id_counter, msg))
self.server.msg_id_counter += 1
with self.server.msg_id_lock:
for msg in args_dict["messages"]:
messages_with_ids.append((self.server.msg_id_counter, msg))
self.server.msg_id_counter += 1
args_dict["messages"] = messages_with_ids
if "err_messages" in args_dict:
err_messages_with_ids = []
for msg in args_dict["err_messages"]:
err_messages_with_ids.append((self.server.msg_id_counter, msg))
self.server.msg_id_counter += 1
with self.server.msg_id_lock:
for msg in args_dict["err_messages"]:
err_messages_with_ids.append((self.server.msg_id_counter, msg))
self.server.msg_id_counter += 1
args_dict["err_messages"] = err_messages_with_ids
if self.path:
@@ -266,15 +273,27 @@ class HttpHandler(BaseHTTPRequestHandler):
args_dict["current_page"] = "index"
shutdown_token = os.urandom(8).hex()
self.server.session_tokens["shutdown"] = shutdown_token
with self.server.session_lock:
self.server.session_tokens["shutdown"] = shutdown_token
args_dict["shutdown_token"] = shutdown_token
encrypted, locked = swap_client.getLockedState()
args_dict["encrypted"] = encrypted
args_dict["locked"] = locked
if is_authenticated:
try:
encrypted, locked = swap_client.getLockedState()
args_dict["encrypted"] = encrypted
args_dict["locked"] = locked
except Exception as e:
args_dict["encrypted"] = False
args_dict["locked"] = False
if swap_client.debug:
swap_client.log.warning(f"Could not get wallet locked state: {e}")
else:
args_dict["encrypted"] = args_dict.get("encrypted", False)
args_dict["locked"] = args_dict.get("locked", False)
if self.server.msg_id_counter >= 0x7FFFFFFF:
self.server.msg_id_counter = 0
with self.server.msg_id_lock:
if self.server.msg_id_counter >= 0x7FFFFFFF:
self.server.msg_id_counter = 0
args_dict["version"] = version
@@ -364,7 +383,8 @@ class HttpHandler(BaseHTTPRequestHandler):
expires = datetime.now(timezone.utc) + timedelta(
minutes=SESSION_DURATION_MINUTES
)
self.server.active_sessions[session_id] = {"expires": expires}
with self.server.session_lock:
self.server.active_sessions[session_id] = {"expires": expires}
cookie_header = self._set_session_cookie(session_id)
if is_json_request:
@@ -628,13 +648,15 @@ class HttpHandler(BaseHTTPRequestHandler):
if len(url_split) > 2:
token = url_split[2]
expect_token = self.server.session_tokens.get("shutdown", None)
with self.server.session_lock:
expect_token = self.server.session_tokens.get("shutdown", None)
if token != expect_token:
return self.page_info("Unexpected token, still running.")
session_id = self._get_session_cookie()
if session_id and session_id in self.server.active_sessions:
del self.server.active_sessions[session_id]
with self.server.session_lock:
if session_id and session_id in self.server.active_sessions:
del self.server.active_sessions[session_id]
clear_cookie_header = self._clear_session_cookie()
extra_headers.append(clear_cookie_header)
@@ -935,22 +957,28 @@ class HttpHandler(BaseHTTPRequestHandler):
return self.page_error(str(ex))
def do_GET(self):
response = self.handle_http(200, self.path)
try:
self.wfile.write(response)
except SocketError as e:
self.server.swap_client.log.debug(f"do_GET SocketError {e}")
response = self.handle_http(200, self.path)
try:
self.wfile.write(response)
except SocketError as e:
self.server.swap_client.log.debug(f"do_GET SocketError {e}")
finally:
pass
def do_POST(self):
content_length = int(self.headers.get("Content-Length", 0))
post_string = self.rfile.read(content_length)
is_json = True if "json" in self.headers.get("Content-Type", "") else False
response = self.handle_http(200, self.path, post_string, is_json)
try:
self.wfile.write(response)
except SocketError as e:
self.server.swap_client.log.debug(f"do_POST SocketError {e}")
content_length = int(self.headers.get("Content-Length", 0))
post_string = self.rfile.read(content_length)
is_json = True if "json" in self.headers.get("Content-Type", "") else False
response = self.handle_http(200, self.path, post_string, is_json)
try:
self.wfile.write(response)
except SocketError as e:
self.server.swap_client.log.debug(f"do_POST SocketError {e}")
finally:
pass
def do_HEAD(self):
self.putHeaders(200, "text/html")
@@ -963,7 +991,9 @@ class HttpHandler(BaseHTTPRequestHandler):
self.end_headers()
class HttpThread(threading.Thread, HTTPServer):
class HttpThread(threading.Thread, ThreadingHTTPServer):
daemon_threads = True
def __init__(self, host_name, port_no, allow_cors, swap_client):
threading.Thread.__init__(self)
@@ -979,8 +1009,15 @@ class HttpThread(threading.Thread, HTTPServer):
self.env = env
self.msg_id_counter = 0
self.session_lock = threading.Lock()
self.form_id_lock = threading.Lock()
self.msg_id_lock = threading.Lock()
self.timeout = 60
HTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
ThreadingHTTPServer.__init__(self, (self.host_name, self.port_no), HttpHandler)
if swap_client.debug:
swap_client.log.info("HTTP server initialized with threading support")
def stop(self):
self.stop_event.set()

View File

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

View File

@@ -6,8 +6,10 @@
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import logging
import traceback
import urllib
import http.client
from xmlrpc.client import (
Fault,
Transport,
@@ -15,6 +17,35 @@ from xmlrpc.client import (
)
from .util import jsonDecimal
_use_rpc_pooling = False
_rpc_pool_settings = {}
def enable_rpc_pooling(settings):
global _use_rpc_pooling, _rpc_pool_settings
_use_rpc_pooling = settings.get("enabled", False)
_rpc_pool_settings = settings
class TimeoutTransport(Transport):
def __init__(self, timeout=10, *args, **kwargs):
self.timeout = timeout
super().__init__(*args, **kwargs)
def make_connection(self, host):
conn = http.client.HTTPConnection(host, timeout=self.timeout)
return conn
class TimeoutSafeTransport(SafeTransport):
def __init__(self, timeout=10, *args, **kwargs):
self.timeout = timeout
super().__init__(*args, **kwargs)
def make_connection(self, host):
conn = http.client.HTTPSConnection(host, timeout=self.timeout)
return conn
class Jsonrpc:
# __getattr__ complicates extending ServerProxy
@@ -29,22 +60,40 @@ class Jsonrpc:
use_builtin_types=False,
*,
context=None,
timeout=10,
):
# establish a "logical" server connection
# get the url
parsed = urllib.parse.urlparse(uri)
if parsed.scheme not in ("http", "https"):
raise OSError("unsupported XML-RPC protocol")
self.__host = parsed.netloc
self.__auth = None
if "@" in parsed.netloc:
auth_part, host_port = parsed.netloc.rsplit("@", 1)
self.__host = host_port
if ":" in auth_part:
import base64
auth_bytes = auth_part.encode("utf-8")
auth_b64 = base64.b64encode(auth_bytes).decode("ascii")
self.__auth = f"Basic {auth_b64}"
else:
self.__host = parsed.netloc
if not self.__host:
raise ValueError(f"Invalid or empty hostname in URI: {uri}")
self.__handler = parsed.path
if not self.__handler:
self.__handler = "/RPC2"
if transport is None:
handler = SafeTransport if parsed.scheme == "https" else Transport
handler = (
TimeoutSafeTransport if parsed.scheme == "https" else TimeoutTransport
)
extra_kwargs = {}
transport = handler(
timeout=timeout,
use_datetime=use_datetime,
use_builtin_types=use_builtin_types,
**extra_kwargs,
@@ -62,6 +111,7 @@ class Jsonrpc:
self.__transport.close()
def json_request(self, method, params):
connection = None
try:
connection = self.__transport.make_connection(self.__host)
headers = self.__transport._extra_headers[:]
@@ -71,6 +121,10 @@ class Jsonrpc:
connection.putrequest("POST", self.__handler)
headers.append(("Content-Type", "application/json"))
headers.append(("User-Agent", "jsonrpc"))
if self.__auth:
headers.append(("Authorization", self.__auth))
self.__transport.send_headers(connection, headers)
self.__transport.send_content(
connection,
@@ -79,18 +133,29 @@ class Jsonrpc:
self.__request_id += 1
resp = connection.getresponse()
return resp.read()
result = resp.read()
connection.close()
return result
except Fault:
raise
except Exception:
# All unexpected errors leave connection in
# a strange state, so we clear it.
self.__transport.close()
raise
finally:
if connection is not None:
try:
connection.close()
except Exception:
pass
def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
if _use_rpc_pooling:
return callrpc_pooled(rpc_port, auth, method, params, wallet, host)
try:
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
if wallet is not None:
@@ -101,7 +166,6 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
x.close()
r = json.loads(v.decode("utf-8"))
except Exception as ex:
traceback.print_exc()
raise ValueError(f"RPC server error: {ex}, method: {method}")
if "error" in r and r["error"] is not None:
@@ -110,6 +174,62 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
return r["result"]
def callrpc_pooled(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
from .rpc_pool import get_rpc_pool
import http.client
import socket
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
if wallet is not None:
url += "wallet/" + urllib.parse.quote(wallet)
max_connections = _rpc_pool_settings.get("max_connections_per_daemon", 5)
pool = get_rpc_pool(url, max_connections)
max_retries = 2
for attempt in range(max_retries):
conn = pool.get_connection()
try:
v = conn.json_request(method, params)
r = json.loads(v.decode("utf-8"))
if "error" in r and r["error"] is not None:
pool.discard_connection(conn)
raise ValueError("RPC error " + str(r["error"]))
pool.return_connection(conn)
return r["result"]
except (
http.client.RemoteDisconnected,
http.client.IncompleteRead,
http.client.BadStatusLine,
ConnectionError,
ConnectionResetError,
ConnectionAbortedError,
BrokenPipeError,
TimeoutError,
socket.timeout,
socket.error,
OSError,
) as ex:
pool.discard_connection(conn)
if attempt < max_retries - 1:
continue
logging.warning(
f"RPC server error after {max_retries} attempts: {ex}, method: {method}"
)
raise ValueError(f"RPC server error: {ex}, method: {method}")
except ValueError:
raise
except Exception as ex:
pool.discard_connection(conn)
logging.error(f"Unexpected RPC error: {ex}, method: {method}")
raise ValueError(f"RPC server error: {ex}, method: {method}")
def openrpc(rpc_port, auth, wallet=None, host="127.0.0.1"):
try:
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
@@ -142,5 +262,6 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
def escape_rpcauth(auth_str: str) -> str:
username, password = auth_str.split(":", 1)
username = urllib.parse.quote(username, safe="")
password = urllib.parse.quote(password, safe="")
return f"{username}:{password}"

131
basicswap/rpc_pool.py Normal file
View File

@@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2025 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import queue
import threading
import time
from basicswap.rpc import Jsonrpc
class RPCConnectionPool:
def __init__(
self, url, max_connections=5, timeout=10, logger=None, max_idle_time=300
):
self.url = url
self.max_connections = max_connections
self.timeout = timeout
self.logger = logger
self.max_idle_time = max_idle_time
self._pool = queue.Queue(maxsize=max_connections)
self._lock = threading.Lock()
self._created_connections = 0
self._connection_timestamps = {}
def get_connection(self):
try:
conn_data = self._pool.get(block=False)
conn, timestamp = (
conn_data if isinstance(conn_data, tuple) else (conn_data, time.time())
)
if time.time() - timestamp > self.max_idle_time:
if self.logger:
self.logger.debug(
f"RPC pool: discarding stale connection (idle for {time.time() - timestamp:.1f}s)"
)
conn.close()
with self._lock:
if self._created_connections > 0:
self._created_connections -= 1
return self._create_new_connection()
return conn
except queue.Empty:
return self._create_new_connection()
def _create_new_connection(self):
with self._lock:
if self._created_connections < self.max_connections:
self._created_connections += 1
return Jsonrpc(self.url)
try:
conn_data = self._pool.get(block=True, timeout=self.timeout)
conn, timestamp = (
conn_data if isinstance(conn_data, tuple) else (conn_data, time.time())
)
if time.time() - timestamp > self.max_idle_time:
if self.logger:
self.logger.debug(
f"RPC pool: discarding stale connection (idle for {time.time() - timestamp:.1f}s)"
)
conn.close()
with self._lock:
if self._created_connections > 0:
self._created_connections -= 1
return Jsonrpc(self.url)
return conn
except queue.Empty:
if self.logger:
self.logger.warning(
f"RPC pool: timeout waiting for connection, creating temporary connection for {self.url}"
)
return Jsonrpc(self.url)
def return_connection(self, conn):
try:
self._pool.put((conn, time.time()), block=False)
except queue.Full:
conn.close()
with self._lock:
if self._created_connections > 0:
self._created_connections -= 1
def discard_connection(self, conn):
conn.close()
with self._lock:
if self._created_connections > 0:
self._created_connections -= 1
def close_all(self):
while not self._pool.empty():
try:
conn_data = self._pool.get(block=False)
conn = conn_data[0] if isinstance(conn_data, tuple) else conn_data
conn.close()
except queue.Empty:
break
with self._lock:
self._created_connections = 0
self._connection_timestamps.clear()
_rpc_pools = {}
_pool_lock = threading.Lock()
_pool_logger = None
def set_pool_logger(logger):
global _pool_logger
_pool_logger = logger
def get_rpc_pool(url, max_connections=5):
with _pool_lock:
if url not in _rpc_pools:
_rpc_pools[url] = RPCConnectionPool(
url, max_connections, logger=_pool_logger
)
return _rpc_pools[url]
def close_all_pools():
with _pool_lock:
for pool in _rpc_pools.values():
pool.close_all()
_rpc_pools.clear()

View File

@@ -4,34 +4,34 @@ const ApiManager = (function() {
isInitialized: false
};
const config = {
requestTimeout: 60000,
retryDelays: [5000, 15000, 30000],
rateLimits: {
coingecko: {
requestsPerMinute: 50,
minInterval: 1200
},
cryptocompare: {
requestsPerMinute: 30,
minInterval: 2000
function getConfig() {
return window.config || window.ConfigManager || {
requestTimeout: 60000,
retryDelays: [5000, 15000, 30000],
rateLimits: {
coingecko: { requestsPerMinute: 50, minInterval: 1200 }
}
}
};
};
}
const rateLimiter = {
lastRequestTime: {},
minRequestInterval: {
coingecko: 1200,
cryptocompare: 2000
},
requestQueue: {},
retryDelays: [5000, 15000, 30000],
getMinInterval: function(apiName) {
const config = getConfig();
return config.rateLimits?.[apiName]?.minInterval || 1200;
},
getRetryDelays: function() {
const config = getConfig();
return config.retryDelays || [5000, 15000, 30000];
},
canMakeRequest: function(apiName) {
const now = Date.now();
const lastRequest = this.lastRequestTime[apiName] || 0;
return (now - lastRequest) >= this.minRequestInterval[apiName];
return (now - lastRequest) >= this.getMinInterval(apiName);
},
updateLastRequestTime: function(apiName) {
@@ -41,7 +41,7 @@ const ApiManager = (function() {
getWaitTime: function(apiName) {
const now = Date.now();
const lastRequest = this.lastRequestTime[apiName] || 0;
return Math.max(0, this.minRequestInterval[apiName] - (now - lastRequest));
return Math.max(0, this.getMinInterval(apiName) - (now - lastRequest));
},
queueRequest: async function(apiName, requestFn, retryCount = 0) {
@@ -55,29 +55,30 @@ const ApiManager = (function() {
const executeRequest = async () => {
const waitTime = this.getWaitTime(apiName);
if (waitTime > 0) {
await new Promise(resolve => setTimeout(resolve, waitTime));
await new Promise(resolve => CleanupManager.setTimeout(resolve, waitTime));
}
try {
this.updateLastRequestTime(apiName);
return await requestFn();
} catch (error) {
if (error.message.includes('429') && retryCount < this.retryDelays.length) {
const delay = this.retryDelays[retryCount];
const retryDelays = this.getRetryDelays();
if (error.message.includes('429') && retryCount < retryDelays.length) {
const delay = retryDelays[retryCount];
console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
}
if ((error.message.includes('timeout') || error.name === 'NetworkError') &&
retryCount < this.retryDelays.length) {
const delay = this.retryDelays[retryCount];
retryCount < retryDelays.length) {
const delay = retryDelays[retryCount];
console.warn(`Request failed, retrying in ${delay/1000} seconds...`, {
apiName,
retryCount,
error: error.message
});
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
}
@@ -118,19 +119,7 @@ const ApiManager = (function() {
}
if (options.config) {
Object.assign(config, options.config);
}
if (config.rateLimits) {
Object.keys(config.rateLimits).forEach(api => {
if (config.rateLimits[api].minInterval) {
rateLimiter.minRequestInterval[api] = config.rateLimits[api].minInterval;
}
});
}
if (config.retryDelays) {
rateLimiter.retryDelays = [...config.retryDelays];
console.log('[ApiManager] Config options provided, but using ConfigManager instead');
}
if (window.CleanupManager) {
@@ -143,6 +132,31 @@ const ApiManager = (function() {
},
makeRequest: async function(url, method = 'GET', headers = {}, body = null) {
if (window.ErrorHandler) {
return window.ErrorHandler.safeExecuteAsync(async () => {
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
...headers
},
signal: AbortSignal.timeout(getConfig().requestTimeout || 60000)
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}, `ApiManager.makeRequest(${url})`, null);
}
try {
const options = {
method: method,
@@ -150,7 +164,7 @@ const ApiManager = (function() {
'Content-Type': 'application/json',
...headers
},
signal: AbortSignal.timeout(config.requestTimeout)
signal: AbortSignal.timeout(getConfig().requestTimeout || 60000)
};
if (body) {
@@ -233,11 +247,8 @@ const ApiManager = (function() {
.join(',') :
'bitcoin,monero,particl,bitcoincash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin';
//console.log('Fetching coin prices for:', coins);
const response = await this.fetchCoinPrices(coins);
//console.log('Full API response:', response);
if (!response || typeof response !== 'object') {
throw new Error('Invalid response type');
}
@@ -260,80 +271,38 @@ const ApiManager = (function() {
fetchVolumeData: async function() {
return this.rateLimiter.queueRequest('coingecko', async () => {
try {
let coinList = (window.config && window.config.coins) ?
window.config.coins
.filter(coin => coin.usesCoinGecko)
.map(coin => {
return window.config.getCoinBackendId ?
window.config.getCoinBackendId(coin.name) :
(typeof getCoinBackendId === 'function' ?
getCoinBackendId(coin.name) : coin.name.toLowerCase());
})
.join(',') :
'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin,wownero';
const coinSymbols = window.CoinManager
? window.CoinManager.getAllCoins().map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
: (window.config.coins
? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
: ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']);
if (!coinList.includes('zcoin') && coinList.includes('firo')) {
coinList = coinList + ',zcoin';
}
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coinList}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`;
const response = await this.makePostRequest(url, {
'User-Agent': 'Mozilla/5.0',
'Accept': 'application/json'
const response = await this.makeRequest('/json/coinvolume', 'POST', {}, {
coins: coinSymbols.join(','),
source: 'coingecko.com',
ttl: 300
});
if (!response || typeof response !== 'object') {
throw new Error('Invalid response from CoinGecko API');
if (!response) {
console.error('No response from backend');
throw new Error('Invalid response from backend');
}
if (!response.data) {
console.error('Response missing data field:', response);
throw new Error('Invalid response from backend');
}
const volumeData = {};
Object.entries(response).forEach(([coinId, data]) => {
if (data && data.usd_24h_vol !== undefined) {
volumeData[coinId] = {
total_volume: data.usd_24h_vol || 0,
price_change_percentage_24h: data.usd_24h_change || 0
};
}
Object.entries(response.data).forEach(([coinSymbol, data]) => {
const coinKey = coinSymbol.toLowerCase();
volumeData[coinKey] = {
total_volume: (data.volume_24h !== undefined && data.volume_24h !== null) ? data.volume_24h : null,
price_change_percentage_24h: data.price_change_24h || 0
};
});
const coinMappings = {
'firo': ['firo', 'zcoin'],
'zcoin': ['zcoin', 'firo'],
'bitcoin-cash': ['bitcoin-cash', 'bitcoincash', 'bch'],
'bitcoincash': ['bitcoincash', 'bitcoin-cash', 'bch'],
'particl': ['particl', 'part']
};
if (response['zcoin'] && (!volumeData['firo'] || volumeData['firo'].total_volume === 0)) {
volumeData['firo'] = {
total_volume: response['zcoin'].usd_24h_vol || 0,
price_change_percentage_24h: response['zcoin'].usd_24h_change || 0
};
}
if (response['bitcoin-cash'] && (!volumeData['bitcoincash'] || volumeData['bitcoincash'].total_volume === 0)) {
volumeData['bitcoincash'] = {
total_volume: response['bitcoin-cash'].usd_24h_vol || 0,
price_change_percentage_24h: response['bitcoin-cash'].usd_24h_change || 0
};
}
for (const [mainCoin, alternativeIds] of Object.entries(coinMappings)) {
if (!volumeData[mainCoin] || volumeData[mainCoin].total_volume === 0) {
for (const altId of alternativeIds) {
if (response[altId] && response[altId].usd_24h_vol) {
volumeData[mainCoin] = {
total_volume: response[altId].usd_24h_vol,
price_change_percentage_24h: response[altId].usd_24h_change || 0
};
break;
}
}
}
}
return volumeData;
} catch (error) {
console.error("Error fetching volume data:", error);
@@ -342,104 +311,45 @@ const ApiManager = (function() {
});
},
fetchCryptoCompareData: function(coin) {
return this.rateLimiter.queueRequest('cryptocompare', async () => {
try {
const apiKey = window.config?.apiKeys?.cryptoCompare || '';
const url = `https://min-api.cryptocompare.com/data/pricemultifull?fsyms=${coin}&tsyms=USD,BTC&api_key=${apiKey}`;
const headers = {
'User-Agent': 'Mozilla/5.0',
'Accept': 'application/json'
};
return await this.makePostRequest(url, headers);
} catch (error) {
console.error(`CryptoCompare request failed for ${coin}:`, error);
throw error;
}
});
},
fetchHistoricalData: async function(coinSymbols, resolution = 'day') {
if (!Array.isArray(coinSymbols)) {
coinSymbols = [coinSymbols];
}
const results = {};
const fetchPromises = coinSymbols.map(async coin => {
let useCoinGecko = false;
let coingeckoId = null;
if (window.CoinManager) {
const coinConfig = window.CoinManager.getCoinByAnyIdentifier(coin);
if (coinConfig) {
useCoinGecko = !coinConfig.usesCryptoCompare || coin === 'PART';
coingeckoId = coinConfig.coingeckoId;
return this.rateLimiter.queueRequest('coingecko', async () => {
try {
let days;
if (resolution === 'day') {
days = 1;
} else if (resolution === 'year') {
days = 365;
} else {
days = 180;
}
} else {
const coinGeckoCoins = {
'WOW': 'wownero',
'PART': 'particl',
'BTC': 'bitcoin'
};
if (coinGeckoCoins[coin]) {
useCoinGecko = true;
coingeckoId = coinGeckoCoins[coin];
const response = await this.makeRequest('/json/coinhistory', 'POST', {}, {
coins: coinSymbols.join(','),
days: days,
source: 'coingecko.com',
ttl: 3600
});
if (!response) {
console.error('No response from backend');
throw new Error('Invalid response from backend');
}
}
if (useCoinGecko && coingeckoId) {
return this.rateLimiter.queueRequest('coingecko', async () => {
let days;
if (resolution === 'day') {
days = 1;
} else if (resolution === 'year') {
days = 365;
} else {
days = 180;
}
const url = `https://api.coingecko.com/api/v3/coins/${coingeckoId}/market_chart?vs_currency=usd&days=${days}`;
try {
const response = await this.makePostRequest(url);
if (response && response.prices) {
results[coin] = response.prices;
}
} catch (error) {
console.error(`Error fetching CoinGecko data for ${coin}:`, error);
throw error;
}
});
} else {
return this.rateLimiter.queueRequest('cryptocompare', async () => {
try {
const apiKey = window.config?.apiKeys?.cryptoCompare || '';
let url;
if (!response.data) {
console.error('Response missing data field:', response);
throw new Error('Invalid response from backend');
}
if (resolution === 'day') {
url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coin}&tsym=USD&limit=24&api_key=${apiKey}`;
} else if (resolution === 'year') {
url = `https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coin}&tsym=USD&limit=365&api_key=${apiKey}`;
} else {
url = `https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coin}&tsym=USD&limit=180&api_key=${apiKey}`;
}
const response = await this.makePostRequest(url);
if (response.Response === "Error") {
console.error(`API Error for ${coin}:`, response.Message);
throw new Error(response.Message);
} else if (response.Data && response.Data.Data) {
results[coin] = response.Data;
}
} catch (error) {
console.error(`Error fetching CryptoCompare data for ${coin}:`, error);
throw error;
}
});
return response.data;
} catch (error) {
console.error('Error fetching historical data:', error);
throw error;
}
});
await Promise.all(fetchPromises);
return results;
},
dispose: function() {
@@ -453,17 +363,6 @@ const ApiManager = (function() {
return publicAPI;
})();
function getCoinBackendId(coinName) {
const nameMap = {
'bitcoin-cash': 'bitcoincash',
'bitcoin cash': 'bitcoincash',
'firo': 'zcoin',
'zcoin': 'zcoin',
'bitcoincash': 'bitcoin-cash'
};
return nameMap[coinName.toLowerCase()] || coinName.toLowerCase();
}
window.Api = ApiManager;
window.ApiManager = ApiManager;
@@ -474,5 +373,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('ApiManager initialized with methods:', Object.keys(ApiManager));
console.log('ApiManager initialized');

View File

@@ -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);
}

View File

@@ -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');

View File

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

View File

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

View File

@@ -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');

View File

@@ -35,38 +35,22 @@ const ConfigManager = (function() {
},
itemsPerPage: 50,
apiEndpoints: {
cryptoCompare: 'https://min-api.cryptocompare.com/data/pricemultifull',
coinGecko: 'https://api.coingecko.com/api/v3',
cryptoCompareHistorical: 'https://min-api.cryptocompare.com/data/v2/histoday',
cryptoCompareHourly: 'https://min-api.cryptocompare.com/data/v2/histohour',
volumeEndpoint: 'https://api.coingecko.com/api/v3/simple/price'
},
rateLimits: {
coingecko: {
requestsPerMinute: 50,
minInterval: 1200
},
cryptocompare: {
requestsPerMinute: 30,
minInterval: 2000
}
},
retryDelays: [5000, 15000, 30000],
get coins() {
return window.CoinManager ? window.CoinManager.getAllCoins() : [
{ symbol: 'BTC', name: 'bitcoin', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'XMR', name: 'monero', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'PART', name: 'particl', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'BCH', name: 'bitcoincash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'PIVX', name: 'pivx', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'FIRO', name: 'firo', displayName: 'Firo', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'DASH', name: 'dash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'LTC', name: 'litecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'DOGE', name: 'dogecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'DCR', name: 'decred', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'NMC', name: 'namecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'WOW', name: 'wownero', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 }
];
if (window.CoinManager) {
return window.CoinManager.getAllCoins();
}
console.warn('[ConfigManager] CoinManager not available, returning empty array');
return [];
},
chartConfig: {
colors: {
@@ -108,12 +92,10 @@ const ConfigManager = (function() {
if (typeof window.getAPIKeys === 'function') {
const apiKeys = window.getAPIKeys();
return {
cryptoCompare: apiKeys.cryptoCompare || '',
coinGecko: apiKeys.coinGecko || ''
};
}
return {
cryptoCompare: '',
coinGecko: ''
};
},
@@ -122,55 +104,20 @@ const ConfigManager = (function() {
if (window.CoinManager) {
return window.CoinManager.getPriceKey(coinName);
}
const nameMap = {
'bitcoin-cash': 'bitcoincash',
'bitcoin cash': 'bitcoincash',
'firo': 'firo',
'zcoin': 'firo',
'bitcoincash': 'bitcoin-cash'
};
const lowerCoinName = typeof coinName === 'string' ? coinName.toLowerCase() : '';
return nameMap[lowerCoinName] || lowerCoinName;
if (window.CoinUtils) {
return window.CoinUtils.normalizeCoinName(coinName);
}
return typeof coinName === 'string' ? coinName.toLowerCase() : '';
},
coinMatches: function(offerCoin, filterCoin) {
if (!offerCoin || !filterCoin) return false;
if (window.CoinManager) {
return window.CoinManager.coinMatches(offerCoin, filterCoin);
}
offerCoin = offerCoin.toLowerCase();
filterCoin = filterCoin.toLowerCase();
if (offerCoin === filterCoin) return true;
if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
(filterCoin === 'firo' || filterCoin === 'zcoin')) {
return true;
if (window.CoinUtils) {
return window.CoinUtils.isSameCoin(offerCoin, filterCoin);
}
if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
(offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
return true;
}
const particlVariants = ['particl', 'particl anon', 'particl blind'];
if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) {
return true;
}
if (filterCoin.includes(' ') || offerCoin.includes(' ')) {
const filterFirstWord = filterCoin.split(' ')[0];
const offerFirstWord = offerCoin.split(' ')[0];
if (filterFirstWord === 'bitcoin' && offerFirstWord === 'bitcoin') {
const filterHasCash = filterCoin.includes('cash');
const offerHasCash = offerCoin.includes('cash');
return filterHasCash === offerHasCash;
}
if (filterFirstWord === offerFirstWord && filterFirstWord.length > 4) {
return true;
}
}
if (particlVariants.includes(filterCoin)) {
return offerCoin === filterCoin;
}
return false;
return offerCoin.toLowerCase() === filterCoin.toLowerCase();
},
update: function(path, value) {
const parts = path.split('.');
@@ -229,7 +176,7 @@ const ConfigManager = (function() {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
timeoutId = CleanupManager.setTimeout(() => func(...args), delay);
};
},
formatTimeLeft: function(timestamp) {

View File

@@ -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);
});
}
})();

View File

@@ -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');

View File

@@ -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);
})();

View File

@@ -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;
})();

View File

@@ -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');

View File

@@ -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');

View File

@@ -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(
'<img src="/static/images/coins/bitcoin.svg" class="w-4 h-4 inline mr-1" alt="BTC" onerror="this.style.display=\'none\'">1.00000000 BTC → <img src="/static/images/coins/monero.svg" class="w-4 h-4 inline mr-1" alt="XMR" onerror="this.style.display=\'none\'">15.50000000 XMR',
'New Network Offer',
'new_offer',
{
offerId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66',
subtitle: 'New offer • Rate: 1 BTC = 15.50000000 XMR',
subtitle: `<img src="/static/images/coins/${btcIcon}" class="w-4 h-4 inline mr-1" alt="BTC" onerror="this.style.display='none'">1.00000000 BTC → <img src="/static/images/coins/${xmrIcon}" class="w-4 h-4 inline mr-1" alt="XMR" onerror="this.style.display='none'">15.50000000 XMR<br>Rate: 1 BTC = 15.50000000 XMR`,
coinFrom: 2,
coinTo: 6
}
);
}, 3000);
setTimeout(() => {
CleanupManager.setTimeout(() => {
const btcIcon = getCoinIcon('BTC');
const xmrIcon = getCoinIcon('XMR');
this.createToast(
'<img src="/static/images/coins/bitcoin.svg" class="w-4 h-4 inline mr-1" alt="BTC" onerror="this.style.display=\'none\'">0.50000000 BTC → <img src="/static/images/coins/monero.svg" class="w-4 h-4 inline mr-1" alt="XMR" onerror="this.style.display=\'none\'">7.75000000 XMR',
'New Bid Received',
'new_bid',
{
bidId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66',
subtitle: 'New bid • Rate: 1 BTC = 15.50000000 XMR',
subtitle: `<img src="/static/images/coins/${btcIcon}" class="w-4 h-4 inline mr-1" alt="BTC" onerror="this.style.display='none'">0.50000000 BTC → <img src="/static/images/coins/${xmrIcon}" class="w-4 h-4 inline mr-1" alt="XMR" onerror="this.style.display='none'">7.75000000 XMR<br>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 = `<img src="/static/images/coins/${coinIcon}" class="w-5 h-5 mr-2" alt="${options.coinSymbol}" onerror="this.style.display='none'">`;
}
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 = `<img src="/static/images/coins/${coinFromIcon}" class="w-4 h-4 inline mr-1" alt="${coinFromName}" onerror="this.style.display='none'">${amountFrom} ${coinFromName} → <img src="/static/images/coins/${coinToIcon}" class="w-4 h-4 inline mr-1" alt="${coinToName}" onerror="this.style.display='none'">${amountTo} ${coinToName}`;
toastOptions.subtitle = `New offer • Rate: 1 ${coinFromName} = ${(data.amount_to / data.amount_from).toFixed(8)} ${coinToName}`;
toastTitle = `New Network Offer`;
toastOptions.subtitle = `<img src="/static/images/coins/${coinFromIcon}" class="w-4 h-4 inline mr-1" alt="${coinFromName}" onerror="this.style.display='none'">${amountFrom} ${coinFromName} → <img src="/static/images/coins/${coinToIcon}" class="w-4 h-4 inline mr-1" alt="${coinToName}" onerror="this.style.display='none'">${amountTo} ${coinToName}<br>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 = `<img src="/static/images/coins/${coinFromIcon}" class="w-4 h-4 inline mr-1" alt="${coinFromName}" onerror="this.style.display='none'">${bidAmountFrom} ${coinFromName} → <img src="/static/images/coins/${coinToIcon}" class="w-4 h-4 inline mr-1" alt="${coinToName}" onerror="this.style.display='none'">${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 = `<img src="/static/images/coins/${coinFromIcon}" class="w-4 h-4 inline mr-1" alt="${coinFromName}" onerror="this.style.display='none'">${bidAmountFrom} ${coinFromName} → <img src="/static/images/coins/${coinToIcon}" class="w-4 h-4 inline mr-1" alt="${coinToName}" onerror="this.style.display='none'">${bidAmountTo} ${coinToName}<br>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;

View File

@@ -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;
}
});

View File

@@ -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;
})();

View File

@@ -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');

View File

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

View File

@@ -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);
};
})();

View File

@@ -11,8 +11,7 @@ const WalletManager = (function() {
defaultTTL: 300,
priceSource: {
primary: 'coingecko.com',
fallback: 'cryptocompare.com',
enabledSources: ['coingecko.com', 'cryptocompare.com']
enabledSources: ['coingecko.com']
}
};
@@ -95,22 +94,32 @@ const WalletManager = (function() {
const fetchCoinsString = coinsToFetch.join(',');
const mainResponse = await fetch("/json/coinprices", {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
let mainData;
if (window.ApiManager) {
mainData = await window.ApiManager.makeRequest("/json/coinprices", "POST", {}, {
coins: fetchCoinsString,
source: currentSource,
ttl: config.defaultTTL
})
});
});
} else {
const mainResponse = await fetch("/json/coinprices", {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
coins: fetchCoinsString,
source: currentSource,
ttl: config.defaultTTL
})
});
if (!mainResponse.ok) {
throw new Error(`HTTP error: ${mainResponse.status}`);
if (!mainResponse.ok) {
throw new Error(`HTTP error: ${mainResponse.status}`);
}
mainData = await mainResponse.json();
}
const mainData = await mainResponse.json();
if (mainData && mainData.rates) {
document.querySelectorAll('.coinname-value').forEach(el => {
const coinName = el.getAttribute('data-coinname');
@@ -154,7 +163,7 @@ const WalletManager = (function() {
if (attempt < config.maxRetries - 1) {
const delay = Math.min(config.baseDelay * Math.pow(2, attempt), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
}
}
}
@@ -428,7 +437,7 @@ const WalletManager = (function() {
clearTimeout(state.toggleDebounceTimer);
}
state.toggleDebounceTimer = window.setTimeout(async () => {
state.toggleDebounceTimer = CleanupManager.setTimeout(async () => {
state.toggleInProgress = false;
if (newVisibility) {
await updatePrices(true);
@@ -539,7 +548,6 @@ const WalletManager = (function() {
}
}
// Public API
const publicAPI = {
initialize: async function(options) {
if (state.initialized) {
@@ -579,7 +587,7 @@ const WalletManager = (function() {
clearInterval(state.priceUpdateInterval);
}
state.priceUpdateInterval = setInterval(() => {
state.priceUpdateInterval = CleanupManager.setInterval(() => {
if (localStorage.getItem('balancesVisible') === 'true' && !state.toggleInProgress) {
updatePrices(false);
}
@@ -661,5 +669,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('WalletManager initialized with methods:', Object.keys(WalletManager));
console.log('WalletManager initialized');

View File

@@ -32,26 +32,24 @@ const WebSocketManager = (function() {
}
function determineWebSocketPort() {
let wsPort;
if (window.ConfigManager && window.ConfigManager.wsPort) {
return window.ConfigManager.wsPort.toString();
}
if (window.config && window.config.wsPort) {
wsPort = window.config.wsPort;
return wsPort;
return window.config.wsPort.toString();
}
if (window.ws_port) {
wsPort = window.ws_port.toString();
return wsPort;
return window.ws_port.toString();
}
if (typeof getWebSocketConfig === 'function') {
const wsConfig = getWebSocketConfig();
wsPort = (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
return wsPort;
return (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
}
wsPort = '11700';
return wsPort;
return '11700';
}
const publicAPI = {
@@ -77,7 +75,11 @@ const WebSocketManager = (function() {
}
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(state.reconnectTimeout);
} else {
clearTimeout(state.reconnectTimeout);
}
state.reconnectTimeout = null;
}
@@ -96,13 +98,17 @@ const WebSocketManager = (function() {
ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
setupEventHandlers();
state.connectTimeout = setTimeout(() => {
const timeoutFn = () => {
if (state.isConnecting) {
log('Connection timeout, cleaning up');
cleanup();
handleReconnect();
}
}, 5000);
};
state.connectTimeout = window.CleanupManager
? window.CleanupManager.setTimeout(timeoutFn, 5000)
: setTimeout(timeoutFn, 5000);
return true;
} catch (error) {
@@ -159,18 +165,25 @@ const WebSocketManager = (function() {
cleanup: function() {
log('Cleaning up WebSocket resources');
clearTimeout(state.connectTimeout);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(state.connectTimeout);
} else {
clearTimeout(state.connectTimeout);
}
stopHealthCheck();
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(state.reconnectTimeout);
} else {
clearTimeout(state.reconnectTimeout);
}
state.reconnectTimeout = null;
}
state.isConnecting = false;
state.messageHandlers = {};
if (ws) {
ws.onopen = null;
ws.onmessage = null;
@@ -228,7 +241,11 @@ const WebSocketManager = (function() {
ws.onopen = () => {
state.isConnecting = false;
config.reconnectAttempts = 0;
clearTimeout(state.connectTimeout);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(state.connectTimeout);
} else {
clearTimeout(state.connectTimeout);
}
state.lastHealthCheck = Date.now();
window.ws = ws;
@@ -298,37 +315,42 @@ const WebSocketManager = (function() {
function handlePageHidden() {
log('Page hidden');
state.isPageHidden = true;
stopHealthCheck();
if (ws && ws.readyState === WebSocket.OPEN) {
state.isIntentionallyClosed = true;
ws.close(1000, 'Page hidden');
}
}
function handlePageVisible() {
log('Page visible');
state.isPageHidden = false;
state.isIntentionallyClosed = false;
setTimeout(() => {
const resumeFn = () => {
if (!publicAPI.isConnected()) {
publicAPI.connect();
}
startHealthCheck();
}, 0);
};
if (window.CleanupManager) {
window.CleanupManager.setTimeout(resumeFn, 0);
} else {
setTimeout(resumeFn, 0);
}
}
function startHealthCheck() {
stopHealthCheck();
state.healthCheckInterval = setInterval(() => {
const healthCheckFn = () => {
performHealthCheck();
}, 30000);
};
state.healthCheckInterval = window.CleanupManager
? window.CleanupManager.setInterval(healthCheckFn, 30000)
: setInterval(healthCheckFn, 30000);
}
function stopHealthCheck() {
if (state.healthCheckInterval) {
clearInterval(state.healthCheckInterval);
if (window.CleanupManager) {
window.CleanupManager.clearInterval(state.healthCheckInterval);
} else {
clearInterval(state.healthCheckInterval);
}
state.healthCheckInterval = null;
}
}
@@ -356,7 +378,11 @@ const WebSocketManager = (function() {
function handleReconnect() {
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(state.reconnectTimeout);
} else {
clearTimeout(state.reconnectTimeout);
}
state.reconnectTimeout = null;
}
@@ -369,23 +395,31 @@ const WebSocketManager = (function() {
log(`Scheduling reconnect in ${delay}ms (attempt ${config.reconnectAttempts})`);
state.reconnectTimeout = setTimeout(() => {
const reconnectFn = () => {
state.reconnectTimeout = null;
if (!state.isIntentionallyClosed) {
publicAPI.connect();
}
}, delay);
};
state.reconnectTimeout = window.CleanupManager
? window.CleanupManager.setTimeout(reconnectFn, delay)
: setTimeout(reconnectFn, delay);
} else {
log('Max reconnect attempts reached');
if (typeof updateConnectionStatus === 'function') {
updateConnectionStatus('error');
}
state.reconnectTimeout = setTimeout(() => {
const resetFn = () => {
state.reconnectTimeout = null;
config.reconnectAttempts = 0;
publicAPI.connect();
}, 60000);
};
state.reconnectTimeout = window.CleanupManager
? window.CleanupManager.setTimeout(resetFn, 60000)
: setTimeout(resetFn, 60000);
}
}
@@ -442,5 +476,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('WebSocketManager initialized with methods:', Object.keys(WebSocketManager));
console.log('WebSocketManager initialized');

View File

@@ -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;
})();

View File

@@ -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();
});
}
}
});

View File

@@ -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);
})();

View File

@@ -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() {
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${balance}</div>
`;
if (pendingBalance && parseFloat(pendingBalance) > 0) {
html += `<div class="text-green-500 text-xs">+${pendingBalance} pending</div>`;
}
@@ -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() {
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
`;
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
}
@@ -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() {
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
`;
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
}
@@ -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);
}
});
}

View File

@@ -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 });

View File

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

View File

@@ -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,7 +190,6 @@ const EventManager = {
};
function cleanup() {
//console.log('Starting comprehensive cleanup process for bids table');
try {
if (searchTimeout) {
@@ -326,7 +325,6 @@ 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();
@@ -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;
}
@@ -926,6 +918,12 @@ const forceTooltipDOMCleanup = () => {
foundCount += allTooltipElements.length;
allTooltipElements.forEach(element => {
const isInTooltipContainer = element.closest('.tooltip-container');
if (isInTooltipContainer) {
return;
}
const isDetached = !document.body.contains(element) ||
element.classList.contains('hidden') ||
element.style.display === 'none';
@@ -1012,7 +1010,7 @@ const forceTooltipDOMCleanup = () => {
});
}
if (removedCount > 0) {
// console.log(`Tooltip cleanup: found ${foundCount}, removed ${removedCount} detached tooltips`);
}
}
@@ -1146,7 +1144,7 @@ const createTableRow = async (bid) => {
`;
}
return `
const rowHtml = `
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<!-- Time Column -->
<td class="py-3 pl-6 pr-3">
@@ -1232,13 +1230,16 @@ const createTableRow = async (bid) => {
</div>
</td>
</tr>
`;
<!-- Tooltips -->
const tooltipIdentityHtml = `
<div id="tooltip-identity-${uniqueId}" role="tooltip" class="fixed z-50 py-3 px-4 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600 max-w-sm pointer-events-none">
${tooltipContent}
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
`;
const tooltipStatusHtml = `
<div id="tooltip-status-${uniqueId}" role="tooltip" class="inline-block absolute z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600">
<div class="text-white">
<p class="font-bold mb-2">Transaction Status</p>
@@ -1256,6 +1257,12 @@ const createTableRow = async (bid) => {
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
`;
return {
rowHtml,
tooltipIdentityHtml,
tooltipStatusHtml
};
};
function cleanupOffscreenTooltips() {
@@ -1323,8 +1330,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 +1377,6 @@ async function fetchBids(type = state.currentTab) {
}
}
//console.log(`Received raw ${type} data:`, data.length, 'bids');
state.filters.with_expired = includeExpired;
let processedData;
@@ -1410,7 +1413,8 @@ const updateTableContent = async (type) => {
}
cleanupTooltips();
forceTooltipDOMCleanup();
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-8 text-gray-500 dark:text-gray-400"><div class="animate-pulse">Loading bids...</div></td></tr>';
tooltipIdsToCleanup.clear();
@@ -1421,47 +1425,55 @@ 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
//});
let tooltipContainerId = `tooltip-container-${type}`;
let tooltipContainer = document.getElementById(tooltipContainerId);
if (!tooltipContainer) {
tooltipContainer = document.createElement('div');
tooltipContainer.id = tooltipContainerId;
tooltipContainer.className = 'tooltip-container';
document.body.appendChild(tooltipContainer);
} else {
tooltipContainer.innerHTML = '';
}
try {
if (currentPageData.length > 0) {
const BATCH_SIZE = 10;
let allRows = [];
let allTooltips = [];
for (let i = 0; i < currentPageData.length; i += BATCH_SIZE) {
const batch = currentPageData.slice(i, i + BATCH_SIZE);
const rowPromises = batch.map(bid => createTableRow(bid));
const rows = await Promise.all(rowPromises);
allRows = allRows.concat(rows);
const rowData = await Promise.all(rowPromises);
if (i + BATCH_SIZE < currentPageData.length) {
await new Promise(resolve => setTimeout(resolve, 5));
}
rowData.forEach(data => {
allRows.push(data.rowHtml);
allTooltips.push(data.tooltipIdentityHtml);
allTooltips.push(data.tooltipStatusHtml);
});
}
const scrollPosition = tbody.parentElement?.scrollTop || 0;
tbody.innerHTML = allRows.join('');
tooltipContainer.innerHTML = allTooltips.join('');
if (tbody.parentElement && scrollPosition > 0) {
tbody.parentElement.scrollTop = scrollPosition;
}
if (document.visibilityState === 'visible') {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
initializeTooltips();
setTimeout(() => {
initializeTooltips();
setTimeout(() => {
forceTooltipDOMCleanup();
}, 100);
}, 10);
setTimeout(() => {
forceTooltipDOMCleanup();
}, 100);
});
});
}
} else {
tbody.innerHTML = `
@@ -1495,7 +1507,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 +1607,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 +2082,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 +2115,9 @@ function setupMemoryMonitoring() {
}
}, MEMORY_CHECK_INTERVAL);
document.addEventListener('beforeunload', () => {
CleanupManager.registerResource('bidsMemoryMonitoring', intervalId, () => {
clearInterval(intervalId);
}, { once: true });
});
}
function initialize() {

View File

@@ -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);
}

View File

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

View File

@@ -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 = '<h4 class="bold">Rates</h4><pre><code>' + JSON.stringify(obj, null, ' ') + '</code></pre>';
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 = '<h4>Rates</h4><p>Updating...</p>';
}
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);
})();

View File

@@ -5,8 +5,8 @@ let jsonData = [];
let originalJsonData = [];
let currentSortColumn = 0;
let currentSortDirection = 'desc';
let filterTimeout = null;
let isPaginationInProgress = false;
let autoRefreshInterval = null;
const isSentOffers = window.offersTableConfig.isSentOffers;
const CACHE_DURATION = window.config.cacheConfig.defaultTTL;
@@ -28,6 +28,9 @@ window.tableRateModule = {
processedOffers: new Set(),
getCachedValue(key) {
if (window.CacheManager) {
return window.CacheManager.get(key);
}
const cachedItem = localStorage.getItem(key);
if (cachedItem) {
const parsedItem = JSON.parse(cachedItem);
@@ -41,6 +44,14 @@ window.tableRateModule = {
},
setCachedValue(key, value, resourceType = null) {
if (window.CacheManager) {
const ttl = resourceType ?
window.config.cacheConfig.ttlSettings[resourceType] ||
window.config.cacheConfig.defaultTTL :
900000;
window.CacheManager.set(key, value, ttl);
return;
}
const ttl = resourceType ?
window.config.cacheConfig.ttlSettings[resourceType] ||
window.config.cacheConfig.defaultTTL :
@@ -65,26 +76,6 @@ window.tableRateModule = {
return true;
},
formatUSD(value) {
if (Math.abs(value) < 0.000001) {
return value.toExponential(8) + ' USD';
} else if (Math.abs(value) < 0.01) {
return value.toFixed(8) + ' USD';
} else {
return value.toFixed(2) + ' USD';
}
},
formatNumber(value, decimals) {
if (Math.abs(value) < 0.000001) {
return value.toExponential(decimals);
} else if (Math.abs(value) < 0.01) {
return value.toFixed(decimals);
} else {
return value.toFixed(Math.min(2, decimals));
}
},
getFallbackValue(coinSymbol) {
if (!coinSymbol) return null;
const normalizedSymbol = coinSymbol.toLowerCase() === 'part' ? 'particl' : coinSymbol.toLowerCase();
@@ -151,6 +142,41 @@ function initializeTooltips() {
}
}
function initializeTooltipsInBatches() {
if (!window.TooltipManager) return;
const tooltipElements = document.querySelectorAll('[data-tooltip-target]');
const BATCH_SIZE = 5;
let currentIndex = 0;
function processBatch() {
const endIndex = Math.min(currentIndex + BATCH_SIZE, tooltipElements.length);
for (let i = currentIndex; i < endIndex; i++) {
const element = tooltipElements[i];
const targetId = element.getAttribute('data-tooltip-target');
if (!targetId) continue;
const tooltipContent = document.getElementById(targetId);
if (tooltipContent) {
window.TooltipManager.create(element, tooltipContent.innerHTML, {
placement: element.getAttribute('data-tooltip-placement') || 'top'
});
}
}
currentIndex = endIndex;
if (currentIndex < tooltipElements.length) {
CleanupManager.setTimeout(processBatch, 0);
}
}
if (tooltipElements.length > 0) {
CleanupManager.setTimeout(processBatch, 0);
}
}
function getValidOffers() {
if (!jsonData) {
return [];
@@ -180,7 +206,6 @@ function saveFilterSettings() {
}));
}
function getSelectedCoins(filterType) {
const dropdown = document.getElementById(`${filterType}_dropdown`);
@@ -188,7 +213,6 @@ function getSelectedCoins(filterType) {
return ['any'];
}
const allCheckboxes = dropdown.querySelectorAll('input[type="checkbox"]');
const selected = [];
@@ -252,7 +276,6 @@ function updateFilterButtonText(filterType) {
textSpan.textContent = `Filter ${filterLabel} (${selected.length} selected)`;
}
button.style.width = '210px';
}
@@ -270,7 +293,6 @@ function updateCoinBadges(filterType) {
const coinName = getCoinNameFromValue(coinValue, filterType);
const badge = document.createElement('span');
const isBidsFilter = filterType === 'coin_to' && !isSentOffers;
const isOffersFilter = filterType === 'coin_from' && !isSentOffers;
const isReceivingFilter = filterType === 'coin_to' && isSentOffers;
@@ -285,7 +307,6 @@ function updateCoinBadges(filterType) {
badge.className = badgeClass + ' cursor-pointer hover:opacity-80';
const coinImage = getCoinImage(coinName);
badge.innerHTML = `
@@ -350,13 +371,7 @@ function coinMatches(offerCoin, filterCoins) {
return true;
}
if ((normalizedOfferCoin === 'firo' || normalizedOfferCoin === 'zcoin') &&
(normalizedFilterCoin === 'firo' || normalizedFilterCoin === 'zcoin')) {
return true;
}
if ((normalizedOfferCoin === 'bitcoincash' && normalizedFilterCoin === 'bitcoin cash') ||
(normalizedOfferCoin === 'bitcoin cash' && normalizedFilterCoin === 'bitcoincash')) {
if (window.CoinUtils && window.CoinUtils.isSameCoin(normalizedOfferCoin, normalizedFilterCoin)) {
return true;
}
@@ -467,7 +482,6 @@ function removeCoinFilter(filterType, coinValue) {
if (checkbox) {
checkbox.checked = false;
updateFilterButtonText(filterType);
updateCoinBadges(filterType);
applyFilters();
@@ -475,7 +489,6 @@ function removeCoinFilter(filterType, coinValue) {
}
}
window.removeCoinFilter = removeCoinFilter;
function filterAndSortData() {
@@ -510,7 +523,6 @@ function filterAndSortData() {
return false;
}
if (selectedCoinTo.length > 0 && !(selectedCoinTo.length === 1 && selectedCoinTo[0] === 'any')) {
const coinNames = selectedCoinTo.map(value => getCoinNameFromValue(value, 'coin_to'));
const matches = coinMatches(offer.coin_to, coinNames);
@@ -519,7 +531,6 @@ function filterAndSortData() {
}
}
if (selectedCoinFrom.length > 0 && !(selectedCoinFrom.length === 1 && selectedCoinFrom[0] === 'any')) {
const coinNames = selectedCoinFrom.map(value => getCoinNameFromValue(value, 'coin_from'));
const matches = coinMatches(offer.coin_from, coinNames);
@@ -674,10 +685,9 @@ async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwn
if (window.CoinManager) {
normalizedCoin = window.CoinManager.getPriceKey(coin) || normalizedCoin;
} else {
if (normalizedCoin === 'zcoin') normalizedCoin = 'firo';
if (normalizedCoin === 'bitcoincash' || normalizedCoin === 'bitcoin cash')
normalizedCoin = 'bitcoin-cash';
if (normalizedCoin.includes('particl')) normalizedCoin = 'particl';
if (window.CoinUtils) {
normalizedCoin = window.CoinUtils.normalizeCoinName(normalizedCoin, latestPrices);
}
}
let price = null;
if (latestPrices && latestPrices[normalizedCoin]) {
@@ -734,11 +744,38 @@ async function fetchLatestPrices() {
}
}
async function fetchPricesAsync() {
try {
const prices = await window.PriceManager.getPrices(false);
return prices;
} catch (error) {
console.error('Error fetching prices asynchronously:', error);
return null;
}
}
async function fetchOffers() {
const refreshButton = document.getElementById('refreshOffers');
const refreshIcon = document.getElementById('refreshIcon');
const refreshText = document.getElementById('refreshText');
const fetchWithRetry = async (url, maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} catch (error) {
if (i === maxRetries - 1) throw error;
console.log(`Fetch retry ${i + 1}/${maxRetries} for ${url}`);
await new Promise(resolve => CleanupManager.setTimeout(resolve, 100 * Math.pow(2, i)));
}
}
};
try {
if (!NetworkManager.isOnline()) {
throw new Error('Network is offline');
@@ -751,15 +788,7 @@ async function fetchOffers() {
refreshButton.classList.add('opacity-75', 'cursor-wait');
}
const [offersResponse, pricesData] = await Promise.all([
fetch(isSentOffers ? '/json/sentoffers' : '/json/offers'),
fetchLatestPrices()
]);
if (!offersResponse.ok) {
throw new Error(`HTTP error! status: ${offersResponse.status}`);
}
const offersResponse = await fetchWithRetry(isSentOffers ? '/json/sentoffers' : '/json/offers');
const data = await offersResponse.json();
if (data.error) {
@@ -789,13 +818,20 @@ async function fetchOffers() {
jsonData = formatInitialData(processedData);
originalJsonData = [...jsonData];
latestPrices = pricesData || getEmptyPriceData();
CacheManager.set('offers_cached', jsonData, 'offers');
applyFilters();
updatePaginationInfo();
fetchPricesAsync().then(prices => {
if (prices) {
latestPrices = prices;
updateProfitLossDisplays();
}
}).catch(error => {
console.error('Error fetching prices after offers refresh:', error);
});
} catch (error) {
console.error('[Debug] Error fetching offers:', error);
NetworkManager.handleNetworkError(error);
@@ -871,27 +907,37 @@ function updateConnectionStatus(status) {
}
function updateRowTimes() {
requestAnimationFrame(() => {
const rows = document.querySelectorAll('[data-offer-id]');
rows.forEach(row => {
const offerId = row.getAttribute('data-offer-id');
const offer = jsonData.find(o => o.offer_id === offerId);
if (!offer) return;
const newPostedTime = formatTime(offer.created_at, true);
const newExpiresIn = formatTimeLeft(offer.expire_at);
const rows = document.querySelectorAll('[data-offer-id]');
const updates = [];
const postedElement = row.querySelector('.text-xs:first-child');
const expiresElement = row.querySelector('.text-xs:last-child');
rows.forEach(row => {
const offerId = row.getAttribute('data-offer-id');
const offer = jsonData.find(o => o.offer_id === offerId);
if (!offer) return;
if (postedElement && postedElement.textContent !== `Posted: ${newPostedTime}`) {
postedElement.textContent = `Posted: ${newPostedTime}`;
}
if (expiresElement && expiresElement.textContent !== `Expires in: ${newExpiresIn}`) {
expiresElement.textContent = `Expires in: ${newExpiresIn}`;
}
const newPostedTime = formatTime(offer.created_at, true);
const newExpiresIn = formatTimeLeft(offer.expire_at);
const postedElement = row.querySelector('.text-xs:first-child');
const expiresElement = row.querySelector('.text-xs:last-child');
updates.push({
postedElement,
expiresElement,
newPostedTime,
newExpiresIn
});
});
updates.forEach(({ postedElement, expiresElement, newPostedTime, newExpiresIn }) => {
if (postedElement && postedElement.textContent !== `Posted: ${newPostedTime}`) {
postedElement.textContent = `Posted: ${newPostedTime}`;
}
if (expiresElement && expiresElement.textContent !== `Expires in: ${newExpiresIn}`) {
expiresElement.textContent = `Expires in: ${newExpiresIn}`;
}
});
}
function updateLastRefreshTime() {
@@ -1097,8 +1143,13 @@ async function updateOffersTable(options = {}) {
return;
}
const isIncrementalUpdate = options.incremental === true;
if (!options.skipSkeleton && !isIncrementalUpdate && offersBody) {
offersBody.innerHTML = '<tr><td colspan="10" class="text-center py-8 text-gray-500 dark:text-gray-400"><div class="animate-pulse">Loading offers...</div></td></tr>';
}
if (window.TooltipManager) {
window.TooltipManager.cleanup();
requestAnimationFrame(() => window.TooltipManager.cleanup());
}
const validOffers = getValidOffers();
@@ -1138,28 +1189,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 +1266,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 +1277,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 +1603,6 @@ function createRateColumn(offer, coinFrom, coinTo) {
`;
}
function createPercentageColumn(offer) {
return `
<td class="py-3 px-2 bold text-sm text-center monospace items-center rate-table-info">
@@ -1731,45 +1839,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 +1922,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 +1997,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 +2076,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 +2121,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 +2161,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 +2175,6 @@ function initializeTableEvents() {
});
}
if (coinFromButton && coinFromDropdown) {
CleanupManager.addListener(coinFromButton, 'click', (e) => {
e.stopPropagation();
@@ -2220,15 +2239,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 +2260,6 @@ function initializeTableEvents() {
return;
}
console.log('Manual refresh initiated');
lastRefreshTime = now;
const refreshIcon = document.getElementById('refreshIcon');
const refreshText = document.getElementById('refreshText');
@@ -2267,10 +2286,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 +2297,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 +2337,7 @@ function initializeTableEvents() {
await updateOffersTable({ fromPaginationClick: true });
updatePaginationInfo();
} finally {
setTimeout(() => {
CleanupManager.setTimeout(() => {
isPaginationInProgress = false;
}, 100);
}
@@ -2340,7 +2357,7 @@ function initializeTableEvents() {
await updateOffersTable({ fromPaginationClick: true });
updatePaginationInfo();
} finally {
setTimeout(() => {
CleanupManager.setTimeout(() => {
isPaginationInProgress = false;
}, 100);
}
@@ -2416,18 +2433,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 +2570,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 previousPrices = latestPrices;
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;
const offersResponse = await fetch(isSentOffers ? '/json/sentoffers' : '/json/offers');
if (!offersResponse.ok) {
throw new Error(`HTTP error! status: ${offersResponse.status}`);
}
await new Promise(resolve => CleanupManager.setTimeout(resolve, 100 * Math.pow(2, i)));
}
}
};
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 +2616,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 +2630,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 +2645,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 +2679,7 @@ document.addEventListener('DOMContentLoaded', async function() {
});
}
if (window.config.autoRefreshEnabled) {
startAutoRefresh();
}
startAutoRefresh();
const filterForm = document.getElementById('filterForm');
if (filterForm) {
@@ -2649,20 +2713,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 +2748,8 @@ function cleanup() {
window.countdownInterval = null;
}
stopAutoRefresh();
if (window._cleanupIntervals && Array.isArray(window._cleanupIntervals)) {
window._cleanupIntervals.forEach(interval => {
clearInterval(interval);
@@ -2739,7 +2795,6 @@ function cleanup() {
}
}
//console.log('Offers.js cleanup completed');
} catch (error) {
console.error('Error during cleanup:', error);
}

View File

@@ -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}`),
@@ -64,7 +24,6 @@ const api = {
}
const volumeData = await Api.fetchVolumeData({
cryptoCompare: apiKeys.cryptoCompare,
coinGecko: apiKeys.coinGecko
});
@@ -94,29 +53,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();
@@ -172,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 => {
@@ -209,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,
@@ -242,7 +174,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 +184,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 +192,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 +235,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 +277,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 +430,7 @@ const ui = {
chartContainer.classList.add('blurred');
if (duration > 0) {
setTimeout(() => {
CleanupManager.setTimeout(() => {
ui.hideErrorMessage();
}, duration);
}
@@ -929,8 +861,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');
@@ -960,6 +895,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;
@@ -1014,6 +951,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 = `
<div style="padding: 20px; background: rgba(0,0,0,0.05); border-radius: 8px;">
<div style="font-size: 16px; margin-bottom: 8px;">No Price Data Available</div>
</div>
`;
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);
@@ -1199,11 +1171,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 +1203,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 +1350,7 @@ const app = {
}
const timeUntilRefresh = nextRefreshTime - now;
app.nextRefreshTime = nextRefreshTime;
app.autoRefreshInterval = setTimeout(() => {
app.autoRefreshInterval = CleanupManager.setTimeout(() => {
if (NetworkManager.isOnline()) {
app.refreshAllData();
} else {
@@ -1394,7 +1362,6 @@ const app = {
},
refreshAllData: async function() {
//console.log('Price refresh started at', new Date().toLocaleTimeString());
if (app.isRefreshing) {
console.log('Refresh already in progress, skipping...');
@@ -1415,7 +1382,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 +1395,6 @@ refreshAllData: async function() {
return;
}
//console.log('Starting refresh of all data...');
app.isRefreshing = true;
app.updateNextRefreshTime();
ui.showLoader();
@@ -1443,7 +1409,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 +1434,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 +1461,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 +1489,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,7 +1499,6 @@ 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);
@@ -1544,7 +1507,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 +1529,6 @@ refreshAllData: async function() {
app.scheduleNextRefresh();
}
//console.log(`Refresh process finished at ${new Date().toLocaleTimeString()}, next refresh scheduled: ${app.isAutoRefreshEnabled ? 'yes' : 'no'}`);
}
},
@@ -1590,7 +1552,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);
}

View File

@@ -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);
})();

View File

@@ -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 = `
<tr>
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white">
@@ -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);
});
});

View File

@@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>`;
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);
})();

View File

@@ -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 = `
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Pending:</h4>
<span class="coinname-value text-sm font-medium text-green-600 dark:text-green-400" data-coinname="${coinData.name}">+${cleanPending} ${coinData.ticker || coinData.name}</span>
`;
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 = `
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Pending USD value:</h4>
<div class="usd-value text-sm font-medium text-green-600 dark:text-green-400">${initialUSD}</div>
`;
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 = `
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">${pendingLabel}</h4>
<span class="coinname-value text-sm font-medium text-green-600 dark:text-green-400" data-coinname="${coinName}">+${cleanPending} ${ticker}</span>
`;
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 = `
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">${pendingLabel.replace(':', '')} USD value:</h4>
<div class="usd-value text-sm font-medium text-green-600 dark:text-green-400">${initialUSD}</div>
`;
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;
})();

View File

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

View File

@@ -1,21 +1,13 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg %}
{% from 'macros.html' import breadcrumb %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/404">404</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': '404', 'url': '/404'}
]) }}
</div>
</div>
</section>

View File

@@ -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 %}
<section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">Swaps in Progress</h2>
<p class="font-normal text-coolGray-200 dark:text-white">Monitor your currently active swap transactions.</p>
</div>
</div>
</div>
</div>
</section>
{{ page_header('Swaps in Progress', 'Monitor your currently active swap transactions.') }}
{% include 'inc_messages.html' %}
@@ -113,6 +100,6 @@
</div>
</section>
<script src="/static/js/swaps_in_progress.js"></script>
<script src="/static/js/pages/swaps-page.js"></script>
{% include 'footer.html' %}

View File

@@ -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 %}
<script>
window.ammTablesConfig = {
@@ -9,31 +10,16 @@
};
</script>
<section class="py-3 px-4 mt-6">
<div class="lg:container lg:px-4 mx-auto">
<div class="relative py-8 px-16 bg-coolGray-900 dark:bg-gray-800 rounded-md overflow-hidden">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="text-2xl font-bold text-white tracking-tighter">Automated Market Maker</h2>
<p class="hidden lg:flex mt-3 font-normal text-coolGray-200 dark:text-white">
Automatically create offers and bids based on your configuration.
</p>
</div>
</div>
</div>
</div>
</section>
{{ page_header('Automated Market Maker', 'Automatically create offers and bids based on your configuration.', dark_bg='dark:bg-gray-500') }}
<div class="xl:container mx-auto">
{% include 'inc_messages.html' %}
<section>
<div class="lg:container px-4 mx-auto">
<div class="lg:container mx-auto mt-6 lg:px-0 px-6">
<div class="flex flex-wrap -mx-4">
<div class="w-full px-4 mb-8">
<div class="p-6 px-0 lg:px-6 bg-white dark:bg-gray-800 rounded-xl shadow-md">
<div class="p-6 px-0 lg:px-6 bg-white dark:bg-gray-500 rounded-xl shadow-md">
<div class="flex sm:pr-6 lg:pr-0 justify-end items-center">
<div class="flex space-x-2">
<button id="add-new-offer-btn" class="flex items-center px-4 py-2.5 bg-green-600 hover:bg-green-700 border-green-600 font-medium text-sm text-white border rounded-md shadow-button focus:ring-0 focus:outline-none">
@@ -189,8 +175,8 @@
<section>
<div class="lg:container px-4 mx-auto">
<div class="flex flex-wrap -mx-4">
<div class="w-full lg:w-1/2 px-4 mb-8">
<div class="p-6 bg-white dark:bg-gray-800 rounded-xl shadow-md">
<div class="w-full lg:w-1/2 pr-4 mb-8">
<div class="p-6 bg-white dark:bg-gray-500 rounded-xl shadow-md">
<h3 class="mb-4 text-xl font-bold text-coolGray-900 dark:text-white">Control</h3>
<form method="post" autocomplete="off">
<div class="mb-4">
@@ -295,7 +281,7 @@
</div>
{% if debug_ui_mode %}
<div class="mt-6 p-6 bg-white dark:bg-gray-800 rounded-xl shadow-md">
<div class="mt-6 p-6 bg-white dark:bg-gray-500 rounded-xl shadow-md">
<h3 class="mb-4 text-xl font-bold text-coolGray-900 dark:text-white">Files</h3>
<div class="mb-4">
<p class="text-sm text-gray-700 dark:text-gray-300">
@@ -327,8 +313,8 @@
{% endif %}
</div>
<div class="w-full lg:w-1/2 px-4 mb-8">
<div class="p-6 bg-white dark:bg-gray-800 rounded-xl shadow-md">
<div class="w-full lg:w-1/2 pl-4 mb-8">
<div class="p-6 bg-white dark:bg-gray-500 rounded-xl shadow-md">
<h3 class="mb-4 text-xl font-bold text-coolGray-900 dark:text-white">Configuration</h3>
<div class="mb-4 border-b pb-5 border-gray-200 dark:border-gray-500">
@@ -744,280 +730,12 @@
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const configForm = document.querySelector('form[method="post"]');
const saveConfigBtn = document.getElementById('save_config_btn');
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');
const addOfferTab = document.getElementById('add-offer-tab');
const addBidTab = document.getElementById('add-bid-tab');
const addOfferContent = document.getElementById('add-offer-content');
const addBidContent = document.getElementById('add-bid-content');
if (jsonTab && settingsTab && overviewTab && jsonContent && settingsContent && overviewContent) {
const activeConfigTab = localStorage.getItem('amm_active_config_tab');
function 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');
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');
}
}
function loadSettingsFromJson() {
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);
}
}
jsonTab.addEventListener('click', function() {
switchConfigTab('json-tab');
});
settingsTab.addEventListener('click', function() {
switchConfigTab('settings-tab');
});
overviewTab.addEventListener('click', function() {
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', function(e) {
updateJsonFromSettings();
});
}
function updateJsonFromSettings() {
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);
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');
config.min_seconds_between_bids = minSecondsBetweenBidsEl ? parseInt(minSecondsBetweenBidsEl.value) || 15 : (config.min_seconds_between_bids || 15);
config.max_seconds_between_bids = maxSecondsBetweenBidsEl ? parseInt(maxSecondsBetweenBidsEl.value) || 60 : (config.max_seconds_between_bids || 60);
config.prune_state_delay = pruneStateDelayEl ? parseInt(pruneStateDelayEl.value) || 120 : (config.prune_state_delay || 120);
config.prune_state_after_seconds = pruneStateAfterSecondsEl ? parseInt(pruneStateAfterSecondsEl.value) || 604800 : (config.prune_state_after_seconds || 604800);
config.auth = document.getElementById('auth').value;
delete config.adjust_rates_based_on_market;
configTextarea.value = JSON.stringify(config, null, 4);
} catch (error) {
console.error('Error updating JSON from settings:', error);
}
}
}
const collapsibleHeaders = document.querySelectorAll('.collapsible-header');
if (collapsibleHeaders.length > 0) {
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 = {};
}
function 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', function() {
toggleCollapsible(header);
});
});
localStorage.setItem('amm_collapsible_states', JSON.stringify(collapsibleStates));
}
if (configForm && saveConfigBtn) {
configForm.addEventListener('submit', function(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');
setTimeout(function() {
if (window.ammTablesManager && window.ammTablesManager.updateTables) {
window.ammTablesManager.updateTables();
}
}, 500);
}
}
if (localStorage.getItem('amm_create_default_refresh') === 'true') {
localStorage.removeItem('amm_create_default_refresh');
setTimeout(function() {
window.location.href = window.location.pathname + window.location.search;
}, 500);
}
const createDefaultBtn = document.getElementById('create_default_btn');
if (createDefaultBtn && configForm) {
createDefaultBtn.addEventListener('click', function(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, function() {
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();
}
}
});
}
});
</script>
<!-- AMM Configuration Tabs handled by external JS -->
</form>
</div>
{% if state_exists and debug_ui_mode %}
<div class="mt-6 p-6 bg-white dark:bg-gray-800 rounded-xl shadow-md">
<div class="mt-6 p-6 bg-white dark:bg-gray-500 rounded-xl shadow-md">
<h3 class="mb-4 text-xl font-bold text-coolGray-900 dark:text-white">State File (JSON)</h3>
<div class="mb-4">
<div class="font-mono bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg p-2.5 h-64 overflow-y-auto dark:bg-gray-700 dark:border-gray-600 dark:text-white">{{ state_content }}</div>
@@ -1036,10 +754,10 @@
</div>
{% if debug_ui_mode %}
<div class="mb-8">
<div class="p-6 bg-white dark:bg-gray-800 rounded-xl shadow-md">
<div class="mb-8 -mx-4">
<div class="p-6 bg-white dark:bg-gray-500 rounded-xl shadow-md">
<h3 class="mb-4 text-xl font-bold text-coolGray-900 dark:text-white">AMM Logs</h3>
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4 h-64 overflow-y-auto font-mono text-sm text-gray-900 dark:text-gray-200">
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 h-64 overflow-y-auto font-mono text-sm text-gray-900 dark:text-gray-200">
{% if logs %}
{% for log in logs %}
<div class="mb-1">{{ log }}</div>
@@ -1117,9 +835,9 @@
<label for="add-amm-amount" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Offer Amount <span class="text-red-500">*</span></label>
<input type="text" id="add-amm-amount" pattern="[0-9]*\.?[0-9]*" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="0.0">
<div class="mt-2 flex space-x-2">
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(0.25, 'add-amm-amount')">25%</button>
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(0.5, 'add-amm-amount')">50%</button>
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(1, 'add-amm-amount')">100%</button>
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" data-set-amm-amount="0.25" data-input-id="add-amm-amount">25%</button>
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" data-set-amm-amount="0.5" data-input-id="add-amm-amount">50%</button>
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" data-set-amm-amount="1" data-input-id="add-amm-amount">100%</button>
</div>
</div>
@@ -1404,9 +1122,9 @@
<label for="edit-amm-amount" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Offer Amount <span class="text-red-500">*</span></label>
<input type="text" id="edit-amm-amount" pattern="[0-9]*\.?[0-9]*" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="0.0">
<div class="mt-2 flex space-x-2">
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(0.25, 'edit-amm-amount')">25%</button>
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(0.5, 'edit-amm-amount')">50%</button>
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(1, 'edit-amm-amount')">100%</button>
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" data-set-amm-amount="0.25" data-input-id="edit-amm-amount">25%</button>
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" data-set-amm-amount="0.5" data-input-id="edit-amm-amount">50%</button>
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" data-set-amm-amount="1" data-input-id="edit-amm-amount">100%</button>
</div>
</div>
@@ -1671,550 +1389,8 @@
</div>
</div>
<script src="/static/js/amm_tables.js"></script>
<script>
function saveDebugSetting() {
const debugCheckbox = document.getElementById('debug-mode');
if (debugCheckbox) {
localStorage.setItem('amm_debug_enabled', debugCheckbox.checked);
}
}
function loadDebugSetting() {
const debugCheckbox = document.getElementById('debug-mode');
if (debugCheckbox) {
const savedSetting = localStorage.getItem('amm_debug_enabled');
if (savedSetting !== null) {
debugCheckbox.checked = savedSetting === 'true';
}
}
}
function saveAutostartSetting(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;
}
});
}
function setupAutostartCheckbox() {
const autostartCheckbox = document.getElementById('autostart-amm');
if (autostartCheckbox) {
autostartCheckbox.addEventListener('change', function() {
saveAutostartSetting(this.checked);
});
}
}
function showErrorModal(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');
}
}
function hideErrorModal() {
const modal = document.getElementById('errorModal');
if (modal) {
modal.classList.add('hidden');
}
}
function showConfirmModal(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;
}
function hideConfirmModal() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
}
window.confirmCallback = null;
}
function setupStartupValidation() {
const controlForm = document.querySelector('form[method="post"]');
if (!controlForm) return;
const startButton = controlForm.querySelector('input[name="start"]');
if (!startButton) return;
startButton.addEventListener('click', function(e) {
e.preventDefault();
performStartupValidation();
});
}
function performStartupValidation() {
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');
let progress = 0;
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;
function runNextStep() {
if (currentStep >= steps.length) {
submitStartForm();
return;
}
const step = steps[currentStep];
messageEl.textContent = step.message;
progressBar.style.width = step.progress + '%';
setTimeout(() => {
validateStep(currentStep).then(result => {
if (result.success) {
currentStep++;
runNextStep();
} else {
showStartupError(result.error);
}
}).catch(error => {
showStartupError('Validation failed: ' + error.message);
});
}, 500);
}
runNextStep();
}
async function validateStep(stepIndex) {
try {
switch (stepIndex) {
case 0:
return await validateConfiguration();
case 1:
return await validateOffersAndBids();
case 2:
return await validateWalletBalances();
case 3:
return await validateApiConnection();
case 4:
return { success: true };
default:
return { success: true };
}
} catch (error) {
return { success: false, error: error.message };
}
}
async function validateConfiguration() {
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 };
}
async function validateOffersAndBids() {
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 };
}
async function validateWalletBalances() {
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 };
}
async function validateApiConnection() {
return { success: true };
}
function showStartupError(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);
}
}
function submitStartForm() {
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 => {
showStartupError('Failed to start AMM: ' + error.message);
});
}
}
window.showErrorModal = showErrorModal;
window.hideErrorModal = hideErrorModal;
window.showConfirmModal = showConfirmModal;
window.hideConfirmModal = hideConfirmModal;
document.addEventListener('DOMContentLoaded', function() {
loadDebugSetting();
setupAutostartCheckbox();
setupStartupValidation();
const debugCheckbox = document.getElementById('debug-mode');
if (debugCheckbox) {
debugCheckbox.addEventListener('change', saveDebugSetting);
}
const errorOkBtn = document.getElementById('errorOk');
if (errorOkBtn) {
errorOkBtn.addEventListener('click', hideErrorModal);
}
const errorModal = document.getElementById('errorModal');
if (errorModal) {
errorModal.addEventListener('click', function(e) {
if (e.target === errorModal) {
hideErrorModal();
}
});
}
const confirmYesBtn = document.getElementById('confirmYes');
if (confirmYesBtn) {
confirmYesBtn.addEventListener('click', function() {
if (window.confirmCallback && typeof window.confirmCallback === 'function') {
window.confirmCallback();
}
hideConfirmModal();
});
}
const confirmNoBtn = document.getElementById('confirmNo');
if (confirmNoBtn) {
confirmNoBtn.addEventListener('click', hideConfirmModal);
}
const confirmModal = document.getElementById('confirmModal');
if (confirmModal) {
confirmModal.addEventListener('click', function(e) {
if (e.target === confirmModal) {
hideConfirmModal();
}
});
}
const clearStateBtn = document.getElementById('clearStateBtn');
if (clearStateBtn) {
clearStateBtn.addEventListener('click', function() {
showConfirmModal(
'Clear AMM State',
'This will clear the AMM state file. All running offers/bids will be lost. Are you sure?',
function() {
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();
}
}
);
});
}
});
function setAmmAmount(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) {
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) {
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);
}
function updateAmmModalBalances(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') {
updateOfferDropdownBalances(balanceData);
} else if (modalType === 'bid') {
updateBidDropdownBalances(balanceData);
}
}
function setupWebSocketBalanceUpdates() {
window.BalanceUpdatesManager.setup({
contextKey: 'amm',
balanceUpdateCallback: updateAmmModalBalances,
swapEventCallback: updateAmmModalBalances,
errorContext: 'AMM',
enablePeriodicRefresh: true,
periodicInterval: 120000
});
}
function updateAmmDropdownBalances(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();
}
}
}
function updateOfferDropdownBalances(balanceData) {
updateAmmDropdownBalances(balanceData);
}
function updateBidDropdownBalances(balanceData) {
updateAmmDropdownBalances(balanceData);
}
function updateOfferCustomDropdownDisplay(select, balanceMap, pendingMap) {
console.log(`[DEBUG] updateOfferCustomDropdownDisplay called for ${select.id} - doing nothing`);
}
function updateBidCustomDropdownDisplay(select, balanceMap, pendingMap) {
console.log(`[DEBUG] updateBidCustomDropdownDisplay called for ${select.id} - doing nothing`);
}
function updateCustomDropdownDisplay(select, balanceMap, pendingMap) {
console.log(`[DEBUG] updateCustomDropdownDisplay called for ${select.id} - doing nothing`);
}
window.BalanceUpdatesManager.initialize();
setupWebSocketBalanceUpdates();
function cleanupAmmBalanceUpdates() {
window.BalanceUpdatesManager.cleanup('amm');
if (window.ammDropdowns) {
window.ammDropdowns.forEach(dropdown => {
if (dropdown.parentNode) {
dropdown.parentNode.removeChild(dropdown);
}
});
window.ammDropdowns = [];
}
}
if (window.CleanupManager) {
window.CleanupManager.registerResource('ammBalanceUpdates', null, cleanupAmmBalanceUpdates);
}
window.addEventListener('beforeunload', cleanupAmmBalanceUpdates);
window.setAmmAmount = setAmmAmount;
</script>
<script src="/static/js/pages/amm-tables.js"></script>
<script src="/static/js/pages/amm-config-tabs.js"></script>
<script src="/static/js/pages/amm-page.js"></script>
{% include 'footer.html' %}

View File

@@ -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 %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/automation">Automation Strategies</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
</ul>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Automation Strategies', 'url': '/automation'}
]) }}
</div>
</div>
</section>

View File

@@ -1,24 +1,14 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg %}
{% from 'macros.html' import breadcrumb %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/automation">Automation Strategies</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/automation">ID:<!-- todo ID here {{ strategy_id }} --></a>
</li>
</ul>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Automation Strategies', 'url': '/automation'},
{'text': 'ID:', 'url': '/automation'}
]) }}
</div>
</div>
</section>

View File

@@ -1,24 +1,14 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg %}
{% from 'macros.html' import breadcrumb %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/automation">Automation Strategies</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/automation">New</a>
</li>
</ul>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Automation Strategies', 'url': '/automation'},
{'text': 'New', 'url': '/automation'}
]) }}
</div>
</div>
</section>

View File

@@ -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 %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="#">Bids</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="{{ bid_id }}">BID ID: {{ bid_id }}</a>
</li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Bids', 'url': '#'},
{'text': 'BID ID: ' ~ bid_id, 'url': bid_id}
]) }}
</div>
</div>
</section>
@@ -534,14 +525,14 @@
</div>
{% if data.can_abandon == true and not edit_bid %}
<div class="w-full md:w-auto p-1.5">
<button name="abandon_bid" type="submit" value="Abandon Bid" onclick="return confirmPopup();" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Abandon Bid</button>
<button name="abandon_bid" type="submit" value="Abandon Bid" data-confirm class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Abandon Bid</button>
</div>
{% endif %}
{% endif %}
{% endif %}
{% if data.was_received and not edit_bid and data.can_accept_bid %}
<div class="w-full md:w-auto p-1.5">
<button name="accept_bid" value="Accept Bid" type="submit" onclick='return confirmPopup("Accept");' class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Accept Bid</button>
<button name="accept_bid" value="Accept Bid" type="submit" data-confirm data-confirm-action="Accept" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Accept Bid</button>
</div>
{% endif %}
</div>

View File

@@ -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 %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="#">Bids</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="{{ bid_id }}">BID ID: {{ bid_id }}</a>
</li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Bids', 'url': '#'},
{'text': 'BID ID: ' ~ bid_id, 'url': bid_id}
]) }}
</div>
</div>
</section>
@@ -810,14 +801,14 @@
</div>
{% if data.can_abandon == true %}
<div class="w-full md:w-auto p-1.5">
<button name="abandon_bid" type="submit" value="Abandon Bid" onclick="return confirmPopup();" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md focus:ring-0 focus:outline-none">Abandon Bid</button>
<button name="abandon_bid" type="submit" value="Abandon Bid" data-confirm class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md focus:ring-0 focus:outline-none">Abandon Bid</button>
</div>
{% endif %}
{% endif %}
{% endif %}
{% if data.was_received and not edit_bid and data.can_accept_bid %}
<div class="w-full md:w-auto p-1.5">
<button name="accept_bid" value="Accept Bid" type="submit" onclick='return confirmPopup("Accept");' class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">Accept Bid</button>
<button name="accept_bid" value="Accept Bid" type="submit" data-confirm data-confirm-action="Accept" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">Accept Bid</button>
</div>
{% endif %}
</div>

View File

@@ -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 %}
<section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">All Bids / Sent Bids / Received Bids</h2>
<p class="font-normal text-coolGray-200 dark:text-white">View, and manage bids.</p>
</div>
</div>
</div>
</div>
</section>
{{ page_header('All Bids / Sent Bids / Received Bids', 'View, and manage bids.') }}
{% include 'inc_messages.html' %}
@@ -467,7 +453,8 @@
</div>
</div>
<script src="/static/js/bids_sentreceived.js"></script>
<script src="/static/js/bids_sentreceived_export.js"></script>
<script src="/static/js/pages/bids-tab-navigation.js"></script>
<script src="/static/js/pages/bids-page.js"></script>
<script src="/static/js/pages/bids-export.js"></script>
{% include 'footer.html' %}

View File

@@ -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 %}
<section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">Bid Requests</h2>
<p class="font-normal text-coolGray-200 dark:text-white">Review and accept bids from other users.</p>
</div>
</div>
</div>
</div>
</section>
{{ page_header('Bid Requests', 'Review and accept bids from other users.') }}
{% include 'inc_messages.html' %}
@@ -113,6 +100,6 @@
</div>
</section>
<script src="/static/js/bids_available.js"></script>
<script src="/static/js/pages/bids-available-page.js"></script>
{% include 'footer.html' %}

View File

@@ -1,28 +1,15 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg %}
{% from 'macros.html' import breadcrumb %}
<div class="container mx-auto">
<!-- Breadcrumb -->
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/changepassword">Change Password</a>
</li>
<li>
<svg width="6" height="15" viewBox="0 0 6 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.34 0.671999L2.076 14.1H0.732L3.984 0.671999H5.34Z" fill="#BBC3CF"></path>
</svg>
</li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Change Password', 'url': '/changepassword'}
]) }}
</div>
</div>
</section>

View File

@@ -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 %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/debug">Debug</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Debug', 'url': '/debug'}
]) }}
</div>
</div>
</section>
@@ -63,7 +56,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Remove expired offers and bids</td>
<td td class="py-3 px-6 ">
<button name="remove_expired" type="submit" value="Yes" class="w-60 flex flex-wrap justify-center py-2 px-4 bg-red-500 hover:bg-red-600 font-medium text-sm text-white border border-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none" onclick="return confirmRemoveExpired();">
<button name="remove_expired" type="submit" value="Yes" class="w-60 flex flex-wrap justify-center py-2 px-4 bg-red-500 hover:bg-red-600 font-medium text-sm text-white border border-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none" data-confirm-remove-expired>
Remove Data</button>
</td>
</tr>

View File

@@ -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 %}
<section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="dots-red">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="wave">
<div class="relative z-20 flex flex-wrap items-center justify-center text-center">
<div class="w-full">
<h2 class="text-3xl font-bold text-white mb-4">
Support BasicSwap Development
</h2>
<p class="text-lg text-white max-w-3xl mx-auto">
Help keep BasicSwap free and open-source. Your donations directly fund development, security audits, and community growth.
</p>
</div>
</div>
</div>
</div>
</section>
{{ 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') }}
<div class="xl:container mx-auto">

View File

@@ -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 %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/explores">Explorers</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Explorers', 'url': '/explores'}
]) }}
</div>
</div>
</section>

View File

@@ -25,7 +25,6 @@
<script>
function getAPIKeys() {
return {
cryptoCompare: "{{ chart_api_key|safe }}",
coinGecko: "{{ coingecko_api_key|safe }}"
};
}
@@ -88,9 +87,13 @@
<script src="/static/js/libs/tippy.js"></script>
<!-- UI Components -->
<script src="/static/js/ui/tabs.js"></script>
<script src="/static/js/ui/bids-tab-navigation.js"></script>
<script src="/static/js/ui/dropdown.js"></script>
<!-- Core functionality -->
<script src="/static/js/modules/error-handler.js"></script>
<script src="/static/js/modules/dom-cache.js"></script>
<script src="/static/js/modules/event-handlers.js"></script>
<script src="/static/js/modules/form-validator.js"></script>
<script src="/static/js/modules/coin-utils.js"></script>
<script src="/static/js/modules/coin-manager.js"></script>
<script src="/static/js/modules/config-manager.js"></script>
<script src="/static/js/modules/cache-manager.js"></script>
@@ -104,7 +107,8 @@
<script src="/static/js/modules/balance-updates.js"></script>
<script src="/static/js/modules/identity-manager.js"></script>
<script src="/static/js/modules/summary-manager.js"></script>
<script src="/static/js/amm_counter.js"></script>
<script src="/static/js/modules/wallet-amount.js"></script>
<script src="/static/js/pages/amm-counter.js"></script>
{% if current_page == 'wallets' or current_page == 'wallet' %}
<script src="/static/js/modules/wallet-manager.js"></script>
{% endif %}
@@ -817,18 +821,6 @@
</section>
<script>
function testNotificationSystem() {
if (window.NotificationManager) {
window.NotificationManager.createToast('Test Withdrawal', 'balance_change', {
coinSymbol: 'PART',
subtitle: 'Funds sent'
});
console.log('Test notification created');
} else {
console.error('NotificationManager not available');
}
}
function handleOutsideClick(event) {
const button = document.getElementById('notification-history-button');
const mobileButton = document.getElementById('notification-history-button-mobile');

View File

@@ -1,24 +1,15 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, red_cross_close_svg %}
{% from 'style.html' import red_cross_close_svg %}
{% from 'macros.html' import breadcrumb %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/smsgaddresses">Identity</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/identity/{{ data.identity_address }}">Address: {{ data.identity_address }}</a>
</li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Identity', 'url': '/smsgaddresses'},
{'text': 'Address: ' ~ data.identity_address, 'url': '/identity/' ~ data.identity_address}
]) }}
</div>
</div>
</section>

View File

@@ -17,7 +17,7 @@
</div>
</div>
<div class="w-auto p-2">
<button type="button" class="ms-auto bg-green-50 text-green-500 rounded-lg focus:ring-0 focus:ring-green-400 p-1.5 hover:bg-green-200 inline-flex items-center justify-center h-8 w-8 focus:outline-none dark:bg-gray-800 dark:text-green-400 dark:hover:bg-gray-700" onclick="document.getElementById('messages_{{ m[0] }}').style.display='none';" aria-label="Close"><span class="sr-only">Close</span>
<button type="button" class="ms-auto bg-green-50 text-green-500 rounded-lg focus:ring-0 focus:ring-green-400 p-1.5 hover:bg-green-200 inline-flex items-center justify-center h-8 w-8 focus:outline-none dark:bg-gray-800 dark:text-green-400 dark:hover:bg-gray-700" data-close-message="messages_{{ m[0] }}" aria-label="Close"><span class="sr-only">Close</span>
{{ green_cross_close_svg | safe }}
</button>
</div>
@@ -47,7 +47,7 @@
</div>
</div>
<div class="w-auto p-2">
<button type="button" class="ml-auto bg-red-100 text-red-500 rounded-lg focus:ring-0 focus:ring-red-400 p-1.5 hover:bg-red-200 inline-flex h-8 w-8 focus:outline-none inline-flex items-center justify-center h-8 w-8 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700" onclick="document.getElementById('err_messages_{{ err_messages[0][0] }}').style.display='none';" aria-label="Close">
<button type="button" class="ml-auto bg-red-100 text-red-500 rounded-lg focus:ring-0 focus:ring-red-400 p-1.5 hover:bg-red-200 inline-flex h-8 w-8 focus:outline-none inline-flex items-center justify-center h-8 w-8 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700" data-close-message="err_messages_{{ err_messages[0][0] }}" aria-label="Close">
<span class="sr-only">Close</span>
{{ red_cross_close_svg | safe }}
</button>

View File

@@ -0,0 +1,52 @@
{# Page Header Banner Macro #}
{% macro page_header(title, description='', icon='', dark_bg='dark:bg-blue-500', container_class='lg:container mx-auto', inner_padding='py-8 px-8', title_size='text-2xl', title_extra_class='mb-3 tracking-tighter', dots_style='two') %}
<section class="py-3 px-4 mt-6">
<div class="{{ container_class }}">
<div class="relative {{ inner_padding }} bg-coolGray-900 {{ dark_bg }} rounded-md overflow-hidden">
{% if dots_style == 'one' %}
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
{% elif dots_style == 'all' %}
<img class="absolute z-10 left-4 top-4 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="dots-red">
{% else %}
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
{% endif %}
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="{% if dots_style == 'one' %}{% else %}wave{% endif %}">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="{{ title_extra_class }} {{ title_size }} font-bold text-white">
{% if icon %}
<span class="inline-block align-middle">
<img class="mr-2 h-16" src="/static/images/coins/{{ icon }}.png" alt="{{ icon }}">
</span>
{% endif %}
{{ title }}
</h2>
{% if description %}
<p class="font-normal text-coolGray-200 dark:text-white">
{{ description }}
</p>
{% endif %}
</div>
</div>
</div>
</div>
</section>
{% endmacro %}
{# Breadcrumb Macro #}
{% macro breadcrumb(items) %}
{% from 'style.html' import breadcrumb_line_svg %}
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
{% for item in items %}
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="{{ item.url }}">
<p>{{ item.text }}</p>
</a>
</li>
{% if not loop.last %}
<li>{{ breadcrumb_line_svg | safe }}</li>
{% endif %}
{% endfor %}
</ul>
{% endmacro %}

View File

@@ -1,29 +1,16 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, input_arrow_down_svg, circular_arrows_svg, confirm_green_svg, green_cross_close_svg, circular_info_messages_svg %}
{% from 'style.html' import input_arrow_down_svg, circular_arrows_svg, confirm_green_svg, green_cross_close_svg, circular_info_messages_svg %}
{% from 'macros.html' import breadcrumb %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li>
{{ breadcrumb_line_svg | safe }}
</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="#">Offer</a>
</li>
<li>
{{ breadcrumb_line_svg | safe }}
</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="{{ offer_id }}">OFFER ID: {{ offer_id }}</a>
</li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Offer', 'url': '#'},
{'text': 'OFFER ID: ' ~ offer_id, 'url': offer_id}
]) }}
</div>
</div>
</section>
@@ -414,58 +401,7 @@
</div>
</td>
</tr>
<script>
function handleBidsPageAddress() {
const selectElement = document.querySelector('select[name="addr_from"]');
const STORAGE_KEY = 'lastUsedAddressBids';
if (!selectElement) return;
function loadInitialAddress() {
const savedAddressJSON = localStorage.getItem(STORAGE_KEY);
if (savedAddressJSON) {
try {
const savedAddress = JSON.parse(savedAddressJSON);
selectElement.value = savedAddress.value;
} catch (e) {
selectFirstAddress();
}
} else {
selectFirstAddress();
}
}
function selectFirstAddress() {
if (selectElement.options.length > 1) {
const firstOption = selectElement.options[1];
if (firstOption) {
selectElement.value = firstOption.value;
saveAddress(firstOption.value, firstOption.text);
}
}
}
function saveAddress(value, text) {
const addressData = {
value: value,
text: text
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(addressData));
}
selectElement.addEventListener('change', (event) => {
saveAddress(event.target.value, event.target.selectedOptions[0].text);
});
loadInitialAddress();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', handleBidsPageAddress);
} else {
handleBidsPageAddress();
}
</script>
<!-- Address handling functionality moved to external JS -->
{% if data.amount_negotiable == true %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="px-6">
@@ -626,7 +562,7 @@ if (document.readyState === 'loading') {
<div class="w-full md:w-0/12">
<div class="flex flex-wrap justify-end pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
<div class="w-full md:w-auto p-1.5">
<button type="button" onclick="resetForm()" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md shadow-button focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">
<button type="button" data-reset-form class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md shadow-button focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">
Clear Form
</button>
</div>
@@ -652,6 +588,29 @@ if (document.readyState === 'loading') {
</div>
</div>
</section>
<div id="errorModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
<div class="bg-white dark:bg-gray-500 rounded-lg max-w-md w-full p-6 shadow-lg transition-opacity duration-300 ease-out">
<div class="text-center">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900 mb-4">
<svg class="h-6 w-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4" id="errorTitle">Error</h2>
<p class="text-gray-600 dark:text-gray-200 mb-6 whitespace-pre-line" id="errorMessage">An error occurred</p>
<div class="flex justify-center">
<button type="button" id="errorOk"
class="px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
OK
</button>
</div>
</div>
</div>
</div>
</div>
<div id="confirmModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
@@ -690,7 +649,7 @@ if (document.readyState === 'loading') {
class="px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
Confirm
</button>
<button type="button" onclick="hideConfirmModal()"
<button type="button" data-hide-modal
class="px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
Cancel
</button>
@@ -699,347 +658,8 @@ if (document.readyState === 'loading') {
</div>
</div>
</div>
<script>
const xhr_rates = new XMLHttpRequest();
xhr_rates.onload = () => {
if (xhr_rates.status == 200) {
const obj = JSON.parse(xhr_rates.response);
const inner_html = '<h4 class="bold>Rates</h4><pre><code>' + JSON.stringify(obj, null, ' ') + '</code></pre>';
const ratesDisplay = document.getElementById('rates_display');
if (ratesDisplay) {
ratesDisplay.innerHTML = inner_html;
}
}
};
const xhr_bid_params = new XMLHttpRequest();
xhr_bid_params.onload = () => {
if (xhr_bid_params.status == 200) {
const obj = JSON.parse(xhr_bid_params.response);
const bidAmountSendInput = document.getElementById('bid_amount_send');
if (bidAmountSendInput) {
bidAmountSendInput.value = obj['amount_to'];
}
updateModalValues();
}
};
function lookup_rates() {
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 = '<h4>Rates</h4><p>Updating...</p>';
}
xhr_rates.open('POST', '/json/rates');
xhr_rates.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr_rates.send(`coin_from=${coin_from}&coin_to=${coin_to}`);
}
function resetForm() {
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) {
updateBidParams('rate');
}
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');
});
}
function roundUpToDecimals(value, decimals) {
const factor = Math.pow(10, decimals);
return Math.ceil(value * factor) / factor;
}
function updateBidParams(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 amountFromInput = document.getElementById('amount_from');
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 = roundUpToDecimals(receiveAmount * rate, coin_to_exp).toFixed(coin_to_exp);
bidAmountSendInput.value = sendAmount;
}
}
validateAmountsAfterChange();
xhr_bid_params.open('POST', '/json/rate');
xhr_bid_params.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr_bid_params.send(`coin_from=${coin_from}&coin_to=${coin_to}&rate=${rate}&amt_from=${bidAmountInput?.value || '0'}`);
updateModalValues();
}
function validateAmountsAfterChange() {
const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidAmountInput = document.getElementById('bid_amount');
if (bidAmountSendInput) {
const maxSend = parseFloat(bidAmountSendInput.getAttribute('max'));
validateMaxAmount(bidAmountSendInput, maxSend);
}
if (bidAmountInput) {
const maxReceive = parseFloat(bidAmountInput.getAttribute('max'));
validateMaxAmount(bidAmountInput, maxReceive);
}
}
function validateMaxAmount(input, maxAmount) {
if (!input) return;
const value = parseFloat(input.value) || 0;
if (value > maxAmount) {
input.value = maxAmount;
}
}
function showConfirmModal() {
const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidAmountInput = document.getElementById('bid_amount');
let sendAmount = 0;
let receiveAmount = 0;
if (bidAmountSendInput && bidAmountSendInput.value) {
sendAmount = parseFloat(bidAmountSendInput.value) || 0;
}
if (bidAmountInput && bidAmountInput.value) {
receiveAmount = parseFloat(bidAmountInput.value) || 0;
}
if (sendAmount <= 0 && bidAmountSendInput && !bidAmountSendInput.disabled) {
return false;
}
if (receiveAmount <= 0 && bidAmountInput && !bidAmountInput.disabled) {
return false;
}
updateModalValues();
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
}
return false;
}
function hideConfirmModal() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
}
return false;
}
function updateModalValues() {
const bidAmountInput = document.getElementById('bid_amount');
const bidAmountSendInput = document.getElementById('bid_amount_send');
if (bidAmountInput) {
const modalAmtReceive = document.getElementById('modal-amt-receive');
if (modalAmtReceive) {
modalAmtReceive.textContent = bidAmountInput.value;
}
const modalReceiveCurrency = document.getElementById('modal-receive-currency');
if (modalReceiveCurrency) {
modalReceiveCurrency.textContent = ' {{ data.tla_from }}';
}
}
if (bidAmountSendInput) {
const modalAmtSend = document.getElementById('modal-amt-send');
if (modalAmtSend) {
modalAmtSend.textContent = bidAmountSendInput.value;
}
const modalSendCurrency = document.getElementById('modal-send-currency');
if (modalSendCurrency) {
modalSendCurrency.textContent = ' {{ data.tla_to }}';
}
}
const modalFeeInfo = document.getElementById('modal-fee-info');
if (modalFeeInfo) {
{% if data.xmr_type == true %}
modalFeeInfo.textContent = `(excluding estimated {{ data.amt_from_lock_spend_tx_fee }} {{ data.tla_from }} in tx fees)`;
{% else %}
modalFeeInfo.textContent = '(excluding a tx fee)';
{% endif %}
}
const addrSelect = document.querySelector('select[name="addr_from"]');
if (addrSelect) {
const modalAddrFrom = document.getElementById('modal-addr-from');
if (modalAddrFrom) {
const selectedOption = addrSelect.options[addrSelect.selectedIndex];
const addrText = selectedOption.value === '-1' ? 'New Address' : selectedOption.text.split(' ')[0];
modalAddrFrom.textContent = addrText;
}
}
const validMinsInput = document.querySelector('input[name="validmins"]');
if (validMinsInput) {
const modalValidMins = document.getElementById('modal-valid-mins');
if (modalValidMins) {
modalValidMins.textContent = validMinsInput.value;
}
}
}
function handleBidsPageAddress() {
const selectElement = document.querySelector('select[name="addr_from"]');
const STORAGE_KEY = 'lastUsedAddressBids';
if (!selectElement) return;
function loadInitialAddress() {
try {
const savedAddressJSON = localStorage.getItem(STORAGE_KEY);
if (savedAddressJSON) {
const savedAddress = JSON.parse(savedAddressJSON);
if (savedAddress && savedAddress.value) {
selectElement.value = savedAddress.value;
} else {
selectFirstAddress();
}
} else {
selectFirstAddress();
}
} catch (e) {
console.error('Error loading saved address:', e);
selectFirstAddress();
}
}
function selectFirstAddress() {
if (selectElement.options.length > 1) {
const firstOption = selectElement.options[1];
if (firstOption) {
selectElement.value = firstOption.value;
saveAddress(firstOption.value, firstOption.text);
}
}
}
selectElement.addEventListener('change', (event) => {
if (event.target.selectedOptions[0]) {
saveAddress(event.target.value, event.target.selectedOptions[0].text);
}
});
loadInitialAddress();
}
function saveAddress(value, text) {
try {
const addressData = { value, text };
localStorage.setItem('lastUsedAddressBids', JSON.stringify(addressData));
} catch (e) {
console.error('Error saving address:', e);
}
}
function confirmPopup() {
return confirm("Are you sure?");
}
function handleCancelClick(event) {
if (event) event.preventDefault();
const pathParts = window.location.pathname.split('/');
const offerId = pathParts[pathParts.indexOf('offer') + 1];
window.location.href = `/offer/${offerId}`;
}
document.addEventListener('DOMContentLoaded', function() {
const sendBidBtn = document.querySelector('button[name="sendbid"][value="Send Bid"]');
if (sendBidBtn) {
sendBidBtn.onclick = showConfirmModal;
}
const modalCancelBtn = document.querySelector('#confirmModal .flex button:last-child');
if (modalCancelBtn) {
modalCancelBtn.onclick = hideConfirmModal;
}
const mainCancelBtn = document.querySelector('button[name="cancel"]');
if (mainCancelBtn) {
mainCancelBtn.onclick = handleCancelClick;
}
const validMinsInput = document.querySelector('input[name="validmins"]');
if (validMinsInput) {
validMinsInput.addEventListener('input', updateModalValues);
}
const addrFromSelect = document.querySelector('select[name="addr_from"]');
if (addrFromSelect) {
addrFromSelect.addEventListener('change', updateModalValues);
}
handleBidsPageAddress();
});
</script>
<script src="/static/js/pages/offer-page.js"></script>
{% else %}
<section>
@@ -1063,13 +683,13 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
<!-- todo
{% if data.active_ind == 1 %}
<div class="w-full md:w-auto p-1.5"><button name="archive_offer" value="Archive Offer" type="submit" onclick="return confirmPopup();" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-red-500 hover:text-red-600 border border-red-400 hover:border-red-500 bg-white rounded-md shadow-button focus:ring-0 focus:outline-none"><svg class="text-gray-500 w-5 h-5 mr-2"
<div class="w-full md:w-auto p-1.5"><button name="archive_offer" value="Archive Offer" type="submit" data-confirm class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-red-500 hover:text-red-600 border border-red-400 hover:border-red-500 bg-white rounded-md shadow-button focus:ring-0 focus:outline-none"><svg class="text-gray-500 w-5 h-5 mr-2"
xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24"><g stroke-linecap="round" stroke-width="2" fill="none" stroke="#ef5844" stroke-linejoin="round"><polyline data-cap="butt" points="23 15 16 15 16 18 8 18 8 15 1 15"></polyline><line data-cap="butt" x1="12" y1="1" x2="12" y2="11" stroke="#ef5844"></line><path d="M18.558,6a2,2,0,0,1,1.9,1.368L23,15v6a2,2,0,0,1-2,2H3a2,2,0,0,1-2-2V15L3.544,7.368A2,2,0,0,1,5.442,6"></path><polyline points="15 8 12 11 9 8" stroke="#ef5844"></polyline></g></svg>Archive Offer</button></div>
{% endif %}
-->
{% if data.was_revoked != true %}
<div class="w-full md:w-auto p-1.5">
<button name="revoke_offer" value="Revoke Offer" type="submit" onclick="return confirmPopup();" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Revoke Offer</button>
<button name="revoke_offer" value="Revoke Offer" type="submit" data-confirm class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Revoke Offer</button>
</div>
{% endif %}
{% endif %}
@@ -1097,6 +717,10 @@ document.addEventListener('DOMContentLoaded', function() {
<input type="hidden" id="rate_var" value="{{ data.rate_negotiable }}">
<input type="hidden" id="amount_from" value="{{ data.amt_from }}">
<input type="hidden" id="offer_rate" value="{{ data.rate }}">
<input type="hidden" id="coin_from_name" value="{{ data.coin_from }}">
<input type="hidden" id="coin_to_name" value="{{ data.coin_to }}">
<input type="hidden" id="tla_from" value="{{ data.tla_from }}">
<input type="hidden" id="tla_to" value="{{ data.tla_to }}">
<input type="hidden" name="formid" value="{{ form_id }}">
</form>
<p id="rates_display"></p>

View File

@@ -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 %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li> <a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a> </li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/newoffer">Place</a></li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="#">Setup</a></li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="#">Confirm</a></li>
<li>{{ breadcrumb_line_svg | safe }}</li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Place', 'url': '/newoffer'},
{'text': 'Setup', 'url': '#'},
{'text': 'Confirm', 'url': '#'}
]) }}
</div>
</div>
</section>
@@ -465,7 +460,7 @@
<input type="hidden" name="rate_var" value="rv">
{% endif %}
</form>
<script src="static/js/new_offer.js"></script>
<script src="static/js/pages/offer-new-page.js"></script>
</div>
</div>
</div>

View File

@@ -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 %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/newoffer">Place</a></li>
<li>{{ breadcrumb_line_svg | safe }}</li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Place', 'url': '/newoffer'}
]) }}
</div>
</div>
</section>
@@ -168,9 +164,9 @@
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none"> </div> <input class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-5 focus:ring-0 bold" placeholder="0" type="text" id="amt_from" name="amt_from" value="{{ data.amt_from }}" onchange="set_rate('amt_from');">
</div>
<div class="mt-2 flex space-x-2">
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setOfferAmount(0.25, 'amt_from')">25%</button>
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setOfferAmount(0.5, 'amt_from')">50%</button>
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setOfferAmount(1, 'amt_from')">100%</button>
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" data-set-offer-amount="0.25" data-input-id="amt_from">25%</button>
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" data-set-offer-amount="0.5" data-input-id="amt_from">50%</button>
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" data-set-offer-amount="1" data-input-id="amt_from">100%</button>
</div>
</div>
</div>
@@ -315,7 +311,7 @@
<div class="px-6">
<div class="flex flex-wrap justify-end">
<div class="w-full md:w-auto p-1.5">
<button name="check_rates" type="button" value="Check Current Prices/Rates (JSON)" onclick='lookup_rates();' class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">
<button name="check_rates" type="button" value="Check Current Prices/Rates (JSON)" data-lookup-rates class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">
<span>Check Current Prices/Rates (JSON)</span>
</button>
</div>
@@ -338,6 +334,29 @@
</section>
</div>
<div id="errorModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
<div class="bg-white dark:bg-gray-500 rounded-lg max-w-md w-full p-6 shadow-lg transition-opacity duration-300 ease-out">
<div class="text-center">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900 mb-4">
<svg class="h-6 w-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
</div>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4" id="errorTitle">Error</h2>
<p class="text-gray-600 dark:text-gray-200 mb-6 whitespace-pre-line" id="errorMessage">An error occurred</p>
<div class="flex justify-center">
<button type="button" id="errorOk"
class="px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
OK
</button>
</div>
</div>
</div>
</div>
</div>
<script>
function setOfferAmount(percent, fieldId) {
const amountInput = document.getElementById(fieldId);
@@ -350,7 +369,11 @@ function setOfferAmount(percent, fieldId) {
const selectedOption = coinFromSelect.options[coinFromSelect.selectedIndex];
if (!selectedOption || selectedOption.value === '-1') {
alert('Please select a coin first');
if (window.showErrorModal) {
window.showErrorModal('Validation Error', 'Please select a coin first');
} else {
alert('Please select a coin first');
}
return;
}
@@ -362,7 +385,11 @@ function setOfferAmount(percent, fieldId) {
const floatBalance = parseFloat(balance);
if (isNaN(floatBalance) || floatBalance <= 0) {
alert('Invalid balance for selected coin');
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;
}
@@ -473,7 +500,7 @@ document.addEventListener('DOMContentLoaded', function() {
</script>
<script src="static/js/new_offer.js"></script>
<script src="static/js/pages/offer-new-page.js"></script>
{% include 'footer.html' %}
</div>
</body>

View File

@@ -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 %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li> <a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
{{ breadcrumb_line_svg | safe }}
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/newoffer">Placer</a>
</li>
{{ breadcrumb_line_svg | safe }}
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="#">Setup</a>
</li>
{{ breadcrumb_line_svg | safe }}
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Place', 'url': '/newoffer'},
{'text': 'Setup', 'url': '#'}
]) }}
</div>
</div>
</section>
@@ -447,7 +438,7 @@
<input type="hidden" name="rate_var" value="true">
{% endif %}
</form>
<script src="static/js/new_offer.js"></script>
<script src="static/js/pages/offer-new-page.js"></script>
</div>
</div>
</div>

View File

@@ -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 %}
<script>
window.offersTableConfig = {
@@ -8,29 +9,7 @@
};
</script>
<section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-gray-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover"
src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3">
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">{{ page_type }}</h2>
<p class="font-normal text-coolGray-200 dark:text-white">{{ page_type_description }}</p>
</div>
<div class="w-full md:w-1/2 p-3 flex justify-end items-center hidden">
<a id="refresh" href="/newoffer"
class="rounded-full flex items-center justify-center px-4 py-2 bg-blue-500 hover:bg-green-600 hover:border-green-600 font-medium text-sm text-white border border-blue-500 rounded-md focus:ring-0 focus:outline-none">
{{ place_new_offer_svg | safe }}
<span>Place new Offer</span>
</a>
</div>
</div>
</div>
</div>
</section>
{{ page_header(page_type, page_type_description, dark_bg='dark:bg-gray-500') }}
{% include 'inc_messages.html' %}
@@ -194,7 +173,7 @@
</div>
</div>
</section>
<script src="/static/js/pricechart.js"></script>
<script src="/static/js/pages/offers-pricechart.js"></script>
{% endif %}
@@ -432,6 +411,6 @@
</section>
<input type="hidden" name="formid" value="{{ form_id }}">
<script src="/static/js/offers.js"></script>
<script src="/static/js/pages/offers-page.js"></script>
{% include 'footer.html' %}

View File

@@ -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 %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/rpc">RPC Console</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'RPC Console', 'url': '/rpc'}
]) }}
</div>
</div>
</section>

View File

@@ -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 %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/settings">Settings</a>
</li>
<li> {{ breadcrumb_line_svg | safe }} </li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Settings', 'url': '/settings'}
]) }}
</div>
</div>
</section>
@@ -268,12 +261,12 @@
Apply Changes
</button>
{% if c.can_disable == true %}
<button name="disable_{{ c.name }}" value="Disable" onclick="return confirmPopup('Disable', '{{ c.display_name }}');" type="submit" class="bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded-lg transition-colors focus:outline-none">
<button name="disable_{{ c.name }}" value="Disable" data-confirm data-confirm-action="Disable" data-confirm-coin="{{ c.display_name }}" type="submit" class="bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded-lg transition-colors focus:outline-none">
Disable Coin
</button>
{% endif %}
{% if c.can_reenable == true %}
<button name="enable_{{ c.name }}" value="Enable" onclick="return confirmPopup('Enable', '{{ c.display_name }}');" type="submit" class="bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-lg transition-colors focus:outline-none">
<button name="enable_{{ c.name }}" value="Enable" data-confirm data-confirm-action="Enable" data-confirm-coin="{{ c.display_name }}" type="submit" class="bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-lg transition-colors focus:outline-none">
Enable Coin
</button>
{% endif %}
@@ -401,12 +394,6 @@
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Enable price charts in the interface</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">CryptoCompare API Key</label>
<input type="text" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-700 dark:text-white border border-gray-300 dark:border-gray-600 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-1 focus:outline-none" name="chartapikey" value="{{ chart_settings.chart_api_key }}" placeholder="Enter API key">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">API key for CryptoCompare price data</p>
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">CoinGecko API Key</label>
<div class="max-w-md">
@@ -507,7 +494,7 @@
<div class="flex items-center">
<input type="checkbox" id="check_updates" name="check_updates" value="true" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"{% if general_settings.check_updates %} checked{% endif %}>
<label for="check_updates" class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-300">Update Notifications</label>
<button type="button" onclick="checkForUpdatesNow()" class="ml-3 text-xs bg-gray-600 hover:bg-gray-700 text-white font-medium py-1 px-3 rounded transition-colors focus:outline-none">
<button type="button" data-check-updates class="ml-3 text-xs bg-gray-600 hover:bg-gray-700 text-white font-medium py-1 px-3 rounded transition-colors focus:outline-none">
Check Now
</button>
</div>
@@ -549,10 +536,10 @@
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">Test Notifications</h4>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<div class="space-y-3">
<button type="button" onclick="window.NotificationManager && window.NotificationManager.testToasts()" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors focus:outline-none">
<button type="button" data-test-notification="all" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors focus:outline-none">
Test All Notification Types
</button>
<button type="button" onclick="testUpdateNotification()" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors focus:outline-none">
<button type="button" data-test-notification="update" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors focus:outline-none">
Test Update Notification
</button>
<button type="button" onclick="testLiveUpdateCheck()" class="bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-lg transition-colors focus:outline-none">
@@ -577,174 +564,6 @@
</form>
</div>
<script>
function syncNotificationSettings() {
if (window.NotificationManager && typeof window.NotificationManager.updateSettings === 'function') {
const backendSettings = {
showNewOffers: document.getElementById('notifications_new_offers').checked,
showNewBids: document.getElementById('notifications_new_bids').checked,
showBidAccepted: document.getElementById('notifications_bid_accepted').checked,
showBalanceChanges: document.getElementById('notifications_balance_changes').checked,
showOutgoingTransactions: document.getElementById('notifications_outgoing_transactions').checked,
showSwapCompleted: document.getElementById('notifications_swap_completed').checked,
showUpdateNotifications: document.getElementById('check_updates').checked,
notificationDuration: parseInt(document.getElementById('notifications_duration').value) * 1000
};
window.NotificationManager.updateSettings(backendSettings);
}
}
document.getElementById('notifications-tab').addEventListener('click', function() {
setTimeout(syncNotificationSettings, 100);
});
function testUpdateNotification() {
if (window.NotificationManager) {
window.NotificationManager.createToast(
'Update Available: v0.15.0',
'update_available',
{
subtitle: 'Current: v{{ version }} • 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.'
}
);
}
}
function testLiveUpdateCheck() {
const button = event.target;
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) {
if (data.update_available) {
window.NotificationManager.createToast(
`Live Update Available: v${data.latest_version}`,
'update_available',
{
subtitle: `Current: v${data.current_version} • Click to view release`,
releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${data.latest_version}`,
releaseNotes: 'This is a real update check from GitHub API.'
}
);
} else {
window.NotificationManager.createToast(
'No Updates Available',
'success',
{
subtitle: `Current version v${data.current_version} 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(() => {
button.textContent = originalText;
button.disabled = false;
});
}
function checkForUpdatesNow() {
const button = event.target;
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) {
if (data.update_available) {
window.NotificationManager.createToast(
`Update Available: v${data.latest_version}`,
'update_available',
{
subtitle: `Current: v${data.current_version} • Click to view release`,
releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${data.latest_version}`,
releaseNotes: `New version v${data.latest_version} is available. Click to view details on GitHub.`
}
);
} else {
window.NotificationManager.createToast(
'You\'re Up to Date!',
'success',
{
subtitle: `Current version v${data.current_version} 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(() => {
button.textContent = originalText;
button.disabled = false;
});
}
document.addEventListener('DOMContentLoaded', function() {
syncNotificationSettings();
});
document.addEventListener('change', function(e) {
if (e.target.closest('#notifications')) {
syncNotificationSettings();
}
});
</script>
<div class="tab-content hidden" id="tor" role="tabpanel" aria-labelledby="tor-tab">
<form method="post" id="tor-form">
<div class="space-y-6">
@@ -837,131 +656,6 @@
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const tabButtons = document.querySelectorAll('.tab-button');
const tabContents = document.querySelectorAll('.tab-content');
function 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);
});
});
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)';
}
});
});
let confirmCallback = null;
let triggerElement = null;
document.getElementById('confirmYes').addEventListener('click', function() {
if (typeof confirmCallback === 'function') {
confirmCallback();
}
hideConfirmDialog();
});
document.getElementById('confirmNo').addEventListener('click', hideConfirmDialog);
function showConfirmDialog(title, message, callback) {
confirmCallback = callback;
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
}
return false;
}
function hideConfirmDialog() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
}
confirmCallback = null;
return false;
}
window.confirmPopup = function(action, coin_name) {
triggerElement = document.activeElement;
const title = `Confirm ${action} ${coin_name}`;
const message = `Are you sure you want to ${action.toLowerCase()} ${coin_name}?\n\nThis will shutdown BasicSwap.`;
return showConfirmDialog(title, message, function() {
if (triggerElement) {
const form = triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = triggerElement.name;
hiddenInput.value = triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
});
};
const overrideButtonConfirm = function(button, action, coinName) {
if (button) {
button.removeAttribute('onclick');
button.addEventListener('click', function(e) {
e.preventDefault();
triggerElement = this;
return confirmPopup(action, coinName);
});
}
};
const disableButtons = document.querySelectorAll('button[name^="disable_"]');
disableButtons.forEach(btn => {
const coinName = btn.name.replace('disable_', '');
const displayName = btn.closest('.coin-card').querySelector('h3').textContent.split(' (')[0];
overrideButtonConfirm(btn, 'Disable', displayName);
});
const enableButtons = document.querySelectorAll('button[name^="enable_"]');
enableButtons.forEach(btn => {
const coinName = btn.name.replace('enable_', '');
const displayName = btn.closest('.coin-card').querySelector('h3').textContent.split(' (')[0];
overrideButtonConfirm(btn, 'Enable', displayName);
});
switchTab('coins');
});
</script>
<script src="/static/js/pages/settings-page.js"></script>
{% include 'footer.html' %}

View File

@@ -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 %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/smsgaddresses">SMSG Addresses</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'SMSG Addresses', 'url': '/smsgaddresses'}
]) }}
</div>
</div>
</section>

View File

@@ -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 %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/tor">Tor</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Tor', 'url': '/tor'}
]) }}
</div>
</div>
</section>

View File

@@ -129,7 +129,7 @@
<div class="mt-8 text-center space-y-2">
<p class="text-sm text-gray-600 dark:text-gray-400">
Need help?
<a href="https://academy.particl.io/en/latest/faq/get_support.html"
<a href="https://docs.basicswapdex.com/docs/intro/"
target="_blank"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline">
View tutorials

File diff suppressed because it is too large Load Diff

View File

@@ -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 %}
<section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto">
@@ -190,425 +190,7 @@
{% include 'footer.html' %}
<script>
document.addEventListener('DOMContentLoaded', function() {
if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') {
window.WebSocketManager.initialize();
}
function setupWalletsWebSocketUpdates() {
window.BalanceUpdatesManager.setup({
contextKey: 'wallets',
balanceUpdateCallback: updateWalletBalances,
swapEventCallback: updateWalletBalances,
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 = setTimeout(() => {
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
window.WalletManager.updatePrices(true);
}
}, 500);
}
}
});
window.walletsPriceHandlerId = priceHandlerId;
}
}
function updateWalletBalances(balanceData) {
if (balanceData) {
balanceData.forEach(coin => {
updateWalletDisplay(coin);
});
setTimeout(() => {
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
window.WalletManager.updatePrices(true);
}
}, 250);
} else {
window.BalanceUpdatesManager.fetchBalanceData()
.then(data => updateWalletBalances(data))
.catch(error => {
console.error('Error updating wallet balances:', error);
});
}
}
function updateWalletDisplay(coinData) {
if (coinData.name === 'Particl') {
updateSpecificBalance('Particl', 'Balance:', coinData.balance, coinData.ticker || 'PART');
} else if (coinData.name === 'Particl Anon') {
updateSpecificBalance('Particl', 'Anon Balance:', coinData.balance, coinData.ticker || 'PART');
removePendingBalance('Particl', 'Anon Balance:');
if (coinData.pending && parseFloat(coinData.pending) > 0) {
updatePendingBalance('Particl', 'Anon Balance:', coinData.pending, coinData.ticker || 'PART', 'Anon Pending:', coinData);
}
} else if (coinData.name === 'Particl Blind') {
updateSpecificBalance('Particl', 'Blind Balance:', coinData.balance, coinData.ticker || 'PART');
removePendingBalance('Particl', 'Blind Balance:');
if (coinData.pending && parseFloat(coinData.pending) > 0) {
updatePendingBalance('Particl', 'Blind Balance:', coinData.pending, coinData.ticker || 'PART', 'Blind Unconfirmed:', coinData);
}
} else {
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) {
updatePendingDisplay(coinData);
} else {
removePendingDisplay(coinData.name);
}
}
}
}
function updateSpecificBalance(coinName, labelText, balance, ticker, isPending = false) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
let found = false;
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}`;
}
found = true;
}
}
}
});
}
function updatePendingDisplay(coinData) {
const walletContainer = 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) {
pendingContainer = document.createElement('div');
pendingContainer.className = 'pending-container';
const pendingRow = document.createElement('div');
pendingRow.className = 'flex mb-2 justify-between items-center';
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
pendingRow.innerHTML = `
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Pending:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 coinname-value" data-coinname="${coinData.name}">+${cleanPending} ${coinData.ticker || coinData.name}</span>
`;
pendingContainer.appendChild(pendingRow);
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 usdRow = document.createElement('div');
usdRow.className = 'flex mb-2 justify-between items-center';
usdRow.innerHTML = `
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Pending USD value:</h4>
<div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value">${initialUSD}</div>
`;
pendingContainer.appendChild(usdRow);
const balanceRow = walletContainer.querySelector('.flex.mb-2.justify-between.items-center');
let insertAfterElement = balanceRow;
if (balanceRow) {
let nextElement = balanceRow.nextElementSibling;
while (nextElement) {
const labelElement = nextElement.querySelector('h4');
if (labelElement && labelElement.textContent.includes('USD value:') &&
!labelElement.textContent.includes('Pending') && !labelElement.textContent.includes('Unconfirmed')) {
insertAfterElement = nextElement;
break;
}
nextElement = nextElement.nextElementSibling;
}
}
if (insertAfterElement && insertAfterElement.nextSibling) {
walletContainer.insertBefore(pendingContainer, insertAfterElement.nextSibling);
} else {
walletContainer.appendChild(pendingContainer);
}
} else {
const pendingElement = pendingContainer.querySelector('.coinname-value');
if (pendingElement) {
const cleanPending = coinData.pending.toString().replace(/^\+/, ''); // Remove existing + if any
pendingElement.textContent = `+${cleanPending} ${coinData.ticker || coinData.name}`;
}
}
}
function removePendingDisplay(coinName) {
const walletContainer = findWalletContainer(coinName);
if (!walletContainer) return;
const pendingContainer = walletContainer.querySelector('.pending-container');
if (pendingContainer) {
pendingContainer.remove();
}
const existingPendingElements = walletContainer.querySelectorAll('.flex.mb-2.justify-between.items-center');
existingPendingElements.forEach(element => {
const labelElement = element.querySelector('h4');
if (labelElement) {
const labelText = labelElement.textContent;
if (labelText.includes('Pending:') || labelText.includes('Pending USD value:')) {
element.style.display = 'none';
}
}
});
}
function removeSpecificPending(coinName, labelText) {
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) {
parentDiv.remove();
}
}
}
});
}
function updatePendingBalance(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 newPendingElement = document.createElement('div');
newPendingElement.className = 'flex mb-2 justify-between items-center';
newPendingElement.innerHTML = `
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">${pendingLabel}</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 coinname-value" data-coinname="${coinName}">+${pendingAmount} ${ticker}</span>
`;
insertAfterElement.parentNode.insertBefore(newPendingElement, 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(pendingAmount) * price.usd).toFixed(2);
initialUSD = `$${usdValue}`;
}
}
const usdElement = document.createElement('div');
usdElement.className = 'flex mb-2 justify-between items-center';
usdElement.innerHTML = `
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">${pendingLabel.replace(':', '')} USD value:</h4>
<div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value">${initialUSD}</div>
`;
newPendingElement.parentNode.insertBefore(usdElement, newPendingElement.nextSibling);
} else {
const pendingSpan = pendingElement.querySelector('.coinname-value');
if (pendingSpan) {
pendingSpan.textContent = `+${pendingAmount} ${ticker}`;
}
}
}
function removePendingBalance(coinName, balanceType) {
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 nextElement = targetElement.nextElementSibling;
while (nextElement) {
const labelElement = nextElement.querySelector('h4');
if (labelElement) {
const labelText = labelElement.textContent;
if (labelText.includes('Pending:') || labelText.includes('Unconfirmed:') ||
labelText.includes('Anon Pending:') || labelText.includes('Blind Unconfirmed:') ||
labelText.includes('Pending USD value:') || labelText.includes('Unconfirmed USD value:') ||
labelText.includes('Anon Pending USD value:') || labelText.includes('Blind Unconfirmed USD value:')) {
const elementToRemove = nextElement;
nextElement = nextElement.nextElementSibling;
elementToRemove.remove();
} else if (labelText.includes('Balance:')) {
break; // Stop if we hit another balance
} else {
nextElement = nextElement.nextElementSibling;
}
} else {
nextElement = nextElement.nextElementSibling;
}
}
}
function findWalletContainer(coinName) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
for (const element of balanceElements) {
if (element.getAttribute('data-coinname') === coinName) {
return element.closest('.bg-coolGray-100, .dark\\:bg-gray-600');
}
}
return null;
}
function cleanupWalletsBalanceUpdates() {
window.BalanceUpdatesManager.cleanup('wallets');
if (window.walletsPriceHandlerId && window.WebSocketManager) {
window.WebSocketManager.removeMessageHandler('message', window.walletsPriceHandlerId);
}
clearTimeout(window.walletsPriceUpdateTimeout);
}
window.BalanceUpdatesManager.initialize();
setupWalletsWebSocketUpdates();
setTimeout(() => {
updateWalletBalances();
}, 1000);
if (window.CleanupManager) {
window.CleanupManager.registerResource('walletsBalanceUpdates', null, cleanupWalletsBalanceUpdates);
}
window.addEventListener('beforeunload', cleanupWalletsBalanceUpdates);
});
</script>
<script src="/static/js/pages/wallets-page.js"></script>
</body>
</html>

View File

@@ -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 %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/watched">Watched Outputs</a>
</li>
<li>{{ breadcrumb_line_svg | safe }}</li>
</ul>
{{ breadcrumb([
{'text': 'Home', 'url': '/'},
{'text': 'Watched Outputs', 'url': '/watched'}
]) }}
</div>
</div>
</section>

View File

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

View File

@@ -43,10 +43,7 @@ from basicswap.chainparams import (
Coins,
ticker_map,
)
from basicswap.explorers import (
default_chart_api_key,
default_coingecko_api_key,
)
from basicswap.explorers import default_coingecko_api_key
def value_or_none(v):
@@ -541,23 +538,9 @@ def page_newoffer(self, url_split, post_string):
automation_filters = {"type_ind": Concepts.OFFER, "sort_by": "label"}
automation_strategies = swap_client.listAutomationStrategies(automation_filters)
chart_api_key = swap_client.settings.get("chart_api_key", "")
if chart_api_key == "":
chart_api_key_enc = swap_client.settings.get("chart_api_key_enc", "")
chart_api_key = (
default_chart_api_key
if chart_api_key_enc == ""
else bytes.fromhex(chart_api_key_enc).decode("utf-8")
)
coingecko_api_key = swap_client.settings.get("coingecko_api_key", "")
if coingecko_api_key == "":
coingecko_api_key_enc = swap_client.settings.get("coingecko_api_key_enc", "")
coingecko_api_key = (
default_coingecko_api_key
if coingecko_api_key_enc == ""
else bytes.fromhex(coingecko_api_key_enc).decode("utf-8")
)
coingecko_api_key = get_api_key_setting(
swap_client.settings, "coingecko_api_key", default_coingecko_api_key
)
return self.render_template(
template,
@@ -575,7 +558,6 @@ def page_newoffer(self, url_split, post_string):
(strSwapType(x), strSwapDesc(x)) for x in SwapTypes if strSwapType(x)
],
"show_chart": swap_client.settings.get("show_chart", True),
"chart_api_key": chart_api_key,
"coingecko_api_key": coingecko_api_key,
},
)
@@ -990,9 +972,6 @@ def page_offers(self, url_split, post_string, sent=False):
coins_from, coins_to = listAvailableCoins(swap_client, split_from=True)
chart_api_key = get_api_key_setting(
swap_client.settings, "chart_api_key", default_chart_api_key
)
coingecko_api_key = get_api_key_setting(
swap_client.settings, "coingecko_api_key", default_coingecko_api_key
)
@@ -1041,7 +1020,6 @@ def page_offers(self, url_split, post_string, sent=False):
"show_chart": (
False if sent else swap_client.settings.get("show_chart", True)
),
"chart_api_key": chart_api_key,
"coingecko_api_key": coingecko_api_key,
"coins_from": coins_from,
"coins": coins_to,

View File

@@ -49,9 +49,6 @@ def page_settings(self, url_split, post_string):
active_tab = "general"
data = {
"show_chart": toBool(get_data_entry(form_data, "showchart")),
"chart_api_key": html.unescape(
get_data_entry_or(form_data, "chartapikey", "")
),
"coingecko_api_key": html.unescape(
get_data_entry_or(form_data, "coingeckoapikey", "")
),
@@ -213,16 +210,12 @@ def page_settings(self, url_split, post_string):
"check_updates": swap_client.settings.get("check_updates", True),
}
chart_api_key = get_api_key_setting(
swap_client.settings, "chart_api_key", escape=True
)
coingecko_api_key = get_api_key_setting(
swap_client.settings, "coingecko_api_key", escape=True
swap_client.settings, "coingecko_api_key", default_value="", escape=True
)
chart_settings = {
"show_chart": swap_client.settings.get("show_chart", True),
"chart_api_key": chart_api_key,
"coingecko_api_key": coingecko_api_key,
"enabled_chart_coins": swap_client.settings.get("enabled_chart_coins", ""),
}

View File

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

View File

@@ -18,7 +18,7 @@ from util import (
BSX_0_PORT,
get_driver,
)
from basicswap.ui.page_offers import default_chart_api_key
from basicswap.explorers import default_coingecko_api_key
def click_option(el, option_text):
@@ -102,13 +102,13 @@ def test_settings(driver):
settings = json.load(fs)
assert settings["show_chart"] is expected_chart_state
chart_api_key = bytes.fromhex(settings.get("chart_api_key_enc", "")).decode(
"utf-8"
)
assert chart_api_key == difficult_text
coingecko_api_key = bytes.fromhex(
settings.get("coingecko_api_key_enc", "")
).decode("utf-8")
assert coingecko_api_key == difficult_text
hex_text = default_chart_api_key
el = driver.find_element(By.NAME, "chartapikey")
hex_text = default_coingecko_api_key
el = driver.find_element(By.NAME, "coingeckoapikey")
el.clear()
el.send_keys(hex_text)
btn_apply_chart = wait.until(
@@ -117,13 +117,13 @@ def test_settings(driver):
btn_apply_chart.click()
time.sleep(1)
el = driver.find_element(By.NAME, "chartapikey")
el = driver.find_element(By.NAME, "coingeckoapikey")
assert el.get_property("value") == hex_text
with open(settings_path_0) as fs:
settings = json.load(fs)
assert settings.get("chart_api_key") == hex_text
assert settings.get("coingecko_api_key") == hex_text
else:
print("Chart settings not accessible, skipping chart tests")
expected_chart_state = None

View File

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