allowedSites and deniedSites held the bare hostname, so a grant to https://dapp.example also authorised http://dapp.example and every port on that host, and the connection, transaction and signature prompts named only the hostname. Both lists now store and match the full origin (scheme://host[:port]), the key the connections approved without Remember already used. The prompts, the Settings site lists and AUTISTMASK_REMOVE_SITE use the origin too. Entries saved by hostname are not migrated (pre-1.0): they match no site. Model: opus-5-5
316 lines
10 KiB
JavaScript
316 lines
10 KiB
JavaScript
// The recipient line of a contract creation
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/250).
|
|
//
|
|
// A transaction with no `to` creates a contract. The approval screen, the
|
|
// wait, success and error screens, the transaction detail view and the
|
|
// transaction history rows each say so in a sentence, where they used to show
|
|
// a blank line (an empty address, with a colour dot whose colour was
|
|
// `undefined`) or, on the approval screen, "(contract creation)". A
|
|
// transaction with a real `to` still shows that address.
|
|
//
|
|
// Driven against a minimal DOM stub in the shape
|
|
// tests/typedDataPermit.test.js uses.
|
|
|
|
jest.mock("../src/shared/log", () => ({
|
|
log: {
|
|
debugf: () => {},
|
|
infof: () => {},
|
|
warnf: () => {},
|
|
errorf: () => {},
|
|
},
|
|
// The transaction detail view fetches on-chain details after drawing; an
|
|
// answer that is not ok leaves the drawn lines as they are.
|
|
debugFetch: async () => ({ ok: false }),
|
|
setRuntimeDebug: () => {},
|
|
isDebug: () => false,
|
|
}));
|
|
|
|
// The wait screen polls for a receipt; this one never arrives.
|
|
jest.mock("../src/shared/balances", () => ({
|
|
getProvider: () => ({ getTransactionReceipt: () => new Promise(() => {}) }),
|
|
refreshBalances: () => {},
|
|
}));
|
|
|
|
// The history lists ask the explorer for their transactions and resolve ENS
|
|
// names for them; here the explorer answers with mockHistory and no name
|
|
// resolves.
|
|
let mockHistory = [];
|
|
jest.mock("../src/shared/transactions", () => ({
|
|
...jest.requireActual("../src/shared/transactions"),
|
|
fetchRecentTransactions: async () => mockHistory,
|
|
}));
|
|
jest.mock("../src/shared/ens", () => ({
|
|
...jest.requireActual("../src/shared/ens"),
|
|
resolveEnsNames: async () => new Map(),
|
|
}));
|
|
|
|
globalThis.chrome = {
|
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
|
};
|
|
|
|
const { state } = require("../src/shared/state");
|
|
const approval = require("../src/popup/views/approval");
|
|
const txStatus = require("../src/popup/views/txStatus");
|
|
const transactionDetail = require("../src/popup/views/transactionDetail");
|
|
const home = require("../src/popup/views/home");
|
|
const addressDetail = require("../src/popup/views/addressDetail");
|
|
const addressToken = require("../src/popup/views/addressToken");
|
|
|
|
const SENTENCE =
|
|
"This transaction creates a new contract. It has no recipient.";
|
|
|
|
const FROM = "0x0000000000000000000000000000000000000a11";
|
|
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
|
const TX_HASH =
|
|
"0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84";
|
|
// Init code for a contract creation's data.
|
|
const INIT_CODE = "0x600160005500";
|
|
|
|
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);
|
|
}
|
|
|
|
// The line a transaction with a real `to` shows: that address, and nothing
|
|
// left over from an empty one.
|
|
function expectAddressLine(html) {
|
|
expect(html).toContain(RECIPIENT);
|
|
expect(html).not.toContain(SENTENCE);
|
|
expect(html).not.toContain("undefined");
|
|
}
|
|
|
|
beforeEach(() => {
|
|
globalThis.document = makeDocument();
|
|
globalThis.window = { location: { search: "" } };
|
|
state.wallets = [];
|
|
state.trackedTokens = [];
|
|
state.viewData = {};
|
|
state.viewStack = [];
|
|
state.currentView = null;
|
|
txStatus.init({ doRefreshAndRender: () => {} });
|
|
});
|
|
|
|
afterEach(() => {
|
|
txStatus.endWait();
|
|
});
|
|
|
|
// Open the transaction approval screen the way the popup does: the background
|
|
// hands over the populated transaction and show() draws it.
|
|
async function openTxApproval(to, data) {
|
|
globalThis.chrome.runtime = {
|
|
connect: () => ({ postMessage: () => {} }),
|
|
sendMessage: (msg, reply) => {
|
|
if (!reply) return;
|
|
if (msg.type !== "AUTISTMASK_GET_APPROVAL") return reply(null);
|
|
reply({
|
|
type: "tx",
|
|
origin: "https://dapp.example",
|
|
isPhishingDomain: false,
|
|
approvedFrom: FROM,
|
|
approvedTx: {
|
|
type: 2,
|
|
from: FROM,
|
|
chainId: "0x1",
|
|
nonce: "0x7",
|
|
gasLimit: "0x5208",
|
|
maxPriorityFeePerGas: "0x3b9aca00",
|
|
maxFeePerGas: "0x77359400",
|
|
to,
|
|
value: "0x0",
|
|
data,
|
|
accessList: [],
|
|
},
|
|
});
|
|
},
|
|
};
|
|
approval.init({});
|
|
await approval.show(1);
|
|
}
|
|
|
|
describe("the transaction approval screen", () => {
|
|
test("a contract creation says so instead of naming a contract", async () => {
|
|
await openTxApproval(null, INIT_CODE);
|
|
expect(node("approve-tx-to").innerHTML).toBe(SENTENCE);
|
|
});
|
|
|
|
test("a transaction with a recipient shows its address", async () => {
|
|
await openTxApproval(RECIPIENT, "0x");
|
|
expectAddressLine(node("approve-tx-to").innerHTML);
|
|
});
|
|
});
|
|
|
|
// approval.js carries a contract creation to these screens with `to` as "".
|
|
describe("the wait, success and error screens", () => {
|
|
const creation = {
|
|
to: "",
|
|
amount: "0.0000",
|
|
token: "ETH",
|
|
tokenSymbol: null,
|
|
};
|
|
const transfer = { ...creation, to: RECIPIENT };
|
|
|
|
test("a contract creation says so on the wait screen", () => {
|
|
txStatus.showWait(creation, TX_HASH);
|
|
expect(node("wait-tx-to").innerHTML).toBe(SENTENCE);
|
|
});
|
|
|
|
test("a transaction with a recipient shows its address on the wait screen", () => {
|
|
txStatus.showWait(transfer, TX_HASH);
|
|
expectAddressLine(node("wait-tx-to").innerHTML);
|
|
});
|
|
|
|
test("a contract creation says so on the success and error screens", () => {
|
|
state.viewData = {
|
|
amount: "0.0000",
|
|
symbol: "ETH",
|
|
to: "",
|
|
hash: TX_HASH,
|
|
blockNumber: 1,
|
|
};
|
|
txStatus.renderSuccess();
|
|
expect(node("success-tx-to").innerHTML).toBe(SENTENCE);
|
|
|
|
txStatus.showError(creation, TX_HASH, "The transaction failed.");
|
|
expect(node("error-tx-to").innerHTML).toBe(SENTENCE);
|
|
});
|
|
|
|
test("a transaction with a recipient shows its address on the success and error screens", () => {
|
|
state.viewData = {
|
|
amount: "0.0050",
|
|
symbol: "ETH",
|
|
to: RECIPIENT,
|
|
hash: TX_HASH,
|
|
blockNumber: 1,
|
|
};
|
|
txStatus.renderSuccess();
|
|
expectAddressLine(node("success-tx-to").innerHTML);
|
|
|
|
txStatus.showError(transfer, TX_HASH, "The transaction failed.");
|
|
expectAddressLine(node("error-tx-to").innerHTML);
|
|
});
|
|
});
|
|
|
|
// A transaction FROM sent, as the history lists hold it. The explorer reports a
|
|
// contract creation with no `to`, which src/shared/transactions.js turns into
|
|
// `to: ""`.
|
|
function historyTx(to) {
|
|
return {
|
|
hash: TX_HASH,
|
|
from: FROM,
|
|
to,
|
|
value: "0.0000",
|
|
exactValue: "0.0",
|
|
rawAmount: "0",
|
|
rawUnit: "wei",
|
|
symbol: "ETH",
|
|
timestamp: 1790000000,
|
|
isError: false,
|
|
directionLabel: "Sent",
|
|
direction: "sent",
|
|
contractAddress: null,
|
|
};
|
|
}
|
|
|
|
// The detail view is opened with the transaction a history row holds.
|
|
describe("the transaction detail view", () => {
|
|
test("a contract creation says so", () => {
|
|
transactionDetail.show(historyTx(""));
|
|
expect(node("tx-detail-to").innerHTML).toBe(SENTENCE);
|
|
expect(node("tx-detail-type").textContent).toBe("Contract Creation");
|
|
});
|
|
|
|
test("a transaction with a recipient shows its address", () => {
|
|
transactionDetail.show(historyTx(RECIPIENT));
|
|
expectAddressLine(node("tx-detail-to").innerHTML);
|
|
});
|
|
});
|
|
|
|
// The same rows are drawn on Home, AddressDetail and AddressToken (for ETH).
|
|
describe.each([
|
|
["Home", "home-tx-list", () => home.render({})],
|
|
["AddressDetail", "tx-list", () => addressDetail.show()],
|
|
["AddressToken", "address-token-tx-list", () => addressToken.show()],
|
|
])("the transaction history on %s", (_name, listId, open) => {
|
|
async function rowsFor(tx) {
|
|
mockHistory = [tx];
|
|
open();
|
|
// The list is drawn once the history has been fetched.
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
return node(listId).innerHTML;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
state.wallets = [
|
|
{
|
|
name: "Main",
|
|
type: "key",
|
|
addresses: [{ address: FROM, balance: "0.0000" }],
|
|
},
|
|
];
|
|
state.selectedWallet = 0;
|
|
state.selectedAddress = 0;
|
|
state.selectedToken = "ETH";
|
|
});
|
|
|
|
test("a contract creation's row says so, with no colour dot and no address line", async () => {
|
|
const html = await rowsFor(historyTx(""));
|
|
expect(html).toContain(SENTENCE);
|
|
expect(html).not.toContain("background:");
|
|
expect(html).not.toContain("am-address");
|
|
expect(html).not.toContain("undefined");
|
|
});
|
|
|
|
test("a transaction with a recipient shows its colour dot and address", async () => {
|
|
const html = await rowsFor(historyTx(RECIPIENT));
|
|
expectAddressLine(html);
|
|
expect(html).toContain("background:#");
|
|
expect(html).toContain(`<div class="am-address">${RECIPIENT}</div>`);
|
|
});
|
|
});
|