Filter spam tokens from balance display
All checks were successful
check / check (push) Successful in 5s
All checks were successful
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.
This commit is contained in:
@@ -497,7 +497,12 @@ async function backgroundRefresh() {
|
|||||||
if (now - (state.lastBalanceRefresh || 0) < BACKGROUND_REFRESH_INTERVAL)
|
if (now - (state.lastBalanceRefresh || 0) < BACKGROUND_REFRESH_INTERVAL)
|
||||||
return;
|
return;
|
||||||
if (state.wallets.length === 0) 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;
|
state.lastBalanceRefresh = now;
|
||||||
await saveState();
|
await saveState();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,12 @@ async function doRefreshAndRender() {
|
|||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
refreshPrices(),
|
refreshPrices(),
|
||||||
refreshBalances(state.wallets, state.rpcUrl, state.blockscoutUrl),
|
refreshBalances(
|
||||||
|
state.wallets,
|
||||||
|
state.rpcUrl,
|
||||||
|
state.blockscoutUrl,
|
||||||
|
state.trackedTokens,
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
state.lastBalanceRefresh = Date.now();
|
state.lastBalanceRefresh = Date.now();
|
||||||
await saveState();
|
await saveState();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const {
|
|||||||
const { ERC20_ABI } = require("./constants");
|
const { ERC20_ABI } = require("./constants");
|
||||||
const { log, debugFetch } = require("./log");
|
const { log, debugFetch } = require("./log");
|
||||||
const { deriveAddressFromXpub } = require("./wallet");
|
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
|
// Use a static network to skip auto-detection (which can fail and cause
|
||||||
// "could not coalesce error" on some RPC endpoints like Cloudflare).
|
// "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.
|
// Fetch token balances for a single address from Blockscout.
|
||||||
// Returns [{ address, symbol, decimals, balance }].
|
// 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 {
|
try {
|
||||||
const resp = await debugFetch(
|
const resp = await debugFetch(
|
||||||
blockscoutUrl + "/addresses/" + address + "/token-balances",
|
blockscoutUrl + "/addresses/" + address + "/token-balances",
|
||||||
@@ -49,18 +52,43 @@ async function fetchTokenBalances(address, blockscoutUrl) {
|
|||||||
}
|
}
|
||||||
const items = await resp.json();
|
const items = await resp.json();
|
||||||
if (!Array.isArray(items)) return null;
|
if (!Array.isArray(items)) return null;
|
||||||
|
|
||||||
|
const trackedSet = new Set(
|
||||||
|
(trackedTokens || []).map((t) => t.address.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
const balances = [];
|
const balances = [];
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.token?.type !== "ERC-20") continue;
|
if (item.token?.type !== "ERC-20") continue;
|
||||||
const decimals = parseInt(item.token.decimals || "18", 10);
|
const decimals = parseInt(item.token.decimals || "18", 10);
|
||||||
const bal = formatTokenBalance(item.value || "0", decimals);
|
const bal = formatTokenBalance(item.value || "0", decimals);
|
||||||
if (bal === "0.0") continue;
|
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({
|
balances.push({
|
||||||
address: item.token.address_hash,
|
address: item.token.address_hash,
|
||||||
symbol: item.token.symbol || "???",
|
symbol: item.token.symbol || "???",
|
||||||
decimals: decimals,
|
decimals: decimals,
|
||||||
balance: bal,
|
balance: bal,
|
||||||
holders: parseInt(item.token.holders_count || "0", 10),
|
holders: holders,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return balances;
|
return balances;
|
||||||
@@ -71,7 +99,7 @@ async function fetchTokenBalances(address, blockscoutUrl) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch ETH balances, ENS names, and ERC-20 token balances for all addresses.
|
// 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);
|
log.debugf("refreshBalances start, rpc:", rpcUrl);
|
||||||
const provider = getProvider(rpcUrl);
|
const provider = getProvider(rpcUrl);
|
||||||
const updates = [];
|
const updates = [];
|
||||||
@@ -109,19 +137,21 @@ async function refreshBalances(wallets, rpcUrl, blockscoutUrl) {
|
|||||||
|
|
||||||
// ERC-20 token balances via Blockscout
|
// ERC-20 token balances via Blockscout
|
||||||
updates.push(
|
updates.push(
|
||||||
fetchTokenBalances(addr.address, blockscoutUrl).then(
|
fetchTokenBalances(
|
||||||
(balances) => {
|
addr.address,
|
||||||
if (balances !== null) {
|
blockscoutUrl,
|
||||||
addr.tokenBalances = balances;
|
trackedTokens,
|
||||||
log.debugf(
|
).then((balances) => {
|
||||||
"Token balances",
|
if (balances !== null) {
|
||||||
addr.address,
|
addr.tokenBalances = balances;
|
||||||
balances.length,
|
log.debugf(
|
||||||
"tokens",
|
"Token balances",
|
||||||
);
|
addr.address,
|
||||||
}
|
balances.length,
|
||||||
},
|
"tokens",
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user