Closing a transaction or signature approval window no longer rejects the request; only Cancel does. The browser can close that window when the user clicks away, for example to copy the password from a password manager. The request stays pending and the site gets no answer. Pressing the toolbar button opens the oldest such request, unless an approved attempt still holds it. A Reject the background refuses keeps the window open with the background's message. The worker points the toolbar back at the wallet when it starts. While such a request waits, the transaction slot stays held, so another site's eth_sendTransaction gets -32002. An address switch still rejects pending approvals with 4001. Model: opus-5-5
311 lines
11 KiB
JavaScript
311 lines
11 KiB
JavaScript
// The approval window shows no screen until the background has described the
|
|
// approval it answers (https://git.eeqj.de/sneak/AutistMask/issues/408). The
|
|
// connection prompt's "Allow" and the network switch prompt's "Switch" answer
|
|
// on the same port, so a window that showed the connection prompt while it
|
|
// waited could approve a network switch.
|
|
//
|
|
// Booted through the real popup entry point, which is where the connection
|
|
// prompt used to be put up before the background had answered.
|
|
|
|
const {
|
|
bootPopup,
|
|
cleanupPopup,
|
|
settle,
|
|
unversionedValidProfile,
|
|
ADDRESS,
|
|
} = require("./support/popupBoot");
|
|
|
|
afterEach(cleanupPopup);
|
|
|
|
test("the approval window shows no screen until the background describes the approval", async () => {
|
|
// The background's answer, held until the test gives it.
|
|
let answer = null;
|
|
const env = await bootPopup(unversionedValidProfile(), {
|
|
search: "?approval=approval-1",
|
|
runtime: {
|
|
connect: () => ({ postMessage: () => {} }),
|
|
sendMessage: (msg, reply) => {
|
|
if (msg.type === "AUTISTMASK_GET_APPROVAL") answer = reply;
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(answer).not.toBeNull();
|
|
// No screen at all, the connection prompt included.
|
|
expect(env.visibleViews()).toEqual([]);
|
|
|
|
answer({
|
|
type: "network",
|
|
origin: "https://dapp.example",
|
|
currentNetworkId: "mainnet",
|
|
requestedNetworkId: "sepolia",
|
|
isPhishingDomain: false,
|
|
});
|
|
await settle();
|
|
|
|
expect(env.visibleViews()).toEqual(["approve-network"]);
|
|
expect(env.pageErrors).toEqual([]);
|
|
});
|
|
|
|
// The background opens the approval window as wide as the page, but the
|
|
// browser counts the window's frame in that width, and a screen taller than
|
|
// the window takes a vertical scrollbar out of it too
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/518). The browser suites run
|
|
// without frames, and only the Firefox suite's sign prompt at a zoom other
|
|
// than 100%, so the widening is checked here too.
|
|
//
|
|
// A window 396 screen pixels wide whose page is drawn at `drawn` screen pixels
|
|
// to the page pixel. Widening it gives the page its new width, less the 15
|
|
// screen pixels of the scrollbar the approval screen has, and then calls the
|
|
// page's resize listeners, each of which asked to be called once.
|
|
function windowsStub(type, drawn) {
|
|
const updates = [];
|
|
return {
|
|
updates,
|
|
getCurrent: (done) => done({ id: 7, type, width: 396 }),
|
|
update: (id, info, done) => {
|
|
updates.push({ id, info });
|
|
done({});
|
|
globalThis.document.documentElement.clientWidth = Math.floor(
|
|
(info.width - 15) / drawn,
|
|
);
|
|
const resized = globalThis.window.listeners.resize || [];
|
|
globalThis.window.listeners.resize = [];
|
|
for (const fn of resized) fn();
|
|
},
|
|
};
|
|
}
|
|
|
|
// The approval page's own tab is at `zoom`. Asked without a tab id, Firefox
|
|
// answers for the window focused last, here a site's tab at 100%.
|
|
function tabsStub(zoom) {
|
|
return {
|
|
getCurrent: (done) => done({ id: 11 }),
|
|
getZoom: (...args) => {
|
|
const done = args.pop();
|
|
done(args[0] === 11 ? zoom : 1);
|
|
},
|
|
};
|
|
}
|
|
|
|
// An approval window at `zoom`, `clientWidth` page pixels wide before its
|
|
// screen is up, whose background holds its answer until the test gives it.
|
|
async function bootZoomedApprovalWindow(windows, zoom, clientWidth) {
|
|
let answer = null;
|
|
const env = await bootPopup(unversionedValidProfile(), {
|
|
search: "?approval=approval-1",
|
|
runtime: {
|
|
connect: () => ({ postMessage: () => {} }),
|
|
sendMessage: (msg, reply) => {
|
|
if (msg.type === "AUTISTMASK_GET_APPROVAL") answer = reply;
|
|
},
|
|
},
|
|
windows,
|
|
tabs: tabsStub(zoom),
|
|
documentElement: { scrollWidth: 396, clientWidth },
|
|
});
|
|
return { env, answer };
|
|
}
|
|
|
|
test("at 150% zoom the approval window widens by what its screen lacks, scrollbar included", async () => {
|
|
const windows = windowsStub("popup", 1.5);
|
|
// At 150% a window 396 screen pixels wide shows 264 page pixels.
|
|
const { env, answer } = await bootZoomedApprovalWindow(windows, 1.5, 264);
|
|
|
|
// Not before there is a screen to fit.
|
|
expect(windows.updates).toEqual([]);
|
|
|
|
// The screen is taller than the window, so once it is up a vertical
|
|
// scrollbar takes 15 screen pixels, 10 page pixels at 150%.
|
|
env.document.documentElement.clientWidth = 254;
|
|
answer({
|
|
type: "network",
|
|
origin: "https://dapp.example",
|
|
currentNetworkId: "mainnet",
|
|
requestedNetworkId: "sepolia",
|
|
isPhishingDomain: false,
|
|
});
|
|
await settle();
|
|
|
|
// 396 - 254 = 142 page pixels short, which at 150% is 213 screen pixels.
|
|
// At the 100% of the site's tab it would be 142, and the screen would
|
|
// still not fit.
|
|
expect(env.visibleViews()).toEqual(["approve-network"]);
|
|
expect(windows.updates).toEqual([{ id: 7, info: { width: 609 } }]);
|
|
expect(env.document.documentElement.clientWidth).toBe(396);
|
|
expect(env.pageErrors).toEqual([]);
|
|
});
|
|
|
|
test("at 170% the approval window is widened again while its screen still falls short", async () => {
|
|
// Firefox reports 170% as 1.7 but draws a page pixel about 1.714 screen
|
|
// pixels wide, so a window 396 wide shows 231 page pixels.
|
|
const windows = windowsStub("popup", 1.714);
|
|
const { env, answer } = await bootZoomedApprovalWindow(windows, 1.7, 231);
|
|
|
|
// Less the scrollbar once the screen is up.
|
|
env.document.documentElement.clientWidth = 222;
|
|
answer({
|
|
type: "network",
|
|
origin: "https://dapp.example",
|
|
currentNetworkId: "mainnet",
|
|
requestedNetworkId: "sepolia",
|
|
isPhishingDomain: false,
|
|
});
|
|
await settle();
|
|
|
|
// 174 page pixels short, so 296 screen pixels wider at 1.7. That shows 394
|
|
// page pixels, still 2 short, so 4 screen pixels wider again.
|
|
expect(windows.updates).toEqual([
|
|
{ id: 7, info: { width: 692 } },
|
|
{ id: 7, info: { width: 696 } },
|
|
]);
|
|
const page = env.document.documentElement;
|
|
expect(page.scrollWidth).toBeLessThanOrEqual(page.clientWidth);
|
|
expect(env.pageErrors).toEqual([]);
|
|
});
|
|
|
|
test("a connection prompt in the toolbar popup leaves the browser window alone", async () => {
|
|
const windows = windowsStub("normal", 1.5);
|
|
const { env, answer } = await bootZoomedApprovalWindow(windows, 1.5, 264);
|
|
|
|
answer({ origin: "https://dapp.example", isPhishingDomain: false });
|
|
await settle();
|
|
|
|
expect(env.visibleViews()).toEqual(["approve-site"]);
|
|
expect(windows.updates).toEqual([]);
|
|
expect(env.pageErrors).toEqual([]);
|
|
});
|
|
|
|
// Each row holds a transaction or signature approval as the background
|
|
// describes it, plus the element ids of the screen that shows it. `refusal` is
|
|
// the message the background refuses a Reject with.
|
|
const TX_AND_SIGN_APPROVALS = [
|
|
{
|
|
kind: "transaction",
|
|
view: "approve-tx",
|
|
reject: "btn-reject-tx",
|
|
error: "approve-tx-error",
|
|
type: "AUTISTMASK_TX_RESPONSE",
|
|
refusal: "This transaction is already being sent.",
|
|
approval: {
|
|
type: "tx",
|
|
approvedFrom: ADDRESS,
|
|
approvedTx: {
|
|
chainId: 1,
|
|
to: ADDRESS,
|
|
value: "0",
|
|
data: "0x",
|
|
gasLimit: "21000",
|
|
maxFeePerGas: "1000000000",
|
|
maxPriorityFeePerGas: "1000000000",
|
|
nonce: 0,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
kind: "signature",
|
|
view: "approve-sign",
|
|
reject: "btn-reject-sign",
|
|
error: "approve-sign-error",
|
|
type: "AUTISTMASK_SIGN_RESPONSE",
|
|
refusal: "This request is already being signed.",
|
|
approval: {
|
|
type: "sign",
|
|
approvedFrom: ADDRESS,
|
|
signParams: {
|
|
method: "personal_sign",
|
|
message: "0x48656c6c6f",
|
|
from: ADDRESS,
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
// A closed transaction or signature window leaves its request pending
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/673). So Reject closes the
|
|
// window only once the background has the rejection. A message sent as the
|
|
// window closes can be lost, and the request would stay pending although the
|
|
// user rejected it.
|
|
test.each(TX_AND_SIGN_APPROVALS)(
|
|
"Reject on a $kind approval closes the window only once the background has answered",
|
|
async ({ view, reject, type, approval }) => {
|
|
let describeApproval = null;
|
|
let answerReject = null;
|
|
const sent = [];
|
|
const env = await bootPopup(unversionedValidProfile(), {
|
|
search: "?approval=approval-1",
|
|
runtime: {
|
|
connect: () => ({ postMessage: () => {} }),
|
|
sendMessage: (msg, reply) => {
|
|
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
|
|
describeApproval = reply;
|
|
}
|
|
if (msg.type === type) {
|
|
sent.push(msg);
|
|
answerReject = reply;
|
|
}
|
|
},
|
|
},
|
|
});
|
|
describeApproval({
|
|
...approval,
|
|
origin: "https://dapp.example",
|
|
isPhishingDomain: false,
|
|
});
|
|
await settle();
|
|
expect(env.visibleViews()).toEqual([view]);
|
|
const close = jest.fn();
|
|
globalThis.window.close = close;
|
|
|
|
const clicked = env.click(reject);
|
|
await settle();
|
|
expect(sent).toEqual([{ type, id: "approval-1", approved: false }]);
|
|
expect(close).not.toHaveBeenCalled();
|
|
|
|
answerReject(null);
|
|
await clicked;
|
|
expect(close).toHaveBeenCalledTimes(1);
|
|
expect(env.pageErrors).toEqual([]);
|
|
},
|
|
);
|
|
|
|
// After Confirm or Sign, the background refuses a Reject while the transaction
|
|
// is being sent or the signature checked. The request is not rejected then, so
|
|
// the window stays open and shows the background's message.
|
|
test.each(TX_AND_SIGN_APPROVALS)(
|
|
"Reject on a $kind approval that the background refuses shows the background's message and keeps the window open",
|
|
async ({ view, reject, error, type, refusal, approval }) => {
|
|
let describeApproval = null;
|
|
let answerReject = null;
|
|
const env = await bootPopup(unversionedValidProfile(), {
|
|
search: "?approval=approval-1",
|
|
runtime: {
|
|
connect: () => ({ postMessage: () => {} }),
|
|
sendMessage: (msg, reply) => {
|
|
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
|
|
describeApproval = reply;
|
|
}
|
|
if (msg.type === type) answerReject = reply;
|
|
},
|
|
},
|
|
});
|
|
describeApproval({
|
|
...approval,
|
|
origin: "https://dapp.example",
|
|
isPhishingDomain: false,
|
|
});
|
|
await settle();
|
|
const close = jest.fn();
|
|
globalThis.window.close = close;
|
|
|
|
const clicked = env.click(reject);
|
|
await settle();
|
|
answerReject({ error: refusal, retryable: false });
|
|
await clicked;
|
|
expect(close).not.toHaveBeenCalled();
|
|
expect(env.visibleViews()).toEqual([view]);
|
|
expect(env.text(error)).toBe(refusal);
|
|
expect(env.pageErrors).toEqual([]);
|
|
},
|
|
);
|