Add total portfolio value, cached prices and balances
All checks were successful
check / check (push) Successful in 16s

Total USD value displayed in 2x type above wallet list on Home.
Value aggregation: getAddressValueUsd (ETH + all tokens) →
getWalletValueUsd → getTotalValueUsd. Price API cached for 5
minutes, balance fetches cached for 60 seconds. Both caches
are app-wide — repeated calls to refreshPrices/refreshBalances
are no-ops within the TTL.
This commit is contained in:
2026-02-25 18:44:29 +07:00
parent 64bd541013
commit 2a8c051377
2 changed files with 62 additions and 13 deletions

View File

@@ -173,6 +173,9 @@
<!-- ============ MAIN VIEW: ALL WALLETS & ADDRESSES ============ --> <!-- ============ MAIN VIEW: ALL WALLETS & ADDRESSES ============ -->
<div id="view-main" class="view hidden"> <div id="view-main" class="view hidden">
<!-- total portfolio value -->
<div id="total-value" class="text-2xl font-bold mb-2"></div>
<!-- wallet list --> <!-- wallet list -->
<div id="wallet-list"></div> <div id="wallet-list"></div>

View File

@@ -129,16 +129,27 @@ function addressFromPrivateKey(key) {
return w.address; return w.address;
} }
// -- price fetching -- // -- caching layer --
const PRICE_CACHE_TTL = 300000; // 5 minutes
const BALANCE_CACHE_TTL = 60000; // 60 seconds
const cache = {
prices: { data: {}, fetchedAt: 0 },
balances: { fetchedAt: 0 },
};
// { "ETH": 1234.56, "LINK": 8.60, ... } // { "ETH": 1234.56, "LINK": 8.60, ... }
const prices = {}; const prices = {};
async function refreshPrices() { async function refreshPrices() {
const now = Date.now();
if (now - cache.prices.fetchedAt < PRICE_CACHE_TTL) return;
try { try {
const fetched = await getTopTokenPrices(25); const fetched = await getTopTokenPrices(25);
Object.assign(prices, fetched); Object.assign(prices, fetched);
cache.prices.fetchedAt = now;
} catch (e) { } catch (e) {
// prices stay empty on error // prices stay stale on error
} }
} }
@@ -187,11 +198,12 @@ function formatBalance(wei) {
} }
async function refreshBalances() { async function refreshBalances() {
const now = Date.now();
if (now - cache.balances.fetchedAt < BALANCE_CACHE_TTL) return;
const provider = getProvider(); const provider = getProvider();
const updates = []; const updates = [];
for (const wallet of state.wallets) { for (const wallet of state.wallets) {
for (const addr of wallet.addresses) { for (const addr of wallet.addresses) {
// Fetch ETH balance
updates.push( updates.push(
provider provider
.getBalance(addr.address) .getBalance(addr.address)
@@ -200,7 +212,6 @@ async function refreshBalances() {
}) })
.catch(() => {}), .catch(() => {}),
); );
// Reverse ENS lookup
updates.push( updates.push(
provider provider
.lookupAddress(addr.address) .lookupAddress(addr.address)
@@ -214,15 +225,54 @@ async function refreshBalances() {
} }
} }
await Promise.all(updates); await Promise.all(updates);
cache.balances.fetchedAt = now;
await saveState(); await saveState();
} }
function getAddressValueUsd(addr) {
let total = 0;
const ethBal = parseFloat(addr.balance || "0");
if (prices.ETH) {
total += ethBal * prices.ETH;
}
for (const token of addr.tokens || []) {
const tokenBal = parseFloat(token.balance || "0");
if (tokenBal > 0 && prices[token.symbol]) {
total += tokenBal * prices[token.symbol];
}
}
return total;
}
function getWalletValueUsd(wallet) {
let total = 0;
for (const addr of wallet.addresses) {
total += getAddressValueUsd(addr);
}
return total;
}
function getTotalValueUsd() {
let total = 0;
for (const wallet of state.wallets) {
total += getWalletValueUsd(wallet);
}
return total;
}
function renderTotalValue() {
const el = $("total-value");
if (!el) return;
el.textContent = formatUsd(getTotalValueUsd());
}
// -- render wallet list on main view -- // -- render wallet list on main view --
function renderWalletList() { function renderWalletList() {
const container = $("wallet-list"); const container = $("wallet-list");
if (state.wallets.length === 0) { if (state.wallets.length === 0) {
container.innerHTML = container.innerHTML =
'<p class="text-muted py-2">No wallets yet. Add one to get started.</p>'; '<p class="text-muted py-2">No wallets yet. Add one to get started.</p>';
renderTotalValue();
return; return;
} }
@@ -244,12 +294,8 @@ function renderWalletList() {
html += `<div class="text-xs break-all">${addr.address}</div>`; html += `<div class="text-xs break-all">${addr.address}</div>`;
html += `<div class="flex justify-between items-center">`; html += `<div class="flex justify-between items-center">`;
html += `<span class="text-xs">${addr.balance} ETH</span>`; html += `<span class="text-xs">${addr.balance} ETH</span>`;
const ethUsd = prices.ETH const addrUsd = getAddressValueUsd(addr);
? parseFloat(addr.balance) * prices.ETH html += `<span class="text-xs text-muted">${formatUsd(addrUsd)}</span>`;
: null;
if (ethUsd !== null) {
html += `<span class="text-xs text-muted">${formatUsd(ethUsd)}</span>`;
}
html += `</div>`; html += `</div>`;
html += `</div>`; html += `</div>`;
}); });
@@ -285,6 +331,8 @@ function renderWalletList() {
renderWalletList(); renderWalletList();
}); });
}); });
renderTotalValue();
} }
function showAddressDetail() { function showAddressDetail() {
@@ -294,9 +342,7 @@ function showAddressDetail() {
$("address-full").textContent = addr.address; $("address-full").textContent = addr.address;
$("address-copied-msg").textContent = ""; $("address-copied-msg").textContent = "";
$("address-eth-balance").textContent = addr.balance; $("address-eth-balance").textContent = addr.balance;
const ethUsd = prices.ETH ? parseFloat(addr.balance) * prices.ETH : null; $("address-usd-value").textContent = formatUsd(getAddressValueUsd(addr));
$("address-usd-value").textContent =
ethUsd !== null ? formatUsd(ethUsd) : "";
const ensEl = $("address-ens"); const ensEl = $("address-ens");
if (addr.ensName) { if (addr.ensName) {
ensEl.textContent = addr.ensName; ensEl.textContent = addr.ensName;