142 lines
4.8 KiB
JavaScript
142 lines
4.8 KiB
JavaScript
// 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 (e) {
|
|
// 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,
|
|
};
|