// The popup's network switch. // // A network switch reaches the popup's `state` in one of two ways: // // - onChainSwitch() below makes the switch the user picks in Settings, with // applyChainSwitchFields() (chainSwitchFields.js). // - followStoredSettings() in src/popup/index.js takes a switch the background // or another popup page saved, from the stored record, with // takeStoredNetwork() (state.js), which copies it with // copyChainSwitchFields() (chainSwitchFields.js). // // Both change `state` inside a save, holding the lock, and both reset the // popup's own caches through clearPopupCaches(), so a reset added there // reaches both. A field applyChainSwitchFields() sets has to be copied by // copyChainSwitchFields() too. // // Adding a new chain (e.g. ETC) requires only a new entry in // networks.js — no per-caller wiring is needed. // // The background does NOT come through here: this function mutates the // module-level `state` singleton, which the MV3 service worker never // populates, and a background switch performed on it wrote DEFAULT_STATE over // the user's whole profile // (https://git.eeqj.de/sneak/AutistMask/issues/316). The field mutations // themselves live in chainSwitchFields.js, which takes the record to mutate as // an argument; src/background/state.js applies them inside a read-modify-write // against storage, and the singleton is not reachable from the background // bundle at all (enforced by the ESLint rule in eslint.config.js). const { applyChainSwitchFields } = require("./chainSwitchFields"); const { clearPrices } = require("./prices"); // The popup's own caches a network switch resets. Prices are chain-specific // (testnet tokens are worthless, ETC has different pricing, etc.). In-memory // and per bundle, so this is the popup's own cache — the only context that // ever fills it. function clearPopupCaches() { clearPrices(); } // Switch the active chain and reset all chain-specific cached state. // Returns the network configuration object for the new chain, or null when // `state` already has that network once the save holds the lock, and nothing // is switched. // // `state` is switched inside the save that stores the switch, holding the // lock, so it is never switched while a follow of a switch saved elsewhere // waits for the lock (followStoredSettings() in src/popup/index.js): that // follow runs first, and this switch is made after it. // // The save writes only the fields that differ from what this page last saved. // When this page last saved a balance of "0", clearing that balance is no // change. So the switch also resets these fields in the stored record: // // - every address's balance and token balances; // - the time of the last balance refresh; // - the fraud contracts. // // A balance or a fraud contract that the background or another page stored // since this page's last save is then not kept under the new network // (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. // // When the stored record is on this page's network, this page first takes the // stored `networkTrackedTokens` (takeStoredNetworkTrackedTokens() in state.js), // because another page's save may have changed a list this page holds. // // 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. async function onChainSwitch(newNetworkId) { const { state, saveState, takeStoredNetwork, takeStoredTrackedTokens, takeStoredNetworkTrackedTokens, } = require("./state"); let net = null; await saveState((stored) => { if (state.networkId === newNetworkId) return false; if (stored.networkId !== state.networkId) { takeStoredNetwork(stored); } else { takeStoredNetworkTrackedTokens(stored); } takeStoredTrackedTokens(stored); net = applyChainSwitchFields(state, newNetworkId); stored.lastBalanceRefresh = 0; stored.fraudContracts = []; for (const wallet of stored.wallets) { for (const addr of wallet.addresses) { addr.balance = "0"; addr.tokenBalances = []; } } clearPopupCaches(); }); return net; } module.exports = { onChainSwitch, clearPopupCaches };