Refactor popup into shared modules, wire up real ERC-20 tokens
All checks were successful
check / check (push) Successful in 13s
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:
130
src/shared/balances.js
Normal file
130
src/shared/balances.js
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user