fix: gate the chain switch and remember endpoints per network (closes #308)
wallet_switchEthereumChain was answered for any origin at all, with no connection check and no prompt, so a page the user had never connected to could move the active chain — clearing the [TESTNET] banner under someone who believed they were on Sepolia. It now takes the same allowedSites/connectedSites gate the signing methods take, ahead of the same-chain and unsupported-chain answers, and refuses an unconnected origin with 4100. The switch also overwrote state.rpcUrl and state.blockscoutUrl with the network defaults, so a user running their own node lost that url permanently and silently to a public endpoint that then sees every address they hold. Endpoints are now remembered per network in state.networkEndpoints: the switch snapshots the network being left and restores the network being entered, falling back to that network's defaults. state.rpcUrl and state.blockscoutUrl remain the live endpoints of the active network, so no reader changed; for the active network they are authoritative and the map entry may be stale, and the snapshot is what reconciles them. A profile written before the map existed has its stored pair adopted for the network it was stored under, so nothing is lost on first load. Neither half held without loading state first. onChainSwitch() mutates the module-level state singleton and persists every field of it, and currentNetwork() reads the same singleton, but the service worker populates nothing at module scope — a worker revived by the page's own message held DEFAULT_STATE, so the same-chain check compared against the wrong network and the save wrote empty wallets, empty allowedSites, no tracked tokens and the default endpoints over the user's stored profile. The handler now awaits loadState() after the gate, as the transaction path already does. A stored networkEndpoints must now be an actual object. The previous guard discarded only falsy values and arrays, so a stored primitive survived the load, the seeding assignment silently no-opped on it, saveState() re-persisted it, and every switch fell back to the public default in place of the user's endpoint — permanently, with no self-healing.
This commit is contained in:
22
TODO.md
22
TODO.md
@@ -44,6 +44,28 @@ but the review is broader than any of them.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 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)).
|
||||
`wallet_switchEthereumChain` was answered for any origin at all, with no
|
||||
connection check and no prompt: any page could clear the `[TESTNET]` banner
|
||||
under a user who believed they were on Sepolia. It now takes the same
|
||||
`allowedSites`/`connectedSites` gate the signing methods take, ahead of the
|
||||
same-chain and unsupported-chain answers, and refuses an unconnected origin
|
||||
with `4100`. The switch itself also overwrote `state.rpcUrl` and
|
||||
`state.blockscoutUrl` with the network defaults, so a user running their own
|
||||
node lost that url permanently and silently to a public endpoint that then
|
||||
sees every address they hold. Endpoints are now remembered per network in
|
||||
`state.networkEndpoints`, snapshotted from the network being left and restored
|
||||
for the network being entered; `state.rpcUrl` stays the live value for the
|
||||
active network, so no reader changed. A profile written before the map existed
|
||||
has its stored pair adopted for the network it was stored under, and loses
|
||||
nothing. The handler now loads state before it switches
|
||||
([#316](https://git.eeqj.de/sneak/AutistMask/issues/316)): the service worker
|
||||
populates nothing at module scope, so a worker revived by the page's own
|
||||
message held `DEFAULT_STATE`, and the switch persisted every field of it —
|
||||
wiping every wallet, every site approval and every tracked token from storage
|
||||
along with the endpoint.
|
||||
- 2026-08-17: The Settings screen is driven in a browser, and every element id
|
||||
the popup looks up is checked statically. Nothing exercised Settings in the
|
||||
e2e suite, and jest runs with no DOM, so the densest run of `$("...")` lookups
|
||||
|
||||
Reference in New Issue
Block a user