fix: Blockscout and Etherscan lookups are off until the user turns them on (closes #668)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s

Out of the box the wallet sent the user's addresses to Blockscout for history
and token lookups, and each recipient to Etherscan for its label. Two settings,
"Use Blockscout" and "Check recipients on Etherscan", now gate those requests
where they are made. Both are off in a new profile and offered unticked on
Welcome and in Settings. While one is off, the screens that relied on it say so
in a plain line naming the setting. README and the user guide say what is
contacted by default and what each setting adds. Unit tests show each request
skipped while its setting is off and made while it is on; the Chrome suite
watches every request from a fresh install, then turns each setting on.

Model: opus-5-5
This commit is contained in:
2026-10-21 13:14:27 +00:00
parent c316d9ee78
commit a17499beed
48 changed files with 1295 additions and 135 deletions
+3
View File
@@ -60,6 +60,8 @@ function getLocalWarnings(address, options = {}) {
* @param {string} [options.fromAddress] - Sender address (for self-send check).
* @param {string} [options.chainId] - Chain id of the network whose explorer
* the Etherscan label check asks; the active network's when omitted.
* @param {boolean} [options.checkRecipientsOnEtherscan] - The user's setting
* of that name. The Etherscan label check is made only while it is on.
* @returns {Promise<Array<{type: string, message: string, severity: string}>>}
*/
async function getFullWarnings(address, provider, options = {}) {
@@ -114,6 +116,7 @@ async function getFullWarnings(address, provider, options = {}) {
try {
const etherscanWarning = await checkEtherscanLabel(
address,
options.checkRecipientsOnEtherscan,
options.chainId,
);
if (etherscanWarning) {
+17 -6
View File
@@ -1,6 +1,7 @@
// Balance fetching: ETH balances and the balances of the tokens the user
// tracks via RPC, the other ERC-20 token balances and the holder counts via
// Blockscout, ENS reverse lookup via RPC.
// Blockscout while the user has "Use Blockscout" on, ENS reverse lookup via
// RPC.
const {
JsonRpcProvider,
@@ -81,6 +82,11 @@ function rawUnits(value) {
// Fetch token balances for a single address from Blockscout.
// Returns [{ address, name, symbol, decimals, balance, holders }].
//
// `useBlockscout` is the user's "Use Blockscout" setting. While it is off,
// Blockscout is not asked and the answer is an empty list: no token the user
// has not tracked is listed.
//
// Filters out spam: only shows tokens that are in the known token list,
// explicitly tracked by the user, or have >= 1000 holders. A tracked token's
// balance is read through the RPC endpoint instead, and its row here gives that
@@ -96,9 +102,11 @@ function rawUnits(value) {
async function fetchTokenBalances(
address,
blockscoutUrl,
useBlockscout,
trackedTokens,
signal,
) {
if (!useBlockscout) return [];
try {
const resp = await debugFetch(
blockscoutUrl + "/addresses/" + address + "/token-balances",
@@ -296,12 +304,13 @@ async function fetchTrackedTokenBalances(
}
// Fetch ETH balances, ENS names, and ERC-20 token balances for all addresses.
// `trackedTokens` are the tokens tracked on the active network. `signal` is as
// for fetchTokenBalances().
// `trackedTokens` are the tokens tracked on the active network.
// `useBlockscout` and `signal` are as for fetchTokenBalances().
async function refreshBalances(
wallets,
rpcUrl,
blockscoutUrl,
useBlockscout,
trackedTokens,
networkId,
signal,
@@ -362,13 +371,15 @@ async function refreshBalances(
// ERC-20 token balances: the tracked tokens' through the RPC
// endpoint, and from Blockscout the tokens the user has not
// tracked. When Blockscout fails, the untracked rows from before
// this refresh stay. The Blockscout request is started first and
// handed to fetchTrackedTokenBalances() too, which waits for it
// after sending its balanceOf reads, so the two run side by side.
// this refresh stay; while "Use Blockscout" is off there are none.
// The Blockscout request is started first and handed to
// fetchTrackedTokenBalances() too, which waits for it after
// sending its balanceOf reads, so the two run side by side.
const previous = addr.tokenBalances || [];
const explorerRequest = fetchTokenBalances(
addr.address,
blockscoutUrl,
useBlockscout,
tracked,
signal,
);
+10 -1
View File
@@ -78,11 +78,20 @@ function parseEtherscanPage(html) {
* when it is omitted: etherscan.io on mainnet, sepolia.etherscan.io on
* Sepolia.
*
* While the user's "Check recipients on Etherscan" setting is off, Etherscan
* is not asked and the answer is null.
*
* @param {string} address - Ethereum address to check.
* @param {boolean} checkRecipientsOnEtherscan - The user's setting.
* @param {string} [chainId] - Chain id of the network to look it up on.
* @returns {Promise<{type: string, message: string, severity: string}|null>}
*/
async function checkEtherscanLabel(address, chainId) {
async function checkEtherscanLabel(
address,
checkRecipientsOnEtherscan,
chainId,
) {
if (!checkRecipientsOnEtherscan) return null;
try {
// Lazy require to avoid pulling in chrome.storage at module scope
// (which breaks unit tests that only exercise parseEtherscanPage).
+12
View File
@@ -48,6 +48,12 @@ const DEFAULT_STATE = {
hideDustTransactions: true,
dustThresholdGwei: 100000,
utcTimestamps: false,
// "Use Blockscout" and "Check recipients on Etherscan" in Settings, one
// setting each for every network. Both off until the user turns them on:
// the requests they allow send the user's addresses to a service other
// than the RPC endpoint.
useBlockscout: false,
checkRecipientsOnEtherscan: false,
fraudContracts: [],
tokenHolderCache: {},
theme: "system",
@@ -406,6 +412,12 @@ function normalizePersisted(saved) {
: 100000;
out.utcTimestamps =
saved.utcTimestamps !== undefined ? saved.utcTimestamps : false;
out.useBlockscout =
saved.useBlockscout !== undefined ? saved.useBlockscout : false;
out.checkRecipientsOnEtherscan =
saved.checkRecipientsOnEtherscan !== undefined
? saved.checkRecipientsOnEtherscan
: false;
// A list of contract addresses, floored the same way: send.js builds its
// fraud set as `(state.fraudContracts || []).map((a) => a.toLowerCase())`
// and filterTransactions() maps the same list through normalizeAddress(),
+6
View File
@@ -235,12 +235,18 @@ function mergeTransactions(txs, tokenTransfers) {
// `chainId` is the chain id of the network `blockscoutUrl` serves. Every entry
// carries it, and a native entry is labelled with that network's
// `nativeCurrency` from networks.js (`ETH`, `SepoliaETH`).
//
// `useBlockscout` is the user's "Use Blockscout" setting. While it is off,
// Blockscout is not asked and there is no history: the answer is an empty
// list.
async function fetchRecentTransactions(
address,
blockscoutUrl,
useBlockscout,
chainId,
count = 25,
) {
if (!useBlockscout) return [];
log.debugf("fetchRecentTransactions", address);
const addrLower = normalizeAddress(address);