fix: Blockscout and Etherscan lookups are off until the user turns them on (closes #668)
Out of the box the wallet sent the user's addresses to Blockscout for history and token lookups. It sent each recipient to Etherscan for its label. Two settings, "Use Blockscout" and "Check recipients on Etherscan", now gate those requests where they are made. Both are off in a new install and offered unticked on Welcome and in Settings. While one is off, the screens that relied on it say so in a plain line naming the setting. Unticking "Use Blockscout" drops every untracked token from every address. After either changes in Settings, Back draws again any screen a reopened popup can draw. ENS lookups ask only the RPC endpoint. Model: opus-5-5
This commit is contained in:
@@ -12,16 +12,27 @@ intercepts tx status links to their own site instead of going to Etherscan, etc.
|
||||
None of the common alternatives work on Firefox.
|
||||
|
||||
Hence, a minimally viable ERC20 browser wallet/signer that works cross-platform.
|
||||
Everything you need, nothing you don't. We import as few libraries as possible,
|
||||
don't implement any crypto, and don't send user-specific data anywhere but a
|
||||
(user-configurable) Ethereum RPC endpoint (which defaults to a public node). The
|
||||
extension contacts three user-configurable services: the configured RPC node for
|
||||
blockchain interactions, a public CoinDesk API (no API key) for realtime price
|
||||
information, and a Blockscout block-explorer API for transaction history and
|
||||
token balances. It also performs best-effort Etherscan address label lookups
|
||||
during transaction confirmation. A community-maintained phishing domain
|
||||
blocklist is built into the extension at build time and checked locally; nothing
|
||||
is fetched for it at runtime.
|
||||
Everything you need, nothing you don't. We import as few libraries as possible
|
||||
and don't implement any crypto. By default the wallet sends user-specific data
|
||||
only to the Ethereum RPC endpoint. The user can change that endpoint in
|
||||
Settings; it defaults to a public node.
|
||||
|
||||
Out of the box the extension contacts two services:
|
||||
|
||||
- the configured RPC node, for blockchain interactions
|
||||
- a public CoinDesk API (no API key), for realtime prices. It is sent token
|
||||
symbols and nothing else.
|
||||
|
||||
Two more services are sent none of the user's data until the user turns them on.
|
||||
Each has a checkbox on the Welcome screen and in Settings:
|
||||
|
||||
- "Use Blockscout": a Blockscout block-explorer API, for transaction history,
|
||||
the tokens the user has not tracked, and transaction details
|
||||
- "Check recipients on Etherscan": a best-effort Etherscan label lookup of the
|
||||
recipient during transaction confirmation
|
||||
|
||||
A community-maintained phishing domain blocklist is built into the extension at
|
||||
build time and checked locally. Nothing is fetched for it at runtime.
|
||||
|
||||
In the extension is a hardcoded list of the top ERC20 contract addresses. You
|
||||
can add any ERC20 contract by contract address if you wish, but the hardcoded
|
||||
@@ -422,6 +433,35 @@ It also covers the red banner on the Settings screen: switching the network
|
||||
there to Sepolia must put up the `[TESTNET]` banner, and switching back to
|
||||
mainnet must take it down, both while Settings is still on screen.
|
||||
|
||||
It also covers what a new install sends to Blockscout and Etherscan
|
||||
([#668](https://git.eeqj.de/sneak/AutistMask/issues/668)). The harness installs
|
||||
a route handler before the first popup page opens. From then on, the handler
|
||||
records every request the extension makes, from its pages and from its
|
||||
background worker. A request the background worker makes while it starts up
|
||||
comes earlier and is not recorded. The tests take these steps in order:
|
||||
|
||||
- Welcome must offer "Use Blockscout" and "Check recipients on Etherscan"
|
||||
unticked.
|
||||
- With neither ticked, Home, AddressDetail, AddressToken and ConfirmTx must each
|
||||
show the line saying what is off.
|
||||
- The popup is then closed until the background has run a balance refresh of its
|
||||
own. No recorded request may have gone to Blockscout or Etherscan.
|
||||
- "Use Blockscout" is ticked from AddressDetail's gear. On "Back", AddressDetail
|
||||
must ask Blockscout for its history and no longer say the setting is off.
|
||||
Blockscout must then be asked for the token balances.
|
||||
- The stub Blockscout then reports a token the user has not tracked. Home lists
|
||||
it. "Use Blockscout" is unticked from AddressDetail's gear. On "Back", the
|
||||
token must be gone from AddressDetail, from Send's token dropdown and from
|
||||
Home. Those three screens must each show the line saying the setting is off.
|
||||
The setting is then ticked again.
|
||||
- "Check recipients on Etherscan" is ticked from ConfirmTx's gear. On "Back",
|
||||
ConfirmTx must ask Etherscan about the recipient and show the label the stub
|
||||
page gives it.
|
||||
|
||||
Both settings stay on for the rest of the run. TransactionDetail with "Use
|
||||
Blockscout" off is not driven here, because no history row leads to it then. The
|
||||
unit tests cover it.
|
||||
|
||||
It also covers the confirmation screen, for both a native ETH send and an ERC-20
|
||||
send: Send disabled while the fee estimate is in flight, enabled once it lands,
|
||||
the fee block quoting the expected cost and the reserve separately, the distinct
|
||||
@@ -845,7 +885,8 @@ src/
|
||||
views/ — one JS module per screen (home, send, approval, etc.)
|
||||
shared/ — modules used by both popup and background
|
||||
alarms.js — recurring background jobs (extension alarms API)
|
||||
balances.js — ETH and tracked token balances via RPC, others via Blockscout
|
||||
balances.js — ETH and tracked token balances via RPC, others via
|
||||
Blockscout while "Use Blockscout" is on
|
||||
constants.js — chain IDs, default RPC endpoint, ERC-20 ABI
|
||||
ens.js — ENS forward/reverse resolution (popup only)
|
||||
holders.js — holder-count parsing and the low-holder rule
|
||||
@@ -1388,23 +1429,36 @@ that token's previous entry, as a failed ETH balance keeps its figure, or takes
|
||||
Blockscout's entry from the same refresh when there is none. Every other token
|
||||
comes from the Blockscout `token-balances` response (`fetchTokenBalances()`), so
|
||||
tokens do appear without the user adding them, and they keep their previous
|
||||
figures while Blockscout fails. 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 more holders. A token claiming a symbol
|
||||
from the bundled list from any other contract address is always dropped, tracked
|
||||
or not, and so is any token claiming a symbol that belongs to the native asset
|
||||
and therefore has no legitimate contract at all (`"ETH"`, and every network's
|
||||
`nativeCurrency`, such as `"SepoliaETH"`, on every network). That filter is
|
||||
unconditional — the "Hide tokens with fewer than 1,000 holders" setting governs
|
||||
the transaction history and the send-screen token selector, not this list. A
|
||||
token's holder count is unknown when the explorer reports none, or reports
|
||||
anything other than a whole number written in digits alone, such as `1,000` or
|
||||
`1e3` (`parseHoldersCount()` in `src/shared/holders.js`). This list does not
|
||||
take an unknown count as 1,000 or more, so such a token is shown only when it is
|
||||
on the bundled list or tracked. Every nonzero holding of a token shown is stored
|
||||
exactly, however small, but a holding below 0.000001 is left out of the balance
|
||||
lists, the send-screen token selector, the address total and the remove-address
|
||||
warning (`isBelowOneMillionth()` in `src/shared/amountDisplay.js`). The Send and
|
||||
figures while Blockscout fails.
|
||||
|
||||
The refresh asks Blockscout only while the user has "Use Blockscout" on. The
|
||||
setting is off until the user turns it on. While it is off:
|
||||
|
||||
- Only tracked tokens are listed. Turning the setting off in Settings drops
|
||||
every other token from every address at once.
|
||||
- A tracked token's entry has no holder count and no `decimals` from Blockscout.
|
||||
- A tracked token's balance uses the decimals from the bundled list, or else the
|
||||
decimals saved when the token was tracked.
|
||||
|
||||
A refresh that started before the setting changed saves nothing when it returns.
|
||||
|
||||
A tracked token is shown when its balance is nonzero, and any other ERC-20 when
|
||||
its balance is nonzero and it is in the bundled known-token list or has 1,000 or
|
||||
more holders. A token claiming a symbol from the bundled list from any other
|
||||
contract address is always dropped, tracked or not, and so is any token claiming
|
||||
a symbol that belongs to the native asset and therefore has no legitimate
|
||||
contract at all (`"ETH"`, and every network's `nativeCurrency`, such as
|
||||
`"SepoliaETH"`, on every network). That filter is unconditional — the "Hide
|
||||
tokens with fewer than 1,000 holders" setting governs the transaction history
|
||||
and the send-screen token selector, not this list. A token's holder count is
|
||||
unknown when the explorer reports none, or reports anything other than a whole
|
||||
number written in digits alone, such as `1,000` or `1e3` (`parseHoldersCount()`
|
||||
in `src/shared/holders.js`). This list does not take an unknown count as 1,000
|
||||
or more, so such a token is shown only when it is on the bundled list or
|
||||
tracked. Every nonzero holding of a token shown is stored exactly, however
|
||||
small, but a holding below 0.000001 is left out of the balance lists, the
|
||||
send-screen token selector, the address total and the remove-address warning
|
||||
(`isBelowOneMillionth()` in `src/shared/amountDisplay.js`). The Send and
|
||||
confirmation screens show it when its token is the one being sent. Tracked
|
||||
tokens with a zero balance are listed as well while "Show tracked tokens with
|
||||
zero balance" is on.
|
||||
@@ -1578,6 +1632,13 @@ such as an edit typed into Settings and not yet saved. Home is the one screen
|
||||
"Back" always re-renders, so the wallet list reflects anything that changed
|
||||
while the user was away from it.
|
||||
|
||||
Changing "Use Blockscout" or "Check recipients on Etherscan" in Settings makes
|
||||
every screen but Settings count as not yet rendered. "Back" then renders again
|
||||
any screen a reopened popup can render. A change to "Use Blockscout" also cuts
|
||||
the stack, as a network switch does (below). The cut drops Send, whose token
|
||||
dropdown was filled under the old setting. "Back" cannot render Send again. A
|
||||
change to "Check recipients on Etherscan" leaves the stack as it is.
|
||||
|
||||
A network switch leaves every screen on the page showing the previous network.
|
||||
The user makes it in Settings, or approves a site's request and the background
|
||||
saves it. Any other popup open meanwhile takes the switch from the stored
|
||||
@@ -1619,8 +1680,15 @@ view would leave a wallet one click from deletion.
|
||||
screen in that case.
|
||||
- **Elements**:
|
||||
- "Welcome! To get started, add a wallet." text
|
||||
- A well with a line saying that the wallet sends the user's data only to
|
||||
the Ethereum RPC endpoint unless one of the two checkboxes below is ticked
|
||||
- In the well, the "Use Blockscout" and "Check recipients on Etherscan"
|
||||
checkboxes, the same two as in Settings. Each has a sentence saying what
|
||||
it sends and where. Both are unticked until the user ticks them
|
||||
- "Add wallet" button
|
||||
- **Transitions**:
|
||||
- Ticking or unticking either checkbox → stores the setting, as Settings
|
||||
does (no screen change)
|
||||
- "Add wallet" → **AddWallet**
|
||||
|
||||
#### Home (`main`)
|
||||
@@ -1641,6 +1709,8 @@ view would leave a wallet one click from deletion.
|
||||
holding more than one address); the ENS name, if resolved, is below it;
|
||||
then the full address on a row of its own, followed by the address USD
|
||||
total and a balance line for ETH and for each token shown for that address
|
||||
- While "Use Blockscout" is off, a line under the wallet list: "Only tracked
|
||||
tokens are listed, because "Use Blockscout" is off in Settings."
|
||||
- "Recent Transactions": up to 25 transactions merged across every address
|
||||
of every wallet, deduplicated by hash and filtered. Each row is three
|
||||
lines: age and direction, then the counterparty's colour dot (with our own
|
||||
@@ -1648,7 +1718,11 @@ view would leave a wallet one click from deletion.
|
||||
counterparty's full address on a row of its own. A contract creation has
|
||||
no counterparty: its second line is the amount alone and its third line
|
||||
says "This transaction creates a new contract. It has no recipient." The
|
||||
transaction lists on AddressDetail and AddressToken draw the same rows
|
||||
transaction lists on AddressDetail and AddressToken draw the same rows.
|
||||
The history comes from Blockscout. While "Use Blockscout" is off,
|
||||
Blockscout is not asked. This list then shows only the line "No
|
||||
transaction history is shown, because "Use Blockscout" is off in
|
||||
Settings."
|
||||
- "Add additional wallet..." link at bottom
|
||||
- **Transitions**:
|
||||
- Tap address row → sets the active address and broadcasts
|
||||
@@ -1717,9 +1791,13 @@ view would leave a wallet one click from deletion.
|
||||
- USD total for address (see [Partial USD totals](#partial-usd-totals))
|
||||
- Balance list: ETH + the ERC-20 tokens shown for this address (4 decimal
|
||||
places, USD inline). Each balance row is clickable → **AddressToken**
|
||||
- While "Use Blockscout" is off, a line under the balance list: "Only
|
||||
tracked tokens are listed, because "Use Blockscout" is off in Settings."
|
||||
- Send / Receive / + Token buttons and a "···" menu button
|
||||
- "···" dropdown containing a single "Export Private Key" entry
|
||||
- Transaction list (with ENS resolution for counterparties)
|
||||
- Transaction list (with ENS resolution for counterparties). While "Use
|
||||
Blockscout" is off, it shows only the line "No transaction history is
|
||||
shown, because "Use Blockscout" is off in Settings."
|
||||
- **Transitions**:
|
||||
- Tap balance row → **AddressToken** (for that token)
|
||||
- "Send" → **Send** (refuses with a flash message on a zero balance)
|
||||
@@ -1787,9 +1865,13 @@ view would leave a wallet one click from deletion.
|
||||
- Token contract well (ERC-20 only): full contract address (tap to copy,
|
||||
etherscan link) plus name, symbol, decimals, holder count and project
|
||||
website where known. The "Holders:" row is left out, not shown as 0, when
|
||||
the token's balance-list entry has no holder count: the explorer did not
|
||||
report a readable one, or the token is not in the balance list
|
||||
- Token-filtered transaction list (only this token's transfers)
|
||||
the token's balance-list entry has no holder count. That happens when:
|
||||
- the explorer did not report a readable one
|
||||
- "Use Blockscout" is off
|
||||
- the token is not in the balance list
|
||||
- Token-filtered transaction list (only this token's transfers). While "Use
|
||||
Blockscout" is off, it shows only the line "No transaction history is
|
||||
shown, because "Use Blockscout" is off in Settings."
|
||||
- **Transitions**:
|
||||
- "Send" → **Send** (token locked: the dropdown is replaced by a static
|
||||
symbol and contract address)
|
||||
@@ -1806,6 +1888,9 @@ view would leave a wallet one click from deletion.
|
||||
- From: address with color dot + etherscan link
|
||||
- What to send: token dropdown (or static display with contract address when
|
||||
locked from AddressToken)
|
||||
- While "Use Blockscout" is off, a line under the token dropdown: "Only
|
||||
tracked tokens are listed, because "Use Blockscout" is off in Settings."
|
||||
The line is hidden with the dropdown when Send is opened from AddressToken
|
||||
- To: address or ENS name input, with an inline validation message
|
||||
- Amount input with current balance display, which reads
|
||||
`Current balance: unknown (SYMBOL)` for a token whose scale is unknown, as
|
||||
@@ -1875,7 +1960,12 @@ view would leave a wallet one click from deletion.
|
||||
recipient that has never sent a transaction and holds no ETH, recipient is
|
||||
a contract, burn address, and an Etherscan phishing/scam label. An address
|
||||
that has received ETH but never sent a transaction still holds that ETH,
|
||||
so it gets no warning
|
||||
so it gets no warning. The Etherscan label check is made only while "Check
|
||||
recipients on Etherscan" is on. While it is off, the Etherscan box is left
|
||||
out. A line in its place says "This address was not checked on Etherscan,
|
||||
because "Check recipients on Etherscan" is off in Settings." The box or
|
||||
the line is chosen when the screen is drawn, so neither moves anything
|
||||
later
|
||||
- Errors (an invalid amount), plus five reserved error boxes, so the balance
|
||||
and the fee arriving later move nothing — the amount exceeds the balance
|
||||
(for a token whose scale is unknown it says the token's balance is
|
||||
@@ -2041,7 +2131,11 @@ view would leave a wallet one click from deletion.
|
||||
AddressToken. The screen describes the transaction on the network it was sent
|
||||
on, whichever network is active: its block, network details, decoded details
|
||||
and raw data are read from that network's Blockscout, the one Settings has for
|
||||
it, and its etherscan links open that network's explorer.
|
||||
it, and its etherscan links open that network's explorer. The block, network
|
||||
details, decoded details and raw data are read only while "Use Blockscout" is
|
||||
on. While it is off, a line in their place says "No block, decoded details,
|
||||
network details or raw data are shown, because "Use Blockscout" is off in
|
||||
Settings."
|
||||
- **Elements** (grouped into logical blocks using light well containers; field
|
||||
labels are self-explanatory so groups have no headings):
|
||||
- "Transaction" heading, "Back" button
|
||||
@@ -2124,8 +2218,21 @@ view would leave a wallet one click from deletion.
|
||||
already active changes nothing and sends nothing
|
||||
- Ethereum RPC: endpoint URL input + "Save" button (validated against
|
||||
`eth_chainId` before being saved)
|
||||
- Blockscout API: endpoint URL input + "Save" button (validated against
|
||||
`/stats` before being saved)
|
||||
- Blockscout API:
|
||||
- "Use Blockscout" checkbox, unticked until the user ticks it
|
||||
- A sentence saying what it shows and what it sends. It sends the
|
||||
Blockscout API the user's addresses and the hash of each transaction
|
||||
opened
|
||||
- Endpoint URL input + "Save" button. "Save" checks the URL by asking it
|
||||
for `/stats` before saving it. That request carries no user data. It
|
||||
is made whether or not "Use Blockscout" is ticked.
|
||||
- Etherscan:
|
||||
- "Check recipients on Etherscan" checkbox, unticked until the user
|
||||
ticks it
|
||||
- A sentence saying it sends each recipient's address to Etherscan to
|
||||
check it for a phishing or scam label
|
||||
- Each of the two settings applies to every network. Welcome offers the same
|
||||
two checkboxes
|
||||
- Token Spam Protection:
|
||||
- "Hide fake tokens impersonating a known symbol" checkbox
|
||||
- "Hide tokens with fewer than 1,000 holders" checkbox
|
||||
@@ -2149,6 +2256,10 @@ view would leave a wallet one click from deletion.
|
||||
- Debug: hidden until revealed, then an "Enable debug mode" checkbox that
|
||||
turns on the red banner and verbose logging
|
||||
- **Transitions**:
|
||||
- Ticking or unticking "Use Blockscout" or "Check recipients on Etherscan" →
|
||||
stores the setting (no screen change). The next balance refresh and every
|
||||
screen drawn after that follow it. Unticking "Use Blockscout" also drops
|
||||
every token the user has not tracked from every address
|
||||
- "+ Add wallet" → **AddWallet**
|
||||
- "+ Add token" → **SettingsAddToken**
|
||||
- `[recovery phrase]` on an HD wallet → **ShowRecoveryPhrase**
|
||||
@@ -2167,7 +2278,8 @@ view would leave a wallet one click from deletion.
|
||||
nothing and tells the background nothing
|
||||
- Ten clicks on the version → reveals the Debug well (no screen change)
|
||||
- "Back" (or Settings gear again) → previous screen (Home); after a network
|
||||
switch, the stack is cut first (see the top of Screen Map)
|
||||
switch or a change to "Use Blockscout", the stack is cut first (see the
|
||||
top of Screen Map)
|
||||
|
||||
#### ShowRecoveryPhrase (`show-phrase`)
|
||||
|
||||
@@ -2324,6 +2436,8 @@ view would leave a wallet one click from deletion.
|
||||
money. The predicate is `addressHoldsFunds()` in
|
||||
`src/popup/views/helpers.js`, unrounded and token-aware. A balance is a
|
||||
warning, never a refusal.
|
||||
- While "Use Blockscout" is off, a line under the warning: "Only tracked
|
||||
tokens are listed, because "Use Blockscout" is off in Settings."
|
||||
- The rule that a wallet always keeps at least one address, and that
|
||||
removing the last one means deleting the wallet from Settings
|
||||
- Error line
|
||||
@@ -2665,17 +2779,31 @@ view would leave a wallet one click from deletion.
|
||||
|
||||
### External Services
|
||||
|
||||
AutistMask is not a fully self-contained offline tool. It necessarily
|
||||
communicates with three external services to function as a wallet:
|
||||
AutistMask is not a fully self-contained offline tool. By default it sends the
|
||||
user's data only to the Ethereum RPC endpoint. It also asks the CoinDesk price
|
||||
API for prices, sending token symbols and nothing else.
|
||||
|
||||
Two more services are sent none of the user's data until the user turns on their
|
||||
settings:
|
||||
|
||||
- Blockscout, with "Use Blockscout"
|
||||
- Etherscan, with "Check recipients on Etherscan"
|
||||
|
||||
Both settings are on the Welcome screen and in Settings. Both are off in a new
|
||||
install. Each setting applies to every network.
|
||||
|
||||
Contacted by default:
|
||||
|
||||
- **Ethereum JSON-RPC endpoint**: The extension needs an Ethereum node to query
|
||||
balances (`eth_getBalance`), read ERC-20 token contracts (`eth_call`), among
|
||||
them the balance of every tracked token at every address (`balanceOf`),
|
||||
estimate gas (`eth_estimateGas`), fetch nonces (`eth_getTransactionCount`),
|
||||
broadcast transactions (`eth_sendRawTransaction`), and check transaction
|
||||
receipts. The default endpoint is a public RPC (configurable by the user to
|
||||
any endpoint they prefer, including a local node). By default the extension
|
||||
talks to `https://ethereum-rpc.publicnode.com`.
|
||||
receipts. Every ENS lookup is made through it alone. A name whose resolver
|
||||
says it is resolved off-chain therefore does not resolve. The default endpoint
|
||||
is a public RPC (configurable by the user to any endpoint they prefer,
|
||||
including a local node). By default the extension talks to
|
||||
`https://ethereum-rpc.publicnode.com`.
|
||||
- **Data sent**: Ethereum addresses, transaction data, contract call
|
||||
parameters. The RPC endpoint can see all on-chain queries and submitted
|
||||
transactions.
|
||||
@@ -2687,13 +2815,39 @@ communicates with three external services to function as a wallet:
|
||||
- **Data sent**: Token symbol strings only (e.g. "ETH", "USDC"). No
|
||||
addresses or user-specific data.
|
||||
|
||||
- **Blockscout block-explorer API**: Used to fetch transaction history (normal
|
||||
transactions and ERC-20 token transfers), ERC-20 token balances, and token
|
||||
holder counts (for spam filtering). The default endpoint is
|
||||
Sent the user's data only while the user has turned the setting on:
|
||||
|
||||
- **Blockscout block-explorer API** ("Use Blockscout"): The default endpoint is
|
||||
`https://eth.blockscout.com/api/v2` (configurable by the user in Settings).
|
||||
- **Data sent**: Ethereum addresses. Blockscout receives the user's
|
||||
addresses to query their transaction history and token balances. No
|
||||
private keys, passwords, or signing operations are sent.
|
||||
- **Used for**:
|
||||
- transaction history (normal transactions and ERC-20 token transfers),
|
||||
for Home, AddressDetail and AddressToken
|
||||
- the balances of the ERC-20 tokens the user has not tracked, for Home,
|
||||
AddressDetail, Send and DeleteAddress
|
||||
- token holder counts, for spam filtering
|
||||
- a transaction's block, network details and calldata, for
|
||||
TransactionDetail
|
||||
- **While the setting is off**: each of those screens shows a line saying
|
||||
what it leaves out and that "Use Blockscout" is off. Only tracked tokens
|
||||
are listed.
|
||||
- **Saving the URL**: pressing "Save" beside the URL in Settings asks that
|
||||
URL for `/stats`. This request carries no user data. It is made whether or
|
||||
not the setting is on.
|
||||
- **Data sent**: Ethereum addresses, and the hash of each transaction the
|
||||
user opens. Blockscout receives the user's addresses to query their
|
||||
transaction history and token balances. No private keys, passwords, or
|
||||
signing operations are sent.
|
||||
|
||||
- **Etherscan address labels** ("Check recipients on Etherscan"): When
|
||||
confirming a transaction, the extension performs a best-effort lookup of the
|
||||
recipient address on Etherscan to check for phishing/scam labels. This is a
|
||||
direct page fetch with no API key; the user's browser makes the request. While
|
||||
the setting is off, ConfirmTx says that no Etherscan check was made.
|
||||
- **Data sent**: the recipient's address and nothing else.
|
||||
|
||||
The etherscan links on the screens open a page in the browser when the user
|
||||
clicks one. They are not requests the wallet makes. Nothing is fetched from them
|
||||
until the user clicks.
|
||||
|
||||
What the extension does NOT do:
|
||||
|
||||
@@ -2702,16 +2856,9 @@ What the extension does NOT do:
|
||||
- No Infura/Alchemy dependency (any JSON-RPC endpoint works)
|
||||
- No backend servers operated by the developer
|
||||
|
||||
In addition to the three user-configurable services above (RPC endpoint,
|
||||
CoinDesk price API, and Blockscout API), AutistMask also contacts:
|
||||
|
||||
- **Etherscan address labels**: When confirming a transaction, the extension
|
||||
performs a best-effort lookup of the recipient address on Etherscan to check
|
||||
for phishing/scam labels. This is a direct page fetch with no API key; the
|
||||
user's browser makes the request.
|
||||
|
||||
Users who want maximum privacy can point the RPC and Blockscout URLs at their
|
||||
own self-hosted instances (price fetching can be disabled in a future version).
|
||||
Users who want maximum privacy can point the RPC URL at their own node. With
|
||||
"Use Blockscout" on, they can point the Blockscout URL at their own instance
|
||||
too.
|
||||
|
||||
### Dependencies
|
||||
|
||||
@@ -2893,8 +3040,11 @@ logged.
|
||||
- Add multiple addresses within an HD wallet
|
||||
- Manage multiple wallets simultaneously
|
||||
- View ETH balance per address
|
||||
- View ERC-20 token balances (tokens on the bundled known-token list, tokens
|
||||
with 1,000 or more holders, and tokens the user adds by contract address)
|
||||
- View ERC-20 token balances:
|
||||
- tokens the user adds by contract address
|
||||
- while "Use Blockscout" is on, tokens on the bundled known-token list and
|
||||
tokens with 1,000 or more holders
|
||||
- View transaction history and transaction details, while "Use Blockscout" is on
|
||||
- Send ETH to an address
|
||||
- Send ERC-20 tokens to an address
|
||||
- Receive ETH/tokens (display address, copy to clipboard, QR code)
|
||||
|
||||
+61
-29
@@ -57,11 +57,12 @@ analytics, use a portfolio tracker. The wallet is not the place for any of that.
|
||||
|
||||
- **No NFT galleries or portfolio views.** This is a wallet, not a dashboard.
|
||||
|
||||
- **No third-party token list APIs.** Token balances come from the same block
|
||||
explorer you configure for transaction history, and the extension ships its
|
||||
own hardcoded list of top ERC-20 contract addresses for symbol-spoofing
|
||||
detection. Any token you want tracked across all your addresses, you add
|
||||
yourself by contract address.
|
||||
- **No third-party token list APIs.** The balances of the tokens you track are
|
||||
read from your RPC endpoint. While "Use Blockscout" is on, the other tokens
|
||||
come from the same block explorer you configure for transaction history. The
|
||||
extension ships its own hardcoded list of top ERC-20 contract addresses for
|
||||
symbol-spoofing detection. Any token you want tracked across all your
|
||||
addresses, you add yourself by contract address.
|
||||
|
||||
- **No backend servers operated by the developer.** Nothing is sent to any
|
||||
server run by AutistMask. Every network destination is listed below.
|
||||
@@ -69,8 +70,13 @@ analytics, use a portfolio tracker. The wallet is not the place for any of that.
|
||||
## How It Works
|
||||
|
||||
AutistMask is a browser extension that runs entirely in your browser. It does
|
||||
not have a backend server. It communicates with five external destinations:
|
||||
three you configure yourself, and two fixed ones used for scam detection.
|
||||
not have a backend server. Out of the box it sends your data only to the
|
||||
Ethereum RPC endpoint. It also asks the CoinDesk price API for prices, sending
|
||||
token symbols and nothing else.
|
||||
|
||||
Blockscout and Etherscan are sent none of your data until you turn them on.
|
||||
Their settings are "Use Blockscout" and "Check recipients on Etherscan", on the
|
||||
Welcome screen and in Settings. Both are off in a new install.
|
||||
|
||||
### External Services
|
||||
|
||||
@@ -96,16 +102,31 @@ addresses are necessarily visible to the RPC provider when querying balances.
|
||||
**Blockscout API** (default: `eth.blockscout.com/api/v2`;
|
||||
`eth-sepolia.blockscout.com/api/v2` on Sepolia)
|
||||
|
||||
Used to fetch token balances and transaction history. Blockscout is an
|
||||
open-source blockchain explorer. AutistMask queries it for your ERC-20 token
|
||||
balances (including the holder counts used for spam filtering) and your recent
|
||||
transactions and token transfers. You can change this in Settings to a
|
||||
self-hosted Blockscout instance.
|
||||
Blockscout is an open-source blockchain explorer. AutistMask uses it only while
|
||||
"Use Blockscout" is on. It then asks Blockscout for:
|
||||
|
||||
When it is contacted: on every balance refresh, and whenever a screen showing
|
||||
transaction history is opened.
|
||||
- your recent transactions and token transfers
|
||||
- the balances of the tokens you have not tracked, with the holder counts used
|
||||
for spam filtering
|
||||
- the block, fees and calldata of a transaction you open
|
||||
|
||||
What gets sent: your Ethereum addresses (to look up balances and transactions).
|
||||
You can change this in Settings to a self-hosted Blockscout instance. While the
|
||||
setting is off, each screen that would show Blockscout's data says so instead.
|
||||
Only the tokens you track are listed. When you turn the setting off, every other
|
||||
token leaves the lists at once.
|
||||
|
||||
When it is contacted, while "Use Blockscout" is on:
|
||||
|
||||
- on every balance refresh
|
||||
- whenever a screen showing transaction history is opened
|
||||
- when you open a transaction
|
||||
|
||||
Pressing "Save" beside the Blockscout URL in Settings also contacts it, even
|
||||
with the setting off. That request checks the URL by asking it for `/stats`. It
|
||||
sends none of your data.
|
||||
|
||||
What gets sent: your Ethereum addresses (to look up balances and transactions)
|
||||
and the hash of each transaction you open.
|
||||
|
||||
**CoinDesk CADLI price API** (`data-api.coindesk.com`)
|
||||
|
||||
@@ -122,16 +143,19 @@ address.
|
||||
|
||||
**Etherscan address labels** (`etherscan.io`; `sepolia.etherscan.io` on Sepolia)
|
||||
|
||||
When you review a send, AutistMask fetches the recipient's public Etherscan
|
||||
address page and looks for a "Fake_Phishing"/"Phish/Hack" label or a scam
|
||||
warning, and shows a red warning if it finds one. This is a plain page fetch
|
||||
with no API key, made by your browser. It is best-effort: if it fails, it is
|
||||
silently ignored. This endpoint is not user-configurable.
|
||||
Off until you turn on "Check recipients on Etherscan". When you review a send,
|
||||
AutistMask fetches the recipient's public Etherscan address page. It looks for a
|
||||
"Fake_Phishing"/"Phish/Hack" label or a scam warning, and shows a red warning if
|
||||
it finds one. This is a plain page fetch with no API key, made by your browser.
|
||||
It is best-effort: if it fails, it is silently ignored. This endpoint is not
|
||||
user-configurable. While it is off, the confirmation screen says that no
|
||||
Etherscan check was made.
|
||||
|
||||
When it is contacted: each time you reach the send confirmation screen.
|
||||
When it is contacted: only while "Check recipients on Etherscan" is on, each
|
||||
time you reach the send confirmation screen.
|
||||
|
||||
What gets sent: the recipient address you are about to send to, and your IP
|
||||
address. Your own addresses are not sent.
|
||||
What gets sent: the recipient's address and nothing else. As with any request,
|
||||
Etherscan sees your IP address.
|
||||
|
||||
Etherscan links shown elsewhere in the UI (on addresses, transactions, and token
|
||||
contracts) are ordinary links. They contact nothing until you click them.
|
||||
@@ -185,6 +209,9 @@ password.
|
||||
|
||||
1. Click the AutistMask icon in your browser toolbar.
|
||||
2. Click "Add wallet" (on first use), or open Settings and click "+ Add wallet".
|
||||
On first use, the screen with "Add wallet" also offers "Use Blockscout" and
|
||||
"Check recipients on Etherscan", both off. Tick either if you want what it
|
||||
does (see External Services). You can change both later in Settings.
|
||||
3. On the "From Phrase" tab, click the die button to generate a random 12-word
|
||||
recovery phrase.
|
||||
4. **Write down the recovery phrase and store it safely.** Anyone with these
|
||||
@@ -220,10 +247,14 @@ always produce the same sequence of addresses.
|
||||
|
||||
### Adding ERC-20 Tokens
|
||||
|
||||
Tokens you hold show up automatically only if they are in the extension's
|
||||
bundled list of well-known tokens or have at least 1,000 holders; everything
|
||||
else is treated as spam and hidden. To track a token explicitly (which also
|
||||
shows it at zero balance), add it by contract address:
|
||||
Tokens you hold show up automatically only while "Use Blockscout" is on. Even
|
||||
then, a token shows up only if it is in the extension's bundled list of
|
||||
well-known tokens or has at least 1,000 holders. Everything else is treated as
|
||||
spam and hidden.
|
||||
|
||||
A token you track is shown whether or not "Use Blockscout" is on. By default it
|
||||
is shown at zero balance too (see Display in Settings). To track a token, add it
|
||||
by contract address:
|
||||
|
||||
1. Go to an address detail view (click `[info]` on any address).
|
||||
2. Click "+ Token".
|
||||
@@ -389,8 +420,9 @@ Click the gear icon on the home screen to access settings:
|
||||
that network's defaults if you have not used it before.
|
||||
- **Ethereum RPC**: Change the Ethereum node endpoint. Default is a public RPC.
|
||||
You can use your own node for maximum privacy.
|
||||
- **Blockscout API**: Change the Blockscout instance used for token balances and
|
||||
transaction history. You can use a self-hosted instance.
|
||||
- **Blockscout API**: the "Use Blockscout" checkbox, off until you tick it, and
|
||||
the URL of the Blockscout instance. You can use a self-hosted instance.
|
||||
- **Etherscan**: "Check recipients on Etherscan", off until you tick it.
|
||||
- **Token Spam Protection**: Toggle individual scam filters and set the dust
|
||||
transaction threshold.
|
||||
- **Allowed Sites / Denied Sites**: View and manage web3 site permissions.
|
||||
|
||||
@@ -1386,6 +1386,10 @@ const RECENT_BALANCE_REFRESH_MS = Math.floor(BALANCE_REFRESH_PERIOD_MS / 2);
|
||||
// back over them (https://git.eeqj.de/sneak/AutistMask/issues/577).
|
||||
// lastBalanceRefresh is left as the switch set it, so the next tick refreshes
|
||||
// the new network.
|
||||
//
|
||||
// Nothing is applied either when "Use Blockscout" stored NOW is not the one the
|
||||
// refresh ran with. Turning it off dropped the tokens the user has not tracked
|
||||
// (src/popup/views/settings.js), and this refresh must not bring them back.
|
||||
async function backgroundRefresh() {
|
||||
const s = await getState();
|
||||
const now = Date.now();
|
||||
@@ -1397,6 +1401,7 @@ async function backgroundRefresh() {
|
||||
wallets,
|
||||
s.rpcUrl,
|
||||
s.blockscoutUrl,
|
||||
s.useBlockscout,
|
||||
s.trackedTokens,
|
||||
s.networkId,
|
||||
);
|
||||
@@ -1410,6 +1415,7 @@ async function backgroundRefresh() {
|
||||
|
||||
await updateState((fresh) => {
|
||||
if (fresh.networkId !== s.networkId) return;
|
||||
if (fresh.useBlockscout !== s.useBlockscout) return;
|
||||
for (const wallet of fresh.wallets) {
|
||||
for (const addr of wallet.addresses || []) {
|
||||
const got = refreshed.get(String(addr.address).toLowerCase());
|
||||
|
||||
+113
-7
@@ -43,6 +43,38 @@
|
||||
<!-- ============ WELCOME / FIRST USE ============ -->
|
||||
<div id="view-welcome" class="view hidden">
|
||||
<p class="mb-3">Welcome! To get started, add a wallet.</p>
|
||||
<div class="bg-well p-3 mx-1 mb-3">
|
||||
<p class="text-xs mb-2">
|
||||
This wallet sends your data only to the Ethereum RPC
|
||||
endpoint, unless you turn on one of these. You can
|
||||
change them later in Settings.
|
||||
</p>
|
||||
<label
|
||||
class="text-xs flex items-center gap-1 cursor-pointer mb-1"
|
||||
>
|
||||
<input type="checkbox" id="welcome-use-blockscout" />
|
||||
Use Blockscout
|
||||
</label>
|
||||
<p class="text-xs text-muted mb-2">
|
||||
Shows transaction history, tokens you have not tracked
|
||||
and transaction details. Sends your addresses, and the
|
||||
hash of each transaction you open, to the Blockscout
|
||||
API.
|
||||
</p>
|
||||
<label
|
||||
class="text-xs flex items-center gap-1 cursor-pointer mb-1"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="welcome-check-recipients-on-etherscan"
|
||||
/>
|
||||
Check recipients on Etherscan
|
||||
</label>
|
||||
<p class="text-xs text-muted">
|
||||
Sends each recipient's address to Etherscan to check it
|
||||
for a phishing or scam label.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
id="btn-welcome-add"
|
||||
class="border border-border px-2 py-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||
@@ -272,6 +304,10 @@
|
||||
|
||||
<!-- wallet list -->
|
||||
<div id="wallet-list"></div>
|
||||
<p id="home-tokens-note" class="text-xs text-muted py-1 hidden">
|
||||
Only tracked tokens are listed, because "Use Blockscout" is
|
||||
off in Settings.
|
||||
</p>
|
||||
|
||||
<!-- recent transactions across all addresses -->
|
||||
<div>
|
||||
@@ -336,6 +372,13 @@
|
||||
<div id="address-balances" class="min-h-[1.25rem]">
|
||||
|
||||
</div>
|
||||
<p
|
||||
id="address-tokens-note"
|
||||
class="text-xs text-muted hidden"
|
||||
>
|
||||
Only tracked tokens are listed, because "Use Blockscout"
|
||||
is off in Settings.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- actions -->
|
||||
@@ -526,12 +569,23 @@
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="block mb-1">What to send</label>
|
||||
<select
|
||||
id="send-token"
|
||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||
>
|
||||
<option value="ETH">ETH</option>
|
||||
</select>
|
||||
<!-- The token dropdown and the line under it. Both are
|
||||
hidden when AddressToken opens Send for its own token. -->
|
||||
<div id="send-token-dropdown">
|
||||
<select
|
||||
id="send-token"
|
||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||
>
|
||||
<option value="ETH">ETH</option>
|
||||
</select>
|
||||
<p
|
||||
id="send-tokens-note"
|
||||
class="text-xs text-muted hidden"
|
||||
>
|
||||
Only tracked tokens are listed, because "Use
|
||||
Blockscout" is off in Settings.
|
||||
</p>
|
||||
</div>
|
||||
<div id="send-token-static" class="text-sm hidden"></div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
@@ -660,6 +714,13 @@
|
||||
phishing/scam. Do not send funds to this address.
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
id="confirm-etherscan-note"
|
||||
class="mb-2 text-xs text-muted hidden"
|
||||
>
|
||||
This address was not checked on Etherscan, because "Check
|
||||
recipients on Etherscan" is off in Settings.
|
||||
</p>
|
||||
<div
|
||||
id="confirm-errors"
|
||||
class="mb-2 border border-border border-dashed p-2 invisible min-h-[1.25rem]"
|
||||
@@ -1002,8 +1063,19 @@
|
||||
|
||||
<div class="bg-well p-3 mx-1 mb-3">
|
||||
<h3 class="font-bold mb-1">Blockscout API</h3>
|
||||
<label
|
||||
class="text-xs flex items-center gap-1 cursor-pointer mb-1"
|
||||
>
|
||||
<input type="checkbox" id="settings-use-blockscout" />
|
||||
Use Blockscout
|
||||
</label>
|
||||
<p class="text-xs text-muted mb-2">
|
||||
Shows transaction history, tokens you have not tracked
|
||||
and transaction details. Sends your addresses, and the
|
||||
hash of each transaction you open, to the Blockscout API
|
||||
below.
|
||||
</p>
|
||||
<p class="text-xs text-muted mb-1">
|
||||
Used to fetch token balances and transaction history.
|
||||
Change this if you run your own Blockscout instance.
|
||||
</p>
|
||||
<input
|
||||
@@ -1019,6 +1091,23 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-well p-3 mx-1 mb-3">
|
||||
<h3 class="font-bold mb-1">Etherscan</h3>
|
||||
<label
|
||||
class="text-xs flex items-center gap-1 cursor-pointer mb-1"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="settings-check-recipients-on-etherscan"
|
||||
/>
|
||||
Check recipients on Etherscan
|
||||
</label>
|
||||
<p class="text-xs text-muted">
|
||||
Sends each recipient's address to Etherscan to check it
|
||||
for a phishing or scam label.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-well p-3 mx-1 mb-3">
|
||||
<h3 class="font-bold mb-1">Token Spam Protection</h3>
|
||||
<p class="text-xs text-muted mb-2">
|
||||
@@ -1291,6 +1380,13 @@
|
||||
>
|
||||
|
||||
</div>
|
||||
<p
|
||||
id="delete-address-tokens-note"
|
||||
class="text-xs text-muted mb-2 hidden"
|
||||
>
|
||||
Only tracked tokens are listed, because "Use Blockscout" is
|
||||
off in Settings.
|
||||
</p>
|
||||
<p class="text-xs text-muted mb-3">
|
||||
A wallet always keeps at least one address. To remove the
|
||||
last one, delete the whole wallet from Settings instead.
|
||||
@@ -1504,6 +1600,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shown in place of the block, decoded details, network
|
||||
details and raw data, which only Blockscout gives. -->
|
||||
<p
|
||||
id="tx-detail-blockscout-note"
|
||||
class="text-xs text-muted mx-1 mb-3 hidden"
|
||||
>
|
||||
No block, decoded details, network details or raw data are
|
||||
shown, because "Use Blockscout" is off in Settings.
|
||||
</p>
|
||||
|
||||
<!-- ── Decoded details ── -->
|
||||
<div id="tx-detail-calldata-section" class="hidden">
|
||||
<div class="bg-well p-3 mx-1 mb-3">
|
||||
|
||||
+10
-4
@@ -79,14 +79,17 @@ let refreshTimer = null;
|
||||
// A switch while the refresh ran cleared every address's balances
|
||||
// (applyChainSwitchFields() in src/shared/chainSwitchFields.js), and the
|
||||
// previous network's must not come back over them
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/577). A dropped refresh is not
|
||||
// recorded as done. Results are copied by address, so an address added or
|
||||
// removed meanwhile is left as it is.
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/577). The same holds when "Use
|
||||
// Blockscout" changed while the refresh ran. Turning it off dropped the tokens
|
||||
// the user has not tracked (views/settings.js), and this refresh must not bring
|
||||
// them back. A dropped refresh is not recorded as done. Results are copied by
|
||||
// address, so an address added or removed meanwhile is left as it is.
|
||||
async function doRefreshAndRender() {
|
||||
if (refreshInFlight) return;
|
||||
refreshInFlight = true;
|
||||
try {
|
||||
const networkId = state.networkId;
|
||||
const useBlockscout = state.useBlockscout;
|
||||
const wallets = structuredClone(state.wallets);
|
||||
await Promise.all([
|
||||
refreshPrices(),
|
||||
@@ -94,6 +97,7 @@ async function doRefreshAndRender() {
|
||||
wallets,
|
||||
state.rpcUrl,
|
||||
state.blockscoutUrl,
|
||||
useBlockscout,
|
||||
state.trackedTokens,
|
||||
networkId,
|
||||
pageClosed.signal,
|
||||
@@ -108,6 +112,8 @@ async function doRefreshAndRender() {
|
||||
await saveState((stored) => {
|
||||
if (state.networkId !== networkId) return false;
|
||||
if (stored.networkId !== networkId) return false;
|
||||
if (state.useBlockscout !== useBlockscout) return false;
|
||||
if (stored.useBlockscout !== useBlockscout) return false;
|
||||
for (const wallet of state.wallets) {
|
||||
for (const addr of wallet.addresses) {
|
||||
const got = refreshed.get(addr.address);
|
||||
@@ -397,7 +403,7 @@ async function init() {
|
||||
followNetworkSwitch();
|
||||
|
||||
if (!state.hasWallet) {
|
||||
showView("welcome");
|
||||
welcome.show();
|
||||
} else {
|
||||
renderWalletList();
|
||||
restoreView();
|
||||
|
||||
@@ -38,10 +38,15 @@ function markViewRendered(view) {
|
||||
if (view) renderedViews.add(view);
|
||||
}
|
||||
|
||||
// Begin a fresh page-load scope. The popup gets one by being loaded, and
|
||||
// asks for one after a network switch, because every screen then on the page
|
||||
// shows the previous network (showNewNetwork() in views/settings.js); the
|
||||
// unit tests, which simulate several page loads against one module
|
||||
// Begin a fresh page-load scope. The popup gets one by being loaded. It also
|
||||
// asks for one when every screen on the page is out of date:
|
||||
//
|
||||
// - after a network switch, since those screens show the previous network
|
||||
// (showNewNetwork() in views/settings.js)
|
||||
// - after "Use Blockscout" or "Check recipients on Etherscan" changes in
|
||||
// Settings, since those screens were drawn under the old setting
|
||||
//
|
||||
// The unit tests, which simulate several page loads against one module
|
||||
// instance, ask for one too.
|
||||
function resetRenderedViews() {
|
||||
renderedViews.clear();
|
||||
|
||||
@@ -4,6 +4,7 @@ const {
|
||||
showFlash,
|
||||
balanceLinesForAddress,
|
||||
txCounterpartyHtml,
|
||||
HISTORY_OFF_TEXT,
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
displaySymbol,
|
||||
@@ -83,6 +84,8 @@ function show() {
|
||||
ctx.showAddressToken();
|
||||
});
|
||||
});
|
||||
// Tokens the user has not tracked are Blockscout's to report.
|
||||
$("address-tokens-note").classList.toggle("hidden", !!state.useBlockscout);
|
||||
renderSendTokenSelect(addr);
|
||||
$("tx-list").innerHTML =
|
||||
'<div class="text-muted text-xs py-1">Loading...</div>';
|
||||
@@ -99,6 +102,7 @@ async function loadTransactions(address) {
|
||||
const rawTxs = await fetchRecentTransactions(
|
||||
address,
|
||||
state.blockscoutUrl,
|
||||
state.useBlockscout,
|
||||
currentNetwork().chainId,
|
||||
);
|
||||
const result = filterTransactions(rawTxs, {
|
||||
@@ -155,6 +159,10 @@ async function loadTransactions(address) {
|
||||
|
||||
function renderTransactions(txs) {
|
||||
const list = $("tx-list");
|
||||
if (!state.useBlockscout) {
|
||||
list.innerHTML = `<div class="text-muted text-xs py-1">${escapeHtml(HISTORY_OFF_TEXT)}</div>`;
|
||||
return;
|
||||
}
|
||||
if (txs.length === 0) {
|
||||
list.innerHTML =
|
||||
'<div class="text-muted text-xs py-1">No transactions found.</div>';
|
||||
@@ -229,7 +237,7 @@ function init(_ctx) {
|
||||
}
|
||||
$("send-to").value = "";
|
||||
$("send-amount").value = "";
|
||||
$("send-token").classList.remove("hidden");
|
||||
$("send-token-dropdown").classList.remove("hidden");
|
||||
$("send-token-static").classList.add("hidden");
|
||||
updateSendBalance();
|
||||
resetSendValidation();
|
||||
|
||||
@@ -7,6 +7,7 @@ const {
|
||||
showFlash,
|
||||
flashCopyFeedback,
|
||||
txCounterpartyHtml,
|
||||
HISTORY_OFF_TEXT,
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
displaySymbol,
|
||||
@@ -173,6 +174,7 @@ async function loadTransactions(address, tokenId) {
|
||||
const rawTxs = await fetchRecentTransactions(
|
||||
address,
|
||||
state.blockscoutUrl,
|
||||
state.useBlockscout,
|
||||
currentNetwork().chainId,
|
||||
);
|
||||
const result = filterTransactions(rawTxs, {
|
||||
@@ -239,6 +241,10 @@ async function loadTransactions(address, tokenId) {
|
||||
|
||||
function renderTransactions(txs) {
|
||||
const list = $("address-token-tx-list");
|
||||
if (!state.useBlockscout) {
|
||||
list.innerHTML = `<div class="text-muted text-xs py-1">${escapeHtml(HISTORY_OFF_TEXT)}</div>`;
|
||||
return;
|
||||
}
|
||||
if (txs.length === 0) {
|
||||
list.innerHTML =
|
||||
'<div class="text-muted text-xs py-1">No transactions found.</div>';
|
||||
@@ -324,7 +330,7 @@ function init(_ctx) {
|
||||
$("send-token").value = tokenId;
|
||||
}
|
||||
// Hide dropdown, show static token display
|
||||
$("send-token").classList.add("hidden");
|
||||
$("send-token-dropdown").classList.add("hidden");
|
||||
let staticHtml = `<div class="font-bold">${escapeHtml(displaySymbol(currentSymbol))}</div>`;
|
||||
if (tokenId !== "ETH") {
|
||||
staticHtml += `<div class="text-xs">${renderAddressHtml(tokenId)}</div>`;
|
||||
|
||||
@@ -334,6 +334,11 @@ function show(txInfo) {
|
||||
$("confirm-contract-warning").style.visibility = "hidden";
|
||||
$("confirm-burn-warning").style.visibility = "hidden";
|
||||
$("confirm-etherscan-warning").style.visibility = "hidden";
|
||||
// While "Check recipients on Etherscan" is off, there is no Etherscan
|
||||
// warning to reserve space for. A line in its place says so.
|
||||
const etherscanOn = !!state.checkRecipientsOnEtherscan;
|
||||
$("confirm-etherscan-warning").classList.toggle("hidden", !etherscanOn);
|
||||
$("confirm-etherscan-note").classList.toggle("hidden", etherscanOn);
|
||||
|
||||
// Show burn warning via reserved element (in addition to inline warning)
|
||||
if (isBurnAddress(txInfo.to)) {
|
||||
@@ -612,6 +617,7 @@ async function checkRecipientHistory(txInfo) {
|
||||
const asyncWarnings = await getFullWarnings(txInfo.to, provider, {
|
||||
fromAddress: txInfo.from,
|
||||
chainId: txInfo.chainId,
|
||||
checkRecipientsOnEtherscan: state.checkRecipientsOnEtherscan,
|
||||
});
|
||||
// The user may have left this transaction while the checks were in
|
||||
// flight; its warnings must not reach another recipient's screen.
|
||||
|
||||
@@ -119,6 +119,11 @@ function show(walletIdx, addrIdx) {
|
||||
|
||||
$("delete-address-recovery").textContent = recoveryPathText(wallet);
|
||||
$("delete-address-balance").innerHTML = balanceWarningHtml(addr);
|
||||
// Tokens the user has not tracked are Blockscout's to report.
|
||||
$("delete-address-tokens-note").classList.toggle(
|
||||
"hidden",
|
||||
!!state.useBlockscout,
|
||||
);
|
||||
|
||||
setFlash("");
|
||||
showView("delete-address-confirm");
|
||||
|
||||
@@ -16,6 +16,7 @@ const {
|
||||
broadcastActiveChanged,
|
||||
} = require("../../shared/walletDelete");
|
||||
const { INVISIBLE_CHARACTERS } = require("../../shared/symbolSpoof");
|
||||
const welcome = require("./welcome");
|
||||
|
||||
let deleteWalletIndex = null;
|
||||
let lostPasswordIndex = null;
|
||||
@@ -135,7 +136,7 @@ async function finishDelete(walletIdx) {
|
||||
// Save before broadcasting: the background reads the active
|
||||
// address back out of storage to build accountsChanged.
|
||||
if (activeAddressChanged) broadcastActiveChanged();
|
||||
showView("welcome");
|
||||
welcome.show();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -534,6 +534,11 @@ function addressTitle(address, wallets) {
|
||||
const CONTRACT_CREATION_TEXT =
|
||||
"This transaction creates a new contract. It has no recipient.";
|
||||
|
||||
// What the transaction lists on Home, AddressDetail and AddressToken say while
|
||||
// the user has "Use Blockscout" off. Their history comes from Blockscout alone.
|
||||
const HISTORY_OFF_TEXT =
|
||||
'No transaction history is shown, because "Use Blockscout" is off in Settings.';
|
||||
|
||||
// The last two lines of a transaction history row: the counterparty's colour
|
||||
// dot and name beside the amount, then its full address. A contract creation
|
||||
// the user sent has no counterparty (its `to` is ""), so its row has the
|
||||
@@ -799,6 +804,7 @@ module.exports = {
|
||||
displaySymbol,
|
||||
addressTitle,
|
||||
CONTRACT_CREATION_TEXT,
|
||||
HISTORY_OFF_TEXT,
|
||||
txCounterpartyHtml,
|
||||
formatAddressHtml,
|
||||
renderAddressHtml,
|
||||
|
||||
@@ -7,6 +7,7 @@ const {
|
||||
timeAgo,
|
||||
addressDotHtml,
|
||||
txCounterpartyHtml,
|
||||
HISTORY_OFF_TEXT,
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
displaySymbol,
|
||||
@@ -100,6 +101,10 @@ let homeTxs = [];
|
||||
function renderHomeTxList(ctx) {
|
||||
const list = $("home-tx-list");
|
||||
if (!list) return;
|
||||
if (!state.useBlockscout) {
|
||||
list.innerHTML = `<div class="text-muted text-xs py-1">${escapeHtml(HISTORY_OFF_TEXT)}</div>`;
|
||||
return;
|
||||
}
|
||||
if (homeTxs.length === 0) {
|
||||
list.innerHTML =
|
||||
'<div class="text-muted text-xs py-1">No transactions found.</div>';
|
||||
@@ -192,6 +197,7 @@ async function loadHomeTxs(ctx) {
|
||||
fetchRecentTransactions(
|
||||
addr,
|
||||
state.blockscoutUrl,
|
||||
state.useBlockscout,
|
||||
currentNetwork().chainId,
|
||||
),
|
||||
);
|
||||
@@ -300,6 +306,8 @@ function walletListHtml() {
|
||||
|
||||
function render(ctx) {
|
||||
const container = $("wallet-list");
|
||||
// Tokens the user has not tracked are Blockscout's to report.
|
||||
$("home-tokens-note").classList.toggle("hidden", !!state.useBlockscout);
|
||||
if (state.wallets.length === 0) {
|
||||
container.innerHTML =
|
||||
'<p class="text-muted py-2">No wallets yet. Add one to get started.</p>';
|
||||
@@ -447,7 +455,7 @@ function init(ctx) {
|
||||
}
|
||||
$("send-to").value = "";
|
||||
$("send-amount").value = "";
|
||||
$("send-token").classList.remove("hidden");
|
||||
$("send-token-dropdown").classList.remove("hidden");
|
||||
$("send-token-static").classList.add("hidden");
|
||||
renderSendTokenSelect(addr);
|
||||
updateSendBalance();
|
||||
|
||||
@@ -141,6 +141,8 @@ function updateToValidation() {
|
||||
}
|
||||
|
||||
function renderSendTokenSelect(addr) {
|
||||
// Tokens the user has not tracked are Blockscout's to report.
|
||||
$("send-tokens-note").classList.toggle("hidden", !!state.useBlockscout);
|
||||
const sel = $("send-token");
|
||||
sel.innerHTML = `<option value="ETH">${escapeHtml(nativeCurrency())}</option>`;
|
||||
const fraudSet = new Set(
|
||||
@@ -426,7 +428,7 @@ function init(_ctx) {
|
||||
});
|
||||
|
||||
$("btn-send-back").addEventListener("click", () => {
|
||||
$("send-token").classList.remove("hidden");
|
||||
$("send-token-dropdown").classList.remove("hidden");
|
||||
$("send-token-static").classList.add("hidden");
|
||||
goBack();
|
||||
});
|
||||
|
||||
@@ -201,6 +201,10 @@ function show() {
|
||||
$("settings-rpc").value = state.rpcUrl;
|
||||
$("settings-blockscout").value = state.blockscoutUrl;
|
||||
$("settings-network").value = state.networkId;
|
||||
// Set on every show, not only at init: Welcome can change these two.
|
||||
$("settings-use-blockscout").checked = state.useBlockscout;
|
||||
$("settings-check-recipients-on-etherscan").checked =
|
||||
state.checkRecipientsOnEtherscan;
|
||||
renderTrackedTokens();
|
||||
renderSiteLists();
|
||||
renderWalletListSettings();
|
||||
@@ -257,6 +261,7 @@ async function renderSiteLists() {
|
||||
// native token: any other token there was picked on the previous network, so
|
||||
// it is no longer selected either. Without a wallet the stack starts at
|
||||
// Welcome, which that cut would drop, and Welcome shows nothing from a network.
|
||||
// A change to "Use Blockscout" cuts the stack the same way (init() below).
|
||||
function cutViewStack() {
|
||||
if (!state.hasWallet) return;
|
||||
if (state.selectedToken !== "ETH") {
|
||||
@@ -363,6 +368,54 @@ function init(_ctx) {
|
||||
showFlash("Saved.");
|
||||
});
|
||||
|
||||
// The screens under Settings were drawn under the old setting. Each change
|
||||
// makes every screen but Settings count as not yet drawn, as a network
|
||||
// switch does. Back then draws again any screen a reopened popup can draw.
|
||||
// Welcome offers the same two settings, and Back only unhides it, so its
|
||||
// boxes are set here too.
|
||||
//
|
||||
// A change to "Use Blockscout" also cuts the stack, as a network switch
|
||||
// does (cutViewStack() above). Send's token dropdown was filled under the
|
||||
// old setting, and Back cannot draw Send again.
|
||||
//
|
||||
// Turning "Use Blockscout" off drops the tokens the user has not tracked
|
||||
// from every address. Only Blockscout reports them, so no screen lists them
|
||||
// from then on.
|
||||
$("settings-use-blockscout").addEventListener("change", async () => {
|
||||
state.useBlockscout = $("settings-use-blockscout").checked;
|
||||
$("welcome-use-blockscout").checked = state.useBlockscout;
|
||||
if (!state.useBlockscout) {
|
||||
const tracked = new Set(
|
||||
state.trackedTokens.map((t) => t.address.toLowerCase()),
|
||||
);
|
||||
for (const wallet of state.wallets) {
|
||||
for (const addr of wallet.addresses) {
|
||||
addr.tokenBalances = (addr.tokenBalances || []).filter(
|
||||
(row) => tracked.has(String(row.address).toLowerCase()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
cutViewStack();
|
||||
resetRenderedViews();
|
||||
markViewRendered("settings");
|
||||
await saveState();
|
||||
});
|
||||
|
||||
$("settings-check-recipients-on-etherscan").addEventListener(
|
||||
"change",
|
||||
async () => {
|
||||
state.checkRecipientsOnEtherscan = $(
|
||||
"settings-check-recipients-on-etherscan",
|
||||
).checked;
|
||||
$("welcome-check-recipients-on-etherscan").checked =
|
||||
state.checkRecipientsOnEtherscan;
|
||||
resetRenderedViews();
|
||||
markViewRendered("settings");
|
||||
await saveState();
|
||||
},
|
||||
);
|
||||
|
||||
const networkSelect = $("settings-network");
|
||||
networkSelect.addEventListener("change", async () => {
|
||||
// Whether `state` already has the network picked is asked only once
|
||||
|
||||
@@ -191,6 +191,11 @@ function render() {
|
||||
if (el) el.classList.add("hidden");
|
||||
}
|
||||
|
||||
// Those details are Blockscout's; this line says why there are none.
|
||||
$("tx-detail-blockscout-note").classList.toggle(
|
||||
"hidden",
|
||||
!!state.useBlockscout,
|
||||
);
|
||||
loadFullTxDetails(tx.hash, tx.chainId);
|
||||
|
||||
const isoStr = isoDate(tx.timestamp);
|
||||
@@ -303,7 +308,11 @@ function populateOnChainDetails(txData, chainId) {
|
||||
}
|
||||
}
|
||||
|
||||
// The block, network details, calldata and its decoding, from Blockscout. While
|
||||
// the user has "Use Blockscout" off, Blockscout is not asked and none of them
|
||||
// is shown.
|
||||
async function loadFullTxDetails(txHash, chainId) {
|
||||
if (!state.useBlockscout) return;
|
||||
const section = $("tx-detail-calldata-section");
|
||||
const actionEl = $("tx-detail-calldata-action");
|
||||
const detailsEl = $("tx-detail-calldata-details");
|
||||
|
||||
@@ -1,7 +1,34 @@
|
||||
const { $ } = require("./helpers");
|
||||
// The first screen, shown while there is no wallet. It offers "Use Blockscout"
|
||||
// and "Check recipients on Etherscan", which Settings offers too. Both are off
|
||||
// until the user ticks them.
|
||||
|
||||
const { $, showView } = require("./helpers");
|
||||
const { state, saveState } = require("../../shared/state");
|
||||
|
||||
function show() {
|
||||
$("welcome-use-blockscout").checked = state.useBlockscout;
|
||||
$("welcome-check-recipients-on-etherscan").checked =
|
||||
state.checkRecipientsOnEtherscan;
|
||||
showView("welcome");
|
||||
}
|
||||
|
||||
function init(ctx) {
|
||||
$("btn-welcome-add").addEventListener("click", ctx.showAddWalletView);
|
||||
|
||||
$("welcome-use-blockscout").addEventListener("change", async () => {
|
||||
state.useBlockscout = $("welcome-use-blockscout").checked;
|
||||
await saveState();
|
||||
});
|
||||
|
||||
$("welcome-check-recipients-on-etherscan").addEventListener(
|
||||
"change",
|
||||
async () => {
|
||||
state.checkRecipientsOnEtherscan = $(
|
||||
"welcome-check-recipients-on-etherscan",
|
||||
).checked;
|
||||
await saveState();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { init };
|
||||
module.exports = { init, show };
|
||||
|
||||
@@ -60,6 +60,8 @@ function getLocalWarnings(address, options = {}) {
|
||||
* @param {string} [options.fromAddress] - Sender address (for self-send check).
|
||||
* @param {string} [options.chainId] - Chain id of the network whose explorer
|
||||
* the Etherscan label check asks; the active network's when omitted.
|
||||
* @param {boolean} [options.checkRecipientsOnEtherscan] - The user's setting
|
||||
* of that name. The Etherscan label check is made only while it is on.
|
||||
* @returns {Promise<Array<{type: string, message: string, severity: string}>>}
|
||||
*/
|
||||
async function getFullWarnings(address, provider, options = {}) {
|
||||
@@ -114,6 +116,7 @@ async function getFullWarnings(address, provider, options = {}) {
|
||||
try {
|
||||
const etherscanWarning = await checkEtherscanLabel(
|
||||
address,
|
||||
options.checkRecipientsOnEtherscan,
|
||||
options.chainId,
|
||||
);
|
||||
if (etherscanWarning) {
|
||||
|
||||
+29
-7
@@ -1,6 +1,9 @@
|
||||
// Balance fetching: ETH balances and the balances of the tokens the user
|
||||
// tracks via RPC, the other ERC-20 token balances and the holder counts via
|
||||
// Blockscout, ENS reverse lookup via RPC.
|
||||
// Balance fetching:
|
||||
//
|
||||
// - ETH balances, and the balances of the tokens the user tracks, via RPC
|
||||
// - the other ERC-20 token balances and the holder counts via Blockscout,
|
||||
// while the user has "Use Blockscout" on
|
||||
// - ENS reverse lookup via RPC
|
||||
|
||||
const {
|
||||
JsonRpcProvider,
|
||||
@@ -37,9 +40,18 @@ const { resolveTokenDecimals } = require("./approvalAmount");
|
||||
// ethers knows chains this wallet does not, so an id that is not one of ours
|
||||
// is a caller bug and must not resolve to a working provider for some other
|
||||
// chain.
|
||||
//
|
||||
// `disableCcipRead` keeps every ENS lookup on the RPC endpoint. A resolver can
|
||||
// say a name is resolved off-chain and give a web address for it. By default
|
||||
// ethers then fetches that address straight from the browser, sending the name
|
||||
// being looked up. For the reverse lookup of the user's addresses, that is the
|
||||
// user's own name (https://git.eeqj.de/sneak/AutistMask/issues/668). With the
|
||||
// flag set, such a name does not resolve.
|
||||
function getProvider(rpcUrl, networkId) {
|
||||
const net = Network.from(requireNetworkId(networkId).id);
|
||||
return new JsonRpcProvider(rpcUrl, net, { staticNetwork: net });
|
||||
const provider = new JsonRpcProvider(rpcUrl, net, { staticNetwork: net });
|
||||
provider.disableCcipRead = true;
|
||||
return provider;
|
||||
}
|
||||
|
||||
function requireNetworkId(networkId) {
|
||||
@@ -81,6 +93,11 @@ function rawUnits(value) {
|
||||
|
||||
// Fetch token balances for a single address from Blockscout.
|
||||
// Returns [{ address, name, symbol, decimals, balance, holders }].
|
||||
//
|
||||
// `useBlockscout` is the user's "Use Blockscout" setting. While it is off,
|
||||
// Blockscout is not asked and the answer is an empty list. So no token the user
|
||||
// has not tracked is listed.
|
||||
//
|
||||
// Filters out spam: only shows tokens that are in the known token list,
|
||||
// explicitly tracked by the user, or have >= 1000 holders. A tracked token's
|
||||
// balance is read through the RPC endpoint instead, and its row here gives that
|
||||
@@ -96,9 +113,11 @@ function rawUnits(value) {
|
||||
async function fetchTokenBalances(
|
||||
address,
|
||||
blockscoutUrl,
|
||||
useBlockscout,
|
||||
trackedTokens,
|
||||
signal,
|
||||
) {
|
||||
if (!useBlockscout) return [];
|
||||
try {
|
||||
const resp = await debugFetch(
|
||||
blockscoutUrl + "/addresses/" + address + "/token-balances",
|
||||
@@ -296,12 +315,13 @@ async function fetchTrackedTokenBalances(
|
||||
}
|
||||
|
||||
// Fetch ETH balances, ENS names, and ERC-20 token balances for all addresses.
|
||||
// `trackedTokens` are the tokens tracked on the active network. `signal` is as
|
||||
// for fetchTokenBalances().
|
||||
// `trackedTokens` are the tokens tracked on the active network.
|
||||
// `useBlockscout` and `signal` are as for fetchTokenBalances().
|
||||
async function refreshBalances(
|
||||
wallets,
|
||||
rpcUrl,
|
||||
blockscoutUrl,
|
||||
useBlockscout,
|
||||
trackedTokens,
|
||||
networkId,
|
||||
signal,
|
||||
@@ -362,13 +382,15 @@ async function refreshBalances(
|
||||
// ERC-20 token balances: the tracked tokens' through the RPC
|
||||
// endpoint, and from Blockscout the tokens the user has not
|
||||
// tracked. When Blockscout fails, the untracked rows from before
|
||||
// this refresh stay. The Blockscout request is started first and
|
||||
// this refresh stay. While "Use Blockscout" is off, there are no
|
||||
// untracked rows. The Blockscout request is started first and
|
||||
// handed to fetchTrackedTokenBalances() too, which waits for it
|
||||
// after sending its balanceOf reads, so the two run side by side.
|
||||
const previous = addr.tokenBalances || [];
|
||||
const explorerRequest = fetchTokenBalances(
|
||||
addr.address,
|
||||
blockscoutUrl,
|
||||
useBlockscout,
|
||||
tracked,
|
||||
signal,
|
||||
);
|
||||
|
||||
@@ -78,11 +78,20 @@ function parseEtherscanPage(html) {
|
||||
* when it is omitted: etherscan.io on mainnet, sepolia.etherscan.io on
|
||||
* Sepolia.
|
||||
*
|
||||
* While the user's "Check recipients on Etherscan" setting is off, Etherscan
|
||||
* is not asked and the answer is null.
|
||||
*
|
||||
* @param {string} address - Ethereum address to check.
|
||||
* @param {boolean} checkRecipientsOnEtherscan - The user's setting.
|
||||
* @param {string} [chainId] - Chain id of the network to look it up on.
|
||||
* @returns {Promise<{type: string, message: string, severity: string}|null>}
|
||||
*/
|
||||
async function checkEtherscanLabel(address, chainId) {
|
||||
async function checkEtherscanLabel(
|
||||
address,
|
||||
checkRecipientsOnEtherscan,
|
||||
chainId,
|
||||
) {
|
||||
if (!checkRecipientsOnEtherscan) return null;
|
||||
try {
|
||||
// Lazy require to avoid pulling in chrome.storage at module scope
|
||||
// (which breaks unit tests that only exercise parseEtherscanPage).
|
||||
|
||||
@@ -48,6 +48,14 @@ const DEFAULT_STATE = {
|
||||
hideDustTransactions: true,
|
||||
dustThresholdGwei: 100000,
|
||||
utcTimestamps: false,
|
||||
// "Use Blockscout" and "Check recipients on Etherscan" in Settings. Each
|
||||
// setting applies to every network. Both are off until the user turns them
|
||||
// on. With "Use Blockscout" on, Blockscout is sent the user's addresses and
|
||||
// the hash of each transaction opened. With "Check recipients on
|
||||
// Etherscan" on, Etherscan is sent the recipient's address and nothing
|
||||
// else.
|
||||
useBlockscout: false,
|
||||
checkRecipientsOnEtherscan: false,
|
||||
fraudContracts: [],
|
||||
tokenHolderCache: {},
|
||||
theme: "system",
|
||||
@@ -406,6 +414,12 @@ function normalizePersisted(saved) {
|
||||
: 100000;
|
||||
out.utcTimestamps =
|
||||
saved.utcTimestamps !== undefined ? saved.utcTimestamps : false;
|
||||
out.useBlockscout =
|
||||
saved.useBlockscout !== undefined ? saved.useBlockscout : false;
|
||||
out.checkRecipientsOnEtherscan =
|
||||
saved.checkRecipientsOnEtherscan !== undefined
|
||||
? saved.checkRecipientsOnEtherscan
|
||||
: false;
|
||||
// A list of contract addresses, floored the same way: send.js builds its
|
||||
// fraud set as `(state.fraudContracts || []).map((a) => a.toLowerCase())`
|
||||
// and filterTransactions() maps the same list through normalizeAddress(),
|
||||
|
||||
@@ -235,12 +235,17 @@ function mergeTransactions(txs, tokenTransfers) {
|
||||
// `chainId` is the chain id of the network `blockscoutUrl` serves. Every entry
|
||||
// carries it, and a native entry is labelled with that network's
|
||||
// `nativeCurrency` from networks.js (`ETH`, `SepoliaETH`).
|
||||
//
|
||||
// `useBlockscout` is the user's "Use Blockscout" setting. While it is off,
|
||||
// Blockscout is not asked and the answer is an empty list.
|
||||
async function fetchRecentTransactions(
|
||||
address,
|
||||
blockscoutUrl,
|
||||
useBlockscout,
|
||||
chainId,
|
||||
count = 25,
|
||||
) {
|
||||
if (!useBlockscout) return [];
|
||||
log.debugf("fetchRecentTransactions", address);
|
||||
const addrLower = normalizeAddress(address);
|
||||
|
||||
|
||||
@@ -410,7 +410,14 @@ describe("a balance refresh under a network switch", () => {
|
||||
const ranOn = [];
|
||||
|
||||
const bg = loadWorker("sepolia", {
|
||||
refreshBalances: async (wallets, rpcUrl, explorer, tokens, id) => {
|
||||
refreshBalances: async (
|
||||
wallets,
|
||||
rpcUrl,
|
||||
explorer,
|
||||
useBlockscout,
|
||||
tokens,
|
||||
id,
|
||||
) => {
|
||||
ranOn.push(id);
|
||||
reachedNetwork.resolve();
|
||||
await roundTrip.promise;
|
||||
@@ -443,3 +450,60 @@ describe("a balance refresh under a network switch", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a balance refresh under a change of Use Blockscout", () => {
|
||||
// Turning "Use Blockscout" off drops the tokens the user has not tracked
|
||||
// (src/popup/views/settings.js). A refresh that started while it was on
|
||||
// must not write them back when it returns. The same refresh in the popup
|
||||
// is in tests/blockscoutEtherscanSettings.test.js.
|
||||
test("a refresh that returns after Use Blockscout was turned off writes nothing", async () => {
|
||||
const roundTrip = deferred();
|
||||
const started = deferred();
|
||||
const handed = [];
|
||||
|
||||
const bg = loadWorker("mainnet", {
|
||||
refreshBalances: async (
|
||||
wallets,
|
||||
rpcUrl,
|
||||
explorer,
|
||||
useBlockscout,
|
||||
) => {
|
||||
handed.push(useBlockscout);
|
||||
started.resolve();
|
||||
await roundTrip.promise;
|
||||
wallets[0].addresses[0].tokenBalances = [
|
||||
{
|
||||
address: "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
name: "Test Token",
|
||||
symbol: "TST",
|
||||
decimals: 18,
|
||||
balance: "5.0",
|
||||
holders: 50000,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
global.chrome.storage.write("autistmask", {
|
||||
...bg.persisted(),
|
||||
useBlockscout: true,
|
||||
});
|
||||
|
||||
const refresh = bg.fireBalanceAlarm();
|
||||
await started.promise;
|
||||
|
||||
// The user unticks "Use Blockscout" in Settings while the round trip
|
||||
// is out.
|
||||
global.chrome.storage.write("autistmask", {
|
||||
...bg.persisted(),
|
||||
useBlockscout: false,
|
||||
});
|
||||
|
||||
roundTrip.resolve();
|
||||
await refresh;
|
||||
|
||||
expect(handed).toEqual([true]);
|
||||
const after = bg.persisted();
|
||||
expect(after.wallets[0].addresses[0].tokenBalances).toEqual([]);
|
||||
expect(after.lastBalanceRefresh).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,6 +54,7 @@ function refresh(signal) {
|
||||
wallets,
|
||||
RPC_URL,
|
||||
EXPLORER_URL,
|
||||
true,
|
||||
[TRACKED],
|
||||
"mainnet",
|
||||
signal,
|
||||
|
||||
@@ -91,7 +91,7 @@ test("a refresh that returns after a switch in Settings writes nothing, and the
|
||||
|
||||
await page.tick();
|
||||
|
||||
expect(refreshBalances.mock.lastCall[4]).toBe("sepolia");
|
||||
expect(refreshBalances.mock.lastCall[5]).toBe("sepolia");
|
||||
expect(page.storage.read("autistmask").lastBalanceRefresh).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,662 @@
|
||||
// Blockscout and Etherscan are sent none of the user's data until the user
|
||||
// turns them on (https://git.eeqj.de/sneak/AutistMask/issues/668). A new
|
||||
// profile has "Use Blockscout" and "Check recipients on Etherscan" both off.
|
||||
// Each request to either service is skipped where it is made while its setting
|
||||
// is off. The popup and the background hand the stored setting to the code that
|
||||
// makes the request. The one exception is the check behind "Save" beside the
|
||||
// Blockscout URL in Settings. It asks that URL only for `/stats` and carries no
|
||||
// user data. It is made whether or not the setting is on.
|
||||
//
|
||||
// Only Blockscout reports the tokens the user has not tracked. Turning "Use
|
||||
// Blockscout" off in Settings drops them from every address, so no screen lists
|
||||
// them from then on.
|
||||
//
|
||||
// The functions that make the requests are driven directly, against a fetch()
|
||||
// that records every URL it is asked for. The screens are driven through the
|
||||
// real popup over a stored profile (tests/support/popupBoot.js), whose fetch()
|
||||
// stub records the same way.
|
||||
|
||||
"use strict";
|
||||
|
||||
const { FetchRequest } = require("ethers");
|
||||
const {
|
||||
bootPopup,
|
||||
cleanupPopup,
|
||||
unversionedValidProfile,
|
||||
ADDRESS,
|
||||
TOKEN_ADDRESS,
|
||||
} = require("./support/popupBoot");
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
const {
|
||||
DEFAULT_STATE,
|
||||
normalizePersisted,
|
||||
} = require("../src/shared/persistedState");
|
||||
const {
|
||||
fetchTokenBalances,
|
||||
refreshBalances,
|
||||
} = require("../src/shared/balances");
|
||||
const { fetchRecentTransactions } = require("../src/shared/transactions");
|
||||
const { getFullWarnings } = require("../src/shared/addressWarnings");
|
||||
const { HISTORY_OFF_TEXT, escapeHtml } = require("../src/popup/views/helpers");
|
||||
|
||||
const RPC_URL = "https://rpc.example.invalid";
|
||||
const BLOCKSCOUT = "https://eth.blockscout.com/api/v2";
|
||||
const RECIPIENT = "0x2222222222222222222222222222222222222222";
|
||||
const TX_HASH = "0x" + "ab".repeat(32);
|
||||
const ZERO_WORD = "0x" + "0".repeat(64);
|
||||
|
||||
// USDC, from the bundled list, as Blockscout reports a holding of it.
|
||||
const USDC_ROW = {
|
||||
value: "5000000",
|
||||
token: {
|
||||
type: "ERC-20",
|
||||
address_hash: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
||||
symbol: "USDC",
|
||||
name: "USD Coin",
|
||||
decimals: "6",
|
||||
holders_count: "50000",
|
||||
},
|
||||
};
|
||||
|
||||
const realFetch = globalThis.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
cleanupPopup();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("a new profile", () => {
|
||||
test("has both settings off", () => {
|
||||
expect(DEFAULT_STATE.useBlockscout).toBe(false);
|
||||
expect(DEFAULT_STATE.checkRecipientsOnEtherscan).toBe(false);
|
||||
const loaded = normalizePersisted(undefined);
|
||||
expect(loaded.useBlockscout).toBe(false);
|
||||
expect(loaded.checkRecipientsOnEtherscan).toBe(false);
|
||||
});
|
||||
|
||||
test("keeps either setting the user turned on", () => {
|
||||
const loaded = normalizePersisted({
|
||||
useBlockscout: true,
|
||||
checkRecipientsOnEtherscan: true,
|
||||
});
|
||||
expect(loaded.useBlockscout).toBe(true);
|
||||
expect(loaded.checkRecipientsOnEtherscan).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("where each request is made", () => {
|
||||
// Every URL fetch() was asked for, in order.
|
||||
let asked;
|
||||
|
||||
beforeEach(() => {
|
||||
asked = [];
|
||||
globalThis.fetch = jest.fn(async (url) => {
|
||||
asked.push(String(url));
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () =>
|
||||
String(url).endsWith("/token-balances")
|
||||
? [USDC_ROW]
|
||||
: { items: [] },
|
||||
text: async () => "<html><body></body></html>",
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = realFetch;
|
||||
});
|
||||
|
||||
test("Blockscout's token balances are asked for only while Use Blockscout is on", async () => {
|
||||
expect(
|
||||
await fetchTokenBalances(ADDRESS, BLOCKSCOUT, false, []),
|
||||
).toEqual([]);
|
||||
expect(asked).toEqual([]);
|
||||
|
||||
const rows = await fetchTokenBalances(ADDRESS, BLOCKSCOUT, true, []);
|
||||
expect(rows.map((row) => row.symbol)).toEqual(["USDC"]);
|
||||
expect(asked).toEqual([
|
||||
BLOCKSCOUT + "/addresses/" + ADDRESS + "/token-balances",
|
||||
]);
|
||||
});
|
||||
|
||||
test("the transaction history is asked for only while Use Blockscout is on", async () => {
|
||||
expect(
|
||||
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, false, "0x1"),
|
||||
).toEqual([]);
|
||||
expect(asked).toEqual([]);
|
||||
|
||||
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, true, "0x1");
|
||||
expect(asked.sort()).toEqual([
|
||||
BLOCKSCOUT +
|
||||
"/addresses/" +
|
||||
ADDRESS +
|
||||
"/token-transfers?type=ERC-20",
|
||||
BLOCKSCOUT + "/addresses/" + ADDRESS + "/transactions",
|
||||
]);
|
||||
});
|
||||
|
||||
test("the Etherscan label check is made only while Check recipients on Etherscan is on", async () => {
|
||||
const provider = {
|
||||
getCode: async () => "0x",
|
||||
getTransactionCount: async () => 1,
|
||||
getBalance: async () => 0n,
|
||||
};
|
||||
await getFullWarnings(RECIPIENT, provider, { chainId: "0x1" });
|
||||
await getFullWarnings(RECIPIENT, provider, {
|
||||
chainId: "0x1",
|
||||
checkRecipientsOnEtherscan: false,
|
||||
});
|
||||
expect(asked).toEqual([]);
|
||||
|
||||
await getFullWarnings(RECIPIENT, provider, {
|
||||
chainId: "0x1",
|
||||
checkRecipientsOnEtherscan: true,
|
||||
});
|
||||
expect(asked).toEqual(["https://etherscan.io/address/" + RECIPIENT]);
|
||||
});
|
||||
|
||||
describe("a balance refresh", () => {
|
||||
// The RPC endpoint answers every balance with zero, and every other
|
||||
// call, the ENS reverse lookup's, with no resolver.
|
||||
beforeEach(() => {
|
||||
FetchRequest.registerGetUrl(async (req) => {
|
||||
const body = JSON.parse(Buffer.from(req.body).toString("utf8"));
|
||||
const reply = (r) => ({
|
||||
jsonrpc: "2.0",
|
||||
id: r.id,
|
||||
result: r.method === "eth_getBalance" ? "0x0" : ZERO_WORD,
|
||||
});
|
||||
const answer = Array.isArray(body)
|
||||
? body.map(reply)
|
||||
: reply(body);
|
||||
return {
|
||||
statusCode: 200,
|
||||
statusMessage: "OK",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: Buffer.from(JSON.stringify(answer)),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc());
|
||||
});
|
||||
|
||||
// An address that was refreshed while Use Blockscout was on, and holds
|
||||
// the USDC Blockscout reported then.
|
||||
function addressWithUsdc() {
|
||||
return {
|
||||
address: ADDRESS,
|
||||
tokenBalances: [
|
||||
{
|
||||
address: USDC_ROW.token.address_hash,
|
||||
name: "USD Coin",
|
||||
symbol: "USDC",
|
||||
decimals: 6,
|
||||
balance: "5.0",
|
||||
holders: 50000,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function refresh(addr, useBlockscout) {
|
||||
await refreshBalances(
|
||||
[{ addresses: [addr] }],
|
||||
RPC_URL,
|
||||
BLOCKSCOUT,
|
||||
useBlockscout,
|
||||
[],
|
||||
"mainnet",
|
||||
);
|
||||
}
|
||||
|
||||
test("asks Blockscout nothing while Use Blockscout is off, and then lists no token the user has not tracked", async () => {
|
||||
const addr = addressWithUsdc();
|
||||
await refresh(addr, false);
|
||||
expect(asked).toEqual([]);
|
||||
expect(addr.tokenBalances).toEqual([]);
|
||||
});
|
||||
|
||||
test("asks Blockscout for the token balances while Use Blockscout is on", async () => {
|
||||
const addr = addressWithUsdc();
|
||||
await refresh(addr, true);
|
||||
expect(asked).toEqual([
|
||||
BLOCKSCOUT + "/addresses/" + ADDRESS + "/token-balances",
|
||||
]);
|
||||
expect(addr.tokenBalances.map((row) => row.symbol)).toEqual([
|
||||
"USDC",
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("the popup", () => {
|
||||
// Every URL the booted page handed to fetch().
|
||||
function fetched() {
|
||||
return globalThis.fetch.mock.calls.map((call) => String(call[0]));
|
||||
}
|
||||
|
||||
function blockscoutOrEtherscan(url) {
|
||||
return url.includes("blockscout") || url.includes("etherscan");
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// The screens below log their failed RPC reads; the stub node in
|
||||
// popupBoot.js answers none.
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
test("hands its balance refresh and Home's history the stored Use Blockscout setting", async () => {
|
||||
await bootPopup(unversionedValidProfile());
|
||||
const {
|
||||
refreshBalances: popupRefresh,
|
||||
} = require("../src/shared/balances");
|
||||
const {
|
||||
fetchRecentTransactions: popupHistory,
|
||||
} = require("../src/shared/transactions");
|
||||
expect(popupRefresh.mock.calls[0][3]).toBe(false);
|
||||
expect(popupHistory.mock.calls[0][2]).toBe(false);
|
||||
cleanupPopup();
|
||||
|
||||
await bootPopup(unversionedValidProfile({ useBlockscout: true }));
|
||||
const {
|
||||
refreshBalances: onRefresh,
|
||||
} = require("../src/shared/balances");
|
||||
const {
|
||||
fetchRecentTransactions: onHistory,
|
||||
} = require("../src/shared/transactions");
|
||||
expect(onRefresh.mock.calls[0][3]).toBe(true);
|
||||
expect(onHistory.mock.calls[0][2]).toBe(true);
|
||||
});
|
||||
|
||||
test("Home says why there is no history and only tracked tokens while Use Blockscout is off", async () => {
|
||||
const page = await bootPopup(unversionedValidProfile());
|
||||
expect(page.node("home-tx-list").innerHTML).toContain(
|
||||
escapeHtml(HISTORY_OFF_TEXT),
|
||||
);
|
||||
expect(page.hidden("home-tokens-note")).toBe(false);
|
||||
cleanupPopup();
|
||||
|
||||
const on = await bootPopup(
|
||||
unversionedValidProfile({ useBlockscout: true }),
|
||||
);
|
||||
expect(on.node("home-tx-list").innerHTML).toContain(
|
||||
"No transactions found.",
|
||||
);
|
||||
expect(on.hidden("home-tokens-note")).toBe(true);
|
||||
});
|
||||
|
||||
// The transaction screen, as a reopened popup draws it.
|
||||
function onTransactionScreen(extra) {
|
||||
return unversionedValidProfile({
|
||||
currentView: "transaction",
|
||||
viewStack: ["main"],
|
||||
selectedWallet: 0,
|
||||
selectedAddress: 0,
|
||||
viewData: {
|
||||
tx: {
|
||||
hash: TX_HASH,
|
||||
from: ADDRESS,
|
||||
to: RECIPIENT,
|
||||
value: "1.0000",
|
||||
exactValue: "1.0",
|
||||
symbol: "ETH",
|
||||
timestamp: 1790000000,
|
||||
isError: false,
|
||||
direction: "sent",
|
||||
directionLabel: "Sent",
|
||||
contractAddress: null,
|
||||
chainId: "0x1",
|
||||
},
|
||||
},
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
test("TransactionDetail asks Blockscout for the transaction only while Use Blockscout is on", async () => {
|
||||
const page = await bootPopup(onTransactionScreen());
|
||||
expect(page.visibleViews()).toEqual(["transaction"]);
|
||||
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([]);
|
||||
expect(page.hidden("tx-detail-blockscout-note")).toBe(false);
|
||||
cleanupPopup();
|
||||
|
||||
const on = await bootPopup(
|
||||
onTransactionScreen({ useBlockscout: true }),
|
||||
);
|
||||
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([
|
||||
BLOCKSCOUT + "/transactions/" + TX_HASH,
|
||||
]);
|
||||
expect(on.hidden("tx-detail-blockscout-note")).toBe(true);
|
||||
});
|
||||
|
||||
// The confirmation screen for a send of 0.1 ETH to RECIPIENT, as a
|
||||
// reopened popup draws it.
|
||||
function onConfirmScreen(extra) {
|
||||
return unversionedValidProfile({
|
||||
currentView: "confirm-tx",
|
||||
viewStack: ["main"],
|
||||
selectedWallet: 0,
|
||||
selectedAddress: 0,
|
||||
viewData: {
|
||||
pendingTx: {
|
||||
token: "ETH",
|
||||
from: ADDRESS,
|
||||
to: RECIPIENT,
|
||||
amount: "0.1",
|
||||
chainId: "0x1",
|
||||
},
|
||||
},
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
test("ConfirmTx asks Etherscan about the recipient only while Check recipients on Etherscan is on", async () => {
|
||||
const page = await bootPopup(onConfirmScreen());
|
||||
expect(page.visibleViews()).toEqual(["confirm-tx"]);
|
||||
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([]);
|
||||
expect(page.hidden("confirm-etherscan-note")).toBe(false);
|
||||
expect(page.hidden("confirm-etherscan-warning")).toBe(true);
|
||||
cleanupPopup();
|
||||
|
||||
const on = await bootPopup(
|
||||
onConfirmScreen({ checkRecipientsOnEtherscan: true }),
|
||||
);
|
||||
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([
|
||||
"https://etherscan.io/address/" + RECIPIENT,
|
||||
]);
|
||||
expect(on.hidden("confirm-etherscan-note")).toBe(true);
|
||||
expect(on.hidden("confirm-etherscan-warning")).toBe(false);
|
||||
});
|
||||
|
||||
// Ticks or unticks a checkbox on the page, as the user does.
|
||||
async function setBox(page, id, checked) {
|
||||
const box = page.node(id);
|
||||
box.checked = checked;
|
||||
for (const fn of box.listeners.change) await fn();
|
||||
await page.settle();
|
||||
}
|
||||
|
||||
test("Welcome offers both settings unticked, and ticking one there stores it", async () => {
|
||||
const page = await bootPopup(undefined);
|
||||
expect(page.visibleViews()).toEqual(["welcome"]);
|
||||
expect(page.node("welcome-use-blockscout").checked).toBe(false);
|
||||
expect(page.node("welcome-check-recipients-on-etherscan").checked).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
await setBox(page, "welcome-use-blockscout", true);
|
||||
expect(page.storage.read("autistmask").useBlockscout).toBe(true);
|
||||
await setBox(page, "welcome-check-recipients-on-etherscan", true);
|
||||
expect(page.storage.read("autistmask").checkRecipientsOnEtherscan).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("Settings shows both settings unticked, and ticking one there stores it", async () => {
|
||||
const page = await bootPopup(unversionedValidProfile());
|
||||
await page.click("btn-settings");
|
||||
expect(page.node("settings-use-blockscout").checked).toBe(false);
|
||||
expect(
|
||||
page.node("settings-check-recipients-on-etherscan").checked,
|
||||
).toBe(false);
|
||||
|
||||
await setBox(page, "settings-use-blockscout", true);
|
||||
expect(page.storage.read("autistmask").useBlockscout).toBe(true);
|
||||
await setBox(page, "settings-check-recipients-on-etherscan", true);
|
||||
expect(page.storage.read("autistmask").checkRecipientsOnEtherscan).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// A token the user tracks, and one Blockscout reported that the user has
|
||||
// not tracked. Both are written in lower case, as the balance rows' and
|
||||
// the dropdown's token ids are compared below.
|
||||
const TRACKED = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
||||
const UNTRACKED = TOKEN_ADDRESS.toLowerCase();
|
||||
const SECOND_ADDRESS = "0x3333333333333333333333333333333333333333";
|
||||
|
||||
function tokenRow(address, symbol) {
|
||||
return {
|
||||
address,
|
||||
name: symbol,
|
||||
symbol,
|
||||
decimals: 18,
|
||||
balance: "5.0",
|
||||
holders: 50000,
|
||||
};
|
||||
}
|
||||
|
||||
// AddressDetail of the first of two addresses, as a reopened popup draws
|
||||
// it. Both addresses hold the tracked token. While "Use Blockscout" is on,
|
||||
// both hold the untracked one too.
|
||||
function onAddressDetail(useBlockscout) {
|
||||
const profile = unversionedValidProfile({
|
||||
useBlockscout,
|
||||
currentView: "address",
|
||||
viewStack: ["main"],
|
||||
selectedWallet: 0,
|
||||
selectedAddress: 0,
|
||||
trackedTokens: [
|
||||
{ address: TRACKED, name: "TRK", symbol: "TRK", decimals: 18 },
|
||||
],
|
||||
});
|
||||
const wallet = profile.wallets[0];
|
||||
wallet.nextIndex = 2;
|
||||
wallet.addresses.push({ address: SECOND_ADDRESS, balance: "0.0" });
|
||||
for (const addr of wallet.addresses) {
|
||||
addr.tokenBalances = useBlockscout
|
||||
? [tokenRow(TRACKED, "TRK"), tokenRow(UNTRACKED, "TST")]
|
||||
: [tokenRow(TRACKED, "TRK")];
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
// The token ids of the options the page has created since it had created
|
||||
// `from` elements. The page stub keeps no children, so the options Send's
|
||||
// dropdown was filled with are read from there.
|
||||
function optionsSince(page, from) {
|
||||
return page.document.created
|
||||
.slice(from)
|
||||
.filter((el) => el.tagName === "OPTION")
|
||||
.map((el) => el.value.toLowerCase());
|
||||
}
|
||||
|
||||
test("unticking Use Blockscout from AddressDetail's gear takes the untracked token off AddressDetail, Send's dropdown and Home on Back", async () => {
|
||||
const page = await bootPopup(onAddressDetail(true));
|
||||
expect(page.visibleViews()).toEqual(["address"]);
|
||||
expect(page.node("address-balances").innerHTML).toContain(UNTRACKED);
|
||||
|
||||
await page.click("btn-settings");
|
||||
await setBox(page, "settings-use-blockscout", false);
|
||||
const from = page.document.created.length;
|
||||
await page.click("btn-settings-back");
|
||||
|
||||
expect(page.visibleViews()).toEqual(["address"]);
|
||||
const balances = page.node("address-balances").innerHTML;
|
||||
expect(balances).toContain(TRACKED);
|
||||
expect(balances).not.toContain(UNTRACKED);
|
||||
expect(page.hidden("address-tokens-note")).toBe(false);
|
||||
expect(optionsSince(page, from)).toEqual([TRACKED]);
|
||||
|
||||
await page.click("btn-send");
|
||||
expect(page.visibleViews()).toEqual(["send"]);
|
||||
expect(page.hidden("send-token-dropdown")).toBe(false);
|
||||
expect(page.hidden("send-tokens-note")).toBe(false);
|
||||
|
||||
await page.click("btn-send-back");
|
||||
await page.click("btn-address-back");
|
||||
expect(page.visibleViews()).toEqual(["main"]);
|
||||
const home = page.node("wallet-list").innerHTML;
|
||||
expect(home).toContain(TRACKED);
|
||||
expect(home).not.toContain(UNTRACKED);
|
||||
|
||||
const stored = page.storage.read("autistmask");
|
||||
for (const addr of stored.wallets[0].addresses) {
|
||||
expect(
|
||||
addr.tokenBalances.map((row) => row.address.toLowerCase()),
|
||||
).toEqual([TRACKED]);
|
||||
}
|
||||
});
|
||||
|
||||
// Send's dropdown was filled under the old setting, and Back cannot draw
|
||||
// Send again. So Back lands on the screen under it.
|
||||
test.each([
|
||||
["unticking", true],
|
||||
["ticking", false],
|
||||
])(
|
||||
"%s Use Blockscout from Send's gear lands Back on AddressDetail",
|
||||
async (_how, before) => {
|
||||
const page = await bootPopup(onAddressDetail(before));
|
||||
await page.click("btn-send");
|
||||
expect(page.visibleViews()).toEqual(["send"]);
|
||||
|
||||
await page.click("btn-settings");
|
||||
await setBox(page, "settings-use-blockscout", !before);
|
||||
await page.click("btn-settings-back");
|
||||
expect(page.visibleViews()).toEqual(["address"]);
|
||||
expect(page.hidden("address-tokens-note")).toBe(!before);
|
||||
},
|
||||
);
|
||||
|
||||
test("Send says under its token dropdown that only tracked tokens are listed while Use Blockscout is off", async () => {
|
||||
const off = await bootPopup(onAddressDetail(false));
|
||||
await off.click("btn-send");
|
||||
expect(off.hidden("send-token-dropdown")).toBe(false);
|
||||
expect(off.hidden("send-tokens-note")).toBe(false);
|
||||
cleanupPopup();
|
||||
|
||||
const on = await bootPopup(onAddressDetail(true));
|
||||
await on.click("btn-send");
|
||||
expect(on.hidden("send-token-dropdown")).toBe(false);
|
||||
expect(on.hidden("send-tokens-note")).toBe(true);
|
||||
});
|
||||
|
||||
test("Send opened from AddressToken shows neither the dropdown nor its line", async () => {
|
||||
const page = await bootPopup({
|
||||
...onAddressDetail(false),
|
||||
currentView: "address-token",
|
||||
viewStack: ["main", "address"],
|
||||
selectedToken: "ETH",
|
||||
});
|
||||
expect(page.visibleViews()).toEqual(["address-token"]);
|
||||
await page.click("btn-address-token-send");
|
||||
expect(page.visibleViews()).toEqual(["send"]);
|
||||
expect(page.hidden("send-token-dropdown")).toBe(true);
|
||||
expect(page.hidden("send-token-static")).toBe(false);
|
||||
});
|
||||
|
||||
test("DeleteAddress says only tracked tokens are listed while Use Blockscout is off", async () => {
|
||||
for (const useBlockscout of [false, true]) {
|
||||
const page = await bootPopup(onAddressDetail(useBlockscout));
|
||||
require("../src/popup/views/deleteAddress").show(0, 1);
|
||||
expect(page.visibleViews()).toEqual(["delete-address-confirm"]);
|
||||
expect(page.hidden("delete-address-tokens-note")).toBe(
|
||||
useBlockscout,
|
||||
);
|
||||
cleanupPopup();
|
||||
}
|
||||
});
|
||||
|
||||
// Turning the setting off dropped the untracked token. A refresh that
|
||||
// started while it was on must not bring the token back when it returns.
|
||||
// The same refresh in the background is in
|
||||
// tests/backgroundStateIsolation.test.js.
|
||||
test("a refresh that returns after Use Blockscout was turned off saves none of its tokens", async () => {
|
||||
const page = await bootPopup(onAddressDetail(true));
|
||||
const { refreshBalances: held } = require("../src/shared/balances");
|
||||
let answer;
|
||||
held.mockImplementationOnce(async (wallets) => {
|
||||
await new Promise((resolve) => {
|
||||
answer = resolve;
|
||||
});
|
||||
wallets[0].addresses[0].tokenBalances = [
|
||||
tokenRow(TRACKED, "TRK"),
|
||||
tokenRow(UNTRACKED, "TST"),
|
||||
];
|
||||
});
|
||||
const ticking = page.tick();
|
||||
await page.settle();
|
||||
|
||||
await page.click("btn-settings");
|
||||
await setBox(page, "settings-use-blockscout", false);
|
||||
const before = page.storage.read("autistmask").lastBalanceRefresh;
|
||||
answer();
|
||||
await ticking;
|
||||
|
||||
const stored = page.storage.read("autistmask");
|
||||
expect(
|
||||
stored.wallets[0].addresses[0].tokenBalances.map((row) =>
|
||||
row.address.toLowerCase(),
|
||||
),
|
||||
).toEqual([TRACKED]);
|
||||
expect(stored.lastBalanceRefresh).toBe(before);
|
||||
await page.click("btn-settings-back");
|
||||
expect(page.node("address-balances").innerHTML).not.toContain(
|
||||
UNTRACKED,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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]),
|
||||
}));
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -247,6 +247,7 @@ async function openSend(token, tokenUnits = null) {
|
||||
state.wallets,
|
||||
"https://rpc.example.invalid",
|
||||
"https://blockscout.example/api/v2",
|
||||
true,
|
||||
state.trackedTokens,
|
||||
"mainnet",
|
||||
);
|
||||
|
||||
@@ -50,8 +50,14 @@ function explorerPage(title) {
|
||||
// The explorer labels no address.
|
||||
global.fetch = jest.fn(async () => explorerPage("Etherscan"));
|
||||
|
||||
// The Etherscan label check is made, so its box can be shown.
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
|
||||
global.chrome = {
|
||||
storage: makeStorageStub({
|
||||
autistmask: { checkRecipientsOnEtherscan: true },
|
||||
}),
|
||||
runtime: { sendMessage() {} },
|
||||
};
|
||||
|
||||
// A stub DOM: every id resolves to a recording element.
|
||||
const elements = new Map();
|
||||
|
||||
@@ -142,13 +142,19 @@ jest.mock("../src/popup/views/txStatus", () => ({
|
||||
}));
|
||||
|
||||
// The Etherscan label check is its only caller here: the explorer pages it
|
||||
// asks for are recorded, and nothing is fetched.
|
||||
// asks for are recorded, and nothing is fetched. The stored profile has the
|
||||
// check turned on.
|
||||
global.fetch = jest.fn(() => {
|
||||
throw new Error("tests must not perform network requests");
|
||||
});
|
||||
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
|
||||
global.chrome = {
|
||||
storage: makeStorageStub({
|
||||
autistmask: { checkRecipientsOnEtherscan: true },
|
||||
}),
|
||||
runtime: { sendMessage() {} },
|
||||
};
|
||||
|
||||
// A stub DOM: every id resolves to a recording element.
|
||||
const elements = new Map();
|
||||
|
||||
@@ -297,6 +297,8 @@ describe.each([
|
||||
state.selectedWallet = 0;
|
||||
state.selectedAddress = 0;
|
||||
state.selectedToken = "ETH";
|
||||
// The history is Blockscout's.
|
||||
state.useBlockscout = true;
|
||||
});
|
||||
|
||||
test("a contract creation's row says so, with no colour dot and no address line", async () => {
|
||||
|
||||
+23
-2
@@ -653,6 +653,12 @@ function traceEnabled(raw) {
|
||||
* confirmed receipt instead of null, so a wait screen resolves.
|
||||
* @param {boolean} [opts.failReceiptLookup] answer eth_getTransactionReceipt
|
||||
* with an error, so every receipt lookup fails; read at request time.
|
||||
* @param {string} [opts.etherscanLabel] the label Etherscan's page gives the
|
||||
* address asked about, such as "Fake_Phishing1"; none when unset. Read at
|
||||
* request time.
|
||||
* @param {object[]} [opts.requests] every request this handler sees, from the
|
||||
* popup's pages and the background worker alike, appended in order as
|
||||
* { method, url, fromWorker }.
|
||||
* @returns {Promise<{waitForServiceWorkerTraffic: (ms: number) =>
|
||||
* Promise<string|null>}>}
|
||||
*/
|
||||
@@ -691,6 +697,13 @@ async function installNetworkStubs(ctx, opts) {
|
||||
const origin = fromWorker ? "[sw] " : "[page] ";
|
||||
console.log("# routed " + origin + req.method() + " " + req.url());
|
||||
}
|
||||
if (Array.isArray(opts.requests)) {
|
||||
opts.requests.push({
|
||||
method: req.method(),
|
||||
url: req.url(),
|
||||
fromWorker,
|
||||
});
|
||||
}
|
||||
|
||||
// JSON-RPC endpoint (any host): a POST with a JSON-RPC body.
|
||||
if (req.method() === "POST") {
|
||||
@@ -762,12 +775,20 @@ async function installNetworkStubs(ctx, opts) {
|
||||
return route.fulfill({ status: 204, body: "" });
|
||||
}
|
||||
|
||||
// Best-effort Etherscan address labels: served as an empty page.
|
||||
// Best-effort Etherscan address labels: served as an empty page, or
|
||||
// as a page labelling the address with opts.etherscanLabel.
|
||||
if (url.hostname.endsWith("etherscan.io")) {
|
||||
const title = opts.etherscanLabel
|
||||
? "<title>" +
|
||||
opts.etherscanLabel +
|
||||
" | Address: " +
|
||||
p.split("/").pop() +
|
||||
" | Etherscan</title>"
|
||||
: "";
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "text/html",
|
||||
body: "<html><body></body></html>",
|
||||
body: "<html><head>" + title + "</head><body></body></html>",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -269,6 +269,65 @@ test("the exact confirmation phrase erases the record and reloads into Welcome (
|
||||
}
|
||||
});
|
||||
|
||||
// ------------------ Blockscout and Etherscan are sent no user data (#668)
|
||||
//
|
||||
// A fresh install sends Blockscout and Etherscan none of the user's data until
|
||||
// the user turns on "Use Blockscout" or "Check recipients on Etherscan". One
|
||||
// request is made with the setting off: pressing "Save" beside the Blockscout
|
||||
// URL asks that URL for `/stats`. It carries no user data.
|
||||
//
|
||||
// The route handler in tests/e2e/network.js records every request the
|
||||
// extension makes in env.routeOpts.requests, from its pages and its background
|
||||
// worker. Recording starts when launch() in tests/e2e/harness.js installs that
|
||||
// handler, before the first popup page opens. A request the background worker
|
||||
// makes while it starts up comes earlier and is not recorded.
|
||||
//
|
||||
// Before either setting is changed, these tests drive Home, AddressDetail,
|
||||
// AddressToken and ConfirmTx, and the background's own balance refresh. Then
|
||||
// they turn each setting on and watch its requests start. "Use Blockscout" is
|
||||
// also turned off once and on again, to see a token the user has not tracked
|
||||
// leave every list. TransactionDetail, which also asks Blockscout, is left to
|
||||
// the unit tests in tests/blockscoutEtherscanSettings.test.js.
|
||||
|
||||
// What the three transaction lists say while Use Blockscout is off. Written
|
||||
// out here rather than imported, so the assertion does not compare the popup
|
||||
// with itself.
|
||||
const HISTORY_OFF =
|
||||
'No transaction history is shown, because "Use Blockscout" is off in Settings.';
|
||||
|
||||
// The requests made so far to Blockscout or Etherscan.
|
||||
function explorerRequests(env) {
|
||||
return env.routeOpts.requests.filter(({ url }) => {
|
||||
const host = new URL(url).hostname;
|
||||
return host.endsWith("blockscout.com") || host.endsWith("etherscan.io");
|
||||
});
|
||||
}
|
||||
|
||||
// Waits until the list `listId` says what Use Blockscout being off leaves out.
|
||||
async function waitForHistoryOff(page, listId) {
|
||||
await page.waitForFunction(
|
||||
([id, want]) => document.getElementById(id).textContent.trim() === want,
|
||||
[listId, HISTORY_OFF],
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
}
|
||||
|
||||
test("Welcome offers Use Blockscout and Check recipients on Etherscan, both unticked (#668)", async (env) => {
|
||||
await visible(env.page, "#view-welcome");
|
||||
for (const id of [
|
||||
"welcome-use-blockscout",
|
||||
"welcome-check-recipients-on-etherscan",
|
||||
]) {
|
||||
const st = await controlState(env.page, id);
|
||||
assert(st !== null, "Welcome has no #" + id);
|
||||
assert(
|
||||
st.tag === "input" && st.type === "checkbox",
|
||||
"#" + id + " is a <" + st.tag + " type=" + st.type + ">",
|
||||
);
|
||||
assert(st.checked === false, "#" + id + " is ticked on a new install");
|
||||
}
|
||||
});
|
||||
|
||||
// The address scan that follows creating the wallet is held, so the popup
|
||||
// reloads while it is in flight. As for the reloads mid-refresh further down,
|
||||
// the assertion for that is the harness's own: a console.error from the page
|
||||
@@ -296,6 +355,261 @@ test("wallet creation through the UI reaches the main view, and reloading mid-sc
|
||||
assert(addrCount > 0, "no addresses rendered in the wallet list");
|
||||
});
|
||||
|
||||
test("with neither setting changed, Home, AddressDetail, AddressToken and ConfirmTx say what is off (#668)", async (env) => {
|
||||
// Funded, so Send leads to the confirmation screen.
|
||||
env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI);
|
||||
try {
|
||||
await visible(env.page, "#view-main");
|
||||
await waitForHistoryOff(env.page, "home-tx-list");
|
||||
await visible(env.page, "#home-tokens-note");
|
||||
await env.page.waitForFunction(
|
||||
(want) =>
|
||||
document
|
||||
.getElementById("wallet-list")
|
||||
.textContent.includes(want),
|
||||
quantity(FUNDED_ETH_WEI),
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
|
||||
await openAddressDetail(env.page);
|
||||
await waitForHistoryOff(env.page, "tx-list");
|
||||
await visible(env.page, "#address-tokens-note");
|
||||
|
||||
await env.page.locator('#address-balances [data-token="ETH"]').click();
|
||||
await visible(env.page, "#view-address-token");
|
||||
await waitForHistoryOff(env.page, "address-token-tx-list");
|
||||
await env.page.click("#btn-address-token-back");
|
||||
await visible(env.page, "#view-address");
|
||||
|
||||
await goToConfirm(env.page, {
|
||||
token: "ETH",
|
||||
balance: FUNDED_ETH_TEXT + " ETH",
|
||||
amount: COMFORTABLE_AMOUNT,
|
||||
});
|
||||
await waitForEstimate(env.page);
|
||||
await visible(env.page, "#confirm-etherscan-note");
|
||||
assert(
|
||||
!(await env.page.isVisible("#confirm-etherscan-warning")),
|
||||
"ConfirmTx keeps the Etherscan warning's place while no Etherscan check is made",
|
||||
);
|
||||
await backToAddress(env.page);
|
||||
} finally {
|
||||
env.routeOpts.ethBalanceWei = null;
|
||||
}
|
||||
});
|
||||
|
||||
// The background refreshes balances on its alarm, once a minute. It skips a
|
||||
// refresh when the stored record says one ran in the last 30 seconds, and the
|
||||
// open popup's own refresh runs every 10. So the popup is closed first. The
|
||||
// background worker then writes that record back to say no refresh has run, as
|
||||
// another extension page saving it would.
|
||||
test("with neither setting changed, nothing has reached Blockscout or Etherscan, the background's refresh included (#668)", async (env) => {
|
||||
await waitForPersisted(
|
||||
env.page,
|
||||
"currentView",
|
||||
"address",
|
||||
"before the popup is closed",
|
||||
);
|
||||
await env.page.close();
|
||||
const [worker] = env.ctx.serviceWorkers();
|
||||
await worker.evaluate(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
chrome.storage.local.get("autistmask", (r) => {
|
||||
const record = r.autistmask;
|
||||
record.lastBalanceRefresh = 0;
|
||||
chrome.storage.local.set({ autistmask: record }, resolve);
|
||||
});
|
||||
}),
|
||||
);
|
||||
const before = env.routeOpts.requests.length;
|
||||
const deadline = Date.now() + 75000;
|
||||
while (
|
||||
!env.routeOpts.requests
|
||||
.slice(before)
|
||||
.some((r) => r.fromWorker && r.method === "POST")
|
||||
) {
|
||||
assert(
|
||||
Date.now() < deadline,
|
||||
"the background's balance refresh never asked the RPC endpoint",
|
||||
);
|
||||
await sleep(250);
|
||||
}
|
||||
// Its RPC reads and any Blockscout request start together.
|
||||
await sleep(3000);
|
||||
|
||||
const seen = explorerRequests(env);
|
||||
assert(
|
||||
seen.length === 0,
|
||||
"a fresh install asked Blockscout or Etherscan: " +
|
||||
JSON.stringify(seen),
|
||||
);
|
||||
// The watch saw the popup's own traffic as well as the worker's.
|
||||
assert(
|
||||
env.routeOpts.requests.some((r) => !r.fromWorker),
|
||||
"no request from the popup was recorded",
|
||||
);
|
||||
// It reopens on the address screen the test above left it on.
|
||||
env.page = await openPopup(env.ctx, env.popupUrl);
|
||||
await visible(env.page, "#view-address");
|
||||
});
|
||||
|
||||
// Ticked from AddressDetail's gear, so Back lands on the screen that said the
|
||||
// setting is off.
|
||||
test("turning on Use Blockscout from AddressDetail's gear asks Blockscout for that screen's history on Back, then for the tokens (#668)", async (env) => {
|
||||
await openAddressDetail(env.page);
|
||||
await waitForHistoryOff(env.page, "tx-list");
|
||||
await openSettings(env.page);
|
||||
await env.page.check("#settings-use-blockscout");
|
||||
await waitForPersisted(env.page, "useBlockscout", true, "once ticked");
|
||||
const historyAsked = env.page.waitForRequest(
|
||||
(r) =>
|
||||
r.url().includes("blockscout.com") &&
|
||||
r.url().endsWith("/transactions"),
|
||||
);
|
||||
const tokensAsked = env.page.waitForRequest(
|
||||
(r) => r.url().endsWith("/token-balances"),
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
await env.page.click("#btn-settings-back");
|
||||
await visible(env.page, "#view-address");
|
||||
await historyAsked;
|
||||
await env.page.waitForFunction(
|
||||
() =>
|
||||
document
|
||||
.getElementById("tx-list")
|
||||
.textContent.includes("No transactions found."),
|
||||
null,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
assert(
|
||||
!(await env.page.isVisible("#address-tokens-note")),
|
||||
"AddressDetail still says only tracked tokens are listed",
|
||||
);
|
||||
|
||||
await goHome(env.page);
|
||||
await tokensAsked;
|
||||
await env.page.waitForFunction(
|
||||
() =>
|
||||
document
|
||||
.getElementById("home-tx-list")
|
||||
.textContent.includes("No transactions found."),
|
||||
null,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
assert(
|
||||
!(await env.page.isVisible("#home-tokens-note")),
|
||||
"Home still says only tracked tokens are listed",
|
||||
);
|
||||
});
|
||||
|
||||
// Unticked from AddressDetail's gear while the address holds the stub token,
|
||||
// which the user has not tracked and only Blockscout reports. Ticked again at
|
||||
// the end, so the rest of the run has Blockscout as before.
|
||||
test("turning off Use Blockscout from AddressDetail's gear takes an untracked token off AddressDetail, Send and Home on Back (#668)", async (env) => {
|
||||
env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI);
|
||||
env.routeOpts.seedTokenBalance = true;
|
||||
const tokenRow = '[data-token="' + STUB_TOKEN.address + '"]';
|
||||
try {
|
||||
await settleOnMain(env, { ethWei: FUNDED_ETH_WEI, expectToken: true });
|
||||
await openAddressDetail(env.page);
|
||||
await visible(env.page, "#address-balances " + tokenRow);
|
||||
|
||||
await openSettings(env.page);
|
||||
await env.page.uncheck("#settings-use-blockscout");
|
||||
await waitForPersisted(
|
||||
env.page,
|
||||
"useBlockscout",
|
||||
false,
|
||||
"once unticked",
|
||||
);
|
||||
await env.page.click("#btn-settings-back");
|
||||
await visible(env.page, "#view-address");
|
||||
await visible(env.page, "#address-tokens-note");
|
||||
assert(
|
||||
(await env.page
|
||||
.locator("#address-balances " + tokenRow)
|
||||
.count()) === 0,
|
||||
"AddressDetail still lists the untracked token",
|
||||
);
|
||||
|
||||
await env.page.click("#btn-send");
|
||||
await visible(env.page, "#view-send");
|
||||
await visible(env.page, "#send-tokens-note");
|
||||
const offered = await env.page.$$eval("#send-token option", (opts) =>
|
||||
opts.map((opt) => opt.value.toLowerCase()),
|
||||
);
|
||||
assert(
|
||||
!offered.includes(STUB_TOKEN.address),
|
||||
"Send's dropdown still offers the untracked token",
|
||||
);
|
||||
await env.page.click("#btn-send-back");
|
||||
await visible(env.page, "#view-address");
|
||||
|
||||
await goHome(env.page);
|
||||
await visible(env.page, "#home-tokens-note");
|
||||
assert(
|
||||
(await env.page.locator("#wallet-list " + tokenRow).count()) === 0,
|
||||
"Home still lists the untracked token",
|
||||
);
|
||||
} finally {
|
||||
await openSettings(env.page);
|
||||
await env.page.check("#settings-use-blockscout");
|
||||
await waitForPersisted(env.page, "useBlockscout", true, "ticked again");
|
||||
await env.page.click("#btn-settings-back");
|
||||
env.routeOpts.ethBalanceWei = null;
|
||||
env.routeOpts.seedTokenBalance = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Ticked from ConfirmTx's gear, so Back lands on the screen that said no
|
||||
// Etherscan check was made. Left on for the rest of the run, so the later
|
||||
// confirmation screens are driven with the check made.
|
||||
test("turning on Check recipients on Etherscan from ConfirmTx's gear has ConfirmTx ask Etherscan on Back and show its label (#668)", async (env) => {
|
||||
env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI);
|
||||
env.routeOpts.etherscanLabel = "Fake_Phishing668";
|
||||
try {
|
||||
await settleOnMain(env, { ethWei: FUNDED_ETH_WEI });
|
||||
await goToConfirm(env.page, {
|
||||
token: "ETH",
|
||||
balance: FUNDED_ETH_TEXT + " ETH",
|
||||
amount: COMFORTABLE_AMOUNT,
|
||||
});
|
||||
await visible(env.page, "#confirm-etherscan-note");
|
||||
|
||||
await openSettings(env.page);
|
||||
await env.page.check("#settings-check-recipients-on-etherscan");
|
||||
await waitForPersisted(
|
||||
env.page,
|
||||
"checkRecipientsOnEtherscan",
|
||||
true,
|
||||
"once ticked",
|
||||
);
|
||||
const before = env.routeOpts.requests.length;
|
||||
await env.page.click("#btn-settings-back");
|
||||
await visible(env.page, "#view-confirm-tx");
|
||||
await visible(env.page, "#confirm-etherscan-warning", 30000);
|
||||
assert(
|
||||
!(await env.page.isVisible("#confirm-etherscan-note")),
|
||||
"ConfirmTx says no Etherscan check was made, with the check on",
|
||||
);
|
||||
const asked = env.routeOpts.requests
|
||||
.slice(before)
|
||||
.map((r) => r.url.toLowerCase())
|
||||
.filter((url) => url.includes("etherscan.io"));
|
||||
assert(
|
||||
asked.length === 1 &&
|
||||
asked[0] ===
|
||||
"https://etherscan.io/address/" + STUB_COUNTERPARTY,
|
||||
"ConfirmTx asked Etherscan for " + JSON.stringify(asked),
|
||||
);
|
||||
await backToAddress(env.page);
|
||||
} finally {
|
||||
env.routeOpts.ethBalanceWei = null;
|
||||
env.routeOpts.etherscanLabel = null;
|
||||
}
|
||||
});
|
||||
|
||||
test("add token screen opens from address detail (#150)", async (env) => {
|
||||
await openAddressDetail(env.page);
|
||||
await env.page.click("#btn-add-token");
|
||||
@@ -5258,10 +5572,17 @@ async function main() {
|
||||
seedReceipt: false,
|
||||
// Whether eth_getTransactionReceipt fails instead of answering (#315).
|
||||
failReceiptLookup: false,
|
||||
// The label Etherscan's page gives an address, when it gives one
|
||||
// (#668).
|
||||
etherscanLabel: null,
|
||||
// Every raw signed transaction handed to eth_sendRawTransaction, in
|
||||
// order. The dApp transaction round trip asserts against these bytes
|
||||
// rather than against anything the extension reported about them.
|
||||
broadcastTransactions: [],
|
||||
// Every request the extension makes, in order, from when launch()
|
||||
// installs the route handler, before the first popup page opens
|
||||
// (#668).
|
||||
requests: [],
|
||||
};
|
||||
|
||||
let session;
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// ENS lookups ask nothing but the RPC endpoint. A resolver can answer that a
|
||||
// name is resolved off-chain, naming a web address to fetch the answer from.
|
||||
// ethers fetches it unless the provider has `disableCcipRead` set. That request
|
||||
// would carry the name to a server other than the RPC endpoint
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/668). With the flag set, such a
|
||||
// name does not resolve.
|
||||
//
|
||||
// The provider getProvider() builds runs against a fake JSON-RPC node. Its
|
||||
// answers go through ethers' own request hook, which also records the address
|
||||
// of every request ethers makes, the off-chain fetch included.
|
||||
|
||||
const { FetchRequest, Interface } = require("ethers");
|
||||
const { getProvider } = require("../src/shared/balances");
|
||||
|
||||
const RPC_URL = "https://rpc.example.invalid";
|
||||
const RESOLVER = "0x5555555555555555555555555555555555555555";
|
||||
const USER = "0x1111111111111111111111111111111111111111";
|
||||
const NAME = "offchain.eth";
|
||||
|
||||
// The ENS registry's resolver(), and the resolver's functions ethers calls:
|
||||
//
|
||||
// - the registry names RESOLVER for every name
|
||||
// - the resolver says it resolves names through resolve()
|
||||
// - it gives NAME as the reverse record of any address
|
||||
// - it answers resolve() with the OffchainLookup error, which says the name
|
||||
// is resolved off-chain
|
||||
const ens = new Interface([
|
||||
"function resolver(bytes32) view returns (address)",
|
||||
"function supportsInterface(bytes4) view returns (bool)",
|
||||
"function name(bytes32) view returns (string)",
|
||||
"function resolve(bytes, bytes) view returns (bytes)",
|
||||
"error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData)",
|
||||
]);
|
||||
|
||||
let asked;
|
||||
|
||||
// One JSON-RPC answer. Anything but a call to one of the functions above is
|
||||
// refused.
|
||||
function reply(req) {
|
||||
const envelope = { jsonrpc: "2.0", id: req.id };
|
||||
const call =
|
||||
req.method === "eth_call" ? ens.parseTransaction(req.params[0]) : null;
|
||||
if (call === null) {
|
||||
return {
|
||||
...envelope,
|
||||
error: { code: -32601, message: "method not found" },
|
||||
};
|
||||
}
|
||||
if (call.name === "resolve") {
|
||||
const offchain = ens.encodeErrorResult("OffchainLookup", [
|
||||
RESOLVER,
|
||||
["https://gateway.example.invalid/{sender}/{data}.json"],
|
||||
"0x1234",
|
||||
"0x12345678",
|
||||
"0x",
|
||||
]);
|
||||
return {
|
||||
...envelope,
|
||||
error: { code: 3, message: "execution reverted", data: offchain },
|
||||
};
|
||||
}
|
||||
const answers = {
|
||||
resolver: [RESOLVER],
|
||||
supportsInterface: [true],
|
||||
name: [NAME],
|
||||
};
|
||||
return {
|
||||
...envelope,
|
||||
result: ens.encodeFunctionResult(call.name, answers[call.name]),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
asked = [];
|
||||
FetchRequest.registerGetUrl(async (req) => {
|
||||
asked.push(req.url);
|
||||
// Any other address is the off-chain fetch, answered as not found.
|
||||
if (req.url !== RPC_URL) {
|
||||
return {
|
||||
statusCode: 404,
|
||||
statusMessage: "Not Found",
|
||||
headers: {},
|
||||
body: null,
|
||||
};
|
||||
}
|
||||
const body = JSON.parse(Buffer.from(req.body).toString("utf8"));
|
||||
const answer = Array.isArray(body) ? body.map(reply) : reply(body);
|
||||
return {
|
||||
statusCode: 200,
|
||||
statusMessage: "OK",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: Buffer.from(JSON.stringify(answer)),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc());
|
||||
});
|
||||
|
||||
test("a name typed into Send that is resolved off-chain does not resolve, and only the RPC endpoint is asked", async () => {
|
||||
const provider = getProvider(RPC_URL, "mainnet");
|
||||
const [outcome] = await Promise.allSettled([provider.resolveName(NAME)]);
|
||||
expect(asked.filter((url) => url !== RPC_URL)).toEqual([]);
|
||||
expect(outcome).toEqual({ status: "fulfilled", value: null });
|
||||
});
|
||||
|
||||
test("the reverse lookup of a user's address whose name is resolved off-chain finds no name, and only the RPC endpoint is asked", async () => {
|
||||
const provider = getProvider(RPC_URL, "mainnet");
|
||||
const [outcome] = await Promise.allSettled([provider.lookupAddress(USER)]);
|
||||
expect(asked.filter((url) => url !== RPC_URL)).toEqual([]);
|
||||
expect(outcome).toEqual({ status: "fulfilled", value: null });
|
||||
});
|
||||
@@ -114,6 +114,7 @@ async function fetchOnto(items, trackedTokens = []) {
|
||||
const balances = await fetchTokenBalances(
|
||||
HOLDER,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
trackedTokens,
|
||||
);
|
||||
state.trackedTokens = trackedTokens;
|
||||
|
||||
@@ -150,19 +150,23 @@ describe("the balance-list spam gate", () => {
|
||||
|
||||
test("a token with plenty of reported holders is listed", async () => {
|
||||
respondWith([item()]);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||
expect(balances).toHaveLength(1);
|
||||
expect(balances[0].holders).toBe(50000);
|
||||
});
|
||||
|
||||
test("a token reporting zero holders is still excluded", async () => {
|
||||
respondWith([item({ token: { holders_count: "0" } })]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
test("an unknown holder count does not admit an unvouched token", async () => {
|
||||
respondWith([item({ token: { holders_count: null } })]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
// The path that reaches the send selector and the history filter: a token
|
||||
@@ -171,7 +175,7 @@ describe("the balance-list spam gate", () => {
|
||||
// zero that would then hide it downstream.
|
||||
test("a tracked token with an unknown count is listed with holders null", async () => {
|
||||
respondWith([item({ token: { holders_count: undefined } })]);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, [
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [
|
||||
{ address: NOVEL_TOKEN.toUpperCase() },
|
||||
]);
|
||||
expect(balances).toHaveLength(1);
|
||||
@@ -188,7 +192,7 @@ describe("the balance-list spam gate", () => {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||
expect(balances).toHaveLength(1);
|
||||
expect(balances[0].holders).toBeNull();
|
||||
});
|
||||
|
||||
@@ -436,6 +436,8 @@ describe.each([
|
||||
});
|
||||
|
||||
test("the transaction detail screen's type line and fee", async () => {
|
||||
// The fee is Blockscout's.
|
||||
state.useBlockscout = true;
|
||||
debugFetch.mockImplementationOnce(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
|
||||
@@ -286,8 +286,12 @@ describe("a malformed fraudContracts", () => {
|
||||
function stubSendDocument() {
|
||||
const select = { innerHTML: "", children: [] };
|
||||
select.appendChild = (child) => select.children.push(child);
|
||||
const elements = {
|
||||
"send-token": select,
|
||||
"send-tokens-note": { classList: { toggle: () => {} } },
|
||||
};
|
||||
globalThis.document = {
|
||||
getElementById: (id) => (id === "send-token" ? select : null),
|
||||
getElementById: (id) => elements[id] || null,
|
||||
createElement: () => ({ value: "", textContent: "" }),
|
||||
};
|
||||
return select;
|
||||
|
||||
@@ -618,6 +618,8 @@ const CONTRACT = [
|
||||
"hideFraudContracts",
|
||||
"hideDustTransactions",
|
||||
"utcTimestamps",
|
||||
"useBlockscout",
|
||||
"checkRecipientsOnEtherscan",
|
||||
"debugMode",
|
||||
].map((field) => ({
|
||||
field,
|
||||
@@ -625,8 +627,9 @@ const CONTRACT = [
|
||||
// A flag: only ever tested for truthiness, and written back verbatim.
|
||||
hostile: [42, "notabool", { a: 1 }],
|
||||
// Both answers to that truthiness test have to be driven, and 0 is a
|
||||
// value src/ never writes for a flag. For utcTimestamps and debugMode
|
||||
// the falsy answer is also the DEFAULT_STATE default.
|
||||
// value src/ never writes for a flag. For utcTimestamps,
|
||||
// useBlockscout, checkRecipientsOnEtherscan and debugMode the falsy
|
||||
// answer is also the DEFAULT_STATE default.
|
||||
falsy: [0],
|
||||
})),
|
||||
];
|
||||
|
||||
@@ -44,7 +44,8 @@ test("a flash message's timer does not fire after cleanupPopup()", async () => {
|
||||
|
||||
// A boot onto a screen that sends a request no mocked module answers: that
|
||||
// request reaches the boot's fetch() stub, never the internet
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/559).
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/559). Both requests are made
|
||||
// only while the user has the setting for them on.
|
||||
function restoringOnto(view, viewData) {
|
||||
return unversionedValidProfile({
|
||||
currentView: view,
|
||||
@@ -52,6 +53,8 @@ function restoringOnto(view, viewData) {
|
||||
selectedAddress: 0,
|
||||
viewStack: ["main"],
|
||||
viewData,
|
||||
useBlockscout: true,
|
||||
checkRecipientsOnEtherscan: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ test("the popup open on AddressDetail shows the new network and its balances", a
|
||||
// Its next refresh runs on Sepolia, and what it fetched is stored.
|
||||
await page.tick();
|
||||
const { refreshBalances } = require("../src/shared/balances");
|
||||
expect(refreshBalances.mock.lastCall[4]).toBe("sepolia");
|
||||
expect(refreshBalances.mock.lastCall[5]).toBe("sepolia");
|
||||
const stored = page.storage.read("autistmask");
|
||||
expect(stored.networkId).toBe("sepolia");
|
||||
expect(stored.lastBalanceRefresh).toBeGreaterThan(0);
|
||||
|
||||
@@ -88,6 +88,7 @@ test("the balance refresh", async () => {
|
||||
wallets,
|
||||
RPC_URL,
|
||||
"https://explorer.example.invalid/api/v2",
|
||||
true,
|
||||
[],
|
||||
"mainnet",
|
||||
);
|
||||
|
||||
@@ -224,6 +224,7 @@ async function refreshWith(balanceWei, tokenItems = []) {
|
||||
state.wallets,
|
||||
"https://rpc.example.invalid",
|
||||
"https://blockscout.example/api/v2",
|
||||
true,
|
||||
state.trackedTokens,
|
||||
"mainnet",
|
||||
);
|
||||
|
||||
@@ -217,6 +217,7 @@ async function refreshWith(balanceWei, tokenItems = []) {
|
||||
state.wallets,
|
||||
"https://rpc.example.invalid",
|
||||
"https://blockscout.example/api/v2",
|
||||
true,
|
||||
state.trackedTokens,
|
||||
"mainnet",
|
||||
);
|
||||
|
||||
@@ -27,8 +27,12 @@ let select;
|
||||
function installStubDocument() {
|
||||
select = { innerHTML: "", children: [] };
|
||||
select.appendChild = (child) => select.children.push(child);
|
||||
const elements = {
|
||||
"send-token": select,
|
||||
"send-tokens-note": { classList: { toggle: () => {} } },
|
||||
};
|
||||
globalThis.document = {
|
||||
getElementById: (id) => (id === "send-token" ? select : null),
|
||||
getElementById: (id) => elements[id] || null,
|
||||
createElement: () => ({ value: "", textContent: "" }),
|
||||
};
|
||||
}
|
||||
|
||||
+28
-11
@@ -485,8 +485,12 @@ describe("surface 2: the Send token selector", () => {
|
||||
function render(tokenBalances) {
|
||||
select = { innerHTML: "", children: [] };
|
||||
select.appendChild = (child) => select.children.push(child);
|
||||
const elements = {
|
||||
"send-token": select,
|
||||
"send-tokens-note": { classList: { toggle: () => {} } },
|
||||
};
|
||||
globalThis.document = {
|
||||
getElementById: (id) => (id === "send-token" ? select : null),
|
||||
getElementById: (id) => elements[id] || null,
|
||||
createElement: () => ({ value: "", textContent: "" }),
|
||||
};
|
||||
renderSendTokenSelect({
|
||||
@@ -582,12 +586,14 @@ describe("surface 3: the balance list", () => {
|
||||
// 1,000-holder floor and was listed as a holding named ETH.
|
||||
test("a fake ETH token clearing the holder floor is filtered", async () => {
|
||||
respondWith([fakeEthItem()]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
test("tracking the fake token manually does not admit it either", async () => {
|
||||
respondWith([fakeEthItem({ holders_count: "0" })]);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, [
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [
|
||||
{ address: FAKE_ETH_CONTRACT },
|
||||
]);
|
||||
expect(balances).toEqual([]);
|
||||
@@ -597,12 +603,16 @@ describe("surface 3: the balance list", () => {
|
||||
// their belief about what they own, and it renders the symbol into HTML.
|
||||
test("a padded fake ETH token is filtered too", async () => {
|
||||
respondWith([fakeEthItem({ symbol: " ETH " })]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
test("a fake ETH token padded with a no-break space is filtered", async () => {
|
||||
respondWith([fakeEthItem({ symbol: NBSP + "ETH" + NBSP })]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
// The false-positive direction on the surface that matters most: a real
|
||||
@@ -617,7 +627,7 @@ describe("surface 3: the balance list", () => {
|
||||
decimals: "6",
|
||||
}),
|
||||
]);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||
expect(balances).toHaveLength(1);
|
||||
expect(balances[0].symbol).toBe(" USDC ");
|
||||
});
|
||||
@@ -631,7 +641,7 @@ describe("surface 3: the balance list", () => {
|
||||
decimals: "6",
|
||||
}),
|
||||
]);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||
expect(balances).toHaveLength(1);
|
||||
expect(balances[0].symbol).toBe("USDC");
|
||||
});
|
||||
@@ -642,7 +652,9 @@ describe("surface 3: the balance list", () => {
|
||||
// before the symbol rule is consulted.
|
||||
test("a non-ERC-20 row claiming ETH never reaches the symbol rule", async () => {
|
||||
respondWith([fakeEthItem({ type: "ERC-721" })]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
// The adjacent finding from the same review as issue #260: the type gate
|
||||
@@ -660,16 +672,20 @@ describe("surface 3: the balance list", () => {
|
||||
decimals: "6",
|
||||
}),
|
||||
]);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||
expect(balances).toHaveLength(1);
|
||||
expect(balances[0].symbol).toBe("USDC");
|
||||
});
|
||||
|
||||
test("case insensitivity does not admit another token type", async () => {
|
||||
respondWith([fakeEthItem({ type: "erc-721" })]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
respondWith([fakeEthItem({ type: "ERC-20-EXTRA" })]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
|
||||
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, [])).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
// The money test: the user holds real ETH and has been airdropped a fake
|
||||
@@ -682,6 +698,7 @@ describe("surface 3: the balance list", () => {
|
||||
[{ addresses: [addr] }],
|
||||
"https://rpc.example.invalid",
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
[],
|
||||
"mainnet",
|
||||
);
|
||||
|
||||
@@ -134,6 +134,8 @@ beforeEach(() => {
|
||||
state.selectedWallet = 0;
|
||||
state.selectedAddress = 0;
|
||||
state.selectedToken = "ETH";
|
||||
// The history is Blockscout's.
|
||||
state.useBlockscout = true;
|
||||
});
|
||||
|
||||
describe.each([
|
||||
|
||||
@@ -158,6 +158,7 @@ beforeEach(() => {
|
||||
globalThis.document = makeDocument();
|
||||
globalThis.window = { location: { search: "" } };
|
||||
state.blockscoutUrl = EXPLORER;
|
||||
state.useBlockscout = true;
|
||||
state.networkEndpoints = {};
|
||||
state.wallets = [
|
||||
{
|
||||
@@ -177,6 +178,7 @@ test("a token transfer's decoded details name the token contract and the amount
|
||||
const [row] = await fetchRecentTransactions(
|
||||
HOLDER,
|
||||
EXPLORER,
|
||||
true,
|
||||
NETWORKS.mainnet.chainId,
|
||||
);
|
||||
expect(row.to).toBe(RECIPIENT);
|
||||
@@ -209,6 +211,7 @@ test("with mainnet active, a Sepolia transaction is read from Sepolia's Blocksco
|
||||
const [row] = await fetchRecentTransactions(
|
||||
HOLDER,
|
||||
EXPLORER,
|
||||
true,
|
||||
NETWORKS.sepolia.chainId,
|
||||
);
|
||||
|
||||
@@ -237,6 +240,7 @@ test("with mainnet active and no Blockscout remembered for Sepolia, a Sepolia tr
|
||||
const [row] = await fetchRecentTransactions(
|
||||
HOLDER,
|
||||
EXPLORER,
|
||||
true,
|
||||
NETWORKS.sepolia.chainId,
|
||||
);
|
||||
mockAsked.length = 0;
|
||||
@@ -258,6 +262,7 @@ async function showSepoliaTransferWithMainnetActive() {
|
||||
const [row] = await fetchRecentTransactions(
|
||||
HOLDER,
|
||||
EXPLORER,
|
||||
true,
|
||||
NETWORKS.sepolia.chainId,
|
||||
);
|
||||
transactionDetail.show(row);
|
||||
|
||||
@@ -132,6 +132,7 @@ async function refresh(addresses, trackedTokens) {
|
||||
[{ addresses }],
|
||||
RPC_URL,
|
||||
EXPLORER_URL,
|
||||
true,
|
||||
trackedTokens,
|
||||
"mainnet",
|
||||
new AbortController().signal,
|
||||
|
||||
@@ -287,6 +287,7 @@ describe("Back after a switch from the Settings gear", () => {
|
||||
currentView: "confirm-tx",
|
||||
selectedWallet: 0,
|
||||
selectedAddress: 0,
|
||||
checkRecipientsOnEtherscan: true,
|
||||
viewData: {
|
||||
pendingTx: {
|
||||
token: "ETH",
|
||||
|
||||
@@ -125,6 +125,8 @@ beforeEach(() => {
|
||||
state.selectedWallet = 0;
|
||||
state.selectedAddress = 0;
|
||||
state.selectedToken = "ETH";
|
||||
// The history is Blockscout's.
|
||||
state.useBlockscout = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -1219,7 +1219,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
|
||||
test("queries only the two Blockscout endpoints for the address", async () => {
|
||||
respondWith([], []);
|
||||
await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
await fetchRecentTransactions(VICTIM, BLOCKSCOUT, true, "0x1");
|
||||
expect(debugFetch).toHaveBeenCalledTimes(2);
|
||||
const urls = debugFetch.mock.calls.map((c) => c[0]);
|
||||
expect(urls).toContain(
|
||||
@@ -1283,7 +1283,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(txs).toHaveLength(1);
|
||||
const merged = txs[0];
|
||||
// The received leg (the swap output) supplies the display amount.
|
||||
@@ -1320,7 +1325,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(txs).toHaveLength(1);
|
||||
expect(txs[0].symbol).toBe("USDC");
|
||||
expect(txs[0].value).toBe("1500.5000");
|
||||
@@ -1346,7 +1356,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
});
|
||||
respondWith([], [leg("1000000"), leg("2000000")]);
|
||||
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(txs).toHaveLength(1);
|
||||
// Keyed by hash plus contract, so the later leg wins.
|
||||
expect(txs[0].exactValue).toBe("2.0");
|
||||
@@ -1386,7 +1401,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(txs.map((t) => t.symbol).sort()).toEqual(["USDC", "WETH"]);
|
||||
});
|
||||
|
||||
@@ -1427,7 +1447,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(txs).toHaveLength(1);
|
||||
expect(txs[0].symbol).toBe("USDC");
|
||||
expect(txs[0].exactValue).toBe("1.0");
|
||||
@@ -1453,7 +1478,13 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
});
|
||||
respondWith([item(6), item(8), item(7)], []);
|
||||
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1", 2);
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
2,
|
||||
);
|
||||
expect(txs.map((t) => t.blockNumber)).toEqual([21000008, 21000007]);
|
||||
});
|
||||
|
||||
@@ -1479,6 +1510,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
const sepolia = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0xaa36a7",
|
||||
);
|
||||
expect(sepolia[0].symbol).toBe("SepoliaETH");
|
||||
@@ -1487,6 +1519,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
const mainnet = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(mainnet[0].symbol).toBe("ETH");
|
||||
@@ -1513,7 +1546,12 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
],
|
||||
);
|
||||
|
||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(txs).toHaveLength(1);
|
||||
expect(txs[0].contractAddress).toBe(FAKE_ETH_CONTRACT);
|
||||
expect(txs[0].holders).toBe(0);
|
||||
@@ -1555,6 +1593,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(txs[0].holders).toBeNull();
|
||||
@@ -1565,6 +1604,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(txs[0].holders).toBeNull();
|
||||
@@ -1575,6 +1615,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(filterTransactions(txs, filters()).transactions).toEqual(
|
||||
@@ -1591,6 +1632,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
const txs = await fetchRecentTransactions(
|
||||
VICTIM,
|
||||
BLOCKSCOUT,
|
||||
true,
|
||||
"0x1",
|
||||
);
|
||||
expect(txs[0].holders).toBe(0);
|
||||
@@ -1608,7 +1650,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
},
|
||||
}));
|
||||
await expect(
|
||||
fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1"),
|
||||
fetchRecentTransactions(VICTIM, BLOCKSCOUT, true, "0x1"),
|
||||
).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
@@ -3456,6 +3456,7 @@ async function showDetail(data) {
|
||||
globalThis.window = { location: { search: "" } };
|
||||
state.networkId = "mainnet";
|
||||
state.blockscoutUrl = "https://explorer.example/api/v2";
|
||||
state.useBlockscout = true;
|
||||
state.viewData = {};
|
||||
state.viewStack = [];
|
||||
state.currentView = null;
|
||||
|
||||
@@ -208,6 +208,7 @@ async function showDetail(row, activeNetworkId) {
|
||||
globalThis.window = { location: { search: "" } };
|
||||
state.networkId = activeNetworkId;
|
||||
state.blockscoutUrl = "https://explorer.example/api/v2";
|
||||
state.useBlockscout = true;
|
||||
state.viewData = {};
|
||||
state.viewStack = [];
|
||||
state.currentView = null;
|
||||
@@ -287,6 +288,7 @@ async function historyRow(method, sentTo, chainId) {
|
||||
const rows = await fetchRecentTransactions(
|
||||
RECIPIENT,
|
||||
"https://explorer.example/api/v2",
|
||||
true,
|
||||
chainId,
|
||||
);
|
||||
return rows[0];
|
||||
|
||||
@@ -172,7 +172,7 @@ async function fetchOnto(items) {
|
||||
statusText: "OK",
|
||||
json: async () => items,
|
||||
}));
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||
state.wallets = [
|
||||
{
|
||||
name: "Wallet 1",
|
||||
@@ -197,14 +197,14 @@ async function fetchOntoBoth(itemsA, itemsB) {
|
||||
statusText: "OK",
|
||||
json: async () => itemsA,
|
||||
}));
|
||||
const a = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
|
||||
const a = await fetchTokenBalances(HOLDER, BLOCKSCOUT, true, []);
|
||||
debugFetch.mockImplementation(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
json: async () => itemsB,
|
||||
}));
|
||||
const b = await fetchTokenBalances(SECOND_HOLDER, BLOCKSCOUT, []);
|
||||
const b = await fetchTokenBalances(SECOND_HOLDER, BLOCKSCOUT, true, []);
|
||||
state.wallets = [
|
||||
{
|
||||
name: "Wallet 1",
|
||||
|
||||
Reference in New Issue
Block a user