fix: a chain switch gives trackedTokens its own copy of the network's list (closes #719)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

After a switch, `trackedTokens` and that network's list in
`networkTrackedTokens` were one array. So a page's next add or removal also
changed `networkTrackedTokens`, and the save wrote that page's older copy over
the stored one. Another page's add on the previous network was lost, and its
removal was undone.

`applyChainSwitchFields()` now copies the incoming network's list. An add or
removal on the active network changes only `trackedTokens`.

Model: opus-5-5
This commit is contained in:
2026-11-03 05:24:31 +00:00
parent 267b69382e
commit b39110544d
2 changed files with 57 additions and 3 deletions
+7 -3
View File
@@ -42,11 +42,15 @@ function applyChainSwitchFields(s, newNetworkId) {
// Tracked tokens are kept per network the same way, because a contract // Tracked tokens are kept per network the same way, because a contract
// address names a token on one network only. s.trackedTokens stays the // address names a token on one network only. s.trackedTokens stays the
// active network's list, so nothing that reads it changes: the outgoing // active network's list, so nothing that reads it changes: the outgoing
// list is put away under its network, and the incoming network's list, or // list is put away under its network, and a copy of the incoming network's
// an empty one, takes its place. // list, or an empty list, takes its place.
//
// It is a copy because a save writes a changed s.networkTrackedTokens whole,
// over any list another page stored. An add or removal on the active network
// must change only s.trackedTokens.
if (!s.networkTrackedTokens) s.networkTrackedTokens = {}; if (!s.networkTrackedTokens) s.networkTrackedTokens = {};
s.networkTrackedTokens[s.networkId] = s.trackedTokens; s.networkTrackedTokens[s.networkId] = s.trackedTokens;
s.trackedTokens = s.networkTrackedTokens[net.id] || []; s.trackedTokens = structuredClone(s.networkTrackedTokens[net.id] || []);
s.networkId = net.id; s.networkId = net.id;
s.rpcUrl = remembered.rpcUrl || net.defaultRpcUrl; s.rpcUrl = remembered.rpcUrl || net.defaultRpcUrl;
+50
View File
@@ -466,6 +466,56 @@ test("a token another page removed on Ethereum Mainnet stays removed when this p
expect(stored.trackedTokens).toEqual([]); expect(stored.trackedTokens).toEqual([]);
}); });
// This page switches to Sepolia, where TST is tracked. Another popup page has
// not taken that switch, and its save tracks DAI on Ethereum Mainnet. This page
// then adds a token on Sepolia. Its save changes only `trackedTokens`, so DAI
// stays on Ethereum Mainnet's list.
test("a token another page added on Ethereum Mainnet stays when this page adds a token on Sepolia", async () => {
const boot = await bootOnAddToken("mainnet", {
networkTrackedTokens: { sepolia: [SEPOLIA_TOKEN] },
});
expect(boot.pageErrors).toEqual([]);
await gearSwitchBack(boot);
boot.storage.write("autistmask", {
...boot.storage.read("autistmask"),
networkTrackedTokens: {
mainnet: [MAINNET_TOKEN],
sepolia: [SEPOLIA_TOKEN],
},
});
const added = await addNewToken(boot);
const stored = boot.storage.read("autistmask");
expect(stored.networkTrackedTokens.mainnet).toEqual([MAINNET_TOKEN]);
expect(stored.networkTrackedTokens.sepolia).toEqual([SEPOLIA_TOKEN]);
expect(stored.trackedTokens).toEqual([SEPOLIA_TOKEN, added]);
});
// This page tracks DAI on Ethereum Mainnet and switches to Sepolia, where TST is
// tracked. Another popup page has not taken that switch, and its save removes
// DAI from Ethereum Mainnet's list. This page then adds a token on Sepolia. Its
// save changes only `trackedTokens`, so DAI does not come back.
test("a token another page removed on Ethereum Mainnet stays removed when this page adds a token on Sepolia", async () => {
const boot = await bootOnAddToken("mainnet", {
trackedTokens: [MAINNET_TOKEN],
networkTrackedTokens: { sepolia: [SEPOLIA_TOKEN] },
});
expect(boot.pageErrors).toEqual([]);
await gearSwitchBack(boot);
boot.storage.write("autistmask", {
...boot.storage.read("autistmask"),
networkTrackedTokens: { mainnet: [], sepolia: [SEPOLIA_TOKEN] },
});
const added = await addNewToken(boot);
const stored = boot.storage.read("autistmask");
expect(stored.networkTrackedTokens.mainnet).toEqual([]);
expect(stored.networkTrackedTokens.sepolia).toEqual([SEPOLIA_TOKEN]);
expect(stored.trackedTokens).toEqual([SEPOLIA_TOKEN, added]);
});
// 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.