README: second rework after review
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

Fix the 23 findings of the second review and the two leftovers from the
first: SiteApproval's connection lifetime, the stateSchema.js module line,
the flash line and title bar, the six network-bound screens, the token-only
0.000001 cutoff, the 12-character symbol cap, the time display, the native
currency labels, the truncateAmountNeverZero screens, the browser suite
rules, the build receipt contents, the External services list, the worker
memory exceptions, POPUP_ONLY_TYPES, the undefined terms, the comma lists,
the Rationale, Entrypoints and TODO sections, and the dropped crypto comment
rule. A fact pass then checked every statement about the code and every
absolute against each case, and a newcomer read removed repeats.

Model: fable-5-1
This commit is contained in:
2026-11-02 02:15:46 +00:00
parent a17563b5bc
commit 93b54c85e8
+236 -178
View File
@@ -1,10 +1,9 @@
# 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).
AutistMask is a GPL-3.0-licensed browser extension by
[@sneak](https://sneak.berlin): an Ethereum wallet for Chrome and Firefox.
The wallet does four things:
AutistMask does four things:
- It holds wallets made from a recovery phrase, a private key or an extended
private key.
@@ -20,7 +19,7 @@ It does nothing else. It has:
- no advertising;
- no tracking.
## Why it exists
## Rationale
Popular browser wallets have grown:
@@ -48,7 +47,8 @@ Settings. The wallet uses that endpoint to:
The RPC endpoint sees every address that the wallet queries and every
transaction that the wallet sends. A user who wants privacy can point the RPC
URL, and the Blockscout URL, at a node of their own.
URL at a node of their own, and the Blockscout URL at a Blockscout server of
their own.
Two more services receive the user's data only after the user turns on a
setting. Both settings are off in a new install. The Welcome screen and Settings
@@ -67,7 +67,7 @@ sends nothing anywhere.
The section External services below lists each request.
## Getting started
## Getting Started
Build the extension from a clone:
@@ -113,12 +113,12 @@ which this repository does not produce.
`manifest/firefox.json` requires Firefox 115 or newer.
The `.xpi` that `make package` writes is not signed. Release Firefox and Firefox
ESR (the extended support release) refuse to install an unsigned add-on, and
`xpinstall.signatures.required` has no effect on them. A permanent install of
the unsigned file needs Firefox Developer Edition, Nightly or an unbranded build
(Mozilla's build without the Firefox branding). On those,
`xpinstall.signatures.required` must be set to `false` in `about:config`.
The `.xpi` (Firefox's add-on package file) that `make package` writes is not
signed. Release Firefox and Firefox ESR (the extended support release) refuse to
install an unsigned add-on, and `xpinstall.signatures.required` has no effect on
them. A permanent install of the unsigned file needs Firefox Developer Edition,
Nightly or an unbranded build (Mozilla's build without the Firefox branding). On
those, `xpinstall.signatures.required` must be set to `false` in `about:config`.
Every Firefox, release builds included, can load the extension as a temporary
add-on from `about:debugging`. A temporary add-on is unloaded when Firefox
@@ -173,8 +173,8 @@ offers "Use Blockscout" and "Check recipients on Etherscan".
- "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.
- "UTC Timestamps": off. While off, a transaction's datetime is 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.
@@ -184,9 +184,9 @@ offers "Use Blockscout" and "Check recipients on Etherscan".
- Tracked Tokens: the ERC-20 tokens tracked on the active network. Each network
has its own list.
- Allowed Sites: the sites allowed with "Remember my choice for this site".
- Connected Sites: the sites allowed without it. Such a connection ends when the
browser closes, when the active address changes, or when the site is removed
in Settings.
- Connected Sites: the sites allowed without "Remember my choice for this site".
Such a connection ends when the browser closes, when the active address
changes, or when the site is removed in Settings.
- Denied Sites: the sites denied with "Remember my choice for this site".
- "Remember my choice for this site": the checkbox on SiteApproval. Its last
state is remembered. Default ticked.
@@ -222,8 +222,8 @@ src/
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
viewRouter.js draws a screen that "Back" returns to when this page
load has not drawn it yet
theme.js applies the theme
dustThreshold.js parses the dust threshold field
styles/main.css Tailwind source, plus four classes of its own
@@ -234,8 +234,9 @@ manifest/
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
script/ the entrypoints (see Development)
tests/ jest unit tests and their helpers; tests/e2e/ holds the
browser suites
```
The shared modules, each in one line:
@@ -253,7 +254,8 @@ The shared modules, each in one line:
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.
- `buildInfo.js`: the version, license, author, 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
@@ -272,8 +274,8 @@ The shared modules, each in one line:
- `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 profile read goes
through.
- `stateSchema.js`: `schemaVersion` and `assertStateUsable()`, the check that
the two state modules run on each read.
- `storedSettings.js`: reads "Use Blockscout" and "Check recipients on
Etherscan" from storage.
- `symbolDisplay.js`: the 12-character cap on a displayed symbol.
@@ -287,8 +289,8 @@ The shared modules, each in one line:
- `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.
- `walletDefects.js`: the notice for an xprv wallet imported from a key that was
not a master key.
- `walletDelete.js`: removing a wallet or an address from the profile.
### Wallets, keys and encryption
@@ -301,14 +303,18 @@ A wallet is one of three types:
- 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 no BIP-39 passphrase (the optional extra word some
wallets add to a phrase). A new recovery phrase has 12 words. An extended
All key derivation and signing goes through `ethers`. Addresses are derived on
the path `m/44'/60'/0'/0/n`, with no BIP-39 passphrase (the optional extra word
some wallets add to a phrase). A new recovery phrase has 12 words. An extended
private key must be a master key, the root of the key tree. AddWallet refuses a
child key, one derived below the root. Each HD or xprv address record stores its
`index`, and signing derives the key at that index. `getSignerForAddress()` in
`src/shared/wallet.js` refuses a key that does not belong to the address.
An xprv wallet imported from a key that was not a master key carries a notice in
the wallet list (`src/shared/walletDefects.js`). Such a wallet shows no "+"
button, and Send, site signing and private key export refuse it.
The secret of each wallet is encrypted with that wallet's password
(`src/shared/vault.js`):
@@ -322,12 +328,14 @@ 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:
Only the wallet secrets are encrypted. Everything else in the profile is stored
as plain data, among it:
- the `xpub` of each HD or xprv wallet, the extended public key that its
addresses are derived from;
- the addresses and their balances;
- the tracked tokens;
- the site lists;
- the settings.
The popup shows all of that without a password. The password is required to:
@@ -350,12 +358,12 @@ 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 profile is never kept in module variables. The worker reads it 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 refresh is skipped when the last refresh finished less than 30
one minute. The refresh is skipped when the last refresh started 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
@@ -363,11 +371,12 @@ Three rules follow from the worker being stopped:
worker stopping. Such a connection ends when the browser closes, when the
active address changes, or when the site is removed in Settings.
Pending approvals are the exception. An approval, and the site's request waiting
on it, live in the worker's memory. While any approval is pending, the worker
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.
Pending approvals and the record of broadcast nonces are the exceptions. Both
live in the worker's memory, and both are lost when the worker stops. An
approval, and the site's request waiting on it, exist nowhere else. So 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.
@@ -431,36 +440,40 @@ 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.
Every other screen falls back to Home. A restorable screen whose data is gone
falls back to Home too. 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 that it lands on.
"Back" onto a screen that this page load has not rendered renders that screen
first (`src/popup/viewRouter.js`). When the data for that screen is gone, Home
is shown instead. "Back" onto a screen already on the page only unhides it, so
unsaved input in Settings survives. Home is rendered again on every "Back".
A reopened popup draws only the wallet list and the screen that it lands on.
When "Back" returns to a screen that this page load has not drawn yet,
`src/popup/viewRouter.js` draws it first. When the data for that screen is gone,
Home is shown instead. "Back" onto a screen already drawn only unhides it, so
unsaved input in Settings survives. Home is drawn 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. Five screens are drawn for the network of the
transaction that they show, whichever network is active:
screen that cannot be drawn from saved state. The cut also drops AddressToken
unless it shows ETH. After either change, and after a change to "Check
recipients on Etherscan", every screen but Settings counts as not drawn.
Settings on top is redrawn in place. Any other screen is redrawn as a reopened
popup would draw it. Six screens are drawn for the network of the transaction
that they show, whichever network is active:
- ConfirmTx;
- WaitTx;
- SuccessTx;
- ErrorTx;
- TransactionDetail.
- TransactionDetail;
- TxApproval.
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.
Two things are present on every screen but StateRecovery: the title bar with the
Settings gear, and the flash message line under it. The flash line holds one
line of text for two seconds.
Every screen has the title bar and the flash message line under it. A flash
message stays two seconds unless the screen sets another time. StateRecovery
hides the Settings gear in the title bar.
On Sepolia, or with debug on, a red banner sits at the top of the page. It reads
`[TESTNET]` on Sepolia, `DEBUG / INSECURE` with debug on, and
@@ -493,10 +506,14 @@ Ticking a checkbox saves the setting at once. "Add wallet" opens AddWallet.
Shown while at least one wallet exists. The screen holds:
- the active address's ETH balance, with its USD value in parentheses;
- the active address's ETH balance, with its USD value in parentheses when a
price is known;
- 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;
- the active address:
- its colour dot;
- the full address. Tapping it copies it;
- an explorer link;
- "Send" and "Receive" buttons for the active address;
- the ETH/USD price, on mainnet;
- the wallet list;
@@ -506,8 +523,9 @@ Shown while at least one wallet exists. The screen holds:
- an "Add additional wallet..." link.
Each wallet in the list shows its name. Tapping the name renames it in place. HD
and xprv wallets also show a "+" button that derives the next address. Each
address of a wallet is a block:
and xprv wallets also show a "+" button that derives the next address, unless
the wallet carries the notice from `src/shared/walletDefects.js`. 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
@@ -539,7 +557,7 @@ Transitions:
- `[info]`: AddressDetail.
- `[x]`: DeleteAddress.
- "Send": Send. Refused with a flash message when the active address has a zero
ETH balance.
ETH balance or belongs to a wallet carrying the `walletDefects.js` notice.
- "Receive": Receive.
- Tap a transaction row: TransactionDetail.
- "Add additional wallet...": AddWallet.
@@ -572,11 +590,12 @@ the RPC endpoint, five at a time. The scan stops after five unused addresses in
a row. When the scan finds two or more used addresses, they replace the wallet's
list. The flash line reports the count.
Refusals:
A missing, short or mismatched password gets a full sentence on the error line.
A flash message refuses:
- a missing, short or mismatched password: a full sentence on the error line;
- an invalid phrase or key, an extended private key that is not a master key, or
a phrase or address already in a wallet: a flash message.
- an invalid phrase or key;
- an extended private key that is not a master key;
- a phrase or address already in a wallet.
"Back" returns to the previous screen.
@@ -587,7 +606,8 @@ 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 full address, with its ENS name and an explorer link. Tapping the address
copies it;
- the address's USD total;
- the balance list: ETH, then each token shown for the address. Each row opens
AddressToken;
@@ -601,7 +621,7 @@ Transitions:
- a balance row: AddressToken;
- "Send": Send. Refused with a flash message when the address has a zero ETH
balance;
balance or belongs to a wallet carrying the `walletDefects.js` notice;
- "Receive": Receive;
- "+ Token": AddToken;
- "Export Private Key": ExportPrivKey;
@@ -650,8 +670,9 @@ Reached by tapping a balance row on AddressDetail. The screen holds:
"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.
and contract address. "Receive" opens Receive, with the ERC-20 warning for a
token. A transaction row opens TransactionDetail. "Back" returns to
AddressDetail.
#### Send (`send`)
@@ -669,7 +690,7 @@ Reached from Home, AddressDetail or AddressToken. The screen holds:
The dropdown offers ETH and the tokens of the address's balance list. It leaves
out:
- holdings below 0.000001;
- token 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
@@ -799,8 +820,8 @@ 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.
- when opened from an ERC-20 token's screen, a warning to send only that token
on this network to this address.
"Back" returns to the previous screen.
@@ -848,7 +869,8 @@ Reached from "+ Token" on AddressDetail. The screen holds:
"Add" looks up `symbol()`, `decimals()` and `name()` on the contract through the
RPC endpoint. On success it adds the token to the tracked list, pops the stack
and shows AddressDetail. A flash message refuses:
and shows AddressDetail. If another screen is showing by then, the screen does
not change. A flash message refuses:
- a token already tracked;
- an address on the scam list;
@@ -862,8 +884,8 @@ says so.
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.
name, a `[recovery phrase]` button on HD wallets, and an `[x]` button. Tapping
the name renames it in place.
- Passwords: the "Allow weak passwords" checkbox.
- Tracked Tokens: one row per token tracked on the active network with an `[x]`
button, and "+ Add token".
@@ -898,8 +920,9 @@ 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.
every address's balances and the fraud contract blocklist. 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 the new
value. Unticking "Use Blockscout" also drops every untracked token from every
@@ -1004,8 +1027,8 @@ separate window. The screen holds:
- "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
it adds the site to Connected Sites (see Settings). "Deny" with "Remember"
ticked adds the origin to Denied Sites. Closing the popup without an answer is a
denial.
#### TxApproval (`approve-tx`)
@@ -1061,11 +1084,11 @@ as a personal message.
Typed data is shown as its domain, the primary type that `ethers` will sign, and
the message fields that this type declares, in the type's order. Keys that the
type does not declare are listed under "Not part of the signature". Typed data
whose primary type is `Permit`, or one of the signature types of Permit2
(Uniswap's token allowance contract), gets a red warning at the top. The warning
names the spender, and each token and amount that the signature lets the spender
take. `Unlimited` marks the field's largest value. `Unknown` marks anything the
type does not declare are listed under "Not part of the signature". A red
warning heads the message when the primary type is `Permit` or a Permit2
signature type. Permit2 is Uniswap's token allowance contract. The warning names
the spender, and each token and amount that the signature lets the spender take.
`Unlimited` marks the field's largest value. `Unknown` marks anything the
declared fields do not give.
"Sign" is disabled, with the reason on the error line, when:
@@ -1112,15 +1135,23 @@ popup closes.
### Amounts
The same data is formatted the same way on every screen. Token and ETH amounts
show four decimal places. History rows cut amounts to four decimals and never
round up (`truncateAmount()` in `src/shared/amountDisplay.js`). The balance
lists and Home's ETH balance round to four places. The screens that state what
is being authorized cut as the history does, and extend an amount below 0.0001
to its first nonzero digit (`truncateAmountNeverZero()`). So a nonzero amount
never reads as zero there. Those screens are:
These helpers write one kind of value on every screen that shows it:
- `formatUsd()` in `src/shared/prices.js`: USD values;
- `isoDate()` and `timeAgo()` in `src/popup/views/helpers.js`: times;
- `renderAddressHtml()` in the same file: most addresses.
Amounts are written by the two helpers below, or rounded by the balance lists.
Most screens show token and ETH amounts with four decimal places. History rows
cut amounts to four decimals and never round up (`truncateAmount()` in
`src/shared/amountDisplay.js`). The balance lists and Home's ETH balance round
to four places. The screens that state what is being authorized cut as the
history does, and extend an amount below 0.0001 to its first nonzero digit
(`truncateAmountNeverZero()`). So a nonzero amount never reads as zero there.
Those screens are:
- TxApproval;
- SignApproval's Permit warning;
- ConfirmTx, and Send's current balance;
- WaitTx, SuccessTx and ErrorTx.
@@ -1149,7 +1180,8 @@ as a token's decimals. A token's `decimals` is stored only as a source reported
it, never guessed. A `null` balance means the decimals are unknown, never that
the balance is zero. While "Use Blockscout" is off, the approval and status
screens add a line under a token that neither the bundled list nor the tracked
tokens name. The line says that Blockscout's report is not used.
tokens name or give decimals for. The line says that Blockscout's report is not
used.
The wallet's own ERC-20 send is encoded with the decimals the screen showed. At
signing time the contract's `decimals()` is read only to be compared with those.
@@ -1158,21 +1190,23 @@ 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.
The decoded swap lines call the native currency `ETH` on both networks.
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
TransactionDetail shows the time as an ISO datetime and a relative age. A
history row shows the age, with the datetime as its tooltip. With "UTC
Timestamps" on, the datetime is in UTC.
Every nonzero holding is stored exactly. A holding below 0.000001 is left out
of:
Every nonzero holding is stored exactly. A token 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`).
Most screens cut a token symbol to 12 characters
(`src/shared/symbolDisplay.js`). The approval screens, the decoded calldata
lines and Send's "Current balance" line show it in full.
#### Partial totals
@@ -1189,12 +1223,11 @@ same way on every screen:
### Addresses
Every address is shown in full, on a row of its own that never wraps (the
`.am-address` class in `src/popup/styles/main.css`). No screen shortens an
address shown as an address. `truncateMiddle()` in `src/popup/views/helpers.js`
removes at most 10 characters, and `renderAddressHtml()` never shortens an
address below 32 characters, but no caller shortens one today. A token with no
known symbol is labelled by the first 10 characters of its contract address and
`…` (`resolveSymbol()` in `src/shared/tokenList.js`); Send and AddressToken show
`.am-address` class in `src/popup/styles/main.css`). No caller shortens an
address. `truncateMiddle()` in `src/popup/views/helpers.js` removes at most 10
characters, and `renderAddressHtml()` keeps at least 32. A token with no known
symbol is labelled by the first 10 characters of its contract address and `…`
(`resolveSymbol()` in `src/shared/tokenList.js`); Send and AddressToken show
that label.
On most screens, tapping an address copies it, and an explorer link sits beside
@@ -1203,12 +1236,13 @@ Each explorer link opens the block explorer of the address's network:
`etherscan.io` on mainnet and `sepolia.etherscan.io` on Sepolia. The links
contact nothing until clicked.
Each address gets a colour dot chosen from its first four hex digits, and an
identicon on the screens where space allows.
Each address gets a colour dot chosen from its first four hex digits. An
identicon appears on AddressDetail, AddressToken, ExportPrivKey, ConfirmTx and
TransactionDetail.
### Scam protection
Five defenses are on by default.
Six defenses are built in. Four of them are settings, 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
@@ -1232,8 +1266,9 @@ 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.
dropdown always leaves them out. Turning off "Hide fake tokens impersonating a
known symbol" also stops new entries being added. A network switch clears the
list.
Dust transactions. While "Hide dust transactions below N gwei" is on, the
history hides native transfers below the threshold. Contract calls and token
@@ -1245,15 +1280,7 @@ has approved them). ConfirmTx warns before a send to one of them. AddToken and
SettingsAddToken refuse to track one. The list holds addresses involved in fraud
only. It is not a sanctions list.
ConfirmTx also warns when the recipient is:
- a contract;
- a burn address;
- the sending address itself;
- an address that has never sent a transaction and holds none of the network's
native token;
- while "Check recipients on Etherscan" is on, an address Etherscan labels as
phishing, scam or exploiter.
The other warnings on ConfirmTx are listed under that screen.
#### Phishing domains
@@ -1292,17 +1319,18 @@ 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.
`Token In` reads `ETH (native)` for ETH; `Token Out` reads `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 that the steps take of `Token In` from the user. `Up to`
is prefixed when the transaction has a V2 or V3 exact-out step. `Unlimited`
marks an amount at or above the `uint160` maximum.
`All available (V4 open delta)` and
`Whatever an earlier step sent to the pair (V2 already paid)` name the two
amounts that are not quantities. The line is left out when the decoder cannot
bound what leaves the wallet; each amount that it did read then gets lines of
its own.
tell the most that can leave the wallet; each amount that it did read then
gets lines of its own.
- `Moved from the sender`, `Token moved` and `Moved to`: an amount taken from
the sender that the `Amount` line does not include. The same three lines show
what a `PERMIT2_TRANSFER_FROM` moves and where.
@@ -1323,10 +1351,11 @@ when both tokens are known. The lines are:
- no `Paid to` line is shown;
- no allowance goes to a spender other than the router.
- `Steps`: each command by name, in order. A step that the router lets fail
without reverting is marked `(allowed to fail)`. A `BALANCE_CHECK_ERC20` step
that the decoder could not read, and a V4 step with a swap action that it
could not read, are marked `(could not be decoded)`. An `EXECUTE_SUB_PLAN`
step is marked `(its steps are not shown)`.
without reverting is marked `(allowed to fail)`. An `EXECUTE_SUB_PLAN` step is
marked `(its steps are not shown)`. Two kinds of step are marked
`(could not be decoded)`:
- a `BALANCE_CHECK_ERC20` step that the decoder could not read;
- a V4 step with a swap action that it could not read.
- `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)`.
@@ -1348,8 +1377,9 @@ decide what the router still holds after each step. Those rules decide what the
The inpage provider (`src/content/inpage.js`) puts a `window.ethereum` provider
on every page, unless another wallet already set one. It announces the provider
through EIP-6963 with a fresh random UUID per page load. The content script
(`src/content/index.js`) relays each request to the background.
through EIP-6963, the standard that lets a page list the installed wallets, with
a fresh random UUID per page load. The content script (`src/content/index.js`)
relays each request to the background.
A site is its full origin: scheme, host and port. A choice for
`https://app.example` says nothing about `http://app.example`. The background
@@ -1365,8 +1395,9 @@ The background answers these methods:
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`.
supported network other than the active one. A request naming the active
network is answered with success at once. 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`:
@@ -1380,9 +1411,9 @@ The background answers these methods:
includes `eth_sendRawTransaction`.
- Anything else is refused with code `4200`.
The background also answers messages from the extension's own pages, such as
`AUTISTMASK_GET_CONNECTED_SITES` and `AUTISTMASK_REMOVE_SITE`. It answers them
only when the sender is one of those pages.
The background also answers messages from the extension's own pages. The types
in `POPUP_ONLY_TYPES` in `src/background/index.js` are answered only when the
sender is one of those pages.
One transaction approval exists at a time, wallet-wide, because filling in a
transaction fixes its nonce. A second `eth_sendTransaction` while one is
@@ -1390,13 +1421,13 @@ 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 are the site's when it gave them, otherwise the node's.
The gas limit is the node's estimate unless the site gave one. A site's nonce is
ignored. When the node refuses the transaction or gives no answer within 20
seconds, no window opens and the site gets the node's error. The popup signs
exactly the object that it was shown. The background then checks the signed
transaction (`verifySignedTx()` in `src/shared/approvalVerify.js`):
window opens (`src/shared/approvalTx.js`). The nonce is the sending address's
next nonce, read from the node. The fees are the site's when it gave them,
otherwise the node's. The gas limit is the node's estimate unless the site gave
one. A site's nonce is ignored. When the node refuses the transaction or gives
no answer within 20 seconds, no window opens and the site gets an error. The
popup signs exactly the object that it was shown. The background then checks the
signed transaction (`verifySignedTx()` in `src/shared/approvalVerify.js`):
- the signer is the address that the approval was raised for;
- the type is 0, 1 or 2;
@@ -1408,18 +1439,21 @@ transaction (`verifySignedTx()` in `src/shared/approvalVerify.js`):
A mismatch spends the approval: the site gets an error, and the signed
transaction is not retried. A wrong password leaves the approval standing. After
a broadcast the nonce is recorded, and a second signed transaction with that
nonce is refused. Signatures are checked the same way: the background recovers
the signer from the signature and refuses any other address.
a broadcast the nonce is recorded, and the background refuses a second signed
transaction with that nonce until the worker stops. 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.
closes its window, except an approval already being signed and sent. It ends
every connection allowed without "Remember". It sends `accountsChanged` to every
tab: the active address to a site with permission, an empty list to the rest.
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.
in focus when that is a normal window. 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
@@ -1440,17 +1474,20 @@ 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);
counterparties when AddressDetail or AddressToken shows its list (cached for
12 hours in the popup);
- the ENS name lookup when Send resolves a name;
- on mainnet, Chainlink `latestRoundData()` reads for ETH and for each held or
tracked token with a feed. They go as one batched request, at most every 5
minutes while the popup is open. A reading older than the feed's heartbeat
plus 30 minutes is no price;
(the longest the feed goes without a new price) plus 30 minutes is no price;
- the address scan on import;
- the `symbol()`, `decimals()` and `name()` lookup when a token is added by
contract address;
- the From address's balance each time ConfirmTx is shown;
- the recipient checks on ConfirmTx: `eth_getCode`, `eth_getTransactionCount`
and `eth_getBalance`;
- the token's `decimals()` when ConfirmTx signs an ERC-20 send;
- fee estimates, nonces, broadcasts and receipt lookups;
- `eth_chainId`, when "Save" beside the RPC URL in Settings checks the URL;
- the methods in `PROXY_METHODS`, for any page that asks.
@@ -1536,8 +1573,8 @@ base-uri 'none'
- `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`.
- `form-action` and `base-uri` are named because neither falls back to
`default-src`. `frame-src 'none'` is stricter than the fallback.
### Debug mode
@@ -1567,21 +1604,23 @@ balances and ENS names, and each token contract looked up.
### Appearance
- Black text on a white background, or white on black in the dark theme. Colour
marks errors, warnings, address dots and identicons.
is used for errors, warnings, the red banner, address dots and identicons.
- Every piece of information is text in the system monospace font. The
identicons and the QR code on Receive are the only pictures.
identicons and the QR code on Receive are the only pictures in the popup.
- Every control looks like a control. Buttons have a border, padding and a hover
state. Clickable text is underlined. There are no invisible hit targets.
- Nothing moves when a result arrives. Every element filled in later reserves
its space with `min-height` or a placeholder. `visibility: hidden` is
state. Clickable text is underlined. A clickable row changes its background on
hover. There are no invisible hit targets.
- Every element filled in later reserves its space with `min-height` or a
placeholder, so a result arriving later moves nothing. `visibility: hidden` is
preferred over `display: none` where the space must stay. An empty USD value
is written as ` `.
- The popup is 396 pixels wide (`body` in `src/popup/styles/main.css`) and up to
600 pixels tall, the most a toolbar popup gets.
- Styling is Tailwind utility classes in the markup. `main.css` adds four
classes of its own: `.copy-flash-active` and `.copy-flash-fade` for the copy
feedback, `.am-address` for the rule that an address never wraps, and
`.am-byte-order` for the signature screen's message.
classes of its own:
- `.copy-flash-active` and `.copy-flash-fade`, the copy feedback;
- `.am-address`, the rule that an address never wraps;
- `.am-byte-order`, the byte-order layout of the signature screen's message.
- No framework. The popup is plain JavaScript with one module per screen or
group of screens.
@@ -1595,15 +1634,15 @@ balances and ENS names, and each token contract looked up.
## Development
### Entry points
### Entrypoints
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/bootstrap`: install the dependencies. Node comes from the system or,
failing that, from a pinned nvm install; yarn comes from corepack. 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, stopped at 90 seconds and rerun verbose on
@@ -1625,8 +1664,9 @@ The `Makefile` targets are thin shims over `script/`:
- `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/check-censored`: one competing wallet's name is barred from code and
documentation; fail when it 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/`
@@ -1675,6 +1715,7 @@ It copies into `dist/chrome/` and `dist/firefox/`:
It compiles into the bundles:
- the version;
- the license and the author;
- the commit hash, marked `-dirty` when the working tree is modified;
- the build date;
- `DEBUG`.
@@ -1729,17 +1770,20 @@ under a tag of its own, and removes the image afterwards. Neither is part of
`make check`. Run them after any change under `src/popup/views/`, before
committing.
Four rules hold for both suites:
Two rules hold for both suites:
- Nothing under `tests/e2e/` is named `*.test.js`, so jest never picks the
suites up.
- Both suites share the test page and stub node in `tests/e2e/dapp.js` and the
fixtures in `tests/e2e/network.js`; the drivers are separate.
Two more hold for the Chrome suite:
- A test that provokes a `console.error` on purpose declares it with
`errors.expect()` (`tests/e2e/harness.js`). An undeclared error fails the run,
and so does a declared one that never comes.
- `playwright-core` in `package.json` stays at the version of the container
image in `tests/e2e/Dockerfile`.
- Both suites share the test page and stub node in `tests/e2e/dapp.js` and the
fixtures in `tests/e2e/network.js`; the drivers are separate.
`make test-e2e` runs three programs in a pinned Playwright container:
@@ -1749,13 +1793,17 @@ Four rules hold for both suites:
uncaught page error or `console.error`. It covers:
- WebAssembly compiling in the real popup under the shipped policy;
- the Welcome settings, and what reaches Blockscout and Etherscan;
- wallet creation, Add Token and TransactionDetail;
- wallet creation, AddToken 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`.
- the site round trips:
- connect;
- `personal_sign`;
- `eth_signTypedData_v4`;
- `eth_sendTransaction`;
- `wallet_switchEthereumChain`.
- `tests/e2e/storagePartition.js` loads the extension from two directories into
one profile, with and without the manifest `key`. With the key, both loads get
the pinned extension id and read the same storage. Without it, the second load
@@ -1769,10 +1817,14 @@ Four rules hold for both suites:
geckodriver. The container has no network. The loopback server in
`tests/e2e/dapp.js` serves the test page and a stub node.
- `tests/e2e/firefox/run.js` installs `dist/firefox/` as a temporary add-on. It
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/run.js` installs `dist/firefox/` as a temporary add-on.
Errors are read from Firefox's console service at each step boundary. Any
uncaught error from an extension source fails the run. It covers:
- popup load;
- StateRecovery;
- wallet creation;
- AddToken;
- the site round trips.
- `tests/e2e/firefox/reinstall.js` installs the packaged `.xpi`, creates a
wallet, restarts the browser and adds the add-on again. It decrypts the stored
secret through the real ShowRecoveryPhrase screen. It then uninstalls and
@@ -1784,9 +1836,15 @@ from the `check` workflow. `e2e-chrome` has a 40-minute cap and `e2e-firefox` a
`.gitea/workflows/check.yml` is a copy of the file in the `sneak/prompts`
repository and is not edited here.
## TODO
Planned work is on
[the issue tracker](https://git.eeqj.de/sneak/AutistMask/issues). `TODO.md`
holds the workflow, the status and the next step.
## Policies
- The other wallet is not named in code or documentation.
- One competing wallet's name is barred from code and 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`. Many sites check them