1 Commits
Author SHA1 Message Date
sneak ceb6a3195b 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. It sent 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 install 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. Unticking "Use Blockscout"
drops every untracked token from every address. After either changes in
Settings, Back draws again any screen a reopened popup can draw. ENS lookups
ask only the RPC endpoint.

Model: opus-5-5
2026-10-26 01:39:30 +00:00
24 changed files with 177 additions and 375 deletions

No files matched your search

+6 -11
View File
@@ -15,7 +15,7 @@ Hence, a minimally viable ERC20 browser wallet/signer that works cross-platform.
Everything you need, nothing you don't. We import as few libraries as possible
and don't implement any crypto. By default the wallet sends user-specific data
only to the Ethereum RPC endpoint. The user can change that endpoint in
Settings. It defaults to a public node.
Settings; it defaults to a public node.
Out of the box the extension contacts two services:
@@ -1434,15 +1434,13 @@ figures while Blockscout fails.
The refresh asks Blockscout only while the user has "Use Blockscout" on. The
setting is off until the user turns it on. While it is off:
- Only tracked tokens are listed. Turning the setting off drops every other
token from every address at once.
- Only tracked tokens are listed. Turning the setting off in Settings drops
every other token from every address at once.
- A tracked token's entry has no holder count and no `decimals` from Blockscout.
- A tracked token's balance uses the decimals from the bundled list, or else the
decimals saved when the token was tracked.
Each refresh, in the popup or the background, reads the stored setting when it
starts. If the setting is off when the refresh saves, the save drops every token
the user has not tracked.
A refresh that started before the setting changed saves nothing when it returns.
A tracked token is shown when its balance is nonzero, and any other ERC-20 when
its balance is nonzero and it is in the bundled known-token list or has 1,000 or
@@ -1641,10 +1639,6 @@ the stack, as a network switch does (below). The cut drops Send, whose token
dropdown was filled under the old setting. "Back" cannot render Send again. A
change to "Check recipients on Etherscan" leaves the stack as it is.
Any other popup open meanwhile takes either change from the stored record, as it
takes a network switch (below). It then draws its screen on top again under the
new setting.
A network switch leaves every screen on the page showing the previous network.
The user makes it in Settings, or approves a site's request and the background
saves it. Any other popup open meanwhile takes the switch from the stored
@@ -2852,7 +2846,8 @@ Sent the user's data only while the user has turned the setting on:
- **Data sent**: the recipient's address and nothing else.
The etherscan links on the screens open a page in the browser when the user
clicks one. They are not requests the wallet makes.
clicks one. They are not requests the wallet makes. Nothing is fetched from them
until the user clicks.
What the extension does NOT do:
+9 -10
View File
@@ -75,8 +75,8 @@ Ethereum RPC endpoint. It also asks the CoinDesk price API for prices, sending
token symbols and nothing else.
Blockscout and Etherscan are sent none of your data until you turn them on.
Their settings are "Use Blockscout" and "Check recipients on Etherscan". Both
are on the Welcome screen and in Settings. Both are off in a new install.
Their settings are "Use Blockscout" and "Check recipients on Etherscan", on the
Welcome screen and in Settings. Both are off in a new install.
### External Services
@@ -143,14 +143,13 @@ address.
**Etherscan address labels** (`etherscan.io`; `sepolia.etherscan.io` on Sepolia)
When you review a send, AutistMask fetches the recipient's public Etherscan
address page. It looks for a "Fake_Phishing"/"Phish/Hack" label or a scam
warning, and shows a red warning if it finds one. This is a plain page fetch
with no API key, made by your browser. It is best-effort: if it fails, it is
silently ignored. This endpoint is not user-configurable.
The check is off until you turn on "Check recipients on Etherscan". While the
setting is off, the confirmation screen says that no Etherscan check was made.
Off until you turn on "Check recipients on Etherscan". When you review a send,
AutistMask fetches the recipient's public Etherscan address page. It looks for a
"Fake_Phishing"/"Phish/Hack" label or a scam warning, and shows a red warning if
it finds one. This is a plain page fetch with no API key, made by your browser.
It is best-effort: if it fails, it is silently ignored. This endpoint is not
user-configurable. While it is off, the confirmation screen says that no
Etherscan check was made.
When it is contacted: only while "Check recipients on Etherscan" is on, each
time you reach the send confirmation screen.
+5 -11
View File
@@ -17,11 +17,7 @@ const { applyChainSwitchFields } = require("../shared/chainSwitchFields");
// the same prohibition reported early, not the guarantee.
const { getState, updateState } = require("./state");
const { StateUnusableError } = require("../shared/stateSchema");
const {
refreshBalances,
dropUntrackedTokens,
getProvider,
} = require("../shared/balances");
const { refreshBalances, getProvider } = require("../shared/balances");
const { debugFetch, log } = require("../shared/log");
const {
verifySignedTx,
@@ -1391,9 +1387,9 @@ const RECENT_BALANCE_REFRESH_MS = Math.floor(BALANCE_REFRESH_PERIOD_MS / 2);
// lastBalanceRefresh is left as the switch set it, so the next tick refreshes
// the new network.
//
// The refresh asks Blockscout only if "Use Blockscout" is on when it starts.
// The user may turn it off while the refresh runs. So when "Use Blockscout"
// stored NOW is off, the save drops every token the user has not tracked.
// Nothing is applied either when "Use Blockscout" stored NOW is not the one the
// refresh ran with. Turning it off dropped the tokens the user has not tracked
// (src/popup/views/settings.js), and this refresh must not bring them back.
async function backgroundRefresh() {
const s = await getState();
const now = Date.now();
@@ -1419,6 +1415,7 @@ async function backgroundRefresh() {
await updateState((fresh) => {
if (fresh.networkId !== s.networkId) return;
if (fresh.useBlockscout !== s.useBlockscout) return;
for (const wallet of fresh.wallets) {
for (const addr of wallet.addresses || []) {
const got = refreshed.get(String(addr.address).toLowerCase());
@@ -1432,9 +1429,6 @@ async function backgroundRefresh() {
}
}
}
if (!fresh.useBlockscout) {
dropUntrackedTokens(fresh.wallets, fresh.trackedTokens);
}
fresh.lastBalanceRefresh = now;
});
}
+25 -56
View File
@@ -11,7 +11,7 @@ const {
const { StateUnusableError } = require("../shared/stateSchema");
const { log, setRuntimeDebug } = require("../shared/log");
const { refreshPrices } = require("../shared/prices");
const { refreshBalances, dropUntrackedTokens } = require("../shared/balances");
const { refreshBalances } = require("../shared/balances");
const { clearPopupCaches } = require("../shared/chainSwitch");
const {
$,
@@ -71,28 +71,25 @@ let refreshInFlight = false;
// the recovery screen: there is no profile left to refresh.
let refreshTimer = null;
// The refresh first takes any change to the network or to "Use Blockscout"
// saved elsewhere (followStoredSettings() below). So it asks Blockscout only if
// the stored setting is on when it starts.
//
// The refresh writes into a copy of the wallets. Inside the save, under the
// lock, its results are copied onto the address records in `state` only if the
// network it ran on is still the active one, on this page and in the stored
// record; the background's switch, after the user approves a site's request,
// reaches this page only after the stored record (followStoredSettings()).
// reaches this page only after the stored record (followNetworkSwitch() below).
// A switch while the refresh ran cleared every address's balances
// (applyChainSwitchFields() in src/shared/chainSwitchFields.js), and the
// previous network's must not come back over them
// (https://git.eeqj.de/sneak/AutistMask/issues/577). A dropped refresh is not
// recorded as done. Results are copied by address, so an address added or
// removed meanwhile is left as it is. If the stored "Use Blockscout" is off
// when the refresh saves, the save drops every token the user has not tracked.
// (https://git.eeqj.de/sneak/AutistMask/issues/577). The same holds when "Use
// Blockscout" changed while the refresh ran. Turning it off dropped the tokens
// the user has not tracked (views/settings.js), and this refresh must not bring
// them back. A dropped refresh is not recorded as done. Results are copied by
// address, so an address added or removed meanwhile is left as it is.
async function doRefreshAndRender() {
if (refreshInFlight) return;
refreshInFlight = true;
try {
await followStoredSettings();
const networkId = state.networkId;
const useBlockscout = state.useBlockscout;
const wallets = structuredClone(state.wallets);
await Promise.all([
refreshPrices(),
@@ -100,7 +97,7 @@ async function doRefreshAndRender() {
wallets,
state.rpcUrl,
state.blockscoutUrl,
state.useBlockscout,
useBlockscout,
state.trackedTokens,
networkId,
pageClosed.signal,
@@ -115,6 +112,8 @@ async function doRefreshAndRender() {
await saveState((stored) => {
if (state.networkId !== networkId) return false;
if (stored.networkId !== networkId) return false;
if (state.useBlockscout !== useBlockscout) return false;
if (stored.useBlockscout !== useBlockscout) return false;
for (const wallet of state.wallets) {
for (const addr of wallet.addresses) {
const got = refreshed.get(addr.address);
@@ -124,9 +123,6 @@ async function doRefreshAndRender() {
}
}
}
if (!stored.useBlockscout) {
dropUntrackedTokens(state.wallets, stored.trackedTokens);
}
state.lastBalanceRefresh = Date.now();
return true;
});
@@ -238,36 +234,17 @@ function fallbackView() {
// src/shared/chainSwitch.js), so a record naming a network other than this
// page's always holds a switch saved elsewhere, and a switch the user picks
// here while this waits for the lock is made after it.
//
// Another page may change "Use Blockscout" or "Check recipients on Etherscan".
// This page takes either change the same way and draws its screens again. Its
// own changes to them are made under the lock too (views/settings.js,
// views/welcome.js).
async function followStoredSettings() {
let changed = false;
async function followNetworkSwitch() {
let switched = false;
// A failed save is reported by saveState() itself: see onSaveFailure().
await saveState((stored) => {
if (stored.networkId !== state.networkId) {
settings.cutViewStack();
takeStoredNetwork(stored);
clearPopupCaches();
changed = true;
}
if (stored.useBlockscout !== state.useBlockscout) {
settings.setUseBlockscout(stored.useBlockscout);
changed = true;
}
if (
stored.checkRecipientsOnEtherscan !==
state.checkRecipientsOnEtherscan
) {
state.checkRecipientsOnEtherscan =
stored.checkRecipientsOnEtherscan;
changed = true;
}
if (!changed) return false;
if (stored.networkId === state.networkId) return false;
settings.cutViewStack();
takeStoredNetwork(stored);
clearPopupCaches();
switched = true;
}).catch(() => {});
if (changed) settings.showNewSettings();
if (switched) settings.showNewNetwork();
}
// The background opens the approval window as wide as the page, but the
@@ -414,24 +391,16 @@ async function init() {
settingsAddToken.init(ctx);
deleteAddress.init(ctx);
// Only a record whose network, "Use Blockscout" or "Check recipients on
// Etherscan" differs from this page's is followed. A record this page's
// own save wrote matches this page. A record naming no network is no
// switch.
// Only a record naming a network other than this page's is followed: the
// one this page's own switch in Settings saves names the network it
// already has, and a record naming none is no switch.
onStorageChanged((changes) => {
const written = changes.autistmask && changes.autistmask.newValue;
if (!written || !written.networkId) return;
if (
written.networkId !== state.networkId ||
written.useBlockscout !== state.useBlockscout ||
written.checkRecipientsOnEtherscan !==
state.checkRecipientsOnEtherscan
) {
followStoredSettings();
}
if (written.networkId !== state.networkId) followNetworkSwitch();
});
// A change saved since loadState() above, before there was a listener.
followStoredSettings();
// A switch saved since loadState() above, before there was a listener.
followNetworkSwitch();
if (!state.hasWallet) {
welcome.show();
+3 -3
View File
@@ -42,9 +42,9 @@ function markViewRendered(view) {
// asks for one when every screen on the page is out of date:
//
// - after a network switch, since those screens show the previous network
// (showNewSettings() in views/settings.js)
// - after "Use Blockscout" or "Check recipients on Etherscan" changes, here
// or on another page, since those screens were drawn under the old setting
// (showNewNetwork() in views/settings.js)
// - after "Use Blockscout" or "Check recipients on Etherscan" changes in
// Settings, since those screens were drawn under the old setting
//
// The unit tests, which simulate several page loads against one module
// instance, ask for one too.
+1
View File
@@ -617,6 +617,7 @@ async function checkRecipientHistory(txInfo) {
const asyncWarnings = await getFullWarnings(txInfo.to, provider, {
fromAddress: txInfo.from,
chainId: txInfo.chainId,
checkRecipientsOnEtherscan: state.checkRecipientsOnEtherscan,
});
// The user may have left this transaction while the checks were in
// flight; its warnings must not reach another recipient's screen.
+44 -48
View File
@@ -17,7 +17,6 @@ const {
const { state, saveState, currentNetwork } = require("../../shared/state");
const { onChainSwitch } = require("../../shared/chainSwitch");
const { restorableStack } = require("../../shared/persistedState");
const { dropUntrackedTokens } = require("../../shared/balances");
const { resetRenderedViews, markViewRendered } = require("../viewRouter");
const {
log,
@@ -262,8 +261,7 @@ async function renderSiteLists() {
// native token: any other token there was picked on the previous network, so
// it is no longer selected either. Without a wallet the stack starts at
// Welcome, which that cut would drop, and Welcome shows nothing from a network.
// A change to "Use Blockscout" cuts the stack the same way (setUseBlockscout()
// below).
// A change to "Use Blockscout" cuts the stack the same way (init() below).
function cutViewStack() {
if (!state.hasWallet) return;
if (state.selectedToken !== "ETH") {
@@ -276,39 +274,18 @@ function cutViewStack() {
state.viewStack = restorableStack(state.viewStack, state.currentView);
}
// Sets "Use Blockscout" to `on` on this page. It is called inside a save,
// holding the lock, for the user's change here or another page's
// (followStoredSettings() in src/popup/index.js).
//
// Send's token dropdown was filled under the old setting, and Back cannot draw
// Send again. So the stack is cut, as for a network switch. Turning the setting
// off also drops every token the user has not tracked.
function setUseBlockscout(on) {
state.useBlockscout = on;
cutViewStack();
if (!on) dropUntrackedTokens(state.wallets, state.trackedTokens);
}
// Draws the page again after a network switch, or after another page changed
// "Use Blockscout" or "Check recipients on Etherscan". Every screen counts as
// not yet rendered, so "Back" renders the one it lands on under the new
// settings. Settings on top is brought up to date in place; any other screen is
// drawn again as a reopened popup draws it, which is Home for one that cannot
// be rendered from saved state. Without a wallet any other screen is Welcome or
// AddWallet. Back only unhides Welcome, so its two checkboxes are set here.
function showNewSettings() {
// Every screen counts as not yet rendered, so "Back" renders the one it lands
// on for the new network. Settings on top is brought up to date in place;
// any other screen is drawn again as a reopened popup draws it, which is Home
// for one that cannot be rendered from saved state. Without a wallet any other
// screen is Welcome or AddWallet, which show nothing from a network.
function showNewNetwork() {
resetRenderedViews();
$("welcome-use-blockscout").checked = state.useBlockscout;
$("welcome-check-recipients-on-etherscan").checked =
state.checkRecipientsOnEtherscan;
if (state.currentView === "settings") {
markViewRendered("settings");
$("settings-network").value = state.networkId;
$("settings-rpc").value = state.rpcUrl;
$("settings-blockscout").value = state.blockscoutUrl;
$("settings-use-blockscout").checked = state.useBlockscout;
$("settings-check-recipients-on-etherscan").checked =
state.checkRecipientsOnEtherscan;
renderTrackedTokens();
} else if (state.hasWallet) {
ctx.restoreView();
@@ -391,31 +368,51 @@ function init(_ctx) {
showFlash("Saved.");
});
// Each change is made inside the save, holding the lock. Then
// followStoredSettings() in src/popup/index.js never takes this page's
// unsaved change for another page's. The screens under Settings were drawn
// under the old setting. So each change makes every screen but Settings count as not yet
// drawn, as a network switch does. Back then draws again any screen a
// reopened popup can draw. Back only unhides Welcome, so its boxes are set
// here too.
// The screens under Settings were drawn under the old setting. Each change
// makes every screen but Settings count as not yet drawn, as a network
// switch does. Back then draws again any screen a reopened popup can draw.
// Welcome offers the same two settings, and Back only unhides it, so its
// boxes are set here too.
//
// A change to "Use Blockscout" also cuts the stack, as a network switch
// does (cutViewStack() above). Send's token dropdown was filled under the
// old setting, and Back cannot draw Send again.
//
// Turning "Use Blockscout" off drops the tokens the user has not tracked
// from every address. Only Blockscout reports them, so no screen lists them
// from then on.
$("settings-use-blockscout").addEventListener("change", async () => {
const on = $("settings-use-blockscout").checked;
await saveState(() => setUseBlockscout(on));
$("welcome-use-blockscout").checked = on;
state.useBlockscout = $("settings-use-blockscout").checked;
$("welcome-use-blockscout").checked = state.useBlockscout;
if (!state.useBlockscout) {
const tracked = new Set(
state.trackedTokens.map((t) => t.address.toLowerCase()),
);
for (const wallet of state.wallets) {
for (const addr of wallet.addresses) {
addr.tokenBalances = (addr.tokenBalances || []).filter(
(row) => tracked.has(String(row.address).toLowerCase()),
);
}
}
}
cutViewStack();
resetRenderedViews();
markViewRendered("settings");
await saveState();
});
$("settings-check-recipients-on-etherscan").addEventListener(
"change",
async () => {
const on = $("settings-check-recipients-on-etherscan").checked;
await saveState(() => {
state.checkRecipientsOnEtherscan = on;
});
$("welcome-check-recipients-on-etherscan").checked = on;
state.checkRecipientsOnEtherscan = $(
"settings-check-recipients-on-etherscan",
).checked;
$("welcome-check-recipients-on-etherscan").checked =
state.checkRecipientsOnEtherscan;
resetRenderedViews();
markViewRendered("settings");
await saveState();
},
);
@@ -432,7 +429,7 @@ function init(_ctx) {
// Open pages are told by the background, as after a site's approved
// switch request.
notify({ type: "AUTISTMASK_NETWORK_CHANGED", chainId: net.chainId });
showNewSettings();
showNewNetwork();
showFlash("Switched to " + net.name + ".");
});
@@ -572,6 +569,5 @@ module.exports = {
show,
renderSiteLists,
cutViewStack,
setUseBlockscout,
showNewSettings,
showNewNetwork,
};
+3 -3
View File
@@ -308,9 +308,9 @@ function populateOnChainDetails(txData, chainId) {
}
}
// Reads the block, network details and calldata from Blockscout, and decodes
// the calldata. While "Use Blockscout" is off, Blockscout is not asked and none
// of them is shown.
// The block, network details, calldata and its decoding, from Blockscout. While
// the user has "Use Blockscout" off, Blockscout is not asked and none of them
// is shown.
async function loadFullTxDetails(txHash, chainId) {
if (!state.useBlockscout) return;
const section = $("tx-detail-calldata-section");
+6 -9
View File
@@ -12,24 +12,21 @@ function show() {
showView("welcome");
}
// Each change is made inside the save, holding the lock, as Settings makes it.
function init(ctx) {
$("btn-welcome-add").addEventListener("click", ctx.showAddWalletView);
$("welcome-use-blockscout").addEventListener("change", async () => {
const on = $("welcome-use-blockscout").checked;
await saveState(() => {
state.useBlockscout = on;
});
state.useBlockscout = $("welcome-use-blockscout").checked;
await saveState();
});
$("welcome-check-recipients-on-etherscan").addEventListener(
"change",
async () => {
const on = $("welcome-check-recipients-on-etherscan").checked;
await saveState(() => {
state.checkRecipientsOnEtherscan = on;
});
state.checkRecipientsOnEtherscan = $(
"welcome-check-recipients-on-etherscan",
).checked;
await saveState();
},
);
}
+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) {
+1 -17
View File
@@ -2,8 +2,7 @@
//
// - 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,
// while the user has "Use Blockscout" on. Those balances are dropped when
// it is off.
// while the user has "Use Blockscout" on
// - ENS reverse lookup via RPC
const {
@@ -427,20 +426,6 @@ async function refreshBalances(
log.debugf("refreshBalances done");
}
// Drops the balance of each token not in `trackedTokens` from every address.
// Callers run it while "Use Blockscout" is off, since only Blockscout reports
// those tokens.
function dropUntrackedTokens(wallets, trackedTokens) {
const tracked = new Set(trackedTokens.map((t) => t.address.toLowerCase()));
for (const wallet of wallets) {
for (const addr of wallet.addresses || []) {
addr.tokenBalances = (addr.tokenBalances || []).filter((row) =>
tracked.has(String(row.address).toLowerCase()),
);
}
}
}
// Look up token metadata from its contract.
// Calls symbol() and decimals() to verify it implements ERC-20.
// `signal` is as for fetchTokenBalances().
@@ -559,7 +544,6 @@ async function scanForAddresses(xpub, rpcUrl, networkId, signal, gapLimit = 5) {
module.exports = {
fetchTokenBalances,
refreshBalances,
dropUntrackedTokens,
lookupTokenInfo,
getProvider,
scanForAddresses,
+2 -2
View File
@@ -4,7 +4,7 @@
//
// - onChainSwitch() below makes the switch the user picks in Settings, with
// applyChainSwitchFields() (chainSwitchFields.js).
// - followStoredSettings() in src/popup/index.js takes a switch the background
// - followNetworkSwitch() in src/popup/index.js takes a switch the background
// or another popup page saved, from the stored record, with
// takeStoredNetwork() (state.js), which copies it with
// copyChainSwitchFields() (chainSwitchFields.js).
@@ -45,7 +45,7 @@ function clearPopupCaches() {
//
// `state` is switched inside the save that stores the switch, holding the
// lock, so it is never switched while a follow of a switch saved elsewhere
// waits for the lock (followStoredSettings() in src/popup/index.js): that
// waits for the lock (followNetworkSwitch() in src/popup/index.js): that
// follow runs first, and this switch is made after it.
async function onChainSwitch(newNetworkId) {
const { state, saveState } = require("./state");
+9 -8
View File
@@ -3,7 +3,6 @@
// This is a best-effort enrichment: network failures return null silently.
const { networkByChainId } = require("./networks");
const { storageGet } = require("./browserApi");
// Patterns in the page title that indicate a flagged address.
// Title format: "Fake_Phishing184810 | Address: 0x... | Etherscan"
@@ -79,18 +78,21 @@ function parseEtherscanPage(html) {
* when it is omitted: etherscan.io on mainnet, sepolia.etherscan.io on
* Sepolia.
*
* Etherscan is asked only while "Check recipients on Etherscan" is on in the
* stored profile. The page's own copy of the setting is not used, because
* another page may have just changed it. While it is off, the answer is null.
* 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 {
const { autistmask } = await storageGet("autistmask");
if (!autistmask || !autistmask.checkRecipientsOnEtherscan) return null;
// Lazy require to avoid pulling in chrome.storage at module scope
// (which breaks unit tests that only exercise parseEtherscanPage).
const { currentNetwork } = require("./state");
@@ -112,7 +114,6 @@ async function checkEtherscanLabel(address, chainId) {
return null;
} catch {
// Network errors are expected — Etherscan may rate-limit or block.
// A failed storage read lands here too, before Etherscan is asked.
return null;
}
}
+6 -4
View File
@@ -414,10 +414,12 @@ function normalizePersisted(saved) {
: 100000;
out.utcTimestamps =
saved.utcTimestamps !== undefined ? saved.utcTimestamps : false;
// Anything but true is off: each setting lets a request reach another
// service.
out.useBlockscout = saved.useBlockscout === true;
out.checkRecipientsOnEtherscan = saved.checkRecipientsOnEtherscan === true;
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(),
+3 -4
View File
@@ -470,9 +470,8 @@ function mergeNetworkEndpoints(base, ours, theirs) {
// (caught by tests/txStatus.test.js). A page's live picture of a field it
// does not own goes on being whatever its last loadState() saw, same as
// before this fix; only the persisted record is guaranteed current. The
// network, "Use Blockscout" and "Check recipients on Etherscan" are the
// exceptions: the popup takes a change another context saved through
// `beforeWrite` (followStoredSettings() in src/popup/index.js).
// network is the exception: the popup takes a switch another context saved
// through `beforeWrite` (followNetworkSwitch() in src/popup/index.js).
async function saveStateOnce(beforeWrite) {
const result = await storageGet("autistmask");
// The record in storage right now is about to be merged into and written
@@ -604,7 +603,7 @@ function saveState(beforeWrite) {
}
// Give this page the network switch held by `stored`, a record a save of this
// page has just read (followStoredSettings() in src/popup/index.js calls this
// page has just read (followNetworkSwitch() in src/popup/index.js calls this
// from `beforeWrite`), in `state` and in `baseline` alike, as loadState() does
// with a whole record. The switch is then not this page's own change, even when
// that save fails to write: a later switch back to the previous network still
-2
View File
@@ -41,8 +41,6 @@ jest.mock("../src/shared/balances", () => ({
refreshBalances: jest.fn(async () => {
mockBalanceRefreshAt.push(Date.now());
}),
dropUntrackedTokens: jest.requireActual("../src/shared/balances")
.dropUntrackedTokens,
getProvider: jest.fn(() => ({})),
}));
+4 -7
View File
@@ -142,8 +142,6 @@ function loadWorker(networkId, opts) {
},
refreshBalances:
options.refreshBalances || jest.fn(async () => undefined),
dropUntrackedTokens: jest.requireActual("../src/shared/balances")
.dropUntrackedTokens,
}));
jest.doMock("../src/shared/phishingDomains", () => ({
isPhishingDomain: () => false,
@@ -456,9 +454,9 @@ describe("a balance refresh under a network switch", () => {
describe("a balance refresh under a change of Use Blockscout", () => {
// Turning "Use Blockscout" off drops the tokens the user has not tracked
// (src/popup/views/settings.js). A refresh that started while it was on
// still saves its balances when it returns, but not those tokens. The same
// refresh in the popup is in tests/blockscoutEtherscanSettings.test.js.
test("a refresh that returns after Use Blockscout was turned off saves its balances without the untracked token", async () => {
// must not write them back when it returns. The same refresh in the popup
// is in tests/blockscoutEtherscanSettings.test.js.
test("a refresh that returns after Use Blockscout was turned off writes nothing", async () => {
const roundTrip = deferred();
const started = deferred();
const handed = [];
@@ -473,7 +471,6 @@ describe("a balance refresh under a change of Use Blockscout", () => {
handed.push(useBlockscout);
started.resolve();
await roundTrip.promise;
wallets[0].addresses[0].balance = "7.0";
wallets[0].addresses[0].tokenBalances = [
{
address: "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
@@ -506,7 +503,7 @@ describe("a balance refresh under a change of Use Blockscout", () => {
expect(handed).toEqual([true]);
const after = bg.persisted();
expect(after.wallets[0].addresses[0].balance).toBe("7.0");
expect(after.wallets[0].addresses[0].tokenBalances).toEqual([]);
expect(after.lastBalanceRefresh).toBe(0);
});
});
+32 -153
View File
@@ -1,22 +1,19 @@
// Blockscout and Etherscan are sent none of the user's data until the user
// turns them on (https://git.eeqj.de/sneak/AutistMask/issues/668). A new
// profile has "Use Blockscout" and "Check recipients on Etherscan" both off.
// While a setting is off, the code that would ask its service asks nothing.
// Each request to either service is skipped where it is made while its setting
// is off. The popup and the background hand the stored setting to the code that
// makes the request. The one exception is the check behind "Save" beside the
// Blockscout URL in Settings. It asks that URL only for `/stats` and carries no
// user data. It is made whether or not the setting is on.
//
// The one request made with the setting off is the check behind "Save" beside
// the Blockscout URL in Settings. It asks that URL only for `/stats` and sends
// none of the user's data.
//
// The balance refresh and the Etherscan check follow the stored setting, not
// the page's copy of it. So a change another page saved applies to them at
// once. An open page also takes such a change and draws its screens again.
//
// Only Blockscout reports the tokens the user has not tracked. While "Use
// Blockscout" is off, no address keeps them and no screen lists them.
// Only Blockscout reports the tokens the user has not tracked. Turning "Use
// Blockscout" off in Settings drops them from every address, so no screen lists
// them from then on.
//
// The functions that make the requests are driven directly, against a fetch()
// that records every URL it is asked for. The screens are driven through the
// real popup over a stored profile (tests/support/popupBoot.js). Its fetch()
// real popup over a stored profile (tests/support/popupBoot.js), whose fetch()
// stub records the same way.
"use strict";
@@ -140,26 +137,23 @@ describe("where each request is made", () => {
]);
});
test("the Etherscan label check is made only while the stored profile has Check recipients on Etherscan on", async () => {
test("the Etherscan label check is made only while Check recipients on Etherscan is on", async () => {
const provider = {
getCode: async () => "0x",
getTransactionCount: async () => 1,
getBalance: async () => 0n,
};
const checkWith = async (stored) => {
globalThis.chrome = { storage: makeStorageStub(stored) };
try {
await getFullWarnings(RECIPIENT, provider, { chainId: "0x1" });
} finally {
delete globalThis.chrome;
}
};
await checkWith({});
await checkWith({ autistmask: { checkRecipientsOnEtherscan: false } });
await getFullWarnings(RECIPIENT, provider, { chainId: "0x1" });
await getFullWarnings(RECIPIENT, provider, {
chainId: "0x1",
checkRecipientsOnEtherscan: false,
});
expect(asked).toEqual([]);
await checkWith({ autistmask: { checkRecipientsOnEtherscan: true } });
await getFullWarnings(RECIPIENT, provider, {
chainId: "0x1",
checkRecipientsOnEtherscan: true,
});
expect(asked).toEqual(["https://etherscan.io/address/" + RECIPIENT]);
});
@@ -460,9 +454,9 @@ describe("the popup", () => {
return profile;
}
// The token ids of the options created after the first `from` elements.
// The page stub keeps no children, so this is where the options Send's
// dropdown was filled with are read.
// The token ids of the options the page has created since it had created
// `from` elements. The page stub keeps no children, so the options Send's
// dropdown was filled with are read from there.
function optionsSince(page, from) {
return page.document.created
.slice(from)
@@ -566,20 +560,11 @@ describe("the popup", () => {
}
});
// The token ids of the first address's balances in the stored profile.
function storedTokens(page) {
return page.storage
.read("autistmask")
.wallets[0].addresses[0].tokenBalances.map((row) =>
row.address.toLowerCase(),
);
}
// Turning the setting off dropped the untracked token. A refresh that
// started while it was on still saves its balances when it returns, but
// not that token. The same refresh in the background is in
// started while it was on must not bring the token back when it returns.
// The same refresh in the background is in
// tests/backgroundStateIsolation.test.js.
test("a refresh that returns after Use Blockscout was turned off saves its balances without the untracked token", async () => {
test("a refresh that returns after Use Blockscout was turned off saves none of its tokens", async () => {
const page = await bootPopup(onAddressDetail(true));
const { refreshBalances: held } = require("../src/shared/balances");
let answer;
@@ -587,7 +572,6 @@ describe("the popup", () => {
await new Promise((resolve) => {
answer = resolve;
});
wallets[0].addresses[0].balance = "7.0";
wallets[0].addresses[0].tokenBalances = [
tokenRow(TRACKED, "TRK"),
tokenRow(UNTRACKED, "TST"),
@@ -598,126 +582,22 @@ describe("the popup", () => {
await page.click("btn-settings");
await setBox(page, "settings-use-blockscout", false);
const before = page.storage.read("autistmask").lastBalanceRefresh;
answer();
await ticking;
const stored = page.storage.read("autistmask");
expect(
page.storage.read("autistmask").wallets[0].addresses[0].balance,
).toBe("7.0");
expect(storedTokens(page)).toEqual([TRACKED]);
stored.wallets[0].addresses[0].tokenBalances.map((row) =>
row.address.toLowerCase(),
),
).toEqual([TRACKED]);
expect(stored.lastBalanceRefresh).toBe(before);
await page.click("btn-settings-back");
expect(page.node("address-balances").innerHTML).not.toContain(
UNTRACKED,
);
});
// Another page saves `change` over the stored profile. The browser is not
// made to tell this page, so this page sees the change only by reading the
// stored profile.
function anotherPageSaves(page, change) {
page.storage.write("autistmask", {
...page.storage.read("autistmask"),
...change,
});
}
test.each([
["off", true, false],
["on", false, true],
])(
"after another page turns Use Blockscout %s, this page's next refresh follows the stored setting and is saved",
async (_how, before, after) => {
const page = await bootPopup(onAddressDetail(before));
const { refreshBalances: held } = require("../src/shared/balances");
held.mockImplementationOnce(
async (wallets, rpcUrl, blockscoutUrl, useBlockscout) => {
const addr = wallets[0].addresses[0];
addr.balance = "7.0";
addr.tokenBalances = useBlockscout
? [tokenRow(TRACKED, "TRK"), tokenRow(UNTRACKED, "TST")]
: [tokenRow(TRACKED, "TRK")];
},
);
anotherPageSaves(page, { useBlockscout: after });
await page.tick();
expect(held.mock.calls.at(-1)[3]).toBe(after);
expect(
page.storage.read("autistmask").wallets[0].addresses[0].balance,
).toBe("7.0");
expect(storedTokens(page)).toEqual(
after ? [TRACKED, UNTRACKED] : [TRACKED],
);
expect(page.hidden("address-tokens-note")).toBe(after);
},
);
test.each([
["off", true, false],
["on", false, true],
])(
"after another page turns Check recipients on Etherscan %s, this page's next Etherscan check follows the stored setting",
async (_how, before, after) => {
const page = await bootPopup(
onConfirmScreen({ checkRecipientsOnEtherscan: before }),
);
anotherPageSaves(page, { checkRecipientsOnEtherscan: after });
const from = fetched().length;
require("../src/popup/views/confirmTx").show({
token: "ETH",
from: ADDRESS,
to: RECIPIENT,
amount: "0.1",
chainId: "0x1",
});
await page.settle();
// The page also takes the change and draws ConfirmTx again, which
// checks once more. So each URL asked for is counted once.
const asked = new Set(
fetched().slice(from).filter(blockscoutOrEtherscan),
);
expect([...asked]).toEqual(
after ? ["https://etherscan.io/address/" + RECIPIENT] : [],
);
},
);
// Another page's save of the whole profile with `change`, which the
// browser announces to this page as it announces any write.
async function anotherPageAnnounces(page, change) {
await page.storage.local.set({
autistmask: { ...page.storage.read("autistmask"), ...change },
});
await page.settle();
}
test("a page open on AddressDetail draws it again when another page turns Use Blockscout off", async () => {
const page = await bootPopup(onAddressDetail(true));
expect(page.hidden("address-tokens-note")).toBe(true);
await anotherPageAnnounces(page, { useBlockscout: false });
expect(page.visibleViews()).toEqual(["address"]);
expect(page.hidden("address-tokens-note")).toBe(false);
expect(page.node("address-balances").innerHTML).not.toContain(
UNTRACKED,
);
expect(storedTokens(page)).toEqual([TRACKED]);
});
test("a page open on ConfirmTx draws it again when another page turns Check recipients on Etherscan off", async () => {
const page = await bootPopup(
onConfirmScreen({ checkRecipientsOnEtherscan: true }),
);
expect(page.hidden("confirm-etherscan-note")).toBe(true);
await anotherPageAnnounces(page, { checkRecipientsOnEtherscan: false });
expect(page.visibleViews()).toEqual(["confirm-tx"]);
expect(page.hidden("confirm-etherscan-note")).toBe(false);
});
});
describe("the background", () => {
@@ -737,7 +617,6 @@ describe("the background", () => {
jest.doMock("../src/shared/balances", () => ({
getProvider: () => ({}),
refreshBalances: async (...args) => handed(args[3]),
dropUntrackedTokens: () => {},
}));
let onAlarm = null;
globalThis.chrome = {
-2
View File
@@ -73,8 +73,6 @@ function loadColdWorker(networkId, opts) {
jest.doMock("../src/shared/balances", () => ({
getProvider: () => ({}),
refreshBalances: options.refreshBalances || jest.fn(async () => {}),
dropUntrackedTokens: jest.requireActual("../src/shared/balances")
.dropUntrackedTokens,
}));
jest.doMock("../src/shared/phishingDomains", () => ({
isPhishingDomain: () => false,
+1 -1
View File
@@ -303,7 +303,7 @@ function explorerRequests(env) {
});
}
// Waits until the list `listId` shows the line saying "Use Blockscout" is off.
// Waits until the list `listId` says what Use Blockscout being off leaves out.
async function waitForHistoryOff(page, listId) {
await page.waitForFunction(
([id, want]) => document.getElementById(id).textContent.trim() === want,
+5 -10
View File
@@ -618,6 +618,8 @@ const CONTRACT = [
"hideFraudContracts",
"hideDustTransactions",
"utcTimestamps",
"useBlockscout",
"checkRecipientsOnEtherscan",
"debugMode",
].map((field) => ({
field,
@@ -625,18 +627,11 @@ const CONTRACT = [
// A flag: only ever tested for truthiness, and written back verbatim.
hostile: [42, "notabool", { a: 1 }],
// Both answers to that truthiness test have to be driven, and 0 is a
// value src/ never writes for a flag. For utcTimestamps and debugMode
// the falsy answer is also the DEFAULT_STATE default.
// value src/ never writes for a flag. For utcTimestamps,
// useBlockscout, checkRecipientsOnEtherscan and debugMode the falsy
// answer is also the DEFAULT_STATE default.
falsy: [0],
})),
// Each lets a request reach another service, so anything but true is off.
...["useBlockscout", "checkRecipientsOnEtherscan"].map((field) => ({
field,
kind: KIND.SCALAR,
hostile: [42, "notabool", { a: 1 }],
floorOnly: [0, null, "true"],
holds: (v) => v === false,
})),
];
function siteMapHolds(v) {
+2 -3
View File
@@ -44,9 +44,8 @@ test("a flash message's timer does not fire after cleanupPopup()", async () => {
// A boot onto a screen that sends a request no mocked module answers: that
// request reaches the boot's fetch() stub, never the internet
// (https://git.eeqj.de/sneak/AutistMask/issues/559). TransactionDetail asks
// Blockscout only while "Use Blockscout" is on. ConfirmTx asks Etherscan only
// while "Check recipients on Etherscan" is on. So the profile turns both on.
// (https://git.eeqj.de/sneak/AutistMask/issues/559). Both requests are made
// only while the user has the setting for them on.
function restoringOnto(view, viewData) {
return unversionedValidProfile({
currentView: view,
+7 -9
View File
@@ -523,15 +523,13 @@ describe("a garbage value in a field the gate does not check", () => {
});
test("a malformed tokenBalances entry is dropped, and the wallet and its address survive", async () => {
// "Use Blockscout" is on, or the boot's balance refresh drops the
// well-formed entry too: the user has not tracked its token.
const profile = profileWithTokenBalances([
null,
42,
{ address: TOKEN_ADDRESS, symbol: "AAA", balance: "2.0" },
]);
profile.useBlockscout = true;
const env = await bootPopup(profile);
const env = await bootPopup(
profileWithTokenBalances([
null,
42,
{ address: TOKEN_ADDRESS, symbol: "AAA", balance: "2.0" },
]),
);
const stored = env.storage.read("autistmask");
expect(stored.wallets[0].encryptedSecret).toBe("encrypted-secret-1");
-2
View File
@@ -283,8 +283,6 @@ async function bootPopup(stored, options) {
jest.doMock("../../src/shared/balances", () => ({
fetchTokenBalances: jest.fn(async () => []),
refreshBalances: jest.fn(async () => {}),
dropUntrackedTokens: jest.requireActual("../../src/shared/balances")
.dropUntrackedTokens,
lookupTokenInfo: jest.fn(async () => null),
getProvider: () => ({}),
scanForAddresses: jest.fn(async () => ({