fix: two pages switching networks no longer lose tracked tokens (closes #701)
A page that had not yet taken another page's network switch could lose tracked tokens. - Its save wrote its network's list as the new network's list. saveState() now keeps that list and merges the change into its own network's list in networkTrackedTokens. - Its own switch wrote its old per-network lists over the stored ones. onChainSwitch() now first takes the stored switch with takeStoredNetwork(). - takeStoredNetwork() dropped the page's unsaved change. It now keeps it in the list of the page's network. This also fixes #706. - A page already on the stored network now takes the stored networkTrackedTokens before it switches, because another page's save may have changed a list this page holds. Model: opus-5-5
This commit was merged in pull request #704.
This commit is contained in:
@@ -267,6 +267,190 @@ 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 adds the token to Ethereum Mainnet's list and leaves Sepolia's list as
|
||||
// it was.
|
||||
test("a token added before this page takes another page's switch is tracked on Ethereum Mainnet", 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,
|
||||
{
|
||||
address: NEW_TOKEN_ADDRESS,
|
||||
symbol: "NEW",
|
||||
decimals: 18,
|
||||
name: "New Token",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// Another popup page tracks DAI on Ethereum Mainnet and then saves a switch to
|
||||
// Sepolia. This page does not track DAI. Before taking that switch, this page
|
||||
// adds a token on Ethereum Mainnet. The add's save puts both DAI and the new
|
||||
// token on Ethereum Mainnet's list.
|
||||
test("a token added before this page takes another page's switch joins that page's tokens on Ethereum Mainnet", async () => {
|
||||
const boot = await bootOnAddToken("mainnet");
|
||||
expect(boot.pageErrors).toEqual([]);
|
||||
boot.storage.write("autistmask", {
|
||||
...boot.storage.read("autistmask"),
|
||||
networkId: "sepolia",
|
||||
trackedTokens: [],
|
||||
networkTrackedTokens: { mainnet: [MAINNET_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([]);
|
||||
expect(stored.networkTrackedTokens.mainnet).toEqual([
|
||||
MAINNET_TOKEN,
|
||||
{
|
||||
address: NEW_TOKEN_ADDRESS,
|
||||
symbol: "NEW",
|
||||
decimals: 18,
|
||||
name: "New Token",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// Another popup page saves a switch to Sepolia, where TST is tracked. Before
|
||||
// taking that switch, this page removes DAI on Ethereum Mainnet, as the `[x]`
|
||||
// on a tracked token in Settings does. The removal's save drops DAI from
|
||||
// Ethereum Mainnet's list and leaves Sepolia's list as it was.
|
||||
test("a token removed before this page takes another page's switch is gone from Ethereum Mainnet", async () => {
|
||||
const boot = await bootPopup(
|
||||
unversionedValidProfile({
|
||||
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 { state, saveState } = require("../src/shared/state");
|
||||
state.trackedTokens.splice(0, 1);
|
||||
await saveState();
|
||||
|
||||
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 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]);
|
||||
});
|
||||
|
||||
// This page switches to Sepolia. Another popup page has not taken that switch,
|
||||
// and its save adds a token to Ethereum Mainnet's list. This page switches back
|
||||
// and tracks that token.
|
||||
test("a token another page added on Ethereum Mainnet is tracked when this page switches back there", async () => {
|
||||
const boot = await bootPopup(
|
||||
unversionedValidProfile({ trackedTokens: [MAINNET_TOKEN] }),
|
||||
);
|
||||
expect(boot.pageErrors).toEqual([]);
|
||||
await boot.click("btn-settings");
|
||||
await chooseNetwork(boot, "sepolia");
|
||||
const newToken = {
|
||||
address: NEW_TOKEN_ADDRESS,
|
||||
symbol: "NEW",
|
||||
decimals: 18,
|
||||
name: "New Token",
|
||||
};
|
||||
boot.storage.write("autistmask", {
|
||||
...boot.storage.read("autistmask"),
|
||||
networkTrackedTokens: { mainnet: [MAINNET_TOKEN, newToken] },
|
||||
});
|
||||
|
||||
await chooseNetwork(boot, "mainnet");
|
||||
|
||||
const stored = boot.storage.read("autistmask");
|
||||
expect(stored.networkId).toBe("mainnet");
|
||||
expect(stored.trackedTokens).toEqual([MAINNET_TOKEN, newToken]);
|
||||
});
|
||||
|
||||
// This page tracks DAI on Ethereum Mainnet and switches to Sepolia. Another
|
||||
// popup page has not taken that switch, and its save removes DAI from Ethereum
|
||||
// Mainnet's list. This page switches back and does not track DAI again.
|
||||
test("a token another page removed on Ethereum Mainnet stays removed when this page switches back there", async () => {
|
||||
const boot = await bootPopup(
|
||||
unversionedValidProfile({ trackedTokens: [MAINNET_TOKEN] }),
|
||||
);
|
||||
expect(boot.pageErrors).toEqual([]);
|
||||
await boot.click("btn-settings");
|
||||
await chooseNetwork(boot, "sepolia");
|
||||
boot.storage.write("autistmask", {
|
||||
...boot.storage.read("autistmask"),
|
||||
networkTrackedTokens: { mainnet: [] },
|
||||
});
|
||||
|
||||
await chooseNetwork(boot, "mainnet");
|
||||
|
||||
const stored = boot.storage.read("autistmask");
|
||||
expect(stored.networkId).toBe("mainnet");
|
||||
expect(stored.trackedTokens).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.
|
||||
@@ -338,6 +522,162 @@ test("a token added while a switch in Settings waits for the lock stays tracked
|
||||
]);
|
||||
});
|
||||
|
||||
// The contract lookup on SettingsAddToken answers on Ethereum Mainnet while
|
||||
// this page's switch to Sepolia waits for the lock. Another popup page holds
|
||||
// the lock and stores its own switch to Sepolia, with DAI tracked on Ethereum
|
||||
// Mainnet. This page's switch keeps DAI and the new token on Ethereum Mainnet's
|
||||
// list.
|
||||
test("a token added while a switch in Settings waits for another page's switch stays tracked on Ethereum Mainnet", async () => {
|
||||
const boot = await bootOnAddToken("mainnet");
|
||||
expect(boot.pageErrors).toEqual([]);
|
||||
const { lookupTokenInfo } = require("../src/shared/balances");
|
||||
let answer;
|
||||
lookupTokenInfo.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
answer = resolve;
|
||||
}),
|
||||
);
|
||||
boot.node("settings-addtoken-address").value = NEW_TOKEN_ADDRESS;
|
||||
const adding = boot.click("btn-settings-addtoken-manual");
|
||||
await boot.click("btn-settings");
|
||||
|
||||
let releaseLock;
|
||||
const lockHeld = navigator.locks.request(
|
||||
"autistmask",
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
releaseLock = resolve;
|
||||
}),
|
||||
);
|
||||
const select = boot.node("settings-network");
|
||||
select.value = "sepolia";
|
||||
const switching = Promise.all(select.listeners.change.map((fn) => fn()));
|
||||
await boot.settle();
|
||||
boot.storage.write("autistmask", {
|
||||
...boot.storage.read("autistmask"),
|
||||
networkId: "sepolia",
|
||||
trackedTokens: [],
|
||||
networkTrackedTokens: { mainnet: [MAINNET_TOKEN] },
|
||||
});
|
||||
|
||||
answer({ symbol: "NEW", decimals: 18, name: "New Token" });
|
||||
await boot.settle();
|
||||
releaseLock();
|
||||
await lockHeld;
|
||||
await switching;
|
||||
await adding;
|
||||
|
||||
const stored = boot.storage.read("autistmask");
|
||||
expect(stored.networkId).toBe("sepolia");
|
||||
expect(stored.trackedTokens).toEqual([]);
|
||||
expect(stored.networkTrackedTokens.mainnet).toEqual([
|
||||
MAINNET_TOKEN,
|
||||
{
|
||||
address: NEW_TOKEN_ADDRESS,
|
||||
symbol: "NEW",
|
||||
decimals: 18,
|
||||
name: "New Token",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// Another popup page holds the lock and stores its switch to Sepolia, with DAI
|
||||
// tracked on Ethereum Mainnet. This page waits for the lock to take that
|
||||
// switch. Meanwhile, the contract lookup on SettingsAddToken answers on
|
||||
// Ethereum Mainnet. When this page takes that switch, DAI and the new token
|
||||
// stay on Ethereum Mainnet's list.
|
||||
test("a token added while this page waits to take another page's switch stays tracked on Ethereum Mainnet", async () => {
|
||||
const boot = await bootOnAddToken("mainnet");
|
||||
expect(boot.pageErrors).toEqual([]);
|
||||
let releaseLock;
|
||||
const lockHeld = navigator.locks.request(
|
||||
"autistmask",
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
releaseLock = resolve;
|
||||
}),
|
||||
);
|
||||
await boot.storage.local.set({
|
||||
autistmask: {
|
||||
...boot.storage.read("autistmask"),
|
||||
networkId: "sepolia",
|
||||
trackedTokens: [],
|
||||
networkTrackedTokens: { mainnet: [MAINNET_TOKEN] },
|
||||
},
|
||||
});
|
||||
await boot.settle();
|
||||
const { lookupTokenInfo } = require("../src/shared/balances");
|
||||
lookupTokenInfo.mockResolvedValueOnce({
|
||||
symbol: "NEW",
|
||||
decimals: 18,
|
||||
name: "New Token",
|
||||
});
|
||||
|
||||
boot.node("settings-addtoken-address").value = NEW_TOKEN_ADDRESS;
|
||||
const adding = boot.click("btn-settings-addtoken-manual");
|
||||
await boot.settle();
|
||||
releaseLock();
|
||||
await lockHeld;
|
||||
await adding;
|
||||
await boot.settle();
|
||||
|
||||
const stored = boot.storage.read("autistmask");
|
||||
expect(stored.networkId).toBe("sepolia");
|
||||
expect(stored.trackedTokens).toEqual([]);
|
||||
expect(stored.networkTrackedTokens.mainnet).toEqual([
|
||||
MAINNET_TOKEN,
|
||||
{
|
||||
address: NEW_TOKEN_ADDRESS,
|
||||
symbol: "NEW",
|
||||
decimals: 18,
|
||||
name: "New Token",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// This page tracks DAI on Ethereum Mainnet. Another popup page holds the lock
|
||||
// and stores its switch to Sepolia, with DAI on Ethereum Mainnet's list. This
|
||||
// page waits for the lock to take that switch. Meanwhile, the user removes DAI,
|
||||
// as the `[x]` on a tracked token in Settings does. When this page takes that
|
||||
// switch, DAI is gone from Ethereum Mainnet's list.
|
||||
test("a token removed while this page waits to take another page's switch is gone from Ethereum Mainnet", async () => {
|
||||
const boot = await bootPopup(
|
||||
unversionedValidProfile({ trackedTokens: [MAINNET_TOKEN] }),
|
||||
);
|
||||
expect(boot.pageErrors).toEqual([]);
|
||||
let releaseLock;
|
||||
const lockHeld = navigator.locks.request(
|
||||
"autistmask",
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
releaseLock = resolve;
|
||||
}),
|
||||
);
|
||||
await boot.storage.local.set({
|
||||
autistmask: {
|
||||
...boot.storage.read("autistmask"),
|
||||
networkId: "sepolia",
|
||||
trackedTokens: [],
|
||||
networkTrackedTokens: { mainnet: [MAINNET_TOKEN] },
|
||||
},
|
||||
});
|
||||
await boot.settle();
|
||||
|
||||
const { state, saveState } = require("../src/shared/state");
|
||||
state.trackedTokens.splice(0, 1);
|
||||
const removing = saveState();
|
||||
releaseLock();
|
||||
await lockHeld;
|
||||
await removing;
|
||||
await boot.settle();
|
||||
|
||||
const stored = boot.storage.read("autistmask");
|
||||
expect(stored.networkId).toBe("sepolia");
|
||||
expect(stored.trackedTokens).toEqual([]);
|
||||
expect(stored.networkTrackedTokens.mainnet).toEqual([]);
|
||||
});
|
||||
|
||||
describe("Back after a switch from the Settings gear", () => {
|
||||
test("SettingsAddToken no longer offers the common tokens", async () => {
|
||||
const boot = await bootOnAddToken("mainnet");
|
||||
|
||||
Reference in New Issue
Block a user