1 Commits
Author SHA1 Message Date
sneak fd3700da9d 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. 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. The balance refresh and the Etherscan check read the stored setting. An
open popup takes a change another page saved and draws its screens again. While
"Use Blockscout" is off, no address keeps an untracked token. ENS lookups ask
only the RPC endpoint.

Model: opus-5-5
2026-10-26 06:38:59 +00:00
32 changed files with 336 additions and 416 deletions

No files matched your search

+18 -29
View File
@@ -893,8 +893,6 @@ src/
prices.js — ETH/USD and token/USD via CoinDesk API prices.js — ETH/USD and token/USD via CoinDesk API
scamlist.js — known fraud contract addresses scamlist.js — known fraud contract addresses
state.js — persisted state (extension storage) state.js — persisted state (extension storage)
storedSettings.js — "Use Blockscout" and "Check recipients on Etherscan",
read from storage by the requests they gate
symbolSpoof.js — the known-symbol spoof rule, shared by all surfaces symbolSpoof.js — the known-symbol spoof rule, shared by all surfaces
tokenList.js — top ERC-20 tokens by market cap (hardcoded) tokenList.js — top ERC-20 tokens by market cap (hardcoded)
transactions.js — tx history fetching + anti-poisoning filters transactions.js — tx history fetching + anti-poisoning filters
@@ -1442,9 +1440,9 @@ setting is off until the user turns it on. While it is off:
- A tracked token's balance uses the decimals from the bundled list, or else the - A tracked token's balance uses the decimals from the bundled list, or else the
decimals saved when the token was tracked. decimals saved when the token was tracked.
Blockscout may answer just before the user turns the setting off. So if the Each refresh, in the popup or the background, reads the stored setting when it
stored setting is off when the refresh saves, in the popup or the background, starts. If the setting is off when the refresh saves, the save drops every token
the save drops every token the user has not tracked. the user has not tracked.
A tracked token is shown when its balance is nonzero, and any other ERC-20 when 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 its balance is nonzero and it is in the bundled known-token list or has 1,000 or
@@ -1918,14 +1916,13 @@ view would leave a wallet one click from deletion.
- **Transitions**: - **Transitions**:
- "Review" (valid inputs, ENS resolved) → **ConfirmTx** - "Review" (valid inputs, ENS resolved) → **ConfirmTx**
- "Review" with an unresolvable ENS name or an invalid amount → flash - "Review" with an unresolvable ENS name or an invalid amount → flash
message, no screen change. A name with no address, or one resolved message, no screen change. An amount is valid when it is a number above
off-chain, gets "That ENS name has no on-chain address." An amount is zero, written as digits with at most one decimal point, with nothing but
valid when it is a number above zero, written as digits with at most one zeros after the token's last decimal place. ETH has 18 places, and a token
decimal point, with nothing but zeros after the token's last decimal with more, or with an unknown number, is held to 18. Zeros past those
place. ETH has 18 places, and a token with more, or with an unknown places leave the amount unchanged, so `1.0000000` of a 6-place token
number, is held to 18. Zeros past those places leave the amount unchanged, passes and is sent as exactly 1. ConfirmTx makes the same check before
so `1.0000000` of a 6-place token passes and is sent as exactly 1. anything can be sent
ConfirmTx makes the same check before anything can be sent
- "Back" → previous screen (Home, AddressDetail, or AddressToken) - "Back" → previous screen (Home, AddressDetail, or AddressToken)
#### ConfirmTx (`confirm-tx`) #### ConfirmTx (`confirm-tx`)
@@ -2801,10 +2798,6 @@ settings:
Both settings are on the Welcome screen and in Settings. Both are off in a new Both settings are on the Welcome screen and in Settings. Both are off in a new
install. Each setting applies to every network. install. Each setting applies to every network.
A request that either setting gates reads the setting from storage before it
asks, and again when the answer lands. While the setting is off, nothing is
asked, and an answer that lands is dropped.
Contacted by default: Contacted by default:
- **Ethereum JSON-RPC endpoint**: The extension needs an Ethereum node to query - **Ethereum JSON-RPC endpoint**: The extension needs an Ethereum node to query
@@ -2840,26 +2833,22 @@ Sent the user's data only while the user has turned the setting on:
- token holder counts, for spam filtering - token holder counts, for spam filtering
- a transaction's block, network details and calldata, for - a transaction's block, network details and calldata, for
TransactionDetail TransactionDetail
- **Contacted**, while the setting is on:
- on every balance refresh, in the popup and in the background
- when Home, AddressDetail or AddressToken loads its history
- when TransactionDetail opens
- **While the setting is off**: each of those screens shows a line saying - **While the setting is off**: each of those screens shows a line saying
what it leaves out and that "Use Blockscout" is off. Only tracked tokens what it leaves out and that "Use Blockscout" is off. Only tracked tokens
are listed. are listed.
- **Saving the URL**: whether or not the setting is on, pressing "Save" - **Saving the URL**: pressing "Save" beside the URL in Settings asks that
beside the URL in Settings asks that URL for `/stats`. This request URL for `/stats`. This request carries no user data. It is made whether or
carries no user data. not the setting is on.
- **Data sent**: Ethereum addresses, and the hash of each transaction the - **Data sent**: Ethereum addresses, and the hash of each transaction the
user opens. Blockscout receives the user's addresses to query their user opens. Blockscout receives the user's addresses to query their
transaction history and token balances. No private keys, passwords, or transaction history and token balances. No private keys, passwords, or
signing operations are sent. signing operations are sent.
- **Etherscan address labels** ("Check recipients on Etherscan"): While the - **Etherscan address labels** ("Check recipients on Etherscan"): When
setting is on, ConfirmTx looks up the recipient's address on Etherscan each confirming a transaction, the extension performs a best-effort lookup of the
time it is shown. It checks for a phishing or scam label, best-effort. This is recipient address on Etherscan to check for phishing/scam labels. This is a
a direct page fetch with no API key; the user's browser makes the request. direct page fetch with no API key; the user's browser makes the request. While
While the setting is off, ConfirmTx says that no Etherscan check was made. the setting is off, ConfirmTx says that no Etherscan check was made.
- **Data sent**: the recipient's address and nothing else. - **Data sent**: the recipient's address and nothing else.
The etherscan links on the screens open a page in the browser when the user The etherscan links on the screens open a page in the browser when the user
+17 -19
View File
@@ -90,10 +90,10 @@ this in Settings to any Ethereum JSON-RPC endpoint, including your own local
node. node.
When it is contacted: on every balance refresh (every 10 seconds while the popup When it is contacted: on every balance refresh (every 10 seconds while the popup
is open, every 60 seconds in the background), when you review a send to an ENS is open, every 60 seconds in the background), when you type an ENS name into the
name, when a send is prepared and broadcast, while a pending transaction is Send screen, when a send is prepared and broadcast, while a pending transaction
polled for its receipt, and for the reverse ENS lookups used to label addresses is polled for its receipt, and for the reverse ENS lookups used to label
(cached for 12 hours). addresses (cached for 12 hours).
What gets sent: standard Ethereum JSON-RPC requests (balance queries, What gets sent: standard Ethereum JSON-RPC requests (balance queries,
transaction broadcasts, gas estimates, ENS lookups, contract-code checks). Your transaction broadcasts, gas estimates, ENS lookups, contract-code checks). Your
@@ -102,8 +102,8 @@ addresses are necessarily visible to the RPC provider when querying balances.
**Blockscout API** (default: `eth.blockscout.com/api/v2`; **Blockscout API** (default: `eth.blockscout.com/api/v2`;
`eth-sepolia.blockscout.com/api/v2` on Sepolia) `eth-sepolia.blockscout.com/api/v2` on Sepolia)
Blockscout is an open-source blockchain explorer. AutistMask sends it your data Blockscout is an open-source blockchain explorer. AutistMask uses it only while
only while "Use Blockscout" is on. It then asks Blockscout for: "Use Blockscout" is on. It then asks Blockscout for:
- your recent transactions and token transfers - your recent transactions and token transfers
- the balances of the tokens you have not tracked, with the holder counts used - the balances of the tokens you have not tracked, with the holder counts used
@@ -121,9 +121,9 @@ When it is contacted, while "Use Blockscout" is on:
- whenever a screen showing transaction history is opened - whenever a screen showing transaction history is opened
- when you open a transaction - when you open a transaction
Whether or not the setting is on, pressing "Save" beside the Blockscout URL in Pressing "Save" beside the Blockscout URL in Settings also contacts it, even
Settings asks that URL for `/stats` to check it. That request sends none of your with the setting off. That request checks the URL by asking it for `/stats`. It
data. sends none of your data.
What gets sent: your Ethereum addresses (to look up balances and transactions) What gets sent: your Ethereum addresses (to look up balances and transactions)
and the hash of each transaction you open. and the hash of each transaction you open.
@@ -143,15 +143,14 @@ address.
**Etherscan address labels** (`etherscan.io`; `sepolia.etherscan.io` on Sepolia) **Etherscan address labels** (`etherscan.io`; `sepolia.etherscan.io` on Sepolia)
While "Check recipients on Etherscan" is on, AutistMask fetches the recipient's When you review a send, AutistMask fetches the recipient's public Etherscan
public Etherscan address page when you review a send. It looks for a address page. It looks for a "Fake_Phishing"/"Phish/Hack" label or a scam
"Fake_Phishing"/"Phish/Hack" label or a scam warning, and shows a red warning if warning, and shows a red warning if it finds one. This is a plain page fetch
it finds one. This is a plain page fetch with no API key, made by your browser. with no API key, made by your browser. It is best-effort: if it fails, it is
It is best-effort: if it fails, it is silently ignored. This endpoint is not silently ignored. This endpoint is not user-configurable.
user-configurable.
The setting is off in a new install. While it is off, the confirmation screen The check is off until you turn on "Check recipients on Etherscan". While the
says that no Etherscan check was made. setting 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 When it is contacted: only while "Check recipients on Etherscan" is on, each
time you reach the send confirmation screen. time you reach the send confirmation screen.
@@ -272,8 +271,7 @@ screen. Tokens can also be added from Settings, under "Tracked Tokens".
1. Click "Send" from the home screen or an address detail view. 1. Click "Send" from the home screen or an address detail view.
2. Select what to send (ETH, or any ERC-20 token with a balance on this address 2. Select what to send (ETH, or any ERC-20 token with a balance on this address
that survives the spam filters). that survives the spam filters).
3. Enter the recipient address or ENS name (e.g. `vitalik.eth`). AutistMask does 3. Enter the recipient address or ENS name (e.g. `vitalik.eth`).
not look up a name resolved off-chain, so enter its address instead.
4. Enter the amount, or click "Max" to fill it in: a token's balance, cut to 18 4. Enter the amount, or click "Max" to fill it in: a token's balance, cut to 18
decimal places, or your ETH balance minus the amount reserved for the network decimal places, or your ETH balance minus the amount reserved for the network
fee. fee.
+4 -3
View File
@@ -1391,9 +1391,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 // lastBalanceRefresh is left as the switch set it, so the next tick refreshes
// the new network. // the new network.
// //
// Blockscout may have answered before the user turned "Use Blockscout" off. So // The refresh asks Blockscout only if "Use Blockscout" is on when it starts.
// when "Use Blockscout" stored NOW is off, the save drops every token the user // The user may turn it off while the refresh runs. So when "Use Blockscout"
// has not tracked. // stored NOW is off, the save drops every token the user has not tracked.
async function backgroundRefresh() { async function backgroundRefresh() {
const s = await getState(); const s = await getState();
const now = Date.now(); const now = Date.now();
@@ -1405,6 +1405,7 @@ async function backgroundRefresh() {
wallets, wallets,
s.rpcUrl, s.rpcUrl,
s.blockscoutUrl, s.blockscoutUrl,
s.useBlockscout,
s.trackedTokens, s.trackedTokens,
s.networkId, s.networkId,
); );
+6 -5
View File
@@ -71,8 +71,9 @@ let refreshInFlight = false;
// the recovery screen: there is no profile left to refresh. // the recovery screen: there is no profile left to refresh.
let refreshTimer = null; let refreshTimer = null;
// The refresh first takes any change to the network or to either setting saved // The refresh first takes any change to the network or to "Use Blockscout"
// elsewhere (followStoredSettings() below), so what it draws follows them. // 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 // 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 // lock, its results are copied onto the address records in `state` only if the
@@ -84,9 +85,8 @@ let refreshTimer = null;
// previous network's must not come back over them // previous network's must not come back over them
// (https://git.eeqj.de/sneak/AutistMask/issues/577). A dropped refresh is not // (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 // recorded as done. Results are copied by address, so an address added or
// removed meanwhile is left as it is. Blockscout may have answered before the // removed meanwhile is left as it is. If the stored "Use Blockscout" is off
// user turned "Use Blockscout" off. So if the stored setting is off when the // when the refresh saves, the save drops every token the user has not tracked.
// refresh saves, the save drops every token the user has not tracked.
async function doRefreshAndRender() { async function doRefreshAndRender() {
if (refreshInFlight) return; if (refreshInFlight) return;
refreshInFlight = true; refreshInFlight = true;
@@ -100,6 +100,7 @@ async function doRefreshAndRender() {
wallets, wallets,
state.rpcUrl, state.rpcUrl,
state.blockscoutUrl, state.blockscoutUrl,
state.useBlockscout,
state.trackedTokens, state.trackedTokens,
networkId, networkId,
pageClosed.signal, pageClosed.signal,
+1
View File
@@ -102,6 +102,7 @@ async function loadTransactions(address) {
const rawTxs = await fetchRecentTransactions( const rawTxs = await fetchRecentTransactions(
address, address,
state.blockscoutUrl, state.blockscoutUrl,
state.useBlockscout,
currentNetwork().chainId, currentNetwork().chainId,
); );
const result = filterTransactions(rawTxs, { const result = filterTransactions(rawTxs, {
+1
View File
@@ -174,6 +174,7 @@ async function loadTransactions(address, tokenId) {
const rawTxs = await fetchRecentTransactions( const rawTxs = await fetchRecentTransactions(
address, address,
state.blockscoutUrl, state.blockscoutUrl,
state.useBlockscout,
currentNetwork().chainId, currentNetwork().chainId,
); );
const result = filterTransactions(rawTxs, { const result = filterTransactions(rawTxs, {
+1
View File
@@ -197,6 +197,7 @@ async function loadHomeTxs(ctx) {
fetchRecentTransactions( fetchRecentTransactions(
addr, addr,
state.blockscoutUrl, state.blockscoutUrl,
state.useBlockscout,
currentNetwork().chainId, currentNetwork().chainId,
), ),
); );
+1 -4
View File
@@ -403,11 +403,8 @@ function init(_ctx) {
try { try {
const provider = getProvider(state.rpcUrl, state.networkId); const provider = getProvider(state.rpcUrl, state.networkId);
const resolved = await provider.resolveName(to); const resolved = await provider.resolveName(to);
// getProvider() does not look up a name resolved off-chain,
// so such a name comes back with no address too. The message
// covers both, in the one line the flash area shows.
if (!resolved) { if (!resolved) {
showFlash("That ENS name has no on-chain address."); showFlash("That ENS name has no address.");
return; return;
} }
resolvedTo = resolved; resolvedTo = resolved;
+3 -6
View File
@@ -26,7 +26,6 @@ const { state } = require("../../shared/state");
const { nativeCurrencyByChainId } = require("../../shared/networks"); const { nativeCurrencyByChainId } = require("../../shared/networks");
const { formatEther, formatUnits } = require("ethers"); const { formatEther, formatUnits } = require("ethers");
const { log, debugFetch } = require("../../shared/log"); const { log, debugFetch } = require("../../shared/log");
const { storedSettingIsOn } = require("../../shared/storedSettings");
const { decodeCalldata } = require("./approval"); const { decodeCalldata } = require("./approval");
/** /**
@@ -310,11 +309,10 @@ function populateOnChainDetails(txData, chainId) {
} }
// Reads the block, network details and calldata from Blockscout, and decodes // Reads the block, network details and calldata from Blockscout, and decodes
// the calldata. While the stored "Use Blockscout" is off, Blockscout is not // the calldata. While "Use Blockscout" is off, Blockscout is not asked and none
// asked, and an answer that lands after it went off is dropped // of them is shown.
// (src/shared/storedSettings.js). Either way none of them is shown.
async function loadFullTxDetails(txHash, chainId) { async function loadFullTxDetails(txHash, chainId) {
if (!(await storedSettingIsOn("useBlockscout"))) return; if (!state.useBlockscout) return;
const section = $("tx-detail-calldata-section"); const section = $("tx-detail-calldata-section");
const actionEl = $("tx-detail-calldata-action"); const actionEl = $("tx-detail-calldata-action");
const detailsEl = $("tx-detail-calldata-details"); const detailsEl = $("tx-detail-calldata-details");
@@ -330,7 +328,6 @@ async function loadFullTxDetails(txHash, chainId) {
); );
if (!resp.ok) return; if (!resp.ok) return;
const txData = await resp.json(); const txData = await resp.json();
if (!(await storedSettingIsOn("useBlockscout"))) return;
// Populate on-chain detail fields (block, nonce, gas, fee) // Populate on-chain detail fields (block, nonce, gas, fee)
populateOnChainDetails(txData, chainId); populateOnChainDetails(txData, chainId);
+9 -9
View File
@@ -22,7 +22,6 @@ const { LOW_HOLDER_THRESHOLD, parseHoldersCount } = require("./holders");
const { isSpoofedSymbol } = require("./symbolSpoof"); const { isSpoofedSymbol } = require("./symbolSpoof");
const { toDecimals } = require("./transferAmount"); const { toDecimals } = require("./transferAmount");
const { resolveTokenDecimals } = require("./approvalAmount"); const { resolveTokenDecimals } = require("./approvalAmount");
const { storedSettingIsOn } = require("./storedSettings");
// 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).
@@ -96,10 +95,9 @@ function rawUnits(value) {
// Fetch token balances for a single address from Blockscout. // Fetch token balances for a single address from Blockscout.
// Returns [{ address, name, symbol, decimals, balance, holders }]. // Returns [{ address, name, symbol, decimals, balance, holders }].
// //
// While the stored "Use Blockscout" is off, Blockscout is not asked, and an // `useBlockscout` is the user's "Use Blockscout" setting. While it is off,
// answer that lands after it went off is dropped (src/shared/storedSettings.js). // Blockscout is not asked and the answer is an empty list. So no token the user
// Either way the answer is an empty list, so no token the user has not tracked // has not tracked is listed.
// is listed.
// //
// Filters out spam: only shows tokens that are in the known token list, // 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 // explicitly tracked by the user, or have >= 1000 holders. A tracked token's
@@ -116,10 +114,11 @@ function rawUnits(value) {
async function fetchTokenBalances( async function fetchTokenBalances(
address, address,
blockscoutUrl, blockscoutUrl,
useBlockscout,
trackedTokens, trackedTokens,
signal, signal,
) { ) {
if (!(await storedSettingIsOn("useBlockscout"))) return []; if (!useBlockscout) return [];
try { try {
const resp = await debugFetch( const resp = await debugFetch(
blockscoutUrl + "/addresses/" + address + "/token-balances", blockscoutUrl + "/addresses/" + address + "/token-balances",
@@ -129,7 +128,6 @@ async function fetchTokenBalances(
return null; return null;
} }
const items = await resp.json(); const items = await resp.json();
if (!(await storedSettingIsOn("useBlockscout"))) return [];
if (!Array.isArray(items)) return null; if (!Array.isArray(items)) return null;
const trackedSet = new Set( const trackedSet = new Set(
@@ -318,12 +316,13 @@ async function fetchTrackedTokenBalances(
} }
// 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.
// `trackedTokens` are the tokens tracked on the active network. `signal` is as // `trackedTokens` are the tokens tracked on the active network.
// for fetchTokenBalances(). // `useBlockscout` and `signal` are as for fetchTokenBalances().
async function refreshBalances( async function refreshBalances(
wallets, wallets,
rpcUrl, rpcUrl,
blockscoutUrl, blockscoutUrl,
useBlockscout,
trackedTokens, trackedTokens,
networkId, networkId,
signal, signal,
@@ -392,6 +391,7 @@ async function refreshBalances(
const explorerRequest = fetchTokenBalances( const explorerRequest = fetchTokenBalances(
addr.address, addr.address,
blockscoutUrl, blockscoutUrl,
useBlockscout,
tracked, tracked,
signal, signal,
); );
+10 -8
View File
@@ -3,7 +3,8 @@
// This is a best-effort enrichment: network failures return null silently. // This is a best-effort enrichment: network failures return null silently.
const { networkByChainId } = require("./networks"); const { networkByChainId } = require("./networks");
const { storedSettingIsOn } = require("./storedSettings"); const { storageGet } = require("./browserApi");
const { normalizePersisted } = require("./persistedState");
// Patterns in the page title that indicate a flagged address. // Patterns in the page title that indicate a flagged address.
// Title format: "Fake_Phishing184810 | Address: 0x... | Etherscan" // Title format: "Fake_Phishing184810 | Address: 0x... | Etherscan"
@@ -79,9 +80,11 @@ function parseEtherscanPage(html) {
* when it is omitted: etherscan.io on mainnet, sepolia.etherscan.io on * when it is omitted: etherscan.io on mainnet, sepolia.etherscan.io on
* Sepolia. * Sepolia.
* *
* While the stored "Check recipients on Etherscan" is off, Etherscan is not * Etherscan is asked only while "Check recipients on Etherscan" is on in the
* asked, and an answer that lands after it went off is dropped * stored profile. The page's own copy of the setting is not used, because
* (src/shared/storedSettings.js). Either way the answer is null. * another page may have just changed it. The stored setting is read through
* normalizePersisted(), so it is on only when a load would find it on. While
* it is off, the answer is null.
* *
* @param {string} address - Ethereum address to check. * @param {string} address - Ethereum address to check.
* @param {string} [chainId] - Chain id of the network to look it up on. * @param {string} [chainId] - Chain id of the network to look it up on.
@@ -89,7 +92,8 @@ function parseEtherscanPage(html) {
*/ */
async function checkEtherscanLabel(address, chainId) { async function checkEtherscanLabel(address, chainId) {
try { try {
if (!(await storedSettingIsOn("checkRecipientsOnEtherscan"))) { const { autistmask } = await storageGet("autistmask");
if (!normalizePersisted(autistmask).checkRecipientsOnEtherscan) {
return null; return null;
} }
// Lazy require to avoid pulling in chrome.storage at module scope // Lazy require to avoid pulling in chrome.storage at module scope
@@ -102,9 +106,6 @@ async function checkEtherscanLabel(address, chainId) {
}); });
if (!resp.ok) return null; if (!resp.ok) return null;
const html = await resp.text(); const html = await resp.text();
if (!(await storedSettingIsOn("checkRecipientsOnEtherscan"))) {
return null;
}
const result = parseEtherscanPage(html); const result = parseEtherscanPage(html);
if (result.isPhishing) { if (result.isPhishing) {
return { return {
@@ -116,6 +117,7 @@ async function checkEtherscanLabel(address, chainId) {
return null; return null;
} catch { } catch {
// Network errors are expected — Etherscan may rate-limit or block. // Network errors are expected — Etherscan may rate-limit or block.
// A failed storage read lands here too, before Etherscan is asked.
return null; return null;
} }
} }
-24
View File
@@ -1,24 +0,0 @@
// Reads "Use Blockscout" or "Check recipients on Etherscan" from the stored
// profile.
//
// A request that either setting gates reads it here twice: before it asks, and
// again when the answer lands. While the setting is off, nothing is asked, and
// an answer that lands is dropped. A page's own copy of the setting is not
// used, because another page may have just changed it.
const { storageGet } = require("./browserApi");
const { normalizePersisted } = require("./persistedState");
// Whether `name`, "useBlockscout" or "checkRecipientsOnEtherscan", is on in the
// stored profile. The profile is read through normalizePersisted(), as a load
// reads it, so only a stored true is on. A failed read counts as off.
async function storedSettingIsOn(name) {
try {
const { autistmask } = await storageGet("autistmask");
return normalizePersisted(autistmask)[name];
} catch {
return false;
}
}
module.exports = { storedSettingIsOn };
+4 -6
View File
@@ -21,7 +21,6 @@ const { toDecimals } = require("./transferAmount");
// detail view is the authoritative record and already shows exact precision. // detail view is the authoritative record and already shows exact precision.
const { truncateAmount: formatTxValue } = require("./amountDisplay"); const { truncateAmount: formatTxValue } = require("./amountDisplay");
const { routerRelease } = require("./uniswap"); const { routerRelease } = require("./uniswap");
const { storedSettingIsOn } = require("./storedSettings");
// Ethereum addresses are case-insensitive: EIP-55 mixed case is a checksum // Ethereum addresses are case-insensitive: EIP-55 mixed case is a checksum
// over the address, not part of its identity. Every address comparison in // over the address, not part of its identity. Every address comparison in
@@ -237,16 +236,16 @@ function mergeTransactions(txs, tokenTransfers) {
// carries it, and a native entry is labelled with that network's // carries it, and a native entry is labelled with that network's
// `nativeCurrency` from networks.js (`ETH`, `SepoliaETH`). // `nativeCurrency` from networks.js (`ETH`, `SepoliaETH`).
// //
// While the stored "Use Blockscout" is off, Blockscout is not asked, and an // `useBlockscout` is the user's "Use Blockscout" setting. While it is off,
// answer that lands after it went off is dropped (src/shared/storedSettings.js). // Blockscout is not asked and the answer is an empty list.
// Either way the answer is an empty list.
async function fetchRecentTransactions( async function fetchRecentTransactions(
address, address,
blockscoutUrl, blockscoutUrl,
useBlockscout,
chainId, chainId,
count = 25, count = 25,
) { ) {
if (!(await storedSettingIsOn("useBlockscout"))) return []; if (!useBlockscout) return [];
log.debugf("fetchRecentTransactions", address); log.debugf("fetchRecentTransactions", address);
const addrLower = normalizeAddress(address); const addrLower = normalizeAddress(address);
@@ -277,7 +276,6 @@ async function fetchRecentTransactions(
const txJson = txResp.ok ? await txResp.json() : {}; const txJson = txResp.ok ? await txResp.json() : {};
const ttJson = ttResp.ok ? await ttResp.json() : {}; const ttJson = ttResp.ok ? await ttResp.json() : {};
if (!(await storedSettingIsOn("useBlockscout"))) return [];
const txs = mergeTransactions( const txs = mergeTransactions(
(txJson.items || []).map((tx) => parseTx(tx, addrLower, chainId)), (txJson.items || []).map((tx) => parseTx(tx, addrLower, chainId)),
+20 -6
View File
@@ -412,7 +412,14 @@ describe("a balance refresh under a network switch", () => {
const ranOn = []; const ranOn = [];
const bg = loadWorker("sepolia", { const bg = loadWorker("sepolia", {
refreshBalances: async (wallets, rpcUrl, explorer, tokens, id) => { refreshBalances: async (
wallets,
rpcUrl,
explorer,
useBlockscout,
tokens,
id,
) => {
ranOn.push(id); ranOn.push(id);
reachedNetwork.resolve(); reachedNetwork.resolve();
await roundTrip.promise; await roundTrip.promise;
@@ -448,16 +455,22 @@ describe("a balance refresh under a network switch", () => {
describe("a balance refresh under a change of Use Blockscout", () => { describe("a balance refresh under a change of Use Blockscout", () => {
// Turning "Use Blockscout" off drops the tokens the user has not tracked // Turning "Use Blockscout" off drops the tokens the user has not tracked
// (src/popup/views/settings.js). Blockscout may have answered a refresh // (src/popup/views/settings.js). A refresh that started while it was on
// just before. That refresh still saves its balances when it returns, but // still saves its balances when it returns, but not those tokens. The same
// not those tokens. The same refresh in the popup is in // refresh in the popup is in tests/blockscoutEtherscanSettings.test.js.
// tests/blockscoutEtherscanSettings.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 its balances without the untracked token", async () => {
const roundTrip = deferred(); const roundTrip = deferred();
const started = deferred(); const started = deferred();
const handed = [];
const bg = loadWorker("mainnet", { const bg = loadWorker("mainnet", {
refreshBalances: async (wallets) => { refreshBalances: async (
wallets,
rpcUrl,
explorer,
useBlockscout,
) => {
handed.push(useBlockscout);
started.resolve(); started.resolve();
await roundTrip.promise; await roundTrip.promise;
wallets[0].addresses[0].balance = "7.0"; wallets[0].addresses[0].balance = "7.0";
@@ -491,6 +504,7 @@ describe("a balance refresh under a change of Use Blockscout", () => {
roundTrip.resolve(); roundTrip.resolve();
await refresh; await refresh;
expect(handed).toEqual([true]);
const after = bg.persisted(); const after = bg.persisted();
expect(after.wallets[0].addresses[0].balance).toBe("7.0"); expect(after.wallets[0].addresses[0].balance).toBe("7.0");
expect(after.wallets[0].addresses[0].tokenBalances).toEqual([]); expect(after.wallets[0].addresses[0].tokenBalances).toEqual([]);
+1 -6
View File
@@ -24,12 +24,6 @@ const TRACKED = {
decimals: 6, decimals: 6,
}; };
// Blockscout is asked only while the stored "Use Blockscout" is on.
const { makeStorageStub } = require("./support/storageStub");
globalThis.chrome = {
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
};
const realFetch = globalThis.fetch; const realFetch = globalThis.fetch;
let logged; let logged;
@@ -60,6 +54,7 @@ function refresh(signal) {
wallets, wallets,
RPC_URL, RPC_URL,
EXPLORER_URL, EXPLORER_URL,
true,
[TRACKED], [TRACKED],
"mainnet", "mainnet",
signal, signal,
+1 -1
View File
@@ -91,7 +91,7 @@ test("a refresh that returns after a switch in Settings writes nothing, and the
await page.tick(); await page.tick();
expect(refreshBalances.mock.lastCall[4]).toBe("sepolia"); expect(refreshBalances.mock.lastCall[5]).toBe("sepolia");
expect(page.storage.read("autistmask").lastBalanceRefresh).toBeGreaterThan( expect(page.storage.read("autistmask").lastBalanceRefresh).toBeGreaterThan(
0, 0,
); );
+130 -185
View File
@@ -7,10 +7,9 @@
// the Blockscout URL in Settings. It asks that URL only for `/stats` and sends // the Blockscout URL in Settings. It asks that URL only for `/stats` and sends
// none of the user's data. // none of the user's data.
// //
// A request that either setting gates reads the stored setting, not the page's // The balance refresh and the Etherscan check follow the stored setting, not
// copy, before it asks and again when the answer lands. So a change another // the page's copy of it. So a change another page saved applies to them at
// page saved applies to it at once. An open page also takes such a change and // once. An open page also takes such a change and draws its screens again.
// draws its screens again.
// //
// Only Blockscout reports the tokens the user has not tracked. While "Use // Only Blockscout reports the tokens the user has not tracked. While "Use
// Blockscout" is off, no address keeps them and no screen lists them. // Blockscout" is off, no address keeps them and no screen lists them.
@@ -41,7 +40,6 @@ const {
} = require("../src/shared/balances"); } = require("../src/shared/balances");
const { fetchRecentTransactions } = require("../src/shared/transactions"); const { fetchRecentTransactions } = require("../src/shared/transactions");
const { getFullWarnings } = require("../src/shared/addressWarnings"); const { getFullWarnings } = require("../src/shared/addressWarnings");
const { checkEtherscanLabel } = require("../src/shared/etherscanLabels");
const { HISTORY_OFF_TEXT, escapeHtml } = require("../src/popup/views/helpers"); const { HISTORY_OFF_TEXT, escapeHtml } = require("../src/popup/views/helpers");
const RPC_URL = "https://rpc.example.invalid"; const RPC_URL = "https://rpc.example.invalid";
@@ -63,25 +61,6 @@ const USDC_ROW = {
}, },
}; };
// A transfer of 1 ETH from RECIPIENT to ADDRESS, as Blockscout lists it in
// ADDRESS's history.
const HISTORY_ITEM = {
hash: TX_HASH,
block_number: 21000000,
timestamp: "2026-02-27T12:00:00.000Z",
from: { hash: RECIPIENT },
to: { hash: ADDRESS, is_contract: false },
value: "1000000000000000000",
method: null,
status: "ok",
};
// Etherscan's page for RECIPIENT, which labels it a phishing address.
const PHISHING_PAGE =
"<html><head><title>Fake_Phishing1 | Address: " +
RECIPIENT +
" | Etherscan</title></head><body></body></html>";
const realFetch = globalThis.fetch; const realFetch = globalThis.fetch;
afterEach(() => { afterEach(() => {
@@ -112,55 +91,33 @@ describe("where each request is made", () => {
// Every URL fetch() was asked for, in order. // Every URL fetch() was asked for, in order.
let asked; let asked;
// What Blockscout and Etherscan answer: USDC among ADDRESS's tokens,
// HISTORY_ITEM in its history, and PHISHING_PAGE for RECIPIENT.
function answer(url) {
return {
ok: true,
status: 200,
json: async () => {
if (url.endsWith("/token-balances")) return [USDC_ROW];
if (url.endsWith("/transactions")) {
return { items: [HISTORY_ITEM] };
}
return { items: [] };
},
text: async () => PHISHING_PAGE,
};
}
async function recordAndAnswer(url) {
asked.push(String(url));
return answer(String(url));
}
beforeEach(() => { beforeEach(() => {
asked = []; asked = [];
globalThis.fetch = jest.fn(recordAndAnswer); globalThis.fetch = jest.fn(async (url) => {
asked.push(String(url));
return {
ok: true,
status: 200,
json: async () =>
String(url).endsWith("/token-balances")
? [USDC_ROW]
: { items: [] },
text: async () => "<html><body></body></html>",
};
});
}); });
afterEach(() => { afterEach(() => {
globalThis.fetch = realFetch; globalThis.fetch = realFetch;
delete globalThis.chrome;
}); });
// Storage holds `profile` as the stored profile, or nothing when it is
// undefined.
function storing(profile) {
globalThis.chrome = {
storage: makeStorageStub(
profile === undefined ? {} : { autistmask: profile },
),
};
}
test("Blockscout's token balances are asked for only while Use Blockscout is on", async () => { test("Blockscout's token balances are asked for only while Use Blockscout is on", async () => {
storing({ useBlockscout: false }); expect(
expect(await fetchTokenBalances(ADDRESS, BLOCKSCOUT, [])).toEqual([]); await fetchTokenBalances(ADDRESS, BLOCKSCOUT, false, []),
).toEqual([]);
expect(asked).toEqual([]); expect(asked).toEqual([]);
storing({ useBlockscout: true }); const rows = await fetchTokenBalances(ADDRESS, BLOCKSCOUT, true, []);
const rows = await fetchTokenBalances(ADDRESS, BLOCKSCOUT, []);
expect(rows.map((row) => row.symbol)).toEqual(["USDC"]); expect(rows.map((row) => row.symbol)).toEqual(["USDC"]);
expect(asked).toEqual([ expect(asked).toEqual([
BLOCKSCOUT + "/addresses/" + ADDRESS + "/token-balances", BLOCKSCOUT + "/addresses/" + ADDRESS + "/token-balances",
@@ -168,14 +125,12 @@ describe("where each request is made", () => {
}); });
test("the transaction history is asked for only while Use Blockscout is on", async () => { test("the transaction history is asked for only while Use Blockscout is on", async () => {
storing({ useBlockscout: false });
expect( expect(
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1"), await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, false, "0x1"),
).toEqual([]); ).toEqual([]);
expect(asked).toEqual([]); expect(asked).toEqual([]);
storing({ useBlockscout: true }); await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, true, "0x1");
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1");
expect(asked.sort()).toEqual([ expect(asked.sort()).toEqual([
BLOCKSCOUT + BLOCKSCOUT +
"/addresses/" + "/addresses/" +
@@ -191,78 +146,26 @@ describe("where each request is made", () => {
getTransactionCount: async () => 1, getTransactionCount: async () => 1,
getBalance: async () => 0n, getBalance: async () => 0n,
}; };
const checkWith = async (profile) => { const checkWith = async (stored) => {
storing(profile); globalThis.chrome = { storage: makeStorageStub(stored) };
await getFullWarnings(RECIPIENT, provider, { chainId: "0x1" }); try {
await getFullWarnings(RECIPIENT, provider, { chainId: "0x1" });
} finally {
delete globalThis.chrome;
}
}; };
await checkWith(undefined); await checkWith({});
await checkWith({ checkRecipientsOnEtherscan: false }); await checkWith({ autistmask: { checkRecipientsOnEtherscan: false } });
// Only a stored true is on, as when the profile is loaded. // Only a stored true is on, as when the profile is loaded.
await checkWith({ checkRecipientsOnEtherscan: "true" }); await checkWith({ autistmask: { checkRecipientsOnEtherscan: "true" } });
await checkWith({ checkRecipientsOnEtherscan: 42 }); await checkWith({ autistmask: { checkRecipientsOnEtherscan: 42 } });
expect(asked).toEqual([]); expect(asked).toEqual([]);
await checkWith({ checkRecipientsOnEtherscan: true }); await checkWith({ autistmask: { checkRecipientsOnEtherscan: true } });
expect(asked).toEqual(["https://etherscan.io/address/" + RECIPIENT]); expect(asked).toEqual(["https://etherscan.io/address/" + RECIPIENT]);
}); });
// Asks with `setting` on in storage. Once every request is out, storage
// holds `after` for it, as when the user changes it here or on another
// page. Only then does each request get its answer.
async function answeredAfter(setting, after, ask) {
storing({ [setting]: true });
let release;
const held = new Promise((resolve) => {
release = resolve;
});
let requested;
const out = new Promise((resolve) => {
requested = resolve;
});
globalThis.fetch = jest.fn(async (url) => {
requested();
await held;
return recordAndAnswer(url);
});
const result = ask();
await out;
globalThis.chrome.storage.write("autistmask", { [setting]: after });
release();
return result;
}
// Each request, the setting that gates it, how to make it, and its answer
// while that setting is off.
test.each([
[
"Blockscout's token balances",
"useBlockscout",
() => fetchTokenBalances(ADDRESS, BLOCKSCOUT, []),
[],
],
[
"the transaction history",
"useBlockscout",
() => fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1"),
[],
],
[
"the Etherscan label check",
"checkRecipientsOnEtherscan",
() => checkEtherscanLabel(RECIPIENT, "0x1"),
null,
],
])(
"%s keeps an answer that lands while its setting is on, and drops one that lands after it went off",
async (_what, setting, ask, dropped) => {
expect(await answeredAfter(setting, true, ask)).not.toEqual(
dropped,
);
expect(await answeredAfter(setting, false, ask)).toEqual(dropped);
},
);
describe("a balance refresh", () => { describe("a balance refresh", () => {
// The RPC endpoint answers every balance with zero, and every other // The RPC endpoint answers every balance with zero, and every other
// call, the ENS reverse lookup's, with no resolver. // call, the ENS reverse lookup's, with no resolver.
@@ -309,11 +212,11 @@ describe("where each request is made", () => {
} }
async function refresh(addr, useBlockscout) { async function refresh(addr, useBlockscout) {
storing({ useBlockscout });
await refreshBalances( await refreshBalances(
[{ addresses: [addr] }], [{ addresses: [addr] }],
RPC_URL, RPC_URL,
BLOCKSCOUT, BLOCKSCOUT,
useBlockscout,
[], [],
"mainnet", "mainnet",
); );
@@ -355,6 +258,29 @@ describe("the popup", () => {
jest.spyOn(console, "error").mockImplementation(() => {}); jest.spyOn(console, "error").mockImplementation(() => {});
}); });
test("hands its balance refresh and Home's history the stored Use Blockscout setting", async () => {
await bootPopup(unversionedValidProfile());
const {
refreshBalances: popupRefresh,
} = require("../src/shared/balances");
const {
fetchRecentTransactions: popupHistory,
} = require("../src/shared/transactions");
expect(popupRefresh.mock.calls[0][3]).toBe(false);
expect(popupHistory.mock.calls[0][2]).toBe(false);
cleanupPopup();
await bootPopup(unversionedValidProfile({ useBlockscout: true }));
const {
refreshBalances: onRefresh,
} = require("../src/shared/balances");
const {
fetchRecentTransactions: onHistory,
} = require("../src/shared/transactions");
expect(onRefresh.mock.calls[0][3]).toBe(true);
expect(onHistory.mock.calls[0][2]).toBe(true);
});
test("Home says why there is no history and only tracked tokens while Use Blockscout is off", async () => { test("Home says why there is no history and only tracked tokens while Use Blockscout is off", async () => {
const page = await bootPopup(unversionedValidProfile()); const page = await bootPopup(unversionedValidProfile());
expect(page.node("home-tx-list").innerHTML).toContain( expect(page.node("home-tx-list").innerHTML).toContain(
@@ -415,51 +341,6 @@ describe("the popup", () => {
expect(on.hidden("tx-detail-blockscout-note")).toBe(true); expect(on.hidden("tx-detail-blockscout-note")).toBe(true);
}); });
// Another page turns "Use Blockscout" off while TransactionDetail's request
// to Blockscout is out. This page draws TransactionDetail again with its
// line, and only then does Blockscout answer.
test("TransactionDetail shows nothing from an answer that lands after another page turned Use Blockscout off", async () => {
const page = await bootPopup(
onTransactionScreen({ useBlockscout: true }),
);
let release;
const held = new Promise((resolve) => {
release = resolve;
});
globalThis.fetch = jest.fn(async () => {
await held;
return {
ok: true,
status: 200,
json: async () => ({
block_number: 21000000,
fee: { value: "21000000000000" },
raw_input: "0x12345678",
method: "doSomething",
}),
};
});
require("../src/popup/views/transactionDetail").render();
await page.settle();
expect(fetched()).toEqual([BLOCKSCOUT + "/transactions/" + TX_HASH]);
await anotherPageAnnounces(page, { useBlockscout: false });
release();
await page.settle();
expect(page.visibleViews()).toEqual(["transaction"]);
expect(page.hidden("tx-detail-blockscout-note")).toBe(false);
for (const id of [
"tx-detail-block-section",
"tx-detail-network-section",
"tx-detail-calldata-section",
"tx-detail-rawdata-section",
]) {
expect(page.hidden(id)).toBe(true);
}
expect(fetched()).toHaveLength(1);
});
// The confirmation screen for a send of 0.1 ETH to RECIPIENT, as a // The confirmation screen for a send of 0.1 ETH to RECIPIENT, as a
// reopened popup draws it. // reopened popup draws it.
function onConfirmScreen(extra) { function onConfirmScreen(extra) {
@@ -747,22 +628,24 @@ describe("the popup", () => {
["off", true, false], ["off", true, false],
["on", false, true], ["on", false, true],
])( ])(
"after another page turns Use Blockscout %s, this page takes the change and its next refresh is saved", "after another page turns Use Blockscout %s, this page's next refresh follows the stored setting and is saved",
async (_how, before, after) => { async (_how, before, after) => {
const page = await bootPopup(onAddressDetail(before)); const page = await bootPopup(onAddressDetail(before));
// The refresh answers as it does under the stored setting.
const { refreshBalances: held } = require("../src/shared/balances"); const { refreshBalances: held } = require("../src/shared/balances");
held.mockImplementationOnce(async (wallets) => { held.mockImplementationOnce(
const addr = wallets[0].addresses[0]; async (wallets, rpcUrl, blockscoutUrl, useBlockscout) => {
addr.balance = "7.0"; const addr = wallets[0].addresses[0];
addr.tokenBalances = after addr.balance = "7.0";
? [tokenRow(TRACKED, "TRK"), tokenRow(UNTRACKED, "TST")] addr.tokenBalances = useBlockscout
: [tokenRow(TRACKED, "TRK")]; ? [tokenRow(TRACKED, "TRK"), tokenRow(UNTRACKED, "TST")]
}); : [tokenRow(TRACKED, "TRK")];
},
);
anotherPageSaves(page, { useBlockscout: after }); anotherPageSaves(page, { useBlockscout: after });
await page.tick(); await page.tick();
expect(held.mock.calls.at(-1)[3]).toBe(after);
expect( expect(
page.storage.read("autistmask").wallets[0].addresses[0].balance, page.storage.read("autistmask").wallets[0].addresses[0].balance,
).toBe("7.0"); ).toBe("7.0");
@@ -839,3 +722,65 @@ describe("the popup", () => {
expect(page.hidden("confirm-etherscan-note")).toBe(false); expect(page.hidden("confirm-etherscan-note")).toBe(false);
}); });
}); });
describe("the background", () => {
afterEach(() => {
delete globalThis.chrome;
jest.dontMock("../src/shared/balances");
});
// Starts the background over `profile`, fires its balance alarm, and
// answers with the Use Blockscout setting its refresh was handed.
async function settingTheRefreshIsHanded(profile) {
jest.resetModules();
let handed;
const called = new Promise((resolve) => {
handed = resolve;
});
jest.doMock("../src/shared/balances", () => ({
getProvider: () => ({}),
refreshBalances: async (...args) => handed(args[3]),
dropUntrackedTokens: () => {},
}));
let onAlarm = null;
globalThis.chrome = {
storage: makeStorageStub({ autistmask: profile }),
alarms: {
create: () => {},
get: async () => undefined,
clear: async () => false,
onAlarm: {
addListener: (fn) => {
onAlarm = fn;
},
},
},
runtime: {
onMessage: { addListener: () => {} },
onConnect: { addListener: () => {} },
onInstalled: { addListener: () => {} },
onStartup: { addListener: () => {} },
getURL: (p) => "chrome-extension://test/" + p,
lastError: null,
},
windows: { onRemoved: { addListener: () => {} } },
tabs: { query: () => {}, sendMessage: () => {} },
action: { setPopup: () => {} },
};
require("../src/background/index");
const { BALANCE_REFRESH_ALARM } = require("../src/shared/alarms");
onAlarm({ name: BALANCE_REFRESH_ALARM });
return called;
}
test("hands its balance refresh the stored Use Blockscout setting", async () => {
const profile = { ...unversionedValidProfile(), lastBalanceRefresh: 0 };
expect(await settingTheRefreshIsHanded(profile)).toBe(false);
expect(
await settingTheRefreshIsHanded({
...profile,
useBlockscout: true,
}),
).toBe(true);
});
});
+2 -7
View File
@@ -108,10 +108,7 @@ global.fetch = jest.fn(() => {
}); });
const { makeStorageStub } = require("./support/storageStub"); const { makeStorageStub } = require("./support/storageStub");
global.chrome = { global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
runtime: { sendMessage() {} },
};
// A stub DOM: every id resolves to a recording element. // A stub DOM: every id resolves to a recording element.
const elements = new Map(); const elements = new Map();
@@ -246,13 +243,11 @@ async function openSend(token, tokenUnits = null) {
]; ];
state.selectedWallet = 0; state.selectedWallet = 0;
state.selectedAddress = 0; state.selectedAddress = 0;
// Blockscout is asked only while the stored "Use Blockscout" is on. The
// screens save this page's copy of it over the stored one.
state.useBlockscout = true;
await refreshBalances( await refreshBalances(
state.wallets, state.wallets,
"https://rpc.example.invalid", "https://rpc.example.invalid",
"https://blockscout.example/api/v2", "https://blockscout.example/api/v2",
true,
state.trackedTokens, state.trackedTokens,
"mainnet", "mainnet",
); );
+2 -4
View File
@@ -35,11 +35,8 @@ global.fetch = jest.fn(() => {
throw new Error("tests must not perform network requests"); throw new Error("tests must not perform network requests");
}); });
// Blockscout is asked only while the stored "Use Blockscout" is on.
const { makeStorageStub } = require("./support/storageStub"); const { makeStorageStub } = require("./support/storageStub");
global.chrome = { global.chrome = { storage: makeStorageStub() };
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
};
const { AbiCoder, Interface } = require("ethers"); const { AbiCoder, Interface } = require("ethers");
const { ERC20_ABI } = require("../src/shared/constants"); const { ERC20_ABI } = require("../src/shared/constants");
@@ -117,6 +114,7 @@ async function fetchOnto(items, trackedTokens = []) {
const balances = await fetchTokenBalances( const balances = await fetchTokenBalances(
HOLDER, HOLDER,
BLOCKSCOUT, BLOCKSCOUT,
true,
trackedTokens, trackedTokens,
); );
state.trackedTokens = trackedTokens; state.trackedTokens = trackedTokens;
+10 -10
View File
@@ -21,11 +21,7 @@ jest.mock("../src/shared/log", () => ({
global.fetch = jest.fn(() => { global.fetch = jest.fn(() => {
throw new Error("tests must not perform network requests"); throw new Error("tests must not perform network requests");
}); });
// Blockscout is asked only while the stored "Use Blockscout" is on. global.chrome = { storage: { local: {} } };
const { makeStorageStub } = require("./support/storageStub");
global.chrome = {
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
};
const { const {
LOW_HOLDER_THRESHOLD, LOW_HOLDER_THRESHOLD,
@@ -154,19 +150,23 @@ describe("the balance-list spam gate", () => {
test("a token with plenty of reported holders is listed", async () => { test("a token with plenty of reported holders is listed", async () => {
respondWith([item()]); respondWith([item()]);
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []); const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
expect(balances).toHaveLength(1); expect(balances).toHaveLength(1);
expect(balances[0].holders).toBe(50000); expect(balances[0].holders).toBe(50000);
}); });
test("a token reporting zero holders is still excluded", async () => { test("a token reporting zero holders is still excluded", async () => {
respondWith([item({ token: { holders_count: "0" } })]); respondWith([item({ token: { holders_count: "0" } })]);
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]); expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
[],
);
}); });
test("an unknown holder count does not admit an unvouched token", async () => { test("an unknown holder count does not admit an unvouched token", async () => {
respondWith([item({ token: { holders_count: null } })]); respondWith([item({ token: { holders_count: null } })]);
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]); expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
[],
);
}); });
// The path that reaches the send selector and the history filter: a token // The path that reaches the send selector and the history filter: a token
@@ -175,7 +175,7 @@ describe("the balance-list spam gate", () => {
// zero that would then hide it downstream. // zero that would then hide it downstream.
test("a tracked token with an unknown count is listed with holders null", async () => { test("a tracked token with an unknown count is listed with holders null", async () => {
respondWith([item({ token: { holders_count: undefined } })]); respondWith([item({ token: { holders_count: undefined } })]);
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, [ const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [
{ address: NOVEL_TOKEN.toUpperCase() }, { address: NOVEL_TOKEN.toUpperCase() },
]); ]);
expect(balances).toHaveLength(1); expect(balances).toHaveLength(1);
@@ -192,7 +192,7 @@ describe("the balance-list spam gate", () => {
}, },
}), }),
]); ]);
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []); const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
expect(balances).toHaveLength(1); expect(balances).toHaveLength(1);
expect(balances[0].holders).toBeNull(); expect(balances[0].holders).toBeNull();
}); });
+1 -6
View File
@@ -436,13 +436,8 @@ describe.each([
}); });
test("the transaction detail screen's type line and fee", async () => { test("the transaction detail screen's type line and fee", async () => {
// The fee is Blockscout's, asked for only while the stored "Use // The fee is Blockscout's.
// Blockscout" is on.
state.useBlockscout = true; state.useBlockscout = true;
global.chrome.storage.write("autistmask", {
...global.chrome.storage.read("autistmask"),
useBlockscout: true,
});
debugFetch.mockImplementationOnce(async () => ({ debugFetch.mockImplementationOnce(async () => ({
ok: true, ok: true,
status: 200, status: 200,
+1 -1
View File
@@ -87,7 +87,7 @@ test("the popup open on AddressDetail shows the new network and its balances", a
// Its next refresh runs on Sepolia, and what it fetched is stored. // Its next refresh runs on Sepolia, and what it fetched is stored.
await page.tick(); await page.tick();
const { refreshBalances } = require("../src/shared/balances"); const { refreshBalances } = require("../src/shared/balances");
expect(refreshBalances.mock.lastCall[4]).toBe("sepolia"); expect(refreshBalances.mock.lastCall[5]).toBe("sepolia");
const stored = page.storage.read("autistmask"); const stored = page.storage.read("autistmask");
expect(stored.networkId).toBe("sepolia"); expect(stored.networkId).toBe("sepolia");
expect(stored.lastBalanceRefresh).toBeGreaterThan(0); expect(stored.lastBalanceRefresh).toBeGreaterThan(0);
+1
View File
@@ -88,6 +88,7 @@ test("the balance refresh", async () => {
wallets, wallets,
RPC_URL, RPC_URL,
"https://explorer.example.invalid/api/v2", "https://explorer.example.invalid/api/v2",
true,
[], [],
"mainnet", "mainnet",
); );
+2 -4
View File
@@ -90,7 +90,7 @@ let approvalDetails = null;
const { makeStorageStub } = require("./support/storageStub"); const { makeStorageStub } = require("./support/storageStub");
global.chrome = { global.chrome = {
storage: makeStorageStub({ autistmask: { useBlockscout: true } }), storage: makeStorageStub(),
runtime: { runtime: {
connect: () => ({ connect: () => ({
postMessage() {}, postMessage() {},
@@ -220,13 +220,11 @@ async function refreshWith(balanceWei, tokenItems = []) {
state.wallets = [{ name: "Wallet 1", addresses: [{ address: HOLDER }] }]; state.wallets = [{ name: "Wallet 1", addresses: [{ address: HOLDER }] }];
state.selectedWallet = 0; state.selectedWallet = 0;
state.selectedAddress = 0; state.selectedAddress = 0;
// Blockscout is asked only while the stored "Use Blockscout" is on. The
// screens save this page's copy of it over the stored one.
state.useBlockscout = true;
await refreshBalances( await refreshBalances(
state.wallets, state.wallets,
"https://rpc.example.invalid", "https://rpc.example.invalid",
"https://blockscout.example/api/v2", "https://blockscout.example/api/v2",
true,
state.trackedTokens, state.trackedTokens,
"mainnet", "mainnet",
); );
+2 -7
View File
@@ -100,10 +100,7 @@ global.fetch = jest.fn(() => {
}); });
const { makeStorageStub } = require("./support/storageStub"); const { makeStorageStub } = require("./support/storageStub");
global.chrome = { global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
runtime: { sendMessage() {} },
};
// A stub DOM: every id resolves to a recording element. // A stub DOM: every id resolves to a recording element.
const elements = new Map(); const elements = new Map();
@@ -216,13 +213,11 @@ async function refreshWith(balanceWei, tokenItems = []) {
]; ];
state.selectedWallet = 0; state.selectedWallet = 0;
state.selectedAddress = 0; state.selectedAddress = 0;
// Blockscout is asked only while the stored "Use Blockscout" is on. The
// screens save this page's copy of it over the stored one.
state.useBlockscout = true;
await refreshBalances( await refreshBalances(
state.wallets, state.wallets,
"https://rpc.example.invalid", "https://rpc.example.invalid",
"https://blockscout.example/api/v2", "https://blockscout.example/api/v2",
true,
state.trackedTokens, state.trackedTokens,
"mainnet", "mainnet",
); );
+24 -17
View File
@@ -52,14 +52,8 @@ jest.mock("../src/shared/log", () => ({
global.fetch = jest.fn(() => { global.fetch = jest.fn(() => {
throw new Error("tests must not perform network requests"); throw new Error("tests must not perform network requests");
}); });
// Blockscout is asked only while the stored "Use Blockscout" is on.
global.chrome = { global.chrome = {
storage: { storage: { local: { get: async () => ({}), set: async () => {} } },
local: {
get: async () => ({ autistmask: { useBlockscout: true } }),
set: async () => {},
},
},
}; };
const { isSpoofedSymbol } = require("../src/shared/symbolSpoof"); const { isSpoofedSymbol } = require("../src/shared/symbolSpoof");
@@ -592,12 +586,14 @@ describe("surface 3: the balance list", () => {
// 1,000-holder floor and was listed as a holding named ETH. // 1,000-holder floor and was listed as a holding named ETH.
test("a fake ETH token clearing the holder floor is filtered", async () => { test("a fake ETH token clearing the holder floor is filtered", async () => {
respondWith([fakeEthItem()]); respondWith([fakeEthItem()]);
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]); expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
[],
);
}); });
test("tracking the fake token manually does not admit it either", async () => { test("tracking the fake token manually does not admit it either", async () => {
respondWith([fakeEthItem({ holders_count: "0" })]); respondWith([fakeEthItem({ holders_count: "0" })]);
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, [ const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [
{ address: FAKE_ETH_CONTRACT }, { address: FAKE_ETH_CONTRACT },
]); ]);
expect(balances).toEqual([]); expect(balances).toEqual([]);
@@ -607,12 +603,16 @@ describe("surface 3: the balance list", () => {
// their belief about what they own, and it renders the symbol into HTML. // their belief about what they own, and it renders the symbol into HTML.
test("a padded fake ETH token is filtered too", async () => { test("a padded fake ETH token is filtered too", async () => {
respondWith([fakeEthItem({ symbol: " ETH " })]); respondWith([fakeEthItem({ symbol: " ETH " })]);
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]); expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
[],
);
}); });
test("a fake ETH token padded with a no-break space is filtered", async () => { test("a fake ETH token padded with a no-break space is filtered", async () => {
respondWith([fakeEthItem({ symbol: NBSP + "ETH" + NBSP })]); respondWith([fakeEthItem({ symbol: NBSP + "ETH" + NBSP })]);
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]); expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
[],
);
}); });
// The false-positive direction on the surface that matters most: a real // The false-positive direction on the surface that matters most: a real
@@ -627,7 +627,7 @@ describe("surface 3: the balance list", () => {
decimals: "6", decimals: "6",
}), }),
]); ]);
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []); const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
expect(balances).toHaveLength(1); expect(balances).toHaveLength(1);
expect(balances[0].symbol).toBe(" USDC "); expect(balances[0].symbol).toBe(" USDC ");
}); });
@@ -641,7 +641,7 @@ describe("surface 3: the balance list", () => {
decimals: "6", decimals: "6",
}), }),
]); ]);
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []); const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
expect(balances).toHaveLength(1); expect(balances).toHaveLength(1);
expect(balances[0].symbol).toBe("USDC"); expect(balances[0].symbol).toBe("USDC");
}); });
@@ -652,7 +652,9 @@ describe("surface 3: the balance list", () => {
// before the symbol rule is consulted. // before the symbol rule is consulted.
test("a non-ERC-20 row claiming ETH never reaches the symbol rule", async () => { test("a non-ERC-20 row claiming ETH never reaches the symbol rule", async () => {
respondWith([fakeEthItem({ type: "ERC-721" })]); respondWith([fakeEthItem({ type: "ERC-721" })]);
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]); expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
[],
);
}); });
// The adjacent finding from the same review as issue #260: the type gate // The adjacent finding from the same review as issue #260: the type gate
@@ -670,16 +672,20 @@ describe("surface 3: the balance list", () => {
decimals: "6", decimals: "6",
}), }),
]); ]);
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []); const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
expect(balances).toHaveLength(1); expect(balances).toHaveLength(1);
expect(balances[0].symbol).toBe("USDC"); expect(balances[0].symbol).toBe("USDC");
}); });
test("case insensitivity does not admit another token type", async () => { test("case insensitivity does not admit another token type", async () => {
respondWith([fakeEthItem({ type: "erc-721" })]); respondWith([fakeEthItem({ type: "erc-721" })]);
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]); expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
[],
);
respondWith([fakeEthItem({ type: "ERC-20-EXTRA" })]); respondWith([fakeEthItem({ type: "ERC-20-EXTRA" })]);
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]); expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
[],
);
}); });
// The money test: the user holds real ETH and has been airdropped a fake // The money test: the user holds real ETH and has been airdropped a fake
@@ -692,6 +698,7 @@ describe("surface 3: the balance list", () => {
[{ addresses: [addr] }], [{ addresses: [addr] }],
"https://rpc.example.invalid", "https://rpc.example.invalid",
BLOCKSCOUT, BLOCKSCOUT,
true,
[], [],
"mainnet", "mainnet",
); );
+5 -7
View File
@@ -38,14 +38,8 @@ jest.mock("../src/shared/log", () => ({
isDebug: () => false, isDebug: () => false,
})); }));
// Blockscout is asked only while the stored "Use Blockscout" is on.
globalThis.chrome = { globalThis.chrome = {
storage: { storage: { local: { get: async () => ({}), set: async () => {} } },
local: {
get: async () => ({ autistmask: { useBlockscout: true } }),
set: async () => {},
},
},
}; };
const { state } = require("../src/shared/state"); const { state } = require("../src/shared/state");
@@ -184,6 +178,7 @@ test("a token transfer's decoded details name the token contract and the amount
const [row] = await fetchRecentTransactions( const [row] = await fetchRecentTransactions(
HOLDER, HOLDER,
EXPLORER, EXPLORER,
true,
NETWORKS.mainnet.chainId, NETWORKS.mainnet.chainId,
); );
expect(row.to).toBe(RECIPIENT); expect(row.to).toBe(RECIPIENT);
@@ -216,6 +211,7 @@ test("with mainnet active, a Sepolia transaction is read from Sepolia's Blocksco
const [row] = await fetchRecentTransactions( const [row] = await fetchRecentTransactions(
HOLDER, HOLDER,
EXPLORER, EXPLORER,
true,
NETWORKS.sepolia.chainId, NETWORKS.sepolia.chainId,
); );
@@ -244,6 +240,7 @@ test("with mainnet active and no Blockscout remembered for Sepolia, a Sepolia tr
const [row] = await fetchRecentTransactions( const [row] = await fetchRecentTransactions(
HOLDER, HOLDER,
EXPLORER, EXPLORER,
true,
NETWORKS.sepolia.chainId, NETWORKS.sepolia.chainId,
); );
mockAsked.length = 0; mockAsked.length = 0;
@@ -265,6 +262,7 @@ async function showSepoliaTransferWithMainnetActive() {
const [row] = await fetchRecentTransactions( const [row] = await fetchRecentTransactions(
HOLDER, HOLDER,
EXPLORER, EXPLORER,
true,
NETWORKS.sepolia.chainId, NETWORKS.sepolia.chainId,
); );
transactionDetail.show(row); transactionDetail.show(row);
+1 -6
View File
@@ -9,12 +9,6 @@
const { AbiCoder, FetchRequest } = require("ethers"); const { AbiCoder, FetchRequest } = require("ethers");
const { refreshBalances } = require("../src/shared/balances"); const { refreshBalances } = require("../src/shared/balances");
const { makeStorageStub } = require("./support/storageStub");
// Blockscout is asked only while the stored "Use Blockscout" is on.
globalThis.chrome = {
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
};
const RPC_URL = "https://rpc.example.invalid"; const RPC_URL = "https://rpc.example.invalid";
const EXPLORER_URL = "https://explorer.example.invalid/api/v2"; const EXPLORER_URL = "https://explorer.example.invalid/api/v2";
@@ -138,6 +132,7 @@ async function refresh(addresses, trackedTokens) {
[{ addresses }], [{ addresses }],
RPC_URL, RPC_URL,
EXPLORER_URL, EXPLORER_URL,
true,
trackedTokens, trackedTokens,
"mainnet", "mainnet",
new AbortController().signal, new AbortController().signal,
+51 -11
View File
@@ -1215,13 +1215,11 @@ function respondWith(txItems, tokenTransferItems) {
describe("fetchRecentTransactions merge and dedup", () => { describe("fetchRecentTransactions merge and dedup", () => {
beforeEach(() => { beforeEach(() => {
debugFetch.mockReset(); debugFetch.mockReset();
// Blockscout is asked only while the stored "Use Blockscout" is on.
global.chrome.storage.write("autistmask", { useBlockscout: true });
}); });
test("queries only the two Blockscout endpoints for the address", async () => { test("queries only the two Blockscout endpoints for the address", async () => {
respondWith([], []); respondWith([], []);
await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1"); await fetchRecentTransactions(VICTIM, BLOCKSCOUT, true, "0x1");
expect(debugFetch).toHaveBeenCalledTimes(2); expect(debugFetch).toHaveBeenCalledTimes(2);
const urls = debugFetch.mock.calls.map((c) => c[0]); const urls = debugFetch.mock.calls.map((c) => c[0]);
expect(urls).toContain( expect(urls).toContain(
@@ -1285,7 +1283,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
], ],
); );
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1"); const txs = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
true,
"0x1",
);
expect(txs).toHaveLength(1); expect(txs).toHaveLength(1);
const merged = txs[0]; const merged = txs[0];
// The received leg (the swap output) supplies the display amount. // The received leg (the swap output) supplies the display amount.
@@ -1322,7 +1325,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
], ],
); );
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1"); const txs = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
true,
"0x1",
);
expect(txs).toHaveLength(1); expect(txs).toHaveLength(1);
expect(txs[0].symbol).toBe("USDC"); expect(txs[0].symbol).toBe("USDC");
expect(txs[0].value).toBe("1500.5000"); expect(txs[0].value).toBe("1500.5000");
@@ -1348,7 +1356,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
}); });
respondWith([], [leg("1000000"), leg("2000000")]); respondWith([], [leg("1000000"), leg("2000000")]);
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1"); const txs = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
true,
"0x1",
);
expect(txs).toHaveLength(1); expect(txs).toHaveLength(1);
// Keyed by hash plus contract, so the later leg wins. // Keyed by hash plus contract, so the later leg wins.
expect(txs[0].exactValue).toBe("2.0"); expect(txs[0].exactValue).toBe("2.0");
@@ -1388,7 +1401,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
], ],
); );
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1"); const txs = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
true,
"0x1",
);
expect(txs.map((t) => t.symbol).sort()).toEqual(["USDC", "WETH"]); expect(txs.map((t) => t.symbol).sort()).toEqual(["USDC", "WETH"]);
}); });
@@ -1429,7 +1447,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
], ],
); );
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1"); const txs = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
true,
"0x1",
);
expect(txs).toHaveLength(1); expect(txs).toHaveLength(1);
expect(txs[0].symbol).toBe("USDC"); expect(txs[0].symbol).toBe("USDC");
expect(txs[0].exactValue).toBe("1.0"); expect(txs[0].exactValue).toBe("1.0");
@@ -1455,7 +1478,13 @@ describe("fetchRecentTransactions merge and dedup", () => {
}); });
respondWith([item(6), item(8), item(7)], []); respondWith([item(6), item(8), item(7)], []);
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1", 2); const txs = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
true,
"0x1",
2,
);
expect(txs.map((t) => t.blockNumber)).toEqual([21000008, 21000007]); expect(txs.map((t) => t.blockNumber)).toEqual([21000008, 21000007]);
}); });
@@ -1481,6 +1510,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
const sepolia = await fetchRecentTransactions( const sepolia = await fetchRecentTransactions(
VICTIM, VICTIM,
BLOCKSCOUT, BLOCKSCOUT,
true,
"0xaa36a7", "0xaa36a7",
); );
expect(sepolia[0].symbol).toBe("SepoliaETH"); expect(sepolia[0].symbol).toBe("SepoliaETH");
@@ -1489,6 +1519,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
const mainnet = await fetchRecentTransactions( const mainnet = await fetchRecentTransactions(
VICTIM, VICTIM,
BLOCKSCOUT, BLOCKSCOUT,
true,
"0x1", "0x1",
); );
expect(mainnet[0].symbol).toBe("ETH"); expect(mainnet[0].symbol).toBe("ETH");
@@ -1515,7 +1546,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
], ],
); );
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1"); const txs = await fetchRecentTransactions(
VICTIM,
BLOCKSCOUT,
true,
"0x1",
);
expect(txs).toHaveLength(1); expect(txs).toHaveLength(1);
expect(txs[0].contractAddress).toBe(FAKE_ETH_CONTRACT); expect(txs[0].contractAddress).toBe(FAKE_ETH_CONTRACT);
expect(txs[0].holders).toBe(0); expect(txs[0].holders).toBe(0);
@@ -1557,6 +1593,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
const txs = await fetchRecentTransactions( const txs = await fetchRecentTransactions(
VICTIM, VICTIM,
BLOCKSCOUT, BLOCKSCOUT,
true,
"0x1", "0x1",
); );
expect(txs[0].holders).toBeNull(); expect(txs[0].holders).toBeNull();
@@ -1567,6 +1604,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
const txs = await fetchRecentTransactions( const txs = await fetchRecentTransactions(
VICTIM, VICTIM,
BLOCKSCOUT, BLOCKSCOUT,
true,
"0x1", "0x1",
); );
expect(txs[0].holders).toBeNull(); expect(txs[0].holders).toBeNull();
@@ -1577,6 +1615,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
const txs = await fetchRecentTransactions( const txs = await fetchRecentTransactions(
VICTIM, VICTIM,
BLOCKSCOUT, BLOCKSCOUT,
true,
"0x1", "0x1",
); );
expect(filterTransactions(txs, filters()).transactions).toEqual( expect(filterTransactions(txs, filters()).transactions).toEqual(
@@ -1593,6 +1632,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
const txs = await fetchRecentTransactions( const txs = await fetchRecentTransactions(
VICTIM, VICTIM,
BLOCKSCOUT, BLOCKSCOUT,
true,
"0x1", "0x1",
); );
expect(txs[0].holders).toBe(0); expect(txs[0].holders).toBe(0);
@@ -1610,7 +1650,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
}, },
})); }));
await expect( await expect(
fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1"), fetchRecentTransactions(VICTIM, BLOCKSCOUT, true, "0x1"),
).resolves.toEqual([]); ).resolves.toEqual([]);
}); });
+1 -7
View File
@@ -42,14 +42,8 @@ jest.mock("../src/shared/log", () => ({
isDebug: () => false, isDebug: () => false,
})); }));
// Blockscout is asked only while the stored "Use Blockscout" is on.
globalThis.chrome = { globalThis.chrome = {
storage: { storage: { local: { get: async () => ({}), set: async () => {} } },
local: {
get: async () => ({ autistmask: { useBlockscout: true } }),
set: async () => {},
},
},
}; };
const uniswap = require("../src/shared/uniswap"); const uniswap = require("../src/shared/uniswap");
+2 -7
View File
@@ -34,14 +34,8 @@ jest.mock("../src/shared/log", () => ({
isDebug: () => false, isDebug: () => false,
})); }));
// Blockscout is asked only while the stored "Use Blockscout" is on.
globalThis.chrome = { globalThis.chrome = {
storage: { storage: { local: { get: async () => ({}), set: async () => {} } },
local: {
get: async () => ({ autistmask: { useBlockscout: true } }),
set: async () => {},
},
},
}; };
const { state } = require("../src/shared/state"); const { state } = require("../src/shared/state");
@@ -294,6 +288,7 @@ async function historyRow(method, sentTo, chainId) {
const rows = await fetchRecentTransactions( const rows = await fetchRecentTransactions(
RECIPIENT, RECIPIENT,
"https://explorer.example/api/v2", "https://explorer.example/api/v2",
true,
chainId, chainId,
); );
return rows[0]; return rows[0];
+4 -11
View File
@@ -72,12 +72,8 @@ global.fetch = jest.fn(() => {
throw new Error("tests must not perform network requests"); throw new Error("tests must not perform network requests");
}); });
// Blockscout is asked only while the stored "Use Blockscout" is on.
const { makeStorageStub } = require("./support/storageStub"); const { makeStorageStub } = require("./support/storageStub");
global.chrome = { global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
runtime: { sendMessage() {} },
};
// A stub DOM. Every id in index.html that these two views touch resolves to a // A stub DOM. Every id in index.html that these two views touch resolves to a
// fresh recording element; nothing here depends on layout, only on what the // fresh recording element; nothing here depends on layout, only on what the
@@ -176,7 +172,7 @@ async function fetchOnto(items) {
statusText: "OK", statusText: "OK",
json: async () => items, json: async () => items,
})); }));
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []); const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
state.wallets = [ state.wallets = [
{ {
name: "Wallet 1", name: "Wallet 1",
@@ -201,14 +197,14 @@ async function fetchOntoBoth(itemsA, itemsB) {
statusText: "OK", statusText: "OK",
json: async () => itemsA, json: async () => itemsA,
})); }));
const a = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []); const a = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
debugFetch.mockImplementation(async () => ({ debugFetch.mockImplementation(async () => ({
ok: true, ok: true,
status: 200, status: 200,
statusText: "OK", statusText: "OK",
json: async () => itemsB, json: async () => itemsB,
})); }));
const b = await fetchTokenBalances(SECOND_HOLDER, BLOCKSCOUT, []); const b = await fetchTokenBalances(SECOND_HOLDER, BLOCKSCOUT, true, []);
state.wallets = [ state.wallets = [
{ {
name: "Wallet 1", name: "Wallet 1",
@@ -272,9 +268,6 @@ beforeEach(() => {
state.fraudContracts = []; state.fraudContracts = [];
state.hideLowHolderTokens = false; state.hideLowHolderTokens = false;
state.currentView = null; state.currentView = null;
// The screens save this page's copy of "Use Blockscout" over the stored
// one, so it is on here too.
state.useBlockscout = true;
}); });
describe("the Send screen resolves the scale rather than reading the stored one", () => { describe("the Send screen resolves the scale rather than reading the stored one", () => {