Updated wallets/wallet with backend coin prices/cache + various fixes.

This commit is contained in:
gerlofvanek 2025-03-06 21:05:27 +01:00
parent 0cf77a4854
commit 19135a3590
6 changed files with 1346 additions and 960 deletions

View file

@ -0,0 +1,649 @@
const Wallets = (function() {
const CONFIG = {
MAX_RETRIES: 5,
BASE_DELAY: 500,
CACHE_EXPIRATION: 5 * 60 * 1000,
PRICE_UPDATE_INTERVAL: 5 * 60 * 1000,
API_TIMEOUT: 30000,
DEBOUNCE_DELAY: 300,
CACHE_MIN_INTERVAL: 60 * 1000,
DEFAULT_TTL: 300,
PRICE_SOURCE: {
PRIMARY: 'coingecko.com',
FALLBACK: 'cryptocompare.com',
ENABLED_SOURCES: ['coingecko.com', 'cryptocompare.com']
}
};
const COIN_SYMBOLS = {
'Bitcoin': 'BTC',
'Particl': 'PART',
'Monero': 'XMR',
'Wownero': 'WOW',
'Litecoin': 'LTC',
'Dogecoin': 'DOGE',
'Firo': 'FIRO',
'Dash': 'DASH',
'PIVX': 'PIVX',
'Decred': 'DCR',
'Bitcoin Cash': 'BCH'
};
const COINGECKO_IDS = {
'BTC': 'btc',
'PART': 'part',
'XMR': 'xmr',
'WOW': 'wownero',
'LTC': 'ltc',
'DOGE': 'doge',
'FIRO': 'firo',
'DASH': 'dash',
'PIVX': 'pivx',
'DCR': 'dcr',
'BCH': 'bch'
};
const SHORT_NAMES = {
'Bitcoin': 'BTC',
'Particl': 'PART',
'Monero': 'XMR',
'Wownero': 'WOW',
'Litecoin': 'LTC',
'Litecoin MWEB': 'LTC MWEB',
'Firo': 'FIRO',
'Dash': 'DASH',
'PIVX': 'PIVX',
'Decred': 'DCR',
'Bitcoin Cash': 'BCH',
'Dogecoin': 'DOGE'
};
class Cache {
constructor(expirationTime) {
this.data = null;
this.timestamp = null;
this.expirationTime = expirationTime;
}
isValid() {
return Boolean(
this.data &&
this.timestamp &&
(Date.now() - this.timestamp < this.expirationTime)
);
}
set(data) {
this.data = data;
this.timestamp = Date.now();
}
get() {
if (this.isValid()) {
return this.data;
}
return null;
}
clear() {
this.data = null;
this.timestamp = null;
}
}
class ApiClient {
constructor() {
this.cache = new Cache(CONFIG.CACHE_EXPIRATION);
this.lastFetchTime = 0;
}
async fetchPrices(forceUpdate = false) {
const now = Date.now();
const timeSinceLastFetch = now - this.lastFetchTime;
if (!forceUpdate && timeSinceLastFetch < CONFIG.CACHE_MIN_INTERVAL) {
const cachedData = this.cache.get();
if (cachedData) {
return cachedData;
}
}
const mainCoins = Object.values(COIN_SYMBOLS)
.filter(symbol => symbol !== 'WOW')
.map(symbol => COINGECKO_IDS[symbol] || symbol.toLowerCase())
.join(',');
let lastError = null;
for (let attempt = 0; attempt < CONFIG.MAX_RETRIES; attempt++) {
try {
const processedData = {};
const mainResponse = await fetch("/json/coinprices", {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
coins: mainCoins,
source: CONFIG.PRICE_SOURCE.PRIMARY,
ttl: CONFIG.DEFAULT_TTL
})
});
if (!mainResponse.ok) {
throw new Error(`HTTP error: ${mainResponse.status}`);
}
const mainData = await mainResponse.json();
if (mainData && mainData.rates) {
Object.entries(mainData.rates).forEach(([coinId, price]) => {
const symbol = Object.entries(COINGECKO_IDS).find(([sym, id]) => id.toLowerCase() === coinId.toLowerCase())?.[0];
if (symbol) {
const coinKey = Object.keys(COIN_SYMBOLS).find(key => COIN_SYMBOLS[key] === symbol);
if (coinKey) {
processedData[coinKey.toLowerCase().replace(' ', '-')] = {
usd: price,
btc: symbol === 'BTC' ? 1 : price / (mainData.rates.btc || 1)
};
}
}
});
}
try {
const wowResponse = await fetch("/json/coinprices", {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
coins: "wownero",
source: "coingecko.com",
ttl: CONFIG.DEFAULT_TTL
})
});
if (wowResponse.ok) {
const wowData = await wowResponse.json();
if (wowData && wowData.rates && wowData.rates.wownero) {
processedData['wownero'] = {
usd: wowData.rates.wownero,
btc: processedData.bitcoin ? wowData.rates.wownero / processedData.bitcoin.usd : 0
};
}
}
} catch (wowError) {
console.error('Error fetching WOW price:', wowError);
}
this.cache.set(processedData);
this.lastFetchTime = now;
return processedData;
} catch (error) {
lastError = error;
console.error(`Price fetch attempt ${attempt + 1} failed:`, error);
if (attempt === CONFIG.MAX_RETRIES - 1 &&
CONFIG.PRICE_SOURCE.FALLBACK &&
CONFIG.PRICE_SOURCE.FALLBACK !== CONFIG.PRICE_SOURCE.PRIMARY) {
const temp = CONFIG.PRICE_SOURCE.PRIMARY;
CONFIG.PRICE_SOURCE.PRIMARY = CONFIG.PRICE_SOURCE.FALLBACK;
CONFIG.PRICE_SOURCE.FALLBACK = temp;
console.warn(`Switching to fallback source: ${CONFIG.PRICE_SOURCE.PRIMARY}`);
attempt = -1;
continue;
}
if (attempt < CONFIG.MAX_RETRIES - 1) {
const delay = Math.min(CONFIG.BASE_DELAY * Math.pow(2, attempt), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
const cachedData = this.cache.get();
if (cachedData) {
console.warn('Using cached data after fetch failures');
return cachedData;
}
throw lastError || new Error('Failed to fetch prices');
}
setPriceSource(primarySource, fallbackSource = null) {
if (!CONFIG.PRICE_SOURCE.ENABLED_SOURCES.includes(primarySource)) {
throw new Error(`Invalid primary source: ${primarySource}`);
}
if (fallbackSource && !CONFIG.PRICE_SOURCE.ENABLED_SOURCES.includes(fallbackSource)) {
throw new Error(`Invalid fallback source: ${fallbackSource}`);
}
CONFIG.PRICE_SOURCE.PRIMARY = primarySource;
if (fallbackSource) {
CONFIG.PRICE_SOURCE.FALLBACK = fallbackSource;
}
}
}
class UiManager {
constructor() {
this.api = new ApiClient();
this.toggleInProgress = false;
this.toggleDebounceTimer = null;
this.priceUpdateInterval = null;
this.lastUpdateTime = parseInt(localStorage.getItem(STATE_KEYS.LAST_UPDATE) || '0');
this.isWalletsPage = document.querySelector('.wallet-list') !== null ||
window.location.pathname.includes('/wallets');
}
getShortName(fullName) {
return SHORT_NAMES[fullName] || fullName;
}
storeOriginalValues() {
document.querySelectorAll('.coinname-value').forEach(el => {
const coinName = el.getAttribute('data-coinname');
const value = el.textContent?.trim() || '';
if (coinName) {
const amount = value ? parseFloat(value.replace(/[^0-9.-]+/g, '')) : 0;
const coinId = COIN_SYMBOLS[coinName];
const shortName = this.getShortName(coinName);
if (coinId) {
if (coinName === 'Particl') {
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
} else if (coinName === 'Litecoin') {
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('MWEB');
const balanceType = isMWEB ? 'mweb' : 'public';
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
} else {
localStorage.setItem(`${coinId.toLowerCase()}-amount`, amount.toString());
}
el.setAttribute('data-original-value', `${amount} ${shortName}`);
}
}
});
document.querySelectorAll('.usd-value').forEach(el => {
const text = el.textContent?.trim() || '';
if (text === 'Loading...') {
el.textContent = '';
}
});
}
async updatePrices(forceUpdate = false) {
try {
const prices = await this.api.fetchPrices(forceUpdate);
let newTotal = 0;
const currentTime = Date.now();
localStorage.setItem(STATE_KEYS.LAST_UPDATE, currentTime.toString());
this.lastUpdateTime = currentTime;
if (prices) {
Object.entries(prices).forEach(([coinId, priceData]) => {
if (priceData?.usd) {
localStorage.setItem(`${coinId}-price`, priceData.usd.toString());
}
});
}
document.querySelectorAll('.coinname-value').forEach(el => {
const coinName = el.getAttribute('data-coinname');
const amountStr = el.getAttribute('data-original-value') || el.textContent?.trim() || '';
if (!coinName) return;
let amount = 0;
if (amountStr) {
const matches = amountStr.match(/([0-9]*[.])?[0-9]+/);
if (matches && matches.length > 0) {
amount = parseFloat(matches[0]);
}
}
const coinId = coinName.toLowerCase().replace(' ', '-');
if (!prices[coinId]) {
return;
}
const price = prices[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
if (!price) return;
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');
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
localStorage.setItem(`particl-${balanceType}-last-value`, usdValue);
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
} else if (coinName === 'Litecoin') {
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('MWEB');
const balanceType = isMWEB ? 'mweb' : 'public';
localStorage.setItem(`litecoin-${balanceType}-last-value`, usdValue);
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
} else {
localStorage.setItem(`${coinId}-last-value`, usdValue);
localStorage.setItem(`${coinId}-amount`, amount.toString());
}
if (amount > 0) {
newTotal += parseFloat(usdValue);
}
let usdEl = null;
const flexContainer = el.closest('.flex');
if (flexContainer) {
const nextFlex = flexContainer.nextElementSibling;
if (nextFlex) {
const usdInNextFlex = nextFlex.querySelector('.usd-value');
if (usdInNextFlex) {
usdEl = usdInNextFlex;
}
}
}
if (!usdEl) {
const parentCell = el.closest('td');
if (parentCell) {
const usdInSameCell = parentCell.querySelector('.usd-value');
if (usdInSameCell) {
usdEl = usdInSameCell;
}
}
}
if (!usdEl) {
const sibling = el.nextElementSibling;
if (sibling && sibling.classList.contains('usd-value')) {
usdEl = sibling;
}
}
if (!usdEl) {
const parentElement = el.parentElement;
if (parentElement) {
const usdElNearby = parentElement.querySelector('.usd-value');
if (usdElNearby) {
usdEl = usdElNearby;
}
}
}
if (usdEl) {
usdEl.textContent = `$${usdValue}`;
usdEl.setAttribute('data-original-value', usdValue);
}
});
document.querySelectorAll('.usd-value').forEach(el => {
if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
const parentCell = el.closest('td');
if (!parentCell) return;
const coinValueEl = parentCell.querySelector('.coinname-value');
if (!coinValueEl) return;
const coinName = coinValueEl.getAttribute('data-coinname');
if (!coinName) return;
const amountStr = coinValueEl.textContent?.trim() || '0';
const amount = parseFloat(amountStr) || 0;
const coinId = coinName.toLowerCase().replace(' ', '-');
if (!prices[coinId]) return;
const price = prices[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
if (!price) return;
const usdValue = (amount * price).toFixed(8);
el.textContent = `$${usdValue}`;
el.setAttribute('data-original-value', usdValue);
}
});
if (this.isWalletsPage) {
this.updateTotalValues(newTotal, prices?.bitcoin?.usd);
}
localStorage.setItem(STATE_KEYS.PREVIOUS_TOTAL, localStorage.getItem(STATE_KEYS.CURRENT_TOTAL) || '0');
localStorage.setItem(STATE_KEYS.CURRENT_TOTAL, newTotal.toString());
return true;
} catch (error) {
console.error('Price update failed:', error);
return false;
}
}
updateTotalValues(totalUsd, btcPrice) {
const totalUsdEl = document.getElementById('total-usd-value');
if (totalUsdEl) {
totalUsdEl.textContent = `$${totalUsd.toFixed(2)}`;
totalUsdEl.setAttribute('data-original-value', totalUsd.toString());
localStorage.setItem('total-usd', totalUsd.toString());
}
if (btcPrice) {
const btcTotal = btcPrice ? totalUsd / btcPrice : 0;
const totalBtcEl = document.getElementById('total-btc-value');
if (totalBtcEl) {
totalBtcEl.textContent = `~ ${btcTotal.toFixed(8)} BTC`;
totalBtcEl.setAttribute('data-original-value', btcTotal.toString());
}
}
}
async toggleBalances() {
if (this.toggleInProgress) return;
try {
this.toggleInProgress = true;
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
const newVisibility = !balancesVisible;
localStorage.setItem('balancesVisible', newVisibility.toString());
this.updateVisibility(newVisibility);
if (this.toggleDebounceTimer) {
clearTimeout(this.toggleDebounceTimer);
}
this.toggleDebounceTimer = window.setTimeout(async () => {
this.toggleInProgress = false;
if (newVisibility) {
await this.updatePrices(true);
}
}, CONFIG.DEBOUNCE_DELAY);
} catch (error) {
console.error('Failed to toggle balances:', error);
this.toggleInProgress = false;
}
}
updateVisibility(isVisible) {
if (isVisible) {
this.showBalances();
} else {
this.hideBalances();
}
const eyeIcon = document.querySelector("#hide-usd-amount-toggle svg");
if (eyeIcon) {
eyeIcon.innerHTML = isVisible ?
'<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z"></path>' :
'<path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z"></path><path d="M12,3C6.292,3,2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z"></path><path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path>';
}
}
showBalances() {
const usdText = document.getElementById('usd-text');
if (usdText) {
usdText.style.display = 'inline';
}
document.querySelectorAll('.coinname-value').forEach(el => {
const originalValue = el.getAttribute('data-original-value');
if (originalValue) {
el.textContent = originalValue;
}
});
document.querySelectorAll('.usd-value').forEach(el => {
const storedValue = el.getAttribute('data-original-value');
if (storedValue !== null && storedValue !== undefined) {
if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
el.textContent = `$${parseFloat(storedValue).toFixed(8)}`;
} else {
el.textContent = `$${parseFloat(storedValue).toFixed(2)}`;
}
} else {
if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
el.textContent = '$0.00000000';
} else {
el.textContent = '$0.00';
}
}
});
if (this.isWalletsPage) {
['total-usd-value', 'total-btc-value'].forEach(id => {
const el = document.getElementById(id);
const originalValue = el?.getAttribute('data-original-value');
if (el && originalValue) {
if (id === 'total-usd-value') {
el.textContent = `$${parseFloat(originalValue).toFixed(2)}`;
el.classList.add('font-extrabold');
} else {
el.textContent = `~ ${parseFloat(originalValue).toFixed(8)} BTC`;
}
}
});
}
}
hideBalances() {
const usdText = document.getElementById('usd-text');
if (usdText) {
usdText.style.display = 'none';
}
document.querySelectorAll('.coinname-value').forEach(el => {
el.textContent = '****';
});
document.querySelectorAll('.usd-value').forEach(el => {
el.textContent = '****';
});
if (this.isWalletsPage) {
['total-usd-value', 'total-btc-value'].forEach(id => {
const el = document.getElementById(id);
if (el) {
el.textContent = '****';
}
});
const totalUsdEl = document.getElementById('total-usd-value');
if (totalUsdEl) {
totalUsdEl.classList.remove('font-extrabold');
}
}
}
async initialize() {
document.querySelectorAll('.usd-value').forEach(el => {
const text = el.textContent?.trim() || '';
if (text === 'Loading...') {
el.textContent = '';
}
});
this.storeOriginalValues();
if (localStorage.getItem('balancesVisible') === null) {
localStorage.setItem('balancesVisible', 'true');
}
const hideBalancesToggle = document.getElementById('hide-usd-amount-toggle');
if (hideBalancesToggle) {
hideBalancesToggle.addEventListener('click', () => this.toggleBalances());
}
await this.loadBalanceVisibility();
if (this.priceUpdateInterval) {
clearInterval(this.priceUpdateInterval);
}
this.priceUpdateInterval = setInterval(() => {
if (localStorage.getItem('balancesVisible') === 'true' && !this.toggleInProgress) {
this.updatePrices(false);
}
}, CONFIG.PRICE_UPDATE_INTERVAL);
}
async loadBalanceVisibility() {
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
this.updateVisibility(balancesVisible);
if (balancesVisible) {
await this.updatePrices(true);
}
}
cleanup() {
if (this.priceUpdateInterval) {
clearInterval(this.priceUpdateInterval);
}
}
}
const STATE_KEYS = {
LAST_UPDATE: 'last-update-time',
PREVIOUS_TOTAL: 'previous-total-usd',
CURRENT_TOTAL: 'current-total-usd',
BALANCES_VISIBLE: 'balancesVisible'
};
return {
initialize: function() {
const uiManager = new UiManager();
window.cryptoPricingManager = uiManager;
window.addEventListener('beforeunload', () => {
uiManager.cleanup();
});
uiManager.initialize().catch(error => {
console.error('Failed to initialize crypto pricing:', error);
});
return uiManager;
},
getUiManager: function() {
return window.cryptoPricingManager;
},
setPriceSource: function(primarySource, fallbackSource = null) {
const uiManager = this.getUiManager();
if (uiManager && uiManager.api) {
uiManager.api.setPriceSource(primarySource, fallbackSource);
}
}
};
})();
document.addEventListener('DOMContentLoaded', function() {
Wallets.initialize();
});

View file

@ -121,6 +121,23 @@
});
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const closeButtons = document.querySelectorAll('[data-dismiss-target]');
closeButtons.forEach(button => {
button.addEventListener('click', function() {
const targetId = this.getAttribute('data-dismiss-target');
const targetElement = document.querySelector(targetId);
if (targetElement) {
targetElement.style.display = 'none';
}
});
});
});
</script>
<script>
function getAPIKeys() {
return {
@ -140,7 +157,6 @@ function getWebSocketConfig() {
</head>
<body class="dark:bg-gray-700">
<!-- Shutdown Modal -->
<div id="shutdownModal" tabindex="-1" class="hidden fixed inset-0 z-50 overflow-y-auto overflow-x-hidden">
<div class="fixed inset-0 bg-black bg-opacity-60 transition-opacity"></div>
<div class="flex items-center justify-center min-h-screen p-4 relative z-10">

View file

@ -1,8 +1,8 @@
{% from 'style.html' import circular_info_messages_svg, green_cross_close_svg, red_cross_close_svg, circular_error_messages_svg %}
{% for m in messages %}
<section class="py-4" id="messages_{{ m[0] }}" role="alert">
<div class="container px-4 mx-auto">
<section class="py-4 px-6" id="messages_{{ m[0] }}" role="alert">
<div class="lg:container mx-auto">
<div class="p-6 text-green-800 rounded-lg bg-green-50 border border-green-500 dark:bg-gray-500 dark:text-green-400 rounded-md">
<div class="flex flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2">
@ -27,8 +27,8 @@
</section>
{% endfor %}
{% if err_messages %}
<section class="py-4" id="err_messages_{{ err_messages[0][0] }}" role="alert">
<div class="container px-4 mx-auto">
<section class="py-4 px-6" id="err_messages_{{ err_messages[0][0] }}" role="alert">
<div class="lg:container mx-auto">
<div class="p-6 text-green-800 rounded-lg bg-red-50 border border-red-400 dark:bg-gray-500 dark:text-red-400 rounded-md">
<div class="flex flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2">

View file

@ -422,7 +422,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Enabled Coins</td>
<td class="py-3 px-6">
<label for="enabledchartcoins" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Coins to show data for: Blank for active coins, "all" for all known coins or comma separated list of coin tickers to show
<label for="enabledchartcoins" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Coins to show data for: Blank for active coins, "all" for all known coins or comma separated<br/> list of coin tickers to show
<br />
</label>
<input name="enabledchartcoins" type="text" 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-2.5 focus:ring-0" value="{{chart_settings.enabled_chart_coins}}">

View file

@ -1,34 +1,26 @@
{% include 'header.html' %}
{% from 'style.html' import select_box_arrow_svg, select_box_class, circular_arrows_svg, circular_error_svg, circular_info_svg, cross_close_svg, breadcrumb_line_svg, withdraw_svg, utxo_groups_svg, create_utxo_svg, red_cross_close_svg, blue_cross_close_svg, circular_update_messages_svg, circular_error_messages_svg %}
<script src="/static/js/libs//qrcode.js"></script>
<section class="p-5 mt-5">
<div class="container mx-auto">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li><a class="flex font-medium text-md lg:text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">Home</a></li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li><a class="flex font-medium text-md lg:text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/wallets">Wallets</a></li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li><a class="flex font-medium text-md lg:text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/wallet/{{ w.ticker }}">{{ w.ticker }}</a></li>
</ul>
</div>
</div>
</div>
</section>
<section class="py-4 px-6">
<section class="py-3 px-4 mt-6">
<div class="lg:container mx-auto">
<div class="relative py-11 px-16 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden"> <img class="absolute z-10 left-4 top-4 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="dots-red"> <img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="wave">
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="dots-red">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="wave">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2">
<h2 class="text-3xl font-bold text-white"> <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>
<h2 class="text-3xl font-bold text-white">
<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>
</div>
</div>
</section>
{% include 'inc_messages.html' %}
{% if w.updating %}
<section class="py-4 px-6" id="messages_updating" role="alert">
<div class="lg:container mx-auto">
@ -55,6 +47,7 @@
</div>
</section>
{% endif %}
{% if w.havedata %}
{% if w.error %}
<section class="py-4 px-6" id="messages_error" role="alert">
@ -83,17 +76,16 @@
</div>
</section>
{% else %}
{% if w.cid == '18' %} {# DOGE #}
<section class="py-4 px-6" id="messages_notice">
<div class="lg:container mx-auto">
<div class="p-6 rounded-lg bg-coolGray-100 dark:bg-gray-500 shadow-sm">
<div class="flex items-start">
<svg class="w-6 h-6 text-blue-500 mt-1 mr-3 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"></path></svg>
<div class="flex flex-wrap -m-1">
<ul class="ml-4">
<li class="font-semibold text-lg dark:text-white mb-2">NOTICE:</li>
<li class="font-medium text-gray-600 dark:text-white leading-relaxed">
This version of DOGE Core is experimental and has been custom-built for compatibility with BasicSwap. As a result, it may not always be fully aligned with upstream changes, features unrelated to BasicSwap might not work as expected, and its code may differ from the official release.
NOTICE: This version of DOGE Core is experimental and has been custom-built for compatibility with BasicSwap. As a result, it may not always be fully aligned with upstream changes, features unrelated to BasicSwap might not work as expected, and its code may differ from the official release.
</li>
</ul>
</div>
@ -102,9 +94,10 @@
</div>
</section>
{% endif %}
<section>
<form method="post" autocomplete="off">
<div class="px-6 py-0 mt-5 h-full overflow-hidden">
<div class="px-6 py-0 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
<div class="flex flex-wrap items-center justify-between -m-2">
<div class="w-full pt-2">
@ -125,7 +118,9 @@
</thead>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Balance: </td>
<td class="py-3 px-6 bold coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }} (<span class="usd-value"></span>)
<td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.pending }} {{ w.ticker }} </span>
{% endif %}
@ -134,7 +129,9 @@
{% if w.cid == '1' %} {# PART #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Blind"> </span>Blind Balance: </td>
<td class="py-3 px-6 bold coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }} (<span class="usd-value"></span>)
<td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.blind_unconfirmed %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Unconfirmed: +{{ w.blind_unconfirmed }} {{ w.ticker }}</span>
{% endif %}
@ -142,18 +139,21 @@
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Anon"> </span>Anon Balance: </td>
<td class="py-3 px-6 bold coinname-value" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }} (<span class="usd-value"></span>)
<td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.anon_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.anon_pending }} {{ w.ticker }}</span>
{% endif %}
</td>
<td class="usd-value"></td>
</tr>
{# / PART #}
{% elif w.cid == '3' %} {# LTC #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} MWEB"> </span>MWEB Balance: </td>
<td class="py-3 px-6 bold coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }} (<span class="usd-value"></span>)
<td class="py-3 px-6 bold">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
{% if w.mweb_pending %}
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.mweb_pending }} {{ w.ticker }} </span>
{% endif %}
@ -223,6 +223,7 @@
</div>
</div>
</section>
{% if block_unknown_seeds and w.expected_seed != true %} {# Only show addresses if wallet seed is correct #}
<section class="px-6 py-0 h-full overflow-hidden">
<div class="pb-6 border-coolGray-100">
@ -246,6 +247,7 @@
<input type="hidden" name="formid" value="{{ form_id }}">
</form>
{% else %}
<section class="p-6">
<div class="lg:container mx-auto">
<div class="flex items-center">
@ -253,6 +255,7 @@
</div>
</div>
</section>
<form method="post" autocomplete="off">
<section>
<div class="px-6 py-0 overflow-hidden">
@ -276,9 +279,8 @@
</div>
<div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">Main Address: </div>
<div class="relative flex justify-center items-center">
<div data-tooltip-target="tooltip-copy-monero-main" class="input-like-container 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-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full focus:ring-0" id="monero_main_address">{{ w.main_address }}</div>
<div class="input-like-container 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-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full focus:ring-0" id="monero_main_address">{{ w.main_address }}</div>
</div>
<div id="tooltip-copy-monero-main" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-lg lg:text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
{% else %}
<div id="qrcode-deposit" class="qrcode"> </div>
</div>
@ -292,11 +294,7 @@
<button type="submit" class="flex justify-center py-2 px-4 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" name="newaddr_{{ w.cid }}" value="New Deposit Address"> {{ circular_arrows_svg }} New {{ w.name }} Deposit Address </button>
</div>
</div>
<div id="tooltip-copy-default" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-lg lg:text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
{% endif %}
<p>Copy to clipboard</p>
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
</div>
</div>
</div>
@ -316,14 +314,13 @@
</div>
<div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">Subaddress: </div>
<div class="relative flex justify-center items-center">
<div data-tooltip-target="tooltip-copy-monero-sub" class="input-like-container 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-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full focus:ring-0" id="monero_sub_address">{{ w.deposit_address }}</div>
<div class="input-like-container 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-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full focus:ring-0" id="monero_sub_address">{{ w.deposit_address }}</div>
</div>
<div class="opacity-100 text-gray-500 dark:text-gray-100 flex justify-center items-center">
<div class="py-3 px-6 bold mt-5">
<button type="submit" class="flex justify-center py-2 px-4 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" name="newaddr_{{ w.cid }}" value="New Subaddress"> {{ circular_arrows_svg }} New {{ w.name }} Deposit Address</button>
</div>
</div>
<div id="tooltip-copy-monero-sub" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-lg lg:text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
{% elif w.cid == '1' %}
{# PART #}
<div id="qrcode-stealth" class="qrcode"> </div>
@ -331,9 +328,8 @@
</div>
<div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">Stealth Address: </div>
<div class="relative flex justify-center items-center">
<div data-tooltip-target="tooltip-copy-particl-stealth" class="input-like-container 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-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-10 focus:ring-0" id="stealth_address"> {{ w.stealth_address }}
<div class="input-like-container 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-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-10 focus:ring-0" id="stealth_address"> {{ w.stealth_address }}
</div>
<div id="tooltip-copy-particl-stealth" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-lg lg:text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
{# / PART #}
{% elif w.cid == '3' %}
{# LTC #}
@ -342,7 +338,7 @@
</div>
<div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">MWEB Address: </div>
<div class="text-center relative">
<div data-tooltip-target="tooltip-copy-litecoin-mweb" class="input-like-container 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-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" id="stealth_address">{{ w.mweb_address }}</div>
<div class="input-like-container 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-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" id="stealth_address">{{ w.mweb_address }}</div>
<span class="absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer" id="copyIcon"></span>
</div>
<div class="opacity-100 text-gray-500 dark:text-gray-100 flex justify-center items-center">
@ -350,11 +346,8 @@
<button type="submit" class="flex justify-center py-2 px-4 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" name="newmwebaddr_{{ w.cid }}" value="New MWEB Address"> {{ circular_arrows_svg }} New MWEB Address </button>
</div>
</div>
<div id="tooltip-copy-litecoin-mweb" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-lg lg:text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
{# / LTC #}
{% endif %}
<p>Copy to clipboard</p>
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
</div>
</div>
@ -368,6 +361,7 @@
</div>
</div>
</section>
{% if w.cid == '1' %}
{# PART #}
<script>
@ -383,6 +377,7 @@
correctLevel: QRCode.CorrectLevel.L
});
</script>
{% elif w.cid == '3' %}
{# LTC #}
<script>
@ -399,6 +394,7 @@
});
</script>
{% endif %}
{% if w.cid in '6, 9' %}
{# XMR | WOW #}
<script>
@ -414,6 +410,7 @@
correctLevel: QRCode.CorrectLevel.L
});
</script>
<script>
// Monero Main
var moneroMainAddress = "{{ w.main_address }}";
@ -427,6 +424,7 @@
correctLevel: QRCode.CorrectLevel.L
});
</script>
{% else %}
<script>
// Default
@ -442,64 +440,148 @@
});
</script>
{% endif %}
<script>
let clickTimeout = null;
document.addEventListener('DOMContentLoaded', function() {
setupAddressCopy();
});
function setupAddressCopy() {
const copyableElements = [
'main_deposit_address',
'monero_main_address',
'monero_sub_address',
'stealth_address'
];
copyableElements.forEach(id => {
const element = document.getElementById(id);
if (!element) return;
element.classList.add('cursor-pointer', 'hover:bg-gray-100', 'dark:hover:bg-gray-600', 'transition-colors');
if (!element.querySelector('.copy-icon')) {
const copyIcon = document.createElement('span');
copyIcon.className = 'copy-icon absolute right-2 inset-y-0 flex items-center text-gray-500 dark:text-gray-300';
copyIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>`;
element.style.position = 'relative';
element.style.paddingRight = '2.5rem';
element.appendChild(copyIcon);
}
element.addEventListener('click', function(e) {
const textToCopy = this.innerText.trim();
copyToClipboard(textToCopy);
this.classList.add('bg-blue-50', 'dark:bg-blue-900');
showCopyFeedback(this);
setTimeout(() => {
this.classList.remove('bg-blue-50', 'dark:bg-blue-900');
}, 1000);
});
});
}
let activeTooltip = null;
function showCopyFeedback(element) {
if (activeTooltip && activeTooltip.parentNode) {
activeTooltip.parentNode.removeChild(activeTooltip);
}
const popup = document.createElement('div');
popup.className = 'copy-feedback-popup fixed z-50 bg-blue-600 text-white text-sm py-2 px-3 rounded-md shadow-lg';
popup.innerText = 'Copied!';
document.body.appendChild(popup);
activeTooltip = popup;
updateTooltipPosition(popup, element);
const scrollHandler = () => {
if (popup.parentNode) {
updateTooltipPosition(popup, element);
}
};
window.addEventListener('scroll', scrollHandler, { passive: true });
popup.style.opacity = '0';
popup.style.transition = 'opacity 0.2s ease-in-out';
setTimeout(() => {
popup.style.opacity = '1';
}, 10);
setTimeout(() => {
window.removeEventListener('scroll', scrollHandler);
popup.style.opacity = '0';
setTimeout(() => {
if (popup.parentNode) {
popup.parentNode.removeChild(popup);
}
if (activeTooltip === popup) {
activeTooltip = null;
}
}, 200);
}, 1500);
}
function updateTooltipPosition(tooltip, element) {
const rect = element.getBoundingClientRect();
let top = rect.top - tooltip.offsetHeight - 8;
const left = rect.left + rect.width / 2;
if (top < 10) {
top = rect.bottom + 8;
}
tooltip.style.top = `${top}px`;
tooltip.style.left = `${left}px`;
tooltip.style.transform = 'translateX(-50%)';
}
function copyToClipboard(text) {
const el = document.createElement('textarea');
el.value = text;
document.body.appendChild(el);
el.select();
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(err => {
console.error('Failed to copy: ', err);
copyToClipboardFallback(text);
});
} else {
copyToClipboardFallback(text);
}
}
function copyToClipboardFallback(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
document.body.removeChild(el);
} catch (err) {
console.error('Failed to copy text: ', err);
}
function copyAndShowMessage(elementId) {
const addressElement = document.getElementById(elementId);
if (!addressElement) return;
const addressText = addressElement.innerText.trim();
if (addressText === 'Copied to clipboard') return;
copyToClipboard(addressText);
addressElement.innerText = 'Copied to clipboard';
const originalWidth = addressElement.offsetWidth;
addressElement.classList.add('copying');
addressElement.parentElement.style.width = `${originalWidth}px`;
setTimeout(function () {
addressElement.innerText = addressText;
addressElement.classList.remove('copying');
addressElement.parentElement.style.width = '';
}, 1000);
document.body.removeChild(textArea);
}
document.addEventListener('DOMContentLoaded', function() {
const stealthAddressElement = document.getElementById('stealth_address');
if (stealthAddressElement) {
stealthAddressElement.addEventListener('click', function() {
copyAndShowMessage('stealth_address');
});
}
const mainDepositAddressElement = document.getElementById('main_deposit_address');
if (mainDepositAddressElement) {
mainDepositAddressElement.addEventListener('click', function() {
copyAndShowMessage('main_deposit_address');
});
}
const moneroMainAddressElement = document.getElementById('monero_main_address');
if (moneroMainAddressElement) {
moneroMainAddressElement.addEventListener('click', function() {
copyAndShowMessage('monero_main_address');
});
}
const moneroSubAddressElement = document.getElementById('monero_sub_address');
if (moneroSubAddressElement) {
moneroSubAddressElement.addEventListener('click', function() {
copyAndShowMessage('monero_sub_address');
});
}
});
</script>
<section class="p-6">
<div class="lg:container mx-auto">
<div class="flex items-center">
@ -507,6 +589,7 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
</div>
</section>
<section>
<div class="px-6 py-0 h-full overflow-hidden">
<div class="border-coolGray-100">
@ -529,23 +612,35 @@ document.addEventListener('DOMContentLoaded', function() {
</thead>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Balance: </td>
<td class="py-3 px-6" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }} </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
</td>
</tr>
{% if w.cid == '3' %}
{# LTC #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold w-1/4"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>MWEB Balance: </td>
<td class="py-3 px-6" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }} </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.mweb_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
</td>
</tr>
{% elif w.cid == '1' %}
{# PART #}
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Blind Balance: </td>
<td class="py-3 px-6" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }} </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.blind_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-4 pl-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Anon Balance: </td>
<td class="py-3 px-6" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }} </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.anon_balance }} {{ w.ticker }}</span>
(<span class="usd-value"></span>)
</td>
</tr>
{% endif %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
@ -557,11 +652,13 @@ document.addEventListener('DOMContentLoaded', function() {
<td class="py-3 px-6">
<div class="flex"> <input placeholder="{{ w.ticker }} Amount" 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-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" type="text" id="amount" name="amt_{{ w.cid }}" value="{{ w.wd_value }}">
<div class="ml-2 flex">
{% if w.cid == '1' %}
{# PART #}
<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="setAmount(0.25, '{{ w.balance }}', {{ w.cid }}, '{{ w.blind_balance }}', '{{ w.anon_balance }}')">25%</button>
<button type="button" class="hidden md:block ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }}, '{{ w.blind_balance }}', '{{ w.anon_balance }}')">50%</button>
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }}, '{{ w.blind_balance }}', '{{ w.anon_balance }}')">100%</button>
<script>
function setAmount(percent, balance, cid, blindBalance, anonBalance) {
var amountInput = document.getElementById('amount');
@ -570,6 +667,75 @@ document.addEventListener('DOMContentLoaded', function() {
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);
if (!isNaN(numValue) && numValue > 0) {
return numValue;
}
console.warn('Invalid balance value:', value);
return 0;
};
switch(selectedType) {
case 'plain':
floatBalance = safeParseFloat(balance);
break;
case 'blind':
floatBalance = safeParseFloat(blindBalance);
break;
case 'anon':
floatBalance = safeParseFloat(anonBalance);
break;
default:
floatBalance = safeParseFloat(balance);
break;
}
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;
}
if (calculatedAmount < 0.00000001) {
console.warn('Calculated amount too small, setting to zero');
calculatedAmount = 0;
}
amountInput.value = calculatedAmount.toFixed(8);
var subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
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);
@ -584,21 +750,31 @@ document.addEventListener('DOMContentLoaded', function() {
floatBalance = parseFloat(balance);
break;
}
calculatedAmount = floatBalance * percent;
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>
{# / PART #}
{% elif w.cid == '3' %}
{# LTC #}
<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="setAmount(0.25, '{{ w.balance }}', {{ w.cid }}, '{{ w.mweb_balance }}')">25%</button>
<button type="button" class="hidden md:block ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }}, '{{ w.mweb_balance }}')">50%</button>
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }}, '{{ w.mweb_balance }}')">100%</button>
<script>
function setAmount(percent, balance, cid, mwebBalance) {
var amountInput = document.getElementById('amount');
@ -627,11 +803,13 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
</script>
{# / LTC #}
{% else %}
<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="setAmount(0.25, '{{ w.balance }}', {{ w.cid }})">25%</button>
<button type="button" class="hidden md:block ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }})">50%</button>
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }})">100%</button>
<script>
function setAmount(percent, balance, cid) {
var amountInput = document.getElementById('amount');
@ -676,7 +854,6 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
</script>
{% endif %}
</div>
</div>
@ -748,7 +925,10 @@ document.addEventListener('DOMContentLoaded', function() {
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Fee Estimate:</td>
<td class="py-3 px-6"> {{ w.est_fee }} </td>
<td class="py-3 px-6">
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.est_fee }}</span>
(<span class="usd-value fee-estimate-usd" data-decimals="8"></span>)
</td>
</tr>
{% endif %}
</table>
@ -761,6 +941,7 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
</div>
</section>
<section>
<div class="px-6 py-0 h-full overflow-hidden">
<div class="pb-6 ">
@ -788,6 +969,7 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
</div>
</section>
{% if w.show_utxo_groups %}
<section class="p-6">
<div class="lg:container mx-auto">
@ -796,6 +978,7 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
</div>
</section>
<section>
<div class="px-6 py-0 h-full overflow-hidden">
<div class="border-coolGray-100">
@ -834,6 +1017,7 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
</div>
</section>
<section>
<div class="px-6 py-0 h-full overflow-hidden ">
<div class="pb-6 ">
@ -851,166 +1035,155 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
</div>
</section>
{% else %}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
<input type="hidden" name="formid" value="{{ form_id }}">
</form>
<div id="confirmModal" class="fixed inset-0 z-50 hidden overflow-y-auto">
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity duration-300 ease-out"></div>
<div class="relative z-50 min-h-screen px-4 flex items-center justify-center">
<div class="bg-white dark:bg-gray-500 rounded-lg max-w-md w-full p-6 shadow-lg transition-opacity duration-300 ease-out">
<div class="text-center">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4" id="confirmTitle">Confirm Action</h2>
<p class="text-gray-600 dark:text-gray-200 mb-6 whitespace-pre-line" id="confirmMessage">Are you sure?</p>
<div class="flex justify-center gap-4">
<button type="button" id="confirmYes"
class="px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
Confirm
</button>
<button type="button" id="confirmNo"
class="px-4 py-2.5 font-medium text-sm text-white hover:text-red border border-red-500 hover:border-red-500 hover:bg-red-600 bg-red-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
<script>
const coinNameToSymbol = {
'Bitcoin': 'BTC',
'Particl': 'PART',
'Particl Blind': 'PART',
'Particl Anon': 'PART',
'Monero': 'XMR',
'Wownero': 'WOW',
'Litecoin': 'LTC',
'Dogecoin': 'DOGE',
'Firo': 'FIRO',
'Dash': 'DASH',
'PIVX': 'PIVX',
'Decred': 'DCR',
'Zano': 'ZANO',
'Bitcoin Cash': 'BCH',
};
let confirmCallback = null;
let triggerElement = null;
let currentCoinId = '';
const getUsdValue = (cryptoValue, coinSymbol) => {
let source = "cryptocompare.com";
let coin_id = coinSymbol;
if (coinSymbol === 'WOW') {
source = "coingecko.com"
coin_id = "wownero"
function showConfirmDialog(title, message, callback) {
confirmCallback = callback;
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
}
return false;
}
return fetch("/json/coinprices", {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
coins: coin_id,
source: source
})
})
.then(response => response.json())
.then(data => {
const exchangeRate = data.rates[coin_id];
if (!isNaN(exchangeRate)) {
return cryptoValue * exchangeRate;
} else {
throw new Error(`Invalid exchange rate for ${coinSymbol}`);
function hideConfirmDialog() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
}
});
};
const updateUsdValue = async (cryptoCell, coinFullName, usdValueSpan) => {
const coinSymbol = coinNameToSymbol[coinFullName] || '';
if (!coinSymbol) {
console.error(`Coin symbol not found for full name: ${coinFullName}`);
return;
confirmCallback = null;
return false;
}
const cryptoValue = parseFloat(cryptoCell.textContent);
if (!isNaN(cryptoValue) && cryptoValue !== 0) {
try {
const usdValue = await getUsdValue(cryptoValue, coinSymbol);
if (usdValueSpan) {
usdValueSpan.textContent = `$${usdValue.toFixed(2)}`;
}
} catch (error) {
console.error('Error in updateUsdValue:', error);
if (usdValueSpan) {
usdValueSpan.textContent = 'Error retrieving exchange rate';
}
}
} else {
if (usdValueSpan) {
usdValueSpan.textContent = `$0.00`;
}
}
};
const calculateTotalUsdValue = async () => {
const coinNameValues = document.querySelectorAll('.coinname-value');
let totalUsdValue = 0;
for (const coinNameValue of coinNameValues) {
const coinFullName = coinNameValue.getAttribute('data-coinname');
const cryptoValue = parseFloat(coinNameValue.textContent);
const coinSymbol = coinNameToSymbol[coinFullName];
if (coinSymbol) {
const usdValueSpan = coinNameValue.querySelector('.usd-value');
if (!isNaN(cryptoValue) && cryptoValue !== 0) {
try {
const usdValue = await getUsdValue(cryptoValue, coinSymbol);
totalUsdValue += usdValue;
if (usdValueSpan) {
usdValueSpan.textContent = `$${usdValue.toFixed(2)}`;
}
} catch (error) {
console.error(`Error retrieving exchange rate for ${coinFullName}`);
}
} else {
if (usdValueSpan) {
usdValueSpan.textContent = `$0.00`;
}
}
} else {
console.error(`Coin symbol not found for full name: ${coinFullName}`);
}
}
const totalUsdValueElement = document.getElementById('total-usd-value');
if (totalUsdValueElement) {
totalUsdValueElement.textContent = `$${totalUsdValue.toFixed(2)}`;
}
};
document.addEventListener('DOMContentLoaded', () => {
const coinNameValues = document.querySelectorAll('.coinname-value');
for (const coinNameValue of coinNameValues) {
const coinFullName = coinNameValue.getAttribute('data-coinname');
const usdValueSpan = coinNameValue.querySelector('.usd-value');
updateUsdValue(coinNameValue, coinFullName, usdValueSpan);
}
calculateTotalUsdValue();
function set_sweep_all(element) {
let input = document.getElementById('amount');
if (element.checked) {
input.disabled = true;
} else {
input.disabled = false;
}
}
let cb_sweepall = document.getElementById('sweepall');
if (cb_sweepall) {
set_sweep_all(cb_sweepall);
cb_sweepall.addEventListener('change', (event) => {
set_sweep_all(event.currentTarget);
});
}
});
function confirmReseed() {
return confirm("Are you sure?\nBackup your wallet before and after.\nWon't detect used keys.\nShould only be used for new wallets.");
triggerElement = document.activeElement;
return showConfirmDialog(
"Confirm Reseed Wallet",
"Are you sure?\nBackup your wallet before and after.\nWon't detect used keys.\nShould only be used for new wallets.",
function() {
if (triggerElement) {
const form = triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = triggerElement.name;
hiddenInput.value = triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}
);
}
function confirmWithdrawal() {
return confirm("Are you sure?");
triggerElement = document.activeElement;
return showConfirmDialog(
"Confirm Withdrawal",
"Are you sure you want to proceed with this withdrawal?",
function() {
if (triggerElement) {
const form = triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = triggerElement.name;
hiddenInput.value = triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}
);
}
function confirmUTXOResize() {
return confirm("Are you sure?");
triggerElement = document.activeElement;
return showConfirmDialog(
"Confirm UTXO Resize",
"Are you sure you want to resize UTXOs?",
function() {
if (triggerElement) {
const form = triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = triggerElement.name;
hiddenInput.value = triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}
);
}
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('confirmYes').addEventListener('click', function() {
if (typeof confirmCallback === 'function') {
confirmCallback();
}
hideConfirmDialog();
});
document.getElementById('confirmNo').addEventListener('click', hideConfirmDialog);
document.querySelectorAll('input[type="submit"][name^="reseed_"]').forEach(function(button) {
button.addEventListener('click', function(e) {
e.preventDefault();
currentCoinId = button.name.split('_')[1];
return confirmReseed();
});
});
document.querySelectorAll('button[name^="withdraw_"]').forEach(function(button) {
button.addEventListener('click', function(e) {
e.preventDefault();
currentCoinId = button.name.split('_')[1];
return confirmWithdrawal();
});
});
const utxoButton = document.getElementById('create_utxo');
if (utxoButton) {
utxoButton.addEventListener('click', function(e) {
e.preventDefault();
return confirmUTXOResize();
});
}
});
</script>
<script src="/static/js/wallets.js"></script>
{% include 'footer.html' %}
</body>
</html>

View file

@ -1,29 +1,15 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg, withdraw_svg, utxo_groups_svg, create_utxo_svg, lock_svg, eye_show_svg %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/"><p>Home</p></a></li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/wallets">Wallets</a></li>
<li>{{ breadcrumb_line_svg | safe }}</li>
</ul>
</div>
</div>
</section>
<section class="py-3">
<div class="container px-4 mx-auto">
<div class="relative py-11 px-16 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<section class="py-3 px-4">
<div class="lg:container mx-auto">>
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="dots-red">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="dots-red">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="wave">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3 h-48">
<h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">Wallets</h2>
<h2 class="text-4xl font-bold text-white tracking-tighter">Wallets</h2>
<div class="flex items-center">
<h2 class="text-lg font-bold text-white tracking-tighter mr-2">Total Assets:</h2>
<button id="hide-usd-amount-toggle" class="flex items-center justify-center p-1 focus:ring-0 focus:outline-none">{{ eye_show_svg | safe }}</button>
@ -46,7 +32,7 @@
{% include 'inc_messages.html' %}
<section class="py-4">
<div class="container px-4 mx-auto">
<div class="container mx-auto">
<div class="flex flex-wrap -m-4">
{% for w in wallets %}
{% if w.havedata %}
@ -73,7 +59,7 @@
<div class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.balance }} {{ w.ticker }}</div>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white usd-text">{{ w.ticker }} USD value:</h4>
<h4 class="text-xs font-medium dark:text-white ">{{ w.ticker }} USD value:</h4>
<div class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 usd-value" data-coinname="{{ w.name }}"></div>
</div>
{% if w.pending %}
@ -202,448 +188,10 @@
{% endfor %}
</div>
</section>
</div>
<script src="/static/js/wallets.js"></script>
{% include 'footer.html' %}
<script>
const CONFIG = {
MAX_RETRIES: 3,
BASE_DELAY: 1000,
CACHE_EXPIRATION: 5 * 60 * 1000,
PRICE_UPDATE_INTERVAL: 5 * 60 * 1000,
API_TIMEOUT: 30000,
DEBOUNCE_DELAY: 500,
CACHE_MIN_INTERVAL: 60 * 1000
};
const STATE_KEYS = {
LAST_UPDATE: 'last-update-time',
PREVIOUS_TOTAL: 'previous-total-usd',
CURRENT_TOTAL: 'current-total-usd',
BALANCES_VISIBLE: 'balancesVisible'
};
const COIN_SYMBOLS = {
'Bitcoin': 'bitcoin',
'Particl': 'particl',
'Monero': 'monero',
'Wownero': 'wownero',
'Litecoin': 'litecoin',
'Dogecoin': 'dogecoin',
'Firo': 'zcoin',
'Dash': 'dash',
'PIVX': 'pivx',
'Decred': 'decred',
'Zano': 'zano',
'Bitcoin Cash': 'bitcoin-cash'
};
const SHORT_NAMES = {
'Bitcoin': 'BTC',
'Particl': 'PART',
'Monero': 'XMR',
'Wownero': 'WOW',
'Litecoin': 'LTC',
'Litecoin MWEB': 'LTC MWEB',
'Firo': 'FIRO',
'Dash': 'DASH',
'PIVX': 'PIVX',
'Decred': 'DCR',
'Zano': 'ZANO',
'Bitcoin Cash': 'BCH'
};
class Cache {
constructor(expirationTime) {
this.data = null;
this.timestamp = null;
this.expirationTime = expirationTime;
}
isValid() {
return Boolean(
this.data &&
this.timestamp &&
(Date.now() - this.timestamp < this.expirationTime)
);
}
set(data) {
this.data = data;
this.timestamp = Date.now();
}
get() {
if (this.isValid()) {
return this.data;
}
return null;
}
clear() {
this.data = null;
this.timestamp = null;
}
}
class ApiClient {
constructor() {
this.cache = new Cache(CONFIG.CACHE_EXPIRATION);
this.lastFetchTime = 0;
}
makeRequest(url, headers = {}) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/json/readurl');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.timeout = CONFIG.API_TIMEOUT;
xhr.ontimeout = () => {
reject(new Error('Request timed out'));
};
xhr.onload = () => {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.Error) {
reject(new Error(response.Error));
} else {
resolve(response);
}
} catch (error) {
reject(new Error(`Invalid JSON response: ${error.message}`));
}
} else {
reject(new Error(`HTTP Error: ${xhr.status} ${xhr.statusText}`));
}
};
xhr.onerror = () => {
reject(new Error('Network error occurred'));
};
xhr.send(JSON.stringify({ url, headers }));
});
}
async fetchPrices(forceUpdate = false) {
const now = Date.now();
const timeSinceLastFetch = now - this.lastFetchTime;
if (!forceUpdate && timeSinceLastFetch < CONFIG.CACHE_MIN_INTERVAL) {
const cachedData = this.cache.get();
if (cachedData) {
return cachedData;
}
}
let lastError = null;
for (let attempt = 0; attempt < CONFIG.MAX_RETRIES; attempt++) {
try {
const prices = await this.makeRequest(
'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC'
);
this.cache.set(prices);
this.lastFetchTime = now;
return prices;
} catch (error) {
lastError = error;
if (attempt < CONFIG.MAX_RETRIES - 1) {
const delay = Math.min(CONFIG.BASE_DELAY * Math.pow(2, attempt), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
const cachedData = this.cache.get();
if (cachedData) {
return cachedData;
}
throw lastError || new Error('Failed to fetch prices');
}
}
class UiManager {
constructor() {
this.api = new ApiClient();
this.toggleInProgress = false;
this.toggleDebounceTimer = null;
this.priceUpdateInterval = null;
this.lastUpdateTime = parseInt(localStorage.getItem(STATE_KEYS.LAST_UPDATE) || '0');
}
getShortName(fullName) {
return SHORT_NAMES[fullName] || fullName;
}
storeOriginalValues() {
document.querySelectorAll('.coinname-value').forEach(el => {
const coinName = el.getAttribute('data-coinname');
const value = el.textContent?.trim() || '';
if (coinName) {
const amount = value ? parseFloat(value.replace(/[^0-9.-]+/g, '')) : 0;
const coinId = COIN_SYMBOLS[coinName];
const shortName = this.getShortName(coinName);
if (coinId) {
if (coinId === 'particl') {
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Blind');
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Anon');
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
} else if (coinId === 'litecoin') {
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent.includes('MWEB');
const balanceType = isMWEB ? 'mweb' : 'public';
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
} else {
localStorage.setItem(`${coinId}-amount`, amount.toString());
}
el.setAttribute('data-original-value', `${amount} ${shortName}`);
}
}
});
}
async updatePrices(forceUpdate = false) {
try {
const prices = await this.api.fetchPrices(forceUpdate);
let newTotal = 0;
const currentTime = Date.now();
localStorage.setItem(STATE_KEYS.LAST_UPDATE, currentTime.toString());
this.lastUpdateTime = currentTime;
if (prices) {
Object.entries(COIN_SYMBOLS).forEach(([coinName, coinId]) => {
if (prices[coinId]?.usd) {
localStorage.setItem(`${coinId}-price`, prices[coinId].usd.toString());
}
});
}
document.querySelectorAll('.coinname-value').forEach(el => {
const coinName = el.getAttribute('data-coinname');
const amountStr = el.getAttribute('data-original-value') || el.textContent?.trim() || '';
if (!coinName) return;
const amount = amountStr ? parseFloat(amountStr.replace(/[^0-9.-]+/g, '')) : 0;
const coinId = COIN_SYMBOLS[coinName];
if (!coinId) return;
const price = prices?.[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
const usdValue = (amount * price).toFixed(2);
if (coinId === 'particl') {
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Blind');
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent.includes('Anon');
const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
localStorage.setItem(`particl-${balanceType}-last-value`, usdValue);
localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
} else if (coinId === 'litecoin') {
const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent.includes('MWEB');
const balanceType = isMWEB ? 'mweb' : 'public';
localStorage.setItem(`litecoin-${balanceType}-last-value`, usdValue);
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
} else {
localStorage.setItem(`${coinId}-last-value`, usdValue);
localStorage.setItem(`${coinId}-amount`, amount.toString());
}
newTotal += parseFloat(usdValue);
const usdEl = el.closest('.flex')?.nextElementSibling?.querySelector('.usd-value');
if (usdEl) {
usdEl.textContent = `$${usdValue} USD`;
}
});
this.updateTotalValues(newTotal, prices?.bitcoin?.usd);
localStorage.setItem(STATE_KEYS.PREVIOUS_TOTAL, localStorage.getItem(STATE_KEYS.CURRENT_TOTAL) || '0');
localStorage.setItem(STATE_KEYS.CURRENT_TOTAL, newTotal.toString());
return true;
} catch (error) {
console.error('Price update failed:', error);
return false;
}
}
updateTotalValues(totalUsd, btcPrice) {
const totalUsdEl = document.getElementById('total-usd-value');
if (totalUsdEl) {
totalUsdEl.textContent = `$${totalUsd.toFixed(2)}`;
totalUsdEl.setAttribute('data-original-value', totalUsd.toString());
localStorage.setItem('total-usd', totalUsd.toString());
}
if (btcPrice) {
const btcTotal = btcPrice ? totalUsd / btcPrice : 0;
const totalBtcEl = document.getElementById('total-btc-value');
if (totalBtcEl) {
totalBtcEl.textContent = `~ ${btcTotal.toFixed(8)} BTC`;
totalBtcEl.setAttribute('data-original-value', btcTotal.toString());
}
}
}
async toggleBalances() {
if (this.toggleInProgress) return;
try {
this.toggleInProgress = true;
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
const newVisibility = !balancesVisible;
localStorage.setItem('balancesVisible', newVisibility.toString());
this.updateVisibility(newVisibility);
if (this.toggleDebounceTimer) {
clearTimeout(this.toggleDebounceTimer);
}
this.toggleDebounceTimer = window.setTimeout(async () => {
this.toggleInProgress = false;
if (newVisibility) {
await this.updatePrices(true);
}
}, CONFIG.DEBOUNCE_DELAY);
} catch (error) {
console.error('Failed to toggle balances:', error);
this.toggleInProgress = false;
}
}
updateVisibility(isVisible) {
if (isVisible) {
this.showBalances();
} else {
this.hideBalances();
}
const eyeIcon = document.querySelector("#hide-usd-amount-toggle svg");
if (eyeIcon) {
eyeIcon.innerHTML = isVisible ?
'<path d="M23.444,10.239C21.905,8.062,17.708,3,12,3S2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0C2.1,15.938,6.292,21,12,21s9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239ZM12,17a5,5,0,1,1,5-5A5,5,0,0,1,12,17Z"></path>' :
'<path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z"></path><path d="M12,3C6.292,3,2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z"></path><path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path>';
}
}
showBalances() {
const usdText = document.getElementById('usd-text');
if (usdText) {
usdText.style.display = 'inline';
}
document.querySelectorAll('.coinname-value').forEach(el => {
const originalValue = el.getAttribute('data-original-value');
if (originalValue) {
el.textContent = originalValue;
}
});
document.querySelectorAll('.usd-value').forEach(el => {
const storedValue = el.getAttribute('data-original-value');
if (storedValue) {
el.textContent = `$${parseFloat(storedValue).toFixed(2)} USD`;
el.style.color = 'white';
}
});
['total-usd-value', 'total-btc-value'].forEach(id => {
const el = document.getElementById(id);
const originalValue = el?.getAttribute('data-original-value');
if (el && originalValue) {
if (id === 'total-usd-value') {
el.textContent = `$${parseFloat(originalValue).toFixed(2)}`;
el.classList.add('font-extrabold');
} else {
el.textContent = `~ ${parseFloat(originalValue).toFixed(8)} BTC`;
}
}
});
}
hideBalances() {
const usdText = document.getElementById('usd-text');
if (usdText) {
usdText.style.display = 'none';
}
document.querySelectorAll('.coinname-value, .usd-value').forEach(el => {
el.textContent = '****';
});
['total-usd-value', 'total-btc-value'].forEach(id => {
const el = document.getElementById(id);
if (el) {
el.textContent = '****';
}
});
const totalUsdEl = document.getElementById('total-usd-value');
if (totalUsdEl) {
totalUsdEl.classList.remove('font-extrabold');
}
}
async initialize() {
this.storeOriginalValues();
if (localStorage.getItem('balancesVisible') === null) {
localStorage.setItem('balancesVisible', 'true');
}
const hideBalancesToggle = document.getElementById('hide-usd-amount-toggle');
if (hideBalancesToggle) {
hideBalancesToggle.addEventListener('click', () => this.toggleBalances());
}
await this.loadBalanceVisibility();
if (this.priceUpdateInterval) {
clearInterval(this.priceUpdateInterval);
}
this.priceUpdateInterval = setInterval(() => {
if (localStorage.getItem('balancesVisible') === 'true' && !this.toggleInProgress) {
this.updatePrices(false);
}
}, CONFIG.PRICE_UPDATE_INTERVAL);
}
async loadBalanceVisibility() {
const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
this.updateVisibility(balancesVisible);
if (balancesVisible) {
await this.updatePrices(true);
}
}
}
window.addEventListener('beforeunload', () => {
const uiManager = window.uiManager;
if (uiManager?.priceUpdateInterval) {
clearInterval(uiManager.priceUpdateInterval);
}
});
window.addEventListener('load', () => {
const uiManager = new UiManager();
window.uiManager = uiManager;
uiManager.initialize().catch(error => {
console.error('Failed to initialize application:', error);
});
});
</script>
</body>
</html>