mirror of
https://github.com/basicswap/basicswap.git
synced 2025-12-29 00:41:39 +01:00
Refactor + Optimizations
This commit is contained in:
@@ -562,9 +562,50 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
|
|
||||||
random.seed(secrets.randbits(128))
|
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):
|
def finalise(self):
|
||||||
self.log.info("Finalising")
|
self.log.info("Finalising")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from basicswap.rpc_pool import close_all_pools
|
||||||
|
|
||||||
|
close_all_pools()
|
||||||
|
except Exception as e:
|
||||||
|
self.log.debug(f"Error closing RPC pools: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
|
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
|
||||||
|
|
||||||
@@ -1143,6 +1184,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
|
|
||||||
self.checkWalletSeed(c)
|
self.checkWalletSeed(c)
|
||||||
|
|
||||||
|
self._enable_rpc_pooling()
|
||||||
|
|
||||||
if "p2p_host" in self.settings:
|
if "p2p_host" in self.settings:
|
||||||
network_key = self.getNetworkKey(1)
|
network_key = self.getNetworkKey(1)
|
||||||
self._network = bsn.Network(
|
self._network = bsn.Network(
|
||||||
@@ -3867,7 +3910,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
|
|
||||||
# Set msg_buf.message_nets to let the remote node know what networks to respond on.
|
# 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
|
# 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()
|
msg_buf.message_nets = self.getMessageNetsString()
|
||||||
|
|
||||||
return msg_buf
|
return msg_buf
|
||||||
@@ -3917,7 +3960,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
|
|
||||||
# Set msg_buf.message_nets to let the remote node know what networks to respond on.
|
# 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
|
# 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()
|
msg_buf.message_nets = self.getMessageNetsString()
|
||||||
|
|
||||||
return msg_buf
|
return msg_buf
|
||||||
@@ -4004,7 +4047,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
|
|
||||||
# Set msg_buf.message_nets to let the remote node know what networks to respond on.
|
# 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
|
# 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()
|
msg_buf.message_nets = self.getMessageNetsString()
|
||||||
|
|
||||||
return msg_buf
|
return msg_buf
|
||||||
@@ -7908,7 +7951,17 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
|
|
||||||
self.add(xmr_offer, cursor)
|
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:
|
else:
|
||||||
existing_offer.setState(OfferStates.OFFER_RECEIVED)
|
existing_offer.setState(OfferStates.OFFER_RECEIVED)
|
||||||
existing_offer.pk_from = pk_from
|
existing_offer.pk_from = pk_from
|
||||||
@@ -12329,9 +12382,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
rate_source: str = "coingecko.com",
|
rate_source: str = "coingecko.com",
|
||||||
saved_ttl: int = 300,
|
saved_ttl: int = 300,
|
||||||
):
|
):
|
||||||
if self.debug:
|
coins_list_display = ", ".join([Coins(c).name for c in coins_list])
|
||||||
coins_list_display = ", ".join([Coins(c).name for c in coins_list])
|
self.log.debug(f"lookupFiatRates {coins_list_display}.")
|
||||||
self.log.debug(f"lookupFiatRates {coins_list_display}.")
|
|
||||||
ensure(len(coins_list) > 0, "Must specify coin/s")
|
ensure(len(coins_list) > 0, "Must specify coin/s")
|
||||||
ensure(saved_ttl >= 0, "Invalid saved time")
|
ensure(saved_ttl >= 0, "Invalid saved time")
|
||||||
|
|
||||||
@@ -12400,7 +12452,6 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
if api_key != "":
|
if api_key != "":
|
||||||
url += f"&api_key={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=10, headers=headers))
|
||||||
|
|
||||||
for k, v in js.items():
|
for k, v in js.items():
|
||||||
@@ -12414,31 +12465,20 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
default_chart_api_key,
|
default_chart_api_key,
|
||||||
escape=True,
|
escape=True,
|
||||||
)
|
)
|
||||||
if len(need_coins) == 1:
|
coin_ids: str = ""
|
||||||
|
for coin_id in coins_list:
|
||||||
|
if len(coin_ids) > 0:
|
||||||
|
coin_ids += ","
|
||||||
coin_ticker: str = chainparams[coin_id]["ticker"]
|
coin_ticker: str = chainparams[coin_id]["ticker"]
|
||||||
url: str = (
|
coin_ids += coin_ticker
|
||||||
f"https://min-api.cryptocompare.com/data/price?fsym={coin_ticker}&tsyms={ticker_to}"
|
exchange_name_map[coin_ticker] = coin_id
|
||||||
)
|
url: str = (
|
||||||
self.log.debug(f"lookupFiatRates: {url}")
|
f"https://min-api.cryptocompare.com/data/pricemulti?fsyms={coin_ids}&tsyms={ticker_to}"
|
||||||
js = json.loads(self.readURL(url, timeout=10, headers=headers))
|
)
|
||||||
return_rates[int(coin_id)] = js[ticker_to]
|
js = json.loads(self.readURL(url, timeout=10, headers=headers))
|
||||||
new_values[coin_id] = js[ticker_to]
|
for k, v in js.items():
|
||||||
else:
|
return_rates[int(exchange_name_map[k])] = v[ticker_to]
|
||||||
coin_ids: str = ""
|
new_values[exchange_name_map[k]] = v[ticker_to]
|
||||||
for coin_id in coins_list:
|
|
||||||
if len(coin_ids) > 0:
|
|
||||||
coin_ids += ","
|
|
||||||
coin_ticker: str = chainparams[coin_id]["ticker"]
|
|
||||||
coin_ids += coin_ticker
|
|
||||||
exchange_name_map[coin_ticker] = coin_id
|
|
||||||
url: str = (
|
|
||||||
f"https://min-api.cryptocompare.com/data/pricemulti?fsyms={coin_ids}&tsyms={ticker_to}"
|
|
||||||
)
|
|
||||||
self.log.debug(f"lookupFiatRates: {url}")
|
|
||||||
js = json.loads(self.readURL(url, timeout=10, headers=headers))
|
|
||||||
for k, v in js.items():
|
|
||||||
return_rates[int(exchange_name_map[k])] = v[ticker_to]
|
|
||||||
new_values[exchange_name_map[k]] = v[ticker_to]
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown rate source {rate_source}")
|
raise ValueError(f"Unknown rate source {rate_source}")
|
||||||
|
|
||||||
@@ -12484,6 +12524,240 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
finally:
|
finally:
|
||||||
self.closeDB(cursor, commit=False)
|
self.closeDB(cursor, commit=False)
|
||||||
|
|
||||||
|
def lookupVolume(
|
||||||
|
self,
|
||||||
|
coins_list,
|
||||||
|
rate_source: str = "coingecko.com",
|
||||||
|
saved_ttl: int = 300,
|
||||||
|
):
|
||||||
|
coins_list_display = ", ".join([Coins(c).name for c in coins_list])
|
||||||
|
self.log.debug(f"lookupVolume {coins_list_display}.")
|
||||||
|
ensure(len(coins_list) > 0, "Must specify coin/s")
|
||||||
|
ensure(saved_ttl >= 0, "Invalid saved time")
|
||||||
|
|
||||||
|
now: int = int(time.time())
|
||||||
|
oldest_time_valid: int = now - saved_ttl
|
||||||
|
return_data = {}
|
||||||
|
|
||||||
|
headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"}
|
||||||
|
|
||||||
|
cursor = self.openDB()
|
||||||
|
try:
|
||||||
|
parameters = {
|
||||||
|
"rate_source": rate_source,
|
||||||
|
"oldest_time_valid": oldest_time_valid,
|
||||||
|
}
|
||||||
|
coins_list_query = ""
|
||||||
|
for i, coin_id in enumerate(coins_list):
|
||||||
|
try:
|
||||||
|
_ = Coins(coin_id)
|
||||||
|
except Exception:
|
||||||
|
raise ValueError(f"Unknown coin type {coin_id}")
|
||||||
|
|
||||||
|
param_name = f"coin_{i}"
|
||||||
|
if i > 0:
|
||||||
|
coins_list_query += ","
|
||||||
|
coins_list_query += f":{param_name}"
|
||||||
|
parameters[param_name] = coin_id
|
||||||
|
|
||||||
|
query = f"SELECT coin_id, volume_24h, price_change_24h FROM coinvolume WHERE coin_id IN ({coins_list_query}) AND source = :rate_source AND last_updated >= :oldest_time_valid"
|
||||||
|
rows = cursor.execute(query, parameters)
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
volume_24h = None
|
||||||
|
price_change_24h = 0.0
|
||||||
|
try:
|
||||||
|
if row[1] is not None and row[1] != "None":
|
||||||
|
volume_24h = float(row[1])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if row[2] is not None and row[2] != "None":
|
||||||
|
price_change_24h = float(row[2])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
return_data[int(row[0])] = {
|
||||||
|
"volume_24h": volume_24h,
|
||||||
|
"price_change_24h": price_change_24h,
|
||||||
|
}
|
||||||
|
|
||||||
|
need_coins = []
|
||||||
|
new_values = {}
|
||||||
|
exchange_name_map = {}
|
||||||
|
for coin_id in coins_list:
|
||||||
|
if coin_id not in return_data:
|
||||||
|
need_coins.append(coin_id)
|
||||||
|
|
||||||
|
if len(need_coins) < 1:
|
||||||
|
return return_data
|
||||||
|
|
||||||
|
if rate_source == "coingecko.com":
|
||||||
|
coin_ids: str = ""
|
||||||
|
for coin_id in coins_list:
|
||||||
|
if len(coin_ids) > 0:
|
||||||
|
coin_ids += ","
|
||||||
|
exchange_name: str = self.getExchangeName(coin_id, rate_source)
|
||||||
|
coin_ids += exchange_name
|
||||||
|
exchange_name_map[exchange_name] = coin_id
|
||||||
|
|
||||||
|
api_key: str = get_api_key_setting(
|
||||||
|
self.settings,
|
||||||
|
"coingecko_api_key",
|
||||||
|
default_coingecko_api_key,
|
||||||
|
escape=True,
|
||||||
|
)
|
||||||
|
url: str = (
|
||||||
|
f"https://api.coingecko.com/api/v3/simple/price?ids={coin_ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true"
|
||||||
|
)
|
||||||
|
if api_key != "":
|
||||||
|
url += f"&api_key={api_key}"
|
||||||
|
|
||||||
|
js = json.loads(self.readURL(url, timeout=10, headers=headers))
|
||||||
|
|
||||||
|
for k, v in js.items():
|
||||||
|
coin_id = int(exchange_name_map[k])
|
||||||
|
volume_24h = v.get("usd_24h_vol")
|
||||||
|
price_change_24h = v.get("usd_24h_change")
|
||||||
|
|
||||||
|
# Convert to float if value exists, otherwise keep as None
|
||||||
|
volume_value = float(volume_24h) if volume_24h is not None else None
|
||||||
|
price_change_value = (
|
||||||
|
float(price_change_24h) if price_change_24h is not None else 0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
return_data[coin_id] = {
|
||||||
|
"volume_24h": volume_value,
|
||||||
|
"price_change_24h": price_change_value,
|
||||||
|
}
|
||||||
|
new_values[coin_id] = {
|
||||||
|
"volume_24h": volume_value,
|
||||||
|
"price_change_24h": price_change_value,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown rate source {rate_source}")
|
||||||
|
|
||||||
|
if len(new_values) < 1:
|
||||||
|
return return_data
|
||||||
|
|
||||||
|
for coin_id, data in new_values.items():
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT OR REPLACE INTO coinvolume (coin_id, volume_24h, price_change_24h, source, last_updated) VALUES (:coin_id, :volume_24h, :price_change_24h, :rate_source, :last_updated)",
|
||||||
|
{
|
||||||
|
"coin_id": coin_id,
|
||||||
|
"volume_24h": (
|
||||||
|
str(data["volume_24h"])
|
||||||
|
if data["volume_24h"] is not None
|
||||||
|
else "None"
|
||||||
|
),
|
||||||
|
"price_change_24h": str(data["price_change_24h"]),
|
||||||
|
"rate_source": rate_source,
|
||||||
|
"last_updated": now,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.commitDB()
|
||||||
|
return return_data
|
||||||
|
finally:
|
||||||
|
self.closeDB(cursor, commit=False)
|
||||||
|
|
||||||
|
def lookupHistoricalData(
|
||||||
|
self,
|
||||||
|
coins_list,
|
||||||
|
days: int = 1,
|
||||||
|
rate_source: str = "coingecko.com",
|
||||||
|
saved_ttl: int = 3600,
|
||||||
|
):
|
||||||
|
coins_list_display = ", ".join([Coins(c).name for c in coins_list])
|
||||||
|
self.log.debug(f"lookupHistoricalData {coins_list_display}, days={days}.")
|
||||||
|
ensure(len(coins_list) > 0, "Must specify coin/s")
|
||||||
|
ensure(saved_ttl >= 0, "Invalid saved time")
|
||||||
|
ensure(days > 0, "Days must be positive")
|
||||||
|
|
||||||
|
now: int = int(time.time())
|
||||||
|
oldest_time_valid: int = now - saved_ttl
|
||||||
|
return_data = {}
|
||||||
|
|
||||||
|
headers = {"User-Agent": "Mozilla/5.0", "Connection": "close"}
|
||||||
|
|
||||||
|
cursor = self.openDB()
|
||||||
|
try:
|
||||||
|
parameters = {
|
||||||
|
"rate_source": rate_source,
|
||||||
|
"oldest_time_valid": oldest_time_valid,
|
||||||
|
"days": days,
|
||||||
|
}
|
||||||
|
coins_list_query = ""
|
||||||
|
for i, coin_id in enumerate(coins_list):
|
||||||
|
try:
|
||||||
|
_ = Coins(coin_id)
|
||||||
|
except Exception:
|
||||||
|
raise ValueError(f"Unknown coin type {coin_id}")
|
||||||
|
|
||||||
|
param_name = f"coin_{i}"
|
||||||
|
if i > 0:
|
||||||
|
coins_list_query += ","
|
||||||
|
coins_list_query += f":{param_name}"
|
||||||
|
parameters[param_name] = coin_id
|
||||||
|
|
||||||
|
query = f"SELECT coin_id, price_data FROM coinhistory WHERE coin_id IN ({coins_list_query}) AND days = :days AND source = :rate_source AND last_updated >= :oldest_time_valid"
|
||||||
|
rows = cursor.execute(query, parameters)
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
return_data[int(row[0])] = json.loads(row[1])
|
||||||
|
|
||||||
|
need_coins = []
|
||||||
|
new_values = {}
|
||||||
|
for coin_id in coins_list:
|
||||||
|
if coin_id not in return_data:
|
||||||
|
need_coins.append(coin_id)
|
||||||
|
|
||||||
|
if len(need_coins) < 1:
|
||||||
|
return return_data
|
||||||
|
|
||||||
|
if rate_source == "coingecko.com":
|
||||||
|
api_key: str = get_api_key_setting(
|
||||||
|
self.settings,
|
||||||
|
"coingecko_api_key",
|
||||||
|
default_coingecko_api_key,
|
||||||
|
escape=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
for coin_id in need_coins:
|
||||||
|
exchange_name: str = self.getExchangeName(coin_id, rate_source)
|
||||||
|
url: str = (
|
||||||
|
f"https://api.coingecko.com/api/v3/coins/{exchange_name}/market_chart?vs_currency=usd&days={days}"
|
||||||
|
)
|
||||||
|
if api_key != "":
|
||||||
|
url += f"&api_key={api_key}"
|
||||||
|
|
||||||
|
js = json.loads(self.readURL(url, timeout=10, headers=headers))
|
||||||
|
|
||||||
|
if "prices" in js:
|
||||||
|
return_data[coin_id] = js["prices"]
|
||||||
|
new_values[coin_id] = js["prices"]
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown rate source {rate_source}")
|
||||||
|
|
||||||
|
if len(new_values) < 1:
|
||||||
|
return return_data
|
||||||
|
|
||||||
|
for coin_id, price_data in new_values.items():
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT OR REPLACE INTO coinhistory (coin_id, days, price_data, source, last_updated) VALUES (:coin_id, :days, :price_data, :rate_source, :last_updated)",
|
||||||
|
{
|
||||||
|
"coin_id": coin_id,
|
||||||
|
"days": days,
|
||||||
|
"price_data": json.dumps(price_data),
|
||||||
|
"rate_source": rate_source,
|
||||||
|
"last_updated": now,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.commitDB()
|
||||||
|
return return_data
|
||||||
|
finally:
|
||||||
|
self.closeDB(cursor, commit=False)
|
||||||
|
|
||||||
def lookupRates(self, coin_from, coin_to, output_array=False):
|
def lookupRates(self, coin_from, coin_to, output_array=False):
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
"lookupRates {}, {}.".format(
|
"lookupRates {}, {}.".format(
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import os
|
|||||||
CONFIG_FILENAME = "basicswap.json"
|
CONFIG_FILENAME = "basicswap.json"
|
||||||
BASICSWAP_DATADIR = os.getenv("BASICSWAP_DATADIR", os.path.join("~", ".basicswap"))
|
BASICSWAP_DATADIR = os.getenv("BASICSWAP_DATADIR", os.path.join("~", ".basicswap"))
|
||||||
DEFAULT_ALLOW_CORS = False
|
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"))
|
TEST_DATADIRS = os.path.expanduser(os.getenv("DATADIRS", "/tmp/basicswap"))
|
||||||
DEFAULT_TEST_BINDIR = os.path.expanduser(
|
DEFAULT_TEST_BINDIR = os.path.expanduser(
|
||||||
os.getenv("DEFAULT_TEST_BINDIR", os.path.join("~", ".basicswap", "bin"))
|
os.getenv("DEFAULT_TEST_BINDIR", os.path.join("~", ".basicswap", "bin"))
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from enum import IntEnum, auto
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
CURRENT_DB_VERSION = 31
|
CURRENT_DB_VERSION = 32
|
||||||
CURRENT_DB_DATA_VERSION = 7
|
CURRENT_DB_DATA_VERSION = 7
|
||||||
|
|
||||||
|
|
||||||
@@ -674,6 +674,28 @@ class CoinRates(Table):
|
|||||||
last_updated = Column("integer")
|
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):
|
class MessageNetworks(Table):
|
||||||
__tablename__ = "message_networks"
|
__tablename__ = "message_networks"
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import threading
|
|||||||
import http.client
|
import http.client
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from jinja2 import Environment, PackageLoader
|
from jinja2 import Environment, PackageLoader
|
||||||
from socket import error as SocketError
|
from socket import error as SocketError
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
@@ -169,15 +169,16 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
if not session_id:
|
if not session_id:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
session_data = self.server.active_sessions.get(session_id)
|
with self.server.session_lock:
|
||||||
if session_data and session_data["expires"] > datetime.now(timezone.utc):
|
session_data = self.server.active_sessions.get(session_id)
|
||||||
session_data["expires"] = datetime.now(timezone.utc) + timedelta(
|
if session_data and session_data["expires"] > datetime.now(timezone.utc):
|
||||||
minutes=SESSION_DURATION_MINUTES
|
session_data["expires"] = datetime.now(timezone.utc) + timedelta(
|
||||||
)
|
minutes=SESSION_DURATION_MINUTES
|
||||||
return True
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
if session_id in self.server.active_sessions:
|
if session_id in self.server.active_sessions:
|
||||||
del self.server.active_sessions[session_id]
|
del self.server.active_sessions[session_id]
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def log_error(self, format, *args):
|
def log_error(self, format, *args):
|
||||||
@@ -195,10 +196,11 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
return None
|
return None
|
||||||
form_data = parse.parse_qs(post_string)
|
form_data = parse.parse_qs(post_string)
|
||||||
form_id = form_data[b"formid"][0].decode("utf-8")
|
form_id = form_data[b"formid"][0].decode("utf-8")
|
||||||
if self.server.last_form_id.get(name, None) == form_id:
|
with self.server.form_id_lock:
|
||||||
messages.append("Prevented double submit for form {}.".format(form_id))
|
if self.server.last_form_id.get(name, None) == form_id:
|
||||||
return None
|
messages.append("Prevented double submit for form {}.".format(form_id))
|
||||||
self.server.last_form_id[name] = form_id
|
return None
|
||||||
|
self.server.last_form_id[name] = form_id
|
||||||
return form_data
|
return form_data
|
||||||
|
|
||||||
def render_template(
|
def render_template(
|
||||||
@@ -244,15 +246,17 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
if "messages" in args_dict:
|
if "messages" in args_dict:
|
||||||
messages_with_ids = []
|
messages_with_ids = []
|
||||||
for msg in args_dict["messages"]:
|
with self.server.msg_id_lock:
|
||||||
messages_with_ids.append((self.server.msg_id_counter, msg))
|
for msg in args_dict["messages"]:
|
||||||
self.server.msg_id_counter += 1
|
messages_with_ids.append((self.server.msg_id_counter, msg))
|
||||||
|
self.server.msg_id_counter += 1
|
||||||
args_dict["messages"] = messages_with_ids
|
args_dict["messages"] = messages_with_ids
|
||||||
if "err_messages" in args_dict:
|
if "err_messages" in args_dict:
|
||||||
err_messages_with_ids = []
|
err_messages_with_ids = []
|
||||||
for msg in args_dict["err_messages"]:
|
with self.server.msg_id_lock:
|
||||||
err_messages_with_ids.append((self.server.msg_id_counter, msg))
|
for msg in args_dict["err_messages"]:
|
||||||
self.server.msg_id_counter += 1
|
err_messages_with_ids.append((self.server.msg_id_counter, msg))
|
||||||
|
self.server.msg_id_counter += 1
|
||||||
args_dict["err_messages"] = err_messages_with_ids
|
args_dict["err_messages"] = err_messages_with_ids
|
||||||
|
|
||||||
if self.path:
|
if self.path:
|
||||||
@@ -266,15 +270,17 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
args_dict["current_page"] = "index"
|
args_dict["current_page"] = "index"
|
||||||
|
|
||||||
shutdown_token = os.urandom(8).hex()
|
shutdown_token = os.urandom(8).hex()
|
||||||
self.server.session_tokens["shutdown"] = shutdown_token
|
with self.server.session_lock:
|
||||||
|
self.server.session_tokens["shutdown"] = shutdown_token
|
||||||
args_dict["shutdown_token"] = shutdown_token
|
args_dict["shutdown_token"] = shutdown_token
|
||||||
|
|
||||||
encrypted, locked = swap_client.getLockedState()
|
encrypted, locked = swap_client.getLockedState()
|
||||||
args_dict["encrypted"] = encrypted
|
args_dict["encrypted"] = encrypted
|
||||||
args_dict["locked"] = locked
|
args_dict["locked"] = locked
|
||||||
|
|
||||||
if self.server.msg_id_counter >= 0x7FFFFFFF:
|
with self.server.msg_id_lock:
|
||||||
self.server.msg_id_counter = 0
|
if self.server.msg_id_counter >= 0x7FFFFFFF:
|
||||||
|
self.server.msg_id_counter = 0
|
||||||
|
|
||||||
args_dict["version"] = version
|
args_dict["version"] = version
|
||||||
|
|
||||||
@@ -364,7 +370,8 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
expires = datetime.now(timezone.utc) + timedelta(
|
expires = datetime.now(timezone.utc) + timedelta(
|
||||||
minutes=SESSION_DURATION_MINUTES
|
minutes=SESSION_DURATION_MINUTES
|
||||||
)
|
)
|
||||||
self.server.active_sessions[session_id] = {"expires": expires}
|
with self.server.session_lock:
|
||||||
|
self.server.active_sessions[session_id] = {"expires": expires}
|
||||||
cookie_header = self._set_session_cookie(session_id)
|
cookie_header = self._set_session_cookie(session_id)
|
||||||
|
|
||||||
if is_json_request:
|
if is_json_request:
|
||||||
@@ -628,13 +635,15 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
if len(url_split) > 2:
|
if len(url_split) > 2:
|
||||||
token = url_split[2]
|
token = url_split[2]
|
||||||
expect_token = self.server.session_tokens.get("shutdown", None)
|
with self.server.session_lock:
|
||||||
|
expect_token = self.server.session_tokens.get("shutdown", None)
|
||||||
if token != expect_token:
|
if token != expect_token:
|
||||||
return self.page_info("Unexpected token, still running.")
|
return self.page_info("Unexpected token, still running.")
|
||||||
|
|
||||||
session_id = self._get_session_cookie()
|
session_id = self._get_session_cookie()
|
||||||
if session_id and session_id in self.server.active_sessions:
|
with self.server.session_lock:
|
||||||
del self.server.active_sessions[session_id]
|
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()
|
clear_cookie_header = self._clear_session_cookie()
|
||||||
extra_headers.append(clear_cookie_header)
|
extra_headers.append(clear_cookie_header)
|
||||||
|
|
||||||
@@ -935,22 +944,28 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
return self.page_error(str(ex))
|
return self.page_error(str(ex))
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
response = self.handle_http(200, self.path)
|
|
||||||
try:
|
try:
|
||||||
self.wfile.write(response)
|
response = self.handle_http(200, self.path)
|
||||||
except SocketError as e:
|
try:
|
||||||
self.server.swap_client.log.debug(f"do_GET SocketError {e}")
|
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):
|
def do_POST(self):
|
||||||
content_length = int(self.headers.get("Content-Length", 0))
|
|
||||||
post_string = self.rfile.read(content_length)
|
|
||||||
|
|
||||||
is_json = True if "json" in self.headers.get("Content-Type", "") else False
|
|
||||||
response = self.handle_http(200, self.path, post_string, is_json)
|
|
||||||
try:
|
try:
|
||||||
self.wfile.write(response)
|
content_length = int(self.headers.get("Content-Length", 0))
|
||||||
except SocketError as e:
|
post_string = self.rfile.read(content_length)
|
||||||
self.server.swap_client.log.debug(f"do_POST SocketError {e}")
|
|
||||||
|
is_json = True if "json" in self.headers.get("Content-Type", "") else False
|
||||||
|
response = self.handle_http(200, self.path, post_string, is_json)
|
||||||
|
try:
|
||||||
|
self.wfile.write(response)
|
||||||
|
except SocketError as e:
|
||||||
|
self.server.swap_client.log.debug(f"do_POST SocketError {e}")
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
def do_HEAD(self):
|
def do_HEAD(self):
|
||||||
self.putHeaders(200, "text/html")
|
self.putHeaders(200, "text/html")
|
||||||
@@ -963,7 +978,9 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
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):
|
def __init__(self, host_name, port_no, allow_cors, swap_client):
|
||||||
threading.Thread.__init__(self)
|
threading.Thread.__init__(self)
|
||||||
|
|
||||||
@@ -979,8 +996,15 @@ class HttpThread(threading.Thread, HTTPServer):
|
|||||||
self.env = env
|
self.env = env
|
||||||
self.msg_id_counter = 0
|
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
|
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):
|
def stop(self):
|
||||||
self.stop_event.set()
|
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:
|
def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
|
||||||
swap_client = self.server.swap_client
|
swap_client = self.server.swap_client
|
||||||
post_data = {} if post_string == "" else getFormData(post_string, is_json)
|
post_data = {} if post_string == "" else getFormData(post_string, is_json)
|
||||||
@@ -1467,6 +1572,8 @@ endpoints = {
|
|||||||
"readurl": js_readurl,
|
"readurl": js_readurl,
|
||||||
"active": js_active,
|
"active": js_active,
|
||||||
"coinprices": js_coinprices,
|
"coinprices": js_coinprices,
|
||||||
|
"coinvolume": js_coinvolume,
|
||||||
|
"coinhistory": js_coinhistory,
|
||||||
"messageroutes": js_messageroutes,
|
"messageroutes": js_messageroutes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
import urllib
|
import urllib
|
||||||
from xmlrpc.client import (
|
from xmlrpc.client import (
|
||||||
@@ -15,6 +16,15 @@ from xmlrpc.client import (
|
|||||||
)
|
)
|
||||||
from .util import jsonDecimal
|
from .util import jsonDecimal
|
||||||
|
|
||||||
|
_use_rpc_pooling = False
|
||||||
|
_rpc_pool_settings = {}
|
||||||
|
|
||||||
|
|
||||||
|
def enable_rpc_pooling(settings):
|
||||||
|
global _use_rpc_pooling, _rpc_pool_settings
|
||||||
|
_use_rpc_pooling = settings.get("enabled", False)
|
||||||
|
_rpc_pool_settings = settings
|
||||||
|
|
||||||
|
|
||||||
class Jsonrpc:
|
class Jsonrpc:
|
||||||
# __getattr__ complicates extending ServerProxy
|
# __getattr__ complicates extending ServerProxy
|
||||||
@@ -91,6 +101,9 @@ class Jsonrpc:
|
|||||||
|
|
||||||
|
|
||||||
def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
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:
|
try:
|
||||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||||
if wallet is not None:
|
if wallet is not None:
|
||||||
@@ -110,6 +123,62 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
|||||||
return r["result"]
|
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"):
|
def openrpc(rpc_port, auth, wallet=None, host="127.0.0.1"):
|
||||||
try:
|
try:
|
||||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||||
|
|||||||
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=30, logger=None, max_idle_time=300
|
||||||
|
):
|
||||||
|
self.url = url
|
||||||
|
self.max_connections = max_connections
|
||||||
|
self.timeout = timeout
|
||||||
|
self.logger = logger
|
||||||
|
self.max_idle_time = max_idle_time
|
||||||
|
self._pool = queue.Queue(maxsize=max_connections)
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._created_connections = 0
|
||||||
|
self._connection_timestamps = {}
|
||||||
|
|
||||||
|
def get_connection(self):
|
||||||
|
try:
|
||||||
|
conn_data = self._pool.get(block=False)
|
||||||
|
conn, timestamp = (
|
||||||
|
conn_data if isinstance(conn_data, tuple) else (conn_data, time.time())
|
||||||
|
)
|
||||||
|
|
||||||
|
if time.time() - timestamp > self.max_idle_time:
|
||||||
|
if self.logger:
|
||||||
|
self.logger.debug(
|
||||||
|
f"RPC pool: discarding stale connection (idle for {time.time() - timestamp:.1f}s)"
|
||||||
|
)
|
||||||
|
conn.close()
|
||||||
|
with self._lock:
|
||||||
|
if self._created_connections > 0:
|
||||||
|
self._created_connections -= 1
|
||||||
|
return self._create_new_connection()
|
||||||
|
|
||||||
|
return conn
|
||||||
|
except queue.Empty:
|
||||||
|
return self._create_new_connection()
|
||||||
|
|
||||||
|
def _create_new_connection(self):
|
||||||
|
with self._lock:
|
||||||
|
if self._created_connections < self.max_connections:
|
||||||
|
self._created_connections += 1
|
||||||
|
return Jsonrpc(self.url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn_data = self._pool.get(block=True, timeout=self.timeout)
|
||||||
|
conn, timestamp = (
|
||||||
|
conn_data if isinstance(conn_data, tuple) else (conn_data, time.time())
|
||||||
|
)
|
||||||
|
|
||||||
|
if time.time() - timestamp > self.max_idle_time:
|
||||||
|
if self.logger:
|
||||||
|
self.logger.debug(
|
||||||
|
f"RPC pool: discarding stale connection (idle for {time.time() - timestamp:.1f}s)"
|
||||||
|
)
|
||||||
|
conn.close()
|
||||||
|
with self._lock:
|
||||||
|
if self._created_connections > 0:
|
||||||
|
self._created_connections -= 1
|
||||||
|
return Jsonrpc(self.url)
|
||||||
|
|
||||||
|
return conn
|
||||||
|
except queue.Empty:
|
||||||
|
if self.logger:
|
||||||
|
self.logger.warning(
|
||||||
|
f"RPC pool: timeout waiting for connection, creating temporary connection for {self.url}"
|
||||||
|
)
|
||||||
|
return Jsonrpc(self.url)
|
||||||
|
|
||||||
|
def return_connection(self, conn):
|
||||||
|
try:
|
||||||
|
self._pool.put((conn, time.time()), block=False)
|
||||||
|
except queue.Full:
|
||||||
|
conn.close()
|
||||||
|
with self._lock:
|
||||||
|
if self._created_connections > 0:
|
||||||
|
self._created_connections -= 1
|
||||||
|
|
||||||
|
def discard_connection(self, conn):
|
||||||
|
conn.close()
|
||||||
|
with self._lock:
|
||||||
|
if self._created_connections > 0:
|
||||||
|
self._created_connections -= 1
|
||||||
|
|
||||||
|
def close_all(self):
|
||||||
|
while not self._pool.empty():
|
||||||
|
try:
|
||||||
|
conn_data = self._pool.get(block=False)
|
||||||
|
conn = conn_data[0] if isinstance(conn_data, tuple) else conn_data
|
||||||
|
conn.close()
|
||||||
|
except queue.Empty:
|
||||||
|
break
|
||||||
|
with self._lock:
|
||||||
|
self._created_connections = 0
|
||||||
|
self._connection_timestamps.clear()
|
||||||
|
|
||||||
|
|
||||||
|
_rpc_pools = {}
|
||||||
|
_pool_lock = threading.Lock()
|
||||||
|
_pool_logger = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_pool_logger(logger):
|
||||||
|
global _pool_logger
|
||||||
|
_pool_logger = logger
|
||||||
|
|
||||||
|
|
||||||
|
def get_rpc_pool(url, max_connections=5):
|
||||||
|
with _pool_lock:
|
||||||
|
if url not in _rpc_pools:
|
||||||
|
_rpc_pools[url] = RPCConnectionPool(
|
||||||
|
url, max_connections, logger=_pool_logger
|
||||||
|
)
|
||||||
|
return _rpc_pools[url]
|
||||||
|
|
||||||
|
|
||||||
|
def close_all_pools():
|
||||||
|
with _pool_lock:
|
||||||
|
for pool in _rpc_pools.values():
|
||||||
|
pool.close_all()
|
||||||
|
_rpc_pools.clear()
|
||||||
@@ -4,34 +4,35 @@ const ApiManager = (function() {
|
|||||||
isInitialized: false
|
isInitialized: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = {
|
function getConfig() {
|
||||||
requestTimeout: 60000,
|
return window.config || window.ConfigManager || {
|
||||||
retryDelays: [5000, 15000, 30000],
|
requestTimeout: 60000,
|
||||||
rateLimits: {
|
retryDelays: [5000, 15000, 30000],
|
||||||
coingecko: {
|
rateLimits: {
|
||||||
requestsPerMinute: 50,
|
coingecko: { requestsPerMinute: 50, minInterval: 1200 },
|
||||||
minInterval: 1200
|
cryptocompare: { requestsPerMinute: 30, minInterval: 2000 }
|
||||||
},
|
|
||||||
cryptocompare: {
|
|
||||||
requestsPerMinute: 30,
|
|
||||||
minInterval: 2000
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
const rateLimiter = {
|
const rateLimiter = {
|
||||||
lastRequestTime: {},
|
lastRequestTime: {},
|
||||||
minRequestInterval: {
|
|
||||||
coingecko: 1200,
|
|
||||||
cryptocompare: 2000
|
|
||||||
},
|
|
||||||
requestQueue: {},
|
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) {
|
canMakeRequest: function(apiName) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const lastRequest = this.lastRequestTime[apiName] || 0;
|
const lastRequest = this.lastRequestTime[apiName] || 0;
|
||||||
return (now - lastRequest) >= this.minRequestInterval[apiName];
|
return (now - lastRequest) >= this.getMinInterval(apiName);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateLastRequestTime: function(apiName) {
|
updateLastRequestTime: function(apiName) {
|
||||||
@@ -41,7 +42,7 @@ const ApiManager = (function() {
|
|||||||
getWaitTime: function(apiName) {
|
getWaitTime: function(apiName) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const lastRequest = this.lastRequestTime[apiName] || 0;
|
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) {
|
queueRequest: async function(apiName, requestFn, retryCount = 0) {
|
||||||
@@ -55,29 +56,30 @@ const ApiManager = (function() {
|
|||||||
const executeRequest = async () => {
|
const executeRequest = async () => {
|
||||||
const waitTime = this.getWaitTime(apiName);
|
const waitTime = this.getWaitTime(apiName);
|
||||||
if (waitTime > 0) {
|
if (waitTime > 0) {
|
||||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
await new Promise(resolve => CleanupManager.setTimeout(resolve, waitTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.updateLastRequestTime(apiName);
|
this.updateLastRequestTime(apiName);
|
||||||
return await requestFn();
|
return await requestFn();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.message.includes('429') && retryCount < this.retryDelays.length) {
|
const retryDelays = this.getRetryDelays();
|
||||||
const delay = this.retryDelays[retryCount];
|
if (error.message.includes('429') && retryCount < retryDelays.length) {
|
||||||
|
const delay = retryDelays[retryCount];
|
||||||
console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`);
|
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);
|
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((error.message.includes('timeout') || error.name === 'NetworkError') &&
|
if ((error.message.includes('timeout') || error.name === 'NetworkError') &&
|
||||||
retryCount < this.retryDelays.length) {
|
retryCount < retryDelays.length) {
|
||||||
const delay = this.retryDelays[retryCount];
|
const delay = retryDelays[retryCount];
|
||||||
console.warn(`Request failed, retrying in ${delay/1000} seconds...`, {
|
console.warn(`Request failed, retrying in ${delay/1000} seconds...`, {
|
||||||
apiName,
|
apiName,
|
||||||
retryCount,
|
retryCount,
|
||||||
error: error.message
|
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);
|
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,19 +120,7 @@ const ApiManager = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.config) {
|
if (options.config) {
|
||||||
Object.assign(config, options.config);
|
console.log('[ApiManager] Config options provided, but using ConfigManager instead');
|
||||||
}
|
|
||||||
|
|
||||||
if (config.rateLimits) {
|
|
||||||
Object.keys(config.rateLimits).forEach(api => {
|
|
||||||
if (config.rateLimits[api].minInterval) {
|
|
||||||
rateLimiter.minRequestInterval[api] = config.rateLimits[api].minInterval;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.retryDelays) {
|
|
||||||
rateLimiter.retryDelays = [...config.retryDelays];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.CleanupManager) {
|
if (window.CleanupManager) {
|
||||||
@@ -143,6 +133,31 @@ const ApiManager = (function() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
makeRequest: async function(url, method = 'GET', headers = {}, body = null) {
|
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 {
|
try {
|
||||||
const options = {
|
const options = {
|
||||||
method: method,
|
method: method,
|
||||||
@@ -150,7 +165,7 @@ const ApiManager = (function() {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...headers
|
...headers
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(config.requestTimeout)
|
signal: AbortSignal.timeout(getConfig().requestTimeout || 60000)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (body) {
|
if (body) {
|
||||||
@@ -233,11 +248,8 @@ const ApiManager = (function() {
|
|||||||
.join(',') :
|
.join(',') :
|
||||||
'bitcoin,monero,particl,bitcoincash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin';
|
'bitcoin,monero,particl,bitcoincash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin';
|
||||||
|
|
||||||
//console.log('Fetching coin prices for:', coins);
|
|
||||||
const response = await this.fetchCoinPrices(coins);
|
const response = await this.fetchCoinPrices(coins);
|
||||||
|
|
||||||
//console.log('Full API response:', response);
|
|
||||||
|
|
||||||
if (!response || typeof response !== 'object') {
|
if (!response || typeof response !== 'object') {
|
||||||
throw new Error('Invalid response type');
|
throw new Error('Invalid response type');
|
||||||
}
|
}
|
||||||
@@ -260,80 +272,38 @@ const ApiManager = (function() {
|
|||||||
fetchVolumeData: async function() {
|
fetchVolumeData: async function() {
|
||||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||||
try {
|
try {
|
||||||
let coinList = (window.config && window.config.coins) ?
|
const coinSymbols = window.CoinManager
|
||||||
window.config.coins
|
? window.CoinManager.getAllCoins().map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
|
||||||
.filter(coin => coin.usesCoinGecko)
|
: (window.config.coins
|
||||||
.map(coin => {
|
? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
|
||||||
return window.config.getCoinBackendId ?
|
: ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']);
|
||||||
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';
|
|
||||||
|
|
||||||
if (!coinList.includes('zcoin') && coinList.includes('firo')) {
|
const response = await this.makeRequest('/json/coinvolume', 'POST', {}, {
|
||||||
coinList = coinList + ',zcoin';
|
coins: coinSymbols.join(','),
|
||||||
}
|
source: 'coingecko.com',
|
||||||
|
ttl: 300
|
||||||
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'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response || typeof response !== 'object') {
|
if (!response) {
|
||||||
throw new Error('Invalid response from CoinGecko API');
|
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 = {};
|
const volumeData = {};
|
||||||
|
|
||||||
Object.entries(response).forEach(([coinId, data]) => {
|
Object.entries(response.data).forEach(([coinSymbol, data]) => {
|
||||||
if (data && data.usd_24h_vol !== undefined) {
|
const coinKey = coinSymbol.toLowerCase();
|
||||||
volumeData[coinId] = {
|
volumeData[coinKey] = {
|
||||||
total_volume: data.usd_24h_vol || 0,
|
total_volume: (data.volume_24h !== undefined && data.volume_24h !== null) ? data.volume_24h : null,
|
||||||
price_change_percentage_24h: data.usd_24h_change || 0
|
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;
|
return volumeData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching volume data:", error);
|
console.error("Error fetching volume data:", error);
|
||||||
@@ -342,104 +312,45 @@ const ApiManager = (function() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchCryptoCompareData: function(coin) {
|
|
||||||
return this.rateLimiter.queueRequest('cryptocompare', async () => {
|
|
||||||
try {
|
|
||||||
const apiKey = window.config?.apiKeys?.cryptoCompare || '';
|
|
||||||
const url = `https://min-api.cryptocompare.com/data/pricemultifull?fsyms=${coin}&tsyms=USD,BTC&api_key=${apiKey}`;
|
|
||||||
const headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0',
|
|
||||||
'Accept': 'application/json'
|
|
||||||
};
|
|
||||||
|
|
||||||
return await this.makePostRequest(url, headers);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`CryptoCompare request failed for ${coin}:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchHistoricalData: async function(coinSymbols, resolution = 'day') {
|
fetchHistoricalData: async function(coinSymbols, resolution = 'day') {
|
||||||
if (!Array.isArray(coinSymbols)) {
|
if (!Array.isArray(coinSymbols)) {
|
||||||
coinSymbols = [coinSymbols];
|
coinSymbols = [coinSymbols];
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = {};
|
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||||
const fetchPromises = coinSymbols.map(async coin => {
|
try {
|
||||||
let useCoinGecko = false;
|
let days;
|
||||||
let coingeckoId = null;
|
if (resolution === 'day') {
|
||||||
|
days = 1;
|
||||||
if (window.CoinManager) {
|
} else if (resolution === 'year') {
|
||||||
const coinConfig = window.CoinManager.getCoinByAnyIdentifier(coin);
|
days = 365;
|
||||||
if (coinConfig) {
|
} else {
|
||||||
useCoinGecko = !coinConfig.usesCryptoCompare || coin === 'PART';
|
days = 180;
|
||||||
coingeckoId = coinConfig.coingeckoId;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const coinGeckoCoins = {
|
const response = await this.makeRequest('/json/coinhistory', 'POST', {}, {
|
||||||
'WOW': 'wownero',
|
coins: coinSymbols.join(','),
|
||||||
'PART': 'particl',
|
days: days,
|
||||||
'BTC': 'bitcoin'
|
source: 'coingecko.com',
|
||||||
};
|
ttl: 3600
|
||||||
if (coinGeckoCoins[coin]) {
|
});
|
||||||
useCoinGecko = true;
|
|
||||||
coingeckoId = coinGeckoCoins[coin];
|
if (!response) {
|
||||||
|
console.error('No response from backend');
|
||||||
|
throw new Error('Invalid response from backend');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (useCoinGecko && coingeckoId) {
|
if (!response.data) {
|
||||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
console.error('Response missing data field:', response);
|
||||||
let days;
|
throw new Error('Invalid response from backend');
|
||||||
if (resolution === 'day') {
|
}
|
||||||
days = 1;
|
|
||||||
} else if (resolution === 'year') {
|
|
||||||
days = 365;
|
|
||||||
} else {
|
|
||||||
days = 180;
|
|
||||||
}
|
|
||||||
const url = `https://api.coingecko.com/api/v3/coins/${coingeckoId}/market_chart?vs_currency=usd&days=${days}`;
|
|
||||||
try {
|
|
||||||
const response = await this.makePostRequest(url);
|
|
||||||
if (response && response.prices) {
|
|
||||||
results[coin] = response.prices;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error fetching CoinGecko data for ${coin}:`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return this.rateLimiter.queueRequest('cryptocompare', async () => {
|
|
||||||
try {
|
|
||||||
const apiKey = window.config?.apiKeys?.cryptoCompare || '';
|
|
||||||
let url;
|
|
||||||
|
|
||||||
if (resolution === 'day') {
|
return response.data;
|
||||||
url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coin}&tsym=USD&limit=24&api_key=${apiKey}`;
|
} catch (error) {
|
||||||
} else if (resolution === 'year') {
|
console.error('Error fetching historical data:', error);
|
||||||
url = `https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coin}&tsym=USD&limit=365&api_key=${apiKey}`;
|
throw error;
|
||||||
} 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() {
|
dispose: function() {
|
||||||
@@ -453,17 +364,6 @@ const ApiManager = (function() {
|
|||||||
return publicAPI;
|
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.Api = ApiManager;
|
||||||
window.ApiManager = ApiManager;
|
window.ApiManager = ApiManager;
|
||||||
|
|
||||||
@@ -474,5 +374,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//console.log('ApiManager initialized with methods:', Object.keys(ApiManager));
|
|
||||||
console.log('ApiManager initialized');
|
console.log('ApiManager initialized');
|
||||||
|
|||||||
@@ -15,7 +15,21 @@ const BalanceUpdatesManager = (function() {
|
|||||||
initialized: false
|
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', {
|
return fetch('/json/walletbalances', {
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
@@ -43,27 +57,41 @@ const BalanceUpdatesManager = (function() {
|
|||||||
|
|
||||||
function clearTimeoutByKey(key) {
|
function clearTimeoutByKey(key) {
|
||||||
if (state.timeouts.has(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);
|
state.timeouts.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTimeoutByKey(key, callback, delay) {
|
function setTimeoutByKey(key, callback, delay) {
|
||||||
clearTimeoutByKey(key);
|
clearTimeoutByKey(key);
|
||||||
const timeoutId = setTimeout(callback, delay);
|
const timeoutId = window.CleanupManager
|
||||||
|
? window.CleanupManager.setTimeout(callback, delay)
|
||||||
|
: setTimeout(callback, delay);
|
||||||
state.timeouts.set(key, timeoutId);
|
state.timeouts.set(key, timeoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearIntervalByKey(key) {
|
function clearIntervalByKey(key) {
|
||||||
if (state.intervals.has(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);
|
state.intervals.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setIntervalByKey(key, callback, interval) {
|
function setIntervalByKey(key, callback, interval) {
|
||||||
clearIntervalByKey(key);
|
clearIntervalByKey(key);
|
||||||
const intervalId = setInterval(callback, interval);
|
const intervalId = window.CleanupManager
|
||||||
|
? window.CleanupManager.setInterval(callback, interval)
|
||||||
|
: setInterval(callback, interval);
|
||||||
state.intervals.set(key, intervalId);
|
state.intervals.set(key, intervalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
const CacheManager = (function() {
|
const CacheManager = (function() {
|
||||||
const defaults = window.config?.cacheConfig?.storage || {
|
function getDefaults() {
|
||||||
maxSizeBytes: 10 * 1024 * 1024,
|
if (window.config?.cacheConfig?.storage) {
|
||||||
maxItems: 200,
|
return window.config.cacheConfig.storage;
|
||||||
defaultTTL: 5 * 60 * 1000
|
}
|
||||||
};
|
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';
|
const PRICES_CACHE_KEY = 'crypto_prices_unified';
|
||||||
|
|
||||||
@@ -45,8 +55,12 @@ const CacheManager = (function() {
|
|||||||
|
|
||||||
const cacheAPI = {
|
const cacheAPI = {
|
||||||
getTTL: function(resourceType) {
|
getTTL: function(resourceType) {
|
||||||
const ttlConfig = window.config?.cacheConfig?.ttlSettings || {};
|
const ttlConfig = window.config?.cacheConfig?.ttlSettings ||
|
||||||
return ttlConfig[resourceType] || window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL;
|
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) {
|
set: function(key, value, resourceTypeOrCustomTtl = null) {
|
||||||
@@ -73,13 +87,18 @@ const CacheManager = (function() {
|
|||||||
expiresAt: Date.now() + ttl
|
expiresAt: Date.now() + ttl
|
||||||
};
|
};
|
||||||
|
|
||||||
let serializedItem;
|
const serializedItem = window.ErrorHandler
|
||||||
try {
|
? window.ErrorHandler.safeExecute(() => JSON.stringify(item), 'CacheManager.set.serialize', null)
|
||||||
serializedItem = JSON.stringify(item);
|
: (() => {
|
||||||
} catch (e) {
|
try {
|
||||||
console.error('Failed to serialize cache item:', e);
|
return JSON.stringify(item);
|
||||||
return false;
|
} catch (e) {
|
||||||
}
|
console.error('Failed to serialize cache item:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!serializedItem) return false;
|
||||||
|
|
||||||
const itemSize = new Blob([serializedItem]).size;
|
const itemSize = new Blob([serializedItem]).size;
|
||||||
if (itemSize > defaults.maxSizeBytes) {
|
if (itemSize > defaults.maxSizeBytes) {
|
||||||
@@ -118,7 +137,7 @@ const CacheManager = (function() {
|
|||||||
const keysToDelete = Array.from(memoryCache.keys())
|
const keysToDelete = Array.from(memoryCache.keys())
|
||||||
.filter(k => isCacheKey(k))
|
.filter(k => isCacheKey(k))
|
||||||
.sort((a, b) => memoryCache.get(a).timestamp - memoryCache.get(b).timestamp)
|
.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));
|
keysToDelete.forEach(k => memoryCache.delete(k));
|
||||||
}
|
}
|
||||||
@@ -285,7 +304,7 @@ const CacheManager = (function() {
|
|||||||
const keysToDelete = Array.from(memoryCache.keys())
|
const keysToDelete = Array.from(memoryCache.keys())
|
||||||
.filter(key => isCacheKey(key))
|
.filter(key => isCacheKey(key))
|
||||||
.sort((a, b) => memoryCache.get(a).timestamp - memoryCache.get(b).timestamp)
|
.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));
|
keysToDelete.forEach(key => memoryCache.delete(key));
|
||||||
}
|
}
|
||||||
@@ -328,7 +347,6 @@ const CacheManager = (function() {
|
|||||||
.filter(key => isCacheKey(key))
|
.filter(key => isCacheKey(key))
|
||||||
.forEach(key => memoryCache.delete(key));
|
.forEach(key => memoryCache.delete(key));
|
||||||
|
|
||||||
//console.log("Cache cleared successfully");
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -531,6 +549,4 @@ const CacheManager = (function() {
|
|||||||
|
|
||||||
window.CacheManager = CacheManager;
|
window.CacheManager = CacheManager;
|
||||||
|
|
||||||
|
|
||||||
//console.log('CacheManager initialized with methods:', Object.keys(CacheManager));
|
|
||||||
console.log('CacheManager initialized');
|
console.log('CacheManager initialized');
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ const CleanupManager = (function() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
setupMemoryOptimization: function(options = {}) {
|
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 maxCacheSize = options.maxCacheSize || 100;
|
||||||
const maxDataSize = options.maxDataSize || 1000;
|
const maxDataSize = options.maxDataSize || 1000;
|
||||||
|
|
||||||
|
|||||||
@@ -178,19 +178,7 @@ const CoinManager = (function() {
|
|||||||
function getCoinByAnyIdentifier(identifier) {
|
function getCoinByAnyIdentifier(identifier) {
|
||||||
if (!identifier) return null;
|
if (!identifier) return null;
|
||||||
const normalizedId = identifier.toString().toLowerCase().trim();
|
const normalizedId = identifier.toString().toLowerCase().trim();
|
||||||
const coin = coinAliasesMap[normalizedId];
|
return coinAliasesMap[normalizedId] || null;
|
||||||
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 {
|
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');
|
||||||
@@ -53,20 +53,11 @@ const ConfigManager = (function() {
|
|||||||
},
|
},
|
||||||
retryDelays: [5000, 15000, 30000],
|
retryDelays: [5000, 15000, 30000],
|
||||||
get coins() {
|
get coins() {
|
||||||
return window.CoinManager ? window.CoinManager.getAllCoins() : [
|
if (window.CoinManager) {
|
||||||
{ symbol: 'BTC', name: 'bitcoin', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 },
|
return window.CoinManager.getAllCoins();
|
||||||
{ symbol: 'XMR', name: 'monero', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
}
|
||||||
{ symbol: 'PART', name: 'particl', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
console.warn('[ConfigManager] CoinManager not available, returning empty array');
|
||||||
{ symbol: 'BCH', name: 'bitcoincash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
return [];
|
||||||
{ symbol: 'PIVX', name: 'pivx', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
|
||||||
{ symbol: 'FIRO', name: 'firo', displayName: 'Firo', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
|
||||||
{ symbol: 'DASH', name: 'dash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
|
||||||
{ symbol: 'LTC', name: 'litecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
|
||||||
{ symbol: 'DOGE', name: 'dogecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
|
||||||
{ symbol: 'DCR', name: 'decred', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
|
||||||
{ symbol: 'NMC', name: 'namecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
|
|
||||||
{ symbol: 'WOW', name: 'wownero', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 }
|
|
||||||
];
|
|
||||||
},
|
},
|
||||||
chartConfig: {
|
chartConfig: {
|
||||||
colors: {
|
colors: {
|
||||||
@@ -122,55 +113,20 @@ const ConfigManager = (function() {
|
|||||||
if (window.CoinManager) {
|
if (window.CoinManager) {
|
||||||
return window.CoinManager.getPriceKey(coinName);
|
return window.CoinManager.getPriceKey(coinName);
|
||||||
}
|
}
|
||||||
const nameMap = {
|
if (window.CoinUtils) {
|
||||||
'bitcoin-cash': 'bitcoincash',
|
return window.CoinUtils.normalizeCoinName(coinName);
|
||||||
'bitcoin cash': 'bitcoincash',
|
}
|
||||||
'firo': 'firo',
|
return typeof coinName === 'string' ? coinName.toLowerCase() : '';
|
||||||
'zcoin': 'firo',
|
|
||||||
'bitcoincash': 'bitcoin-cash'
|
|
||||||
};
|
|
||||||
const lowerCoinName = typeof coinName === 'string' ? coinName.toLowerCase() : '';
|
|
||||||
return nameMap[lowerCoinName] || lowerCoinName;
|
|
||||||
},
|
},
|
||||||
coinMatches: function(offerCoin, filterCoin) {
|
coinMatches: function(offerCoin, filterCoin) {
|
||||||
if (!offerCoin || !filterCoin) return false;
|
if (!offerCoin || !filterCoin) return false;
|
||||||
if (window.CoinManager) {
|
if (window.CoinManager) {
|
||||||
return window.CoinManager.coinMatches(offerCoin, filterCoin);
|
return window.CoinManager.coinMatches(offerCoin, filterCoin);
|
||||||
}
|
}
|
||||||
offerCoin = offerCoin.toLowerCase();
|
if (window.CoinUtils) {
|
||||||
filterCoin = filterCoin.toLowerCase();
|
return window.CoinUtils.isSameCoin(offerCoin, filterCoin);
|
||||||
if (offerCoin === filterCoin) return true;
|
|
||||||
if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
|
|
||||||
(filterCoin === 'firo' || filterCoin === 'zcoin')) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
|
return offerCoin.toLowerCase() === filterCoin.toLowerCase();
|
||||||
(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;
|
|
||||||
},
|
},
|
||||||
update: function(path, value) {
|
update: function(path, value) {
|
||||||
const parts = path.split('.');
|
const parts = path.split('.');
|
||||||
@@ -229,7 +185,7 @@ const ConfigManager = (function() {
|
|||||||
let timeoutId;
|
let timeoutId;
|
||||||
return function(...args) {
|
return function(...args) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
timeoutId = setTimeout(() => func(...args), delay);
|
timeoutId = CleanupManager.setTimeout(() => func(...args), delay);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
formatTimeLeft: function(timestamp) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedData = this.getCachedIdentity(address);
|
const cached = state.cache.get(address);
|
||||||
if (cachedData) {
|
const now = Date.now();
|
||||||
log(`Cache hit for ${address}`);
|
|
||||||
return cachedData;
|
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)) {
|
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) {
|
getCachedIdentity: function(address) {
|
||||||
const cached = state.cache.get(address);
|
const cached = state.cache.get(address);
|
||||||
if (cached && (Date.now() - cached.timestamp) < state.config.cacheTimeout) {
|
if (cached && (Date.now() - cached.timestamp) < state.config.cacheTimeout) {
|
||||||
@@ -155,15 +183,23 @@ const IdentityManager = (function() {
|
|||||||
|
|
||||||
async function fetchWithRetry(address, attempt = 1) {
|
async function fetchWithRetry(address, attempt = 1) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/json/identities/${address}`, {
|
let data;
|
||||||
signal: AbortSignal.timeout(5000)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (window.ApiManager) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
data = await window.ApiManager.makeRequest(`/json/identities/${address}`, 'GET');
|
||||||
|
} else {
|
||||||
|
const response = await fetch(`/json/identities/${address}`, {
|
||||||
|
signal: AbortSignal.timeout(5000)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
data = await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (attempt >= state.config.maxRetries) {
|
if (attempt >= state.config.maxRetries) {
|
||||||
console.error(`[IdentityManager] Error:`, error.message);
|
console.error(`[IdentityManager] Error:`, error.message);
|
||||||
@@ -171,7 +207,10 @@ const IdentityManager = (function() {
|
|||||||
return null;
|
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);
|
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');
|
console.log('IdentityManager initialized');
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ const NetworkManager = (function() {
|
|||||||
|
|
||||||
log(`Scheduling reconnection attempt in ${delay/1000} seconds`);
|
log(`Scheduling reconnection attempt in ${delay/1000} seconds`);
|
||||||
|
|
||||||
state.reconnectTimer = setTimeout(() => {
|
state.reconnectTimer = CleanupManager.setTimeout(() => {
|
||||||
state.reconnectTimer = null;
|
state.reconnectTimer = null;
|
||||||
this.attemptReconnect();
|
this.attemptReconnect();
|
||||||
}, delay);
|
}, 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, {
|
return fetch(config.connectionTestEndpoint, {
|
||||||
method: 'HEAD',
|
method: 'HEAD',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -275,6 +288,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//console.log('NetworkManager initialized with methods:', Object.keys(NetworkManager));
|
|
||||||
console.log('NetworkManager initialized');
|
console.log('NetworkManager initialized');
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
const NotificationManager = (function() {
|
const NotificationManager = (function() {
|
||||||
|
|
||||||
|
|
||||||
const defaultConfig = {
|
const defaultConfig = {
|
||||||
showNewOffers: false,
|
showNewOffers: false,
|
||||||
showNewBids: true,
|
showNewBids: true,
|
||||||
@@ -12,7 +11,6 @@ const NotificationManager = (function() {
|
|||||||
notificationDuration: 20000
|
notificationDuration: 20000
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
const saved = localStorage.getItem('notification_settings');
|
const saved = localStorage.getItem('notification_settings');
|
||||||
if (saved) {
|
if (saved) {
|
||||||
@@ -25,7 +23,6 @@ const NotificationManager = (function() {
|
|||||||
return { ...defaultConfig };
|
return { ...defaultConfig };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function saveConfig(newConfig) {
|
function saveConfig(newConfig) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('notification_settings', JSON.stringify(newConfig));
|
localStorage.setItem('notification_settings', JSON.stringify(newConfig));
|
||||||
@@ -269,7 +266,6 @@ function ensureToastContainer() {
|
|||||||
return colors[type] || colors['success'];
|
return colors[type] || colors['success'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: Remove later and use global.
|
|
||||||
function getCoinDisplayName(coinId) {
|
function getCoinDisplayName(coinId) {
|
||||||
const coinMap = {
|
const coinMap = {
|
||||||
1: 'PART',
|
1: 'PART',
|
||||||
@@ -292,25 +288,24 @@ function ensureToastContainer() {
|
|||||||
return coinMap[coinId] || `Coin ${coinId}`;
|
return coinMap[coinId] || `Coin ${coinId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: Remove later.
|
|
||||||
function formatCoinAmount(amount, coinId) {
|
function formatCoinAmount(amount, coinId) {
|
||||||
const divisors = {
|
const divisors = {
|
||||||
1: 100000000, // PART - 8 decimals
|
1: 100000000, // PART - 8 decimals
|
||||||
2: 100000000, // BTC - 8 decimals
|
2: 100000000, // BTC - 8 decimals
|
||||||
3: 100000000, // LTC - 8 decimals
|
3: 100000000, // LTC - 8 decimals
|
||||||
4: 100000000, // DCR - 8 decimals
|
4: 100000000, // DCR - 8 decimals
|
||||||
5: 100000000, // NMC - 8 decimals
|
5: 100000000, // NMC - 8 decimals
|
||||||
6: 1000000000000, // XMR - 12 decimals
|
6: 1000000000000, // XMR - 12 decimals
|
||||||
7: 100000000, // PART (Blind) - 8 decimals
|
7: 100000000, // PART (Blind) - 8 decimals
|
||||||
8: 100000000, // PART (Anon) - 8 decimals
|
8: 100000000, // PART (Anon) - 8 decimals
|
||||||
9: 100000000000, // WOW - 11 decimals
|
9: 100000000000, // WOW - 11 decimals
|
||||||
11: 100000000, // PIVX - 8 decimals
|
11: 100000000, // PIVX - 8 decimals
|
||||||
12: 100000000, // DASH - 8 decimals
|
12: 100000000, // DASH - 8 decimals
|
||||||
13: 100000000, // FIRO - 8 decimals
|
13: 100000000, // FIRO - 8 decimals
|
||||||
14: 100000000, // NAV - 8 decimals
|
14: 100000000, // NAV - 8 decimals
|
||||||
15: 100000000, // LTC (MWEB) - 8 decimals
|
15: 100000000, // LTC (MWEB) - 8 decimals
|
||||||
17: 100000000, // BCH - 8 decimals
|
17: 100000000, // BCH - 8 decimals
|
||||||
18: 100000000 // DOGE - 8 decimals
|
18: 100000000 // DOGE - 8 decimals
|
||||||
};
|
};
|
||||||
|
|
||||||
const divisor = divisors[coinId] || 100000000;
|
const divisor = divisors[coinId] || 100000000;
|
||||||
@@ -358,7 +353,7 @@ function ensureToastContainer() {
|
|||||||
testToasts: function() {
|
testToasts: function() {
|
||||||
if (!this.createToast) return;
|
if (!this.createToast) return;
|
||||||
|
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
this.createToast(
|
this.createToast(
|
||||||
'+0.05000000 PART',
|
'+0.05000000 PART',
|
||||||
'balance_change',
|
'balance_change',
|
||||||
@@ -366,7 +361,7 @@ function ensureToastContainer() {
|
|||||||
);
|
);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
this.createToast(
|
this.createToast(
|
||||||
'+0.00123456 XMR',
|
'+0.00123456 XMR',
|
||||||
'balance_change',
|
'balance_change',
|
||||||
@@ -374,7 +369,7 @@ function ensureToastContainer() {
|
|||||||
);
|
);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
this.createToast(
|
this.createToast(
|
||||||
'-29.86277595 PART',
|
'-29.86277595 PART',
|
||||||
'balance_change',
|
'balance_change',
|
||||||
@@ -382,7 +377,7 @@ function ensureToastContainer() {
|
|||||||
);
|
);
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
this.createToast(
|
this.createToast(
|
||||||
'-0.05000000 PART (Anon)',
|
'-0.05000000 PART (Anon)',
|
||||||
'balance_change',
|
'balance_change',
|
||||||
@@ -390,7 +385,7 @@ function ensureToastContainer() {
|
|||||||
);
|
);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
this.createToast(
|
this.createToast(
|
||||||
'+1.23456789 PART (Anon)',
|
'+1.23456789 PART (Anon)',
|
||||||
'balance_change',
|
'balance_change',
|
||||||
@@ -398,33 +393,37 @@ function ensureToastContainer() {
|
|||||||
);
|
);
|
||||||
}, 2500);
|
}, 2500);
|
||||||
|
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
|
const btcIcon = getCoinIcon('BTC');
|
||||||
|
const xmrIcon = getCoinIcon('XMR');
|
||||||
this.createToast(
|
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',
|
'new_offer',
|
||||||
{
|
{
|
||||||
offerId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66',
|
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,
|
coinFrom: 2,
|
||||||
coinTo: 6
|
coinTo: 6
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
|
const btcIcon = getCoinIcon('BTC');
|
||||||
|
const xmrIcon = getCoinIcon('XMR');
|
||||||
this.createToast(
|
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',
|
'new_bid',
|
||||||
{
|
{
|
||||||
bidId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66',
|
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,
|
coinFrom: 2,
|
||||||
coinTo: 6
|
coinTo: 6
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}, 3500);
|
}, 3500);
|
||||||
|
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
this.createToast(
|
this.createToast(
|
||||||
'Swap completed successfully',
|
'Swap completed successfully',
|
||||||
'swap_completed',
|
'swap_completed',
|
||||||
@@ -435,25 +434,68 @@ function ensureToastContainer() {
|
|||||||
);
|
);
|
||||||
}, 4000);
|
}, 4000);
|
||||||
|
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(async () => {
|
||||||
this.createToast(
|
try {
|
||||||
'Update Available: v0.15.0',
|
const response = await fetch('/json/checkupdates', {
|
||||||
'update_available',
|
method: 'POST',
|
||||||
{
|
headers: { 'Content-Type': 'application/json' }
|
||||||
subtitle: 'Current: v0.14.6 • Click to view release',
|
});
|
||||||
releaseUrl: 'https://github.com/basicswap/basicswap/releases/tag/v0.15.0',
|
const data = await response.json();
|
||||||
releaseNotes: 'New version v0.15.0 is available. Click to view details on GitHub.'
|
|
||||||
|
if (data.error) {
|
||||||
|
console.warn('Test notification - API returned error, using fallback:', data.error);
|
||||||
|
this.createToast(
|
||||||
|
'Update Available: v0.15.0',
|
||||||
|
'update_available',
|
||||||
|
{
|
||||||
|
subtitle: 'Current: v0.14.6 • Click to view release',
|
||||||
|
releaseUrl: 'https://github.com/basicswap/basicswap/releases/tag/v0.15.0',
|
||||||
|
releaseNotes: 'New version v0.15.0 is available. Click to view details on GitHub.'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
const currentVer = (data.current_version && String(data.current_version) !== 'null' && String(data.current_version) !== 'None')
|
||||||
|
? String(data.current_version)
|
||||||
|
: '0.14.6';
|
||||||
|
const latestVer = (data.latest_version && String(data.latest_version) !== 'null' && String(data.latest_version) !== 'None')
|
||||||
|
? String(data.latest_version)
|
||||||
|
: currentVer;
|
||||||
|
|
||||||
|
this.createToast(
|
||||||
|
`Update Available: v${latestVer}`,
|
||||||
|
'update_available',
|
||||||
|
{
|
||||||
|
subtitle: `Current: v${currentVer} • Click to view release`,
|
||||||
|
releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${latestVer}`,
|
||||||
|
releaseNotes: `New version v${latestVer} is available. Click to view details on GitHub.`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Test notification - API error:', error);
|
||||||
|
this.createToast(
|
||||||
|
'Update Available: v0.15.0',
|
||||||
|
'update_available',
|
||||||
|
{
|
||||||
|
subtitle: 'Current: v0.14.6 • Click to view release',
|
||||||
|
releaseUrl: 'https://github.com/basicswap/basicswap/releases/tag/v0.15.0',
|
||||||
|
releaseNotes: 'New version v0.15.0 is available. Click to view details on GitHub.'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}, 4500);
|
}, 4500);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
initializeBalanceTracking: function() {
|
initializeBalanceTracking: async function() {
|
||||||
this.checkAndResetStaleBalanceTracking();
|
this.checkAndResetStaleBalanceTracking();
|
||||||
|
|
||||||
fetch('/json/walletbalances')
|
const fetchBalances = window.ApiManager
|
||||||
.then(response => response.json())
|
? window.ApiManager.makeRequest('/json/walletbalances', 'GET')
|
||||||
|
: fetch('/json/walletbalances').then(response => response.json());
|
||||||
|
|
||||||
|
fetchBalances
|
||||||
.then(balanceData => {
|
.then(balanceData => {
|
||||||
if (Array.isArray(balanceData)) {
|
if (Array.isArray(balanceData)) {
|
||||||
balanceData.forEach(coin => {
|
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'">`;
|
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 clickAction = '';
|
||||||
let cursorStyle = 'cursor-default';
|
let cursorStyle = 'cursor-default';
|
||||||
|
|
||||||
@@ -585,10 +626,10 @@ function ensureToastContainer() {
|
|||||||
messages.appendChild(message);
|
messages.appendChild(message);
|
||||||
|
|
||||||
if (!isPersistent) {
|
if (!isPersistent) {
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
if (message.parentNode) {
|
if (message.parentNode) {
|
||||||
message.classList.add('toast-slide-out');
|
message.classList.add('toast-slide-out');
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
if (message.parentNode) {
|
if (message.parentNode) {
|
||||||
message.parentNode.removeChild(message);
|
message.parentNode.removeChild(message);
|
||||||
}
|
}
|
||||||
@@ -613,12 +654,12 @@ function ensureToastContainer() {
|
|||||||
const amountTo = formatCoinAmount(data.amount_to, data.coin_to);
|
const amountTo = formatCoinAmount(data.amount_to, data.coin_to);
|
||||||
const coinFromIcon = getCoinIcon(coinFromName);
|
const coinFromIcon = getCoinIcon(coinFromName);
|
||||||
const coinToIcon = getCoinIcon(coinToName);
|
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}`;
|
toastTitle = `New Network Offer`;
|
||||||
toastOptions.subtitle = `New offer • Rate: 1 ${coinFromName} = ${(data.amount_to / data.amount_from).toFixed(8)} ${coinToName}`;
|
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.coinFrom = data.coin_from;
|
||||||
toastOptions.coinTo = data.coin_to;
|
toastOptions.coinTo = data.coin_to;
|
||||||
} else {
|
} else {
|
||||||
toastTitle = `New network offer`;
|
toastTitle = `New Network Offer`;
|
||||||
toastOptions.subtitle = 'Click to view offer';
|
toastOptions.subtitle = 'Click to view offer';
|
||||||
}
|
}
|
||||||
toastType = 'new_offer';
|
toastType = 'new_offer';
|
||||||
@@ -633,12 +674,12 @@ function ensureToastContainer() {
|
|||||||
const bidAmountTo = formatCoinAmount(data.bid_amount_to, data.coin_to);
|
const bidAmountTo = formatCoinAmount(data.bid_amount_to, data.coin_to);
|
||||||
const coinFromIcon = getCoinIcon(coinFromName);
|
const coinFromIcon = getCoinIcon(coinFromName);
|
||||||
const coinToIcon = getCoinIcon(coinToName);
|
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}`;
|
toastTitle = `New Bid Received`;
|
||||||
toastOptions.subtitle = `New bid • Rate: 1 ${coinFromName} = ${(data.bid_amount_to / data.bid_amount).toFixed(8)} ${coinToName}`;
|
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.coinFrom = data.coin_from;
|
||||||
toastOptions.coinTo = data.coin_to;
|
toastOptions.coinTo = data.coin_to;
|
||||||
} else {
|
} else {
|
||||||
toastTitle = `New bid received`;
|
toastTitle = `New Bid Received`;
|
||||||
toastOptions.subtitle = 'Click to view bid';
|
toastOptions.subtitle = 'Click to view bid';
|
||||||
}
|
}
|
||||||
toastOptions.bidId = data.bid_id;
|
toastOptions.bidId = data.bid_id;
|
||||||
@@ -696,14 +737,17 @@ function ensureToastContainer() {
|
|||||||
this.balanceTimeouts = {};
|
this.balanceTimeouts = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.balanceTimeouts[balanceKey] = setTimeout(() => {
|
this.balanceTimeouts[balanceKey] = CleanupManager.setTimeout(() => {
|
||||||
this.fetchAndShowBalanceChange(data.coin);
|
this.fetchAndShowBalanceChange(data.coin);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchAndShowBalanceChange: function(coinSymbol) {
|
fetchAndShowBalanceChange: function(coinSymbol) {
|
||||||
fetch('/json/walletbalances')
|
const fetchBalances = window.ApiManager
|
||||||
.then(response => response.json())
|
? window.ApiManager.makeRequest('/json/walletbalances', 'GET')
|
||||||
|
: fetch('/json/walletbalances').then(response => response.json());
|
||||||
|
|
||||||
|
fetchBalances
|
||||||
.then(balanceData => {
|
.then(balanceData => {
|
||||||
if (Array.isArray(balanceData)) {
|
if (Array.isArray(balanceData)) {
|
||||||
|
|
||||||
@@ -748,13 +792,10 @@ function ensureToastContainer() {
|
|||||||
const pendingIncrease = currentPending - prevPending;
|
const pendingIncrease = currentPending - prevPending;
|
||||||
const pendingDecrease = prevPending - currentPending;
|
const pendingDecrease = prevPending - currentPending;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const totalChange = Math.abs(balanceIncrease) + Math.abs(pendingIncrease);
|
const totalChange = Math.abs(balanceIncrease) + Math.abs(pendingIncrease);
|
||||||
const maxReasonableChange = Math.max(currentBalance, prevBalance) * 0.5;
|
const maxReasonableChange = Math.max(currentBalance, prevBalance) * 0.5;
|
||||||
|
|
||||||
if (totalChange > maxReasonableChange && totalChange > 1.0) {
|
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(storageKey, currentBalance.toString());
|
||||||
localStorage.setItem(pendingStorageKey, currentPending.toString());
|
localStorage.setItem(pendingStorageKey, currentPending.toString());
|
||||||
return;
|
return;
|
||||||
@@ -782,7 +823,6 @@ function ensureToastContainer() {
|
|||||||
|
|
||||||
const isPendingToConfirmed = pendingDecrease > 0.00000001 && balanceIncrease > 0.00000001;
|
const isPendingToConfirmed = pendingDecrease > 0.00000001 && balanceIncrease > 0.00000001;
|
||||||
|
|
||||||
|
|
||||||
const displaySymbol = originalCoinSymbol;
|
const displaySymbol = originalCoinSymbol;
|
||||||
let variantInfo = '';
|
let variantInfo = '';
|
||||||
|
|
||||||
@@ -871,8 +911,6 @@ function ensureToastContainer() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
updateConfig: function(newConfig) {
|
updateConfig: function(newConfig) {
|
||||||
Object.assign(config, newConfig);
|
Object.assign(config, newConfig);
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const PriceManager = (function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => this.getPrices(), 1500);
|
CleanupManager.setTimeout(() => this.getPrices(), 1500);
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
console.log('PriceManager initialized');
|
console.log('PriceManager initialized');
|
||||||
return this;
|
return this;
|
||||||
@@ -60,7 +60,6 @@ const PriceManager = (function() {
|
|||||||
return fetchPromise;
|
return fetchPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
lastFetchTime = Date.now();
|
lastFetchTime = Date.now();
|
||||||
fetchPromise = this.fetchPrices()
|
fetchPromise = this.fetchPrices()
|
||||||
.then(prices => {
|
.then(prices => {
|
||||||
@@ -90,8 +89,6 @@ const PriceManager = (function() {
|
|||||||
? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
|
? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
|
||||||
: ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']);
|
: ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']);
|
||||||
|
|
||||||
//console.log('PriceManager: lookupFiatRates ' + coinSymbols.join(', '));
|
|
||||||
|
|
||||||
if (!coinSymbols.length) {
|
if (!coinSymbols.length) {
|
||||||
throw new Error('No valid coins configured');
|
throw new Error('No valid coins configured');
|
||||||
}
|
}
|
||||||
@@ -133,15 +130,15 @@ const PriceManager = (function() {
|
|||||||
const coin = window.CoinManager.getCoinByAnyIdentifier(coinId);
|
const coin = window.CoinManager.getCoinByAnyIdentifier(coinId);
|
||||||
if (coin) {
|
if (coin) {
|
||||||
normalizedCoinId = window.CoinManager.getPriceKey(coin.name);
|
normalizedCoinId = window.CoinManager.getPriceKey(coin.name);
|
||||||
|
} else if (window.CoinUtils) {
|
||||||
|
normalizedCoinId = window.CoinUtils.normalizeCoinName(coinId);
|
||||||
} else {
|
} else {
|
||||||
normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
|
normalizedCoinId = coinId.toLowerCase();
|
||||||
}
|
}
|
||||||
|
} else if (window.CoinUtils) {
|
||||||
|
normalizedCoinId = window.CoinUtils.normalizeCoinName(coinId);
|
||||||
} else {
|
} else {
|
||||||
normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
|
normalizedCoinId = coinId.toLowerCase();
|
||||||
}
|
|
||||||
|
|
||||||
if (coinId.toLowerCase() === 'zcoin') {
|
|
||||||
normalizedCoinId = 'firo';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
processedData[normalizedCoinId] = {
|
processedData[normalizedCoinId] = {
|
||||||
@@ -230,5 +227,3 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
window.priceManagerInitialized = true;
|
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') {
|
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
|
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
|
||||||
debugLog(`Re-initialized tooltips for ${tooltipId}`);
|
debugLog(`Re-initialized tooltips for ${tooltipId}`);
|
||||||
}, 50);
|
}, 50);
|
||||||
@@ -205,8 +205,16 @@ const SummaryManager = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fetchSummaryDataWithTimeout() {
|
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 controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), config.requestTimeout);
|
const timeoutId = CleanupManager.setTimeout(() => controller.abort(), config.requestTimeout);
|
||||||
|
|
||||||
return fetch(config.summaryEndpoint, {
|
return fetch(config.summaryEndpoint, {
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
@@ -217,7 +225,11 @@ const SummaryManager = (function() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
clearTimeout(timeoutId);
|
if (window.CleanupManager) {
|
||||||
|
window.CleanupManager.clearTimeout(timeoutId);
|
||||||
|
} else {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
@@ -226,7 +238,11 @@ const SummaryManager = (function() {
|
|||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
clearTimeout(timeoutId);
|
if (window.CleanupManager) {
|
||||||
|
window.CleanupManager.clearTimeout(timeoutId);
|
||||||
|
} else {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -275,7 +291,7 @@ const SummaryManager = (function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
webSocket.onclose = () => {
|
webSocket.onclose = () => {
|
||||||
setTimeout(setupWebSocket, 5000);
|
CleanupManager.setTimeout(setupWebSocket, 5000);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +319,7 @@ const SummaryManager = (function() {
|
|||||||
.then(() => {})
|
.then(() => {})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
refreshTimer = setInterval(() => {
|
refreshTimer = CleanupManager.setInterval(() => {
|
||||||
publicAPI.fetchSummaryData()
|
publicAPI.fetchSummaryData()
|
||||||
.then(() => {})
|
.then(() => {})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
@@ -386,7 +402,7 @@ const SummaryManager = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
resolve(this.fetchSummaryData());
|
resolve(this.fetchSummaryData());
|
||||||
}, config.retryDelay);
|
}, config.retryDelay);
|
||||||
});
|
});
|
||||||
@@ -446,5 +462,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//console.log('SummaryManager initialized with methods:', Object.keys(SummaryManager));
|
|
||||||
console.log('SummaryManager initialized');
|
console.log('SummaryManager initialized');
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ const TooltipManager = (function() {
|
|||||||
this.debug = false;
|
this.debug = false;
|
||||||
this.tooltipData = new WeakMap();
|
this.tooltipData = new WeakMap();
|
||||||
this.resources = {};
|
this.resources = {};
|
||||||
|
this.creationQueue = [];
|
||||||
|
this.batchSize = 5;
|
||||||
|
this.isProcessingQueue = false;
|
||||||
|
|
||||||
if (window.CleanupManager) {
|
if (window.CleanupManager) {
|
||||||
CleanupManager.registerResource(
|
CleanupManager.registerResource(
|
||||||
@@ -48,40 +51,69 @@ const TooltipManager = (function() {
|
|||||||
this.performPeriodicCleanup(true);
|
this.performPeriodicCleanup(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const createTooltip = () => {
|
this.creationQueue.push({ element, content, options });
|
||||||
if (!document.body.contains(element)) return;
|
|
||||||
|
|
||||||
const rect = element.getBoundingClientRect();
|
if (!this.isProcessingQueue) {
|
||||||
if (rect.width > 0 && rect.height > 0) {
|
this.processCreationQueue();
|
||||||
this.createTooltipInstance(element, content, options);
|
}
|
||||||
} else {
|
|
||||||
let retryCount = 0;
|
|
||||||
const maxRetries = 3;
|
|
||||||
|
|
||||||
const retryCreate = () => {
|
|
||||||
const newRect = element.getBoundingClientRect();
|
|
||||||
if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) {
|
|
||||||
if (newRect.width > 0 && newRect.height > 0) {
|
|
||||||
this.createTooltipInstance(element, content, options);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
retryCount++;
|
|
||||||
CleanupManager.setTimeout(() => {
|
|
||||||
CleanupManager.requestAnimationFrame(retryCreate);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
CleanupManager.setTimeout(() => {
|
|
||||||
CleanupManager.requestAnimationFrame(retryCreate);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
CleanupManager.requestAnimationFrame(createTooltip);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
processCreationQueue() {
|
||||||
|
if (this.creationQueue.length === 0) {
|
||||||
|
this.isProcessingQueue = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isProcessingQueue = true;
|
||||||
|
const batch = this.creationQueue.splice(0, this.batchSize);
|
||||||
|
|
||||||
|
CleanupManager.requestAnimationFrame(() => {
|
||||||
|
batch.forEach(({ element, content, options }) => {
|
||||||
|
this.createTooltipSync(element, content, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.creationQueue.length > 0) {
|
||||||
|
CleanupManager.setTimeout(() => {
|
||||||
|
this.processCreationQueue();
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
this.isProcessingQueue = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createTooltipSync(element, content, options) {
|
||||||
|
if (!document.body.contains(element)) return;
|
||||||
|
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
if (rect.width > 0 && rect.height > 0) {
|
||||||
|
this.createTooltipInstance(element, content, options);
|
||||||
|
} else {
|
||||||
|
let retryCount = 0;
|
||||||
|
const maxRetries = 3;
|
||||||
|
|
||||||
|
const retryCreate = () => {
|
||||||
|
const newRect = element.getBoundingClientRect();
|
||||||
|
if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) {
|
||||||
|
if (newRect.width > 0 && newRect.height > 0) {
|
||||||
|
this.createTooltipInstance(element, content, options);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
retryCount++;
|
||||||
|
CleanupManager.setTimeout(() => {
|
||||||
|
CleanupManager.requestAnimationFrame(retryCreate);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CleanupManager.setTimeout(() => {
|
||||||
|
CleanupManager.requestAnimationFrame(retryCreate);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createTooltipInstance(element, content, options = {}) {
|
createTooltipInstance(element, content, options = {}) {
|
||||||
if (!element || !document.body.contains(element)) {
|
if (!element || !document.body.contains(element)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -191,6 +223,9 @@ const TooltipManager = (function() {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (window.ErrorHandler) {
|
||||||
|
return window.ErrorHandler.handleError(error, 'TooltipManager.createTooltipInstance', null);
|
||||||
|
}
|
||||||
console.error('Error creating tooltip:', error);
|
console.error('Error creating tooltip:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -199,7 +234,7 @@ const TooltipManager = (function() {
|
|||||||
destroy(element) {
|
destroy(element) {
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
try {
|
const destroyFn = () => {
|
||||||
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
|
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
|
||||||
if (!tooltipId) return;
|
if (!tooltipId) return;
|
||||||
|
|
||||||
@@ -224,8 +259,16 @@ const TooltipManager = (function() {
|
|||||||
|
|
||||||
this.tooltipData.delete(element);
|
this.tooltipData.delete(element);
|
||||||
tooltipInstanceMap.delete(element);
|
tooltipInstanceMap.delete(element);
|
||||||
} catch (error) {
|
};
|
||||||
console.error('Error destroying tooltip:', error);
|
|
||||||
|
if (window.ErrorHandler) {
|
||||||
|
window.ErrorHandler.safeExecute(destroyFn, 'TooltipManager.destroy', null);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
destroyFn();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error destroying tooltip:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,10 +781,40 @@ const TooltipManager = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initializeLazyTooltips(selector = '[data-tooltip-target]') {
|
||||||
|
|
||||||
|
const initializedTooltips = new Set();
|
||||||
|
|
||||||
|
const initializeTooltip = (element) => {
|
||||||
|
const targetId = element.getAttribute('data-tooltip-target');
|
||||||
|
if (!targetId || initializedTooltips.has(targetId)) return;
|
||||||
|
|
||||||
|
const tooltipContent = document.getElementById(targetId);
|
||||||
|
if (tooltipContent) {
|
||||||
|
this.create(element, tooltipContent.innerHTML, {
|
||||||
|
placement: element.getAttribute('data-tooltip-placement') || 'top'
|
||||||
|
});
|
||||||
|
initializedTooltips.add(targetId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mouseover', (e) => {
|
||||||
|
const target = e.target.closest(selector);
|
||||||
|
if (target) {
|
||||||
|
initializeTooltip(target);
|
||||||
|
}
|
||||||
|
}, { passive: true, capture: true });
|
||||||
|
|
||||||
|
this.log('Lazy tooltip initialization enabled');
|
||||||
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this.log('Disposing TooltipManager');
|
this.log('Disposing TooltipManager');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.creationQueue = [];
|
||||||
|
this.isProcessingQueue = false;
|
||||||
|
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
|
|
||||||
Object.values(this.resources).forEach(resourceId => {
|
Object.values(this.resources).forEach(resourceId => {
|
||||||
@@ -830,6 +903,11 @@ const TooltipManager = (function() {
|
|||||||
return manager.initializeTooltips(...args);
|
return manager.initializeTooltips(...args);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
initializeLazyTooltips: function(...args) {
|
||||||
|
const manager = this.getInstance();
|
||||||
|
return manager.initializeLazyTooltips(...args);
|
||||||
|
},
|
||||||
|
|
||||||
setDebugMode: function(enabled) {
|
setDebugMode: function(enabled) {
|
||||||
const manager = this.getInstance();
|
const manager = this.getInstance();
|
||||||
return manager.setDebugMode(enabled);
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
||||||
@@ -95,22 +95,32 @@ const WalletManager = (function() {
|
|||||||
|
|
||||||
const fetchCoinsString = coinsToFetch.join(',');
|
const fetchCoinsString = coinsToFetch.join(',');
|
||||||
|
|
||||||
const mainResponse = await fetch("/json/coinprices", {
|
let mainData;
|
||||||
method: "POST",
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
if (window.ApiManager) {
|
||||||
body: JSON.stringify({
|
mainData = await window.ApiManager.makeRequest("/json/coinprices", "POST", {}, {
|
||||||
coins: fetchCoinsString,
|
coins: fetchCoinsString,
|
||||||
source: currentSource,
|
source: currentSource,
|
||||||
ttl: config.defaultTTL
|
ttl: config.defaultTTL
|
||||||
})
|
});
|
||||||
});
|
} else {
|
||||||
|
const mainResponse = await fetch("/json/coinprices", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({
|
||||||
|
coins: fetchCoinsString,
|
||||||
|
source: currentSource,
|
||||||
|
ttl: config.defaultTTL
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
if (!mainResponse.ok) {
|
if (!mainResponse.ok) {
|
||||||
throw new Error(`HTTP error: ${mainResponse.status}`);
|
throw new Error(`HTTP error: ${mainResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
mainData = await mainResponse.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainData = await mainResponse.json();
|
|
||||||
|
|
||||||
if (mainData && mainData.rates) {
|
if (mainData && mainData.rates) {
|
||||||
document.querySelectorAll('.coinname-value').forEach(el => {
|
document.querySelectorAll('.coinname-value').forEach(el => {
|
||||||
const coinName = el.getAttribute('data-coinname');
|
const coinName = el.getAttribute('data-coinname');
|
||||||
@@ -154,7 +164,7 @@ const WalletManager = (function() {
|
|||||||
|
|
||||||
if (attempt < config.maxRetries - 1) {
|
if (attempt < config.maxRetries - 1) {
|
||||||
const delay = Math.min(config.baseDelay * Math.pow(2, attempt), 10000);
|
const delay = Math.min(config.baseDelay * Math.pow(2, attempt), 10000);
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -428,7 +438,7 @@ const WalletManager = (function() {
|
|||||||
clearTimeout(state.toggleDebounceTimer);
|
clearTimeout(state.toggleDebounceTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.toggleDebounceTimer = window.setTimeout(async () => {
|
state.toggleDebounceTimer = CleanupManager.setTimeout(async () => {
|
||||||
state.toggleInProgress = false;
|
state.toggleInProgress = false;
|
||||||
if (newVisibility) {
|
if (newVisibility) {
|
||||||
await updatePrices(true);
|
await updatePrices(true);
|
||||||
@@ -539,7 +549,6 @@ const WalletManager = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public API
|
|
||||||
const publicAPI = {
|
const publicAPI = {
|
||||||
initialize: async function(options) {
|
initialize: async function(options) {
|
||||||
if (state.initialized) {
|
if (state.initialized) {
|
||||||
@@ -579,7 +588,7 @@ const WalletManager = (function() {
|
|||||||
clearInterval(state.priceUpdateInterval);
|
clearInterval(state.priceUpdateInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.priceUpdateInterval = setInterval(() => {
|
state.priceUpdateInterval = CleanupManager.setInterval(() => {
|
||||||
if (localStorage.getItem('balancesVisible') === 'true' && !state.toggleInProgress) {
|
if (localStorage.getItem('balancesVisible') === 'true' && !state.toggleInProgress) {
|
||||||
updatePrices(false);
|
updatePrices(false);
|
||||||
}
|
}
|
||||||
@@ -661,5 +670,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//console.log('WalletManager initialized with methods:', Object.keys(WalletManager));
|
|
||||||
console.log('WalletManager initialized');
|
console.log('WalletManager initialized');
|
||||||
|
|||||||
@@ -32,26 +32,24 @@ const WebSocketManager = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function determineWebSocketPort() {
|
function determineWebSocketPort() {
|
||||||
let wsPort;
|
if (window.ConfigManager && window.ConfigManager.wsPort) {
|
||||||
|
return window.ConfigManager.wsPort.toString();
|
||||||
|
}
|
||||||
|
|
||||||
if (window.config && window.config.wsPort) {
|
if (window.config && window.config.wsPort) {
|
||||||
wsPort = window.config.wsPort;
|
return window.config.wsPort.toString();
|
||||||
return wsPort;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.ws_port) {
|
if (window.ws_port) {
|
||||||
wsPort = window.ws_port.toString();
|
return window.ws_port.toString();
|
||||||
return wsPort;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof getWebSocketConfig === 'function') {
|
if (typeof getWebSocketConfig === 'function') {
|
||||||
const wsConfig = getWebSocketConfig();
|
const wsConfig = getWebSocketConfig();
|
||||||
wsPort = (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
|
return (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
|
||||||
return wsPort;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wsPort = '11700';
|
return '11700';
|
||||||
return wsPort;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicAPI = {
|
const publicAPI = {
|
||||||
@@ -77,7 +75,11 @@ const WebSocketManager = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.reconnectTimeout) {
|
if (state.reconnectTimeout) {
|
||||||
clearTimeout(state.reconnectTimeout);
|
if (window.CleanupManager) {
|
||||||
|
window.CleanupManager.clearTimeout(state.reconnectTimeout);
|
||||||
|
} else {
|
||||||
|
clearTimeout(state.reconnectTimeout);
|
||||||
|
}
|
||||||
state.reconnectTimeout = null;
|
state.reconnectTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,13 +98,17 @@ const WebSocketManager = (function() {
|
|||||||
ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
|
||||||
setupEventHandlers();
|
setupEventHandlers();
|
||||||
|
|
||||||
state.connectTimeout = setTimeout(() => {
|
const timeoutFn = () => {
|
||||||
if (state.isConnecting) {
|
if (state.isConnecting) {
|
||||||
log('Connection timeout, cleaning up');
|
log('Connection timeout, cleaning up');
|
||||||
cleanup();
|
cleanup();
|
||||||
handleReconnect();
|
handleReconnect();
|
||||||
}
|
}
|
||||||
}, 5000);
|
};
|
||||||
|
|
||||||
|
state.connectTimeout = window.CleanupManager
|
||||||
|
? window.CleanupManager.setTimeout(timeoutFn, 5000)
|
||||||
|
: setTimeout(timeoutFn, 5000);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -159,18 +165,25 @@ const WebSocketManager = (function() {
|
|||||||
cleanup: function() {
|
cleanup: function() {
|
||||||
log('Cleaning up WebSocket resources');
|
log('Cleaning up WebSocket resources');
|
||||||
|
|
||||||
clearTimeout(state.connectTimeout);
|
if (window.CleanupManager) {
|
||||||
|
window.CleanupManager.clearTimeout(state.connectTimeout);
|
||||||
|
} else {
|
||||||
|
clearTimeout(state.connectTimeout);
|
||||||
|
}
|
||||||
stopHealthCheck();
|
stopHealthCheck();
|
||||||
|
|
||||||
if (state.reconnectTimeout) {
|
if (state.reconnectTimeout) {
|
||||||
clearTimeout(state.reconnectTimeout);
|
if (window.CleanupManager) {
|
||||||
|
window.CleanupManager.clearTimeout(state.reconnectTimeout);
|
||||||
|
} else {
|
||||||
|
clearTimeout(state.reconnectTimeout);
|
||||||
|
}
|
||||||
state.reconnectTimeout = null;
|
state.reconnectTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.isConnecting = false;
|
state.isConnecting = false;
|
||||||
state.messageHandlers = {};
|
state.messageHandlers = {};
|
||||||
|
|
||||||
|
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.onopen = null;
|
ws.onopen = null;
|
||||||
ws.onmessage = null;
|
ws.onmessage = null;
|
||||||
@@ -228,7 +241,11 @@ const WebSocketManager = (function() {
|
|||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
state.isConnecting = false;
|
state.isConnecting = false;
|
||||||
config.reconnectAttempts = 0;
|
config.reconnectAttempts = 0;
|
||||||
clearTimeout(state.connectTimeout);
|
if (window.CleanupManager) {
|
||||||
|
window.CleanupManager.clearTimeout(state.connectTimeout);
|
||||||
|
} else {
|
||||||
|
clearTimeout(state.connectTimeout);
|
||||||
|
}
|
||||||
state.lastHealthCheck = Date.now();
|
state.lastHealthCheck = Date.now();
|
||||||
window.ws = ws;
|
window.ws = ws;
|
||||||
|
|
||||||
@@ -311,24 +328,37 @@ const WebSocketManager = (function() {
|
|||||||
state.isPageHidden = false;
|
state.isPageHidden = false;
|
||||||
state.isIntentionallyClosed = false;
|
state.isIntentionallyClosed = false;
|
||||||
|
|
||||||
setTimeout(() => {
|
const resumeFn = () => {
|
||||||
if (!publicAPI.isConnected()) {
|
if (!publicAPI.isConnected()) {
|
||||||
publicAPI.connect();
|
publicAPI.connect();
|
||||||
}
|
}
|
||||||
startHealthCheck();
|
startHealthCheck();
|
||||||
}, 0);
|
};
|
||||||
|
|
||||||
|
if (window.CleanupManager) {
|
||||||
|
window.CleanupManager.setTimeout(resumeFn, 0);
|
||||||
|
} else {
|
||||||
|
setTimeout(resumeFn, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startHealthCheck() {
|
function startHealthCheck() {
|
||||||
stopHealthCheck();
|
stopHealthCheck();
|
||||||
state.healthCheckInterval = setInterval(() => {
|
const healthCheckFn = () => {
|
||||||
performHealthCheck();
|
performHealthCheck();
|
||||||
}, 30000);
|
};
|
||||||
|
state.healthCheckInterval = window.CleanupManager
|
||||||
|
? window.CleanupManager.setInterval(healthCheckFn, 30000)
|
||||||
|
: setInterval(healthCheckFn, 30000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopHealthCheck() {
|
function stopHealthCheck() {
|
||||||
if (state.healthCheckInterval) {
|
if (state.healthCheckInterval) {
|
||||||
clearInterval(state.healthCheckInterval);
|
if (window.CleanupManager) {
|
||||||
|
window.CleanupManager.clearInterval(state.healthCheckInterval);
|
||||||
|
} else {
|
||||||
|
clearInterval(state.healthCheckInterval);
|
||||||
|
}
|
||||||
state.healthCheckInterval = null;
|
state.healthCheckInterval = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,7 +386,11 @@ const WebSocketManager = (function() {
|
|||||||
function handleReconnect() {
|
function handleReconnect() {
|
||||||
|
|
||||||
if (state.reconnectTimeout) {
|
if (state.reconnectTimeout) {
|
||||||
clearTimeout(state.reconnectTimeout);
|
if (window.CleanupManager) {
|
||||||
|
window.CleanupManager.clearTimeout(state.reconnectTimeout);
|
||||||
|
} else {
|
||||||
|
clearTimeout(state.reconnectTimeout);
|
||||||
|
}
|
||||||
state.reconnectTimeout = null;
|
state.reconnectTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,23 +403,31 @@ const WebSocketManager = (function() {
|
|||||||
|
|
||||||
log(`Scheduling reconnect in ${delay}ms (attempt ${config.reconnectAttempts})`);
|
log(`Scheduling reconnect in ${delay}ms (attempt ${config.reconnectAttempts})`);
|
||||||
|
|
||||||
state.reconnectTimeout = setTimeout(() => {
|
const reconnectFn = () => {
|
||||||
state.reconnectTimeout = null;
|
state.reconnectTimeout = null;
|
||||||
if (!state.isIntentionallyClosed) {
|
if (!state.isIntentionallyClosed) {
|
||||||
publicAPI.connect();
|
publicAPI.connect();
|
||||||
}
|
}
|
||||||
}, delay);
|
};
|
||||||
|
|
||||||
|
state.reconnectTimeout = window.CleanupManager
|
||||||
|
? window.CleanupManager.setTimeout(reconnectFn, delay)
|
||||||
|
: setTimeout(reconnectFn, delay);
|
||||||
} else {
|
} else {
|
||||||
log('Max reconnect attempts reached');
|
log('Max reconnect attempts reached');
|
||||||
if (typeof updateConnectionStatus === 'function') {
|
if (typeof updateConnectionStatus === 'function') {
|
||||||
updateConnectionStatus('error');
|
updateConnectionStatus('error');
|
||||||
}
|
}
|
||||||
|
|
||||||
state.reconnectTimeout = setTimeout(() => {
|
const resetFn = () => {
|
||||||
state.reconnectTimeout = null;
|
state.reconnectTimeout = null;
|
||||||
config.reconnectAttempts = 0;
|
config.reconnectAttempts = 0;
|
||||||
publicAPI.connect();
|
publicAPI.connect();
|
||||||
}, 60000);
|
};
|
||||||
|
|
||||||
|
state.reconnectTimeout = window.CleanupManager
|
||||||
|
? window.CleanupManager.setTimeout(resetFn, 60000)
|
||||||
|
: setTimeout(resetFn, 60000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,5 +484,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//console.log('WebSocketManager initialized with methods:', Object.keys(WebSocketManager));
|
|
||||||
console.log('WebSocketManager initialized');
|
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) {
|
function debugLog(message, data) {
|
||||||
// if (isDebugEnabled()) {
|
|
||||||
// if (data) {
|
|
||||||
// console.log(`[AmmCounter] ${message}`, data);
|
|
||||||
// } else {
|
|
||||||
// console.log(`[AmmCounter] ${message}`);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAmmCounter(count, status) {
|
function updateAmmCounter(count, status) {
|
||||||
@@ -103,7 +97,7 @@ const AmmCounterManager = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
|
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
|
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
|
||||||
debugLog(`Re-initialized tooltips for ${tooltipId}`);
|
debugLog(`Re-initialized tooltips for ${tooltipId}`);
|
||||||
}, 50);
|
}, 50);
|
||||||
@@ -148,7 +142,7 @@ const AmmCounterManager = (function() {
|
|||||||
debugLog(`Retrying AMM status fetch (${fetchRetryCount}/${config.maxRetries}) in ${config.retryDelay/1000}s`);
|
debugLog(`Retrying AMM status fetch (${fetchRetryCount}/${config.maxRetries}) in ${config.retryDelay/1000}s`);
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
resolve(fetchAmmStatus());
|
resolve(fetchAmmStatus());
|
||||||
}, config.retryDelay);
|
}, config.retryDelay);
|
||||||
});
|
});
|
||||||
@@ -168,7 +162,7 @@ const AmmCounterManager = (function() {
|
|||||||
.then(() => {})
|
.then(() => {})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
refreshTimer = setInterval(() => {
|
refreshTimer = CleanupManager.setInterval(() => {
|
||||||
fetchAmmStatus()
|
fetchAmmStatus()
|
||||||
.then(() => {})
|
.then(() => {})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
@@ -251,5 +245,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (!window.ammCounterManagerInitialized) {
|
if (!window.ammCounterManagerInitialized) {
|
||||||
window.AmmCounterManager = AmmCounterManager.initialize();
|
window.AmmCounterManager = AmmCounterManager.initialize();
|
||||||
window.ammCounterManagerInitialized = true;
|
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) {
|
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) {
|
if (window.CoinManager && window.CoinManager.getDisplayName) {
|
||||||
const displayName = window.CoinManager.getDisplayName(coinId);
|
return window.CoinManager.getDisplayName(coinId) || 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 coinId;
|
return coinId;
|
||||||
}
|
}
|
||||||
@@ -303,7 +258,6 @@ const AmmTablesManager = (function() {
|
|||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (offersBody.innerHTML.trim() !== tableHtml.trim()) {
|
if (offersBody.innerHTML.trim() !== tableHtml.trim()) {
|
||||||
offersBody.innerHTML = tableHtml;
|
offersBody.innerHTML = tableHtml;
|
||||||
}
|
}
|
||||||
@@ -438,7 +392,6 @@ const AmmTablesManager = (function() {
|
|||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (bidsBody.innerHTML.trim() !== tableHtml.trim()) {
|
if (bidsBody.innerHTML.trim() !== tableHtml.trim()) {
|
||||||
bidsBody.innerHTML = tableHtml;
|
bidsBody.innerHTML = tableHtml;
|
||||||
}
|
}
|
||||||
@@ -540,7 +493,6 @@ const AmmTablesManager = (function() {
|
|||||||
coinPrice = window.latestPrices[coinName.toUpperCase()];
|
coinPrice = window.latestPrices[coinName.toUpperCase()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!coinPrice || isNaN(coinPrice)) {
|
if (!coinPrice || isNaN(coinPrice)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -550,6 +502,9 @@ const AmmTablesManager = (function() {
|
|||||||
|
|
||||||
function formatUSDPrice(usdValue) {
|
function formatUSDPrice(usdValue) {
|
||||||
if (!usdValue || isNaN(usdValue)) return '';
|
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)`;
|
return `($${usdValue.toFixed(2)} USD)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,7 +683,6 @@ const AmmTablesManager = (function() {
|
|||||||
const isMakerDropdown = select.id.includes('coin-from');
|
const isMakerDropdown = select.id.includes('coin-from');
|
||||||
const isTakerDropdown = select.id.includes('coin-to');
|
const isTakerDropdown = select.id.includes('coin-to');
|
||||||
|
|
||||||
|
|
||||||
const addModal = document.getElementById('add-amm-modal');
|
const addModal = document.getElementById('add-amm-modal');
|
||||||
const editModal = document.getElementById('edit-amm-modal');
|
const editModal = document.getElementById('edit-amm-modal');
|
||||||
const addModalVisible = addModal && !addModal.classList.contains('hidden');
|
const addModalVisible = addModal && !addModal.classList.contains('hidden');
|
||||||
@@ -755,7 +709,6 @@ const AmmTablesManager = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const result = isBidModal ? isTakerDropdown : isMakerDropdown;
|
const result = isBidModal ? isTakerDropdown : isMakerDropdown;
|
||||||
|
|
||||||
console.log(`[DEBUG] shouldDropdownOptionsShowBalance: ${select.id}, isBidModal=${isBidModal}, isMaker=${isMakerDropdown}, isTaker=${isTakerDropdown}, result=${result}`);
|
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');
|
const wrapper = select.parentNode.querySelector('.relative');
|
||||||
if (!wrapper) return;
|
if (!wrapper) return;
|
||||||
|
|
||||||
|
|
||||||
const dropdown = wrapper.querySelector('[role="listbox"]');
|
const dropdown = wrapper.querySelector('[role="listbox"]');
|
||||||
if (!dropdown) return;
|
if (!dropdown) return;
|
||||||
|
|
||||||
|
|
||||||
const options = dropdown.querySelectorAll('[data-value]');
|
const options = dropdown.querySelectorAll('[data-value]');
|
||||||
options.forEach(optionElement => {
|
options.forEach(optionElement => {
|
||||||
const coinValue = optionElement.getAttribute('data-value');
|
const coinValue = optionElement.getAttribute('data-value');
|
||||||
const originalOption = Array.from(select.options).find(opt => opt.value === coinValue);
|
const originalOption = Array.from(select.options).find(opt => opt.value === coinValue);
|
||||||
if (!originalOption) return;
|
if (!originalOption) return;
|
||||||
|
|
||||||
|
|
||||||
const textContainer = optionElement.querySelector('div.flex.flex-col, div.flex.items-center');
|
const textContainer = optionElement.querySelector('div.flex.flex-col, div.flex.items-center');
|
||||||
if (!textContainer) return;
|
if (!textContainer) return;
|
||||||
|
|
||||||
|
|
||||||
textContainer.innerHTML = '';
|
textContainer.innerHTML = '';
|
||||||
|
|
||||||
const shouldShowBalance = shouldDropdownOptionsShowBalance(select);
|
const shouldShowBalance = shouldDropdownOptionsShowBalance(select);
|
||||||
@@ -828,7 +777,6 @@ const AmmTablesManager = (function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function refreshDropdownBalances() {
|
function refreshDropdownBalances() {
|
||||||
const dropdownIds = ['add-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-from', 'edit-amm-coin-to'];
|
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');
|
const wrapper = select.parentNode.querySelector('.relative');
|
||||||
if (!wrapper) return;
|
if (!wrapper) return;
|
||||||
|
|
||||||
|
|
||||||
const dropdownItems = wrapper.querySelectorAll('[data-value]');
|
const dropdownItems = wrapper.querySelectorAll('[data-value]');
|
||||||
dropdownItems.forEach(item => {
|
dropdownItems.forEach(item => {
|
||||||
const value = item.getAttribute('data-value');
|
const value = item.getAttribute('data-value');
|
||||||
@@ -852,7 +799,6 @@ const AmmTablesManager = (function() {
|
|||||||
if (balanceDiv) {
|
if (balanceDiv) {
|
||||||
balanceDiv.textContent = `Balance: ${balance}`;
|
balanceDiv.textContent = `Balance: ${balance}`;
|
||||||
|
|
||||||
|
|
||||||
let pendingDiv = item.querySelector('.text-green-500');
|
let pendingDiv = item.querySelector('.text-green-500');
|
||||||
if (pendingBalance && parseFloat(pendingBalance) > 0) {
|
if (pendingBalance && parseFloat(pendingBalance) > 0) {
|
||||||
if (!pendingDiv) {
|
if (!pendingDiv) {
|
||||||
@@ -880,7 +826,6 @@ const AmmTablesManager = (function() {
|
|||||||
|
|
||||||
balanceDiv.textContent = `Balance: ${balance}`;
|
balanceDiv.textContent = `Balance: ${balance}`;
|
||||||
|
|
||||||
|
|
||||||
let pendingDiv = textContainer.querySelector('.text-green-500');
|
let pendingDiv = textContainer.querySelector('.text-green-500');
|
||||||
if (pendingBalance && parseFloat(pendingBalance) > 0) {
|
if (pendingBalance && parseFloat(pendingBalance) > 0) {
|
||||||
if (!pendingDiv) {
|
if (!pendingDiv) {
|
||||||
@@ -940,10 +885,8 @@ const AmmTablesManager = (function() {
|
|||||||
|
|
||||||
if (!coinFromSelect || !coinToSelect) return;
|
if (!coinFromSelect || !coinToSelect) return;
|
||||||
|
|
||||||
|
|
||||||
const balanceData = {};
|
const balanceData = {};
|
||||||
|
|
||||||
|
|
||||||
Array.from(coinFromSelect.options).forEach(option => {
|
Array.from(coinFromSelect.options).forEach(option => {
|
||||||
const balance = option.getAttribute('data-balance');
|
const balance = option.getAttribute('data-balance');
|
||||||
if (balance) {
|
if (balance) {
|
||||||
@@ -951,7 +894,6 @@ const AmmTablesManager = (function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
Array.from(coinToSelect.options).forEach(option => {
|
Array.from(coinToSelect.options).forEach(option => {
|
||||||
const balance = option.getAttribute('data-balance');
|
const balance = option.getAttribute('data-balance');
|
||||||
if (balance) {
|
if (balance) {
|
||||||
@@ -959,7 +901,6 @@ const AmmTablesManager = (function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
updateDropdownOptions(coinFromSelect, balanceData);
|
updateDropdownOptions(coinFromSelect, balanceData);
|
||||||
updateDropdownOptions(coinToSelect, balanceData);
|
updateDropdownOptions(coinToSelect, balanceData);
|
||||||
}
|
}
|
||||||
@@ -970,11 +911,9 @@ const AmmTablesManager = (function() {
|
|||||||
const balance = balanceData[coinName] || '0.00000000';
|
const balance = balanceData[coinName] || '0.00000000';
|
||||||
const pending = pendingData[coinName] || '0.0';
|
const pending = pendingData[coinName] || '0.0';
|
||||||
|
|
||||||
|
|
||||||
option.setAttribute('data-balance', balance);
|
option.setAttribute('data-balance', balance);
|
||||||
option.setAttribute('data-pending-balance', pending);
|
option.setAttribute('data-pending-balance', pending);
|
||||||
|
|
||||||
|
|
||||||
option.textContent = coinName;
|
option.textContent = coinName;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -982,7 +921,6 @@ const AmmTablesManager = (function() {
|
|||||||
function createSimpleDropdown(select, showBalance = false) {
|
function createSimpleDropdown(select, showBalance = false) {
|
||||||
if (!select) return;
|
if (!select) return;
|
||||||
|
|
||||||
|
|
||||||
const existingWrapper = select.parentNode.querySelector('.relative');
|
const existingWrapper = select.parentNode.querySelector('.relative');
|
||||||
if (existingWrapper) {
|
if (existingWrapper) {
|
||||||
existingWrapper.remove();
|
existingWrapper.remove();
|
||||||
@@ -994,13 +932,11 @@ const AmmTablesManager = (function() {
|
|||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'relative';
|
wrapper.className = 'relative';
|
||||||
|
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.type = '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.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';
|
button.style.minHeight = '60px';
|
||||||
|
|
||||||
|
|
||||||
const displayContent = document.createElement('div');
|
const displayContent = document.createElement('div');
|
||||||
displayContent.className = 'flex items-center';
|
displayContent.className = 'flex items-center';
|
||||||
|
|
||||||
@@ -1019,11 +955,9 @@ const AmmTablesManager = (function() {
|
|||||||
button.appendChild(displayContent);
|
button.appendChild(displayContent);
|
||||||
button.appendChild(arrow);
|
button.appendChild(arrow);
|
||||||
|
|
||||||
|
|
||||||
const dropdown = document.createElement('div');
|
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';
|
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 => {
|
Array.from(select.options).forEach(option => {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer';
|
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>
|
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${balance}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
if (pendingBalance && parseFloat(pendingBalance) > 0) {
|
if (pendingBalance && parseFloat(pendingBalance) > 0) {
|
||||||
html += `<div class="text-green-500 text-xs">+${pendingBalance} pending</div>`;
|
html += `<div class="text-green-500 text-xs">+${pendingBalance} pending</div>`;
|
||||||
}
|
}
|
||||||
@@ -1061,11 +994,9 @@ const AmmTablesManager = (function() {
|
|||||||
item.appendChild(itemIcon);
|
item.appendChild(itemIcon);
|
||||||
item.appendChild(itemText);
|
item.appendChild(itemText);
|
||||||
|
|
||||||
|
|
||||||
item.addEventListener('click', function() {
|
item.addEventListener('click', function() {
|
||||||
select.value = this.getAttribute('data-value');
|
select.value = this.getAttribute('data-value');
|
||||||
|
|
||||||
|
|
||||||
const selectedOption = select.options[select.selectedIndex];
|
const selectedOption = select.options[select.selectedIndex];
|
||||||
const selectedCoinName = selectedOption.textContent.trim();
|
const selectedCoinName = selectedOption.textContent.trim();
|
||||||
const selectedBalance = selectedOption.getAttribute('data-balance') || '0.00000000';
|
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>
|
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
|
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
|
||||||
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
|
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
|
||||||
}
|
}
|
||||||
@@ -1093,7 +1023,6 @@ const AmmTablesManager = (function() {
|
|||||||
|
|
||||||
dropdown.classList.add('hidden');
|
dropdown.classList.add('hidden');
|
||||||
|
|
||||||
|
|
||||||
const event = new Event('change', { bubbles: true });
|
const event = new Event('change', { bubbles: true });
|
||||||
select.dispatchEvent(event);
|
select.dispatchEvent(event);
|
||||||
});
|
});
|
||||||
@@ -1101,7 +1030,6 @@ const AmmTablesManager = (function() {
|
|||||||
dropdown.appendChild(item);
|
dropdown.appendChild(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const selectedOption = select.options[select.selectedIndex];
|
const selectedOption = select.options[select.selectedIndex];
|
||||||
if (selectedOption) {
|
if (selectedOption) {
|
||||||
const selectedCoinName = selectedOption.textContent.trim();
|
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>
|
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
|
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
|
||||||
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
|
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
|
||||||
}
|
}
|
||||||
@@ -1129,12 +1056,10 @@ const AmmTablesManager = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
button.addEventListener('click', function() {
|
button.addEventListener('click', function() {
|
||||||
dropdown.classList.toggle('hidden');
|
dropdown.classList.toggle('hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
if (!wrapper.contains(e.target)) {
|
if (!wrapper.contains(e.target)) {
|
||||||
dropdown.classList.add('hidden');
|
dropdown.classList.add('hidden');
|
||||||
@@ -1267,7 +1192,6 @@ const AmmTablesManager = (function() {
|
|||||||
modalTitle.textContent = `Add New ${type.charAt(0).toUpperCase() + type.slice(1)}`;
|
modalTitle.textContent = `Add New ${type.charAt(0).toUpperCase() + type.slice(1)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const modal = document.getElementById('add-amm-modal');
|
const modal = document.getElementById('add-amm-modal');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
@@ -1275,17 +1199,14 @@ const AmmTablesManager = (function() {
|
|||||||
modal.setAttribute('data-amm-type', type);
|
modal.setAttribute('data-amm-type', type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
||||||
updateDropdownsForModalType('add');
|
updateDropdownsForModalType('add');
|
||||||
|
|
||||||
initializeCustomSelects(type);
|
initializeCustomSelects(type);
|
||||||
|
|
||||||
|
|
||||||
refreshDropdownBalanceDisplay(type);
|
refreshDropdownBalanceDisplay(type);
|
||||||
|
|
||||||
|
|
||||||
if (typeof fetchBalanceData === 'function') {
|
if (typeof fetchBalanceData === 'function') {
|
||||||
fetchBalanceData()
|
fetchBalanceData()
|
||||||
.then(balanceData => {
|
.then(balanceData => {
|
||||||
@@ -1721,7 +1642,6 @@ const AmmTablesManager = (function() {
|
|||||||
modalTitle.textContent = `Edit ${type.charAt(0).toUpperCase() + type.slice(1)}`;
|
modalTitle.textContent = `Edit ${type.charAt(0).toUpperCase() + type.slice(1)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const modal = document.getElementById('edit-amm-modal');
|
const modal = document.getElementById('edit-amm-modal');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.classList.remove('hidden');
|
modal.classList.remove('hidden');
|
||||||
@@ -1729,17 +1649,14 @@ const AmmTablesManager = (function() {
|
|||||||
modal.setAttribute('data-amm-type', type);
|
modal.setAttribute('data-amm-type', type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
||||||
updateDropdownsForModalType('edit');
|
updateDropdownsForModalType('edit');
|
||||||
|
|
||||||
initializeCustomSelects(type);
|
initializeCustomSelects(type);
|
||||||
|
|
||||||
|
|
||||||
refreshDropdownBalanceDisplay(type);
|
refreshDropdownBalanceDisplay(type);
|
||||||
|
|
||||||
|
|
||||||
if (typeof fetchBalanceData === 'function') {
|
if (typeof fetchBalanceData === 'function') {
|
||||||
fetchBalanceData()
|
fetchBalanceData()
|
||||||
.then(balanceData => {
|
.then(balanceData => {
|
||||||
@@ -1873,7 +1790,6 @@ const AmmTablesManager = (function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function closeEditModal() {
|
function closeEditModal() {
|
||||||
const modal = document.getElementById('edit-amm-modal');
|
const modal = document.getElementById('edit-amm-modal');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
@@ -2306,14 +2222,11 @@ const AmmTablesManager = (function() {
|
|||||||
document.getElementById('edit-offer-swap-type')
|
document.getElementById('edit-offer-swap-type')
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function createSwapTypeDropdown(select) {
|
function createSwapTypeDropdown(select) {
|
||||||
if (!select) return;
|
if (!select) return;
|
||||||
|
|
||||||
|
|
||||||
if (select.style.display === 'none' && select.parentNode.querySelector('.relative')) {
|
if (select.style.display === 'none' && select.parentNode.querySelector('.relative')) {
|
||||||
return; // Custom dropdown already exists
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
@@ -2416,9 +2329,9 @@ const AmmTablesManager = (function() {
|
|||||||
|
|
||||||
let showBalance = false;
|
let showBalance = false;
|
||||||
if (modalType === 'offer' && select.id.includes('coin-from')) {
|
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')) {
|
} else if (modalType === 'bid' && select.id.includes('coin-to')) {
|
||||||
showBalance = true; // BID: taker shows balance
|
showBalance = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
createSimpleDropdown(select, showBalance);
|
createSimpleDropdown(select, showBalance);
|
||||||
@@ -2720,7 +2633,7 @@ const AmmTablesManager = (function() {
|
|||||||
icon.classList.remove('animate-spin');
|
icon.classList.remove('animate-spin');
|
||||||
}
|
}
|
||||||
refreshButton.disabled = false;
|
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 now = Math.floor(Date.now() / 1000);
|
||||||
const timeLeft = expireTime - now;
|
const timeLeft = expireTime - now;
|
||||||
|
|
||||||
if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less
|
if (timeLeft <= 300) return '#9CA3AF';
|
||||||
if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less
|
if (timeLeft <= 1800) return '#3B82F6';
|
||||||
return '#10B981'; // More than 30 minutes
|
return '#10B981';
|
||||||
};
|
};
|
||||||
|
|
||||||
const createTimeTooltip = (bid) => {
|
const createTimeTooltip = (bid) => {
|
||||||
@@ -249,7 +249,7 @@ const updateLoadingState = (isLoading) => {
|
|||||||
const refreshText = elements.refreshBidsButton.querySelector('#refreshText');
|
const refreshText = elements.refreshBidsButton.querySelector('#refreshText');
|
||||||
|
|
||||||
if (refreshIcon) {
|
if (refreshIcon) {
|
||||||
// Add CSS transition for smoother animation
|
|
||||||
refreshIcon.style.transition = 'transform 0.3s ease';
|
refreshIcon.style.transition = 'transform 0.3s ease';
|
||||||
refreshIcon.classList.toggle('animate-spin', isLoading);
|
refreshIcon.classList.toggle('animate-spin', isLoading);
|
||||||
}
|
}
|
||||||
@@ -631,7 +631,7 @@ if (elements.refreshBidsButton) {
|
|||||||
|
|
||||||
updateLoadingState(true);
|
updateLoadingState(true);
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
await new Promise(resolve => CleanupManager.setTimeout(resolve, 500));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateBidsTable({ resetPage: true, refreshData: true });
|
await updateBidsTable({ resetPage: true, refreshData: true });
|
||||||
@@ -66,7 +66,7 @@ const BidExporter = {
|
|||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
|
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}, 100);
|
}, 100);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -104,7 +104,7 @@ const BidExporter = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
setTimeout(function() {
|
CleanupManager.setTimeout(function() {
|
||||||
if (typeof state !== 'undefined' && typeof EventManager !== 'undefined') {
|
if (typeof state !== 'undefined' && typeof EventManager !== 'undefined') {
|
||||||
const exportAllButton = document.getElementById('exportAllBids');
|
const exportAllButton = document.getElementById('exportAllBids');
|
||||||
if (exportAllButton) {
|
if (exportAllButton) {
|
||||||
@@ -32,7 +32,7 @@ document.addEventListener('tabactivated', function(event) {
|
|||||||
if (event.detail && event.detail.tabId) {
|
if (event.detail && event.detail.tabId) {
|
||||||
const tabType = event.detail.type || (event.detail.tabId === '#all' ? 'all' :
|
const tabType = event.detail.type || (event.detail.tabId === '#all' ? 'all' :
|
||||||
(event.detail.tabId === '#sent' ? 'sent' : 'received'));
|
(event.detail.tabId === '#sent' ? 'sent' : 'received'));
|
||||||
//console.log('Tab activation event received for:', tabType);
|
|
||||||
state.currentTab = tabType;
|
state.currentTab = tabType;
|
||||||
updateBidsTable();
|
updateBidsTable();
|
||||||
}
|
}
|
||||||
@@ -190,8 +190,7 @@ const EventManager = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
//console.log('Starting comprehensive cleanup process for bids table');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (searchTimeout) {
|
if (searchTimeout) {
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
@@ -326,8 +325,7 @@ window.cleanupBidsTable = cleanup;
|
|||||||
|
|
||||||
CleanupManager.addListener(document, 'visibilitychange', () => {
|
CleanupManager.addListener(document, 'visibilitychange', () => {
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
//console.log('Page hidden - pausing WebSocket and optimizing memory');
|
|
||||||
|
|
||||||
if (WebSocketManager && typeof WebSocketManager.pause === 'function') {
|
if (WebSocketManager && typeof WebSocketManager.pause === 'function') {
|
||||||
WebSocketManager.pause();
|
WebSocketManager.pause();
|
||||||
} else if (WebSocketManager && typeof WebSocketManager.disconnect === 'function') {
|
} else if (WebSocketManager && typeof WebSocketManager.disconnect === 'function') {
|
||||||
@@ -351,7 +349,7 @@ CleanupManager.addListener(document, 'visibilitychange', () => {
|
|||||||
|
|
||||||
const lastUpdateTime = state.lastRefresh || 0;
|
const lastUpdateTime = state.lastRefresh || 0;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const refreshInterval = 5 * 60 * 1000; // 5 minutes
|
const refreshInterval = 5 * 60 * 1000;
|
||||||
|
|
||||||
if (now - lastUpdateTime > refreshInterval) {
|
if (now - lastUpdateTime > refreshInterval) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -490,13 +488,7 @@ function coinMatches(offerCoin, filterCoin) {
|
|||||||
|
|
||||||
if (offerCoin === filterCoin) return true;
|
if (offerCoin === filterCoin) return true;
|
||||||
|
|
||||||
if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
|
if (window.CoinUtils && window.CoinUtils.isSameCoin(offerCoin, filterCoin)) {
|
||||||
(filterCoin === 'firo' || filterCoin === 'zcoin')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
|
|
||||||
(offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1012,7 +1004,7 @@ const forceTooltipDOMCleanup = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (removedCount > 0) {
|
if (removedCount > 0) {
|
||||||
// console.log(`Tooltip cleanup: found ${foundCount}, removed ${removedCount} detached tooltips`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1323,8 +1315,6 @@ async function fetchBids(type = state.currentTab) {
|
|||||||
const withExpiredSelect = document.getElementById('with_expired');
|
const withExpiredSelect = document.getElementById('with_expired');
|
||||||
const includeExpired = withExpiredSelect ? withExpiredSelect.value === 'true' : true;
|
const includeExpired = withExpiredSelect ? withExpiredSelect.value === 'true' : true;
|
||||||
|
|
||||||
//console.log(`Fetching ${type} bids, include expired:`, includeExpired);
|
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
if (activeFetchController) {
|
if (activeFetchController) {
|
||||||
activeFetchController.abort();
|
activeFetchController.abort();
|
||||||
@@ -1372,8 +1362,6 @@ async function fetchBids(type = state.currentTab) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log(`Received raw ${type} data:`, data.length, 'bids');
|
|
||||||
|
|
||||||
state.filters.with_expired = includeExpired;
|
state.filters.with_expired = includeExpired;
|
||||||
|
|
||||||
let processedData;
|
let processedData;
|
||||||
@@ -1405,12 +1393,16 @@ const updateTableContent = async (type) => {
|
|||||||
const tbody = elements[`${type}BidsBody`];
|
const tbody = elements[`${type}BidsBody`];
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
|
|
||||||
|
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>';
|
||||||
|
|
||||||
if (window.TooltipManager) {
|
if (window.TooltipManager) {
|
||||||
window.TooltipManager.cleanup();
|
requestAnimationFrame(() => window.TooltipManager.cleanup());
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupTooltips();
|
requestAnimationFrame(() => {
|
||||||
forceTooltipDOMCleanup();
|
cleanupTooltips();
|
||||||
|
forceTooltipDOMCleanup();
|
||||||
|
});
|
||||||
|
|
||||||
tooltipIdsToCleanup.clear();
|
tooltipIdsToCleanup.clear();
|
||||||
|
|
||||||
@@ -1421,14 +1413,6 @@ const updateTableContent = async (type) => {
|
|||||||
|
|
||||||
const currentPageData = filteredData.slice(startIndex, endIndex);
|
const currentPageData = filteredData.slice(startIndex, endIndex);
|
||||||
|
|
||||||
//console.log('Updating table content:', {
|
|
||||||
// type: type,
|
|
||||||
// totalFilteredBids: filteredData.length,
|
|
||||||
// currentPageBids: currentPageData.length,
|
|
||||||
// startIndex: startIndex,
|
|
||||||
// endIndex: endIndex
|
|
||||||
//});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (currentPageData.length > 0) {
|
if (currentPageData.length > 0) {
|
||||||
const BATCH_SIZE = 10;
|
const BATCH_SIZE = 10;
|
||||||
@@ -1440,9 +1424,6 @@ const updateTableContent = async (type) => {
|
|||||||
const rows = await Promise.all(rowPromises);
|
const rows = await Promise.all(rowPromises);
|
||||||
allRows = allRows.concat(rows);
|
allRows = allRows.concat(rows);
|
||||||
|
|
||||||
if (i + BATCH_SIZE < currentPageData.length) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 5));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollPosition = tbody.parentElement?.scrollTop || 0;
|
const scrollPosition = tbody.parentElement?.scrollTop || 0;
|
||||||
@@ -1495,7 +1476,7 @@ const initializeTooltips = () => {
|
|||||||
const tooltipTriggers = document.querySelectorAll(selector);
|
const tooltipTriggers = document.querySelectorAll(selector);
|
||||||
const tooltipCount = tooltipTriggers.length;
|
const tooltipCount = tooltipTriggers.length;
|
||||||
if (tooltipCount > 50) {
|
if (tooltipCount > 50) {
|
||||||
//console.log(`Optimizing ${tooltipCount} tooltips`);
|
|
||||||
const viewportMargin = 200;
|
const viewportMargin = 200;
|
||||||
const viewportTooltips = Array.from(tooltipTriggers).filter(trigger => {
|
const viewportTooltips = Array.from(tooltipTriggers).filter(trigger => {
|
||||||
const rect = trigger.getBoundingClientRect();
|
const rect = trigger.getBoundingClientRect();
|
||||||
@@ -1595,13 +1576,6 @@ const updatePaginationControls = (type) => {
|
|||||||
const currentPageSpan = elements[`currentPage${type.charAt(0).toUpperCase() + type.slice(1)}`];
|
const currentPageSpan = elements[`currentPage${type.charAt(0).toUpperCase() + type.slice(1)}`];
|
||||||
const bidsCount = elements[`${type}BidsCount`];
|
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) {
|
if (state.currentPage[type] > totalPages) {
|
||||||
state.currentPage[type] = totalPages > 0 ? totalPages : 1;
|
state.currentPage[type] = totalPages > 0 ? totalPages : 1;
|
||||||
}
|
}
|
||||||
@@ -2077,7 +2051,7 @@ const setupEventListeners = () => {
|
|||||||
function setupMemoryMonitoring() {
|
function setupMemoryMonitoring() {
|
||||||
const MEMORY_CHECK_INTERVAL = 2 * 60 * 1000;
|
const MEMORY_CHECK_INTERVAL = 2 * 60 * 1000;
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = CleanupManager.setInterval(() => {
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
console.log('Tab hidden - running memory optimization');
|
console.log('Tab hidden - running memory optimization');
|
||||||
|
|
||||||
@@ -2110,9 +2084,9 @@ function setupMemoryMonitoring() {
|
|||||||
}
|
}
|
||||||
}, MEMORY_CHECK_INTERVAL);
|
}, MEMORY_CHECK_INTERVAL);
|
||||||
|
|
||||||
document.addEventListener('beforeunload', () => {
|
CleanupManager.registerResource('bidsMemoryMonitoring', intervalId, () => {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
}, { once: true });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initialize() {
|
function initialize() {
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
originalOnload();
|
originalOnload();
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(function() {
|
CleanupManager.setTimeout(function() {
|
||||||
initBidsTabNavigation();
|
initBidsTabNavigation();
|
||||||
handleInitialNavigation();
|
handleInitialNavigation();
|
||||||
}, 100);
|
}, 100);
|
||||||
@@ -15,6 +15,12 @@
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
initBidsTabNavigation();
|
initBidsTabNavigation();
|
||||||
|
|
||||||
|
if (window.CleanupManager) {
|
||||||
|
CleanupManager.registerResource('bidsTabHashChange', handleHashChange, () => {
|
||||||
|
window.removeEventListener('hashchange', handleHashChange);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('hashchange', handleHashChange);
|
window.addEventListener('hashchange', handleHashChange);
|
||||||
@@ -43,7 +49,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.bidsTabNavigationInitialized = true;
|
window.bidsTabNavigationInitialized = true;
|
||||||
//console.log('Bids tab navigation initialized');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInitialNavigation() {
|
function handleInitialNavigation() {
|
||||||
@@ -97,15 +103,13 @@
|
|||||||
if (!tabButton) {
|
if (!tabButton) {
|
||||||
if (retryCount < 5) {
|
if (retryCount < 5) {
|
||||||
|
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
activateTabWithRetry(normalizedTabId, retryCount + 1);
|
activateTabWithRetry(normalizedTabId, retryCount + 1);
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
tabButton.click();
|
tabButton.click();
|
||||||
|
|
||||||
if (window.Tabs) {
|
if (window.Tabs) {
|
||||||
@@ -160,7 +164,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function triggerDataLoad(tabId) {
|
function triggerDataLoad(tabId) {
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
if (window.state) {
|
if (window.state) {
|
||||||
window.state.currentTab = tabId === '#all' ? 'all' :
|
window.state.currentTab = tabId === '#all' ? 'all' :
|
||||||
(tabId === '#sent' ? 'sent' : 'received');
|
(tabId === '#sent' ? 'sent' : 'received');
|
||||||
@@ -181,7 +185,7 @@
|
|||||||
document.dispatchEvent(event);
|
document.dispatchEvent(event);
|
||||||
|
|
||||||
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
|
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
window.TooltipManager.cleanup();
|
window.TooltipManager.cleanup();
|
||||||
if (typeof window.initializeTooltips === 'function') {
|
if (typeof window.initializeTooltips === 'function') {
|
||||||
window.initializeTooltips();
|
window.initializeTooltips();
|
||||||
@@ -196,7 +200,7 @@
|
|||||||
|
|
||||||
activateTabWithRetry(tabId);
|
activateTabWithRetry(tabId);
|
||||||
|
|
||||||
setTimeout(function() {
|
CleanupManager.setTimeout(function() {
|
||||||
window.scrollTo(0, oldScrollPosition);
|
window.scrollTo(0, oldScrollPosition);
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,30 @@ const DOM = {
|
|||||||
queryAll: (selector) => document.querySelectorAll(selector)
|
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 = {
|
const Storage = {
|
||||||
get: (key) => {
|
get: (key) => {
|
||||||
try {
|
try {
|
||||||
@@ -450,20 +474,17 @@ const UIEnhancer = {
|
|||||||
const coinName = parts[0];
|
const coinName = parts[0];
|
||||||
const balanceInfo = parts[1] || '';
|
const balanceInfo = parts[1] || '';
|
||||||
|
|
||||||
|
|
||||||
selectNameElement.innerHTML = '';
|
selectNameElement.innerHTML = '';
|
||||||
selectNameElement.style.display = 'flex';
|
selectNameElement.style.display = 'flex';
|
||||||
selectNameElement.style.flexDirection = 'column';
|
selectNameElement.style.flexDirection = 'column';
|
||||||
selectNameElement.style.alignItems = 'flex-start';
|
selectNameElement.style.alignItems = 'flex-start';
|
||||||
selectNameElement.style.lineHeight = '1.2';
|
selectNameElement.style.lineHeight = '1.2';
|
||||||
|
|
||||||
|
|
||||||
const coinNameDiv = document.createElement('div');
|
const coinNameDiv = document.createElement('div');
|
||||||
coinNameDiv.textContent = coinName;
|
coinNameDiv.textContent = coinName;
|
||||||
coinNameDiv.style.fontWeight = 'normal';
|
coinNameDiv.style.fontWeight = 'normal';
|
||||||
coinNameDiv.style.color = 'inherit';
|
coinNameDiv.style.color = 'inherit';
|
||||||
|
|
||||||
|
|
||||||
const balanceDiv = document.createElement('div');
|
const balanceDiv = document.createElement('div');
|
||||||
balanceDiv.textContent = `Balance: ${balanceInfo}`;
|
balanceDiv.textContent = `Balance: ${balanceInfo}`;
|
||||||
balanceDiv.style.fontSize = '0.75rem';
|
balanceDiv.style.fontSize = '0.75rem';
|
||||||
@@ -473,8 +494,6 @@ const UIEnhancer = {
|
|||||||
selectNameElement.appendChild(coinNameDiv);
|
selectNameElement.appendChild(coinNameDiv);
|
||||||
selectNameElement.appendChild(balanceDiv);
|
selectNameElement.appendChild(balanceDiv);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
selectNameElement.textContent = name;
|
selectNameElement.textContent = name;
|
||||||
@@ -575,6 +594,8 @@ function initializeApp() {
|
|||||||
UIEnhancer.handleErrorHighlighting();
|
UIEnhancer.handleErrorHighlighting();
|
||||||
UIEnhancer.updateDisabledStyles();
|
UIEnhancer.updateDisabledStyles();
|
||||||
UIEnhancer.setupCustomSelects();
|
UIEnhancer.setupCustomSelects();
|
||||||
|
|
||||||
|
ErrorModal.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
@@ -582,3 +603,6 @@ if (document.readyState === 'loading') {
|
|||||||
} else {
|
} else {
|
||||||
initializeApp();
|
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 originalJsonData = [];
|
||||||
let currentSortColumn = 0;
|
let currentSortColumn = 0;
|
||||||
let currentSortDirection = 'desc';
|
let currentSortDirection = 'desc';
|
||||||
let filterTimeout = null;
|
|
||||||
let isPaginationInProgress = false;
|
let isPaginationInProgress = false;
|
||||||
|
let autoRefreshInterval = null;
|
||||||
|
|
||||||
const isSentOffers = window.offersTableConfig.isSentOffers;
|
const isSentOffers = window.offersTableConfig.isSentOffers;
|
||||||
const CACHE_DURATION = window.config.cacheConfig.defaultTTL;
|
const CACHE_DURATION = window.config.cacheConfig.defaultTTL;
|
||||||
@@ -28,6 +28,9 @@ window.tableRateModule = {
|
|||||||
processedOffers: new Set(),
|
processedOffers: new Set(),
|
||||||
|
|
||||||
getCachedValue(key) {
|
getCachedValue(key) {
|
||||||
|
if (window.CacheManager) {
|
||||||
|
return window.CacheManager.get(key);
|
||||||
|
}
|
||||||
const cachedItem = localStorage.getItem(key);
|
const cachedItem = localStorage.getItem(key);
|
||||||
if (cachedItem) {
|
if (cachedItem) {
|
||||||
const parsedItem = JSON.parse(cachedItem);
|
const parsedItem = JSON.parse(cachedItem);
|
||||||
@@ -41,6 +44,14 @@ window.tableRateModule = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
setCachedValue(key, value, resourceType = null) {
|
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 ?
|
const ttl = resourceType ?
|
||||||
window.config.cacheConfig.ttlSettings[resourceType] ||
|
window.config.cacheConfig.ttlSettings[resourceType] ||
|
||||||
window.config.cacheConfig.defaultTTL :
|
window.config.cacheConfig.defaultTTL :
|
||||||
@@ -65,26 +76,6 @@ window.tableRateModule = {
|
|||||||
return true;
|
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) {
|
getFallbackValue(coinSymbol) {
|
||||||
if (!coinSymbol) return null;
|
if (!coinSymbol) return null;
|
||||||
const normalizedSymbol = coinSymbol.toLowerCase() === 'part' ? 'particl' : coinSymbol.toLowerCase();
|
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() {
|
function getValidOffers() {
|
||||||
if (!jsonData) {
|
if (!jsonData) {
|
||||||
return [];
|
return [];
|
||||||
@@ -180,7 +206,6 @@ function saveFilterSettings() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getSelectedCoins(filterType) {
|
function getSelectedCoins(filterType) {
|
||||||
|
|
||||||
const dropdown = document.getElementById(`${filterType}_dropdown`);
|
const dropdown = document.getElementById(`${filterType}_dropdown`);
|
||||||
@@ -188,7 +213,6 @@ function getSelectedCoins(filterType) {
|
|||||||
return ['any'];
|
return ['any'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const allCheckboxes = dropdown.querySelectorAll('input[type="checkbox"]');
|
const allCheckboxes = dropdown.querySelectorAll('input[type="checkbox"]');
|
||||||
|
|
||||||
const selected = [];
|
const selected = [];
|
||||||
@@ -252,7 +276,6 @@ function updateFilterButtonText(filterType) {
|
|||||||
textSpan.textContent = `Filter ${filterLabel} (${selected.length} selected)`;
|
textSpan.textContent = `Filter ${filterLabel} (${selected.length} selected)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
button.style.width = '210px';
|
button.style.width = '210px';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +293,6 @@ function updateCoinBadges(filterType) {
|
|||||||
const coinName = getCoinNameFromValue(coinValue, filterType);
|
const coinName = getCoinNameFromValue(coinValue, filterType);
|
||||||
const badge = document.createElement('span');
|
const badge = document.createElement('span');
|
||||||
|
|
||||||
|
|
||||||
const isBidsFilter = filterType === 'coin_to' && !isSentOffers;
|
const isBidsFilter = filterType === 'coin_to' && !isSentOffers;
|
||||||
const isOffersFilter = filterType === 'coin_from' && !isSentOffers;
|
const isOffersFilter = filterType === 'coin_from' && !isSentOffers;
|
||||||
const isReceivingFilter = filterType === 'coin_to' && isSentOffers;
|
const isReceivingFilter = filterType === 'coin_to' && isSentOffers;
|
||||||
@@ -285,7 +307,6 @@ function updateCoinBadges(filterType) {
|
|||||||
|
|
||||||
badge.className = badgeClass + ' cursor-pointer hover:opacity-80';
|
badge.className = badgeClass + ' cursor-pointer hover:opacity-80';
|
||||||
|
|
||||||
|
|
||||||
const coinImage = getCoinImage(coinName);
|
const coinImage = getCoinImage(coinName);
|
||||||
|
|
||||||
badge.innerHTML = `
|
badge.innerHTML = `
|
||||||
@@ -350,13 +371,7 @@ function coinMatches(offerCoin, filterCoins) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((normalizedOfferCoin === 'firo' || normalizedOfferCoin === 'zcoin') &&
|
if (window.CoinUtils && window.CoinUtils.isSameCoin(normalizedOfferCoin, normalizedFilterCoin)) {
|
||||||
(normalizedFilterCoin === 'firo' || normalizedFilterCoin === 'zcoin')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((normalizedOfferCoin === 'bitcoincash' && normalizedFilterCoin === 'bitcoin cash') ||
|
|
||||||
(normalizedOfferCoin === 'bitcoin cash' && normalizedFilterCoin === 'bitcoincash')) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +482,6 @@ function removeCoinFilter(filterType, coinValue) {
|
|||||||
if (checkbox) {
|
if (checkbox) {
|
||||||
checkbox.checked = false;
|
checkbox.checked = false;
|
||||||
|
|
||||||
|
|
||||||
updateFilterButtonText(filterType);
|
updateFilterButtonText(filterType);
|
||||||
updateCoinBadges(filterType);
|
updateCoinBadges(filterType);
|
||||||
applyFilters();
|
applyFilters();
|
||||||
@@ -475,7 +489,6 @@ function removeCoinFilter(filterType, coinValue) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
window.removeCoinFilter = removeCoinFilter;
|
window.removeCoinFilter = removeCoinFilter;
|
||||||
|
|
||||||
function filterAndSortData() {
|
function filterAndSortData() {
|
||||||
@@ -510,7 +523,6 @@ function filterAndSortData() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (selectedCoinTo.length > 0 && !(selectedCoinTo.length === 1 && selectedCoinTo[0] === 'any')) {
|
if (selectedCoinTo.length > 0 && !(selectedCoinTo.length === 1 && selectedCoinTo[0] === 'any')) {
|
||||||
const coinNames = selectedCoinTo.map(value => getCoinNameFromValue(value, 'coin_to'));
|
const coinNames = selectedCoinTo.map(value => getCoinNameFromValue(value, 'coin_to'));
|
||||||
const matches = coinMatches(offer.coin_to, coinNames);
|
const matches = coinMatches(offer.coin_to, coinNames);
|
||||||
@@ -519,7 +531,6 @@ function filterAndSortData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (selectedCoinFrom.length > 0 && !(selectedCoinFrom.length === 1 && selectedCoinFrom[0] === 'any')) {
|
if (selectedCoinFrom.length > 0 && !(selectedCoinFrom.length === 1 && selectedCoinFrom[0] === 'any')) {
|
||||||
const coinNames = selectedCoinFrom.map(value => getCoinNameFromValue(value, 'coin_from'));
|
const coinNames = selectedCoinFrom.map(value => getCoinNameFromValue(value, 'coin_from'));
|
||||||
const matches = coinMatches(offer.coin_from, coinNames);
|
const matches = coinMatches(offer.coin_from, coinNames);
|
||||||
@@ -674,10 +685,9 @@ async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwn
|
|||||||
if (window.CoinManager) {
|
if (window.CoinManager) {
|
||||||
normalizedCoin = window.CoinManager.getPriceKey(coin) || normalizedCoin;
|
normalizedCoin = window.CoinManager.getPriceKey(coin) || normalizedCoin;
|
||||||
} else {
|
} else {
|
||||||
if (normalizedCoin === 'zcoin') normalizedCoin = 'firo';
|
if (window.CoinUtils) {
|
||||||
if (normalizedCoin === 'bitcoincash' || normalizedCoin === 'bitcoin cash')
|
normalizedCoin = window.CoinUtils.normalizeCoinName(normalizedCoin, latestPrices);
|
||||||
normalizedCoin = 'bitcoin-cash';
|
}
|
||||||
if (normalizedCoin.includes('particl')) normalizedCoin = 'particl';
|
|
||||||
}
|
}
|
||||||
let price = null;
|
let price = null;
|
||||||
if (latestPrices && latestPrices[normalizedCoin]) {
|
if (latestPrices && latestPrices[normalizedCoin]) {
|
||||||
@@ -739,6 +749,23 @@ async function fetchOffers() {
|
|||||||
const refreshIcon = document.getElementById('refreshIcon');
|
const refreshIcon = document.getElementById('refreshIcon');
|
||||||
const refreshText = document.getElementById('refreshText');
|
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 {
|
try {
|
||||||
if (!NetworkManager.isOnline()) {
|
if (!NetworkManager.isOnline()) {
|
||||||
throw new Error('Network is offline');
|
throw new Error('Network is offline');
|
||||||
@@ -752,14 +779,10 @@ async function fetchOffers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [offersResponse, pricesData] = await Promise.all([
|
const [offersResponse, pricesData] = await Promise.all([
|
||||||
fetch(isSentOffers ? '/json/sentoffers' : '/json/offers'),
|
fetchWithRetry(isSentOffers ? '/json/sentoffers' : '/json/offers'),
|
||||||
fetchLatestPrices()
|
fetchLatestPrices()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!offersResponse.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${offersResponse.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await offersResponse.json();
|
const data = await offersResponse.json();
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
@@ -871,27 +894,37 @@ function updateConnectionStatus(status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateRowTimes() {
|
function updateRowTimes() {
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const rows = document.querySelectorAll('[data-offer-id]');
|
const rows = document.querySelectorAll('[data-offer-id]');
|
||||||
rows.forEach(row => {
|
const updates = [];
|
||||||
const offerId = row.getAttribute('data-offer-id');
|
|
||||||
const offer = jsonData.find(o => o.offer_id === offerId);
|
|
||||||
if (!offer) return;
|
|
||||||
|
|
||||||
const newPostedTime = formatTime(offer.created_at, true);
|
rows.forEach(row => {
|
||||||
const newExpiresIn = formatTimeLeft(offer.expire_at);
|
const offerId = row.getAttribute('data-offer-id');
|
||||||
|
const offer = jsonData.find(o => o.offer_id === offerId);
|
||||||
|
if (!offer) return;
|
||||||
|
|
||||||
const postedElement = row.querySelector('.text-xs:first-child');
|
const newPostedTime = formatTime(offer.created_at, true);
|
||||||
const expiresElement = row.querySelector('.text-xs:last-child');
|
const newExpiresIn = formatTimeLeft(offer.expire_at);
|
||||||
|
|
||||||
if (postedElement && postedElement.textContent !== `Posted: ${newPostedTime}`) {
|
const postedElement = row.querySelector('.text-xs:first-child');
|
||||||
postedElement.textContent = `Posted: ${newPostedTime}`;
|
const expiresElement = row.querySelector('.text-xs:last-child');
|
||||||
}
|
|
||||||
if (expiresElement && expiresElement.textContent !== `Expires in: ${newExpiresIn}`) {
|
updates.push({
|
||||||
expiresElement.textContent = `Expires in: ${newExpiresIn}`;
|
postedElement,
|
||||||
}
|
expiresElement,
|
||||||
|
newPostedTime,
|
||||||
|
newExpiresIn
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updates.forEach(({ postedElement, expiresElement, newPostedTime, newExpiresIn }) => {
|
||||||
|
if (postedElement && postedElement.textContent !== `Posted: ${newPostedTime}`) {
|
||||||
|
postedElement.textContent = `Posted: ${newPostedTime}`;
|
||||||
|
}
|
||||||
|
if (expiresElement && expiresElement.textContent !== `Expires in: ${newExpiresIn}`) {
|
||||||
|
expiresElement.textContent = `Expires in: ${newExpiresIn}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateLastRefreshTime() {
|
function updateLastRefreshTime() {
|
||||||
@@ -1097,8 +1130,13 @@ async function updateOffersTable(options = {}) {
|
|||||||
return;
|
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) {
|
if (window.TooltipManager) {
|
||||||
window.TooltipManager.cleanup();
|
requestAnimationFrame(() => window.TooltipManager.cleanup());
|
||||||
}
|
}
|
||||||
|
|
||||||
const validOffers = getValidOffers();
|
const validOffers = getValidOffers();
|
||||||
@@ -1138,28 +1176,72 @@ async function updateOffersTable(options = {}) {
|
|||||||
if (row) fragment.appendChild(row);
|
if (row) fragment.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (i + BATCH_SIZE < itemsToDisplay.length) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 16));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offersBody) {
|
if (offersBody) {
|
||||||
const existingRows = offersBody.querySelectorAll('tr');
|
if (isIncrementalUpdate && offersBody.children.length > 0) {
|
||||||
existingRows.forEach(row => cleanupRow(row));
|
|
||||||
offersBody.textContent = '';
|
const existingRows = Array.from(offersBody.querySelectorAll('tr[data-offer-id]'));
|
||||||
offersBody.appendChild(fragment);
|
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();
|
updateRowTimes();
|
||||||
updatePaginationInfo();
|
updatePaginationInfo();
|
||||||
updateProfitLossDisplays();
|
updateProfitLossDisplays();
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
CleanupManager.setTimeout(() => {
|
||||||
if (tableRateModule?.initializeTable) {
|
if (tableRateModule?.initializeTable) {
|
||||||
tableRateModule.initializeTable();
|
tableRateModule.initializeTable();
|
||||||
}
|
}
|
||||||
});
|
}, 50);
|
||||||
|
|
||||||
lastRefreshTime = Date.now();
|
lastRefreshTime = Date.now();
|
||||||
updateLastRefreshTime();
|
updateLastRefreshTime();
|
||||||
@@ -1171,7 +1253,10 @@ async function updateOffersTable(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateProfitLossDisplays() {
|
function updateProfitLossDisplays() {
|
||||||
|
|
||||||
const rows = document.querySelectorAll('[data-offer-id]');
|
const rows = document.querySelectorAll('[data-offer-id]');
|
||||||
|
const updates = [];
|
||||||
|
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
const offerId = row.getAttribute('data-offer-id');
|
const offerId = row.getAttribute('data-offer-id');
|
||||||
const offer = jsonData.find(o => o.offer_id === offerId);
|
const offer = jsonData.find(o => o.offer_id === offerId);
|
||||||
@@ -1179,6 +1264,17 @@ function updateProfitLossDisplays() {
|
|||||||
|
|
||||||
const fromAmount = parseFloat(offer.amount_from) || 0;
|
const fromAmount = parseFloat(offer.amount_from) || 0;
|
||||||
const toAmount = parseFloat(offer.amount_to) || 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);
|
updateProfitLoss(row, offer.coin_from, offer.coin_to, fromAmount, toAmount, offer.is_own_offer);
|
||||||
|
|
||||||
const rateTooltipId = `tooltip-rate-${offerId}`;
|
const rateTooltipId = `tooltip-rate-${offerId}`;
|
||||||
@@ -1494,7 +1590,6 @@ function createRateColumn(offer, coinFrom, coinTo) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function createPercentageColumn(offer) {
|
function createPercentageColumn(offer) {
|
||||||
return `
|
return `
|
||||||
<td class="py-3 px-2 bold text-sm text-center monospace items-center rate-table-info">
|
<td class="py-3 px-2 bold text-sm text-center monospace items-center rate-table-info">
|
||||||
@@ -1731,45 +1826,10 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou
|
|||||||
|
|
||||||
const getPriceKey = (coin) => {
|
const getPriceKey = (coin) => {
|
||||||
if (!coin) return null;
|
if (!coin) return null;
|
||||||
|
if (window.CoinUtils) {
|
||||||
const lowerCoin = coin.toLowerCase();
|
return window.CoinUtils.normalizeCoinName(coin, latestPrices);
|
||||||
|
|
||||||
if (lowerCoin === 'zcoin') return 'firo';
|
|
||||||
|
|
||||||
if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') {
|
|
||||||
|
|
||||||
if (latestPrices && latestPrices['bitcoin-cash']) {
|
|
||||||
return 'bitcoin-cash';
|
|
||||||
} else if (latestPrices && latestPrices['bch']) {
|
|
||||||
return 'bch';
|
|
||||||
}
|
|
||||||
return 'bitcoin-cash';
|
|
||||||
}
|
}
|
||||||
|
return coin.toLowerCase();
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fromSymbol = getPriceKey(coinFrom);
|
const fromSymbol = getPriceKey(coinFrom);
|
||||||
@@ -1849,44 +1909,10 @@ function createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer) {
|
|||||||
|
|
||||||
const getPriceKey = (coin) => {
|
const getPriceKey = (coin) => {
|
||||||
if (!coin) return null;
|
if (!coin) return null;
|
||||||
|
if (window.CoinUtils) {
|
||||||
const lowerCoin = coin.toLowerCase();
|
return window.CoinUtils.normalizeCoinName(coin, latestPrices);
|
||||||
|
|
||||||
if (lowerCoin === 'zcoin') return 'firo';
|
|
||||||
|
|
||||||
if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') {
|
|
||||||
if (latestPrices && latestPrices['bitcoin-cash']) {
|
|
||||||
return 'bitcoin-cash';
|
|
||||||
} else if (latestPrices && latestPrices['bch']) {
|
|
||||||
return 'bch';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'bitcoin-cash';
|
|
||||||
}
|
}
|
||||||
|
return coin.toLowerCase();
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fromSymbol = getPriceKey(coinFrom);
|
const fromSymbol = getPriceKey(coinFrom);
|
||||||
@@ -1958,23 +1984,23 @@ function updateTooltipTargets(row, uniqueId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyFilters() {
|
function applyFilters(options = {}) {
|
||||||
if (filterTimeout) {
|
if (window.filterTimeout) {
|
||||||
clearTimeout(filterTimeout);
|
clearTimeout(window.filterTimeout);
|
||||||
filterTimeout = null;
|
window.filterTimeout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
filterTimeout = setTimeout(() => {
|
window.filterTimeout = CleanupManager.setTimeout(() => {
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
jsonData = filterAndSortData();
|
jsonData = filterAndSortData();
|
||||||
updateOffersTable();
|
updateOffersTable(options);
|
||||||
updateClearFiltersButton();
|
updateClearFiltersButton();
|
||||||
filterTimeout = null;
|
window.filterTimeout = null;
|
||||||
}, 250);
|
}, 250);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in filter timeout:', error);
|
console.error('Error in filter timeout:', error);
|
||||||
filterTimeout = null;
|
window.filterTimeout = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2037,13 +2063,10 @@ function formatTimeLeft(timestamp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayName(coinName) {
|
function getDisplayName(coinName) {
|
||||||
if (window.CoinManager) {
|
if (window.CoinManager && window.CoinManager.getDisplayName) {
|
||||||
return window.CoinManager.getDisplayName(coinName) || coinName;
|
return window.CoinManager.getDisplayName(coinName) || coinName;
|
||||||
}
|
}
|
||||||
if (coinName.toLowerCase() === 'zcoin') {
|
return coinName;
|
||||||
return 'Firo';
|
|
||||||
}
|
|
||||||
return window.config.coinMappings.nameToDisplayName[coinName] || coinName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCoinSymbolLowercase(coin) {
|
function getCoinSymbolLowercase(coin) {
|
||||||
@@ -2085,38 +2108,23 @@ function escapeHtml(unsafe) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPriceKey(coin) {
|
function getPriceKey(coin) {
|
||||||
|
|
||||||
if (window.CoinManager) {
|
if (window.CoinManager) {
|
||||||
return window.CoinManager.getPriceKey(coin);
|
return window.CoinManager.getPriceKey(coin);
|
||||||
}
|
}
|
||||||
|
if (window.CoinUtils) {
|
||||||
if (!coin) return null;
|
return window.CoinUtils.normalizeCoinName(coin);
|
||||||
|
|
||||||
const lowerCoin = coin.toLowerCase();
|
|
||||||
|
|
||||||
if (lowerCoin === 'zcoin') {
|
|
||||||
return 'firo';
|
|
||||||
}
|
}
|
||||||
|
return coin ? coin.toLowerCase() : null;
|
||||||
if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') {
|
|
||||||
return 'bitcoin-cash';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lowerCoin === 'part' || lowerCoin === 'particl' ||
|
|
||||||
lowerCoin.includes('particl')) {
|
|
||||||
return 'particl';
|
|
||||||
}
|
|
||||||
|
|
||||||
return lowerCoin;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCoinSymbol(fullName) {
|
function getCoinSymbol(fullName) {
|
||||||
|
|
||||||
if (window.CoinManager) {
|
if (window.CoinManager) {
|
||||||
return window.CoinManager.getSymbol(fullName) || fullName;
|
return window.CoinManager.getSymbol(fullName) || fullName;
|
||||||
}
|
}
|
||||||
|
if (window.CoinUtils) {
|
||||||
return window.config.coinMappings.nameToSymbol[fullName] || fullName;
|
return window.CoinUtils.getCoinSymbol(fullName);
|
||||||
|
}
|
||||||
|
return fullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeTableEvents() {
|
function initializeTableEvents() {
|
||||||
@@ -2140,7 +2148,6 @@ function initializeTableEvents() {
|
|||||||
const statusSelect = document.getElementById('status');
|
const statusSelect = document.getElementById('status');
|
||||||
const sentFromSelect = document.getElementById('sent_from');
|
const sentFromSelect = document.getElementById('sent_from');
|
||||||
|
|
||||||
|
|
||||||
if (coinToButton && coinToDropdown) {
|
if (coinToButton && coinToDropdown) {
|
||||||
CleanupManager.addListener(coinToButton, 'click', (e) => {
|
CleanupManager.addListener(coinToButton, 'click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -2155,7 +2162,6 @@ function initializeTableEvents() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (coinFromButton && coinFromDropdown) {
|
if (coinFromButton && coinFromDropdown) {
|
||||||
CleanupManager.addListener(coinFromButton, 'click', (e) => {
|
CleanupManager.addListener(coinFromButton, 'click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -2220,15 +2226,16 @@ function initializeTableEvents() {
|
|||||||
refreshButton.classList.remove('bg-blue-600', 'hover:bg-green-600', 'border-blue-500', 'hover:border-green-600');
|
refreshButton.classList.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');
|
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 currentTime = Date.now();
|
||||||
const elapsedTime = currentTime - startTime;
|
const elapsedTime = currentTime - startTime;
|
||||||
const remainingTime = Math.ceil((REFRESH_COOLDOWN - elapsedTime) / 1000);
|
const remainingTime = Math.ceil((REFRESH_COOLDOWN - elapsedTime) / 1000);
|
||||||
|
|
||||||
if (remainingTime <= 0) {
|
if (remainingTime <= 0) {
|
||||||
clearInterval(countdownInterval);
|
clearInterval(window.countdownInterval);
|
||||||
|
window.countdownInterval = null;
|
||||||
refreshText.textContent = 'Refresh';
|
refreshText.textContent = 'Refresh';
|
||||||
|
|
||||||
refreshButton.classList.remove('bg-red-600', 'border-red-500', 'cursor-not-allowed');
|
refreshButton.classList.remove('bg-red-600', 'border-red-500', 'cursor-not-allowed');
|
||||||
@@ -2240,7 +2247,6 @@ function initializeTableEvents() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Manual refresh initiated');
|
|
||||||
lastRefreshTime = now;
|
lastRefreshTime = now;
|
||||||
const refreshIcon = document.getElementById('refreshIcon');
|
const refreshIcon = document.getElementById('refreshIcon');
|
||||||
const refreshText = document.getElementById('refreshText');
|
const refreshText = document.getElementById('refreshText');
|
||||||
@@ -2267,10 +2273,10 @@ function initializeTableEvents() {
|
|||||||
if (!priceData && previousPrices) {
|
if (!priceData && previousPrices) {
|
||||||
console.log('Using previous price data after failed refresh');
|
console.log('Using previous price data after failed refresh');
|
||||||
latestPrices = previousPrices;
|
latestPrices = previousPrices;
|
||||||
applyFilters();
|
applyFilters({ incremental: false });
|
||||||
} else if (priceData) {
|
} else if (priceData) {
|
||||||
latestPrices = priceData;
|
latestPrices = priceData;
|
||||||
applyFilters();
|
applyFilters({ incremental: false });
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unable to fetch price data');
|
throw new Error('Unable to fetch price data');
|
||||||
}
|
}
|
||||||
@@ -2278,8 +2284,6 @@ function initializeTableEvents() {
|
|||||||
lastRefreshTime = now;
|
lastRefreshTime = now;
|
||||||
updateLastRefreshTime();
|
updateLastRefreshTime();
|
||||||
|
|
||||||
console.log('Manual refresh completed successfully');
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during manual refresh:', error);
|
console.error('Error during manual refresh:', error);
|
||||||
NetworkManager.handleNetworkError(error);
|
NetworkManager.handleNetworkError(error);
|
||||||
@@ -2320,7 +2324,7 @@ function initializeTableEvents() {
|
|||||||
await updateOffersTable({ fromPaginationClick: true });
|
await updateOffersTable({ fromPaginationClick: true });
|
||||||
updatePaginationInfo();
|
updatePaginationInfo();
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
isPaginationInProgress = false;
|
isPaginationInProgress = false;
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
@@ -2340,7 +2344,7 @@ function initializeTableEvents() {
|
|||||||
await updateOffersTable({ fromPaginationClick: true });
|
await updateOffersTable({ fromPaginationClick: true });
|
||||||
updatePaginationInfo();
|
updatePaginationInfo();
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
isPaginationInProgress = false;
|
isPaginationInProgress = false;
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
@@ -2416,18 +2420,44 @@ function handleTableSort(columnIndex, header) {
|
|||||||
clearTimeout(window.sortTimeout);
|
clearTimeout(window.sortTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.sortTimeout = setTimeout(() => {
|
window.sortTimeout = CleanupManager.setTimeout(() => {
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}, 100);
|
}, 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() {
|
async function initializeTableAndData() {
|
||||||
loadSavedSettings();
|
loadSavedSettings();
|
||||||
updateClearFiltersButton();
|
updateClearFiltersButton();
|
||||||
initializeTableEvents();
|
initializeTableEvents();
|
||||||
initializeTooltips();
|
initializeTooltips();
|
||||||
|
|
||||||
|
|
||||||
updateFilterButtonText('coin_to');
|
updateFilterButtonText('coin_to');
|
||||||
updateFilterButtonText('coin_from');
|
updateFilterButtonText('coin_from');
|
||||||
updateCoinBadges('coin_to');
|
updateCoinBadges('coin_to');
|
||||||
@@ -2527,24 +2557,44 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||||||
if (window.WebSocketManager) {
|
if (window.WebSocketManager) {
|
||||||
WebSocketManager.addMessageHandler('message', async (data) => {
|
WebSocketManager.addMessageHandler('message', async (data) => {
|
||||||
if (data.event === 'new_offer' || data.event === 'offer_revoked') {
|
if (data.event === 'new_offer' || data.event === 'offer_revoked') {
|
||||||
//console.log('WebSocket event received:', data.event);
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
const fetchWithRetry = async (url, maxRetries = 3) => {
|
||||||
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
if (i === maxRetries - 1) throw error;
|
||||||
|
|
||||||
|
await new Promise(resolve => CleanupManager.setTimeout(resolve, 100 * Math.pow(2, i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const previousPrices = latestPrices;
|
const offersResponse = await fetchWithRetry(isSentOffers ? '/json/sentoffers' : '/json/offers');
|
||||||
|
|
||||||
const offersResponse = await fetch(isSentOffers ? '/json/sentoffers' : '/json/offers');
|
|
||||||
if (!offersResponse.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${offersResponse.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newData = await offersResponse.json();
|
const newData = await offersResponse.json();
|
||||||
const processedNewData = Array.isArray(newData) ? newData : Object.values(newData);
|
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];
|
originalJsonData = [...jsonData];
|
||||||
|
|
||||||
|
const previousPrices = latestPrices;
|
||||||
let priceData;
|
let priceData;
|
||||||
if (window.PriceManager) {
|
if (window.PriceManager) {
|
||||||
priceData = await window.PriceManager.getPrices(true);
|
priceData = await window.PriceManager.getPrices(false);
|
||||||
} else {
|
} else {
|
||||||
priceData = await fetchLatestPrices();
|
priceData = await fetchLatestPrices();
|
||||||
}
|
}
|
||||||
@@ -2553,12 +2603,10 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||||||
latestPrices = priceData;
|
latestPrices = priceData;
|
||||||
CacheManager.set('prices_coingecko', priceData, 'prices');
|
CacheManager.set('prices_coingecko', priceData, 'prices');
|
||||||
} else if (previousPrices) {
|
} else if (previousPrices) {
|
||||||
console.log('Using previous price data after failed refresh');
|
|
||||||
latestPrices = previousPrices;
|
latestPrices = previousPrices;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyFilters();
|
applyFilters({ incremental: true, skipSkeleton: true });
|
||||||
|
|
||||||
updateProfitLossDisplays();
|
updateProfitLossDisplays();
|
||||||
|
|
||||||
document.querySelectorAll('.usd-value').forEach(usdValue => {
|
document.querySelectorAll('.usd-value').forEach(usdValue => {
|
||||||
@@ -2569,8 +2617,14 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||||||
if (price !== undefined && price !== null) {
|
if (price !== undefined && price !== null) {
|
||||||
const amount = parseFloat(usdValue.getAttribute('data-amount') || '0');
|
const amount = parseFloat(usdValue.getAttribute('data-amount') || '0');
|
||||||
if (!isNaN(amount) && amount > 0) {
|
if (!isNaN(amount) && amount > 0) {
|
||||||
const usdValue = amount * price;
|
const calculatedUSD = amount * price;
|
||||||
usdValue.textContent = tableRateModule.formatUSD(usdValue);
|
const formattedUSD = calculatedUSD < 0.01
|
||||||
|
? calculatedUSD.toFixed(8) + ' USD'
|
||||||
|
: calculatedUSD.toFixed(2) + ' USD';
|
||||||
|
|
||||||
|
if (usdValue.textContent !== formattedUSD) {
|
||||||
|
usdValue.textContent = formattedUSD;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2578,7 +2632,6 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||||||
|
|
||||||
updatePaginationInfo();
|
updatePaginationInfo();
|
||||||
|
|
||||||
//console.log('WebSocket-triggered refresh completed successfully');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during WebSocket-triggered refresh:', error);
|
console.error('Error during WebSocket-triggered refresh:', error);
|
||||||
NetworkManager.handleNetworkError(error);
|
NetworkManager.handleNetworkError(error);
|
||||||
@@ -2613,9 +2666,7 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.config.autoRefreshEnabled) {
|
startAutoRefresh();
|
||||||
startAutoRefresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterForm = document.getElementById('filterForm');
|
const filterForm = document.getElementById('filterForm');
|
||||||
if (filterForm) {
|
if (filterForm) {
|
||||||
@@ -2649,20 +2700,10 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const rowTimeInterval = setInterval(updateRowTimes, 30000);
|
const rowTimeInterval = CleanupManager.setInterval(updateRowTimes, 30000);
|
||||||
if (CleanupManager.registerResource) {
|
CleanupManager.registerResource('rowTimeInterval', rowTimeInterval, () => {
|
||||||
CleanupManager.registerResource('rowTimeInterval', rowTimeInterval, () => {
|
clearInterval(rowTimeInterval);
|
||||||
clearInterval(rowTimeInterval);
|
});
|
||||||
});
|
|
||||||
} else if (CleanupManager.addResource) {
|
|
||||||
CleanupManager.addResource('rowTimeInterval', rowTimeInterval, () => {
|
|
||||||
clearInterval(rowTimeInterval);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
|
|
||||||
window._cleanupIntervals = window._cleanupIntervals || [];
|
|
||||||
window._cleanupIntervals.push(rowTimeInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during initialization:', error);
|
console.error('Error during initialization:', error);
|
||||||
@@ -2694,6 +2735,8 @@ function cleanup() {
|
|||||||
window.countdownInterval = null;
|
window.countdownInterval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopAutoRefresh();
|
||||||
|
|
||||||
if (window._cleanupIntervals && Array.isArray(window._cleanupIntervals)) {
|
if (window._cleanupIntervals && Array.isArray(window._cleanupIntervals)) {
|
||||||
window._cleanupIntervals.forEach(interval => {
|
window._cleanupIntervals.forEach(interval => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
@@ -2739,7 +2782,6 @@ function cleanup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log('Offers.js cleanup completed');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during cleanup:', error);
|
console.error('Error during cleanup:', error);
|
||||||
}
|
}
|
||||||
@@ -2,46 +2,6 @@ const chartConfig = window.config.chartConfig;
|
|||||||
const coins = window.config.coins;
|
const coins = window.config.coins;
|
||||||
const apiKeys = window.config.getAPIKeys();
|
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 = {
|
const logger = {
|
||||||
log: (message) => console.log(`[AppLog] ${new Date().toISOString()}: ${message}`),
|
log: (message) => console.log(`[AppLog] ${new Date().toISOString()}: ${message}`),
|
||||||
warn: (message) => console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`),
|
warn: (message) => console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`),
|
||||||
@@ -94,29 +54,6 @@ const api = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchCryptoCompareDataXHR: (coin) => {
|
|
||||||
try {
|
|
||||||
if (!NetworkManager.isOnline()) {
|
|
||||||
throw new Error('Network is offline');
|
|
||||||
}
|
|
||||||
|
|
||||||
return Api.fetchCryptoCompareData(coin, {
|
|
||||||
cryptoCompare: apiKeys.cryptoCompare
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`CryptoCompare request failed for ${coin}:`, error);
|
|
||||||
|
|
||||||
NetworkManager.handleNetworkError(error);
|
|
||||||
|
|
||||||
const cachedData = CacheManager.get(`coinData_${coin}`);
|
|
||||||
if (cachedData) {
|
|
||||||
logger.info(`Using cached data for ${coin}`);
|
|
||||||
return cachedData.value;
|
|
||||||
}
|
|
||||||
return { error: error.message };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchCoinGeckoDataXHR: async () => {
|
fetchCoinGeckoDataXHR: async () => {
|
||||||
try {
|
try {
|
||||||
const priceData = await window.PriceManager.getPrices();
|
const priceData = await window.PriceManager.getPrices();
|
||||||
@@ -242,7 +179,7 @@ const rateLimiter = {
|
|||||||
const executeRequest = async () => {
|
const executeRequest = async () => {
|
||||||
const waitTime = this.getWaitTime(apiName);
|
const waitTime = this.getWaitTime(apiName);
|
||||||
if (waitTime > 0) {
|
if (waitTime > 0) {
|
||||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
await new Promise(resolve => CleanupManager.setTimeout(resolve, waitTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -252,7 +189,7 @@ const rateLimiter = {
|
|||||||
if (error.message.includes('429') && retryCount < this.retryDelays.length) {
|
if (error.message.includes('429') && retryCount < this.retryDelays.length) {
|
||||||
const delay = this.retryDelays[retryCount];
|
const delay = this.retryDelays[retryCount];
|
||||||
console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`);
|
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);
|
return this.queueRequest(apiName, requestFn, retryCount + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +197,7 @@ const rateLimiter = {
|
|||||||
retryCount < this.retryDelays.length) {
|
retryCount < this.retryDelays.length) {
|
||||||
const delay = this.retryDelays[retryCount];
|
const delay = this.retryDelays[retryCount];
|
||||||
logger.warn(`Request failed, retrying in ${delay/1000} seconds...`);
|
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);
|
return this.queueRequest(apiName, requestFn, retryCount + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +240,7 @@ const ui = {
|
|||||||
if (isError || volume24h === null || volume24h === undefined) {
|
if (isError || volume24h === null || volume24h === undefined) {
|
||||||
volumeElement.textContent = 'N/A';
|
volumeElement.textContent = 'N/A';
|
||||||
} else {
|
} else {
|
||||||
volumeElement.textContent = `${utils.formatNumber(volume24h, 0)} USD`;
|
volumeElement.textContent = `${window.config.utils.formatNumber(volume24h, 0)} USD`;
|
||||||
}
|
}
|
||||||
volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none';
|
volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
@@ -345,7 +282,7 @@ const ui = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
priceChange1d = data.price_change_percentage_24h || 0;
|
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)) {
|
if (isNaN(priceUSD) || isNaN(priceBTC)) {
|
||||||
throw new Error(`Invalid numeric values in data for ${coin}`);
|
throw new Error(`Invalid numeric values in data for ${coin}`);
|
||||||
}
|
}
|
||||||
@@ -498,7 +435,7 @@ const ui = {
|
|||||||
chartContainer.classList.add('blurred');
|
chartContainer.classList.add('blurred');
|
||||||
|
|
||||||
if (duration > 0) {
|
if (duration > 0) {
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
ui.hideErrorMessage();
|
ui.hideErrorMessage();
|
||||||
}, duration);
|
}, duration);
|
||||||
}
|
}
|
||||||
@@ -1199,11 +1136,11 @@ const app = {
|
|||||||
if (coinData) {
|
if (coinData) {
|
||||||
coinData.displayName = coin.displayName || coin.symbol;
|
coinData.displayName = coin.displayName || coin.symbol;
|
||||||
|
|
||||||
const backendId = getCoinBackendId ? getCoinBackendId(coin.name) : coin.name;
|
const volumeKey = coin.symbol.toLowerCase();
|
||||||
if (volumeData[backendId]) {
|
if (volumeData[volumeKey]) {
|
||||||
coinData.total_volume = volumeData[backendId].total_volume;
|
coinData.total_volume = volumeData[volumeKey].total_volume;
|
||||||
if (!coinData.price_change_percentage_24h && volumeData[backendId].price_change_percentage_24h) {
|
if (!coinData.price_change_percentage_24h && volumeData[volumeKey].price_change_percentage_24h) {
|
||||||
coinData.price_change_percentage_24h = volumeData[backendId].price_change_percentage_24h;
|
coinData.price_change_percentage_24h = volumeData[volumeKey].price_change_percentage_24h;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1231,11 +1168,7 @@ const app = {
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
ui.showCoinLoader(coin.symbol);
|
ui.showCoinLoader(coin.symbol);
|
||||||
if (coin.usesCoinGecko) {
|
data = await api.fetchCoinGeckoDataXHR(coin.symbol);
|
||||||
data = await api.fetchCoinGeckoDataXHR(coin.symbol);
|
|
||||||
} else {
|
|
||||||
data = await api.fetchCryptoCompareDataXHR(coin.symbol);
|
|
||||||
}
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
throw new Error(data.error);
|
throw new Error(data.error);
|
||||||
}
|
}
|
||||||
@@ -1382,7 +1315,7 @@ const app = {
|
|||||||
}
|
}
|
||||||
const timeUntilRefresh = nextRefreshTime - now;
|
const timeUntilRefresh = nextRefreshTime - now;
|
||||||
app.nextRefreshTime = nextRefreshTime;
|
app.nextRefreshTime = nextRefreshTime;
|
||||||
app.autoRefreshInterval = setTimeout(() => {
|
app.autoRefreshInterval = CleanupManager.setTimeout(() => {
|
||||||
if (NetworkManager.isOnline()) {
|
if (NetworkManager.isOnline()) {
|
||||||
app.refreshAllData();
|
app.refreshAllData();
|
||||||
} else {
|
} else {
|
||||||
@@ -1394,8 +1327,7 @@ const app = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
refreshAllData: async function() {
|
refreshAllData: async function() {
|
||||||
//console.log('Price refresh started at', new Date().toLocaleTimeString());
|
|
||||||
|
|
||||||
if (app.isRefreshing) {
|
if (app.isRefreshing) {
|
||||||
console.log('Refresh already in progress, skipping...');
|
console.log('Refresh already in progress, skipping...');
|
||||||
return;
|
return;
|
||||||
@@ -1415,7 +1347,7 @@ refreshAllData: async function() {
|
|||||||
ui.displayErrorMessage(`Rate limit: Please wait ${seconds} seconds before refreshing`);
|
ui.displayErrorMessage(`Rate limit: Please wait ${seconds} seconds before refreshing`);
|
||||||
|
|
||||||
let remainingTime = seconds;
|
let remainingTime = seconds;
|
||||||
const countdownInterval = setInterval(() => {
|
const countdownInterval = CleanupManager.setInterval(() => {
|
||||||
remainingTime--;
|
remainingTime--;
|
||||||
if (remainingTime > 0) {
|
if (remainingTime > 0) {
|
||||||
ui.displayErrorMessage(`Rate limit: Please wait ${remainingTime} seconds before refreshing`);
|
ui.displayErrorMessage(`Rate limit: Please wait ${remainingTime} seconds before refreshing`);
|
||||||
@@ -1428,7 +1360,6 @@ refreshAllData: async function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log('Starting refresh of all data...');
|
|
||||||
app.isRefreshing = true;
|
app.isRefreshing = true;
|
||||||
app.updateNextRefreshTime();
|
app.updateNextRefreshTime();
|
||||||
ui.showLoader();
|
ui.showLoader();
|
||||||
@@ -1443,7 +1374,7 @@ refreshAllData: async function() {
|
|||||||
console.warn('BTC price update failed, continuing with cached or default value');
|
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();
|
const allCoinData = await api.fetchCoinGeckoDataXHR();
|
||||||
if (allCoinData.error) {
|
if (allCoinData.error) {
|
||||||
@@ -1468,11 +1399,11 @@ refreshAllData: async function() {
|
|||||||
|
|
||||||
coinData.displayName = coin.displayName || coin.symbol;
|
coinData.displayName = coin.displayName || coin.symbol;
|
||||||
|
|
||||||
const backendId = getCoinBackendId ? getCoinBackendId(coin.name) : coin.name;
|
const volumeKey = coin.symbol.toLowerCase();
|
||||||
if (volumeData[backendId]) {
|
if (volumeData[volumeKey]) {
|
||||||
coinData.total_volume = volumeData[backendId].total_volume;
|
coinData.total_volume = volumeData[volumeKey].total_volume;
|
||||||
if (!coinData.price_change_percentage_24h && volumeData[backendId].price_change_percentage_24h) {
|
if (!coinData.price_change_percentage_24h && volumeData[volumeKey].price_change_percentage_24h) {
|
||||||
coinData.price_change_percentage_24h = volumeData[backendId].price_change_percentage_24h;
|
coinData.price_change_percentage_24h = volumeData[volumeKey].price_change_percentage_24h;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
@@ -1495,15 +1426,13 @@ refreshAllData: async function() {
|
|||||||
const cacheKey = `coinData_${coin.symbol}`;
|
const cacheKey = `coinData_${coin.symbol}`;
|
||||||
CacheManager.set(cacheKey, coinData, 'prices');
|
CacheManager.set(cacheKey, coinData, 'prices');
|
||||||
|
|
||||||
//console.log(`Updated price for ${coin.symbol}: $${coinData.current_price}`);
|
|
||||||
|
|
||||||
} catch (coinError) {
|
} catch (coinError) {
|
||||||
console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`);
|
console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`);
|
||||||
failedCoins.push(coin.symbol);
|
failedCoins.push(coin.symbol);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => CleanupManager.setTimeout(resolve, 1000));
|
||||||
|
|
||||||
if (chartModule.currentCoin) {
|
if (chartModule.currentCoin) {
|
||||||
try {
|
try {
|
||||||
@@ -1525,7 +1454,7 @@ refreshAllData: async function() {
|
|||||||
let countdown = 5;
|
let countdown = 5;
|
||||||
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
|
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
|
||||||
|
|
||||||
const countdownInterval = setInterval(() => {
|
const countdownInterval = CleanupManager.setInterval(() => {
|
||||||
countdown--;
|
countdown--;
|
||||||
if (countdown > 0) {
|
if (countdown > 0) {
|
||||||
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
|
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
|
||||||
@@ -1535,8 +1464,7 @@ refreshAllData: async function() {
|
|||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
//console.log(`Price refresh completed at ${new Date().toLocaleTimeString()}. Updated ${window.config.coins.length - failedCoins.length}/${window.config.coins.length} coins.`);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Critical error during refresh:', error);
|
console.error('Critical error during refresh:', error);
|
||||||
NetworkManager.handleNetworkError(error);
|
NetworkManager.handleNetworkError(error);
|
||||||
@@ -1544,7 +1472,7 @@ refreshAllData: async function() {
|
|||||||
let countdown = 10;
|
let countdown = 10;
|
||||||
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
|
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
|
||||||
|
|
||||||
const countdownInterval = setInterval(() => {
|
const countdownInterval = CleanupManager.setInterval(() => {
|
||||||
countdown--;
|
countdown--;
|
||||||
if (countdown > 0) {
|
if (countdown > 0) {
|
||||||
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
|
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
|
||||||
@@ -1566,7 +1494,6 @@ refreshAllData: async function() {
|
|||||||
app.scheduleNextRefresh();
|
app.scheduleNextRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.log(`Refresh process finished at ${new Date().toLocaleTimeString()}, next refresh scheduled: ${app.isAutoRefreshEnabled ? 'yes' : 'no'}`);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1590,7 +1517,7 @@ refreshAllData: async function() {
|
|||||||
const svg = document.querySelector('#toggle-auto-refresh svg');
|
const svg = document.querySelector('#toggle-auto-refresh svg');
|
||||||
if (svg) {
|
if (svg) {
|
||||||
svg.classList.add('animate-spin');
|
svg.classList.add('animate-spin');
|
||||||
setTimeout(() => {
|
CleanupManager.setTimeout(() => {
|
||||||
svg.classList.remove('animate-spin');
|
svg.classList.remove('animate-spin');
|
||||||
}, 2000);
|
}, 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 now = Math.floor(Date.now() / 1000);
|
||||||
const timeLeft = expireTime - now;
|
const timeLeft = expireTime - now;
|
||||||
|
|
||||||
if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less
|
if (timeLeft <= 300) return '#9CA3AF';
|
||||||
if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less
|
if (timeLeft <= 1800) return '#3B82F6';
|
||||||
return '#10B981'; // More than 30 minutes
|
return '#10B981';
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateConnectionStatus = (status) => {
|
const updateConnectionStatus = (status) => {
|
||||||
@@ -520,8 +520,6 @@ const createSwapTableRow = async (swap) => {
|
|||||||
async function updateSwapsTable(options = {}) {
|
async function updateSwapsTable(options = {}) {
|
||||||
const { resetPage = false, refreshData = true } = options;
|
const { resetPage = false, refreshData = true } = options;
|
||||||
|
|
||||||
//console.log('Updating swaps table:', { resetPage, refreshData });
|
|
||||||
|
|
||||||
if (state.refreshPromise) {
|
if (state.refreshPromise) {
|
||||||
await state.refreshPromise;
|
await state.refreshPromise;
|
||||||
return;
|
return;
|
||||||
@@ -547,19 +545,17 @@ async function updateSwapsTable(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
//console.log('Received swap data:', data);
|
|
||||||
|
|
||||||
state.swapsData = Array.isArray(data)
|
state.swapsData = Array.isArray(data)
|
||||||
? data.filter(swap => {
|
? data.filter(swap => {
|
||||||
const isActive = isActiveSwap(swap);
|
const isActive = isActiveSwap(swap);
|
||||||
//console.log(`Swap ${swap.bid_id}: ${isActive ? 'Active' : 'Inactive'}`, swap.bid_state);
|
|
||||||
return isActive;
|
return isActive;
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
//console.log('Filtered active swaps:', state.swapsData);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//console.error('Error fetching swap data:', error);
|
|
||||||
state.swapsData = [];
|
state.swapsData = [];
|
||||||
} finally {
|
} finally {
|
||||||
state.refreshPromise = null;
|
state.refreshPromise = null;
|
||||||
@@ -585,8 +581,6 @@ async function updateSwapsTable(options = {}) {
|
|||||||
const endIndex = startIndex + PAGE_SIZE;
|
const endIndex = startIndex + PAGE_SIZE;
|
||||||
const currentPageSwaps = state.swapsData.slice(startIndex, endIndex);
|
const currentPageSwaps = state.swapsData.slice(startIndex, endIndex);
|
||||||
|
|
||||||
//console.log('Current page swaps:', currentPageSwaps);
|
|
||||||
|
|
||||||
if (elements.swapsBody) {
|
if (elements.swapsBody) {
|
||||||
if (currentPageSwaps.length > 0) {
|
if (currentPageSwaps.length > 0) {
|
||||||
const rowPromises = currentPageSwaps.map(swap => createSwapTableRow(swap));
|
const rowPromises = currentPageSwaps.map(swap => createSwapTableRow(swap));
|
||||||
@@ -607,7 +601,7 @@ async function updateSwapsTable(options = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//console.log('No active swaps found, displaying empty state');
|
|
||||||
elements.swapsBody.innerHTML = `
|
elements.swapsBody.innerHTML = `
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white">
|
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white">
|
||||||
@@ -679,7 +673,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
WebSocketManager.initialize();
|
WebSocketManager.initialize();
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
await updateSwapsTable({ resetPage: true, refreshData: true });
|
await updateSwapsTable({ resetPage: true, refreshData: true });
|
||||||
const autoRefreshInterval = setInterval(async () => {
|
|
||||||
|
const autoRefreshInterval = CleanupManager.setInterval(async () => {
|
||||||
await updateSwapsTable({ resetPage: false, refreshData: true });
|
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) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') this.hide();
|
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);
|
window.addEventListener('resize', this._handleResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
document.removeEventListener('click', this._handleOutsideClick);
|
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);
|
window.removeEventListener('resize', this._handleResize);
|
||||||
|
|
||||||
const index = dropdownInstances.indexOf(this);
|
const index = dropdownInstances.indexOf(this);
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
{% include 'header.html' %}
|
{% include 'header.html' %}
|
||||||
{% from 'style.html' import breadcrumb_line_svg %}
|
{% from 'macros.html' import breadcrumb %}
|
||||||
<div class="container mx-auto">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': '404', 'url': '/404'}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
{% include 'header.html' %}
|
{% 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">
|
{{ page_header('Swaps in Progress', 'Monitor your currently active swap transactions.') }}
|
||||||
<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>
|
|
||||||
|
|
||||||
{% include 'inc_messages.html' %}
|
{% include 'inc_messages.html' %}
|
||||||
|
|
||||||
@@ -113,6 +100,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script src="/static/js/swaps_in_progress.js"></script>
|
<script src="/static/js/pages/swaps-page.js"></script>
|
||||||
|
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% include 'header.html' %}
|
{% 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>
|
<script>
|
||||||
window.ammTablesConfig = {
|
window.ammTablesConfig = {
|
||||||
@@ -9,31 +10,16 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="py-3 px-4 mt-6">
|
{{ page_header('Automated Market Maker', 'Automatically create offers and bids based on your configuration.', dark_bg='dark:bg-gray-500') }}
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="xl:container mx-auto">
|
<div class="xl:container mx-auto">
|
||||||
{% include 'inc_messages.html' %}
|
{% include 'inc_messages.html' %}
|
||||||
|
|
||||||
<section>
|
<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="flex flex-wrap -mx-4">
|
||||||
<div class="w-full px-4 mb-8">
|
<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 sm:pr-6 lg:pr-0 justify-end items-center">
|
||||||
<div class="flex space-x-2">
|
<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">
|
<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>
|
<section>
|
||||||
<div class="lg:container px-4 mx-auto">
|
<div class="lg:container px-4 mx-auto">
|
||||||
<div class="flex flex-wrap -mx-4">
|
<div class="flex flex-wrap -mx-4">
|
||||||
<div class="w-full lg:w-1/2 px-4 mb-8">
|
<div class="w-full lg:w-1/2 pr-4 mb-8">
|
||||||
<div class="p-6 bg-white dark:bg-gray-800 rounded-xl shadow-md">
|
<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>
|
<h3 class="mb-4 text-xl font-bold text-coolGray-900 dark:text-white">Control</h3>
|
||||||
<form method="post" autocomplete="off">
|
<form method="post" autocomplete="off">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
@@ -295,7 +281,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if debug_ui_mode %}
|
{% 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>
|
<h3 class="mb-4 text-xl font-bold text-coolGray-900 dark:text-white">Files</h3>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<p class="text-sm text-gray-700 dark:text-gray-300">
|
<p class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
@@ -327,8 +313,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full lg:w-1/2 px-4 mb-8">
|
<div class="w-full lg:w-1/2 pl-4 mb-8">
|
||||||
<div class="p-6 bg-white dark:bg-gray-800 rounded-xl shadow-md">
|
<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>
|
<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">
|
<div class="mb-4 border-b pb-5 border-gray-200 dark:border-gray-500">
|
||||||
@@ -744,280 +730,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<!-- AMM Configuration Tabs handled by external JS -->
|
||||||
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>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if state_exists and debug_ui_mode %}
|
{% 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>
|
<h3 class="mb-4 text-xl font-bold text-coolGray-900 dark:text-white">State File (JSON)</h3>
|
||||||
<div class="mb-4">
|
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
{% if debug_ui_mode %}
|
{% if debug_ui_mode %}
|
||||||
<div class="mb-8">
|
<div class="mb-8 -mx-4">
|
||||||
<div class="p-6 bg-white dark:bg-gray-800 rounded-xl shadow-md">
|
<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>
|
<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 %}
|
{% if logs %}
|
||||||
{% for log in logs %}
|
{% for log in logs %}
|
||||||
<div class="mb-1">{{ log }}</div>
|
<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>
|
<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">
|
<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">
|
<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" 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" onclick="setAmmAmount(0.5, 'add-amm-amount')">50%</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" onclick="setAmmAmount(1, 'add-amm-amount')">100%</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>
|
||||||
</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>
|
<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">
|
<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">
|
<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" 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" onclick="setAmmAmount(0.5, 'edit-amm-amount')">50%</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" onclick="setAmmAmount(1, 'edit-amm-amount')">100%</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1671,550 +1389,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/amm_tables.js"></script>
|
<script src="/static/js/pages/amm-tables.js"></script>
|
||||||
<script>
|
<script src="/static/js/pages/amm-config-tabs.js"></script>
|
||||||
function saveDebugSetting() {
|
<script src="/static/js/pages/amm-page.js"></script>
|
||||||
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>
|
|
||||||
|
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
{% include 'header.html' %}
|
{% 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">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'Automation Strategies', 'url': '/automation'}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
{% include 'header.html' %}
|
{% include 'header.html' %}
|
||||||
{% from 'style.html' import breadcrumb_line_svg %}
|
{% from 'macros.html' import breadcrumb %}
|
||||||
<div class="container mx-auto">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'Automation Strategies', 'url': '/automation'},
|
||||||
<p>Home</p>
|
{'text': 'ID:', 'url': '/automation'}
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
{% include 'header.html' %}
|
{% include 'header.html' %}
|
||||||
{% from 'style.html' import breadcrumb_line_svg %}
|
{% from 'macros.html' import breadcrumb %}
|
||||||
<div class="container mx-auto">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'Automation Strategies', 'url': '/automation'},
|
||||||
<p>Home</p>
|
{'text': 'New', 'url': '/automation'}
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,25 +1,16 @@
|
|||||||
{% include 'header.html' %}
|
{% 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">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'Bids', 'url': '#'},
|
||||||
<p>Home</p>
|
{'text': 'BID ID: ' ~ bid_id, 'url': bid_id}
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -534,14 +525,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if data.can_abandon == true and not edit_bid %}
|
{% if data.can_abandon == true and not edit_bid %}
|
||||||
<div class="w-full md:w-auto p-1.5">
|
<div class="w-full md:w-auto p-1.5">
|
||||||
<button name="abandon_bid" type="submit" value="Abandon Bid" onclick="return confirmPopup();" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Abandon Bid</button>
|
<button name="abandon_bid" type="submit" value="Abandon Bid" 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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if data.was_received and not edit_bid and data.can_accept_bid %}
|
{% if data.was_received and not edit_bid and data.can_accept_bid %}
|
||||||
<div class="w-full md:w-auto p-1.5">
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,25 +1,16 @@
|
|||||||
{% include 'header.html' %}
|
{% 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">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'Bids', 'url': '#'},
|
||||||
<p>Home</p>
|
{'text': 'BID ID: ' ~ bid_id, 'url': bid_id}
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -810,14 +801,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if data.can_abandon == true %}
|
{% if data.can_abandon == true %}
|
||||||
<div class="w-full md:w-auto p-1.5">
|
<div class="w-full md:w-auto p-1.5">
|
||||||
<button name="abandon_bid" type="submit" value="Abandon Bid" onclick="return confirmPopup();" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md focus:ring-0 focus:outline-none">Abandon Bid</button>
|
<button name="abandon_bid" type="submit" value="Abandon Bid" 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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if data.was_received and not edit_bid and data.can_accept_bid %}
|
{% if data.was_received and not edit_bid and data.can_accept_bid %}
|
||||||
<div class="w-full md:w-auto p-1.5">
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,22 +1,8 @@
|
|||||||
{% include 'header.html' %}
|
{% 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 %}
|
||||||
|
|
||||||
|
{{ page_header('All Bids / Sent Bids / Received Bids', 'View, and manage bids.') }}
|
||||||
<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>
|
|
||||||
|
|
||||||
{% include 'inc_messages.html' %}
|
{% include 'inc_messages.html' %}
|
||||||
|
|
||||||
@@ -467,7 +453,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/bids_sentreceived.js"></script>
|
<script src="/static/js/pages/bids-tab-navigation.js"></script>
|
||||||
<script src="/static/js/bids_sentreceived_export.js"></script>
|
<script src="/static/js/pages/bids-page.js"></script>
|
||||||
|
<script src="/static/js/pages/bids-export.js"></script>
|
||||||
|
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
{% include 'header.html' %}
|
{% 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">
|
{{ page_header('Bid Requests', 'Review and accept bids from other users.') }}
|
||||||
<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>
|
|
||||||
|
|
||||||
{% include 'inc_messages.html' %}
|
{% include 'inc_messages.html' %}
|
||||||
|
|
||||||
@@ -113,6 +100,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script src="/static/js/bids_available.js"></script>
|
<script src="/static/js/pages/bids-available-page.js"></script>
|
||||||
|
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
|
|||||||
@@ -1,28 +1,15 @@
|
|||||||
{% include 'header.html' %}
|
{% include 'header.html' %}
|
||||||
{% from 'style.html' import breadcrumb_line_svg %}
|
{% from 'macros.html' import breadcrumb %}
|
||||||
|
|
||||||
<div class="container mx-auto">
|
<div class="container mx-auto">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<li>
|
{'text': 'Change Password', 'url': '/changepassword'}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
{% include 'header.html' %}
|
{% 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">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'Debug', 'url': '/debug'}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -63,7 +56,7 @@
|
|||||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||||
<td class="py-3 px-6 bold">Remove expired offers and bids</td>
|
<td class="py-3 px-6 bold">Remove expired offers and bids</td>
|
||||||
<td td class="py-3 px-6 ">
|
<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>
|
Remove Data</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,24 +1,8 @@
|
|||||||
{% include 'header.html' %}
|
{% 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">
|
{{ 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="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>
|
|
||||||
|
|
||||||
<div class="xl:container mx-auto">
|
<div class="xl:container mx-auto">
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
{% include 'header.html' %}
|
{% 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">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'Explorers', 'url': '/explores'}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -88,9 +88,13 @@
|
|||||||
<script src="/static/js/libs/tippy.js"></script>
|
<script src="/static/js/libs/tippy.js"></script>
|
||||||
<!-- UI Components -->
|
<!-- UI Components -->
|
||||||
<script src="/static/js/ui/tabs.js"></script>
|
<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>
|
<script src="/static/js/ui/dropdown.js"></script>
|
||||||
<!-- Core functionality -->
|
<!-- 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/coin-manager.js"></script>
|
||||||
<script src="/static/js/modules/config-manager.js"></script>
|
<script src="/static/js/modules/config-manager.js"></script>
|
||||||
<script src="/static/js/modules/cache-manager.js"></script>
|
<script src="/static/js/modules/cache-manager.js"></script>
|
||||||
@@ -104,7 +108,8 @@
|
|||||||
<script src="/static/js/modules/balance-updates.js"></script>
|
<script src="/static/js/modules/balance-updates.js"></script>
|
||||||
<script src="/static/js/modules/identity-manager.js"></script>
|
<script src="/static/js/modules/identity-manager.js"></script>
|
||||||
<script src="/static/js/modules/summary-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' %}
|
{% if current_page == 'wallets' or current_page == 'wallet' %}
|
||||||
<script src="/static/js/modules/wallet-manager.js"></script>
|
<script src="/static/js/modules/wallet-manager.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -817,18 +822,6 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script>
|
<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) {
|
function handleOutsideClick(event) {
|
||||||
const button = document.getElementById('notification-history-button');
|
const button = document.getElementById('notification-history-button');
|
||||||
const mobileButton = document.getElementById('notification-history-button-mobile');
|
const mobileButton = document.getElementById('notification-history-button-mobile');
|
||||||
|
|||||||
@@ -1,24 +1,15 @@
|
|||||||
{% include 'header.html' %}
|
{% 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">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'Identity', 'url': '/smsgaddresses'},
|
||||||
<p>Home</p>
|
{'text': 'Address: ' ~ data.identity_address, 'url': '/identity/' ~ data.identity_address}
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-auto p-2">
|
<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 }}
|
{{ green_cross_close_svg | safe }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-auto p-2">
|
<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>
|
<span class="sr-only">Close</span>
|
||||||
{{ red_cross_close_svg | safe }}
|
{{ red_cross_close_svg | safe }}
|
||||||
</button>
|
</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' %}
|
{% 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">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'Offer', 'url': '#'},
|
||||||
<p>Home</p>
|
{'text': 'OFFER ID: ' ~ offer_id, 'url': offer_id}
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -414,58 +401,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<script>
|
<!-- Address handling functionality moved to external JS -->
|
||||||
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>
|
|
||||||
{% if data.amount_negotiable == true %}
|
{% 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">
|
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||||
<td class="px-6">
|
<td class="px-6">
|
||||||
@@ -626,7 +562,7 @@ if (document.readyState === 'loading') {
|
|||||||
<div class="w-full md:w-0/12">
|
<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="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">
|
<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
|
Clear Form
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -652,6 +588,29 @@ if (document.readyState === 'loading') {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 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="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="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">
|
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
|
Confirm
|
||||||
</button>
|
</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">
|
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
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -699,347 +658,8 @@ if (document.readyState === 'loading') {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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();
|
<script src="/static/js/pages/offer-page.js"></script>
|
||||||
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>
|
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<section>
|
<section>
|
||||||
@@ -1063,13 +683,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
</div>
|
</div>
|
||||||
<!-- todo
|
<!-- todo
|
||||||
{% if data.active_ind == 1 %}
|
{% 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>
|
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 %}
|
{% endif %}
|
||||||
-->
|
-->
|
||||||
{% if data.was_revoked != true %}
|
{% if data.was_revoked != true %}
|
||||||
<div class="w-full md:w-auto p-1.5">
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -1097,6 +717,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
<input type="hidden" id="rate_var" value="{{ data.rate_negotiable }}">
|
<input type="hidden" id="rate_var" value="{{ data.rate_negotiable }}">
|
||||||
<input type="hidden" id="amount_from" value="{{ data.amt_from }}">
|
<input type="hidden" id="amount_from" value="{{ data.amt_from }}">
|
||||||
<input type="hidden" id="offer_rate" value="{{ data.rate }}">
|
<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 }}">
|
<input type="hidden" name="formid" value="{{ form_id }}">
|
||||||
</form>
|
</form>
|
||||||
<p id="rates_display"></p>
|
<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">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li> <a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'Home', 'url': '/'},
|
||||||
<p>Home</p>
|
{'text': 'Place', 'url': '/newoffer'},
|
||||||
</a> </li>
|
{'text': 'Setup', 'url': '#'},
|
||||||
<li>{{ breadcrumb_line_svg | safe }}</li>
|
{'text': 'Confirm', 'url': '#'}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -465,7 +460,7 @@
|
|||||||
<input type="hidden" name="rate_var" value="rv">
|
<input type="hidden" name="rate_var" value="rv">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
<script src="static/js/new_offer.js"></script>
|
<script src="static/js/pages/offer-new-page.js"></script>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'Home', 'url': '/'},
|
||||||
<p>Home</p>
|
{'text': 'Place', 'url': '/newoffer'}
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 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>
|
||||||
<div class="mt-2 flex space-x-2">
|
<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" 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" onclick="setOfferAmount(0.5, 'amt_from')">50%</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" onclick="setOfferAmount(1, 'amt_from')">100%</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -315,7 +311,7 @@
|
|||||||
<div class="px-6">
|
<div class="px-6">
|
||||||
<div class="flex flex-wrap justify-end">
|
<div class="flex flex-wrap justify-end">
|
||||||
<div class="w-full md:w-auto p-1.5">
|
<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>
|
<span>Check Current Prices/Rates (JSON)</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -338,6 +334,29 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</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>
|
<script>
|
||||||
function setOfferAmount(percent, fieldId) {
|
function setOfferAmount(percent, fieldId) {
|
||||||
const amountInput = document.getElementById(fieldId);
|
const amountInput = document.getElementById(fieldId);
|
||||||
@@ -350,7 +369,11 @@ function setOfferAmount(percent, fieldId) {
|
|||||||
|
|
||||||
const selectedOption = coinFromSelect.options[coinFromSelect.selectedIndex];
|
const selectedOption = coinFromSelect.options[coinFromSelect.selectedIndex];
|
||||||
if (!selectedOption || selectedOption.value === '-1') {
|
if (!selectedOption || selectedOption.value === '-1') {
|
||||||
alert('Please select a coin first');
|
if (window.showErrorModal) {
|
||||||
|
window.showErrorModal('Validation Error', 'Please select a coin first');
|
||||||
|
} else {
|
||||||
|
alert('Please select a coin first');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +385,11 @@ function setOfferAmount(percent, fieldId) {
|
|||||||
|
|
||||||
const floatBalance = parseFloat(balance);
|
const floatBalance = parseFloat(balance);
|
||||||
if (isNaN(floatBalance) || floatBalance <= 0) {
|
if (isNaN(floatBalance) || floatBalance <= 0) {
|
||||||
alert('Invalid balance for selected coin');
|
if (window.showErrorModal) {
|
||||||
|
window.showErrorModal('Invalid Balance', 'The selected coin has no available balance. Please select a coin with a positive balance.');
|
||||||
|
} else {
|
||||||
|
alert('Invalid balance for selected coin');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,7 +500,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="static/js/new_offer.js"></script>
|
<script src="static/js/pages/offer-new-page.js"></script>
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</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">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li> <a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'Home', 'url': '/'},
|
||||||
<p>Home</p>
|
{'text': 'Place', 'url': '/newoffer'},
|
||||||
</a>
|
{'text': 'Setup', 'url': '#'}
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -447,7 +438,7 @@
|
|||||||
<input type="hidden" name="rate_var" value="true">
|
<input type="hidden" name="rate_var" value="true">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
<script src="static/js/new_offer.js"></script>
|
<script src="static/js/pages/offer-new-page.js"></script>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% include 'header.html' %}
|
{% 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>
|
<script>
|
||||||
window.offersTableConfig = {
|
window.offersTableConfig = {
|
||||||
@@ -8,29 +9,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="py-3 px-4 mt-6">
|
{{ page_header(page_type, page_type_description, dark_bg='dark:bg-gray-500') }}
|
||||||
<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>
|
|
||||||
|
|
||||||
{% include 'inc_messages.html' %}
|
{% include 'inc_messages.html' %}
|
||||||
|
|
||||||
@@ -194,7 +173,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<script src="/static/js/pricechart.js"></script>
|
<script src="/static/js/pages/offers-pricechart.js"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
@@ -432,6 +411,6 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<input type="hidden" name="formid" value="{{ form_id }}">
|
<input type="hidden" name="formid" value="{{ form_id }}">
|
||||||
<script src="/static/js/offers.js"></script>
|
<script src="/static/js/pages/offers-page.js"></script>
|
||||||
|
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
{% include 'header.html' %}
|
{% 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">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'RPC Console', 'url': '/rpc'}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
{% include 'header.html' %}
|
{% 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">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'Settings', 'url': '/settings'}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -268,12 +261,12 @@
|
|||||||
Apply Changes
|
Apply Changes
|
||||||
</button>
|
</button>
|
||||||
{% if c.can_disable == true %}
|
{% 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
|
Disable Coin
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if c.can_reenable == true %}
|
{% 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
|
Enable Coin
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -507,7 +500,7 @@
|
|||||||
<div class="flex items-center">
|
<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 %}>
|
<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>
|
<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
|
Check Now
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -549,10 +542,10 @@
|
|||||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">Test Notifications</h4>
|
<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="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
||||||
<div class="space-y-3">
|
<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
|
Test All Notification Types
|
||||||
</button>
|
</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
|
Test Update Notification
|
||||||
</button>
|
</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">
|
<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 +570,6 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</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">
|
<div class="tab-content hidden" id="tor" role="tabpanel" aria-labelledby="tor-tab">
|
||||||
<form method="post" id="tor-form">
|
<form method="post" id="tor-form">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
@@ -837,131 +662,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script src="/static/js/pages/settings-page.js"></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>
|
|
||||||
|
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
{% include 'header.html' %}
|
{% 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">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'SMSG Addresses', 'url': '/smsgaddresses'}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
{% include 'header.html' %}
|
{% 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">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'Tor', 'url': '/tor'}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -129,7 +129,7 @@
|
|||||||
<div class="mt-8 text-center space-y-2">
|
<div class="mt-8 text-center space-y-2">
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
Need help?
|
Need help?
|
||||||
<a href="https://academy.particl.io/en/latest/faq/get_support.html"
|
<a href="https://docs.basicswapdex.com/docs/intro/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline">
|
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline">
|
||||||
View tutorials
|
View tutorials
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
{% include 'header.html' %}
|
{% include 'header.html' %}
|
||||||
{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg, withdraw_svg, utxo_groups_svg, create_utxo_svg, lock_svg, eye_show_svg %}
|
{% from 'style.html' import circular_arrows_svg, withdraw_svg, utxo_groups_svg, create_utxo_svg, lock_svg, eye_show_svg %}
|
||||||
|
|
||||||
<section class="py-3 px-4 mt-6">
|
<section class="py-3 px-4 mt-6">
|
||||||
<div class="lg:container mx-auto">
|
<div class="lg:container mx-auto">
|
||||||
@@ -190,425 +190,7 @@
|
|||||||
|
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
|
|
||||||
<script>
|
<script src="/static/js/pages/wallets-page.js"></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>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
{% include 'header.html' %}
|
{% 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">
|
<div class="container mx-auto">
|
||||||
<section class="p-5 mt-5">
|
<section class="p-5 mt-5">
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="flex flex-wrap items-center -m-2">
|
||||||
<div class="w-full md:w-1/2 p-2">
|
<div class="w-full md:w-1/2 p-2">
|
||||||
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
|
{{ breadcrumb([
|
||||||
<li>
|
{'text': 'Home', 'url': '/'},
|
||||||
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
|
{'text': 'Watched Outputs', 'url': '/watched'}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -409,10 +409,6 @@ def get_amm_active_count(swap_client, debug_override=False):
|
|||||||
|
|
||||||
state_path = get_amm_state_path(swap_client)
|
state_path = get_amm_state_path(swap_client)
|
||||||
if not os.path.exists(state_path):
|
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
|
return 0
|
||||||
|
|
||||||
config_path = get_amm_config_path(swap_client)
|
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):
|
if bid.get("enabled", False):
|
||||||
enabled_bids.add(bid.get("name", ""))
|
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:
|
except Exception as e:
|
||||||
if debug_enabled:
|
if debug_enabled:
|
||||||
swap_client.log.error(f"Error reading config file: {str(e)}")
|
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:
|
with open(state_path, "r") as f:
|
||||||
state_data = json.load(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:
|
try:
|
||||||
network_offers = swap_client.listOffers()
|
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())
|
swap_client.log.error(traceback.format_exc())
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if debug_enabled:
|
|
||||||
swap_client.log.debug(
|
|
||||||
f"Found {len(active_network_offers)} active offers in the network"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if debug_enabled:
|
if debug_enabled:
|
||||||
swap_client.log.error(f"Error getting network offers: {str(e)}")
|
swap_client.log.error(f"Error getting network offers: {str(e)}")
|
||||||
swap_client.log.error(traceback.format_exc())
|
swap_client.log.error(traceback.format_exc())
|
||||||
|
|
||||||
if len(active_network_offers) == 0:
|
if len(active_network_offers) == 0:
|
||||||
if debug_enabled:
|
|
||||||
swap_client.log.info(
|
|
||||||
"No active network offers found, trying direct API call"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
global amm_host, amm_port
|
global amm_host, amm_port
|
||||||
if "amm_host" not in globals() or "amm_port" not in globals():
|
if "amm_host" not in globals() or "amm_port" not in globals():
|
||||||
amm_host = "127.0.0.1"
|
amm_host = "127.0.0.1"
|
||||||
amm_port = 12700
|
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"
|
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"]
|
offer_id = offer["id"]
|
||||||
active_network_offers[offer_id] = True
|
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:
|
except Exception as e:
|
||||||
if debug_enabled:
|
if debug_enabled:
|
||||||
swap_client.log.error(f"Error getting offers via API: {str(e)}")
|
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
|
active_offer_count += 1
|
||||||
|
|
||||||
amm_count += active_offer_count
|
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:
|
if "bids" in state_data:
|
||||||
for template_name, bids in state_data["bids"].items():
|
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
|
active_bid_count += 1
|
||||||
|
|
||||||
amm_count += active_bid_count
|
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 (
|
if (
|
||||||
amm_count == 0
|
amm_count == 0
|
||||||
and len(active_network_offers) == 0
|
and len(active_network_offers) == 0
|
||||||
and "offers" in state_data
|
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_time = 0
|
||||||
most_recent_offer = None
|
most_recent_offer = None
|
||||||
|
|
||||||
@@ -631,21 +559,8 @@ def get_amm_active_count(swap_client, debug_override=False):
|
|||||||
|
|
||||||
if offer_age < 3600:
|
if offer_age < 3600:
|
||||||
amm_count = 1
|
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 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)
|
config_path = get_amm_config_path(swap_client)
|
||||||
if os.path.exists(config_path):
|
if os.path.exists(config_path):
|
||||||
try:
|
try:
|
||||||
@@ -654,10 +569,6 @@ def get_amm_active_count(swap_client, debug_override=False):
|
|||||||
|
|
||||||
for offer in config_data.get("offers", []):
|
for offer in config_data.get("offers", []):
|
||||||
if offer.get("enabled", False):
|
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
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if debug_enabled:
|
if debug_enabled:
|
||||||
@@ -669,10 +580,6 @@ def get_amm_active_count(swap_client, debug_override=False):
|
|||||||
and "offers" in state_data
|
and "offers" in state_data
|
||||||
and len(state_data["offers"]) > 0
|
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
|
amm_count = 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if debug_enabled:
|
if debug_enabled:
|
||||||
@@ -680,9 +587,6 @@ def get_amm_active_count(swap_client, debug_override=False):
|
|||||||
swap_client.log.error(traceback.format_exc())
|
swap_client.log.error(traceback.format_exc())
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if debug_enabled:
|
|
||||||
swap_client.log.debug(f"Final AMM active count: {amm_count}")
|
|
||||||
|
|
||||||
return amm_count
|
return amm_count
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -711,17 +711,25 @@ def process_offers(args, config, script_state) -> None:
|
|||||||
print(f"Wallet data: {wallet_from}")
|
print(f"Wallet data: {wallet_from}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for offer in sent_offers:
|
created_offers = script_state.get("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(
|
template_offer_ids = set()
|
||||||
(x for x in prev_template_offers if x["offer_id"] == offer["offer_id"]),
|
for prev_offer in prev_template_offers:
|
||||||
None,
|
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
|
offers_found += 1
|
||||||
|
|
||||||
if wallet_balance <= float(offer_template["min_coin_from_amt"]):
|
if wallet_balance <= float(offer_template["min_coin_from_amt"]):
|
||||||
offer_id = offer["offer_id"]
|
|
||||||
print(
|
print(
|
||||||
"Revoking offer {}, wallet from balance below minimum".format(
|
"Revoking offer {}, wallet from balance below minimum".format(
|
||||||
offer_id
|
offer_id
|
||||||
@@ -732,6 +740,57 @@ def process_offers(args, config, script_state) -> None:
|
|||||||
print("revokeoffer", result)
|
print("revokeoffer", result)
|
||||||
else:
|
else:
|
||||||
print("Offer revoked successfully")
|
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:
|
if offers_found > 0:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -293,8 +293,14 @@ def test_swap_dir(driver):
|
|||||||
try:
|
try:
|
||||||
bid_rows = dict()
|
bid_rows = dict()
|
||||||
table = driver.find_element(By.XPATH, "//tbody[@id='active-swaps-body']")
|
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")
|
tds = row.find_elements(By.XPATH, ".//td")
|
||||||
|
if len(tds) < 6:
|
||||||
|
continue
|
||||||
td_details = tds[2]
|
td_details = tds[2]
|
||||||
td_send = tds[5]
|
td_send = tds[5]
|
||||||
td_recv = tds[3]
|
td_recv = tds[3]
|
||||||
@@ -336,8 +342,14 @@ def test_swap_dir(driver):
|
|||||||
try:
|
try:
|
||||||
bid_rows = dict()
|
bid_rows = dict()
|
||||||
table = driver.find_element(By.XPATH, "//tbody[@id='active-swaps-body']")
|
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")
|
tds = row.find_elements(By.XPATH, ".//td")
|
||||||
|
if len(tds) < 6:
|
||||||
|
continue
|
||||||
td_details = tds[2]
|
td_details = tds[2]
|
||||||
td_send = tds[5]
|
td_send = tds[5]
|
||||||
td_recv = tds[3]
|
td_recv = tds[3]
|
||||||
|
|||||||
Reference in New Issue
Block a user