// 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", 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); }); }); // 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(`
${RECIPIENT}
`); }); });