Corrects every statement the review found false against the code, among them which methods the background relays and for whom, where Connected Sites live, which fields the stored-data check and the defaults cover, what the balance lists round, and which address rows are plain text. Puts the test-file citations under Tests, defines each technical word on first use, turns every comma-chained list into bullets, and adds the coverage the review asked for: valid amounts, the Firefox minimum version, the RPC privacy note, the schema-version rule, the browser-suite rules. Part of #672. Model: fable-5-1
1822 lines
81 KiB
Markdown
1822 lines
81 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;
|
|
- 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.
|
|
|
|
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.
|
|
|
|
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, scam or exploiter.
|
|
|
|
The phishing domain list is built into the extension. Checking a site against it
|
|
sends nothing anywhere.
|
|
|
|
The section External services below lists each 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.
|
|
|
|
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,
|
|
and nothing here needs it. It signs a CRX, Chrome's packaged extension file,
|
|
which this repository does not produce.
|
|
|
|
### Installing on Firefox
|
|
|
|
`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`.
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
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
|
|
|
|
Each default below is the value in a new install. Connected Sites are kept in
|
|
the browser's session storage; every other setting is stored in the wallet's
|
|
profile (see Stored data). The Settings screen offers every setting but
|
|
"Remember my choice for this site", which is on SiteApproval. 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 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.
|
|
- Debug mode: off. The checkbox is hidden until the user clicks the version in
|
|
About ten times.
|
|
|
|
These 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 browser suite print every request it
|
|
intercepts.
|
|
- `FIREFOX_BIN` and `GECKODRIVER` tell the Firefox browser suite where its two
|
|
binaries are. The suite's image sets both.
|
|
|
|
## 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 inpage provider: window.ethereum, 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 or group of screens, 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 the rule that a nonzero amount
|
|
never shows as zero.
|
|
- `approvalAmount.js`: the decimals 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 defaults.
|
|
- `phishingBlocklist.json`: the 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 profile read 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 with the decimals
|
|
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"`, for hierarchical deterministic) 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 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.
|
|
|
|
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` 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 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 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. 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.
|
|
|
|
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`. The version is raised only when the
|
|
meaning of a stored field changes. A new field with a safe absent value needs
|
|
only a default in `normalizePersisted()`.
|
|
|
|
Every profile read in `src/shared/state.js` and `src/background/state.js` 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.
|
|
|
|
`normalizePersisted()` in `src/shared/persistedState.js` gives most malformed
|
|
fields a default. A list that is not a list becomes empty. A malformed entry is
|
|
dropped. A URL that is not text becomes the default URL. The fields marked
|
|
`LOOSE` in `tests/persistedFieldContract.test.js` pass through unchecked.
|
|
|
|
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 that other pages saved to three fields: the network,
|
|
"Use Blockscout" and "Check recipients on Etherscan". It takes those changes
|
|
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 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 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:
|
|
|
|
- ConfirmTx;
|
|
- WaitTx;
|
|
- SuccessTx;
|
|
- ErrorTx;
|
|
- TransactionDetail.
|
|
|
|
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.
|
|
|
|
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
|
|
`DEBUG / INSECURE [TESTNET]` with both. 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 boxed section 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. 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:
|
|
|
|
- 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, as plain text;
|
|
- 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, as plain text.
|
|
|
|
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. The hint's height is reserved, so a longer hint never moves the
|
|
password fields;
|
|
- 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. 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: 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.
|
|
|
|
"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 the key in place of the password input. Tapping the key
|
|
copies it. A wrong password shows "That password is incorrect. Please try
|
|
again." A key that 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 the key from the
|
|
page, and a decrypt still running at that moment 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 boxed section with the contract address and, where
|
|
known:
|
|
- the name and the symbol;
|
|
- the decimals;
|
|
- the holder count, left out when no count is known;
|
|
- the project website;
|
|
- 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 that ConfirmTx reserves. 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 the sending
|
|
address itself 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 this screen. 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 sending address;
|
|
- the recipient is a contract;
|
|
- the recipient has never sent a transaction and holds no ETH;
|
|
- Etherscan labels the recipient as phishing, scam or exploiter. 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 saying the network could
|
|
not be reached 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 signing or a broadcast fails, or when 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 that 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";
|
|
- "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.
|
|
Tapping either copies it;
|
|
- 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 flash message refuses:
|
|
|
|
- a token already tracked;
|
|
- an address on the scam list;
|
|
- a contract that fails the lookup.
|
|
|
|
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 the new
|
|
value. Unticking "Use Blockscout" also drops every untracked token from every
|
|
address.
|
|
- `[x]` on an allowed or connected site disconnects the site. The site's origin
|
|
is dropped under every address. The background ends the site's session
|
|
connections and sends `accountsChanged` with an empty list to the site's open
|
|
tabs.
|
|
- `[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. Tapping the phrase
|
|
copies it. 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:
|
|
|
|
- the password cannot be recovered or reset, so the wallet cannot be unlocked
|
|
again;
|
|
- no password is needed to delete the wallet;
|
|
- deleting erases only the copy of the key stored on this device; nothing on
|
|
chain changes and no money moves;
|
|
- with a kept copy of the wallet's backup, the wallet can be added again with a
|
|
new password;
|
|
- without one, deleting the wallet loses everything it holds, forever;
|
|
- the other wallets are not touched.
|
|
|
|
The user types the wallet's name to confirm. The comparison ignores:
|
|
|
|
- letter case;
|
|
- spaces before and after the name;
|
|
- repeated spaces;
|
|
- invisible characters.
|
|
|
|
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 the address 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".
|
|
|
|
The buttons and the dropdown do no lookup. A token already tracked is disabled
|
|
in both, and picking one in the dropdown anyway gets "Already tracked." The
|
|
address input refuses as AddToken does.
|
|
|
|
#### SiteApproval (`approve-site`)
|
|
|
|
Raised when a site calls `eth_requestAccounts` or `wallet_requestPermissions`
|
|
and is not on the allowed list, not on the denied list, and not connected
|
|
without "Remember". 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 transaction field 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. When the background refuses the signed transaction for good (see Sites
|
|
and approvals), ErrorTx opens. "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. A line feed is shown as a line break. These characters are
|
|
shown as a bordered `U+XXXX` mark:
|
|
|
|
- every other control character;
|
|
- the line separator and the paragraph separator;
|
|
- characters that paint nothing, such as zero-width characters.
|
|
|
|
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 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
|
|
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 that 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
|
|
|
|
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:
|
|
|
|
- TxApproval;
|
|
- ConfirmTx, and Send's current balance;
|
|
- WaitTx, SuccessTx and ErrorTx.
|
|
|
|
TransactionDetail shows the exact amount and its base units.
|
|
|
|
An amount to send is valid when it is a number above zero, written as digits
|
|
with at most one decimal point. Nothing but zeros may follow the token's last
|
|
decimal place. ETH has 18 decimal places. A token with more than 18, or with
|
|
unknown decimals, is held to 18. Send checks this before ConfirmTx opens, and
|
|
ConfirmTx checks again (`isValidAmount()` in `src/shared/txValidation.js`).
|
|
|
|
A token amount in a site's calldata carries no decimals, so the wallet needs the
|
|
token's `decimals` to show it. 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 with guessed decimals. A holding whose decimals are unknown shows
|
|
"quantity unknown" in the balance lists. Only a whole number from 0 to 80 counts
|
|
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.
|
|
|
|
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.
|
|
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 that result 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 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
|
|
that label.
|
|
|
|
On most screens, tapping an address copies it, and an explorer link sits beside
|
|
it. The address rows of Home's wallet list and the history rows are plain text.
|
|
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.
|
|
|
|
### 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 Unicode compatibility
|
|
normalization (NFKC), with invisible characters removed and case ignored. The
|
|
rule always applies to the Send dropdown and to the balance rows fetched for an
|
|
address. A tracked token listed at zero balance is not checked. In the
|
|
transaction history, the rule applies while "Hide fake tokens impersonating a
|
|
known symbol" is on.
|
|
|
|
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 rule: 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 the addresses of known frauds,
|
|
phishing operations and wallet drainers (contracts that empty a wallet once it
|
|
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.
|
|
|
|
#### Phishing domains
|
|
|
|
The extension ships a phishing domain list in
|
|
`src/shared/phishingBlocklist.json`. `make vendor-blocklist` refreshes the file
|
|
from its upstream (see Development). The file holds digests, not domain 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
|
|
refresh 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 that 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. Each byte of the router's
|
|
`commands` field names one step. The decoder 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 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.
|
|
- `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.
|
|
- `Permit2 allowance`, `Allowance token` and `Allowance spender`: each allowance
|
|
that 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 is `Min. paid out` instead unless
|
|
all three hold:
|
|
- a step pays the sender;
|
|
- 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)`.
|
|
- `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 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.
|
|
|
|
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.
|
|
- The methods in `PROXY_METHODS` in `src/background/index.js` are passed to the
|
|
RPC endpoint for any page that asks, connected or not. The list holds node
|
|
methods such as `eth_call`, `eth_getBalance` and `eth_estimateGas`, and it
|
|
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.
|
|
|
|
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 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`):
|
|
|
|
- the signer is the address that 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 signed
|
|
transaction 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 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.
|
|
|
|
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
|
|
transaction is checked, 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);
|
|
- 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 address scan on import;
|
|
- the `symbol()`, `decimals()` and `name()` lookup when a token is added by
|
|
contract address;
|
|
- the recipient checks on ConfirmTx: `eth_getCode`, `eth_getTransactionCount`
|
|
and `eth_getBalance`;
|
|
- 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.
|
|
|
|
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, scam or exploiter 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 and key derivation;
|
|
- signing;
|
|
- transaction construction;
|
|
- ERC-20 calls;
|
|
- JSON-RPC.
|
|
- `libsodium-wrappers-sumo` (ISC): Argon2id and XSalsa20-Poly1305 for the
|
|
encrypted secret.
|
|
- `qrcode` (MIT): the QR code on Receive.
|
|
- `ethereum-blockies-base64` (MIT): 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
|
|
|
|
Hashing, key derivation, signing and encryption go through `ethers` or
|
|
`libsodium-wrappers-sumo`. No primitive is implemented in this repository. A new
|
|
recovery phrase draws its random bytes from the browser's
|
|
`crypto.getRandomValues()`. The build tooling, `build.js` and `script/lib/`,
|
|
hashes files with Node's `crypto`; none of it ships in the extension.
|
|
|
|
### 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'
|
|
```
|
|
|
|
- Every directive not named falls back to `default-src 'self'`.
|
|
- `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`.
|
|
|
|
### 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.
|
|
|
|
No `if (DEBUG)` branch may skip functionality or bypass a security check. Adding
|
|
any other branch on `DEBUG` needs the project owner's approval.
|
|
|
|
Never distribute a debug build. `make build` never produces one.
|
|
`make build-debug` does, and so does `make dev` while `AUTISTMASK_DEBUG=1` is
|
|
exported.
|
|
|
|
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.
|
|
|
|
At the raised log level the console also prints the wallet's addresses, their
|
|
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.
|
|
- Every piece of information is text in the system monospace font. The
|
|
identicons and the QR code on Receive are the only pictures.
|
|
- 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
|
|
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.
|
|
- No framework. The popup is plain JavaScript with one module per screen or
|
|
group of screens.
|
|
|
|
### Wording
|
|
|
|
- "Recovery phrase", never "seed phrase" or "mnemonic".
|
|
- "Address", never "account" or "derived key".
|
|
- "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, stopped at 90 seconds and 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;
|
|
- the inpage provider.
|
|
|
|
It copies into `dist/chrome/` and `dist/firefox/`:
|
|
|
|
- the popup markup;
|
|
- the stylesheet;
|
|
- the icons;
|
|
- the manifest.
|
|
|
|
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 60
|
|
seconds; the test stage stops it at 90. `make check` runs the tests, the lint
|
|
stage and the formatting check.
|
|
|
|
Some unit tests pin facts stated elsewhere in this file:
|
|
|
|
- `tests/extensionId.test.js`: the Chrome `key`, the extension id derived from
|
|
it, and that no private key is committed anywhere in the tree.
|
|
- `tests/manifest.test.js`: the exact Content Security Policy in both manifests,
|
|
the Firefox `incognito` setting, and the icons.
|
|
- `tests/vaultBackend.test.js`: libsodium runs on its WebAssembly build.
|
|
- `tests/persistedFieldContract.test.js`: one row per stored field, stating how
|
|
the field is handled and driving the code with hostile values. It boots the
|
|
real popup entry point over them.
|
|
- `tests/popupElementIds.test.js`: every element id the popup looks up exists in
|
|
`src/popup/index.html`, and no id is defined twice.
|
|
|
|
### Browser tests
|
|
|
|
Two suites drive the real popup in a real browser. 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 after any change under `src/popup/views/`, before
|
|
committing.
|
|
|
|
Four rules hold for both suites:
|
|
|
|
- Nothing under `tests/e2e/` is named `*.test.js`, so jest never picks the
|
|
suites up.
|
|
- 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:
|
|
|
|
- `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:
|
|
- 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;
|
|
- 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`. With the key, both loads get
|
|
the pinned extension id and read the same storage. Without it, the second load
|
|
sees empty 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. 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/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
|
|
reinstalls, and observes 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.
|
|
`.gitea/workflows/check.yml` is a copy of the file in the `sneak/prompts`
|
|
repository and is not edited here.
|
|
|
|
## 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`. Many sites check them
|
|
before they will talk to a wallet, so leaving them out would stop the
|
|
wallet working on those sites;
|
|
- 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).
|
|
|
|
Two data files hold data from three other projects, under those projects'
|
|
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)
|