Refactor popup into shared modules, wire up real ERC-20 tokens
All checks were successful
check / check (push) Successful in 13s

Split popup/index.js (784 lines) into focused modules:
- shared/state.js: state management, storage persistence
- shared/wallet.js: mnemonic gen, HD derivation, signing
- shared/prices.js: price cache (5min TTL), USD formatting,
  value aggregation (address → wallet → total)
- shared/balances.js: ETH + ERC-20 balance cache (60s TTL),
  ENS lookup, token contract metadata lookup
- shared/vault.js: unchanged (libsodium encryption)
- shared/tokens.js: unchanged (token list + CoinDesk client)
- popup/index.js: view switching and event wiring only

Token tracking is now app-wide: trackedTokens stored in state,
balances fetched for all tracked tokens across all addresses.
Add Token now calls the real contract to read name/symbol/decimals.
Total portfolio value shown in 2x type on Home screen.
This commit is contained in:
2026-02-25 18:48:44 +07:00
parent 2a8c051377
commit f50a2a0389
5 changed files with 494 additions and 333 deletions

130
src/shared/balances.js Normal file
View File

@@ -0,0 +1,130 @@
// Balance fetching: ETH balances, ERC-20 token balances, ENS reverse lookup.
// Cached for 60 seconds.
const {
JsonRpcProvider,
Contract,
formatEther,
formatUnits,
} = require("ethers");
const { ERC20_ABI } = require("./constants");
const BALANCE_CACHE_TTL = 60000; // 60 seconds
let lastFetchedAt = 0;
function getProvider(rpcUrl) {
return new JsonRpcProvider(rpcUrl);
}
function formatBalance(wei) {
const eth = formatEther(wei);
const parts = eth.split(".");
if (parts.length === 1) return eth + ".0";
const dec = parts[1].slice(0, 6).replace(/0+$/, "") || "0";
return parts[0] + "." + dec;
}
function formatTokenBalance(raw, decimals) {
const val = formatUnits(raw, decimals);
const parts = val.split(".");
if (parts.length === 1) return val + ".0";
const dec = parts[1].slice(0, 6).replace(/0+$/, "") || "0";
return parts[0] + "." + dec;
}
// Fetch ETH balances, ENS names, and ERC-20 token balances for all addresses.
// trackedTokens: [{ address, symbol, decimals }]
async function refreshBalances(wallets, trackedTokens, rpcUrl) {
const now = Date.now();
if (now - lastFetchedAt < BALANCE_CACHE_TTL) return;
const provider = getProvider(rpcUrl);
const updates = [];
for (const wallet of wallets) {
for (const addr of wallet.addresses) {
// ETH balance
updates.push(
provider
.getBalance(addr.address)
.then((bal) => {
addr.balance = formatBalance(bal);
})
.catch(() => {}),
);
// ENS reverse lookup
updates.push(
provider
.lookupAddress(addr.address)
.then((name) => {
addr.ensName = name || null;
})
.catch(() => {
addr.ensName = null;
}),
);
// ERC-20 token balances
if (!addr.tokenBalances) addr.tokenBalances = [];
for (const token of trackedTokens) {
updates.push(
(async () => {
try {
const contract = new Contract(
token.address,
ERC20_ABI,
provider,
);
const raw = await contract.balanceOf(addr.address);
const existing = addr.tokenBalances.find(
(t) =>
t.address.toLowerCase() ===
token.address.toLowerCase(),
);
const bal = formatTokenBalance(raw, token.decimals);
if (existing) {
existing.balance = bal;
} else {
addr.tokenBalances.push({
address: token.address,
symbol: token.symbol,
decimals: token.decimals,
balance: bal,
});
}
} catch (e) {
// skip on error
}
})(),
);
}
}
}
await Promise.all(updates);
lastFetchedAt = now;
}
// Look up token metadata from its contract.
async function lookupTokenInfo(contractAddress, rpcUrl) {
const provider = getProvider(rpcUrl);
const contract = new Contract(contractAddress, ERC20_ABI, provider);
const [name, symbol, decimals] = await Promise.all([
contract.name(),
contract.symbol(),
contract.decimals(),
]);
return { name, symbol, decimals: Number(decimals) };
}
// Force-invalidate the balance cache (e.g. after sending a tx).
function invalidateBalanceCache() {
lastFetchedAt = 0;
}
module.exports = {
refreshBalances,
lookupTokenInfo,
invalidateBalanceCache,
getProvider,
};