Filter spam tokens from balance display
check / check (push) Successful in 5s

Token balances from Blockscout are now filtered before display.
A token only appears if it meets at least one criterion:
- In the known 511-token list (by contract address)
- Explicitly tracked by the user (added via + Token)
- Has >= 1,000 holders on-chain

Also rejects tokens spoofing a known symbol from a different
contract address (same check used for transaction filtering).

This prevents airdropped spam tokens like "OpenClaw" from
appearing in the wallet without the user ever tracking them.
此提交包含在:
2026-02-27 13:02:05 +07:00
父節點 b64f9b56cc
當前提交 5af8a7873d
共有 3 個檔案被更改,包括 58 行新增 和 18 行删除
+6 -1
查看文件
@@ -497,7 +497,12 @@ async function backgroundRefresh() {
if (now - (state.lastBalanceRefresh || 0) < BACKGROUND_REFRESH_INTERVAL)
return;
if (state.wallets.length === 0) return;
await refreshBalances(state.wallets, state.rpcUrl, state.blockscoutUrl);
await refreshBalances(
state.wallets,
state.rpcUrl,
state.blockscoutUrl,
state.trackedTokens,
);
state.lastBalanceRefresh = now;
await saveState();
}
+6 -1
查看文件
@@ -34,7 +34,12 @@ async function doRefreshAndRender() {
try {
await Promise.all([
refreshPrices(),
refreshBalances(state.wallets, state.rpcUrl, state.blockscoutUrl),
refreshBalances(
state.wallets,
state.rpcUrl,
state.blockscoutUrl,
state.trackedTokens,
),
]);
state.lastBalanceRefresh = Date.now();
await saveState();
+46 -16
查看文件
@@ -11,6 +11,7 @@ const {
const { ERC20_ABI } = require("./constants");
const { log, debugFetch } = require("./log");
const { deriveAddressFromXpub } = require("./wallet");
const { KNOWN_SYMBOLS, TOKEN_BY_ADDRESS } = require("./tokenList");
// Use a static network to skip auto-detection (which can fail and cause
// "could not coalesce error" on some RPC endpoints like Cloudflare).
@@ -38,7 +39,9 @@ function formatTokenBalance(raw, decimals) {
// Fetch token balances for a single address from Blockscout.
// Returns [{ address, symbol, decimals, balance }].
async function fetchTokenBalances(address, blockscoutUrl) {
// Filters out spam: only shows tokens that are in the known token list,
// explicitly tracked by the user, or have >= 1000 holders.
async function fetchTokenBalances(address, blockscoutUrl, trackedTokens) {
try {
const resp = await debugFetch(
blockscoutUrl + "/addresses/" + address + "/token-balances",
@@ -49,18 +52,43 @@ async function fetchTokenBalances(address, blockscoutUrl) {
}
const items = await resp.json();
if (!Array.isArray(items)) return null;
const trackedSet = new Set(
(trackedTokens || []).map((t) => t.address.toLowerCase()),
);
const balances = [];
for (const item of items) {
if (item.token?.type !== "ERC-20") continue;
const decimals = parseInt(item.token.decimals || "18", 10);
const bal = formatTokenBalance(item.value || "0", decimals);
if (bal === "0.0") continue;
const tokenAddr = (item.token.address_hash || "").toLowerCase();
const holders = parseInt(item.token.holders_count || "0", 10);
const isKnown = TOKEN_BY_ADDRESS.has(tokenAddr);
const isTracked = trackedSet.has(tokenAddr);
const hasEnoughHolders = holders >= 1000;
// Skip spam tokens the user never asked to see
if (!isKnown && !isTracked && !hasEnoughHolders) continue;
// Skip tokens spoofing a known symbol from a different address
const sym = (item.token.symbol || "").toUpperCase();
const legitAddr = KNOWN_SYMBOLS.get(sym);
if (
legitAddr !== undefined &&
legitAddr !== null &&
tokenAddr !== legitAddr
)
continue;
balances.push({
address: item.token.address_hash,
symbol: item.token.symbol || "???",
decimals: decimals,
balance: bal,
holders: parseInt(item.token.holders_count || "0", 10),
holders: holders,
});
}
return balances;
@@ -71,7 +99,7 @@ async function fetchTokenBalances(address, blockscoutUrl) {
}
// Fetch ETH balances, ENS names, and ERC-20 token balances for all addresses.
async function refreshBalances(wallets, rpcUrl, blockscoutUrl) {
async function refreshBalances(wallets, rpcUrl, blockscoutUrl, trackedTokens) {
log.debugf("refreshBalances start, rpc:", rpcUrl);
const provider = getProvider(rpcUrl);
const updates = [];
@@ -109,19 +137,21 @@ async function refreshBalances(wallets, rpcUrl, blockscoutUrl) {
// ERC-20 token balances via Blockscout
updates.push(
fetchTokenBalances(addr.address, blockscoutUrl).then(
(balances) => {
if (balances !== null) {
addr.tokenBalances = balances;
log.debugf(
"Token balances",
addr.address,
balances.length,
"tokens",
);
}
},
),
fetchTokenBalances(
addr.address,
blockscoutUrl,
trackedTokens,
).then((balances) => {
if (balances !== null) {
addr.tokenBalances = balances;
log.debugf(
"Token balances",
addr.address,
balances.length,
"tokens",
);
}
}),
);
}
}