diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py
index 29e56b5..1ff4327 100644
--- a/basicswap/basicswap.py
+++ b/basicswap/basicswap.py
@@ -186,6 +186,53 @@ def validOfferStateToReceiveBid(offer_state):
return False
+def checkAndNotifyBalanceChange(
+ swap_client, coin_type, ci, cc, new_height, trigger_source="block"
+):
+ if not swap_client.ws_server:
+ return
+
+ try:
+ blockchain_info = ci.getBlockchainInfo()
+ verification_progress = blockchain_info.get("verificationprogress", 1.0)
+ if verification_progress < 0.99:
+ return
+ except Exception:
+ return
+
+ try:
+ current_balance = ci.getSpendableBalance()
+ current_total_balance = swap_client.getTotalBalance(coin_type)
+ cached_balance = cc.get("cached_balance", None)
+ cached_total_balance = cc.get("cached_total_balance", None)
+
+ current_unconfirmed = current_total_balance - current_balance
+ cached_unconfirmed = cc.get("cached_unconfirmed", None)
+
+ if (
+ cached_balance is None
+ or current_balance != cached_balance
+ or cached_total_balance is None
+ or current_total_balance != cached_total_balance
+ or cached_unconfirmed is None
+ or current_unconfirmed != cached_unconfirmed
+ ):
+ cc["cached_balance"] = current_balance
+ cc["cached_total_balance"] = current_total_balance
+ cc["cached_unconfirmed"] = current_unconfirmed
+ balance_event = {
+ "event": "coin_balance_updated",
+ "coin": ci.ticker(),
+ "height": new_height,
+ "trigger": trigger_source,
+ }
+ swap_client.ws_server.send_message_to_all(json.dumps(balance_event))
+ except Exception:
+ cc["cached_balance"] = None
+ cc["cached_total_balance"] = None
+ cc["cached_unconfirmed"] = None
+
+
def threadPollXMRChainState(swap_client, coin_type):
ci = swap_client.ci(coin_type)
cc = swap_client.coin_clients[coin_type]
@@ -198,6 +245,11 @@ def threadPollXMRChainState(swap_client, coin_type):
)
with swap_client.mxDB:
cc["chain_height"] = new_height
+
+ checkAndNotifyBalanceChange(
+ swap_client, coin_type, ci, cc, new_height, "block"
+ )
+
except Exception as e:
swap_client.log.warning(
f"threadPollXMRChainState {ci.ticker()}, error: {e}"
@@ -219,6 +271,11 @@ def threadPollWOWChainState(swap_client, coin_type):
)
with swap_client.mxDB:
cc["chain_height"] = new_height
+
+ checkAndNotifyBalanceChange(
+ swap_client, coin_type, ci, cc, new_height, "block"
+ )
+
except Exception as e:
swap_client.log.warning(
f"threadPollWOWChainState {ci.ticker()}, error: {e}"
@@ -231,6 +288,12 @@ def threadPollWOWChainState(swap_client, coin_type):
def threadPollChainState(swap_client, coin_type):
ci = swap_client.ci(coin_type)
cc = swap_client.coin_clients[coin_type]
+
+ if coin_type == Coins.PART and swap_client._zmq_queue_enabled:
+ poll_delay_range = (40, 60)
+ else:
+ poll_delay_range = (20, 30)
+
while not swap_client.chainstate_delay_event.is_set():
try:
chain_state = ci.getBlockchainInfo()
@@ -244,11 +307,14 @@ def threadPollChainState(swap_client, coin_type):
cc["chain_best_block"] = chain_state["bestblockhash"]
if "mediantime" in chain_state:
cc["chain_median_time"] = chain_state["mediantime"]
+
+ checkAndNotifyBalanceChange(
+ swap_client, coin_type, ci, cc, new_height, "block"
+ )
+
except Exception as e:
swap_client.log.warning(f"threadPollChainState {ci.ticker()}, error: {e}")
- swap_client.chainstate_delay_event.wait(
- random.randrange(20, 30)
- ) # Random to stagger updates
+ swap_client.chainstate_delay_event.wait(random.randrange(*poll_delay_range))
class WatchedOutput: # Watch for spends
@@ -498,6 +564,9 @@ class BasicSwap(BaseApp, UIApp):
)
self.zmqSubscriber.setsockopt_string(zmq.SUBSCRIBE, "smsg")
+ if Coins.PART in chainparams:
+ self.zmqSubscriber.setsockopt_string(zmq.SUBSCRIBE, "hashwtx")
+
self.with_coins_override = extra_opts.get("with_coins", set())
self.without_coins_override = extra_opts.get("without_coins", set())
self._force_db_upgrade = extra_opts.get("force_db_upgrade", False)
@@ -5410,6 +5479,39 @@ class BasicSwap(BaseApp, UIApp):
# bid saved in checkBidState
+ def getTotalBalance(self, coin_type) -> int:
+ try:
+ ci = self.ci(coin_type)
+ if hasattr(ci, "rpc_wallet"):
+ if coin_type in (Coins.XMR, Coins.WOW):
+ balance_info = ci.rpc_wallet("get_balance")
+ return balance_info["balance"]
+ elif coin_type == Coins.PART:
+ balances = ci.rpc_wallet("getbalances")
+ return ci.make_int(
+ balances["mine"]["trusted"]
+ + balances["mine"]["untrusted_pending"]
+ )
+ else:
+ try:
+ balances = ci.rpc_wallet("getbalances")
+ return ci.make_int(
+ balances["mine"]["trusted"]
+ + balances["mine"]["untrusted_pending"]
+ )
+ except Exception:
+ wallet_info = ci.rpc_wallet("getwalletinfo")
+ total = wallet_info.get("balance", 0)
+ if "unconfirmed_balance" in wallet_info:
+ total += wallet_info["unconfirmed_balance"]
+ if "immature_balance" in wallet_info:
+ total += wallet_info["immature_balance"]
+ return ci.make_int(total)
+ else:
+ return ci.getSpendableBalance()
+ except Exception:
+ return ci.getSpendableBalance()
+
def getAddressBalance(self, coin_type, address: str) -> int:
if self.coin_clients[coin_type]["chain_lookups"] == "explorer":
explorers = self.coin_clients[coin_type]["explorers"]
@@ -10399,6 +10501,29 @@ class BasicSwap(BaseApp, UIApp):
self.processMsg(msg)
+ def processZmqHashwtx(self) -> None:
+ self.zmqSubscriber.recv()
+
+ try:
+ if Coins.PART not in self.coin_clients:
+ return
+
+ ci = self.ci(Coins.PART)
+ cc = self.coin_clients[Coins.PART]
+
+ current_height = cc.get("chain_height", 0)
+
+ import time
+
+ time.sleep(0.1)
+
+ checkAndNotifyBalanceChange(self, Coins.PART, ci, cc, current_height, "zmq")
+
+ except Exception as e:
+ self.log.warning(f"Error processing PART wallet transaction: {e}")
+ if self.debug:
+ self.log.error(traceback.format_exc())
+
def expireBidsAndOffers(self, now) -> None:
bids_to_expire = set()
offers_to_expire = set()
@@ -10530,6 +10655,8 @@ class BasicSwap(BaseApp, UIApp):
message = self.zmqSubscriber.recv(flags=zmq.NOBLOCK)
if message == b"smsg":
self.processZmqSmsg()
+ elif message == b"hashwtx":
+ self.processZmqHashwtx()
except zmq.Again as e: # noqa: F841
pass
except Exception as e:
@@ -10831,6 +10958,76 @@ class BasicSwap(BaseApp, UIApp):
settings_copy["enabled_chart_coins"] = new_value
settings_changed = True
+ if "notifications_new_offers" in data:
+ new_value = data["notifications_new_offers"]
+ ensure(
+ isinstance(new_value, bool),
+ "New notifications_new_offers value not boolean",
+ )
+ if settings_copy.get("notifications_new_offers", False) != new_value:
+ settings_copy["notifications_new_offers"] = new_value
+ settings_changed = True
+
+ if "notifications_new_bids" in data:
+ new_value = data["notifications_new_bids"]
+ ensure(
+ isinstance(new_value, bool),
+ "New notifications_new_bids value not boolean",
+ )
+ if settings_copy.get("notifications_new_bids", True) != new_value:
+ settings_copy["notifications_new_bids"] = new_value
+ settings_changed = True
+
+ if "notifications_bid_accepted" in data:
+ new_value = data["notifications_bid_accepted"]
+ ensure(
+ isinstance(new_value, bool),
+ "New notifications_bid_accepted value not boolean",
+ )
+ if settings_copy.get("notifications_bid_accepted", True) != new_value:
+ settings_copy["notifications_bid_accepted"] = new_value
+ settings_changed = True
+
+ if "notifications_balance_changes" in data:
+ new_value = data["notifications_balance_changes"]
+ ensure(
+ isinstance(new_value, bool),
+ "New notifications_balance_changes value not boolean",
+ )
+ if (
+ settings_copy.get("notifications_balance_changes", True)
+ != new_value
+ ):
+ settings_copy["notifications_balance_changes"] = new_value
+ settings_changed = True
+
+ if "notifications_outgoing_transactions" in data:
+ new_value = data["notifications_outgoing_transactions"]
+ ensure(
+ isinstance(new_value, bool),
+ "New notifications_outgoing_transactions value not boolean",
+ )
+ if (
+ settings_copy.get("notifications_outgoing_transactions", True)
+ != new_value
+ ):
+ settings_copy["notifications_outgoing_transactions"] = new_value
+ settings_changed = True
+
+ if "notifications_duration" in data:
+ new_value = data["notifications_duration"]
+ ensure(
+ isinstance(new_value, int),
+ "New notifications_duration value not integer",
+ )
+ ensure(
+ 5 <= new_value <= 60,
+ "notifications_duration must be between 5 and 60 seconds",
+ )
+ if settings_copy.get("notifications_duration", 20) != new_value:
+ settings_copy["notifications_duration"] = new_value
+ settings_changed = True
+
if settings_changed:
settings_path = os.path.join(self.data_dir, cfg.CONFIG_FILENAME)
settings_path_new = settings_path + ".new"
diff --git a/basicswap/bin/prepare.py b/basicswap/bin/prepare.py
index 0ef3919..e0f7bc6 100755
--- a/basicswap/bin/prepare.py
+++ b/basicswap/bin/prepare.py
@@ -1382,6 +1382,11 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
fp.write(
"zmqpubsmsg=tcp://{}:{}\n".format(COINS_RPCBIND_IP, settings["zmqport"])
)
+ fp.write(
+ "zmqpubhashwtx=tcp://{}:{}\n".format(
+ COINS_RPCBIND_IP, settings["zmqport"]
+ )
+ )
fp.write("spentindex=1\n")
fp.write("txindex=1\n")
fp.write("staking=0\n")
diff --git a/basicswap/bin/run.py b/basicswap/bin/run.py
index 819cd53..649d3e5 100755
--- a/basicswap/bin/run.py
+++ b/basicswap/bin/run.py
@@ -56,6 +56,42 @@ def signal_handler(sig, frame):
swap_client.stopRunning()
+def checkPARTZmqConfigBeforeStart(part_settings, swap_settings):
+ try:
+ datadir = part_settings.get("datadir")
+ if not datadir:
+ return
+
+ config_path = os.path.join(datadir, "particl.conf")
+ if not os.path.exists(config_path):
+ return
+
+ with open(config_path, "r") as f:
+ config_content = f.read()
+
+ zmq_host = swap_settings.get("zmqhost", "tcp://127.0.0.1")
+ zmq_port = swap_settings.get("zmqport", 14792)
+ expected_line = f"zmqpubhashwtx={zmq_host}:{zmq_port}"
+
+ if "zmqpubhashwtx=" not in config_content:
+ with open(config_path, "a") as f:
+ f.write(f"{expected_line}\n")
+ elif expected_line not in config_content:
+ lines = config_content.split("\n")
+ updated_lines = []
+ for line in lines:
+ if line.startswith("zmqpubhashwtx="):
+ updated_lines.append(expected_line)
+ else:
+ updated_lines.append(line)
+
+ with open(config_path, "w") as f:
+ f.write("\n".join(updated_lines))
+
+ except Exception as e:
+ logger.debug(f"Error checking PART ZMQ config: {e}")
+
+
def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
datadir_path = os.path.expanduser(node_dir)
@@ -548,6 +584,9 @@ def runClient(
continue # /decred
if v["manage_daemon"] is True:
+ if c == "particl" and swap_client._zmq_queue_enabled:
+ checkPARTZmqConfigBeforeStart(v, swap_client.settings)
+
swap_client.log.info(f"Starting {display_name} daemon")
filename: str = getCoreBinName(coin_id, v, c + "d")
diff --git a/basicswap/js_server.py b/basicswap/js_server.py
index 61f6fec..d8e3273 100644
--- a/basicswap/js_server.py
+++ b/basicswap/js_server.py
@@ -123,6 +123,145 @@ def js_coins(self, url_split, post_string, is_json) -> bytes:
return bytes(json.dumps(coins), "UTF-8")
+def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
+ swap_client = self.server.swap_client
+
+ try:
+
+ swap_client.updateWalletsInfo()
+ wallets = swap_client.getCachedWalletsInfo()
+ coins_with_balances = []
+
+ for k, v in swap_client.coin_clients.items():
+ if k not in chainparams:
+ continue
+ if v["connection_type"] == "rpc":
+
+ balance = "0.0"
+ if k in wallets:
+ w = wallets[k]
+ if "balance" in w and "error" not in w and "no_data" not in w:
+ raw_balance = w["balance"]
+ if isinstance(raw_balance, float):
+ balance = f"{raw_balance:.8f}".rstrip("0").rstrip(".")
+ elif isinstance(raw_balance, int):
+ balance = str(raw_balance)
+ else:
+ balance = raw_balance
+
+ pending = "0.0"
+ if k in wallets:
+ w = wallets[k]
+ if "error" not in w and "no_data" not in w:
+ ci = swap_client.ci(k)
+ pending_amount = 0
+ if "unconfirmed" in w and float(w["unconfirmed"]) > 0.0:
+ pending_amount += ci.make_int(w["unconfirmed"])
+ if "immature" in w and float(w["immature"]) > 0.0:
+ pending_amount += ci.make_int(w["immature"])
+ if pending_amount > 0:
+ pending = ci.format_amount(pending_amount)
+
+ coin_entry = {
+ "id": int(k),
+ "name": getCoinName(k),
+ "balance": balance,
+ "pending": pending,
+ "ticker": chainparams[k]["ticker"],
+ }
+
+ coins_with_balances.append(coin_entry)
+
+ if k == Coins.PART:
+ variants = [
+ {
+ "coin": Coins.PART_ANON,
+ "balance_field": "anon_balance",
+ "pending_field": "anon_pending",
+ },
+ {
+ "coin": Coins.PART_BLIND,
+ "balance_field": "blind_balance",
+ "pending_field": "blind_unconfirmed",
+ },
+ ]
+
+ for variant_info in variants:
+ variant_balance = "0.0"
+ variant_pending = "0.0"
+
+ if k in wallets:
+ w = wallets[k]
+ if "error" not in w and "no_data" not in w:
+ if variant_info["balance_field"] in w:
+ raw_balance = w[variant_info["balance_field"]]
+ if isinstance(raw_balance, float):
+ variant_balance = f"{raw_balance:.8f}".rstrip(
+ "0"
+ ).rstrip(".")
+ elif isinstance(raw_balance, int):
+ variant_balance = str(raw_balance)
+ else:
+ variant_balance = raw_balance
+
+ if (
+ variant_info["pending_field"] in w
+ and float(w[variant_info["pending_field"]]) > 0.0
+ ):
+ variant_pending = str(
+ w[variant_info["pending_field"]]
+ )
+
+ variant_entry = {
+ "id": int(variant_info["coin"]),
+ "name": getCoinName(variant_info["coin"]),
+ "balance": variant_balance,
+ "pending": variant_pending,
+ "ticker": chainparams[Coins.PART]["ticker"],
+ }
+
+ coins_with_balances.append(variant_entry)
+
+ elif k == Coins.LTC:
+ variant_balance = "0.0"
+ variant_pending = "0.0"
+
+ if k in wallets:
+ w = wallets[k]
+ if "error" not in w and "no_data" not in w:
+ if "mweb_balance" in w:
+ variant_balance = w["mweb_balance"]
+
+ pending_amount = 0
+ if (
+ "mweb_unconfirmed" in w
+ and float(w["mweb_unconfirmed"]) > 0.0
+ ):
+ pending_amount += float(w["mweb_unconfirmed"])
+ if "mweb_immature" in w and float(w["mweb_immature"]) > 0.0:
+ pending_amount += float(w["mweb_immature"])
+ if pending_amount > 0:
+ variant_pending = f"{pending_amount:.8f}".rstrip(
+ "0"
+ ).rstrip(".")
+
+ variant_entry = {
+ "id": int(Coins.LTC_MWEB),
+ "name": getCoinName(Coins.LTC_MWEB),
+ "balance": variant_balance,
+ "pending": variant_pending,
+ "ticker": chainparams[Coins.LTC]["ticker"],
+ }
+
+ coins_with_balances.append(variant_entry)
+
+ return bytes(json.dumps(coins_with_balances), "UTF-8")
+
+ except Exception as e:
+ error_data = {"error": str(e)}
+ return bytes(json.dumps(error_data), "UTF-8")
+
+
def js_wallets(self, url_split, post_string, is_json):
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
@@ -1214,6 +1353,7 @@ def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
endpoints = {
"coins": js_coins,
+ "walletbalances": js_walletbalances,
"wallets": js_wallets,
"offers": js_offers,
"sentoffers": js_sentoffers,
diff --git a/basicswap/static/css/style.css b/basicswap/static/css/style.css
index 7c098c0..f9519dd 100644
--- a/basicswap/static/css/style.css
+++ b/basicswap/static/css/style.css
@@ -14,6 +14,62 @@
z-index: 9999;
}
+/* Toast Notification Animations */
+.toast-slide-in {
+ animation: slideInRight 0.3s ease-out;
+}
+
+.toast-slide-out {
+ animation: slideOutRight 0.3s ease-in forwards;
+}
+
+@keyframes slideInRight {
+ from {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+@keyframes slideOutRight {
+ from {
+ transform: translateX(0);
+ opacity: 1;
+ }
+ to {
+ transform: translateX(100%);
+ opacity: 0;
+ }
+}
+
+/* Toast Container Styles */
+#ul_updates {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ max-width: 400px;
+}
+
+#ul_updates li {
+ margin-bottom: 0.5rem;
+}
+
+/* Toast Hover Effects */
+#ul_updates .bg-white:hover {
+ box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+ transform: translateY(-1px);
+ transition: all 0.2s ease-in-out;
+}
+
+.dark #ul_updates .dark\:bg-gray-800:hover {
+ box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
+ transform: translateY(-1px);
+ transition: all 0.2s ease-in-out;
+}
+
/* Table Styles */
.padded_row td {
padding-top: 1.5em;
diff --git a/basicswap/static/js/amm_tables.js b/basicswap/static/js/amm_tables.js
index ec2881b..0e5ab1f 100644
--- a/basicswap/static/js/amm_tables.js
+++ b/basicswap/static/js/amm_tables.js
@@ -23,13 +23,7 @@ const AmmTablesManager = (function() {
}
function debugLog(message, data) {
- // if (isDebugEnabled()) {
- // if (data) {
- // console.log(`[AmmTables] ${message}`, data);
- // } else {
- // console.log(`[AmmTables] ${message}`);
- // }
- // }
+
}
function initializeTabs() {
@@ -309,7 +303,10 @@ const AmmTablesManager = (function() {
`;
});
- offersBody.innerHTML = tableHtml;
+
+ if (offersBody.innerHTML.trim() !== tableHtml.trim()) {
+ offersBody.innerHTML = tableHtml;
+ }
}
function renderBidsTable(stateData) {
@@ -441,7 +438,10 @@ const AmmTablesManager = (function() {
`;
});
- bidsBody.innerHTML = tableHtml;
+
+ if (bidsBody.innerHTML.trim() !== tableHtml.trim()) {
+ bidsBody.innerHTML = tableHtml;
+ }
}
function formatDuration(seconds) {
@@ -724,6 +724,429 @@ const AmmTablesManager = (function() {
}
}
+ function shouldDropdownOptionsShowBalance(select) {
+ const isMakerDropdown = select.id.includes('coin-from');
+ const isTakerDropdown = select.id.includes('coin-to');
+
+
+ const addModal = document.getElementById('add-amm-modal');
+ const editModal = document.getElementById('edit-amm-modal');
+ const addModalVisible = addModal && !addModal.classList.contains('hidden');
+ const editModalVisible = editModal && !editModal.classList.contains('hidden');
+
+ let isBidModal = false;
+ if (addModalVisible) {
+ const dataType = addModal.getAttribute('data-amm-type');
+ if (dataType) {
+ isBidModal = dataType === 'bid';
+ } else {
+
+ const modalTitle = document.getElementById('add-modal-title');
+ isBidModal = modalTitle && modalTitle.textContent && modalTitle.textContent.includes('Bid');
+ }
+ } else if (editModalVisible) {
+ const dataType = editModal.getAttribute('data-amm-type');
+ if (dataType) {
+ isBidModal = dataType === 'bid';
+ } else {
+
+ const modalTitle = document.getElementById('edit-modal-title');
+ isBidModal = modalTitle && modalTitle.textContent && modalTitle.textContent.includes('Bid');
+ }
+ }
+
+
+ const result = isBidModal ? isTakerDropdown : isMakerDropdown;
+
+ console.log(`[DEBUG] shouldDropdownOptionsShowBalance: ${select.id}, isBidModal=${isBidModal}, isMaker=${isMakerDropdown}, isTaker=${isTakerDropdown}, result=${result}`);
+
+ return result;
+ }
+
+ function refreshDropdownOptions() {
+ const dropdownIds = ['add-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-from', 'edit-amm-coin-to'];
+
+ dropdownIds.forEach(dropdownId => {
+ const select = document.getElementById(dropdownId);
+ if (!select || select.style.display !== 'none') return;
+
+ const wrapper = select.parentNode.querySelector('.relative');
+ if (!wrapper) return;
+
+
+ const dropdown = wrapper.querySelector('[role="listbox"]');
+ if (!dropdown) return;
+
+
+ const options = dropdown.querySelectorAll('[data-value]');
+ options.forEach(optionElement => {
+ const coinValue = optionElement.getAttribute('data-value');
+ const originalOption = Array.from(select.options).find(opt => opt.value === coinValue);
+ if (!originalOption) return;
+
+
+ const textContainer = optionElement.querySelector('div.flex.flex-col, div.flex.items-center');
+ if (!textContainer) return;
+
+
+ textContainer.innerHTML = '';
+
+ const shouldShowBalance = shouldDropdownOptionsShowBalance(select);
+ const fullText = originalOption.textContent.trim();
+ const balance = originalOption.getAttribute('data-balance') || '0.00000000';
+
+ console.log(`[DEBUG] refreshDropdownOptions: ${select.id}, option=${coinValue}, shouldShowBalance=${shouldShowBalance}, balance=${balance}`);
+
+ if (shouldShowBalance) {
+
+ textContainer.className = 'flex flex-col';
+
+ const coinName = fullText.includes(' - Balance: ') ? fullText.split(' - Balance: ')[0] : fullText;
+
+ const coinNameSpan = document.createElement('span');
+ coinNameSpan.textContent = coinName;
+ coinNameSpan.className = 'text-gray-900 dark:text-white';
+
+ const balanceSpan = document.createElement('span');
+ balanceSpan.textContent = `Balance: ${balance}`;
+ balanceSpan.className = 'text-gray-500 dark:text-gray-400 text-xs';
+
+ textContainer.appendChild(coinNameSpan);
+ textContainer.appendChild(balanceSpan);
+ } else {
+
+ textContainer.className = 'flex items-center';
+
+ const coinNameSpan = document.createElement('span');
+ const coinName = fullText.includes(' - Balance: ') ? fullText.split(' - Balance: ')[0] : fullText;
+ coinNameSpan.textContent = coinName;
+ coinNameSpan.className = 'text-gray-900 dark:text-white';
+
+ textContainer.appendChild(coinNameSpan);
+ }
+ });
+ });
+ }
+
+
+ function refreshDropdownBalances() {
+ const dropdownIds = ['add-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-from', 'edit-amm-coin-to'];
+
+ dropdownIds.forEach(dropdownId => {
+ const select = document.getElementById(dropdownId);
+ if (!select || select.style.display !== 'none') return;
+
+ const wrapper = select.parentNode.querySelector('.relative');
+ if (!wrapper) return;
+
+
+ const dropdownItems = wrapper.querySelectorAll('[data-value]');
+ dropdownItems.forEach(item => {
+ const value = item.getAttribute('data-value');
+ const option = select.querySelector(`option[value="${value}"]`);
+ if (option) {
+ const balance = option.getAttribute('data-balance') || '0.00000000';
+ const pendingBalance = option.getAttribute('data-pending-balance') || '';
+
+ const balanceDiv = item.querySelector('.text-xs');
+ if (balanceDiv) {
+ balanceDiv.textContent = `Balance: ${balance}`;
+
+
+ let pendingDiv = item.querySelector('.text-green-500');
+ if (pendingBalance && parseFloat(pendingBalance) > 0) {
+ if (!pendingDiv) {
+
+ pendingDiv = document.createElement('div');
+ pendingDiv.className = 'text-green-500 text-xs';
+ balanceDiv.parentNode.appendChild(pendingDiv);
+ }
+ pendingDiv.textContent = `+${pendingBalance} pending`;
+ } else if (pendingDiv) {
+
+ pendingDiv.remove();
+ }
+ }
+ }
+ });
+
+ const selectedOption = select.options[select.selectedIndex];
+ if (selectedOption) {
+ const textContainer = wrapper.querySelector('button .flex-grow');
+ const balanceDiv = textContainer ? textContainer.querySelector('.text-xs') : null;
+ if (balanceDiv) {
+ const balance = selectedOption.getAttribute('data-balance') || '0.00000000';
+ const pendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
+
+ balanceDiv.textContent = `Balance: ${balance}`;
+
+
+ let pendingDiv = textContainer.querySelector('.text-green-500');
+ if (pendingBalance && parseFloat(pendingBalance) > 0) {
+ if (!pendingDiv) {
+
+ pendingDiv = document.createElement('div');
+ pendingDiv.className = 'text-green-500 text-xs';
+ textContainer.appendChild(pendingDiv);
+ }
+ pendingDiv.textContent = `+${pendingBalance} pending`;
+ } else if (pendingDiv) {
+
+ pendingDiv.remove();
+ }
+ }
+ }
+ });
+ }
+
+ function refreshOfferDropdownBalanceDisplay() {
+ refreshDropdownBalances();
+ }
+
+ function refreshBidDropdownBalanceDisplay() {
+ refreshDropdownBalances();
+ }
+
+ function refreshDropdownBalanceDisplay(modalType = null) {
+ if (modalType === 'offer') {
+ refreshOfferDropdownBalanceDisplay();
+ } else if (modalType === 'bid') {
+ refreshBidDropdownBalanceDisplay();
+ } else {
+
+ const addModal = document.getElementById('add-amm-modal');
+ const editModal = document.getElementById('edit-amm-modal');
+ const addModalVisible = addModal && !addModal.classList.contains('hidden');
+ const editModalVisible = editModal && !editModal.classList.contains('hidden');
+
+ let detectedType = null;
+ if (addModalVisible) {
+ detectedType = addModal.getAttribute('data-amm-type');
+ } else if (editModalVisible) {
+ detectedType = editModal.getAttribute('data-amm-type');
+ }
+
+ if (detectedType === 'offer') {
+ refreshOfferDropdownBalanceDisplay();
+ } else if (detectedType === 'bid') {
+ refreshBidDropdownBalanceDisplay();
+ }
+ }
+ }
+
+ function updateDropdownsForModalType(modalPrefix) {
+ const coinFromSelect = document.getElementById(`${modalPrefix}-amm-coin-from`);
+ const coinToSelect = document.getElementById(`${modalPrefix}-amm-coin-to`);
+
+ if (!coinFromSelect || !coinToSelect) return;
+
+
+ const balanceData = {};
+
+
+ Array.from(coinFromSelect.options).forEach(option => {
+ const balance = option.getAttribute('data-balance');
+ if (balance) {
+ balanceData[option.value] = balance;
+ }
+ });
+
+
+ Array.from(coinToSelect.options).forEach(option => {
+ const balance = option.getAttribute('data-balance');
+ if (balance) {
+ balanceData[option.value] = balance;
+ }
+ });
+
+
+ updateDropdownOptions(coinFromSelect, balanceData);
+ updateDropdownOptions(coinToSelect, balanceData);
+ }
+
+ function updateDropdownOptions(select, balanceData, pendingData = {}) {
+ Array.from(select.options).forEach(option => {
+ const coinName = option.value;
+ const balance = balanceData[coinName] || '0.00000000';
+ const pending = pendingData[coinName] || '0.0';
+
+
+ option.setAttribute('data-balance', balance);
+ option.setAttribute('data-pending-balance', pending);
+
+
+ option.textContent = coinName;
+ });
+ }
+
+ function createSimpleDropdown(select, showBalance = false) {
+ if (!select) return;
+
+
+ const existingWrapper = select.parentNode.querySelector('.relative');
+ if (existingWrapper) {
+ existingWrapper.remove();
+ select.style.display = '';
+ }
+
+ select.style.display = 'none';
+
+ const wrapper = document.createElement('div');
+ wrapper.className = 'relative';
+
+
+ const button = document.createElement('button');
+ button.type = 'button';
+ button.className = 'flex items-center justify-between w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:text-white';
+ button.style.minHeight = '60px';
+
+
+ const displayContent = document.createElement('div');
+ displayContent.className = 'flex items-center';
+
+ const icon = document.createElement('img');
+ icon.className = 'w-5 h-5 mr-2';
+ icon.alt = '';
+
+ const textContainer = document.createElement('div');
+ textContainer.className = 'flex-grow text-left';
+
+ const arrow = document.createElement('div');
+ arrow.innerHTML = ` `;
+
+ 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 = `
+
${coinName}
+ Balance: ${balance}
+ `;
+
+
+ if (pendingBalance && parseFloat(pendingBalance) > 0) {
+ html += `+${pendingBalance} pending
`;
+ }
+
+ 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 = `
+ ${selectedCoinName}
+ Balance: ${selectedBalance}
+ `;
+
+
+ if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
+ html += `+${selectedPendingBalance} pending
`;
+ }
+
+ 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 = `
+ ${selectedCoinName}
+ Balance: ${selectedBalance}
+ `;
+
+
+ if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
+ html += `+${selectedPendingBalance} pending
`;
+ }
+
+ textContainer.innerHTML = html;
+ textContainer.className = 'flex-grow text-left flex flex-col justify-center';
+ } else {
+ textContainer.textContent = selectedCoinName;
+ textContainer.className = 'flex-grow text-left';
+ }
+ }
+
+
+ button.addEventListener('click', function() {
+ dropdown.classList.toggle('hidden');
+ });
+
+
+ document.addEventListener('click', function(e) {
+ if (!wrapper.contains(e.target)) {
+ dropdown.classList.add('hidden');
+ }
+ });
+
+ wrapper.appendChild(button);
+ wrapper.appendChild(dropdown);
+ select.parentNode.insertBefore(wrapper, select);
+
+ }
+
function setupButtonHandlers() {
const addOfferButton = document.getElementById('add-new-offer-btn');
if (addOfferButton) {
@@ -844,6 +1267,40 @@ const AmmTablesManager = (function() {
modalTitle.textContent = `Add New ${type.charAt(0).toUpperCase() + type.slice(1)}`;
}
+
+ const modal = document.getElementById('add-amm-modal');
+ if (modal) {
+ modal.classList.remove('hidden');
+
+ modal.setAttribute('data-amm-type', type);
+ }
+
+
+ setTimeout(() => {
+
+ updateDropdownsForModalType('add');
+
+ initializeCustomSelects(type);
+
+
+ refreshDropdownBalanceDisplay(type);
+
+
+ if (typeof fetchBalanceData === 'function') {
+ fetchBalanceData()
+ .then(balanceData => {
+ if (type === 'offer' && typeof updateOfferDropdownBalances === 'function') {
+ updateOfferDropdownBalances(balanceData);
+ } else if (type === 'bid' && typeof updateBidDropdownBalances === 'function') {
+ updateBidDropdownBalances(balanceData);
+ }
+ })
+ .catch(error => {
+ console.error('Error updating dropdown balances:', error);
+ });
+ }
+ }, 50);
+
document.getElementById('add-amm-type').value = type;
document.getElementById('add-amm-name').value = 'Unnamed Offer';
@@ -940,11 +1397,6 @@ const AmmTablesManager = (function() {
if (type === 'offer') {
setupBiddingControls('add');
}
-
- const modal = document.getElementById('add-amm-modal');
- if (modal) {
- modal.classList.remove('hidden');
- }
}
function closeAddModal() {
@@ -1269,6 +1721,40 @@ const AmmTablesManager = (function() {
modalTitle.textContent = `Edit ${type.charAt(0).toUpperCase() + type.slice(1)}`;
}
+
+ const modal = document.getElementById('edit-amm-modal');
+ if (modal) {
+ modal.classList.remove('hidden');
+
+ modal.setAttribute('data-amm-type', type);
+ }
+
+
+ setTimeout(() => {
+
+ updateDropdownsForModalType('edit');
+
+ initializeCustomSelects(type);
+
+
+ refreshDropdownBalanceDisplay(type);
+
+
+ if (typeof fetchBalanceData === 'function') {
+ fetchBalanceData()
+ .then(balanceData => {
+ if (type === 'offer' && typeof updateOfferDropdownBalances === 'function') {
+ updateOfferDropdownBalances(balanceData);
+ } else if (type === 'bid' && typeof updateBidDropdownBalances === 'function') {
+ updateBidDropdownBalances(balanceData);
+ }
+ })
+ .catch(error => {
+ console.error('Error updating dropdown balances:', error);
+ });
+ }
+ }, 50);
+
document.getElementById('edit-amm-type').value = type;
document.getElementById('edit-amm-id').value = id || '';
document.getElementById('edit-amm-original-name').value = name;
@@ -1282,8 +1768,12 @@ const AmmTablesManager = (function() {
coinFromSelect.value = item.coin_from || '';
coinToSelect.value = item.coin_to || '';
- coinFromSelect.dispatchEvent(new Event('change', { bubbles: true }));
- coinToSelect.dispatchEvent(new Event('change', { bubbles: true }));
+ if (coinFromSelect) {
+ coinFromSelect.dispatchEvent(new Event('change', { bubbles: true }));
+ }
+ if (coinToSelect) {
+ coinToSelect.dispatchEvent(new Event('change', { bubbles: true }));
+ }
document.getElementById('edit-amm-amount').value = item.amount || '';
@@ -1370,11 +1860,6 @@ const AmmTablesManager = (function() {
setupBiddingControls('edit');
populateBiddingControls('edit', item);
}
-
- const modal = document.getElementById('edit-amm-modal');
- if (modal) {
- modal.classList.remove('hidden');
- }
} catch (error) {
alert(`Error processing the configuration: ${error.message}`);
debugLog('Error opening edit modal:', error);
@@ -1808,7 +2293,7 @@ const AmmTablesManager = (function() {
}
}
- function initializeCustomSelects() {
+ function initializeCustomSelects(modalType = null) {
const coinSelects = [
document.getElementById('add-amm-coin-from'),
document.getElementById('add-amm-coin-to'),
@@ -1821,116 +2306,16 @@ const AmmTablesManager = (function() {
document.getElementById('edit-offer-swap-type')
];
- function createCoinDropdown(select) {
- if (!select) return;
- const wrapper = document.createElement('div');
- wrapper.className = 'relative';
-
- const display = document.createElement('div');
- display.className = 'flex items-center w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white cursor-pointer';
-
- const icon = document.createElement('img');
- icon.className = 'w-5 h-5 mr-2';
- icon.alt = '';
-
- const text = document.createElement('span');
- text.className = 'flex-grow';
-
- const arrow = document.createElement('span');
- arrow.className = 'ml-2';
- arrow.innerHTML = `
-
-
-
- `;
-
- display.appendChild(icon);
- display.appendChild(text);
- display.appendChild(arrow);
-
- const dropdown = document.createElement('div');
- dropdown.className = 'absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden dark:bg-gray-700 dark:border-gray-600 max-h-60 overflow-y-auto';
-
- Array.from(select.options).forEach(option => {
- const item = document.createElement('div');
- item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer text-gray-900 dark:text-white';
- item.setAttribute('data-value', option.value);
- item.setAttribute('data-symbol', option.getAttribute('data-symbol') || '');
-
- const optionIcon = document.createElement('img');
- optionIcon.className = 'w-5 h-5 mr-2';
- optionIcon.src = `/static/images/coins/${getImageFilename(option.value)}`;
- optionIcon.alt = '';
-
- const optionText = document.createElement('span');
- optionText.textContent = option.textContent.trim();
-
- item.appendChild(optionIcon);
- item.appendChild(optionText);
-
- item.addEventListener('click', function() {
- select.value = this.getAttribute('data-value');
-
- text.textContent = optionText.textContent;
- icon.src = optionIcon.src;
-
- dropdown.classList.add('hidden');
-
- const event = new Event('change', { bubbles: true });
- select.dispatchEvent(event);
-
- if (select.id === 'add-amm-coin-from' || select.id === 'add-amm-coin-to') {
- const coinFrom = document.getElementById('add-amm-coin-from');
- const coinTo = document.getElementById('add-amm-coin-to');
- const swapType = document.getElementById('add-offer-swap-type');
-
- if (coinFrom && coinTo && swapType) {
- updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType);
- }
- } else if (select.id === 'edit-amm-coin-from' || select.id === 'edit-amm-coin-to') {
- const coinFrom = document.getElementById('edit-amm-coin-from');
- const coinTo = document.getElementById('edit-amm-coin-to');
- const swapType = document.getElementById('edit-offer-swap-type');
-
- if (coinFrom && coinTo && swapType) {
- updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType);
- }
- }
- });
-
- dropdown.appendChild(item);
- });
-
- const selectedOption = select.options[select.selectedIndex];
- text.textContent = selectedOption.textContent.trim();
- icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`;
-
- display.addEventListener('click', function(e) {
- e.stopPropagation();
- dropdown.classList.toggle('hidden');
- });
-
- document.addEventListener('click', function() {
- dropdown.classList.add('hidden');
- });
-
- wrapper.appendChild(display);
- wrapper.appendChild(dropdown);
- select.parentNode.insertBefore(wrapper, select);
-
- select.style.display = 'none';
-
- select.addEventListener('change', function() {
- const selectedOption = this.options[this.selectedIndex];
- text.textContent = selectedOption.textContent.trim();
- icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`;
- });
- }
function createSwapTypeDropdown(select) {
if (!select) return;
+
+ if (select.style.display === 'none' && select.parentNode.querySelector('.relative')) {
+ return; // Custom dropdown already exists
+ }
+
const wrapper = document.createElement('div');
wrapper.className = 'relative';
@@ -1980,7 +2365,9 @@ const AmmTablesManager = (function() {
});
const selectedOption = select.options[select.selectedIndex];
- text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
+ if (selectedOption) {
+ text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
+ }
display.addEventListener('click', function(e) {
if (select.disabled) return;
@@ -2000,7 +2387,9 @@ const AmmTablesManager = (function() {
select.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
- text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
+ if (selectedOption) {
+ text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
+ }
});
const observer = new MutationObserver(function(mutations) {
@@ -2022,7 +2411,18 @@ const AmmTablesManager = (function() {
}
}
- coinSelects.forEach(select => createCoinDropdown(select));
+ coinSelects.forEach(select => {
+ if (!select) return;
+
+ let showBalance = false;
+ if (modalType === 'offer' && select.id.includes('coin-from')) {
+ showBalance = true; // OFFER: maker shows balance
+ } else if (modalType === 'bid' && select.id.includes('coin-to')) {
+ showBalance = true; // BID: taker shows balance
+ }
+
+ createSimpleDropdown(select, showBalance);
+ });
swapTypeSelects.forEach(select => createSwapTypeDropdown(select));
}
@@ -2301,19 +2701,27 @@ const AmmTablesManager = (function() {
if (refreshButton) {
refreshButton.addEventListener('click', async function() {
+
+ if (refreshButton.disabled) return;
+
const icon = refreshButton.querySelector('svg');
+ refreshButton.disabled = true;
+
if (icon) {
icon.classList.add('animate-spin');
}
- await initializePrices();
- updateTables();
-
- setTimeout(() => {
- if (icon) {
- icon.classList.remove('animate-spin');
- }
- }, 1000);
+ try {
+ await initializePrices();
+ updateTables();
+ } finally {
+ setTimeout(() => {
+ if (icon) {
+ icon.classList.remove('animate-spin');
+ }
+ refreshButton.disabled = false;
+ }, 500); // Reduced from 1000ms to 500ms
+ }
});
}
@@ -2326,7 +2734,11 @@ const AmmTablesManager = (function() {
return {
updateTables,
startRefreshTimer,
- stopRefreshTimer
+ stopRefreshTimer,
+ refreshDropdownBalanceDisplay,
+ refreshOfferDropdownBalanceDisplay,
+ refreshBidDropdownBalanceDisplay,
+ refreshDropdownOptions
};
}
diff --git a/basicswap/static/js/modules/api-manager.js b/basicswap/static/js/modules/api-manager.js
index bac3f22..40c938b 100644
--- a/basicswap/static/js/modules/api-manager.js
+++ b/basicswap/static/js/modules/api-manager.js
@@ -367,16 +367,45 @@ const ApiManager = (function() {
const results = {};
const fetchPromises = coinSymbols.map(async coin => {
- if (coin === 'WOW') {
+ let useCoinGecko = false;
+ let coingeckoId = null;
+
+ if (window.CoinManager) {
+ const coinConfig = window.CoinManager.getCoinByAnyIdentifier(coin);
+ if (coinConfig) {
+ useCoinGecko = !coinConfig.usesCryptoCompare || coin === 'PART';
+ coingeckoId = coinConfig.coingeckoId;
+ }
+ } else {
+ const coinGeckoCoins = {
+ 'WOW': 'wownero',
+ 'PART': 'particl',
+ 'BTC': 'bitcoin'
+ };
+ if (coinGeckoCoins[coin]) {
+ useCoinGecko = true;
+ coingeckoId = coinGeckoCoins[coin];
+ }
+ }
+
+ if (useCoinGecko && coingeckoId) {
return this.rateLimiter.queueRequest('coingecko', async () => {
- const url = `https://api.coingecko.com/api/v3/coins/wownero/market_chart?vs_currency=usd&days=1`;
+ let days;
+ if (resolution === 'day') {
+ days = 1;
+ } else if (resolution === 'year') {
+ days = 365;
+ } else {
+ days = 180;
+ }
+ const url = `https://api.coingecko.com/api/v3/coins/${coingeckoId}/market_chart?vs_currency=usd&days=${days}`;
try {
const response = await this.makePostRequest(url);
if (response && response.prices) {
results[coin] = response.prices;
}
} catch (error) {
- console.error(`Error fetching CoinGecko data for WOW:`, error);
+ console.error(`Error fetching CoinGecko data for ${coin}:`, error);
throw error;
}
});
diff --git a/basicswap/static/js/modules/balance-updates.js b/basicswap/static/js/modules/balance-updates.js
new file mode 100644
index 0000000..92f1735
--- /dev/null
+++ b/basicswap/static/js/modules/balance-updates.js
@@ -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;
+}
diff --git a/basicswap/static/js/modules/notification-manager.js b/basicswap/static/js/modules/notification-manager.js
index 7265b15..f22378d 100644
--- a/basicswap/static/js/modules/notification-manager.js
+++ b/basicswap/static/js/modules/notification-manager.js
@@ -1,11 +1,40 @@
const NotificationManager = (function() {
- const config = {
+
+ const defaultConfig = {
showNewOffers: false,
showNewBids: true,
- showBidAccepted: true
+ showBidAccepted: true,
+ showBalanceChanges: true,
+ showOutgoingTransactions: true,
+ notificationDuration: 20000
};
+
+ function loadConfig() {
+ const saved = localStorage.getItem('notification_settings');
+ if (saved) {
+ try {
+ return { ...defaultConfig, ...JSON.parse(saved) };
+ } catch (e) {
+ console.error('Error loading notification settings:', e);
+ }
+ }
+ return { ...defaultConfig };
+ }
+
+
+ function saveConfig(newConfig) {
+ try {
+ localStorage.setItem('notification_settings', JSON.stringify(newConfig));
+ Object.assign(config, newConfig);
+ } catch (e) {
+ console.error('Error saving notification settings:', e);
+ }
+ }
+
+ let config = loadConfig();
+
function ensureToastContainer() {
let container = document.getElementById('ul_updates');
if (!container) {
@@ -19,13 +48,67 @@ const NotificationManager = (function() {
return container;
}
+ function getCoinIcon(coinSymbol) {
+ if (window.CoinManager && typeof window.CoinManager.getCoinIcon === 'function') {
+ return window.CoinManager.getCoinIcon(coinSymbol);
+ }
+ return 'default.png';
+ }
+
+ function getToastIcon(type) {
+ const icons = {
+ 'new_offer': `
+
+ `,
+ 'new_bid': `
+
+ `,
+ 'bid_accepted': `
+
+ `,
+ 'balance_change': `
+
+ `,
+ 'success': `
+
+ `
+ };
+ return icons[type] || icons['success'];
+ }
+
+ function getToastColor(type, options = {}) {
+ const colors = {
+ 'new_offer': 'bg-blue-500',
+ 'new_bid': 'bg-green-500',
+ 'bid_accepted': 'bg-purple-500',
+ 'balance_change': 'bg-yellow-500',
+ 'success': 'bg-blue-500'
+ };
+
+ if (type === 'balance_change' && options.subtitle) {
+ if (options.subtitle.includes('sent') || options.subtitle.includes('sending')) {
+ return 'bg-red-500';
+ } else {
+ return 'bg-green-500';
+ }
+ }
+
+ return colors[type] || colors['success'];
+ }
+
const publicAPI = {
initialize: function(options = {}) {
Object.assign(config, options);
+
+ this.initializeBalanceTracking();
+
if (window.CleanupManager) {
window.CleanupManager.registerResource('notificationManager', this, (mgr) => {
-
+
+ if (this.balanceTimeouts) {
+ Object.values(this.balanceTimeouts).forEach(timeout => clearTimeout(timeout));
+ }
console.log('NotificationManager disposed');
});
}
@@ -33,33 +116,160 @@ const NotificationManager = (function() {
return this;
},
- createToast: function(title, type = 'success') {
+ updateSettings: function(newSettings) {
+ saveConfig(newSettings);
+ return this;
+ },
+
+ getSettings: function() {
+ return { ...config };
+ },
+
+ testToasts: function() {
+ if (!this.createToast) return;
+
+ setTimeout(() => {
+ this.createToast(
+ '+0.05000000 PART',
+ 'balance_change',
+ { coinSymbol: 'PART', subtitle: 'Incoming funds pending' }
+ );
+ }, 500);
+
+ setTimeout(() => {
+ this.createToast(
+ '+0.00123456 XMR',
+ 'balance_change',
+ { coinSymbol: 'XMR', subtitle: 'Incoming funds confirmed' }
+ );
+ }, 1000);
+
+ setTimeout(() => {
+ this.createToast(
+ '-29.86277595 PART',
+ 'balance_change',
+ { coinSymbol: 'PART', subtitle: 'Funds sent' }
+ );
+ }, 1500);
+
+ setTimeout(() => {
+ this.createToast(
+ '-0.05000000 PART (Anon)',
+ 'balance_change',
+ { coinSymbol: 'PART', subtitle: 'Funds sending' }
+ );
+ }, 2000);
+
+ setTimeout(() => {
+ this.createToast(
+ '+1.23456789 PART (Anon)',
+ 'balance_change',
+ { coinSymbol: 'PART', subtitle: 'Incoming funds confirmed' }
+ );
+ }, 2500);
+
+ setTimeout(() => {
+ this.createToast(
+ 'New network offer',
+ 'new_offer',
+ { offerId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66', subtitle: 'Click to view offer' }
+ );
+ }, 3000);
+
+ setTimeout(() => {
+ this.createToast(
+ 'New bid received',
+ 'new_bid',
+ { bidId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66', subtitle: 'Click to view bid' }
+ );
+ }, 3500);
+ },
+
+
+
+ initializeBalanceTracking: function() {
+
+ fetch('/json/walletbalances')
+ .then(response => response.json())
+ .then(balanceData => {
+ if (Array.isArray(balanceData)) {
+ balanceData.forEach(coin => {
+ const balance = parseFloat(coin.balance) || 0;
+ const pending = parseFloat(coin.pending) || 0;
+
+ const coinKey = coin.name.replace(/\s+/g, '_');
+ const storageKey = `prev_balance_${coinKey}`;
+ const pendingStorageKey = `prev_pending_${coinKey}`;
+
+
+ if (!localStorage.getItem(storageKey)) {
+ localStorage.setItem(storageKey, balance.toString());
+ }
+ if (!localStorage.getItem(pendingStorageKey)) {
+ localStorage.setItem(pendingStorageKey, pending.toString());
+ }
+ });
+ }
+ })
+ .catch(error => {
+ console.error('Error initializing balance tracking:', error);
+ });
+ },
+
+ createToast: function(title, type = 'success', options = {}) {
const messages = ensureToastContainer();
const message = document.createElement('li');
+ const toastId = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ const iconColor = getToastColor(type, options);
+ const icon = getToastIcon(type);
+
+
+ let coinIconHtml = '';
+ if (options.coinSymbol) {
+ const coinIcon = getCoinIcon(options.coinSymbol);
+ coinIconHtml = ` `;
+ }
+
+
+ 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 = `
-
-
-
-
-
-
-
-
+
+
+
+ ${icon}
-
${title}
-
+
+ ${coinIconHtml}
+
+ ${title}
+ ${options.subtitle ? `${options.subtitle} ` : ''}
+
+
+
Close
-
-
+
@@ -67,34 +277,206 @@ const NotificationManager = (function() {
`;
messages.appendChild(message);
+
+
+ setTimeout(() => {
+ if (message.parentNode) {
+ message.classList.add('toast-slide-out');
+ setTimeout(() => {
+ if (message.parentNode) {
+ message.parentNode.removeChild(message);
+ }
+ }, 300);
+ }
+ }, config.notificationDuration);
},
handleWebSocketEvent: function(data) {
if (!data || !data.event) return;
- let toastTitle;
+ let toastTitle, toastType, toastOptions = {};
let shouldShowToast = false;
switch (data.event) {
case 'new_offer':
- toastTitle = `New network
offer `;
+ toastTitle = `New network offer`;
+ toastType = 'new_offer';
+ toastOptions.offerId = data.offer_id;
+ toastOptions.subtitle = 'Click to view offer';
shouldShowToast = config.showNewOffers;
break;
case 'new_bid':
- toastTitle = `
New bid on
-
offer `;
+ toastTitle = `New bid received`;
+ toastOptions.bidId = data.bid_id;
+ toastOptions.subtitle = 'Click to view bid';
+ toastType = 'new_bid';
shouldShowToast = config.showNewBids;
break;
case 'bid_accepted':
- toastTitle = `
Bid accepted`;
+ toastTitle = `Bid accepted`;
+ toastOptions.bidId = data.bid_id;
+ toastOptions.subtitle = 'Click to view swap';
+ toastType = 'bid_accepted';
shouldShowToast = config.showBidAccepted;
break;
+ case 'coin_balance_updated':
+ if (data.coin && config.showBalanceChanges) {
+ this.handleBalanceUpdate(data);
+ }
+ return;
}
if (toastTitle && shouldShowToast) {
- this.createToast(toastTitle);
+ this.createToast(toastTitle, toastType, toastOptions);
}
},
+ handleBalanceUpdate: function(data) {
+ if (!data.coin) return;
+
+ this.fetchAndShowBalanceChange(data.coin);
+ const balanceKey = `balance_${data.coin}`;
+
+ if (this.balanceTimeouts && this.balanceTimeouts[balanceKey]) {
+ clearTimeout(this.balanceTimeouts[balanceKey]);
+ }
+
+ if (!this.balanceTimeouts) {
+ this.balanceTimeouts = {};
+ }
+
+ this.balanceTimeouts[balanceKey] = setTimeout(() => {
+ this.fetchAndShowBalanceChange(data.coin);
+ }, 2000);
+ },
+
+ fetchAndShowBalanceChange: function(coinSymbol) {
+
+ fetch('/json/walletbalances')
+ .then(response => response.json())
+ .then(balanceData => {
+ if (Array.isArray(balanceData)) {
+
+ let coinsToCheck;
+ if (coinSymbol === 'PART') {
+ coinsToCheck = balanceData.filter(coin => coin.ticker === 'PART');
+ } else if (coinSymbol === 'LTC') {
+ coinsToCheck = balanceData.filter(coin => coin.ticker === 'LTC');
+ } else {
+ coinsToCheck = balanceData.filter(coin =>
+ coin.ticker === coinSymbol ||
+ coin.name.toLowerCase() === coinSymbol.toLowerCase()
+ );
+ }
+
+ coinsToCheck.forEach(coinData => {
+ this.checkSingleCoinBalance(coinData, coinSymbol);
+ });
+ }
+ })
+ .catch(error => {
+ console.error('Error fetching balance for notification:', error);
+ });
+ },
+
+ checkSingleCoinBalance: function(coinData, originalCoinSymbol) {
+ const currentBalance = parseFloat(coinData.balance) || 0;
+ const currentPending = parseFloat(coinData.pending) || 0;
+
+ const coinKey = coinData.name.replace(/\s+/g, '_');
+ const storageKey = `prev_balance_${coinKey}`;
+ const pendingStorageKey = `prev_pending_${coinKey}`;
+ const prevBalance = parseFloat(localStorage.getItem(storageKey)) || 0;
+ const prevPending = parseFloat(localStorage.getItem(pendingStorageKey)) || 0;
+
+
+ const balanceIncrease = currentBalance - prevBalance;
+ const pendingIncrease = currentPending - prevPending;
+ const pendingDecrease = prevPending - currentPending;
+
+
+ const isPendingToConfirmed = pendingDecrease > 0.00000001 && balanceIncrease > 0.00000001;
+
+
+ const displaySymbol = originalCoinSymbol;
+ let variantInfo = '';
+
+ if (coinData.name !== 'Particl' && coinData.name.includes('Particl')) {
+
+ variantInfo = ` (${coinData.name.replace('Particl ', '')})`;
+ } else if (coinData.name !== 'Litecoin' && coinData.name.includes('Litecoin')) {
+
+ variantInfo = ` (${coinData.name.replace('Litecoin ', '')})`;
+ }
+
+ if (balanceIncrease > 0.00000001) {
+ const displayAmount = balanceIncrease.toFixed(8).replace(/\.?0+$/, '');
+ const subtitle = isPendingToConfirmed ? 'Funds confirmed' : 'Incoming funds confirmed';
+ this.createToast(
+ `+${displayAmount} ${displaySymbol}${variantInfo}`,
+ 'balance_change',
+ {
+ coinSymbol: originalCoinSymbol,
+ subtitle: subtitle
+ }
+ );
+ }
+
+ if (balanceIncrease < -0.00000001 && config.showOutgoingTransactions) {
+ const displayAmount = Math.abs(balanceIncrease).toFixed(8).replace(/\.?0+$/, '');
+ this.createToast(
+ `-${displayAmount} ${displaySymbol}${variantInfo}`,
+ 'balance_change',
+ {
+ coinSymbol: originalCoinSymbol,
+ subtitle: 'Funds sent'
+ }
+ );
+ }
+
+ if (pendingIncrease > 0.00000001) {
+ const displayAmount = pendingIncrease.toFixed(8).replace(/\.?0+$/, '');
+ this.createToast(
+ `+${displayAmount} ${displaySymbol}${variantInfo}`,
+ 'balance_change',
+ {
+ coinSymbol: originalCoinSymbol,
+ subtitle: 'Incoming funds pending'
+ }
+ );
+ }
+
+ if (pendingIncrease < -0.00000001 && config.showOutgoingTransactions && !isPendingToConfirmed) {
+ const displayAmount = Math.abs(pendingIncrease).toFixed(8).replace(/\.?0+$/, '');
+ this.createToast(
+ `-${displayAmount} ${displaySymbol}${variantInfo}`,
+ 'balance_change',
+ {
+ coinSymbol: originalCoinSymbol,
+ subtitle: 'Funds sending'
+ }
+ );
+ }
+
+
+ if (pendingDecrease > 0.00000001 && !isPendingToConfirmed) {
+ const displayAmount = pendingDecrease.toFixed(8).replace(/\.?0+$/, '');
+ this.createToast(
+ `${displayAmount} ${displaySymbol}${variantInfo}`,
+ 'balance_change',
+ {
+ coinSymbol: originalCoinSymbol,
+ subtitle: 'Pending funds confirmed'
+ }
+ );
+ }
+
+
+ localStorage.setItem(storageKey, currentBalance.toString());
+ localStorage.setItem(pendingStorageKey, currentPending.toString());
+ },
+
+
+
updateConfig: function(newConfig) {
Object.assign(config, newConfig);
return this;
@@ -122,5 +504,5 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
-//console.log('NotificationManager initialized with methods:', Object.keys(NotificationManager));
+
console.log('NotificationManager initialized');
diff --git a/basicswap/static/js/modules/price-manager.js b/basicswap/static/js/modules/price-manager.js
index b109bc0..5ef672b 100644
--- a/basicswap/static/js/modules/price-manager.js
+++ b/basicswap/static/js/modules/price-manager.js
@@ -44,6 +44,7 @@ const PriceManager = (function() {
setTimeout(() => this.getPrices(), 1500);
isInitialized = true;
+ console.log('PriceManager initialized');
return this;
},
@@ -59,7 +60,7 @@ const PriceManager = (function() {
return fetchPromise;
}
- //console.log('PriceManager: Fetching latest prices.');
+
lastFetchTime = Date.now();
fetchPromise = this.fetchPrices()
.then(prices => {
@@ -166,14 +167,14 @@ const PriceManager = (function() {
const cachedData = CacheManager.get(PRICES_CACHE_KEY);
if (cachedData) {
- console.log('Using cached price data');
+
return cachedData.value;
}
try {
const existingCache = localStorage.getItem(PRICES_CACHE_KEY);
if (existingCache) {
- console.log('Using localStorage cached price data');
+
return JSON.parse(existingCache).value;
}
} catch (e) {
@@ -230,4 +231,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
-console.log('PriceManager initialized');
+
diff --git a/basicswap/static/js/modules/summary-manager.js b/basicswap/static/js/modules/summary-manager.js
index 6f25360..251ebf2 100644
--- a/basicswap/static/js/modules/summary-manager.js
+++ b/basicswap/static/js/modules/summary-manager.js
@@ -261,9 +261,12 @@ const SummaryManager = (function() {
}
if (data.event) {
- publicAPI.fetchSummaryData()
- .then(() => {})
- .catch(() => {});
+ const summaryEvents = ['new_offer', 'offer_revoked', 'new_bid', 'bid_accepted', 'swap_completed'];
+ if (summaryEvents.includes(data.event)) {
+ publicAPI.fetchSummaryData()
+ .then(() => {})
+ .catch(() => {});
+ }
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
window.NotificationManager.handleWebSocketEvent(data);
@@ -334,9 +337,12 @@ const SummaryManager = (function() {
wsManager.addMessageHandler('message', (data) => {
if (data.event) {
- this.fetchSummaryData()
- .then(() => {})
- .catch(() => {});
+ const summaryEvents = ['new_offer', 'offer_revoked', 'new_bid', 'bid_accepted', 'swap_completed'];
+ if (summaryEvents.includes(data.event)) {
+ this.fetchSummaryData()
+ .then(() => {})
+ .catch(() => {});
+ }
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
window.NotificationManager.handleWebSocketEvent(data);
diff --git a/basicswap/static/js/modules/wallet-manager.js b/basicswap/static/js/modules/wallet-manager.js
index 4447765..dc4a1ee 100644
--- a/basicswap/static/js/modules/wallet-manager.js
+++ b/basicswap/static/js/modules/wallet-manager.js
@@ -180,8 +180,29 @@ const WalletManager = (function() {
if (coinSymbol) {
if (coinName === 'Particl') {
- const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
- const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
+ let isBlind = false;
+ let isAnon = false;
+
+ const flexContainer = el.closest('.flex');
+ if (flexContainer) {
+ const h4Element = flexContainer.querySelector('h4');
+ if (h4Element) {
+ isBlind = h4Element.textContent?.includes('Blind');
+ isAnon = h4Element.textContent?.includes('Anon');
+ }
+ }
+
+ if (!isBlind && !isAnon) {
+ const parentRow = el.closest('tr');
+ if (parentRow) {
+ const labelCell = parentRow.querySelector('td:first-child');
+ if (labelCell) {
+ isBlind = labelCell.textContent?.includes('Blind');
+ isAnon = labelCell.textContent?.includes('Anon');
+ }
+ }
+ }
+
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
} else if (coinName === 'Litecoin') {
@@ -248,8 +269,29 @@ const WalletManager = (function() {
const usdValue = (amount * price).toFixed(2);
if (coinName === 'Particl') {
- const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
- const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
+ let isBlind = false;
+ let isAnon = false;
+
+ const flexContainer = el.closest('.flex');
+ if (flexContainer) {
+ const h4Element = flexContainer.querySelector('h4');
+ if (h4Element) {
+ isBlind = h4Element.textContent?.includes('Blind');
+ isAnon = h4Element.textContent?.includes('Anon');
+ }
+ }
+
+ if (!isBlind && !isAnon) {
+ const parentRow = el.closest('tr');
+ if (parentRow) {
+ const labelCell = parentRow.querySelector('td:first-child');
+ if (labelCell) {
+ isBlind = labelCell.textContent?.includes('Blind');
+ isAnon = labelCell.textContent?.includes('Anon');
+ }
+ }
+ }
+
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
localStorage.setItem(`particl-${balanceType}-last-value`, usdValue);
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
diff --git a/basicswap/static/js/new_offer.js b/basicswap/static/js/new_offer.js
index 6716c2c..f4b615f 100644
--- a/basicswap/static/js/new_offer.js
+++ b/basicswap/static/js/new_offer.js
@@ -443,7 +443,45 @@ const UIEnhancer = {
const selectNameElement = select.nextElementSibling?.querySelector('.select-name');
if (selectNameElement) {
- selectNameElement.textContent = name;
+
+ if (select.id === 'coin_from' && name.includes(' - Balance: ')) {
+
+ const parts = name.split(' - Balance: ');
+ const coinName = parts[0];
+ const balanceInfo = parts[1] || '';
+
+
+ selectNameElement.innerHTML = '';
+ selectNameElement.style.display = 'flex';
+ selectNameElement.style.flexDirection = 'column';
+ selectNameElement.style.alignItems = 'flex-start';
+ selectNameElement.style.lineHeight = '1.2';
+
+
+ const coinNameDiv = document.createElement('div');
+ coinNameDiv.textContent = coinName;
+ coinNameDiv.style.fontWeight = 'normal';
+ coinNameDiv.style.color = 'inherit';
+
+
+ const balanceDiv = document.createElement('div');
+ balanceDiv.textContent = `Balance: ${balanceInfo}`;
+ balanceDiv.style.fontSize = '0.75rem';
+ balanceDiv.style.color = '#6b7280';
+ balanceDiv.style.marginTop = '1px';
+
+ selectNameElement.appendChild(coinNameDiv);
+ selectNameElement.appendChild(balanceDiv);
+
+
+
+ } else {
+
+ selectNameElement.textContent = name;
+ selectNameElement.style.display = 'block';
+ selectNameElement.style.flexDirection = '';
+ selectNameElement.style.alignItems = '';
+ }
}
updateSelectCache(select);
diff --git a/basicswap/static/js/ui/bids-tab-navigation.js b/basicswap/static/js/ui/bids-tab-navigation.js
index c88d732..ec0b8ea 100644
--- a/basicswap/static/js/ui/bids-tab-navigation.js
+++ b/basicswap/static/js/ui/bids-tab-navigation.js
@@ -43,7 +43,7 @@
});
window.bidsTabNavigationInitialized = true;
- console.log('Bids tab navigation initialized');
+ //console.log('Bids tab navigation initialized');
}
function handleInitialNavigation() {
@@ -54,14 +54,14 @@
const tabToActivate = localStorage.getItem('bidsTabToActivate');
if (tabToActivate) {
- //console.log('Activating tab from localStorage:', tabToActivate);
+
localStorage.removeItem('bidsTabToActivate');
activateTabWithRetry('#' + tabToActivate);
} else if (window.location.hash) {
- //console.log('Activating tab from hash:', window.location.hash);
+
activateTabWithRetry(window.location.hash);
} else {
- //console.log('Activating default tab: #all');
+
activateTabWithRetry('#all');
}
}
@@ -73,10 +73,10 @@
const hash = window.location.hash;
if (hash) {
- //console.log('Hash changed, activating tab:', hash);
+
activateTabWithRetry(hash);
} else {
- //console.log('Hash cleared, activating default tab: #all');
+
activateTabWithRetry('#all');
}
}
@@ -85,7 +85,7 @@
const normalizedTabId = tabId.startsWith('#') ? tabId : '#' + tabId;
if (normalizedTabId !== '#all' && normalizedTabId !== '#sent' && normalizedTabId !== '#received') {
- //console.log('Invalid tab ID, defaulting to #all');
+
activateTabWithRetry('#all');
return;
}
@@ -96,17 +96,17 @@
if (!tabButton) {
if (retryCount < 5) {
- //console.log('Tab button not found, retrying...', retryCount + 1);
+
setTimeout(() => {
activateTabWithRetry(normalizedTabId, retryCount + 1);
}, 100);
} else {
- //console.error('Failed to find tab button after retries');
+
}
return;
}
- //console.log('Activating tab:', normalizedTabId);
+
tabButton.click();
@@ -168,7 +168,7 @@
(tabId === '#sent' ? 'sent' : 'received');
if (typeof window.updateBidsTable === 'function') {
- //console.log('Triggering data load for', tabId);
+
window.updateBidsTable();
}
}
diff --git a/basicswap/templates/amm.html b/basicswap/templates/amm.html
index 4cc3398..40bab02 100644
--- a/basicswap/templates/amm.html
+++ b/basicswap/templates/amm.html
@@ -51,16 +51,11 @@
Add Bid
{% endif %}
-
-
-
-
- Refresh
-
+
-
+
@@ -290,6 +285,7 @@
Kill Orphans
+
Use "Check Processes" to see running AMM processes. Use "Kill Orphans" to clean up duplicate processes. Use "Force Start" to automatically clean up and start fresh.
@@ -974,7 +970,7 @@
if (localStorage.getItem('amm_create_default_refresh') === 'true') {
localStorage.removeItem('amm_create_default_refresh');
- console.log('[AMM] Page loaded after create default config - redirecting to fresh page');
+
setTimeout(function() {
window.location.href = window.location.pathname + window.location.search;
@@ -1066,7 +1062,7 @@
-
+
@@ -1086,7 +1082,7 @@
-
+
Maker
@@ -1094,15 +1090,14 @@
{% for c in coins %}
-
- {{ c[1] }}
-
+ {{ c[1] }}
{% endfor %}
+
-
+
Taker
@@ -1110,12 +1105,11 @@
{% for c in coins %}
-
- {{ c[1] }}
-
+ {{ c[1] }}
{% endfor %}
+
@@ -1123,13 +1117,18 @@
@@ -1348,7 +1347,7 @@
-
+
@@ -1370,7 +1369,7 @@
-
+
Maker
@@ -1378,15 +1377,14 @@
{% for c in coins %}
-
- {{ c[1] }}
-
+ {{ c[1] }}
{% endfor %}
+
-
+
Taker
@@ -1394,12 +1392,11 @@
{% for c in coins %}
-
- {{ c[1] }}
-
+ {{ c[1] }}
{% endfor %}
+
@@ -1407,13 +1404,18 @@
@@ -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;
{% include 'footer.html' %}
diff --git a/basicswap/templates/header.html b/basicswap/templates/header.html
index 5041143..a3cbf77 100644
--- a/basicswap/templates/header.html
+++ b/basicswap/templates/header.html
@@ -75,6 +75,7 @@
+
diff --git a/basicswap/templates/offer_new_1.html b/basicswap/templates/offer_new_1.html
index 777f744..8ae5c5a 100644
--- a/basicswap/templates/offer_new_1.html
+++ b/basicswap/templates/offer_new_1.html
@@ -155,7 +155,7 @@
Select coin you send
{% for c in coins_from %}
- {{ c[1] }}
+ {{ c[1] }} - Balance: {{ c[2] }}
{% endfor %}
@@ -167,6 +167,11 @@
+
+ 25%
+ 50%
+ 100%
+
@@ -332,6 +337,142 @@
+
+
+
{% include 'footer.html' %}
diff --git a/basicswap/templates/settings.html b/basicswap/templates/settings.html
index 2e333ab..27cdddf 100644
--- a/basicswap/templates/settings.html
+++ b/basicswap/templates/settings.html
@@ -49,6 +49,9 @@
General
+
+ Notifications
+
Tor
@@ -432,6 +435,153 @@
+
+
+
+
@@ -667,16 +667,7 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
var floatBalance;
var calculatedAmount;
- console.log('SetAmount Called with:', {
- percent: percent,
- balance: balance,
- cid: cid,
- blindBalance: blindBalance,
- anonBalance: anonBalance,
- selectedType: selectedType,
- blindBalanceType: typeof blindBalance,
- blindBalanceNumeric: Number(blindBalance)
- });
+
const safeParseFloat = (value) => {
const numValue = Number(value);
@@ -706,11 +697,7 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
calculatedAmount = Math.max(0, Math.floor(floatBalance * percent * 100000000) / 100000000);
- console.log('Calculated Amount:', {
- floatBalance: floatBalance,
- calculatedAmount: calculatedAmount,
- percent: percent
- });
+
if (percent === 1) {
calculatedAmount = floatBalance;
@@ -727,43 +714,6 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
if (subfeeCheckbox) {
subfeeCheckbox.checked = (percent === 1);
}
-
- console.log('Final Amount Set:', amountInput.value);
-}function setAmount(percent, balance, cid, blindBalance, anonBalance) {
- var amountInput = document.getElementById('amount');
- var typeSelect = document.getElementById('withdraw_type');
- var selectedType = typeSelect.value;
- var floatBalance;
- var calculatedAmount;
-
- switch(selectedType) {
- case 'plain':
- floatBalance = parseFloat(balance);
- break;
- case 'blind':
- floatBalance = parseFloat(blindBalance);
- break;
- case 'anon':
- floatBalance = parseFloat(anonBalance);
- break;
- default:
- floatBalance = parseFloat(balance);
- break;
- }
-
- calculatedAmount = Math.floor(floatBalance * percent * 100000000) / 100000000;
-
- if (percent === 1) {
- calculatedAmount = floatBalance;
- }
-
- amountInput.value = calculatedAmount.toFixed(8);
-
- var subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
- if (subfeeCheckbox) {
- subfeeCheckbox.checked = (percent === 1);
- }
-
}
@@ -818,17 +768,14 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
const specialCids = [6, 9];
- console.log("CID:", cid);
- console.log("Percent:", percent);
- console.log("Balance:", balance);
- console.log("Calculated Amount:", calculatedAmount);
+
if (specialCids.includes(parseInt(cid)) && percent === 1) {
amountInput.setAttribute('data-hidden', 'true');
amountInput.placeholder = 'Sweep All';
amountInput.value = '';
amountInput.disabled = true;
- console.log("Sweep All activated for special CID:", cid);
+
} else {
amountInput.value = calculatedAmount.toFixed(8);
amountInput.setAttribute('data-hidden', 'false');
@@ -840,17 +787,15 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
if (sweepAllCheckbox) {
if (specialCids.includes(parseInt(cid)) && percent === 1) {
sweepAllCheckbox.checked = true;
- console.log("Sweep All checkbox checked");
} else {
sweepAllCheckbox.checked = false;
- console.log("Sweep All checkbox unchecked");
}
}
let subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
if (subfeeCheckbox) {
subfeeCheckbox.checked = (percent === 1);
- console.log("Subfee checkbox status for CID", cid, ":", subfeeCheckbox.checked);
+
}
}
@@ -1148,6 +1093,375 @@ function confirmUTXOResize() {
}
document.addEventListener('DOMContentLoaded', function() {
+
+ if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') {
+ window.WebSocketManager.initialize();
+ }
+
+ window.currentBalances = window.currentBalances || {};
+
+ {% if w.cid == '1' %}
+ window.currentBalances = {
+ mainBalance: '{{ w.balance }}',
+ blindBalance: '{{ w.blind_balance }}',
+ anonBalance: '{{ w.anon_balance }}'
+ };
+ {% elif w.cid == '3' %}
+ window.currentBalances = {
+ mainBalance: '{{ w.balance }}',
+ mwebBalance: '{{ w.mweb_balance }}'
+ };
+ {% endif %}
+
+ function updateWalletBalances() {
+ fetch('/json/walletbalances', {
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ }
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`Server error: ${response.status} ${response.statusText}`);
+ }
+ return response.json();
+ })
+ .then(balanceData => {
+ if (balanceData.error) {
+ throw new Error(balanceData.error);
+ }
+
+ if (!Array.isArray(balanceData)) {
+ throw new Error('Invalid response format');
+ }
+
+
+ balanceData.forEach(coin => {
+ updateWalletDisplay(coin);
+ updatePercentageButtons(coin);
+ });
+
+
+ setTimeout(() => {
+ if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
+ window.WalletManager.updatePrices(true);
+ }
+
+ }, 100);
+ })
+ .catch(error => {
+ console.error('[Wallet] Error updating wallet balances:', error);
+ });
+ }
+
+ function updatePercentageButtons(coinData) {
+ const currentCoinId = {{ w.cid }};
+
+ const isPartVariant = (currentCoinId === 1 && (coinData.name === 'Particl' || coinData.name === 'Particl Anon' || coinData.name === 'Particl Blind'));
+ const isLtcVariant = (currentCoinId === 3 && (coinData.name === 'Litecoin' || coinData.name === 'Litecoin MWEB'));
+ const isCurrentCoin = (coinData.id === currentCoinId);
+
+ if (isPartVariant || isLtcVariant || isCurrentCoin) {
+ const buttons = document.querySelectorAll('button[onclick*="setAmount"]');
+ buttons.forEach(button => {
+ const onclick = button.getAttribute('onclick');
+ if (onclick) {
+ if (currentCoinId === 1) {
+ window.currentBalances = window.currentBalances || {};
+
+ const anonBalance = coinData.name === 'Particl Anon' ? coinData.balance :
+ (window.currentBalances.anonBalance || '0');
+ const blindBalance = coinData.name === 'Particl Blind' ? coinData.balance :
+ (window.currentBalances.blindBalance || '0');
+ const mainBalance = coinData.name === 'Particl' ? coinData.balance :
+ (window.currentBalances.mainBalance || '0');
+
+ if (coinData.name === 'Particl') window.currentBalances.mainBalance = coinData.balance;
+ if (coinData.name === 'Particl Anon') window.currentBalances.anonBalance = coinData.balance;
+ if (coinData.name === 'Particl Blind') window.currentBalances.blindBalance = coinData.balance;
+
+ const newOnclick = onclick.replace(
+ /setAmount\([^,]+,\s*'[^']*',\s*\d+,\s*'[^']*',\s*'[^']*'\)/,
+ `setAmount(${onclick.match(/setAmount\(([^,]+)/)[1]}, '${mainBalance}', ${currentCoinId}, '${blindBalance}', '${anonBalance}')`
+ );
+ button.setAttribute('onclick', newOnclick);
+ } else if (currentCoinId === 3) {
+ window.currentBalances = window.currentBalances || {};
+
+ const mwebBalance = coinData.name === 'Litecoin MWEB' ? coinData.balance :
+ (window.currentBalances.mwebBalance || '0');
+ const mainBalance = coinData.name === 'Litecoin' ? coinData.balance :
+ (window.currentBalances.mainBalance || '0');
+
+ if (coinData.name === 'Litecoin') window.currentBalances.mainBalance = coinData.balance;
+ if (coinData.name === 'Litecoin MWEB') window.currentBalances.mwebBalance = coinData.balance;
+
+ const newOnclick = onclick.replace(
+ /setAmount\([^,]+,\s*'[^']*',\s*\d+,\s*'[^']*'\)/,
+ `setAmount(${onclick.match(/setAmount\(([^,]+)/)[1]}, '${mainBalance}', ${currentCoinId}, '${mwebBalance}')`
+ );
+ button.setAttribute('onclick', newOnclick);
+ } else {
+ const newOnclick = onclick.replace(
+ /setAmount\([^,]+,\s*'[^']*',\s*\d+\)/,
+ `setAmount(${onclick.match(/setAmount\(([^,]+)/)[1]}, '${coinData.balance}', ${currentCoinId})`
+ );
+ button.setAttribute('onclick', newOnclick);
+ }
+ }
+ });
+ }
+ }
+
+ function updateWalletDisplay(coinData) {
+
+ if (coinData.name === 'Particl') {
+
+ updateSpecificBalance('Particl', 'Balance:', coinData.balance, coinData.ticker || 'PART');
+ } else if (coinData.name === 'Particl Anon') {
+
+ updateSpecificBalance('Particl', 'Anon Balance:', coinData.balance, coinData.ticker || 'PART');
+
+ removePendingBalance('Particl', 'Anon Balance:');
+ if (coinData.pending && parseFloat(coinData.pending) > 0) {
+ updatePendingBalance('Particl', 'Anon Balance:', coinData.pending, coinData.ticker || 'PART', 'Pending:', coinData);
+ }
+ } else if (coinData.name === 'Particl Blind') {
+
+ updateSpecificBalance('Particl', 'Blind Balance:', coinData.balance, coinData.ticker || 'PART');
+
+ removePendingBalance('Particl', 'Blind Balance:');
+ if (coinData.pending && parseFloat(coinData.pending) > 0) {
+ updatePendingBalance('Particl', 'Blind Balance:', coinData.pending, coinData.ticker || 'PART', 'Unconfirmed:', coinData);
+ }
+ } else if (coinData.name === 'Litecoin MWEB') {
+
+ updateSpecificBalance('Litecoin', 'MWEB Balance:', coinData.balance, coinData.ticker || 'LTC');
+
+ if (coinData.pending && parseFloat(coinData.pending) > 0) {
+ updatePendingBalance('Litecoin', 'MWEB Balance:', coinData.pending, coinData.ticker || 'LTC', 'Pending:', coinData);
+ }
+ } else {
+
+ updateSpecificBalance(coinData.name, 'Balance:', coinData.balance, coinData.ticker || coinData.name);
+
+
+ if (coinData.pending && parseFloat(coinData.pending) > 0) {
+ updatePendingDisplay(coinData);
+ } else {
+ removePendingDisplay(coinData.name);
+ }
+ }
+ }
+
+ function updateSpecificBalance(coinName, labelText, balance, ticker, isPending = false) {
+ const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
+ let found = false;
+
+ balanceElements.forEach(element => {
+ const elementCoinName = element.getAttribute('data-coinname');
+
+ if (elementCoinName === coinName) {
+ const parentRow = element.closest('tr');
+ if (parentRow) {
+ const labelCell = parentRow.querySelector('td:first-child');
+ if (labelCell) {
+ const currentLabel = labelCell.textContent.trim();
+
+ if (currentLabel.includes(labelText)) {
+ if (isPending) {
+ element.textContent = `+${balance} ${ticker}`;
+ } else {
+ element.textContent = `${balance} ${ticker}`;
+ }
+
+
+ setTimeout(() => {
+ if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
+ window.WalletManager.updatePrices(false);
+ }
+ }, 50);
+
+ found = true;
+ }
+ }
+ }
+ }
+ });
+ }
+
+ function updatePendingDisplay(coinData) {
+
+ const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
+
+ balanceElements.forEach(element => {
+ const elementCoinName = element.getAttribute('data-coinname');
+
+ if (elementCoinName === coinData.name) {
+ const parentRow = element.closest('tr');
+ if (parentRow) {
+ const labelCell = parentRow.querySelector('td:first-child');
+ if (labelCell && labelCell.textContent.includes('Balance:')) {
+
+ const balanceCell = parentRow.querySelector('td:nth-child(2)');
+ if (balanceCell) {
+ let pendingSpan = balanceCell.querySelector('.inline-block.py-1.px-2.rounded-full.bg-green-100');
+
+ if (coinData.pending && parseFloat(coinData.pending) > 0) {
+ if (!pendingSpan) {
+
+ pendingSpan = document.createElement('span');
+ pendingSpan.className = 'inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500';
+ balanceCell.appendChild(document.createTextNode(' '));
+ balanceCell.appendChild(pendingSpan);
+ }
+
+ const ticker = coinData.ticker || coinData.name;
+
+ pendingSpan.textContent = `Pending: +${coinData.pending} ${ticker}`;
+ } else {
+
+ if (pendingSpan) {
+ pendingSpan.remove();
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+ }
+
+ function removePendingDisplay(coinName) {
+ const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
+
+ balanceElements.forEach(element => {
+ const elementCoinName = element.getAttribute('data-coinname');
+
+ if (elementCoinName === coinName) {
+ const parentRow = element.closest('tr');
+ if (parentRow) {
+ const balanceCell = parentRow.querySelector('td:nth-child(2)');
+ if (balanceCell) {
+ const pendingSpan = balanceCell.querySelector('.inline-block.py-1.px-2.rounded-full.bg-green-100');
+ if (pendingSpan) {
+ pendingSpan.remove();
+ }
+ }
+ }
+ }
+ });
+ }
+
+ function updatePendingBalance(coinName, balanceType, pendingAmount, ticker, pendingLabel, coinData) {
+ const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
+
+ balanceElements.forEach(element => {
+ const elementCoinName = element.getAttribute('data-coinname');
+
+ if (elementCoinName === coinName) {
+ const parentRow = element.closest('tr');
+ if (parentRow) {
+ const labelCell = parentRow.querySelector('td:first-child');
+ if (labelCell && labelCell.textContent.includes(balanceType)) {
+
+ const balanceCell = parentRow.querySelector('td:nth-child(2)');
+ if (balanceCell) {
+ let pendingSpan = balanceCell.querySelector('.inline-block.py-1.px-2.rounded-full.bg-green-100');
+
+ if (pendingAmount && parseFloat(pendingAmount) > 0) {
+ if (!pendingSpan) {
+
+ pendingSpan = document.createElement('span');
+ pendingSpan.className = 'inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500';
+ balanceCell.appendChild(document.createTextNode(' '));
+ balanceCell.appendChild(pendingSpan);
+ }
+
+
+ pendingSpan.textContent = `${pendingLabel} +${pendingAmount} ${ticker}`;
+ } else if (pendingSpan) {
+
+ pendingSpan.remove();
+ }
+ }
+ }
+ }
+ }
+ });
+ }
+
+ function removePendingBalance(coinName, balanceType) {
+ const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
+
+ balanceElements.forEach(element => {
+ const elementCoinName = element.getAttribute('data-coinname');
+
+ if (elementCoinName === coinName) {
+ const parentRow = element.closest('tr');
+ if (parentRow) {
+ const labelCell = parentRow.querySelector('td:first-child');
+ if (labelCell && labelCell.textContent.includes(balanceType)) {
+
+ const balanceCell = parentRow.querySelector('td:nth-child(2)');
+ if (balanceCell) {
+ const pendingSpan = balanceCell.querySelector('.inline-block.py-1.px-2.rounded-full.bg-green-100');
+ if (pendingSpan) {
+ pendingSpan.remove();
+ }
+ }
+ }
+ }
+ }
+ });
+ }
+
+ function setupWalletWebSocketUpdates() {
+ window.BalanceUpdatesManager.setup({
+ contextKey: 'wallet',
+ balanceUpdateCallback: updateWalletBalances,
+ swapEventCallback: updateWalletBalances,
+ errorContext: 'Wallet',
+ enablePeriodicRefresh: true,
+ periodicInterval: 120000
+ });
+
+ if (window.WebSocketManager && typeof window.WebSocketManager.addMessageHandler === 'function') {
+ const priceHandlerId = window.WebSocketManager.addMessageHandler('message', (data) => {
+ if (data && data.event) {
+ if (data.event === 'price_updated' || data.event === 'prices_updated') {
+ clearTimeout(window.walletPriceUpdateTimeout);
+ window.walletPriceUpdateTimeout = setTimeout(() => {
+ if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
+ window.WalletManager.updatePrices(true);
+ }
+ }, 500);
+ }
+ }
+ });
+ window.walletPriceHandlerId = priceHandlerId;
+ }
+ }
+
+ function cleanupWalletBalanceUpdates() {
+ window.BalanceUpdatesManager.cleanup('wallet');
+
+ if (window.walletPriceHandlerId && window.WebSocketManager) {
+ window.WebSocketManager.removeMessageHandler('message', window.walletPriceHandlerId);
+ }
+
+ clearTimeout(window.walletPriceUpdateTimeout);
+ }
+
+ window.BalanceUpdatesManager.initialize();
+ setupWalletWebSocketUpdates();
+
+ setTimeout(() => {
+ updateWalletBalances();
+ }, 1000);
+
+ window.addEventListener('beforeunload', cleanupWalletBalanceUpdates);
+
document.getElementById('confirmYes').addEventListener('click', function() {
if (typeof confirmCallback === 'function') {
confirmCallback();
diff --git a/basicswap/templates/wallets.html b/basicswap/templates/wallets.html
index aa697f1..464b5ba 100644
--- a/basicswap/templates/wallets.html
+++ b/basicswap/templates/wallets.html
@@ -21,8 +21,7 @@
@@ -190,5 +189,426 @@
{% include 'footer.html' %}
+
+
+