Files
AutistMask/README.md
T
sneak 4dc7814607
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
docs: rewrite README.md sentence by sentence against the prose rules
Every sentence is written for a newcomer to read once: one idea, subject
and verb first, under about 25 words, bullets instead of comma lists, no
tables, no project history. The order is what the extension is, what it
talks to, how to build and load it, the settings, the design, then
development and tests. Every statement was checked against the code on
next; the disagreements with the old text are listed in the PR.

Part of #672.

Model: fable-5-1
2026-11-01 23:35:37 +00:00

1668 lines
75 KiB
Markdown

# AutistMask
AutistMask is a browser extension. It is an Ethereum wallet for Chrome and
Firefox. It is free software under the GPL-3.0 license, written by
[@sneak](https://sneak.berlin).
The wallet does four things:
- It holds wallets made from a recovery phrase, a private key or an extended
private key.
- It shows the ETH and ERC-20 token balances of each address.
- It sends and receives ETH and ERC-20 tokens.
- It connects to web3 sites and signs what the user approves.
It does nothing else. It has:
- no swap screen;
- no portfolio view;
- no NFT gallery;
- no advertising;
- no tracking.
## Why it exists
Popular browser wallets have grown swap screens, portfolio dashboards,
promotions and tracking. Most of them run only on Chromium browsers. AutistMask
is a wallet and nothing more. One code base serves both Chrome and Firefox.
## What it talks to
The extension runs entirely in the browser. No server is run by the developer.
By default the extension contacts one service: the Ethereum RPC endpoint set in
Settings. The wallet uses that endpoint to:
- read balances;
- look up ENS names;
- estimate fees;
- broadcast transactions;
- check receipts;
- on Ethereum Mainnet, read USD prices from Chainlink price feed contracts.
Two more services receive the user's data only after the user turns on a
setting. Both settings are off in a new install. The Welcome screen and Settings
both offer them.
- "Use Blockscout" sends the user's addresses, and the hash of each transaction
the user opens, to the Blockscout API set in Settings. In return the wallet
shows transaction history, transaction details and the tokens the user has not
tracked.
- "Check recipients on Etherscan" sends the recipient's address to Etherscan
each time the user reviews a send. The wallet warns when Etherscan labels the
address as phishing or scam.
The phishing domain list is built into the extension. Checking a site against it
sends nothing anywhere.
The section External services below lists every request.
## Getting started
Build the extension from a clone:
```bash
git clone https://git.eeqj.de/sneak/autistmask.git
cd autistmask
make setup
make build
```
`make setup` installs the dependencies from the lockfile and installs the git
pre-commit hook. `make build` writes the extension into `dist/chrome/` and
`dist/firefox/`.
Load it:
- Chrome: open `chrome://extensions/`, turn on "Developer mode", click "Load
unpacked" and pick `dist/chrome/`.
- Firefox: open `about:debugging#/runtime/this-firefox`, click "Load Temporary
Add-on" and pick `dist/firefox/manifest.json`. A temporary add-on is unloaded
when Firefox exits. Read Installing on Firefox below before relying on it.
The user guide, [docs/README.md](docs/README.md), explains how to use the
wallet. The release procedure is in [docs/RELEASE.md](docs/RELEASE.md).
### Installing on Chrome
`manifest/chrome.json` carries a fixed `key`. The key is the public half of an
RSA key pair. Chrome derives the extension id from it, so the id is
`gipbhkogfopeahplcjhipkgpcimdpkip` wherever the extension is loaded from.
`tests/extensionId.test.js` pins that id. `tests/e2e/storagePartition.js`
observes it in a real Chrome.
Without a `key`, Chrome derives the id from the directory the extension was
loaded from. `chrome.storage.local`, which holds the wallet, is kept per
extension id. A copy loaded from another directory would then start with empty
storage. Changing `key` changes the extension id, and Chrome then opens the
extension on a new, empty storage area.
The private half of the key pair is a credential. It is not in this repository.
`tests/extensionId.test.js` fails if a private key is committed anywhere in the
tree. The private half is not needed to build, load or test anything here. It
signs a CRX, which this repository does not produce.
### Installing on Firefox
The `.xpi` that `make package` writes is not signed. Release Firefox and Firefox
ESR refuse to install an unsigned add-on, and `xpinstall.signatures.required`
has no effect on them. A permanent install of the unsigned file needs Firefox
Developer Edition, Nightly or an unbranded build, with
`xpinstall.signatures.required` set to `false` in `about:config`.
Every Firefox, release builds included, can load the extension as a temporary
add-on from `about:debugging`. A temporary add-on is unloaded when Firefox
exits, so it must be added again on every start.
The wallet survives that restart. `manifest/firefox.json` declares a fixed
`browser_specific_settings.gecko.id`, and Firefox keys the extension's storage
on that id. Adding the add-on again in the same profile finds the stored
wallets. `tests/e2e/firefox/reinstall.js` checks this in a real Firefox. It
creates a wallet, restarts the browser, adds the add-on again and decrypts the
vault with the original password.
Removing the add-on in `about:addons` destroys its storage, and the wallets with
it. On Firefox, Remove is final. The recovery phrase, private key or extended
private key is the only way back. The same suite observes this.
AutistMask does not run in Firefox private windows. `manifest/firefox.json` sets
`"incognito": "not_allowed"`. If Firefox is set to never remember history, every
window is private, and the extension does not run at all.
### Release archives
`make package` runs `make build` and then writes three files into `release/`:
```
release/autistmask-chrome-<version>.zip
release/autistmask-firefox-<version>.xpi
release/SHA256SUMS
```
It publishes nothing. [docs/RELEASE.md](docs/RELEASE.md) has the procedure.
## Settings
Every setting below is stored in the wallet's profile (see Stored data). The
default named is the one a new install has. The Settings screen offers each of
them. Welcome also offers "Use Blockscout" and "Check recipients on Etherscan".
- Network: `Ethereum Mainnet` or `Sepolia Testnet`. Default `Ethereum Mainnet`.
Only the user switches it, in Settings or by approving a site's request on
NetworkApproval.
- Ethereum RPC: the JSON-RPC endpoint URL. Default
`https://ethereum-rpc.publicnode.com` on mainnet and
`https://ethereum-sepolia-rpc.publicnode.com` on Sepolia. Remembered per
network.
- "Use Blockscout": off. While off, Blockscout is sent none of the user's data.
- Blockscout API: the Blockscout API URL. Default
`https://eth.blockscout.com/api/v2` on mainnet and
`https://eth-sepolia.blockscout.com/api/v2` on Sepolia. Remembered per
network.
- "Check recipients on Etherscan": off. While off, Etherscan is not asked.
- "Allow weak passwords": off. While off, AddWallet refuses a password shorter
than 12 characters. While on, one character is enough.
- "Show tracked tokens with zero balance": on.
- "UTC Timestamps": off. While off, timestamps are shown in local time with the
UTC offset.
- Theme: `System`, `Light` or `Dark`. Default `System`.
- "Hide fake tokens impersonating a known symbol": on.
- "Hide tokens with fewer than 1,000 holders": on.
- "Hide transactions from detected fraud contracts": on.
- "Hide dust transactions below N gwei": on, with a threshold of `100000` gwei
(0.0001 ETH). The threshold is a whole number of gwei. `0` hides nothing.
- Tracked Tokens: the ERC-20 tokens tracked on the active network. Each network
has its own list.
- Allowed Sites: the sites allowed with "Remember my choice for this site".
- Connected Sites: the sites allowed without it. Such a connection lasts until
the browser closes.
- Denied Sites: the sites denied with "Remember my choice for this site".
- "Remember my choice for this site": the checkbox on SiteApproval. Its last
state is remembered. Default ticked.
- Debug mode: off. The checkbox is hidden until the user clicks the version in
About ten times.
Three environment variables affect the build and the tests:
- `AUTISTMASK_DEBUG=1` makes `build.js` compile a debug build. Only the exact
value `1` does; any other value gives a release build.
- `AUTISTMASK_BUILD_RECEIPT` names the file `build.js` writes its receipt to.
The `Makefile` sets it.
- `E2E_TRACE_NETWORK=1` makes the Chrome end-to-end suite print every request it
intercepts.
## Design
### Code layout
```
src/
background/
index.js the background worker: site requests, approvals,
broadcasting, the balance refresh alarm
state.js the background's access to the stored profile
content/
index.js content script: relays messages between the page and
the background
inpage.js the window.ethereum provider injected into pages
popup/
index.html the markup of every screen
index.js popup entry point: loads the profile, restores the
screen, runs the refresh
viewRouter.js renders a screen the popup lands on without navigating
to it
theme.js applies the theme
dustThreshold.js parses the dust threshold field
styles/main.css Tailwind source, plus four classes of its own
views/ one module per screen, and helpers.js
shared/ modules used by both the popup and the background
manifest/
chrome.json Manifest V3 for Chrome
firefox.json Manifest V2 for Firefox
icons/ the toolbar icon at the four sizes the manifests declare
build.js bundles the extension into dist/
script/ the entry points (see Development)
tests/ jest unit tests; tests/e2e/ holds the browser suites
```
The shared modules, each in one line:
- `addressWarnings.js`: the warnings for a recipient address.
- `alarms.js`: the balance refresh alarm.
- `amountDisplay.js`: the four-decimal rule and its nonzero floor.
- `approvalAmount.js`: the scale and symbol a token amount is shown with.
- `approvalTx.js`: fills in the transaction a site asks for, before the approval
window opens.
- `approvalVerify.js`: checks a signed transaction or signature against what was
approved; the fee ceilings; the typed data refusals.
- `balances.js`: ETH and token balances, the RPC provider, token lookup and the
address scan on import.
- `browserApi.js`: the one module that names `chrome` or `browser`; storage,
messaging, windows and the lock.
- `buildInfo.js`: the version, commit and date compiled into About.
- `chainSwitch.js`: the popup's network switch.
- `chainSwitchFields.js`: the fields a network switch changes.
- `constants.js`: `DEBUG`, the default endpoints, the ERC-20 ABI and the burn
addresses.
- `domainHash.js`: how a domain becomes a phishing blocklist entry.
- `ens.js`: cached ENS reverse lookups. Popup only.
- `etherscanLabels.js`: the Etherscan label check.
- `holders.js`: holder counts and the 1,000-holder rule.
- `html.js`: HTML escaping.
- `log.js`: the logger and `debugFetch()`.
- `networks.js`: the two networks.
- `persistedState.js`: the shape of the stored profile and its normalization.
- `phishingBlocklist.json`: the vendored phishing domain digests.
- `phishingDomains.js`: the phishing domain check.
- `prices.js`: USD prices from Chainlink feeds, and address totals.
- `restorableViews.js`: the screens a reopened popup may land on.
- `scamlist.js`: known scam addresses.
- `state.js`: the popup's in-memory profile and `saveState()`.
- `stateSchema.js`: `schemaVersion` and the check every read of the profile goes
through.
- `storedSettings.js`: reads "Use Blockscout" and "Check recipients on
Etherscan" from storage.
- `symbolDisplay.js`: the 12-character cap on a displayed symbol.
- `symbolSpoof.js`: the known-symbol spoof rule.
- `tokenList.js`: the bundled token list and the Chainlink feeds.
- `transactions.js`: transaction history from Blockscout, and the history
filters.
- `transferAmount.js`: encodes the wallet's own ERC-20 send at the scale the
screen showed.
- `txValidation.js`: the amount and balance checks for Send and ConfirmTx.
- `uniswap.js`: the Uniswap Universal Router decoder.
- `vault.js`: password-based encryption with libsodium.
- `wallet.js`: recovery phrases, key derivation and signers.
- `walletDefects.js`: the notice for an xprv wallet whose stored key is not a
master key.
- `walletDelete.js`: removing a wallet or an address from the profile.
### Wallets, keys and encryption
A wallet is one of three types:
- An HD wallet (`type: "hd"`) comes from a 12 or 24 word recovery phrase. It can
hold many addresses.
- A key wallet (`type: "key"`) comes from one private key. It holds one address.
- An xprv wallet (`type: "xprv"`) comes from an extended private key. It behaves
like an HD wallet but has no recovery phrase.
All key handling goes through `ethers`. Addresses are derived on the path
`m/44'/60'/0'/0/n`, with an empty BIP-39 passphrase. A new recovery phrase has
12 words. An extended private key must be a master key. AddWallet refuses a
child key. Each HD or xprv address record stores its `index`, and signing
derives the key at that index. `getSignerForAddress()` in `src/shared/wallet.js`
refuses a key that does not belong to the address.
The secret of each wallet is encrypted with that wallet's password
(`src/shared/vault.js`):
- The password goes through Argon2id (`crypto_pwhash`) at libsodium's
interactive setting: two passes over 64 MiB.
- The derived key encrypts the secret with XSalsa20-Poly1305
(`crypto_secretbox`).
- The stored value is `{ salt, nonce, ciphertext }`, each base64.
The password is not part of address derivation. Anyone holding the recovery
phrase can restore the wallet elsewhere without it. The password protects the
copy stored in this browser and nothing else.
Public data is stored unencrypted: the `xpub`, the addresses, the balances, the
tracked tokens and the settings. The popup shows all of that without a password.
The password is required to:
- send a transaction;
- sign a transaction, message or typed data for a site;
- show a recovery phrase;
- export a private key;
- delete a wallet on DeleteWallet.
Removing an address needs no password. Neither does DeleteWalletLostPassword,
which asks for the wallet's name instead.
### The background worker
Chrome runs `src/background/index.js` as a Manifest V3 service worker. Chrome
stops the worker after about 30 seconds without activity and starts it again on
the next event. Firefox runs the same bundle as a Manifest V2 background page,
which stays loaded. Both take the same code paths.
Three rules follow from the worker being stopped:
- Nothing is kept in module variables between events. The worker reads the
profile from storage on every call (`getState()` in `src/background/state.js`)
and writes it back under the lock (`updateState()`).
- The recurring balance refresh runs off an alarm (`src/shared/alarms.js`), not
a timer. The browser holds the schedule and wakes the worker. The period is
one minute, the shortest the alarms API allows. The refresh is skipped when
the last refresh finished less than 30 seconds ago. An open popup refreshes
every 10 seconds, so while the popup is open the background stays quiet.
- Connections allowed without "Remember my choice for this site" are kept in the
browser's session storage (`storage.session`). Session storage survives the
worker stopping and is cleared when the browser closes.
Pending approvals are the exception. An approval, and the site's request waiting
on it, live in the worker's memory. While any approval is pending, the worker
calls `runtime.getPlatformInfo()` every 20 seconds. Chrome counts that call as
activity and keeps the worker running. The timer stops itself once nothing is
pending.
Alarms the code no longer handles are listed in `OBSOLETE_ALARMS` and cleared on
every start. The worker fetches nothing when it starts.
### Stored data
The whole profile is one value under the extension storage key `autistmask`. The
popup pages and the background all read and write it.
Every write holds the browser lock named `autistmask` (`navigator.locks`,
through `withLock()` in `src/shared/browserApi.js`) from its read to its write.
So no write lands between another write's read and its write. Where the lock API
is missing, a write fails.
The popup's `saveState()` (`src/shared/state.js`) reads the stored record,
merges this page's changes into it and writes it back. It writes only the fields
this page changed since its last save. Wallets, site lists and endpoints are
merged entry by entry, so two pages changing different things do not overwrite
each other. The background's `updateState()` reads the record, applies one
change and writes it back.
Every write stamps `schemaVersion` with `STATE_SCHEMA_VERSION` from
`src/shared/stateSchema.js`, currently `1`.
Every read goes through `assertStateUsable()` first, on the raw stored value.
The check refuses a record that:
- is not an object;
- carries a `schemaVersion` this build does not know;
- has a `wallets` that is not a list of wallet records with address records in
them;
- names a `networkId` that is not a network in `src/shared/networks.js`.
A refused record is left in storage untouched. The popup shows StateRecovery. A
site's request is answered with code `-32007` and a message saying the saved
data cannot be read.
Every other field is floored by `normalizePersisted()` in
`src/shared/persistedState.js`. A list that is not a list becomes empty. A
malformed entry is dropped. A URL that is not text becomes the default.
`tests/persistedFieldContract.test.js` holds one row per stored field. Each row
states the field's floor and drives the code with hostile values.
A save that fails for a storage reason puts up a red "NOT SAVED" banner. The
banner stays until the popup closes. A save that fails because the stored record
fails the check above shows StateRecovery instead.
A popup follows changes other pages saved to three fields: the network, "Use
Blockscout" and "Check recipients on Etherscan". It takes them from the stored
record under the lock and redraws its screens (`followStoredSettings()` in
`src/popup/index.js`).
### Navigation
Navigation is a stack. A forward step pushes the current screen onto
`state.viewStack`. "Back" pops it (`pushCurrentView()` and `goBack()` in
`src/popup/views/helpers.js`). The root is Welcome when no wallet exists and
Home otherwise. The Settings gear in the title bar is the only global control.
The popup saves the current screen. Reopening the popup lands on that screen
when the screen is in `RESTORABLE_VIEWS` (`src/shared/restorableViews.js`).
Every other screen falls back to Home. The two screens that show a secret,
ExportPrivKey and ShowRecoveryPhrase, are not restorable. Neither are the two
that erase a wallet, DeleteWallet and DeleteWalletLostPassword.
A reopened popup renders only the wallet list and the screen it lands on. "Back"
onto a screen this page load has not rendered renders it first
(`src/popup/viewRouter.js`). When the data that screen needs is gone, Home is
shown instead. "Back" onto a screen already on the page only unhides it, so
unsaved input in Settings survives. Home is rendered again on every "Back".
A network switch, or a change to "Use Blockscout", cuts the stack at the first
screen that cannot be rendered from saved state. Every screen then counts as not
rendered. Settings on top is redrawn in place. Any other screen is redrawn as a
reopened popup would draw it. ConfirmTx, WaitTx, SuccessTx, ErrorTx and
TransactionDetail are drawn for the network of the transaction they show,
whichever network is active.
A screen that holds a secret in the page registers a cleanup with
`onViewLeave()`. `showView()` runs the cleanup on every exit from that screen.
So a revealed key or phrase, or a typed password, never survives in a hidden
screen.
Three things are present on every screen but StateRecovery:
- the title bar, with the Settings gear;
- the flash message line under it, which holds one line of text for two seconds;
- the red banner at the top of the page. The banner reads `[TESTNET]` on Sepolia
and `DEBUG / INSECURE` on a debug build or with debug mode on. A debug build
also appends the screen's view id.
### Screens
Each heading gives the screen's name and its view id. The markup of a screen is
the element `view-` plus its id in `src/popup/index.html`. The list of ids is
`VIEWS` in `src/popup/views/helpers.js`.
The active address is the address shown at the top of Home. It is the address
shared with connected sites. The selected address is the address a screen such
as AddressDetail shows. A screen reaches it through `state.selectedWallet` and
`state.selectedAddress`.
#### Welcome (`welcome`)
Shown while no wallet exists. The screen holds:
- the text "Welcome! To get started, add a wallet.";
- a well with the "Use Blockscout" and "Check recipients on Etherscan"
checkboxes, each with a sentence saying what it sends and to whom;
- an "Add wallet" button.
Ticking a checkbox saves the setting at once. "Add wallet" opens AddWallet.
#### Home (`main`)
Shown while at least one wallet exists. The screen holds:
- the active address's ETH balance, with its USD value in parentheses;
- a "Total:" line with the USD value of the address's holdings (see Partial
totals below);
- the active address: colour dot, full address, explorer link, tap to copy;
- "Send" and "Receive" buttons for the active address;
- the ETH/USD price, on mainnet;
- the wallet list;
- while "Use Blockscout" is off, the line "Only tracked tokens are listed,
because "Use Blockscout" is off in Settings.";
- "Recent Transactions";
- an "Add additional wallet..." link.
Each wallet in the list shows its name, which is tap to rename in place. HD and
xprv wallets also show a "+" button that derives the next address. Each address
of a wallet is a block:
- a row with the colour dot and "Address N", bold when the address is active;
- on that row, an `[info]` button and, on HD and xprv wallets holding more than
one address, an `[x]` button;
- the ENS name, when one resolved;
- the full address, on a row of its own;
- the address's USD total;
- a balance line for ETH and for each token shown.
"Recent Transactions" lists up to 25 transactions across every address of every
wallet, deduplicated by hash and filtered (see Scam protection). Each row has
three lines:
- the age and the direction;
- the counterparty's colour dot and label, with the amount on the right;
- the counterparty's full address.
A contract creation has no counterparty. Its last line reads "This transaction
creates a new contract. It has no recipient." While "Use Blockscout" is off, the
list shows only the line "No transaction history is shown, because "Use
Blockscout" is off in Settings."
Transitions:
- Tap an address row: that address becomes active, and connected sites are told
(no screen change).
- Tap a wallet name: rename in place.
- "+": derives the next address in place.
- `[info]`: AddressDetail.
- `[x]`: DeleteAddress.
- "Send": Send. Refused with a flash message when the active address has a zero
ETH balance.
- "Receive": Receive.
- Tap a transaction row: TransactionDetail.
- "Add additional wallet...": AddWallet.
- Settings gear: Settings. On Settings, the gear goes back.
#### AddWallet (`add-wallet`)
Reached from Welcome, Home or Settings. The screen holds:
- a "Back" button and the heading "Add Wallet";
- three tabs: "From Phrase" (`tab-mnemonic`), "From Key" (`tab-privkey`) and
"From xprv" (`tab-xprv`);
- under "From Phrase": a die button that generates a phrase, a text area for the
phrase, and a backup warning that appears once the die was used;
- under "From Key": a masked input for a private key;
- under "From xprv": a masked input for an extended private key;
- a password input and a confirmation input, with a hint line. The hint names
the backup of this wallet type and says the password cannot be recovered or
reset. Its height is reserved, so switching tabs moves nothing;
- an "Import" button with an error line beside it.
The password needs 12 characters, or one while "Allow weak passwords" is on.
"Import" with a valid entry and a matching password creates the wallet, named
"Wallet N", and encrypts the secret with the password. It clears the stack and
shows Home. When no address is active yet, the new wallet's first address
becomes active. A phrase or xprv import then scans for used addresses through
the RPC endpoint, five at a time, and stops after five unused addresses in a
row. When the scan finds two or more used addresses, they replace the wallet's
list. The flash line reports the count.
Refusals:
- a missing, short or mismatched password: a full sentence on the error line;
- an invalid phrase or key, a child extended private key, or a phrase or address
already in a wallet: a flash message.
"Back" returns to the previous screen.
#### AddressDetail (`address`)
Reached from `[info]` on Home. The screen holds:
- a "Back" button;
- the address's identicon;
- the title "Wallet Name — Address N";
- the full address with its ENS name, explorer link and tap to copy;
- the address's USD total;
- the balance list: ETH, then each token shown for the address. Each row opens
AddressToken;
- while "Use Blockscout" is off, the "Only tracked tokens are listed" line;
- "Send", "Receive" and "+ Token" buttons, and a "···" menu holding "Export
Private Key";
- the address's transaction list, or the "No transaction history is shown" line
while "Use Blockscout" is off.
Transitions:
- a balance row: AddressToken;
- "Send": Send. Refused with a flash message when the address has a zero ETH
balance;
- "Receive": Receive;
- "+ Token": AddToken;
- "Export Private Key": ExportPrivKey;
- a transaction row: TransactionDetail;
- "Back": Home.
#### ExportPrivKey (`export-privkey`)
Reached from the "···" menu on AddressDetail. The screen holds:
- a "Back" button;
- the identicon and the heading "Export Private Key";
- the wallet title and the full address;
- a warning that anyone holding the key can take everything at the address;
- a password input, an error line and a "Reveal" button.
"Reveal" with the right password decrypts the wallet's secret, derives this
address's key and shows it in place of the password input. The key is tap to
copy. A wrong password shows "That password is incorrect. Please try again." A
key the wallet refuses (see Wallets, keys and encryption) shows the refusal and
reveals nothing.
Nothing is decrypted or written into the page before the password is accepted.
The key is never stored. Leaving the screen by any route wipes it, and a decrypt
still running when the screen is left is discarded. The screen is not
restorable. The Settings gear leaves the screen and takes it off the stack.
The wallet never clears the clipboard after a copy. The clipboard is the user's.
#### AddressToken (`address-token`)
Reached by tapping a balance row on AddressDetail. The screen holds:
- a "Back" button;
- the identicon and the title "Wallet Name — Address N — SYMBOL";
- the full address;
- the USD value of this holding, and one balance line;
- "Send" and "Receive" buttons;
- for an ERC-20 token, a well with the contract address and, where known, the
name, the symbol, the decimals, the holder count and the project website. The
"Holders:" row is left out when no count is known;
- the token's transactions, or the "No transaction history is shown" line while
"Use Blockscout" is off.
"Send" opens Send with the token locked: the dropdown is replaced by the symbol
and contract address. "Receive" opens Receive with the ERC-20 warning. A
transaction row opens TransactionDetail. "Back" returns to AddressDetail.
#### Send (`send`)
Reached from Home, AddressDetail or AddressToken. The screen holds:
- a "Back" button and the heading "Send";
- the From address with its colour dot and explorer link;
- a dropdown of what to send, or the locked token when opened from AddressToken;
- while "Use Blockscout" is off, the "Only tracked tokens are listed" line under
the dropdown;
- a "To" input for an address or ENS name, with an inline validation message;
- an amount input, a "Max" button and the current balance;
- a "Review" button, disabled until the recipient validates.
The dropdown offers ETH and the tokens of the address's balance list. It leaves
out:
- holdings below 0.000001;
- tokens with a spoofed symbol;
- tokens on the fraud contract blocklist;
- while "Hide tokens with fewer than 1,000 holders" is on, tokens with a
reported holder count below 1,000.
"Max" fills in a token's whole balance, cut to 18 decimal places. For ETH it
fills in the exact balance minus the fee reserve ConfirmTx checks against. The
ETH fee is estimated for the recipient entered, so a recipient is needed first.
Where there is nothing to fill in, a flash message says why.
The "To" input accepts an Ethereum address or an ENS name. A mixed-case address
must pass its checksum. The zero address is refused. Sending to one's own
address is allowed, with a warning.
"Review" checks the amount (see Amounts below), resolves an ENS name through the
RPC endpoint and opens ConfirmTx. A name with no on-chain address, including a
name resolved off-chain, gets the flash message "That ENS name has no on-chain
address."
#### ConfirmTx (`confirm-tx`)
Shows the send the user reviewed, on the network that was active when Send
opened it. The screen holds:
- a "Back" button and the heading "Confirm Transaction";
- the type: "Native ETH transfer" or "ERC-20 token transfer (SYMBOL)";
- the token contract, for an ERC-20 send;
- the From and To addresses, each with identicon, colour dot, full address and
explorer link;
- the amount, with its USD value;
- the From address's balance, read from the network's RPC endpoint each time the
screen is shown;
- the network fee: the expected cost, and under it the larger amount reserved
until the transaction confirms;
- warnings, each in its own box:
- the recipient is on the scam address list;
- the recipient is a burn address;
- the recipient is the sender;
- the recipient is a contract;
- the recipient has never sent a transaction and holds no ETH;
- Etherscan labels the recipient as phishing or scam. While "Check
recipients on Etherscan" is off, a line says instead that the address was
not checked on Etherscan;
- errors, each in its own box:
- the amount is invalid;
- the amount exceeds the balance;
- the balance could not be read;
- the amount plus the fee exceeds the balance;
- not enough ETH to pay the fee of a token transfer;
- the fee could not be estimated;
- a password input with its error line;
- a "Sign & Send" button, disabled while any error stands or the fee is unknown.
Every warning and error has its space reserved, so a result arriving later moves
nothing.
"Sign & Send" with the right password signs and broadcasts the transaction and
opens WaitTx. A broadcast that fails opens ErrorTx. A wrong password shows "That
password is incorrect. Please try again."
Before signing, the screen refuses, with a message in its own box, when:
- the active network was switched since the transaction was reviewed;
- the selected wallet does not hold the From address;
- the gas limit or a fee is over its ceiling (see Ceilings below).
An ERC-20 send is also refused when the token contract reports a different
number of decimals than the amount was shown with. Nothing is signed, and
ErrorTx names both numbers.
An ETH amount that "Max" filled in is worked out again from this screen's own
fee estimate. The transaction is then signed with that estimate's fee fields.
"Back" returns to Send.
#### WaitTx (`wait-tx`)
Shown after a broadcast. The screen holds:
- the heading "Transaction Broadcast", with no "Back" button;
- the amount and symbol;
- the recipient;
- the transaction hash, with an explorer link;
- the counter "Waiting for confirmation... Ns".
The screen asks the sending network's RPC endpoint for the receipt every 10
seconds. The wait is saved, so a reopened popup resumes it with the original
broadcast time. Outcomes:
- a receipt: SuccessTx;
- no receipt 60 seconds or more after the broadcast: ErrorTx with a timeout
message;
- six failed lookups in a row: ErrorTx with a message naming the unreachable
network and pointing at the RPC URL in Settings. A lookup that answers resets
the count.
For a swap whose amount could not be decoded, the amount reads
`Unknown (could not be decoded)` with no symbol.
#### SuccessTx (`success-tx`)
Shown when the receipt arrives. The screen holds:
- the heading "Transaction Confirmed";
- the amount and the recipient, or the decoded action when there is one;
- the block number, with an explorer link;
- the transaction hash, with an explorer link;
- a "Done" button.
"Done" in an approval window closes the window. Elsewhere "Done" resets the
stack and opens AddressToken or AddressDetail.
#### ErrorTx (`error-tx`)
Shown when a broadcast fails or a wait ends without a receipt. The screen holds:
- the heading "Transaction Failed";
- the amount and the recipient;
- the error message, in a dashed box;
- the transaction hash, when one exists;
- a "Done" button that behaves as on SuccessTx.
#### Receive (`receive`)
Reached from Home, AddressDetail or AddressToken. The screen holds:
- a QR code of the address;
- the full address;
- a "Copy address" button;
- when opened from a token's screen, a warning to send only that token on this
network to this address.
"Back" returns to the previous screen.
#### TransactionDetail (`transaction`)
Reached by tapping a transaction row. The screen describes the transaction on
the network it was sent on. It holds:
- the transaction hash, with an explorer link;
- the type: one of "Native ETH Transfer", "ERC-20 Token Transfer", "Swap",
"Token Approval", "Contract Call" and "Contract Creation";
- the status, "Success" or "Failed";
- the From and To addresses, each with identicon, full address, explorer link
and ENS name;
- the time: ISO datetime and relative age;
- the amount, exact and in full, and under it the quantity in base units;
- the token contract, for an ERC-20 transfer;
- the details that come from Blockscout:
- the block number;
- the nonce, the gas price, the gas used and the fee;
- the decoded details;
- the raw calldata.
While "Use Blockscout" is off, one line says that none of the Blockscout details
is shown.
A call named `execute` or `multicall` is a "Swap" only when it was sent to a
Uniswap Universal Router address (see Transaction decoding). Sent to any other
contract, it is a "Contract Call".
#### AddToken (`add-token`)
Reached from "+ Token" on AddressDetail. The screen holds:
- a contract address input;
- a status line;
- quick-pick buttons for the top 25 tokens of the bundled list, on mainnet only;
- an "Add" button.
"Add" looks up `symbol()`, `decimals()` and `name()` on the contract through the
RPC endpoint. On success it adds the token to the tracked list, pops the stack
and shows AddressDetail. A token already tracked, an address on the scam list,
or a contract that fails the lookup gets a flash message. A network switch
before the lookup answers adds nothing, and a flash message says so.
#### Settings (`settings`)
Reached from the gear. The screen holds, under headings:
- Wallets: one row per wallet, with "+ Add wallet" under them. A row holds the
name (tap to rename), a `[recovery phrase]` button on HD wallets, and an `[x]`
button.
- Passwords: the "Allow weak passwords" checkbox.
- Tracked Tokens: one row per token tracked on the active network with an `[x]`
button, and "+ Add token".
- Display: "Show tracked tokens with zero balance", "UTC Timestamps" and the
theme selector.
- Network: the network selector.
- Ethereum RPC: the URL and a "Save" button. "Save" asks the URL for
`eth_chainId` and stores the URL only when the answer is the shown network's
chain id.
- Blockscout API: the "Use Blockscout" checkbox, the URL and a "Save" button.
"Save" asks the URL for `/stats` and stores the URL only when that request
succeeds. The request carries no user data and is made whether or not the
checkbox is ticked.
- Etherscan: the "Check recipients on Etherscan" checkbox.
- Token Spam Protection: the four filter checkboxes and the dust threshold
input. Input that is not a whole number of gwei is refused with a flash
message, and the field snaps back to the stored value.
- Allowed Sites, Connected Sites and Denied Sites: one row per site origin with
an `[x]` button.
- About:
- the project link;
- the license and the author;
- the version and the release date;
- a link to the commit the build came from.
- Debug: hidden until the version is clicked ten times; then a checkbox that
turns debug mode on.
Neither "Save" stores a URL when the network changed during the check, or when
the input no longer shows the checked URL. A flash message says which.
Transitions:
- Switching the network restores the RPC and Blockscout URLs last used on that
network, or the network's defaults, and the tokens tracked there. It clears
every address's balances. It tells every open tab the new chain id with
`chainChanged`. Choosing the network already active changes nothing.
- Ticking or unticking "Use Blockscout" or "Check recipients on Etherscan"
stores the setting. From then on every screen and every refresh uses it.
Unticking "Use Blockscout" also drops every untracked token from every
address.
- `[x]` on an allowed or connected site disconnects the site. Its origin is
dropped under every address. The background ends its session connections and
tells its open tabs `accountsChanged` with an empty list.
- `[x]` on a denied site forgets the refusal.
- "+ Add wallet": AddWallet. "+ Add token": SettingsAddToken.
`[recovery phrase]`: ShowRecoveryPhrase. `[x]` on a wallet: DeleteWallet.
- "Back", or the gear: the previous screen.
#### ShowRecoveryPhrase (`show-phrase`)
Reached from `[recovery phrase]` in Settings, on HD wallets only. The screen
holds:
- the wallet name;
- a warning that anyone holding the words can take everything in the wallet;
- a password input, an error line and a "Reveal" button.
The right password replaces the prompt with the phrase, tap to copy. A wrong
password shows a full-sentence error. Secret handling is as on ExportPrivKey.
#### DeleteWallet (`delete-wallet-confirm`)
Reached from `[x]` on a wallet in Settings. The screen holds:
- a warning naming the wallet and the kind of backup that can restore it;
- a password input and an error line;
- a "Confirm Delete" button;
- an "I have lost my password" link.
"Confirm Delete" with the right password removes the wallet and every site
permission of its addresses. When wallets remain, Settings is shown with the
flash message "Wallet deleted."; otherwise Welcome. The active address moves
only when it belonged to the deleted wallet, and connected sites are then told.
"I have lost my password" opens DeleteWalletLostPassword.
#### DeleteWalletLostPassword (`delete-wallet-lost-password`)
Reached from DeleteWallet. The screen says three things:
- the password cannot be recovered;
- deletion erases only the copy of the key stored in this browser;
- the wallet can be added again from a kept copy of its backup.
The user types the wallet's name to confirm. Letter case, surrounding spaces,
repeated spaces and invisible characters are ignored in the comparison. An empty
field is refused.
"Delete This Wallet Forever" with the name typed does the same as "Confirm
Delete". "Back" returns to DeleteWallet. No password is asked, because a
password in front of discarding a secret protects nobody.
#### DeleteAddress (`delete-address-confirm`)
Reached from `[x]` on an address on Home. Offered only on HD and xprv wallets
holding more than one address. The screen shows the address and explains that
removing it only stops the wallet tracking it. It explains the way back: delete
the whole wallet in Settings and import its backup again. The scan after an
import finds only addresses with on-chain activity. When the address holds
anything, the screen lists the holdings and the USD total under a warning.
"Remove Address" removes the address and its site permissions. It returns to
Home with the flash message "Address removed." The wallet's `nextIndex` is not
rewound, so "+" derives a fresh address. No password is asked.
#### SettingsAddToken (`settings-addtoken`)
Reached from "+ Add token" in Settings. The screen offers the top 10 tokens of
the bundled list as buttons, the top 100 in a dropdown, and a contract address
input. The two lists are shown on mainnet only. Each path adds the token to the
tracked list and shows "Added SYMBOL". Refusals are as on AddToken.
#### SiteApproval (`approve-site`)
Raised when a site calls `eth_requestAccounts` or `wallet_requestPermissions`
and is on neither the allowed nor the denied list. The background prefers the
toolbar popup and falls back to a separate window. The screen holds:
- the site's origin;
- a phishing warning, when the host is on the blocklist;
- the address that will be shared;
- the "Remember my choice for this site" checkbox;
- "Allow" and "Deny" buttons.
"Allow" with "Remember" ticked adds the origin to Allowed Sites. "Allow" without
it connects the site until the browser closes. "Deny" with "Remember" ticked
adds the origin to Denied Sites. Closing the popup without an answer is a
denial.
#### TxApproval (`approve-tx`)
Raised when a connected site calls `eth_sendTransaction`. Always opened in a
separate window. The screen holds:
- the site's origin, and the phishing warning when it applies;
- the decoded action, when the calldata is recognized (see Transaction
decoding);
- the From address and the contract or recipient, each in full with an explorer
link. The contract's symbol is shown when known;
- the value in ETH with its USD value;
- the maximum network fee, with the gas limit and the fee per gas under it;
- the network and the nonce;
- the raw calldata;
- a password input, and "Confirm" and "Reject" buttons.
Every value shown was filled in by the background before the window opened. The
background compares the signed transaction against every one of them. See Sites
and approvals.
"Confirm" with the right password signs the transaction as shown, hands it to
the background and opens WaitTx in the same window. A wrong password shows an
error. A refusal that spends the approval opens ErrorTx. "Reject" answers the
site with code `4001` and closes the window. Closing the window without an
answer leaves the request pending; the toolbar button then opens this screen.
#### SignApproval (`approve-sign`)
Raised when a connected site calls `personal_sign`, `eth_sign`,
`eth_signTypedData` or `eth_signTypedData_v4`. Always opened in a separate
window. The screen holds:
- the site's origin, and the phishing warning when it applies;
- the type: "Personal message" or "Typed data (EIP-712)";
- the From address;
- the message;
- a password input, and "Sign" and "Reject" buttons.
A personal message is shown as the text its bytes decode to, laid out in the
order of the bytes. Each control character, line or paragraph separator, and
character that paints nothing is shown as a bordered `U+XXXX` mark. Bytes that
are not UTF-8 read "This message is not text." The hex is shown under the text.
An `eth_sign` request carries the warning that the data will be signed as a
personal message.
Typed data is shown as its domain, the primary type `ethers` will sign, and the
message fields that type declares, in the type's order. Keys the type does not
declare are listed under "Not part of the signature". Typed data whose primary
type is `Permit` or one of Permit2's signature types gets a red warning at the
top. The warning names the spender and each token and amount the signature lets
the spender take. `Unlimited` marks the field's largest value. `Unknown` marks
anything the declared fields do not give.
"Sign" is disabled, with the reason on the error line, when:
- the typed data cannot be read, names no primary type, or names one other than
the type it would be signed as;
- a declared field is missing, or a `bool` field is not `true` or `false`;
- the domain's `chainId` is not the active network's;
- a personal message is not hex.
"Sign" with the right password signs and closes the window. "Reject" answers the
site with code `4001`. Closing the window leaves the request pending, as on
TxApproval.
#### NetworkApproval (`approve-network`)
Raised when a connected site calls `wallet_switchEthereumChain` naming a
supported network other than the active one. Opened in a separate window. The
screen holds:
- the site's origin, and the phishing warning when it applies;
- the current network and the requested network;
- a line saying the switch applies to the whole wallet and every site;
- "Switch" and "Reject" buttons.
"Switch" switches the network as Settings does and answers the site with
success. "Reject", or closing the window, answers the site with code `4001` and
changes nothing.
#### StateRecovery (`state-recovery`)
Shown when the stored profile fails the check in Stored data, at open or at a
save while the popup is open. The screen states that nothing has been changed
and nothing can be signed or sent. It names the problem in one sentence and
offers two controls:
- "Export Saved Data" puts the raw stored record in a text box on the screen,
and downloads `autistmask-saved-data.json` where the browser allows it.
- "Erase Saved Data" removes the record and reloads the popup into Welcome. The
user must type `ERASE MY WALLET` first.
The Settings gear is hidden on this screen. Once up, the screen stays until the
popup closes.
### Amounts
Token and ETH amounts are shown with exactly four decimal places, cut and never
rounded up (`truncateAmount()` in `src/shared/amountDisplay.js`). On the screens
that state what is being authorized, an amount below 0.0001 is extended to its
first nonzero digit instead (`truncateAmountNeverZero()`). So a nonzero amount
never reads as zero. Those screens are:
- TxApproval;
- ConfirmTx, and Send's current balance;
- WaitTx, SuccessTx and ErrorTx.
The balance lists round to four places. TransactionDetail shows the exact amount
and its base units.
A token amount in a site's calldata carries no scale, so the wallet needs the
token's `decimals`. It looks in three places, in this order
(`resolveTokenDecimals()` in `src/shared/approvalAmount.js`):
- the bundled token list;
- the tokens tracked on the transaction's network;
- what Blockscout reported for the token in the wallet's balances, while "Use
Blockscout" is on.
The token's name comes from the same places. When none of them answers, the
amount reads `5000000000 base units (decimals unknown)`, and nothing is
formatted at a guessed scale. A holding whose scale nothing knows shows
"quantity unknown" in the balance lists. Only a whole number from 0 to 80 counts
as a scale. While "Use Blockscout" is off, a token the first two sources cannot
name gets a line under it on the approval and status screens. The line says that
Blockscout's report is not used.
The wallet's own ERC-20 send is encoded at the scale the screen showed. At
signing time the contract's `decimals()` is read only to be compared with it. A
disagreement refuses the send (`src/shared/transferAmount.js`).
A native amount is labelled with the native currency of the transaction's chain:
`ETH` on mainnet, `SepoliaETH` on Sepolia (`nativeCurrencyByChainId()` in
`src/shared/networks.js`). The balance lists and Send use the active network's.
A transaction's time is shown as an ISO datetime and a relative age, written by
`isoDate()` and `timeAgo()` in `src/popup/views/helpers.js`. With "UTC
Timestamps" on, the datetime is in UTC.
Every nonzero holding is stored exactly. A holding below 0.000001 is left out
of:
- the balance lists;
- the Send dropdown;
- the address total;
- the DeleteAddress warning.
A displayed token symbol is cut to 12 characters
(`src/shared/symbolDisplay.js`).
#### Partial totals
Only a token with a Chainlink feed has a price. `getAddressValue()` in
`src/shared/prices.js` returns the value of the priced holdings and whether an
unpriced holding was left out. `formatAddressTotal()` renders it the same way on
every screen:
- on Sepolia, or before the first price fetch: no total;
- everything priced: `Total: $5,500.00`;
- part priced: `Total: $3,000.00 plus unpriced tokens`;
- nothing priced but something held: `Total: unpriced tokens only`.
### Addresses
Every address is shown in full, on a row of its own that never wraps (the
`.am-address` class in `src/popup/styles/main.css`). No screen truncates an
address. `truncateMiddle()` in `src/popup/views/helpers.js` removes at most 10
characters, and `renderAddressHtml()` floors a request at 32 characters, but no
caller asks for either today. Tapping an address copies it in full.
Each address gets a colour dot chosen from its first four hex digits, and an
identicon on the screens where space allows. Each address links to the block
explorer of its network: `etherscan.io` on mainnet and `sepolia.etherscan.io` on
Sepolia. The links contact nothing until clicked.
### Scam protection
Five defenses are on by default.
Known symbol spoofing. `KNOWN_SYMBOLS` in `src/shared/tokenList.js` maps each
symbol of the bundled list to the contracts that may bear it. `ETH` and every
network's native currency map to no contract at all. A token bearing a known
symbol from any other contract is a spoof (`isSpoofedSymbol()` in
`src/shared/symbolSpoof.js`). The symbol is compared after NFKC normalization,
with invisible characters removed and case ignored. The rule applies always to
the balance lists and the Send dropdown. In the transaction history it is the
setting "Hide fake tokens impersonating a known symbol".
Low-holder tokens. While "Hide tokens with fewer than 1,000 holders" is on, the
transaction history and the Send dropdown hide a token with a reported holder
count below 1,000. The balance list has its own floor: an untracked token
appears only when it is on the bundled list or has 1,000 or more reported
holders. A token whose count is unknown is never hidden from the history or the
dropdown, and never reaches the balance list unless bundled or tracked. Only a
whole number written in digits counts as a count (`src/shared/holders.js`).
Fraud contract blocklist. Detecting a spoofed transfer adds its contract to
`fraudContracts` in the profile. While "Hide transactions from detected fraud
contracts" is on, the history hides transfers from those contracts. The Send
dropdown always leaves them out. Turning the spoof setting off also stops new
entries being learned.
Dust transactions. While "Hide dust transactions below N gwei" is on, the
history hides native transfers below the threshold. Contract calls and token
transfers are never hidden as dust.
Scam addresses. `src/shared/scamlist.js` holds known fraud, drainer and phishing
addresses. ConfirmTx warns before a send to one of them. AddToken and
SettingsAddToken refuse to track one. The list holds addresses involved in fraud
only. It is not a sanctions list.
ConfirmTx also warns when the recipient is:
- a contract;
- a burn address;
- the user's own address;
- an address that has never sent a transaction and holds none of the network's
native token;
- while "Check recipients on Etherscan" is on, an address Etherscan labels as
phishing or scam.
#### Phishing domains
A domain blocklist is vendored into the extension at build time
(`make vendor-blocklist`, see Development). The artifact holds digests, not
names. Each entry is the first 16 hex characters of the sha256 of the lowercased
domain (`src/shared/domainHash.js`). The entries are concatenated in sorted
order. A lookup hashes the hostname and each parent domain and binary-searches
the string (`src/shared/phishingDomains.js`). A trailing dot is dropped first.
The check runs locally and sends nothing. The list is as current as the last
vendoring run that shipped. When the requesting site's host is on the list, all
four approval screens show a red warning.
### Transaction decoding
TxApproval and TransactionDetail try to decode calldata into a summary
(`decodeCalldata()` in `src/popup/views/approval.js`). SuccessTx repeats
TxApproval's summary. Decoding is a display aid. When it fails, the raw calldata
is shown in full. No contract gets any treatment beyond a legible confirmation
screen.
Two shapes are decoded.
ERC-20 `approve()` and `transfer()` calls show the token, the spender or
recipient, and the amount. An `approve` of the `uint256` maximum reads
`Unlimited`.
`src/shared/uniswap.js` decodes Uniswap Universal Router `execute()` calls. It
does so only for a transaction sent to one of the router deployments it lists
for the network: five on mainnet and four on Sepolia. The same calldata sent to
any other address is shown as raw data. The decoder reads each step as the
command that router release runs for its command byte. It names the action
`Swap IN → OUT` when both tokens are known. The lines are:
- `Protocol`: `Uniswap Universal Router`, with the router's address.
- `Token In` and `Token Out`: the currencies the swap spends and buys.
`ETH (native)` for ETH. `Unknown (not named in the calldata)` when no step
names one. `Unknown (could not be decoded)` when a V4 swap action could not be
read or the transaction has an `EXECUTE_SUB_PLAN` step.
- `Amount`: the total the steps take of `Token In` from the user. `Up to` is
prefixed when the transaction has an exact-out step. `Unlimited` marks an
amount at or above the `uint160` maximum. `All available (V4 open delta)` and
`Whatever an earlier step sent to the pair (V2 already paid)` name the two
amounts that are not quantities. The line is left out when the decoder cannot
bound what leaves the wallet; each amount it did read then gets lines of its
own.
- `Moved from the sender`, `Token moved` and `Moved to`: a take the `Amount`
line does not cover, or what a `PERMIT2_TRANSFER_FROM` moves and where.
- `Permit2 allowance`, `Allowance token` and `Allowance spender`: each allowance
a `PERMIT2_PERMIT` or `PERMIT2_PERMIT_BATCH` grants, unless the permit's
amount already stands as the `Amount` figure.
- `Paid to`: each address other than the sender that a step pays. When no step
pays anyone, the line reads `Unknown (not named in the calldata)`. The line
also covers four other cases:
- tokens moved into the router;
- a token that stays in the router after the last step;
- a deposit that pays a bridge;
- a payee that could not be read.
- `Min. received`: the minimum the sender gets. A zero minimum reads
`None (no minimum guaranteed)`. The label changes to `Min. paid out` in three
cases:
- another address is paid;
- a token stays in the router;
- an allowance goes to a spender other than the router.
- `Steps`: each command by name, in order. A step the router lets fail without
reverting is marked `(allowed to fail)`. A step the decoder could not read is
marked `(could not be decoded)`. An `EXECUTE_SUB_PLAN` step is marked
`(its steps are not shown)`.
- `Deadline`: the router's deadline as a UTC date and time. A deadline past the
range of a JavaScript date reads
`After 275760-09-13 00:00:00 (no deadline in practice)`.
The decoder reads these commands:
- `PERMIT2_PERMIT`, `PERMIT2_PERMIT_BATCH` and `PERMIT2_TRANSFER_FROM`;
- the V2, V3 and V4 swaps;
- `SWEEP`, `TRANSFER`, `PAY_PORTION` and `PAY_PORTION_FULL_PRECISION`;
- `WRAP_ETH` and `UNWRAP_WETH`;
- `BALANCE_CHECK_ERC20` and `ACROSS_V4_DEPOSIT_V3`.
Any other command is named in `Steps` but not read, and the summary then says so
where it matters. The comments of `src/shared/uniswap.js` hold the rules that
decide what the router still holds after each step. Those rules decide what the
`Paid to` and minimum lines say.
### Sites and approvals
The content script (`src/content/inpage.js`) puts a `window.ethereum` provider
on every page, unless another wallet already set one. It announces the provider
through EIP-6963 with a fresh random UUID per page load. The provider relays
each request through `src/content/index.js` to the background.
A site is its full origin: scheme, host and port. A choice for
`https://app.example` says nothing about `http://app.example`. The background
takes the origin from the browser, never from the message. A request whose
origin has no host, such as the `null` origin of a sandboxed page, is refused
with code `4100`.
The background answers these methods:
- `eth_requestAccounts` and `wallet_requestPermissions`: SiteApproval, unless
the site is already allowed, connected or denied.
- `eth_accounts` and `wallet_getPermissions`: the active address when the site
is allowed or connected, otherwise nothing.
- `eth_chainId` and `net_version`: the active network's values.
- `wallet_switchEthereumChain`: NetworkApproval, for a connected site naming a
supported network. An unsupported chain is refused with code `4902`. A site
that is not connected is refused with code `4100`.
- `wallet_addEthereumChain`: success for a supported chain, code `4902`
otherwise. It never switches the network.
- `personal_sign`, `eth_sign`, `eth_signTypedData` and `eth_signTypedData_v4`:
SignApproval, for a connected site. A request naming an address other than the
active one is refused with code `4100`.
- `eth_sendTransaction`: TxApproval, for a connected site, with the same address
rule.
- Read-only node methods such as `eth_call`, `eth_getBalance`, `eth_estimateGas`
and `eth_getTransactionReceipt` are passed to the RPC endpoint.
`PROXY_METHODS` in `src/background/index.js` is the list.
- Anything else is refused with code `4200`.
One transaction approval exists at a time, wallet-wide, because filling in a
transaction fixes its nonce. A second `eth_sendTransaction` while one is
unanswered is refused with code `-32002`. One site approval, one signature
approval and one network approval exist per site at a time, with the same code.
For `eth_sendTransaction`, the background fills in the transaction before any
window opens (`src/shared/approvalTx.js`). The nonce is the account's next nonce
from the node. The fees come from the node. The gas limit is the node's estimate
unless the site gave one. A site's nonce is ignored. A transaction the node
refuses, or that the node does not answer within 20 seconds, raises no window,
and the site gets the node's error. The popup signs exactly the object it was
shown. The background then checks the signed transaction (`verifySignedTx()` in
`src/shared/approvalVerify.js`):
- the signer is the address the approval was raised for;
- the type is 0, 1 or 2;
- the chain id is the active network's;
- every field the type serializes equals the approved transaction;
- the transaction rebuilt from those fields has the same bytes, and the artifact
is its canonical encoding;
- the gas limit and fees are within the ceilings.
A mismatch spends the approval: the site gets an error and the transaction is
not retried. A wrong password leaves the approval standing. After a broadcast
the nonce is recorded, and a second artifact at that nonce is refused.
Signatures are checked the same way: the background recovers the signer from the
signature and refuses any other address.
Switching the active address rejects every pending approval with code `4001` and
closes its window. It ends every connection allowed without "Remember", and
sends `accountsChanged` to every tab with permission.
Approval windows open at 396 by 600 pixels, centred on the browser window last
in focus. The window widens itself by however far the screen would scroll
sideways. A transaction or signature approval whose window closes stays pending.
The toolbar button then opens the oldest such approval instead of the wallet.
#### Ceilings
`assertWithinCeilings()` in `src/shared/approvalVerify.js` refuses a transaction
when:
- the gas limit is above 100,000,000;
- a fee per gas is above 100,000 gwei;
- the gas limit times the maximum fee per gas is above 1 ETH.
The check runs when a site's transaction is filled in, when its signed artifact
is verified, and before the wallet's own send is signed.
### External services
Ethereum JSON-RPC endpoint, contacted by default:
- balance reads (`eth_getBalance`, `balanceOf` through `eth_call`) every 10
seconds while the popup is open and every minute in the background;
- ENS reverse lookups of the user's addresses on each refresh, and of history
counterparties when a list is shown (cached for 12 hours in the popup);
- on mainnet, Chainlink `latestRoundData()` reads for ETH and for each held or
tracked token with a feed. They go as one batched request, at most every 5
minutes while the popup is open. A reading older than the feed's heartbeat
plus 30 minutes is no price;
- the address scan on import, fee estimates, nonces, broadcasts and receipt
lookups;
- the methods a connected site asks for from `PROXY_METHODS`.
Every ENS lookup stays on the RPC endpoint. A name whose resolver points
off-chain does not resolve.
Blockscout API, only while "Use Blockscout" is on:
- `/addresses/<address>/token-balances` on each refresh, for the untracked
tokens and the holder counts;
- `/addresses/<address>/transactions` and
`/addresses/<address>/token-transfers?type=ERC-20` when Home, AddressDetail or
AddressToken loads its history;
- `/transactions/<hash>` when TransactionDetail opens.
The stored setting is read before each of these requests and again when the
answer lands (`src/shared/storedSettings.js`). An answer that lands after the
setting went off is dropped. "Save" beside the Blockscout URL asks `/stats`
whether or not the setting is on. That request carries no user data.
Etherscan, only while "Check recipients on Etherscan" is on: ConfirmTx fetches
the recipient's address page from the network's explorer each time the screen is
shown. The page is read for a phishing or scam label. A failed fetch is ignored.
The recipient's address is the only data sent.
At the raised log level, `debugFetch()` in `src/shared/log.js` logs each request
it makes. It logs the HTTP method, the origin of the URL and, for a JSON-RPC
call, the method name. The path, the query string and the body are never logged.
### Dependencies
Four libraries ship in the extension. `package.json` names them and `yarn.lock`
pins them:
- `ethers` (MIT): every Ethereum operation. Recovery phrases, key derivation,
signing, transaction construction, ERC-20 calls and JSON-RPC.
- `libsodium-wrappers-sumo` (ISC): Argon2id and XSalsa20-Poly1305 for the vault.
- `qrcode` (MIT): the QR code on Receive.
- `ethereum-blockies-base64` (ISC): the identicons.
The development dependencies ship nowhere:
- `esbuild`, the bundler;
- `tailwindcss` and `@tailwindcss/cli`, the stylesheet compiler;
- `jest`, the test runner;
- `prettier`, the formatter;
- `eslint`, with `@eslint/js` and `globals`;
- `playwright-core`, for the Chrome browser suite.
### Crypto policy
No cryptographic primitive is implemented in this repository. Every
cryptographic operation goes through `ethers` or `libsodium-wrappers-sumo`. An
exception needs a code comment that cites this policy. There is none.
### Content Security Policy
Both manifests declare the same policy for extension pages:
```
default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; object-src 'self';
style-src 'self'; img-src 'self' data:;
connect-src 'self' https: http:; frame-src 'none'; form-action 'none';
base-uri 'none'
```
- `default-src 'self'` is the floor. Everything not named falls back to it.
- `script-src 'wasm-unsafe-eval'` lets libsodium compile its WebAssembly build.
Without it libsodium falls back to a JavaScript translation, and a key
derivation takes about 20 times longer. The keyword permits WebAssembly only,
not `eval()` or inline script.
- `style-src 'self'` refuses inline styles. The markup carries Tailwind classes
and no `style` attribute. Script may still set `element.style`.
- `img-src data:` admits the identicons, which are `data:` PNGs.
- `connect-src https: http:` admits any RPC endpoint, a local node over
`http://127.0.0.1` included. What the wallet contacts is bounded by its code,
not by this directive.
- `frame-src 'none'`, `form-action 'none'` and `base-uri 'none'` are named
because the last two do not fall back to `default-src`.
`tests/manifest.test.js` pins the exact policy in both manifests.
`tests/vaultBackend.test.js` asserts the unit tests run libsodium's WebAssembly
backend. The Chrome browser suite compiles a WebAssembly module in the real
popup.
### Debug mode
`DEBUG` in `src/shared/constants.js` is a build-time constant. `build.js` sets
it to `true` only when `AUTISTMASK_DEBUG=1`. A debug build:
- shows the red `DEBUG / INSECURE` banner, with the view id appended;
- raises the log level;
- makes the die button on AddWallet produce one fixed, publicly known recovery
phrase.
Never distribute a debug build. `make build` never produces one;
`make build-debug` is the only target that does.
The debug checkbox in Settings is a separate runtime flag. It raises the log
level and shows the banner, and nothing else. `generateMnemonic()` reads the
build-time constant, so no runtime setting in a release build can reach the test
phrase. Adding any other branch on `DEBUG` needs the project owner's approval.
### Appearance
- Black text on a white background, or white on black in the dark theme. Colour
marks errors, warnings, address dots and identicons.
- Every piece of information is text in the system monospace font. Identicons
are the only images.
- Every control looks like a control: buttons have a border and a hover state,
and clickable text is underlined.
- Nothing moves when a result arrives. Every element filled in later has its
space reserved. An empty USD value is written as `&nbsp;`.
- The popup is 396 pixels wide (`body` in `src/popup/styles/main.css`) and up to
600 pixels tall, the most a toolbar popup gets.
- Styling is Tailwind utility classes in the markup. `main.css` adds four
classes of its own: `.copy-flash-active` and `.copy-flash-fade` for the copy
feedback, `.am-address` for the rule that an address never wraps, and
`.am-byte-order` for the signature screen's message.
- No framework. The popup is plain JavaScript with one module per screen.
### Wording
- "Recovery phrase", never "seed phrase" or "mnemonic".
- "Address", never "account".
- "Password", never "encryption key" or "passphrase".
- Buttons use plain verbs: "Send", "Receive", "Add", "Back", "Allow", "Deny".
- Every error message is a full sentence.
## Development
### Entry points
The repository follows
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all).
The `Makefile` targets are thin shims over `script/`:
- `script/bootstrap`: install the dependencies. Node and yarn come from the
system or, failing that, from a pinned nvm install. Fails when node cannot
find a package named in `package.json`.
- `script/setup`: `script/bootstrap`, then the git pre-commit hook.
- `script/test`: build the `test` stage of the `Dockerfile` with `--no-cache`.
The stage runs the jest suite with a 90-second timeout, rerun verbose on
failure, then `script/test-verify-build`.
- `script/lint`: build the `lint` stage with `--no-cache`. The stage runs
ESLint, the prettier check and `script/check-censored`. Linting runs only in
Docker.
- `script/fmt` and `script/fmt-check`: run the repository's own `prettier` on
the host, writing or checking. Both refuse to run with a `prettier` from the
`PATH`.
- `script/check`: `script/test`, `script/lint`, then `script/fmt-check`. It
changes no file.
- `script/cibuild`: `script/bootstrap`, `script/check`, then the image build of
`script/docker`. The `check` workflow runs it on every push.
- `script/docker`: build the Docker image with `--no-cache`, tagged with
`script/projectname`, passing `git describe` as `VERSION`.
- `script/precommit`: what the pre-commit hook runs. It runs `script/check`.
- `script/install-precommit`: install the hook.
- `script/projectname`: prints `autistmask`.
- `script/package`: `make build`, then the release archives (see Release
archives).
- `script/check-censored`: fail when the other wallet's name appears anywhere
but its allowed places. See Policies.
- `script/vendor-blocklist`: refresh `src/shared/phishingBlocklist.json` from
its upstream, pinned to a commit and to the sha256 of the fetched bytes.
- `script/verify-build --expect release|debug --receipt PATH`: check `dist/`
against a build receipt (see Builds).
- `script/discard-dist-on-failure COMMAND`: run one step of `make build` and
remove `dist/` if the step fails.
- `script/test-verify-build`: exercise every failure of `script/verify-build`
and `script/discard-dist-on-failure` against a fixture tree. Part of the
`test` stage.
- `script/test-e2e` and `script/test-e2e-firefox`: the browser suites (see
Browser tests).
Targets with no script of their own:
- `make install`: `yarn install --frozen-lockfile` alone.
- `make hooks`: `script/install-precommit`.
- `make build` and `make build-debug`: see Builds.
- `make icons`: redraw the toolbar icons in `icons/` from `script/lib/icons.js`.
`tests/icons.test.js` fails while the files differ from the drawing.
- `make enrich-tokens`: fetch a name and website for each bundled token into
`script/lib/token-enrichment.json`.
- `make apply-enrichment`: write that data into `src/shared/tokenList.js`.
`tests/applyEnrichment.test.js` fails while the list differs from what this
writes.
- `make clean`: remove `dist/` and `release/`.
- `make dev`: build without the checks, then build again after every change
under `src/`, `manifest/` or `icons/`. A watch build writes no receipt.
### Builds
`build.js` empties `dist/` and compiles the Tailwind stylesheet. It then bundles
four entry points per browser with `esbuild`: the popup, the background, the
content script and the inpage provider. It copies the popup markup, the
stylesheet, the icons and the manifest into `dist/chrome/` and `dist/firefox/`.
It compiles into the bundles:
- the version;
- the commit hash, marked `-dirty` when the working tree is modified;
- the build date;
- `DEBUG`.
There is one version. `package.json`, `manifest/chrome.json` and
`manifest/firefox.json` each declare it, and `script/lib/version.js` fails the
build when they disagree.
The background bundle may not contain `src/shared/state.js`. `build.js` checks
that against `esbuild`'s own report of what each bundle contains, from the table
in `script/lib/forbiddenBundleInputs.js`. An ESLint rule reports the same
prohibition in the editor.
`build.js` records every file it writes, with its sha256, in a build receipt.
The `Makefile` creates the receipt path with `mktemp` outside the repository and
deletes it afterwards. `script/verify-build` then checks that `dist/` holds
exactly the recorded files with exactly the recorded bytes. It also checks that
each bundle containing `src/shared/constants.js` carries the requested `DEBUG`
marker. For a release build it checks that no file carries the test recovery
phrase. The expected mode is an argument, never read from the environment.
`make build` wraps every step in `script/discard-dist-on-failure`. A release
build that fails removes `dist/` and says so, because a failed release build may
have left a complete, loadable debug bundle behind. `make build-debug` keeps its
`dist/` on failure. `make build` ends by running `script/check-censored` with
`--require-dist` against the built files.
### Tests
`make test` runs the jest suite in Docker. The suite must finish within 90
seconds. `tests/popupElementIds.test.js` checks that every element id the popup
looks up exists in `src/popup/index.html` and that no id is defined twice.
`tests/persistedFieldContract.test.js` boots the real popup entry point over
hostile stored values. `make check` runs the tests, the lint stage and the
formatting check.
### Browser tests
Two suites drive the real popup in a real browser. They share no code. Both need
Docker and nothing else. Each builds an image carrying the repository and a
fresh extension build, under a tag of its own, and removes the image afterwards.
Neither is part of `make check`. Run them before changing anything under
`src/popup/views/`.
`make test-e2e` runs three programs in a pinned Playwright container:
- `tests/e2e/run.js` drives the popup and a local test page in Chromium. Every
request from the popup and the background worker is answered from fixtures in
`tests/e2e/network.js`. An unexpected request fails the run, and so does any
uncaught page error or `console.error`. It covers:
- the Welcome settings, and what reaches Blockscout and Etherscan;
- wallet creation, Add Token and TransactionDetail;
- the recovery phrase and private key screens;
- address removal and Settings;
- ConfirmTx's fee and balance checks, and the wallet's own ERC-20 send;
- the receipt wait and StateRecovery;
- the site round trips: connect, `personal_sign`, `eth_signTypedData_v4`,
`eth_sendTransaction` and `wallet_switchEthereumChain`.
- `tests/e2e/storagePartition.js` loads the extension from two directories into
one profile, with and without the manifest `key`, and asserts what Chrome does
with its storage.
- `tests/e2e/slowApproval.js` leaves a transaction approval and a signature
approval open for seven minutes with their windows minimized, then approves
both. It starts Chromium itself and speaks the DevTools protocol, because
Chrome never stops a worker that Playwright is attached to.
`make test-e2e-firefox` runs two programs in an image with a pinned Firefox and
geckodriver. The container has no network. A loopback server in
`tests/e2e/dapp.js` serves the test page and a stub node.
- `tests/e2e/firefox/run.js` installs `dist/firefox/` as a temporary add-on. It
covers popup load, StateRecovery, wallet creation, Add Token and the site
round trips. Errors are read from Firefox's console service at each step
boundary. Any uncaught error from an extension source fails the run.
- `tests/e2e/firefox/reinstall.js` installs the packaged `.xpi`, creates a
wallet, restarts the browser, adds the add-on again and decrypts the vault
through the real ShowRecoveryPhrase screen. It then uninstalls and reinstalls,
and asserts that Firefox destroyed the storage.
`.gitea/workflows/e2e.yml` runs both suites on every push as two jobs, separate
from the `check` workflow. `e2e-chrome` has a 40-minute cap and `e2e-firefox` a
15-minute cap. The jobs report; they do not gate a merge.
## Policies
- The other wallet is not named in code or documentation.
`script/check-censored` fails the lint stage and the build when the name
appears outside its allowed places:
- the two provider flags in `src/content/inpage.js`;
- one token's on-chain name in `src/shared/tokenList.js` and
`script/lib/token-enrichment.json`;
- the upstream source in `script/vendor-blocklist`.
- This file is the authoritative technical documentation of the extension. It
may be long.
## License
GPL-3.0. See [LICENSE](LICENSE).
Three data files come from other projects under their own licenses. The full
license texts are in [LICENSE](LICENSE).
- `src/shared/phishingBlocklist.json` is derived from the `eth-phishing-detect`
community blocklist, `src/config.json`. Copyright (c) 2018 kumavis. DBAD
license. `script/vendor-blocklist` records the upstream, the pinned commit and
the sha256 of the fetched bytes.
- `src/shared/scamlist.js` holds addresses from
[ethereum-lists](https://github.com/MyEtherWallet/ethereum-lists)
`addresses-darklist.json`. Copyright (c) 2020 MyEtherWallet. MIT license.
- `src/shared/scamlist.js` also holds addresses from
[EtherScamDB](https://github.com/MrLuit/EtherScamDB) `scams.yaml`. Copyright
(c) 2018 Luit Hollander. MIT license.
## Author
[@sneak](https://sneak.berlin)