// 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, and the transaction detail view each say // so in a sentence on the recipient line, 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: () => {}, })); 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 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: () => [], }; // 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", hostname: "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); }); }); // The history list gives a contract creation `to: ""` (src/shared/ // transactions.js), and that is what the detail view is opened with. describe("the transaction detail view", () => { 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", }; } 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); }); });