Files
AutistMask/tests/approvalOrigin.test.js
T
sneak 8b68b3e681
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
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
2026-10-07 21:33:11 +00:00

163 lines
5.2 KiB
JavaScript

// The connection, transaction, signature and network switch prompts name the
// site by its full origin, scheme and port included, not by its bare hostname
// (https://git.eeqj.de/sneak/AutistMask/issues/402). A page served over http,
// or on another port, of a host the user trusts over https must not raise a
// prompt that reads as that trusted site.
//
// Driven against a minimal DOM stub in the shape
// tests/contractCreation.test.js uses.
globalThis.chrome = {
storage: { local: { get: async () => ({}), set: async () => {} } },
};
const { state } = require("../src/shared/state");
const approval = require("../src/popup/views/approval");
// The site asking, in cleartext and on a port, which is what the hostname
// alone, dapp.example, used to hide.
const ORIGIN = "http://dapp.example:8080";
const FROM = "0x0000000000000000000000000000000000000a11";
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
function makeElement(id) {
const classes = new Set();
const el = {
id,
textContent: "",
value: "",
innerHTML: "",
disabled: false,
style: {},
dataset: {},
classList: {
add: (...names) => names.forEach((n) => classes.add(n)),
remove: (...names) => names.forEach((n) => classes.delete(n)),
contains: (n) => classes.has(n),
toggle: (n, force) => {
const on = force === undefined ? !classes.has(n) : force;
if (on) classes.add(n);
else classes.delete(n);
return on;
},
},
addEventListener: () => {},
querySelectorAll: () => [],
appendChild: () => {},
};
// Views reach for .parentElement to hide whole sections.
Object.defineProperty(el, "parentElement", {
get: () => node(id + "-parent"),
});
return el;
}
function makeDocument() {
const els = new Map();
return {
getElementById(id) {
// The debug banner is created on demand by helpers.js; absent
// is the state a non-debug, non-testnet popup is in.
if (id === "debug-banner") return null;
if (!els.has(id)) els.set(id, makeElement(id));
return els.get(id);
},
createElement: () => makeElement("created"),
body: { prepend: () => {} },
};
}
function node(id) {
return globalThis.document.getElementById(id);
}
// Open the prompt the background describes with `details`, the way the popup
// does: it asks for the approval and show() draws it.
async function openApproval(details) {
globalThis.document = makeDocument();
globalThis.window = { location: { search: "" } };
globalThis.chrome.runtime = {
connect: () => ({ postMessage: () => {} }),
sendMessage: (msg, reply) => {
if (!reply) return;
if (msg.type !== "AUTISTMASK_GET_APPROVAL") return reply(null);
reply({
origin: ORIGIN,
isPhishingDomain: false,
approvedFrom: FROM,
...details,
});
},
};
approval.init({});
await approval.show(1);
}
beforeEach(() => {
state.wallets = [];
state.activeAddress = FROM;
state.viewData = {};
state.viewStack = [];
state.currentView = null;
});
test("the connection prompt shows the origin", async () => {
await openApproval({});
expect(node("approve-origin").textContent).toBe(ORIGIN);
expect(node("view-approve-site").classList.contains("hidden")).toBe(false);
});
test("the transaction prompt shows the origin", async () => {
await openApproval({
type: "tx",
approvedTx: {
type: 2,
from: FROM,
chainId: "0x1",
nonce: "0x7",
gasLimit: "0x5208",
maxPriorityFeePerGas: "0x3b9aca00",
maxFeePerGas: "0x77359400",
to: RECIPIENT,
value: "0x0",
data: "0x",
accessList: [],
},
});
expect(node("approve-tx-origin").textContent).toBe(ORIGIN);
});
test("the signature prompt shows the origin", async () => {
await openApproval({
type: "sign",
// "Hello" as the hex a dApp passes to personal_sign.
signParams: {
method: "personal_sign",
message: "0x48656c6c6f",
from: FROM,
},
});
expect(node("approve-sign-origin").textContent).toBe(ORIGIN);
});
test("the network switch prompt shows the origin and both networks", async () => {
await openApproval({
type: "network",
currentNetworkId: "mainnet",
requestedNetworkId: "sepolia",
});
expect(node("approve-network-origin").textContent).toBe(ORIGIN);
expect(node("approve-network-current").textContent).toBe(
"Ethereum Mainnet",
);
expect(node("approve-network-requested").textContent).toBe(
"Sepolia Testnet",
);
// Its own screen, and not the connection prompt, whose "Allow" answers
// on the same port.
expect(node("view-approve-network").classList.contains("hidden")).toBe(
false,
);
expect(node("view-approve-site").classList.contains("hidden")).toBe(true);
});