fix: a network switch in Settings keeps another page's token on the network it was added on (closes #692) #700
@@ -53,18 +53,31 @@ function clearPopupCaches() {
|
|||||||
// change. So the switch also resets these fields in the stored record:
|
// change. So the switch also resets these fields in the stored record:
|
||||||
//
|
//
|
||||||
// - every address's balance and token balances;
|
// - every address's balance and token balances;
|
||||||
// - the time of the last balance refresh.
|
// - the time of the last balance refresh;
|
||||||
|
// - the fraud contracts.
|
||||||
//
|
//
|
||||||
// A balance that the background or another page stored since this page's last
|
// A balance or a fraud contract that the background or another page stored
|
||||||
// save is then not kept under the new network
|
// 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/653,
|
||||||
|
// https://git.eeqj.de/sneak/AutistMask/issues/692).
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
async function onChainSwitch(newNetworkId) {
|
async function onChainSwitch(newNetworkId) {
|
||||||
const { state, saveState } = require("./state");
|
const { state, saveState, takeStoredTrackedTokens } = require("./state");
|
||||||
let net = null;
|
let net = null;
|
||||||
await saveState((stored) => {
|
await saveState((stored) => {
|
||||||
if (state.networkId === newNetworkId) return false;
|
if (state.networkId === newNetworkId) return false;
|
||||||
|
if (stored.networkId === state.networkId) {
|
||||||
|
takeStoredTrackedTokens(stored);
|
||||||
|
}
|
||||||
net = applyChainSwitchFields(state, newNetworkId);
|
net = applyChainSwitchFields(state, newNetworkId);
|
||||||
stored.lastBalanceRefresh = 0;
|
stored.lastBalanceRefresh = 0;
|
||||||
|
stored.fraudContracts = [];
|
||||||
for (const wallet of stored.wallets) {
|
for (const wallet of stored.wallets) {
|
||||||
for (const addr of wallet.addresses) {
|
for (const addr of wallet.addresses) {
|
||||||
addr.balance = "0";
|
addr.balance = "0";
|
||||||
|
|||||||
+33
-7
@@ -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 any network
|
// loadState() finished, or when its last successful saveState() began.
|
||||||
// switch saved elsewhere that this page has taken since (takeStoredNetwork()).
|
// takeStoredNetwork() and takeStoredTrackedTokens() also write into it what
|
||||||
// saveState() diffs the live state against this to find only the fields THIS
|
// this page has taken from the stored record since then. saveState() diffs the
|
||||||
// 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
|
||||||
@@ -615,6 +618,28 @@ function takeStoredNetwork(stored) {
|
|||||||
copyChainSwitchFields(baseline, structuredClone(stored));
|
copyChainSwitchFields(baseline, structuredClone(stored));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Give this page the tracked tokens of `stored`, a record on this page's
|
||||||
|
// network that a save of this page has just read (onChainSwitch() in
|
||||||
|
// src/shared/chainSwitch.js calls this from `beforeWrite`):
|
||||||
|
//
|
||||||
|
// - A token that another page tracked or removed since this page's last save
|
||||||
|
// is tracked or removed in `state` too.
|
||||||
|
// - A token that this page tracked or removed since then stays as this page
|
||||||
|
// has it.
|
||||||
|
//
|
||||||
|
// `baseline` takes the stored list, so the save stores the list in `state`
|
||||||
|
// whenever the two differ.
|
||||||
|
function takeStoredTrackedTokens(stored) {
|
||||||
|
rawState.trackedTokens = mergeListByIdentity(
|
||||||
|
baseline.trackedTokens,
|
||||||
|
rawState.trackedTokens,
|
||||||
|
stored.trackedTokens,
|
||||||
|
(token) => token.address.toLowerCase(),
|
||||||
|
(base, ours) => ours,
|
||||||
|
);
|
||||||
|
baseline.trackedTokens = structuredClone(stored.trackedTokens);
|
||||||
|
}
|
||||||
|
|
||||||
// Rejects with StateUnusableError for a stored record this build cannot make
|
// Rejects with StateUnusableError for a stored record this build cannot make
|
||||||
// sense of. Nothing is assigned and `loaded` stays false in that case, so a
|
// sense of. Nothing is assigned and `loaded` stays false in that case, so a
|
||||||
// caller that ignores the rejection gets StateNotLoadedError on the first
|
// caller that ignores the rejection gets StateNotLoadedError on the first
|
||||||
@@ -661,6 +686,7 @@ module.exports = {
|
|||||||
saveState,
|
saveState,
|
||||||
onSaveFailure,
|
onSaveFailure,
|
||||||
takeStoredNetwork,
|
takeStoredNetwork,
|
||||||
|
takeStoredTrackedTokens,
|
||||||
loadState,
|
loadState,
|
||||||
currentAddress,
|
currentAddress,
|
||||||
currentNetwork,
|
currentNetwork,
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
// a contract lookup that answers after one. Driven through the real popup over
|
// a contract lookup that answers after one. Driven through the real popup over
|
||||||
// a stored profile; the same switch made by the background is in
|
// a stored profile; the same switch made by the background is in
|
||||||
// tests/coldWorkerChainSwitch.test.js.
|
// tests/coldWorkerChainSwitch.test.js.
|
||||||
|
//
|
||||||
|
// The tests of what another page stored before a switch in Settings cover a
|
||||||
|
// fraud contract too (https://git.eeqj.de/sneak/AutistMask/issues/692).
|
||||||
|
|
||||||
const {
|
const {
|
||||||
bootPopup,
|
bootPopup,
|
||||||
@@ -178,9 +181,8 @@ test("a token added by contract address on Sepolia is tracked on Sepolia only",
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Another popup page tracks DAI on Ethereum Mainnet after this page's last
|
// Another popup page tracks DAI on Ethereum Mainnet after this page's last
|
||||||
// save. The switch's save writes only the token lists that this page changed.
|
// save. The switch to Sepolia puts DAI away under Ethereum Mainnet.
|
||||||
// So DAI stays in `trackedTokens`, the active network's list.
|
test("a token another page tracked before a switch in Settings stays tracked on Ethereum Mainnet", async () => {
|
||||||
test("a token another page tracked before a switch in Settings stays in trackedTokens", async () => {
|
|
||||||
const boot = await bootPopup(unversionedValidProfile());
|
const boot = await bootPopup(unversionedValidProfile());
|
||||||
expect(boot.pageErrors).toEqual([]);
|
expect(boot.pageErrors).toEqual([]);
|
||||||
boot.storage.write("autistmask", {
|
boot.storage.write("autistmask", {
|
||||||
@@ -193,10 +195,97 @@ test("a token another page tracked before a switch in Settings stays in trackedT
|
|||||||
|
|
||||||
const stored = boot.storage.read("autistmask");
|
const stored = boot.storage.read("autistmask");
|
||||||
expect(stored.networkId).toBe("sepolia");
|
expect(stored.networkId).toBe("sepolia");
|
||||||
expect(stored.trackedTokens).toEqual([MAINNET_TOKEN]);
|
expect(stored.trackedTokens).toEqual([]);
|
||||||
|
expect(stored.networkTrackedTokens.mainnet).toEqual([MAINNET_TOKEN]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Another popup page stops tracking DAI on Ethereum Mainnet after this page's
|
||||||
|
// last save. This page still holds DAI. The switch to Sepolia does not store
|
||||||
|
// DAI under Ethereum Mainnet again.
|
||||||
|
test("a token another page removed before a switch in Settings stays removed", async () => {
|
||||||
|
const boot = await bootPopup(
|
||||||
|
unversionedValidProfile({ trackedTokens: [MAINNET_TOKEN] }),
|
||||||
|
);
|
||||||
|
expect(boot.pageErrors).toEqual([]);
|
||||||
|
boot.storage.write("autistmask", {
|
||||||
|
...boot.storage.read("autistmask"),
|
||||||
|
trackedTokens: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await boot.click("btn-settings");
|
||||||
|
await chooseNetwork(boot, "sepolia");
|
||||||
|
|
||||||
|
const stored = boot.storage.read("autistmask");
|
||||||
|
expect(stored.networkId).toBe("sepolia");
|
||||||
|
expect(stored.trackedTokens).toEqual([]);
|
||||||
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
|
||||||
|
// page's last save. The switch to Sepolia drops that fraud contract, as the
|
||||||
|
// switch drops this page's own.
|
||||||
|
test("a fraud contract another page recorded before a switch in Settings is not kept on Sepolia", async () => {
|
||||||
|
const boot = await bootPopup(unversionedValidProfile());
|
||||||
|
expect(boot.pageErrors).toEqual([]);
|
||||||
|
boot.storage.write("autistmask", {
|
||||||
|
...boot.storage.read("autistmask"),
|
||||||
|
fraudContracts: [NEW_TOKEN_ADDRESS],
|
||||||
|
});
|
||||||
|
|
||||||
|
await boot.click("btn-settings");
|
||||||
|
await chooseNetwork(boot, "sepolia");
|
||||||
|
|
||||||
|
const stored = boot.storage.read("autistmask");
|
||||||
|
expect(stored.networkId).toBe("sepolia");
|
||||||
|
expect(stored.fraudContracts).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
// The contract lookup on SettingsAddToken answers on Ethereum Mainnet while the
|
// The contract lookup on SettingsAddToken answers on Ethereum Mainnet while the
|
||||||
// switch to Sepolia waits for the lock. The page adds the token to Ethereum
|
// switch to Sepolia waits for the lock. The page adds the token to Ethereum
|
||||||
// Mainnet's list. The switch then keeps that list for Ethereum Mainnet.
|
// Mainnet's list. The switch then keeps that list for Ethereum Mainnet.
|
||||||
|
|||||||
Reference in New Issue
Block a user