// Price fetching with 5-minute cache, USD formatting, value aggregation. const { getTopTokenPrices } = require("./tokenList"); const PRICE_CACHE_TTL = 300000; // 5 minutes const prices = {}; let lastFetchedAt = 0; async function refreshPrices() { // Testnet tokens have no real market value — skip price fetching // and clear any stale mainnet prices so the UI shows no USD values. const { currentNetwork } = require("./state"); if (currentNetwork().isTestnet) { clearPrices(); return; } const now = Date.now(); if (now - lastFetchedAt < PRICE_CACHE_TTL) return; try { const fetched = await getTopTokenPrices(25); Object.assign(prices, fetched); lastFetchedAt = now; } catch { // prices stay stale on error } } // Clear all cached prices and reset the fetch timestamp so the // next refreshPrices() call will fetch fresh data. function clearPrices() { for (const key of Object.keys(prices)) { delete prices[key]; } lastFetchedAt = 0; } // Return the USD price for a symbol, or null on testnet / unknown. function getPrice(symbol) { const { currentNetwork } = require("./state"); if (currentNetwork().isTestnet) return null; return prices[symbol] || null; } function formatUsd(amount) { if (amount === null || amount === undefined || isNaN(amount)) return ""; if (amount === 0) return "$0.00"; if (amount < 0.01) return "< $0.01"; return ( "$" + amount.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2, }) ); } // What an address is worth, as { usd, partial }. // // Prices are fetched for the top 25 tokens only, so an address can hold real // assets this code has no price for. Adding up the priced ones and calling the // result the total states a number the holdings do not support: an address // holding nothing but unpriced tokens comes out at $0.00, which tells the user // their address is worth nothing when it may hold a great deal. Worth zero and // worth an unknown amount are separate facts and get separate fields, the same // way an absent holders_count is not a count of zero. // // usd: the value of the holdings a price is known for, or null when // nothing is knowable at all — testnet, or before the first fetch. // partial: the address also holds a token with no price, so usd is a floor // and not the total. // // Render it through formatAddressTotal() rather than reading usd alone. function getAddressValue(addr) { const { currentNetwork } = require("./state"); if (currentNetwork().isTestnet) return { usd: null, partial: false }; if (!prices.ETH) return { usd: null, partial: false }; let usd = parseFloat(addr.balance || "0") * prices.ETH; let partial = false; for (const token of addr.tokenBalances || []) { const tokenBal = parseFloat(token.balance || "0"); // A balance of zero is not a holding: it can neither add to the total // nor make it incomplete. if (!(tokenBal > 0)) continue; if (prices[token.symbol]) { usd += tokenBal * prices[token.symbol]; } else { partial = true; } } return { usd, partial }; } // The same pair for a whole wallet, and for every wallet at once. One // unpriced holding anywhere makes the sum a floor, so partial carries up. function getWalletValue(wallet) { return sumValues(wallet.addresses.map(getAddressValue)); } function getTotalValue(wallets) { return sumValues(wallets.map(getWalletValue)); } function sumValues(values) { let usd = null; let partial = false; for (const value of values) { if (value.usd === null) continue; usd = (usd === null ? 0 : usd) + value.usd; partial = partial || value.partial; } return { usd, partial }; } // The one rendering of an address total, so no screen says it differently. // // A partial total is shown and named as partial: the figure is the ETH and // priced tokens the user does hold, which is worth having, and suppressing it // would throw away a number that is correct as far as it goes. What is never // shown is a figure covering no holdings at all — the $0.00 sum of an empty // set beside a list of tokens is the bug this replaces. function formatAddressTotal(value) { if (!value || value.usd === null) return ""; if (!value.partial) return "Total: " + formatUsd(value.usd); if (value.usd > 0) { return "Total: " + formatUsd(value.usd) + " plus unpriced tokens"; } return "Total: unpriced tokens only"; } module.exports = { prices, refreshPrices, clearPrices, getPrice, formatUsd, formatAddressTotal, getAddressValue, getWalletValue, getTotalValue, };