fix: version stored state, validate its shape, and give a corrupt blob a way out (closes #311)
Stored state had no version and no structural validation, so a corrupt blob produced a completely blank popup with no message and no recovery control, and made every dApp RPC call from every page answer a generic -32603. There was no reset or wipe control anywhere in the UI. saveState() now stamps a schema version and loadState() validates the shape. A version it does not understand, or a wallets array it cannot parse, lands on a recovery screen that names the problem, offers the stored record verbatim for export, and offers a destructive reset behind a typed confirmation. Unversioned but valid state -- which every existing install has -- migrates in place and keeps working; it is never shown a wipe prompt. A dApp call against unusable state answers -32007, which EIP-1474 leaves unassigned, rather than -32603. networkById() refuses an unknown id loudly instead of returning mainnet, and networkId is validated so a corrupt value cannot be used as an object key. Fields the gate does not refuse are floored by type, container and entries both: a malformed trackedTokens or tokenBalances entry is dropped rather than dereferenced. Verified by an independent sweep of 1152 corrupt blobs producing no blank popup, with the same harness showing 9 blanks against the previous revision.
This commit was merged in pull request #360.
This commit is contained in:
40
TODO.md
40
TODO.md
@@ -57,6 +57,46 @@ but the review is broader than any of them.
|
||||
`Token Out` now reads `Unknown (not named in the calldata)` and
|
||||
`Min. received` falls to the base-unit refusal from
|
||||
[#340](https://git.eeqj.de/sneak/AutistMask/issues/340).
|
||||
|
||||
- 2026-08-23: The stored profile carries a schema version, and a record the
|
||||
wallet cannot read produces a screen instead of a blank popup
|
||||
([#311](https://git.eeqj.de/sneak/AutistMask/issues/311)). `saveState()` and
|
||||
`updateState()` both stamp `STATE_SCHEMA_VERSION`
|
||||
(`src/shared/stateSchema.js`), and every read goes through
|
||||
`assertStateUsable()` on the raw bytes before normalization gets a chance to
|
||||
paper over them. Version 1 is the shape that shipped unversioned, so the
|
||||
profile every existing install holds loads normally and is migrated in place
|
||||
by being stamped on the first write — an upgrade shows nobody a wipe prompt
|
||||
for a wallet that is fine. A record this build cannot vouch for is refused
|
||||
instead: not normalized, not written back, not half-loaded. The popup shows
|
||||
the new StateRecovery screen, which names the problem, exports the raw record
|
||||
verbatim into the page (and downloads it where the browser allows), and offers
|
||||
an erase behind a typed `ERASE MY WALLET` — both controls, because an export
|
||||
with no reset leaves the user stuck and a reset with no export destroys the
|
||||
only copy of possibly recoverable key material. The background refuses the
|
||||
same record and answers dApps `-32007` — a code EIP-1474 leaves unassigned,
|
||||
unlike `-32000`..`-32006` — with a message saying the saved data cannot be
|
||||
read and that nothing was signed or sent, rather than the generic `-32603`
|
||||
that every request used to get. Fields the gate deliberately does not check
|
||||
produced the same blank popup on their own: `trackedTokens` and
|
||||
`activeAddress` were floored on truthiness rather than on type, and
|
||||
`trackedTokens`' ENTRIES and each address's `tokenBalances` were not floored
|
||||
at all — `[1, 2]` is a list, and the dereference is `t.address.toLowerCase()`
|
||||
one level below the container. All of them are type-checked now, entries
|
||||
included, and the header of `src/shared/stateSchema.js` lists which fields of
|
||||
the record get a type check and which get a `saved.x || default` or a verbatim
|
||||
passthrough, rather than asserting a rule the module does not follow.
|
||||
`networkById()` now throws on an id it does not know instead of quietly
|
||||
answering mainnet, and the gate's key tests are all own-property tests:
|
||||
`networkId` is an object key into `networkEndpoints`, so an unvalidated
|
||||
`"__proto__"` used to set that map's prototype and drop the user's endpoint
|
||||
silently. The three corrupt blobs from the issue drive the real popup entry
|
||||
point in `tests/stateRecovery.test.js` and the real worker in
|
||||
`tests/stateUnusableRpc.test.js`; each rendered nothing at all and answered
|
||||
`-32603` before this. `src/popup/restorableViews.js` moved to
|
||||
`src/shared/restorableViews.js`, since `persistedState.js` requires it and
|
||||
that module is in the background bundle.
|
||||
|
||||
- 2026-08-23: The background no longer reads or writes the shared `state`
|
||||
singleton ([#324](https://git.eeqj.de/sneak/AutistMask/issues/324)), which
|
||||
also closes the cold-worker wrong-chain send
|
||||
|
||||
Reference in New Issue
Block a user