diff --git a/README.md b/README.md index 19c5905..e6b599e 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,15 @@ It does nothing else. It has: ## Why it exists -Popular browser wallets have grown swap screens, portfolio dashboards, -promotions and tracking. Most of them run only on Chromium browsers. AutistMask -is a wallet and nothing more. One code base serves both Chrome and Firefox. +Popular browser wallets have grown: + +- swap screens; +- portfolio dashboards; +- promotions; +- tracking. + +Most of them run only on Chromium browsers. AutistMask is a wallet and nothing +more. One code base serves both Chrome and Firefox. ## What it talks to @@ -40,6 +46,10 @@ Settings. The wallet uses that endpoint to: - check receipts; - on Ethereum Mainnet, read USD prices from Chainlink price feed contracts. +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. + 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 both offer them. @@ -50,12 +60,12 @@ both offer them. tracked. - "Check recipients on Etherscan" sends the recipient's address to Etherscan each time the user reviews a send. The wallet warns when Etherscan labels the - address as phishing or scam. + address as phishing, scam or exploiter. The phishing domain list is built into the extension. Checking a site against it sends nothing anywhere. -The section External services below lists every request. +The section External services below lists each request. ## Getting started @@ -80,16 +90,14 @@ Load it: Add-on" and pick `dist/firefox/manifest.json`. A temporary add-on is unloaded when Firefox exits. Read Installing on Firefox below before relying on it. -The user guide, [docs/README.md](docs/README.md), explains how to use the -wallet. The release procedure is in [docs/RELEASE.md](docs/RELEASE.md). +The user guide, [`docs/README.md`](docs/README.md), explains how to use the +wallet. The release procedure is in [`docs/RELEASE.md`](docs/RELEASE.md). ### Installing on Chrome `manifest/chrome.json` carries a fixed `key`. The key is the public half of an RSA key pair. Chrome derives the extension id from it, so the id is `gipbhkogfopeahplcjhipkgpcimdpkip` wherever the extension is loaded from. -`tests/extensionId.test.js` pins that id. `tests/e2e/storagePartition.js` -observes it in a real Chrome. Without a `key`, Chrome derives the id from the directory the extension was loaded from. `chrome.storage.local`, which holds the wallet, is kept per @@ -97,18 +105,20 @@ extension id. A copy loaded from another directory would then start with empty storage. Changing `key` changes the extension id, and Chrome then opens the extension on a new, empty storage area. -The private half of the key pair is a credential. It is not in this repository. -`tests/extensionId.test.js` fails if a private key is committed anywhere in the -tree. The private half is not needed to build, load or test anything here. It -signs a CRX, which this repository does not produce. +The private half of the key pair is a credential. It is not in this repository, +and nothing here needs it. It signs a CRX, Chrome's packaged extension file, +which this repository does not produce. ### Installing on Firefox +`manifest/firefox.json` requires Firefox 115 or newer. + The `.xpi` that `make package` writes is not signed. Release Firefox and Firefox -ESR 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, with -`xpinstall.signatures.required` set to `false` in `about:config`. +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 @@ -117,13 +127,11 @@ exits, so it must be added again on every start. The wallet survives that restart. `manifest/firefox.json` declares a fixed `browser_specific_settings.gecko.id`, and Firefox keys the extension's storage on that id. Adding the add-on again in the same profile finds the stored -wallets. `tests/e2e/firefox/reinstall.js` checks this in a real Firefox. It -creates a wallet, restarts the browser, adds the add-on again and decrypts the -vault with the original password. +wallets. Removing the add-on in `about:addons` destroys its storage, and the wallets with it. On Firefox, Remove is final. The recovery phrase, private key or extended -private key is the only way back. The same suite observes this. +private key is the only way back. AutistMask does not run in Firefox private windows. `manifest/firefox.json` sets `"incognito": "not_allowed"`. If Firefox is set to never remember history, every @@ -139,13 +147,15 @@ release/autistmask-firefox-.xpi release/SHA256SUMS ``` -It publishes nothing. [docs/RELEASE.md](docs/RELEASE.md) has the procedure. +It publishes nothing. [`docs/RELEASE.md`](docs/RELEASE.md) has the procedure. ## Settings -Every setting below is stored in the wallet's profile (see Stored data). The -default named is the one a new install has. The Settings screen offers each of -them. Welcome also offers "Use Blockscout" and "Check recipients on Etherscan". +Each default below is the value in a new install. Connected Sites are kept in +the browser's session storage; every other setting is stored in the wallet's +profile (see Stored data). The Settings screen offers every setting but +"Remember my choice for this site", which is on SiteApproval. Welcome also +offers "Use Blockscout" and "Check recipients on Etherscan". - Network: `Ethereum Mainnet` or `Sepolia Testnet`. Default `Ethereum Mainnet`. Only the user switches it, in Settings or by approving a site's request on @@ -174,22 +184,25 @@ them. Welcome also 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 lasts until - the browser closes. +- 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. - 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. - Debug mode: off. The checkbox is hidden until the user clicks the version in About ten times. -Three environment variables affect the build and the tests: +These environment variables affect the build and the tests: - `AUTISTMASK_DEBUG=1` makes `build.js` compile a debug build. Only the exact value `1` does; any other value gives a release build. - `AUTISTMASK_BUILD_RECEIPT` names the file `build.js` writes its receipt to. The `Makefile` sets it. -- `E2E_TRACE_NETWORK=1` makes the Chrome end-to-end suite print every request it +- `E2E_TRACE_NETWORK=1` makes the Chrome browser suite print every request it intercepts. +- `FIREFOX_BIN` and `GECKODRIVER` tell the Firefox browser suite where its two + binaries are. The suite's image sets both. ## Design @@ -204,7 +217,7 @@ src/ content/ index.js content script: relays messages between the page and the background - inpage.js the window.ethereum provider injected into pages + inpage.js the inpage provider: window.ethereum, injected into pages popup/ index.html the markup of every screen index.js popup entry point: loads the profile, restores the @@ -214,7 +227,7 @@ src/ theme.js applies the theme dustThreshold.js parses the dust threshold field styles/main.css Tailwind source, plus four classes of its own - views/ one module per screen, and helpers.js + views/ one module per screen or group of screens, and helpers.js shared/ modules used by both the popup and the background manifest/ chrome.json Manifest V3 for Chrome @@ -229,8 +242,9 @@ The shared modules, each in one line: - `addressWarnings.js`: the warnings for a recipient address. - `alarms.js`: the balance refresh alarm. -- `amountDisplay.js`: the four-decimal rule and its nonzero floor. -- `approvalAmount.js`: the scale and symbol a token amount is shown with. +- `amountDisplay.js`: the four-decimal rule, and the rule that a nonzero amount + never shows as zero. +- `approvalAmount.js`: the decimals and symbol a token amount is shown with. - `approvalTx.js`: fills in the transaction a site asks for, before the approval window opens. - `approvalVerify.js`: checks a signed transaction or signature against what was @@ -251,14 +265,14 @@ The shared modules, each in one line: - `html.js`: HTML escaping. - `log.js`: the logger and `debugFetch()`. - `networks.js`: the two networks. -- `persistedState.js`: the shape of the stored profile and its normalization. -- `phishingBlocklist.json`: the vendored phishing domain digests. +- `persistedState.js`: the shape of the stored profile and its defaults. +- `phishingBlocklist.json`: the phishing domain digests. - `phishingDomains.js`: the phishing domain check. - `prices.js`: USD prices from Chainlink feeds, and address totals. - `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 read of the profile goes +- `stateSchema.js`: `schemaVersion` and the check every profile read goes through. - `storedSettings.js`: reads "Use Blockscout" and "Check recipients on Etherscan" from storage. @@ -267,8 +281,8 @@ The shared modules, each in one line: - `tokenList.js`: the bundled token list and the Chainlink feeds. - `transactions.js`: transaction history from Blockscout, and the history filters. -- `transferAmount.js`: encodes the wallet's own ERC-20 send at the scale the - screen showed. +- `transferAmount.js`: encodes the wallet's own ERC-20 send with the decimals + the screen showed. - `txValidation.js`: the amount and balance checks for Send and ConfirmTx. - `uniswap.js`: the Uniswap Universal Router decoder. - `vault.js`: password-based encryption with libsodium. @@ -281,18 +295,19 @@ The shared modules, each in one line: A wallet is one of three types: -- An HD wallet (`type: "hd"`) comes from a 12 or 24 word recovery phrase. It can - hold many addresses. +- An HD wallet (`type: "hd"`, for hierarchical deterministic) comes from a 12 or + 24 word recovery phrase. It can hold many addresses. - A key wallet (`type: "key"`) comes from one private key. It holds one address. - 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 an empty BIP-39 passphrase. A new recovery phrase has -12 words. An extended private key must be a master key. AddWallet refuses a -child key. 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. +`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. The secret of each wallet is encrypted with that wallet's password (`src/shared/vault.js`): @@ -307,9 +322,15 @@ 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: the `xpub`, the addresses, the balances, the -tracked tokens and the settings. The popup shows all of that without a password. -The password is required to: +Public data is stored unencrypted: + +- 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 settings. + +The popup shows all of that without a password. The password is required to: - send a transaction; - sign a transaction, message or typed data for a site; @@ -334,12 +355,13 @@ Three rules follow from the worker being stopped: 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 shortest the alarms API allows. The refresh is skipped when - the last refresh finished less than 30 seconds ago. An open popup refreshes - every 10 seconds, so while the popup is open the background stays quiet. + one minute. The refresh is skipped when the last refresh finished 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 browser's session storage (`storage.session`). Session storage survives the - worker stopping and is cleared when the browser closes. + 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 @@ -368,10 +390,13 @@ each other. The background's `updateState()` reads the record, applies one change and writes it back. Every write stamps `schemaVersion` with `STATE_SCHEMA_VERSION` from -`src/shared/stateSchema.js`, currently `1`. +`src/shared/stateSchema.js`, currently `1`. The version is raised only when the +meaning of a stored field changes. A new field with a safe absent value needs +only a default in `normalizePersisted()`. -Every read goes through `assertStateUsable()` first, on the raw stored value. -The check refuses a record that: +Every profile read in `src/shared/state.js` and `src/background/state.js` goes +through `assertStateUsable()` first, on the raw stored value. The check refuses +a record that: - is not an object; - carries a `schemaVersion` this build does not know; @@ -383,20 +408,19 @@ A refused record is left in storage untouched. The popup shows StateRecovery. A site's request is answered with code `-32007` and a message saying the saved data cannot be read. -Every other field is floored by `normalizePersisted()` in -`src/shared/persistedState.js`. A list that is not a list becomes empty. A -malformed entry is dropped. A URL that is not text becomes the default. -`tests/persistedFieldContract.test.js` holds one row per stored field. Each row -states the field's floor and drives the code with hostile values. +`normalizePersisted()` in `src/shared/persistedState.js` gives most malformed +fields a default. A list that is not a list becomes empty. A malformed entry is +dropped. A URL that is not text becomes the default URL. The fields marked +`LOOSE` in `tests/persistedFieldContract.test.js` pass through unchecked. A save that fails for a storage reason puts up a red "NOT SAVED" banner. The banner stays until the popup closes. A save that fails because the stored record fails the check above shows StateRecovery instead. -A popup follows changes other pages saved to three fields: the network, "Use -Blockscout" and "Check recipients on Etherscan". It takes them from the stored -record under the lock and redraws its screens (`followStoredSettings()` in -`src/popup/index.js`). +A popup follows changes that other pages saved to three fields: the network, +"Use Blockscout" and "Check recipients on Etherscan". It takes those changes +from the stored record under the lock and redraws its screens +(`followStoredSettings()` in `src/popup/index.js`). ### Navigation @@ -411,31 +435,37 @@ 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. -A reopened popup renders only the wallet list and the screen it lands on. "Back" -onto a screen this page load has not rendered renders it first -(`src/popup/viewRouter.js`). When the data that screen needs is gone, Home is -shown instead. "Back" onto a screen already on the page only unhides it, so +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 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. ConfirmTx, WaitTx, SuccessTx, ErrorTx and -TransactionDetail are drawn for the network of the transaction they show, -whichever network is active. +reopened popup would draw it. Five screens are drawn for the network of the +transaction that they show, whichever network is active: + +- ConfirmTx; +- WaitTx; +- SuccessTx; +- ErrorTx; +- TransactionDetail. 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. -Three things are present on every screen but StateRecovery: +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. -- the title bar, with the Settings gear; -- the flash message line under it, which holds one line of text for two seconds; -- the red banner at the top of the page. The banner reads `[TESTNET]` on Sepolia - and `DEBUG / INSECURE` on a debug build or with debug mode on. A debug build - also appends the screen's view id. +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 +`DEBUG / INSECURE [TESTNET]` with both. A debug build also appends the screen's +view id. ### Screens @@ -453,7 +483,7 @@ as AddressDetail shows. A screen reaches it through `state.selectedWallet` and Shown while no wallet exists. The screen holds: - the text "Welcome! To get started, add a wallet."; -- a well with the "Use Blockscout" and "Check recipients on Etherscan" +- a boxed section with the "Use Blockscout" and "Check recipients on Etherscan" checkboxes, each with a sentence saying what it sends and to whom; - an "Add wallet" button. @@ -475,15 +505,15 @@ Shown while at least one wallet exists. The screen holds: - "Recent Transactions"; - an "Add additional wallet..." link. -Each wallet in the list shows its name, which is tap to rename in place. HD and -xprv wallets also show a "+" button that derives the next address. Each address -of a wallet is a block: +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: - 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 one address, an `[x]` button; - the ENS name, when one resolved; -- the full address, on a row of its own; +- the full address, on a row of its own, as plain text; - the address's USD total; - a balance line for ETH and for each token shown. @@ -493,7 +523,7 @@ three lines: - the age and the direction; - the counterparty's colour dot and label, with the amount on the right; -- the counterparty's full address. +- the counterparty's full address, as plain text. A contract creation has no counterparty. Its last line reads "This transaction creates a new contract. It has no recipient." While "Use Blockscout" is off, the @@ -528,7 +558,8 @@ Reached from Welcome, Home or Settings. The screen holds: - under "From xprv": a masked input for an extended private key; - a password input and a confirmation input, with a hint line. The hint names the backup of this wallet type and says the password cannot be recovered or - reset. Its height is reserved, so switching tabs moves nothing; + reset. The hint's height is reserved, so a longer hint never moves the + password fields; - an "Import" button with an error line beside it. The password needs 12 characters, or one while "Allow weak passwords" is on. @@ -537,15 +568,15 @@ The password needs 12 characters, or one while "Allow weak passwords" is on. "Wallet N", and encrypts the secret with the password. It clears the stack and shows Home. When no address is active yet, the new wallet's first address becomes active. A phrase or xprv import then scans for used addresses through -the RPC endpoint, five at a time, and stops after five unused addresses in a -row. When the scan finds two or more used addresses, they replace the wallet's +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: a full sentence on the error line; -- an invalid phrase or key, a child extended private 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, or + a phrase or address already in a wallet: a flash message. "Back" returns to the previous screen. @@ -588,14 +619,14 @@ Reached from the "···" menu on AddressDetail. The screen holds: - a password input, an error line and a "Reveal" button. "Reveal" with the right password decrypts the wallet's secret, derives this -address's key and shows it in place of the password input. The key is tap to -copy. A wrong password shows "That password is incorrect. Please try again." A -key the wallet refuses (see Wallets, keys and encryption) shows the refusal and -reveals nothing. +address's key and shows the key in place of the password input. Tapping the key +copies it. A wrong password shows "That password is incorrect. Please try +again." A key that the wallet refuses (see Wallets, keys and encryption) shows +the refusal and reveals nothing. Nothing is decrypted or written into the page before the password is accepted. -The key is never stored. Leaving the screen by any route wipes it, and a decrypt -still running when the screen is left is discarded. The screen is not +The key is never stored. Leaving the screen by any route wipes the key from the +page, and a decrypt still running at that moment is discarded. The screen is not restorable. The Settings gear leaves the screen and takes it off the stack. The wallet never clears the clipboard after a copy. The clipboard is the user's. @@ -609,9 +640,12 @@ Reached by tapping a balance row on AddressDetail. The screen holds: - the full address; - the USD value of this holding, and one balance line; - "Send" and "Receive" buttons; -- for an ERC-20 token, a well with the contract address and, where known, the - name, the symbol, the decimals, the holder count and the project website. The - "Holders:" row is left out when no count is known; +- for an ERC-20 token, a boxed section with the contract address and, where + known: + - the name and the symbol; + - the decimals; + - the holder count, left out when no count is known; + - the project website; - the token's transactions, or the "No transaction history is shown" line while "Use Blockscout" is off. @@ -642,13 +676,13 @@ out: reported holder count below 1,000. "Max" fills in a token's whole balance, cut to 18 decimal places. For ETH it -fills in the exact balance minus the fee reserve ConfirmTx checks against. The -ETH fee is estimated for the recipient entered, so a recipient is needed first. -Where there is nothing to fill in, a flash message says why. +fills in the exact balance minus the fee that ConfirmTx reserves. The ETH fee is +estimated for the recipient entered, so a recipient is needed first. Where there +is nothing to fill in, a flash message says why. The "To" input accepts an Ethereum address or an ENS name. A mixed-case address -must pass its checksum. The zero address is refused. Sending to one's own -address is allowed, with a warning. +must pass its checksum. The zero address is refused. Sending to the sending +address itself is allowed, with a warning. "Review" checks the amount (see Amounts below), resolves an ENS name through the RPC endpoint and opens ConfirmTx. A name with no on-chain address, including a @@ -658,7 +692,7 @@ address." #### ConfirmTx (`confirm-tx`) Shows the send the user reviewed, on the network that was active when Send -opened it. The screen holds: +opened this screen. The screen holds: - a "Back" button and the heading "Confirm Transaction"; - the type: "Native ETH transfer" or "ERC-20 token transfer (SYMBOL)"; @@ -673,12 +707,12 @@ opened it. The screen holds: - warnings, each in its own box: - the recipient is on the scam address list; - the recipient is a burn address; - - the recipient is the sender; + - the recipient is the sending address; - the recipient is a contract; - the recipient has never sent a transaction and holds no ETH; - - Etherscan labels the recipient as phishing or scam. While "Check - recipients on Etherscan" is off, a line says instead that the address was - not checked on Etherscan; + - Etherscan labels the recipient as phishing, scam or exploiter. While + "Check recipients on Etherscan" is off, a line says instead that the + address was not checked on Etherscan; - errors, each in its own box: - the amount is invalid; - the amount exceeds the balance; @@ -727,9 +761,9 @@ broadcast time. Outcomes: - a receipt: SuccessTx; - no receipt 60 seconds or more after the broadcast: ErrorTx with a timeout message; -- six failed lookups in a row: ErrorTx with a message naming the unreachable - network and pointing at the RPC URL in Settings. A lookup that answers resets - the count. +- six failed lookups in a row: ErrorTx with a message saying the network could + not be reached and pointing at the RPC URL in Settings. A lookup that answers + resets the count. For a swap whose amount could not be decoded, the amount reads `Unknown (could not be decoded)` with no symbol. @@ -749,7 +783,8 @@ stack and opens AddressToken or AddressDetail. #### ErrorTx (`error-tx`) -Shown when a broadcast fails or a wait ends without a receipt. The screen holds: +Shown when signing or a broadcast fails, or when a wait ends without a receipt. +The screen holds: - the heading "Transaction Failed"; - the amount and the recipient; @@ -772,16 +807,22 @@ Reached from Home, AddressDetail or AddressToken. The screen holds: #### TransactionDetail (`transaction`) Reached by tapping a transaction row. The screen describes the transaction on -the network it was sent on. It holds: +the network that it was sent on. It holds: - the transaction hash, with an explorer link; -- the type: one of "Native ETH Transfer", "ERC-20 Token Transfer", "Swap", - "Token Approval", "Contract Call" and "Contract Creation"; +- the type, one of: + - "Native ETH Transfer"; + - "ERC-20 Token Transfer"; + - "Swap"; + - "Token Approval"; + - "Contract Call"; + - "Contract Creation"; - the status, "Success" or "Failed"; - the From and To addresses, each with identicon, full address, explorer link and ENS name; - the time: ISO datetime and relative age; -- the amount, exact and in full, and under it the quantity in base units; +- the amount, exact and in full, and under it the quantity in base units. + Tapping either copies it; - the token contract, for an ERC-20 transfer; - the details that come from Blockscout: - the block number; @@ -807,9 +848,14 @@ 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 token already tracked, an address on the scam list, -or a contract that fails the lookup gets a flash message. A network switch -before the lookup answers adds nothing, and a flash message says so. +and shows AddressDetail. A flash message refuses: + +- a token already tracked; +- an address on the scam list; +- a contract that fails the lookup. + +A network switch before the lookup answers adds nothing, and a flash message +says so. #### Settings (`settings`) @@ -855,12 +901,13 @@ Transitions: every address's balances. 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 it. - Unticking "Use Blockscout" also drops every untracked token from every + 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 address. -- `[x]` on an allowed or connected site disconnects the site. Its origin is - dropped under every address. The background ends its session connections and - tells its open tabs `accountsChanged` with an empty list. +- `[x]` on an allowed or connected site disconnects the site. The site's origin + is dropped under every address. The background ends the site's session + connections and sends `accountsChanged` with an empty list to the site's open + tabs. - `[x]` on a denied site forgets the refusal. - "+ Add wallet": AddWallet. "+ Add token": SettingsAddToken. `[recovery phrase]`: ShowRecoveryPhrase. `[x]` on a wallet: DeleteWallet. @@ -875,8 +922,9 @@ holds: - a warning that anyone holding the words can take everything in the wallet; - a password input, an error line and a "Reveal" button. -The right password replaces the prompt with the phrase, tap to copy. A wrong -password shows a full-sentence error. Secret handling is as on ExportPrivKey. +The right password replaces the prompt with the phrase. Tapping the phrase +copies it. A wrong password shows a full-sentence error. Secret handling is as +on ExportPrivKey. #### DeleteWallet (`delete-wallet-confirm`) @@ -895,28 +943,37 @@ only when it belonged to the deleted wallet, and connected sites are then told. #### DeleteWalletLostPassword (`delete-wallet-lost-password`) -Reached from DeleteWallet. The screen says three things: +Reached from DeleteWallet. The screen says: -- the password cannot be recovered; -- deletion erases only the copy of the key stored in this browser; -- the wallet can be added again from a kept copy of its backup. +- the password cannot be recovered or reset, so the wallet cannot be unlocked + again; +- no password is needed to delete the wallet; +- deleting erases only the copy of the key stored on this device; nothing on + chain changes and no money moves; +- with a kept copy of the wallet's backup, the wallet can be added again with a + new password; +- without one, deleting the wallet loses everything it holds, forever; +- the other wallets are not touched. -The user types the wallet's name to confirm. Letter case, surrounding spaces, -repeated spaces and invisible characters are ignored in the comparison. An empty -field is refused. +The user types the wallet's name to confirm. The comparison ignores: -"Delete This Wallet Forever" with the name typed does the same as "Confirm -Delete". "Back" returns to DeleteWallet. No password is asked, because a -password in front of discarding a secret protects nobody. +- letter case; +- spaces before and after the name; +- repeated spaces; +- invisible characters. + +An empty field is refused. "Delete This Wallet Forever" with the name typed does +the same as "Confirm Delete". "Back" returns to DeleteWallet. No password is +asked, because a password in front of discarding a secret protects nobody. #### DeleteAddress (`delete-address-confirm`) Reached from `[x]` on an address on Home. Offered only on HD and xprv wallets holding more than one address. The screen shows the address and explains that -removing it only stops the wallet tracking it. It explains the way back: delete -the whole wallet in Settings and import its backup again. The scan after an -import finds only addresses with on-chain activity. When the address holds -anything, the screen lists the holdings and the USD total under a warning. +removing the address only stops the wallet tracking it. It explains the way +back: delete the whole wallet in Settings and import its backup again. The scan +after an import finds only addresses with on-chain activity. When the address +holds anything, the screen lists the holdings and the USD total under a warning. "Remove Address" removes the address and its site permissions. It returns to Home with the flash message "Address removed." The wallet's `nextIndex` is not @@ -927,13 +984,18 @@ rewound, so "+" derives a fresh address. No password is asked. Reached from "+ Add token" in Settings. The screen offers the top 10 tokens of the bundled list as buttons, the top 100 in a dropdown, and a contract address input. The two lists are shown on mainnet only. Each path adds the token to the -tracked list and shows "Added SYMBOL". Refusals are as on AddToken. +tracked list and shows "Added SYMBOL". + +The buttons and the dropdown do no lookup. A token already tracked is disabled +in both, and picking one in the dropdown anyway gets "Already tracked." The +address input refuses as AddToken does. #### SiteApproval (`approve-site`) Raised when a site calls `eth_requestAccounts` or `wallet_requestPermissions` -and is on neither the allowed nor the denied list. The background prefers the -toolbar popup and falls back to a separate window. The screen holds: +and is not on the allowed list, not on the denied list, and not connected +without "Remember". The background prefers the toolbar popup and falls back to a +separate window. The screen holds: - the site's origin; - a phishing warning, when the host is on the blocklist; @@ -962,15 +1024,16 @@ separate window. The screen holds: - the raw calldata; - a password input, and "Confirm" and "Reject" buttons. -Every value shown was filled in by the background before the window opened. The -background compares the signed transaction against every one of them. See Sites -and approvals. +Every transaction field shown was filled in by the background before the window +opened. The background compares the signed transaction against every one of +them. See Sites and approvals. "Confirm" with the right password signs the transaction as shown, hands it to the background and opens WaitTx in the same window. A wrong password shows an -error. A refusal that spends the approval opens ErrorTx. "Reject" answers the -site with code `4001` and closes the window. Closing the window without an -answer leaves the request pending; the toolbar button then opens this screen. +error. When the background refuses the signed transaction for good (see Sites +and approvals), ErrorTx opens. "Reject" answers the site with code `4001` and +closes the window. Closing the window without an answer leaves the request +pending; the toolbar button then opens this screen. #### SignApproval (`approve-sign`) @@ -985,24 +1048,30 @@ window. The screen holds: - a password input, and "Sign" and "Reject" buttons. A personal message is shown as the text its bytes decode to, laid out in the -order of the bytes. Each control character, line or paragraph separator, and -character that paints nothing is shown as a bordered `U+XXXX` mark. Bytes that -are not UTF-8 read "This message is not text." The hex is shown under the text. -An `eth_sign` request carries the warning that the data will be signed as a -personal message. +order of the bytes. A line feed is shown as a line break. These characters are +shown as a bordered `U+XXXX` mark: -Typed data is shown as its domain, the primary type `ethers` will sign, and the -message fields that type declares, in the type's order. Keys the type does not -declare are listed under "Not part of the signature". Typed data whose primary -type is `Permit` or one of Permit2's signature types gets a red warning at the -top. The warning names the spender and each token and amount the signature lets -the spender take. `Unlimited` marks the field's largest value. `Unknown` marks -anything the declared fields do not give. +- every other control character; +- the line separator and the paragraph separator; +- characters that paint nothing, such as zero-width characters. + +Bytes that are not UTF-8 read "This message is not text." The hex is shown under +the text. An `eth_sign` request carries the warning that the data will be signed +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 +declared fields do not give. "Sign" is disabled, with the reason on the error line, when: - the typed data cannot be read, names no primary type, or names one other than - the type it would be signed as; + the type that it would be signed as; - a declared field is missing, or a `bool` field is not `true` or `false`; - the domain's `chainId` is not the active network's; - a personal message is not hex. @@ -1043,21 +1112,28 @@ popup closes. ### Amounts -Token and ETH amounts are shown with exactly four decimal places, cut and never -rounded up (`truncateAmount()` in `src/shared/amountDisplay.js`). On the screens -that state what is being authorized, an amount below 0.0001 is extended to its -first nonzero digit instead (`truncateAmountNeverZero()`). So a nonzero amount -never reads as zero. Those screens are: +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: - TxApproval; - ConfirmTx, and Send's current balance; - WaitTx, SuccessTx and ErrorTx. -The balance lists round to four places. TransactionDetail shows the exact amount -and its base units. +TransactionDetail shows the exact amount and its base units. -A token amount in a site's calldata carries no scale, so the wallet needs the -token's `decimals`. It looks in three places, in this order +An amount to send is valid when it is a number above zero, written as digits +with at most one decimal point. Nothing but zeros may follow the token's last +decimal place. ETH has 18 decimal places. A token with more than 18, or with +unknown decimals, is held to 18. Send checks this before ConfirmTx opens, and +ConfirmTx checks again (`isValidAmount()` in `src/shared/txValidation.js`). + +A token amount in a site's calldata carries no decimals, so the wallet needs the +token's `decimals` to show it. It looks in three places, in this order (`resolveTokenDecimals()` in `src/shared/approvalAmount.js`): - the bundled token list; @@ -1067,15 +1143,17 @@ token's `decimals`. It looks in three places, in this order The token's name comes from the same places. When none of them answers, the amount reads `5000000000 base units (decimals unknown)`, and nothing is -formatted at a guessed scale. A holding whose scale nothing knows shows +formatted with guessed decimals. A holding whose decimals are unknown shows "quantity unknown" in the balance lists. Only a whole number from 0 to 80 counts -as a scale. While "Use Blockscout" is off, a token the first two sources cannot -name gets a line under it on the approval and status screens. The line says that -Blockscout's report is not used. +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. -The wallet's own ERC-20 send is encoded at the scale the screen showed. At -signing time the contract's `decimals()` is read only to be compared with it. A -disagreement refuses the send (`src/shared/transferAmount.js`). +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. +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 @@ -1100,8 +1178,8 @@ A displayed token symbol is cut to 12 characters Only a token with a Chainlink feed has a price. `getAddressValue()` in `src/shared/prices.js` returns the value of the priced holdings and whether an -unpriced holding was left out. `formatAddressTotal()` renders it the same way on -every screen: +unpriced holding was left out. `formatAddressTotal()` renders that result the +same way on every screen: - on Sepolia, or before the first price fetch: no total; - everything priced: `Total: $5,500.00`; @@ -1111,15 +1189,22 @@ 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 truncates an -address. `truncateMiddle()` in `src/popup/views/helpers.js` removes at most 10 -characters, and `renderAddressHtml()` floors a request at 32 characters, but no -caller asks for either today. Tapping an address copies it in full. +`.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 +that label. + +On most screens, tapping an address copies it, and an explorer link sits beside +it. The address rows of Home's wallet list and the history rows are plain text. +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 links to the block -explorer of its network: `etherscan.io` on mainnet and `sepolia.etherscan.io` on -Sepolia. The links contact nothing until clicked. +identicon on the screens where space allows. ### Scam protection @@ -1129,18 +1214,20 @@ 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 network's native currency map to no contract at all. A token bearing a known symbol from any other contract is a spoof (`isSpoofedSymbol()` in -`src/shared/symbolSpoof.js`). The symbol is compared after NFKC normalization, -with invisible characters removed and case ignored. The rule applies always to -the balance lists and the Send dropdown. In the transaction history it is the -setting "Hide fake tokens impersonating a known symbol". +`src/shared/symbolSpoof.js`). The symbol is compared after Unicode compatibility +normalization (NFKC), with invisible characters removed and case ignored. The +rule always applies to the Send dropdown and to the balance rows fetched for an +address. A tracked token listed at zero balance is not checked. In the +transaction history, the rule applies while "Hide fake tokens impersonating a +known symbol" is on. Low-holder tokens. While "Hide tokens with fewer than 1,000 holders" is on, the transaction history and the Send dropdown hide a token with a reported holder -count below 1,000. The balance list has its own floor: an untracked token -appears only when it is on the bundled list or has 1,000 or more reported -holders. A token whose count is unknown is never hidden from the history or the -dropdown, and never reaches the balance list unless bundled or tracked. Only a -whole number written in digits counts as a count (`src/shared/holders.js`). +count below 1,000. The balance list has its own rule: an untracked token appears +only when it is on the bundled list or has 1,000 or more reported holders. A +token whose count is unknown is never hidden from the history or the dropdown, +and never reaches the balance list unless bundled or tracked. Only a whole +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 @@ -1152,8 +1239,9 @@ Dust transactions. While "Hide dust transactions below N gwei" is on, the history hides native transfers below the threshold. Contract calls and token transfers are never hidden as dust. -Scam addresses. `src/shared/scamlist.js` holds known fraud, drainer and phishing -addresses. ConfirmTx warns before a send to one of them. AddToken and +Scam addresses. `src/shared/scamlist.js` holds the addresses of known frauds, +phishing operations and wallet drainers (contracts that empty a wallet once it +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. @@ -1161,24 +1249,25 @@ ConfirmTx also warns when the recipient is: - a contract; - a burn address; -- the user's own 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 or scam. + phishing, scam or exploiter. #### Phishing domains -A domain blocklist is vendored into the extension at build time -(`make vendor-blocklist`, see Development). The artifact holds digests, not -names. Each entry is the first 16 hex characters of the sha256 of the lowercased -domain (`src/shared/domainHash.js`). The entries are concatenated in sorted -order. A lookup hashes the hostname and each parent domain and binary-searches -the string (`src/shared/phishingDomains.js`). A trailing dot is dropped first. +The extension ships a phishing domain list in +`src/shared/phishingBlocklist.json`. `make vendor-blocklist` refreshes the file +from its upstream (see Development). The file holds digests, not domain names. +Each entry is the first 16 hex characters of the sha256 of the lowercased domain +(`src/shared/domainHash.js`). The entries are concatenated in sorted order. A +lookup hashes the hostname and each parent domain and binary-searches the string +(`src/shared/phishingDomains.js`). A trailing dot is dropped first. The check runs locally and sends nothing. The list is as current as the last -vendoring run that shipped. When the requesting site's host is on the list, all -four approval screens show a red warning. +refresh that shipped. When the requesting site's host is on the list, all four +approval screens show a red warning. ### Transaction decoding @@ -1195,28 +1284,30 @@ recipient, and the amount. An `approve` of the `uint256` maximum reads `Unlimited`. `src/shared/uniswap.js` decodes Uniswap Universal Router `execute()` calls. It -does so only for a transaction sent to one of the router deployments it lists -for the network: five on mainnet and four on Sepolia. The same calldata sent to -any other address is shown as raw data. The decoder reads each step as the -command that router release runs for its command byte. It names the action -`Swap IN → OUT` when both tokens are known. The lines are: +does so only for a transaction sent to one of the router deployments that it +lists for the network: five on mainnet and four on Sepolia. The same calldata +sent to any other address is shown as raw data. Each byte of the router's +`commands` field names one step. The decoder names the action `Swap IN → OUT` +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. -- `Amount`: the total the steps take of `Token In` from the user. `Up to` is - prefixed when the transaction has an exact-out step. `Unlimited` marks an - amount at or above the `uint160` maximum. `All available (V4 open delta)` and +- `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 it did read then gets lines of its - own. -- `Moved from the sender`, `Token moved` and `Moved to`: a take the `Amount` - line does not cover, or what a `PERMIT2_TRANSFER_FROM` moves and where. + bound what leaves 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. - `Permit2 allowance`, `Allowance token` and `Allowance spender`: each allowance - a `PERMIT2_PERMIT` or `PERMIT2_PERMIT_BATCH` grants, unless the permit's + that a `PERMIT2_PERMIT` or `PERMIT2_PERMIT_BATCH` grants, unless the permit's amount already stands as the `Amount` figure. - `Paid to`: each address other than the sender that a step pays. When no step pays anyone, the line reads `Unknown (not named in the calldata)`. The line @@ -1226,15 +1317,16 @@ command that router release runs for its command byte. It names the action - a deposit that pays a bridge; - a payee that could not be read. - `Min. received`: the minimum the sender gets. A zero minimum reads - `None (no minimum guaranteed)`. The label changes to `Min. paid out` in three - cases: - - another address is paid; - - a token stays in the router; - - an allowance goes to a spender other than the router. -- `Steps`: each command by name, in order. A step the router lets fail without - reverting is marked `(allowed to fail)`. A step the decoder could not read is - marked `(could not be decoded)`. An `EXECUTE_SUB_PLAN` step is marked - `(its steps are not shown)`. + `None (no minimum guaranteed)`. The label is `Min. paid out` instead unless + all three hold: + - a step pays the sender; + - 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)`. - `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)`. @@ -1254,10 +1346,10 @@ decide what the router still holds after each step. Those rules decide what the ### Sites and approvals -The content script (`src/content/inpage.js`) puts a `window.ethereum` provider +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 provider relays -each request through `src/content/index.js` to the background. +through EIP-6963 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 @@ -1282,11 +1374,16 @@ The background answers these methods: active one is refused with code `4100`. - `eth_sendTransaction`: TxApproval, for a connected site, with the same address rule. -- Read-only node methods such as `eth_call`, `eth_getBalance`, `eth_estimateGas` - and `eth_getTransactionReceipt` are passed to the RPC endpoint. - `PROXY_METHODS` in `src/background/index.js` is the list. +- The methods in `PROXY_METHODS` in `src/background/index.js` are passed to the + RPC endpoint for any page that asks, connected or not. The list holds node + methods such as `eth_call`, `eth_getBalance` and `eth_estimateGas`, and it + 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. + One transaction approval exists at a time, wallet-wide, because filling in a transaction fixes its nonce. A second `eth_sendTransaction` while one is unanswered is refused with code `-32002`. One site approval, one signature @@ -1294,26 +1391,26 @@ 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 come from the node. The gas limit is the node's estimate -unless the site gave one. A site's nonce is ignored. A transaction the node -refuses, or that the node does not answer within 20 seconds, raises no window, -and the site gets the node's error. The popup signs exactly the object it was -shown. The background then checks the signed transaction (`verifySignedTx()` in -`src/shared/approvalVerify.js`): +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`): -- the signer is the address the approval was raised for; +- the signer is the address that the approval was raised for; - the type is 0, 1 or 2; - the chain id is the active network's; - every field the type serializes equals the approved transaction; -- the transaction rebuilt from those fields has the same bytes, and the artifact - is its canonical encoding; +- the transaction rebuilt from those fields has the same bytes, and the signed + transaction is its canonical encoding; - the gas limit and fees are within the ceilings. -A mismatch spends the approval: the site gets an error and the transaction is -not retried. A wrong password leaves the approval standing. After a broadcast -the nonce is recorded, and a second artifact at that nonce is refused. -Signatures are checked the same way: the background recovers the signer from the -signature and refuses any other address. +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. Switching the active address rejects every pending approval with code `4001` and closes its window. It ends every connection allowed without "Remember", and @@ -1333,8 +1430,8 @@ when: - a fee per gas is above 100,000 gwei; - the gas limit times the maximum fee per gas is above 1 ETH. -The check runs when a site's transaction is filled in, when its signed artifact -is verified, and before the wallet's own send is signed. +The check runs when a site's transaction is filled in, when its signed +transaction is checked, and before the wallet's own send is signed. ### External services @@ -1344,13 +1441,19 @@ Ethereum JSON-RPC endpoint, contacted by default: 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); +- 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 address scan on import, fee estimates, nonces, broadcasts and receipt - lookups; -- the methods a connected site asks for from `PROXY_METHODS`. +- the address scan on import; +- the `symbol()`, `decimals()` and `name()` lookup when a token is added by + contract address; +- the recipient checks on ConfirmTx: `eth_getCode`, `eth_getTransactionCount` + and `eth_getBalance`; +- 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. Every ENS lookup stays on the RPC endpoint. A name whose resolver points off-chain does not resolve. @@ -1371,8 +1474,8 @@ whether or not the setting is on. That request carries no user data. Etherscan, only while "Check recipients on Etherscan" is on: ConfirmTx fetches the recipient's address page from the network's explorer each time the screen is -shown. The page is read for a phishing or scam label. A failed fetch is ignored. -The recipient's address is the only data sent. +shown. The page is read for a phishing, scam or exploiter label. A failed fetch +is ignored. The recipient's address is the only data sent. At the raised log level, `debugFetch()` in `src/shared/log.js` logs each request it makes. It logs the HTTP method, the origin of the URL and, for a JSON-RPC @@ -1383,11 +1486,16 @@ call, the method name. The path, the query string and the body are never logged. Four libraries ship in the extension. `package.json` names them and `yarn.lock` pins them: -- `ethers` (MIT): every Ethereum operation. Recovery phrases, key derivation, - signing, transaction construction, ERC-20 calls and JSON-RPC. -- `libsodium-wrappers-sumo` (ISC): Argon2id and XSalsa20-Poly1305 for the vault. +- `ethers` (MIT): every Ethereum operation: + - recovery phrases and key derivation; + - signing; + - transaction construction; + - ERC-20 calls; + - JSON-RPC. +- `libsodium-wrappers-sumo` (ISC): Argon2id and XSalsa20-Poly1305 for the + encrypted secret. - `qrcode` (MIT): the QR code on Receive. -- `ethereum-blockies-base64` (ISC): the identicons. +- `ethereum-blockies-base64` (MIT): the identicons. The development dependencies ship nowhere: @@ -1400,9 +1508,11 @@ The development dependencies ship nowhere: ### Crypto policy -No cryptographic primitive is implemented in this repository. Every -cryptographic operation goes through `ethers` or `libsodium-wrappers-sumo`. An -exception needs a code comment that cites this policy. There is none. +Hashing, key derivation, signing and encryption go through `ethers` or +`libsodium-wrappers-sumo`. No primitive is implemented in this repository. A new +recovery phrase draws its random bytes from the browser's +`crypto.getRandomValues()`. The build tooling, `build.js` and `script/lib/`, +hashes files with Node's `crypto`; none of it ships in the extension. ### Content Security Policy @@ -1415,7 +1525,7 @@ connect-src 'self' https: http:; frame-src 'none'; form-action 'none'; base-uri 'none' ``` -- `default-src 'self'` is the floor. Everything not named falls back to it. +- Every directive not named falls back to `default-src 'self'`. - `script-src 'wasm-unsafe-eval'` lets libsodium compile its WebAssembly build. Without it libsodium falls back to a JavaScript translation, and a key derivation takes about 20 times longer. The keyword permits WebAssembly only, @@ -1429,11 +1539,6 @@ base-uri 'none' - `frame-src 'none'`, `form-action 'none'` and `base-uri 'none'` are named because the last two do not fall back to `default-src`. -`tests/manifest.test.js` pins the exact policy in both manifests. -`tests/vaultBackend.test.js` asserts the unit tests run libsodium's WebAssembly -backend. The Chrome browser suite compiles a WebAssembly module in the real -popup. - ### Debug mode `DEBUG` in `src/shared/constants.js` is a build-time constant. `build.js` sets @@ -1444,36 +1549,46 @@ it to `true` only when `AUTISTMASK_DEBUG=1`. A debug build: - makes the die button on AddWallet produce one fixed, publicly known recovery phrase. -Never distribute a debug build. `make build` never produces one; -`make build-debug` is the only target that does. +No `if (DEBUG)` branch may skip functionality or bypass a security check. Adding +any other branch on `DEBUG` needs the project owner's approval. + +Never distribute a debug build. `make build` never produces one. +`make build-debug` does, and so does `make dev` while `AUTISTMASK_DEBUG=1` is +exported. The debug checkbox in Settings is a separate runtime flag. It raises the log level and shows the banner, and nothing else. `generateMnemonic()` reads the build-time constant, so no runtime setting in a release build can reach the test -phrase. Adding any other branch on `DEBUG` needs the project owner's approval. +phrase. + +At the raised log level the console also prints the wallet's addresses, their +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. -- Every piece of information is text in the system monospace font. Identicons - are the only images. -- Every control looks like a control: buttons have a border and a hover state, - and clickable text is underlined. -- Nothing moves when a result arrives. Every element filled in later has its - space reserved. An empty USD value is written as ` `. +- Every piece of information is text in the system monospace font. The + identicons and the QR code on Receive are the only pictures. +- 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 + 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. -- No framework. The popup is plain JavaScript with one module per screen. +- No framework. The popup is plain JavaScript with one module per screen or + group of screens. ### Wording - "Recovery phrase", never "seed phrase" or "mnemonic". -- "Address", never "account". +- "Address", never "account" or "derived key". - "Password", never "encryption key" or "passphrase". - Buttons use plain verbs: "Send", "Receive", "Add", "Back", "Allow", "Deny". - Every error message is a full sentence. @@ -1491,7 +1606,7 @@ The `Makefile` targets are thin shims over `script/`: 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 with a 90-second timeout, rerun verbose on + The stage runs the jest suite, stopped at 90 seconds and rerun verbose on failure, then `script/test-verify-build`. - `script/lint`: build the `lint` stage with `--no-cache`. The stage runs ESLint, the prettier check and `script/check-censored`. Linting runs only in @@ -1543,9 +1658,20 @@ Targets with no script of their own: ### Builds `build.js` empties `dist/` and compiles the Tailwind stylesheet. It then bundles -four entry points per browser with `esbuild`: the popup, the background, the -content script and the inpage provider. It copies the popup markup, the -stylesheet, the icons and the manifest into `dist/chrome/` and `dist/firefox/`. +four entry points per browser with `esbuild`: + +- the popup; +- the background; +- the content script; +- the inpage provider. + +It copies into `dist/chrome/` and `dist/firefox/`: + +- the popup markup; +- the stylesheet; +- the icons; +- the manifest. + It compiles into the bundles: - the version; @@ -1578,20 +1704,42 @@ have left a complete, loadable debug bundle behind. `make build-debug` keeps its ### Tests -`make test` runs the jest suite in Docker. The suite must finish within 90 -seconds. `tests/popupElementIds.test.js` checks that every element id the popup -looks up exists in `src/popup/index.html` and that no id is defined twice. -`tests/persistedFieldContract.test.js` boots the real popup entry point over -hostile stored values. `make check` runs the tests, the lint stage and the -formatting check. +`make test` runs the jest suite in Docker. The suite must finish within 60 +seconds; the test stage stops it at 90. `make check` runs the tests, the lint +stage and the formatting check. + +Some unit tests pin facts stated elsewhere in this file: + +- `tests/extensionId.test.js`: the Chrome `key`, the extension id derived from + it, and that no private key is committed anywhere in the tree. +- `tests/manifest.test.js`: the exact Content Security Policy in both manifests, + the Firefox `incognito` setting, and the icons. +- `tests/vaultBackend.test.js`: libsodium runs on its WebAssembly build. +- `tests/persistedFieldContract.test.js`: one row per stored field, stating how + the field is handled and driving the code with hostile values. It boots the + real popup entry point over them. +- `tests/popupElementIds.test.js`: every element id the popup looks up exists in + `src/popup/index.html`, and no id is defined twice. ### Browser tests -Two suites drive the real popup in a real browser. They share no code. Both need -Docker and nothing else. Each builds an image carrying the repository and a -fresh extension build, under a tag of its own, and removes the image afterwards. -Neither is part of `make check`. Run them before changing anything under -`src/popup/views/`. +Two suites drive the real popup in a real browser. Both need Docker and nothing +else. Each builds an image carrying the repository and a fresh extension build, +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: + +- Nothing under `tests/e2e/` is named `*.test.js`, so jest never picks the + suites up. +- 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: @@ -1599,6 +1747,7 @@ Neither is part of `make check`. Run them before changing anything under request from the popup and the background worker is answered from fixtures in `tests/e2e/network.js`. An unexpected request fails the run, and so does any 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; - the recovery phrase and private key screens; @@ -1608,15 +1757,16 @@ Neither is part of `make check`. Run them before changing anything under - the site round trips: connect, `personal_sign`, `eth_signTypedData_v4`, `eth_sendTransaction` and `wallet_switchEthereumChain`. - `tests/e2e/storagePartition.js` loads the extension from two directories into - one profile, with and without the manifest `key`, and asserts what Chrome does - with its storage. + 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 + sees empty storage. - `tests/e2e/slowApproval.js` leaves a transaction approval and a signature approval open for seven minutes with their windows minimized, then approves both. It starts Chromium itself and speaks the DevTools protocol, because Chrome never stops a worker that Playwright is attached to. `make test-e2e-firefox` runs two programs in an image with a pinned Firefox and -geckodriver. The container has no network. A loopback server in +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 @@ -1624,20 +1774,24 @@ geckodriver. The container has no network. A loopback server in 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/reinstall.js` installs the packaged `.xpi`, creates a - wallet, restarts the browser, adds the add-on again and decrypts the vault - through the real ShowRecoveryPhrase screen. It then uninstalls and reinstalls, - and asserts that Firefox destroyed the storage. + wallet, restarts the browser and adds the add-on again. It decrypts the stored + secret through the real ShowRecoveryPhrase screen. It then uninstalls and + reinstalls, and observes that Firefox destroyed the storage. `.gitea/workflows/e2e.yml` runs both suites on every push as two jobs, separate from the `check` workflow. `e2e-chrome` has a 40-minute cap and `e2e-firefox` a 15-minute cap. The jobs report; they do not gate a merge. +`.gitea/workflows/check.yml` is a copy of the file in the `sneak/prompts` +repository and is not edited here. ## Policies - The other wallet is not named in code or 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`; + - the two provider flags in `src/content/inpage.js`. Many sites check them + before they will talk to a wallet, so leaving them out would stop the + wallet working on those sites; - one token's on-chain name in `src/shared/tokenList.js` and `script/lib/token-enrichment.json`; - the upstream source in `script/vendor-blocklist`. @@ -1646,10 +1800,10 @@ from the `check` workflow. `e2e-chrome` has a 40-minute cap and `e2e-firefox` a ## License -GPL-3.0. See [LICENSE](LICENSE). +GPL-3.0. See [`LICENSE`](LICENSE). -Three data files come from other projects under their own licenses. The full -license texts are in [LICENSE](LICENSE). +Two data files hold data from three other projects, under those projects' +licenses. The full license texts are in [`LICENSE`](LICENSE). - `src/shared/phishingBlocklist.json` is derived from the `eth-phishing-detect` community blocklist, `src/config.json`. Copyright (c) 2018 kumavis. DBAD