fix: floor malformed allowedSites, fraudContracts and selectedToken entries (closes #362)
All checks were successful
check / check (push) Successful in 42s
e2e / e2e-chrome (push) Successful in 1m45s
e2e / e2e-firefox (push) Successful in 31s

A stored allowedSites whose value was not a list rendered a working popup and then made every subsequent save fail silently, so the user operated a wallet that persisted nothing -- worse than a blank popup, which is at least visibly broken. fraudContracts and selectedToken had the same shape: a container floored by truthiness or not at all, while its entries were dereferenced. Entries are now floored as well as containers, following the idiom #311 established, and a failed save raises a persistent banner instead of vanishing into a swallowed rejection.

The per-field justifications that used to live in a hand-written header are replaced by a contract test that drives each field's hostile and falsy values through a real popup boot, so a claim about a field answers to the code rather than to prose. Its guarantee is stated narrowly and deliberately: no structural dereference on the code paths a wholly-corrupted profile takes, which is not every path a stored record takes. The paths it does not drive are named where the claim is made, and are tracked in #379.
This commit was merged in pull request #366.
This commit is contained in:
2026-08-23 23:06:17 +02:00
parent 45500e66cf
commit a098bb0c32
14 changed files with 2100 additions and 354 deletions

View File

@@ -0,0 +1,864 @@
// What the floor under each persisted field actually guarantees — as a table
// that RUNS, one row per field.
//
// This file replaces a hand-written per-field justification in the header of
// src/shared/stateSchema.js. That comment shipped a false claim in three
// consecutive changes: every author wrote plausible prose about thirty fields,
// every reviewer re-derived it by hand, and it kept being wrong in a different
// place each time. The artifact was the problem. A claim nobody can execute is
// worse than no claim, because it is believed.
//
// So the claim is a row here instead:
//
// KIND.REFUSED assertStateUsable() refuses the record outright. Proven by
// stateProblem() naming a problem for every hostile value.
// KIND.ENTRIES normalizePersisted() floors the container AND its entries.
// Proven by holds() over the normalized value.
// KIND.SCALAR normalizePersisted() floors it to one scalar type, or to a
// fixed fallback. Proven the same way.
// KIND.LOOSE `saved.x || default`, no type check at all. The claim is
// that no structural dereference of it is reachable from a
// stored record — which cannot be argued, only driven, so the
// proof is a boot of the REAL popup entry point over a stored
// record carrying the hostile value, ONTO EVERY RESTORABLE
// VIEW. Home is not where this class of defect lives.
//
// Every row is driven through a boot regardless of kind, but only a LOOSE row
// (or a row that sets `alsoSweep`) is swept across the restore path: that is
// what declaring LOOSE costs. ENTRIES and SCALAR rows are proven by their
// holds() instead, because a floored value is not hostile by the time a
// renderer sees it. A LOOSE row must additionally prove it is loose: if
// someone floors the field — even partially — and leaves the row saying LOOSE,
// the "survives verbatim" assertion fails. A field added to PERSISTED_FIELDS
// with no row fails the first test in the file.
//
// The sweep is what makes a LOOSE row falsifiable, so read how it is driven
// before trusting it. A row the ROUTER reads (`routes`) gets its own boot per
// view, because a hostile value in it legitimately changes which view renders.
// Every other swept field is corrupted on the SAME boot, one boot per view per
// slot, and that boot has to land on the view it stored — so a field that does
// move the routing cannot hide in the crowd. Every swept field is driven at
// BOTH POLARITIES: a value nothing in src/ writes is a wrong-typed one and so
// always truthy, which leaves `if (!state.x) { state.y.deref() }` unentered on
// the very boot that corrupts x. The last slot is the falsy one for that
// reason, and a field that cannot be falsy after the floor says so in its row
// and is proven so.
//
// READ THE CLAIM NARROWLY. What this file proves is: NO STRUCTURAL
// DEREFERENCE ON THE CODE PATHS A WHOLLY-CORRUPTED PROFILE TAKES. That is not
// every path a stored record takes, and the difference is the whole of what
// this file does not cover:
//
// - Only the values in the table, in the SLOT arrangement below: four value
// combinations per view, not the product of twelve fields. A dereference
// reached only under a pairing no slot produces is not driven at all.
// - Only what a stored record reaches by ITSELF. A view only forward
// navigation opens, and anything behind a click, is not driven.
// - Nothing about the paths a HEALTHY profile takes, which is most of the
// popup. This file is a floor under one defect class, not a proof about
// the renderers.
//
// Within that boundary it is unconditional: if one of these boots leaves the
// popup unhealthy or off the view it stored, this file goes red — including
// when it takes two corrupted fields at once, because the verdict is the
// combined boot itself and the per-field re-boot below can only decorate the
// message. That last part is the one thing an earlier version got wrong: it
// asserted on the per-field list, so an observed dead popup that no single
// field reproduced was reported green.
//
// Booting every field separately at every value would be several hundred boots
// and most of the suite's budget; this is forty-four. Widening it further is
// out of scope — proving no field is dereferenced on any reachable render path
// is exhaustive verification of the popup, not a floor under a stored record.
//
// The three claims this replaced, all false, all caught here by construction:
// rpcUrl reaching `new JsonRpcProvider()` (a synchronous throw, not a caught
// request); viewData's ENTRIES being dereferenced by four restore branches
// that gate on one truthy field each; and selectedWallet, where a stale
// integer index is the SAFE case and `wallets["map"]` is the throwing one.
const {
PERSISTED_FIELDS,
normalizePersisted,
} = require("../src/shared/persistedState");
const { stateProblem } = require("../src/shared/stateSchema");
const { RESTORABLE_VIEWS } = require("../src/shared/restorableViews");
const {
bootPopup,
cleanupPopup,
unversionedValidProfile,
ADDRESS,
TOKEN_ADDRESS,
} = require("./support/popupBoot");
const KIND = {
REFUSED: "refused by the gate",
ENTRIES: "container and entries type-checked",
SCALAR: "scalar type-checked",
LOOSE: "loosely floored; safety proven by driving the popup",
};
const isText = (v) => typeof v === "string";
const isRecord = (v) =>
typeof v === "object" && v !== null && !Array.isArray(v);
const isIndexOrNull = (v) => v === null || (Number.isInteger(v) && v >= 0);
const isTextOrNull = (v) => v === null || (isText(v) && v !== "");
const everyEntry = (v, fn) => Array.isArray(v) && v.every(fn);
// A row is SWEPT — driven onto every restorable view rather than only onto
// Home — when its claim is that no restore path dereferences the field. That
// is what LOOSE means. The two index rows opt in with `alsoSweep` although
// they are floored, because the restore path is precisely why they gained a
// floor and the sweep is the regression guard on it.
const swept = (row) => row.kind === KIND.LOOSE || Boolean(row.alsoSweep);
// Every value a swept row drives through a boot: the hostile set, plus the
// falsy slot that gives the field its other polarity. `hostile` values are all
// TRUTHY by nature — a value nothing in src/ writes is a wrong-typed one, and
// wrong-typed values are objects, non-empty strings and non-zero numbers. A
// field that is only ever truthy on the boot that corrupts it cannot falsify
// `if (!state.x) { state.y.deref() }`, so the falsy slot is not optional.
const sweptValues = (row) => [...row.hostile, ...(row.falsy || [])];
// ------------------------------------------------------------------ the table
//
// `hostile` is values a stored record can carry that nothing in src/ ever
// writes. Each one is driven through the floor AND through a real popup boot —
// and, for a swept row, through one boot per restorable view — so keep the
// list short and pointed. `floorOnly` is extra values checked against the
// floor alone, which is pure and free. `hostileRestore` is extra values driven
// through the restore path only, for a value that means nothing until a
// particular branch's gate has let it past.
//
// `falsy` is the other POLARITY of a swept field, driven for the same reason.
// It is not a value src/ never writes — for three of these fields it is the
// DEFAULT_STATE default, which is the branch every ordinary install takes —
// and that is the point: without it, a dereference behind `if (!state.x)` is
// unreachable on the one boot that corrupts x. A swept row that cannot supply
// one says `neverFalsy` instead, which is proven rather than asserted: every
// falsy value stored under that field comes back TRUTHY from the floor, so no
// `!state.x` branch is reachable from a stored record at all.
const CONTRACT = [
{
field: "wallets",
kind: KIND.REFUSED,
hostile: [42, "notastructure", { a: 1 }, [null], [{ addresses: 1 }]],
},
{
field: "networkId",
kind: KIND.REFUSED,
hostile: [42, "notanetwork", { a: 1 }, "__proto__"],
},
{
field: "trackedTokens",
kind: KIND.ENTRIES,
hostile: [42, "notalist", { a: 1 }],
floorOnly: [[1, 2], [null], [{}], [[TOKEN_ADDRESS]]],
holds: (v) => everyEntry(v, (t) => isRecord(t) && isText(t.address)),
},
{
field: "allowedSites",
kind: KIND.ENTRIES,
hostile: [42, "notarecord", { [ADDRESS]: "notalist" }],
floorOnly: [
[ADDRESS],
{ [ADDRESS]: 42 },
{ [ADDRESS]: [42, null, {}] },
JSON.parse('{"__proto__":["evil.invalid"]}'),
],
holds: siteMapHolds,
},
{
field: "deniedSites",
kind: KIND.ENTRIES,
hostile: [42, "notarecord", { [ADDRESS]: "notalist" }],
floorOnly: [
[ADDRESS],
{ [ADDRESS]: 42 },
{ [ADDRESS]: [42, null, {}] },
JSON.parse('{"__proto__":["evil.invalid"]}'),
],
holds: siteMapHolds,
},
{
field: "fraudContracts",
kind: KIND.ENTRIES,
hostile: [42, "notalist", { a: 1 }],
floorOnly: [[42], [null], [{}], [[TOKEN_ADDRESS]]],
holds: (v) => everyEntry(v, isText),
},
{
field: "viewStack",
kind: KIND.ENTRIES,
hostile: [42, "notalist", ["main", "show-phrase", "settings"]],
floorOnly: [[1, 2], [null], [{}], ["export-privkey"]],
// Truncated at the first entry the popup will not reopen onto, rather
// than filtered: every surviving entry's Back target has to stay the
// one it had. restorableStack() may also substitute ["main"] under a
// view restored below the root, so this is the one ENTRIES field whose
// result is not always a subset of what was stored.
holds: (v) => everyEntry(v, (e) => RESTORABLE_VIEWS.has(e)),
},
{
field: "networkEndpoints",
kind: KIND.ENTRIES,
hostile: [42, "notarecord", { mainnet: "notapair" }],
floorOnly: [
[1, 2],
{ mainnet: { rpcUrl: 42, blockscoutUrl: {} } },
{ mainnet: { rpcUrl: "", blockscoutUrl: [] } },
{ sepolia: 42 },
],
// Entries are coerced rather than dropped: an unknown network id is
// KEPT, so a profile that has been on a build with more networks does
// not lose their endpoints here. What is floored is the two URL fields
// inside the pair, which applyChainSwitchFields() assigns straight onto
// s.rpcUrl / s.blockscoutUrl on the next switch.
holds: (v) =>
isRecord(v) &&
Object.keys(v).every((id) => {
const pair = v[id];
return (
isRecord(pair) &&
(pair.rpcUrl === undefined ||
(isText(pair.rpcUrl) && pair.rpcUrl !== "")) &&
(pair.blockscoutUrl === undefined ||
(isText(pair.blockscoutUrl) &&
pair.blockscoutUrl !== ""))
);
}),
},
{
field: "rpcUrl",
kind: KIND.SCALAR,
hostile: [42, true, { a: 1 }],
floorOnly: [[], "", null],
holds: (v) => isText(v) && v !== "",
// The claim this row replaced said a bad value "fails the request on a
// path that already catches". It does not: getProvider() hands rpcUrl
// to `new JsonRpcProvider()`, which throws SYNCHRONOUSLY, from two call
// sites outside any try — and a stored `currentView: "wait-tx"` reaches
// one of them through restoreView(). So the row proves the claim
// against the real constructor rather than describing it.
alsoProven: (normalized, hostile) => {
// requireActual: bootPopup() mocks this module out for the boots
// above, and a mocked getProvider() would prove nothing at all
// about the constructor this row is a claim about.
const { getProvider } = jest.requireActual(
"../src/shared/balances",
);
expect(() => getProvider(hostile, "mainnet")).toThrow();
const provider = getProvider(normalized, "mainnet");
expect(provider).toBeTruthy();
provider.destroy();
},
},
{
field: "blockscoutUrl",
kind: KIND.SCALAR,
hostile: [42, true, { a: 1 }],
floorOnly: [[], "", null],
holds: (v) => isText(v) && v !== "",
},
{
field: "activeAddress",
kind: KIND.SCALAR,
hostile: [42, true, { a: 1 }],
floorOnly: [[ADDRESS], ""],
holds: isTextOrNull,
},
{
field: "selectedToken",
kind: KIND.SCALAR,
hostile: [42, true, { a: 1 }],
floorOnly: [[TOKEN_ADDRESS], ""],
holds: isTextOrNull,
},
{
field: "selectedWallet",
kind: KIND.SCALAR,
// The prototype members are the whole point: `wallets["map"]` is
// TRUTHY, so hasValidAddress()'s `&&` does not short-circuit and
// `.addresses[…]` throws. A stale INTEGER is the safe case.
hostile: ["map", "__proto__", { a: 1 }],
floorOnly: ["length", "constructor", "toString", "0", -1, 1.5, true],
holds: isIndexOrNull,
// SCALAR, and swept anyway: the restore path is precisely why this
// field gained a floor, so the sweep is the regression guard on it.
alsoSweep: true,
routes: true,
// A stale INTEGER index, which reaches the restore path by a different
// route from the prototype members above — falsy or out of range
// rather than truthy — and has to keep being the safe case.
hostileRestore: [{ value: "length" }, { value: 5 }],
},
{
field: "selectedAddress",
kind: KIND.SCALAR,
hostile: ["map", "__proto__", { a: 1 }],
floorOnly: ["length", "constructor", "toString", "0", -1, 1.5, true],
holds: isIndexOrNull,
alsoSweep: true,
routes: true,
hostileRestore: [{ value: 5 }],
},
{
field: "currentView",
kind: KIND.LOOSE,
routes: true,
// Compared, and concatenated into the debug banner's textContent
// (src/popup/views/helpers.js) with no gate in front of it, which
// coerces. Nothing renders FROM it without RESTORABLE_VIEWS.has()
// first, and Set.has() answers false for any value.
hostile: [42, "no-such-view", { a: 1 }],
// `saved.currentView || null`: the falsy polarity is the popup landing
// on Home, which every boot in "booting onto Home" below also drives.
falsy: [""],
},
{
field: "viewData",
kind: KIND.LOOSE,
routes: true,
// The container is taken verbatim; what makes its ENTRIES safe is the
// per-branch guard in src/popup/viewRouter.js. The sweep drives the
// container shapes below onto every restorable view; hostileRestore
// adds the records that PASS a branch's gate and then hand its
// renderer something it dereferences, which is where the entries are
// actually decided.
hostile: [42, "notarecord", { a: 1 }, [1, 2]],
// `structuredClone(saved.viewData || {})`: the container is never falsy
// in state whatever was stored, so no `!state.viewData` branch exists to
// drive.
neverFalsy: true,
hostileRestore: [
// success-tx passes on `data.hash`, and renderSuccess() then calls
// toAddressHtml(d.to) -> addressTitle() -> address.toLowerCase().
{ value: { hash: "0x1" }, views: ["success-tx"] },
{ value: { hash: "0x1", to: 42 }, views: ["success-tx"] },
{
value: { hash: "0x1", to: ADDRESS, decoded: { details: 7 } },
views: ["success-tx"],
},
{
value: {
hash: "0x1",
to: ADDRESS,
decoded: { details: [{ address: 42 }] },
},
views: ["success-tx"],
},
// error-tx passes on `data.message`, same dereference.
{ value: { message: "boom" }, views: ["error-tx"] },
{ value: { message: "boom", to: 42 }, views: ["error-tx"] },
// transaction passes on `data.tx`.
{ value: { tx: { hash: "0x1" } }, views: ["transaction"] },
{
value: {
tx: {
hash: "0x1",
from: ADDRESS,
to: ADDRESS,
contractAddress: 42,
},
},
views: ["transaction"],
},
// confirm-tx passes on `data.pendingTx`.
{ value: { pendingTx: { amount: "1" } }, views: ["confirm-tx"] },
{
value: {
pendingTx: {
token: 42,
from: ADDRESS,
to: ADDRESS,
amount: "1",
},
},
views: ["confirm-tx"],
},
// wait-tx passes on `pendingWait.hash`; restoreWait() has checked
// the fields below it since it was written, and this is the
// regression guard.
{
value: {
pendingWait: {
hash: "0x1",
txInfo: { to: 42, amount: "1" },
},
},
views: ["wait-tx"],
},
// A record that passes EVERY branch's gate at once, driven onto
// every restorable view: a branch a view does not read must stay
// one it does not read, and each renderer must survive the fields
// another branch left behind.
{
value: {
hash: "0x1",
message: "boom",
tx: { hash: "0x1" },
pendingTx: { amount: "1" },
pendingWait: { hash: "0x1" },
},
},
],
},
{
field: "lastBalanceRefresh",
kind: KIND.LOOSE,
// Arithmetic only: `now - (s.lastBalanceRefresh || 0)` compares false
// for a non-number and forces a refresh.
hostile: [true, "notatime", { a: 1 }],
// `|| 0` collapses every falsy stored value to 0, so 0 IS the whole
// falsy polarity of this field — and it is the DEFAULT_STATE default,
// the value a profile carries until its first refresh lands.
falsy: [0],
},
{
field: "tokenHolderCache",
kind: KIND.LOOSE,
// Nothing DEREFERENCES it structurally. It is read by the
// field-agnostic snapshotPersisted()/deepEqual() in
// src/shared/state.js, which are safe for any value, and otherwise
// only reset wholesale in src/shared/chainSwitchFields.js.
hostile: [42, "notarecord", [1, 2]],
// `structuredClone(saved.tokenHolderCache || {})`.
neverFalsy: true,
},
{
field: "theme",
kind: KIND.LOOSE,
// Compared against "dark"/"light" in applyTheme() and otherwise falls
// to the system branch; assigned into an input .value, which coerces.
hostile: [42, "chartreuse", { a: 1 }],
// `saved.theme || "system"`.
neverFalsy: true,
},
{
field: "dustThresholdGwei",
kind: KIND.LOOSE,
hostile: ["notanumber", true, { a: 1 }],
// Survives verbatim, so the falsy slot is also wrong-typed: "" reaches
// filterTransactions() as a comparand and a settings input .value.
falsy: [""],
},
...[
"rememberSiteChoice",
"showZeroBalanceTokens",
"hideSpoofedSymbols",
"hideLowHolderTokens",
"hideFraudContracts",
"hideDustTransactions",
"utcTimestamps",
"debugMode",
].map((field) => ({
field,
kind: KIND.LOOSE,
// A flag: only ever tested for truthiness, and written back verbatim.
hostile: [42, "notabool", { a: 1 }],
// Both answers to that truthiness test have to be driven, and 0 is a
// value src/ never writes for a flag. For utcTimestamps and debugMode
// the falsy answer is also the DEFAULT_STATE default.
falsy: [0],
})),
];
function siteMapHolds(v) {
return (
isRecord(v) &&
Object.getPrototypeOf(v) === Object.prototype &&
!Object.prototype.hasOwnProperty.call(v, "__proto__") &&
Object.keys(v).every((key) => everyEntry(v[key], isText))
);
}
afterEach(() => {
cleanupPopup();
});
// -------------------------------------------------------------- exhaustive
describe("the contract covers the record", () => {
test("every persisted field has exactly one row, and no row invents one", () => {
const rows = CONTRACT.map((row) => row.field);
expect([...rows].sort()).toEqual([...PERSISTED_FIELDS].sort());
});
test("every row declares a kind this file knows how to prove", () => {
const kinds = Object.values(KIND);
for (const row of CONTRACT) {
expect(kinds).toContain(row.kind);
expect(row.hostile.length).toBeGreaterThan(0);
}
});
});
// ------------------------------------------------------------- the floors
function profileWith(field, value) {
return unversionedValidProfile({ [field]: value });
}
describe("the floor each row claims", () => {
// The falsy slot is deliberately NOT in here. `saved.x || default` is a
// floor on falsy values and on nothing else, so a falsy value is the one
// thing a LOOSE field need not carry through verbatim; what it has to carry
// through is being falsy, which "both polarities" below asserts.
for (const row of CONTRACT) {
const values = [...row.hostile, ...(row.floorOnly || [])];
if (row.kind === KIND.REFUSED) {
test(`${row.field}: the gate refuses it`, () => {
for (const value of values) {
expect(
typeof stateProblem(profileWith(row.field, value)),
).toBe("string");
}
});
continue;
}
test(`${row.field}: ${row.kind}`, () => {
for (const value of values) {
const out = normalizePersisted(profileWith(row.field, value));
if (row.kind === KIND.LOOSE) {
// The claim IS that there is no floor. A field that grows
// one has to move to another kind rather than keep a row
// saying its readers are what make it safe.
continue;
}
expect({
value: value,
holds: row.holds(out[row.field]),
}).toEqual({ value: value, holds: true });
}
});
if (row.kind === KIND.LOOSE) {
test(`${row.field}: is genuinely unfloored`, () => {
// EVERY value, not some: a PARTIAL floor is still a floor, and
// a row that keeps saying LOOSE because one hostile value out
// of three still survives is exactly the stale claim this file
// exists to stop.
for (const value of values) {
const out = normalizePersisted(
profileWith(row.field, value),
);
expect({
value: value,
survived: JSON.stringify(out[row.field]),
}).toEqual({
value: value,
survived: JSON.stringify(value),
});
}
});
}
}
});
// --------------------------------------------- driving the real popup boot
// A booted popup is healthy when nothing threw out of init() and something is
// on screen. A throw out of restoreView() is neither: init() does not guard it,
// so the rest of popup init never runs and the user gets a popup with no view,
// no message and no control on it.
async function bootHealth(profile) {
const env = await bootPopup(profile);
return {
errors: env.pageErrors,
blank: env.visibleViews().length === 0,
};
}
const HEALTHY = { errors: [], blank: false };
// unversionedValidProfile() stores no currentView, so every boot in here lands
// on Home. That is the cheap half of the proof; the restore path below is the
// half that matters.
// Both polarities of every swept field are driven, or the field is proven
// unable to take one of them. This is the guard on the sweep itself: a hostile
// set is all-truthy by construction, so without a falsy slot a dereference
// behind `if (!state.x)` is never reached on the boot that corrupts x — the
// same falsy-collapse blind spot the fields below were floored for.
describe("both polarities of every swept field are driven", () => {
const FALSY_STORED = [0, "", false, null];
const floored = (field, value) =>
normalizePersisted(profileWith(field, value))[field];
for (const row of CONTRACT) {
if (!swept(row)) continue;
if (row.neverFalsy) {
test(`${row.field}: cannot be falsy in state at all`, () => {
for (const value of FALSY_STORED) {
expect({
stored: value,
truthy: Boolean(floored(row.field, value)),
}).toEqual({ stored: value, truthy: true });
}
});
continue;
}
test(`${row.field}: truthy and falsy`, () => {
// What the boots below actually drive, floored the way a renderer
// sees it — not what the row says it drives.
const driven = [
...sweptValues(row),
...(row.hostileRestore || []).map((entry) => entry.value),
].map((value) => floored(row.field, value));
expect({
truthy: driven.some((value) => Boolean(value)),
falsy: driven.some((value) => !value),
}).toEqual({ truthy: true, falsy: true });
});
}
});
describe("a hostile value for one field, booting onto Home", () => {
for (const row of CONTRACT) {
for (const value of sweptValues(row)) {
test(`${row.field} = ${JSON.stringify(value)}`, async () => {
await expect(
bootHealth(profileWith(row.field, value)),
).resolves.toEqual(HEALTHY);
});
}
}
});
describe("a row's extra proof against the real reader", () => {
for (const row of CONTRACT) {
if (!row.alsoProven) continue;
test(row.field, () => {
for (const value of row.hostile) {
const out = normalizePersisted(profileWith(row.field, value));
row.alsoProven(out[row.field], value);
}
});
}
});
// ------------------------------------------------ driving the restore path
// Everything above lands on Home. Home is not where this class of defect
// lives: all three of the false claims this file replaced were falsified by a
// RESTORE, through the unguarded restoreView() in src/popup/index.js. So a
// swept row's hostile values are driven onto EVERY restorable view, one boot
// each.
//
// This is what makes a LOOSE row falsifiable. A field that gains a structural
// dereference on any restorable view — `state.theme.toLowerCase()` in a view's
// show(), say — turns the row red here, instead of waiting for a reviewer to
// re-derive the claim by hand.
// restoreWait() resumes from this, so it has to be a finite number and recent
// enough that the resumed deadline has not already passed — a wait that has
// outlived its deadline resolves on the first poll instead of staying on
// screen. Read once at module load, so every boot in one run shares it.
const BROADCAST_TIME = Date.now();
// A viewData well formed for every restorable branch at once, so the only
// thing a swept boot can fail on is the field the row corrupts. "the base
// profile the sweep corrupts" below proves this really does render each view
// rather than falling back — without that, a sweep could pass by never
// reaching a renderer at all.
const WELL_FORMED_DATA = {
hash: "0x1",
message: "boom",
to: ADDRESS,
decoded: { details: [{ address: TOKEN_ADDRESS }] },
tx: { hash: "0x1", from: ADDRESS, to: ADDRESS, contractAddress: null },
pendingTx: {
token: "ETH",
from: ADDRESS,
to: ADDRESS,
amount: "1",
balance: "2",
},
pendingWait: {
hash: "0x1",
txInfo: { to: ADDRESS, amount: "1" },
broadcastTime: BROADCAST_TIME,
},
};
function restoringOnto(view, extra) {
return unversionedValidProfile({
currentView: view,
selectedWallet: 0,
selectedAddress: 0,
selectedToken: TOKEN_ADDRESS,
viewStack: ["main"],
viewData: WELL_FORMED_DATA,
...extra,
});
}
// A boot that RESTORED is healthy and landed on the view it stored, rather
// than falling back to Home — which a healthy boot also does, and which would
// let a sweep pass by never running the renderer it is aimed at.
async function restoredHealth(profile, view) {
const env = await bootPopup(profile);
return {
errors: env.pageErrors,
restored: env.visibleViews().includes(view),
};
}
const RESTORED = { errors: [], restored: true };
describe("the base profile the sweep corrupts", () => {
for (const view of RESTORABLE_VIEWS) {
test(`renders ${view} rather than falling back`, async () => {
await expect(
restoredHealth(restoringOnto(view), view),
).resolves.toEqual(RESTORED);
});
}
});
// A field the ROUTER itself reads — the two it gates on and the two
// hasValidAddress() indexes with. A hostile value in one of these legitimately
// changes which view renders, so each gets its own boot per view and is held
// only to "healthy", not to "restored onto the view it stored".
const routes = (row) => Boolean(row.routes);
// Every routing row × every hostile value × every restorable view. Profiles
// are deduplicated because a hostile `currentView` REPLACES the view being
// restored onto, which would otherwise be the same boot eleven times.
describe("a hostile routing value restoring onto", () => {
for (const row of CONTRACT) {
if (!swept(row) || !routes(row)) continue;
const seen = new Set();
for (const value of sweptValues(row)) {
for (const view of RESTORABLE_VIEWS) {
const profile = restoringOnto(view, { [row.field]: value });
const key = JSON.stringify(profile);
if (seen.has(key)) continue;
seen.add(key);
test(`${view}: ${row.field} = ${JSON.stringify(
value,
)}`, async () => {
await expect(bootHealth(profile)).resolves.toEqual(HEALTHY);
});
}
}
}
});
// Every OTHER swept field, corrupted at once, one boot per view per hostile
// slot: twelve fields on one boot rather than twelve boots. A field is only in
// here because it is not one the router reads — and that is ASSERTED, not
// argued, because the boot has to land on `view`. A field that does move the
// routing turns this red and has to declare `routes` and take the individual
// sweep above.
//
// Combining hides one thing, and the last slot is what stops it. A hostile
// value is wrong-typed and therefore TRUTHY, so on a boot where every swept
// field is hostile, no `if (!state.x)` branch is entered — and a dereference
// inside such a branch would go unseen however loudly it throws. The last slot
// is the falsy one: every swept field that CAN be falsy is falsy on it, which
// is also the state an ordinary install boots in for three of them, while the
// fields that cannot be falsy stay hostile-truthy. That makes it a MIX, and a
// deliberate one — the interaction between a falsy flag and a still-hostile
// theme is a shape a stored record really produces.
//
// The verdict is the combined boot, always. When it goes red the same view is
// re-booted one field at a time, so the failure NAMES a culprit instead of
// leaving a reader to bisect twelve fields — but that loop only decorates the
// message. It cannot clear the failure. A dereference that needs two corrupted
// fields at once is reproduced by neither field alone, and a version of this
// file that asserted on the named list reported exactly that case green while
// watching the popup die.
const UNROUTED = CONTRACT.filter((row) => swept(row) && !routes(row));
const HOSTILE_SLOTS = Math.max(
...UNROUTED.map((row) => sweptValues(row).length),
);
function unroutedValues(slot) {
const fields = {};
for (const row of UNROUTED) {
const values = sweptValues(row);
fields[row.field] = values[slot % values.length];
}
return fields;
}
describe("every field the router does not read, corrupted at once, onto", () => {
for (const view of RESTORABLE_VIEWS) {
for (let slot = 0; slot < HOSTILE_SLOTS; slot++) {
test(`${view}: hostile value ${slot + 1} in all ${
UNROUTED.length
} of them`, async () => {
const fields = unroutedValues(slot);
const together = await restoredHealth(
restoringOnto(view, fields),
view,
);
// The per-field re-boot only DECORATES the message. The
// verdict is `together`, unconditionally: a dereference that
// needs two corrupted fields at once is reproduced by NEITHER
// field alone, so an assertion on the named list would report
// an observed dead popup as green.
const named = [];
if (together.errors.length > 0 || !together.restored) {
for (const row of UNROUTED) {
const one = await restoredHealth(
restoringOnto(view, {
[row.field]: fields[row.field],
}),
view,
);
if (one.errors.length === 0 && one.restored) continue;
named.push(
`${row.field}=${JSON.stringify(
fields[row.field],
)}: ` +
(one.errors.join("; ") || `fell off ${view}`),
);
}
if (named.length === 0) {
named.push(
"no single field reproduces it; it takes two or " +
`more of ${JSON.stringify(fields)}`,
);
}
}
expect({
view: view,
together: together,
fields: named,
}).toEqual({ view: view, together: RESTORED, fields: [] });
});
}
}
});
// The values that only mean something on the restore path: a viewData that
// PASSES a branch's gate and then hands its renderer something dereferenced,
// and the index values whose route through hasValidAddress() differs from the
// row's own hostile set.
describe("a restore-only hostile value onto", () => {
for (const row of CONTRACT) {
for (const entry of row.hostileRestore || []) {
for (const view of entry.views || RESTORABLE_VIEWS) {
test(`${view}: ${row.field} = ${JSON.stringify(
entry.value,
)}`, async () => {
await expect(
bootHealth(
restoringOnto(view, { [row.field]: entry.value }),
),
).resolves.toEqual(HEALTHY);
});
}
}
}
});