harden: stop the background reading the shared state singleton, and enforce it at build time (closes #324)
All checks were successful
check / check (push) Successful in 33s
e2e / e2e-chrome (push) Successful in 1m45s
e2e / e2e-firefox (push) Successful in 31s

Five defects, one of which destroyed every wallet, came from src/background reading and writing the module-level state singleton the MV3 worker never populates, which silently served DEFAULT_STATE. Each point fix created the next defect. The background now has its own per-call getState() and a queued read-modify-write updateState(); the singleton is unreachable from it, and an unpopulated read throws instead of serving defaults.

The prohibition is enforced by the build, not by review: build.js asserts over esbuild's own metafile that no forbidden module is an input of a background bundle, so every specifier syntax esbuild resolves is covered, and both halves of the table are checked for rot -- a stale key, a stale module, an empty list, or an unlisted entry point under src/background/ all fail the build. The ESLint rule remains as fast local feedback and reads the same shared table. Known bounds are documented where the table lives.

Also closes #320: getProvider() now requires a validated network id, so a cold worker no longer prepares a non-mainnet dApp transaction for mainnet and gets refused by the wallet's own verifier. backgroundRefresh() no longer mutates address objects across a network round trip, the broadcast path takes its endpoint and chain id from one snapshot, and eight test storage stubs now structured-clone on get as the real chrome.storage.local does.

closes #320
This commit was merged in pull request #344.
This commit is contained in:
2026-08-23 17:57:30 +02:00
parent 36bc6bee0e
commit bd0a626e7b
40 changed files with 2959 additions and 653 deletions

View File

@@ -30,8 +30,11 @@ global.fetch = jest.fn(() => {
});
// state.js reads chrome.storage.local at module load; stub it so the
// default settings can be asserted against what the README promises.
global.chrome = { storage: { local: {} } };
// default settings can be asserted against what the README promises. Empty
// storage, so a load produces exactly the defaults.
const { makeStorageStub } = require("./support/storageStub");
global.chrome = { storage: makeStorageStub() };
const {
fetchRecentTransactions,
@@ -745,6 +748,16 @@ describe("dust threshold filtering", () => {
});
describe("filter defaults promised by the README and Settings", () => {
// The defaults are what a load of empty storage produces, so the load is
// part of the assertion rather than an incantation before it: reading the
// singleton before any load now throws (StateNotLoadedError), because a
// context served DEFAULT_STATE without asking for it is the whole subject
// of https://git.eeqj.de/sneak/AutistMask/issues/324. The stub at the top
// of this file has storage empty.
beforeAll(async () => {
await require("../src/shared/state").loadState();
});
test("all four toggles default to on and the threshold to 100,000 gwei", () => {
expect(state.hideSpoofedSymbols).toBe(true);
expect(state.hideLowHolderTokens).toBe(true);