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
163 lines
5.2 KiB
JavaScript
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);
|
|
});
|