A network switch in Settings filed under the new network a token or fraud contract that another page stored since this page's last save, because the switch's save writes only the fields this page changed. Before the switch, onChainSwitch() now takes the stored tracked tokens into the page's own list with takeStoredTrackedTokens(), matched by contract address, so a token either page added or removed stays as that page left it. The switch then files that list under the previous network. When the stored record is on another network, no tokens are taken. Fraud contracts are cleared on every switch, as before. Model: opus-5-5
536 lines
20 KiB
JavaScript
536 lines
20 KiB
JavaScript
// Tracked tokens are kept per network, and the two add-token screens offer the
|
|
// bundled token list, which holds Ethereum Mainnet contracts, on Ethereum
|
|
// Mainnet only (https://git.eeqj.de/sneak/AutistMask/issues/523), including on
|
|
// the screen "Back" lands on after a switch made from the Settings gear and for
|
|
// 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
|
|
// 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 {
|
|
bootPopup,
|
|
cleanupPopup,
|
|
unversionedValidProfile,
|
|
ADDRESS,
|
|
TOKEN_ADDRESS,
|
|
} = require("./support/popupBoot");
|
|
const { NETWORKS } = require("../src/shared/networks");
|
|
|
|
const SEPOLIA_TOKEN = {
|
|
address: TOKEN_ADDRESS,
|
|
symbol: "TST",
|
|
decimals: 18,
|
|
name: "Test Token",
|
|
};
|
|
const MAINNET_TOKEN = {
|
|
address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
|
symbol: "DAI",
|
|
decimals: 18,
|
|
name: "Dai Stablecoin",
|
|
};
|
|
// Tracked on neither network.
|
|
const NEW_TOKEN_ADDRESS = "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
|
|
const NETWORK_CHANGED_FLASH =
|
|
"The active network changed before the lookup finished, so the token was" +
|
|
" not added.";
|
|
|
|
afterEach(() => {
|
|
cleanupPopup();
|
|
});
|
|
|
|
// The popup reopened onto SettingsAddToken, on `networkId`.
|
|
function bootOnAddToken(networkId, extra) {
|
|
return bootPopup(
|
|
unversionedValidProfile({
|
|
networkId,
|
|
currentView: "settings-addtoken",
|
|
...extra,
|
|
}),
|
|
);
|
|
}
|
|
|
|
// The popup reopened onto AddressDetail on Ethereum Mainnet, with DAI tracked
|
|
// there and TST tracked on Sepolia.
|
|
function bootOnAddress() {
|
|
return bootPopup(
|
|
unversionedValidProfile({
|
|
currentView: "address",
|
|
selectedWallet: 0,
|
|
selectedAddress: 0,
|
|
trackedTokens: [MAINNET_TOKEN],
|
|
networkTrackedTokens: { sepolia: [SEPOLIA_TOKEN] },
|
|
}),
|
|
);
|
|
}
|
|
|
|
// The popup reopened onto AddressToken for `token`, with AddressDetail beneath
|
|
// it, over the same tracked tokens as bootOnAddress().
|
|
function bootOnToken(token) {
|
|
return bootPopup(
|
|
unversionedValidProfile({
|
|
currentView: "address-token",
|
|
viewStack: ["main", "address"],
|
|
selectedWallet: 0,
|
|
selectedAddress: 0,
|
|
selectedToken: token,
|
|
trackedTokens: [MAINNET_TOKEN],
|
|
networkTrackedTokens: { sepolia: [SEPOLIA_TOKEN] },
|
|
}),
|
|
);
|
|
}
|
|
|
|
// AddressToken for `token` opened from AddressDetail, as tapping the token's
|
|
// row there does.
|
|
async function openToken(boot, token) {
|
|
const { state } = require("../src/shared/state");
|
|
state.selectedToken = token;
|
|
require("../src/popup/views/helpers").pushCurrentView();
|
|
require("../src/popup/views/addressToken").show();
|
|
await boot.settle();
|
|
}
|
|
|
|
// Presses `addButton` with a contract lookup that answers only after the
|
|
// Settings gear and a switch to `networkId`.
|
|
async function addAcrossSwitch(boot, addButton, networkId) {
|
|
const { lookupTokenInfo } = require("../src/shared/balances");
|
|
let answer;
|
|
lookupTokenInfo.mockImplementationOnce(
|
|
() =>
|
|
new Promise((resolve) => {
|
|
answer = resolve;
|
|
}),
|
|
);
|
|
const adding = boot.click(addButton);
|
|
await boot.click("btn-settings");
|
|
await chooseNetwork(boot, networkId);
|
|
answer({ symbol: "NEW", decimals: 18, name: "New Token" });
|
|
await adding;
|
|
}
|
|
|
|
// Picks `networkId` in the Settings network selector, as the user does.
|
|
async function chooseNetwork(boot, networkId) {
|
|
const select = boot.node("settings-network");
|
|
select.value = networkId;
|
|
for (const fn of select.listeners.change) await fn();
|
|
await boot.settle();
|
|
}
|
|
|
|
// The Settings gear, a switch to Sepolia, and Back.
|
|
async function gearSwitchBack(boot) {
|
|
await boot.click("btn-settings");
|
|
expect(boot.visibleViews()).toEqual(["settings"]);
|
|
await chooseNetwork(boot, "sepolia");
|
|
await boot.click("btn-settings-back");
|
|
}
|
|
|
|
describe.each([
|
|
["mainnet", false],
|
|
["sepolia", true],
|
|
])("on %s", (networkId, hidden) => {
|
|
test(`the common tokens are hidden: ${hidden}`, async () => {
|
|
const boot = await bootOnAddToken(networkId);
|
|
expect(boot.pageErrors).toEqual([]);
|
|
expect(boot.visibleViews()).toEqual(["settings-addtoken"]);
|
|
expect(boot.hidden("settings-addtoken-common")).toBe(hidden);
|
|
|
|
require("../src/popup/views/addToken").show();
|
|
expect(boot.visibleViews()).toEqual(["add-token"]);
|
|
expect(boot.hidden("add-token-common")).toBe(hidden);
|
|
});
|
|
});
|
|
|
|
test("a token added by contract address on Sepolia is tracked on Sepolia only", async () => {
|
|
const boot = await bootOnAddToken("sepolia", {
|
|
networkTrackedTokens: { mainnet: [MAINNET_TOKEN] },
|
|
});
|
|
const stored = () => boot.storage.read("autistmask");
|
|
const { lookupTokenInfo } = require("../src/shared/balances");
|
|
lookupTokenInfo.mockResolvedValueOnce({
|
|
symbol: SEPOLIA_TOKEN.symbol,
|
|
decimals: SEPOLIA_TOKEN.decimals,
|
|
name: SEPOLIA_TOKEN.name,
|
|
});
|
|
|
|
boot.node("settings-addtoken-address").value = TOKEN_ADDRESS;
|
|
await boot.click("btn-settings-addtoken-manual");
|
|
|
|
expect(lookupTokenInfo).toHaveBeenCalledWith(
|
|
TOKEN_ADDRESS,
|
|
expect.any(String),
|
|
"sepolia",
|
|
expect.anything(),
|
|
);
|
|
expect(stored().trackedTokens).toEqual([SEPOLIA_TOKEN]);
|
|
|
|
await boot.click("btn-settings");
|
|
await chooseNetwork(boot, "mainnet");
|
|
expect(stored().networkId).toBe("mainnet");
|
|
expect(stored().trackedTokens).toEqual([MAINNET_TOKEN]);
|
|
const onMainnet = boot.node("settings-tracked-tokens").innerHTML;
|
|
expect(onMainnet).toContain("DAI");
|
|
expect(onMainnet).not.toContain("TST");
|
|
|
|
await chooseNetwork(boot, "sepolia");
|
|
expect(stored().networkId).toBe("sepolia");
|
|
expect(stored().trackedTokens).toEqual([SEPOLIA_TOKEN]);
|
|
const onSepolia = boot.node("settings-tracked-tokens").innerHTML;
|
|
expect(onSepolia).toContain("TST");
|
|
expect(onSepolia).not.toContain("DAI");
|
|
});
|
|
|
|
// Another popup page tracks DAI on Ethereum Mainnet after this page's last
|
|
// save. The switch to Sepolia puts DAI away under Ethereum Mainnet.
|
|
test("a token another page tracked before a switch in Settings stays tracked on Ethereum Mainnet", async () => {
|
|
const boot = await bootPopup(unversionedValidProfile());
|
|
expect(boot.pageErrors).toEqual([]);
|
|
boot.storage.write("autistmask", {
|
|
...boot.storage.read("autistmask"),
|
|
trackedTokens: [MAINNET_TOKEN],
|
|
});
|
|
|
|
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([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([]);
|
|
});
|
|
|
|
// 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
|
|
// 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.
|
|
test("a token added while a switch in Settings waits for the lock 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");
|
|
|
|
// Another write holds the lock, so the switch's save waits for it.
|
|
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();
|
|
|
|
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([
|
|
{
|
|
address: NEW_TOKEN_ADDRESS,
|
|
symbol: "NEW",
|
|
decimals: 18,
|
|
name: "New Token",
|
|
},
|
|
]);
|
|
});
|
|
|
|
describe("Back after a switch from the Settings gear", () => {
|
|
test("SettingsAddToken no longer offers the common tokens", async () => {
|
|
const boot = await bootOnAddToken("mainnet");
|
|
expect(boot.hidden("settings-addtoken-common")).toBe(false);
|
|
|
|
await gearSwitchBack(boot);
|
|
expect(boot.visibleViews()).toEqual(["settings-addtoken"]);
|
|
expect(boot.hidden("settings-addtoken-common")).toBe(true);
|
|
});
|
|
|
|
test("AddressDetail lists the new network's tracked tokens", async () => {
|
|
const boot = await bootOnAddress();
|
|
expect(boot.node("address-balances").innerHTML).toContain("DAI");
|
|
|
|
await gearSwitchBack(boot);
|
|
expect(boot.visibleViews()).toEqual(["address"]);
|
|
const balances = boot.node("address-balances").innerHTML;
|
|
expect(balances).toContain("TST");
|
|
expect(balances).not.toContain("DAI");
|
|
});
|
|
|
|
// Neither can be rendered from saved state, so neither is landed on.
|
|
test.each([
|
|
["btn-add-token", "add-token"],
|
|
["btn-send", "send"],
|
|
])("from over %s, it lands on AddressDetail", async (button, view) => {
|
|
const boot = await bootOnAddress();
|
|
await boot.click(button);
|
|
expect(boot.visibleViews()).toEqual([view]);
|
|
|
|
await gearSwitchBack(boot);
|
|
expect(boot.visibleViews()).toEqual(["address"]);
|
|
expect(boot.node("address-balances").innerHTML).not.toContain("DAI");
|
|
});
|
|
|
|
// DAI was picked on Ethereum Mainnet, where alone it is tracked.
|
|
test.each([
|
|
[
|
|
"opened from AddressDetail",
|
|
async () => {
|
|
const boot = await bootOnAddress();
|
|
await openToken(boot, MAINNET_TOKEN.address);
|
|
return boot;
|
|
},
|
|
],
|
|
["in a reopened popup", () => bootOnToken(MAINNET_TOKEN.address)],
|
|
])(
|
|
"from AddressToken for DAI %s, it lands on AddressDetail",
|
|
async (_how, open) => {
|
|
const boot = await open();
|
|
expect(boot.visibleViews()).toEqual(["address-token"]);
|
|
expect(boot.text("address-token-title")).toContain("DAI");
|
|
|
|
await gearSwitchBack(boot);
|
|
expect(boot.visibleViews()).toEqual(["address"]);
|
|
const balances = boot.node("address-balances").innerHTML;
|
|
expect(balances).toContain("TST");
|
|
expect(balances).not.toContain("DAI");
|
|
},
|
|
);
|
|
|
|
// The stack is saved cut with the switch, so a popup closed straight after
|
|
// it and reopened onto Settings lands where this one would. Loading a
|
|
// saved stack cuts it at AddToken and Send as well, but not at
|
|
// AddressToken.
|
|
test.each([
|
|
[
|
|
"AddressToken for DAI",
|
|
(boot) => openToken(boot, MAINNET_TOKEN.address),
|
|
],
|
|
["AddToken", (boot) => boot.click("btn-add-token")],
|
|
["Send", (boot) => boot.click("btn-send")],
|
|
])(
|
|
"from %s, a popup reopened after the switch lands on AddressDetail",
|
|
async (_view, open) => {
|
|
const boot = await bootOnAddress();
|
|
await open(boot);
|
|
await boot.click("btn-settings");
|
|
await chooseNetwork(boot, "sepolia");
|
|
|
|
const reopened = await bootPopup(boot.storage.read("autistmask"));
|
|
expect(reopened.visibleViews()).toEqual(["settings"]);
|
|
await reopened.click("btn-settings-back");
|
|
expect(reopened.visibleViews()).toEqual(["address"]);
|
|
const balances = reopened.node("address-balances").innerHTML;
|
|
expect(balances).toContain("TST");
|
|
expect(balances).not.toContain("DAI");
|
|
},
|
|
);
|
|
|
|
test("AddressToken for the native token is drawn again for the new network", async () => {
|
|
const boot = await bootOnToken("ETH");
|
|
expect(boot.visibleViews()).toEqual(["address-token"]);
|
|
|
|
await gearSwitchBack(boot);
|
|
expect(boot.visibleViews()).toEqual(["address-token"]);
|
|
expect(boot.text("address-token-title")).toContain("SepoliaETH");
|
|
});
|
|
|
|
// A popup reopened onto ConfirmTx has no Send beneath it, so "Back" draws
|
|
// ConfirmTx again from the saved transaction. It is drawn for the network
|
|
// the transaction was reviewed on, and Sign & Send refuses it on the new
|
|
// one.
|
|
test("ConfirmTx reviewed on Sepolia is not sent on Ethereum Mainnet", async () => {
|
|
const boot = await bootPopup(
|
|
unversionedValidProfile({
|
|
networkId: "sepolia",
|
|
currentView: "confirm-tx",
|
|
selectedWallet: 0,
|
|
selectedAddress: 0,
|
|
checkRecipientsOnEtherscan: true,
|
|
viewData: {
|
|
pendingTx: {
|
|
token: "ETH",
|
|
from: ADDRESS,
|
|
to: ADDRESS,
|
|
amount: "1",
|
|
chainId: NETWORKS.sepolia.chainId,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
expect(boot.visibleViews()).toEqual(["confirm-tx"]);
|
|
|
|
await boot.click("btn-settings");
|
|
await chooseNetwork(boot, "mainnet");
|
|
globalThis.fetch.mockClear();
|
|
await boot.click("btn-settings-back");
|
|
expect(boot.visibleViews()).toEqual(["confirm-tx"]);
|
|
// Drawn again: its Etherscan label check asks Sepolia's explorer.
|
|
expect(globalThis.fetch.mock.calls.map((call) => call[0])).toEqual([
|
|
NETWORKS.sepolia.explorerUrl + "/address/" + ADDRESS,
|
|
]);
|
|
expect(boot.text("confirm-type")).toBe("Native SepoliaETH transfer");
|
|
|
|
boot.node("confirm-tx-password").value = "any password";
|
|
await boot.click("btn-confirm-send");
|
|
expect(boot.visibleViews()).toEqual(["confirm-tx"]);
|
|
expect(
|
|
boot.node("confirm-network-changed-error").style.visibility,
|
|
).toBe("visible");
|
|
// Refused before the password is even tried.
|
|
expect(boot.text("confirm-tx-password-error")).toBe("");
|
|
});
|
|
|
|
test("without a wallet, it lands on Welcome", async () => {
|
|
const boot = await bootPopup(undefined);
|
|
expect(boot.visibleViews()).toEqual(["welcome"]);
|
|
|
|
await gearSwitchBack(boot);
|
|
expect(boot.visibleViews()).toEqual(["welcome"]);
|
|
});
|
|
|
|
test("a later Back onto Settings keeps what was typed there", async () => {
|
|
const boot = await bootOnAddress();
|
|
await boot.click("btn-settings");
|
|
await chooseNetwork(boot, "sepolia");
|
|
boot.node("settings-rpc").value = "https://typed.example";
|
|
|
|
await boot.click("btn-settings-add-token");
|
|
expect(boot.visibleViews()).toEqual(["settings-addtoken"]);
|
|
await boot.click("btn-settings-addtoken-back");
|
|
expect(boot.visibleViews()).toEqual(["settings"]);
|
|
expect(boot.value("settings-rpc")).toBe("https://typed.example");
|
|
});
|
|
});
|
|
|
|
describe("a contract lookup that answers after a switch adds nothing", () => {
|
|
test("from SettingsAddToken", async () => {
|
|
const boot = await bootOnAddToken("sepolia", {
|
|
networkTrackedTokens: { mainnet: [MAINNET_TOKEN] },
|
|
});
|
|
const stored = () => boot.storage.read("autistmask");
|
|
boot.node("settings-addtoken-address").value = NEW_TOKEN_ADDRESS;
|
|
|
|
await addAcrossSwitch(boot, "btn-settings-addtoken-manual", "mainnet");
|
|
expect(boot.visibleViews()).toEqual(["settings"]);
|
|
expect(boot.text("flash-msg")).toBe(NETWORK_CHANGED_FLASH);
|
|
expect(stored().trackedTokens).toEqual([MAINNET_TOKEN]);
|
|
expect(stored().networkTrackedTokens.sepolia).toEqual([]);
|
|
});
|
|
|
|
test("from AddToken", async () => {
|
|
const boot = await bootOnAddress();
|
|
const stored = () => boot.storage.read("autistmask");
|
|
await boot.click("btn-add-token");
|
|
boot.node("add-token-address").value = NEW_TOKEN_ADDRESS;
|
|
|
|
await addAcrossSwitch(boot, "btn-add-token-confirm", "sepolia");
|
|
expect(boot.visibleViews()).toEqual(["settings"]);
|
|
expect(boot.text("flash-msg")).toBe(NETWORK_CHANGED_FLASH);
|
|
expect(stored().trackedTokens).toEqual([SEPOLIA_TOKEN]);
|
|
expect(stored().networkTrackedTokens.mainnet).toEqual([MAINNET_TOKEN]);
|
|
});
|
|
});
|