GUI: Dynamic balances (WS) + Better Notifications (Toasts) + various fixes. (#332)

* GUI: Dynamic balances (WS) + various fixes.

* BLACK + FLAKE8

* Clean-up.

* Fix refresh intervals + Fix pending balance.

* Fix amounts scientific notation (1e-8)

* Better Notifications (Toasts)

* Removed duplicated code + Balance skip if the chain is still syncing.

* Fix MWEB doesnt show as pending + Various fixes.

* Fix: USD values are off with part blind.

* Fix: Percentage change buttons on wallet page.

* Cleanup debug on wallet page.

* Use ZMQ for part balances.

* Fix ZMQ config.

* Fix PART price in chart.
This commit is contained in:
Gerlof van Ek
2025-07-22 23:45:45 +02:00
committed by GitHub
parent d6ef4f2edb
commit a5cc83157d
24 changed files with 3199 additions and 302 deletions

View File

@@ -186,6 +186,53 @@ def validOfferStateToReceiveBid(offer_state):
return False
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"

View File

@@ -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")

View File

@@ -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")

View File

@@ -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,

View File

@@ -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;

View File

@@ -23,13 +23,7 @@ const AmmTablesManager = (function() {
}
function debugLog(message, data) {
// if (isDebugEnabled()) {
// if (data) {
// console.log(`[AmmTables] ${message}`, data);
// } else {
// console.log(`[AmmTables] ${message}`);
// }
// }
}
function initializeTabs() {
@@ -309,7 +303,10 @@ const AmmTablesManager = (function() {
`;
});
offersBody.innerHTML = tableHtml;
if (offersBody.innerHTML.trim() !== tableHtml.trim()) {
offersBody.innerHTML = tableHtml;
}
}
function renderBidsTable(stateData) {
@@ -441,7 +438,10 @@ const AmmTablesManager = (function() {
`;
});
bidsBody.innerHTML = tableHtml;
if (bidsBody.innerHTML.trim() !== tableHtml.trim()) {
bidsBody.innerHTML = tableHtml;
}
}
function formatDuration(seconds) {
@@ -724,6 +724,429 @@ const AmmTablesManager = (function() {
}
}
function shouldDropdownOptionsShowBalance(select) {
const isMakerDropdown = select.id.includes('coin-from');
const isTakerDropdown = select.id.includes('coin-to');
const addModal = document.getElementById('add-amm-modal');
const editModal = document.getElementById('edit-amm-modal');
const addModalVisible = addModal && !addModal.classList.contains('hidden');
const editModalVisible = editModal && !editModal.classList.contains('hidden');
let isBidModal = false;
if (addModalVisible) {
const dataType = addModal.getAttribute('data-amm-type');
if (dataType) {
isBidModal = dataType === 'bid';
} else {
const modalTitle = document.getElementById('add-modal-title');
isBidModal = modalTitle && modalTitle.textContent && modalTitle.textContent.includes('Bid');
}
} else if (editModalVisible) {
const dataType = editModal.getAttribute('data-amm-type');
if (dataType) {
isBidModal = dataType === 'bid';
} else {
const modalTitle = document.getElementById('edit-modal-title');
isBidModal = modalTitle && modalTitle.textContent && modalTitle.textContent.includes('Bid');
}
}
const result = isBidModal ? isTakerDropdown : isMakerDropdown;
console.log(`[DEBUG] shouldDropdownOptionsShowBalance: ${select.id}, isBidModal=${isBidModal}, isMaker=${isMakerDropdown}, isTaker=${isTakerDropdown}, result=${result}`);
return result;
}
function refreshDropdownOptions() {
const dropdownIds = ['add-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-from', 'edit-amm-coin-to'];
dropdownIds.forEach(dropdownId => {
const select = document.getElementById(dropdownId);
if (!select || select.style.display !== 'none') return;
const wrapper = select.parentNode.querySelector('.relative');
if (!wrapper) return;
const dropdown = wrapper.querySelector('[role="listbox"]');
if (!dropdown) return;
const options = dropdown.querySelectorAll('[data-value]');
options.forEach(optionElement => {
const coinValue = optionElement.getAttribute('data-value');
const originalOption = Array.from(select.options).find(opt => opt.value === coinValue);
if (!originalOption) return;
const textContainer = optionElement.querySelector('div.flex.flex-col, div.flex.items-center');
if (!textContainer) return;
textContainer.innerHTML = '';
const shouldShowBalance = shouldDropdownOptionsShowBalance(select);
const fullText = originalOption.textContent.trim();
const balance = originalOption.getAttribute('data-balance') || '0.00000000';
console.log(`[DEBUG] refreshDropdownOptions: ${select.id}, option=${coinValue}, shouldShowBalance=${shouldShowBalance}, balance=${balance}`);
if (shouldShowBalance) {
textContainer.className = 'flex flex-col';
const coinName = fullText.includes(' - Balance: ') ? fullText.split(' - Balance: ')[0] : fullText;
const coinNameSpan = document.createElement('span');
coinNameSpan.textContent = coinName;
coinNameSpan.className = 'text-gray-900 dark:text-white';
const balanceSpan = document.createElement('span');
balanceSpan.textContent = `Balance: ${balance}`;
balanceSpan.className = 'text-gray-500 dark:text-gray-400 text-xs';
textContainer.appendChild(coinNameSpan);
textContainer.appendChild(balanceSpan);
} else {
textContainer.className = 'flex items-center';
const coinNameSpan = document.createElement('span');
const coinName = fullText.includes(' - Balance: ') ? fullText.split(' - Balance: ')[0] : fullText;
coinNameSpan.textContent = coinName;
coinNameSpan.className = 'text-gray-900 dark:text-white';
textContainer.appendChild(coinNameSpan);
}
});
});
}
function refreshDropdownBalances() {
const dropdownIds = ['add-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-from', 'edit-amm-coin-to'];
dropdownIds.forEach(dropdownId => {
const select = document.getElementById(dropdownId);
if (!select || select.style.display !== 'none') return;
const wrapper = select.parentNode.querySelector('.relative');
if (!wrapper) return;
const dropdownItems = wrapper.querySelectorAll('[data-value]');
dropdownItems.forEach(item => {
const value = item.getAttribute('data-value');
const option = select.querySelector(`option[value="${value}"]`);
if (option) {
const balance = option.getAttribute('data-balance') || '0.00000000';
const pendingBalance = option.getAttribute('data-pending-balance') || '';
const balanceDiv = item.querySelector('.text-xs');
if (balanceDiv) {
balanceDiv.textContent = `Balance: ${balance}`;
let pendingDiv = item.querySelector('.text-green-500');
if (pendingBalance && parseFloat(pendingBalance) > 0) {
if (!pendingDiv) {
pendingDiv = document.createElement('div');
pendingDiv.className = 'text-green-500 text-xs';
balanceDiv.parentNode.appendChild(pendingDiv);
}
pendingDiv.textContent = `+${pendingBalance} pending`;
} else if (pendingDiv) {
pendingDiv.remove();
}
}
}
});
const selectedOption = select.options[select.selectedIndex];
if (selectedOption) {
const textContainer = wrapper.querySelector('button .flex-grow');
const balanceDiv = textContainer ? textContainer.querySelector('.text-xs') : null;
if (balanceDiv) {
const balance = selectedOption.getAttribute('data-balance') || '0.00000000';
const pendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
balanceDiv.textContent = `Balance: ${balance}`;
let pendingDiv = textContainer.querySelector('.text-green-500');
if (pendingBalance && parseFloat(pendingBalance) > 0) {
if (!pendingDiv) {
pendingDiv = document.createElement('div');
pendingDiv.className = 'text-green-500 text-xs';
textContainer.appendChild(pendingDiv);
}
pendingDiv.textContent = `+${pendingBalance} pending`;
} else if (pendingDiv) {
pendingDiv.remove();
}
}
}
});
}
function refreshOfferDropdownBalanceDisplay() {
refreshDropdownBalances();
}
function refreshBidDropdownBalanceDisplay() {
refreshDropdownBalances();
}
function refreshDropdownBalanceDisplay(modalType = null) {
if (modalType === 'offer') {
refreshOfferDropdownBalanceDisplay();
} else if (modalType === 'bid') {
refreshBidDropdownBalanceDisplay();
} else {
const addModal = document.getElementById('add-amm-modal');
const editModal = document.getElementById('edit-amm-modal');
const addModalVisible = addModal && !addModal.classList.contains('hidden');
const editModalVisible = editModal && !editModal.classList.contains('hidden');
let detectedType = null;
if (addModalVisible) {
detectedType = addModal.getAttribute('data-amm-type');
} else if (editModalVisible) {
detectedType = editModal.getAttribute('data-amm-type');
}
if (detectedType === 'offer') {
refreshOfferDropdownBalanceDisplay();
} else if (detectedType === 'bid') {
refreshBidDropdownBalanceDisplay();
}
}
}
function updateDropdownsForModalType(modalPrefix) {
const coinFromSelect = document.getElementById(`${modalPrefix}-amm-coin-from`);
const coinToSelect = document.getElementById(`${modalPrefix}-amm-coin-to`);
if (!coinFromSelect || !coinToSelect) return;
const balanceData = {};
Array.from(coinFromSelect.options).forEach(option => {
const balance = option.getAttribute('data-balance');
if (balance) {
balanceData[option.value] = balance;
}
});
Array.from(coinToSelect.options).forEach(option => {
const balance = option.getAttribute('data-balance');
if (balance) {
balanceData[option.value] = balance;
}
});
updateDropdownOptions(coinFromSelect, balanceData);
updateDropdownOptions(coinToSelect, balanceData);
}
function updateDropdownOptions(select, balanceData, pendingData = {}) {
Array.from(select.options).forEach(option => {
const coinName = option.value;
const balance = balanceData[coinName] || '0.00000000';
const pending = pendingData[coinName] || '0.0';
option.setAttribute('data-balance', balance);
option.setAttribute('data-pending-balance', pending);
option.textContent = coinName;
});
}
function createSimpleDropdown(select, showBalance = false) {
if (!select) return;
const existingWrapper = select.parentNode.querySelector('.relative');
if (existingWrapper) {
existingWrapper.remove();
select.style.display = '';
}
select.style.display = 'none';
const wrapper = document.createElement('div');
wrapper.className = 'relative';
const button = document.createElement('button');
button.type = 'button';
button.className = 'flex items-center justify-between w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:text-white';
button.style.minHeight = '60px';
const displayContent = document.createElement('div');
displayContent.className = 'flex items-center';
const icon = document.createElement('img');
icon.className = 'w-5 h-5 mr-2';
icon.alt = '';
const textContainer = document.createElement('div');
textContainer.className = 'flex-grow text-left';
const arrow = document.createElement('div');
arrow.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>`;
displayContent.appendChild(icon);
displayContent.appendChild(textContainer);
button.appendChild(displayContent);
button.appendChild(arrow);
const dropdown = document.createElement('div');
dropdown.className = 'absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden dark:bg-gray-700 dark:border-gray-600 max-h-60 overflow-y-auto';
Array.from(select.options).forEach(option => {
const item = document.createElement('div');
item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer';
item.setAttribute('data-value', option.value);
const itemIcon = document.createElement('img');
itemIcon.className = 'w-5 h-5 mr-2';
itemIcon.src = `/static/images/coins/${getImageFilename(option.value)}`;
itemIcon.alt = '';
const itemText = document.createElement('div');
const coinName = option.textContent.trim();
const balance = option.getAttribute('data-balance') || '0.00000000';
const pendingBalance = option.getAttribute('data-pending-balance') || '';
if (showBalance) {
itemText.className = 'flex flex-col';
let html = `
<div class="text-gray-900 dark:text-white">${coinName}</div>
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${balance}</div>
`;
if (pendingBalance && parseFloat(pendingBalance) > 0) {
html += `<div class="text-green-500 text-xs">+${pendingBalance} pending</div>`;
}
itemText.innerHTML = html;
} else {
itemText.className = 'text-gray-900 dark:text-white';
itemText.textContent = coinName;
}
item.appendChild(itemIcon);
item.appendChild(itemText);
item.addEventListener('click', function() {
select.value = this.getAttribute('data-value');
const selectedOption = select.options[select.selectedIndex];
const selectedCoinName = selectedOption.textContent.trim();
const selectedBalance = selectedOption.getAttribute('data-balance') || '0.00000000';
const selectedPendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
icon.src = itemIcon.src;
if (showBalance) {
let html = `
<div class="text-gray-900 dark:text-white">${selectedCoinName}</div>
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
`;
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
}
textContainer.innerHTML = html;
textContainer.className = 'flex-grow text-left flex flex-col justify-center';
} else {
textContainer.textContent = selectedCoinName;
textContainer.className = 'flex-grow text-left';
}
dropdown.classList.add('hidden');
const event = new Event('change', { bubbles: true });
select.dispatchEvent(event);
});
dropdown.appendChild(item);
});
const selectedOption = select.options[select.selectedIndex];
if (selectedOption) {
const selectedCoinName = selectedOption.textContent.trim();
const selectedBalance = selectedOption.getAttribute('data-balance') || '0.00000000';
const selectedPendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`;
if (showBalance) {
let html = `
<div class="text-gray-900 dark:text-white">${selectedCoinName}</div>
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
`;
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
}
textContainer.innerHTML = html;
textContainer.className = 'flex-grow text-left flex flex-col justify-center';
} else {
textContainer.textContent = selectedCoinName;
textContainer.className = 'flex-grow text-left';
}
}
button.addEventListener('click', function() {
dropdown.classList.toggle('hidden');
});
document.addEventListener('click', function(e) {
if (!wrapper.contains(e.target)) {
dropdown.classList.add('hidden');
}
});
wrapper.appendChild(button);
wrapper.appendChild(dropdown);
select.parentNode.insertBefore(wrapper, select);
}
function setupButtonHandlers() {
const addOfferButton = document.getElementById('add-new-offer-btn');
if (addOfferButton) {
@@ -844,6 +1267,40 @@ const AmmTablesManager = (function() {
modalTitle.textContent = `Add New ${type.charAt(0).toUpperCase() + type.slice(1)}`;
}
const modal = document.getElementById('add-amm-modal');
if (modal) {
modal.classList.remove('hidden');
modal.setAttribute('data-amm-type', type);
}
setTimeout(() => {
updateDropdownsForModalType('add');
initializeCustomSelects(type);
refreshDropdownBalanceDisplay(type);
if (typeof fetchBalanceData === 'function') {
fetchBalanceData()
.then(balanceData => {
if (type === 'offer' && typeof updateOfferDropdownBalances === 'function') {
updateOfferDropdownBalances(balanceData);
} else if (type === 'bid' && typeof updateBidDropdownBalances === 'function') {
updateBidDropdownBalances(balanceData);
}
})
.catch(error => {
console.error('Error updating dropdown balances:', error);
});
}
}, 50);
document.getElementById('add-amm-type').value = type;
document.getElementById('add-amm-name').value = 'Unnamed Offer';
@@ -940,11 +1397,6 @@ const AmmTablesManager = (function() {
if (type === 'offer') {
setupBiddingControls('add');
}
const modal = document.getElementById('add-amm-modal');
if (modal) {
modal.classList.remove('hidden');
}
}
function closeAddModal() {
@@ -1269,6 +1721,40 @@ const AmmTablesManager = (function() {
modalTitle.textContent = `Edit ${type.charAt(0).toUpperCase() + type.slice(1)}`;
}
const modal = document.getElementById('edit-amm-modal');
if (modal) {
modal.classList.remove('hidden');
modal.setAttribute('data-amm-type', type);
}
setTimeout(() => {
updateDropdownsForModalType('edit');
initializeCustomSelects(type);
refreshDropdownBalanceDisplay(type);
if (typeof fetchBalanceData === 'function') {
fetchBalanceData()
.then(balanceData => {
if (type === 'offer' && typeof updateOfferDropdownBalances === 'function') {
updateOfferDropdownBalances(balanceData);
} else if (type === 'bid' && typeof updateBidDropdownBalances === 'function') {
updateBidDropdownBalances(balanceData);
}
})
.catch(error => {
console.error('Error updating dropdown balances:', error);
});
}
}, 50);
document.getElementById('edit-amm-type').value = type;
document.getElementById('edit-amm-id').value = id || '';
document.getElementById('edit-amm-original-name').value = name;
@@ -1282,8 +1768,12 @@ const AmmTablesManager = (function() {
coinFromSelect.value = item.coin_from || '';
coinToSelect.value = item.coin_to || '';
coinFromSelect.dispatchEvent(new Event('change', { bubbles: true }));
coinToSelect.dispatchEvent(new Event('change', { bubbles: true }));
if (coinFromSelect) {
coinFromSelect.dispatchEvent(new Event('change', { bubbles: true }));
}
if (coinToSelect) {
coinToSelect.dispatchEvent(new Event('change', { bubbles: true }));
}
document.getElementById('edit-amm-amount').value = item.amount || '';
@@ -1370,11 +1860,6 @@ const AmmTablesManager = (function() {
setupBiddingControls('edit');
populateBiddingControls('edit', item);
}
const modal = document.getElementById('edit-amm-modal');
if (modal) {
modal.classList.remove('hidden');
}
} catch (error) {
alert(`Error processing the configuration: ${error.message}`);
debugLog('Error opening edit modal:', error);
@@ -1808,7 +2293,7 @@ const AmmTablesManager = (function() {
}
}
function initializeCustomSelects() {
function initializeCustomSelects(modalType = null) {
const coinSelects = [
document.getElementById('add-amm-coin-from'),
document.getElementById('add-amm-coin-to'),
@@ -1821,116 +2306,16 @@ const AmmTablesManager = (function() {
document.getElementById('edit-offer-swap-type')
];
function createCoinDropdown(select) {
if (!select) return;
const wrapper = document.createElement('div');
wrapper.className = 'relative';
const display = document.createElement('div');
display.className = 'flex items-center w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white cursor-pointer';
const icon = document.createElement('img');
icon.className = 'w-5 h-5 mr-2';
icon.alt = '';
const text = document.createElement('span');
text.className = 'flex-grow';
const arrow = document.createElement('span');
arrow.className = 'ml-2';
arrow.innerHTML = `
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
`;
display.appendChild(icon);
display.appendChild(text);
display.appendChild(arrow);
const dropdown = document.createElement('div');
dropdown.className = 'absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden dark:bg-gray-700 dark:border-gray-600 max-h-60 overflow-y-auto';
Array.from(select.options).forEach(option => {
const item = document.createElement('div');
item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer text-gray-900 dark:text-white';
item.setAttribute('data-value', option.value);
item.setAttribute('data-symbol', option.getAttribute('data-symbol') || '');
const optionIcon = document.createElement('img');
optionIcon.className = 'w-5 h-5 mr-2';
optionIcon.src = `/static/images/coins/${getImageFilename(option.value)}`;
optionIcon.alt = '';
const optionText = document.createElement('span');
optionText.textContent = option.textContent.trim();
item.appendChild(optionIcon);
item.appendChild(optionText);
item.addEventListener('click', function() {
select.value = this.getAttribute('data-value');
text.textContent = optionText.textContent;
icon.src = optionIcon.src;
dropdown.classList.add('hidden');
const event = new Event('change', { bubbles: true });
select.dispatchEvent(event);
if (select.id === 'add-amm-coin-from' || select.id === 'add-amm-coin-to') {
const coinFrom = document.getElementById('add-amm-coin-from');
const coinTo = document.getElementById('add-amm-coin-to');
const swapType = document.getElementById('add-offer-swap-type');
if (coinFrom && coinTo && swapType) {
updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType);
}
} else if (select.id === 'edit-amm-coin-from' || select.id === 'edit-amm-coin-to') {
const coinFrom = document.getElementById('edit-amm-coin-from');
const coinTo = document.getElementById('edit-amm-coin-to');
const swapType = document.getElementById('edit-offer-swap-type');
if (coinFrom && coinTo && swapType) {
updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType);
}
}
});
dropdown.appendChild(item);
});
const selectedOption = select.options[select.selectedIndex];
text.textContent = selectedOption.textContent.trim();
icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`;
display.addEventListener('click', function(e) {
e.stopPropagation();
dropdown.classList.toggle('hidden');
});
document.addEventListener('click', function() {
dropdown.classList.add('hidden');
});
wrapper.appendChild(display);
wrapper.appendChild(dropdown);
select.parentNode.insertBefore(wrapper, select);
select.style.display = 'none';
select.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
text.textContent = selectedOption.textContent.trim();
icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`;
});
}
function createSwapTypeDropdown(select) {
if (!select) return;
if (select.style.display === 'none' && select.parentNode.querySelector('.relative')) {
return; // Custom dropdown already exists
}
const wrapper = document.createElement('div');
wrapper.className = 'relative';
@@ -1980,7 +2365,9 @@ const AmmTablesManager = (function() {
});
const selectedOption = select.options[select.selectedIndex];
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
if (selectedOption) {
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
}
display.addEventListener('click', function(e) {
if (select.disabled) return;
@@ -2000,7 +2387,9 @@ const AmmTablesManager = (function() {
select.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
if (selectedOption) {
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
}
});
const observer = new MutationObserver(function(mutations) {
@@ -2022,7 +2411,18 @@ const AmmTablesManager = (function() {
}
}
coinSelects.forEach(select => createCoinDropdown(select));
coinSelects.forEach(select => {
if (!select) return;
let showBalance = false;
if (modalType === 'offer' && select.id.includes('coin-from')) {
showBalance = true; // OFFER: maker shows balance
} else if (modalType === 'bid' && select.id.includes('coin-to')) {
showBalance = true; // BID: taker shows balance
}
createSimpleDropdown(select, showBalance);
});
swapTypeSelects.forEach(select => createSwapTypeDropdown(select));
}
@@ -2301,19 +2701,27 @@ const AmmTablesManager = (function() {
if (refreshButton) {
refreshButton.addEventListener('click', async function() {
if (refreshButton.disabled) return;
const icon = refreshButton.querySelector('svg');
refreshButton.disabled = true;
if (icon) {
icon.classList.add('animate-spin');
}
await initializePrices();
updateTables();
setTimeout(() => {
if (icon) {
icon.classList.remove('animate-spin');
}
}, 1000);
try {
await initializePrices();
updateTables();
} finally {
setTimeout(() => {
if (icon) {
icon.classList.remove('animate-spin');
}
refreshButton.disabled = false;
}, 500); // Reduced from 1000ms to 500ms
}
});
}
@@ -2326,7 +2734,11 @@ const AmmTablesManager = (function() {
return {
updateTables,
startRefreshTimer,
stopRefreshTimer
stopRefreshTimer,
refreshDropdownBalanceDisplay,
refreshOfferDropdownBalanceDisplay,
refreshBidDropdownBalanceDisplay,
refreshDropdownOptions
};
}

View File

@@ -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;
}
});

View File

@@ -0,0 +1,216 @@
const BalanceUpdatesManager = (function() {
'use strict';
const config = {
balanceUpdateDelay: 2000,
swapEventDelay: 5000,
periodicRefreshInterval: 120000,
walletPeriodicRefreshInterval: 60000,
};
const state = {
handlers: new Map(),
timeouts: new Map(),
intervals: new Map(),
initialized: false
};
function fetchBalanceData() {
return fetch('/json/walletbalances', {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`Server error: ${response.status} ${response.statusText}`);
}
return response.json();
})
.then(balanceData => {
if (balanceData.error) {
throw new Error(balanceData.error);
}
if (!Array.isArray(balanceData)) {
throw new Error('Invalid response format');
}
return balanceData;
});
}
function clearTimeoutByKey(key) {
if (state.timeouts.has(key)) {
clearTimeout(state.timeouts.get(key));
state.timeouts.delete(key);
}
}
function setTimeoutByKey(key, callback, delay) {
clearTimeoutByKey(key);
const timeoutId = setTimeout(callback, delay);
state.timeouts.set(key, timeoutId);
}
function clearIntervalByKey(key) {
if (state.intervals.has(key)) {
clearInterval(state.intervals.get(key));
state.intervals.delete(key);
}
}
function setIntervalByKey(key, callback, interval) {
clearIntervalByKey(key);
const intervalId = setInterval(callback, interval);
state.intervals.set(key, intervalId);
}
function handleBalanceUpdate(contextKey, updateCallback, errorContext) {
clearTimeoutByKey(`${contextKey}_balance_update`);
setTimeoutByKey(`${contextKey}_balance_update`, () => {
fetchBalanceData()
.then(balanceData => {
updateCallback(balanceData);
})
.catch(error => {
console.error(`Error updating ${errorContext} balances via WebSocket:`, error);
});
}, config.balanceUpdateDelay);
}
function handleSwapEvent(contextKey, updateCallback, errorContext) {
clearTimeoutByKey(`${contextKey}_swap_event`);
setTimeoutByKey(`${contextKey}_swap_event`, () => {
fetchBalanceData()
.then(balanceData => {
updateCallback(balanceData);
})
.catch(error => {
console.error(`Error updating ${errorContext} balances via swap event:`, error);
});
}, config.swapEventDelay);
}
function setupWebSocketHandler(contextKey, balanceUpdateCallback, swapEventCallback, errorContext) {
const handlerId = window.WebSocketManager.addMessageHandler('message', (data) => {
if (data && data.event) {
if (data.event === 'coin_balance_updated') {
handleBalanceUpdate(contextKey, balanceUpdateCallback, errorContext);
}
if (swapEventCallback) {
const swapEvents = ['new_bid', 'bid_accepted', 'swap_completed'];
if (swapEvents.includes(data.event)) {
handleSwapEvent(contextKey, swapEventCallback, errorContext);
}
}
}
});
state.handlers.set(contextKey, handlerId);
return handlerId;
}
function setupPeriodicRefresh(contextKey, updateCallback, errorContext, interval) {
const refreshInterval = interval || config.periodicRefreshInterval;
setIntervalByKey(`${contextKey}_periodic`, () => {
fetchBalanceData()
.then(balanceData => {
updateCallback(balanceData);
})
.catch(error => {
console.error(`Error in periodic ${errorContext} balance refresh:`, error);
});
}, refreshInterval);
}
function cleanup(contextKey) {
if (state.handlers.has(contextKey)) {
const handlerId = state.handlers.get(contextKey);
if (window.WebSocketManager && typeof window.WebSocketManager.removeMessageHandler === 'function') {
window.WebSocketManager.removeMessageHandler('message', handlerId);
}
state.handlers.delete(contextKey);
}
clearTimeoutByKey(`${contextKey}_balance_update`);
clearTimeoutByKey(`${contextKey}_swap_event`);
clearIntervalByKey(`${contextKey}_periodic`);
}
function cleanupAll() {
state.handlers.forEach((handlerId) => {
if (window.WebSocketManager && typeof window.WebSocketManager.removeMessageHandler === 'function') {
window.WebSocketManager.removeMessageHandler('message', handlerId);
}
});
state.handlers.clear();
state.timeouts.forEach(timeoutId => clearTimeout(timeoutId));
state.timeouts.clear();
state.intervals.forEach(intervalId => clearInterval(intervalId));
state.intervals.clear();
state.initialized = false;
}
return {
initialize: function() {
if (state.initialized) {
return this;
}
if (window.CleanupManager) {
window.CleanupManager.registerResource('balanceUpdatesManager', this, (mgr) => mgr.dispose());
}
window.addEventListener('beforeunload', cleanupAll);
state.initialized = true;
console.log('BalanceUpdatesManager initialized');
return this;
},
setup: function(options) {
const {
contextKey,
balanceUpdateCallback,
swapEventCallback,
errorContext,
enablePeriodicRefresh = false,
periodicInterval
} = options;
if (!contextKey || !balanceUpdateCallback || !errorContext) {
throw new Error('Missing required options: contextKey, balanceUpdateCallback, errorContext');
}
setupWebSocketHandler(contextKey, balanceUpdateCallback, swapEventCallback, errorContext);
if (enablePeriodicRefresh) {
setupPeriodicRefresh(contextKey, balanceUpdateCallback, errorContext, periodicInterval);
}
return this;
},
fetchBalanceData: fetchBalanceData,
cleanup: cleanup,
dispose: cleanupAll,
isInitialized: function() {
return state.initialized;
}
};
})();
if (typeof window !== 'undefined') {
window.BalanceUpdatesManager = BalanceUpdatesManager;
}

View File

@@ -1,11 +1,40 @@
const NotificationManager = (function() {
const config = {
const defaultConfig = {
showNewOffers: false,
showNewBids: true,
showBidAccepted: true
showBidAccepted: true,
showBalanceChanges: true,
showOutgoingTransactions: true,
notificationDuration: 20000
};
function loadConfig() {
const saved = localStorage.getItem('notification_settings');
if (saved) {
try {
return { ...defaultConfig, ...JSON.parse(saved) };
} catch (e) {
console.error('Error loading notification settings:', e);
}
}
return { ...defaultConfig };
}
function saveConfig(newConfig) {
try {
localStorage.setItem('notification_settings', JSON.stringify(newConfig));
Object.assign(config, newConfig);
} catch (e) {
console.error('Error saving notification settings:', e);
}
}
let config = loadConfig();
function ensureToastContainer() {
let container = document.getElementById('ul_updates');
if (!container) {
@@ -19,13 +48,67 @@ const NotificationManager = (function() {
return container;
}
function getCoinIcon(coinSymbol) {
if (window.CoinManager && typeof window.CoinManager.getCoinIcon === 'function') {
return window.CoinManager.getCoinIcon(coinSymbol);
}
return 'default.png';
}
function getToastIcon(type) {
const icons = {
'new_offer': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>`,
'new_bid': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path>
</svg>`,
'bid_accepted': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>`,
'balance_change': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 4a2 2 0 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4zm2 6a2 2 0 012-2h8a2 2 0 012 2v4a2 2 0 01-2 2H8a2 2 0 01-2-2v-4zm6 4a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path>
</svg>`,
'success': `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>`
};
return icons[type] || icons['success'];
}
function getToastColor(type, options = {}) {
const colors = {
'new_offer': 'bg-blue-500',
'new_bid': 'bg-green-500',
'bid_accepted': 'bg-purple-500',
'balance_change': 'bg-yellow-500',
'success': 'bg-blue-500'
};
if (type === 'balance_change' && options.subtitle) {
if (options.subtitle.includes('sent') || options.subtitle.includes('sending')) {
return 'bg-red-500';
} else {
return 'bg-green-500';
}
}
return colors[type] || colors['success'];
}
const publicAPI = {
initialize: function(options = {}) {
Object.assign(config, options);
this.initializeBalanceTracking();
if (window.CleanupManager) {
window.CleanupManager.registerResource('notificationManager', this, (mgr) => {
if (this.balanceTimeouts) {
Object.values(this.balanceTimeouts).forEach(timeout => clearTimeout(timeout));
}
console.log('NotificationManager disposed');
});
}
@@ -33,33 +116,160 @@ const NotificationManager = (function() {
return this;
},
createToast: function(title, type = 'success') {
updateSettings: function(newSettings) {
saveConfig(newSettings);
return this;
},
getSettings: function() {
return { ...config };
},
testToasts: function() {
if (!this.createToast) return;
setTimeout(() => {
this.createToast(
'+0.05000000 PART',
'balance_change',
{ coinSymbol: 'PART', subtitle: 'Incoming funds pending' }
);
}, 500);
setTimeout(() => {
this.createToast(
'+0.00123456 XMR',
'balance_change',
{ coinSymbol: 'XMR', subtitle: 'Incoming funds confirmed' }
);
}, 1000);
setTimeout(() => {
this.createToast(
'-29.86277595 PART',
'balance_change',
{ coinSymbol: 'PART', subtitle: 'Funds sent' }
);
}, 1500);
setTimeout(() => {
this.createToast(
'-0.05000000 PART (Anon)',
'balance_change',
{ coinSymbol: 'PART', subtitle: 'Funds sending' }
);
}, 2000);
setTimeout(() => {
this.createToast(
'+1.23456789 PART (Anon)',
'balance_change',
{ coinSymbol: 'PART', subtitle: 'Incoming funds confirmed' }
);
}, 2500);
setTimeout(() => {
this.createToast(
'New network offer',
'new_offer',
{ offerId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66', subtitle: 'Click to view offer' }
);
}, 3000);
setTimeout(() => {
this.createToast(
'New bid received',
'new_bid',
{ bidId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66', subtitle: 'Click to view bid' }
);
}, 3500);
},
initializeBalanceTracking: function() {
fetch('/json/walletbalances')
.then(response => response.json())
.then(balanceData => {
if (Array.isArray(balanceData)) {
balanceData.forEach(coin => {
const balance = parseFloat(coin.balance) || 0;
const pending = parseFloat(coin.pending) || 0;
const coinKey = coin.name.replace(/\s+/g, '_');
const storageKey = `prev_balance_${coinKey}`;
const pendingStorageKey = `prev_pending_${coinKey}`;
if (!localStorage.getItem(storageKey)) {
localStorage.setItem(storageKey, balance.toString());
}
if (!localStorage.getItem(pendingStorageKey)) {
localStorage.setItem(pendingStorageKey, pending.toString());
}
});
}
})
.catch(error => {
console.error('Error initializing balance tracking:', error);
});
},
createToast: function(title, type = 'success', options = {}) {
const messages = ensureToastContainer();
const message = document.createElement('li');
const toastId = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const iconColor = getToastColor(type, options);
const icon = getToastIcon(type);
let coinIconHtml = '';
if (options.coinSymbol) {
const coinIcon = getCoinIcon(options.coinSymbol);
coinIconHtml = `<img src="/static/images/coins/${coinIcon}" class="w-5 h-5 mr-2" alt="${options.coinSymbol}" onerror="this.style.display='none'">`;
}
let clickAction = '';
let cursorStyle = 'cursor-default';
if (options.offerId) {
clickAction = `onclick="window.location.href='/offer/${options.offerId}'"`;
cursorStyle = 'cursor-pointer';
} else if (options.bidId) {
clickAction = `onclick="window.location.href='/bid/${options.bidId}'"`;
cursorStyle = 'cursor-pointer';
} else if (options.coinSymbol) {
clickAction = `onclick="window.location.href='/wallet/${options.coinSymbol}'"`;
cursorStyle = 'cursor-pointer';
}
message.innerHTML = `
<div id="hide">
<div id="toast-${type}" class="flex items-center p-4 mb-4 w-full max-w-xs text-gray-500
bg-white rounded-lg shadow" role="alert">
<div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10
bg-blue-500 rounded-lg">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="18" width="18"
viewBox="0 0 24 24">
<g fill="#ffffff">
<path d="M8.5,20a1.5,1.5,0,0,1-1.061-.439L.379,12.5,2.5,10.379l6,6,13-13L23.621,
5.5,9.561,19.561A1.5,1.5,0,0,1,8.5,20Z"></path>
</g>
</svg>
<div class="toast-slide-in">
<div id="${toastId}" class="flex items-center p-4 mb-3 w-full max-w-sm text-gray-500
bg-white dark:bg-gray-800 dark:text-gray-400 rounded-lg shadow-lg border border-gray-200
dark:border-gray-700 ${cursorStyle} hover:shadow-xl transition-shadow" role="alert" ${clickAction}>
<div class="inline-flex flex-shrink-0 justify-center items-center w-10 h-10
${iconColor} rounded-lg text-white">
${icon}
</div>
<div class="uppercase w-40 ml-3 text-sm font-semibold text-gray-900">${title}</div>
<button type="button" onclick="closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5
bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-0 focus:outline-none
focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8">
<div class="flex items-center ml-3 text-sm font-medium text-gray-900 dark:text-white">
${coinIconHtml}
<div class="flex flex-col">
<span class="font-semibold">${title}</span>
${options.subtitle ? `<span class="text-xs text-gray-500 dark:text-gray-400">${options.subtitle}</span>` : ''}
</div>
</div>
<button type="button" onclick="event.stopPropagation(); closeAlert(event)" class="ml-auto -mx-1.5 -my-1.5
bg-white dark:bg-gray-800 text-gray-400 hover:text-gray-900 dark:hover:text-white
rounded-lg p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 inline-flex h-8 w-8 transition-colors
focus:outline-none">
<span class="sr-only">Close</span>
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1
1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293
4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1
1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293
4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</button>
@@ -67,34 +277,206 @@ const NotificationManager = (function() {
</div>
`;
messages.appendChild(message);
setTimeout(() => {
if (message.parentNode) {
message.classList.add('toast-slide-out');
setTimeout(() => {
if (message.parentNode) {
message.parentNode.removeChild(message);
}
}, 300);
}
}, config.notificationDuration);
},
handleWebSocketEvent: function(data) {
if (!data || !data.event) return;
let toastTitle;
let toastTitle, toastType, toastOptions = {};
let shouldShowToast = false;
switch (data.event) {
case 'new_offer':
toastTitle = `New network <a class="underline" href=/offer/${data.offer_id}>offer</a>`;
toastTitle = `New network offer`;
toastType = 'new_offer';
toastOptions.offerId = data.offer_id;
toastOptions.subtitle = 'Click to view offer';
shouldShowToast = config.showNewOffers;
break;
case 'new_bid':
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>New bid</a> on
<a class="underline" href=/offer/${data.offer_id}>offer</a>`;
toastTitle = `New bid received`;
toastOptions.bidId = data.bid_id;
toastOptions.subtitle = 'Click to view bid';
toastType = 'new_bid';
shouldShowToast = config.showNewBids;
break;
case 'bid_accepted':
toastTitle = `<a class="underline" href=/bid/${data.bid_id}>Bid</a> accepted`;
toastTitle = `Bid accepted`;
toastOptions.bidId = data.bid_id;
toastOptions.subtitle = 'Click to view swap';
toastType = 'bid_accepted';
shouldShowToast = config.showBidAccepted;
break;
case 'coin_balance_updated':
if (data.coin && config.showBalanceChanges) {
this.handleBalanceUpdate(data);
}
return;
}
if (toastTitle && shouldShowToast) {
this.createToast(toastTitle);
this.createToast(toastTitle, toastType, toastOptions);
}
},
handleBalanceUpdate: function(data) {
if (!data.coin) return;
this.fetchAndShowBalanceChange(data.coin);
const balanceKey = `balance_${data.coin}`;
if (this.balanceTimeouts && this.balanceTimeouts[balanceKey]) {
clearTimeout(this.balanceTimeouts[balanceKey]);
}
if (!this.balanceTimeouts) {
this.balanceTimeouts = {};
}
this.balanceTimeouts[balanceKey] = setTimeout(() => {
this.fetchAndShowBalanceChange(data.coin);
}, 2000);
},
fetchAndShowBalanceChange: function(coinSymbol) {
fetch('/json/walletbalances')
.then(response => response.json())
.then(balanceData => {
if (Array.isArray(balanceData)) {
let coinsToCheck;
if (coinSymbol === 'PART') {
coinsToCheck = balanceData.filter(coin => coin.ticker === 'PART');
} else if (coinSymbol === 'LTC') {
coinsToCheck = balanceData.filter(coin => coin.ticker === 'LTC');
} else {
coinsToCheck = balanceData.filter(coin =>
coin.ticker === coinSymbol ||
coin.name.toLowerCase() === coinSymbol.toLowerCase()
);
}
coinsToCheck.forEach(coinData => {
this.checkSingleCoinBalance(coinData, coinSymbol);
});
}
})
.catch(error => {
console.error('Error fetching balance for notification:', error);
});
},
checkSingleCoinBalance: function(coinData, originalCoinSymbol) {
const currentBalance = parseFloat(coinData.balance) || 0;
const currentPending = parseFloat(coinData.pending) || 0;
const coinKey = coinData.name.replace(/\s+/g, '_');
const storageKey = `prev_balance_${coinKey}`;
const pendingStorageKey = `prev_pending_${coinKey}`;
const prevBalance = parseFloat(localStorage.getItem(storageKey)) || 0;
const prevPending = parseFloat(localStorage.getItem(pendingStorageKey)) || 0;
const balanceIncrease = currentBalance - prevBalance;
const pendingIncrease = currentPending - prevPending;
const pendingDecrease = prevPending - currentPending;
const isPendingToConfirmed = pendingDecrease > 0.00000001 && balanceIncrease > 0.00000001;
const displaySymbol = originalCoinSymbol;
let variantInfo = '';
if (coinData.name !== 'Particl' && coinData.name.includes('Particl')) {
variantInfo = ` (${coinData.name.replace('Particl ', '')})`;
} else if (coinData.name !== 'Litecoin' && coinData.name.includes('Litecoin')) {
variantInfo = ` (${coinData.name.replace('Litecoin ', '')})`;
}
if (balanceIncrease > 0.00000001) {
const displayAmount = balanceIncrease.toFixed(8).replace(/\.?0+$/, '');
const subtitle = isPendingToConfirmed ? 'Funds confirmed' : 'Incoming funds confirmed';
this.createToast(
`+${displayAmount} ${displaySymbol}${variantInfo}`,
'balance_change',
{
coinSymbol: originalCoinSymbol,
subtitle: subtitle
}
);
}
if (balanceIncrease < -0.00000001 && config.showOutgoingTransactions) {
const displayAmount = Math.abs(balanceIncrease).toFixed(8).replace(/\.?0+$/, '');
this.createToast(
`-${displayAmount} ${displaySymbol}${variantInfo}`,
'balance_change',
{
coinSymbol: originalCoinSymbol,
subtitle: 'Funds sent'
}
);
}
if (pendingIncrease > 0.00000001) {
const displayAmount = pendingIncrease.toFixed(8).replace(/\.?0+$/, '');
this.createToast(
`+${displayAmount} ${displaySymbol}${variantInfo}`,
'balance_change',
{
coinSymbol: originalCoinSymbol,
subtitle: 'Incoming funds pending'
}
);
}
if (pendingIncrease < -0.00000001 && config.showOutgoingTransactions && !isPendingToConfirmed) {
const displayAmount = Math.abs(pendingIncrease).toFixed(8).replace(/\.?0+$/, '');
this.createToast(
`-${displayAmount} ${displaySymbol}${variantInfo}`,
'balance_change',
{
coinSymbol: originalCoinSymbol,
subtitle: 'Funds sending'
}
);
}
if (pendingDecrease > 0.00000001 && !isPendingToConfirmed) {
const displayAmount = pendingDecrease.toFixed(8).replace(/\.?0+$/, '');
this.createToast(
`${displayAmount} ${displaySymbol}${variantInfo}`,
'balance_change',
{
coinSymbol: originalCoinSymbol,
subtitle: 'Pending funds confirmed'
}
);
}
localStorage.setItem(storageKey, currentBalance.toString());
localStorage.setItem(pendingStorageKey, currentPending.toString());
},
updateConfig: function(newConfig) {
Object.assign(config, newConfig);
return this;
@@ -122,5 +504,5 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('NotificationManager initialized with methods:', Object.keys(NotificationManager));
console.log('NotificationManager initialized');

View File

@@ -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');

View File

@@ -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);

View File

@@ -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());

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -51,16 +51,11 @@
Add Bid
</button>
{% endif %}
<button id="refreshAmmTables" class="flex items-center px-4 py-2.5 bg-blue-600 hover:bg-blue-600 border-blue-500 font-medium text-sm text-white border rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
Refresh
</button>
</div>
</div>
<div class="border-b pb-5 border-gray-200 dark:border-gray-500">
<div class="pb-5">
<ul class="flex flex-wrap text-sm font-medium text-center text-gray-500 dark:text-gray-400" id="ammTabs" role="tablist">
<li class="mr-2" role="presentation">
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="offers-tab" data-tabs-target="#offers-content" type="button" role="tab" aria-controls="offers" aria-selected="true">
@@ -290,6 +285,7 @@
<button type="submit" name="kill_orphans" value="1" class="inline-flex items-center px-3 py-1.5 bg-red-500 hover:bg-red-600 text-white text-xs font-medium rounded-md shadow-sm focus:ring-0 focus:outline-none" title="Kill orphaned AMM processes">
Kill Orphans
</button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">
Use "Check Processes" to see running AMM processes. Use "Kill Orphans" to clean up duplicate processes. Use "Force Start" to automatically clean up and start fresh.
@@ -974,7 +970,7 @@
if (localStorage.getItem('amm_create_default_refresh') === 'true') {
localStorage.removeItem('amm_create_default_refresh');
console.log('[AMM] Page loaded after create default config - redirecting to fresh page');
setTimeout(function() {
window.location.href = window.location.pathname + window.location.search;
@@ -1066,7 +1062,7 @@
<div class="absolute inset-0 bg-gray-500 opacity-75 dark:bg-gray-900 dark:opacity-90"></div>
</div>
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle md:max-w-2xl md:w-full">
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-visible shadow-xl transform transition-all sm:my-8 sm:align-middle md:max-w-2xl md:w-full">
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:text-left w-full">
@@ -1086,7 +1082,7 @@
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<div class="min-h-[80px]">
<label for="add-amm-coin-from" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Maker</label>
<div class="relative">
<svg class="absolute top-1/2 right-3 transform -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -1094,15 +1090,14 @@
</svg>
<select id="add-amm-coin-from" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 appearance-none">
{% for c in coins %}
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}">
{{ c[1] }}
</option>
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}" data-balance="{{ c[2] }}">{{ c[1] }}</option>
{% endfor %}
</select>
</div>
</div>
<div>
<div class="min-h-[80px]">
<label for="add-amm-coin-to" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Taker</label>
<div class="relative">
<svg class="absolute top-1/2 right-3 transform -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -1110,12 +1105,11 @@
</svg>
<select id="add-amm-coin-to" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 appearance-none">
{% for c in coins %}
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}">
{{ c[1] }}
</option>
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}" data-balance="{{ c[2] }}">{{ c[1] }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
@@ -1123,13 +1117,18 @@
<div>
<label for="add-amm-amount" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Offer Amount <span class="text-red-500">*</span></label>
<input type="text" id="add-amm-amount" pattern="[0-9]*\.?[0-9]*" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="0.0">
<div class="mt-2 flex space-x-2">
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(0.25, 'add-amm-amount')">25%</button>
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(0.5, 'add-amm-amount')">50%</button>
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(1, 'add-amm-amount')">100%</button>
</div>
</div>
<div>
<label id="add-amm-rate-label" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Minimum Rate <span class="text-red-500">*</span></label>
<div class="flex items-center gap-2">
<input type="text" id="add-amm-rate" pattern="[0-9]*\.?[0-9]*" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="0.0">
<button type="button" id="add-get-rate-button" class="px-2 py-1.5 bg-blue-500 hover:bg-blue-600 font-medium text-xs text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Get Rate</button>
<input type="text" id="add-amm-rate" pattern="[0-9]*\.?[0-9]*" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="0.0">
<div class="mt-2">
<button type="button" id="add-get-rate-button" class="py-1 px-2 bg-blue-500 hover:bg-blue-600 text-white text-xs rounded-md focus:outline-none">Get Rate</button>
</div>
</div>
</div>
@@ -1348,7 +1347,7 @@
<div class="absolute inset-0 bg-gray-500 opacity-75 dark:bg-gray-900 dark:opacity-90"></div>
</div>
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle lg:max-w-xl md:max-w-2xl md:w-full">
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-visible shadow-xl transform transition-all sm:my-8 sm:align-middle lg:max-w-xl md:max-w-2xl md:w-full">
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mt-3 text-center sm:mt-0 sm:text-left w-full">
@@ -1370,7 +1369,7 @@
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<div class="min-h-[80px]">
<label for="edit-amm-coin-from" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Maker</label>
<div class="relative">
<svg class="absolute top-1/2 right-3 transform -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -1378,15 +1377,14 @@
</svg>
<select id="edit-amm-coin-from" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 appearance-none">
{% for c in coins %}
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}">
{{ c[1] }}
</option>
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}" data-balance="{{ c[2] }}">{{ c[1] }}</option>
{% endfor %}
</select>
</div>
</div>
<div>
<div class="min-h-[80px]">
<label for="edit-amm-coin-to" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Taker</label>
<div class="relative">
<svg class="absolute top-1/2 right-3 transform -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -1394,12 +1392,11 @@
</svg>
<select id="edit-amm-coin-to" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 appearance-none">
{% for c in coins %}
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}">
{{ c[1] }}
</option>
<option value="{{ c[1] }}" data-symbol="{{ c[0] }}" data-balance="{{ c[2] }}">{{ c[1] }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
@@ -1407,13 +1404,18 @@
<div>
<label for="edit-amm-amount" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Offer Amount <span class="text-red-500">*</span></label>
<input type="text" id="edit-amm-amount" pattern="[0-9]*\.?[0-9]*" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="0.0">
<div class="mt-2 flex space-x-2">
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(0.25, 'edit-amm-amount')">25%</button>
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(0.5, 'edit-amm-amount')">50%</button>
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-xs rounded-md focus:outline-none" onclick="setAmmAmount(1, 'edit-amm-amount')">100%</button>
</div>
</div>
<div>
<label id="edit-amm-rate-label" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Minimum Rate <span class="text-red-500">*</span></label>
<div class="flex items-center gap-2">
<input type="text" id="edit-amm-rate" pattern="[0-9]*\.?[0-9]*" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="0.0">
<button type="button" id="edit-get-rate-button" class="px-2 py-1.5 bg-blue-500 hover:bg-blue-600 font-medium text-xs text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">Get Rate</button>
<input type="text" id="edit-amm-rate" pattern="[0-9]*\.?[0-9]*" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="0.0">
<div class="mt-2">
<button type="button" id="edit-get-rate-button" class="py-1 px-2 bg-blue-500 hover:bg-blue-600 text-white text-xs rounded-md focus:outline-none">Get Rate</button>
</div>
</div>
</div>
@@ -2033,6 +2035,187 @@
});
}
});
function setAmmAmount(percent, fieldId) {
const amountInput = document.getElementById(fieldId);
let coinSelect;
let modalType = null;
if (fieldId.includes('add-amm')) {
const addModal = document.getElementById('add-amm-modal');
modalType = addModal ? addModal.getAttribute('data-amm-type') : null;
} else if (fieldId.includes('edit-amm')) {
const editModal = document.getElementById('edit-amm-modal');
modalType = editModal ? editModal.getAttribute('data-amm-type') : null;
}
if (fieldId.includes('add-amm')) {
const isBidModal = modalType === 'bid';
coinSelect = document.getElementById(isBidModal ? 'add-amm-coin-to' : 'add-amm-coin-from');
} else if (fieldId.includes('edit-amm')) {
const isBidModal = modalType === 'bid';
coinSelect = document.getElementById(isBidModal ? 'edit-amm-coin-to' : 'edit-amm-coin-from');
}
if (!amountInput || !coinSelect) {
console.error('Required elements not found');
return;
}
const selectedOption = coinSelect.options[coinSelect.selectedIndex];
if (!selectedOption) {
alert('Please select a coin first');
return;
}
const balance = selectedOption.getAttribute('data-balance');
if (!balance) {
console.error('Balance not found for selected coin');
return;
}
const floatBalance = parseFloat(balance);
if (isNaN(floatBalance) || floatBalance <= 0) {
alert('Invalid balance for selected coin');
return;
}
const calculatedAmount = floatBalance * percent;
amountInput.value = calculatedAmount.toFixed(8);
const event = new Event('input', { bubbles: true });
amountInput.dispatchEvent(event);
}
function updateAmmModalBalances(balanceData) {
const addModal = document.getElementById('add-amm-modal');
const editModal = document.getElementById('edit-amm-modal');
const addModalVisible = addModal && !addModal.classList.contains('hidden');
const editModalVisible = editModal && !editModal.classList.contains('hidden');
let modalType = null;
if (addModalVisible) {
modalType = addModal.getAttribute('data-amm-type');
} else if (editModalVisible) {
modalType = editModal.getAttribute('data-amm-type');
}
if (modalType === 'offer') {
updateOfferDropdownBalances(balanceData);
} else if (modalType === 'bid') {
updateBidDropdownBalances(balanceData);
}
}
function setupWebSocketBalanceUpdates() {
window.BalanceUpdatesManager.setup({
contextKey: 'amm',
balanceUpdateCallback: updateAmmModalBalances,
swapEventCallback: updateAmmModalBalances,
errorContext: 'AMM',
enablePeriodicRefresh: true,
periodicInterval: 120000
});
}
function updateAmmDropdownBalances(balanceData) {
const balanceMap = {};
const pendingMap = {};
balanceData.forEach(coin => {
balanceMap[coin.name] = coin.balance;
pendingMap[coin.name] = coin.pending || '0.0';
});
const dropdownIds = ['add-amm-coin-from', 'edit-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-to'];
dropdownIds.forEach(dropdownId => {
const select = document.getElementById(dropdownId);
if (!select) {
return;
}
Array.from(select.options).forEach(option => {
const coinName = option.value;
const balance = balanceMap[coinName] || '0.0';
const pending = pendingMap[coinName] || '0.0';
option.setAttribute('data-balance', balance);
option.setAttribute('data-pending-balance', pending);
});
});
const addModal = document.getElementById('add-amm-modal');
const editModal = document.getElementById('edit-amm-modal');
const addModalVisible = addModal && !addModal.classList.contains('hidden');
const editModalVisible = editModal && !editModal.classList.contains('hidden');
let currentModalType = null;
if (addModalVisible) {
currentModalType = addModal.getAttribute('data-amm-type');
} else if (editModalVisible) {
currentModalType = editModal.getAttribute('data-amm-type');
}
if (currentModalType && window.ammTablesManager) {
if (currentModalType === 'offer' && typeof window.ammTablesManager.refreshOfferDropdownBalanceDisplay === 'function') {
window.ammTablesManager.refreshOfferDropdownBalanceDisplay();
} else if (currentModalType === 'bid' && typeof window.ammTablesManager.refreshBidDropdownBalanceDisplay === 'function') {
window.ammTablesManager.refreshBidDropdownBalanceDisplay();
}
}
}
function updateOfferDropdownBalances(balanceData) {
updateAmmDropdownBalances(balanceData);
}
function updateBidDropdownBalances(balanceData) {
updateAmmDropdownBalances(balanceData);
}
function updateOfferCustomDropdownDisplay(select, balanceMap, pendingMap) {
console.log(`[DEBUG] updateOfferCustomDropdownDisplay called for ${select.id} - doing nothing`);
}
function updateBidCustomDropdownDisplay(select, balanceMap, pendingMap) {
console.log(`[DEBUG] updateBidCustomDropdownDisplay called for ${select.id} - doing nothing`);
}
function updateCustomDropdownDisplay(select, balanceMap, pendingMap) {
console.log(`[DEBUG] updateCustomDropdownDisplay called for ${select.id} - doing nothing`);
}
window.BalanceUpdatesManager.initialize();
setupWebSocketBalanceUpdates();
function cleanupAmmBalanceUpdates() {
window.BalanceUpdatesManager.cleanup('amm');
if (window.ammDropdowns) {
window.ammDropdowns.forEach(dropdown => {
if (dropdown.parentNode) {
dropdown.parentNode.removeChild(dropdown);
}
});
window.ammDropdowns = [];
}
}
if (window.CleanupManager) {
window.CleanupManager.registerResource('ammBalanceUpdates', null, cleanupAmmBalanceUpdates);
}
window.addEventListener('beforeunload', cleanupAmmBalanceUpdates);
window.setAmmAmount = setAmmAmount;
</script>
{% include 'footer.html' %}

View File

@@ -75,6 +75,7 @@
<script src="/static/js/modules/price-manager.js"></script>
<script src="/static/js/modules/tooltips-manager.js"></script>
<script src="/static/js/modules/notification-manager.js"></script>
<script src="/static/js/modules/balance-updates.js"></script>
<script src="/static/js/modules/identity-manager.js"></script>
<script src="/static/js/modules/summary-manager.js"></script>
<script src="/static/js/amm_counter.js"></script>

View File

@@ -155,7 +155,7 @@
<select class="select hover:border-blue-500 pl-10 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-5 bold focus:ring-0" id="coin_from" name="coin_from" onchange="set_rate('coin_from');">
<option value="-1">Select coin you send</option>
{% for c in coins_from %}
<option{% if data.coin_from==c[0] %} selected{% endif %} value="{{ c[0] }}" data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}-20.png">{{ c[1] }}</option>
<option{% if data.coin_from==c[0] %} selected{% endif %} value="{{ c[0] }}" data-image="/static/images/coins/{{ c[1]|replace(" ", "-") }}-20.png" data-balance="{{ c[2] }}">{{ c[1] }} - Balance: {{ c[2] }}</option>
{% endfor %}
</select>
<div class="select-dropdown"> <img class="select-icon" src="" alt=""> <img class="select-image" src="" alt=""> </div>
@@ -167,6 +167,11 @@
<div class="relative">
<div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none"> </div> <input class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-5 focus:ring-0 bold" placeholder="0" type="text" id="amt_from" name="amt_from" value="{{ data.amt_from }}" onchange="set_rate('amt_from');">
</div>
<div class="mt-2 flex space-x-2">
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setOfferAmount(0.25, 'amt_from')">25%</button>
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setOfferAmount(0.5, 'amt_from')">50%</button>
<button type="button" class="py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setOfferAmount(1, 'amt_from')">100%</button>
</div>
</div>
</div>
</div>
@@ -332,6 +337,142 @@
</div>
</section>
</div>
<script>
function setOfferAmount(percent, fieldId) {
const amountInput = document.getElementById(fieldId);
const coinFromSelect = document.getElementById('coin_from');
if (!amountInput || !coinFromSelect) {
console.error('Required elements not found');
return;
}
const selectedOption = coinFromSelect.options[coinFromSelect.selectedIndex];
if (!selectedOption || selectedOption.value === '-1') {
alert('Please select a coin first');
return;
}
const balance = selectedOption.getAttribute('data-balance');
if (!balance) {
console.error('Balance not found for selected coin');
return;
}
const floatBalance = parseFloat(balance);
if (isNaN(floatBalance) || floatBalance <= 0) {
alert('Invalid balance for selected coin');
return;
}
const calculatedAmount = floatBalance * percent;
amountInput.value = calculatedAmount.toFixed(8);
if (amountInput.onchange) {
amountInput.onchange();
}
if (typeof set_rate === 'function') {
set_rate(fieldId);
}
}
function updateCustomDropdownDisplay(select, coinName, balance, pending) {
const selectNameElement = select.nextElementSibling?.querySelector('.select-name');
if (!selectNameElement) return;
selectNameElement.innerHTML = '';
selectNameElement.style.display = 'flex';
selectNameElement.style.flexDirection = 'column';
selectNameElement.style.alignItems = 'flex-start';
selectNameElement.style.lineHeight = '1.2';
const coinNameDiv = document.createElement('div');
coinNameDiv.textContent = coinName;
coinNameDiv.style.fontWeight = 'normal';
coinNameDiv.style.color = 'inherit';
const balanceDiv = document.createElement('div');
balanceDiv.textContent = `Balance: ${balance}`;
balanceDiv.style.fontSize = '0.75rem';
balanceDiv.style.color = '#6b7280';
balanceDiv.style.marginTop = '1px';
selectNameElement.appendChild(coinNameDiv);
selectNameElement.appendChild(balanceDiv);
if (pending !== '0.0' && parseFloat(pending) > 0) {
const pendingDiv = document.createElement('div');
pendingDiv.textContent = `+${pending} pending`;
pendingDiv.style.fontSize = '0.75rem';
pendingDiv.style.color = '#10b981';
pendingDiv.style.marginTop = '1px';
selectNameElement.appendChild(pendingDiv);
}
}
function updateCoinDropdownBalances(balanceData) {
const coinFromSelect = document.getElementById('coin_from');
if (!coinFromSelect) return;
const balanceMap = {};
const pendingMap = {};
balanceData.forEach(coin => {
balanceMap[coin.id] = coin.balance;
pendingMap[coin.id] = coin.pending || '0.0';
});
Array.from(coinFromSelect.options).forEach(option => {
if (option.value === '-1') return;
const coinId = parseInt(option.value);
const balance = balanceMap[coinId] || '0.0';
const pending = pendingMap[coinId] || '0.0';
option.setAttribute('data-balance', balance);
const coinName = option.textContent.split(' - Balance:')[0];
if (pending !== '0.0' && parseFloat(pending) > 0) {
option.textContent = `${coinName} - Balance: ${balance} (+${pending} pending)`;
} else {
option.textContent = `${coinName} - Balance: ${balance}`;
}
const select = option.closest('select');
if (select && select.value === option.value) {
updateCustomDropdownDisplay(select, coinName, balance, pending);
}
});
}
document.addEventListener('DOMContentLoaded', function() {
window.BalanceUpdatesManager.initialize();
window.BalanceUpdatesManager.setup({
contextKey: 'offer',
balanceUpdateCallback: updateCoinDropdownBalances,
swapEventCallback: updateCoinDropdownBalances,
errorContext: 'New Offer',
enablePeriodicRefresh: true,
periodicInterval: 120000
});
});
</script>
<script src="static/js/new_offer.js"></script>
{% include 'footer.html' %}
</div>

View File

@@ -49,6 +49,9 @@
<button class="tab-button border-b-2 border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600 py-4 px-1 text-sm font-medium focus:outline-none focus:ring-0" data-tab="general" id="general-tab">
General
</button>
<button class="tab-button border-b-2 border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600 py-4 px-1 text-sm font-medium focus:outline-none focus:ring-0" data-tab="notifications" id="notifications-tab">
Notifications
</button>
<button class="tab-button border-b-2 border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600 py-4 px-1 text-sm font-medium focus:outline-none focus:ring-0" data-tab="tor" id="tor-tab">
Tor
</button>
@@ -432,6 +435,153 @@
</form>
</div>
<div class="tab-content hidden" id="notifications" role="tabpanel" aria-labelledby="notifications-tab">
<form method="post">
<div class="space-y-6">
<div class="bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-6 py-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
Notification Settings
</h3>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300">
Configure which notifications you want to see.
</p>
</div>
<div class="px-6 pb-6 space-y-6">
<div>
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">Notification Types</h4>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<div class="space-y-4">
<div class="py-2">
<div class="flex items-center">
<input type="checkbox" id="notifications_new_offers" name="notifications_new_offers" value="true" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"{% if notification_settings.notifications_new_offers %} checked{% endif %}>
<label for="notifications_new_offers" class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-300">New Offers</label>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 ml-7 mt-1">Show notifications for new network offers</p>
</div>
<div class="py-2">
<div class="flex items-center">
<input type="checkbox" id="notifications_new_bids" name="notifications_new_bids" value="true" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"{% if notification_settings.notifications_new_bids %} checked{% endif %}>
<label for="notifications_new_bids" class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-300">New Bids</label>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 ml-7 mt-1">Show notifications for new bids on your offers</p>
</div>
<div class="py-2">
<div class="flex items-center">
<input type="checkbox" id="notifications_bid_accepted" name="notifications_bid_accepted" value="true" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"{% if notification_settings.notifications_bid_accepted %} checked{% endif %}>
<label for="notifications_bid_accepted" class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-300">Bid Accepted</label>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 ml-7 mt-1">Show notifications when your bids are accepted</p>
</div>
<div class="py-2">
<div class="flex items-center">
<input type="checkbox" id="notifications_balance_changes" name="notifications_balance_changes" value="true" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"{% if notification_settings.notifications_balance_changes %} checked{% endif %}>
<label for="notifications_balance_changes" class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-300">Balance Changes</label>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 ml-7 mt-1">Show notifications for incoming and outgoing funds</p>
</div>
<div class="py-2">
<div class="flex items-center">
<input type="checkbox" id="notifications_outgoing_transactions" name="notifications_outgoing_transactions" value="true" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"{% if notification_settings.notifications_outgoing_transactions %} checked{% endif %}>
<label for="notifications_outgoing_transactions" class="ml-3 text-sm font-medium text-gray-700 dark:text-gray-300">Outgoing Transactions</label>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 ml-7 mt-1">Show notifications for sent funds</p>
</div>
</div>
</div>
</div>
<div>
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">Display Duration</h4>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<div>
<label for="notifications_duration" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Duration</label>
<div class="relative w-48">
<select id="notifications_duration" name="notifications_duration" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-700 dark:text-white border border-gray-300 dark:border-gray-600 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-1 focus:outline-none">
<option value="5"{% if notification_settings.notifications_duration == 5 %} selected{% endif %}>5 seconds</option>
<option value="10"{% if notification_settings.notifications_duration == 10 %} selected{% endif %}>10 seconds</option>
<option value="15"{% if notification_settings.notifications_duration == 15 %} selected{% endif %}>15 seconds</option>
<option value="20"{% if notification_settings.notifications_duration == 20 %} selected{% endif %}>20 seconds</option>
<option value="30"{% if notification_settings.notifications_duration == 30 %} selected{% endif %}>30 seconds</option>
<option value="60"{% if notification_settings.notifications_duration == 60 %} selected{% endif %}>1 minute</option>
</select>
<div class="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
</div>
</div>
</div>
{% if general_settings.debug_ui %}
<div>
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">Test Notifications</h4>
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
<div>
<button type="button" onclick="window.NotificationManager && window.NotificationManager.testToasts()" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors focus:outline-none">
Test All Notification Types
</button>
</div>
</div>
</div>
{% endif %}
<div class="flex justify-end mt-6">
<button name="apply_notifications" value="Apply" type="submit" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors focus:outline-none">
Apply Changes
</button>
</div>
</div>
</div>
</div>
<input type="hidden" name="formid" value="{{ form_id }}">
</form>
</div>
<script>
function syncNotificationSettings() {
if (window.NotificationManager && typeof window.NotificationManager.updateSettings === 'function') {
const backendSettings = {
showNewOffers: document.getElementById('notifications_new_offers').checked,
showNewBids: document.getElementById('notifications_new_bids').checked,
showBidAccepted: document.getElementById('notifications_bid_accepted').checked,
showBalanceChanges: document.getElementById('notifications_balance_changes').checked,
showOutgoingTransactions: document.getElementById('notifications_outgoing_transactions').checked,
notificationDuration: parseInt(document.getElementById('notifications_duration').value) * 1000
};
window.NotificationManager.updateSettings(backendSettings);
}
}
document.getElementById('notifications-tab').addEventListener('click', function() {
setTimeout(syncNotificationSettings, 100);
});
document.addEventListener('DOMContentLoaded', function() {
syncNotificationSettings();
});
document.addEventListener('change', function(e) {
if (e.target.closest('#notifications')) {
syncNotificationSettings();
}
});
</script>
<div class="tab-content hidden" id="tor" role="tabpanel" aria-labelledby="tor-tab">
<form method="post" id="tor-form">
<div class="space-y-6">

View File

@@ -13,7 +13,7 @@
<span class="inline-block align-middle">
<img class="mr-2 h-16" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"></span>({{ w.ticker }}) {{ w.name }} Wallet </h2>
</div>
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto"> <a class="rounded-full mr-5 flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-lg lg:text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/wallet/{{ w.ticker }}"> {{ circular_arrows_svg | safe }}<span>Refresh</span> </a> </div>
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto"></div>
</div>
</div>
</div>
@@ -667,16 +667,7 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
var floatBalance;
var calculatedAmount;
console.log('SetAmount Called with:', {
percent: percent,
balance: balance,
cid: cid,
blindBalance: blindBalance,
anonBalance: anonBalance,
selectedType: selectedType,
blindBalanceType: typeof blindBalance,
blindBalanceNumeric: Number(blindBalance)
});
const safeParseFloat = (value) => {
const numValue = Number(value);
@@ -706,11 +697,7 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
calculatedAmount = Math.max(0, Math.floor(floatBalance * percent * 100000000) / 100000000);
console.log('Calculated Amount:', {
floatBalance: floatBalance,
calculatedAmount: calculatedAmount,
percent: percent
});
if (percent === 1) {
calculatedAmount = floatBalance;
@@ -727,43 +714,6 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
if (subfeeCheckbox) {
subfeeCheckbox.checked = (percent === 1);
}
console.log('Final Amount Set:', amountInput.value);
}function setAmount(percent, balance, cid, blindBalance, anonBalance) {
var amountInput = document.getElementById('amount');
var typeSelect = document.getElementById('withdraw_type');
var selectedType = typeSelect.value;
var floatBalance;
var calculatedAmount;
switch(selectedType) {
case 'plain':
floatBalance = parseFloat(balance);
break;
case 'blind':
floatBalance = parseFloat(blindBalance);
break;
case 'anon':
floatBalance = parseFloat(anonBalance);
break;
default:
floatBalance = parseFloat(balance);
break;
}
calculatedAmount = Math.floor(floatBalance * percent * 100000000) / 100000000;
if (percent === 1) {
calculatedAmount = floatBalance;
}
amountInput.value = calculatedAmount.toFixed(8);
var subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
if (subfeeCheckbox) {
subfeeCheckbox.checked = (percent === 1);
}
}
</script>
@@ -818,17 +768,14 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
const specialCids = [6, 9];
console.log("CID:", cid);
console.log("Percent:", percent);
console.log("Balance:", balance);
console.log("Calculated Amount:", calculatedAmount);
if (specialCids.includes(parseInt(cid)) && percent === 1) {
amountInput.setAttribute('data-hidden', 'true');
amountInput.placeholder = 'Sweep All';
amountInput.value = '';
amountInput.disabled = true;
console.log("Sweep All activated for special CID:", cid);
} else {
amountInput.value = calculatedAmount.toFixed(8);
amountInput.setAttribute('data-hidden', 'false');
@@ -840,17 +787,15 @@ function setAmount(percent, balance, cid, blindBalance, anonBalance) {
if (sweepAllCheckbox) {
if (specialCids.includes(parseInt(cid)) && percent === 1) {
sweepAllCheckbox.checked = true;
console.log("Sweep All checkbox checked");
} else {
sweepAllCheckbox.checked = false;
console.log("Sweep All checkbox unchecked");
}
}
let subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
if (subfeeCheckbox) {
subfeeCheckbox.checked = (percent === 1);
console.log("Subfee checkbox status for CID", cid, ":", subfeeCheckbox.checked);
}
}
</script>
@@ -1148,6 +1093,375 @@ function confirmUTXOResize() {
}
document.addEventListener('DOMContentLoaded', function() {
if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') {
window.WebSocketManager.initialize();
}
window.currentBalances = window.currentBalances || {};
{% if w.cid == '1' %}
window.currentBalances = {
mainBalance: '{{ w.balance }}',
blindBalance: '{{ w.blind_balance }}',
anonBalance: '{{ w.anon_balance }}'
};
{% elif w.cid == '3' %}
window.currentBalances = {
mainBalance: '{{ w.balance }}',
mwebBalance: '{{ w.mweb_balance }}'
};
{% endif %}
function updateWalletBalances() {
fetch('/json/walletbalances', {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`Server error: ${response.status} ${response.statusText}`);
}
return response.json();
})
.then(balanceData => {
if (balanceData.error) {
throw new Error(balanceData.error);
}
if (!Array.isArray(balanceData)) {
throw new Error('Invalid response format');
}
balanceData.forEach(coin => {
updateWalletDisplay(coin);
updatePercentageButtons(coin);
});
setTimeout(() => {
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
window.WalletManager.updatePrices(true);
}
}, 100);
})
.catch(error => {
console.error('[Wallet] Error updating wallet balances:', error);
});
}
function updatePercentageButtons(coinData) {
const currentCoinId = {{ w.cid }};
const isPartVariant = (currentCoinId === 1 && (coinData.name === 'Particl' || coinData.name === 'Particl Anon' || coinData.name === 'Particl Blind'));
const isLtcVariant = (currentCoinId === 3 && (coinData.name === 'Litecoin' || coinData.name === 'Litecoin MWEB'));
const isCurrentCoin = (coinData.id === currentCoinId);
if (isPartVariant || isLtcVariant || isCurrentCoin) {
const buttons = document.querySelectorAll('button[onclick*="setAmount"]');
buttons.forEach(button => {
const onclick = button.getAttribute('onclick');
if (onclick) {
if (currentCoinId === 1) {
window.currentBalances = window.currentBalances || {};
const anonBalance = coinData.name === 'Particl Anon' ? coinData.balance :
(window.currentBalances.anonBalance || '0');
const blindBalance = coinData.name === 'Particl Blind' ? coinData.balance :
(window.currentBalances.blindBalance || '0');
const mainBalance = coinData.name === 'Particl' ? coinData.balance :
(window.currentBalances.mainBalance || '0');
if (coinData.name === 'Particl') window.currentBalances.mainBalance = coinData.balance;
if (coinData.name === 'Particl Anon') window.currentBalances.anonBalance = coinData.balance;
if (coinData.name === 'Particl Blind') window.currentBalances.blindBalance = coinData.balance;
const newOnclick = onclick.replace(
/setAmount\([^,]+,\s*'[^']*',\s*\d+,\s*'[^']*',\s*'[^']*'\)/,
`setAmount(${onclick.match(/setAmount\(([^,]+)/)[1]}, '${mainBalance}', ${currentCoinId}, '${blindBalance}', '${anonBalance}')`
);
button.setAttribute('onclick', newOnclick);
} else if (currentCoinId === 3) {
window.currentBalances = window.currentBalances || {};
const mwebBalance = coinData.name === 'Litecoin MWEB' ? coinData.balance :
(window.currentBalances.mwebBalance || '0');
const mainBalance = coinData.name === 'Litecoin' ? coinData.balance :
(window.currentBalances.mainBalance || '0');
if (coinData.name === 'Litecoin') window.currentBalances.mainBalance = coinData.balance;
if (coinData.name === 'Litecoin MWEB') window.currentBalances.mwebBalance = coinData.balance;
const newOnclick = onclick.replace(
/setAmount\([^,]+,\s*'[^']*',\s*\d+,\s*'[^']*'\)/,
`setAmount(${onclick.match(/setAmount\(([^,]+)/)[1]}, '${mainBalance}', ${currentCoinId}, '${mwebBalance}')`
);
button.setAttribute('onclick', newOnclick);
} else {
const newOnclick = onclick.replace(
/setAmount\([^,]+,\s*'[^']*',\s*\d+\)/,
`setAmount(${onclick.match(/setAmount\(([^,]+)/)[1]}, '${coinData.balance}', ${currentCoinId})`
);
button.setAttribute('onclick', newOnclick);
}
}
});
}
}
function updateWalletDisplay(coinData) {
if (coinData.name === 'Particl') {
updateSpecificBalance('Particl', 'Balance:', coinData.balance, coinData.ticker || 'PART');
} else if (coinData.name === 'Particl Anon') {
updateSpecificBalance('Particl', 'Anon Balance:', coinData.balance, coinData.ticker || 'PART');
removePendingBalance('Particl', 'Anon Balance:');
if (coinData.pending && parseFloat(coinData.pending) > 0) {
updatePendingBalance('Particl', 'Anon Balance:', coinData.pending, coinData.ticker || 'PART', 'Pending:', coinData);
}
} else if (coinData.name === 'Particl Blind') {
updateSpecificBalance('Particl', 'Blind Balance:', coinData.balance, coinData.ticker || 'PART');
removePendingBalance('Particl', 'Blind Balance:');
if (coinData.pending && parseFloat(coinData.pending) > 0) {
updatePendingBalance('Particl', 'Blind Balance:', coinData.pending, coinData.ticker || 'PART', 'Unconfirmed:', coinData);
}
} else if (coinData.name === 'Litecoin MWEB') {
updateSpecificBalance('Litecoin', 'MWEB Balance:', coinData.balance, coinData.ticker || 'LTC');
if (coinData.pending && parseFloat(coinData.pending) > 0) {
updatePendingBalance('Litecoin', 'MWEB Balance:', coinData.pending, coinData.ticker || 'LTC', 'Pending:', coinData);
}
} else {
updateSpecificBalance(coinData.name, 'Balance:', coinData.balance, coinData.ticker || coinData.name);
if (coinData.pending && parseFloat(coinData.pending) > 0) {
updatePendingDisplay(coinData);
} else {
removePendingDisplay(coinData.name);
}
}
}
function updateSpecificBalance(coinName, labelText, balance, ticker, isPending = false) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
let found = false;
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinName) {
const parentRow = element.closest('tr');
if (parentRow) {
const labelCell = parentRow.querySelector('td:first-child');
if (labelCell) {
const currentLabel = labelCell.textContent.trim();
if (currentLabel.includes(labelText)) {
if (isPending) {
element.textContent = `+${balance} ${ticker}`;
} else {
element.textContent = `${balance} ${ticker}`;
}
setTimeout(() => {
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
window.WalletManager.updatePrices(false);
}
}, 50);
found = true;
}
}
}
}
});
}
function updatePendingDisplay(coinData) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinData.name) {
const parentRow = element.closest('tr');
if (parentRow) {
const labelCell = parentRow.querySelector('td:first-child');
if (labelCell && labelCell.textContent.includes('Balance:')) {
const balanceCell = parentRow.querySelector('td:nth-child(2)');
if (balanceCell) {
let pendingSpan = balanceCell.querySelector('.inline-block.py-1.px-2.rounded-full.bg-green-100');
if (coinData.pending && parseFloat(coinData.pending) > 0) {
if (!pendingSpan) {
pendingSpan = document.createElement('span');
pendingSpan.className = 'inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500';
balanceCell.appendChild(document.createTextNode(' '));
balanceCell.appendChild(pendingSpan);
}
const ticker = coinData.ticker || coinData.name;
pendingSpan.textContent = `Pending: +${coinData.pending} ${ticker}`;
} else {
if (pendingSpan) {
pendingSpan.remove();
}
}
}
}
}
}
});
}
function removePendingDisplay(coinName) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinName) {
const parentRow = element.closest('tr');
if (parentRow) {
const balanceCell = parentRow.querySelector('td:nth-child(2)');
if (balanceCell) {
const pendingSpan = balanceCell.querySelector('.inline-block.py-1.px-2.rounded-full.bg-green-100');
if (pendingSpan) {
pendingSpan.remove();
}
}
}
}
});
}
function updatePendingBalance(coinName, balanceType, pendingAmount, ticker, pendingLabel, coinData) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinName) {
const parentRow = element.closest('tr');
if (parentRow) {
const labelCell = parentRow.querySelector('td:first-child');
if (labelCell && labelCell.textContent.includes(balanceType)) {
const balanceCell = parentRow.querySelector('td:nth-child(2)');
if (balanceCell) {
let pendingSpan = balanceCell.querySelector('.inline-block.py-1.px-2.rounded-full.bg-green-100');
if (pendingAmount && parseFloat(pendingAmount) > 0) {
if (!pendingSpan) {
pendingSpan = document.createElement('span');
pendingSpan.className = 'inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500';
balanceCell.appendChild(document.createTextNode(' '));
balanceCell.appendChild(pendingSpan);
}
pendingSpan.textContent = `${pendingLabel} +${pendingAmount} ${ticker}`;
} else if (pendingSpan) {
pendingSpan.remove();
}
}
}
}
}
});
}
function removePendingBalance(coinName, balanceType) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinName) {
const parentRow = element.closest('tr');
if (parentRow) {
const labelCell = parentRow.querySelector('td:first-child');
if (labelCell && labelCell.textContent.includes(balanceType)) {
const balanceCell = parentRow.querySelector('td:nth-child(2)');
if (balanceCell) {
const pendingSpan = balanceCell.querySelector('.inline-block.py-1.px-2.rounded-full.bg-green-100');
if (pendingSpan) {
pendingSpan.remove();
}
}
}
}
}
});
}
function setupWalletWebSocketUpdates() {
window.BalanceUpdatesManager.setup({
contextKey: 'wallet',
balanceUpdateCallback: updateWalletBalances,
swapEventCallback: updateWalletBalances,
errorContext: 'Wallet',
enablePeriodicRefresh: true,
periodicInterval: 120000
});
if (window.WebSocketManager && typeof window.WebSocketManager.addMessageHandler === 'function') {
const priceHandlerId = window.WebSocketManager.addMessageHandler('message', (data) => {
if (data && data.event) {
if (data.event === 'price_updated' || data.event === 'prices_updated') {
clearTimeout(window.walletPriceUpdateTimeout);
window.walletPriceUpdateTimeout = setTimeout(() => {
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
window.WalletManager.updatePrices(true);
}
}, 500);
}
}
});
window.walletPriceHandlerId = priceHandlerId;
}
}
function cleanupWalletBalanceUpdates() {
window.BalanceUpdatesManager.cleanup('wallet');
if (window.walletPriceHandlerId && window.WebSocketManager) {
window.WebSocketManager.removeMessageHandler('message', window.walletPriceHandlerId);
}
clearTimeout(window.walletPriceUpdateTimeout);
}
window.BalanceUpdatesManager.initialize();
setupWalletWebSocketUpdates();
setTimeout(() => {
updateWalletBalances();
}, 1000);
window.addEventListener('beforeunload', cleanupWalletBalanceUpdates);
document.getElementById('confirmYes').addEventListener('click', function() {
if (typeof confirmCallback === 'function') {
confirmCallback();

View File

@@ -21,8 +21,7 @@
<div id="total-btc-value" class="text-sm text-white mt-2"></div>
</div>
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto">
<a class="rounded-full mr-5 flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/changepassword">{{ lock_svg | safe }}<span>Change/Set Password</span></a>
<a class="rounded-full flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/wallets">{{ circular_arrows_svg | safe }}<span>Refresh</span></a>
<a class="rounded-full flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" href="/changepassword">{{ lock_svg | safe }}<span>Change/Set Password</span></a>
</div>
</div>
</div>
@@ -190,5 +189,426 @@
</section>
{% include 'footer.html' %}
<script>
document.addEventListener('DOMContentLoaded', function() {
if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') {
window.WebSocketManager.initialize();
}
function setupWalletsWebSocketUpdates() {
window.BalanceUpdatesManager.setup({
contextKey: 'wallets',
balanceUpdateCallback: updateWalletBalances,
swapEventCallback: updateWalletBalances,
errorContext: 'Wallets',
enablePeriodicRefresh: true,
periodicInterval: 60000
});
if (window.WebSocketManager && typeof window.WebSocketManager.addMessageHandler === 'function') {
const priceHandlerId = window.WebSocketManager.addMessageHandler('message', (data) => {
if (data && data.event) {
if (data.event === 'price_updated' || data.event === 'prices_updated') {
clearTimeout(window.walletsPriceUpdateTimeout);
window.walletsPriceUpdateTimeout = setTimeout(() => {
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
window.WalletManager.updatePrices(true);
}
}, 500);
}
}
});
window.walletsPriceHandlerId = priceHandlerId;
}
}
function updateWalletBalances(balanceData) {
if (balanceData) {
balanceData.forEach(coin => {
updateWalletDisplay(coin);
});
setTimeout(() => {
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
window.WalletManager.updatePrices(true);
}
}, 250);
} else {
window.BalanceUpdatesManager.fetchBalanceData()
.then(data => updateWalletBalances(data))
.catch(error => {
console.error('Error updating wallet balances:', error);
});
}
}
function updateWalletDisplay(coinData) {
if (coinData.name === 'Particl') {
updateSpecificBalance('Particl', 'Balance:', coinData.balance, coinData.ticker || 'PART');
} else if (coinData.name === 'Particl Anon') {
updateSpecificBalance('Particl', 'Anon Balance:', coinData.balance, coinData.ticker || 'PART');
removePendingBalance('Particl', 'Anon Balance:');
if (coinData.pending && parseFloat(coinData.pending) > 0) {
updatePendingBalance('Particl', 'Anon Balance:', coinData.pending, coinData.ticker || 'PART', 'Anon Pending:', coinData);
}
} else if (coinData.name === 'Particl Blind') {
updateSpecificBalance('Particl', 'Blind Balance:', coinData.balance, coinData.ticker || 'PART');
removePendingBalance('Particl', 'Blind Balance:');
if (coinData.pending && parseFloat(coinData.pending) > 0) {
updatePendingBalance('Particl', 'Blind Balance:', coinData.pending, coinData.ticker || 'PART', 'Blind Unconfirmed:', coinData);
}
} else {
updateSpecificBalance(coinData.name, 'Balance:', coinData.balance, coinData.ticker || coinData.name);
if (coinData.name !== 'Particl Anon' && coinData.name !== 'Particl Blind' && coinData.name !== 'Litecoin MWEB') {
if (coinData.pending && parseFloat(coinData.pending) > 0) {
updatePendingDisplay(coinData);
} else {
removePendingDisplay(coinData.name);
}
}
}
}
function updateSpecificBalance(coinName, labelText, balance, ticker, isPending = false) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
let found = false;
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinName) {
const parentDiv = element.closest('.flex.mb-2.justify-between.items-center');
const labelElement = parentDiv ? parentDiv.querySelector('h4') : null;
if (labelElement) {
const currentLabel = labelElement.textContent.trim();
if (currentLabel === labelText) {
if (isPending) {
const cleanBalance = balance.toString().replace(/^\+/, '');
element.textContent = `+${cleanBalance} ${ticker}`;
} else {
element.textContent = `${balance} ${ticker}`;
}
found = true;
}
}
}
});
}
function updatePendingDisplay(coinData) {
const walletContainer = findWalletContainer(coinData.name);
if (!walletContainer) return;
const existingPendingElements = walletContainer.querySelectorAll('.flex.mb-2.justify-between.items-center');
let staticPendingElement = null;
let staticUsdElement = null;
existingPendingElements.forEach(element => {
const labelElement = element.querySelector('h4');
if (labelElement) {
const labelText = labelElement.textContent;
if (labelText.includes('Pending:') && !labelText.includes('USD')) {
staticPendingElement = element;
} else if (labelText.includes('Pending USD value:')) {
staticUsdElement = element;
}
}
});
if (staticPendingElement && staticUsdElement) {
const pendingSpan = staticPendingElement.querySelector('.coinname-value');
if (pendingSpan) {
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
pendingSpan.textContent = `+${cleanPending} ${coinData.ticker || coinData.name}`;
}
let initialUSD = '$0.00';
if (window.WalletManager && window.WalletManager.coinPrices) {
const coinId = coinData.name.toLowerCase().replace(' ', '-');
const price = window.WalletManager.coinPrices[coinId];
if (price && price.usd) {
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
initialUSD = `$${usdValue}`;
}
}
const usdDiv = staticUsdElement.querySelector('.usd-value');
if (usdDiv) {
usdDiv.textContent = initialUSD;
}
return;
}
let pendingContainer = walletContainer.querySelector('.pending-container');
if (!pendingContainer) {
pendingContainer = document.createElement('div');
pendingContainer.className = 'pending-container';
const pendingRow = document.createElement('div');
pendingRow.className = 'flex mb-2 justify-between items-center';
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
pendingRow.innerHTML = `
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Pending:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 coinname-value" data-coinname="${coinData.name}">+${cleanPending} ${coinData.ticker || coinData.name}</span>
`;
pendingContainer.appendChild(pendingRow);
let initialUSD = '$0.00';
if (window.WalletManager && window.WalletManager.coinPrices) {
const coinId = coinData.name.toLowerCase().replace(' ', '-');
const price = window.WalletManager.coinPrices[coinId];
if (price && price.usd) {
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
initialUSD = `$${usdValue}`;
}
}
const usdRow = document.createElement('div');
usdRow.className = 'flex mb-2 justify-between items-center';
usdRow.innerHTML = `
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Pending USD value:</h4>
<div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value">${initialUSD}</div>
`;
pendingContainer.appendChild(usdRow);
const balanceRow = walletContainer.querySelector('.flex.mb-2.justify-between.items-center');
let insertAfterElement = balanceRow;
if (balanceRow) {
let nextElement = balanceRow.nextElementSibling;
while (nextElement) {
const labelElement = nextElement.querySelector('h4');
if (labelElement && labelElement.textContent.includes('USD value:') &&
!labelElement.textContent.includes('Pending') && !labelElement.textContent.includes('Unconfirmed')) {
insertAfterElement = nextElement;
break;
}
nextElement = nextElement.nextElementSibling;
}
}
if (insertAfterElement && insertAfterElement.nextSibling) {
walletContainer.insertBefore(pendingContainer, insertAfterElement.nextSibling);
} else {
walletContainer.appendChild(pendingContainer);
}
} else {
const pendingElement = pendingContainer.querySelector('.coinname-value');
if (pendingElement) {
const cleanPending = coinData.pending.toString().replace(/^\+/, ''); // Remove existing + if any
pendingElement.textContent = `+${cleanPending} ${coinData.ticker || coinData.name}`;
}
}
}
function removePendingDisplay(coinName) {
const walletContainer = findWalletContainer(coinName);
if (!walletContainer) return;
const pendingContainer = walletContainer.querySelector('.pending-container');
if (pendingContainer) {
pendingContainer.remove();
}
const existingPendingElements = walletContainer.querySelectorAll('.flex.mb-2.justify-between.items-center');
existingPendingElements.forEach(element => {
const labelElement = element.querySelector('h4');
if (labelElement) {
const labelText = labelElement.textContent;
if (labelText.includes('Pending:') || labelText.includes('Pending USD value:')) {
element.style.display = 'none';
}
}
});
}
function removeSpecificPending(coinName, labelText) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinName) {
const parentDiv = element.closest('.flex.mb-2.justify-between.items-center');
const labelElement = parentDiv ? parentDiv.querySelector('h4') : null;
if (labelElement) {
const currentLabel = labelElement.textContent.trim();
if (currentLabel === labelText) {
parentDiv.remove();
}
}
}
});
}
function updatePendingBalance(coinName, balanceType, pendingAmount, ticker, pendingLabel, coinData) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
let targetElement = null;
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinName) {
const parentElement = element.closest('.flex.mb-2.justify-between.items-center');
if (parentElement) {
const labelElement = parentElement.querySelector('h4');
if (labelElement && labelElement.textContent.includes(balanceType)) {
targetElement = parentElement;
}
}
}
});
if (!targetElement) return;
let insertAfterElement = targetElement;
let nextElement = targetElement.nextElementSibling;
while (nextElement) {
const labelElement = nextElement.querySelector('h4');
if (labelElement) {
const labelText = labelElement.textContent;
if (labelText.includes('USD value:') && !labelText.includes('Pending') && !labelText.includes('Unconfirmed')) {
insertAfterElement = nextElement;
break;
}
if (labelText.includes('Balance:') || labelText.includes('Pending:') || labelText.includes('Unconfirmed:')) {
break;
}
}
nextElement = nextElement.nextElementSibling;
}
let pendingElement = insertAfterElement.nextElementSibling;
while (pendingElement && !pendingElement.querySelector('h4')?.textContent.includes(pendingLabel)) {
pendingElement = pendingElement.nextElementSibling;
if (pendingElement && pendingElement.querySelector('h4')?.textContent.includes('Balance:')) {
pendingElement = null;
break;
}
}
if (!pendingElement) {
const newPendingElement = document.createElement('div');
newPendingElement.className = 'flex mb-2 justify-between items-center';
newPendingElement.innerHTML = `
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">${pendingLabel}</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 coinname-value" data-coinname="${coinName}">+${pendingAmount} ${ticker}</span>
`;
insertAfterElement.parentNode.insertBefore(newPendingElement, insertAfterElement.nextSibling);
let initialUSD = '$0.00';
if (window.WalletManager && window.WalletManager.coinPrices) {
const coinId = coinName.toLowerCase().replace(' ', '-');
const price = window.WalletManager.coinPrices[coinId];
if (price && price.usd) {
const usdValue = (parseFloat(pendingAmount) * price.usd).toFixed(2);
initialUSD = `$${usdValue}`;
}
}
const usdElement = document.createElement('div');
usdElement.className = 'flex mb-2 justify-between items-center';
usdElement.innerHTML = `
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">${pendingLabel.replace(':', '')} USD value:</h4>
<div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value">${initialUSD}</div>
`;
newPendingElement.parentNode.insertBefore(usdElement, newPendingElement.nextSibling);
} else {
const pendingSpan = pendingElement.querySelector('.coinname-value');
if (pendingSpan) {
pendingSpan.textContent = `+${pendingAmount} ${ticker}`;
}
}
}
function removePendingBalance(coinName, balanceType) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
let targetElement = null;
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinName) {
const parentElement = element.closest('.flex.mb-2.justify-between.items-center');
if (parentElement) {
const labelElement = parentElement.querySelector('h4');
if (labelElement && labelElement.textContent.includes(balanceType)) {
targetElement = parentElement;
}
}
}
});
if (!targetElement) return;
let nextElement = targetElement.nextElementSibling;
while (nextElement) {
const labelElement = nextElement.querySelector('h4');
if (labelElement) {
const labelText = labelElement.textContent;
if (labelText.includes('Pending:') || labelText.includes('Unconfirmed:') ||
labelText.includes('Anon Pending:') || labelText.includes('Blind Unconfirmed:') ||
labelText.includes('Pending USD value:') || labelText.includes('Unconfirmed USD value:') ||
labelText.includes('Anon Pending USD value:') || labelText.includes('Blind Unconfirmed USD value:')) {
const elementToRemove = nextElement;
nextElement = nextElement.nextElementSibling;
elementToRemove.remove();
} else if (labelText.includes('Balance:')) {
break; // Stop if we hit another balance
} else {
nextElement = nextElement.nextElementSibling;
}
} else {
nextElement = nextElement.nextElementSibling;
}
}
}
function findWalletContainer(coinName) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
for (const element of balanceElements) {
if (element.getAttribute('data-coinname') === coinName) {
return element.closest('.bg-coolGray-100, .dark\\:bg-gray-600');
}
}
return null;
}
function cleanupWalletsBalanceUpdates() {
window.BalanceUpdatesManager.cleanup('wallets');
if (window.walletsPriceHandlerId && window.WebSocketManager) {
window.WebSocketManager.removeMessageHandler('message', window.walletsPriceHandlerId);
}
clearTimeout(window.walletsPriceUpdateTimeout);
}
window.BalanceUpdatesManager.initialize();
setupWalletsWebSocketUpdates();
setTimeout(() => {
updateWalletBalances();
}, 1000);
if (window.CleanupManager) {
window.CleanupManager.registerResource('walletsBalanceUpdates', null, cleanupWalletsBalanceUpdates);
}
window.addEventListener('beforeunload', cleanupWalletsBalanceUpdates);
});
</script>
</body>
</html>

View File

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

View File

@@ -16,6 +16,7 @@ from .util import (
have_data_entry,
inputAmount,
listAvailableCoins,
listAvailableCoinsWithBalances,
PAGE_LIMIT,
setCoinFilter,
set_pagination_filters,
@@ -526,7 +527,7 @@ def page_newoffer(self, url_split, post_string):
if swap_client.debug_ui:
messages.append("Debug mode active.")
coins_from, coins_to = listAvailableCoins(swap_client, split_from=True)
coins_from, coins_to = listAvailableCoinsWithBalances(swap_client, split_from=True)
addrs_from_raw = swap_client.listSMSGAddresses("offer_send_from")
addrs_to_raw = swap_client.listSMSGAddresses("offer_send_to")

View File

@@ -60,6 +60,38 @@ def page_settings(self, url_split, post_string):
),
}
swap_client.editGeneralSettings(data)
elif have_data_entry(form_data, "apply_notifications"):
active_tab = "notifications"
data = {
"notifications_new_offers": toBool(
get_data_entry_or(
form_data, "notifications_new_offers", "false"
)
),
"notifications_new_bids": toBool(
get_data_entry_or(form_data, "notifications_new_bids", "false")
),
"notifications_bid_accepted": toBool(
get_data_entry_or(
form_data, "notifications_bid_accepted", "false"
)
),
"notifications_balance_changes": toBool(
get_data_entry_or(
form_data, "notifications_balance_changes", "false"
)
),
"notifications_outgoing_transactions": toBool(
get_data_entry_or(
form_data, "notifications_outgoing_transactions", "false"
)
),
"notifications_duration": int(
get_data_entry_or(form_data, "notifications_duration", "20")
),
}
swap_client.editGeneralSettings(data)
messages.append("Notification settings applied.")
elif have_data_entry(form_data, "apply_tor"):
active_tab = "tor"
# TODO: Detect if running in docker
@@ -186,6 +218,27 @@ def page_settings(self, url_split, post_string):
"enabled_chart_coins": swap_client.settings.get("enabled_chart_coins", ""),
}
notification_settings = {
"notifications_new_offers": swap_client.settings.get(
"notifications_new_offers", False
),
"notifications_new_bids": swap_client.settings.get(
"notifications_new_bids", True
),
"notifications_bid_accepted": swap_client.settings.get(
"notifications_bid_accepted", True
),
"notifications_balance_changes": swap_client.settings.get(
"notifications_balance_changes", True
),
"notifications_outgoing_transactions": swap_client.settings.get(
"notifications_outgoing_transactions", True
),
"notifications_duration": swap_client.settings.get(
"notifications_duration", 20
),
}
tor_control_password = (
""
if swap_client.tor_control_password is None
@@ -209,6 +262,7 @@ def page_settings(self, url_split, post_string):
"chains": chains_formatted,
"general_settings": general_settings,
"chart_settings": chart_settings,
"notification_settings": notification_settings,
"tor_settings": tor_settings,
"active_tab": active_tab,
},

View File

@@ -659,6 +659,76 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
return coins
def listAvailableCoinsWithBalances(swap_client, with_variants=True, split_from=False):
swap_client.updateWalletsInfo()
wallets = swap_client.getCachedWalletsInfo()
coins_from = []
coins = []
for k, v in swap_client.coin_clients.items():
if k not in chainparams:
continue
if v["connection_type"] == "rpc":
balance = "0.0"
if k in wallets:
w = wallets[k]
if "balance" in w and "error" not in w and "no_data" not in w:
balance = w["balance"]
coin_entry = (int(k), getCoinName(k), balance)
coins.append(coin_entry)
if split_from:
coins_from.append(coin_entry)
if with_variants and k == Coins.PART:
for variant in (Coins.PART_ANON, Coins.PART_BLIND):
variant_balance = "0.0"
if k in wallets:
w = wallets[k]
if "error" not in w and "no_data" not in w:
if variant == Coins.PART_ANON and "anon_balance" in w:
variant_balance = w["anon_balance"]
elif variant == Coins.PART_BLIND and "blind_balance" in w:
variant_balance = w["blind_balance"]
variant_entry = (
int(variant),
getCoinName(variant),
variant_balance,
)
coins.append(variant_entry)
if split_from:
coins_from.append(variant_entry)
if with_variants and k == Coins.LTC:
for variant in (Coins.LTC_MWEB,):
variant_balance = "0.0"
if k in wallets:
w = wallets[k]
if (
"error" not in w
and "no_data" not in w
and "mweb_balance" in w
):
variant_balance = w["mweb_balance"]
variant_entry = (
int(variant),
getCoinName(variant),
variant_balance,
)
pass
if split_from:
return coins_from, coins
return coins
def checkAddressesOwned(swap_client, ci, wallet_info):
if "stealth_address" in wallet_info: