fix: only the user switches the wallet's network (closes #408)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

A connected site's wallet_switchEthereumChain request for the other
supported network now opens a prompt in its own window, through the
existing approval machinery, naming the site and both networks. The
network, endpoints, balances and caches change, and chainChanged is
sent, only when the user approves it; rejecting or closing the prompt
answers 4001. One such prompt per site at a time; a request for the
active network needs none. The approval window no longer shows the
connection prompt while it waits for the approval's description,
since both prompts answer on the same port.

Model: opus-5-5
This commit is contained in:
2026-10-08 04:48:44 +00:00
parent ca18beb97f
commit 80d8877349
18 changed files with 911 additions and 124 deletions
+154
View File
@@ -63,6 +63,7 @@ const {
const { ConsoleErrors, EXTENSION_ORIGIN, start, sleep } = require("./driver");
const { startDappServer } = require("./dapp");
const { STUB_COUNTERPARTY } = require("../network");
const { NETWORKS } = require("../../../src/shared/networks");
const {
STATE_SCHEMA_VERSION,
stateProblem,
@@ -684,6 +685,159 @@ step(
},
);
// The network fields of the stored record, read on the popup page, the one
// moz-extension:// document the suite has open.
async function storedNetwork(env) {
const d = env.driver;
await d.switchToWindow(env.popupWindow);
const stored = await d.executeAsync(
`const done = arguments[arguments.length - 1];
const api = typeof browser !== "undefined" ? browser : chrome;
Promise.resolve(api.storage.local.get("autistmask")).then(
(r) => done({
networkId: r.autistmask.networkId,
rpcUrl: r.autistmask.rpcUrl,
blockscoutUrl: r.autistmask.blockscoutUrl,
}),
(e) => done({ error: String((e && e.message) || e) }),
);`,
);
assert(
stored && !stored.error,
"could not read the stored network: " + (stored && stored.error),
);
return stored;
}
// Every chainChanged event the test page has been sent, waiting up to
// `timeout` for there to be `count` of them: the background sends the event
// alongside its answer to the request, so it can arrive just after it. Leaves
// the driver on the page.
async function chainChangedEvents(env, count = 0, timeout = 5000) {
const d = env.driver;
await d.switchToWindow(env.dappWindow);
const deadline = Date.now() + timeout;
for (;;) {
const events = (await dappMessages(d, "AUTISTMASK_EVENT")).filter(
(m) => m.eventName === "chainChanged",
);
if (events.length >= count || Date.now() > deadline) return events;
await sleep(100);
}
}
// Ask, from the page, to switch from network `from` to network `to`, and
// return the prompt that opens, checked to name the site and both networks.
// Leaves the driver on the prompt.
async function openNetworkPrompt(env, key, from, to) {
const d = env.driver;
await d.switchToWindow(env.dappWindow);
await startRequest(d, key, "wallet_switchEthereumChain", [
{ chainId: to.chainId },
]);
const popup = await waitForApprovalWindow(d);
await d.switchToWindow(popup);
await d.waitVisible("#view-approve-network");
const screen = {
origin: await d.text("#approve-network-origin"),
current: await d.text("#approve-network-current"),
requested: await d.text("#approve-network-requested"),
};
assert(
isDeepStrictEqual(screen, {
origin: env.server.origin,
current: from.name,
requested: to.name,
}),
"the network switch prompt shows " + JSON.stringify(screen),
);
return popup;
}
// Only the user switches the network. A connected site's request opens a
// prompt, and until the user approves it the stored network does not move and
// no page is told it did (https://git.eeqj.de/sneak/AutistMask/issues/408).
// The wallet goes back to mainnet the same way at the end, for the transaction
// step after this one.
step(
"a site's network switch changes nothing until the user approves it",
async (env) => {
const d = env.driver;
const { mainnet, sepolia } = NETWORKS;
const before = await storedNetwork(env);
assert(
before.networkId === "mainnet",
"this step starts on mainnet, not on " + before.networkId,
);
const eventsBefore = (await chainChangedEvents(env)).length;
const rejected = await openNetworkPrompt(
env,
"switch-reject",
mainnet,
sepolia,
);
assert(
isDeepStrictEqual(await storedNetwork(env), before),
"the network moved while its prompt was still open",
);
// Nothing else closes this window, so a click that did not land
// leaves the request unanswered and the assertion below fails.
await d.switchToWindow(rejected);
await d.click("#btn-reject-network");
await d.switchToWindow(env.dappWindow);
await assertUserRejection(
d,
"switch-reject",
"the network switch rejection",
);
assert(
isDeepStrictEqual(await storedNetwork(env), before),
"a rejected network switch moved the network",
);
assert(
(await chainChangedEvents(env)).length === eventsBefore,
"a rejected network switch told the page the chain changed",
);
await openNetworkPrompt(env, "switch-approve", mainnet, sepolia);
await d.click("#btn-approve-network");
await d.switchToWindow(env.dappWindow);
let outcome = await settleRequest(d, "switch-approve");
assert(
outcome.settled === "resolved" && outcome.result === null,
"the approved network switch did not resolve: " +
JSON.stringify(outcome),
);
assert(
(await storedNetwork(env)).networkId === "sepolia",
"the approved network switch did not move the network",
);
const events = await chainChangedEvents(env, eventsBefore + 1);
assert(
events.length === eventsBefore + 1 &&
events[events.length - 1].data === sepolia.chainId,
"the page was not told of the approved switch: " +
JSON.stringify(events),
);
await openNetworkPrompt(env, "switch-restore", sepolia, mainnet);
await d.click("#btn-approve-network");
await d.switchToWindow(env.dappWindow);
outcome = await settleRequest(d, "switch-restore");
assert(
outcome.settled === "resolved",
"switching back to mainnet did not resolve: " +
JSON.stringify(outcome),
);
assert(
isDeepStrictEqual(await storedNetwork(env), before),
"switching back did not restore the mainnet network and endpoints",
);
await d.switchToWindow(env.dappWindow);
},
);
step(
"eth_sendTransaction shows the transaction and returns its hash",
async (env) => {