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:
Gerlof van Ek
2025-07-22 23:45:45 +02:00
committed by GitHub
parent d6ef4f2edb
commit a5cc83157d
24 changed files with 3199 additions and 302 deletions

View File

@@ -186,6 +186,53 @@ def validOfferStateToReceiveBid(offer_state):
return False 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): def threadPollXMRChainState(swap_client, coin_type):
ci = swap_client.ci(coin_type) ci = swap_client.ci(coin_type)
cc = swap_client.coin_clients[coin_type] cc = swap_client.coin_clients[coin_type]
@@ -198,6 +245,11 @@ def threadPollXMRChainState(swap_client, coin_type):
) )
with swap_client.mxDB: with swap_client.mxDB:
cc["chain_height"] = new_height cc["chain_height"] = new_height
checkAndNotifyBalanceChange(
swap_client, coin_type, ci, cc, new_height, "block"
)
except Exception as e: except Exception as e:
swap_client.log.warning( swap_client.log.warning(
f"threadPollXMRChainState {ci.ticker()}, error: {e}" f"threadPollXMRChainState {ci.ticker()}, error: {e}"
@@ -219,6 +271,11 @@ def threadPollWOWChainState(swap_client, coin_type):
) )
with swap_client.mxDB: with swap_client.mxDB:
cc["chain_height"] = new_height cc["chain_height"] = new_height
checkAndNotifyBalanceChange(
swap_client, coin_type, ci, cc, new_height, "block"
)
except Exception as e: except Exception as e:
swap_client.log.warning( swap_client.log.warning(
f"threadPollWOWChainState {ci.ticker()}, error: {e}" f"threadPollWOWChainState {ci.ticker()}, error: {e}"
@@ -231,6 +288,12 @@ def threadPollWOWChainState(swap_client, coin_type):
def threadPollChainState(swap_client, coin_type): def threadPollChainState(swap_client, coin_type):
ci = swap_client.ci(coin_type) ci = swap_client.ci(coin_type)
cc = swap_client.coin_clients[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(): while not swap_client.chainstate_delay_event.is_set():
try: try:
chain_state = ci.getBlockchainInfo() chain_state = ci.getBlockchainInfo()
@@ -244,11 +307,14 @@ def threadPollChainState(swap_client, coin_type):
cc["chain_best_block"] = chain_state["bestblockhash"] cc["chain_best_block"] = chain_state["bestblockhash"]
if "mediantime" in chain_state: if "mediantime" in chain_state:
cc["chain_median_time"] = chain_state["mediantime"] cc["chain_median_time"] = chain_state["mediantime"]
checkAndNotifyBalanceChange(
swap_client, coin_type, ci, cc, new_height, "block"
)
except Exception as e: except Exception as e:
swap_client.log.warning(f"threadPollChainState {ci.ticker()}, error: {e}") swap_client.log.warning(f"threadPollChainState {ci.ticker()}, error: {e}")
swap_client.chainstate_delay_event.wait( swap_client.chainstate_delay_event.wait(random.randrange(*poll_delay_range))
random.randrange(20, 30)
) # Random to stagger updates
class WatchedOutput: # Watch for spends class WatchedOutput: # Watch for spends
@@ -498,6 +564,9 @@ class BasicSwap(BaseApp, UIApp):
) )
self.zmqSubscriber.setsockopt_string(zmq.SUBSCRIBE, "smsg") 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.with_coins_override = extra_opts.get("with_coins", set())
self.without_coins_override = extra_opts.get("without_coins", set()) self.without_coins_override = extra_opts.get("without_coins", set())
self._force_db_upgrade = extra_opts.get("force_db_upgrade", False) self._force_db_upgrade = extra_opts.get("force_db_upgrade", False)
@@ -5410,6 +5479,39 @@ class BasicSwap(BaseApp, UIApp):
# bid saved in checkBidState # 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: def getAddressBalance(self, coin_type, address: str) -> int:
if self.coin_clients[coin_type]["chain_lookups"] == "explorer": if self.coin_clients[coin_type]["chain_lookups"] == "explorer":
explorers = self.coin_clients[coin_type]["explorers"] explorers = self.coin_clients[coin_type]["explorers"]
@@ -10399,6 +10501,29 @@ class BasicSwap(BaseApp, UIApp):
self.processMsg(msg) 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: def expireBidsAndOffers(self, now) -> None:
bids_to_expire = set() bids_to_expire = set()
offers_to_expire = set() offers_to_expire = set()
@@ -10530,6 +10655,8 @@ class BasicSwap(BaseApp, UIApp):
message = self.zmqSubscriber.recv(flags=zmq.NOBLOCK) message = self.zmqSubscriber.recv(flags=zmq.NOBLOCK)
if message == b"smsg": if message == b"smsg":
self.processZmqSmsg() self.processZmqSmsg()
elif message == b"hashwtx":
self.processZmqHashwtx()
except zmq.Again as e: # noqa: F841 except zmq.Again as e: # noqa: F841
pass pass
except Exception as e: except Exception as e:
@@ -10831,6 +10958,76 @@ class BasicSwap(BaseApp, UIApp):
settings_copy["enabled_chart_coins"] = new_value settings_copy["enabled_chart_coins"] = new_value
settings_changed = True 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: if settings_changed:
settings_path = os.path.join(self.data_dir, cfg.CONFIG_FILENAME) settings_path = os.path.join(self.data_dir, cfg.CONFIG_FILENAME)
settings_path_new = settings_path + ".new" settings_path_new = settings_path + ".new"

View File

@@ -1382,6 +1382,11 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
fp.write( fp.write(
"zmqpubsmsg=tcp://{}:{}\n".format(COINS_RPCBIND_IP, settings["zmqport"]) "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("spentindex=1\n")
fp.write("txindex=1\n") fp.write("txindex=1\n")
fp.write("staking=0\n") fp.write("staking=0\n")

View File

@@ -56,6 +56,42 @@ def signal_handler(sig, frame):
swap_client.stopRunning() 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={}): def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin)) daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
datadir_path = os.path.expanduser(node_dir) datadir_path = os.path.expanduser(node_dir)
@@ -548,6 +584,9 @@ def runClient(
continue # /decred continue # /decred
if v["manage_daemon"] is True: 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") swap_client.log.info(f"Starting {display_name} daemon")
filename: str = getCoreBinName(coin_id, v, c + "d") filename: str = getCoreBinName(coin_id, v, c + "d")

View File

@@ -123,6 +123,145 @@ def js_coins(self, url_split, post_string, is_json) -> bytes:
return bytes(json.dumps(coins), "UTF-8") 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): def js_wallets(self, url_split, post_string, is_json):
swap_client = self.server.swap_client swap_client = self.server.swap_client
swap_client.checkSystemStatus() swap_client.checkSystemStatus()
@@ -1214,6 +1353,7 @@ def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
endpoints = { endpoints = {
"coins": js_coins, "coins": js_coins,
"walletbalances": js_walletbalances,
"wallets": js_wallets, "wallets": js_wallets,
"offers": js_offers, "offers": js_offers,
"sentoffers": js_sentoffers, "sentoffers": js_sentoffers,

View File

@@ -14,6 +14,62 @@
z-index: 9999; 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 */ /* Table Styles */
.padded_row td { .padded_row td {
padding-top: 1.5em; padding-top: 1.5em;

View File

@@ -23,13 +23,7 @@ const AmmTablesManager = (function() {
} }
function debugLog(message, data) { function debugLog(message, data) {
// if (isDebugEnabled()) {
// if (data) {
// console.log(`[AmmTables] ${message}`, data);
// } else {
// console.log(`[AmmTables] ${message}`);
// }
// }
} }
function initializeTabs() { function initializeTabs() {
@@ -309,8 +303,11 @@ const AmmTablesManager = (function() {
`; `;
}); });
if (offersBody.innerHTML.trim() !== tableHtml.trim()) {
offersBody.innerHTML = tableHtml; offersBody.innerHTML = tableHtml;
} }
}
function renderBidsTable(stateData) { function renderBidsTable(stateData) {
if (!bidsBody) return; if (!bidsBody) return;
@@ -441,8 +438,11 @@ const AmmTablesManager = (function() {
`; `;
}); });
if (bidsBody.innerHTML.trim() !== tableHtml.trim()) {
bidsBody.innerHTML = tableHtml; bidsBody.innerHTML = tableHtml;
} }
}
function formatDuration(seconds) { function formatDuration(seconds) {
if (seconds < 60) return `${seconds}s`; if (seconds < 60) return `${seconds}s`;
@@ -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() { function setupButtonHandlers() {
const addOfferButton = document.getElementById('add-new-offer-btn'); const addOfferButton = document.getElementById('add-new-offer-btn');
if (addOfferButton) { if (addOfferButton) {
@@ -844,6 +1267,40 @@ const AmmTablesManager = (function() {
modalTitle.textContent = `Add New ${type.charAt(0).toUpperCase() + type.slice(1)}`; modalTitle.textContent = `Add New ${type.charAt(0).toUpperCase() + type.slice(1)}`;
} }
const modal = document.getElementById('add-amm-modal');
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-type').value = type;
document.getElementById('add-amm-name').value = 'Unnamed Offer'; document.getElementById('add-amm-name').value = 'Unnamed Offer';
@@ -940,11 +1397,6 @@ const AmmTablesManager = (function() {
if (type === 'offer') { if (type === 'offer') {
setupBiddingControls('add'); setupBiddingControls('add');
} }
const modal = document.getElementById('add-amm-modal');
if (modal) {
modal.classList.remove('hidden');
}
} }
function closeAddModal() { function closeAddModal() {
@@ -1269,6 +1721,40 @@ const AmmTablesManager = (function() {
modalTitle.textContent = `Edit ${type.charAt(0).toUpperCase() + type.slice(1)}`; modalTitle.textContent = `Edit ${type.charAt(0).toUpperCase() + type.slice(1)}`;
} }
const modal = document.getElementById('edit-amm-modal');
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-type').value = type;
document.getElementById('edit-amm-id').value = id || ''; document.getElementById('edit-amm-id').value = id || '';
document.getElementById('edit-amm-original-name').value = name; document.getElementById('edit-amm-original-name').value = name;
@@ -1282,8 +1768,12 @@ const AmmTablesManager = (function() {
coinFromSelect.value = item.coin_from || ''; coinFromSelect.value = item.coin_from || '';
coinToSelect.value = item.coin_to || ''; coinToSelect.value = item.coin_to || '';
if (coinFromSelect) {
coinFromSelect.dispatchEvent(new Event('change', { bubbles: true })); coinFromSelect.dispatchEvent(new Event('change', { bubbles: true }));
}
if (coinToSelect) {
coinToSelect.dispatchEvent(new Event('change', { bubbles: true })); coinToSelect.dispatchEvent(new Event('change', { bubbles: true }));
}
document.getElementById('edit-amm-amount').value = item.amount || ''; document.getElementById('edit-amm-amount').value = item.amount || '';
@@ -1370,11 +1860,6 @@ const AmmTablesManager = (function() {
setupBiddingControls('edit'); setupBiddingControls('edit');
populateBiddingControls('edit', item); populateBiddingControls('edit', item);
} }
const modal = document.getElementById('edit-amm-modal');
if (modal) {
modal.classList.remove('hidden');
}
} catch (error) { } catch (error) {
alert(`Error processing the configuration: ${error.message}`); alert(`Error processing the configuration: ${error.message}`);
debugLog('Error opening edit modal:', error); debugLog('Error opening edit modal:', error);
@@ -1808,7 +2293,7 @@ const AmmTablesManager = (function() {
} }
} }
function initializeCustomSelects() { function initializeCustomSelects(modalType = null) {
const coinSelects = [ const coinSelects = [
document.getElementById('add-amm-coin-from'), document.getElementById('add-amm-coin-from'),
document.getElementById('add-amm-coin-to'), document.getElementById('add-amm-coin-to'),
@@ -1821,116 +2306,16 @@ const AmmTablesManager = (function() {
document.getElementById('edit-offer-swap-type') 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) { function createSwapTypeDropdown(select) {
if (!select) return; if (!select) return;
if (select.style.display === 'none' && select.parentNode.querySelector('.relative')) {
return; // Custom dropdown already exists
}
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = 'relative'; wrapper.className = 'relative';
@@ -1980,7 +2365,9 @@ const AmmTablesManager = (function() {
}); });
const selectedOption = select.options[select.selectedIndex]; const selectedOption = select.options[select.selectedIndex];
if (selectedOption) {
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim(); text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
}
display.addEventListener('click', function(e) { display.addEventListener('click', function(e) {
if (select.disabled) return; if (select.disabled) return;
@@ -2000,7 +2387,9 @@ const AmmTablesManager = (function() {
select.addEventListener('change', function() { select.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex]; const selectedOption = this.options[this.selectedIndex];
if (selectedOption) {
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim(); text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
}
}); });
const observer = new MutationObserver(function(mutations) { 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)); swapTypeSelects.forEach(select => createSwapTypeDropdown(select));
} }
@@ -2301,19 +2701,27 @@ const AmmTablesManager = (function() {
if (refreshButton) { if (refreshButton) {
refreshButton.addEventListener('click', async function() { refreshButton.addEventListener('click', async function() {
if (refreshButton.disabled) return;
const icon = refreshButton.querySelector('svg'); const icon = refreshButton.querySelector('svg');
refreshButton.disabled = true;
if (icon) { if (icon) {
icon.classList.add('animate-spin'); icon.classList.add('animate-spin');
} }
try {
await initializePrices(); await initializePrices();
updateTables(); updateTables();
} finally {
setTimeout(() => { setTimeout(() => {
if (icon) { if (icon) {
icon.classList.remove('animate-spin'); icon.classList.remove('animate-spin');
} }
}, 1000); refreshButton.disabled = false;
}, 500); // Reduced from 1000ms to 500ms
}
}); });
} }
@@ -2326,7 +2734,11 @@ const AmmTablesManager = (function() {
return { return {
updateTables, updateTables,
startRefreshTimer, startRefreshTimer,
stopRefreshTimer stopRefreshTimer,
refreshDropdownBalanceDisplay,
refreshOfferDropdownBalanceDisplay,
refreshBidDropdownBalanceDisplay,
refreshDropdownOptions
}; };
} }

View File

@@ -367,16 +367,45 @@ const ApiManager = (function() {
const results = {}; const results = {};
const fetchPromises = coinSymbols.map(async coin => { 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 () => { 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 { try {
const response = await this.makePostRequest(url); const response = await this.makePostRequest(url);
if (response && response.prices) { if (response && response.prices) {
results[coin] = response.prices; results[coin] = response.prices;
} }
} catch (error) { } catch (error) {
console.error(`Error fetching CoinGecko data for WOW:`, error); console.error(`Error fetching CoinGecko data for ${coin}:`, error);
throw error; throw error;
} }
}); });

View 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;
}

View File

@@ -1,11 +1,40 @@
const NotificationManager = (function() { const NotificationManager = (function() {
const config = {
const defaultConfig = {
showNewOffers: false, showNewOffers: false,
showNewBids: true, 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() { function ensureToastContainer() {
let container = document.getElementById('ul_updates'); let container = document.getElementById('ul_updates');
if (!container) { if (!container) {
@@ -19,13 +48,67 @@ const NotificationManager = (function() {
return container; 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 = { const publicAPI = {
initialize: function(options = {}) { initialize: function(options = {}) {
Object.assign(config, options); Object.assign(config, options);
this.initializeBalanceTracking();
if (window.CleanupManager) { if (window.CleanupManager) {
window.CleanupManager.registerResource('notificationManager', this, (mgr) => { window.CleanupManager.registerResource('notificationManager', this, (mgr) => {
if (this.balanceTimeouts) {
Object.values(this.balanceTimeouts).forEach(timeout => clearTimeout(timeout));
}
console.log('NotificationManager disposed'); console.log('NotificationManager disposed');
}); });
} }
@@ -33,30 +116,157 @@ const NotificationManager = (function() {
return this; 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 messages = ensureToastContainer();
const message = document.createElement('li'); 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 = ` message.innerHTML = `
<div id="hide"> <div class="toast-slide-in">
<div id="toast-${type}" class="flex items-center p-4 mb-4 w-full max-w-xs text-gray-500 <div id="${toastId}" class="flex items-center p-4 mb-3 w-full max-w-sm text-gray-500
bg-white rounded-lg shadow" role="alert"> 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 <div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10
bg-blue-500 rounded-lg"> ${iconColor} rounded-lg text-white">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="18" width="18" ${icon}
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> </div>
<div class="uppercase w-40 ml-3 text-sm font-semibold text-gray-900">${title}</div> <div class="flex items-center ml-3 text-sm font-medium text-gray-900 dark:text-white">
<button type="button" onclick="closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5 ${coinIconHtml}
bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-0 focus:outline-none <div class="flex flex-col">
focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8"> <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> <span class="sr-only">Close</span>
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" <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 <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 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" 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
@@ -67,34 +277,206 @@ const NotificationManager = (function() {
</div> </div>
`; `;
messages.appendChild(message); 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) { handleWebSocketEvent: function(data) {
if (!data || !data.event) return; if (!data || !data.event) return;
let toastTitle; let toastTitle, toastType, toastOptions = {};
let shouldShowToast = false; let shouldShowToast = false;
switch (data.event) { switch (data.event) {
case 'new_offer': 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; shouldShowToast = config.showNewOffers;
break; break;
case 'new_bid': case 'new_bid':
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>New bid</a> on toastTitle = `New bid received`;
<a class="underline" href=/offer/${data.offer_id}>offer</a>`; toastOptions.bidId = data.bid_id;
toastOptions.subtitle = 'Click to view bid';
toastType = 'new_bid';
shouldShowToast = config.showNewBids; shouldShowToast = config.showNewBids;
break; break;
case 'bid_accepted': 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; shouldShowToast = config.showBidAccepted;
break; break;
case 'coin_balance_updated':
if (data.coin && config.showBalanceChanges) {
this.handleBalanceUpdate(data);
}
return;
} }
if (toastTitle && shouldShowToast) { 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) { updateConfig: function(newConfig) {
Object.assign(config, newConfig); Object.assign(config, newConfig);
return this; return this;
@@ -122,5 +504,5 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
//console.log('NotificationManager initialized with methods:', Object.keys(NotificationManager));
console.log('NotificationManager initialized'); console.log('NotificationManager initialized');

View File

@@ -44,6 +44,7 @@ const PriceManager = (function() {
setTimeout(() => this.getPrices(), 1500); setTimeout(() => this.getPrices(), 1500);
isInitialized = true; isInitialized = true;
console.log('PriceManager initialized');
return this; return this;
}, },
@@ -59,7 +60,7 @@ const PriceManager = (function() {
return fetchPromise; return fetchPromise;
} }
//console.log('PriceManager: Fetching latest prices.');
lastFetchTime = Date.now(); lastFetchTime = Date.now();
fetchPromise = this.fetchPrices() fetchPromise = this.fetchPrices()
.then(prices => { .then(prices => {
@@ -166,14 +167,14 @@ const PriceManager = (function() {
const cachedData = CacheManager.get(PRICES_CACHE_KEY); const cachedData = CacheManager.get(PRICES_CACHE_KEY);
if (cachedData) { if (cachedData) {
console.log('Using cached price data');
return cachedData.value; return cachedData.value;
} }
try { try {
const existingCache = localStorage.getItem(PRICES_CACHE_KEY); const existingCache = localStorage.getItem(PRICES_CACHE_KEY);
if (existingCache) { if (existingCache) {
console.log('Using localStorage cached price data');
return JSON.parse(existingCache).value; return JSON.parse(existingCache).value;
} }
} catch (e) { } catch (e) {
@@ -230,4 +231,4 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
console.log('PriceManager initialized');

View File

@@ -261,9 +261,12 @@ const SummaryManager = (function() {
} }
if (data.event) { if (data.event) {
const summaryEvents = ['new_offer', 'offer_revoked', 'new_bid', 'bid_accepted', 'swap_completed'];
if (summaryEvents.includes(data.event)) {
publicAPI.fetchSummaryData() publicAPI.fetchSummaryData()
.then(() => {}) .then(() => {})
.catch(() => {}); .catch(() => {});
}
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') { if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
window.NotificationManager.handleWebSocketEvent(data); window.NotificationManager.handleWebSocketEvent(data);
@@ -334,9 +337,12 @@ const SummaryManager = (function() {
wsManager.addMessageHandler('message', (data) => { wsManager.addMessageHandler('message', (data) => {
if (data.event) { if (data.event) {
const summaryEvents = ['new_offer', 'offer_revoked', 'new_bid', 'bid_accepted', 'swap_completed'];
if (summaryEvents.includes(data.event)) {
this.fetchSummaryData() this.fetchSummaryData()
.then(() => {}) .then(() => {})
.catch(() => {}); .catch(() => {});
}
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') { if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
window.NotificationManager.handleWebSocketEvent(data); window.NotificationManager.handleWebSocketEvent(data);

View File

@@ -180,8 +180,29 @@ const WalletManager = (function() {
if (coinSymbol) { if (coinSymbol) {
if (coinName === 'Particl') { if (coinName === 'Particl') {
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind'); let isBlind = false;
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon'); 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'; const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString()); localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
} else if (coinName === 'Litecoin') { } else if (coinName === 'Litecoin') {
@@ -248,8 +269,29 @@ const WalletManager = (function() {
const usdValue = (amount * price).toFixed(2); const usdValue = (amount * price).toFixed(2);
if (coinName === 'Particl') { if (coinName === 'Particl') {
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind'); let isBlind = false;
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon'); 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'; const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
localStorage.setItem(`particl-${balanceType}-last-value`, usdValue); localStorage.setItem(`particl-${balanceType}-last-value`, usdValue);
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString()); localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());

View File

@@ -443,7 +443,45 @@ const UIEnhancer = {
const selectNameElement = select.nextElementSibling?.querySelector('.select-name'); const selectNameElement = select.nextElementSibling?.querySelector('.select-name');
if (selectNameElement) { if (selectNameElement) {
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.textContent = name;
selectNameElement.style.display = 'block';
selectNameElement.style.flexDirection = '';
selectNameElement.style.alignItems = '';
}
} }
updateSelectCache(select); updateSelectCache(select);

View File

@@ -43,7 +43,7 @@
}); });
window.bidsTabNavigationInitialized = true; window.bidsTabNavigationInitialized = true;
console.log('Bids tab navigation initialized'); //console.log('Bids tab navigation initialized');
} }
function handleInitialNavigation() { function handleInitialNavigation() {
@@ -54,14 +54,14 @@
const tabToActivate = localStorage.getItem('bidsTabToActivate'); const tabToActivate = localStorage.getItem('bidsTabToActivate');
if (tabToActivate) { if (tabToActivate) {
//console.log('Activating tab from localStorage:', tabToActivate);
localStorage.removeItem('bidsTabToActivate'); localStorage.removeItem('bidsTabToActivate');
activateTabWithRetry('#' + tabToActivate); activateTabWithRetry('#' + tabToActivate);
} else if (window.location.hash) { } else if (window.location.hash) {
//console.log('Activating tab from hash:', window.location.hash);
activateTabWithRetry(window.location.hash); activateTabWithRetry(window.location.hash);
} else { } else {
//console.log('Activating default tab: #all');
activateTabWithRetry('#all'); activateTabWithRetry('#all');
} }
} }
@@ -73,10 +73,10 @@
const hash = window.location.hash; const hash = window.location.hash;
if (hash) { if (hash) {
//console.log('Hash changed, activating tab:', hash);
activateTabWithRetry(hash); activateTabWithRetry(hash);
} else { } else {
//console.log('Hash cleared, activating default tab: #all');
activateTabWithRetry('#all'); activateTabWithRetry('#all');
} }
} }
@@ -85,7 +85,7 @@
const normalizedTabId = tabId.startsWith('#') ? tabId : '#' + tabId; const normalizedTabId = tabId.startsWith('#') ? tabId : '#' + tabId;
if (normalizedTabId !== '#all' && normalizedTabId !== '#sent' && normalizedTabId !== '#received') { if (normalizedTabId !== '#all' && normalizedTabId !== '#sent' && normalizedTabId !== '#received') {
//console.log('Invalid tab ID, defaulting to #all');
activateTabWithRetry('#all'); activateTabWithRetry('#all');
return; return;
} }
@@ -96,17 +96,17 @@
if (!tabButton) { if (!tabButton) {
if (retryCount < 5) { if (retryCount < 5) {
//console.log('Tab button not found, retrying...', retryCount + 1);
setTimeout(() => { setTimeout(() => {
activateTabWithRetry(normalizedTabId, retryCount + 1); activateTabWithRetry(normalizedTabId, retryCount + 1);
}, 100); }, 100);
} else { } else {
//console.error('Failed to find tab button after retries');
} }
return; return;
} }
//console.log('Activating tab:', normalizedTabId);
tabButton.click(); tabButton.click();
@@ -168,7 +168,7 @@
(tabId === '#sent' ? 'sent' : 'received'); (tabId === '#sent' ? 'sent' : 'received');
if (typeof window.updateBidsTable === 'function') { if (typeof window.updateBidsTable === 'function') {
//console.log('Triggering data load for', tabId);
window.updateBidsTable(); window.updateBidsTable();
} }
} }

View File

@@ -51,16 +51,11 @@
Add Bid Add Bid
</button> </button>
{% endif %} {% 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> </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"> <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"> <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"> <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"> <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 Kill Orphans
</button> </button>
</div> </div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2"> <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. 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') { if (localStorage.getItem('amm_create_default_refresh') === 'true') {
localStorage.removeItem('amm_create_default_refresh'); localStorage.removeItem('amm_create_default_refresh');
console.log('[AMM] Page loaded after create default config - redirecting to fresh page');
setTimeout(function() { setTimeout(function() {
window.location.href = window.location.pathname + window.location.search; 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 class="absolute inset-0 bg-gray-500 opacity-75 dark:bg-gray-900 dark:opacity-90"></div>
</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="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="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:text-left w-full"> <div class="mt-3 text-center sm:mt-0 sm:text-left w-full">
@@ -1086,7 +1082,7 @@
</div> </div>
<div class="grid grid-cols-2 gap-4"> <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> <label for="add-amm-coin-from" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Maker</label>
<div class="relative"> <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"> <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> </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"> <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 %} {% for c in coins %}
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}"> <option value="{{ c[1] }}" data-symbol="{{ c[0] }}" data-balance="{{ c[2] }}">{{ c[1] }}</option>
{{ c[1] }}
</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
</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> <label for="add-amm-coin-to" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Taker</label>
<div class="relative"> <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"> <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> </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"> <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 %} {% for c in coins %}
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}"> <option value="{{ c[1] }}" data-symbol="{{ c[0] }}" data-balance="{{ c[2] }}">{{ c[1] }}</option>
{{ c[1] }}
</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
</div> </div>
</div> </div>
@@ -1123,13 +1117,18 @@
<div> <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> <label for="add-amm-amount" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Offer Amount <span class="text-red-500">*</span></label>
<input type="text" id="add-amm-amount" pattern="[0-9]*\.?[0-9]*" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="0.0"> <input type="text" id="add-amm-amount" pattern="[0-9]*\.?[0-9]*" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="0.0">
<div class="mt-2 flex space-x-2">
<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>
<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> <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"> <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> <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> </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 class="absolute inset-0 bg-gray-500 opacity-75 dark:bg-gray-900 dark:opacity-90"></div>
</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="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="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:text-left w-full"> <div class="mt-3 text-center sm:mt-0 sm:text-left w-full">
@@ -1370,7 +1369,7 @@
</div> </div>
<div class="grid grid-cols-2 gap-4"> <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> <label for="edit-amm-coin-from" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Maker</label>
<div class="relative"> <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"> <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> </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"> <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 %} {% for c in coins %}
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}"> <option value="{{ c[1] }}" data-symbol="{{ c[0] }}" data-balance="{{ c[2] }}">{{ c[1] }}</option>
{{ c[1] }}
</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
</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> <label for="edit-amm-coin-to" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Taker</label>
<div class="relative"> <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"> <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> </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"> <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 %} {% for c in coins %}
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}"> <option value="{{ c[1] }}" data-symbol="{{ c[0] }}" data-balance="{{ c[2] }}">{{ c[1] }}</option>
{{ c[1] }}
</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
</div> </div>
</div> </div>
@@ -1407,13 +1404,18 @@
<div> <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> <label for="edit-amm-amount" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Offer Amount <span class="text-red-500">*</span></label>
<input type="text" id="edit-amm-amount" pattern="[0-9]*\.?[0-9]*" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="0.0"> <input type="text" id="edit-amm-amount" pattern="[0-9]*\.?[0-9]*" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="0.0">
<div class="mt-2 flex space-x-2">
<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>
<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> <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"> <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> <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> </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> </script>
{% include 'footer.html' %} {% include 'footer.html' %}

View File

@@ -75,6 +75,7 @@
<script src="/static/js/modules/price-manager.js"></script> <script src="/static/js/modules/price-manager.js"></script>
<script src="/static/js/modules/tooltips-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/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/identity-manager.js"></script>
<script src="/static/js/modules/summary-manager.js"></script> <script src="/static/js/modules/summary-manager.js"></script>
<script src="/static/js/amm_counter.js"></script> <script src="/static/js/amm_counter.js"></script>

View File

@@ -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');"> <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> <option value="-1">Select coin you send</option>
{% for c in coins_from %} {% 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 %} {% endfor %}
</select> </select>
<div class="select-dropdown"> <img class="select-icon" src="" alt=""> <img class="select-image" src="" alt=""> </div> <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="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 class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none"> </div> <input class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-5 focus:ring-0 bold" placeholder="0" type="text" id="amt_from" name="amt_from" value="{{ data.amt_from }}" onchange="set_rate('amt_from');">
</div> </div>
<div class="mt-2 flex space-x-2">
<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> </div>
</div> </div>
@@ -332,6 +337,142 @@
</div> </div>
</section> </section>
</div> </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> <script src="static/js/new_offer.js"></script>
{% include 'footer.html' %} {% include 'footer.html' %}
</div> </div>

View File

@@ -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"> <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 General
</button> </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"> <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 Tor
</button> </button>
@@ -432,6 +435,153 @@
</form> </form>
</div> </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"> <div class="tab-content hidden" id="tor" role="tabpanel" aria-labelledby="tor-tab">
<form method="post" id="tor-form"> <form method="post" id="tor-form">
<div class="space-y-6"> <div class="space-y-6">

View File

@@ -13,7 +13,7 @@
<span class="inline-block align-middle"> <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> <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>
<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> </div>
</div> </div>
@@ -667,16 +667,7 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
var floatBalance; var floatBalance;
var calculatedAmount; 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 safeParseFloat = (value) => {
const numValue = Number(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); calculatedAmount = Math.max(0, Math.floor(floatBalance * percent * 100000000) / 100000000);
console.log('Calculated Amount:', {
floatBalance: floatBalance,
calculatedAmount: calculatedAmount,
percent: percent
});
if (percent === 1) { if (percent === 1) {
calculatedAmount = floatBalance; calculatedAmount = floatBalance;
@@ -727,43 +714,6 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
if (subfeeCheckbox) { if (subfeeCheckbox) {
subfeeCheckbox.checked = (percent === 1); 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> </script>
@@ -818,17 +768,14 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
const specialCids = [6, 9]; 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) { if (specialCids.includes(parseInt(cid)) && percent === 1) {
amountInput.setAttribute('data-hidden', 'true'); amountInput.setAttribute('data-hidden', 'true');
amountInput.placeholder = 'Sweep All'; amountInput.placeholder = 'Sweep All';
amountInput.value = ''; amountInput.value = '';
amountInput.disabled = true; amountInput.disabled = true;
console.log("Sweep All activated for special CID:", cid);
} else { } else {
amountInput.value = calculatedAmount.toFixed(8); amountInput.value = calculatedAmount.toFixed(8);
amountInput.setAttribute('data-hidden', 'false'); amountInput.setAttribute('data-hidden', 'false');
@@ -840,17 +787,15 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
if (sweepAllCheckbox) { if (sweepAllCheckbox) {
if (specialCids.includes(parseInt(cid)) && percent === 1) { if (specialCids.includes(parseInt(cid)) && percent === 1) {
sweepAllCheckbox.checked = true; sweepAllCheckbox.checked = true;
console.log("Sweep All checkbox checked");
} else { } else {
sweepAllCheckbox.checked = false; sweepAllCheckbox.checked = false;
console.log("Sweep All checkbox unchecked");
} }
} }
let subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`); let subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
if (subfeeCheckbox) { if (subfeeCheckbox) {
subfeeCheckbox.checked = (percent === 1); subfeeCheckbox.checked = (percent === 1);
console.log("Subfee checkbox status for CID", cid, ":", subfeeCheckbox.checked);
} }
} }
</script> </script>
@@ -1148,6 +1093,375 @@ function confirmUTXOResize() {
} }
document.addEventListener('DOMContentLoaded', function() { 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() { document.getElementById('confirmYes').addEventListener('click', function() {
if (typeof confirmCallback === 'function') { if (typeof confirmCallback === 'function') {
confirmCallback(); confirmCallback();

View File

@@ -21,8 +21,7 @@
<div id="total-btc-value" class="text-sm text-white mt-2"></div> <div id="total-btc-value" class="text-sm text-white mt-2"></div>
</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"> <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" 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>
</div> </div>
</div> </div>
</div> </div>
@@ -190,5 +189,426 @@
</section> </section>
{% include 'footer.html' %} {% 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> </body>
</html> </html>

View File

@@ -14,7 +14,7 @@ import traceback
import sys import sys
from urllib import parse from urllib import parse
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
from .util import listAvailableCoins from .util import listAvailableCoinsWithBalances
DEFAULT_AMM_CONFIG_FILE = "createoffers.json" DEFAULT_AMM_CONFIG_FILE = "createoffers.json"
DEFAULT_AMM_STATE_FILE = "createoffers_state.json" DEFAULT_AMM_STATE_FILE = "createoffers_state.json"
@@ -1286,7 +1286,7 @@ def page_amm(self, _, post_string):
except Exception as e: except Exception as e:
err_messages.append(f"Failed to read state file: {str(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") template = server.env.get_template("amm.html")
return self.render_template( return self.render_template(

View File

@@ -16,6 +16,7 @@ from .util import (
have_data_entry, have_data_entry,
inputAmount, inputAmount,
listAvailableCoins, listAvailableCoins,
listAvailableCoinsWithBalances,
PAGE_LIMIT, PAGE_LIMIT,
setCoinFilter, setCoinFilter,
set_pagination_filters, set_pagination_filters,
@@ -526,7 +527,7 @@ def page_newoffer(self, url_split, post_string):
if swap_client.debug_ui: if swap_client.debug_ui:
messages.append("Debug mode active.") 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_from_raw = swap_client.listSMSGAddresses("offer_send_from")
addrs_to_raw = swap_client.listSMSGAddresses("offer_send_to") addrs_to_raw = swap_client.listSMSGAddresses("offer_send_to")

View File

@@ -60,6 +60,38 @@ def page_settings(self, url_split, post_string):
), ),
} }
swap_client.editGeneralSettings(data) 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"): elif have_data_entry(form_data, "apply_tor"):
active_tab = "tor" active_tab = "tor"
# TODO: Detect if running in docker # 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", ""), "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 = ( tor_control_password = (
"" ""
if swap_client.tor_control_password is None if swap_client.tor_control_password is None
@@ -209,6 +262,7 @@ def page_settings(self, url_split, post_string):
"chains": chains_formatted, "chains": chains_formatted,
"general_settings": general_settings, "general_settings": general_settings,
"chart_settings": chart_settings, "chart_settings": chart_settings,
"notification_settings": notification_settings,
"tor_settings": tor_settings, "tor_settings": tor_settings,
"active_tab": active_tab, "active_tab": active_tab,
}, },

View File

@@ -659,6 +659,76 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
return coins 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): def checkAddressesOwned(swap_client, ci, wallet_info):
if "stealth_address" in wallet_info: if "stealth_address" in wallet_info: