Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95b579d7f0 |
No files matched your search
@@ -893,6 +893,8 @@ src/
|
||||
prices.js — ETH/USD and token/USD via CoinDesk API
|
||||
scamlist.js — known fraud contract addresses
|
||||
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
|
||||
tokenList.js — top ERC-20 tokens by market cap (hardcoded)
|
||||
transactions.js — tx history fetching + anti-poisoning filters
|
||||
@@ -1440,9 +1442,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
|
||||
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.
|
||||
Blockscout may answer just before the user turns the setting off. So if the
|
||||
stored setting is off when the refresh saves, in the popup or the background,
|
||||
the save drops every token the user has not tracked.
|
||||
|
||||
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
|
||||
@@ -1916,13 +1918,14 @@ view would leave a wallet one click from deletion.
|
||||
- **Transitions**:
|
||||
- "Review" (valid inputs, ENS resolved) → **ConfirmTx**
|
||||
- "Review" with an unresolvable ENS name or an invalid amount → flash
|
||||
message, no screen change. An amount is valid when it is a number above
|
||||
zero, written as digits with at most one decimal point, with nothing but
|
||||
zeros after the token's last decimal place. ETH has 18 places, and a token
|
||||
with more, or with an unknown number, is held to 18. Zeros past those
|
||||
places leave the amount unchanged, so `1.0000000` of a 6-place token
|
||||
passes and is sent as exactly 1. ConfirmTx makes the same check before
|
||||
anything can be sent
|
||||
message, no screen change. A name with no address, or one resolved
|
||||
off-chain, gets "That ENS name has no on-chain address." An amount is
|
||||
valid when it is a number above zero, written as digits with at most one
|
||||
decimal point, with nothing but zeros after the token's last decimal
|
||||
place. ETH has 18 places, and a token with more, or with an unknown
|
||||
number, is held to 18. Zeros past those places leave the amount unchanged,
|
||||
so `1.0000000` of a 6-place token passes and is sent as exactly 1.
|
||||
ConfirmTx makes the same check before anything can be sent
|
||||
- "Back" → previous screen (Home, AddressDetail, or AddressToken)
|
||||
|
||||
#### ConfirmTx (`confirm-tx`)
|
||||
@@ -2798,6 +2801,10 @@ settings:
|
||||
Both settings are on the Welcome screen and in Settings. Both are off in a new
|
||||
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:
|
||||
|
||||
- **Ethereum JSON-RPC endpoint**: The extension needs an Ethereum node to query
|
||||
@@ -2833,22 +2840,26 @@ Sent the user's data only while the user has turned the setting on:
|
||||
- token holder counts, for spam filtering
|
||||
- a transaction's block, network details and calldata, for
|
||||
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
|
||||
what it leaves out and that "Use Blockscout" is off. Only tracked tokens
|
||||
are listed.
|
||||
- **Saving the URL**: pressing "Save" beside the URL in Settings asks that
|
||||
URL for `/stats`. This request carries no user data. It is made whether or
|
||||
not the setting is on.
|
||||
- **Saving the URL**: whether or not the setting is on, pressing "Save"
|
||||
beside the URL in Settings asks that URL for `/stats`. This request
|
||||
carries no user data.
|
||||
- **Data sent**: Ethereum addresses, and the hash of each transaction the
|
||||
user opens. Blockscout receives the user's addresses to query their
|
||||
transaction history and token balances. No private keys, passwords, or
|
||||
signing operations are sent.
|
||||
|
||||
- **Etherscan address labels** ("Check recipients on Etherscan"): When
|
||||
confirming a transaction, the extension performs a best-effort lookup of the
|
||||
recipient address on Etherscan to check for phishing/scam labels. This is a
|
||||
direct page fetch with no API key; the user's browser makes the request. While
|
||||
the setting is off, ConfirmTx says that no Etherscan check was made.
|
||||
- **Etherscan address labels** ("Check recipients on Etherscan"): While the
|
||||
setting is on, ConfirmTx looks up the recipient's address on Etherscan each
|
||||
time it is shown. It checks for a phishing or scam label, best-effort. This is
|
||||
a direct page fetch with no API key; the user's browser makes the request.
|
||||
While the setting is off, ConfirmTx says that no Etherscan check was made.
|
||||
- **Data sent**: the recipient's address and nothing else.
|
||||
|
||||
The etherscan links on the screens open a page in the browser when the user
|
||||
|
||||
+19
-17
@@ -90,10 +90,10 @@ this in Settings to any Ethereum JSON-RPC endpoint, including your own local
|
||||
node.
|
||||
|
||||
When it is contacted: on every balance refresh (every 10 seconds while the popup
|
||||
is open, every 60 seconds in the background), when you type an ENS name into the
|
||||
Send screen, when a send is prepared and broadcast, while a pending transaction
|
||||
is polled for its receipt, and for the reverse ENS lookups used to label
|
||||
addresses (cached for 12 hours).
|
||||
is open, every 60 seconds in the background), when you review a send to an ENS
|
||||
name, when a send is prepared and broadcast, while a pending transaction is
|
||||
polled for its receipt, and for the reverse ENS lookups used to label addresses
|
||||
(cached for 12 hours).
|
||||
|
||||
What gets sent: standard Ethereum JSON-RPC requests (balance queries,
|
||||
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`;
|
||||
`eth-sepolia.blockscout.com/api/v2` on Sepolia)
|
||||
|
||||
Blockscout is an open-source blockchain explorer. AutistMask uses it only while
|
||||
"Use Blockscout" is on. It then asks Blockscout for:
|
||||
Blockscout is an open-source blockchain explorer. AutistMask sends it your data
|
||||
only while "Use Blockscout" is on. It then asks Blockscout for:
|
||||
|
||||
- your recent transactions and token transfers
|
||||
- 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
|
||||
- when you open a transaction
|
||||
|
||||
Pressing "Save" beside the Blockscout URL in Settings also contacts it, even
|
||||
with the setting off. That request checks the URL by asking it for `/stats`. It
|
||||
sends none of your data.
|
||||
Whether or not the setting is on, pressing "Save" beside the Blockscout URL in
|
||||
Settings asks that URL for `/stats` to check it. That request sends none of your
|
||||
data.
|
||||
|
||||
What gets sent: your Ethereum addresses (to look up balances and transactions)
|
||||
and the hash of each transaction you open.
|
||||
@@ -143,14 +143,15 @@ 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.
|
||||
While "Check recipients on Etherscan" is on, AutistMask fetches the recipient's
|
||||
public Etherscan address page when you review a send. 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.
|
||||
The setting is off in a new install. 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.
|
||||
@@ -271,7 +272,8 @@ screen. Tokens can also be added from Settings, under "Tracked Tokens".
|
||||
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
|
||||
that survives the spam filters).
|
||||
3. Enter the recipient address or ENS name (e.g. `vitalik.eth`).
|
||||
3. Enter the recipient address or ENS name (e.g. `vitalik.eth`). AutistMask does
|
||||
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
|
||||
decimal places, or your ETH balance minus the amount reserved for the network
|
||||
fee.
|
||||
|
||||
@@ -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
|
||||
// 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.
|
||||
// Blockscout may have answered before the user turned "Use Blockscout" off. So
|
||||
// when "Use Blockscout" stored NOW is off, the save drops every token the user
|
||||
// has not tracked.
|
||||
async function backgroundRefresh() {
|
||||
const s = await getState();
|
||||
const now = Date.now();
|
||||
@@ -1405,7 +1405,6 @@ async function backgroundRefresh() {
|
||||
wallets,
|
||||
s.rpcUrl,
|
||||
s.blockscoutUrl,
|
||||
s.useBlockscout,
|
||||
s.trackedTokens,
|
||||
s.networkId,
|
||||
);
|
||||
|
||||
+5
-6
@@ -71,9 +71,8 @@ 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 first takes any change to the network or to either setting saved
|
||||
// elsewhere (followStoredSettings() below), so what it draws follows them.
|
||||
//
|
||||
// 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
|
||||
@@ -85,8 +84,9 @@ let refreshTimer = null;
|
||||
// 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.
|
||||
// removed meanwhile is left as it is. Blockscout may have answered before the
|
||||
// user turned "Use Blockscout" off. So if the stored setting is off when the
|
||||
// refresh saves, the save drops every token the user has not tracked.
|
||||
async function doRefreshAndRender() {
|
||||
if (refreshInFlight) return;
|
||||
refreshInFlight = true;
|
||||
@@ -100,7 +100,6 @@ async function doRefreshAndRender() {
|
||||
wallets,
|
||||
state.rpcUrl,
|
||||
state.blockscoutUrl,
|
||||
state.useBlockscout,
|
||||
state.trackedTokens,
|
||||
networkId,
|
||||
pageClosed.signal,
|
||||
|
||||
@@ -102,7 +102,6 @@ async function loadTransactions(address) {
|
||||
const rawTxs = await fetchRecentTransactions(
|
||||
address,
|
||||
state.blockscoutUrl,
|
||||
state.useBlockscout,
|
||||
currentNetwork().chainId,
|
||||
);
|
||||
const result = filterTransactions(rawTxs, {
|
||||
|
||||
@@ -174,7 +174,6 @@ async function loadTransactions(address, tokenId) {
|
||||
const rawTxs = await fetchRecentTransactions(
|
||||
address,
|
||||
state.blockscoutUrl,
|
||||
state.useBlockscout,
|
||||
currentNetwork().chainId,
|
||||
);
|
||||
const result = filterTransactions(rawTxs, {
|
||||
|
||||
@@ -197,7 +197,6 @@ async function loadHomeTxs(ctx) {
|
||||
fetchRecentTransactions(
|
||||
addr,
|
||||
state.blockscoutUrl,
|
||||
state.useBlockscout,
|
||||
currentNetwork().chainId,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -403,8 +403,11 @@ function init(_ctx) {
|
||||
try {
|
||||
const provider = getProvider(state.rpcUrl, state.networkId);
|
||||
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) {
|
||||
showFlash("That ENS name has no address.");
|
||||
showFlash("That ENS name has no on-chain address.");
|
||||
return;
|
||||
}
|
||||
resolvedTo = resolved;
|
||||
|
||||
@@ -26,6 +26,7 @@ const { state } = require("../../shared/state");
|
||||
const { nativeCurrencyByChainId } = require("../../shared/networks");
|
||||
const { formatEther, formatUnits } = require("ethers");
|
||||
const { log, debugFetch } = require("../../shared/log");
|
||||
const { storedSettingIsOn } = require("../../shared/storedSettings");
|
||||
const { decodeCalldata } = require("./approval");
|
||||
|
||||
/**
|
||||
@@ -309,10 +310,11 @@ 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 calldata. While the stored "Use Blockscout" is off, Blockscout is not
|
||||
// asked, and an answer that lands after it went off is dropped
|
||||
// (src/shared/storedSettings.js). Either way none of them is shown.
|
||||
async function loadFullTxDetails(txHash, chainId) {
|
||||
if (!state.useBlockscout) return;
|
||||
if (!(await storedSettingIsOn("useBlockscout"))) return;
|
||||
const section = $("tx-detail-calldata-section");
|
||||
const actionEl = $("tx-detail-calldata-action");
|
||||
const detailsEl = $("tx-detail-calldata-details");
|
||||
@@ -328,6 +330,7 @@ async function loadFullTxDetails(txHash, chainId) {
|
||||
);
|
||||
if (!resp.ok) return;
|
||||
const txData = await resp.json();
|
||||
if (!(await storedSettingIsOn("useBlockscout"))) return;
|
||||
|
||||
// Populate on-chain detail fields (block, nonce, gas, fee)
|
||||
populateOnChainDetails(txData, chainId);
|
||||
|
||||
@@ -22,6 +22,7 @@ const { LOW_HOLDER_THRESHOLD, parseHoldersCount } = require("./holders");
|
||||
const { isSpoofedSymbol } = require("./symbolSpoof");
|
||||
const { toDecimals } = require("./transferAmount");
|
||||
const { resolveTokenDecimals } = require("./approvalAmount");
|
||||
const { storedSettingIsOn } = require("./storedSettings");
|
||||
|
||||
// Use a static network to skip auto-detection (which can fail and cause
|
||||
// "could not coalesce error" on some RPC endpoints like Cloudflare).
|
||||
@@ -95,9 +96,10 @@ function rawUnits(value) {
|
||||
// Fetch token balances for a single address from Blockscout.
|
||||
// Returns [{ address, name, symbol, decimals, balance, holders }].
|
||||
//
|
||||
// `useBlockscout` is the user's "Use Blockscout" setting. While it is off,
|
||||
// Blockscout is not asked and the answer is an empty list. So no token the user
|
||||
// has not tracked is listed.
|
||||
// While the stored "Use Blockscout" is off, Blockscout is not asked, and an
|
||||
// answer that lands after it went off is dropped (src/shared/storedSettings.js).
|
||||
// Either way the answer is an empty list, so no token the user has not tracked
|
||||
// is listed.
|
||||
//
|
||||
// Filters out spam: only shows tokens that are in the known token list,
|
||||
// explicitly tracked by the user, or have >= 1000 holders. A tracked token's
|
||||
@@ -114,11 +116,10 @@ function rawUnits(value) {
|
||||
async function fetchTokenBalances(
|
||||
address,
|
||||
blockscoutUrl,
|
||||
useBlockscout,
|
||||
trackedTokens,
|
||||
signal,
|
||||
) {
|
||||
if (!useBlockscout) return [];
|
||||
if (!(await storedSettingIsOn("useBlockscout"))) return [];
|
||||
try {
|
||||
const resp = await debugFetch(
|
||||
blockscoutUrl + "/addresses/" + address + "/token-balances",
|
||||
@@ -128,6 +129,7 @@ async function fetchTokenBalances(
|
||||
return null;
|
||||
}
|
||||
const items = await resp.json();
|
||||
if (!(await storedSettingIsOn("useBlockscout"))) return [];
|
||||
if (!Array.isArray(items)) return null;
|
||||
|
||||
const trackedSet = new Set(
|
||||
@@ -316,13 +318,12 @@ async function fetchTrackedTokenBalances(
|
||||
}
|
||||
|
||||
// Fetch ETH balances, ENS names, and ERC-20 token balances for all addresses.
|
||||
// `trackedTokens` are the tokens tracked on the active network.
|
||||
// `useBlockscout` and `signal` are as for fetchTokenBalances().
|
||||
// `trackedTokens` are the tokens tracked on the active network. `signal` is as
|
||||
// for fetchTokenBalances().
|
||||
async function refreshBalances(
|
||||
wallets,
|
||||
rpcUrl,
|
||||
blockscoutUrl,
|
||||
useBlockscout,
|
||||
trackedTokens,
|
||||
networkId,
|
||||
signal,
|
||||
@@ -391,7 +392,6 @@ async function refreshBalances(
|
||||
const explorerRequest = fetchTokenBalances(
|
||||
addr.address,
|
||||
blockscoutUrl,
|
||||
useBlockscout,
|
||||
tracked,
|
||||
signal,
|
||||
);
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
// This is a best-effort enrichment: network failures return null silently.
|
||||
|
||||
const { networkByChainId } = require("./networks");
|
||||
const { storageGet } = require("./browserApi");
|
||||
const { normalizePersisted } = require("./persistedState");
|
||||
const { storedSettingIsOn } = require("./storedSettings");
|
||||
|
||||
// Patterns in the page title that indicate a flagged address.
|
||||
// Title format: "Fake_Phishing184810 | Address: 0x... | Etherscan"
|
||||
@@ -80,11 +79,9 @@ 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. 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.
|
||||
* While the stored "Check recipients on Etherscan" is off, Etherscan is not
|
||||
* asked, and an answer that lands after it went off is dropped
|
||||
* (src/shared/storedSettings.js). Either way the answer is null.
|
||||
*
|
||||
* @param {string} address - Ethereum address to check.
|
||||
* @param {string} [chainId] - Chain id of the network to look it up on.
|
||||
@@ -92,8 +89,7 @@ function parseEtherscanPage(html) {
|
||||
*/
|
||||
async function checkEtherscanLabel(address, chainId) {
|
||||
try {
|
||||
const { autistmask } = await storageGet("autistmask");
|
||||
if (!normalizePersisted(autistmask).checkRecipientsOnEtherscan) {
|
||||
if (!(await storedSettingIsOn("checkRecipientsOnEtherscan"))) {
|
||||
return null;
|
||||
}
|
||||
// Lazy require to avoid pulling in chrome.storage at module scope
|
||||
@@ -106,6 +102,9 @@ async function checkEtherscanLabel(address, chainId) {
|
||||
});
|
||||
if (!resp.ok) return null;
|
||||
const html = await resp.text();
|
||||
if (!(await storedSettingIsOn("checkRecipientsOnEtherscan"))) {
|
||||
return null;
|
||||
}
|
||||
const result = parseEtherscanPage(html);
|
||||
if (result.isPhishing) {
|
||||
return {
|
||||
@@ -117,7 +116,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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 };
|
||||
@@ -21,6 +21,7 @@ const { toDecimals } = require("./transferAmount");
|
||||
// detail view is the authoritative record and already shows exact precision.
|
||||
const { truncateAmount: formatTxValue } = require("./amountDisplay");
|
||||
const { routerRelease } = require("./uniswap");
|
||||
const { storedSettingIsOn } = require("./storedSettings");
|
||||
|
||||
// Ethereum addresses are case-insensitive: EIP-55 mixed case is a checksum
|
||||
// over the address, not part of its identity. Every address comparison in
|
||||
@@ -236,16 +237,16 @@ function mergeTransactions(txs, tokenTransfers) {
|
||||
// carries it, and a native entry is labelled with that network's
|
||||
// `nativeCurrency` from networks.js (`ETH`, `SepoliaETH`).
|
||||
//
|
||||
// `useBlockscout` is the user's "Use Blockscout" setting. While it is off,
|
||||
// Blockscout is not asked and the answer is an empty list.
|
||||
// While the stored "Use Blockscout" is off, Blockscout is not asked, and an
|
||||
// answer that lands after it went off is dropped (src/shared/storedSettings.js).
|
||||
// Either way the answer is an empty list.
|
||||
async function fetchRecentTransactions(
|
||||
address,
|
||||
blockscoutUrl,
|
||||
useBlockscout,
|
||||
chainId,
|
||||
count = 25,
|
||||
) {
|
||||
if (!useBlockscout) return [];
|
||||
if (!(await storedSettingIsOn("useBlockscout"))) return [];
|
||||
log.debugf("fetchRecentTransactions", address);
|
||||
const addrLower = normalizeAddress(address);
|
||||
|
||||
@@ -276,6 +277,7 @@ async function fetchRecentTransactions(
|
||||
|
||||
const txJson = txResp.ok ? await txResp.json() : {};
|
||||
const ttJson = ttResp.ok ? await ttResp.json() : {};
|
||||
if (!(await storedSettingIsOn("useBlockscout"))) return [];
|
||||
|
||||
const txs = mergeTransactions(
|
||||
(txJson.items || []).map((tx) => parseTx(tx, addrLower, chainId)),
|
||||
|
||||
@@ -412,14 +412,7 @@ describe("a balance refresh under a network switch", () => {
|
||||
const ranOn = [];
|
||||
|
||||
const bg = loadWorker("sepolia", {
|
||||
refreshBalances: async (
|
||||
wallets,
|
||||
rpcUrl,
|
||||
explorer,
|
||||
useBlockscout,
|
||||
tokens,
|
||||
id,
|
||||
) => {
|
||||
refreshBalances: async (wallets, rpcUrl, explorer, tokens, id) => {
|
||||
ranOn.push(id);
|
||||
reachedNetwork.resolve();
|
||||
await roundTrip.promise;
|
||||
@@ -455,22 +448,16 @@ 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.
|
||||
// (src/popup/views/settings.js). Blockscout may have answered a refresh
|
||||
// just before. That refresh 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 () => {
|
||||
const roundTrip = deferred();
|
||||
const started = deferred();
|
||||
const handed = [];
|
||||
|
||||
const bg = loadWorker("mainnet", {
|
||||
refreshBalances: async (
|
||||
wallets,
|
||||
rpcUrl,
|
||||
explorer,
|
||||
useBlockscout,
|
||||
) => {
|
||||
handed.push(useBlockscout);
|
||||
refreshBalances: async (wallets) => {
|
||||
started.resolve();
|
||||
await roundTrip.promise;
|
||||
wallets[0].addresses[0].balance = "7.0";
|
||||
@@ -504,7 +491,6 @@ describe("a balance refresh under a change of Use Blockscout", () => {
|
||||
roundTrip.resolve();
|
||||
await refresh;
|
||||
|
||||
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([]);
|
||||
|
||||
@@ -24,6 +24,12 @@ const TRACKED = {
|
||||
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;
|
||||
let logged;
|
||||
|
||||
@@ -54,7 +60,6 @@ function refresh(signal) {
|
||||
wallets,
|
||||
RPC_URL,
|
||||
EXPLORER_URL,
|
||||
true,
|
||||
[TRACKED],
|
||||
"mainnet",
|
||||
signal,
|
||||
|
||||
@@ -91,7 +91,7 @@ test("a refresh that returns after a switch in Settings writes nothing, and the
|
||||
|
||||
await page.tick();
|
||||
|
||||
expect(refreshBalances.mock.lastCall[5]).toBe("sepolia");
|
||||
expect(refreshBalances.mock.lastCall[4]).toBe("sepolia");
|
||||
expect(page.storage.read("autistmask").lastBalanceRefresh).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
// 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.
|
||||
// A request that either setting gates reads the stored setting, not the page's
|
||||
// copy, before it asks and again when the answer lands. So a change another
|
||||
// page saved applies to it 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.
|
||||
@@ -40,6 +41,7 @@ const {
|
||||
} = require("../src/shared/balances");
|
||||
const { fetchRecentTransactions } = require("../src/shared/transactions");
|
||||
const { getFullWarnings } = require("../src/shared/addressWarnings");
|
||||
const { checkEtherscanLabel } = require("../src/shared/etherscanLabels");
|
||||
const { HISTORY_OFF_TEXT, escapeHtml } = require("../src/popup/views/helpers");
|
||||
|
||||
const RPC_URL = "https://rpc.example.invalid";
|
||||
@@ -61,6 +63,25 @@ 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;
|
||||
|
||||
afterEach(() => {
|
||||
@@ -91,33 +112,55 @@ describe("where each request is made", () => {
|
||||
// Every URL fetch() was asked for, in order.
|
||||
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(() => {
|
||||
asked = [];
|
||||
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>",
|
||||
};
|
||||
});
|
||||
globalThis.fetch = jest.fn(recordAndAnswer);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
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 () => {
|
||||
expect(
|
||||
await fetchTokenBalances(ADDRESS, BLOCKSCOUT, false, []),
|
||||
).toEqual([]);
|
||||
storing({ useBlockscout: false });
|
||||
expect(await fetchTokenBalances(ADDRESS, BLOCKSCOUT, [])).toEqual([]);
|
||||
expect(asked).toEqual([]);
|
||||
|
||||
const rows = await fetchTokenBalances(ADDRESS, BLOCKSCOUT, true, []);
|
||||
storing({ useBlockscout: true });
|
||||
const rows = await fetchTokenBalances(ADDRESS, BLOCKSCOUT, []);
|
||||
expect(rows.map((row) => row.symbol)).toEqual(["USDC"]);
|
||||
expect(asked).toEqual([
|
||||
BLOCKSCOUT + "/addresses/" + ADDRESS + "/token-balances",
|
||||
@@ -125,12 +168,14 @@ describe("where each request is made", () => {
|
||||
});
|
||||
|
||||
test("the transaction history is asked for only while Use Blockscout is on", async () => {
|
||||
storing({ useBlockscout: false });
|
||||
expect(
|
||||
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, false, "0x1"),
|
||||
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1"),
|
||||
).toEqual([]);
|
||||
expect(asked).toEqual([]);
|
||||
|
||||
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, true, "0x1");
|
||||
storing({ useBlockscout: true });
|
||||
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1");
|
||||
expect(asked.sort()).toEqual([
|
||||
BLOCKSCOUT +
|
||||
"/addresses/" +
|
||||
@@ -146,26 +191,78 @@ describe("where each request is made", () => {
|
||||
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;
|
||||
}
|
||||
const checkWith = async (profile) => {
|
||||
storing(profile);
|
||||
await getFullWarnings(RECIPIENT, provider, { chainId: "0x1" });
|
||||
};
|
||||
|
||||
await checkWith({});
|
||||
await checkWith({ autistmask: { checkRecipientsOnEtherscan: false } });
|
||||
await checkWith(undefined);
|
||||
await checkWith({ checkRecipientsOnEtherscan: false });
|
||||
// Only a stored true is on, as when the profile is loaded.
|
||||
await checkWith({ autistmask: { checkRecipientsOnEtherscan: "true" } });
|
||||
await checkWith({ autistmask: { checkRecipientsOnEtherscan: 42 } });
|
||||
await checkWith({ checkRecipientsOnEtherscan: "true" });
|
||||
await checkWith({ checkRecipientsOnEtherscan: 42 });
|
||||
expect(asked).toEqual([]);
|
||||
|
||||
await checkWith({ autistmask: { checkRecipientsOnEtherscan: true } });
|
||||
await checkWith({ checkRecipientsOnEtherscan: true });
|
||||
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", () => {
|
||||
// The RPC endpoint answers every balance with zero, and every other
|
||||
// call, the ENS reverse lookup's, with no resolver.
|
||||
@@ -212,11 +309,11 @@ describe("where each request is made", () => {
|
||||
}
|
||||
|
||||
async function refresh(addr, useBlockscout) {
|
||||
storing({ useBlockscout });
|
||||
await refreshBalances(
|
||||
[{ addresses: [addr] }],
|
||||
RPC_URL,
|
||||
BLOCKSCOUT,
|
||||
useBlockscout,
|
||||
[],
|
||||
"mainnet",
|
||||
);
|
||||
@@ -258,29 +355,6 @@ describe("the popup", () => {
|
||||
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 () => {
|
||||
const page = await bootPopup(unversionedValidProfile());
|
||||
expect(page.node("home-tx-list").innerHTML).toContain(
|
||||
@@ -341,6 +415,51 @@ describe("the popup", () => {
|
||||
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
|
||||
// reopened popup draws it.
|
||||
function onConfirmScreen(extra) {
|
||||
@@ -628,24 +747,22 @@ describe("the popup", () => {
|
||||
["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",
|
||||
"after another page turns Use Blockscout %s, this page takes the change and its next refresh is saved",
|
||||
async (_how, before, after) => {
|
||||
const page = await bootPopup(onAddressDetail(before));
|
||||
// The refresh answers as it does under the stored setting.
|
||||
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")];
|
||||
},
|
||||
);
|
||||
held.mockImplementationOnce(async (wallets) => {
|
||||
const addr = wallets[0].addresses[0];
|
||||
addr.balance = "7.0";
|
||||
addr.tokenBalances = after
|
||||
? [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");
|
||||
@@ -722,65 +839,3 @@ describe("the popup", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -108,7 +108,10 @@ global.fetch = jest.fn(() => {
|
||||
});
|
||||
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
|
||||
global.chrome = {
|
||||
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
|
||||
runtime: { sendMessage() {} },
|
||||
};
|
||||
|
||||
// A stub DOM: every id resolves to a recording element.
|
||||
const elements = new Map();
|
||||
@@ -243,11 +246,13 @@ async function openSend(token, tokenUnits = null) {
|
||||
];
|
||||
state.selectedWallet = 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(
|
||||
state.wallets,
|
||||
"https://rpc.example.invalid",
|
||||
"https://blockscout.example/api/v2",
|
||||
true,
|
||||
state.trackedTokens,
|
||||
"mainnet",
|
||||
);
|
||||
|
||||
@@ -35,8 +35,11 @@ global.fetch = jest.fn(() => {
|
||||
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");
|
||||
global.chrome = { storage: makeStorageStub() };
|
||||
global.chrome = {
|
||||
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
|
||||
};
|
||||
|
||||
const { AbiCoder, Interface } = require("ethers");
|
||||
const { ERC20_ABI } = require("../src/shared/constants");
|
||||
@@ -114,7 +117,6 @@ async function fetchOnto(items, trackedTokens = []) {
|
||||
const balances = await fetchTokenBalances(
|
||||
HOLDER,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
trackedTokens,
|
||||
);
|
||||
state.trackedTokens = trackedTokens;
|
||||
|
||||
+10
-10
@@ -21,7 +21,11 @@ jest.mock("../src/shared/log", () => ({
|
||||
global.fetch = jest.fn(() => {
|
||||
throw new Error("tests must not perform network requests");
|
||||
});
|
||||
global.chrome = { storage: { local: {} } };
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on.
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
global.chrome = {
|
||||
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
|
||||
};
|
||||
|
||||
const {
|
||||
LOW_HOLDER_THRESHOLD,
|
||||
@@ -150,23 +154,19 @@ describe("the balance-list spam gate", () => {
|
||||
|
||||
test("a token with plenty of reported holders is listed", async () => {
|
||||
respondWith([item()]);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||
expect(balances).toHaveLength(1);
|
||||
expect(balances[0].holders).toBe(50000);
|
||||
});
|
||||
|
||||
test("a token reporting zero holders is still excluded", async () => {
|
||||
respondWith([item({ token: { holders_count: "0" } })]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
});
|
||||
|
||||
test("an unknown holder count does not admit an unvouched token", async () => {
|
||||
respondWith([item({ token: { holders_count: null } })]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
});
|
||||
|
||||
// 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.
|
||||
test("a tracked token with an unknown count is listed with holders null", async () => {
|
||||
respondWith([item({ token: { holders_count: undefined } })]);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, [
|
||||
{ address: NOVEL_TOKEN.toUpperCase() },
|
||||
]);
|
||||
expect(balances).toHaveLength(1);
|
||||
@@ -192,7 +192,7 @@ describe("the balance-list spam gate", () => {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||
expect(balances).toHaveLength(1);
|
||||
expect(balances[0].holders).toBeNull();
|
||||
});
|
||||
|
||||
@@ -436,8 +436,13 @@ describe.each([
|
||||
});
|
||||
|
||||
test("the transaction detail screen's type line and fee", async () => {
|
||||
// The fee is Blockscout's.
|
||||
// The fee is Blockscout's, asked for only while the stored "Use
|
||||
// Blockscout" is on.
|
||||
state.useBlockscout = true;
|
||||
global.chrome.storage.write("autistmask", {
|
||||
...global.chrome.storage.read("autistmask"),
|
||||
useBlockscout: true,
|
||||
});
|
||||
debugFetch.mockImplementationOnce(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
|
||||
@@ -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.
|
||||
await page.tick();
|
||||
const { refreshBalances } = require("../src/shared/balances");
|
||||
expect(refreshBalances.mock.lastCall[5]).toBe("sepolia");
|
||||
expect(refreshBalances.mock.lastCall[4]).toBe("sepolia");
|
||||
const stored = page.storage.read("autistmask");
|
||||
expect(stored.networkId).toBe("sepolia");
|
||||
expect(stored.lastBalanceRefresh).toBeGreaterThan(0);
|
||||
|
||||
@@ -88,7 +88,6 @@ test("the balance refresh", async () => {
|
||||
wallets,
|
||||
RPC_URL,
|
||||
"https://explorer.example.invalid/api/v2",
|
||||
true,
|
||||
[],
|
||||
"mainnet",
|
||||
);
|
||||
|
||||
@@ -90,7 +90,7 @@ let approvalDetails = null;
|
||||
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
global.chrome = {
|
||||
storage: makeStorageStub(),
|
||||
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
|
||||
runtime: {
|
||||
connect: () => ({
|
||||
postMessage() {},
|
||||
@@ -220,11 +220,13 @@ async function refreshWith(balanceWei, tokenItems = []) {
|
||||
state.wallets = [{ name: "Wallet 1", addresses: [{ address: HOLDER }] }];
|
||||
state.selectedWallet = 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(
|
||||
state.wallets,
|
||||
"https://rpc.example.invalid",
|
||||
"https://blockscout.example/api/v2",
|
||||
true,
|
||||
state.trackedTokens,
|
||||
"mainnet",
|
||||
);
|
||||
|
||||
@@ -100,7 +100,10 @@ global.fetch = jest.fn(() => {
|
||||
});
|
||||
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
|
||||
global.chrome = {
|
||||
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
|
||||
runtime: { sendMessage() {} },
|
||||
};
|
||||
|
||||
// A stub DOM: every id resolves to a recording element.
|
||||
const elements = new Map();
|
||||
@@ -213,11 +216,13 @@ async function refreshWith(balanceWei, tokenItems = []) {
|
||||
];
|
||||
state.selectedWallet = 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(
|
||||
state.wallets,
|
||||
"https://rpc.example.invalid",
|
||||
"https://blockscout.example/api/v2",
|
||||
true,
|
||||
state.trackedTokens,
|
||||
"mainnet",
|
||||
);
|
||||
|
||||
+17
-24
@@ -52,8 +52,14 @@ jest.mock("../src/shared/log", () => ({
|
||||
global.fetch = jest.fn(() => {
|
||||
throw new Error("tests must not perform network requests");
|
||||
});
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on.
|
||||
global.chrome = {
|
||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||
storage: {
|
||||
local: {
|
||||
get: async () => ({ autistmask: { useBlockscout: true } }),
|
||||
set: async () => {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { isSpoofedSymbol } = require("../src/shared/symbolSpoof");
|
||||
@@ -586,14 +592,12 @@ describe("surface 3: the balance list", () => {
|
||||
// 1,000-holder floor and was listed as a holding named ETH.
|
||||
test("a fake ETH token clearing the holder floor is filtered", async () => {
|
||||
respondWith([fakeEthItem()]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
});
|
||||
|
||||
test("tracking the fake token manually does not admit it either", async () => {
|
||||
respondWith([fakeEthItem({ holders_count: "0" })]);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, [
|
||||
{ address: FAKE_ETH_CONTRACT },
|
||||
]);
|
||||
expect(balances).toEqual([]);
|
||||
@@ -603,16 +607,12 @@ describe("surface 3: the balance list", () => {
|
||||
// their belief about what they own, and it renders the symbol into HTML.
|
||||
test("a padded fake ETH token is filtered too", async () => {
|
||||
respondWith([fakeEthItem({ symbol: " ETH " })]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
});
|
||||
|
||||
test("a fake ETH token padded with a no-break space is filtered", async () => {
|
||||
respondWith([fakeEthItem({ symbol: NBSP + "ETH" + NBSP })]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
});
|
||||
|
||||
// The false-positive direction on the surface that matters most: a real
|
||||
@@ -627,7 +627,7 @@ describe("surface 3: the balance list", () => {
|
||||
decimals: "6",
|
||||
}),
|
||||
]);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||
expect(balances).toHaveLength(1);
|
||||
expect(balances[0].symbol).toBe(" USDC ");
|
||||
});
|
||||
@@ -641,7 +641,7 @@ describe("surface 3: the balance list", () => {
|
||||
decimals: "6",
|
||||
}),
|
||||
]);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||
expect(balances).toHaveLength(1);
|
||||
expect(balances[0].symbol).toBe("USDC");
|
||||
});
|
||||
@@ -652,9 +652,7 @@ describe("surface 3: the balance list", () => {
|
||||
// before the symbol rule is consulted.
|
||||
test("a non-ERC-20 row claiming ETH never reaches the symbol rule", async () => {
|
||||
respondWith([fakeEthItem({ type: "ERC-721" })]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
});
|
||||
|
||||
// The adjacent finding from the same review as issue #260: the type gate
|
||||
@@ -672,20 +670,16 @@ describe("surface 3: the balance list", () => {
|
||||
decimals: "6",
|
||||
}),
|
||||
]);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||
expect(balances).toHaveLength(1);
|
||||
expect(balances[0].symbol).toBe("USDC");
|
||||
});
|
||||
|
||||
test("case insensitivity does not admit another token type", async () => {
|
||||
respondWith([fakeEthItem({ type: "erc-721" })]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
respondWith([fakeEthItem({ type: "ERC-20-EXTRA" })]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
});
|
||||
|
||||
// The money test: the user holds real ETH and has been airdropped a fake
|
||||
@@ -698,7 +692,6 @@ describe("surface 3: the balance list", () => {
|
||||
[{ addresses: [addr] }],
|
||||
"https://rpc.example.invalid",
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
[],
|
||||
"mainnet",
|
||||
);
|
||||
|
||||
@@ -38,8 +38,14 @@ jest.mock("../src/shared/log", () => ({
|
||||
isDebug: () => false,
|
||||
}));
|
||||
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on.
|
||||
globalThis.chrome = {
|
||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||
storage: {
|
||||
local: {
|
||||
get: async () => ({ autistmask: { useBlockscout: true } }),
|
||||
set: async () => {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { state } = require("../src/shared/state");
|
||||
@@ -178,7 +184,6 @@ test("a token transfer's decoded details name the token contract and the amount
|
||||
const [row] = await fetchRecentTransactions(
|
||||
HOLDER,
|
||||
EXPLORER,
|
||||
true,
|
||||
NETWORKS.mainnet.chainId,
|
||||
);
|
||||
expect(row.to).toBe(RECIPIENT);
|
||||
@@ -211,7 +216,6 @@ test("with mainnet active, a Sepolia transaction is read from Sepolia's Blocksco
|
||||
const [row] = await fetchRecentTransactions(
|
||||
HOLDER,
|
||||
EXPLORER,
|
||||
true,
|
||||
NETWORKS.sepolia.chainId,
|
||||
);
|
||||
|
||||
@@ -240,7 +244,6 @@ test("with mainnet active and no Blockscout remembered for Sepolia, a Sepolia tr
|
||||
const [row] = await fetchRecentTransactions(
|
||||
HOLDER,
|
||||
EXPLORER,
|
||||
true,
|
||||
NETWORKS.sepolia.chainId,
|
||||
);
|
||||
mockAsked.length = 0;
|
||||
@@ -262,7 +265,6 @@ async function showSepoliaTransferWithMainnetActive() {
|
||||
const [row] = await fetchRecentTransactions(
|
||||
HOLDER,
|
||||
EXPLORER,
|
||||
true,
|
||||
NETWORKS.sepolia.chainId,
|
||||
);
|
||||
transactionDetail.show(row);
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
|
||||
const { AbiCoder, FetchRequest } = require("ethers");
|
||||
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 EXPLORER_URL = "https://explorer.example.invalid/api/v2";
|
||||
@@ -132,7 +138,6 @@ async function refresh(addresses, trackedTokens) {
|
||||
[{ addresses }],
|
||||
RPC_URL,
|
||||
EXPLORER_URL,
|
||||
true,
|
||||
trackedTokens,
|
||||
"mainnet",
|
||||
new AbortController().signal,
|
||||
|
||||
+11
-51
@@ -1215,11 +1215,13 @@ function respondWith(txItems, tokenTransferItems) {
|
||||
describe("fetchRecentTransactions merge and dedup", () => {
|
||||
beforeEach(() => {
|
||||
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 () => {
|
||||
respondWith([], []);
|
||||
await fetchRecentTransactions(VICTIM, BLOCKSCOUT, true, "0x1");
|
||||
await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
expect(debugFetch).toHaveBeenCalledTimes(2);
|
||||
const urls = debugFetch.mock.calls.map((c) => c[0]);
|
||||
expect(urls).toContain(
|
||||
@@ -1283,12 +1285,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
expect(txs).toHaveLength(1);
|
||||
const merged = txs[0];
|
||||
// The received leg (the swap output) supplies the display amount.
|
||||
@@ -1325,12 +1322,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
expect(txs).toHaveLength(1);
|
||||
expect(txs[0].symbol).toBe("USDC");
|
||||
expect(txs[0].value).toBe("1500.5000");
|
||||
@@ -1356,12 +1348,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
});
|
||||
respondWith([], [leg("1000000"), leg("2000000")]);
|
||||
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
expect(txs).toHaveLength(1);
|
||||
// Keyed by hash plus contract, so the later leg wins.
|
||||
expect(txs[0].exactValue).toBe("2.0");
|
||||
@@ -1401,12 +1388,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
expect(txs.map((t) => t.symbol).sort()).toEqual(["USDC", "WETH"]);
|
||||
});
|
||||
|
||||
@@ -1447,12 +1429,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
expect(txs).toHaveLength(1);
|
||||
expect(txs[0].symbol).toBe("USDC");
|
||||
expect(txs[0].exactValue).toBe("1.0");
|
||||
@@ -1478,13 +1455,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
});
|
||||
respondWith([item(6), item(8), item(7)], []);
|
||||
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
2,
|
||||
);
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1", 2);
|
||||
expect(txs.map((t) => t.blockNumber)).toEqual([21000008, 21000007]);
|
||||
});
|
||||
|
||||
@@ -1510,7 +1481,6 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
const sepolia = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0xaa36a7",
|
||||
);
|
||||
expect(sepolia[0].symbol).toBe("SepoliaETH");
|
||||
@@ -1519,7 +1489,6 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
const mainnet = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(mainnet[0].symbol).toBe("ETH");
|
||||
@@ -1546,12 +1515,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
expect(txs).toHaveLength(1);
|
||||
expect(txs[0].contractAddress).toBe(FAKE_ETH_CONTRACT);
|
||||
expect(txs[0].holders).toBe(0);
|
||||
@@ -1593,7 +1557,6 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(txs[0].holders).toBeNull();
|
||||
@@ -1604,7 +1567,6 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(txs[0].holders).toBeNull();
|
||||
@@ -1615,7 +1577,6 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(filterTransactions(txs, filters()).transactions).toEqual(
|
||||
@@ -1632,7 +1593,6 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(txs[0].holders).toBe(0);
|
||||
@@ -1650,7 +1610,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
},
|
||||
}));
|
||||
await expect(
|
||||
fetchRecentTransactions(VICTIM, BLOCKSCOUT, true, "0x1"),
|
||||
fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1"),
|
||||
).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
@@ -42,8 +42,14 @@ jest.mock("../src/shared/log", () => ({
|
||||
isDebug: () => false,
|
||||
}));
|
||||
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on.
|
||||
globalThis.chrome = {
|
||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||
storage: {
|
||||
local: {
|
||||
get: async () => ({ autistmask: { useBlockscout: true } }),
|
||||
set: async () => {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const uniswap = require("../src/shared/uniswap");
|
||||
|
||||
@@ -34,8 +34,14 @@ jest.mock("../src/shared/log", () => ({
|
||||
isDebug: () => false,
|
||||
}));
|
||||
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on.
|
||||
globalThis.chrome = {
|
||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||
storage: {
|
||||
local: {
|
||||
get: async () => ({ autistmask: { useBlockscout: true } }),
|
||||
set: async () => {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { state } = require("../src/shared/state");
|
||||
@@ -288,7 +294,6 @@ async function historyRow(method, sentTo, chainId) {
|
||||
const rows = await fetchRecentTransactions(
|
||||
RECIPIENT,
|
||||
"https://explorer.example/api/v2",
|
||||
true,
|
||||
chainId,
|
||||
);
|
||||
return rows[0];
|
||||
|
||||
@@ -72,8 +72,12 @@ global.fetch = jest.fn(() => {
|
||||
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");
|
||||
global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
|
||||
global.chrome = {
|
||||
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
|
||||
runtime: { sendMessage() {} },
|
||||
};
|
||||
|
||||
// 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
|
||||
@@ -172,7 +176,7 @@ async function fetchOnto(items) {
|
||||
statusText: "OK",
|
||||
json: async () => items,
|
||||
}));
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||
state.wallets = [
|
||||
{
|
||||
name: "Wallet 1",
|
||||
@@ -197,14 +201,14 @@ async function fetchOntoBoth(itemsA, itemsB) {
|
||||
statusText: "OK",
|
||||
json: async () => itemsA,
|
||||
}));
|
||||
const a = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||
const a = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||
debugFetch.mockImplementation(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
json: async () => itemsB,
|
||||
}));
|
||||
const b = await fetchTokenBalances(SECOND_HOLDER, BLOCKSCOUT, true, []);
|
||||
const b = await fetchTokenBalances(SECOND_HOLDER, BLOCKSCOUT, []);
|
||||
state.wallets = [
|
||||
{
|
||||
name: "Wallet 1",
|
||||
@@ -268,6 +272,9 @@ beforeEach(() => {
|
||||
state.fraudContracts = [];
|
||||
state.hideLowHolderTokens = false;
|
||||
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", () => {
|
||||
|
||||
Reference in new issue
Block a user