1 Commits
Author SHA1 Message Date
sneak 9267a5070c fix: a network switch in Settings keeps another page's token on the network it was added on (closes #692)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
A switch in Settings filed under the new network a token or fraud contract that another page stored since this page's last save. The switch's save wrote only the lists this page had changed, so the other page's entry stayed in the active list.

Before the switch, the page now takes the stored tracked tokens into its own list, by contract address. A token that either page added or removed since this page's last save stays as that page left it. The switch then files the list under the previous network. The switch's save also clears the stored fraud contracts, as the switch clears this page's own.

Model: opus-5-5
2026-10-27 15:54:01 +00:00
2 changed files with 56 additions and 7 deletions

No files matched your search

+10 -7
View File
@@ -125,11 +125,12 @@ function currentNetwork() {
return networkById(state.networkId); return networkById(state.networkId);
} }
// The persisted fields as this page held them when its last loadState() // `baseline` holds the persisted fields as this page held them when its last
// finished, or when its last successful saveState() began, with what this page // loadState() finished, or when its last successful saveState() began.
// has taken from the stored record since (takeStoredNetwork() and // takeStoredNetwork() and takeStoredTrackedTokens() also write into it what
// takeStoredTrackedTokens()). saveState() diffs the live state against this to // this page has taken from the stored record since then. saveState() diffs the
// find only the fields THIS page changed since. // live state against `baseline` to find only the fields THIS page changed
// since.
// //
// Deep-cloned, not a reference: callers mutate persisted objects and arrays // Deep-cloned, not a reference: callers mutate persisted objects and arrays
// in place (state.wallets.push(...)), and a reference baseline would mutate // in place (state.wallets.push(...)), and a reference baseline would mutate
@@ -442,8 +443,10 @@ function mergeNetworkEndpoints(base, ours, theirs) {
// milder than the other two. // milder than the other two.
// //
// Every other persisted field stays a whole-field diff: // Every other persisted field stays a whole-field diff:
// `trackedTokens`/`fraudContracts`/`viewStack` are arrays of scalars with no // `fraudContracts`/`viewStack` are arrays of scalars with no per-element
// per-element identity to merge by; `networkTrackedTokens` is diffed whole like // identity to merge by; a save diffs `trackedTokens` whole, and a switch in
// Settings first merges that list by contract address
// (takeStoredTrackedTokens()); `networkTrackedTokens` is diffed whole like
// `trackedTokens` and `networkId`, the two fields a network switch moves // `trackedTokens` and `networkId`, the two fields a network switch moves
// together with it; `tokenHolderCache` is a map shaped like the ones above, // together with it; `tokenHolderCache` is a map shaped like the ones above,
// but nothing in src/ ever writes an entry into it — it is only ever reset // but nothing in src/ ever writes an entry into it — it is only ever reset
+46
View File
@@ -221,6 +221,52 @@ test("a token another page removed before a switch in Settings stays removed", a
expect(stored.networkTrackedTokens.mainnet).toEqual([]); expect(stored.networkTrackedTokens.mainnet).toEqual([]);
}); });
// Another popup page removes DAI after this page's last save. That page then
// adds DAI again, typing the contract address in lower case. The two pages'
// entries for DAI match, because contract addresses are compared ignoring
// letter case. The switch to Sepolia keeps this page's entry under Ethereum
// Mainnet.
test("a token another page added again with a lower-case address keeps this page's entry", async () => {
const boot = await bootPopup(
unversionedValidProfile({ trackedTokens: [MAINNET_TOKEN] }),
);
expect(boot.pageErrors).toEqual([]);
boot.storage.write("autistmask", {
...boot.storage.read("autistmask"),
trackedTokens: [
{ ...MAINNET_TOKEN, address: MAINNET_TOKEN.address.toLowerCase() },
],
});
await boot.click("btn-settings");
await chooseNetwork(boot, "sepolia");
const stored = boot.storage.read("autistmask");
expect(stored.networkId).toBe("sepolia");
expect(stored.networkTrackedTokens.mainnet).toEqual([MAINNET_TOKEN]);
});
// Another popup page saves a switch to Sepolia after this page's last save.
// TST is tracked on Sepolia. This page has not taken that switch yet. A switch
// to Sepolia in this page's Settings keeps TST off Ethereum Mainnet's list.
test("a token tracked on Sepolia when another page switched there is not tracked on Ethereum Mainnet", async () => {
const boot = await bootPopup(unversionedValidProfile());
expect(boot.pageErrors).toEqual([]);
await boot.click("btn-settings");
boot.storage.write("autistmask", {
...boot.storage.read("autistmask"),
networkId: "sepolia",
trackedTokens: [SEPOLIA_TOKEN],
});
await chooseNetwork(boot, "sepolia");
const stored = boot.storage.read("autistmask");
expect(stored.networkId).toBe("sepolia");
expect(stored.trackedTokens).toEqual([SEPOLIA_TOKEN]);
expect(stored.networkTrackedTokens.mainnet).toEqual([]);
});
// Another popup page records a fraud contract on Ethereum Mainnet after this // Another popup page records a fraud contract on Ethereum Mainnet after this
// page's last save. The switch to Sepolia drops that fraud contract, as the // page's last save. The switch to Sepolia drops that fraud contract, as the
// switch drops this page's own. // switch drops this page's own.