A saved wait whose decoded record is in another shape threw when its receipt arrived. WaitTx then stayed on screen with the counter stopped. decodedDetailsHtml() now shows only the lines whose label and value are text. SuccessTx shows the amount and recipient when no line can be read. WaitTx and ErrorTx now show each allowance's Allowance token line. Long amounts use break-words, so words stay whole. ErrorTx sets up copying when it has no hash, so the token's address can be copied. Model: opus-5-5
1955 lines
86 KiB
Markdown
1955 lines
86 KiB
Markdown
# AutistMask
|
|
|
|
AutistMask is a GPL-3.0-licensed browser extension by
|
|
[@sneak](https://sneak.berlin): an Ethereum wallet for Chrome and Firefox.
|
|
|
|
AutistMask 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.
|
|
|
|
## Rationale
|
|
|
|
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 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
|
|
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:
|
|
1. Open `chrome://extensions/`.
|
|
2. Turn on "Developer mode".
|
|
3. Click "Load unpacked".
|
|
4. 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` (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
|
|
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, 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.
|
|
- "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 "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. Elsewhere in this file, a
|
|
connected site is a site on Allowed Sites or Connected Sites.
|
|
- 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 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
|
|
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 entrypoints (see Development)
|
|
tests/ jest unit tests and their helpers; 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, 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
|
|
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 `assertStateUsable()`, the check that
|
|
the two state modules run on each read.
|
|
- `storedSettings.js`: reads "Use Blockscout", "Check recipients on Etherscan"
|
|
and the active network 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 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
|
|
|
|
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 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`):
|
|
|
|
- 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.
|
|
|
|
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:
|
|
|
|
- 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:
|
|
|
|
- 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 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
|
|
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.
|
|
|
|
Two things live only in the worker's memory and are lost when it stops: pending
|
|
approvals and the record of broadcast nonces. 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.
|
|
|
|
### 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. It merges the fields below entry by
|
|
entry, so two pages changing different things do not overwrite each other:
|
|
|
|
- wallets;
|
|
- site lists;
|
|
- endpoints;
|
|
- tracked tokens.
|
|
|
|
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. 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 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". When a
|
|
balance refresh returns while AddressDetail or AddressToken is showing, that
|
|
screen draws its balances again.
|
|
|
|
A network switch, or a change to "Use Blockscout", cuts the stack at the first
|
|
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. When
|
|
Settings is showing, it 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;
|
|
- 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.
|
|
|
|
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
|
|
`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 that 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 when a
|
|
price is known;
|
|
- a "Total:" line with the USD value of the address's holdings (see Partial
|
|
totals below);
|
|
- 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;
|
|
- 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, 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
|
|
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." After a network switch, the list shows
|
|
"Loading..." in place of the previous network's rows or failure message.
|
|
|
|
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 or belongs to a wallet carrying the `walletDefects.js` notice.
|
|
- "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.
|
|
|
|
A missing, short or mismatched password gets a full sentence on the error line.
|
|
A flash message refuses:
|
|
|
|
- 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.
|
|
|
|
#### 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 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;
|
|
- 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 or belongs to a wallet carrying the `walletDefects.js` notice;
|
|
- "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 for a
|
|
token. 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:
|
|
|
|
- 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
|
|
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:
|
|
- an identicon;
|
|
- a colour dot;
|
|
- the full address;
|
|
- an 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;
|
|
- each `Moved from the sender` amount of a swap;
|
|
- each `Permit2 allowance` amount of a swap, with its `Allowance token` line;
|
|
- 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;
|
|
- each `Moved from the sender` amount of a swap;
|
|
- each `Permit2 allowance` amount of a swap, with its `Allowance token` line;
|
|
- 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 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.
|
|
|
|
#### 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:
|
|
- an identicon;
|
|
- the full address;
|
|
- an explorer link;
|
|
- the 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;
|
|
- 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. 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;
|
|
- 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, 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".
|
|
- 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. When the
|
|
same "Save" is tapped again before the endpoint answers, the earlier tap stores
|
|
nothing and shows no flash message.
|
|
|
|
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 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
|
|
address.
|
|
- `[x]` on a 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 already connected or denied. 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 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`)
|
|
|
|
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". 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:
|
|
|
|
- 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
|
|
|
|
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`: transaction
|
|
times;
|
|
- `renderAddressHtml()` in the same file: most addresses.
|
|
|
|
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.
|
|
|
|
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 symbol comes from the same places. The bundled list answers for a
|
|
token on Ethereum Mainnet only. 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.
|
|
|
|
A tracked token with no stored balance at an address can show "quantity unknown"
|
|
there in place of a zero. It does so after each of these:
|
|
|
|
- the token is added;
|
|
- an address or a wallet is added;
|
|
- the network is switched.
|
|
|
|
This lasts until a refresh has read the token's balance at every address.
|
|
|
|
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 bundled list and the tracked tokens may lack a
|
|
token's symbol or decimals. The approval and status screens then add a line
|
|
under that token saying 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.
|
|
The decoded swap lines call the native currency `ETH` on both networks.
|
|
|
|
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 token holding below 0.000001 is left
|
|
out of:
|
|
|
|
- the balance lists;
|
|
- the Send dropdown;
|
|
- the address total;
|
|
- the DeleteAddress warning.
|
|
|
|
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
|
|
|
|
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 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
|
|
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. An
|
|
identicon appears on:
|
|
|
|
- AddressDetail;
|
|
- AddressToken;
|
|
- ExportPrivKey;
|
|
- ConfirmTx;
|
|
- TransactionDetail.
|
|
|
|
### Scam protection
|
|
|
|
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
|
|
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 bundled list's symbols are checked on Ethereum
|
|
Mainnet only. The native currency's labels are checked on every network. 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 token that the
|
|
rule refuses cannot be added on AddToken or SettingsAddToken, and gets no zero
|
|
row in the balance lists. 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 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
|
|
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.
|
|
|
|
The other warnings on ConfirmTx are listed under that screen.
|
|
|
|
#### 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.
|
|
`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
|
|
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. A `WRAP_ETH` can pay its WETH
|
|
to a recipient other than the router. When its amount gets these lines, its
|
|
`Moved to` line names that recipient, or reads `The sender`. A `WRAP_ETH`
|
|
after the step that sets `Token In` takes the amount it states from the
|
|
sender. When it pays anyone but the router, that amount gets these lines. When
|
|
it pays the router, that amount joins the `Amount` line if `Token In` is ETH,
|
|
and gets these lines if not. A later `WRAP_ETH` of `CONTRACT_BALANCE` states
|
|
no amount, so it gets no lines. ETH that the sender pays a V4 step or a
|
|
`WRAP_ETH` of a stated amount can get these three lines when the router may
|
|
hold ETH from an earlier step. When the decoder knows the amount, its
|
|
`Moved from the sender` line reads `Up to` and the amount.
|
|
- `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. When a swap is split across
|
|
steps that each pay `Token Out` to the sender, the line shows the sum of their
|
|
minimums. 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)`. 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)`.
|
|
|
|
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, 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
|
|
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 connected or denied.
|
|
- `eth_accounts` and `wallet_getPermissions`: the active address when the site
|
|
is connected, otherwise nothing.
|
|
- `eth_chainId` and `net_version`: the active network's values.
|
|
- `wallet_switchEthereumChain`: NetworkApproval, for a connected site naming a
|
|
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`:
|
|
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. 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
|
|
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 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;
|
|
- 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 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, 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 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
|
|
|
|
`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 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
|
|
(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;
|
|
- 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. A history answer is also dropped once the network,
|
|
the address or the token on screen has changed. "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.
|
|
- `form-action` and `base-uri` are named because neither falls back to
|
|
`default-src`. `frame-src 'none'` is stricter than the fallback.
|
|
|
|
### 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
|
|
is used for:
|
|
- errors;
|
|
- warnings;
|
|
- the red banner;
|
|
- address dots;
|
|
- identicons.
|
|
- Every piece of information is text in the system monospace font. The
|
|
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. 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`, 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.
|
|
|
|
### 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
|
|
|
|
### 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 when present. Otherwise node comes from a pinned nvm install and yarn
|
|
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
|
|
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`: 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/`
|
|
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 license and the author;
|
|
- 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`.
|
|
`script/lib/forbiddenBundleInputs.js` lists the forbidden inputs. `build.js`
|
|
checks each bundle against `esbuild`'s report of what it contains. 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.
|
|
|
|
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`.
|
|
|
|
`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, 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`;
|
|
- `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.
|
|
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`:
|
|
1. installs the packaged `.xpi`;
|
|
2. creates a wallet;
|
|
3. restarts the browser;
|
|
4. 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.
|
|
|
|
## 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. Also planned:
|
|
|
|
- research and document each address in `src/shared/scamlist.js`;
|
|
- add more known fraud addresses from Etherscan labels;
|
|
- show values in currencies other than USD, such as EUR and GBP.
|
|
|
|
## Policies
|
|
|
|
- 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
|
|
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)
|