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
+14 -14
View File
@@ -25,6 +25,7 @@
const { Wallet } = require("ethers");
const { networkById } = require("../src/shared/networks");
const { applyChainSwitchFields } = require("../src/shared/chainSwitchFields");
const { makeStorageStub } = require("./support/storageStub");
const SIGNER_KEY =
@@ -240,8 +241,8 @@ describe("a chain switch under a transaction already committed to a chain", () =
// The artifact is verified against the chain read at the top of the
// attempt. Whatever endpoint it is then broadcast to has to be that same
// chain's — otherwise the wallet checks a transaction against Sepolia and
// sends it to a mainnet node. A connected site can switch the chain at any
// moment, including this one.
// sends it to a mainnet node. The user can switch the network in Settings
// at any moment, including this one.
test("the artifact is broadcast to the endpoint of the chain it was verified against", async () => {
const bg = loadWorker("sepolia");
@@ -264,9 +265,9 @@ describe("a chain switch under a transaction already committed to a chain", () =
populated(Number(SEPOLIA.networkVersion)),
);
// A connected site switches the chain while the attempt is running,
// and the switch is committed to storage in full before the attempt
// goes any further.
// The user switches the network in Settings while the attempt is
// running: the popup writes the switched record to storage in full
// before the attempt goes any further.
//
// It is fired from inside the attempt's SECOND state read, because
// that is where the window used to be: the chain id was captured at
@@ -277,18 +278,18 @@ describe("a chain switch under a transaction already committed to a chain", () =
// this to fire on, and the switch below runs after the attempt is
// done instead — which is the point.
let reads = 0;
let switched = null;
const doSwitch = async () => {
switched = bg.rpc("wallet_switchEthereumChain", [
{ chainId: MAINNET.chainId },
]);
await settle();
let switched = false;
const doSwitch = () => {
const record = bg.persisted();
applyChainSwitchFields(record, MAINNET.id);
global.chrome.storage.write("autistmask", record);
switched = true;
};
bg.setGetHook(async () => {
reads++;
if (reads !== 2) return;
bg.setGetHook(null);
await doSwitch();
doSwitch();
});
const attempt = bg.send(
@@ -303,8 +304,7 @@ describe("a chain switch under a transaction already committed to a chain", () =
await settle();
bg.setGetHook(null);
if (!switched) await doSwitch();
expect(switched.result()).toEqual({ result: null });
if (!switched) doSwitch();
expect(bg.persisted().networkId).toBe("mainnet");
await settle();