mirror of
https://github.com/basicswap/basicswap.git
synced 2025-05-05 20:32:15 +00:00
Updated wallets/wallet with backend coin prices/cache + various fixes.
This commit is contained in:
parent
0cf77a4854
commit
19135a3590
6 changed files with 1346 additions and 960 deletions
basicswap
649
basicswap/static/js/wallets.js
Normal file
649
basicswap/static/js/wallets.js
Normal 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();
|
||||||
|
});
|
|
@ -120,8 +120,25 @@
|
||||||
updateShutdownButtons();
|
updateShutdownButtons();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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() {
|
function getAPIKeys() {
|
||||||
return {
|
return {
|
||||||
cryptoCompare: "{{ chart_api_key|safe }}",
|
cryptoCompare: "{{ chart_api_key|safe }}",
|
||||||
|
@ -140,7 +157,6 @@ function getWebSocketConfig() {
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="dark:bg-gray-700">
|
<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 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="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">
|
<div class="flex items-center justify-center min-h-screen p-4 relative z-10">
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% from 'style.html' import circular_info_messages_svg, green_cross_close_svg, red_cross_close_svg, circular_error_messages_svg %}
|
{% from 'style.html' import circular_info_messages_svg, green_cross_close_svg, red_cross_close_svg, circular_error_messages_svg %}
|
||||||
|
|
||||||
{% for m in messages %}
|
{% for m in messages %}
|
||||||
<section class="py-4" id="messages_{{ m[0] }}" role="alert">
|
<section class="py-4 px-6" id="messages_{{ m[0] }}" role="alert">
|
||||||
<div class="container px-4 mx-auto">
|
<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="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 flex-wrap justify-between items-center -m-2">
|
||||||
<div class="flex-1 p-2">
|
<div class="flex-1 p-2">
|
||||||
|
@ -27,8 +27,8 @@
|
||||||
</section>
|
</section>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if err_messages %}
|
{% if err_messages %}
|
||||||
<section class="py-4" id="err_messages_{{ err_messages[0][0] }}" role="alert">
|
<section class="py-4 px-6" id="err_messages_{{ err_messages[0][0] }}" role="alert">
|
||||||
<div class="container px-4 mx-auto">
|
<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="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 flex-wrap justify-between items-center -m-2">
|
||||||
<div class="flex-1 p-2">
|
<div class="flex-1 p-2">
|
||||||
|
|
|
@ -422,7 +422,7 @@
|
||||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
<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 bold">Enabled Coins</td>
|
||||||
<td class="py-3 px-6">
|
<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 />
|
<br />
|
||||||
</label>
|
</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}}">
|
<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}}">
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,29 +1,15 @@
|
||||||
{% include 'header.html' %}
|
{% 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 %}
|
{% 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="py-3 px-4">
|
||||||
<section class="p-5 mt-5">
|
<div class="lg:container mx-auto">>
|
||||||
<div class="flex flex-wrap items-center -m-2">
|
<div class="relative py-8 px-8 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
|
||||||
<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">
|
|
||||||
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="dots-red">
|
<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 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">
|
<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="relative z-20 flex flex-wrap items-center -m-3">
|
||||||
<div class="w-full md:w-1/2 p-3 h-48">
|
<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">
|
<div class="flex items-center">
|
||||||
<h2 class="text-lg font-bold text-white tracking-tighter mr-2">Total Assets:</h2>
|
<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>
|
<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>
|
||||||
|
@ -42,11 +28,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% include 'inc_messages.html' %}
|
{% include 'inc_messages.html' %}
|
||||||
|
|
||||||
<section class="py-4">
|
<section class="py-4">
|
||||||
<div class="container px-4 mx-auto">
|
<div class="container mx-auto">
|
||||||
<div class="flex flex-wrap -m-4">
|
<div class="flex flex-wrap -m-4">
|
||||||
{% for w in wallets %}
|
{% for w in wallets %}
|
||||||
{% if w.havedata %}
|
{% 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 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>
|
||||||
<div class="flex mb-2 justify-between items-center">
|
<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 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>
|
</div>
|
||||||
{% if w.pending %}
|
{% if w.pending %}
|
||||||
|
@ -202,448 +188,10 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<script src="/static/js/wallets.js"></script>
|
||||||
|
|
||||||
{% include 'footer.html' %}
|
{% 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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in a new issue