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
|
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
|
||||||
@@ -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
|
- 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.
|
||||||
|
|
||||||
Each refresh, in the popup or the background, reads the stored setting when it
|
Blockscout may answer just before the user turns the setting off. So if the
|
||||||
starts. If the setting is off when the refresh saves, the save drops every token
|
stored setting is off when the refresh saves, in the popup or the background,
|
||||||
the user has not tracked.
|
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
|
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
|
||||||
@@ -1916,13 +1918,14 @@ 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. An amount is valid when it is a number above
|
message, no screen change. A name with no address, or one resolved
|
||||||
zero, written as digits with at most one decimal point, with nothing but
|
off-chain, gets "That ENS name has no on-chain address." An amount is
|
||||||
zeros after the token's last decimal place. ETH has 18 places, and a token
|
valid when it is a number above zero, written as digits with at most one
|
||||||
with more, or with an unknown number, is held to 18. Zeros past those
|
decimal point, with nothing but zeros after the token's last decimal
|
||||||
places leave the amount unchanged, so `1.0000000` of a 6-place token
|
place. ETH has 18 places, and a token with more, or with an unknown
|
||||||
passes and is sent as exactly 1. ConfirmTx makes the same check before
|
number, is held to 18. Zeros past those places leave the amount unchanged,
|
||||||
anything can be sent
|
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)
|
- "Back" → previous screen (Home, AddressDetail, or AddressToken)
|
||||||
|
|
||||||
#### ConfirmTx (`confirm-tx`)
|
#### ConfirmTx (`confirm-tx`)
|
||||||
@@ -2798,6 +2801,10 @@ 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
|
||||||
@@ -2833,22 +2840,26 @@ 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**: pressing "Save" beside the URL in Settings asks that
|
- **Saving the URL**: whether or not the setting is on, pressing "Save"
|
||||||
URL for `/stats`. This request carries no user data. It is made whether or
|
beside the URL in Settings asks that URL for `/stats`. This request
|
||||||
not the setting is on.
|
carries no user data.
|
||||||
- **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"): When
|
- **Etherscan address labels** ("Check recipients on Etherscan"): While the
|
||||||
confirming a transaction, the extension performs a best-effort lookup of the
|
setting is on, ConfirmTx looks up the recipient's address on Etherscan each
|
||||||
recipient address on Etherscan to check for phishing/scam labels. This is a
|
time it is shown. It checks for a phishing or scam label, best-effort. This is
|
||||||
direct page fetch with no API key; the user's browser makes the request. While
|
a direct page fetch with no API key; the user's browser makes the request.
|
||||||
the setting is off, ConfirmTx says that no Etherscan check was made.
|
While 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
|
||||||
|
|||||||
+19
-17
@@ -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 type an ENS name into the
|
is open, every 60 seconds in the background), when you review a send to an ENS
|
||||||
Send screen, when a send is prepared and broadcast, while a pending transaction
|
name, when a send is prepared and broadcast, while a pending transaction is
|
||||||
is polled for its receipt, and for the reverse ENS lookups used to label
|
polled for its receipt, and for the reverse ENS lookups used to label addresses
|
||||||
addresses (cached for 12 hours).
|
(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 uses it only while
|
Blockscout is an open-source blockchain explorer. AutistMask sends it your data
|
||||||
"Use Blockscout" is on. It then asks Blockscout for:
|
only while "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
|
||||||
|
|
||||||
Pressing "Save" beside the Blockscout URL in Settings also contacts it, even
|
Whether or not the setting is on, pressing "Save" beside the Blockscout URL in
|
||||||
with the setting off. That request checks the URL by asking it for `/stats`. It
|
Settings asks that URL for `/stats` to check it. That request sends none of your
|
||||||
sends none of your data.
|
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,14 +143,15 @@ address.
|
|||||||
|
|
||||||
**Etherscan address labels** (`etherscan.io`; `sepolia.etherscan.io` on Sepolia)
|
**Etherscan address labels** (`etherscan.io`; `sepolia.etherscan.io` on Sepolia)
|
||||||
|
|
||||||
When you review a send, AutistMask fetches the recipient's public Etherscan
|
While "Check recipients on Etherscan" is on, AutistMask fetches the recipient's
|
||||||
address page. It looks for a "Fake_Phishing"/"Phish/Hack" label or a scam
|
public Etherscan address page when you review a send. It looks for a
|
||||||
warning, and shows a red warning if it finds one. This is a plain page fetch
|
"Fake_Phishing"/"Phish/Hack" label or a scam warning, and shows a red warning if
|
||||||
with no API key, made by your browser. It is best-effort: if it fails, it is
|
it finds one. This is a plain page fetch with no API key, made by your browser.
|
||||||
silently ignored. This endpoint is not user-configurable.
|
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
|
The setting is off in a new install. While it is off, the confirmation screen
|
||||||
setting is off, the confirmation screen says that no Etherscan check was made.
|
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.
|
||||||
@@ -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.
|
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`).
|
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
|
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.
|
||||||
|
|||||||
@@ -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.
|
||||||
//
|
//
|
||||||
// The refresh asks Blockscout only if "Use Blockscout" is on when it starts.
|
// Blockscout may have answered before the user turned "Use Blockscout" off. So
|
||||||
// The user may turn it off while the refresh runs. So when "Use Blockscout"
|
// when "Use Blockscout" stored NOW is off, the save drops every token the user
|
||||||
// stored NOW is off, the save drops every token the user has not tracked.
|
// 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,7 +1405,6 @@ async function backgroundRefresh() {
|
|||||||
wallets,
|
wallets,
|
||||||
s.rpcUrl,
|
s.rpcUrl,
|
||||||
s.blockscoutUrl,
|
s.blockscoutUrl,
|
||||||
s.useBlockscout,
|
|
||||||
s.trackedTokens,
|
s.trackedTokens,
|
||||||
s.networkId,
|
s.networkId,
|
||||||
);
|
);
|
||||||
|
|||||||
+5
-6
@@ -71,9 +71,8 @@ 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 "Use Blockscout"
|
// The refresh first takes any change to the network or to either setting saved
|
||||||
// saved elsewhere (followStoredSettings() below). So it asks Blockscout only if
|
// elsewhere (followStoredSettings() below), so what it draws follows them.
|
||||||
// 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
|
||||||
@@ -85,8 +84,9 @@ 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. If the stored "Use Blockscout" is off
|
// removed meanwhile is left as it is. Blockscout may have answered before the
|
||||||
// when the refresh saves, the save drops every token the user has not tracked.
|
// 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() {
|
async function doRefreshAndRender() {
|
||||||
if (refreshInFlight) return;
|
if (refreshInFlight) return;
|
||||||
refreshInFlight = true;
|
refreshInFlight = true;
|
||||||
@@ -100,7 +100,6 @@ 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,
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ 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, {
|
||||||
|
|||||||
@@ -174,7 +174,6 @@ 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, {
|
||||||
|
|||||||
@@ -197,7 +197,6 @@ async function loadHomeTxs(ctx) {
|
|||||||
fetchRecentTransactions(
|
fetchRecentTransactions(
|
||||||
addr,
|
addr,
|
||||||
state.blockscoutUrl,
|
state.blockscoutUrl,
|
||||||
state.useBlockscout,
|
|
||||||
currentNetwork().chainId,
|
currentNetwork().chainId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -403,8 +403,11 @@ 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 address.");
|
showFlash("That ENS name has no on-chain address.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resolvedTo = resolved;
|
resolvedTo = resolved;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ 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");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -309,10 +310,11 @@ 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 "Use Blockscout" is off, Blockscout is not asked and none
|
// the calldata. While the stored "Use Blockscout" is off, Blockscout is not
|
||||||
// of them is shown.
|
// 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) {
|
async function loadFullTxDetails(txHash, chainId) {
|
||||||
if (!state.useBlockscout) return;
|
if (!(await storedSettingIsOn("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");
|
||||||
@@ -328,6 +330,7 @@ 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);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ 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).
|
||||||
@@ -95,9 +96,10 @@ 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 }].
|
||||||
//
|
//
|
||||||
// `useBlockscout` is the user's "Use Blockscout" setting. While it is off,
|
// While the stored "Use Blockscout" is off, Blockscout is not asked, and an
|
||||||
// Blockscout is not asked and the answer is an empty list. So no token the user
|
// answer that lands after it went off is dropped (src/shared/storedSettings.js).
|
||||||
// has not tracked is listed.
|
// 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,
|
// 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
|
||||||
@@ -114,11 +116,10 @@ function rawUnits(value) {
|
|||||||
async function fetchTokenBalances(
|
async function fetchTokenBalances(
|
||||||
address,
|
address,
|
||||||
blockscoutUrl,
|
blockscoutUrl,
|
||||||
useBlockscout,
|
|
||||||
trackedTokens,
|
trackedTokens,
|
||||||
signal,
|
signal,
|
||||||
) {
|
) {
|
||||||
if (!useBlockscout) return [];
|
if (!(await storedSettingIsOn("useBlockscout"))) return [];
|
||||||
try {
|
try {
|
||||||
const resp = await debugFetch(
|
const resp = await debugFetch(
|
||||||
blockscoutUrl + "/addresses/" + address + "/token-balances",
|
blockscoutUrl + "/addresses/" + address + "/token-balances",
|
||||||
@@ -128,6 +129,7 @@ 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(
|
||||||
@@ -316,13 +318,12 @@ 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.
|
// `trackedTokens` are the tokens tracked on the active network. `signal` is as
|
||||||
// `useBlockscout` and `signal` are as for fetchTokenBalances().
|
// for fetchTokenBalances().
|
||||||
async function refreshBalances(
|
async function refreshBalances(
|
||||||
wallets,
|
wallets,
|
||||||
rpcUrl,
|
rpcUrl,
|
||||||
blockscoutUrl,
|
blockscoutUrl,
|
||||||
useBlockscout,
|
|
||||||
trackedTokens,
|
trackedTokens,
|
||||||
networkId,
|
networkId,
|
||||||
signal,
|
signal,
|
||||||
@@ -391,7 +392,6 @@ async function refreshBalances(
|
|||||||
const explorerRequest = fetchTokenBalances(
|
const explorerRequest = fetchTokenBalances(
|
||||||
addr.address,
|
addr.address,
|
||||||
blockscoutUrl,
|
blockscoutUrl,
|
||||||
useBlockscout,
|
|
||||||
tracked,
|
tracked,
|
||||||
signal,
|
signal,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
// 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 { storageGet } = require("./browserApi");
|
const { storedSettingIsOn } = require("./storedSettings");
|
||||||
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"
|
||||||
@@ -80,11 +79,9 @@ 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.
|
||||||
*
|
*
|
||||||
* Etherscan is asked only while "Check recipients on Etherscan" is on in the
|
* While the stored "Check recipients on Etherscan" is off, Etherscan is not
|
||||||
* stored profile. The page's own copy of the setting is not used, because
|
* asked, and an answer that lands after it went off is dropped
|
||||||
* another page may have just changed it. The stored setting is read through
|
* (src/shared/storedSettings.js). Either way the answer is null.
|
||||||
* 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.
|
||||||
@@ -92,8 +89,7 @@ function parseEtherscanPage(html) {
|
|||||||
*/
|
*/
|
||||||
async function checkEtherscanLabel(address, chainId) {
|
async function checkEtherscanLabel(address, chainId) {
|
||||||
try {
|
try {
|
||||||
const { autistmask } = await storageGet("autistmask");
|
if (!(await storedSettingIsOn("checkRecipientsOnEtherscan"))) {
|
||||||
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
|
||||||
@@ -106,6 +102,9 @@ 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 {
|
||||||
@@ -117,7 +116,6 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
// 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
|
||||||
@@ -236,16 +237,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`).
|
||||||
//
|
//
|
||||||
// `useBlockscout` is the user's "Use Blockscout" setting. While it is off,
|
// While the stored "Use Blockscout" is off, Blockscout is not asked, and an
|
||||||
// Blockscout is not asked and the answer is an empty list.
|
// answer that lands after it went off is dropped (src/shared/storedSettings.js).
|
||||||
|
// 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 (!useBlockscout) return [];
|
if (!(await storedSettingIsOn("useBlockscout"))) return [];
|
||||||
log.debugf("fetchRecentTransactions", address);
|
log.debugf("fetchRecentTransactions", address);
|
||||||
const addrLower = normalizeAddress(address);
|
const addrLower = normalizeAddress(address);
|
||||||
|
|
||||||
@@ -276,6 +277,7 @@ 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)),
|
||||||
|
|||||||
@@ -412,14 +412,7 @@ describe("a balance refresh under a network switch", () => {
|
|||||||
const ranOn = [];
|
const ranOn = [];
|
||||||
|
|
||||||
const bg = loadWorker("sepolia", {
|
const bg = loadWorker("sepolia", {
|
||||||
refreshBalances: async (
|
refreshBalances: async (wallets, rpcUrl, explorer, tokens, id) => {
|
||||||
wallets,
|
|
||||||
rpcUrl,
|
|
||||||
explorer,
|
|
||||||
useBlockscout,
|
|
||||||
tokens,
|
|
||||||
id,
|
|
||||||
) => {
|
|
||||||
ranOn.push(id);
|
ranOn.push(id);
|
||||||
reachedNetwork.resolve();
|
reachedNetwork.resolve();
|
||||||
await roundTrip.promise;
|
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", () => {
|
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). A refresh that started while it was on
|
// (src/popup/views/settings.js). Blockscout may have answered a refresh
|
||||||
// still saves its balances when it returns, but not those tokens. The same
|
// just before. That refresh still saves its balances when it returns, but
|
||||||
// refresh in the popup is in tests/blockscoutEtherscanSettings.test.js.
|
// 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 () => {
|
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 (
|
refreshBalances: async (wallets) => {
|
||||||
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";
|
||||||
@@ -504,7 +491,6 @@ 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([]);
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ 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;
|
||||||
|
|
||||||
@@ -54,7 +60,6 @@ function refresh(signal) {
|
|||||||
wallets,
|
wallets,
|
||||||
RPC_URL,
|
RPC_URL,
|
||||||
EXPLORER_URL,
|
EXPLORER_URL,
|
||||||
true,
|
|
||||||
[TRACKED],
|
[TRACKED],
|
||||||
"mainnet",
|
"mainnet",
|
||||||
signal,
|
signal,
|
||||||
|
|||||||
@@ -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[5]).toBe("sepolia");
|
expect(refreshBalances.mock.lastCall[4]).toBe("sepolia");
|
||||||
expect(page.storage.read("autistmask").lastBalanceRefresh).toBeGreaterThan(
|
expect(page.storage.read("autistmask").lastBalanceRefresh).toBeGreaterThan(
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
// 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.
|
||||||
//
|
//
|
||||||
// The balance refresh and the Etherscan check follow the stored setting, not
|
// A request that either setting gates reads the stored setting, not the page's
|
||||||
// the page's copy of it. So a change another page saved applies to them at
|
// copy, before it asks and again when the answer lands. So a change another
|
||||||
// once. An open page also takes such a change and draws its screens again.
|
// 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
|
// 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.
|
||||||
@@ -40,6 +41,7 @@ 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";
|
||||||
@@ -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;
|
const realFetch = globalThis.fetch;
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -91,33 +112,55 @@ 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(async (url) => {
|
globalThis.fetch = jest.fn(recordAndAnswer);
|
||||||
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 () => {
|
||||||
expect(
|
storing({ useBlockscout: false });
|
||||||
await fetchTokenBalances(ADDRESS, BLOCKSCOUT, false, []),
|
expect(await fetchTokenBalances(ADDRESS, BLOCKSCOUT, [])).toEqual([]);
|
||||||
).toEqual([]);
|
|
||||||
expect(asked).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(rows.map((row) => row.symbol)).toEqual(["USDC"]);
|
||||||
expect(asked).toEqual([
|
expect(asked).toEqual([
|
||||||
BLOCKSCOUT + "/addresses/" + ADDRESS + "/token-balances",
|
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 () => {
|
test("the transaction history is asked for only while Use Blockscout is on", async () => {
|
||||||
|
storing({ useBlockscout: false });
|
||||||
expect(
|
expect(
|
||||||
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, false, "0x1"),
|
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1"),
|
||||||
).toEqual([]);
|
).toEqual([]);
|
||||||
expect(asked).toEqual([]);
|
expect(asked).toEqual([]);
|
||||||
|
|
||||||
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, true, "0x1");
|
storing({ useBlockscout: true });
|
||||||
|
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1");
|
||||||
expect(asked.sort()).toEqual([
|
expect(asked.sort()).toEqual([
|
||||||
BLOCKSCOUT +
|
BLOCKSCOUT +
|
||||||
"/addresses/" +
|
"/addresses/" +
|
||||||
@@ -146,26 +191,78 @@ describe("where each request is made", () => {
|
|||||||
getTransactionCount: async () => 1,
|
getTransactionCount: async () => 1,
|
||||||
getBalance: async () => 0n,
|
getBalance: async () => 0n,
|
||||||
};
|
};
|
||||||
const checkWith = async (stored) => {
|
const checkWith = async (profile) => {
|
||||||
globalThis.chrome = { storage: makeStorageStub(stored) };
|
storing(profile);
|
||||||
try {
|
await getFullWarnings(RECIPIENT, provider, { chainId: "0x1" });
|
||||||
await getFullWarnings(RECIPIENT, provider, { chainId: "0x1" });
|
|
||||||
} finally {
|
|
||||||
delete globalThis.chrome;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await checkWith({});
|
await checkWith(undefined);
|
||||||
await checkWith({ autistmask: { checkRecipientsOnEtherscan: false } });
|
await checkWith({ 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({ autistmask: { checkRecipientsOnEtherscan: "true" } });
|
await checkWith({ checkRecipientsOnEtherscan: "true" });
|
||||||
await checkWith({ autistmask: { checkRecipientsOnEtherscan: 42 } });
|
await checkWith({ checkRecipientsOnEtherscan: 42 });
|
||||||
expect(asked).toEqual([]);
|
expect(asked).toEqual([]);
|
||||||
|
|
||||||
await checkWith({ autistmask: { checkRecipientsOnEtherscan: true } });
|
await checkWith({ 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.
|
||||||
@@ -212,11 +309,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",
|
||||||
);
|
);
|
||||||
@@ -258,29 +355,6 @@ 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(
|
||||||
@@ -341,6 +415,51 @@ 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) {
|
||||||
@@ -628,24 +747,22 @@ describe("the popup", () => {
|
|||||||
["off", true, false],
|
["off", true, false],
|
||||||
["on", false, true],
|
["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) => {
|
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(
|
held.mockImplementationOnce(async (wallets) => {
|
||||||
async (wallets, rpcUrl, blockscoutUrl, useBlockscout) => {
|
const addr = wallets[0].addresses[0];
|
||||||
const addr = wallets[0].addresses[0];
|
addr.balance = "7.0";
|
||||||
addr.balance = "7.0";
|
addr.tokenBalances = after
|
||||||
addr.tokenBalances = useBlockscout
|
? [tokenRow(TRACKED, "TRK"), tokenRow(UNTRACKED, "TST")]
|
||||||
? [tokenRow(TRACKED, "TRK"), tokenRow(UNTRACKED, "TST")]
|
: [tokenRow(TRACKED, "TRK")];
|
||||||
: [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");
|
||||||
@@ -722,65 +839,3 @@ 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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -108,7 +108,10 @@ global.fetch = jest.fn(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { makeStorageStub } = require("./support/storageStub");
|
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.
|
// A stub DOM: every id resolves to a recording element.
|
||||||
const elements = new Map();
|
const elements = new Map();
|
||||||
@@ -243,11 +246,13 @@ 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",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,8 +35,11 @@ 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 = { storage: makeStorageStub() };
|
global.chrome = {
|
||||||
|
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");
|
||||||
@@ -114,7 +117,6 @@ 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
@@ -21,7 +21,11 @@ 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");
|
||||||
});
|
});
|
||||||
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 {
|
const {
|
||||||
LOW_HOLDER_THRESHOLD,
|
LOW_HOLDER_THRESHOLD,
|
||||||
@@ -150,23 +154,19 @@ 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, true, []);
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||||
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, true, [])).toEqual(
|
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).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, true, [])).toEqual(
|
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).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, true, [
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, [
|
||||||
{ 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, true, []);
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||||
expect(balances).toHaveLength(1);
|
expect(balances).toHaveLength(1);
|
||||||
expect(balances[0].holders).toBeNull();
|
expect(balances[0].holders).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -436,8 +436,13 @@ 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.
|
// The fee is Blockscout's, asked for only while the stored "Use
|
||||||
|
// 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,
|
||||||
|
|||||||
@@ -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[5]).toBe("sepolia");
|
expect(refreshBalances.mock.lastCall[4]).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);
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ 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",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ let approvalDetails = null;
|
|||||||
|
|
||||||
const { makeStorageStub } = require("./support/storageStub");
|
const { makeStorageStub } = require("./support/storageStub");
|
||||||
global.chrome = {
|
global.chrome = {
|
||||||
storage: makeStorageStub(),
|
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
|
||||||
runtime: {
|
runtime: {
|
||||||
connect: () => ({
|
connect: () => ({
|
||||||
postMessage() {},
|
postMessage() {},
|
||||||
@@ -220,11 +220,13 @@ 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",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -100,7 +100,10 @@ global.fetch = jest.fn(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { makeStorageStub } = require("./support/storageStub");
|
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.
|
// A stub DOM: every id resolves to a recording element.
|
||||||
const elements = new Map();
|
const elements = new Map();
|
||||||
@@ -213,11 +216,13 @@ 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",
|
||||||
);
|
);
|
||||||
|
|||||||
+17
-24
@@ -52,8 +52,14 @@ 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: { local: { get: async () => ({}), set: async () => {} } },
|
storage: {
|
||||||
|
local: {
|
||||||
|
get: async () => ({ autistmask: { useBlockscout: true } }),
|
||||||
|
set: async () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { isSpoofedSymbol } = require("../src/shared/symbolSpoof");
|
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.
|
// 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, true, [])).toEqual(
|
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).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, true, [
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, [
|
||||||
{ address: FAKE_ETH_CONTRACT },
|
{ address: FAKE_ETH_CONTRACT },
|
||||||
]);
|
]);
|
||||||
expect(balances).toEqual([]);
|
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.
|
// 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, true, [])).toEqual(
|
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).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, true, [])).toEqual(
|
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).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, true, []);
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||||
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, true, []);
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||||
expect(balances).toHaveLength(1);
|
expect(balances).toHaveLength(1);
|
||||||
expect(balances[0].symbol).toBe("USDC");
|
expect(balances[0].symbol).toBe("USDC");
|
||||||
});
|
});
|
||||||
@@ -652,9 +652,7 @@ 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, true, [])).toEqual(
|
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).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
|
||||||
@@ -672,20 +670,16 @@ describe("surface 3: the balance list", () => {
|
|||||||
decimals: "6",
|
decimals: "6",
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||||
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, true, [])).toEqual(
|
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||||
[],
|
|
||||||
);
|
|
||||||
respondWith([fakeEthItem({ type: "ERC-20-EXTRA" })]);
|
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
|
// 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] }],
|
[{ addresses: [addr] }],
|
||||||
"https://rpc.example.invalid",
|
"https://rpc.example.invalid",
|
||||||
BLOCKSCOUT,
|
BLOCKSCOUT,
|
||||||
true,
|
|
||||||
[],
|
[],
|
||||||
"mainnet",
|
"mainnet",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,8 +38,14 @@ 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: { local: { get: async () => ({}), set: async () => {} } },
|
storage: {
|
||||||
|
local: {
|
||||||
|
get: async () => ({ autistmask: { useBlockscout: true } }),
|
||||||
|
set: async () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { state } = require("../src/shared/state");
|
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(
|
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);
|
||||||
@@ -211,7 +216,6 @@ 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,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -240,7 +244,6 @@ 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;
|
||||||
@@ -262,7 +265,6 @@ 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);
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
|
|
||||||
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";
|
||||||
@@ -132,7 +138,6 @@ 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,
|
||||||
|
|||||||
+11
-51
@@ -1215,11 +1215,13 @@ 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, true, "0x1");
|
await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "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(
|
||||||
@@ -1283,12 +1285,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(
|
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||||
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.
|
||||||
@@ -1325,12 +1322,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(
|
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||||
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");
|
||||||
@@ -1356,12 +1348,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
});
|
});
|
||||||
respondWith([], [leg("1000000"), leg("2000000")]);
|
respondWith([], [leg("1000000"), leg("2000000")]);
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(
|
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||||
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");
|
||||||
@@ -1401,12 +1388,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(
|
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||||
VICTIM,
|
|
||||||
BLOCKSCOUT,
|
|
||||||
true,
|
|
||||||
"0x1",
|
|
||||||
);
|
|
||||||
expect(txs.map((t) => t.symbol).sort()).toEqual(["USDC", "WETH"]);
|
expect(txs.map((t) => t.symbol).sort()).toEqual(["USDC", "WETH"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1447,12 +1429,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(
|
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||||
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");
|
||||||
@@ -1478,13 +1455,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
});
|
});
|
||||||
respondWith([item(6), item(8), item(7)], []);
|
respondWith([item(6), item(8), item(7)], []);
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(
|
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1", 2);
|
||||||
VICTIM,
|
|
||||||
BLOCKSCOUT,
|
|
||||||
true,
|
|
||||||
"0x1",
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
expect(txs.map((t) => t.blockNumber)).toEqual([21000008, 21000007]);
|
expect(txs.map((t) => t.blockNumber)).toEqual([21000008, 21000007]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1510,7 +1481,6 @@ 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");
|
||||||
@@ -1519,7 +1489,6 @@ 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");
|
||||||
@@ -1546,12 +1515,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(
|
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||||
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);
|
||||||
@@ -1593,7 +1557,6 @@ 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();
|
||||||
@@ -1604,7 +1567,6 @@ 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();
|
||||||
@@ -1615,7 +1577,6 @@ 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(
|
||||||
@@ -1632,7 +1593,6 @@ 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);
|
||||||
@@ -1650,7 +1610,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
await expect(
|
await expect(
|
||||||
fetchRecentTransactions(VICTIM, BLOCKSCOUT, true, "0x1"),
|
fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1"),
|
||||||
).resolves.toEqual([]);
|
).resolves.toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -42,8 +42,14 @@ 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: { local: { get: async () => ({}), set: async () => {} } },
|
storage: {
|
||||||
|
local: {
|
||||||
|
get: async () => ({ autistmask: { useBlockscout: true } }),
|
||||||
|
set: async () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const uniswap = require("../src/shared/uniswap");
|
const uniswap = require("../src/shared/uniswap");
|
||||||
|
|||||||
@@ -34,8 +34,14 @@ 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: { local: { get: async () => ({}), set: async () => {} } },
|
storage: {
|
||||||
|
local: {
|
||||||
|
get: async () => ({ autistmask: { useBlockscout: true } }),
|
||||||
|
set: async () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { state } = require("../src/shared/state");
|
const { state } = require("../src/shared/state");
|
||||||
@@ -288,7 +294,6 @@ 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];
|
||||||
|
|||||||
@@ -72,8 +72,12 @@ 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 = { 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
|
// 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
|
||||||
@@ -172,7 +176,7 @@ async function fetchOnto(items) {
|
|||||||
statusText: "OK",
|
statusText: "OK",
|
||||||
json: async () => items,
|
json: async () => items,
|
||||||
}));
|
}));
|
||||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||||
state.wallets = [
|
state.wallets = [
|
||||||
{
|
{
|
||||||
name: "Wallet 1",
|
name: "Wallet 1",
|
||||||
@@ -197,14 +201,14 @@ async function fetchOntoBoth(itemsA, itemsB) {
|
|||||||
statusText: "OK",
|
statusText: "OK",
|
||||||
json: async () => itemsA,
|
json: async () => itemsA,
|
||||||
}));
|
}));
|
||||||
const a = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
const a = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||||
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, true, []);
|
const b = await fetchTokenBalances(SECOND_HOLDER, BLOCKSCOUT, []);
|
||||||
state.wallets = [
|
state.wallets = [
|
||||||
{
|
{
|
||||||
name: "Wallet 1",
|
name: "Wallet 1",
|
||||||
@@ -268,6 +272,9 @@ 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", () => {
|
||||||
|
|||||||
Reference in new issue
Block a user