From 93b54c85e84923cec7ff1ae3333b65998de392b6 Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 2 Nov 2026 02:15:46 +0000 Subject: [PATCH] README: second rework after review Fix the 23 findings of the second review and the two leftovers from the first: SiteApproval's connection lifetime, the stateSchema.js module line, the flash line and title bar, the six network-bound screens, the token-only 0.000001 cutoff, the 12-character symbol cap, the time display, the native currency labels, the truncateAmountNeverZero screens, the browser suite rules, the build receipt contents, the External services list, the worker memory exceptions, POPUP_ONLY_TYPES, the undefined terms, the comma lists, the Rationale, Entrypoints and TODO sections, and the dropped crypto comment rule. A fact pass then checked every statement about the code and every absolute against each case, and a newcomer read removed repeats. Model: fable-5-1 --- README.md | 414 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 236 insertions(+), 178 deletions(-) diff --git a/README.md b/README.md index e6b599e..2da9e68 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ # AutistMask -AutistMask is a browser extension. It is an Ethereum wallet for Chrome and -Firefox. It is free software under the GPL-3.0 license, written by -[@sneak](https://sneak.berlin). +AutistMask is a GPL-3.0-licensed browser extension by +[@sneak](https://sneak.berlin): an Ethereum wallet for Chrome and Firefox. -The wallet does four things: +AutistMask does four things: - It holds wallets made from a recovery phrase, a private key or an extended private key. @@ -20,7 +19,7 @@ It does nothing else. It has: - no advertising; - no tracking. -## Why it exists +## Rationale Popular browser wallets have grown: @@ -48,7 +47,8 @@ Settings. The wallet uses that endpoint to: The RPC endpoint sees every address that the wallet queries and every transaction that the wallet sends. A user who wants privacy can point the RPC -URL, and the Blockscout URL, at a node of their own. +URL at a node of their own, and the Blockscout URL at a Blockscout server of +their own. Two more services receive the user's data only after the user turns on a setting. Both settings are off in a new install. The Welcome screen and Settings @@ -67,7 +67,7 @@ sends nothing anywhere. The section External services below lists each request. -## Getting started +## Getting Started Build the extension from a clone: @@ -113,12 +113,12 @@ which this repository does not produce. `manifest/firefox.json` requires Firefox 115 or newer. -The `.xpi` that `make package` writes is not signed. Release Firefox and Firefox -ESR (the extended support release) refuse to install an unsigned add-on, and -`xpinstall.signatures.required` has no effect on them. A permanent install of -the unsigned file needs Firefox Developer Edition, Nightly or an unbranded build -(Mozilla's build without the Firefox branding). On those, -`xpinstall.signatures.required` must be set to `false` in `about:config`. +The `.xpi` (Firefox's add-on package file) that `make package` writes is not +signed. Release Firefox and Firefox ESR (the extended support release) refuse to +install an unsigned add-on, and `xpinstall.signatures.required` has no effect on +them. A permanent install of the unsigned file needs Firefox Developer Edition, +Nightly or an unbranded build (Mozilla's build without the Firefox branding). On +those, `xpinstall.signatures.required` must be set to `false` in `about:config`. Every Firefox, release builds included, can load the extension as a temporary add-on from `about:debugging`. A temporary add-on is unloaded when Firefox @@ -173,8 +173,8 @@ offers "Use Blockscout" and "Check recipients on Etherscan". - "Allow weak passwords": off. While off, AddWallet refuses a password shorter than 12 characters. While on, one character is enough. - "Show tracked tokens with zero balance": on. -- "UTC Timestamps": off. While off, timestamps are shown in local time with the - UTC offset. +- "UTC Timestamps": off. While off, a transaction's datetime is shown in local + time with the UTC offset. - Theme: `System`, `Light` or `Dark`. Default `System`. - "Hide fake tokens impersonating a known symbol": on. - "Hide tokens with fewer than 1,000 holders": on. @@ -184,9 +184,9 @@ offers "Use Blockscout" and "Check recipients on Etherscan". - Tracked Tokens: the ERC-20 tokens tracked on the active network. Each network has its own list. - Allowed Sites: the sites allowed with "Remember my choice for this site". -- Connected Sites: the sites allowed without it. Such a connection ends when the - browser closes, when the active address changes, or when the site is removed - in Settings. +- Connected Sites: the sites allowed without "Remember my choice for this site". + Such a connection ends when the browser closes, when the active address + changes, or when the site is removed in Settings. - Denied Sites: the sites denied with "Remember my choice for this site". - "Remember my choice for this site": the checkbox on SiteApproval. Its last state is remembered. Default ticked. @@ -222,8 +222,8 @@ src/ index.html the markup of every screen index.js popup entry point: loads the profile, restores the screen, runs the refresh - viewRouter.js renders a screen the popup lands on without navigating - to it + viewRouter.js draws a screen that "Back" returns to when this page + load has not drawn it yet theme.js applies the theme dustThreshold.js parses the dust threshold field styles/main.css Tailwind source, plus four classes of its own @@ -234,8 +234,9 @@ manifest/ firefox.json Manifest V2 for Firefox icons/ the toolbar icon at the four sizes the manifests declare build.js bundles the extension into dist/ -script/ the entry points (see Development) -tests/ jest unit tests; tests/e2e/ holds the browser suites +script/ the entrypoints (see Development) +tests/ jest unit tests and their helpers; tests/e2e/ holds the + browser suites ``` The shared modules, each in one line: @@ -253,7 +254,8 @@ The shared modules, each in one line: address scan on import. - `browserApi.js`: the one module that names `chrome` or `browser`; storage, messaging, windows and the lock. -- `buildInfo.js`: the version, commit and date compiled into About. +- `buildInfo.js`: the version, license, author, commit and date compiled into + About. - `chainSwitch.js`: the popup's network switch. - `chainSwitchFields.js`: the fields a network switch changes. - `constants.js`: `DEBUG`, the default endpoints, the ERC-20 ABI and the burn @@ -272,8 +274,8 @@ The shared modules, each in one line: - `restorableViews.js`: the screens a reopened popup may land on. - `scamlist.js`: known scam addresses. - `state.js`: the popup's in-memory profile and `saveState()`. -- `stateSchema.js`: `schemaVersion` and the check every profile read goes - through. +- `stateSchema.js`: `schemaVersion` and `assertStateUsable()`, the check that + the two state modules run on each read. - `storedSettings.js`: reads "Use Blockscout" and "Check recipients on Etherscan" from storage. - `symbolDisplay.js`: the 12-character cap on a displayed symbol. @@ -287,8 +289,8 @@ The shared modules, each in one line: - `uniswap.js`: the Uniswap Universal Router decoder. - `vault.js`: password-based encryption with libsodium. - `wallet.js`: recovery phrases, key derivation and signers. -- `walletDefects.js`: the notice for an xprv wallet whose stored key is not a - master key. +- `walletDefects.js`: the notice for an xprv wallet imported from a key that was + not a master key. - `walletDelete.js`: removing a wallet or an address from the profile. ### Wallets, keys and encryption @@ -301,14 +303,18 @@ A wallet is one of three types: - An xprv wallet (`type: "xprv"`) comes from an extended private key. It behaves like an HD wallet but has no recovery phrase. -All key handling goes through `ethers`. Addresses are derived on the path -`m/44'/60'/0'/0/n`, with no BIP-39 passphrase (the optional extra word some -wallets add to a phrase). A new recovery phrase has 12 words. An extended +All key derivation and signing goes through `ethers`. Addresses are derived on +the path `m/44'/60'/0'/0/n`, with no BIP-39 passphrase (the optional extra word +some wallets add to a phrase). A new recovery phrase has 12 words. An extended private key must be a master key, the root of the key tree. AddWallet refuses a child key, one derived below the root. Each HD or xprv address record stores its `index`, and signing derives the key at that index. `getSignerForAddress()` in `src/shared/wallet.js` refuses a key that does not belong to the address. +An xprv wallet imported from a key that was not a master key carries a notice in +the wallet list (`src/shared/walletDefects.js`). Such a wallet shows no "+" +button, and Send, site signing and private key export refuse it. + The secret of each wallet is encrypted with that wallet's password (`src/shared/vault.js`): @@ -322,12 +328,14 @@ The password is not part of address derivation. Anyone holding the recovery phrase can restore the wallet elsewhere without it. The password protects the copy stored in this browser and nothing else. -Public data is stored unencrypted: +Only the wallet secrets are encrypted. Everything else in the profile is stored +as plain data, among it: - the `xpub` of each HD or xprv wallet, the extended public key that its addresses are derived from; - the addresses and their balances; - the tracked tokens; +- the site lists; - the settings. The popup shows all of that without a password. The password is required to: @@ -350,12 +358,12 @@ which stays loaded. Both take the same code paths. Three rules follow from the worker being stopped: -- Nothing is kept in module variables between events. The worker reads the - profile from storage on every call (`getState()` in `src/background/state.js`) - and writes it back under the lock (`updateState()`). +- The profile is never kept in module variables. The worker reads it from + storage on every call (`getState()` in `src/background/state.js`) and writes + it back under the lock (`updateState()`). - The recurring balance refresh runs off an alarm (`src/shared/alarms.js`), not a timer. The browser holds the schedule and wakes the worker. The period is - one minute. The refresh is skipped when the last refresh finished less than 30 + one minute. The refresh is skipped when the last refresh started less than 30 seconds ago. An open popup refreshes every 10 seconds, so while the popup is open the background stays quiet. - Connections allowed without "Remember my choice for this site" are kept in the @@ -363,11 +371,12 @@ Three rules follow from the worker being stopped: worker stopping. Such a connection ends when the browser closes, when the active address changes, or when the site is removed in Settings. -Pending approvals are the exception. An approval, and the site's request waiting -on it, live in the worker's memory. While any approval is pending, the worker -calls `runtime.getPlatformInfo()` every 20 seconds. Chrome counts that call as -activity and keeps the worker running. The timer stops itself once nothing is -pending. +Pending approvals and the record of broadcast nonces are the exceptions. Both +live in the worker's memory, and both are lost when the worker stops. An +approval, and the site's request waiting on it, exist nowhere else. So while any +approval is pending, the worker calls `runtime.getPlatformInfo()` every 20 +seconds. Chrome counts that call as activity and keeps the worker running. The +timer stops itself once nothing is pending. Alarms the code no longer handles are listed in `OBSOLETE_ALARMS` and cleared on every start. The worker fetches nothing when it starts. @@ -431,36 +440,40 @@ Home otherwise. The Settings gear in the title bar is the only global control. The popup saves the current screen. Reopening the popup lands on that screen when the screen is in `RESTORABLE_VIEWS` (`src/shared/restorableViews.js`). -Every other screen falls back to Home. The two screens that show a secret, -ExportPrivKey and ShowRecoveryPhrase, are not restorable. Neither are the two -that erase a wallet, DeleteWallet and DeleteWalletLostPassword. +Every other screen falls back to Home. A restorable screen whose data is gone +falls back to Home too. The two screens that show a secret, ExportPrivKey and +ShowRecoveryPhrase, are not restorable. Neither are the two that erase a wallet, +DeleteWallet and DeleteWalletLostPassword. -A reopened popup renders only the wallet list and the screen that it lands on. -"Back" onto a screen that this page load has not rendered renders that screen -first (`src/popup/viewRouter.js`). When the data for that screen is gone, Home -is shown instead. "Back" onto a screen already on the page only unhides it, so -unsaved input in Settings survives. Home is rendered again on every "Back". +A reopened popup draws only the wallet list and the screen that it lands on. +When "Back" returns to a screen that this page load has not drawn yet, +`src/popup/viewRouter.js` draws it first. When the data for that screen is gone, +Home is shown instead. "Back" onto a screen already drawn only unhides it, so +unsaved input in Settings survives. Home is drawn again on every "Back". A network switch, or a change to "Use Blockscout", cuts the stack at the first -screen that cannot be rendered from saved state. Every screen then counts as not -rendered. Settings on top is redrawn in place. Any other screen is redrawn as a -reopened popup would draw it. Five screens are drawn for the network of the -transaction that they show, whichever network is active: +screen that cannot be drawn from saved state. The cut also drops AddressToken +unless it shows ETH. After either change, and after a change to "Check +recipients on Etherscan", every screen but Settings counts as not drawn. +Settings on top is redrawn in place. Any other screen is redrawn as a reopened +popup would draw it. Six screens are drawn for the network of the transaction +that they show, whichever network is active: - ConfirmTx; - WaitTx; - SuccessTx; - ErrorTx; -- TransactionDetail. +- TransactionDetail; +- TxApproval. A screen that holds a secret in the page registers a cleanup with `onViewLeave()`. `showView()` runs the cleanup on every exit from that screen. So a revealed key or phrase, or a typed password, never survives in a hidden screen. -Two things are present on every screen but StateRecovery: the title bar with the -Settings gear, and the flash message line under it. The flash line holds one -line of text for two seconds. +Every screen has the title bar and the flash message line under it. A flash +message stays two seconds unless the screen sets another time. StateRecovery +hides the Settings gear in the title bar. On Sepolia, or with debug on, a red banner sits at the top of the page. It reads `[TESTNET]` on Sepolia, `DEBUG / INSECURE` with debug on, and @@ -493,10 +506,14 @@ Ticking a checkbox saves the setting at once. "Add wallet" opens AddWallet. Shown while at least one wallet exists. The screen holds: -- the active address's ETH balance, with its USD value in parentheses; +- the active address's ETH balance, with its USD value in parentheses when a + price is known; - a "Total:" line with the USD value of the address's holdings (see Partial totals below); -- the active address: colour dot, full address, explorer link, tap to copy; +- the active address: + - its colour dot; + - the full address. Tapping it copies it; + - an explorer link; - "Send" and "Receive" buttons for the active address; - the ETH/USD price, on mainnet; - the wallet list; @@ -506,8 +523,9 @@ Shown while at least one wallet exists. The screen holds: - an "Add additional wallet..." link. Each wallet in the list shows its name. Tapping the name renames it in place. HD -and xprv wallets also show a "+" button that derives the next address. Each -address of a wallet is a block: +and xprv wallets also show a "+" button that derives the next address, unless +the wallet carries the notice from `src/shared/walletDefects.js`. Each address +of a wallet is a block: - a row with the colour dot and "Address N", bold when the address is active; - on that row, an `[info]` button and, on HD and xprv wallets holding more than @@ -539,7 +557,7 @@ Transitions: - `[info]`: AddressDetail. - `[x]`: DeleteAddress. - "Send": Send. Refused with a flash message when the active address has a zero - ETH balance. + ETH balance or belongs to a wallet carrying the `walletDefects.js` notice. - "Receive": Receive. - Tap a transaction row: TransactionDetail. - "Add additional wallet...": AddWallet. @@ -572,11 +590,12 @@ the RPC endpoint, five at a time. The scan stops after five unused addresses in a row. When the scan finds two or more used addresses, they replace the wallet's list. The flash line reports the count. -Refusals: +A missing, short or mismatched password gets a full sentence on the error line. +A flash message refuses: -- a missing, short or mismatched password: a full sentence on the error line; -- an invalid phrase or key, an extended private key that is not a master key, or - a phrase or address already in a wallet: a flash message. +- an invalid phrase or key; +- an extended private key that is not a master key; +- a phrase or address already in a wallet. "Back" returns to the previous screen. @@ -587,7 +606,8 @@ Reached from `[info]` on Home. The screen holds: - a "Back" button; - the address's identicon; - the title "Wallet Name — Address N"; -- the full address with its ENS name, explorer link and tap to copy; +- the full address, with its ENS name and an explorer link. Tapping the address + copies it; - the address's USD total; - the balance list: ETH, then each token shown for the address. Each row opens AddressToken; @@ -601,7 +621,7 @@ Transitions: - a balance row: AddressToken; - "Send": Send. Refused with a flash message when the address has a zero ETH - balance; + balance or belongs to a wallet carrying the `walletDefects.js` notice; - "Receive": Receive; - "+ Token": AddToken; - "Export Private Key": ExportPrivKey; @@ -650,8 +670,9 @@ Reached by tapping a balance row on AddressDetail. The screen holds: "Use Blockscout" is off. "Send" opens Send with the token locked: the dropdown is replaced by the symbol -and contract address. "Receive" opens Receive with the ERC-20 warning. A -transaction row opens TransactionDetail. "Back" returns to AddressDetail. +and contract address. "Receive" opens Receive, with the ERC-20 warning for a +token. A transaction row opens TransactionDetail. "Back" returns to +AddressDetail. #### Send (`send`) @@ -669,7 +690,7 @@ Reached from Home, AddressDetail or AddressToken. The screen holds: The dropdown offers ETH and the tokens of the address's balance list. It leaves out: -- holdings below 0.000001; +- token holdings below 0.000001; - tokens with a spoofed symbol; - tokens on the fraud contract blocklist; - while "Hide tokens with fewer than 1,000 holders" is on, tokens with a @@ -799,8 +820,8 @@ Reached from Home, AddressDetail or AddressToken. The screen holds: - a QR code of the address; - the full address; - a "Copy address" button; -- when opened from a token's screen, a warning to send only that token on this - network to this address. +- when opened from an ERC-20 token's screen, a warning to send only that token + on this network to this address. "Back" returns to the previous screen. @@ -848,7 +869,8 @@ Reached from "+ Token" on AddressDetail. The screen holds: "Add" looks up `symbol()`, `decimals()` and `name()` on the contract through the RPC endpoint. On success it adds the token to the tracked list, pops the stack -and shows AddressDetail. A flash message refuses: +and shows AddressDetail. If another screen is showing by then, the screen does +not change. A flash message refuses: - a token already tracked; - an address on the scam list; @@ -862,8 +884,8 @@ says so. Reached from the gear. The screen holds, under headings: - Wallets: one row per wallet, with "+ Add wallet" under them. A row holds the - name (tap to rename), a `[recovery phrase]` button on HD wallets, and an `[x]` - button. + name, a `[recovery phrase]` button on HD wallets, and an `[x]` button. Tapping + the name renames it in place. - Passwords: the "Allow weak passwords" checkbox. - Tracked Tokens: one row per token tracked on the active network with an `[x]` button, and "+ Add token". @@ -898,8 +920,9 @@ Transitions: - Switching the network restores the RPC and Blockscout URLs last used on that network, or the network's defaults, and the tokens tracked there. It clears - every address's balances. It tells every open tab the new chain id with - `chainChanged`. Choosing the network already active changes nothing. + every address's balances and the fraud contract blocklist. It tells every open + tab the new chain id with `chainChanged`. Choosing the network already active + changes nothing. - Ticking or unticking "Use Blockscout" or "Check recipients on Etherscan" stores the setting. From then on every screen and every refresh uses the new value. Unticking "Use Blockscout" also drops every untracked token from every @@ -1004,8 +1027,8 @@ separate window. The screen holds: - "Allow" and "Deny" buttons. "Allow" with "Remember" ticked adds the origin to Allowed Sites. "Allow" without -it connects the site until the browser closes. "Deny" with "Remember" ticked -adds the origin to Denied Sites. Closing the popup without an answer is a +it adds the site to Connected Sites (see Settings). "Deny" with "Remember" +ticked adds the origin to Denied Sites. Closing the popup without an answer is a denial. #### TxApproval (`approve-tx`) @@ -1061,11 +1084,11 @@ as a personal message. Typed data is shown as its domain, the primary type that `ethers` will sign, and the message fields that this type declares, in the type's order. Keys that the -type does not declare are listed under "Not part of the signature". Typed data -whose primary type is `Permit`, or one of the signature types of Permit2 -(Uniswap's token allowance contract), gets a red warning at the top. The warning -names the spender, and each token and amount that the signature lets the spender -take. `Unlimited` marks the field's largest value. `Unknown` marks anything the +type does not declare are listed under "Not part of the signature". A red +warning heads the message when the primary type is `Permit` or a Permit2 +signature type. Permit2 is Uniswap's token allowance contract. The warning names +the spender, and each token and amount that the signature lets the spender take. +`Unlimited` marks the field's largest value. `Unknown` marks anything the declared fields do not give. "Sign" is disabled, with the reason on the error line, when: @@ -1112,15 +1135,23 @@ popup closes. ### Amounts -The same data is formatted the same way on every screen. Token and ETH amounts -show four decimal places. History rows cut amounts to four decimals and never -round up (`truncateAmount()` in `src/shared/amountDisplay.js`). The balance -lists and Home's ETH balance round to four places. The screens that state what -is being authorized cut as the history does, and extend an amount below 0.0001 -to its first nonzero digit (`truncateAmountNeverZero()`). So a nonzero amount -never reads as zero there. Those screens are: +These helpers write one kind of value on every screen that shows it: + +- `formatUsd()` in `src/shared/prices.js`: USD values; +- `isoDate()` and `timeAgo()` in `src/popup/views/helpers.js`: times; +- `renderAddressHtml()` in the same file: most addresses. + +Amounts are written by the two helpers below, or rounded by the balance lists. +Most screens show token and ETH amounts with four decimal places. History rows +cut amounts to four decimals and never round up (`truncateAmount()` in +`src/shared/amountDisplay.js`). The balance lists and Home's ETH balance round +to four places. The screens that state what is being authorized cut as the +history does, and extend an amount below 0.0001 to its first nonzero digit +(`truncateAmountNeverZero()`). So a nonzero amount never reads as zero there. +Those screens are: - TxApproval; +- SignApproval's Permit warning; - ConfirmTx, and Send's current balance; - WaitTx, SuccessTx and ErrorTx. @@ -1149,7 +1180,8 @@ as a token's decimals. A token's `decimals` is stored only as a source reported it, never guessed. A `null` balance means the decimals are unknown, never that the balance is zero. While "Use Blockscout" is off, the approval and status screens add a line under a token that neither the bundled list nor the tracked -tokens name. The line says that Blockscout's report is not used. +tokens name or give decimals for. The line says that Blockscout's report is not +used. The wallet's own ERC-20 send is encoded with the decimals the screen showed. At signing time the contract's `decimals()` is read only to be compared with those. @@ -1158,21 +1190,23 @@ A disagreement refuses the send (`src/shared/transferAmount.js`). A native amount is labelled with the native currency of the transaction's chain: `ETH` on mainnet, `SepoliaETH` on Sepolia (`nativeCurrencyByChainId()` in `src/shared/networks.js`). The balance lists and Send use the active network's. +The decoded swap lines call the native currency `ETH` on both networks. -A transaction's time is shown as an ISO datetime and a relative age, written by -`isoDate()` and `timeAgo()` in `src/popup/views/helpers.js`. With "UTC +TransactionDetail shows the time as an ISO datetime and a relative age. A +history row shows the age, with the datetime as its tooltip. With "UTC Timestamps" on, the datetime is in UTC. -Every nonzero holding is stored exactly. A holding below 0.000001 is left out -of: +Every nonzero holding is stored exactly. A token holding below 0.000001 is left +out of: - the balance lists; - the Send dropdown; - the address total; - the DeleteAddress warning. -A displayed token symbol is cut to 12 characters -(`src/shared/symbolDisplay.js`). +Most screens cut a token symbol to 12 characters +(`src/shared/symbolDisplay.js`). The approval screens, the decoded calldata +lines and Send's "Current balance" line show it in full. #### Partial totals @@ -1189,12 +1223,11 @@ same way on every screen: ### Addresses Every address is shown in full, on a row of its own that never wraps (the -`.am-address` class in `src/popup/styles/main.css`). No screen shortens an -address shown as an address. `truncateMiddle()` in `src/popup/views/helpers.js` -removes at most 10 characters, and `renderAddressHtml()` never shortens an -address below 32 characters, but no caller shortens one today. A token with no -known symbol is labelled by the first 10 characters of its contract address and -`…` (`resolveSymbol()` in `src/shared/tokenList.js`); Send and AddressToken show +`.am-address` class in `src/popup/styles/main.css`). No caller shortens an +address. `truncateMiddle()` in `src/popup/views/helpers.js` removes at most 10 +characters, and `renderAddressHtml()` keeps at least 32. A token with no known +symbol is labelled by the first 10 characters of its contract address and `…` +(`resolveSymbol()` in `src/shared/tokenList.js`); Send and AddressToken show that label. On most screens, tapping an address copies it, and an explorer link sits beside @@ -1203,12 +1236,13 @@ Each explorer link opens the block explorer of the address's network: `etherscan.io` on mainnet and `sepolia.etherscan.io` on Sepolia. The links contact nothing until clicked. -Each address gets a colour dot chosen from its first four hex digits, and an -identicon on the screens where space allows. +Each address gets a colour dot chosen from its first four hex digits. An +identicon appears on AddressDetail, AddressToken, ExportPrivKey, ConfirmTx and +TransactionDetail. ### Scam protection -Five defenses are on by default. +Six defenses are built in. Four of them are settings, on by default. Known symbol spoofing. `KNOWN_SYMBOLS` in `src/shared/tokenList.js` maps each symbol of the bundled list to the contracts that may bear it. `ETH` and every @@ -1232,8 +1266,9 @@ number written in digits counts as a count (`src/shared/holders.js`). Fraud contract blocklist. Detecting a spoofed transfer adds its contract to `fraudContracts` in the profile. While "Hide transactions from detected fraud contracts" is on, the history hides transfers from those contracts. The Send -dropdown always leaves them out. Turning the spoof setting off also stops new -entries being learned. +dropdown always leaves them out. Turning off "Hide fake tokens impersonating a +known symbol" also stops new entries being added. A network switch clears the +list. Dust transactions. While "Hide dust transactions below N gwei" is on, the history hides native transfers below the threshold. Contract calls and token @@ -1245,15 +1280,7 @@ has approved them). ConfirmTx warns before a send to one of them. AddToken and SettingsAddToken refuse to track one. The list holds addresses involved in fraud only. It is not a sanctions list. -ConfirmTx also warns when the recipient is: - -- a contract; -- a burn address; -- the sending address itself; -- an address that has never sent a transaction and holds none of the network's - native token; -- while "Check recipients on Etherscan" is on, an address Etherscan labels as - phishing, scam or exploiter. +The other warnings on ConfirmTx are listed under that screen. #### Phishing domains @@ -1292,17 +1319,18 @@ when both tokens are known. The lines are: - `Protocol`: `Uniswap Universal Router`, with the router's address. - `Token In` and `Token Out`: the currencies the swap spends and buys. - `ETH (native)` for ETH. `Unknown (not named in the calldata)` when no step - names one. `Unknown (could not be decoded)` when a V4 swap action could not be - read or the transaction has an `EXECUTE_SUB_PLAN` step. + `Token In` reads `ETH (native)` for ETH; `Token Out` reads `ETH`. + `Unknown (not named in the calldata)` when no step names one. + `Unknown (could not be decoded)` when a V4 swap action could not be read or + the transaction has an `EXECUTE_SUB_PLAN` step. - `Amount`: the total that the steps take of `Token In` from the user. `Up to` is prefixed when the transaction has a V2 or V3 exact-out step. `Unlimited` marks an amount at or above the `uint160` maximum. `All available (V4 open delta)` and `Whatever an earlier step sent to the pair (V2 already paid)` name the two amounts that are not quantities. The line is left out when the decoder cannot - bound what leaves the wallet; each amount that it did read then gets lines of - its own. + tell the most that can leave the wallet; each amount that it did read then + gets lines of its own. - `Moved from the sender`, `Token moved` and `Moved to`: an amount taken from the sender that the `Amount` line does not include. The same three lines show what a `PERMIT2_TRANSFER_FROM` moves and where. @@ -1323,10 +1351,11 @@ when both tokens are known. The lines are: - no `Paid to` line is shown; - no allowance goes to a spender other than the router. - `Steps`: each command by name, in order. A step that the router lets fail - without reverting is marked `(allowed to fail)`. A `BALANCE_CHECK_ERC20` step - that the decoder could not read, and a V4 step with a swap action that it - could not read, are marked `(could not be decoded)`. An `EXECUTE_SUB_PLAN` - step is marked `(its steps are not shown)`. + without reverting is marked `(allowed to fail)`. An `EXECUTE_SUB_PLAN` step is + marked `(its steps are not shown)`. Two kinds of step are marked + `(could not be decoded)`: + - a `BALANCE_CHECK_ERC20` step that the decoder could not read; + - a V4 step with a swap action that it could not read. - `Deadline`: the router's deadline as a UTC date and time. A deadline past the range of a JavaScript date reads `After 275760-09-13 00:00:00 (no deadline in practice)`. @@ -1348,8 +1377,9 @@ decide what the router still holds after each step. Those rules decide what the The inpage provider (`src/content/inpage.js`) puts a `window.ethereum` provider on every page, unless another wallet already set one. It announces the provider -through EIP-6963 with a fresh random UUID per page load. The content script -(`src/content/index.js`) relays each request to the background. +through EIP-6963, the standard that lets a page list the installed wallets, with +a fresh random UUID per page load. The content script (`src/content/index.js`) +relays each request to the background. A site is its full origin: scheme, host and port. A choice for `https://app.example` says nothing about `http://app.example`. The background @@ -1365,8 +1395,9 @@ The background answers these methods: is allowed or connected, otherwise nothing. - `eth_chainId` and `net_version`: the active network's values. - `wallet_switchEthereumChain`: NetworkApproval, for a connected site naming a - supported network. An unsupported chain is refused with code `4902`. A site - that is not connected is refused with code `4100`. + supported network other than the active one. A request naming the active + network is answered with success at once. An unsupported chain is refused with + code `4902`. A site that is not connected is refused with code `4100`. - `wallet_addEthereumChain`: success for a supported chain, code `4902` otherwise. It never switches the network. - `personal_sign`, `eth_sign`, `eth_signTypedData` and `eth_signTypedData_v4`: @@ -1380,9 +1411,9 @@ The background answers these methods: includes `eth_sendRawTransaction`. - Anything else is refused with code `4200`. -The background also answers messages from the extension's own pages, such as -`AUTISTMASK_GET_CONNECTED_SITES` and `AUTISTMASK_REMOVE_SITE`. It answers them -only when the sender is one of those pages. +The background also answers messages from the extension's own pages. The types +in `POPUP_ONLY_TYPES` in `src/background/index.js` are answered only when the +sender is one of those pages. One transaction approval exists at a time, wallet-wide, because filling in a transaction fixes its nonce. A second `eth_sendTransaction` while one is @@ -1390,13 +1421,13 @@ unanswered is refused with code `-32002`. One site approval, one signature approval and one network approval exist per site at a time, with the same code. For `eth_sendTransaction`, the background fills in the transaction before any -window opens (`src/shared/approvalTx.js`). The nonce is the account's next nonce -from the node. The fees are the site's when it gave them, otherwise the node's. -The gas limit is the node's estimate unless the site gave one. A site's nonce is -ignored. When the node refuses the transaction or gives no answer within 20 -seconds, no window opens and the site gets the node's error. The popup signs -exactly the object that it was shown. The background then checks the signed -transaction (`verifySignedTx()` in `src/shared/approvalVerify.js`): +window opens (`src/shared/approvalTx.js`). The nonce is the sending address's +next nonce, read from the node. The fees are the site's when it gave them, +otherwise the node's. The gas limit is the node's estimate unless the site gave +one. A site's nonce is ignored. When the node refuses the transaction or gives +no answer within 20 seconds, no window opens and the site gets an error. The +popup signs exactly the object that it was shown. The background then checks the +signed transaction (`verifySignedTx()` in `src/shared/approvalVerify.js`): - the signer is the address that the approval was raised for; - the type is 0, 1 or 2; @@ -1408,18 +1439,21 @@ transaction (`verifySignedTx()` in `src/shared/approvalVerify.js`): A mismatch spends the approval: the site gets an error, and the signed transaction is not retried. A wrong password leaves the approval standing. After -a broadcast the nonce is recorded, and a second signed transaction with that -nonce is refused. Signatures are checked the same way: the background recovers -the signer from the signature and refuses any other address. +a broadcast the nonce is recorded, and the background refuses a second signed +transaction with that nonce until the worker stops. Signatures are checked the +same way: the background recovers the signer from the signature and refuses any +other address. Switching the active address rejects every pending approval with code `4001` and -closes its window. It ends every connection allowed without "Remember", and -sends `accountsChanged` to every tab with permission. +closes its window, except an approval already being signed and sent. It ends +every connection allowed without "Remember". It sends `accountsChanged` to every +tab: the active address to a site with permission, an empty list to the rest. Approval windows open at 396 by 600 pixels, centred on the browser window last -in focus. The window widens itself by however far the screen would scroll -sideways. A transaction or signature approval whose window closes stays pending. -The toolbar button then opens the oldest such approval instead of the wallet. +in focus when that is a normal window. The window widens itself by however far +the screen would scroll sideways. A transaction or signature approval whose +window closes stays pending. The toolbar button then opens the oldest such +approval instead of the wallet. #### Ceilings @@ -1440,17 +1474,20 @@ Ethereum JSON-RPC endpoint, contacted by default: - balance reads (`eth_getBalance`, `balanceOf` through `eth_call`) every 10 seconds while the popup is open and every minute in the background; - ENS reverse lookups of the user's addresses on each refresh, and of history - counterparties when a list is shown (cached for 12 hours in the popup); + counterparties when AddressDetail or AddressToken shows its list (cached for + 12 hours in the popup); - the ENS name lookup when Send resolves a name; - on mainnet, Chainlink `latestRoundData()` reads for ETH and for each held or tracked token with a feed. They go as one batched request, at most every 5 minutes while the popup is open. A reading older than the feed's heartbeat - plus 30 minutes is no price; + (the longest the feed goes without a new price) plus 30 minutes is no price; - the address scan on import; - the `symbol()`, `decimals()` and `name()` lookup when a token is added by contract address; +- the From address's balance each time ConfirmTx is shown; - the recipient checks on ConfirmTx: `eth_getCode`, `eth_getTransactionCount` and `eth_getBalance`; +- the token's `decimals()` when ConfirmTx signs an ERC-20 send; - fee estimates, nonces, broadcasts and receipt lookups; - `eth_chainId`, when "Save" beside the RPC URL in Settings checks the URL; - the methods in `PROXY_METHODS`, for any page that asks. @@ -1536,8 +1573,8 @@ base-uri 'none' - `connect-src https: http:` admits any RPC endpoint, a local node over `http://127.0.0.1` included. What the wallet contacts is bounded by its code, not by this directive. -- `frame-src 'none'`, `form-action 'none'` and `base-uri 'none'` are named - because the last two do not fall back to `default-src`. +- `form-action` and `base-uri` are named because neither falls back to + `default-src`. `frame-src 'none'` is stricter than the fallback. ### Debug mode @@ -1567,21 +1604,23 @@ balances and ENS names, and each token contract looked up. ### Appearance - Black text on a white background, or white on black in the dark theme. Colour - marks errors, warnings, address dots and identicons. + is used for errors, warnings, the red banner, address dots and identicons. - Every piece of information is text in the system monospace font. The - identicons and the QR code on Receive are the only pictures. + identicons and the QR code on Receive are the only pictures in the popup. - Every control looks like a control. Buttons have a border, padding and a hover - state. Clickable text is underlined. There are no invisible hit targets. -- Nothing moves when a result arrives. Every element filled in later reserves - its space with `min-height` or a placeholder. `visibility: hidden` is + state. Clickable text is underlined. A clickable row changes its background on + hover. There are no invisible hit targets. +- Every element filled in later reserves its space with `min-height` or a + placeholder, so a result arriving later moves nothing. `visibility: hidden` is preferred over `display: none` where the space must stay. An empty USD value is written as ` `. - The popup is 396 pixels wide (`body` in `src/popup/styles/main.css`) and up to 600 pixels tall, the most a toolbar popup gets. - Styling is Tailwind utility classes in the markup. `main.css` adds four - classes of its own: `.copy-flash-active` and `.copy-flash-fade` for the copy - feedback, `.am-address` for the rule that an address never wraps, and - `.am-byte-order` for the signature screen's message. + classes of its own: + - `.copy-flash-active` and `.copy-flash-fade`, the copy feedback; + - `.am-address`, the rule that an address never wraps; + - `.am-byte-order`, the byte-order layout of the signature screen's message. - No framework. The popup is plain JavaScript with one module per screen or group of screens. @@ -1595,15 +1634,15 @@ balances and ENS names, and each token contract looked up. ## Development -### Entry points +### Entrypoints The repository follows [Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all). The `Makefile` targets are thin shims over `script/`: -- `script/bootstrap`: install the dependencies. Node and yarn come from the - system or, failing that, from a pinned nvm install. Fails when node cannot - find a package named in `package.json`. +- `script/bootstrap`: install the dependencies. Node comes from the system or, + failing that, from a pinned nvm install; yarn comes from corepack. Fails when + node cannot find a package named in `package.json`. - `script/setup`: `script/bootstrap`, then the git pre-commit hook. - `script/test`: build the `test` stage of the `Dockerfile` with `--no-cache`. The stage runs the jest suite, stopped at 90 seconds and rerun verbose on @@ -1625,8 +1664,9 @@ The `Makefile` targets are thin shims over `script/`: - `script/projectname`: prints `autistmask`. - `script/package`: `make build`, then the release archives (see Release archives). -- `script/check-censored`: fail when the other wallet's name appears anywhere - but its allowed places. See Policies. +- `script/check-censored`: one competing wallet's name is barred from code and + documentation; fail when it appears anywhere but its allowed places (see + Policies). - `script/vendor-blocklist`: refresh `src/shared/phishingBlocklist.json` from its upstream, pinned to a commit and to the sha256 of the fetched bytes. - `script/verify-build --expect release|debug --receipt PATH`: check `dist/` @@ -1675,6 +1715,7 @@ It copies into `dist/chrome/` and `dist/firefox/`: It compiles into the bundles: - the version; +- the license and the author; - the commit hash, marked `-dirty` when the working tree is modified; - the build date; - `DEBUG`. @@ -1729,17 +1770,20 @@ under a tag of its own, and removes the image afterwards. Neither is part of `make check`. Run them after any change under `src/popup/views/`, before committing. -Four rules hold for both suites: +Two rules hold for both suites: - Nothing under `tests/e2e/` is named `*.test.js`, so jest never picks the suites up. +- Both suites share the test page and stub node in `tests/e2e/dapp.js` and the + fixtures in `tests/e2e/network.js`; the drivers are separate. + +Two more hold for the Chrome suite: + - A test that provokes a `console.error` on purpose declares it with `errors.expect()` (`tests/e2e/harness.js`). An undeclared error fails the run, and so does a declared one that never comes. - `playwright-core` in `package.json` stays at the version of the container image in `tests/e2e/Dockerfile`. -- Both suites share the test page and stub node in `tests/e2e/dapp.js` and the - fixtures in `tests/e2e/network.js`; the drivers are separate. `make test-e2e` runs three programs in a pinned Playwright container: @@ -1749,13 +1793,17 @@ Four rules hold for both suites: uncaught page error or `console.error`. It covers: - WebAssembly compiling in the real popup under the shipped policy; - the Welcome settings, and what reaches Blockscout and Etherscan; - - wallet creation, Add Token and TransactionDetail; + - wallet creation, AddToken and TransactionDetail; - the recovery phrase and private key screens; - address removal and Settings; - ConfirmTx's fee and balance checks, and the wallet's own ERC-20 send; - the receipt wait and StateRecovery; - - the site round trips: connect, `personal_sign`, `eth_signTypedData_v4`, - `eth_sendTransaction` and `wallet_switchEthereumChain`. + - the site round trips: + - connect; + - `personal_sign`; + - `eth_signTypedData_v4`; + - `eth_sendTransaction`; + - `wallet_switchEthereumChain`. - `tests/e2e/storagePartition.js` loads the extension from two directories into one profile, with and without the manifest `key`. With the key, both loads get the pinned extension id and read the same storage. Without it, the second load @@ -1769,10 +1817,14 @@ Four rules hold for both suites: geckodriver. The container has no network. The loopback server in `tests/e2e/dapp.js` serves the test page and a stub node. -- `tests/e2e/firefox/run.js` installs `dist/firefox/` as a temporary add-on. It - covers popup load, StateRecovery, wallet creation, Add Token and the site - round trips. Errors are read from Firefox's console service at each step - boundary. Any uncaught error from an extension source fails the run. +- `tests/e2e/firefox/run.js` installs `dist/firefox/` as a temporary add-on. + Errors are read from Firefox's console service at each step boundary. Any + uncaught error from an extension source fails the run. It covers: + - popup load; + - StateRecovery; + - wallet creation; + - AddToken; + - the site round trips. - `tests/e2e/firefox/reinstall.js` installs the packaged `.xpi`, creates a wallet, restarts the browser and adds the add-on again. It decrypts the stored secret through the real ShowRecoveryPhrase screen. It then uninstalls and @@ -1784,9 +1836,15 @@ from the `check` workflow. `e2e-chrome` has a 40-minute cap and `e2e-firefox` a `.gitea/workflows/check.yml` is a copy of the file in the `sneak/prompts` repository and is not edited here. +## TODO + +Planned work is on +[the issue tracker](https://git.eeqj.de/sneak/AutistMask/issues). `TODO.md` +holds the workflow, the status and the next step. + ## Policies -- The other wallet is not named in code or documentation. +- One competing wallet's name is barred from code and documentation. `script/check-censored` fails the lint stage and the build when the name appears outside its allowed places: - the two provider flags in `src/content/inpage.js`. Many sites check them