Add total portfolio value, cached prices and balances
All checks were successful
check / check (push) Successful in 16s
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:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user