diff --git a/src/shared/chainSwitch.js b/src/shared/chainSwitch.js index e58337e..9e4950f 100644 --- a/src/shared/chainSwitch.js +++ b/src/shared/chainSwitch.js @@ -61,20 +61,31 @@ function clearPopupCaches() { // (https://git.eeqj.de/sneak/AutistMask/issues/653, // https://git.eeqj.de/sneak/AutistMask/issues/692). // +// When the stored record is on another network, a switch saved elsewhere has +// not reached this page yet. This page first takes that switch with +// takeStoredNetwork() (state.js), as followStoredSettings() does. The switch to +// `newNetworkId` is then made from the stored record's network. So every +// network's list of tracked tokens in the stored record is kept +// (https://git.eeqj.de/sneak/AutistMask/issues/701). When the stored record is +// already on `newNetworkId`, the switch made from there only resets the +// balances and caches. +// // Before the switch, `state` takes the tracked tokens that another page stored // since this page's last save (takeStoredTrackedTokens() in state.js). The // switch then puts them away under the previous network, the network they were -// added on. When the stored record is on another network, a switch saved -// elsewhere has not reached this page yet. The stored record's tokens are then -// that network's, and none are taken. +// added on. async function onChainSwitch(newNetworkId) { - const { state, saveState, takeStoredTrackedTokens } = require("./state"); + const { + state, + saveState, + takeStoredNetwork, + takeStoredTrackedTokens, + } = require("./state"); let net = null; await saveState((stored) => { if (state.networkId === newNetworkId) return false; - if (stored.networkId === state.networkId) { - takeStoredTrackedTokens(stored); - } + if (stored.networkId !== state.networkId) takeStoredNetwork(stored); + takeStoredTrackedTokens(stored); net = applyChainSwitchFields(state, newNetworkId); stored.lastBalanceRefresh = 0; stored.fraudContracts = []; diff --git a/src/shared/state.js b/src/shared/state.js index 7ceba28..b2459fc 100644 --- a/src/shared/state.js +++ b/src/shared/state.js @@ -527,6 +527,14 @@ async function saveStateOnce(beforeWrite) { merged[key] = current[key]; } } + // When the merged record names a network other than this page's, this page + // has not yet taken a switch saved elsewhere. This page's `trackedTokens` + // is then the list of the network that this page still has. So the merged + // record keeps the stored list + // (https://git.eeqj.de/sneak/AutistMask/issues/701). + if (merged.networkId !== current.networkId) { + merged.trackedTokens = fresh.trackedTokens; + } merged.hasWallet = Boolean(merged.wallets && merged.wallets.length > 0); // Stamped on every write, never merged or diffed: the record that goes to // storage is in THIS build's shape whatever shape it was read in, which is @@ -608,11 +616,12 @@ function saveState(beforeWrite) { } // Give this page the network switch held by `stored`, a record a save of this -// page has just read (followStoredSettings() in src/popup/index.js calls this -// from `beforeWrite`), in `state` and in `baseline` alike, as loadState() does -// with a whole record. The switch is then not this page's own change, even when -// that save fails to write: a later switch back to the previous network still -// differs from `baseline`, and is stored. +// page has just read (followStoredSettings() in src/popup/index.js and +// onChainSwitch() in src/shared/chainSwitch.js call this from `beforeWrite`), +// in `state` and in `baseline` alike, as loadState() does with a whole +// record. The switch is then not this page's own change, even when that save +// fails to write: a later switch back to the previous network still differs +// from `baseline`, and is stored. function takeStoredNetwork(stored) { copyChainSwitchFields(rawState, stored); copyChainSwitchFields(baseline, structuredClone(stored)); diff --git a/tests/trackedTokensPerNetwork.test.js b/tests/trackedTokensPerNetwork.test.js index ff0b113..88350be 100644 --- a/tests/trackedTokensPerNetwork.test.js +++ b/tests/trackedTokensPerNetwork.test.js @@ -257,6 +257,7 @@ test("a token tracked on Sepolia when another page switched there is not tracked ...boot.storage.read("autistmask"), networkId: "sepolia", trackedTokens: [SEPOLIA_TOKEN], + networkTrackedTokens: { mainnet: [] }, }); await chooseNetwork(boot, "sepolia"); @@ -267,6 +268,62 @@ test("a token tracked on Sepolia when another page switched there is not tracked expect(stored.networkTrackedTokens.mainnet).toEqual([]); }); +// Another popup page saves a switch to Sepolia, where TST is tracked. Before +// taking that switch, this page adds a token on Ethereum Mainnet. The add's +// save does not write this page's Ethereum Mainnet list as Sepolia's list. +test("a token added before this page takes another page's switch leaves Sepolia's tokens tracked", async () => { + const boot = await bootOnAddToken("mainnet", { + trackedTokens: [MAINNET_TOKEN], + networkTrackedTokens: { sepolia: [SEPOLIA_TOKEN] }, + }); + expect(boot.pageErrors).toEqual([]); + boot.storage.write("autistmask", { + ...boot.storage.read("autistmask"), + networkId: "sepolia", + trackedTokens: [SEPOLIA_TOKEN], + networkTrackedTokens: { + mainnet: [MAINNET_TOKEN], + sepolia: [SEPOLIA_TOKEN], + }, + }); + const { lookupTokenInfo } = require("../src/shared/balances"); + lookupTokenInfo.mockResolvedValueOnce({ + symbol: "NEW", + decimals: 18, + name: "New Token", + }); + + boot.node("settings-addtoken-address").value = NEW_TOKEN_ADDRESS; + await boot.click("btn-settings-addtoken-manual"); + + const stored = boot.storage.read("autistmask"); + expect(stored.networkId).toBe("sepolia"); + expect(stored.trackedTokens).toEqual([SEPOLIA_TOKEN]); + expect(stored.networkTrackedTokens.mainnet).toEqual([MAINNET_TOKEN]); +}); + +// Another popup page tracks DAI on Ethereum Mainnet and then saves a switch to +// Sepolia. That switch puts DAI away under Ethereum Mainnet. This page has not +// taken the switch yet. A switch to Sepolia in this page's Settings keeps DAI +// on Ethereum Mainnet's list. +test("a token another page tracked before its own switch stays 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: [], + networkTrackedTokens: { mainnet: [MAINNET_TOKEN] }, + }); + + 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 records a fraud contract on Ethereum Mainnet after this // page's last save. The switch to Sepolia drops that fraud contract, as the // switch drops this page's own.