mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-05 10:28:10 +01:00
GUI: Dynamic balances (WS) + Better Notifications (Toasts) + various fixes. (#332)
* GUI: Dynamic balances (WS) + various fixes. * BLACK + FLAKE8 * Clean-up. * Fix refresh intervals + Fix pending balance. * Fix amounts scientific notation (1e-8) * Better Notifications (Toasts) * Removed duplicated code + Balance skip if the chain is still syncing. * Fix MWEB doesnt show as pending + Various fixes. * Fix: USD values are off with part blind. * Fix: Percentage change buttons on wallet page. * Cleanup debug on wallet page. * Use ZMQ for part balances. * Fix ZMQ config. * Fix PART price in chart.
This commit is contained in:
@@ -186,6 +186,53 @@ def validOfferStateToReceiveBid(offer_state):
|
||||
return False
|
||||
|
||||
|
||||
def checkAndNotifyBalanceChange(
|
||||
swap_client, coin_type, ci, cc, new_height, trigger_source="block"
|
||||
):
|
||||
if not swap_client.ws_server:
|
||||
return
|
||||
|
||||
try:
|
||||
blockchain_info = ci.getBlockchainInfo()
|
||||
verification_progress = blockchain_info.get("verificationprogress", 1.0)
|
||||
if verification_progress < 0.99:
|
||||
return
|
||||
except Exception:
|
||||
return
|
||||
|
||||
try:
|
||||
current_balance = ci.getSpendableBalance()
|
||||
current_total_balance = swap_client.getTotalBalance(coin_type)
|
||||
cached_balance = cc.get("cached_balance", None)
|
||||
cached_total_balance = cc.get("cached_total_balance", None)
|
||||
|
||||
current_unconfirmed = current_total_balance - current_balance
|
||||
cached_unconfirmed = cc.get("cached_unconfirmed", None)
|
||||
|
||||
if (
|
||||
cached_balance is None
|
||||
or current_balance != cached_balance
|
||||
or cached_total_balance is None
|
||||
or current_total_balance != cached_total_balance
|
||||
or cached_unconfirmed is None
|
||||
or current_unconfirmed != cached_unconfirmed
|
||||
):
|
||||
cc["cached_balance"] = current_balance
|
||||
cc["cached_total_balance"] = current_total_balance
|
||||
cc["cached_unconfirmed"] = current_unconfirmed
|
||||
balance_event = {
|
||||
"event": "coin_balance_updated",
|
||||
"coin": ci.ticker(),
|
||||
"height": new_height,
|
||||
"trigger": trigger_source,
|
||||
}
|
||||
swap_client.ws_server.send_message_to_all(json.dumps(balance_event))
|
||||
except Exception:
|
||||
cc["cached_balance"] = None
|
||||
cc["cached_total_balance"] = None
|
||||
cc["cached_unconfirmed"] = None
|
||||
|
||||
|
||||
def threadPollXMRChainState(swap_client, coin_type):
|
||||
ci = swap_client.ci(coin_type)
|
||||
cc = swap_client.coin_clients[coin_type]
|
||||
@@ -198,6 +245,11 @@ def threadPollXMRChainState(swap_client, coin_type):
|
||||
)
|
||||
with swap_client.mxDB:
|
||||
cc["chain_height"] = new_height
|
||||
|
||||
checkAndNotifyBalanceChange(
|
||||
swap_client, coin_type, ci, cc, new_height, "block"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
swap_client.log.warning(
|
||||
f"threadPollXMRChainState {ci.ticker()}, error: {e}"
|
||||
@@ -219,6 +271,11 @@ def threadPollWOWChainState(swap_client, coin_type):
|
||||
)
|
||||
with swap_client.mxDB:
|
||||
cc["chain_height"] = new_height
|
||||
|
||||
checkAndNotifyBalanceChange(
|
||||
swap_client, coin_type, ci, cc, new_height, "block"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
swap_client.log.warning(
|
||||
f"threadPollWOWChainState {ci.ticker()}, error: {e}"
|
||||
@@ -231,6 +288,12 @@ def threadPollWOWChainState(swap_client, coin_type):
|
||||
def threadPollChainState(swap_client, coin_type):
|
||||
ci = swap_client.ci(coin_type)
|
||||
cc = swap_client.coin_clients[coin_type]
|
||||
|
||||
if coin_type == Coins.PART and swap_client._zmq_queue_enabled:
|
||||
poll_delay_range = (40, 60)
|
||||
else:
|
||||
poll_delay_range = (20, 30)
|
||||
|
||||
while not swap_client.chainstate_delay_event.is_set():
|
||||
try:
|
||||
chain_state = ci.getBlockchainInfo()
|
||||
@@ -244,11 +307,14 @@ def threadPollChainState(swap_client, coin_type):
|
||||
cc["chain_best_block"] = chain_state["bestblockhash"]
|
||||
if "mediantime" in chain_state:
|
||||
cc["chain_median_time"] = chain_state["mediantime"]
|
||||
|
||||
checkAndNotifyBalanceChange(
|
||||
swap_client, coin_type, ci, cc, new_height, "block"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
swap_client.log.warning(f"threadPollChainState {ci.ticker()}, error: {e}")
|
||||
swap_client.chainstate_delay_event.wait(
|
||||
random.randrange(20, 30)
|
||||
) # Random to stagger updates
|
||||
swap_client.chainstate_delay_event.wait(random.randrange(*poll_delay_range))
|
||||
|
||||
|
||||
class WatchedOutput: # Watch for spends
|
||||
@@ -498,6 +564,9 @@ class BasicSwap(BaseApp, UIApp):
|
||||
)
|
||||
self.zmqSubscriber.setsockopt_string(zmq.SUBSCRIBE, "smsg")
|
||||
|
||||
if Coins.PART in chainparams:
|
||||
self.zmqSubscriber.setsockopt_string(zmq.SUBSCRIBE, "hashwtx")
|
||||
|
||||
self.with_coins_override = extra_opts.get("with_coins", set())
|
||||
self.without_coins_override = extra_opts.get("without_coins", set())
|
||||
self._force_db_upgrade = extra_opts.get("force_db_upgrade", False)
|
||||
@@ -5410,6 +5479,39 @@ class BasicSwap(BaseApp, UIApp):
|
||||
|
||||
# bid saved in checkBidState
|
||||
|
||||
def getTotalBalance(self, coin_type) -> int:
|
||||
try:
|
||||
ci = self.ci(coin_type)
|
||||
if hasattr(ci, "rpc_wallet"):
|
||||
if coin_type in (Coins.XMR, Coins.WOW):
|
||||
balance_info = ci.rpc_wallet("get_balance")
|
||||
return balance_info["balance"]
|
||||
elif coin_type == Coins.PART:
|
||||
balances = ci.rpc_wallet("getbalances")
|
||||
return ci.make_int(
|
||||
balances["mine"]["trusted"]
|
||||
+ balances["mine"]["untrusted_pending"]
|
||||
)
|
||||
else:
|
||||
try:
|
||||
balances = ci.rpc_wallet("getbalances")
|
||||
return ci.make_int(
|
||||
balances["mine"]["trusted"]
|
||||
+ balances["mine"]["untrusted_pending"]
|
||||
)
|
||||
except Exception:
|
||||
wallet_info = ci.rpc_wallet("getwalletinfo")
|
||||
total = wallet_info.get("balance", 0)
|
||||
if "unconfirmed_balance" in wallet_info:
|
||||
total += wallet_info["unconfirmed_balance"]
|
||||
if "immature_balance" in wallet_info:
|
||||
total += wallet_info["immature_balance"]
|
||||
return ci.make_int(total)
|
||||
else:
|
||||
return ci.getSpendableBalance()
|
||||
except Exception:
|
||||
return ci.getSpendableBalance()
|
||||
|
||||
def getAddressBalance(self, coin_type, address: str) -> int:
|
||||
if self.coin_clients[coin_type]["chain_lookups"] == "explorer":
|
||||
explorers = self.coin_clients[coin_type]["explorers"]
|
||||
@@ -10399,6 +10501,29 @@ class BasicSwap(BaseApp, UIApp):
|
||||
|
||||
self.processMsg(msg)
|
||||
|
||||
def processZmqHashwtx(self) -> None:
|
||||
self.zmqSubscriber.recv()
|
||||
|
||||
try:
|
||||
if Coins.PART not in self.coin_clients:
|
||||
return
|
||||
|
||||
ci = self.ci(Coins.PART)
|
||||
cc = self.coin_clients[Coins.PART]
|
||||
|
||||
current_height = cc.get("chain_height", 0)
|
||||
|
||||
import time
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
checkAndNotifyBalanceChange(self, Coins.PART, ci, cc, current_height, "zmq")
|
||||
|
||||
except Exception as e:
|
||||
self.log.warning(f"Error processing PART wallet transaction: {e}")
|
||||
if self.debug:
|
||||
self.log.error(traceback.format_exc())
|
||||
|
||||
def expireBidsAndOffers(self, now) -> None:
|
||||
bids_to_expire = set()
|
||||
offers_to_expire = set()
|
||||
@@ -10530,6 +10655,8 @@ class BasicSwap(BaseApp, UIApp):
|
||||
message = self.zmqSubscriber.recv(flags=zmq.NOBLOCK)
|
||||
if message == b"smsg":
|
||||
self.processZmqSmsg()
|
||||
elif message == b"hashwtx":
|
||||
self.processZmqHashwtx()
|
||||
except zmq.Again as e: # noqa: F841
|
||||
pass
|
||||
except Exception as e:
|
||||
@@ -10831,6 +10958,76 @@ class BasicSwap(BaseApp, UIApp):
|
||||
settings_copy["enabled_chart_coins"] = new_value
|
||||
settings_changed = True
|
||||
|
||||
if "notifications_new_offers" in data:
|
||||
new_value = data["notifications_new_offers"]
|
||||
ensure(
|
||||
isinstance(new_value, bool),
|
||||
"New notifications_new_offers value not boolean",
|
||||
)
|
||||
if settings_copy.get("notifications_new_offers", False) != new_value:
|
||||
settings_copy["notifications_new_offers"] = new_value
|
||||
settings_changed = True
|
||||
|
||||
if "notifications_new_bids" in data:
|
||||
new_value = data["notifications_new_bids"]
|
||||
ensure(
|
||||
isinstance(new_value, bool),
|
||||
"New notifications_new_bids value not boolean",
|
||||
)
|
||||
if settings_copy.get("notifications_new_bids", True) != new_value:
|
||||
settings_copy["notifications_new_bids"] = new_value
|
||||
settings_changed = True
|
||||
|
||||
if "notifications_bid_accepted" in data:
|
||||
new_value = data["notifications_bid_accepted"]
|
||||
ensure(
|
||||
isinstance(new_value, bool),
|
||||
"New notifications_bid_accepted value not boolean",
|
||||
)
|
||||
if settings_copy.get("notifications_bid_accepted", True) != new_value:
|
||||
settings_copy["notifications_bid_accepted"] = new_value
|
||||
settings_changed = True
|
||||
|
||||
if "notifications_balance_changes" in data:
|
||||
new_value = data["notifications_balance_changes"]
|
||||
ensure(
|
||||
isinstance(new_value, bool),
|
||||
"New notifications_balance_changes value not boolean",
|
||||
)
|
||||
if (
|
||||
settings_copy.get("notifications_balance_changes", True)
|
||||
!= new_value
|
||||
):
|
||||
settings_copy["notifications_balance_changes"] = new_value
|
||||
settings_changed = True
|
||||
|
||||
if "notifications_outgoing_transactions" in data:
|
||||
new_value = data["notifications_outgoing_transactions"]
|
||||
ensure(
|
||||
isinstance(new_value, bool),
|
||||
"New notifications_outgoing_transactions value not boolean",
|
||||
)
|
||||
if (
|
||||
settings_copy.get("notifications_outgoing_transactions", True)
|
||||
!= new_value
|
||||
):
|
||||
settings_copy["notifications_outgoing_transactions"] = new_value
|
||||
settings_changed = True
|
||||
|
||||
if "notifications_duration" in data:
|
||||
new_value = data["notifications_duration"]
|
||||
ensure(
|
||||
isinstance(new_value, int),
|
||||
"New notifications_duration value not integer",
|
||||
)
|
||||
ensure(
|
||||
5 <= new_value <= 60,
|
||||
"notifications_duration must be between 5 and 60 seconds",
|
||||
)
|
||||
if settings_copy.get("notifications_duration", 20) != new_value:
|
||||
settings_copy["notifications_duration"] = new_value
|
||||
settings_changed = True
|
||||
|
||||
if settings_changed:
|
||||
settings_path = os.path.join(self.data_dir, cfg.CONFIG_FILENAME)
|
||||
settings_path_new = settings_path + ".new"
|
||||
|
||||
@@ -1382,6 +1382,11 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
|
||||
fp.write(
|
||||
"zmqpubsmsg=tcp://{}:{}\n".format(COINS_RPCBIND_IP, settings["zmqport"])
|
||||
)
|
||||
fp.write(
|
||||
"zmqpubhashwtx=tcp://{}:{}\n".format(
|
||||
COINS_RPCBIND_IP, settings["zmqport"]
|
||||
)
|
||||
)
|
||||
fp.write("spentindex=1\n")
|
||||
fp.write("txindex=1\n")
|
||||
fp.write("staking=0\n")
|
||||
|
||||
@@ -56,6 +56,42 @@ def signal_handler(sig, frame):
|
||||
swap_client.stopRunning()
|
||||
|
||||
|
||||
def checkPARTZmqConfigBeforeStart(part_settings, swap_settings):
|
||||
try:
|
||||
datadir = part_settings.get("datadir")
|
||||
if not datadir:
|
||||
return
|
||||
|
||||
config_path = os.path.join(datadir, "particl.conf")
|
||||
if not os.path.exists(config_path):
|
||||
return
|
||||
|
||||
with open(config_path, "r") as f:
|
||||
config_content = f.read()
|
||||
|
||||
zmq_host = swap_settings.get("zmqhost", "tcp://127.0.0.1")
|
||||
zmq_port = swap_settings.get("zmqport", 14792)
|
||||
expected_line = f"zmqpubhashwtx={zmq_host}:{zmq_port}"
|
||||
|
||||
if "zmqpubhashwtx=" not in config_content:
|
||||
with open(config_path, "a") as f:
|
||||
f.write(f"{expected_line}\n")
|
||||
elif expected_line not in config_content:
|
||||
lines = config_content.split("\n")
|
||||
updated_lines = []
|
||||
for line in lines:
|
||||
if line.startswith("zmqpubhashwtx="):
|
||||
updated_lines.append(expected_line)
|
||||
else:
|
||||
updated_lines.append(line)
|
||||
|
||||
with open(config_path, "w") as f:
|
||||
f.write("\n".join(updated_lines))
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking PART ZMQ config: {e}")
|
||||
|
||||
|
||||
def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
|
||||
daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
|
||||
datadir_path = os.path.expanduser(node_dir)
|
||||
@@ -548,6 +584,9 @@ def runClient(
|
||||
continue # /decred
|
||||
|
||||
if v["manage_daemon"] is True:
|
||||
if c == "particl" and swap_client._zmq_queue_enabled:
|
||||
checkPARTZmqConfigBeforeStart(v, swap_client.settings)
|
||||
|
||||
swap_client.log.info(f"Starting {display_name} daemon")
|
||||
|
||||
filename: str = getCoreBinName(coin_id, v, c + "d")
|
||||
|
||||
@@ -123,6 +123,145 @@ def js_coins(self, url_split, post_string, is_json) -> bytes:
|
||||
return bytes(json.dumps(coins), "UTF-8")
|
||||
|
||||
|
||||
def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
|
||||
try:
|
||||
|
||||
swap_client.updateWalletsInfo()
|
||||
wallets = swap_client.getCachedWalletsInfo()
|
||||
coins_with_balances = []
|
||||
|
||||
for k, v in swap_client.coin_clients.items():
|
||||
if k not in chainparams:
|
||||
continue
|
||||
if v["connection_type"] == "rpc":
|
||||
|
||||
balance = "0.0"
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "balance" in w and "error" not in w and "no_data" not in w:
|
||||
raw_balance = w["balance"]
|
||||
if isinstance(raw_balance, float):
|
||||
balance = f"{raw_balance:.8f}".rstrip("0").rstrip(".")
|
||||
elif isinstance(raw_balance, int):
|
||||
balance = str(raw_balance)
|
||||
else:
|
||||
balance = raw_balance
|
||||
|
||||
pending = "0.0"
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "error" not in w and "no_data" not in w:
|
||||
ci = swap_client.ci(k)
|
||||
pending_amount = 0
|
||||
if "unconfirmed" in w and float(w["unconfirmed"]) > 0.0:
|
||||
pending_amount += ci.make_int(w["unconfirmed"])
|
||||
if "immature" in w and float(w["immature"]) > 0.0:
|
||||
pending_amount += ci.make_int(w["immature"])
|
||||
if pending_amount > 0:
|
||||
pending = ci.format_amount(pending_amount)
|
||||
|
||||
coin_entry = {
|
||||
"id": int(k),
|
||||
"name": getCoinName(k),
|
||||
"balance": balance,
|
||||
"pending": pending,
|
||||
"ticker": chainparams[k]["ticker"],
|
||||
}
|
||||
|
||||
coins_with_balances.append(coin_entry)
|
||||
|
||||
if k == Coins.PART:
|
||||
variants = [
|
||||
{
|
||||
"coin": Coins.PART_ANON,
|
||||
"balance_field": "anon_balance",
|
||||
"pending_field": "anon_pending",
|
||||
},
|
||||
{
|
||||
"coin": Coins.PART_BLIND,
|
||||
"balance_field": "blind_balance",
|
||||
"pending_field": "blind_unconfirmed",
|
||||
},
|
||||
]
|
||||
|
||||
for variant_info in variants:
|
||||
variant_balance = "0.0"
|
||||
variant_pending = "0.0"
|
||||
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "error" not in w and "no_data" not in w:
|
||||
if variant_info["balance_field"] in w:
|
||||
raw_balance = w[variant_info["balance_field"]]
|
||||
if isinstance(raw_balance, float):
|
||||
variant_balance = f"{raw_balance:.8f}".rstrip(
|
||||
"0"
|
||||
).rstrip(".")
|
||||
elif isinstance(raw_balance, int):
|
||||
variant_balance = str(raw_balance)
|
||||
else:
|
||||
variant_balance = raw_balance
|
||||
|
||||
if (
|
||||
variant_info["pending_field"] in w
|
||||
and float(w[variant_info["pending_field"]]) > 0.0
|
||||
):
|
||||
variant_pending = str(
|
||||
w[variant_info["pending_field"]]
|
||||
)
|
||||
|
||||
variant_entry = {
|
||||
"id": int(variant_info["coin"]),
|
||||
"name": getCoinName(variant_info["coin"]),
|
||||
"balance": variant_balance,
|
||||
"pending": variant_pending,
|
||||
"ticker": chainparams[Coins.PART]["ticker"],
|
||||
}
|
||||
|
||||
coins_with_balances.append(variant_entry)
|
||||
|
||||
elif k == Coins.LTC:
|
||||
variant_balance = "0.0"
|
||||
variant_pending = "0.0"
|
||||
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "error" not in w and "no_data" not in w:
|
||||
if "mweb_balance" in w:
|
||||
variant_balance = w["mweb_balance"]
|
||||
|
||||
pending_amount = 0
|
||||
if (
|
||||
"mweb_unconfirmed" in w
|
||||
and float(w["mweb_unconfirmed"]) > 0.0
|
||||
):
|
||||
pending_amount += float(w["mweb_unconfirmed"])
|
||||
if "mweb_immature" in w and float(w["mweb_immature"]) > 0.0:
|
||||
pending_amount += float(w["mweb_immature"])
|
||||
if pending_amount > 0:
|
||||
variant_pending = f"{pending_amount:.8f}".rstrip(
|
||||
"0"
|
||||
).rstrip(".")
|
||||
|
||||
variant_entry = {
|
||||
"id": int(Coins.LTC_MWEB),
|
||||
"name": getCoinName(Coins.LTC_MWEB),
|
||||
"balance": variant_balance,
|
||||
"pending": variant_pending,
|
||||
"ticker": chainparams[Coins.LTC]["ticker"],
|
||||
}
|
||||
|
||||
coins_with_balances.append(variant_entry)
|
||||
|
||||
return bytes(json.dumps(coins_with_balances), "UTF-8")
|
||||
|
||||
except Exception as e:
|
||||
error_data = {"error": str(e)}
|
||||
return bytes(json.dumps(error_data), "UTF-8")
|
||||
|
||||
|
||||
def js_wallets(self, url_split, post_string, is_json):
|
||||
swap_client = self.server.swap_client
|
||||
swap_client.checkSystemStatus()
|
||||
@@ -1214,6 +1353,7 @@ def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
|
||||
|
||||
endpoints = {
|
||||
"coins": js_coins,
|
||||
"walletbalances": js_walletbalances,
|
||||
"wallets": js_wallets,
|
||||
"offers": js_offers,
|
||||
"sentoffers": js_sentoffers,
|
||||
|
||||
@@ -14,6 +14,62 @@
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Toast Notification Animations */
|
||||
.toast-slide-in {
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
.toast-slide-out {
|
||||
animation: slideOutRight 0.3s ease-in forwards;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutRight {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Toast Container Styles */
|
||||
#ul_updates {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
#ul_updates li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Toast Hover Effects */
|
||||
#ul_updates .bg-white:hover {
|
||||
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
transform: translateY(-1px);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.dark #ul_updates .dark\:bg-gray-800:hover {
|
||||
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-1px);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Table Styles */
|
||||
.padded_row td {
|
||||
padding-top: 1.5em;
|
||||
|
||||
@@ -23,13 +23,7 @@ const AmmTablesManager = (function() {
|
||||
}
|
||||
|
||||
function debugLog(message, data) {
|
||||
// if (isDebugEnabled()) {
|
||||
// if (data) {
|
||||
// console.log(`[AmmTables] ${message}`, data);
|
||||
// } else {
|
||||
// console.log(`[AmmTables] ${message}`);
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
function initializeTabs() {
|
||||
@@ -309,7 +303,10 @@ const AmmTablesManager = (function() {
|
||||
`;
|
||||
});
|
||||
|
||||
offersBody.innerHTML = tableHtml;
|
||||
|
||||
if (offersBody.innerHTML.trim() !== tableHtml.trim()) {
|
||||
offersBody.innerHTML = tableHtml;
|
||||
}
|
||||
}
|
||||
|
||||
function renderBidsTable(stateData) {
|
||||
@@ -441,7 +438,10 @@ const AmmTablesManager = (function() {
|
||||
`;
|
||||
});
|
||||
|
||||
bidsBody.innerHTML = tableHtml;
|
||||
|
||||
if (bidsBody.innerHTML.trim() !== tableHtml.trim()) {
|
||||
bidsBody.innerHTML = tableHtml;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
@@ -724,6 +724,429 @@ const AmmTablesManager = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function shouldDropdownOptionsShowBalance(select) {
|
||||
const isMakerDropdown = select.id.includes('coin-from');
|
||||
const isTakerDropdown = select.id.includes('coin-to');
|
||||
|
||||
|
||||
const addModal = document.getElementById('add-amm-modal');
|
||||
const editModal = document.getElementById('edit-amm-modal');
|
||||
const addModalVisible = addModal && !addModal.classList.contains('hidden');
|
||||
const editModalVisible = editModal && !editModal.classList.contains('hidden');
|
||||
|
||||
let isBidModal = false;
|
||||
if (addModalVisible) {
|
||||
const dataType = addModal.getAttribute('data-amm-type');
|
||||
if (dataType) {
|
||||
isBidModal = dataType === 'bid';
|
||||
} else {
|
||||
|
||||
const modalTitle = document.getElementById('add-modal-title');
|
||||
isBidModal = modalTitle && modalTitle.textContent && modalTitle.textContent.includes('Bid');
|
||||
}
|
||||
} else if (editModalVisible) {
|
||||
const dataType = editModal.getAttribute('data-amm-type');
|
||||
if (dataType) {
|
||||
isBidModal = dataType === 'bid';
|
||||
} else {
|
||||
|
||||
const modalTitle = document.getElementById('edit-modal-title');
|
||||
isBidModal = modalTitle && modalTitle.textContent && modalTitle.textContent.includes('Bid');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const result = isBidModal ? isTakerDropdown : isMakerDropdown;
|
||||
|
||||
console.log(`[DEBUG] shouldDropdownOptionsShowBalance: ${select.id}, isBidModal=${isBidModal}, isMaker=${isMakerDropdown}, isTaker=${isTakerDropdown}, result=${result}`);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function refreshDropdownOptions() {
|
||||
const dropdownIds = ['add-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-from', 'edit-amm-coin-to'];
|
||||
|
||||
dropdownIds.forEach(dropdownId => {
|
||||
const select = document.getElementById(dropdownId);
|
||||
if (!select || select.style.display !== 'none') return;
|
||||
|
||||
const wrapper = select.parentNode.querySelector('.relative');
|
||||
if (!wrapper) return;
|
||||
|
||||
|
||||
const dropdown = wrapper.querySelector('[role="listbox"]');
|
||||
if (!dropdown) return;
|
||||
|
||||
|
||||
const options = dropdown.querySelectorAll('[data-value]');
|
||||
options.forEach(optionElement => {
|
||||
const coinValue = optionElement.getAttribute('data-value');
|
||||
const originalOption = Array.from(select.options).find(opt => opt.value === coinValue);
|
||||
if (!originalOption) return;
|
||||
|
||||
|
||||
const textContainer = optionElement.querySelector('div.flex.flex-col, div.flex.items-center');
|
||||
if (!textContainer) return;
|
||||
|
||||
|
||||
textContainer.innerHTML = '';
|
||||
|
||||
const shouldShowBalance = shouldDropdownOptionsShowBalance(select);
|
||||
const fullText = originalOption.textContent.trim();
|
||||
const balance = originalOption.getAttribute('data-balance') || '0.00000000';
|
||||
|
||||
console.log(`[DEBUG] refreshDropdownOptions: ${select.id}, option=${coinValue}, shouldShowBalance=${shouldShowBalance}, balance=${balance}`);
|
||||
|
||||
if (shouldShowBalance) {
|
||||
|
||||
textContainer.className = 'flex flex-col';
|
||||
|
||||
const coinName = fullText.includes(' - Balance: ') ? fullText.split(' - Balance: ')[0] : fullText;
|
||||
|
||||
const coinNameSpan = document.createElement('span');
|
||||
coinNameSpan.textContent = coinName;
|
||||
coinNameSpan.className = 'text-gray-900 dark:text-white';
|
||||
|
||||
const balanceSpan = document.createElement('span');
|
||||
balanceSpan.textContent = `Balance: ${balance}`;
|
||||
balanceSpan.className = 'text-gray-500 dark:text-gray-400 text-xs';
|
||||
|
||||
textContainer.appendChild(coinNameSpan);
|
||||
textContainer.appendChild(balanceSpan);
|
||||
} else {
|
||||
|
||||
textContainer.className = 'flex items-center';
|
||||
|
||||
const coinNameSpan = document.createElement('span');
|
||||
const coinName = fullText.includes(' - Balance: ') ? fullText.split(' - Balance: ')[0] : fullText;
|
||||
coinNameSpan.textContent = coinName;
|
||||
coinNameSpan.className = 'text-gray-900 dark:text-white';
|
||||
|
||||
textContainer.appendChild(coinNameSpan);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function refreshDropdownBalances() {
|
||||
const dropdownIds = ['add-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-from', 'edit-amm-coin-to'];
|
||||
|
||||
dropdownIds.forEach(dropdownId => {
|
||||
const select = document.getElementById(dropdownId);
|
||||
if (!select || select.style.display !== 'none') return;
|
||||
|
||||
const wrapper = select.parentNode.querySelector('.relative');
|
||||
if (!wrapper) return;
|
||||
|
||||
|
||||
const dropdownItems = wrapper.querySelectorAll('[data-value]');
|
||||
dropdownItems.forEach(item => {
|
||||
const value = item.getAttribute('data-value');
|
||||
const option = select.querySelector(`option[value="${value}"]`);
|
||||
if (option) {
|
||||
const balance = option.getAttribute('data-balance') || '0.00000000';
|
||||
const pendingBalance = option.getAttribute('data-pending-balance') || '';
|
||||
|
||||
const balanceDiv = item.querySelector('.text-xs');
|
||||
if (balanceDiv) {
|
||||
balanceDiv.textContent = `Balance: ${balance}`;
|
||||
|
||||
|
||||
let pendingDiv = item.querySelector('.text-green-500');
|
||||
if (pendingBalance && parseFloat(pendingBalance) > 0) {
|
||||
if (!pendingDiv) {
|
||||
|
||||
pendingDiv = document.createElement('div');
|
||||
pendingDiv.className = 'text-green-500 text-xs';
|
||||
balanceDiv.parentNode.appendChild(pendingDiv);
|
||||
}
|
||||
pendingDiv.textContent = `+${pendingBalance} pending`;
|
||||
} else if (pendingDiv) {
|
||||
|
||||
pendingDiv.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
if (selectedOption) {
|
||||
const textContainer = wrapper.querySelector('button .flex-grow');
|
||||
const balanceDiv = textContainer ? textContainer.querySelector('.text-xs') : null;
|
||||
if (balanceDiv) {
|
||||
const balance = selectedOption.getAttribute('data-balance') || '0.00000000';
|
||||
const pendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
|
||||
|
||||
balanceDiv.textContent = `Balance: ${balance}`;
|
||||
|
||||
|
||||
let pendingDiv = textContainer.querySelector('.text-green-500');
|
||||
if (pendingBalance && parseFloat(pendingBalance) > 0) {
|
||||
if (!pendingDiv) {
|
||||
|
||||
pendingDiv = document.createElement('div');
|
||||
pendingDiv.className = 'text-green-500 text-xs';
|
||||
textContainer.appendChild(pendingDiv);
|
||||
}
|
||||
pendingDiv.textContent = `+${pendingBalance} pending`;
|
||||
} else if (pendingDiv) {
|
||||
|
||||
pendingDiv.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function refreshOfferDropdownBalanceDisplay() {
|
||||
refreshDropdownBalances();
|
||||
}
|
||||
|
||||
function refreshBidDropdownBalanceDisplay() {
|
||||
refreshDropdownBalances();
|
||||
}
|
||||
|
||||
function refreshDropdownBalanceDisplay(modalType = null) {
|
||||
if (modalType === 'offer') {
|
||||
refreshOfferDropdownBalanceDisplay();
|
||||
} else if (modalType === 'bid') {
|
||||
refreshBidDropdownBalanceDisplay();
|
||||
} else {
|
||||
|
||||
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 detectedType = null;
|
||||
if (addModalVisible) {
|
||||
detectedType = addModal.getAttribute('data-amm-type');
|
||||
} else if (editModalVisible) {
|
||||
detectedType = editModal.getAttribute('data-amm-type');
|
||||
}
|
||||
|
||||
if (detectedType === 'offer') {
|
||||
refreshOfferDropdownBalanceDisplay();
|
||||
} else if (detectedType === 'bid') {
|
||||
refreshBidDropdownBalanceDisplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateDropdownsForModalType(modalPrefix) {
|
||||
const coinFromSelect = document.getElementById(`${modalPrefix}-amm-coin-from`);
|
||||
const coinToSelect = document.getElementById(`${modalPrefix}-amm-coin-to`);
|
||||
|
||||
if (!coinFromSelect || !coinToSelect) return;
|
||||
|
||||
|
||||
const balanceData = {};
|
||||
|
||||
|
||||
Array.from(coinFromSelect.options).forEach(option => {
|
||||
const balance = option.getAttribute('data-balance');
|
||||
if (balance) {
|
||||
balanceData[option.value] = balance;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Array.from(coinToSelect.options).forEach(option => {
|
||||
const balance = option.getAttribute('data-balance');
|
||||
if (balance) {
|
||||
balanceData[option.value] = balance;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
updateDropdownOptions(coinFromSelect, balanceData);
|
||||
updateDropdownOptions(coinToSelect, balanceData);
|
||||
}
|
||||
|
||||
function updateDropdownOptions(select, balanceData, pendingData = {}) {
|
||||
Array.from(select.options).forEach(option => {
|
||||
const coinName = option.value;
|
||||
const balance = balanceData[coinName] || '0.00000000';
|
||||
const pending = pendingData[coinName] || '0.0';
|
||||
|
||||
|
||||
option.setAttribute('data-balance', balance);
|
||||
option.setAttribute('data-pending-balance', pending);
|
||||
|
||||
|
||||
option.textContent = coinName;
|
||||
});
|
||||
}
|
||||
|
||||
function createSimpleDropdown(select, showBalance = false) {
|
||||
if (!select) return;
|
||||
|
||||
|
||||
const existingWrapper = select.parentNode.querySelector('.relative');
|
||||
if (existingWrapper) {
|
||||
existingWrapper.remove();
|
||||
select.style.display = '';
|
||||
}
|
||||
|
||||
select.style.display = 'none';
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'relative';
|
||||
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'flex items-center justify-between w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:text-white';
|
||||
button.style.minHeight = '60px';
|
||||
|
||||
|
||||
const displayContent = document.createElement('div');
|
||||
displayContent.className = 'flex items-center';
|
||||
|
||||
const icon = document.createElement('img');
|
||||
icon.className = 'w-5 h-5 mr-2';
|
||||
icon.alt = '';
|
||||
|
||||
const textContainer = document.createElement('div');
|
||||
textContainer.className = 'flex-grow text-left';
|
||||
|
||||
const arrow = document.createElement('div');
|
||||
arrow.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>`;
|
||||
|
||||
displayContent.appendChild(icon);
|
||||
displayContent.appendChild(textContainer);
|
||||
button.appendChild(displayContent);
|
||||
button.appendChild(arrow);
|
||||
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden dark:bg-gray-700 dark:border-gray-600 max-h-60 overflow-y-auto';
|
||||
|
||||
|
||||
Array.from(select.options).forEach(option => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer';
|
||||
item.setAttribute('data-value', option.value);
|
||||
|
||||
const itemIcon = document.createElement('img');
|
||||
itemIcon.className = 'w-5 h-5 mr-2';
|
||||
itemIcon.src = `/static/images/coins/${getImageFilename(option.value)}`;
|
||||
itemIcon.alt = '';
|
||||
|
||||
const itemText = document.createElement('div');
|
||||
const coinName = option.textContent.trim();
|
||||
const balance = option.getAttribute('data-balance') || '0.00000000';
|
||||
const pendingBalance = option.getAttribute('data-pending-balance') || '';
|
||||
|
||||
if (showBalance) {
|
||||
itemText.className = 'flex flex-col';
|
||||
|
||||
let html = `
|
||||
<div class="text-gray-900 dark:text-white">${coinName}</div>
|
||||
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${balance}</div>
|
||||
`;
|
||||
|
||||
|
||||
if (pendingBalance && parseFloat(pendingBalance) > 0) {
|
||||
html += `<div class="text-green-500 text-xs">+${pendingBalance} pending</div>`;
|
||||
}
|
||||
|
||||
itemText.innerHTML = html;
|
||||
} else {
|
||||
itemText.className = 'text-gray-900 dark:text-white';
|
||||
itemText.textContent = coinName;
|
||||
}
|
||||
|
||||
item.appendChild(itemIcon);
|
||||
item.appendChild(itemText);
|
||||
|
||||
|
||||
item.addEventListener('click', function() {
|
||||
select.value = this.getAttribute('data-value');
|
||||
|
||||
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
const selectedCoinName = selectedOption.textContent.trim();
|
||||
const selectedBalance = selectedOption.getAttribute('data-balance') || '0.00000000';
|
||||
const selectedPendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
|
||||
|
||||
icon.src = itemIcon.src;
|
||||
|
||||
if (showBalance) {
|
||||
let html = `
|
||||
<div class="text-gray-900 dark:text-white">${selectedCoinName}</div>
|
||||
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
|
||||
`;
|
||||
|
||||
|
||||
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
|
||||
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
|
||||
}
|
||||
|
||||
textContainer.innerHTML = html;
|
||||
textContainer.className = 'flex-grow text-left flex flex-col justify-center';
|
||||
} else {
|
||||
textContainer.textContent = selectedCoinName;
|
||||
textContainer.className = 'flex-grow text-left';
|
||||
}
|
||||
|
||||
dropdown.classList.add('hidden');
|
||||
|
||||
|
||||
const event = new Event('change', { bubbles: true });
|
||||
select.dispatchEvent(event);
|
||||
});
|
||||
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
|
||||
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
if (selectedOption) {
|
||||
const selectedCoinName = selectedOption.textContent.trim();
|
||||
const selectedBalance = selectedOption.getAttribute('data-balance') || '0.00000000';
|
||||
const selectedPendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
|
||||
|
||||
icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`;
|
||||
|
||||
if (showBalance) {
|
||||
let html = `
|
||||
<div class="text-gray-900 dark:text-white">${selectedCoinName}</div>
|
||||
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
|
||||
`;
|
||||
|
||||
|
||||
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
|
||||
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
|
||||
}
|
||||
|
||||
textContainer.innerHTML = html;
|
||||
textContainer.className = 'flex-grow text-left flex flex-col justify-center';
|
||||
} else {
|
||||
textContainer.textContent = selectedCoinName;
|
||||
textContainer.className = 'flex-grow text-left';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
button.addEventListener('click', function() {
|
||||
dropdown.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!wrapper.contains(e.target)) {
|
||||
dropdown.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
wrapper.appendChild(button);
|
||||
wrapper.appendChild(dropdown);
|
||||
select.parentNode.insertBefore(wrapper, select);
|
||||
|
||||
}
|
||||
|
||||
function setupButtonHandlers() {
|
||||
const addOfferButton = document.getElementById('add-new-offer-btn');
|
||||
if (addOfferButton) {
|
||||
@@ -844,6 +1267,40 @@ const AmmTablesManager = (function() {
|
||||
modalTitle.textContent = `Add New ${type.charAt(0).toUpperCase() + type.slice(1)}`;
|
||||
}
|
||||
|
||||
|
||||
const modal = document.getElementById('add-amm-modal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
modal.setAttribute('data-amm-type', type);
|
||||
}
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
updateDropdownsForModalType('add');
|
||||
|
||||
initializeCustomSelects(type);
|
||||
|
||||
|
||||
refreshDropdownBalanceDisplay(type);
|
||||
|
||||
|
||||
if (typeof fetchBalanceData === 'function') {
|
||||
fetchBalanceData()
|
||||
.then(balanceData => {
|
||||
if (type === 'offer' && typeof updateOfferDropdownBalances === 'function') {
|
||||
updateOfferDropdownBalances(balanceData);
|
||||
} else if (type === 'bid' && typeof updateBidDropdownBalances === 'function') {
|
||||
updateBidDropdownBalances(balanceData);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error updating dropdown balances:', error);
|
||||
});
|
||||
}
|
||||
}, 50);
|
||||
|
||||
document.getElementById('add-amm-type').value = type;
|
||||
|
||||
document.getElementById('add-amm-name').value = 'Unnamed Offer';
|
||||
@@ -940,11 +1397,6 @@ const AmmTablesManager = (function() {
|
||||
if (type === 'offer') {
|
||||
setupBiddingControls('add');
|
||||
}
|
||||
|
||||
const modal = document.getElementById('add-amm-modal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeAddModal() {
|
||||
@@ -1269,6 +1721,40 @@ const AmmTablesManager = (function() {
|
||||
modalTitle.textContent = `Edit ${type.charAt(0).toUpperCase() + type.slice(1)}`;
|
||||
}
|
||||
|
||||
|
||||
const modal = document.getElementById('edit-amm-modal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
modal.setAttribute('data-amm-type', type);
|
||||
}
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
updateDropdownsForModalType('edit');
|
||||
|
||||
initializeCustomSelects(type);
|
||||
|
||||
|
||||
refreshDropdownBalanceDisplay(type);
|
||||
|
||||
|
||||
if (typeof fetchBalanceData === 'function') {
|
||||
fetchBalanceData()
|
||||
.then(balanceData => {
|
||||
if (type === 'offer' && typeof updateOfferDropdownBalances === 'function') {
|
||||
updateOfferDropdownBalances(balanceData);
|
||||
} else if (type === 'bid' && typeof updateBidDropdownBalances === 'function') {
|
||||
updateBidDropdownBalances(balanceData);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error updating dropdown balances:', error);
|
||||
});
|
||||
}
|
||||
}, 50);
|
||||
|
||||
document.getElementById('edit-amm-type').value = type;
|
||||
document.getElementById('edit-amm-id').value = id || '';
|
||||
document.getElementById('edit-amm-original-name').value = name;
|
||||
@@ -1282,8 +1768,12 @@ const AmmTablesManager = (function() {
|
||||
coinFromSelect.value = item.coin_from || '';
|
||||
coinToSelect.value = item.coin_to || '';
|
||||
|
||||
coinFromSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
coinToSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
if (coinFromSelect) {
|
||||
coinFromSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
if (coinToSelect) {
|
||||
coinToSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
document.getElementById('edit-amm-amount').value = item.amount || '';
|
||||
|
||||
@@ -1370,11 +1860,6 @@ const AmmTablesManager = (function() {
|
||||
setupBiddingControls('edit');
|
||||
populateBiddingControls('edit', item);
|
||||
}
|
||||
|
||||
const modal = document.getElementById('edit-amm-modal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Error processing the configuration: ${error.message}`);
|
||||
debugLog('Error opening edit modal:', error);
|
||||
@@ -1808,7 +2293,7 @@ const AmmTablesManager = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
function initializeCustomSelects() {
|
||||
function initializeCustomSelects(modalType = null) {
|
||||
const coinSelects = [
|
||||
document.getElementById('add-amm-coin-from'),
|
||||
document.getElementById('add-amm-coin-to'),
|
||||
@@ -1821,116 +2306,16 @@ const AmmTablesManager = (function() {
|
||||
document.getElementById('edit-offer-swap-type')
|
||||
];
|
||||
|
||||
function createCoinDropdown(select) {
|
||||
if (!select) return;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'relative';
|
||||
|
||||
const display = document.createElement('div');
|
||||
display.className = 'flex items-center w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white cursor-pointer';
|
||||
|
||||
const icon = document.createElement('img');
|
||||
icon.className = 'w-5 h-5 mr-2';
|
||||
icon.alt = '';
|
||||
|
||||
const text = document.createElement('span');
|
||||
text.className = 'flex-grow';
|
||||
|
||||
const arrow = document.createElement('span');
|
||||
arrow.className = 'ml-2';
|
||||
arrow.innerHTML = `
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
display.appendChild(icon);
|
||||
display.appendChild(text);
|
||||
display.appendChild(arrow);
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden dark:bg-gray-700 dark:border-gray-600 max-h-60 overflow-y-auto';
|
||||
|
||||
Array.from(select.options).forEach(option => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer text-gray-900 dark:text-white';
|
||||
item.setAttribute('data-value', option.value);
|
||||
item.setAttribute('data-symbol', option.getAttribute('data-symbol') || '');
|
||||
|
||||
const optionIcon = document.createElement('img');
|
||||
optionIcon.className = 'w-5 h-5 mr-2';
|
||||
optionIcon.src = `/static/images/coins/${getImageFilename(option.value)}`;
|
||||
optionIcon.alt = '';
|
||||
|
||||
const optionText = document.createElement('span');
|
||||
optionText.textContent = option.textContent.trim();
|
||||
|
||||
item.appendChild(optionIcon);
|
||||
item.appendChild(optionText);
|
||||
|
||||
item.addEventListener('click', function() {
|
||||
select.value = this.getAttribute('data-value');
|
||||
|
||||
text.textContent = optionText.textContent;
|
||||
icon.src = optionIcon.src;
|
||||
|
||||
dropdown.classList.add('hidden');
|
||||
|
||||
const event = new Event('change', { bubbles: true });
|
||||
select.dispatchEvent(event);
|
||||
|
||||
if (select.id === 'add-amm-coin-from' || select.id === 'add-amm-coin-to') {
|
||||
const coinFrom = document.getElementById('add-amm-coin-from');
|
||||
const coinTo = document.getElementById('add-amm-coin-to');
|
||||
const swapType = document.getElementById('add-offer-swap-type');
|
||||
|
||||
if (coinFrom && coinTo && swapType) {
|
||||
updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType);
|
||||
}
|
||||
} else if (select.id === 'edit-amm-coin-from' || select.id === 'edit-amm-coin-to') {
|
||||
const coinFrom = document.getElementById('edit-amm-coin-from');
|
||||
const coinTo = document.getElementById('edit-amm-coin-to');
|
||||
const swapType = document.getElementById('edit-offer-swap-type');
|
||||
|
||||
if (coinFrom && coinTo && swapType) {
|
||||
updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
text.textContent = selectedOption.textContent.trim();
|
||||
icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`;
|
||||
|
||||
display.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
dropdown.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
document.addEventListener('click', function() {
|
||||
dropdown.classList.add('hidden');
|
||||
});
|
||||
|
||||
wrapper.appendChild(display);
|
||||
wrapper.appendChild(dropdown);
|
||||
select.parentNode.insertBefore(wrapper, select);
|
||||
|
||||
select.style.display = 'none';
|
||||
|
||||
select.addEventListener('change', function() {
|
||||
const selectedOption = this.options[this.selectedIndex];
|
||||
text.textContent = selectedOption.textContent.trim();
|
||||
icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`;
|
||||
});
|
||||
}
|
||||
|
||||
function createSwapTypeDropdown(select) {
|
||||
if (!select) return;
|
||||
|
||||
|
||||
if (select.style.display === 'none' && select.parentNode.querySelector('.relative')) {
|
||||
return; // Custom dropdown already exists
|
||||
}
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'relative';
|
||||
|
||||
@@ -1980,7 +2365,9 @@ const AmmTablesManager = (function() {
|
||||
});
|
||||
|
||||
const selectedOption = select.options[select.selectedIndex];
|
||||
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
|
||||
if (selectedOption) {
|
||||
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
|
||||
}
|
||||
|
||||
display.addEventListener('click', function(e) {
|
||||
if (select.disabled) return;
|
||||
@@ -2000,7 +2387,9 @@ const AmmTablesManager = (function() {
|
||||
|
||||
select.addEventListener('change', function() {
|
||||
const selectedOption = this.options[this.selectedIndex];
|
||||
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
|
||||
if (selectedOption) {
|
||||
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
|
||||
}
|
||||
});
|
||||
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
@@ -2022,7 +2411,18 @@ const AmmTablesManager = (function() {
|
||||
}
|
||||
}
|
||||
|
||||
coinSelects.forEach(select => createCoinDropdown(select));
|
||||
coinSelects.forEach(select => {
|
||||
if (!select) return;
|
||||
|
||||
let showBalance = false;
|
||||
if (modalType === 'offer' && select.id.includes('coin-from')) {
|
||||
showBalance = true; // OFFER: maker shows balance
|
||||
} else if (modalType === 'bid' && select.id.includes('coin-to')) {
|
||||
showBalance = true; // BID: taker shows balance
|
||||
}
|
||||
|
||||
createSimpleDropdown(select, showBalance);
|
||||
});
|
||||
|
||||
swapTypeSelects.forEach(select => createSwapTypeDropdown(select));
|
||||
}
|
||||
@@ -2301,19 +2701,27 @@ const AmmTablesManager = (function() {
|
||||
|
||||
if (refreshButton) {
|
||||
refreshButton.addEventListener('click', async function() {
|
||||
|
||||
if (refreshButton.disabled) return;
|
||||
|
||||
const icon = refreshButton.querySelector('svg');
|
||||
refreshButton.disabled = true;
|
||||
|
||||
if (icon) {
|
||||
icon.classList.add('animate-spin');
|
||||
}
|
||||
|
||||
await initializePrices();
|
||||
updateTables();
|
||||
|
||||
setTimeout(() => {
|
||||
if (icon) {
|
||||
icon.classList.remove('animate-spin');
|
||||
}
|
||||
}, 1000);
|
||||
try {
|
||||
await initializePrices();
|
||||
updateTables();
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
if (icon) {
|
||||
icon.classList.remove('animate-spin');
|
||||
}
|
||||
refreshButton.disabled = false;
|
||||
}, 500); // Reduced from 1000ms to 500ms
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2326,7 +2734,11 @@ const AmmTablesManager = (function() {
|
||||
return {
|
||||
updateTables,
|
||||
startRefreshTimer,
|
||||
stopRefreshTimer
|
||||
stopRefreshTimer,
|
||||
refreshDropdownBalanceDisplay,
|
||||
refreshOfferDropdownBalanceDisplay,
|
||||
refreshBidDropdownBalanceDisplay,
|
||||
refreshDropdownOptions
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -367,16 +367,45 @@ const ApiManager = (function() {
|
||||
|
||||
const results = {};
|
||||
const fetchPromises = coinSymbols.map(async coin => {
|
||||
if (coin === 'WOW') {
|
||||
let useCoinGecko = false;
|
||||
let coingeckoId = null;
|
||||
|
||||
if (window.CoinManager) {
|
||||
const coinConfig = window.CoinManager.getCoinByAnyIdentifier(coin);
|
||||
if (coinConfig) {
|
||||
useCoinGecko = !coinConfig.usesCryptoCompare || coin === 'PART';
|
||||
coingeckoId = coinConfig.coingeckoId;
|
||||
}
|
||||
} else {
|
||||
const coinGeckoCoins = {
|
||||
'WOW': 'wownero',
|
||||
'PART': 'particl',
|
||||
'BTC': 'bitcoin'
|
||||
};
|
||||
if (coinGeckoCoins[coin]) {
|
||||
useCoinGecko = true;
|
||||
coingeckoId = coinGeckoCoins[coin];
|
||||
}
|
||||
}
|
||||
|
||||
if (useCoinGecko && coingeckoId) {
|
||||
return this.rateLimiter.queueRequest('coingecko', async () => {
|
||||
const url = `https://api.coingecko.com/api/v3/coins/wownero/market_chart?vs_currency=usd&days=1`;
|
||||
let days;
|
||||
if (resolution === 'day') {
|
||||
days = 1;
|
||||
} else if (resolution === 'year') {
|
||||
days = 365;
|
||||
} else {
|
||||
days = 180;
|
||||
}
|
||||
const url = `https://api.coingecko.com/api/v3/coins/${coingeckoId}/market_chart?vs_currency=usd&days=${days}`;
|
||||
try {
|
||||
const response = await this.makePostRequest(url);
|
||||
if (response && response.prices) {
|
||||
results[coin] = response.prices;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching CoinGecko data for WOW:`, error);
|
||||
console.error(`Error fetching CoinGecko data for ${coin}:`, error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
216
basicswap/static/js/modules/balance-updates.js
Normal file
216
basicswap/static/js/modules/balance-updates.js
Normal file
@@ -0,0 +1,216 @@
|
||||
const BalanceUpdatesManager = (function() {
|
||||
'use strict';
|
||||
|
||||
const config = {
|
||||
balanceUpdateDelay: 2000,
|
||||
swapEventDelay: 5000,
|
||||
periodicRefreshInterval: 120000,
|
||||
walletPeriodicRefreshInterval: 60000,
|
||||
};
|
||||
|
||||
const state = {
|
||||
handlers: new Map(),
|
||||
timeouts: new Map(),
|
||||
intervals: new Map(),
|
||||
initialized: false
|
||||
};
|
||||
|
||||
function fetchBalanceData() {
|
||||
return fetch('/json/walletbalances', {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(balanceData => {
|
||||
if (balanceData.error) {
|
||||
throw new Error(balanceData.error);
|
||||
}
|
||||
|
||||
if (!Array.isArray(balanceData)) {
|
||||
throw new Error('Invalid response format');
|
||||
}
|
||||
|
||||
return balanceData;
|
||||
});
|
||||
}
|
||||
|
||||
function clearTimeoutByKey(key) {
|
||||
if (state.timeouts.has(key)) {
|
||||
clearTimeout(state.timeouts.get(key));
|
||||
state.timeouts.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
function setTimeoutByKey(key, callback, delay) {
|
||||
clearTimeoutByKey(key);
|
||||
const timeoutId = setTimeout(callback, delay);
|
||||
state.timeouts.set(key, timeoutId);
|
||||
}
|
||||
|
||||
function clearIntervalByKey(key) {
|
||||
if (state.intervals.has(key)) {
|
||||
clearInterval(state.intervals.get(key));
|
||||
state.intervals.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
function setIntervalByKey(key, callback, interval) {
|
||||
clearIntervalByKey(key);
|
||||
const intervalId = setInterval(callback, interval);
|
||||
state.intervals.set(key, intervalId);
|
||||
}
|
||||
|
||||
function handleBalanceUpdate(contextKey, updateCallback, errorContext) {
|
||||
clearTimeoutByKey(`${contextKey}_balance_update`);
|
||||
setTimeoutByKey(`${contextKey}_balance_update`, () => {
|
||||
fetchBalanceData()
|
||||
.then(balanceData => {
|
||||
updateCallback(balanceData);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Error updating ${errorContext} balances via WebSocket:`, error);
|
||||
});
|
||||
}, config.balanceUpdateDelay);
|
||||
}
|
||||
|
||||
function handleSwapEvent(contextKey, updateCallback, errorContext) {
|
||||
clearTimeoutByKey(`${contextKey}_swap_event`);
|
||||
setTimeoutByKey(`${contextKey}_swap_event`, () => {
|
||||
fetchBalanceData()
|
||||
.then(balanceData => {
|
||||
updateCallback(balanceData);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Error updating ${errorContext} balances via swap event:`, error);
|
||||
});
|
||||
}, config.swapEventDelay);
|
||||
}
|
||||
|
||||
function setupWebSocketHandler(contextKey, balanceUpdateCallback, swapEventCallback, errorContext) {
|
||||
const handlerId = window.WebSocketManager.addMessageHandler('message', (data) => {
|
||||
if (data && data.event) {
|
||||
if (data.event === 'coin_balance_updated') {
|
||||
handleBalanceUpdate(contextKey, balanceUpdateCallback, errorContext);
|
||||
}
|
||||
|
||||
if (swapEventCallback) {
|
||||
const swapEvents = ['new_bid', 'bid_accepted', 'swap_completed'];
|
||||
if (swapEvents.includes(data.event)) {
|
||||
handleSwapEvent(contextKey, swapEventCallback, errorContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
state.handlers.set(contextKey, handlerId);
|
||||
return handlerId;
|
||||
}
|
||||
|
||||
function setupPeriodicRefresh(contextKey, updateCallback, errorContext, interval) {
|
||||
const refreshInterval = interval || config.periodicRefreshInterval;
|
||||
|
||||
setIntervalByKey(`${contextKey}_periodic`, () => {
|
||||
fetchBalanceData()
|
||||
.then(balanceData => {
|
||||
updateCallback(balanceData);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Error in periodic ${errorContext} balance refresh:`, error);
|
||||
});
|
||||
}, refreshInterval);
|
||||
}
|
||||
|
||||
function cleanup(contextKey) {
|
||||
if (state.handlers.has(contextKey)) {
|
||||
const handlerId = state.handlers.get(contextKey);
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.removeMessageHandler === 'function') {
|
||||
window.WebSocketManager.removeMessageHandler('message', handlerId);
|
||||
}
|
||||
state.handlers.delete(contextKey);
|
||||
}
|
||||
|
||||
clearTimeoutByKey(`${contextKey}_balance_update`);
|
||||
clearTimeoutByKey(`${contextKey}_swap_event`);
|
||||
|
||||
clearIntervalByKey(`${contextKey}_periodic`);
|
||||
}
|
||||
|
||||
function cleanupAll() {
|
||||
state.handlers.forEach((handlerId) => {
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.removeMessageHandler === 'function') {
|
||||
window.WebSocketManager.removeMessageHandler('message', handlerId);
|
||||
}
|
||||
});
|
||||
state.handlers.clear();
|
||||
|
||||
state.timeouts.forEach(timeoutId => clearTimeout(timeoutId));
|
||||
state.timeouts.clear();
|
||||
|
||||
state.intervals.forEach(intervalId => clearInterval(intervalId));
|
||||
state.intervals.clear();
|
||||
|
||||
state.initialized = false;
|
||||
}
|
||||
|
||||
return {
|
||||
initialize: function() {
|
||||
if (state.initialized) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('balanceUpdatesManager', this, (mgr) => mgr.dispose());
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', cleanupAll);
|
||||
|
||||
state.initialized = true;
|
||||
console.log('BalanceUpdatesManager initialized');
|
||||
return this;
|
||||
},
|
||||
|
||||
setup: function(options) {
|
||||
const {
|
||||
contextKey,
|
||||
balanceUpdateCallback,
|
||||
swapEventCallback,
|
||||
errorContext,
|
||||
enablePeriodicRefresh = false,
|
||||
periodicInterval
|
||||
} = options;
|
||||
|
||||
if (!contextKey || !balanceUpdateCallback || !errorContext) {
|
||||
throw new Error('Missing required options: contextKey, balanceUpdateCallback, errorContext');
|
||||
}
|
||||
|
||||
setupWebSocketHandler(contextKey, balanceUpdateCallback, swapEventCallback, errorContext);
|
||||
|
||||
if (enablePeriodicRefresh) {
|
||||
setupPeriodicRefresh(contextKey, balanceUpdateCallback, errorContext, periodicInterval);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
fetchBalanceData: fetchBalanceData,
|
||||
|
||||
cleanup: cleanup,
|
||||
|
||||
dispose: cleanupAll,
|
||||
|
||||
isInitialized: function() {
|
||||
return state.initialized;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.BalanceUpdatesManager = BalanceUpdatesManager;
|
||||
}
|
||||
@@ -1,11 +1,40 @@
|
||||
const NotificationManager = (function() {
|
||||
|
||||
const config = {
|
||||
|
||||
const defaultConfig = {
|
||||
showNewOffers: false,
|
||||
showNewBids: true,
|
||||
showBidAccepted: true
|
||||
showBidAccepted: true,
|
||||
showBalanceChanges: true,
|
||||
showOutgoingTransactions: true,
|
||||
notificationDuration: 20000
|
||||
};
|
||||
|
||||
|
||||
function loadConfig() {
|
||||
const saved = localStorage.getItem('notification_settings');
|
||||
if (saved) {
|
||||
try {
|
||||
return { ...defaultConfig, ...JSON.parse(saved) };
|
||||
} catch (e) {
|
||||
console.error('Error loading notification settings:', e);
|
||||
}
|
||||
}
|
||||
return { ...defaultConfig };
|
||||
}
|
||||
|
||||
|
||||
function saveConfig(newConfig) {
|
||||
try {
|
||||
localStorage.setItem('notification_settings', JSON.stringify(newConfig));
|
||||
Object.assign(config, newConfig);
|
||||
} catch (e) {
|
||||
console.error('Error saving notification settings:', e);
|
||||
}
|
||||
}
|
||||
|
||||
let config = loadConfig();
|
||||
|
||||
function ensureToastContainer() {
|
||||
let container = document.getElementById('ul_updates');
|
||||
if (!container) {
|
||||
@@ -19,13 +48,67 @@ const NotificationManager = (function() {
|
||||
return container;
|
||||
}
|
||||
|
||||
function getCoinIcon(coinSymbol) {
|
||||
if (window.CoinManager && typeof window.CoinManager.getCoinIcon === 'function') {
|
||||
return window.CoinManager.getCoinIcon(coinSymbol);
|
||||
}
|
||||
return 'default.png';
|
||||
}
|
||||
|
||||
function getToastIcon(type) {
|
||||
const icons = {
|
||||
'new_offer': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
||||
</svg>`,
|
||||
'new_bid': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path>
|
||||
</svg>`,
|
||||
'bid_accepted': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>`,
|
||||
'balance_change': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4zm2 6a2 2 0 012-2h8a2 2 0 012 2v4a2 2 0 01-2 2H8a2 2 0 01-2-2v-4zm6 4a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path>
|
||||
</svg>`,
|
||||
'success': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
|
||||
</svg>`
|
||||
};
|
||||
return icons[type] || icons['success'];
|
||||
}
|
||||
|
||||
function getToastColor(type, options = {}) {
|
||||
const colors = {
|
||||
'new_offer': 'bg-blue-500',
|
||||
'new_bid': 'bg-green-500',
|
||||
'bid_accepted': 'bg-purple-500',
|
||||
'balance_change': 'bg-yellow-500',
|
||||
'success': 'bg-blue-500'
|
||||
};
|
||||
|
||||
if (type === 'balance_change' && options.subtitle) {
|
||||
if (options.subtitle.includes('sent') || options.subtitle.includes('sending')) {
|
||||
return 'bg-red-500';
|
||||
} else {
|
||||
return 'bg-green-500';
|
||||
}
|
||||
}
|
||||
|
||||
return colors[type] || colors['success'];
|
||||
}
|
||||
|
||||
const publicAPI = {
|
||||
initialize: function(options = {}) {
|
||||
Object.assign(config, options);
|
||||
|
||||
|
||||
this.initializeBalanceTracking();
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('notificationManager', this, (mgr) => {
|
||||
|
||||
|
||||
if (this.balanceTimeouts) {
|
||||
Object.values(this.balanceTimeouts).forEach(timeout => clearTimeout(timeout));
|
||||
}
|
||||
console.log('NotificationManager disposed');
|
||||
});
|
||||
}
|
||||
@@ -33,33 +116,160 @@ const NotificationManager = (function() {
|
||||
return this;
|
||||
},
|
||||
|
||||
createToast: function(title, type = 'success') {
|
||||
updateSettings: function(newSettings) {
|
||||
saveConfig(newSettings);
|
||||
return this;
|
||||
},
|
||||
|
||||
getSettings: function() {
|
||||
return { ...config };
|
||||
},
|
||||
|
||||
testToasts: function() {
|
||||
if (!this.createToast) return;
|
||||
|
||||
setTimeout(() => {
|
||||
this.createToast(
|
||||
'+0.05000000 PART',
|
||||
'balance_change',
|
||||
{ coinSymbol: 'PART', subtitle: 'Incoming funds pending' }
|
||||
);
|
||||
}, 500);
|
||||
|
||||
setTimeout(() => {
|
||||
this.createToast(
|
||||
'+0.00123456 XMR',
|
||||
'balance_change',
|
||||
{ coinSymbol: 'XMR', subtitle: 'Incoming funds confirmed' }
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
setTimeout(() => {
|
||||
this.createToast(
|
||||
'-29.86277595 PART',
|
||||
'balance_change',
|
||||
{ coinSymbol: 'PART', subtitle: 'Funds sent' }
|
||||
);
|
||||
}, 1500);
|
||||
|
||||
setTimeout(() => {
|
||||
this.createToast(
|
||||
'-0.05000000 PART (Anon)',
|
||||
'balance_change',
|
||||
{ coinSymbol: 'PART', subtitle: 'Funds sending' }
|
||||
);
|
||||
}, 2000);
|
||||
|
||||
setTimeout(() => {
|
||||
this.createToast(
|
||||
'+1.23456789 PART (Anon)',
|
||||
'balance_change',
|
||||
{ coinSymbol: 'PART', subtitle: 'Incoming funds confirmed' }
|
||||
);
|
||||
}, 2500);
|
||||
|
||||
setTimeout(() => {
|
||||
this.createToast(
|
||||
'New network offer',
|
||||
'new_offer',
|
||||
{ offerId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66', subtitle: 'Click to view offer' }
|
||||
);
|
||||
}, 3000);
|
||||
|
||||
setTimeout(() => {
|
||||
this.createToast(
|
||||
'New bid received',
|
||||
'new_bid',
|
||||
{ bidId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66', subtitle: 'Click to view bid' }
|
||||
);
|
||||
}, 3500);
|
||||
},
|
||||
|
||||
|
||||
|
||||
initializeBalanceTracking: function() {
|
||||
|
||||
fetch('/json/walletbalances')
|
||||
.then(response => response.json())
|
||||
.then(balanceData => {
|
||||
if (Array.isArray(balanceData)) {
|
||||
balanceData.forEach(coin => {
|
||||
const balance = parseFloat(coin.balance) || 0;
|
||||
const pending = parseFloat(coin.pending) || 0;
|
||||
|
||||
const coinKey = coin.name.replace(/\s+/g, '_');
|
||||
const storageKey = `prev_balance_${coinKey}`;
|
||||
const pendingStorageKey = `prev_pending_${coinKey}`;
|
||||
|
||||
|
||||
if (!localStorage.getItem(storageKey)) {
|
||||
localStorage.setItem(storageKey, balance.toString());
|
||||
}
|
||||
if (!localStorage.getItem(pendingStorageKey)) {
|
||||
localStorage.setItem(pendingStorageKey, pending.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error initializing balance tracking:', error);
|
||||
});
|
||||
},
|
||||
|
||||
createToast: function(title, type = 'success', options = {}) {
|
||||
const messages = ensureToastContainer();
|
||||
const message = document.createElement('li');
|
||||
const toastId = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const iconColor = getToastColor(type, options);
|
||||
const icon = getToastIcon(type);
|
||||
|
||||
|
||||
let coinIconHtml = '';
|
||||
if (options.coinSymbol) {
|
||||
const coinIcon = getCoinIcon(options.coinSymbol);
|
||||
coinIconHtml = `<img src="/static/images/coins/${coinIcon}" class="w-5 h-5 mr-2" alt="${options.coinSymbol}" onerror="this.style.display='none'">`;
|
||||
}
|
||||
|
||||
|
||||
let clickAction = '';
|
||||
let cursorStyle = 'cursor-default';
|
||||
|
||||
if (options.offerId) {
|
||||
clickAction = `onclick="window.location.href='/offer/${options.offerId}'"`;
|
||||
cursorStyle = 'cursor-pointer';
|
||||
} else if (options.bidId) {
|
||||
clickAction = `onclick="window.location.href='/bid/${options.bidId}'"`;
|
||||
cursorStyle = 'cursor-pointer';
|
||||
} else if (options.coinSymbol) {
|
||||
clickAction = `onclick="window.location.href='/wallet/${options.coinSymbol}'"`;
|
||||
cursorStyle = 'cursor-pointer';
|
||||
}
|
||||
|
||||
message.innerHTML = `
|
||||
<div id="hide">
|
||||
<div id="toast-${type}" class="flex items-center p-4 mb-4 w-full max-w-xs text-gray-500
|
||||
bg-white rounded-lg shadow" role="alert">
|
||||
<div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10
|
||||
bg-blue-500 rounded-lg">
|
||||
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="18" width="18"
|
||||
viewBox="0 0 24 24">
|
||||
<g fill="#ffffff">
|
||||
<path d="M8.5,20a1.5,1.5,0,0,1-1.061-.439L.379,12.5,2.5,10.379l6,6,13-13L23.621,
|
||||
5.5,9.561,19.561A1.5,1.5,0,0,1,8.5,20Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="toast-slide-in">
|
||||
<div id="${toastId}" class="flex items-center p-4 mb-3 w-full max-w-sm text-gray-500
|
||||
bg-white dark:bg-gray-800 dark:text-gray-400 rounded-lg shadow-lg border border-gray-200
|
||||
dark:border-gray-700 ${cursorStyle} hover:shadow-xl transition-shadow" role="alert" ${clickAction}>
|
||||
<div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10
|
||||
${iconColor} rounded-lg text-white">
|
||||
${icon}
|
||||
</div>
|
||||
<div class="uppercase w-40 ml-3 text-sm font-semibold text-gray-900">${title}</div>
|
||||
<button type="button" onclick="closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5
|
||||
bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-0 focus:outline-none
|
||||
focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8">
|
||||
<div class="flex items-center ml-3 text-sm font-medium text-gray-900 dark:text-white">
|
||||
${coinIconHtml}
|
||||
<div class="flex flex-col">
|
||||
<span class="font-semibold">${title}</span>
|
||||
${options.subtitle ? `<span class="text-xs text-gray-500 dark:text-gray-400">${options.subtitle}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onclick="event.stopPropagation(); closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5
|
||||
bg-white dark:bg-gray-800 text-gray-400 hover:text-gray-900 dark:hover:text-white
|
||||
rounded-lg p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 inline-flex h-8 w-8 transition-colors
|
||||
focus:outline-none">
|
||||
<span class="sr-only">Close</span>
|
||||
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1
|
||||
1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293
|
||||
4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1
|
||||
1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293
|
||||
4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
@@ -67,34 +277,206 @@ const NotificationManager = (function() {
|
||||
</div>
|
||||
`;
|
||||
messages.appendChild(message);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
if (message.parentNode) {
|
||||
message.classList.add('toast-slide-out');
|
||||
setTimeout(() => {
|
||||
if (message.parentNode) {
|
||||
message.parentNode.removeChild(message);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, config.notificationDuration);
|
||||
},
|
||||
|
||||
handleWebSocketEvent: function(data) {
|
||||
if (!data || !data.event) return;
|
||||
let toastTitle;
|
||||
let toastTitle, toastType, toastOptions = {};
|
||||
let shouldShowToast = false;
|
||||
|
||||
switch (data.event) {
|
||||
case 'new_offer':
|
||||
toastTitle = `New network <a class="underline" href=/offer/${data.offer_id}>offer</a>`;
|
||||
toastTitle = `New network offer`;
|
||||
toastType = 'new_offer';
|
||||
toastOptions.offerId = data.offer_id;
|
||||
toastOptions.subtitle = 'Click to view offer';
|
||||
shouldShowToast = config.showNewOffers;
|
||||
break;
|
||||
case 'new_bid':
|
||||
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>New bid</a> on
|
||||
<a class="underline" href=/offer/${data.offer_id}>offer</a>`;
|
||||
toastTitle = `New bid received`;
|
||||
toastOptions.bidId = data.bid_id;
|
||||
toastOptions.subtitle = 'Click to view bid';
|
||||
toastType = 'new_bid';
|
||||
shouldShowToast = config.showNewBids;
|
||||
break;
|
||||
case 'bid_accepted':
|
||||
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>Bid</a> accepted`;
|
||||
toastTitle = `Bid accepted`;
|
||||
toastOptions.bidId = data.bid_id;
|
||||
toastOptions.subtitle = 'Click to view swap';
|
||||
toastType = 'bid_accepted';
|
||||
shouldShowToast = config.showBidAccepted;
|
||||
break;
|
||||
case 'coin_balance_updated':
|
||||
if (data.coin && config.showBalanceChanges) {
|
||||
this.handleBalanceUpdate(data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (toastTitle && shouldShowToast) {
|
||||
this.createToast(toastTitle);
|
||||
this.createToast(toastTitle, toastType, toastOptions);
|
||||
}
|
||||
},
|
||||
|
||||
handleBalanceUpdate: function(data) {
|
||||
if (!data.coin) return;
|
||||
|
||||
this.fetchAndShowBalanceChange(data.coin);
|
||||
const balanceKey = `balance_${data.coin}`;
|
||||
|
||||
if (this.balanceTimeouts && this.balanceTimeouts[balanceKey]) {
|
||||
clearTimeout(this.balanceTimeouts[balanceKey]);
|
||||
}
|
||||
|
||||
if (!this.balanceTimeouts) {
|
||||
this.balanceTimeouts = {};
|
||||
}
|
||||
|
||||
this.balanceTimeouts[balanceKey] = setTimeout(() => {
|
||||
this.fetchAndShowBalanceChange(data.coin);
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
fetchAndShowBalanceChange: function(coinSymbol) {
|
||||
|
||||
fetch('/json/walletbalances')
|
||||
.then(response => response.json())
|
||||
.then(balanceData => {
|
||||
if (Array.isArray(balanceData)) {
|
||||
|
||||
let coinsToCheck;
|
||||
if (coinSymbol === 'PART') {
|
||||
coinsToCheck = balanceData.filter(coin => coin.ticker === 'PART');
|
||||
} else if (coinSymbol === 'LTC') {
|
||||
coinsToCheck = balanceData.filter(coin => coin.ticker === 'LTC');
|
||||
} else {
|
||||
coinsToCheck = balanceData.filter(coin =>
|
||||
coin.ticker === coinSymbol ||
|
||||
coin.name.toLowerCase() === coinSymbol.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
coinsToCheck.forEach(coinData => {
|
||||
this.checkSingleCoinBalance(coinData, coinSymbol);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching balance for notification:', error);
|
||||
});
|
||||
},
|
||||
|
||||
checkSingleCoinBalance: function(coinData, originalCoinSymbol) {
|
||||
const currentBalance = parseFloat(coinData.balance) || 0;
|
||||
const currentPending = parseFloat(coinData.pending) || 0;
|
||||
|
||||
const coinKey = coinData.name.replace(/\s+/g, '_');
|
||||
const storageKey = `prev_balance_${coinKey}`;
|
||||
const pendingStorageKey = `prev_pending_${coinKey}`;
|
||||
const prevBalance = parseFloat(localStorage.getItem(storageKey)) || 0;
|
||||
const prevPending = parseFloat(localStorage.getItem(pendingStorageKey)) || 0;
|
||||
|
||||
|
||||
const balanceIncrease = currentBalance - prevBalance;
|
||||
const pendingIncrease = currentPending - prevPending;
|
||||
const pendingDecrease = prevPending - currentPending;
|
||||
|
||||
|
||||
const isPendingToConfirmed = pendingDecrease > 0.00000001 && balanceIncrease > 0.00000001;
|
||||
|
||||
|
||||
const displaySymbol = originalCoinSymbol;
|
||||
let variantInfo = '';
|
||||
|
||||
if (coinData.name !== 'Particl' && coinData.name.includes('Particl')) {
|
||||
|
||||
variantInfo = ` (${coinData.name.replace('Particl ', '')})`;
|
||||
} else if (coinData.name !== 'Litecoin' && coinData.name.includes('Litecoin')) {
|
||||
|
||||
variantInfo = ` (${coinData.name.replace('Litecoin ', '')})`;
|
||||
}
|
||||
|
||||
if (balanceIncrease > 0.00000001) {
|
||||
const displayAmount = balanceIncrease.toFixed(8).replace(/\.?0+$/, '');
|
||||
const subtitle = isPendingToConfirmed ? 'Funds confirmed' : 'Incoming funds confirmed';
|
||||
this.createToast(
|
||||
`+${displayAmount} ${displaySymbol}${variantInfo}`,
|
||||
'balance_change',
|
||||
{
|
||||
coinSymbol: originalCoinSymbol,
|
||||
subtitle: subtitle
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (balanceIncrease < -0.00000001 && config.showOutgoingTransactions) {
|
||||
const displayAmount = Math.abs(balanceIncrease).toFixed(8).replace(/\.?0+$/, '');
|
||||
this.createToast(
|
||||
`-${displayAmount} ${displaySymbol}${variantInfo}`,
|
||||
'balance_change',
|
||||
{
|
||||
coinSymbol: originalCoinSymbol,
|
||||
subtitle: 'Funds sent'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (pendingIncrease > 0.00000001) {
|
||||
const displayAmount = pendingIncrease.toFixed(8).replace(/\.?0+$/, '');
|
||||
this.createToast(
|
||||
`+${displayAmount} ${displaySymbol}${variantInfo}`,
|
||||
'balance_change',
|
||||
{
|
||||
coinSymbol: originalCoinSymbol,
|
||||
subtitle: 'Incoming funds pending'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (pendingIncrease < -0.00000001 && config.showOutgoingTransactions && !isPendingToConfirmed) {
|
||||
const displayAmount = Math.abs(pendingIncrease).toFixed(8).replace(/\.?0+$/, '');
|
||||
this.createToast(
|
||||
`-${displayAmount} ${displaySymbol}${variantInfo}`,
|
||||
'balance_change',
|
||||
{
|
||||
coinSymbol: originalCoinSymbol,
|
||||
subtitle: 'Funds sending'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (pendingDecrease > 0.00000001 && !isPendingToConfirmed) {
|
||||
const displayAmount = pendingDecrease.toFixed(8).replace(/\.?0+$/, '');
|
||||
this.createToast(
|
||||
`${displayAmount} ${displaySymbol}${variantInfo}`,
|
||||
'balance_change',
|
||||
{
|
||||
coinSymbol: originalCoinSymbol,
|
||||
subtitle: 'Pending funds confirmed'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
localStorage.setItem(storageKey, currentBalance.toString());
|
||||
localStorage.setItem(pendingStorageKey, currentPending.toString());
|
||||
},
|
||||
|
||||
|
||||
|
||||
updateConfig: function(newConfig) {
|
||||
Object.assign(config, newConfig);
|
||||
return this;
|
||||
@@ -122,5 +504,5 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
//console.log('NotificationManager initialized with methods:', Object.keys(NotificationManager));
|
||||
|
||||
console.log('NotificationManager initialized');
|
||||
|
||||
@@ -44,6 +44,7 @@ const PriceManager = (function() {
|
||||
|
||||
setTimeout(() => this.getPrices(), 1500);
|
||||
isInitialized = true;
|
||||
console.log('PriceManager initialized');
|
||||
return this;
|
||||
},
|
||||
|
||||
@@ -59,7 +60,7 @@ const PriceManager = (function() {
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
//console.log('PriceManager: Fetching latest prices.');
|
||||
|
||||
lastFetchTime = Date.now();
|
||||
fetchPromise = this.fetchPrices()
|
||||
.then(prices => {
|
||||
@@ -166,14 +167,14 @@ const PriceManager = (function() {
|
||||
|
||||
const cachedData = CacheManager.get(PRICES_CACHE_KEY);
|
||||
if (cachedData) {
|
||||
console.log('Using cached price data');
|
||||
|
||||
return cachedData.value;
|
||||
}
|
||||
|
||||
try {
|
||||
const existingCache = localStorage.getItem(PRICES_CACHE_KEY);
|
||||
if (existingCache) {
|
||||
console.log('Using localStorage cached price data');
|
||||
|
||||
return JSON.parse(existingCache).value;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -230,4 +231,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
console.log('PriceManager initialized');
|
||||
|
||||
|
||||
@@ -261,9 +261,12 @@ const SummaryManager = (function() {
|
||||
}
|
||||
|
||||
if (data.event) {
|
||||
publicAPI.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
const summaryEvents = ['new_offer', 'offer_revoked', 'new_bid', 'bid_accepted', 'swap_completed'];
|
||||
if (summaryEvents.includes(data.event)) {
|
||||
publicAPI.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
|
||||
window.NotificationManager.handleWebSocketEvent(data);
|
||||
@@ -334,9 +337,12 @@ const SummaryManager = (function() {
|
||||
|
||||
wsManager.addMessageHandler('message', (data) => {
|
||||
if (data.event) {
|
||||
this.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
const summaryEvents = ['new_offer', 'offer_revoked', 'new_bid', 'bid_accepted', 'swap_completed'];
|
||||
if (summaryEvents.includes(data.event)) {
|
||||
this.fetchSummaryData()
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
|
||||
window.NotificationManager.handleWebSocketEvent(data);
|
||||
|
||||
@@ -180,8 +180,29 @@ const WalletManager = (function() {
|
||||
|
||||
if (coinSymbol) {
|
||||
if (coinName === 'Particl') {
|
||||
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
|
||||
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
|
||||
let isBlind = false;
|
||||
let isAnon = false;
|
||||
|
||||
const flexContainer = el.closest('.flex');
|
||||
if (flexContainer) {
|
||||
const h4Element = flexContainer.querySelector('h4');
|
||||
if (h4Element) {
|
||||
isBlind = h4Element.textContent?.includes('Blind');
|
||||
isAnon = h4Element.textContent?.includes('Anon');
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBlind && !isAnon) {
|
||||
const parentRow = el.closest('tr');
|
||||
if (parentRow) {
|
||||
const labelCell = parentRow.querySelector('td:first-child');
|
||||
if (labelCell) {
|
||||
isBlind = labelCell.textContent?.includes('Blind');
|
||||
isAnon = labelCell.textContent?.includes('Anon');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
|
||||
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
|
||||
} else if (coinName === 'Litecoin') {
|
||||
@@ -248,8 +269,29 @@ const WalletManager = (function() {
|
||||
const usdValue = (amount * price).toFixed(2);
|
||||
|
||||
if (coinName === 'Particl') {
|
||||
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
|
||||
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
|
||||
let isBlind = false;
|
||||
let isAnon = false;
|
||||
|
||||
const flexContainer = el.closest('.flex');
|
||||
if (flexContainer) {
|
||||
const h4Element = flexContainer.querySelector('h4');
|
||||
if (h4Element) {
|
||||
isBlind = h4Element.textContent?.includes('Blind');
|
||||
isAnon = h4Element.textContent?.includes('Anon');
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBlind && !isAnon) {
|
||||
const parentRow = el.closest('tr');
|
||||
if (parentRow) {
|
||||
const labelCell = parentRow.querySelector('td:first-child');
|
||||
if (labelCell) {
|
||||
isBlind = labelCell.textContent?.includes('Blind');
|
||||
isAnon = labelCell.textContent?.includes('Anon');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
|
||||
localStorage.setItem(`particl-${balanceType}-last-value`, usdValue);
|
||||
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
|
||||
|
||||
@@ -443,7 +443,45 @@ const UIEnhancer = {
|
||||
|
||||
const selectNameElement = select.nextElementSibling?.querySelector('.select-name');
|
||||
if (selectNameElement) {
|
||||
selectNameElement.textContent = name;
|
||||
|
||||
if (select.id === 'coin_from' && name.includes(' - Balance: ')) {
|
||||
|
||||
const parts = name.split(' - Balance: ');
|
||||
const coinName = parts[0];
|
||||
const balanceInfo = parts[1] || '';
|
||||
|
||||
|
||||
selectNameElement.innerHTML = '';
|
||||
selectNameElement.style.display = 'flex';
|
||||
selectNameElement.style.flexDirection = 'column';
|
||||
selectNameElement.style.alignItems = 'flex-start';
|
||||
selectNameElement.style.lineHeight = '1.2';
|
||||
|
||||
|
||||
const coinNameDiv = document.createElement('div');
|
||||
coinNameDiv.textContent = coinName;
|
||||
coinNameDiv.style.fontWeight = 'normal';
|
||||
coinNameDiv.style.color = 'inherit';
|
||||
|
||||
|
||||
const balanceDiv = document.createElement('div');
|
||||
balanceDiv.textContent = `Balance: ${balanceInfo}`;
|
||||
balanceDiv.style.fontSize = '0.75rem';
|
||||
balanceDiv.style.color = '#6b7280';
|
||||
balanceDiv.style.marginTop = '1px';
|
||||
|
||||
selectNameElement.appendChild(coinNameDiv);
|
||||
selectNameElement.appendChild(balanceDiv);
|
||||
|
||||
|
||||
|
||||
} else {
|
||||
|
||||
selectNameElement.textContent = name;
|
||||
selectNameElement.style.display = 'block';
|
||||
selectNameElement.style.flexDirection = '';
|
||||
selectNameElement.style.alignItems = '';
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectCache(select);
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
});
|
||||
|
||||
window.bidsTabNavigationInitialized = true;
|
||||
console.log('Bids tab navigation initialized');
|
||||
//console.log('Bids tab navigation initialized');
|
||||
}
|
||||
|
||||
function handleInitialNavigation() {
|
||||
@@ -54,14 +54,14 @@
|
||||
const tabToActivate = localStorage.getItem('bidsTabToActivate');
|
||||
|
||||
if (tabToActivate) {
|
||||
//console.log('Activating tab from localStorage:', tabToActivate);
|
||||
|
||||
localStorage.removeItem('bidsTabToActivate');
|
||||
activateTabWithRetry('#' + tabToActivate);
|
||||
} else if (window.location.hash) {
|
||||
//console.log('Activating tab from hash:', window.location.hash);
|
||||
|
||||
activateTabWithRetry(window.location.hash);
|
||||
} else {
|
||||
//console.log('Activating default tab: #all');
|
||||
|
||||
activateTabWithRetry('#all');
|
||||
}
|
||||
}
|
||||
@@ -73,10 +73,10 @@
|
||||
|
||||
const hash = window.location.hash;
|
||||
if (hash) {
|
||||
//console.log('Hash changed, activating tab:', hash);
|
||||
|
||||
activateTabWithRetry(hash);
|
||||
} else {
|
||||
//console.log('Hash cleared, activating default tab: #all');
|
||||
|
||||
activateTabWithRetry('#all');
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@
|
||||
const normalizedTabId = tabId.startsWith('#') ? tabId : '#' + tabId;
|
||||
|
||||
if (normalizedTabId !== '#all' && normalizedTabId !== '#sent' && normalizedTabId !== '#received') {
|
||||
//console.log('Invalid tab ID, defaulting to #all');
|
||||
|
||||
activateTabWithRetry('#all');
|
||||
return;
|
||||
}
|
||||
@@ -96,17 +96,17 @@
|
||||
|
||||
if (!tabButton) {
|
||||
if (retryCount < 5) {
|
||||
//console.log('Tab button not found, retrying...', retryCount + 1);
|
||||
|
||||
setTimeout(() => {
|
||||
activateTabWithRetry(normalizedTabId, retryCount + 1);
|
||||
}, 100);
|
||||
} else {
|
||||
//console.error('Failed to find tab button after retries');
|
||||
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//console.log('Activating tab:', normalizedTabId);
|
||||
|
||||
|
||||
tabButton.click();
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
(tabId === '#sent' ? 'sent' : 'received');
|
||||
|
||||
if (typeof window.updateBidsTable === 'function') {
|
||||
//console.log('Triggering data load for', tabId);
|
||||
|
||||
window.updateBidsTable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,16 +51,11 @@
|
||||
Add Bid
|
||||
</button>
|
||||
{% endif %}
|
||||
<button id="refreshAmmTables" class="flex items-center px-4 py-2.5 bg-blue-600 hover:bg-blue-600 border-blue-500 font-medium text-sm text-white border rounded-md shadow-button focus:ring-0 focus:outline-none">
|
||||
<svg class="w-4 h-4 mr-2" 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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-b pb-5 border-gray-200 dark:border-gray-500">
|
||||
<div class="pb-5">
|
||||
<ul class="flex flex-wrap text-sm font-medium text-center text-gray-500 dark:text-gray-400" id="ammTabs" role="tablist">
|
||||
<li class="mr-2" role="presentation">
|
||||
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="offers-tab" data-tabs-target="#offers-content" type="button" role="tab" aria-controls="offers" aria-selected="true">
|
||||
@@ -290,6 +285,7 @@
|
||||
<button type="submit" name="kill_orphans" value="1" class="inline-flex items-center px-3 py-1.5 bg-red-500 hover:bg-red-600 text-white text-xs font-medium rounded-md shadow-sm focus:ring-0 focus:outline-none" title="Kill orphaned AMM processes">
|
||||
Kill Orphans
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||||
Use "Check Processes" to see running AMM processes. Use "Kill Orphans" to clean up duplicate processes. Use "Force Start" to automatically clean up and start fresh.
|
||||
@@ -974,7 +970,7 @@
|
||||
|
||||
if (localStorage.getItem('amm_create_default_refresh') === 'true') {
|
||||
localStorage.removeItem('amm_create_default_refresh');
|
||||
console.log('[AMM] Page loaded after create default config - redirecting to fresh page');
|
||||
|
||||
|
||||
setTimeout(function() {
|
||||
window.location.href = window.location.pathname + window.location.search;
|
||||
@@ -1066,7 +1062,7 @@
|
||||
<div class="absolute inset-0 bg-gray-500 opacity-75 dark:bg-gray-900 dark:opacity-90"></div>
|
||||
</div>
|
||||
|
||||
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle md:max-w-2xl md:w-full">
|
||||
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-visible shadow-xl transform transition-all sm:my-8 sm:align-middle md:max-w-2xl md:w-full">
|
||||
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mt-3 text-center sm:mt-0 sm:text-left w-full">
|
||||
@@ -1086,7 +1082,7 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div class="min-h-[80px]">
|
||||
<label for="add-amm-coin-from" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Maker</label>
|
||||
<div class="relative">
|
||||
<svg class="absolute top-1/2 right-3 transform -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -1094,15 +1090,14 @@
|
||||
</svg>
|
||||
<select id="add-amm-coin-from" 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 appearance-none">
|
||||
{% for c in coins %}
|
||||
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}">
|
||||
{{ c[1] }}
|
||||
</option>
|
||||
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}" data-balance="{{ c[2] }}">{{ c[1] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="min-h-[80px]">
|
||||
<label for="add-amm-coin-to" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Taker</label>
|
||||
<div class="relative">
|
||||
<svg class="absolute top-1/2 right-3 transform -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -1110,12 +1105,11 @@
|
||||
</svg>
|
||||
<select id="add-amm-coin-to" 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 appearance-none">
|
||||
{% for c in coins %}
|
||||
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}">
|
||||
{{ c[1] }}
|
||||
</option>
|
||||
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}" data-balance="{{ c[2] }}">{{ c[1] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1123,13 +1117,18 @@
|
||||
<div>
|
||||
<label for="add-amm-amount" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Offer Amount <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="add-amm-amount" pattern="[0-9]*\.?[0-9]*" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="0.0">
|
||||
<div class="mt-2 flex space-x-2">
|
||||
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(0.25, 'add-amm-amount')">25%</button>
|
||||
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(0.5, 'add-amm-amount')">50%</button>
|
||||
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(1, 'add-amm-amount')">100%</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label id="add-amm-rate-label" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Minimum Rate <span class="text-red-500">*</span></label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="text" id="add-amm-rate" 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">
|
||||
<button type="button" id="add-get-rate-button" class="px-2 py-1.5 bg-blue-500 hover:bg-blue-600 font-medium text-xs text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Get Rate</button>
|
||||
<input type="text" id="add-amm-rate" 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">
|
||||
<button type="button" id="add-get-rate-button" class="py-1 px-2 bg-blue-500 hover:bg-blue-600 text-white text-xs rounded-md focus:outline-none">Get Rate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1348,7 +1347,7 @@
|
||||
<div class="absolute inset-0 bg-gray-500 opacity-75 dark:bg-gray-900 dark:opacity-90"></div>
|
||||
</div>
|
||||
|
||||
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle lg:max-w-xl md:max-w-2xl md:w-full">
|
||||
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-visible shadow-xl transform transition-all sm:my-8 sm:align-middle lg:max-w-xl md:max-w-2xl md:w-full">
|
||||
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mt-3 text-center sm:mt-0 sm:text-left w-full">
|
||||
@@ -1370,7 +1369,7 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div class="min-h-[80px]">
|
||||
<label for="edit-amm-coin-from" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Maker</label>
|
||||
<div class="relative">
|
||||
<svg class="absolute top-1/2 right-3 transform -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -1378,15 +1377,14 @@
|
||||
</svg>
|
||||
<select id="edit-amm-coin-from" 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 appearance-none">
|
||||
{% for c in coins %}
|
||||
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}">
|
||||
{{ c[1] }}
|
||||
</option>
|
||||
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}" data-balance="{{ c[2] }}">{{ c[1] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="min-h-[80px]">
|
||||
<label for="edit-amm-coin-to" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Taker</label>
|
||||
<div class="relative">
|
||||
<svg class="absolute top-1/2 right-3 transform -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -1394,12 +1392,11 @@
|
||||
</svg>
|
||||
<select id="edit-amm-coin-to" 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 appearance-none">
|
||||
{% for c in coins %}
|
||||
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}">
|
||||
{{ c[1] }}
|
||||
</option>
|
||||
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}" data-balance="{{ c[2] }}">{{ c[1] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1407,13 +1404,18 @@
|
||||
<div>
|
||||
<label for="edit-amm-amount" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Offer Amount <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="edit-amm-amount" pattern="[0-9]*\.?[0-9]*" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="0.0">
|
||||
<div class="mt-2 flex space-x-2">
|
||||
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(0.25, 'edit-amm-amount')">25%</button>
|
||||
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(0.5, 'edit-amm-amount')">50%</button>
|
||||
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(1, 'edit-amm-amount')">100%</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label id="edit-amm-rate-label" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Minimum Rate <span class="text-red-500">*</span></label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="text" id="edit-amm-rate" 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">
|
||||
<button type="button" id="edit-get-rate-button" class="px-2 py-1.5 bg-blue-500 hover:bg-blue-600 font-medium text-xs text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Get Rate</button>
|
||||
<input type="text" id="edit-amm-rate" 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">
|
||||
<button type="button" id="edit-get-rate-button" class="py-1 px-2 bg-blue-500 hover:bg-blue-600 text-white text-xs rounded-md focus:outline-none">Get Rate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2033,6 +2035,187 @@
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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' %}
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
<script src="/static/js/modules/price-manager.js"></script>
|
||||
<script src="/static/js/modules/tooltips-manager.js"></script>
|
||||
<script src="/static/js/modules/notification-manager.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/summary-manager.js"></script>
|
||||
<script src="/static/js/amm_counter.js"></script>
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
<select class="select hover:border-blue-500 pl-10 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 bold focus:ring-0" id="coin_from" name="coin_from" onchange="set_rate('coin_from');">
|
||||
<option value="-1">Select coin you send</option>
|
||||
{% for c in coins_from %}
|
||||
<option{% if data.coin_from==c[0] %} selected{% endif %} value="{{ c[0] }}" data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}-20.png">{{ c[1] }}</option>
|
||||
<option{% if data.coin_from==c[0] %} selected{% endif %} value="{{ c[0] }}" data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}-20.png" data-balance="{{ c[2] }}">{{ c[1] }} - Balance: {{ c[2] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="select-dropdown"> <img class="select-icon" src="" alt=""> <img class="select-image" src="" alt=""> </div>
|
||||
@@ -167,6 +167,11 @@
|
||||
<div class="relative">
|
||||
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none"> </div> <input class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-5 focus:ring-0 bold" placeholder="0" type="text" id="amt_from" name="amt_from" value="{{ data.amt_from }}" onchange="set_rate('amt_from');">
|
||||
</div>
|
||||
<div class="mt-2 flex space-x-2">
|
||||
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setOfferAmount(0.25, 'amt_from')">25%</button>
|
||||
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setOfferAmount(0.5, 'amt_from')">50%</button>
|
||||
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setOfferAmount(1, 'amt_from')">100%</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -332,6 +337,142 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function setOfferAmount(percent, fieldId) {
|
||||
const amountInput = document.getElementById(fieldId);
|
||||
const coinFromSelect = document.getElementById('coin_from');
|
||||
|
||||
if (!amountInput || !coinFromSelect) {
|
||||
console.error('Required elements not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOption = coinFromSelect.options[coinFromSelect.selectedIndex];
|
||||
if (!selectedOption || selectedOption.value === '-1') {
|
||||
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);
|
||||
|
||||
|
||||
if (amountInput.onchange) {
|
||||
amountInput.onchange();
|
||||
}
|
||||
|
||||
|
||||
if (typeof set_rate === 'function') {
|
||||
set_rate(fieldId);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCustomDropdownDisplay(select, coinName, balance, pending) {
|
||||
const selectNameElement = select.nextElementSibling?.querySelector('.select-name');
|
||||
if (!selectNameElement) return;
|
||||
|
||||
|
||||
selectNameElement.innerHTML = '';
|
||||
selectNameElement.style.display = 'flex';
|
||||
selectNameElement.style.flexDirection = 'column';
|
||||
selectNameElement.style.alignItems = 'flex-start';
|
||||
selectNameElement.style.lineHeight = '1.2';
|
||||
|
||||
|
||||
const coinNameDiv = document.createElement('div');
|
||||
coinNameDiv.textContent = coinName;
|
||||
coinNameDiv.style.fontWeight = 'normal';
|
||||
coinNameDiv.style.color = 'inherit';
|
||||
|
||||
|
||||
const balanceDiv = document.createElement('div');
|
||||
balanceDiv.textContent = `Balance: ${balance}`;
|
||||
balanceDiv.style.fontSize = '0.75rem';
|
||||
balanceDiv.style.color = '#6b7280';
|
||||
balanceDiv.style.marginTop = '1px';
|
||||
|
||||
selectNameElement.appendChild(coinNameDiv);
|
||||
selectNameElement.appendChild(balanceDiv);
|
||||
|
||||
|
||||
if (pending !== '0.0' && parseFloat(pending) > 0) {
|
||||
const pendingDiv = document.createElement('div');
|
||||
pendingDiv.textContent = `+${pending} pending`;
|
||||
pendingDiv.style.fontSize = '0.75rem';
|
||||
pendingDiv.style.color = '#10b981';
|
||||
pendingDiv.style.marginTop = '1px';
|
||||
selectNameElement.appendChild(pendingDiv);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCoinDropdownBalances(balanceData) {
|
||||
|
||||
const coinFromSelect = document.getElementById('coin_from');
|
||||
if (!coinFromSelect) return;
|
||||
|
||||
|
||||
const balanceMap = {};
|
||||
const pendingMap = {};
|
||||
balanceData.forEach(coin => {
|
||||
balanceMap[coin.id] = coin.balance;
|
||||
pendingMap[coin.id] = coin.pending || '0.0';
|
||||
});
|
||||
|
||||
|
||||
Array.from(coinFromSelect.options).forEach(option => {
|
||||
if (option.value === '-1') return;
|
||||
|
||||
const coinId = parseInt(option.value);
|
||||
const balance = balanceMap[coinId] || '0.0';
|
||||
const pending = pendingMap[coinId] || '0.0';
|
||||
|
||||
|
||||
option.setAttribute('data-balance', balance);
|
||||
|
||||
|
||||
const coinName = option.textContent.split(' - Balance:')[0];
|
||||
if (pending !== '0.0' && parseFloat(pending) > 0) {
|
||||
option.textContent = `${coinName} - Balance: ${balance} (+${pending} pending)`;
|
||||
} else {
|
||||
option.textContent = `${coinName} - Balance: ${balance}`;
|
||||
}
|
||||
|
||||
|
||||
const select = option.closest('select');
|
||||
if (select && select.value === option.value) {
|
||||
updateCustomDropdownDisplay(select, coinName, balance, pending);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
window.BalanceUpdatesManager.initialize();
|
||||
|
||||
window.BalanceUpdatesManager.setup({
|
||||
contextKey: 'offer',
|
||||
balanceUpdateCallback: updateCoinDropdownBalances,
|
||||
swapEventCallback: updateCoinDropdownBalances,
|
||||
errorContext: 'New Offer',
|
||||
enablePeriodicRefresh: true,
|
||||
periodicInterval: 120000
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<script src="static/js/new_offer.js"></script>
|
||||
{% include 'footer.html' %}
|
||||
</div>
|
||||
|
||||
@@ -49,6 +49,9 @@
|
||||
<button class="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" data-tab="general" id="general-tab">
|
||||
General
|
||||
</button>
|
||||
<button class="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" data-tab="notifications" id="notifications-tab">
|
||||
Notifications
|
||||
</button>
|
||||
<button class="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" data-tab="tor" id="tor-tab">
|
||||
Tor
|
||||
</button>
|
||||
@@ -432,6 +435,153 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="tab-content hidden" id="notifications" role="tabpanel" aria-labelledby="notifications-tab">
|
||||
<form method="post">
|
||||
<div class="space-y-6">
|
||||
|
||||
<div class="bg-coolGray-100 dark:bg-gray-500 rounded-xl">
|
||||
<div class="px-6 py-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Notification Settings
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
Configure which notifications you want to see.
|
||||
</p>
|
||||
</div>
|
||||
<div class="px-6 pb-6 space-y-6">
|
||||
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">Notification Types</h4>
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
||||
<div class="space-y-4">
|
||||
|
||||
<div class="py-2">
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="notifications_new_offers" name="notifications_new_offers" 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 notification_settings.notifications_new_offers %} checked{% endif %}>
|
||||
<label for="notifications_new_offers" class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-300">New Offers</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 ml-7 mt-1">Show notifications for new network offers</p>
|
||||
</div>
|
||||
|
||||
<div class="py-2">
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="notifications_new_bids" name="notifications_new_bids" 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 notification_settings.notifications_new_bids %} checked{% endif %}>
|
||||
<label for="notifications_new_bids" class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-300">New Bids</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 ml-7 mt-1">Show notifications for new bids on your offers</p>
|
||||
</div>
|
||||
|
||||
<div class="py-2">
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="notifications_bid_accepted" name="notifications_bid_accepted" 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 notification_settings.notifications_bid_accepted %} checked{% endif %}>
|
||||
<label for="notifications_bid_accepted" class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-300">Bid Accepted</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 ml-7 mt-1">Show notifications when your bids are accepted</p>
|
||||
</div>
|
||||
|
||||
<div class="py-2">
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="notifications_balance_changes" name="notifications_balance_changes" 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 notification_settings.notifications_balance_changes %} checked{% endif %}>
|
||||
<label for="notifications_balance_changes" class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-300">Balance Changes</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 ml-7 mt-1">Show notifications for incoming and outgoing funds</p>
|
||||
</div>
|
||||
|
||||
<div class="py-2">
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="notifications_outgoing_transactions" name="notifications_outgoing_transactions" 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 notification_settings.notifications_outgoing_transactions %} checked{% endif %}>
|
||||
<label for="notifications_outgoing_transactions" class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-300">Outgoing Transactions</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 ml-7 mt-1">Show notifications for sent funds</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">Display Duration</h4>
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
||||
<div>
|
||||
<label for="notifications_duration" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Duration</label>
|
||||
<div class="relative w-48">
|
||||
<select id="notifications_duration" name="notifications_duration" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-700 dark:text-white border border-gray-300 dark:border-gray-600 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-1 focus:outline-none">
|
||||
<option value="5"{% if notification_settings.notifications_duration == 5 %} selected{% endif %}>5 seconds</option>
|
||||
<option value="10"{% if notification_settings.notifications_duration == 10 %} selected{% endif %}>10 seconds</option>
|
||||
<option value="15"{% if notification_settings.notifications_duration == 15 %} selected{% endif %}>15 seconds</option>
|
||||
<option value="20"{% if notification_settings.notifications_duration == 20 %} selected{% endif %}>20 seconds</option>
|
||||
<option value="30"{% if notification_settings.notifications_duration == 30 %} selected{% endif %}>30 seconds</option>
|
||||
<option value="60"{% if notification_settings.notifications_duration == 60 %} selected{% endif %}>1 minute</option>
|
||||
</select>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
|
||||
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if general_settings.debug_ui %}
|
||||
<div>
|
||||
<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>
|
||||
<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">
|
||||
Test All Notification Types
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex justify-end mt-6">
|
||||
<button name="apply_notifications" value="Apply" type="submit" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors focus:outline-none">
|
||||
Apply Changes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<input type="hidden" name="formid" value="{{ form_id }}">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function syncNotificationSettings() {
|
||||
if (window.NotificationManager && typeof window.NotificationManager.updateSettings === 'function') {
|
||||
|
||||
const backendSettings = {
|
||||
showNewOffers: document.getElementById('notifications_new_offers').checked,
|
||||
showNewBids: document.getElementById('notifications_new_bids').checked,
|
||||
showBidAccepted: document.getElementById('notifications_bid_accepted').checked,
|
||||
showBalanceChanges: document.getElementById('notifications_balance_changes').checked,
|
||||
showOutgoingTransactions: document.getElementById('notifications_outgoing_transactions').checked,
|
||||
notificationDuration: parseInt(document.getElementById('notifications_duration').value) * 1000
|
||||
};
|
||||
|
||||
window.NotificationManager.updateSettings(backendSettings);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('notifications-tab').addEventListener('click', function() {
|
||||
setTimeout(syncNotificationSettings, 100);
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
syncNotificationSettings();
|
||||
});
|
||||
|
||||
document.addEventListener('change', function(e) {
|
||||
if (e.target.closest('#notifications')) {
|
||||
syncNotificationSettings();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="tab-content hidden" id="tor" role="tabpanel" aria-labelledby="tor-tab">
|
||||
<form method="post" id="tor-form">
|
||||
<div class="space-y-6">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<span class="inline-block align-middle">
|
||||
<img class="mr-2 h-16" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"></span>({{ w.ticker }}) {{ w.name }} Wallet </h2>
|
||||
</div>
|
||||
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto"> <a class="rounded-full mr-5 flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-lg lg:text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/wallet/{{ w.ticker }}"> {{ circular_arrows_svg | safe }}<span>Refresh</span> </a> </div>
|
||||
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -667,16 +667,7 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
var floatBalance;
|
||||
var calculatedAmount;
|
||||
|
||||
console.log('SetAmount Called with:', {
|
||||
percent: percent,
|
||||
balance: balance,
|
||||
cid: cid,
|
||||
blindBalance: blindBalance,
|
||||
anonBalance: anonBalance,
|
||||
selectedType: selectedType,
|
||||
blindBalanceType: typeof blindBalance,
|
||||
blindBalanceNumeric: Number(blindBalance)
|
||||
});
|
||||
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
const numValue = Number(value);
|
||||
@@ -706,11 +697,7 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
|
||||
calculatedAmount = Math.max(0, Math.floor(floatBalance * percent * 100000000) / 100000000);
|
||||
|
||||
console.log('Calculated Amount:', {
|
||||
floatBalance: floatBalance,
|
||||
calculatedAmount: calculatedAmount,
|
||||
percent: percent
|
||||
});
|
||||
|
||||
|
||||
if (percent === 1) {
|
||||
calculatedAmount = floatBalance;
|
||||
@@ -727,43 +714,6 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
if (subfeeCheckbox) {
|
||||
subfeeCheckbox.checked = (percent === 1);
|
||||
}
|
||||
|
||||
console.log('Final Amount Set:', amountInput.value);
|
||||
}function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
var amountInput = document.getElementById('amount');
|
||||
var typeSelect = document.getElementById('withdraw_type');
|
||||
var selectedType = typeSelect.value;
|
||||
var floatBalance;
|
||||
var calculatedAmount;
|
||||
|
||||
switch(selectedType) {
|
||||
case 'plain':
|
||||
floatBalance = parseFloat(balance);
|
||||
break;
|
||||
case 'blind':
|
||||
floatBalance = parseFloat(blindBalance);
|
||||
break;
|
||||
case 'anon':
|
||||
floatBalance = parseFloat(anonBalance);
|
||||
break;
|
||||
default:
|
||||
floatBalance = parseFloat(balance);
|
||||
break;
|
||||
}
|
||||
|
||||
calculatedAmount = Math.floor(floatBalance * percent * 100000000) / 100000000;
|
||||
|
||||
if (percent === 1) {
|
||||
calculatedAmount = floatBalance;
|
||||
}
|
||||
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
|
||||
var subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
|
||||
if (subfeeCheckbox) {
|
||||
subfeeCheckbox.checked = (percent === 1);
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -818,17 +768,14 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
|
||||
const specialCids = [6, 9];
|
||||
|
||||
console.log("CID:", cid);
|
||||
console.log("Percent:", percent);
|
||||
console.log("Balance:", balance);
|
||||
console.log("Calculated Amount:", calculatedAmount);
|
||||
|
||||
|
||||
if (specialCids.includes(parseInt(cid)) && percent === 1) {
|
||||
amountInput.setAttribute('data-hidden', 'true');
|
||||
amountInput.placeholder = 'Sweep All';
|
||||
amountInput.value = '';
|
||||
amountInput.disabled = true;
|
||||
console.log("Sweep All activated for special CID:", cid);
|
||||
|
||||
} else {
|
||||
amountInput.value = calculatedAmount.toFixed(8);
|
||||
amountInput.setAttribute('data-hidden', 'false');
|
||||
@@ -840,17 +787,15 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
|
||||
if (sweepAllCheckbox) {
|
||||
if (specialCids.includes(parseInt(cid)) && percent === 1) {
|
||||
sweepAllCheckbox.checked = true;
|
||||
console.log("Sweep All checkbox checked");
|
||||
} else {
|
||||
sweepAllCheckbox.checked = false;
|
||||
console.log("Sweep All checkbox unchecked");
|
||||
}
|
||||
}
|
||||
|
||||
let subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
|
||||
if (subfeeCheckbox) {
|
||||
subfeeCheckbox.checked = (percent === 1);
|
||||
console.log("Subfee checkbox status for CID", cid, ":", subfeeCheckbox.checked);
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1148,6 +1093,375 @@ function confirmUTXOResize() {
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') {
|
||||
window.WebSocketManager.initialize();
|
||||
}
|
||||
|
||||
window.currentBalances = window.currentBalances || {};
|
||||
|
||||
{% if w.cid == '1' %}
|
||||
window.currentBalances = {
|
||||
mainBalance: '{{ w.balance }}',
|
||||
blindBalance: '{{ w.blind_balance }}',
|
||||
anonBalance: '{{ w.anon_balance }}'
|
||||
};
|
||||
{% elif w.cid == '3' %}
|
||||
window.currentBalances = {
|
||||
mainBalance: '{{ w.balance }}',
|
||||
mwebBalance: '{{ w.mweb_balance }}'
|
||||
};
|
||||
{% endif %}
|
||||
|
||||
function updateWalletBalances() {
|
||||
fetch('/json/walletbalances', {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(balanceData => {
|
||||
if (balanceData.error) {
|
||||
throw new Error(balanceData.error);
|
||||
}
|
||||
|
||||
if (!Array.isArray(balanceData)) {
|
||||
throw new Error('Invalid response format');
|
||||
}
|
||||
|
||||
|
||||
balanceData.forEach(coin => {
|
||||
updateWalletDisplay(coin);
|
||||
updatePercentageButtons(coin);
|
||||
});
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
|
||||
window.WalletManager.updatePrices(true);
|
||||
}
|
||||
|
||||
}, 100);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[Wallet] Error updating wallet balances:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function updatePercentageButtons(coinData) {
|
||||
const currentCoinId = {{ w.cid }};
|
||||
|
||||
const isPartVariant = (currentCoinId === 1 && (coinData.name === 'Particl' || coinData.name === 'Particl Anon' || coinData.name === 'Particl Blind'));
|
||||
const isLtcVariant = (currentCoinId === 3 && (coinData.name === 'Litecoin' || coinData.name === 'Litecoin MWEB'));
|
||||
const isCurrentCoin = (coinData.id === currentCoinId);
|
||||
|
||||
if (isPartVariant || isLtcVariant || isCurrentCoin) {
|
||||
const buttons = document.querySelectorAll('button[onclick*="setAmount"]');
|
||||
buttons.forEach(button => {
|
||||
const onclick = button.getAttribute('onclick');
|
||||
if (onclick) {
|
||||
if (currentCoinId === 1) {
|
||||
window.currentBalances = window.currentBalances || {};
|
||||
|
||||
const anonBalance = coinData.name === 'Particl Anon' ? coinData.balance :
|
||||
(window.currentBalances.anonBalance || '0');
|
||||
const blindBalance = coinData.name === 'Particl Blind' ? coinData.balance :
|
||||
(window.currentBalances.blindBalance || '0');
|
||||
const mainBalance = coinData.name === 'Particl' ? coinData.balance :
|
||||
(window.currentBalances.mainBalance || '0');
|
||||
|
||||
if (coinData.name === 'Particl') window.currentBalances.mainBalance = coinData.balance;
|
||||
if (coinData.name === 'Particl Anon') window.currentBalances.anonBalance = coinData.balance;
|
||||
if (coinData.name === 'Particl Blind') window.currentBalances.blindBalance = coinData.balance;
|
||||
|
||||
const newOnclick = onclick.replace(
|
||||
/setAmount\([^,]+,\s*'[^']*',\s*\d+,\s*'[^']*',\s*'[^']*'\)/,
|
||||
`setAmount(${onclick.match(/setAmount\(([^,]+)/)[1]}, '${mainBalance}', ${currentCoinId}, '${blindBalance}', '${anonBalance}')`
|
||||
);
|
||||
button.setAttribute('onclick', newOnclick);
|
||||
} else if (currentCoinId === 3) {
|
||||
window.currentBalances = window.currentBalances || {};
|
||||
|
||||
const mwebBalance = coinData.name === 'Litecoin MWEB' ? coinData.balance :
|
||||
(window.currentBalances.mwebBalance || '0');
|
||||
const mainBalance = coinData.name === 'Litecoin' ? coinData.balance :
|
||||
(window.currentBalances.mainBalance || '0');
|
||||
|
||||
if (coinData.name === 'Litecoin') window.currentBalances.mainBalance = coinData.balance;
|
||||
if (coinData.name === 'Litecoin MWEB') window.currentBalances.mwebBalance = coinData.balance;
|
||||
|
||||
const newOnclick = onclick.replace(
|
||||
/setAmount\([^,]+,\s*'[^']*',\s*\d+,\s*'[^']*'\)/,
|
||||
`setAmount(${onclick.match(/setAmount\(([^,]+)/)[1]}, '${mainBalance}', ${currentCoinId}, '${mwebBalance}')`
|
||||
);
|
||||
button.setAttribute('onclick', newOnclick);
|
||||
} else {
|
||||
const newOnclick = onclick.replace(
|
||||
/setAmount\([^,]+,\s*'[^']*',\s*\d+\)/,
|
||||
`setAmount(${onclick.match(/setAmount\(([^,]+)/)[1]}, '${coinData.balance}', ${currentCoinId})`
|
||||
);
|
||||
button.setAttribute('onclick', newOnclick);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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', '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', 'Unconfirmed:', coinData);
|
||||
}
|
||||
} else if (coinData.name === 'Litecoin MWEB') {
|
||||
|
||||
updateSpecificBalance('Litecoin', 'MWEB Balance:', coinData.balance, coinData.ticker || 'LTC');
|
||||
|
||||
if (coinData.pending && parseFloat(coinData.pending) > 0) {
|
||||
updatePendingBalance('Litecoin', 'MWEB Balance:', coinData.pending, coinData.ticker || 'LTC', 'Pending:', coinData);
|
||||
}
|
||||
} else {
|
||||
|
||||
updateSpecificBalance(coinData.name, 'Balance:', coinData.balance, coinData.ticker || coinData.name);
|
||||
|
||||
|
||||
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 parentRow = element.closest('tr');
|
||||
if (parentRow) {
|
||||
const labelCell = parentRow.querySelector('td:first-child');
|
||||
if (labelCell) {
|
||||
const currentLabel = labelCell.textContent.trim();
|
||||
|
||||
if (currentLabel.includes(labelText)) {
|
||||
if (isPending) {
|
||||
element.textContent = `+${balance} ${ticker}`;
|
||||
} else {
|
||||
element.textContent = `${balance} ${ticker}`;
|
||||
}
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
|
||||
window.WalletManager.updatePrices(false);
|
||||
}
|
||||
}, 50);
|
||||
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatePendingDisplay(coinData) {
|
||||
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
|
||||
balanceElements.forEach(element => {
|
||||
const elementCoinName = element.getAttribute('data-coinname');
|
||||
|
||||
if (elementCoinName === coinData.name) {
|
||||
const parentRow = element.closest('tr');
|
||||
if (parentRow) {
|
||||
const labelCell = parentRow.querySelector('td:first-child');
|
||||
if (labelCell && labelCell.textContent.includes('Balance:')) {
|
||||
|
||||
const balanceCell = parentRow.querySelector('td:nth-child(2)');
|
||||
if (balanceCell) {
|
||||
let pendingSpan = balanceCell.querySelector('.inline-block.py-1.px-2.rounded-full.bg-green-100');
|
||||
|
||||
if (coinData.pending && parseFloat(coinData.pending) > 0) {
|
||||
if (!pendingSpan) {
|
||||
|
||||
pendingSpan = document.createElement('span');
|
||||
pendingSpan.className = 'inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500';
|
||||
balanceCell.appendChild(document.createTextNode(' '));
|
||||
balanceCell.appendChild(pendingSpan);
|
||||
}
|
||||
|
||||
const ticker = coinData.ticker || coinData.name;
|
||||
|
||||
pendingSpan.textContent = `Pending: +${coinData.pending} ${ticker}`;
|
||||
} else {
|
||||
|
||||
if (pendingSpan) {
|
||||
pendingSpan.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removePendingDisplay(coinName) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
|
||||
balanceElements.forEach(element => {
|
||||
const elementCoinName = element.getAttribute('data-coinname');
|
||||
|
||||
if (elementCoinName === coinName) {
|
||||
const parentRow = element.closest('tr');
|
||||
if (parentRow) {
|
||||
const balanceCell = parentRow.querySelector('td:nth-child(2)');
|
||||
if (balanceCell) {
|
||||
const pendingSpan = balanceCell.querySelector('.inline-block.py-1.px-2.rounded-full.bg-green-100');
|
||||
if (pendingSpan) {
|
||||
pendingSpan.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatePendingBalance(coinName, balanceType, pendingAmount, ticker, pendingLabel, coinData) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
|
||||
balanceElements.forEach(element => {
|
||||
const elementCoinName = element.getAttribute('data-coinname');
|
||||
|
||||
if (elementCoinName === coinName) {
|
||||
const parentRow = element.closest('tr');
|
||||
if (parentRow) {
|
||||
const labelCell = parentRow.querySelector('td:first-child');
|
||||
if (labelCell && labelCell.textContent.includes(balanceType)) {
|
||||
|
||||
const balanceCell = parentRow.querySelector('td:nth-child(2)');
|
||||
if (balanceCell) {
|
||||
let pendingSpan = balanceCell.querySelector('.inline-block.py-1.px-2.rounded-full.bg-green-100');
|
||||
|
||||
if (pendingAmount && parseFloat(pendingAmount) > 0) {
|
||||
if (!pendingSpan) {
|
||||
|
||||
pendingSpan = document.createElement('span');
|
||||
pendingSpan.className = 'inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500';
|
||||
balanceCell.appendChild(document.createTextNode(' '));
|
||||
balanceCell.appendChild(pendingSpan);
|
||||
}
|
||||
|
||||
|
||||
pendingSpan.textContent = `${pendingLabel} +${pendingAmount} ${ticker}`;
|
||||
} else if (pendingSpan) {
|
||||
|
||||
pendingSpan.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removePendingBalance(coinName, balanceType) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
|
||||
balanceElements.forEach(element => {
|
||||
const elementCoinName = element.getAttribute('data-coinname');
|
||||
|
||||
if (elementCoinName === coinName) {
|
||||
const parentRow = element.closest('tr');
|
||||
if (parentRow) {
|
||||
const labelCell = parentRow.querySelector('td:first-child');
|
||||
if (labelCell && labelCell.textContent.includes(balanceType)) {
|
||||
|
||||
const balanceCell = parentRow.querySelector('td:nth-child(2)');
|
||||
if (balanceCell) {
|
||||
const pendingSpan = balanceCell.querySelector('.inline-block.py-1.px-2.rounded-full.bg-green-100');
|
||||
if (pendingSpan) {
|
||||
pendingSpan.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setupWalletWebSocketUpdates() {
|
||||
window.BalanceUpdatesManager.setup({
|
||||
contextKey: 'wallet',
|
||||
balanceUpdateCallback: updateWalletBalances,
|
||||
swapEventCallback: updateWalletBalances,
|
||||
errorContext: 'Wallet',
|
||||
enablePeriodicRefresh: true,
|
||||
periodicInterval: 120000
|
||||
});
|
||||
|
||||
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.walletPriceUpdateTimeout);
|
||||
window.walletPriceUpdateTimeout = setTimeout(() => {
|
||||
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
|
||||
window.WalletManager.updatePrices(true);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
window.walletPriceHandlerId = priceHandlerId;
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupWalletBalanceUpdates() {
|
||||
window.BalanceUpdatesManager.cleanup('wallet');
|
||||
|
||||
if (window.walletPriceHandlerId && window.WebSocketManager) {
|
||||
window.WebSocketManager.removeMessageHandler('message', window.walletPriceHandlerId);
|
||||
}
|
||||
|
||||
clearTimeout(window.walletPriceUpdateTimeout);
|
||||
}
|
||||
|
||||
window.BalanceUpdatesManager.initialize();
|
||||
setupWalletWebSocketUpdates();
|
||||
|
||||
setTimeout(() => {
|
||||
updateWalletBalances();
|
||||
}, 1000);
|
||||
|
||||
window.addEventListener('beforeunload', cleanupWalletBalanceUpdates);
|
||||
|
||||
document.getElementById('confirmYes').addEventListener('click', function() {
|
||||
if (typeof confirmCallback === 'function') {
|
||||
confirmCallback();
|
||||
|
||||
@@ -21,8 +21,7 @@
|
||||
<div id="total-btc-value" class="text-sm text-white mt-2"></div>
|
||||
</div>
|
||||
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto">
|
||||
<a class="rounded-full mr-5 flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/changepassword">{{ lock_svg | safe }}<span>Change/Set Password</span></a>
|
||||
<a class="rounded-full flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/wallets">{{ circular_arrows_svg | safe }}<span>Refresh</span></a>
|
||||
<a class="rounded-full flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" href="/changepassword">{{ lock_svg | safe }}<span>Change/Set Password</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,5 +189,426 @@
|
||||
</section>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') {
|
||||
window.WebSocketManager.initialize();
|
||||
}
|
||||
|
||||
function setupWalletsWebSocketUpdates() {
|
||||
window.BalanceUpdatesManager.setup({
|
||||
contextKey: 'wallets',
|
||||
balanceUpdateCallback: updateWalletBalances,
|
||||
swapEventCallback: updateWalletBalances,
|
||||
errorContext: 'Wallets',
|
||||
enablePeriodicRefresh: true,
|
||||
periodicInterval: 60000
|
||||
});
|
||||
|
||||
if (window.WebSocketManager && typeof window.WebSocketManager.addMessageHandler === 'function') {
|
||||
const priceHandlerId = window.WebSocketManager.addMessageHandler('message', (data) => {
|
||||
if (data && data.event) {
|
||||
if (data.event === 'price_updated' || data.event === 'prices_updated') {
|
||||
clearTimeout(window.walletsPriceUpdateTimeout);
|
||||
window.walletsPriceUpdateTimeout = setTimeout(() => {
|
||||
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
|
||||
window.WalletManager.updatePrices(true);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
});
|
||||
window.walletsPriceHandlerId = priceHandlerId;
|
||||
}
|
||||
}
|
||||
|
||||
function updateWalletBalances(balanceData) {
|
||||
if (balanceData) {
|
||||
balanceData.forEach(coin => {
|
||||
updateWalletDisplay(coin);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
|
||||
window.WalletManager.updatePrices(true);
|
||||
}
|
||||
}, 250);
|
||||
} else {
|
||||
window.BalanceUpdatesManager.fetchBalanceData()
|
||||
.then(data => updateWalletBalances(data))
|
||||
.catch(error => {
|
||||
console.error('Error updating wallet balances:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateWalletDisplay(coinData) {
|
||||
if (coinData.name === 'Particl') {
|
||||
updateSpecificBalance('Particl', 'Balance:', coinData.balance, coinData.ticker || 'PART');
|
||||
} else if (coinData.name === 'Particl Anon') {
|
||||
updateSpecificBalance('Particl', 'Anon Balance:', coinData.balance, coinData.ticker || 'PART');
|
||||
removePendingBalance('Particl', 'Anon Balance:');
|
||||
if (coinData.pending && parseFloat(coinData.pending) > 0) {
|
||||
updatePendingBalance('Particl', 'Anon Balance:', coinData.pending, coinData.ticker || 'PART', 'Anon Pending:', coinData);
|
||||
}
|
||||
} else if (coinData.name === 'Particl Blind') {
|
||||
updateSpecificBalance('Particl', 'Blind Balance:', coinData.balance, coinData.ticker || 'PART');
|
||||
removePendingBalance('Particl', 'Blind Balance:');
|
||||
if (coinData.pending && parseFloat(coinData.pending) > 0) {
|
||||
updatePendingBalance('Particl', 'Blind Balance:', coinData.pending, coinData.ticker || 'PART', 'Blind Unconfirmed:', coinData);
|
||||
}
|
||||
} else {
|
||||
updateSpecificBalance(coinData.name, 'Balance:', coinData.balance, coinData.ticker || coinData.name);
|
||||
|
||||
if (coinData.name !== 'Particl Anon' && coinData.name !== 'Particl Blind' && coinData.name !== 'Litecoin MWEB') {
|
||||
if (coinData.pending && parseFloat(coinData.pending) > 0) {
|
||||
updatePendingDisplay(coinData);
|
||||
} else {
|
||||
removePendingDisplay(coinData.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateSpecificBalance(coinName, labelText, balance, ticker, isPending = false) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
let found = false;
|
||||
|
||||
balanceElements.forEach(element => {
|
||||
const elementCoinName = element.getAttribute('data-coinname');
|
||||
|
||||
if (elementCoinName === coinName) {
|
||||
const parentDiv = element.closest('.flex.mb-2.justify-between.items-center');
|
||||
const labelElement = parentDiv ? parentDiv.querySelector('h4') : null;
|
||||
|
||||
if (labelElement) {
|
||||
const currentLabel = labelElement.textContent.trim();
|
||||
|
||||
if (currentLabel === labelText) {
|
||||
if (isPending) {
|
||||
const cleanBalance = balance.toString().replace(/^\+/, '');
|
||||
element.textContent = `+${cleanBalance} ${ticker}`;
|
||||
} else {
|
||||
element.textContent = `${balance} ${ticker}`;
|
||||
}
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function updatePendingDisplay(coinData) {
|
||||
const walletContainer = findWalletContainer(coinData.name);
|
||||
if (!walletContainer) return;
|
||||
|
||||
const existingPendingElements = walletContainer.querySelectorAll('.flex.mb-2.justify-between.items-center');
|
||||
let staticPendingElement = null;
|
||||
let staticUsdElement = null;
|
||||
|
||||
existingPendingElements.forEach(element => {
|
||||
const labelElement = element.querySelector('h4');
|
||||
if (labelElement) {
|
||||
const labelText = labelElement.textContent;
|
||||
if (labelText.includes('Pending:') && !labelText.includes('USD')) {
|
||||
staticPendingElement = element;
|
||||
} else if (labelText.includes('Pending USD value:')) {
|
||||
staticUsdElement = element;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (staticPendingElement && staticUsdElement) {
|
||||
const pendingSpan = staticPendingElement.querySelector('.coinname-value');
|
||||
if (pendingSpan) {
|
||||
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
|
||||
pendingSpan.textContent = `+${cleanPending} ${coinData.ticker || coinData.name}`;
|
||||
}
|
||||
|
||||
let initialUSD = '$0.00';
|
||||
if (window.WalletManager && window.WalletManager.coinPrices) {
|
||||
const coinId = coinData.name.toLowerCase().replace(' ', '-');
|
||||
const price = window.WalletManager.coinPrices[coinId];
|
||||
if (price && price.usd) {
|
||||
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
|
||||
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
|
||||
initialUSD = `$${usdValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
const usdDiv = staticUsdElement.querySelector('.usd-value');
|
||||
if (usdDiv) {
|
||||
usdDiv.textContent = initialUSD;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let pendingContainer = walletContainer.querySelector('.pending-container');
|
||||
|
||||
if (!pendingContainer) {
|
||||
pendingContainer = document.createElement('div');
|
||||
pendingContainer.className = 'pending-container';
|
||||
|
||||
const pendingRow = document.createElement('div');
|
||||
pendingRow.className = 'flex mb-2 justify-between items-center';
|
||||
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
|
||||
pendingRow.innerHTML = `
|
||||
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Pending:</h4>
|
||||
<span class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 coinname-value" data-coinname="${coinData.name}">+${cleanPending} ${coinData.ticker || coinData.name}</span>
|
||||
`;
|
||||
|
||||
pendingContainer.appendChild(pendingRow);
|
||||
|
||||
let initialUSD = '$0.00';
|
||||
if (window.WalletManager && window.WalletManager.coinPrices) {
|
||||
const coinId = coinData.name.toLowerCase().replace(' ', '-');
|
||||
const price = window.WalletManager.coinPrices[coinId];
|
||||
if (price && price.usd) {
|
||||
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
|
||||
initialUSD = `$${usdValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
const usdRow = document.createElement('div');
|
||||
usdRow.className = 'flex mb-2 justify-between items-center';
|
||||
usdRow.innerHTML = `
|
||||
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Pending USD value:</h4>
|
||||
<div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value">${initialUSD}</div>
|
||||
`;
|
||||
pendingContainer.appendChild(usdRow);
|
||||
|
||||
const balanceRow = walletContainer.querySelector('.flex.mb-2.justify-between.items-center');
|
||||
let insertAfterElement = balanceRow;
|
||||
|
||||
if (balanceRow) {
|
||||
let nextElement = balanceRow.nextElementSibling;
|
||||
while (nextElement) {
|
||||
const labelElement = nextElement.querySelector('h4');
|
||||
if (labelElement && labelElement.textContent.includes('USD value:') &&
|
||||
!labelElement.textContent.includes('Pending') && !labelElement.textContent.includes('Unconfirmed')) {
|
||||
insertAfterElement = nextElement;
|
||||
break;
|
||||
}
|
||||
nextElement = nextElement.nextElementSibling;
|
||||
}
|
||||
}
|
||||
|
||||
if (insertAfterElement && insertAfterElement.nextSibling) {
|
||||
walletContainer.insertBefore(pendingContainer, insertAfterElement.nextSibling);
|
||||
} else {
|
||||
walletContainer.appendChild(pendingContainer);
|
||||
}
|
||||
} else {
|
||||
const pendingElement = pendingContainer.querySelector('.coinname-value');
|
||||
if (pendingElement) {
|
||||
const cleanPending = coinData.pending.toString().replace(/^\+/, ''); // Remove existing + if any
|
||||
pendingElement.textContent = `+${cleanPending} ${coinData.ticker || coinData.name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removePendingDisplay(coinName) {
|
||||
const walletContainer = findWalletContainer(coinName);
|
||||
if (!walletContainer) return;
|
||||
|
||||
const pendingContainer = walletContainer.querySelector('.pending-container');
|
||||
if (pendingContainer) {
|
||||
pendingContainer.remove();
|
||||
}
|
||||
|
||||
const existingPendingElements = walletContainer.querySelectorAll('.flex.mb-2.justify-between.items-center');
|
||||
existingPendingElements.forEach(element => {
|
||||
const labelElement = element.querySelector('h4');
|
||||
if (labelElement) {
|
||||
const labelText = labelElement.textContent;
|
||||
if (labelText.includes('Pending:') || labelText.includes('Pending USD value:')) {
|
||||
element.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeSpecificPending(coinName, labelText) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
|
||||
balanceElements.forEach(element => {
|
||||
const elementCoinName = element.getAttribute('data-coinname');
|
||||
|
||||
if (elementCoinName === coinName) {
|
||||
const parentDiv = element.closest('.flex.mb-2.justify-between.items-center');
|
||||
const labelElement = parentDiv ? parentDiv.querySelector('h4') : null;
|
||||
|
||||
if (labelElement) {
|
||||
const currentLabel = labelElement.textContent.trim();
|
||||
|
||||
if (currentLabel === labelText) {
|
||||
parentDiv.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatePendingBalance(coinName, balanceType, pendingAmount, ticker, pendingLabel, coinData) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
let targetElement = null;
|
||||
|
||||
balanceElements.forEach(element => {
|
||||
const elementCoinName = element.getAttribute('data-coinname');
|
||||
if (elementCoinName === coinName) {
|
||||
const parentElement = element.closest('.flex.mb-2.justify-between.items-center');
|
||||
if (parentElement) {
|
||||
const labelElement = parentElement.querySelector('h4');
|
||||
if (labelElement && labelElement.textContent.includes(balanceType)) {
|
||||
targetElement = parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!targetElement) return;
|
||||
|
||||
let insertAfterElement = targetElement;
|
||||
let nextElement = targetElement.nextElementSibling;
|
||||
while (nextElement) {
|
||||
const labelElement = nextElement.querySelector('h4');
|
||||
if (labelElement) {
|
||||
const labelText = labelElement.textContent;
|
||||
if (labelText.includes('USD value:') && !labelText.includes('Pending') && !labelText.includes('Unconfirmed')) {
|
||||
insertAfterElement = nextElement;
|
||||
break;
|
||||
}
|
||||
if (labelText.includes('Balance:') || labelText.includes('Pending:') || labelText.includes('Unconfirmed:')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
nextElement = nextElement.nextElementSibling;
|
||||
}
|
||||
|
||||
let pendingElement = insertAfterElement.nextElementSibling;
|
||||
while (pendingElement && !pendingElement.querySelector('h4')?.textContent.includes(pendingLabel)) {
|
||||
pendingElement = pendingElement.nextElementSibling;
|
||||
if (pendingElement && pendingElement.querySelector('h4')?.textContent.includes('Balance:')) {
|
||||
pendingElement = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pendingElement) {
|
||||
const newPendingElement = document.createElement('div');
|
||||
newPendingElement.className = 'flex mb-2 justify-between items-center';
|
||||
newPendingElement.innerHTML = `
|
||||
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">${pendingLabel}</h4>
|
||||
<span class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 coinname-value" data-coinname="${coinName}">+${pendingAmount} ${ticker}</span>
|
||||
`;
|
||||
|
||||
insertAfterElement.parentNode.insertBefore(newPendingElement, insertAfterElement.nextSibling);
|
||||
|
||||
let initialUSD = '$0.00';
|
||||
if (window.WalletManager && window.WalletManager.coinPrices) {
|
||||
const coinId = coinName.toLowerCase().replace(' ', '-');
|
||||
const price = window.WalletManager.coinPrices[coinId];
|
||||
if (price && price.usd) {
|
||||
const usdValue = (parseFloat(pendingAmount) * price.usd).toFixed(2);
|
||||
initialUSD = `$${usdValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
const usdElement = document.createElement('div');
|
||||
usdElement.className = 'flex mb-2 justify-between items-center';
|
||||
usdElement.innerHTML = `
|
||||
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">${pendingLabel.replace(':', '')} USD value:</h4>
|
||||
<div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value">${initialUSD}</div>
|
||||
`;
|
||||
|
||||
newPendingElement.parentNode.insertBefore(usdElement, newPendingElement.nextSibling);
|
||||
} else {
|
||||
const pendingSpan = pendingElement.querySelector('.coinname-value');
|
||||
if (pendingSpan) {
|
||||
pendingSpan.textContent = `+${pendingAmount} ${ticker}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removePendingBalance(coinName, balanceType) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
let targetElement = null;
|
||||
|
||||
balanceElements.forEach(element => {
|
||||
const elementCoinName = element.getAttribute('data-coinname');
|
||||
if (elementCoinName === coinName) {
|
||||
const parentElement = element.closest('.flex.mb-2.justify-between.items-center');
|
||||
if (parentElement) {
|
||||
const labelElement = parentElement.querySelector('h4');
|
||||
if (labelElement && labelElement.textContent.includes(balanceType)) {
|
||||
targetElement = parentElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!targetElement) return;
|
||||
|
||||
let nextElement = targetElement.nextElementSibling;
|
||||
while (nextElement) {
|
||||
const labelElement = nextElement.querySelector('h4');
|
||||
if (labelElement) {
|
||||
const labelText = labelElement.textContent;
|
||||
if (labelText.includes('Pending:') || labelText.includes('Unconfirmed:') ||
|
||||
labelText.includes('Anon Pending:') || labelText.includes('Blind Unconfirmed:') ||
|
||||
labelText.includes('Pending USD value:') || labelText.includes('Unconfirmed USD value:') ||
|
||||
labelText.includes('Anon Pending USD value:') || labelText.includes('Blind Unconfirmed USD value:')) {
|
||||
const elementToRemove = nextElement;
|
||||
nextElement = nextElement.nextElementSibling;
|
||||
elementToRemove.remove();
|
||||
} else if (labelText.includes('Balance:')) {
|
||||
break; // Stop if we hit another balance
|
||||
} else {
|
||||
nextElement = nextElement.nextElementSibling;
|
||||
}
|
||||
} else {
|
||||
nextElement = nextElement.nextElementSibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findWalletContainer(coinName) {
|
||||
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
|
||||
for (const element of balanceElements) {
|
||||
if (element.getAttribute('data-coinname') === coinName) {
|
||||
return element.closest('.bg-coolGray-100, .dark\\:bg-gray-600');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function cleanupWalletsBalanceUpdates() {
|
||||
window.BalanceUpdatesManager.cleanup('wallets');
|
||||
|
||||
if (window.walletsPriceHandlerId && window.WebSocketManager) {
|
||||
window.WebSocketManager.removeMessageHandler('message', window.walletsPriceHandlerId);
|
||||
}
|
||||
|
||||
clearTimeout(window.walletsPriceUpdateTimeout);
|
||||
}
|
||||
|
||||
window.BalanceUpdatesManager.initialize();
|
||||
setupWalletsWebSocketUpdates();
|
||||
|
||||
setTimeout(() => {
|
||||
updateWalletBalances();
|
||||
}, 1000);
|
||||
|
||||
if (window.CleanupManager) {
|
||||
window.CleanupManager.registerResource('walletsBalanceUpdates', null, cleanupWalletsBalanceUpdates);
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', cleanupWalletsBalanceUpdates);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -14,7 +14,7 @@ import traceback
|
||||
import sys
|
||||
from urllib import parse
|
||||
from urllib.request import Request, urlopen
|
||||
from .util import listAvailableCoins
|
||||
from .util import listAvailableCoinsWithBalances
|
||||
|
||||
DEFAULT_AMM_CONFIG_FILE = "createoffers.json"
|
||||
DEFAULT_AMM_STATE_FILE = "createoffers_state.json"
|
||||
@@ -1286,7 +1286,7 @@ def page_amm(self, _, post_string):
|
||||
except Exception as e:
|
||||
err_messages.append(f"Failed to read state file: {str(e)}")
|
||||
|
||||
coins = listAvailableCoins(swap_client)
|
||||
coins = listAvailableCoinsWithBalances(swap_client)
|
||||
|
||||
template = server.env.get_template("amm.html")
|
||||
return self.render_template(
|
||||
|
||||
@@ -16,6 +16,7 @@ from .util import (
|
||||
have_data_entry,
|
||||
inputAmount,
|
||||
listAvailableCoins,
|
||||
listAvailableCoinsWithBalances,
|
||||
PAGE_LIMIT,
|
||||
setCoinFilter,
|
||||
set_pagination_filters,
|
||||
@@ -526,7 +527,7 @@ def page_newoffer(self, url_split, post_string):
|
||||
if swap_client.debug_ui:
|
||||
messages.append("Debug mode active.")
|
||||
|
||||
coins_from, coins_to = listAvailableCoins(swap_client, split_from=True)
|
||||
coins_from, coins_to = listAvailableCoinsWithBalances(swap_client, split_from=True)
|
||||
|
||||
addrs_from_raw = swap_client.listSMSGAddresses("offer_send_from")
|
||||
addrs_to_raw = swap_client.listSMSGAddresses("offer_send_to")
|
||||
|
||||
@@ -60,6 +60,38 @@ def page_settings(self, url_split, post_string):
|
||||
),
|
||||
}
|
||||
swap_client.editGeneralSettings(data)
|
||||
elif have_data_entry(form_data, "apply_notifications"):
|
||||
active_tab = "notifications"
|
||||
data = {
|
||||
"notifications_new_offers": toBool(
|
||||
get_data_entry_or(
|
||||
form_data, "notifications_new_offers", "false"
|
||||
)
|
||||
),
|
||||
"notifications_new_bids": toBool(
|
||||
get_data_entry_or(form_data, "notifications_new_bids", "false")
|
||||
),
|
||||
"notifications_bid_accepted": toBool(
|
||||
get_data_entry_or(
|
||||
form_data, "notifications_bid_accepted", "false"
|
||||
)
|
||||
),
|
||||
"notifications_balance_changes": toBool(
|
||||
get_data_entry_or(
|
||||
form_data, "notifications_balance_changes", "false"
|
||||
)
|
||||
),
|
||||
"notifications_outgoing_transactions": toBool(
|
||||
get_data_entry_or(
|
||||
form_data, "notifications_outgoing_transactions", "false"
|
||||
)
|
||||
),
|
||||
"notifications_duration": int(
|
||||
get_data_entry_or(form_data, "notifications_duration", "20")
|
||||
),
|
||||
}
|
||||
swap_client.editGeneralSettings(data)
|
||||
messages.append("Notification settings applied.")
|
||||
elif have_data_entry(form_data, "apply_tor"):
|
||||
active_tab = "tor"
|
||||
# TODO: Detect if running in docker
|
||||
@@ -186,6 +218,27 @@ def page_settings(self, url_split, post_string):
|
||||
"enabled_chart_coins": swap_client.settings.get("enabled_chart_coins", ""),
|
||||
}
|
||||
|
||||
notification_settings = {
|
||||
"notifications_new_offers": swap_client.settings.get(
|
||||
"notifications_new_offers", False
|
||||
),
|
||||
"notifications_new_bids": swap_client.settings.get(
|
||||
"notifications_new_bids", True
|
||||
),
|
||||
"notifications_bid_accepted": swap_client.settings.get(
|
||||
"notifications_bid_accepted", True
|
||||
),
|
||||
"notifications_balance_changes": swap_client.settings.get(
|
||||
"notifications_balance_changes", True
|
||||
),
|
||||
"notifications_outgoing_transactions": swap_client.settings.get(
|
||||
"notifications_outgoing_transactions", True
|
||||
),
|
||||
"notifications_duration": swap_client.settings.get(
|
||||
"notifications_duration", 20
|
||||
),
|
||||
}
|
||||
|
||||
tor_control_password = (
|
||||
""
|
||||
if swap_client.tor_control_password is None
|
||||
@@ -209,6 +262,7 @@ def page_settings(self, url_split, post_string):
|
||||
"chains": chains_formatted,
|
||||
"general_settings": general_settings,
|
||||
"chart_settings": chart_settings,
|
||||
"notification_settings": notification_settings,
|
||||
"tor_settings": tor_settings,
|
||||
"active_tab": active_tab,
|
||||
},
|
||||
|
||||
@@ -659,6 +659,76 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
|
||||
return coins
|
||||
|
||||
|
||||
def listAvailableCoinsWithBalances(swap_client, with_variants=True, split_from=False):
|
||||
swap_client.updateWalletsInfo()
|
||||
wallets = swap_client.getCachedWalletsInfo()
|
||||
|
||||
coins_from = []
|
||||
coins = []
|
||||
|
||||
for k, v in swap_client.coin_clients.items():
|
||||
if k not in chainparams:
|
||||
continue
|
||||
if v["connection_type"] == "rpc":
|
||||
|
||||
balance = "0.0"
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "balance" in w and "error" not in w and "no_data" not in w:
|
||||
balance = w["balance"]
|
||||
|
||||
coin_entry = (int(k), getCoinName(k), balance)
|
||||
coins.append(coin_entry)
|
||||
if split_from:
|
||||
coins_from.append(coin_entry)
|
||||
|
||||
if with_variants and k == Coins.PART:
|
||||
|
||||
for variant in (Coins.PART_ANON, Coins.PART_BLIND):
|
||||
variant_balance = "0.0"
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if "error" not in w and "no_data" not in w:
|
||||
if variant == Coins.PART_ANON and "anon_balance" in w:
|
||||
variant_balance = w["anon_balance"]
|
||||
elif variant == Coins.PART_BLIND and "blind_balance" in w:
|
||||
variant_balance = w["blind_balance"]
|
||||
|
||||
variant_entry = (
|
||||
int(variant),
|
||||
getCoinName(variant),
|
||||
variant_balance,
|
||||
)
|
||||
coins.append(variant_entry)
|
||||
if split_from:
|
||||
coins_from.append(variant_entry)
|
||||
|
||||
if with_variants and k == Coins.LTC:
|
||||
|
||||
for variant in (Coins.LTC_MWEB,):
|
||||
variant_balance = "0.0"
|
||||
if k in wallets:
|
||||
w = wallets[k]
|
||||
if (
|
||||
"error" not in w
|
||||
and "no_data" not in w
|
||||
and "mweb_balance" in w
|
||||
):
|
||||
variant_balance = w["mweb_balance"]
|
||||
|
||||
variant_entry = (
|
||||
int(variant),
|
||||
getCoinName(variant),
|
||||
variant_balance,
|
||||
)
|
||||
|
||||
pass
|
||||
|
||||
if split_from:
|
||||
return coins_from, coins
|
||||
return coins
|
||||
|
||||
|
||||
def checkAddressesOwned(swap_client, ci, wallet_info):
|
||||
if "stealth_address" in wallet_info:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user