fix: answer eth_chainId and net_version from loaded state (closes #317)
All checks were successful
check / check (push) Successful in 28s
e2e / e2e-chrome (push) Successful in 1m10s
e2e / e2e-firefox (push) Successful in 22s

Both methods answered from currentNetwork(), which reads the module-level state
singleton, and nothing populates that at module scope. A service worker revived
by the page's own message therefore held DEFAULT_STATE and reported mainnet
0x1 / 1 to a page whose user was on Sepolia, so a dApp asking which chain the
wallet is on built its interaction for the wrong one. Neither method is gated on
a connection, so any page got the stale answer.

One await loadState() covers the pair: they are the same read of the same value,
and a second load in a sibling branch would be redundant. Same shape and
placement idiom as the chain-switch handler and the transaction path.

Read-side audit of the background, which the fix was the occasion for: the other
singleton reads are wallet_switchEthereumChain, the transaction verify/broadcast
path and backgroundRefresh, and all three already load first. Every other
handler answers from storage per call through getState(). One stale read remains
and is deliberately not fixed here, being a different handler rather than the
same one-line shape: handleSendTransaction calls getProvider() with no network
name, so balances.js falls back to the same unloaded singleton for ethers'
static network hint, and a cold-worker send on Sepolia is prepared with a
mainnet hint. It is caught later — the artifact is verified against the loaded
chain before broadcast — so it fails the send rather than sending on the wrong
chain.

Verified failing first: reverting only src/background/index.js to next gives 3
failed / 775 passed, exactly the three cases that read the chain on a cold
worker; the mainnet case and the persists-nothing case pass either way by
design. With the fix, 778 passed / 36 suites, and lint ran uncached in the
pinned container (eslint + prettier over the changed files).
This commit is contained in:
2026-08-20 10:47:18 +00:00
parent 6350aad591
commit 726b69216a
3 changed files with 222 additions and 6 deletions

15
TODO.md
View File

@@ -44,6 +44,21 @@ but the review is broader than any of them.
# Completed Steps
- 2026-08-20: A page asking which chain the wallet is on is told the chain the
user is actually on ([#317](https://git.eeqj.de/sneak/AutistMask/issues/317)).
`eth_chainId` and `net_version` answered from `currentNetwork()`, which reads
the module-level `state` singleton that nothing populates at module scope, so
a service worker revived by the page's own message answered out of
`DEFAULT_STATE` and reported mainnet `0x1`/`1` to a user on Sepolia — a dApp
building its interaction for the wrong chain. Both now `await loadState()`
first, under one load covering the pair. The read side of the background was
audited with it: the remaining singleton reads are the chain switch, the
transaction verification path and `backgroundRefresh`, which each already
load, and everything else answers from storage per call through `getState()`.
One stale read is left named but unfixed, outside this issue's scope:
`handleSendTransaction` builds its provider with no network name, so
`getProvider()` falls back to the same unloaded singleton for ethers' static
network hint.
- 2026-08-20: A web page can no longer switch the wallet's chain, and switching
no longer destroys the user's endpoints
([#308](https://git.eeqj.de/sneak/AutistMask/issues/308)).