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(
|
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;
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue