# 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 install
make build
```

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

## 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 and the transaction detail screen for an ERC-20
transfer. 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 unconditionally — 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
    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
    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
```

### 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. "Import private key") 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 either:
    - An **HD wallet** (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** (private key): a single address imported directly from a
      private key. No "+" button since there is only one address.
- An **address** holds ETH and any user-added ERC-20 tokens.
- The user can have multiple wallets, each with multiple addresses (HD) or a
  single address (key).

#### 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 flat — every view has a "Back" or "Cancel" button
that returns to the previous context. No deep nesting, no tabs, no hamburger
menus.

### Screen Map

Navigation uses a stack model (like iOS): each action pushes a screen onto the
stack, and "Back" pops it. The root screen is either Welcome (no wallets) or
Home (has wallets). Screens are listed below with their elements and
transitions.

#### Welcome

- **When**: No wallets exist yet.
- **Elements**: "AutistMask" heading, brief intro text, "Add wallet" button.
- **Transitions**:
    - "Add wallet" → **AddWallet**

#### Home

- **When**: At least one wallet exists. This is the root screen.
- **Elements**:
    - Header: "AutistMask", Settings gear button
    - Active address ETH balance (large) + USD value (inline parentheses)
    - Total USD value across all tokens (small text)
    - Active address (color dot, full address, etherscan link, tap to copy)
    - Send / Receive quick-action buttons
    - ETH/USD price display
    - Wallet list: each wallet shows name (tap to rename), "+" button (HD only),
      and its addresses with color dots, balances, and `[info]` buttons
    - Recent transactions across all addresses (merged, deduplicated, filtered)
    - "Add additional wallet..." link at bottom
- **Transitions**:
    - Tap address row → sets active address (no screen change)
    - `[info]` on address → **AddressDetail**
    - "Send" → **Send** (selects active address)
    - "Receive" → **Receive** (shows active address QR)
    - "+" on wallet → derives next address inline
    - "Add additional wallet..." → **AddWallet**
    - Settings gear → **Settings** (toggles; tap again to return)
    - Tap home tx row → **AddressDetail** (for the address involved)

#### AddWallet

- **When**: User wants to add a new wallet (from Home, Welcome, or Settings).
- **Elements**:
    - "Add Wallet" heading, "Back" button
    - Instruction text
    - Die button `[die]` (generates random recovery phrase)
    - Recovery phrase textarea
    - Backup warning box (shown after die is clicked)
    - Password + confirm password inputs
    - "Add" button
    - "Have a private key instead?" link
- **Transitions**:
    - "Add" (valid phrase + password) → **Home**
    - "Back" → previous screen (Home or Welcome)
    - "Have a private key instead?" → **ImportKey**

#### ImportKey

- **When**: User wants to import a single private key.
- **Elements**:
    - "Import Private Key" heading, "Back" button
    - Instruction text
    - Private key input (password-masked)
    - Password + confirm password inputs
    - "Import" button
- **Transitions**:
    - "Import" (valid key + password) → **Home**
    - "Back" → **AddWallet**

#### AddressDetail

- **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 with color dot)
    - Full address (color dot, etherscan link, tap to copy)
    - USD total for address
    - Balance list: ETH + tracked ERC-20 tokens (4 decimal places, USD inline).
      Each balance row is clickable → **AddressToken**
    - Send / Receive / + Token buttons
    - Transaction list (with ENS resolution for counterparties)
- **Transitions**:
    - Tap balance row → **AddressToken** (for that token)
    - "Send" → **Send**
    - "Receive" → **Receive**
    - "+ Token" → **AddToken**
    - Tap transaction row → **TransactionDetail**
    - "Back" → **Home**

#### AddressToken

- **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-filtered transaction list (only this token's transfers)
- **Transitions**:
    - "Send" → **Send** (token pre-selected and locked in dropdown)
    - "Receive" → **Receive** (ERC-20 warning shown for non-ETH tokens)
    - Tap transaction row → **TransactionDetail**
    - "Back" → **AddressDetail**

#### Send

- **When**: User wants to send ETH or a token from this address.
- **Elements**:
    - "Send" heading, "Back" button
    - 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
    - Amount input with current balance display
    - "Review" button
- **Transitions**:
    - "Review" (valid inputs, ENS resolved) → **ConfirmTx**
    - "Back" → **AddressToken** (if came from token view) or **AddressDetail**

#### ConfirmTx

- **When**: User reviewed send details and is ready to authorize.
- **Elements**:
    - "Confirm Transaction" heading, "Back" button
    - 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)
    - Estimated network fee: ETH amount (USD in parentheses), fetched async
    - Warnings (scam address, self-send)
    - Errors (insufficient balance)
    - "Send" button (disabled if errors)
- **Transitions**:
    - "Send" → password modal → broadcast tx → **WaitTx**
    - "Send" → password modal → broadcast fails → **ErrorTx**
    - "Back" → **Send**

#### WaitTx

- **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.
- **Transitions**:
    - Receipt found → **SuccessTx**
    - 60 seconds without confirmation → **ErrorTx** (timeout message)

#### SuccessTx

- **When**: Transaction confirmed on-chain.
- **Elements**:
    - "Transaction Confirmed" heading
    - Amount + symbol
    - To: color dot + full address + etherscan link
    - Block number
    - Transaction hash: full hash (tap to copy) + etherscan link
    - "Done" button
- **Transitions**:
    - "Done" → **AddressToken** (if `selectedToken` set) or **AddressDetail**

#### ErrorTx

- **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" → **AddressToken** (if `selectedToken` set) or **AddressDetail**

#### Receive

- **When**: User wants to receive funds at this address.
- **Elements**:
    - "Receive" heading, "Back" button
    - 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" → **AddressToken** (if `selectedToken` set) or **AddressDetail**

#### TransactionDetail

- **When**: User tapped a transaction row from 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" → **AddressToken** (if `selectedToken` set) or **AddressDetail**

#### AddToken

- **When**: User wants to track an ERC-20 token on this address.
- **Elements**:
    - "Add Token" heading, "Back" button
    - Instruction text (find contract address on Etherscan)
    - Contract address input
    - Token info preview (name, symbol — fetched from contract)
    - Common token quick-pick buttons
    - "Add" button
- **Transitions**:
    - "Add" (valid contract) → **AddressDetail**
    - "Back" → **AddressDetail**

#### Settings

- **When**: User tapped Settings gear from Home.
- **Elements**:
    - "Settings" heading, "Back" button
    - Wallets: "+ Add wallet" button
    - Display: "Show tracked tokens with zero balance" checkbox
    - Ethereum RPC: endpoint URL input + "Save" button
    - Blockscout API: endpoint URL input + "Save" button
    - Token Spam Protection:
        - "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
- **Transitions**:
    - "+ Add wallet" → **AddWallet**
    - "Back" (or Settings gear again) → **Home**

#### SiteApproval

- **When**: A website requests wallet access via `eth_requestAccounts`. Opened
  in a separate popup by the background script.
- **Elements**:
    - "Connection Request" heading
    - Site hostname (bold)
    - 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)

#### TxApproval

- **When**: A connected website requests a transaction via
  `eth_sendTransaction`. Opened via the toolbar popup by the background script.
- **Elements**:
    - "Transaction Request" heading
    - 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
    - To/Contract: color dot + full address + etherscan link (or "contract
      creation"), token symbol label if known
    - Value: amount in ETH (4 decimal places)
    - Raw data: full calldata displayed inline (shown if present)
    - Password input
    - "Confirm" / "Reject" buttons
- **Transitions**:
    - "Confirm" (with password) → closes popup (returns result to background)
    - "Reject" → closes popup (returns rejection to background)

#### SignApproval

- **When**: A connected website requests a message signature via
  `personal_sign`, `eth_sign`, or `eth_signTypedData_v4`. Opened via the toolbar
  popup by the background script.
- **Elements**:
    - "Signature Request" heading
    - Site hostname (bold) + "wants you to sign a message"
    - 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
    - "Sign" / "Reject" buttons
- **Transitions**:
    - "Sign" (with password) → signs locally → closes popup (returns signature)
    - "Reject" → closes popup (returns rejection to background)

### 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 (user adds tokens manually by contract address)
- 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. Only the delta (domains not already in the vendored list) is kept in
  memory, keeping runtime memory usage small. The delta is persisted to
  localStorage if it is under 256 KiB.
- **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
- Add multiple addresses within an HD wallet
- Manage multiple wallets simultaneously
- View ETH balance per address
- View ERC-20 token balances (user adds token 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 the
  top 250 ERC-20 tokens with their legitimate contract addresses and symbols.
  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.

- **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.

- **User-configurable**: All of the above 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.

#### 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.

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 (user adds tokens manually)
    - 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)
- [ ] Show wallet's recovery phrase (requires password)

### Transactions

- [ ] Gas estimation and fee display before confirming

### Testing

- [ ] Tests for mnemonic generation and address derivation
- [ ] 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](https://github.com/AugurProject/eth-phishing-detect) community-maintained phishing domain blocklist | 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.

## Author

[@sneak](https://sneak.berlin)
