fix: a network switch in Settings leaves no balance from the previous network stored (closes #653)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

The switch is now made on the stored record that its save has just read. The switched fields are then copied into the page's state. A save writes only the fields that the page changed since its last save. A switch made on the page's state alone mishandled what another page stored since then:

- A balance from the previous network stayed under the new network.
- A token tracked on the previous network moved to the new network.
- An endpoint URL saved for the previous network was replaced by the page's older one.

Tests store each of these before the switch and check the stored record after it.

Model: opus-5-5
This commit is contained in:
2026-10-27 07:22:50 +00:00
parent fc6cffcaba
commit 1effa7c6eb
6 changed files with 120 additions and 10 deletions
+21 -5
View File
@@ -2,8 +2,10 @@
//
// 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).
// - onChainSwitch() below makes the switch the user picks in Settings. It
// makes the switch on the stored record with applyChainSwitchFields(), and
// copies the switched fields into `state` with copyChainSwitchFields()
// (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
@@ -27,7 +29,10 @@
// 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 {
applyChainSwitchFields,
copyChainSwitchFields,
} = require("./chainSwitchFields");
const { clearPrices } = require("./prices");
// The popup's own caches a network switch resets. Prices are chain-specific
@@ -47,12 +52,23 @@ function clearPopupCaches() {
// 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 switch is made on the stored record that the save has just read, as the
// background makes its own switch. The switched fields are then copied from
// that record into `state`. So the switch also covers what another page stored
// since this page's last save:
//
// - A token tracked on the previous network stays tracked on that network.
// - An endpoint URL saved for the previous network stays with that network.
// - A balance from the previous network is cleared
// (https://git.eeqj.de/sneak/AutistMask/issues/653).
async function onChainSwitch(newNetworkId) {
const { state, saveState } = require("./state");
let net = null;
await saveState(() => {
await saveState((stored) => {
if (state.networkId === newNetworkId) return false;
net = applyChainSwitchFields(state, newNetworkId);
net = applyChainSwitchFields(stored, newNetworkId);
copyChainSwitchFields(state, stored);
clearPopupCaches();
});
return net;
+4 -3
View File
@@ -3,9 +3,10 @@
//
// Split out of chainSwitch.js so the background can perform a chain switch
// without the singleton being reachable from its bundle at all. The popup
// still goes through onChainSwitch() (chainSwitch.js), which applies this to
// the singleton and saves; the background applies it to the detached record of
// its own read-modify-write (src/background/state.js).
// still goes through onChainSwitch() (chainSwitch.js). That function applies
// this to the stored record that the switch's save reads, and copies the
// result into the singleton. The background applies it to the detached record
// of its own read-modify-write (src/background/state.js).
//
// Everything here is synchronous and touches nothing but the objects it is
// given: no storage, no caches, no imports beyond the network table. That is
+4 -2
View File
@@ -591,8 +591,10 @@ function reportSaveFailure(err) {
//
// `beforeWrite`, when given, is called inside the lock with the stored record
// as just read, before this page's state is compared against it. It may change
// `state`, and this save writes that change. When it returns false, nothing is
// written; any other return lets the save go ahead.
// `state`, and this save writes that change. It may also change the stored
// record: a field that this page did not change is written as `beforeWrite`
// left it. When it returns false, nothing is written; any other return lets the
// save go ahead.
function saveState(beforeWrite) {
const turn = withLock("autistmask", () => saveStateOnce(beforeWrite));
// Every failed save is reported, whether or not the caller awaited this