diff --git a/README.md b/README.md index f4e8ae7..0d2ac1e 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,29 @@ 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. The endpoint 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. The API receives + token symbols and nothing else. + +Two more services are sent none of the user's data until the user ticks a +checkbox for each. Welcome and Settings both offer the two checkboxes: + +- "Use Blockscout": a Blockscout block-explorer API, used for: + - transaction history + - the tokens the user has not tracked + - 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 +435,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. The background worker starts earlier, but it makes no request +while it starts up. 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 the recipient. + +Both settings stay on for the rest of the run. TransactionDetail with "Use +Blockscout" off is not driven here: while the setting is off, there is no +history row to open it from. The unit tests cover that case. + 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 @@ -724,13 +766,11 @@ Three limits are worth knowing, all real differences from the Chrome suite: in `tests/e2e/dapp.js`, which serves the dApp page and a JSON-RPC node and which the extension's `rpcUrl` is pointed at for the dApp steps; a JSON-RPC method that fixture does not model fails the run rather than answering `null`. - Everything else, Blockscout and the price feed among it, has no fixture and - simply fails, and the extension swallows its own fetch failures, so only the - _failure_ branches of that code are ever executed. A `ReferenceError` in the - success path of `renderTransactions`, or of price rendering, passes this suite - green. The offline run is also weaker than the Chrome suite's interception for - those calls: it proves nothing got out, but it cannot report which requests - were attempted. + The suite leaves "Use Blockscout" off, so Blockscout is not asked. A + `ReferenceError` in the success path of `renderTransactions`, or of price + rendering, passes this suite green. The offline run is also weaker than the + Chrome suite's interception: it proves nothing got out, but it cannot report + which requests were attempted. - **The site-connection prompt always opens in a window of its own.** The profile turns off `extensions.openPopupWithoutUserGesture.enabled`, so `src/background/index.js` falls back from the toolbar popup, which WebDriver @@ -845,13 +885,16 @@ 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 prices.js — ETH/USD and token/USD via CoinDesk API scamlist.js — known fraud contract addresses state.js — persisted state (extension storage) + storedSettings.js — "Use Blockscout" and "Check recipients on Etherscan", + read from storage for the requests those settings gate symbolSpoof.js — the known-symbol spoof rule, shared by all surfaces tokenList.js — top ERC-20 tokens by market cap (hardcoded) transactions.js — tx history fetching + anti-poisoning filters @@ -1122,25 +1165,39 @@ with the same floor (`.5` reads `.5000`). **Specific Exception — unknown token scale:** Calldata carries base units and no scale, so every amount on the dApp approval screen needs the token's `decimals`. -It is resolved from the bundled token list, then from the tokens the user tracks -on the network the transaction is for, then, when that is the active network, -from what the block explorer reported for the contract in the wallet's balances, -which are the active network's only (`resolveTokenDecimals()` in -`src/shared/approvalAmount.js`). Where none of them answers, the amount is not -formatted: the line reads `5000000000 base units (decimals unknown)`. A guessed -scale is not an approximation but a different number — 1,000 units of a -6-decimal token formatted at 18 decimals reads `0.000000001` — on the screen -whose only job is to state what is being authorized. Both amount paths of that -screen take this rule: the ERC-20 `transfer`/`approve` line -(`src/popup/views/approval.js`) and the swap's `Amount`, `Min. received`, -`Moved from the sender` and `Permit2 allowance` lines and the `Paid to` line of -a move into the router (`src/shared/uniswap.js`). The token permission warning -on the signature screen takes the same rule for its amounts. An unbounded -allowance or permit needs no scale to describe and is still shown as -`Unlimited`. A source's answer counts only if it is a whole number from 0 to 80: -`decimals()` returns a `uint8`, but `formatUnits()` cannot format more than 80 -decimal places, so a token that reports 81 to 255 is shown as one whose scale -nothing knows. +The wallet takes the `decimals` from these sources, in this order +(`resolveTokenDecimals()` in `src/shared/approvalAmount.js`): + +- the bundled token list +- the tokens the user tracks on the network the transaction is for +- what the block explorer reported for the contract in the wallet's balances. + Those balances cover the active network only. This source is used only while + "Use Blockscout" is on (`tokenSourcesByChainId()` in + `src/popup/views/helpers.js`). + +The token's name comes from the same sources. While "Use Blockscout" is off, a +token gets a line under it when the bundled list and the tracked tokens lack its +name or decimals. The line says "Blockscout's report of your balances is not +used to name this token, because "Use Blockscout" is off in Settings." These +screens show the line: + +- TxApproval and SignApproval +- WaitTx, SuccessTx and ErrorTx, which follow TxApproval + +Where none of the sources answers, the amount is not formatted. The amount line +then reads `5000000000 base units (decimals unknown)`. A guessed scale is not an +approximation but a different number — 1,000 units of a 6-decimal token +formatted at 18 decimals reads `0.000000001` — on the screen whose only job is +to state what is being authorized. Both amount paths of that screen take this +rule: the ERC-20 `transfer`/`approve` line (`src/popup/views/approval.js`) and +the swap's `Amount`, `Min. received`, `Moved from the sender` and +`Permit2 allowance` lines and the `Paid to` line of a move into the router +(`src/shared/uniswap.js`). The token permission warning on the signature screen +takes the same rule for its amounts. An unbounded allowance or permit needs no +scale to describe and is still shown as `Unlimited`. A source's answer counts +only if it is a whole number from 0 to 80: `decimals()` returns a `uint8`, but +`formatUnits()` cannot format more than 80 decimal places, so a token that +reports 81 to 255 is shown as one whose scale nothing knows. The rule holds only if nothing invents a scale UPSTREAM of it. Those three sources are read as authoritative, so a value written into one of them cannot be @@ -1163,12 +1220,13 @@ and compare against. Reading the stored field directly instead answers `null` for a bundled or tracked token the explorer merely omitted, which is not a refusal the wallet has any reason to make. -The Send screen also consults every address's explorer reports, so a contract -two addresses report different `decimals` for has no scale there, and the stored -balance, formatted at one of those scales, is withdrawn with it. The Send -screen's `Current balance` and the confirmation screen's balance line then both -read `unknown (SYMBOL)`. The balance list formats each explorer row as it is -fetched, without that cross-address check, and shows the row's figure. +While "Use Blockscout" is on, the Send screen also consults every address's +explorer reports. The reports for two addresses may give different `decimals` +for one contract. That token then has no scale on Send. Its stored balance is +not shown either. The Send screen's `Current balance` and the confirmation +screen's balance line then both read `unknown (SYMBOL)`. The balance list +formats each explorer row as it is fetched, without that cross-address check, +and shows the row's figure. **Decoded amount lines on the transaction approval screen:** the `Amount` line of a decoded ERC-20 call, and the `Amount` and `Min. received` lines of a @@ -1388,23 +1446,39 @@ 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 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. + +Blockscout may answer just before the user turns the setting off. So the popup's +refresh and the background's both check the stored setting again when they save. +If the setting is off by then, the save drops every token the user has not +tracked. + +A tracked token is shown when its balance is nonzero, and any other ERC-20 when +its balance is nonzero and it is in the bundled known-token list or has 1,000 or +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 +1652,18 @@ 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 that 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. + +Any other open popup takes either change from the stored record, as it takes a +network switch (below). That popup then redraws its top screen as a reopened +popup would. A screen that a reopened popup cannot render, such as Send, is +replaced by Home. + 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 +1705,17 @@ view would leave a wallet one click from deletion. screen in that case. - **Elements**: - "Welcome! To get started, add a wallet." text + - A well opening with "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." + - In the well, the "Use Blockscout" and "Check recipients on Etherscan" + checkboxes, the same two as in Settings. Under each, a sentence says what + the setting sends and to which service. 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 +1736,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 +1745,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 +1818,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 +1892,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 +1915,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 @@ -1825,13 +1937,14 @@ view would leave a wallet one click from deletion. - **Transitions**: - "Review" (valid inputs, ENS resolved) → **ConfirmTx** - "Review" with an unresolvable ENS name or an invalid amount → flash - message, no screen change. An amount is valid when it is a number above - zero, written as digits with at most one decimal point, with nothing but - zeros after the token's last decimal place. ETH has 18 places, and a token - with more, or with an unknown number, is held to 18. Zeros past those - places leave the amount unchanged, so `1.0000000` of a 6-place token - passes and is sent as exactly 1. ConfirmTx makes the same check before - anything can be sent + message, no screen change. A name with no address, or one resolved + off-chain, gets "That ENS name has no on-chain address." An amount is + valid when it is a number above zero, written as digits with at most one + decimal point, with nothing but zeros after the token's last decimal + place. ETH has 18 places, and a token with more, or with an unknown + number, is held to 18. Zeros past those places leave the amount unchanged, + so `1.0000000` of a 6-place token passes and is sent as exactly 1. + ConfirmTx makes the same check before anything can be sent - "Back" → previous screen (Home, AddressDetail, or AddressToken) #### ConfirmTx (`confirm-tx`) @@ -1875,7 +1988,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 the setting 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 @@ -1952,6 +2070,9 @@ view would leave a wallet one click from deletion. `Unknown (could not be decoded)` with no symbol, not the transaction's ETH value. When that swap's `Token In` is `ETH (native)`, the transaction's ETH value in the network's currency, as for any other transaction + - A line under the amount, worded as on TxApproval. While "Use Blockscout" + is off, the line shows when the bundled list and the tracked tokens lack + the token's name or decimals. - To: color dot + full address + etherscan link; for a contract creation, which has no recipient, "This transaction creates a new contract. It has no recipient." instead @@ -1984,7 +2105,9 @@ view would leave a wallet one click from deletion. - **Elements**: - "Transaction Confirmed" heading - Decoded action well (shown when the transaction carried recognized - calldata; the top-level Amount and To are hidden in that case) + calldata; the top-level Amount and To are hidden in that case). Each token + in the well gets TxApproval's line under it whenever TxApproval would show + that line. - Amount + symbol - To: color dot + full address + etherscan link, or for a contract creation the same sentence as on WaitTx @@ -2007,6 +2130,7 @@ view would leave a wallet one click from deletion. - Amount + symbol, or for a swap the approval screen showed no `Amount` for, the same as on WaitTx: `Unknown (could not be decoded)`, unless its `Token In` is `ETH (native)` + - While "Use Blockscout" is off, the same line under the amount as on WaitTx - To: color dot + full address + etherscan link, or for a contract creation the same sentence as on WaitTx - Error message (dashed border box) @@ -2041,7 +2165,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. While "Use + Blockscout" is off, Blockscout is not asked. One line then takes the place of + the block, network details, decoded details and raw data: "This screen shows + no block, decoded details, network details or raw data, 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 +2252,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 the setting shows and what it sends. With the + setting on, the wallet sends the Blockscout API the user's addresses + and the hash of each transaction opened + - Endpoint URL input + "Save" button. "Save" asks the URL for `/stats` + and saves the URL only if that request succeeds. The request carries + no user data and is made whether or not "Use Blockscout" is ticked. + - Etherscan: + - "Check recipients on Etherscan" checkbox, unticked until the user + ticks it + - A sentence saying the setting sends each recipient's address to + Etherscan, to check the address 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 +2290,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). From then on, every screen drawn + and every balance refresh uses the new setting. 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 +2312,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 +2470,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 @@ -2456,6 +2604,12 @@ view would leave a wallet one click from deletion. a swap pays only the From address, a `Paid to` line for each other address it pays, or saying that a token stays in the router or who it pays is unknown (see Transaction Decoding) + - A line under each token of the decoded action: "Blockscout's report of + your balances is not used to name this token, because "Use Blockscout" is + off in Settings." While "Use Blockscout" is off, the line shows when the + bundled list and the tracked tokens lack the token's name or decimals. + While the setting is on, Blockscout's report of the user's balances is a + third source of a token's name and decimals (see Unknown token scale). - From: color dot + full address + etherscan link - Contract: color dot + full address + etherscan link, token symbol label if known; for a contract creation, the same sentence as on WaitTx @@ -2535,7 +2689,10 @@ view would leave a wallet one click from deletion. the site puts in the message, except a `Permit`'s token, which is the domain's `verifyingContract`; any those fields do not give is shown as `Unknown`, and the domain, type and message lines still follow. Only typed - data that cannot be read at all is shown as raw text. + data that cannot be read at all is shown as raw text. A token gets the + same line under it as on TxApproval, whenever TxApproval would show that + line. As there, Blockscout's report of the user's balances is a source of + a token's name and decimals only while "Use Blockscout" is on. - Raw data (`personal_sign` and `eth_sign`): the message's hex exactly as the site sent it. The bytes it encodes are what is signed, as an EIP-191 personal message. @@ -2665,17 +2822,36 @@ 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 a +setting for each: + +- Blockscout, with "Use Blockscout" +- Etherscan, with "Check recipients on Etherscan" + +Both settings are off in a new install. Welcome and Settings both offer them. +Each setting applies to every network. + +Before each request that sends the user's data to Blockscout or Etherscan, the +wallet reads that service's setting from storage. The wallet reads the setting +again when the answer lands. While the setting is off, no such request is made, +and an answer that lands after the setting went off is dropped. + +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 the RPC endpoint alone. So a name + does not resolve when its resolver says the name is resolved off-chain. 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 +2863,44 @@ 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 its setting is 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 + - the names and decimals of those tokens, for TxApproval, SignApproval, + WaitTx, SuccessTx and ErrorTx + - token holder counts, for spam filtering + - a transaction's block, network details and calldata, for + TransactionDetail + - **Contacted**, while the setting is on: + - on every balance refresh, in the popup and in the background + - when Home, AddressDetail or AddressToken loads its history + - when TransactionDetail opens + - **While the setting is off**: Each screen that would show Blockscout's + data says instead that the setting is off. Only tracked tokens are listed. + - **Saving the URL**: pressing "Save" beside the URL in Settings asks that + URL for `/stats`, whether or not the setting is on. This request carries + no user data. + - **Data sent**: Ethereum addresses, and the hash of each transaction the + user opens. Blockscout receives the user's addresses to query their + transaction history and token balances. No private keys, passwords, or + signing operations are sent. + +- **Etherscan address labels** ("Check recipients on Etherscan"): While the + setting is on, ConfirmTx looks up the recipient's address on Etherscan each + time the screen is shown. The lookup checks for a phishing or scam label, + best-effort. This is a direct page fetch with no API key; the user's browser + makes the request. While the setting is off, ConfirmTx says that no Etherscan + check was made. + - **Data sent**: the recipient's address and nothing else. + +The etherscan links on the screens open a page in the browser when the user +clicks one. They are not requests the wallet makes. What the extension does NOT do: @@ -2702,16 +2909,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 +3093,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) diff --git a/docs/README.md b/docs/README.md index 424d7a2..63eb77f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 on a setting +for each. The settings are "Use Blockscout" and "Check recipients on Etherscan". +Both are off in a new install. Welcome and Settings both offer them. ### External Services @@ -83,11 +89,14 @@ verify confirmations. The default is a free public RPC endpoint. You can change this in Settings to any Ethereum JSON-RPC endpoint, including your own local node. -When it is contacted: on every balance refresh (every 10 seconds while the popup -is open, every 60 seconds in the background), when you type an ENS name into the -Send screen, when a send is prepared and broadcast, while a pending transaction -is polled for its receipt, and for the reverse ENS lookups used to label -addresses (cached for 12 hours). +When it is contacted: + +- on every balance refresh (every 10 seconds while the popup is open, every 60 + seconds in the background) +- when you review a send to an ENS name +- when a send is prepared and broadcast +- while a pending transaction is polled for its receipt +- for the reverse ENS lookups used to label addresses (cached for 12 hours) What gets sent: standard Ethereum JSON-RPC requests (balance queries, transaction broadcasts, gas estimates, ENS lookups, contract-code checks). Your @@ -96,16 +105,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 sends it your data +only while "Use Blockscout" is on. AutistMask 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 the endpoint in Settings to a self-hosted Blockscout instance. +While the setting is off, each screen that would show Blockscout's data says +instead that the setting is off. 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 checks the URL by asking +it for `/stats`. That happens whether or not the setting is on. The request +sends none of your data. + +What gets sent: your Ethereum addresses (to look up balances and transactions) +and the hash of each transaction you open. **CoinDesk CADLI price API** (`data-api.coindesk.com`) @@ -122,16 +146,21 @@ 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. +While "Check recipients on Etherscan" is on, AutistMask fetches the recipient's +public Etherscan address page when you review a send. AutistMask 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. -When it is contacted: each time you reach the send confirmation screen. +The setting is off in a new install. While it is off, the confirmation screen +says that no Etherscan check was made. -What gets sent: the recipient address you are about to send to, and your IP -address. Your own addresses are not sent. +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'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 +214,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. External Services above says what + each one does. 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 +252,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: +The wallet looks for tokens you hold 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. + +The wallet shows every token you track, whether or not "Use Blockscout" is on. +By default a tracked token 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". @@ -237,9 +273,12 @@ screen. Tokens can also be added from Settings, under "Tracked Tokens". ## Sending 1. Click "Send" from the home screen or an address detail view. -2. Select what to send (ETH, or any ERC-20 token with a balance on this address - that survives the spam filters). -3. Enter the recipient address or ENS name (e.g. `vitalik.eth`). +2. Select what to send: ETH, or a token you track that this address holds. While + "Use Blockscout" is on, Send also offers other tokens with a balance on this + address that survive the spam filters. +3. Enter the recipient address or ENS name (e.g. `vitalik.eth`). AutistMask does + not look up a name resolved off-chain. For such a name, enter the address + instead. 4. Enter the amount, or click "Max" to fill it in: a token's balance, cut to 18 decimal places, or your ETH balance minus the amount reserved for the network fee. @@ -257,8 +296,8 @@ The confirmation screen shows: reserve is what the network requires up front and what the balance check gates on; the refund of the difference is why the two differ - **Warnings** if the recipient is a contract, a burn address, one of your own - addresses, on the bundled scam-address list, or labelled as a phisher on - Etherscan + addresses, on the bundled scam-address list, or, while "Check recipients on + Etherscan" is on, labelled as a phisher on Etherscan After reviewing, enter your password and click "Sign & Send". The transaction will be broadcast to the network and you will see a waiting screen with a timer. @@ -269,8 +308,9 @@ screen with the transaction hash and an Etherscan link. You can also send from a token-specific view: on the address detail screen, click any token balance row. This opens a focused view showing only that token's -transactions. Clicking "Send" from here pre-selects and locks the token so you -cannot accidentally switch to a different one. +transactions. While "Use Blockscout" is off, the view says instead that no +transaction history is shown. Clicking "Send" from here pre-selects and locks +the token so you cannot accidentally switch to a different one. ## Receiving @@ -344,11 +384,12 @@ balances unless they are on the bundled known-token list or you added them yourself. Legitimate tokens have substantial holder counts; scam tokens deployed for address poisoning typically have zero. When the explorer reports no holder count for a token, or reports something other than a whole number in plain -digits (such as "1,000"), the count is unknown. An unknown count does not hide a -token from your transaction history or the send token list, and it does not get -a token into your balances either: such a token is listed only if it is on the -bundled known-token list or you added it yourself. The screen you reach by -clicking a token balance shows a "Holders:" line only when the count is known. +digits (such as "1,000"), the count is unknown. While "Use Blockscout" is off, +no holder count is known. An unknown count does not hide a token from your +transaction history or the send token list, and it does not get a token into +your balances either: such a token is listed only if it is on the bundled +known-token list or you added it yourself. The screen you reach by clicking a +token balance shows a "Holders:" line only when the count is known. **Fraud contract blocklist.** When AutistMask detects a fraudulent transfer, it adds the contract address to a local blocklist. Future transactions from that @@ -389,8 +430,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. diff --git a/src/background/index.js b/src/background/index.js index f9d6b9d..17adbf6 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -17,7 +17,11 @@ const { applyChainSwitchFields } = require("../shared/chainSwitchFields"); // the same prohibition reported early, not the guarantee. const { getState, updateState } = require("./state"); const { StateUnusableError } = require("../shared/stateSchema"); -const { refreshBalances, getProvider } = require("../shared/balances"); +const { + refreshBalances, + dropUntrackedTokens, + getProvider, +} = require("../shared/balances"); const { debugFetch, log } = require("../shared/log"); const { verifySignedTx, @@ -1386,6 +1390,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. +// +// Blockscout may have answered just before the user turned "Use Blockscout" +// off. So the save reads the stored setting again, and drops every token the +// user has not tracked if the setting is off. async function backgroundRefresh() { const s = await getState(); const now = Date.now(); @@ -1423,6 +1431,9 @@ async function backgroundRefresh() { } } } + if (!fresh.useBlockscout) { + dropUntrackedTokens(fresh.wallets, fresh.trackedTokens); + } fresh.lastBalanceRefresh = now; }); } diff --git a/src/popup/index.html b/src/popup/index.html index b41cf9e..bc758fb 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -43,6 +43,38 @@
+ Shows transaction history, transaction details and the + tokens you have not tracked. Sends your addresses, and + the hash of each transaction you open, to the Blockscout + API below. +
- Used to fetch token balances and transaction history. - Change this if you run your own Blockscout instance. + Change the URL below if you run your own Blockscout + instance.
+ Sends each recipient's address to Etherscan to check it + for a phishing or scam label. +
+@@ -1291,6 +1383,13 @@ >
A wallet always keeps at least one address. To remove the last one, delete the whole wallet from Settings instead. @@ -1504,6 +1603,16 @@
No wallets yet. Add one to get started.
'; @@ -447,7 +454,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(); diff --git a/src/popup/views/send.js b/src/popup/views/send.js index 6556c9f..a699562 100644 --- a/src/popup/views/send.js +++ b/src/popup/views/send.js @@ -10,6 +10,7 @@ const { renderAddressHtml, attachCopyHandlers, goBack, + tokenSourcesByChainId, } = require("./helpers"); const { state, currentAddress } = require("../../shared/state"); let ctx; @@ -141,6 +142,8 @@ function updateToValidation() { } function renderSendTokenSelect(addr) { + // Only Blockscout reports the tokens the user has not tracked. + $("send-tokens-note").classList.toggle("hidden", !!state.useBlockscout); const sel = $("send-token"); sel.innerHTML = ``; const fraudSet = new Set( @@ -185,7 +188,9 @@ function renderSendTokenSelect(addr) { // so the transfer is encoded with the number the user read rather than with // whatever the contract answers at signing time (see // src/shared/transferAmount.js). balances.js is formatting one explorer row at -// fetch time and cannot consult a state it is in the middle of replacing. +// fetch time and cannot consult a state it is in the middle of replacing. As on +// the approval screens, tokenSourcesByChainId() gives the wallets only while +// "Use Blockscout" is on. // // The two resolutions can therefore differ, and where they do, the stored // `balance` is a quantity computed at a scale this screen has just declined to @@ -198,10 +203,7 @@ function tokenBalanceAndDecimals(addr, token) { const tb = (addr.tokenBalances || []).find( (t) => t.address.toLowerCase() === token.toLowerCase(), ); - const tokenDecimals = resolveTokenDecimals(token, { - trackedTokens: state.trackedTokens, - wallets: state.wallets, - }); + const tokenDecimals = resolveTokenDecimals(token, tokenSourcesByChainId()); if (!tb) return { tokenBalance: "0", tokenDecimals }; if (tokenDecimals === null) return { tokenBalance: null, tokenDecimals }; return { tokenBalance: tb.balance ?? null, tokenDecimals }; @@ -401,8 +403,11 @@ function init(_ctx) { try { const provider = getProvider(state.rpcUrl, state.networkId); const resolved = await provider.resolveName(to); + // getProvider() does not look up a name resolved off-chain, + // so such a name comes back with no address too. The message + // covers both, in the one line the flash area shows. if (!resolved) { - showFlash("That ENS name has no address."); + showFlash("That ENS name has no on-chain address."); return; } resolvedTo = resolved; @@ -426,7 +431,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(); }); diff --git a/src/popup/views/settings.js b/src/popup/views/settings.js index 4d0d53a..1162a64 100644 --- a/src/popup/views/settings.js +++ b/src/popup/views/settings.js @@ -17,6 +17,7 @@ const { const { state, saveState, currentNetwork } = require("../../shared/state"); const { onChainSwitch } = require("../../shared/chainSwitch"); const { restorableStack } = require("../../shared/persistedState"); +const { dropUntrackedTokens } = require("../../shared/balances"); const { resetRenderedViews, markViewRendered } = require("../viewRouter"); const { log, @@ -201,6 +202,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(); @@ -247,16 +252,20 @@ async function renderSiteLists() { // A network switch, made by the user here or saved by the background once the // user approved a site's request (src/popup/index.js), leaves every screen on -// the page showing the previous network. These two leave the page as README's -// Screen Map describes, the first before the switch is saved, so that save -// keeps the cut for a popup closed straight after, the second once `state` -// holds the new network. +// the page showing the previous network. Two functions leave the page as +// README's Screen Map describes: +// +// - cutViewStack(), before the switch is saved, so that save keeps the cut for +// a popup closed straight after +// - showNewSettings(), once `state` holds the new network // // The stack is cut at the first screen that cannot be rendered from saved // state, such as Send or AddToken, and at AddressToken unless it shows the // native token: any other token there was picked on the previous network, so // it is no longer selected either. Without a wallet the stack starts at // Welcome, which that cut would drop, and Welcome shows nothing from a network. +// A change to "Use Blockscout" cuts the stack the same way (setUseBlockscout() +// below). function cutViewStack() { if (!state.hasWallet) return; if (state.selectedToken !== "ETH") { @@ -269,18 +278,40 @@ function cutViewStack() { state.viewStack = restorableStack(state.viewStack, state.currentView); } -// Every screen counts as not yet rendered, so "Back" renders the one it lands -// on for the new network. Settings on top is brought up to date in place; -// any other screen is drawn again as a reopened popup draws it, which is Home -// for one that cannot be rendered from saved state. Without a wallet any other -// screen is Welcome or AddWallet, which show nothing from a network. -function showNewNetwork() { +// Sets "Use Blockscout" to `on` on this page. It is called inside a save, +// holding the lock, for the user's change here or another page's +// (followStoredSettings() in src/popup/index.js). +// +// Send's token dropdown was filled under the old setting, and Back cannot draw +// Send again. So the stack is cut, as for a network switch. Turning the setting +// off also drops every token the user has not tracked. +function setUseBlockscout(on) { + state.useBlockscout = on; + cutViewStack(); + if (!on) dropUntrackedTokens(state.wallets, state.trackedTokens); +} + +// Draws the page again after a network switch, or after another page changed +// "Use Blockscout" or "Check recipients on Etherscan". Every screen counts as +// not yet rendered, so "Back" renders again any screen that a reopened popup +// can render. Settings on top is brought up to date in place. Any other screen +// is drawn again as a reopened popup draws it, which is Home for one that +// cannot be rendered from saved state. Without a wallet any other screen is +// Welcome or AddWallet. Back only unhides Welcome, so its two checkboxes are +// set here. +function showNewSettings() { resetRenderedViews(); + $("welcome-use-blockscout").checked = state.useBlockscout; + $("welcome-check-recipients-on-etherscan").checked = + state.checkRecipientsOnEtherscan; if (state.currentView === "settings") { markViewRendered("settings"); $("settings-network").value = state.networkId; $("settings-rpc").value = state.rpcUrl; $("settings-blockscout").value = state.blockscoutUrl; + $("settings-use-blockscout").checked = state.useBlockscout; + $("settings-check-recipients-on-etherscan").checked = + state.checkRecipientsOnEtherscan; renderTrackedTokens(); } else if (state.hasWallet) { ctx.restoreView(); @@ -363,6 +394,35 @@ function init(_ctx) { showFlash("Saved."); }); + // Each change is made inside the save, holding the lock. So + // followStoredSettings() in src/popup/index.js never mistakes this page's + // unsaved change for one that another page saved. + // + // The screens under Settings were drawn under the old setting. So each + // change makes every screen but Settings count as not yet drawn, as a + // network switch does. Back then draws again any screen that a reopened + // popup can draw. Back only unhides Welcome, so its boxes are set here too. + $("settings-use-blockscout").addEventListener("change", async () => { + const on = $("settings-use-blockscout").checked; + await saveState(() => setUseBlockscout(on)); + $("welcome-use-blockscout").checked = on; + resetRenderedViews(); + markViewRendered("settings"); + }); + + $("settings-check-recipients-on-etherscan").addEventListener( + "change", + async () => { + const on = $("settings-check-recipients-on-etherscan").checked; + await saveState(() => { + state.checkRecipientsOnEtherscan = on; + }); + $("welcome-check-recipients-on-etherscan").checked = on; + resetRenderedViews(); + markViewRendered("settings"); + }, + ); + const networkSelect = $("settings-network"); networkSelect.addEventListener("change", async () => { // Whether `state` already has the network picked is asked only once @@ -376,7 +436,7 @@ function init(_ctx) { // Open pages are told by the background, as after a site's approved // switch request. notify({ type: "AUTISTMASK_NETWORK_CHANGED", chainId: net.chainId }); - showNewNetwork(); + showNewSettings(); showFlash("Switched to " + net.name + "."); }); @@ -516,5 +576,6 @@ module.exports = { show, renderSiteLists, cutViewStack, - showNewNetwork, + setUseBlockscout, + showNewSettings, }; diff --git a/src/popup/views/transactionDetail.js b/src/popup/views/transactionDetail.js index 832211f..84ca5fe 100644 --- a/src/popup/views/transactionDetail.js +++ b/src/popup/views/transactionDetail.js @@ -26,6 +26,7 @@ const { state } = require("../../shared/state"); const { nativeCurrencyByChainId } = require("../../shared/networks"); const { formatEther, formatUnits } = require("ethers"); const { log, debugFetch } = require("../../shared/log"); +const { storedSettingIsOn } = require("../../shared/storedSettings"); const { decodeCalldata } = require("./approval"); /** @@ -172,7 +173,8 @@ function render() { } } - // Hide calldata and raw data sections; always fetch full tx details + // Hide the calldata and raw data sections. loadFullTxDetails() shows them + // once it has their data. const calldataSection = $("tx-detail-calldata-section"); if (calldataSection) calldataSection.classList.add("hidden"); const rawDataSection = $("tx-detail-rawdata-section"); @@ -191,6 +193,12 @@ function render() { if (el) el.classList.add("hidden"); } + // The block, network details, decoded details and raw data come from + // Blockscout. While the setting is off, this line says why none is shown. + $("tx-detail-blockscout-note").classList.toggle( + "hidden", + !!state.useBlockscout, + ); loadFullTxDetails(tx.hash, tx.chainId); const isoStr = isoDate(tx.timestamp); @@ -303,7 +311,12 @@ function populateOnChainDetails(txData, chainId) { } } +// Reads the block, network details and calldata from Blockscout, and decodes +// the calldata. While the stored "Use Blockscout" is off, Blockscout is not +// asked. An answer that lands after the setting went off is dropped +// (src/shared/storedSettings.js). Either way none of them is shown. async function loadFullTxDetails(txHash, chainId) { + if (!(await storedSettingIsOn("useBlockscout"))) return; const section = $("tx-detail-calldata-section"); const actionEl = $("tx-detail-calldata-action"); const detailsEl = $("tx-detail-calldata-details"); @@ -319,6 +332,7 @@ async function loadFullTxDetails(txHash, chainId) { ); if (!resp.ok) return; const txData = await resp.json(); + if (!(await storedSettingIsOn("useBlockscout"))) return; // Populate on-chain detail fields (block, nonce, gas, fee) populateOnChainDetails(txData, chainId); diff --git a/src/popup/views/txStatus.js b/src/popup/views/txStatus.js index e55913f..3653b32 100644 --- a/src/popup/views/txStatus.js +++ b/src/popup/views/txStatus.js @@ -14,6 +14,7 @@ const { displaySymbol, clearViewStack, tokenLabel, + blockscoutOffTokenNoteHtml, reviewedNetworkProvider, } = require("./helpers"); const { state } = require("../../shared/state"); @@ -108,6 +109,10 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) { } $("wait-tx-summary").textContent = txInfo.amount + (symbol ? " " + symbol : ""); + $("wait-tx-token-note").innerHTML = blockscoutOffTokenNoteHtml( + txInfo.token, + txInfo.chainId, + ); $("wait-tx-to").innerHTML = toAddressHtml(txInfo.to, txInfo.chainId); $("wait-tx-hash").innerHTML = txHashHtml(txHash, txInfo.chainId); attachCopyHandlers("view-wait-tx"); @@ -215,9 +220,10 @@ function showWait(txInfo, txHash) { // init and leaving wait-tx on screen with no back control. A non-numeric // broadcastTime leaves an unexitable wait counting "NaNs". txInfo.chainId picks // the node the receipt is asked of and the explorer the links open, so it has -// to name a network. txInfo.token and txInfo.tokenSymbol are deliberately -// unchecked: they are compared and coalesced rather than dereferenced, and -// tokenSymbol is null for ETH. +// to name a network. txInfo.token and txInfo.tokenSymbol are not checked here. +// blockscoutOffTokenNoteHtml() shows no line for a token that is not text. +// Everything else only compares and coalesces them, and tokenSymbol is null for +// ETH. function restoreWait() { const d = state.viewData; if (!d || !d.pendingWait) return false; @@ -282,6 +288,7 @@ function decodedDetailsHtml(decoded, chainId) { if (d.isToken) { const sym = tokenLabel(d.address, chainId) || "Unknown token"; html += `