fix(rwallet): filter spam tokens via CoinGecko verification

ERC-20 tokens not recognized by CoinGecko and valued < $1 by Safe API
are now stripped from balance responses, removing fake ETH and airdrop spam.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-25 16:08:52 -07:00
parent 4d7d2c0108
commit 5f853322b0
2 changed files with 22 additions and 6 deletions

View File

@ -146,8 +146,9 @@ interface BalanceItem {
export async function enrichWithPrices( export async function enrichWithPrices(
balances: BalanceItem[], balances: BalanceItem[],
chainId: string, chainId: string,
options?: { filterSpam?: boolean },
): Promise<BalanceItem[]> { ): Promise<BalanceItem[]> {
// Skip testnets and unsupported chains // Skip testnets and unsupported chains — no CoinGecko data to verify against
if (!CHAIN_PLATFORM[chainId] && !NATIVE_COIN_ID[chainId]) return balances; if (!CHAIN_PLATFORM[chainId] && !NATIVE_COIN_ID[chainId]) return balances;
// Check if any balance actually needs pricing // Check if any balance actually needs pricing
@ -165,7 +166,7 @@ export async function enrichWithPrices(
try { try {
const priceData = await fetchChainPrices(chainId, tokenAddresses); const priceData = await fetchChainPrices(chainId, tokenAddresses);
return balances.map((b) => { const enriched = balances.map((b) => {
// Skip if already has a real fiat value // Skip if already has a real fiat value
if (b.fiatBalance && b.fiatBalance !== "0" && parseFloat(b.fiatBalance) > 0) { if (b.fiatBalance && b.fiatBalance !== "0" && parseFloat(b.fiatBalance) > 0) {
return b; return b;
@ -194,6 +195,21 @@ export async function enrichWithPrices(
fiatBalance: String(fiatValue), fiatBalance: String(fiatValue),
}; };
}); });
if (options?.filterSpam) {
return enriched.filter((b) => {
// Native tokens always pass
if (!b.tokenAddress || b.tokenAddress === "0x0000000000000000000000000000000000000000") return true;
// CoinGecko recognized this token
if (priceData.prices.has(b.tokenAddress.toLowerCase())) return true;
// Safe API independently valued it at >= $1
if (parseFloat(b.fiatBalance || "0") >= 1) return true;
// Unknown ERC-20 with no verified value = spam
return false;
});
}
return enriched;
} catch (e) { } catch (e) {
console.warn(`[price-feed] Failed to enrich prices for chain ${chainId}:`, e); console.warn(`[price-feed] Failed to enrich prices for chain ${chainId}:`, e);
return balances; return balances;

View File

@ -47,7 +47,7 @@ routes.get("/api/safe/:chainId/:address/balances", async (c) => {
fiatBalance: item.fiatBalance || "0", fiatBalance: item.fiatBalance || "0",
fiatConversion: item.fiatConversion || "0", fiatConversion: item.fiatConversion || "0",
})); }));
const enriched = (await enrichWithPrices(data, chainId)) const enriched = (await enrichWithPrices(data, chainId, { filterSpam: true }))
.filter(b => BigInt(b.balance || "0") > 0n); .filter(b => BigInt(b.balance || "0") > 0n);
c.header("Cache-Control", "public, max-age=30"); c.header("Cache-Control", "public, max-age=30");
return c.json(enriched); return c.json(enriched);
@ -600,7 +600,7 @@ routes.get("/api/eoa/:chainId/:address/balances", async (c) => {
await Promise.allSettled(promises); await Promise.allSettled(promises);
const enriched = await enrichWithPrices(balances, chainId); const enriched = await enrichWithPrices(balances, chainId, { filterSpam: true });
c.header("Cache-Control", "public, max-age=30"); c.header("Cache-Control", "public, max-age=30");
return c.json(enriched); return c.json(enriched);
}); });
@ -661,7 +661,7 @@ routes.get("/api/eoa/:address/all-balances", async (c) => {
await Promise.allSettled(tokenPromises); await Promise.allSettled(tokenPromises);
if (chainBalances.length > 0) { if (chainBalances.length > 0) {
const enriched = await enrichWithPrices(chainBalances, chainId); const enriched = await enrichWithPrices(chainBalances, chainId, { filterSpam: true });
results.push({ chainId, chainName: info.name, balances: enriched }); results.push({ chainId, chainName: info.name, balances: enriched });
} }
}) })
@ -700,7 +700,7 @@ routes.get("/api/safe/:address/all-balances", async (c) => {
})).filter((b: BalanceItem) => BigInt(b.balance || "0") > 0n); })).filter((b: BalanceItem) => BigInt(b.balance || "0") > 0n);
if (chainBalances.length > 0) { if (chainBalances.length > 0) {
const enriched = await enrichWithPrices(chainBalances, chainId); const enriched = await enrichWithPrices(chainBalances, chainId, { filterSpam: true });
results.push({ chainId, chainName: info.name, balances: enriched }); results.push({ chainId, chainName: info.name, balances: enriched });
} }
} catch {} } catch {}