fix: only the user switches the wallet's network (closes #408)
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 was merged in pull request #501.
This commit is contained in:
+156
-7
@@ -3499,7 +3499,7 @@ async function closeApprovalPages(ctx) {
|
||||
}
|
||||
|
||||
// Click a button whose own handler closes the window it lives in — every
|
||||
// Reject, and Allow on the site prompt.
|
||||
// Reject, Allow on the site prompt, and Switch on the network switch prompt.
|
||||
//
|
||||
// page.click() dispatches the click and then waits for the renderer to
|
||||
// acknowledge it, and a page torn down by the handler never gets to. The
|
||||
@@ -3514,12 +3514,13 @@ async function closeApprovalPages(ctx) {
|
||||
// #btn-reject-sign, #btn-reject-tx — their disconnect leaves the approval
|
||||
// pending, so a click that never landed leaves the dApp promise unsettled
|
||||
// and the assertion after the call fails on its own.
|
||||
// #btn-approve — only a decision resolves the promise, and a swallowed click
|
||||
// cannot produce settled === "resolved".
|
||||
// #btn-reject on the site prompt — NOT self-proving. A page that went away
|
||||
// without the click landing disconnects the approval port, the background
|
||||
// settles that as 4001, and 4001 is exactly what assertUserRejection
|
||||
// accepts. Both call sites arm the click trace below and assert it.
|
||||
// #btn-approve, #btn-approve-network — only a decision resolves the promise,
|
||||
// and a swallowed click cannot produce settled === "resolved".
|
||||
// #btn-reject on the site prompt, #btn-reject-network — NOT self-proving. A
|
||||
// page that went away without the click landing disconnects the approval
|
||||
// port, the background settles that as 4001, and 4001 is exactly what
|
||||
// assertUserRejection accepts. Every call site arms the click trace below
|
||||
// and asserts it.
|
||||
//
|
||||
// A button that is missing or unclickable raises a different error, which is
|
||||
// rethrown.
|
||||
@@ -4389,6 +4390,154 @@ test("a prompt raised while another approval window has focus opens its own (#29
|
||||
await assertUserRejection(env.dapp, "focus-sign", "the sign prompt");
|
||||
});
|
||||
|
||||
// The network fields of the stored record.
|
||||
async function storedNetwork(page) {
|
||||
const s = await storedRecord(page);
|
||||
return {
|
||||
networkId: s.networkId,
|
||||
rpcUrl: s.rpcUrl,
|
||||
blockscoutUrl: s.blockscoutUrl,
|
||||
};
|
||||
}
|
||||
|
||||
// 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.
|
||||
async function chainChangedEvents(page, count = 0, timeout = 5000) {
|
||||
const deadline = Date.now() + timeout;
|
||||
for (;;) {
|
||||
const events = (await dappMessages(page, "AUTISTMASK_EVENT")).filter(
|
||||
(m) => m.eventName === "chainChanged",
|
||||
);
|
||||
if (events.length >= count || Date.now() > deadline) return events;
|
||||
await sleep(50);
|
||||
}
|
||||
}
|
||||
|
||||
// Ask, from the test page, to switch from network `from` to network `to`, and
|
||||
// return the prompt that opens, checked to name the site and both networks.
|
||||
async function openNetworkPrompt(env, key, from, to) {
|
||||
await startRequest(env.dapp, key, "wallet_switchEthereumChain", [
|
||||
{ chainId: to.chainId },
|
||||
]);
|
||||
const popup = await waitForApprovalWindow(env.ctx);
|
||||
await visible(popup, "#view-approve-network");
|
||||
const screen = await popup.evaluate(() => ({
|
||||
origin: document.getElementById("approve-network-origin").textContent,
|
||||
current: document.getElementById("approve-network-current").textContent,
|
||||
requested: document.getElementById("approve-network-requested")
|
||||
.textContent,
|
||||
}));
|
||||
assert(
|
||||
isDeepStrictEqual(screen, {
|
||||
origin: DAPP_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 tests after
|
||||
// this one.
|
||||
test("a site's network switch changes nothing until the user approves it (#408)", async (env) => {
|
||||
const { mainnet, sepolia } = NETWORKS;
|
||||
const before = await storedNetwork(env.page);
|
||||
assert(
|
||||
before.networkId === "mainnet",
|
||||
"this test starts on mainnet, not on " + before.networkId,
|
||||
);
|
||||
const eventsBefore = (await chainChangedEvents(env.dapp)).length;
|
||||
|
||||
const rejected = await openNetworkPrompt(
|
||||
env,
|
||||
"switch-reject",
|
||||
mainnet,
|
||||
sepolia,
|
||||
);
|
||||
try {
|
||||
assert(
|
||||
isDeepStrictEqual(await storedNetwork(env.page), before),
|
||||
"the network moved while its prompt was still open",
|
||||
);
|
||||
// Closing the prompt unanswered is also a rejection, so the click
|
||||
// itself is witnessed.
|
||||
await armClickTrace(env, rejected, "#btn-reject-network");
|
||||
await clickAndClose(rejected, "#btn-reject-network");
|
||||
await assertClickLanded(env, "#btn-reject-network");
|
||||
await assertUserRejection(
|
||||
env.dapp,
|
||||
"switch-reject",
|
||||
"the network switch rejection",
|
||||
);
|
||||
} finally {
|
||||
await closeApprovalPages(env.ctx);
|
||||
}
|
||||
assert(
|
||||
isDeepStrictEqual(await storedNetwork(env.page), before),
|
||||
"a rejected network switch moved the network",
|
||||
);
|
||||
assert(
|
||||
(await chainChangedEvents(env.dapp)).length === eventsBefore,
|
||||
"a rejected network switch told the page the chain changed",
|
||||
);
|
||||
|
||||
const approved = await openNetworkPrompt(
|
||||
env,
|
||||
"switch-approve",
|
||||
mainnet,
|
||||
sepolia,
|
||||
);
|
||||
let outcome;
|
||||
try {
|
||||
await clickAndClose(approved, "#btn-approve-network");
|
||||
outcome = await settleRequest(env.dapp, "switch-approve");
|
||||
} finally {
|
||||
await closeApprovalPages(env.ctx);
|
||||
}
|
||||
assert(
|
||||
outcome.settled === "resolved" && outcome.result === null,
|
||||
"the approved network switch did not resolve: " +
|
||||
JSON.stringify(outcome),
|
||||
);
|
||||
assert(
|
||||
(await storedNetwork(env.page)).networkId === "sepolia",
|
||||
"the approved network switch did not move the network",
|
||||
);
|
||||
const events = await chainChangedEvents(env.dapp, 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),
|
||||
);
|
||||
|
||||
const restored = await openNetworkPrompt(
|
||||
env,
|
||||
"switch-restore",
|
||||
sepolia,
|
||||
mainnet,
|
||||
);
|
||||
try {
|
||||
await clickAndClose(restored, "#btn-approve-network");
|
||||
outcome = await settleRequest(env.dapp, "switch-restore");
|
||||
} finally {
|
||||
await closeApprovalPages(env.ctx);
|
||||
}
|
||||
assert(
|
||||
outcome.settled === "resolved",
|
||||
"switching back to mainnet did not resolve: " + JSON.stringify(outcome),
|
||||
);
|
||||
assert(
|
||||
isDeepStrictEqual(await storedNetwork(env.page), before),
|
||||
"switching back did not restore the mainnet network and endpoints",
|
||||
);
|
||||
});
|
||||
|
||||
// The closing pass over both boundaries at once. Every message the section
|
||||
// put on either channel is re-read here and required to be free of the
|
||||
// password — and required to be there at all, method by method, so the
|
||||
|
||||
Reference in New Issue
Block a user