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:
parent
4d7d2c0108
commit
5f853322b0
|
|
@ -146,8 +146,9 @@ interface BalanceItem {
|
|||
export async function enrichWithPrices(
|
||||
balances: BalanceItem[],
|
||||
chainId: string,
|
||||
options?: { filterSpam?: boolean },
|
||||
): 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;
|
||||
|
||||
// Check if any balance actually needs pricing
|
||||
|
|
@ -165,7 +166,7 @@ export async function enrichWithPrices(
|
|||
try {
|
||||
const priceData = await fetchChainPrices(chainId, tokenAddresses);
|
||||
|
||||
return balances.map((b) => {
|
||||
const enriched = balances.map((b) => {
|
||||
// Skip if already has a real fiat value
|
||||
if (b.fiatBalance && b.fiatBalance !== "0" && parseFloat(b.fiatBalance) > 0) {
|
||||
return b;
|
||||
|
|
@ -194,6 +195,21 @@ export async function enrichWithPrices(
|
|||
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) {
|
||||
console.warn(`[price-feed] Failed to enrich prices for chain ${chainId}:`, e);
|
||||
return balances;
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ routes.get("/api/safe/:chainId/:address/balances", async (c) => {
|
|||
fiatBalance: item.fiatBalance || "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);
|
||||
c.header("Cache-Control", "public, max-age=30");
|
||||
return c.json(enriched);
|
||||
|
|
@ -600,7 +600,7 @@ routes.get("/api/eoa/:chainId/:address/balances", async (c) => {
|
|||
|
||||
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");
|
||||
return c.json(enriched);
|
||||
});
|
||||
|
|
@ -661,7 +661,7 @@ routes.get("/api/eoa/:address/all-balances", async (c) => {
|
|||
|
||||
await Promise.allSettled(tokenPromises);
|
||||
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 });
|
||||
}
|
||||
})
|
||||
|
|
@ -700,7 +700,7 @@ routes.get("/api/safe/:address/all-balances", async (c) => {
|
|||
})).filter((b: BalanceItem) => BigInt(b.balance || "0") > 0n);
|
||||
|
||||
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 });
|
||||
}
|
||||
} catch {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue