mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-05 10:28:10 +01:00
Merge pull request #378 from gerlofvanek/refactor-2
Refactor + Optimizations
This commit is contained in:
@@ -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):
|
||||
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,7 +12601,6 @@ 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}.")
|
||||
ensure(len(coins_list) > 0, "Must specify coin/s")
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
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/simple/price?ids={coin_ids}&vs_currencies={ticker_to}"
|
||||
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}"
|
||||
|
||||
self.log.debug(f"lookupFiatRates: {url}")
|
||||
js = json.loads(self.readURL(url, timeout=10, headers=headers))
|
||||
js = json.loads(self.readURL(url, timeout=5, headers=headers))
|
||||
|
||||
for k, v in js.items():
|
||||
return_rates[int(exchange_name_map[k])] = v[ticker_to]
|
||||
new_values[exchange_name_map[k]] = v[ticker_to]
|
||||
elif rate_source == "cryptocompare.com":
|
||||
ticker_to: str = fiatTicker(currency_to).upper()
|
||||
api_key: str = get_api_key_setting(
|
||||
self.settings,
|
||||
"chart_api_key",
|
||||
default_chart_api_key,
|
||||
escape=True,
|
||||
if "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}"
|
||||
)
|
||||
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]
|
||||
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,
|
||||
"rate_source": rate_source,
|
||||
"last_updated": now,
|
||||
},
|
||||
)
|
||||
if cursor.rowcount < 1:
|
||||
cursor.execute(
|
||||
insert_query,
|
||||
{
|
||||
"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,
|
||||
},
|
||||
)
|
||||
|
||||
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:
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
import json
|
||||
|
||||
|
||||
default_chart_api_key = (
|
||||
"95dd900af910656e0e17c41f2ddc5dba77d01bf8b0e7d2787634a16bd976c553"
|
||||
)
|
||||
default_coingecko_api_key = "CG-8hm3r9iLfpEXv4ied8oLbeUj"
|
||||
|
||||
|
||||
|
||||
@@ -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,6 +169,7 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
if not session_id:
|
||||
return False
|
||||
|
||||
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(
|
||||
@@ -195,6 +196,7 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return None
|
||||
form_data = parse.parse_qs(post_string)
|
||||
form_id = form_data[b"formid"][0].decode("utf-8")
|
||||
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
|
||||
@@ -216,40 +218,45 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
args_dict["debug_mode"] = True
|
||||
if swap_client.debug_ui:
|
||||
args_dict["debug_ui_mode"] = 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 as e:
|
||||
except Exception:
|
||||
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())
|
||||
|
||||
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:
|
||||
except Exception:
|
||||
args_dict["current_status"] = "stopped"
|
||||
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()
|
||||
else:
|
||||
args_dict["current_status"] = "unknown"
|
||||
args_dict["amm_active_count"] = 0
|
||||
|
||||
if "messages" in args_dict:
|
||||
messages_with_ids = []
|
||||
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 = []
|
||||
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
|
||||
@@ -266,13 +273,25 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
args_dict["current_page"] = "index"
|
||||
|
||||
shutdown_token = os.urandom(8).hex()
|
||||
with self.server.session_lock:
|
||||
self.server.session_tokens["shutdown"] = shutdown_token
|
||||
args_dict["shutdown_token"] = shutdown_token
|
||||
|
||||
if is_authenticated:
|
||||
try:
|
||||
encrypted, locked = swap_client.getLockedState()
|
||||
args_dict["encrypted"] = encrypted
|
||||
args_dict["locked"] = locked
|
||||
except Exception as e:
|
||||
args_dict["encrypted"] = False
|
||||
args_dict["locked"] = False
|
||||
if swap_client.debug:
|
||||
swap_client.log.warning(f"Could not get wallet locked state: {e}")
|
||||
else:
|
||||
args_dict["encrypted"] = args_dict.get("encrypted", False)
|
||||
args_dict["locked"] = args_dict.get("locked", False)
|
||||
|
||||
with self.server.msg_id_lock:
|
||||
if self.server.msg_id_counter >= 0x7FFFFFFF:
|
||||
self.server.msg_id_counter = 0
|
||||
|
||||
@@ -364,6 +383,7 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
expires = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=SESSION_DURATION_MINUTES
|
||||
)
|
||||
with self.server.session_lock:
|
||||
self.server.active_sessions[session_id] = {"expires": expires}
|
||||
cookie_header = self._set_session_cookie(session_id)
|
||||
|
||||
@@ -628,11 +648,13 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
|
||||
if len(url_split) > 2:
|
||||
token = url_split[2]
|
||||
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()
|
||||
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()
|
||||
@@ -935,13 +957,17 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
return self.page_error(str(ex))
|
||||
|
||||
def do_GET(self):
|
||||
try:
|
||||
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):
|
||||
try:
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
post_string = self.rfile.read(content_length)
|
||||
|
||||
@@ -951,6 +977,8 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
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()
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
133
basicswap/rpc.py
133
basicswap/rpc.py
@@ -6,8 +6,10 @@
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
import urllib
|
||||
import http.client
|
||||
from xmlrpc.client import (
|
||||
Fault,
|
||||
Transport,
|
||||
@@ -15,6 +17,35 @@ from xmlrpc.client import (
|
||||
)
|
||||
from .util import jsonDecimal
|
||||
|
||||
_use_rpc_pooling = False
|
||||
_rpc_pool_settings = {}
|
||||
|
||||
|
||||
def enable_rpc_pooling(settings):
|
||||
global _use_rpc_pooling, _rpc_pool_settings
|
||||
_use_rpc_pooling = settings.get("enabled", False)
|
||||
_rpc_pool_settings = settings
|
||||
|
||||
|
||||
class TimeoutTransport(Transport):
|
||||
def __init__(self, timeout=10, *args, **kwargs):
|
||||
self.timeout = timeout
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def make_connection(self, host):
|
||||
conn = http.client.HTTPConnection(host, timeout=self.timeout)
|
||||
return conn
|
||||
|
||||
|
||||
class TimeoutSafeTransport(SafeTransport):
|
||||
def __init__(self, timeout=10, *args, **kwargs):
|
||||
self.timeout = timeout
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def make_connection(self, host):
|
||||
conn = http.client.HTTPSConnection(host, timeout=self.timeout)
|
||||
return conn
|
||||
|
||||
|
||||
class Jsonrpc:
|
||||
# __getattr__ complicates extending ServerProxy
|
||||
@@ -29,22 +60,40 @@ class Jsonrpc:
|
||||
use_builtin_types=False,
|
||||
*,
|
||||
context=None,
|
||||
timeout=10,
|
||||
):
|
||||
# establish a "logical" server connection
|
||||
|
||||
# get the url
|
||||
parsed = urllib.parse.urlparse(uri)
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
raise OSError("unsupported XML-RPC protocol")
|
||||
|
||||
self.__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
131
basicswap/rpc_pool.py
Normal 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()
|
||||
@@ -4,34 +4,34 @@ const ApiManager = (function() {
|
||||
isInitialized: false
|
||||
};
|
||||
|
||||
const config = {
|
||||
function getConfig() {
|
||||
return window.config || window.ConfigManager || {
|
||||
requestTimeout: 60000,
|
||||
retryDelays: [5000, 15000, 30000],
|
||||
rateLimits: {
|
||||
coingecko: {
|
||||
requestsPerMinute: 50,
|
||||
minInterval: 1200
|
||||
},
|
||||
cryptocompare: {
|
||||
requestsPerMinute: 30,
|
||||
minInterval: 2000
|
||||
}
|
||||
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,54 +311,13 @@ 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;
|
||||
}
|
||||
} else {
|
||||
const coinGeckoCoins = {
|
||||
'WOW': 'wownero',
|
||||
'PART': 'particl',
|
||||
'BTC': 'bitcoin'
|
||||
};
|
||||
if (coinGeckoCoins[coin]) {
|
||||
useCoinGecko = true;
|
||||
coingeckoId = coinGeckoCoins[coin];
|
||||
}
|
||||
}
|
||||
|
||||
if (useCoinGecko && coingeckoId) {
|
||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||
try {
|
||||
let days;
|
||||
if (resolution === 'day') {
|
||||
days = 1;
|
||||
@@ -398,48 +326,30 @@ const ApiManager = (function() {
|
||||
} 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;
|
||||
|
||||
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 (!response.data) {
|
||||
console.error('Response missing data field:', response);
|
||||
throw new Error('Invalid response from backend');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching CoinGecko data for ${coin}:`, error);
|
||||
console.error('Error fetching historical data:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return this.rateLimiter.queueRequest('cryptocompare', async () => {
|
||||
try {
|
||||
const apiKey = window.config?.apiKeys?.cryptoCompare || '';
|
||||
let url;
|
||||
|
||||
if (resolution === 'day') {
|
||||
url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coin}&tsym=USD&limit=24&api_key=${apiKey}`;
|
||||
} else if (resolution === 'year') {
|
||||
url = `https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coin}&tsym=USD&limit=365&api_key=${apiKey}`;
|
||||
} else {
|
||||
url = `https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coin}&tsym=USD&limit=180&api_key=${apiKey}`;
|
||||
}
|
||||
|
||||
const response = await this.makePostRequest(url);
|
||||
if (response.Response === "Error") {
|
||||
console.error(`API Error for ${coin}:`, response.Message);
|
||||
throw new Error(response.Message);
|
||||
} else if (response.Data && response.Data.Data) {
|
||||
results[coin] = response.Data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching CryptoCompare data for ${coin}:`, error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(fetchPromises);
|
||||
return results;
|
||||
},
|
||||
|
||||
dispose: function() {
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
const CacheManager = (function() {
|
||||
const defaults = window.config?.cacheConfig?.storage || {
|
||||
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;
|
||||
const serializedItem = window.ErrorHandler
|
||||
? window.ErrorHandler.safeExecute(() => JSON.stringify(item), 'CacheManager.set.serialize', null)
|
||||
: (() => {
|
||||
try {
|
||||
serializedItem = JSON.stringify(item);
|
||||
return JSON.stringify(item);
|
||||
} catch (e) {
|
||||
console.error('Failed to serialize cache item:', e);
|
||||
return false;
|
||||
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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
191
basicswap/static/js/modules/coin-utils.js
Normal file
191
basicswap/static/js/modules/coin-utils.js
Normal 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');
|
||||
@@ -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) {
|
||||
|
||||
207
basicswap/static/js/modules/dom-cache.js
Normal file
207
basicswap/static/js/modules/dom-cache.js
Normal 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);
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
215
basicswap/static/js/modules/error-handler.js
Normal file
215
basicswap/static/js/modules/error-handler.js
Normal 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');
|
||||
342
basicswap/static/js/modules/event-handlers.js
Normal file
342
basicswap/static/js/modules/event-handlers.js
Normal 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);
|
||||
|
||||
})();
|
||||
225
basicswap/static/js/modules/form-validator.js
Normal file
225
basicswap/static/js/modules/form-validator.js
Normal 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;
|
||||
|
||||
})();
|
||||
@@ -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,6 +183,11 @@ const IdentityManager = (function() {
|
||||
|
||||
async function fetchWithRetry(address, attempt = 1) {
|
||||
try {
|
||||
let data;
|
||||
|
||||
if (window.ApiManager) {
|
||||
data = await window.ApiManager.makeRequest(`/json/identities/${address}`, 'GET');
|
||||
} else {
|
||||
const response = await fetch(`/json/identities/${address}`, {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
@@ -163,7 +196,10 @@ const IdentityManager = (function() {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
data = 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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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,7 +288,6 @@ function ensureToastContainer() {
|
||||
return coinMap[coinId] || `Coin ${coinId}`;
|
||||
}
|
||||
|
||||
// Todo: Remove later.
|
||||
function formatCoinAmount(amount, coinId) {
|
||||
const divisors = {
|
||||
1: 100000000, // PART - 8 decimals
|
||||
@@ -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,7 +434,16 @@ function ensureToastContainer() {
|
||||
);
|
||||
}, 4000);
|
||||
|
||||
setTimeout(() => {
|
||||
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',
|
||||
@@ -445,15 +453,49 @@ function ensureToastContainer() {
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
79
basicswap/static/js/modules/qrcode-manager.js
Normal file
79
basicswap/static/js/modules/qrcode-manager.js
Normal 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;
|
||||
|
||||
})();
|
||||
@@ -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 => {
|
||||
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 => {
|
||||
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');
|
||||
|
||||
@@ -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,7 +51,40 @@ const TooltipManager = (function() {
|
||||
this.performPeriodicCleanup(true);
|
||||
}
|
||||
|
||||
const createTooltip = () => {
|
||||
this.creationQueue.push({ element, content, options });
|
||||
|
||||
if (!this.isProcessingQueue) {
|
||||
this.processCreationQueue();
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -76,10 +112,6 @@ const TooltipManager = (function() {
|
||||
CleanupManager.requestAnimationFrame(retryCreate);
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
CleanupManager.requestAnimationFrame(createTooltip);
|
||||
return null;
|
||||
}
|
||||
|
||||
createTooltipInstance(element, content, options = {}) {
|
||||
@@ -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,10 +259,18 @@ const TooltipManager = (function() {
|
||||
|
||||
this.tooltipData.delete(element);
|
||||
tooltipInstanceMap.delete(element);
|
||||
};
|
||||
|
||||
if (window.ErrorHandler) {
|
||||
window.ErrorHandler.safeExecute(destroyFn, 'TooltipManager.destroy', null);
|
||||
} else {
|
||||
try {
|
||||
destroyFn();
|
||||
} catch (error) {
|
||||
console.error('Error destroying tooltip:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getActiveTooltipInstances() {
|
||||
const result = [];
|
||||
@@ -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);
|
||||
|
||||
196
basicswap/static/js/modules/wallet-amount.js
Normal file
196
basicswap/static/js/modules/wallet-amount.js
Normal 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);
|
||||
};
|
||||
|
||||
})();
|
||||
@@ -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,6 +94,15 @@ const WalletManager = (function() {
|
||||
|
||||
const fetchCoinsString = coinsToFetch.join(',');
|
||||
|
||||
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'},
|
||||
@@ -109,7 +117,8 @@ const WalletManager = (function() {
|
||||
throw new Error(`HTTP error: ${mainResponse.status}`);
|
||||
}
|
||||
|
||||
const mainData = await mainResponse.json();
|
||||
mainData = await mainResponse.json();
|
||||
}
|
||||
|
||||
if (mainData && mainData.rates) {
|
||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
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');
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.clearTimeout(state.connectTimeout);
|
||||
} else {
|
||||
clearTimeout(state.connectTimeout);
|
||||
}
|
||||
stopHealthCheck();
|
||||
|
||||
if (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;
|
||||
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) {
|
||||
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) {
|
||||
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');
|
||||
|
||||
294
basicswap/static/js/pages/amm-config-tabs.js
Normal file
294
basicswap/static/js/pages/amm-config-tabs.js
Normal 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;
|
||||
|
||||
})();
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
573
basicswap/static/js/pages/amm-page.js
Normal file
573
basicswap/static/js/pages/amm-page.js
Normal 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);
|
||||
|
||||
})();
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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 });
|
||||
@@ -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) {
|
||||
@@ -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') {
|
||||
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
initializeTooltips();
|
||||
|
||||
setTimeout(() => {
|
||||
forceTooltipDOMCleanup();
|
||||
}, 100);
|
||||
}, 10);
|
||||
});
|
||||
});
|
||||
}
|
||||
} 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() {
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
364
basicswap/static/js/pages/offer-page.js
Normal file
364
basicswap/static/js/pages/offer-page.js
Normal 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);
|
||||
|
||||
})();
|
||||
@@ -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,8 +907,10 @@ function updateConnectionStatus(status) {
|
||||
}
|
||||
|
||||
function updateRowTimes() {
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
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);
|
||||
@@ -884,6 +922,15 @@ function updateRowTimes() {
|
||||
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}`;
|
||||
}
|
||||
@@ -891,7 +938,6 @@ function updateRowTimes() {
|
||||
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) {
|
||||
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';
|
||||
if (window.CoinUtils) {
|
||||
return window.CoinUtils.normalizeCoinName(coin, latestPrices);
|
||||
}
|
||||
return 'bitcoin-cash';
|
||||
}
|
||||
|
||||
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';
|
||||
if (window.CoinUtils) {
|
||||
return window.CoinUtils.normalizeCoinName(coin, latestPrices);
|
||||
}
|
||||
|
||||
return 'bitcoin-cash';
|
||||
}
|
||||
|
||||
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 offersResponse = await fetch(isSentOffers ? '/json/sentoffers' : '/json/offers');
|
||||
if (!offersResponse.ok) {
|
||||
throw new Error(`HTTP error! status: ${offersResponse.status}`);
|
||||
const fetchWithRetry = async (url, maxRetries = 3) => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (i === maxRetries - 1) throw error;
|
||||
|
||||
await new Promise(resolve => CleanupManager.setTimeout(resolve, 100 * Math.pow(2, i)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const 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();
|
||||
}
|
||||
|
||||
const filterForm = document.getElementById('filterForm');
|
||||
if (filterForm) {
|
||||
@@ -2649,20 +2713,10 @@ document.addEventListener('DOMContentLoaded', async function() {
|
||||
}
|
||||
});
|
||||
|
||||
const rowTimeInterval = setInterval(updateRowTimes, 30000);
|
||||
if (CleanupManager.registerResource) {
|
||||
const rowTimeInterval = CleanupManager.setInterval(updateRowTimes, 30000);
|
||||
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);
|
||||
}
|
||||
|
||||
} 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
332
basicswap/static/js/pages/settings-page.js
Normal file
332
basicswap/static/js/pages/settings-page.js
Normal 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);
|
||||
|
||||
})();
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
372
basicswap/static/js/pages/wallet-page.js
Normal file
372
basicswap/static/js/pages/wallet-page.js
Normal 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);
|
||||
|
||||
})();
|
||||
344
basicswap/static/js/pages/wallets-page.js
Normal file
344
basicswap/static/js/pages/wallets-page.js
Normal 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;
|
||||
|
||||
})();
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
52
basicswap/templates/macros.html
Normal file
52
basicswap/templates/macros.html
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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') {
|
||||
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) {
|
||||
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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,22 +538,8 @@ 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(
|
||||
@@ -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,
|
||||
|
||||
@@ -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", ""),
|
||||
}
|
||||
|
||||
@@ -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"], {})
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user