Compare commits
1 Commits
d749ed9212
...
9e209e27bd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e209e27bd |
55
README.md
55
README.md
@@ -362,32 +362,17 @@ The core hierarchy is **Wallets → Addresses**:
|
||||
address scan on import, but imported from an extended private key rather
|
||||
than a recovery phrase. It therefore has no recovery phrase to display or
|
||||
back up.
|
||||
- An **address** holds ETH and ERC-20 tokens.
|
||||
- An **address** holds ETH and any user-added ERC-20 tokens.
|
||||
- The user can have multiple wallets, each with multiple addresses (HD) or a
|
||||
single address (key).
|
||||
|
||||
Which tokens an address shows is decided by `fetchTokenBalances()` in
|
||||
`src/shared/balances.js`, from the Blockscout `token-balances` response, so
|
||||
tokens do appear without the user adding them. An ERC-20 is shown when its
|
||||
balance is nonzero and it is in the bundled top-250 token list, is tracked by
|
||||
the user, or has 1,000 or more holders; a token claiming a symbol from the
|
||||
bundled list from any other contract address is always dropped. That filter is
|
||||
unconditional — the "Hide tokens with fewer than 1,000 holders" setting governs
|
||||
the transaction history and the send-screen token selector, not this list.
|
||||
Tracked tokens with a zero balance are listed as well while "Show tracked tokens
|
||||
with zero balance" is on.
|
||||
|
||||
#### Navigation
|
||||
|
||||
The main view shows all addresses grouped by wallet, with ETH balances inline.
|
||||
The user taps an address to see its detail view (full address, balance, tokens,
|
||||
send/receive). Navigation is a stack: each forward action pushes the current
|
||||
screen, and every view has a "Back" or "Cancel" button that pops back to it (see
|
||||
the Screen Map below). There is no hamburger menu and no persistent tab bar; the
|
||||
Settings gear in the title bar is the only global control. Two screens carry an
|
||||
in-screen control beyond that: AddWallet uses three tabs to select the import
|
||||
mode, and AddressDetail keeps its one rarely-used action ("Export Private Key")
|
||||
behind a "···" menu.
|
||||
send/receive). Navigation is flat — every view has a "Back" or "Cancel" button
|
||||
that returns to the previous context. No deep nesting, no tabs, no hamburger
|
||||
menus.
|
||||
|
||||
### Screen Map
|
||||
|
||||
@@ -424,7 +409,7 @@ screen, including ExportPrivKey, falls back to Home.
|
||||
- **When**: At least one wallet exists. This is the root screen.
|
||||
- **Elements**:
|
||||
- Active address ETH balance (large) + USD value in parentheses
|
||||
- "Total:" USD value across ETH and every token shown for the active address
|
||||
- "Total:" USD value across ETH and all tracked tokens of the active address
|
||||
- Active address (color dot, full address, etherscan link, tap to copy)
|
||||
- Send / Receive quick-action buttons, both acting on the active address
|
||||
- ETH/USD price display
|
||||
@@ -432,7 +417,7 @@ screen, including ExportPrivKey, falls back to Home.
|
||||
button for HD and xprv wallets, then one block per address with "Address
|
||||
N" (bold when active), the ENS name if resolved, the full address, an
|
||||
`[info]` button, the address USD total, and a balance line for ETH and for
|
||||
each token shown for that address
|
||||
each tracked token
|
||||
- "Recent Transactions": up to 25 transactions merged across every address
|
||||
of every wallet, deduplicated by hash and filtered
|
||||
- "Add additional wallet..." link at bottom
|
||||
@@ -485,8 +470,8 @@ screen, including ExportPrivKey, falls back to Home.
|
||||
- ENS name (if resolved, bold above the address)
|
||||
- Full address (color dot, etherscan link, tap to copy)
|
||||
- USD total for address
|
||||
- Balance list: ETH + the ERC-20 tokens shown for this address (4 decimal
|
||||
places, USD inline). Each balance row is clickable → **AddressToken**
|
||||
- Balance list: ETH + tracked ERC-20 tokens (4 decimal places, USD inline).
|
||||
Each balance row is clickable → **AddressToken**
|
||||
- Send / Receive / + Token buttons and a "···" menu button
|
||||
- "···" dropdown containing a single "Export Private Key" entry
|
||||
- Transaction list (with ENS resolution for counterparties)
|
||||
@@ -892,7 +877,7 @@ communicates with three external services to function as a wallet:
|
||||
What the extension does NOT do:
|
||||
|
||||
- No analytics or telemetry services
|
||||
- No token list APIs (the top-250 token list is bundled at build time)
|
||||
- No token list APIs (user adds tokens manually by contract address)
|
||||
- No Infura/Alchemy dependency (any JSON-RPC endpoint works)
|
||||
- No backend servers operated by the developer
|
||||
|
||||
@@ -1015,8 +1000,7 @@ hardcoded test phrase.
|
||||
- Add multiple addresses within an HD wallet
|
||||
- Manage multiple wallets simultaneously
|
||||
- View ETH balance per address
|
||||
- View ERC-20 token balances (bundled top-250 tokens, tokens with 1,000 or more
|
||||
holders, and tokens the user adds by contract address)
|
||||
- View ERC-20 token balances (user adds token by contract address)
|
||||
- Send ETH to an address
|
||||
- Send ERC-20 tokens to an address
|
||||
- Receive ETH/tokens (display address, copy to clipboard, QR code)
|
||||
@@ -1162,8 +1146,7 @@ Currently supported:
|
||||
- Built in token swaps (use a DEX in the browser)
|
||||
- Analytics, telemetry, or tracking of any kind
|
||||
- Advertisements or promotions
|
||||
- Obscure token list auto-discovery — nothing outside the bundled list, the
|
||||
1,000-holder floor, and the tokens the user added by contract address
|
||||
- Obscure token list auto-discovery (user adds tokens manually)
|
||||
- We detect common/popular ERC20s in the basic case
|
||||
- Fiat on/off ramps
|
||||
- Extensive transaction decoding/parsing
|
||||
@@ -1220,18 +1203,14 @@ This repository includes data files from third-party projects that are not
|
||||
covered by the GPL-3.0 license above. These files, their copyright holders, and
|
||||
their licenses are:
|
||||
|
||||
| File | Source | Copyright | License |
|
||||
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------- | -------------------------------------------------------------- |
|
||||
| `src/shared/phishingBlocklist.json` | `eth-phishing-detect` community-maintained phishing domain blocklist, vendored from its `src/config.json` | Copyright (c) 2018 kumavis | [DBAD (Don't Be a Dick)](https://github.com/philsturgeon/dbad) |
|
||||
| `src/shared/scamlist.js` (address data from MyEtherWallet) | [ethereum-lists](https://github.com/MyEtherWallet/ethereum-lists) `addresses-darklist.json` | Copyright (c) 2020 MyEtherWallet | MIT |
|
||||
| `src/shared/scamlist.js` (address data from EtherScamDB) | [EtherScamDB](https://github.com/MrLuit/EtherScamDB) `scams.yaml` | Copyright (c) 2018 Luit Hollander | MIT |
|
||||
| File | Source | Copyright | License |
|
||||
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | -------------------------------------------------------------- |
|
||||
| `src/shared/phishingBlocklist.json` | [eth-phishing-detect](https://github.com/AugurProject/eth-phishing-detect) community-maintained phishing domain blocklist | Copyright (c) 2018 kumavis | [DBAD (Don't Be a Dick)](https://github.com/philsturgeon/dbad) |
|
||||
| `src/shared/scamlist.js` (address data from MyEtherWallet) | [ethereum-lists](https://github.com/MyEtherWallet/ethereum-lists) `addresses-darklist.json` | Copyright (c) 2020 MyEtherWallet | MIT |
|
||||
| `src/shared/scamlist.js` (address data from EtherScamDB) | [EtherScamDB](https://github.com/MrLuit/EtherScamDB) `scams.yaml` | Copyright (c) 2018 Luit Hollander | MIT |
|
||||
|
||||
The full license texts for these third-party files are included in the
|
||||
[LICENSE](LICENSE) file. The `eth-phishing-detect` row carries no repository
|
||||
link because the upstream is hosted under a competitor's organization name,
|
||||
which project policy keeps out of code and documentation; the vendored copy and
|
||||
the runtime refresh both come from that upstream, whose URL is the
|
||||
`BLOCKLIST_URL` constant in `src/shared/phishingDomains.js`.
|
||||
[LICENSE](LICENSE) file.
|
||||
|
||||
## Author
|
||||
|
||||
|
||||
7
TODO.md
7
TODO.md
@@ -48,19 +48,12 @@ undefined identifiers, which is how
|
||||
Tailwind binary instead of `npx`, `--frozen-lockfile` on `make install`, and
|
||||
the Makefile-only targets documented in the README
|
||||
([#166](https://git.eeqj.de/sneak/AutistMask/issues/166)).
|
||||
- 2026-08-11: Three `README.md` claims corrected against the code — blocklist
|
||||
attribution, token-display rule, navigation model
|
||||
([#213](https://git.eeqj.de/sneak/AutistMask/issues/213)).
|
||||
- 2026-08-11: README Screen Map rebuilt from the code — every screen, element
|
||||
and transition re-verified against `src/popup/`
|
||||
([#164](https://git.eeqj.de/sneak/AutistMask/issues/164)).
|
||||
- 2026-08-11: `docs/README.md` rewritten against the code: no competitor names,
|
||||
all five network destinations documented, password/Settings/Add Wallet
|
||||
sections corrected ([#163](https://git.eeqj.de/sneak/AutistMask/issues/163)).
|
||||
- 2026-08-11: `loadState()` now derives `hasWallet` from the wallet list instead
|
||||
of trusting the persisted flag, so a profile already saved inconsistent no
|
||||
longer stays broken on every load
|
||||
([#195](https://git.eeqj.de/sneak/AutistMask/issues/195)).
|
||||
- 2026-08-11: Wallet deletion repairs its own state — `hasWallet` follows the
|
||||
remaining wallets, the selection only moves when it was deleted, and the
|
||||
active-address change is broadcast to connected sites
|
||||
|
||||
@@ -84,11 +84,8 @@ async function loadState() {
|
||||
const result = await storageApi.get("autistmask");
|
||||
if (result.autistmask) {
|
||||
const saved = result.autistmask;
|
||||
state.hasWallet = saved.hasWallet;
|
||||
state.wallets = saved.wallets || [];
|
||||
// Derived, never read from storage: a profile persisted with the flag
|
||||
// out of step with the wallet list would otherwise stay broken on
|
||||
// every load. Nothing depends on the two disagreeing.
|
||||
state.hasWallet = state.wallets.length > 0;
|
||||
state.trackedTokens = saved.trackedTokens || [];
|
||||
state.networkId = saved.networkId || DEFAULT_STATE.networkId;
|
||||
state.rpcUrl = saved.rpcUrl || DEFAULT_STATE.rpcUrl;
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
const ADDRESS = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||
|
||||
function oneWallet() {
|
||||
return [{ name: "Wallet 1", type: "hd", addresses: [ADDRESS] }];
|
||||
}
|
||||
|
||||
// state.js resolves the storage API at require time, so the stub has to exist
|
||||
// before the module is loaded, and the module registry has to be reset between
|
||||
// cases because `state` is a module-level singleton.
|
||||
function loadModuleWith(persisted) {
|
||||
jest.resetModules();
|
||||
const set = jest.fn(async () => {});
|
||||
global.chrome = {
|
||||
storage: {
|
||||
local: {
|
||||
get: jest.fn(async () =>
|
||||
persisted ? { autistmask: persisted } : {},
|
||||
),
|
||||
set,
|
||||
},
|
||||
},
|
||||
};
|
||||
return { mod: require("../src/shared/state"), set };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
delete global.chrome;
|
||||
});
|
||||
|
||||
describe("loadState hasWallet reconciliation", () => {
|
||||
// A profile that deleted its last wallet on a build predating the write
|
||||
// path fix keeps hasWallet: true forever. It must load as no wallet, which
|
||||
// is what sends the popup to the welcome view.
|
||||
test("stored hasWallet true with zero wallets loads as no wallet", async () => {
|
||||
const { mod } = loadModuleWith({ hasWallet: true, wallets: [] });
|
||||
await mod.loadState();
|
||||
expect(mod.state.hasWallet).toBe(false);
|
||||
});
|
||||
|
||||
test("stored hasWallet true with a missing wallets key loads as no wallet", async () => {
|
||||
const { mod } = loadModuleWith({ hasWallet: true });
|
||||
await mod.loadState();
|
||||
expect(mod.state.wallets).toEqual([]);
|
||||
expect(mod.state.hasWallet).toBe(false);
|
||||
});
|
||||
|
||||
test("stored hasWallet false with one wallet loads as having a wallet", async () => {
|
||||
const { mod } = loadModuleWith({
|
||||
hasWallet: false,
|
||||
wallets: oneWallet(),
|
||||
});
|
||||
await mod.loadState();
|
||||
expect(mod.state.hasWallet).toBe(true);
|
||||
});
|
||||
|
||||
test("absent hasWallet with wallets present loads as having a wallet", async () => {
|
||||
const { mod } = loadModuleWith({ wallets: oneWallet() });
|
||||
await mod.loadState();
|
||||
expect(mod.state.hasWallet).toBe(true);
|
||||
});
|
||||
|
||||
test("consistent stored states are preserved", async () => {
|
||||
const withWallet = loadModuleWith({
|
||||
hasWallet: true,
|
||||
wallets: oneWallet(),
|
||||
});
|
||||
await withWallet.mod.loadState();
|
||||
expect(withWallet.mod.state.hasWallet).toBe(true);
|
||||
|
||||
const without = loadModuleWith({ hasWallet: false, wallets: [] });
|
||||
await without.mod.loadState();
|
||||
expect(without.mod.state.hasWallet).toBe(false);
|
||||
});
|
||||
|
||||
test("empty storage leaves the default no-wallet state", async () => {
|
||||
const { mod } = loadModuleWith(null);
|
||||
await mod.loadState();
|
||||
expect(mod.state.hasWallet).toBe(false);
|
||||
expect(mod.state.wallets).toEqual([]);
|
||||
});
|
||||
|
||||
// The correction is derived on every load rather than written back, so a
|
||||
// load never has a storage side effect.
|
||||
test("loadState does not write to storage", async () => {
|
||||
const { mod, set } = loadModuleWith({ hasWallet: true, wallets: [] });
|
||||
await mod.loadState();
|
||||
expect(set).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Deriving must not disturb the rest of the load.
|
||||
test("other persisted fields still load", async () => {
|
||||
const { mod } = loadModuleWith({
|
||||
hasWallet: false,
|
||||
wallets: oneWallet(),
|
||||
networkId: "sepolia",
|
||||
theme: "dark",
|
||||
activeAddress: ADDRESS,
|
||||
});
|
||||
await mod.loadState();
|
||||
expect(mod.state.networkId).toBe("sepolia");
|
||||
expect(mod.state.theme).toBe("dark");
|
||||
expect(mod.state.activeAddress).toBe(ADDRESS);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user