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.
Cette révision appartient à :
@@ -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",
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Référencer dans un nouveau ticket
Bloquer un utilisateur