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.
此提交包含在:
+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",
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
新增問題並參考
封鎖使用者