From 4dc7814607bf0a5bb26f626a75afc4a81ab14a16 Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 1 Nov 2026 23:35:37 +0000 Subject: [PATCH] docs: rewrite README.md sentence by sentence against the prose rules Every sentence is written for a newcomer to read once: one idea, subject and verb first, under about 25 words, bullets instead of comma lists, no tables, no project history. The order is what the extension is, what it talks to, how to build and load it, the settings, the design, then development and tests. Every statement was checked against the code on next; the disagreements with the old text are listed in the PR. Part of https://git.eeqj.de/sneak/AutistMask/issues/672. Model: fable-5-1 --- README.md | 4984 ++++++++++++++++------------------------------------- 1 file changed, 1473 insertions(+), 3511 deletions(-) diff --git a/README.md b/README.md index a524852..19c5905 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,65 @@ # AutistMask -AutistMask is a GPL-licensed JavaScript browser extension by -[@sneak](https://sneak.berlin) that provides a minimal Ethereum wallet for -Chrome and Firefox. It manages HD wallets derived from BIP-39 seed phrases and -supports sending and receiving ETH and ERC-20 tokens, as well as web3 site -connection and authentication via the EIP-1193 provider API. +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). -The most popular browser-based EVM wallet has a cute mascot, but sucks now. It -has tracking, ads, preferred placement for swaps, tx broadcast fuckery, -intercepts tx status links to their own site instead of going to Etherscan, etc. -None of the common alternatives work on Firefox. +The wallet does four things: -Hence, a minimally viable ERC20 browser wallet/signer that works cross-platform. -Everything you need, nothing you don't. We import as few libraries as possible -and don't implement any crypto. By default the wallet sends user-specific data -only to the Ethereum RPC endpoint. The user can change that endpoint in -Settings. The endpoint defaults to a public node. +- It holds wallets made from a recovery phrase, a private key or an extended + private key. +- It shows the ETH and ERC-20 token balances of each address. +- It sends and receives ETH and ERC-20 tokens. +- It connects to web3 sites and signs what the user approves. -Out of the box the extension contacts one service, the configured RPC node. The -wallet uses the node for: +It does nothing else. It has: -- blockchain interactions -- USD prices on mainnet, read from Chainlink price feed contracts +- no swap screen; +- no portfolio view; +- no NFT gallery; +- no advertising; +- no tracking. -Two more services are sent none of the user's data until the user ticks a -checkbox for each. Welcome and Settings both offer the two checkboxes: +## Why it exists -- "Use Blockscout": a Blockscout block-explorer API, used for: - - transaction history - - the tokens the user has not tracked - - transaction details -- "Check recipients on Etherscan": a best-effort Etherscan label lookup of the - recipient during transaction confirmation +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. -A community-maintained phishing domain blocklist is built into the extension at -build time and checked locally. Nothing is fetched for it at runtime. +## What it talks to -In the extension is a hardcoded list of the top ERC20 contract addresses. You -can add any ERC20 contract by contract address if you wish, but the hardcoded -list exists to detect symbol spoofing attacks and improve UX. +The extension runs entirely in the browser. No server is run by the developer. -## Getting Started +By default the extension contacts one service: the Ethereum RPC endpoint set in +Settings. The wallet uses that endpoint to: + +- read balances; +- look up ENS names; +- estimate fees; +- broadcast transactions; +- check receipts; +- on Ethereum Mainnet, read USD prices from Chainlink price feed contracts. + +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. + +- "Use Blockscout" sends the user's addresses, and the hash of each transaction + the user opens, to the Blockscout API set in Settings. In return the wallet + shows transaction history, transaction details and the tokens the user has not + 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. + +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. + +## Getting started + +Build the extension from a clone: ```bash git clone https://git.eeqj.de/sneak/autistmask.git @@ -49,26 +68,70 @@ make setup make build ``` -`make setup` is the entrypoint for a fresh clone: it installs dependencies from -the lockfile and installs the git pre-commit hook. +`make setup` installs the dependencies from the lockfile and installs the git +pre-commit hook. `make build` writes the extension into `dist/chrome/` and +`dist/firefox/`. -Load the extension: +Load it: -- **Chrome**: Navigate to `chrome://extensions/`, enable "Developer mode", click - "Load unpacked", and select the `dist/chrome/` directory. -- **Firefox**: Navigate to `about:debugging#/runtime/this-firefox`, click "Load - Temporary Add-on", and select `dist/firefox/manifest.json`. Read - [Installing on Firefox](#installing-on-firefox) before relying on this: a - temporary add-on does not survive closing the browser. +- Chrome: open `chrome://extensions/`, turn on "Developer mode", click "Load + unpacked" and pick `dist/chrome/`. +- Firefox: open `about:debugging#/runtime/this-firefox`, click "Load Temporary + 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. -### Release Artifacts +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). -`make package` runs `make build` and then writes one self-contained, versioned -archive per browser into `release/`, plus a `SHA256SUMS` for them: +### Installing on Chrome -```bash -make package -``` +`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 +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. + +### Installing on Firefox + +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`. + +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 +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. + +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. + +AutistMask does not run in Firefox private windows. `manifest/firefox.json` sets +`"incognito": "not_allowed"`. If Firefox is set to never remember history, every +window is private, and the extension does not run at all. + +### Release archives + +`make package` runs `make build` and then writes three files into `release/`: ``` release/autistmask-chrome-.zip @@ -76,2921 +139,1274 @@ release/autistmask-firefox-.xpi release/SHA256SUMS ``` -Nothing is published by this. Tagging, CRX packing and any upload are -outward-facing acts and are the owner's alone. The full procedure that turns a -green `main` into a tagged, packaged release — the order of steps, who performs -each, and how to check it worked — is in [docs/RELEASE.md](docs/RELEASE.md). - -The archives are deterministic — entries sorted, timestamps fixed, compression -level fixed — so two builds of one commit produce byte-identical files and the -recorded digest is a property of the input rather than of the clock. - -**Self-containment is checked, not assumed.** `build.js` writes the compiled -Tailwind output to `dist/styles.css` at the `dist/` ROOT, outside both browser -directories, and copies it into each of them as `src/popup/styles.css`; a naive -`zip -r dist/chrome` is therefore correct only by accident. So the packager -resolves every path referenced by the manifest and by every HTML document in the -archive, requires each to be inside the archive, and fails on any reference that -climbs out of the extension root. Files left at the `dist/` root are printed as -deliberately not shipped rather than dropped by a glob. The archive is then read -back off disk and compared member by member against the directory it was built -from: an archive nobody opened is a claim, not an artifact. - -There is one version, and the build enforces it. `package.json`, -`manifest/chrome.json` and `manifest/firefox.json` each declare one and none is -derived from another — the manifests are copied to `dist/` verbatim, which is -what `tests/manifest.test.js` asserts — so `script/lib/version.js` requires all -three to agree and **fails the build when they do not**, naming each file and -what it said. `tests/version.test.js` covers that rule in `make check`. - -### Installing on Chrome - -`manifest/chrome.json` carries a fixed `key`: the public half of an RSA keypair, -base64-encoded DER. It exists for one reason. An unpacked Chrome extension with -no `key` gets an extension id derived from the **absolute path it was loaded -from**, and `chrome.storage.local` — which is where the wallet lives — is -partitioned by that id. Move the checkout, re-clone it, or load a second copy -from anywhere else, and the extension comes up on a fresh, empty storage -partition: the wallet is simply gone, with no error and nothing in the UI to say -so. With the `key` in place the id is derived from the key instead, and follows -the extension wherever it is loaded from. That id is -`gipbhkogfopeahplcjhipkgpcimdpkip`, pinned in `tests/extensionId.test.js` and -observed against a real Chrome in `tests/e2e/storagePartition.js`. - -**Changing `key` changes the extension id, and orphans every wallet stored under -the old one.** It is a migration, not an edit. - -The **private** half is a credential. It is not in this repository, no target -generates one into the working tree, `*.pem` and `*.key` are gitignored, and -`tests/extensionId.test.js` fails if such a file is ever committed. It is not -needed to build, load or test anything here — it signs a CRX, and this repo does -not pack one. A packer would take it from outside the repo, e.g. -`chrome --pack-extension=dist/chrome --pack-extension-key=`. - -### Installing on Firefox - -**The XPI this repo produces is UNSIGNED, and release Firefox and Firefox ESR -will refuse to install it.** Those builds enforce add-on signing with no working -override — `xpinstall.signatures.required` does nothing on them — so a permanent -install needs Firefox Developer Edition, Nightly, or an Unbranded build, with -`xpinstall.signatures.required` set to `false` in `about:config`. - -Signing means submitting to AMO (self-distribution is enough, and does not -require listing), which needs credentials this repository does not have and is -the owner's decision. - -The other route is `about:debugging#/runtime/this-firefox` -> "Load Temporary -Add-on", which works on every Firefox including release. **A temporary add-on is -unloaded when Firefox exits**, so daily use means re-adding it by hand on every -browser start. - -**The wallet survives the restart.** `manifest/firefox.json` declares a fixed -`browser_specific_settings.gecko.id`, and Firefox keys the extension's storage -area on that id rather than on the install, so adding the temporary add-on again -in the same profile finds the vault where it left it. That is asserted, not -assumed: `tests/e2e/firefox/reinstall.js` installs the packaged XPI in a real -Firefox, creates a wallet through the UI, quits the browser, starts it again on -the same profile, adds the add-on again, and decrypts the vault with the -original password back to the original recovery phrase. The `moz-extension://` -origin the popup is served from is _not_ stable across installs and does not -need to be — nothing durable is keyed on it. - -**Removing the add-on does not.** An explicit uninstall — about:addons "Remove" -— destroys the extension's storage, and the vault with it. That is ordinary, -correct browser behaviour and it is observed in the same suite, but for a wallet -it is worth saying out loud: **on Firefox, Remove is irreversible, and the -recovery phrase is the only way back.** - -### Debug Builds - -`make build` never hands back a debug build. `make build-debug` is the only -target that produces one: - -```bash -make build-debug -``` - -`AUTISTMASK_DEBUG=1` still selects the debug compile, and only the exact value -`1` does; any other value (including unset, empty, or `true`) yields a release -build, so a typo cannot accidentally ship the debug behavior. But it is the -compiler's input, not the verifier's: if it happens to be exported in the shell -that runs `make build`, that target compiles a debug bundle and then **fails**, -because it tells `script/verify-build` in so many words that it was supposed to -produce a release build. It used to be that the verifier read the same variable -out of its own environment, agreed with itself, and reported a debug artifact as -verified. The build prints which mode it used. A release build that fails also -**removes `dist/`**, and says so: the bundle it had already written is loadable, -and a loud failure is no protection against someone loading `dist/chrome/` -anyway. `make build-debug` keeps its `dist/` on failure — that output is not -mistakable for a release build, and it is the evidence of what went wrong. See -the [DEBUG Mode Policy](#debug-mode-policy) for what the flag changes. **Never -distribute a debug build** — every wallet it creates gets the same publicly -known test recovery phrase. - -Both targets end by running `script/verify-build`, which reads the compiled -`DEBUG` state back out of the emitted bundles and fails the build if it is not -the one that was asked for. The test suite cannot check this: it loads -`src/shared/constants.js` outside a bundle, so it only ever sees the fallback -value. The assertion is on the artifacts because that is where the property -lives. - -### Build Receipts - -`build.js` records every file it emits — path, sha256, and whether the file is -one of the bundles containing `src/shared/constants.js` — into a build receipt, -and `script/verify-build` checks `dist/` against that receipt: every recorded -file present with exactly the recorded bytes, every audited bundle carrying the -requested `DEBUG` marker, and no regular file or symlink under `dist/` that the -build did not write. The `Makefile` creates the receipt path with `mktemp` per -invocation, outside the repo, and deletes it afterwards. - -That is what ties the check to a build rather than to a directory. What it -establishes is narrow and worth stating exactly: `dist/` is byte for byte the -output of the `build.js` run that just finished, with no regular file or symlink -added, removed or altered in between. Regular files and symlinks are the whole -of what the tree walk covers; fifos, sockets, device nodes and empty directories -under `dist/` are not checked, because a build emits none of them, none can -carry a shippable payload, and `grep` on a fifo would hang rather than fail. It -establishes nothing about whether the source tree or `build.js` were honest, and -it offers nothing to someone handed a `dist/` from elsewhere — without the -receipt from its own build there is no input to the check. Verifiable provenance -for a third party is signing, which this is not. - -There is deliberately no target that re-verifies an existing `dist/` on its own. -The list of files to check has to come from the build that produced them; read -back out of `dist/`, it is the artifact vouching for itself, which is how a -26-byte file containing only the marker string, a hostile content script, and an -entire hand-written `dist/` all used to verify green. - -## Entrypoints - -This repository adheres to the -[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all) -standard: normalized scripts in `script/` are the entrypoints for the -development workflow, and the Makefile targets are thin shims that call them. We -provide: - -- `script/bootstrap` — install all dependencies (pinned node via nvm if needed, - yarn via corepack, `yarn install --frozen-lockfile`), then fail, naming the - package, if node cannot find a package listed in `dependencies` or - `devDependencies` of `package.json` -- `script/setup` — make a fresh clone ready for development: bootstrap plus the - git pre-commit hook -- `script/projectname` — print the project name (used for the Docker image tag) -- `script/test` — build the Dockerfile's `test` phase, uncached: the jest suite, - stopped after 90 seconds and rerun verbose if it fails, then - `script/test-verify-build` -- `script/test-e2e` — run the Chrome browser end-to-end suite (docker is the - only prerequisite: it builds a pinned image that carries the repo and a fresh - extension build, see [End-to-End Tests](#end-to-end-tests)) -- `script/test-e2e-firefox` — run the Firefox browser end-to-end suite (same, - against an image with a pinned Firefox and geckodriver, see - [End-to-End Tests](#end-to-end-tests)) -- `script/lint` — build the Dockerfile's `lint` phase, uncached: ESLint - (`eslint.config.js`), `prettier --check`, then `script/check-censored`, - failing on any of them. It never writes: `--fix` is not in this path, so - `make check` stays non-mutating. Linting runs only in the container, because - an ESLint result that depends on whichever ESLint the host happens to have is - not a result, so docker is required to lint. -- `script/fmt` — format all files (writes), on the host, with the `prettier` - that `script/bootstrap` installs in `node_modules/`; when that one is missing - it fails and says to run `script/bootstrap`, instead of using a `prettier` - from the `PATH` -- `script/fmt-check` — check formatting (read-only), on the host, with the same - `prettier` and the same refusal -- `script/check` — run `script/test`, `script/lint` and `script/fmt-check` -- `script/check-censored` — assert the competitor name RULES.md bars appears - nowhere in the working tree or under `dist/` outside its documented - exceptions: the pinned source reference in `script/vendor-blocklist`, the two - provider-shim identifiers in `src/content/inpage.js`, and one ERC-20's - on-chain name in `src/shared/tokenList.js` and in - `script/lib/token-enrichment.json`, the fetched data that file's names are - written from. Each is scoped to the paths named and fails anywhere else. Run - by the `lint` phase, so part of `make check`; it inspects `dist/` when there - is one and says loudly when there is not, and the build context of that phase - never has one. `make build` re-runs it with `--require-dist`, so a build - artifact is always covered -- `script/package` — produce the release artifacts: `make build` first, so the - archives can only ever be made from a `dist/` that has been verified against - that build's own receipt as a RELEASE build, then one self-contained, - versioned archive per browser into `release/` (see - [Release Artifacts](#release-artifacts)). It packages and does not publish -- `script/vendor-blocklist` — refresh `src/shared/phishingBlocklist.json` from - its upstream, pinned to a commit and to the sha256 of the bytes that commit - serves. Run deliberately, never as part of a build: the output is committed - and there is no runtime fetch, so the shipped list is as fresh as the last - vendoring run that was released -- `script/verify-build --expect release|debug --receipt PATH` — assert that the - regular files and symlinks under `dist/` are exactly what the build that just - ran emitted (other file types are out of scope), and that the compiled `DEBUG` - state of the bundles in it is the one that was asked for. Both arguments are - required and neither has a default: the expected mode is stated by the caller - rather than read from `AUTISTMASK_DEBUG`, and the file list comes from the - build's receipt rather than from `dist/` (see - [Build Receipts](#build-receipts)). Run automatically at the end of - `make build` and `make build-debug`; fails loudly rather than passing whenever - it cannot determine something. Not part of `make check`, which does not depend - on build artifacts existing. -- `script/discard-dist-on-failure COMMAND [ARG...]` — run one step of the - **release** build and, if it fails, remove `dist/` before returning that - step's exit status, saying on stderr that it did and why. Every step of - `make build` runs through it; `make build-debug` runs none of them through it. - A step that succeeds removes nothing, and a removal that cannot be completed - is reported as loudly as one that was. -- `script/test-verify-build` — exercise every failure mode of - `script/verify-build` against a fixture tree in a temp dir, asserting the exit - status and the message of each, assert the state of `dist/` on disk after a - failing and a succeeding release build step, and read the `make build` and - `make build-debug` recipes back out of `make -n` to check that they pass the - mode as an argument on a scrubbed environment and wrap only the release path. - Run by the `test` phase, so part of `make check`; it reads no build artifacts - and writes nothing under `dist/`. The cases that depend on file permissions - cannot mean anything for a process that is not subject to them, so the harness - proves its runner against a mode-000 file before counting them, dropping to an - unprivileged user when run as root; if it cannot, it skips those cases and - says so in a banner rather than passing them. -- `script/docker` — build the Docker image, uncached and tagged via - `script/projectname`: the `lint` and `test` phases, then `make build` in the - last stage. It passes what `git describe --tags --always --dirty` prints on - the host as the `VERSION` build argument, or `unknown` when that prints - nothing, and the last stage fails on `unknown` whenever the build context - carries `.git`. Only a build given no `VERSION` at all, such as a plain - `docker build .`, runs `git describe` on the `.git` in the build context, - which `.dockerignore` sends without its `config`, and fails if git cannot read - it -- `script/cibuild` — CI entrypoint: `script/bootstrap`, `script/check`, then the - same image build as `script/docker` -- `script/precommit` — run by the git pre-commit hook; runs `script/check` -- `script/install-precommit` — install the git pre-commit hook - -The Makefile shims to those. It also carries a few targets that have no -`script/` counterpart and are Makefile-only conveniences: - -- `make install` — `yarn install --frozen-lockfile` on its own, without the rest - of `script/bootstrap`. Frozen so a stale `yarn.lock` fails instead of being - silently rewritten. Use `make setup` for a fresh clone. -- `make hooks` — shims to `script/install-precommit` -- `make build` — build the extension into `dist/chrome/` and `dist/firefox/`, - then verify the result against the build's receipt as a release build. A - failure at any step removes `dist/` -- `make build-debug` — the same build with `AUTISTMASK_DEBUG=1`, verified as a - debug build, and keeping its `dist/` on failure (see - [Debug Builds](#debug-builds)) -- `make icons` — redraw the toolbar icons in `icons/` from - `script/lib/icons.js`, at every size `manifest/chrome.json` declares. A file - that already holds the drawn image, the same IHDR and every pixel, is left - untouched. Commit the files with the drawing: `make check` fails while their - image differs from what it draws -- `make enrich-tokens` — fetch the name of each token in - `src/shared/tokenList.js` from CoinGecko's coin list and its website from the - `ethereum-lists/tokens` repository, at a pinned commit, into - `script/lib/token-enrichment.json`, by running `script/lib/enrich-tokens.js`. - CoinGecko's list is live and has no fixed version, so a later run can return - different names -- `make apply-enrichment` — write the names and URLs in - `script/lib/token-enrichment.json` into `src/shared/tokenList.js`, by running - `script/lib/apply-enrichment.js`, taking a URL from the table of project - websites in that script only where the fetched data has none. Commit the list - with the data file: `make check` fails while the list differs from what this - writes. Neither target changes which tokens are on the list, and no build runs - either -- `make clean` — remove `dist/` and `release/` -- `make dev` — run the build `make build` runs, without the checks that follow - it, then run it again after every change to a file under `src/`, `manifest/` - or `icons/`, until interrupted. A failed build is reported, may leave `dist/` - partly written, and watching carries on. It writes no build receipt, so - nothing can verify what it leaves in `dist/` (see - [Build Receipts](#build-receipts)): anything handed on comes from - `make build`. A release build unless `AUTISTMASK_DEBUG=1` is exported. A - change anywhere else, `package.json` and `build.js` included, starts no build, - and a directory created while it runs is not watched; restart it after either - -## End-to-End Tests - -There are two suites, one per browser, and they share no code. Chrome runs on -Playwright; Firefox has its own WebDriver client, because Playwright cannot -observe errors on a Firefox extension page at all — see -[Firefox](#firefox-make-test-e2e-firefox) below. Both require docker, and both -are outside `make check`. Neither opens the popup from the toolbar button: both -load its page in an ordinary tab, so what the toolbar popup itself adds, its -size and its closing when it loses focus, is covered by neither. - -Each run builds its image under a tag of its own, `autistmask-e2e-chrome:` or -`autistmask-e2e-firefox:` followed by twelve random hex digits, so two clones -can run the same suite at once without one build taking the other's image away. -The run removes that image, and its container if one is still running, after the -suite passes or fails, on Ctrl-C, on `kill`, and when the terminal or SSH -session it runs in closes. A run killed outright (`kill -9`) cannot clean up: -its image stays under its tag until removed by hand with `docker image rm`. - -### Chrome (`make test-e2e`) - -`make test-e2e` builds `dist/chrome/` and drives the **real popup in a real -Chrome**, loaded as an unpacked MV3 extension inside a pinned -`mcr.microsoft.com/playwright` container (pinned by digest in -`tests/e2e/Dockerfile`, which is also where the extension is built; docker is -required and the suite fails loudly rather than skipping if it is unavailable). -The suite lives in `tests/e2e/` and is driven by `playwright-core`, whose -version must stay matched to the container's Playwright version — the browsers -ship inside the image. - -It covers popup load, WebAssembly compilation under the shipped CSP (see -[Content Security Policy](#content-security-policy)), wallet creation through -the UI, the Add Token screen, the transaction detail screen for an ERC-20 -transfer, and the recovery phrase screen — which wallet types are offered it, -that it holds nothing before the password is accepted, that a wrong password -reveals nothing, that leaving it by either route wipes it — including a leave -taken while the decrypt is still running — and that reopening the popup does not -land on it. It also covers address removal: which wallets offer the control at -all, that the confirmation states the route back rather than showing an empty -paragraph, that leaving the confirmation removes nothing, and that confirming it -does. All outbound network is intercepted at the browser level and served from -fixtures in `tests/e2e/network.js`, so the run is deterministic and fully -offline; unrecognised outbound requests are reported as failures rather than -silently allowed. - -It also covers the StateRecovery screen, under the shipped CSP: a stored record -this build cannot read opens the popup on it, its export text box holds that -record exactly as stored, a near-miss confirmation phrase erases nothing, and -the exact one erases the record and reloads into Welcome. - -It also covers the **Settings screen**, which holds the densest run of element -id lookups in the codebase and where one wrong id leaves the whole popup blank -rather than only degrading Settings: that the screen renders populated — the -About well and the wallet list are read back, so a `show()` that stopped early -is caught rather than merely a view that failed to appear — that the four Token -Spam Protection controls are real checkboxes defaulted on, and that the theme -and network selectors offer exactly the choices `src/shared/networks.js` and -`index.html` define. What the selectors persist is then driven through the UI to -`dark` and `sepolia` and reasserted after a popup reopen before being restored -the same way; neither value is its selector's first `