# 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. 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. Hence, a minimally viable ERC20 browser wallet/signer that works cross-platform. Everything you need, nothing you don't. We import as few libraries as possible, don't implement any crypto, and don't send user-specific data anywhere but a (user-configurable) Ethereum RPC endpoint (which defaults to a public node). The extension contacts three user-configurable services: the configured RPC node for blockchain interactions, a public CoinDesk API (no API key) for realtime price information, and a Blockscout block-explorer API for transaction history and token balances. It also fetches a community-maintained phishing domain blocklist periodically and performs best-effort Etherscan address label lookups during transaction confirmation. 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. ## Getting Started ```bash git clone https://git.eeqj.de/sneak/autistmask.git cd autistmask 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. Load the extension: - **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`. ### Debug Builds `make build` always produces a release build: the build-time `DEBUG` constant is `false`, so wallet creation uses real entropy and the red banner is off. To produce a debug build instead, set `AUTISTMASK_DEBUG=1` in the environment: ```bash make build-debug # or: AUTISTMASK_DEBUG=1 make build ``` Only the exact value `1` enables it; any other value (including unset, empty, or `true`) yields a release build, so a typo cannot accidentally ship the debug behavior. The build prints which mode it used. 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 builds 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. ## 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`) - `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` — run the test suite (jest) - `script/test-e2e` — run the browser end-to-end suite (docker required; see [End-to-End Tests](#end-to-end-tests)) - `script/lint` — run the linter - `script/fmt` — format all files (writes) - `script/fmt-check` — check formatting (read-only) - `script/check` — run test, lint, and fmt-check - `script/verify-build` — assert the compiled `DEBUG` state of the bundles in `dist/`: every bundle containing `src/shared/constants.js` must have `DEBUG` off, or on when `AUTISTMASK_DEBUG=1`. Run automatically at the end of `make build` and `make build-debug`; fails loudly rather than passing if it cannot determine a bundle's state. Not part of `make check`, which does not depend on build artifacts existing. - `script/docker` — build the Docker image tagged via `script/projectname` - `script/cibuild` — CI entrypoint: plain `docker build .` - `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/` - `make build-debug` — the same build with `AUTISTMASK_DEBUG=1` (see [Debug Builds](#debug-builds)) - `make clean` — remove `dist/` - `make dev` — build in watch mode ## End-to-End Tests `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 `script/test-e2e`; 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. 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. That reporting has one bound worth knowing. Observation ends when the browser context is torn down, and nothing can watch traffic after that, so the run keeps collecting for a fixed grace period after the last test returns (`TRAILING_WATCH_MS` in `tests/e2e/run.js`, currently 1500ms) and then closes the context. A request whose _first_ dispatch falls after that window is never seen at all and cannot fail the run. In practice a request a test fires without awaiting reaches the route handler about 10ms later, and anything on a repeating timer gets observed on an earlier tick during the ~20s suite — but a one-shot call deliberately deferred past the window will escape. That interception covers the MV3 background service worker as well as the popup page, which it does not by default — `script/test-e2e` sets `PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1` for it. Because that flag is experimental, the harness does not take it on trust. At launch it waits for the background worker's **own** startup request — the phishing blocklist fetch that `src/background/index.js` issues on startup, which on the suite's throwaway profile always happens because no previous fetch timestamp is persisted — to arrive in the route handler, and aborts the entire suite if none does within 30 seconds (`tests/e2e/harness.js`). The check is passive on purpose: a synthetic probe fetched from inside the worker via `worker.evaluate()` was tried first and rejected, because evaluating in an extension service worker that early kills the worker outright, destroying the thing being measured. Observing traffic the extension already generates perturbs nothing. Losing the race fails closed — the suite refuses to run rather than passing quietly. As defence in depth, Chrome is also started with `--host-resolver-rules=MAP * ~NOTFOUND`, so a request that ever did slip past the route handler could not resolve a host at all. That only bounds the damage; detecting escaping traffic remains the canary's job. To see what is actually being intercepted, run with `E2E_TRACE_NETWORK=1` and every routed request is printed, tagged `[sw]` or `[page]`. **Any uncaught page error or `console.error` fails the run.** That is the point: a `ReferenceError` from a used-but-not-imported identifier is invisible to `make check` (`script/lint` is only `prettier --check`) but fatal in a browser, and this suite exists because exactly that class of bug shipped twice. `make test-e2e` is deliberately **not** part of `make check` or `make test`. `REPO_POLICIES.md` caps `make test` at 20 seconds and a browser suite does not fit; nothing in `tests/e2e/` is named `*.test.js`, so jest cannot pick it up either. It is also not wired into the Gitea workflow yet — docker-in-docker in CI is a separate question. Run it locally before changing anything under `src/popup/views/`. ## Rationale Common popular EVM wallets have become bloated with swap UIs, portfolio dashboards, analytics, tracking, and advertisements. It is no longer a simple wallet. Most alternatives only support Chromium browsers, leaving Firefox users without a usable option. AutistMask exists to provide the absolute minimum viable Ethereum wallet experience: manage seed phrases, derive HD addresses, send and receive ETH and ERC-20 tokens, and connect to web3 sites. Nothing else. No swaps (that's what the web is for), no analytics, no tracking, no ads, no portfolio views, no NFT galleries. Just a wallet. ## Design AutistMask is a browser extension targeting both Chrome (Manifest V3) and Firefox (Manifest V2/V3 as supported). The codebase is shared between both targets with platform-specific manifest files and a build step that produces separate output directories. ### Architecture ``` src/ background/ — service worker / background script index.js — RPC routing, approval flows, message signing content/ — content script injected into web pages index.js — relay between inpage provider and background inpage.js — the window.ethereum provider object (EIP-1193) popup/ — popup UI (the main wallet interface) index.html index.js — entry point, view routing, state restore styles/main.css — Tailwind source views/ — one JS module per screen (home, send, approval, etc.) shared/ — modules used by both popup and background alarms.js — recurring background jobs (extension alarms API) balances.js — ETH + ERC-20 balance fetching via RPC + Blockscout constants.js — chain IDs, default RPC endpoint, ERC-20 ABI ens.js — ENS forward/reverse resolution (popup only) prices.js — ETH/USD and token/USD via CoinDesk API scamlist.js — known fraud contract addresses state.js — persisted state (extension storage) tokenList.js — top ERC-20 tokens by market cap (hardcoded) transactions.js — tx history fetching + anti-poisoning filters uniswap.js — Uniswap Universal Router calldata decoder vault.js — password-based encryption via libsodium wallet.js — mnemonic generation, HD derivation, signing manifest/ chrome.json — Manifest V3 for Chrome firefox.json — Manifest V2 for Firefox ``` ### Background scheduling Chrome runs `src/background/index.js` as a Manifest V3 service worker, which the browser terminates after roughly 30 seconds idle and re-evaluates from scratch on the next event. Two consequences shape every recurring job in the background: - `setInterval` and `setTimeout` are useless. They are destroyed with the worker, so a job scheduled that way runs until the first idle period and never again. Both recurring jobs — the 60-second balance refresh and the 24-hour phishing blocklist refresh — are scheduled through the extension alarms API (`src/shared/alarms.js`) instead. The browser holds the schedule and wakes the worker to deliver it. Alarm periods are clamped to a one-minute minimum, so the balance refresh is expressed as exactly one minute and nothing is silently slowed down. - Module-level variables do not survive either. Anything that must be remembered across a restart goes in extension storage, including the timestamp of the last phishing list fetch: without it a revived worker would either re-fetch on every wake or, with a naive in-memory guard, never notice that an update is due. `localStorage` does not exist in a service worker at all — the one remaining user of it, `src/shared/ens.js`, runs only in the popup and is marked as such. Both jobs also carry a freshness guard, and a guard must never be timed to the alarm period it gates. Each guard is measured from the moment the last run finished, which is one run-duration after the alarm that started it, so a guard of exactly one period vetoes the very next tick and the real cadence becomes two periods. The two jobs solve this differently, because their guards exist for different reasons: - The phishing refresh has a 24-hour cache TTL whose job is to keep the worker off the network on the wakes between scheduled refreshes — Chrome revives the worker every ~30 seconds while the browser is busy, and every revival runs the startup path. The scheduled alarm tick is not one of those wakes, so it bypasses the TTL and fetches unconditionally. Shortening the TTL instead would not work: the startup path re-checks it on every wake, so a shorter TTL simply becomes the real refresh rate. - The balance refresh guard exists to skip work an open popup has already done — the popup refreshes every 10 seconds and stamps the same field. That has to keep applying on the scheduled tick, so the guard is shortened to half the alarm period instead of bypassed: comfortably above the popup's 10 seconds, so an open popup still suppresses the background job, and comfortably below the 60-second period, so the schedule always wins. Two timestamps are persisted for the phishing list, not one. `lastFetchTime` records a fetch that produced a usable delta and drives the TTL. `lastAttemptTime` records that the network was contacted at all, and is written even when the result is unusable — a failed request, or a delta over the 256 KiB cap. Without it those cases leave no freshness mark and the worker re-downloads the full blocklist on every wake, indefinitely; with it, unscheduled retries are floored at one hour. Both are discarded on load if they are in the future, since a stamp from a skewed clock or a restored backup would otherwise suppress updates until that time arrives, permanently and with no way out. The startup path (`ensureRecurringAlarms()` plus the phishing list init) runs on `onInstalled`, on `onStartup`, and at the top level of the worker, so every way the background context can start re-establishes the schedule. On a fresh install more than one of those fires, so they share a single in-flight run rather than racing. It is idempotent: an alarm that already exists with the period the code asks for is left alone, because re-creating one restarts its schedule and a busy extension would push the next fire out indefinitely. An alarm carrying a different period — one created by an earlier version — is re-created once, or a period changed in a new release would never reach an existing install. Firefox uses Manifest V2 with a persistent background page, where timers would survive. Both browsers are built from one bundle and both take the alarm path, so there is a single code path to reason about; `"alarms"` is declared in both `manifest/chrome.json` and `manifest/firefox.json`. ### UI Design Philosophy The UI is inspired by _Universal Paperclips_. It's deliberately minimal, monochrome, fast, and includes once-popular usability affordances that seem to have fallen out of fashion in modern UI design. Clickable things look clickable. Things don't flash or spin or move around unnecessarily. This is a tool for getting work done, not a toy. This is designed for a normal audience. Basic familiarity with cryptocurrency terms is required, but you need not be a programmer or software engineer to use this wallet. If you _are_ basically familiar with cryptocurrency terms, you should be able to use all of the main features of this wallet without having to read the documentation; i.e. we wish for the primary functionality to remain easily discoverable. #### Visual Style - **Monochrome**: Black text on white background. Color is only used when and where it is semantically meaningful and explicitly useful, such as error messages, critical warnings, or address disambiguation. (Notable exception: we use color dots, and identicons, to help a user easily distinguish addresses.) - **Text-first**: Every piece of information is presented as text. Balances are numbers. Addresses are hex strings. Flash messages are sentences. All fiddly bits can be clicked to copy to the clipboard, and external links to Etherscan are provided everywhere they might be useful. - **Monospace font**: All text is rendered in the system monospace font. Ethereum addresses, transaction hashes, and balances are inherently fixed-width data. Rather than mixing proportional and monospace fonts, we use monospace everywhere for visual consistency and alignment. - **No images**: Zero image assets in the entire extension. No logos, no illustrations, no token icons. Token identity is conveyed by symbol text (ETH, USDC, etc.). We display [Blockie identicons](https://github.com/MyCryptoHQ/ethereum-blockies-base64) on critical screens and when space is available to allow users to disambiguate addresses visually, as a security feature. - **Tailwind CSS**: Utility-first CSS via Tailwind. No custom CSS classes for styling. Tailwind is configured with a minimal monochrome palette. This keeps the styling co-located with the markup and eliminates CSS file management. - **Vanilla JS**: No framework (React, Vue, Svelte, etc.). The popup UI is small enough that vanilla JS with simple view switching is sufficient. A framework would add bundle size, build complexity, and attack surface for no benefit at this scale. - **360x600 popup**: Standard browser extension popup dimensions. The UI is designed for this fixed viewport. #### No Layout Shift Asynchronous state changes (clipboard confirmation, transaction status, error messages, flash notifications, API results returning) must _never_ move around the existing UI elements. All dynamic content areas must reserve their space up front using `min-height` or always-present wrapper elements. `visibility: hidden` is preferred over `display: none` when the element's space must be preserved. This prevents jarring content jumps that disorient users and can cause dangerous mis-clicks. Anyone who has multi-tabled on ClubGG and smashed the big red "all-in blind preflop" button when trying to simply "call" on a different table knows exactly tf I am talking about. #### Clickable Affordance Every interactive element must visually indicate that it is clickable. Buttons use a visible border, padding, and a hover state (invert to white-on-black). Text that triggers an action (e.g. "Add additional wallet...") uses an underline. No invisible hit targets, no bare text that happens to have a click handler. If it does something when you click it, it must look like it does something when you click it. #### Display Consistency The same data must be formatted identically everywhere it appears. Token and ETH amounts are displayed with exactly 4 decimal places (e.g. "1.0500 ETH", "17.1900 USDT") in balance lists, transaction lists, send confirmations, and approval screens. Timestamps include both an ISO datetime and a humanized relative age wherever shown. If a formatting rule applies in one place, it applies in every place. Users should never see the same value rendered differently on two screens. **Specific Exception — Truncation:** On some non-critical display locations, we may truncate _a small number_ of characters from the middle of an address solely due to display size constraints. Wherever possible, and, notably, **in all critical contexts (transaction confirmation view before signing, transaction history detail view) addresses will _NEVER_ be truncated**. Even in places we truncate addresses, we truncate only a maximum of 10 characters, which means that the portions still displayed will be more than adequate for the user to verify addresses even in the case of address spoofing attacks. Clicking an address will always copy the full, untruncated value. **Specific Exception — Transaction Detail view:** The transaction detail screen is the authoritative record of a specific transaction and shows the exact, untruncated amount with all meaningful decimal places (e.g. "0.00498824598498216 ETH"). It also shows the native quantity (e.g. "4988245984982160 wei") below it. Both are click-copyable. Truncating to 4 decimals in summary views is acceptable for scannability, but the detail view must never discard precision — it is the one place the user can always use to verify exact details. #### Language & Labeling All user-facing text avoids unnecessary jargon wherever possible: - "Recovery phrase" instead of "seed phrase", "mnemonic", or "BIP-39 mnemonic" - "Address" instead of "account", "derived key", or "HD child" - "Password" instead of "encryption key" or "vault passphrase" - Buttons use plain verbs: "Send", "Receive", "Copy address", "Add", "Back", "Cancel", "Lock", "Unlock", "Allow", "Deny" - Helpful inline descriptions where needed (e.g. "This password locks the wallet on this device. It is not the same as your recovery phrase.") - Error messages are full sentences ("Please enter your password." not "password required") #### Full Identifiers Policy Addresses, transaction hashes, contract addresses, and all other cryptographic identifiers are displayed in full whenever possible. We truncate only in specific, limited, non-critical places and even then only a small amount that still prevents spoofing attacks. Address poisoning attacks exploit truncated displays by generating fraud addresses that share the same prefix and suffix as a legitimate address. If a user only sees `0xAbCd...1234`, an attacker can create an address with the same visible characters and trick the user into sending funds to it. Showing the complete identifier defeats this class of attack. #### Data Model The core hierarchy is **Wallets → Addresses**: - A **wallet** is one of three types: - An **HD wallet** (`type: "hd"`, recovery phrase): generates multiple addresses from a single 12/24 word recovery phrase using BIP-39/BIP-44 derivation. The user can add more addresses with a "+" button. - A **key wallet** (`type: "key"`, private key): a single address imported directly from a private key. No "+" button since there is only one address. - An **xprv wallet** (`type: "xprv"`, extended private key): the same multi-address behavior as an HD wallet, including the "+" button and the address scan on import, but imported from an extended private key rather than a recovery phrase. It therefore has no recovery phrase to display or back up. Only a master key may be imported; an xprv wallet already in storage that was imported from a non-master key is detected from the depth of its stored `xpub` by `src/shared/walletDefects.js`, explained in the wallet list, and blocked from signing, sending and private-key export. It is never deleted or rewritten. - An **address** holds ETH and ERC-20 tokens. - The user can have multiple wallets, each with multiple addresses (HD) or a single address (key). Which tokens an address shows is decided by `fetchTokenBalances()` in `src/shared/balances.js`, from the Blockscout `token-balances` response, so tokens do appear without the user adding them. An ERC-20 is shown when its balance is nonzero and it is in the bundled known-token list, is tracked by the user, or has 1,000 or more holders; a token claiming a symbol from the bundled list from any other contract address is always dropped. That filter is unconditional — the "Hide tokens with fewer than 1,000 holders" setting governs the transaction history and the send-screen token selector, not this list. Tracked tokens with a zero balance are listed as well while "Show tracked tokens with zero balance" is on. #### Navigation The main view shows all addresses grouped by wallet, with ETH balances inline. The user taps an address to see its detail view (full address, balance, tokens, send/receive). Navigation is a stack: each forward action pushes the current screen, and every view has a "Back" or "Cancel" button that pops back to it (see the Screen Map below). There is no hamburger menu and no persistent tab bar; the Settings gear in the title bar is the only global control. Two screens carry an in-screen control beyond that: AddWallet uses three tabs to select the import mode, and AddressDetail keeps its one rarely-used action ("Export Private Key") behind a "···" menu. ### Screen Map Navigation uses a stack model (like iOS): each forward action pushes the current screen onto `state.viewStack`, and "Back" pops it (`pushCurrentView()` and `goBack()` in `src/popup/views/helpers.js`). The root screen is either Welcome (no wallets) or Home (has wallets). Each screen below gives its view id in parentheses; the registry of view ids is the `VIEWS` array in `src/popup/views/helpers.js`, and the markup for a screen is the element with id `view-` plus that view id in `src/popup/index.html`. Three elements sit outside the screens and are present on all of them: the title bar ("AutistMask by @sneak" plus the Settings gear), the flash message line under it, and the red banner at the very top that appears on a debug build, when runtime debug mode is on, or when the active network is a testnet. They are not repeated in the element lists below. Closing and reopening the popup returns to the screen the user was last on only for the views listed in `RESTORABLE_VIEWS` (`src/popup/restorableViews.js`). Every other screen falls back to Home. The screens that display a secret — ExportPrivKey and ShowRecoveryPhrase — are deliberately absent from that list, so the popup can never reopen onto one of them with no password prompt in front of it. Every screen that holds secret material in the page registers a cleanup with `onViewLeave()` (`src/popup/views/helpers.js`), which `showView()` runs on every exit from that screen rather than only on its "Back" button, so nothing secret survives in a hidden view once the user has navigated away by any route. That covers the revealed private key and recovery phrase, the recovery phrase, private key or extended private key entered on AddWallet, and the password typed on ConfirmTx, DeleteWallet, ApproveTx and ApproveSign. #### Welcome (`welcome`) - **When**: No wallets exist yet (`state.hasWallet` is false). This is the root screen in that case. - **Elements**: - "Welcome! To get started, add a wallet." text - "Add wallet" button - **Transitions**: - "Add wallet" → **AddWallet** #### Home (`main`) - **When**: At least one wallet exists. This is the root screen. - **Elements**: - Active address ETH balance (large) + USD value in parentheses - "Total:" USD value across ETH and every token shown for the active address - Active address (color dot, full address, etherscan link, tap to copy) - Send / Receive quick-action buttons, both acting on the active address - ETH/USD price display - Wallet list: each wallet shows its name (tap to rename inline) and a "+" button for HD and xprv wallets, then one block per address with "Address N" (bold when active), the ENS name if resolved, the full address, an `[info]` button, the address USD total, and a balance line for ETH and for each token shown for that address - "Recent Transactions": up to 25 transactions merged across every address of every wallet, deduplicated by hash and filtered - "Add additional wallet..." link at bottom - **Transitions**: - Tap address row → sets the active address and broadcasts `AUTISTMASK_ACTIVE_CHANGED` (no screen change) - Tap wallet name → inline rename field (no screen change) - "+" on wallet → derives the next address inline (no screen change) - `[info]` on address → **AddressDetail** - "Send" → **Send** (refuses with a flash message on a zero balance) - "Receive" → **Receive** (shows active address QR) - Tap home tx row → **TransactionDetail** - "Add additional wallet..." → **AddWallet** - Settings gear → **Settings** (toggles; tap again to return) #### AddWallet (`add-wallet`) - **When**: User wants to add a new wallet (from Welcome, Home, or Settings). This one screen covers all three import modes; there is no separate import screen. - **Elements**: - "Back" button, "Add Wallet" heading - Three tabs — "From Phrase" (`tab-mnemonic`), "From Key" (`tab-privkey`), "From xprv" (`tab-xprv`) — each showing its own form section: - **From Phrase**: instruction text, a die button that generates a random recovery phrase, a recovery phrase textarea, and a backup warning box that becomes visible once the die button has been used - **From Key**: instruction text and a masked private key input - **From xprv**: instruction text and a masked extended private key input - Password + confirm password inputs, with a hint line whose wording depends on the selected tab - "Import" button - **Transitions**: - "Import" with a valid entry and a matching password of at least 12 characters → creates the wallet, clears the navigation stack, and → **Home**. The phrase and xprv modes then scan for further used addresses and report the count as a flash message. - "Import" with an invalid entry, a duplicate wallet or address, or a short or mismatched password → flash message, no screen change - "Back" → previous screen (Welcome, Home, or Settings) #### AddressDetail (`address`) - **When**: User tapped `[info]` on an address from Home. - **Elements**: - "Back" button - Blockie identicon (48px, centered) - Title: "Wallet Name — Address N" - ENS name (if resolved, bold above the address) - Full address (color dot, etherscan link, tap to copy) - USD total for address - Balance list: ETH + the ERC-20 tokens shown for this address (4 decimal places, USD inline). Each balance row is clickable → **AddressToken** - Send / Receive / + Token buttons and a "···" menu button - "···" dropdown containing a single "Export Private Key" entry - Transaction list (with ENS resolution for counterparties) - **Transitions**: - Tap balance row → **AddressToken** (for that token) - "Send" → **Send** (refuses with a flash message on a zero balance) - "Receive" → **Receive** - "+ Token" → **AddToken** - "···" → "Export Private Key" → **ExportPrivKey** - Tap transaction row → **TransactionDetail** - "Back" → previous screen (Home) #### ExportPrivKey (`export-privkey`) - **When**: User chose "Export Private Key" from the "···" menu on AddressDetail. This screen discloses secret material. - **Elements**: - "Back" button - Blockie identicon (48px, centered) - "Export Private Key" heading - "Wallet Name — Address N" and the full address (etherscan link, tap to copy) - Warning that anyone holding the private key can transfer all funds from the address - Error line - Password input and "Reveal" button, shown until the key is revealed - The private key on a highlighted background, tap to copy, shown only after the password has been accepted - **Transitions**: - "Reveal" (correct password) → decrypts the wallet secret, derives this address's key, hides the password input and shows the key (no screen change) - "Reveal" (wrong password) → full-sentence error on the error line, nothing revealed (no screen change) - "Back" → previous screen (AddressDetail) - **Secret handling**: nothing is decrypted, no key is derived, and nothing is written into the page until the password is accepted; the key is never stored in state, and it is wiped from the page whenever the screen is left by any route, including the Settings gear. A decrypt still running when the screen is left is discarded rather than written. The screen is not restorable, so reopening the popup lands on Home rather than back on the key. #### AddressToken (`address-token`) - **When**: User clicked a specific token balance on AddressDetail. - **Elements**: - "Back" button - Blockie identicon (48px, centered) - Title: "Wallet Name — Address N — TOKEN" - Full address (color dot, etherscan link, tap to copy) - USD total for this token - Single token balance line (4 decimal places) - Send / Receive buttons - Token contract well (ERC-20 only): full contract address (tap to copy, etherscan link) plus name, symbol, decimals, holder count and project website where known - Token-filtered transaction list (only this token's transfers) - **Transitions**: - "Send" → **Send** (token locked: the dropdown is replaced by a static symbol and contract address) - "Receive" → **Receive** (ERC-20 warning shown for non-ETH tokens) - Tap transaction row → **TransactionDetail** - "Back" → previous screen (AddressDetail) #### Send (`send`) - **When**: User wants to send ETH or a token, from Home, AddressDetail, or AddressToken. - **Elements**: - "Back" button, "Send" heading - From: address with color dot + etherscan link - What to send: token dropdown (or static display with contract address when locked from AddressToken) - To: address or ENS name input, with an inline validation message - Amount input with current balance display - "Review" button, disabled until the recipient validates - **Transitions**: - "Review" (valid inputs, ENS resolved) → **ConfirmTx** - "Review" with an unresolvable ENS name or an invalid amount → flash message, no screen change - "Back" → previous screen (Home, AddressDetail, or AddressToken) #### ConfirmTx (`confirm-tx`) - **When**: User reviewed send details and is ready to authorize. - **Elements**: - "Back" button, "Confirm Transaction" heading - Type: "Native ETH transfer" or "ERC-20 token transfer (SYMBOL)" - Token contract: full address + etherscan link (ERC-20 only) - From: blockie + color dot + full address + etherscan link + wallet title - To: blockie + color dot + full address + etherscan link + ENS name - Amount: value + symbol (USD in parentheses) - Your balance: value + symbol (USD in parentheses) - Network fee: "Estimating..." then two lines, or "Unable to estimate", fetched async. The first line is what the transfer is expected to cost, `gasLimit * gasPrice` (USD in parentheses); the second is the `gasLimit * maxFeePerGas` reserve the node requires, which is what the balance check gates on. The second line is omitted on a network with no type-2 pricing, where the two are the same number, but its space is reserved either way - Warnings: inline warnings from the local checks (scam address, self-send) plus four reserved warning boxes made visible by the async checks — recipient with no transaction history, recipient is a contract, burn address, and an Etherscan phishing/scam label - Errors (insufficient balance), plus three reserved error boxes — the amount plus the fee exceeds the balance (ETH transfers), not enough ETH to pay the fee for the transfer (ERC-20 transfers), and the fee could not be estimated. The first two are mutually exclusive per transfer type, so only the applicable one holds space - Password: an inline field on this screen, not a modal, with its own error line - "Sign & Send" button (disabled if errors, and while the network fee estimate is pending or unavailable) - **Transitions**: - "Sign & Send" (correct password) → broadcast tx → **WaitTx** - "Sign & Send" (correct password) → broadcast fails → **ErrorTx** - "Sign & Send" (wrong password) → "Wrong password." on the password error line, no screen change - "Back" → **Send** #### WaitTx (`wait-tx`) - **When**: Transaction has been broadcast, waiting for on-chain confirmation. - **Elements**: - "Transaction Broadcast" heading (no back button — tx is irreversible) - Amount + symbol - To: color dot + full address + etherscan link - Transaction hash: full hash (tap to copy) + etherscan link - Count-up timer: "Waiting for confirmation... Ns" - **Behavior**: Polls `getTransactionReceipt` every 10 seconds. The wait is persisted: closing and reopening the popup resumes the poll, with the elapsed counter and the timeout deadline still measured from the original broadcast. A lookup that fails is retried on the next tick rather than counted as a missing receipt, because a failed lookup says nothing about the transaction; but six failures in a row (60 seconds at the poll cadence) end the wait, so an RPC that never answers cannot leave it running indefinitely. Any lookup that answers resets that count. - **Transitions**: - Receipt found → **SuccessTx** - A lookup that answers "no receipt" 60 seconds or more after broadcast → **ErrorTx** (timeout message) - Six consecutive failed lookups → **ErrorTx**, with a message naming the unreachable network and pointing at the RPC URL in Settings. This is a different fact from the timeout — the chain was never asked — and says so - Exactly one outcome: a receipt found on the tick that crosses the deadline wins, and no outcome can be rendered over another #### SuccessTx (`success-tx`) - **When**: Transaction confirmed on-chain. - **Elements**: - "Transaction Confirmed" heading - Decoded action well (shown when the transaction carried recognized calldata; the top-level Amount and To are hidden in that case) - Amount + symbol - To: color dot + full address + etherscan link - Block number - Transaction hash: full hash (tap to copy) + etherscan link - "Done" button - **Transitions**: - "Done" in the approval popup → closes the popup window - "Done" otherwise → resets the navigation stack, then → **AddressToken** (if `selectedToken` set) or **AddressDetail** #### ErrorTx (`error-tx`) - **When**: Transaction broadcast failed, or timed out waiting for confirmation. - **Elements**: - "Transaction Failed" heading - Amount + symbol - To: color dot + full address + etherscan link - Error message (dashed border box) - Transaction hash section (hidden if broadcast failed before getting hash): full hash (tap to copy) + etherscan link - "Done" button - **Transitions**: - "Done" in the approval popup → closes the popup window - "Done" otherwise → resets the navigation stack, then → **AddressToken** (if `selectedToken` set) or **AddressDetail** #### Receive (`receive`) - **When**: User wants to receive funds at this address, from Home, AddressDetail, or AddressToken. - **Elements**: - "Back" button, "Receive" heading - Instruction text - QR code encoding the address - Full address (color dot, selectable, etherscan link) - "Copy address" button - ERC-20 warning (shown when navigating from AddressToken for non-ETH token) - **Transitions**: - "Back" → previous screen (Home, AddressDetail, or AddressToken) #### TransactionDetail (`transaction`) - **When**: User tapped a transaction row on Home, AddressDetail, or AddressToken. - **Elements** (grouped into logical blocks using light well containers; field labels are self-explanatory so groups have no headings): - "Transaction" heading, "Back" button - Transaction hash: full hash (tap to copy) + etherscan link - Type: transaction classification — one of: Native ETH Transfer, ERC-20 Token Transfer, Swap, Token Approval, Contract Call, Contract Creation - Status: "Success" or "Failed" - From: blockie + color dot + full address (tap to copy) + etherscan link; ENS name if available - To: blockie + color dot + full address (tap to copy) + etherscan link; ENS name if available - Time: ISO datetime + relative age in parentheses - Block: block number (tap to copy) + etherscan block link - Amount: value + symbol (bold) - Native quantity: raw integer + unit (shown when available) - Token contract: shown for ERC-20 transfers — color dot + full contract address (tap to copy) + etherscan token link - Decoded details (shown for contract calls): action name, decoded parameters, token details, swap steps - Network details (shown when on-chain data is available): nonce, gas price, gas used, transaction fee (all tap to copy) - Raw data (shown when calldata is present): full calldata in monospace dashed border - **Transitions**: - "Back" → previous screen (Home, AddressDetail, or AddressToken) #### AddToken (`add-token`) - **When**: User wants to track an ERC-20 token, reached from "+ Token" on AddressDetail. - **Elements**: - "Back" button, "Add Token" heading - Instruction text (find contract address on Etherscan) - Contract address input - Status line ("Looking up token...", cleared or replaced on failure) - Common token quick-pick buttons (top 25 by market cap), which fill the contract address input - "Add" button - **Transitions**: - "Add" (valid contract) → tracks the token, pops the stack, and re-renders **AddressDetail** - "Add" with a token already tracked, a scam-listed address, or a failed contract lookup → flash message, no screen change - "Back" → previous screen (AddressDetail) #### Settings (`settings`) - **When**: User tapped the Settings gear. - **Elements**: - "Back" button, "Settings" heading - Wallets: one row per wallet with its name (tap to rename inline), a `[recovery phrase]` button on HD wallets only, and an `[x]` delete button, plus a "+ Add wallet" button - Tracked Tokens: one row per tracked token with an `[x]` remove button, plus a "+ Add token" button - Display: "Show tracked tokens with zero balance" checkbox, "UTC Timestamps" checkbox, and a Theme selector (System / Light / Dark) - Network: network selector (Ethereum Mainnet / Sepolia Testnet); switching resets the RPC and Blockscout endpoints to that network's defaults - Ethereum RPC: endpoint URL input + "Save" button (validated against `eth_chainId` before being saved) - Blockscout API: endpoint URL input + "Save" button (validated against `/stats` before being saved) - Token Spam Protection: - "Hide fake tokens impersonating a known symbol" checkbox - "Hide tokens with fewer than 1,000 holders" checkbox - "Hide transactions from detected fraud contracts" checkbox - "Hide dust transactions below N gwei" checkbox + threshold input - Allowed Sites: list with remove buttons - Denied Sites: list with remove buttons - About: project link, license, author, version, release date, and the commit, which links to the commit in the repository - Debug: hidden until revealed, then an "Enable debug mode" checkbox that turns on the red banner and verbose logging - **Transitions**: - "+ Add wallet" → **AddWallet** - "+ Add token" → **SettingsAddToken** - `[recovery phrase]` on an HD wallet → **ShowRecoveryPhrase** - `[x]` on a wallet → **DeleteWallet** - Tap wallet name → inline rename field (no screen change) - `[x]` on a tracked token or a site → removes it in place (no screen change) - Ten clicks on the version → reveals the Debug well (no screen change) - "Back" (or Settings gear again) → previous screen (Home) #### ShowRecoveryPhrase (`show-phrase`) - **When**: User tapped `[recovery phrase]` on a wallet row in Settings. HD wallets only: key and xprv wallets have no recovery phrase, so their rows do not offer the action at all. - **Elements**: - "Back" button, "Recovery Phrase" heading - Wallet name - Warning box stating that anyone holding these words can take everything in the wallet, from any device, without the password - Error line - Password input + "Reveal" button, shown until the password is accepted - The recovery phrase itself, in full and click-to-copy, shown only after a correct password and in place of the password prompt - **Transitions**: - "Reveal" (correct password) → the phrase replaces the password prompt (no screen change) - "Reveal" (wrong password) → full-sentence error, nothing revealed (no screen change) - "Back" → previous screen (Settings) - **Secret handling**: nothing is decrypted or written into the page until the password is accepted; the phrase is never stored in state, and it is wiped from the page whenever the screen is left by any route, including the Settings gear. A decrypt still running when the screen is left is discarded rather than written. The screen is not restorable, so reopening the popup lands on Home rather than back on the phrase. #### DeleteWallet (`delete-wallet-confirm`) - **When**: User tapped the `[x]` next to a wallet in Settings. - **Elements**: - "Back" button, "Delete Wallet" heading - Warning naming the wallet and stating that deletion is permanent and any funds are unrecoverable without the recovery phrase - Error line - Password input - "Confirm Delete" button - **Transitions**: - "Confirm Delete" (correct password, other wallets remain) → deletes the wallet and its site permissions, then → **Settings** with a "Wallet deleted." flash message - "Confirm Delete" (correct password, last wallet) → deletes the wallet, clears the selection and the navigation stack, then → **Welcome** - Either way, the active address moves only if it belonged to the deleted wallet, and `AUTISTMASK_ACTIVE_CHANGED` is broadcast when it does (`src/shared/walletDelete.js`) - "Confirm Delete" (wrong password) → "Wrong password." on the error line, nothing deleted - "Back" → previous screen (Settings) #### SettingsAddToken (`settings-addtoken`) - **When**: User tapped "+ Add token" in Settings. Tokens added here are tracked across every address, unlike AddToken which is reached from one address. - **Elements**: - "Back" button, "Add Token" heading - Instruction text - "Top tokens:" quick-pick buttons (top 10 by market cap; already-tracked tokens are disabled) - "Or pick from top 100:" dropdown (already-tracked tokens are disabled) + "Add selected" button - "Or enter contract address:" input, a status line, and an "Add" button - **Transitions**: - Any of the three add paths, on success → adds the token and shows an "Added SYMBOL" flash message (no screen change) - A duplicate, a scam-listed address, or a failed contract lookup → flash message, no screen change - "Back" → previous screen (Settings) #### SiteApproval (`approve-site`) - **When**: A website requests wallet access via `eth_requestAccounts` or `wallet_requestPermissions` and is on neither the allowed nor the denied list. The background script prefers the toolbar popup (`action.openPopup()`) and falls back to a separate popup window (`src/background/index.js`, `requestApproval()`). - **Elements**: - "Connection Request" heading - Phishing warning banner (shown when the hostname is on the phishing blocklist) - Site hostname (bold) + "wants to connect to your wallet" - Address that will be shared (color dot + full address + etherscan link) - "Remember my choice for this site" checkbox - "Allow" / "Deny" buttons - **Transitions**: - "Allow" / "Deny" → closes popup (returns result to background script; the choice is persisted to the allowed or denied list when "Remember" is checked) - Popup closed without answering → treated as a denial #### TxApproval (`approve-tx`) - **When**: A connected website requests a transaction via `eth_sendTransaction`. Always opened in a separate popup window by the background script (`windows.create()`), because the request is triggered programmatically rather than by a user gesture. - **Elements**: - "Transaction Request" heading - Phishing warning banner (shown when the hostname is on the phishing blocklist) - Site hostname (bold) + "wants to send a transaction" - Decoded action (if calldata is recognized): action name, token details, amounts, steps, deadline (see Transaction Decoding) - From: color dot + full address + etherscan link - Contract: color dot + full address + etherscan link (or "contract creation"), token symbol label if known - Value: amount in ETH (4 decimal places, USD in parentheses) - Raw data: full calldata displayed inline (shown if present) - Password input and an error line - "Confirm" / "Reject" buttons - **Transitions**: - "Confirm" (correct password) → decrypts and signs in the popup, hands the signed transaction to the background to broadcast, then → **WaitTx** in the same popup window - "Confirm" (wrong password) → error line, no screen change - "Reject" → closes popup (returns rejection to background) - Popup window closed without answering → the request is rejected with EIP-1193 code 4001 #### SignApproval (`approve-sign`) - **When**: A connected website requests a message signature via `personal_sign`, `eth_sign`, or `eth_signTypedData_v4`. Opened the same way as TxApproval, in a separate popup window. - **Elements**: - "Signature Request" heading - Phishing warning banner (shown when the hostname is on the phishing blocklist) - Site hostname (bold) + "wants you to sign a message" - Danger warning box (shown for `eth_sign`, which signs a raw hash) - Type: "Personal message" or "Typed data (EIP-712)" - From: color dot + full address + etherscan link - Message: decoded UTF-8 text (personal_sign) or formatted domain/type/ message fields (EIP-712 typed data) - Password input and an error line - "Sign" / "Reject" buttons - **Transitions**: - "Sign" (correct password) → signs locally → closes popup (returns signature) - "Sign" (wrong password, or a signing failure) → error line, no screen change - "Reject" → closes popup (returns rejection to background) - Popup window closed without answering → the request is rejected with EIP-1193 code 4001 ### External Services AutistMask is not a fully self-contained offline tool. It necessarily communicates with three external services to function as a wallet: - **Ethereum JSON-RPC endpoint**: The extension needs an Ethereum node to query balances (`eth_getBalance`), read ERC-20 token contracts (`eth_call`), estimate gas (`eth_estimateGas`), fetch nonces (`eth_getTransactionCount`), broadcast transactions (`eth_sendRawTransaction`), and check transaction receipts. The default endpoint is a public RPC (configurable by the user to any endpoint they prefer, including a local node). By default the extension talks to `https://ethereum-rpc.publicnode.com`. - **Data sent**: Ethereum addresses, transaction data, contract call parameters. The RPC endpoint can see all on-chain queries and submitted transactions. - **CoinDesk CADLI price API**: Used to fetch ETH/USD and token/USD prices for displaying fiat values. The price is cached for 5 minutes to avoid excessive requests. No API key required. No user data is sent — only a list of token symbols. Note that CoinDesk will receive your client IP. - **Data sent**: Token symbol strings only (e.g. "ETH", "USDC"). No addresses or user-specific data. - **Blockscout block-explorer API**: Used to fetch transaction history (normal transactions and ERC-20 token transfers), ERC-20 token balances, and token holder counts (for spam filtering). The default endpoint is `https://eth.blockscout.com/api/v2` (configurable by the user in Settings). - **Data sent**: Ethereum addresses. Blockscout receives the user's addresses to query their transaction history and token balances. No private keys, passwords, or signing operations are sent. What the extension does NOT do: - No analytics or telemetry services - No token list APIs (the known-token list is bundled at build time) - No Infura/Alchemy dependency (any JSON-RPC endpoint works) - No backend servers operated by the developer In addition to the three user-configurable services above (RPC endpoint, CoinDesk price API, and Blockscout API), AutistMask also contacts: - **Phishing domain blocklist**: A community-maintained phishing domain blocklist is vendored into the extension at build time. At runtime, the extension fetches the live list once every 24 hours to detect newly added domains, plus once on a start where the list is more than 24 hours old. Only the delta (domains not already in the vendored list) is kept in memory, keeping runtime memory usage small. The delta and the timestamp of the fetch that produced it are persisted to extension storage if the record is under 256 KiB; an oversized delta is dropped along with its timestamp, so a later start fetches again rather than claiming freshness for data it no longer holds. A fetch that fails, or one whose delta was too large to store, is not retried more than once an hour outside the 24-hour schedule. - **Etherscan address labels**: When confirming a transaction, the extension performs a best-effort lookup of the recipient address on Etherscan to check for phishing/scam labels. This is a direct page fetch with no API key; the user's browser makes the request. Users who want maximum privacy can point the RPC and Blockscout URLs at their own self-hosted instances (price fetching can be disabled in a future version). ### Dependencies AutistMask uses four runtime libraries. All cryptographic operations are delegated to ethers and libsodium — see the Crypto Policy section below. | Package | Version | License | Purpose | | -------------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ethers` | 6.16.0 | MIT | All Ethereum operations: BIP-39 mnemonic generation/validation, BIP-32/BIP-44 HD key derivation (`m/44'/60'/0'/0/n`), secp256k1 signing, transaction construction, ERC-20 contract interaction, JSON-RPC communication, address derivation (keccak256). | | `libsodium-wrappers-sumo` | 0.8.2 | ISC | Password-based encryption of secrets at rest: Argon2id key derivation (`crypto_pwhash`), authenticated encryption (`crypto_secretbox` / XSalsa20-Poly1305). | | `qrcode` | 1.5.4 | MIT | QR code generation for the Receive screen (renders address as scannable QR on canvas). | | `ethereum-blockies-base64` | 1.0.2 | ISC | Deterministic pixelated identicon generation from Ethereum addresses (same style used by Etherscan). | Dev dependencies (not shipped in extension): | Package | Version | License | Purpose | | ------------------ | ------- | ------- | ------------------------- | | `esbuild` | 0.27.3 | MIT | JS bundler (inlines deps) | | `tailwindcss` | 4.2.1 | MIT | CSS compilation | | `@tailwindcss/cli` | 4.2.1 | MIT | Tailwind CLI | | `jest` | 30.2.0 | MIT | Test runner | | `prettier` | 3.8.1 | MIT | Code formatter | ### Crypto Policy **No raw crypto primitives in application code.** If the strings `aes`, `sha`, `pbkdf`, `hmac`, `encrypt`, `decrypt`, `hash`, `cipher`, `digest`, `sign` (case-insensitive) appear in our own source code (outside of `node_modules/`), it is almost certainly a bug. All cryptographic operations must go through `ethers` or `libsodium-wrappers-sumo`. Both are widely audited and battle-tested. Exceptions require explicit authorization in a code comment referencing this policy, but as of now there are none. ### Content Security Policy Both manifests declare the same policy for extension pages — `script-src 'self' 'wasm-unsafe-eval'; object-src 'self'` — as an object under `content_security_policy.extension_pages` in `manifest/chrome.json` (MV3) and as a bare string in `manifest/firefox.json` (MV2). `'wasm-unsafe-eval'` is there for one reason: libsodium. It ships a WebAssembly build and a `wasm2js` translation of it in one file, tries WASM first, and silently falls back to the translation if instantiation throws. Under a plain `script-src 'self'` the fallback was taken on every popup load, announced by nothing but an uncaught `CompileError`. Measured on the same Argon2id parameters the vault uses (`OPSLIMIT_INTERACTIVE`, `MEMLIMIT_INTERACTIVE`), WASM derives a key in 141-198ms and `wasm2js` in 3204-3660ms. The work factor is identical — it is set by the ops and memory parameters, not by wall time — so the fallback bought nothing and cost about three and a half seconds on every operation that asks for the password, which is every signature. The keyword permits compiling WebAssembly and nothing else: not `eval()` of strings, not inline script, not remote script. Using it requires already executing script in an extension page, which is complete compromise on its own. `'unsafe-eval'` is a different proposition and is not granted. The grant is pinned in both directions. `tests/manifest.test.js` asserts the exact token set in both manifests, so dropping `'wasm-unsafe-eval'` (a silent 20x regression on the key derivation) and adding anything beyond it both fail `make check`. `tests/vaultBackend.test.js` asserts the unit tests run the WASM backend, and `make test-e2e` compiles a WebAssembly module inside the real popup under the real manifest. ### DEBUG Mode Policy The `DEBUG` constant in the popup JS enables a red "DEBUG / INSECURE" banner and a hardcoded test mnemonic. **DEBUG mode must behave as close to normal mode as possible.** No `if (DEBUG)` branches that skip functionality, bypass security flows, or alter program behavior beyond the banner and the hardcoded mnemonic. Adding new DEBUG-conditional branches requires explicit approval from the project owner. `DEBUG` is a build-time constant, not a runtime setting. `build.js` injects it into the bundle as the `__BUILD_DEBUG__` define — `false` unless the build was run with `AUTISTMASK_DEBUG=1` (see [Debug Builds](#debug-builds)) — and `src/shared/constants.js` reads it. It cannot be changed after the bundle is produced. The debug-mode toggle in settings is a separate, runtime-only flag. It raises the log level and turns the banner on, and that is all it may ever do: it feeds `isDebug()` in `src/shared/log.js`, which is deliberately not what `generateMnemonic()` consults. Mnemonic generation reads the build-time `DEBUG` constant directly, so no runtime toggle in a release build can reach the hardcoded test phrase. ### Key Decisions - **No framework**: The popup UI is vanilla JS and HTML. The extension is small enough that a framework adds unnecessary complexity and attack surface. - **Split storage model**: Public data (xpubs, derived addresses, token lists, balances) is stored unencrypted in extension local storage so the user can view their wallets and balances at any time without entering a password. Private data (recovery phrases, private keys) will be encrypted at rest using libsodium — a password is only required when the user needs to sign a transaction or message. The encryption scheme for private data: - The user's password is run through Argon2id (`crypto_pwhash`) to derive a 256-bit encryption key. Argon2id is memory-hard, making GPU/ASIC brute force attacks expensive. - The derived key encrypts the secret material using XSalsa20-Poly1305 (`crypto_secretbox`), which provides authenticated encryption (the ciphertext cannot be tampered with without detection). - Stored blob: `{ salt, nonce, ciphertext }` (the auth tag is part of the `crypto_secretbox` output). - **The password is NOT used in address derivation.** It exists solely to protect the recovery phrase / private key on disk. Anyone with the recovery phrase can restore the wallet on any device without this password. This matches standard EVM wallet behavior. - **BIP-39 / BIP-44 via ethers.js**: Mnemonic generation, validation, and HD key derivation (`m/44'/60'/0'/0/n`) are handled entirely by ethers.js. The BIP-39 passphrase is always empty (matching most wallet software). The user's password is completely separate and has no effect on which addresses are generated. - **ethers.js for everything Ethereum**: Transaction construction, signing, gas estimation, RPC communication, ERC-20 contract calls, and address derivation are all handled by ethers.js. This means zero hand-rolled Ethereum logic. - **EIP-1193 provider**: The content script injects a `window.ethereum` object that implements the EIP-1193 provider interface, enabling web3 site connectivity. - **Minimal RPC**: The extension communicates with Ethereum nodes via JSON-RPC through ethers.js's `JsonRpcProvider`. The default endpoint is configurable. No Infura dependency — users can point it at any Ethereum JSON-RPC endpoint. ### Supported Functionality - Create new HD wallet (generates 12-word recovery phrase) - Import HD wallet from existing 12 or 24 word recovery phrase - Import single-address wallet from private key - Import multi-address wallet from an extended private key (`xprv`) - Add multiple addresses within an HD wallet - Manage multiple wallets simultaneously - View ETH balance per address - View ERC-20 token balances (tokens on the bundled known-token list, tokens with 1,000 or more holders, and tokens the user adds by contract address) - Send ETH to an address - Send ERC-20 tokens to an address - Receive ETH/tokens (display address, copy to clipboard, QR code) - Connect to web3 sites (EIP-1193 `eth_requestAccounts`) - Sign transactions requested by connected sites (`eth_sendTransaction`) - Sign messages (`personal_sign`, `eth_sign`) - Sign typed data (`eth_signTypedData_v4`, `eth_signTypedData`) - Human-readable transaction decoding (ERC-20, Uniswap Universal Router) - ETH/USD and token/USD price display - Configurable RPC endpoint and Blockscout API - Address poisoning protection (spam token filtering, dust filtering, fraud contract blocklist) ### Address Poisoning and Fake Token Transfer Attacks During development, one of our test addresses (`0x66133E8ea0f5D1d612D2502a968757D1048c214a`) sent 0.005 ETH to `0xC3c693Ae04BaD5f13C45885C1e85a9557798f37E`. Within seconds, a fraudulent transaction appeared in the address's token transfer history (`0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84`) showing a 0.005 "ETH" transfer from our address to `0xC3C0AEA127c575B9FFD03BF11C6a878e8979c37F` — a scam address whose first four characters (`0xC3C0`) visually resemble the legitimate recipient (`0xC3c6`). **How it works:** A scammer deploys a malicious ERC-20 contract (in this case, `0xD05339f9Ea5ab9d9F03B9d57F671d2abD1F55c82`, a fake token calling itself "Ethereum" with symbol "ETH" and zero holders). This contract has a function that emits an ERC-20 `Transfer(from, to, amount)` event with arbitrary parameters. The EVM does not enforce that the `from` address in a Transfer event actually initiated or authorized the transfer — any contract can emit any event with any parameters. The scammer calls their contract, which emits a Transfer event claiming the victim sent tokens to the scam address. Every blockchain indexer (Blockscout, Etherscan, etc.) sees a valid Transfer event log and indexes it as a real token transfer. **The attack has two goals:** 1. **Autocomplete poisoning**: Wallets that offer address autocomplete based on transaction history will suggest the scam address (which looks similar to a legitimate recent recipient) when the user starts typing. The user copies the wrong address and sends real funds to the scammer. 2. **Transaction history confusion**: The fake transfer appears in the victim's history as an outbound transaction, making it look like the user sent funds to the scam address. Users who copy-paste addresses from their own transaction history may grab the wrong one. **What AutistMask does about it:** - **Minimal, careful truncation**: Where space constraints require truncation (e.g. the transaction history list), AutistMask truncates conservatively — displaying enough characters that generating a vanity address matching the visible portion is computationally infeasible. All confirmation screens (transaction signing, send confirmation) display the complete untruncated address. Users should always verify the full address on the confirmation screen before signing or sending. - **Known token symbol verification**: AutistMask ships a hardcoded list of high-market-cap ERC-20 tokens with their legitimate contract addresses and symbols. The list is a point-in-time snapshot of the highest-market-cap Ethereum mainnet ERC-20s taken from the CoinGecko API, with decimals verified on-chain and addresses EIP-55 checksummed; `TOKENS` in `src/shared/tokenList.js` is the authoritative set. It is bundled at build time and only changes when that file is regenerated. Any token transfer claiming a symbol from this list (e.g. "ETH", "USDT", "USDC") but originating from an unrecognized contract address is identified as a spoof and filtered from display. The fake "Ethereum" token in the attack above used symbol "ETH" from contract `0xD05339f9Ea5ab9d9F03B9d57F671d2abD1F55c82`, which does not match the known WETH contract — so it would be caught by this check. Detecting a spoof is also what adds a contract to the fraud contract blocklist below; that is the only thing that populates it. In the transaction history the check is the "Hide fake tokens impersonating a known symbol" setting, on by default; with it off, spoofed transfers are shown and no new blocklist entries are learned from them. The send-screen token selector applies the same check unconditionally, because it decides which tokens the user can act on rather than what the history displays. The balance list applies it unconditionally too, but not identically: it exempts symbols that `KNOWN_SYMBOLS` maps to `null`, and `"ETH"` is the only one. So the fake "Ethereum" token above is filtered from the transaction history and from the send selector, but a fake-`ETH` ERC-20 that clears the balance list's own 1,000-holder floor — or that the user tracked manually — is still shown in the balance list. - **Low-holder token filtering**: Token transfers from ERC-20 contracts with fewer than 1,000 holders are hidden from transaction history by default. Legitimate tokens have substantial holder counts; poisoning tokens typically have zero. This catches new poisoning contracts that use novel symbols not in the known token list. - **Fraud contract blocklist**: AutistMask maintains a local list of known fraud contract addresses. Token transfers involving these contracts are filtered from the transaction history display. The list is populated when a fraudulent transfer is detected and persists across sessions. - **Send-side token filtering**: Tokens with fewer than 1,000 holders are excluded from the token selector on the send screen. This prevents users from accidentally interacting with a spoofed token that appeared in their balance via a fake Transfer event. - **Dust transaction filtering**: A second wave of the same attack used real native ETH transfers instead of fake tokens. Transaction `0x2708ebddfb9b5fa3f7a89d3ea398ef9fd8771b83ed861ecb7c21cd55d18edc74` sent 1 gwei (0.000000001 ETH) from `0xC3c6B3b4402bD78A9582aB6b00E747769344F37E` — another look-alike of the legitimate recipient `0xC3c693...`. Because this is a real ETH transfer (not a fake token), none of the token-level filters catch it. AutistMask hides transactions below a configurable dust threshold (default: 100,000 gwei / 0.0001 ETH). This is high enough to filter poisoning dust while low enough to preserve any transfer a user would plausibly care about. The threshold is user-configurable in Settings; a threshold of `0` hides nothing, exactly as clearing the checkbox does. - **User-configurable**: All four filters (known symbol verification, low-holder threshold, fraud contract blocklist, dust threshold) are settings that default to on but can be individually disabled by the user. AutistMask is designed as a sharp tool — users who understand the risks can configure the wallet to show everything unfiltered, unix-style. All four settings govern the transaction history; what else each one reaches varies. The known-symbol check also runs unconditionally on the send-screen token selector, and on the balance list except for symbols mapped to `null` (`"ETH"` alone), which the balance list does not filter. The fraud contract blocklist is applied unconditionally on that selector and is not consulted by the balance list at all. The low-holder setting also gates the send selector, while the balance list's own 1,000-holder floor is unconditional (see Data Model). The dust threshold applies to the transaction history alone. #### Phishing Domain Protection AutistMask protects users from known phishing sites when they connect their wallet or approve transactions/signatures. A community-maintained domain blocklist is vendored into the extension at build time, providing immediate protection without any network requests. At runtime, the extension fetches the live list once every 24 hours and keeps only the delta (newly added domains not in the vendored list) in memory. This architecture keeps runtime memory usage small while ensuring fresh coverage of new phishing domains. The 24-hour cadence is an alarm, not a timer; the alarm tick fetches unconditionally rather than re-checking the 24-hour cache TTL that gates the startup path; and the fetch timestamps live in extension storage rather than in module variables — see [Background scheduling](#background-scheduling) for why all three are required. When a dApp on a blocklisted domain requests a wallet connection, transaction approval, or signature, the approval popup displays a prominent red warning banner alerting the user. The domain checker matches exact hostnames and all parent domains (subdomain matching). #### Transaction Decoding When a dApp asks the user to approve a transaction, AutistMask attempts to decode the calldata into a human-readable summary. This is purely a display convenience to help the user understand what they are signing — it is not endorsement, special treatment, or partnership with any protocol. AutistMask is a generic web3 wallet. It treats all dApps, protocols, and contracts equally. No contract gets special handling, priority, or integration beyond what is needed to show the user a legible confirmation screen. Our commitment is to the user, not to any service, site, or contract. Decoded transaction summaries are best-effort. If decoding fails, the raw calldata is displayed in full. The decoders live in self-contained modules under `src/shared/` (e.g. `uniswap.js`) so they can be added for common contracts without polluting wallet-specific code. Contributions of decoders for other widely-used contracts are welcome. Currently supported: - **ERC-20**: `approve()` and `transfer()` calls — shows token symbol, spender or recipient, and amount. - **Uniswap Universal Router**: `execute()` calls — shows swap direction (e.g. "Swap USDT → ETH"), token addresses, amounts, execution steps, and deadline. Decodes Permit2, V2/V3/V4 swaps, wrap/unwrap, and balance checks. ### Non-Goals Forever - Built in token swaps (use a DEX in the browser) - Analytics, telemetry, or tracking of any kind - Advertisements or promotions - Obscure token list auto-discovery — nothing outside the bundled list, the 1,000-holder floor, and the tokens the user added by contract address - We detect common/popular ERC20s in the basic case - Fiat on/off ramps - Extensive transaction decoding/parsing - For common ones we will do best-effort, but you should just use a block explorer. ### Non-Goals for 1.0 - Multi-chain support (Ethereum mainnet only) - Hardware wallet support ## TODO ### Wallet Management - [x] Delete wallet (with confirmation) - [ ] Delete address from HD wallet (with confirmation) - [x] Show wallet's recovery phrase (requires password) ### Transactions - [x] Gas estimation and fee display before confirming ### Testing - [x] Tests for mnemonic generation and address derivation - [x] Tests for xpub derivation and child address generation - [ ] Test on Firefox (Manifest V2) ### Scam List - [ ] Research and document each address in scamlist.js - [ ] Add more known fraud addresses from Etherscan labels ### Future - [ ] Multi-currency fiat display (EUR, GBP, etc.) - [ ] Security audit of key management ## Policies - We don't mention "the other wallet" by name in code or documentation. We're our own thing. - The README is the complete authoritative technical documentation. It's ok if it gets big. ## License GPL-3.0. See [LICENSE](LICENSE). ### Third-Party Data Files This repository includes data files from third-party projects that are not covered by the GPL-3.0 license above. These files, their copyright holders, and their licenses are: | File | Source | Copyright | License | | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------- | -------------------------------------------------------------- | | `src/shared/phishingBlocklist.json` | `eth-phishing-detect` community-maintained phishing domain blocklist, vendored from its `src/config.json` | Copyright (c) 2018 kumavis | [DBAD (Don't Be a Dick)](https://github.com/philsturgeon/dbad) | | `src/shared/scamlist.js` (address data from MyEtherWallet) | [ethereum-lists](https://github.com/MyEtherWallet/ethereum-lists) `addresses-darklist.json` | Copyright (c) 2020 MyEtherWallet | MIT | | `src/shared/scamlist.js` (address data from EtherScamDB) | [EtherScamDB](https://github.com/MrLuit/EtherScamDB) `scams.yaml` | Copyright (c) 2018 Luit Hollander | MIT | The full license texts for these third-party files are included in the [LICENSE](LICENSE) file. The `eth-phishing-detect` row carries no repository link because the upstream is hosted under a competitor's organization name, which project policy keeps out of code and documentation; the vendored copy and the runtime refresh both come from that upstream, whose URL is the `BLOCKLIST_URL` constant in `src/shared/phishingDomains.js`. ## Author [@sneak](https://sneak.berlin)