// TransactionDetail shows only details that Blockscout gave for the // transaction on screen (https://git.eeqj.de/sneak/AutistMask/issues/675). // // The screen asks Blockscout for its transaction and does not wait for the // answer. So the user can press Back and open another transaction before // Blockscout answers for the first. The first answer must then fill nothing on // the second transaction's screen. An answer that lands while Settings is up // must still fill TransactionDetail. // // Driven through the real popup over a stored profile // (tests/support/popupBoot.js). Each transaction is opened as a history screen // opens it: the current screen is pushed, then transactionDetail.show() draws // the transaction. "use strict"; const { bootPopup, cleanupPopup, unversionedValidProfile, ADDRESS, } = require("./support/popupBoot"); const MAINNET_BLOCKSCOUT = "https://eth.blockscout.com/api/v2"; const SEPOLIA_BLOCKSCOUT = "https://eth-sepolia.blockscout.com/api/v2"; const RECIPIENT = "0x2222222222222222222222222222222222222222"; // A history entry for a send of 1 `symbol` from ADDRESS to RECIPIENT, on the // network with `chainId`. function historyEntry(hash, chainId, symbol) { return { hash, from: ADDRESS, to: RECIPIENT, value: "1.0000", exactValue: "1.0", symbol, timestamp: 1790000000, isError: false, direction: "sent", directionLabel: "Sent", contractAddress: null, chainId, }; } const FIRST = historyEntry("0x" + "aa".repeat(32), "0x1", "ETH"); const SECOND = historyEntry("0x" + "bb".repeat(32), "0x1", "ETH"); // A transaction on Sepolia with the same hash as FIRST. const FIRST_ON_SEPOLIA = historyEntry(FIRST.hash, "0xaa36a7", "SepoliaETH"); // The URL that asks Blockscout for `entry`, on the network it was sent on. function requestUrl(entry) { const blockscout = entry.chainId === "0x1" ? MAINNET_BLOCKSCOUT : SEPOLIA_BLOCKSCOUT; return blockscout + "/transactions/" + entry.hash; } // What Blockscout reports for each transaction, by the URL that asks for it. const ANSWERS = { [requestUrl(FIRST)]: { block_number: 21000001, nonce: 4441, fee: { value: "21000000000000" }, raw_input: "0x11111111", method: "firstMethod", }, [requestUrl(SECOND)]: { block_number: 21000002, nonce: 4442, fee: { value: "42000000000000" }, raw_input: "0x22222222", method: "secondMethod", }, [requestUrl(FIRST_ON_SEPOLIA)]: { block_number: 21000003, nonce: 4443, fee: { value: "63000000000000" }, raw_input: "0x33333333", method: "sepoliaMethod", }, }; // The text each answer puts on screen: its block, nonce, fee, method name and // raw data. No text from one answer appears in another. const FIRST_SHOWN = [ "21000001", "4441", "0.000021 ETH", "firstMethod", "0x11111111", ]; const SECOND_SHOWN = [ "21000002", "4442", "0.000042 ETH", "secondMethod", "0x22222222", ]; const FIRST_ON_SEPOLIA_SHOWN = [ "21000003", "4443", "0.000063 SepoliaETH", "sepoliaMethod", "0x33333333", ]; // Everything drawn in the fields that Blockscout's answer fills. function drawnDetails(page) { return [ "tx-detail-block", "tx-detail-nonce", "tx-detail-fee", "tx-detail-calldata-action", "tx-detail-rawdata", ] .map((id) => page.node(id).innerHTML + page.node(id).textContent) .join("\n"); } // Opens `entry` on TransactionDetail, as a history screen does. function open(entry) { require("../src/popup/views/helpers").pushCurrentView(); require("../src/popup/views/transactionDetail").show(entry); } // The URLs of every request to Blockscout so far, in order. function requestedUrls() { return globalThis.fetch.mock.calls.map(([url]) => url); } // Boots the popup on Home with "Use Blockscout" on. Blockscout answers every // request at once from ANSWERS, except the request for `held`. Returns the // page and answer(), which lets the answer for `held` land. async function bootWithHeldRequest(held) { const page = await bootPopup( unversionedValidProfile({ useBlockscout: true }), ); let answer; const heldAnswer = new Promise((resolve) => { answer = resolve; }); globalThis.fetch = jest.fn(async (url) => { if (url === requestUrl(held)) await heldAnswer; return { ok: true, status: 200, json: async () => ANSWERS[url] }; }); return { page, answer: () => answer() }; } // The user opens `first`, whose request to Blockscout is held. Then the user // presses Back and opens `second`. Then the answer for `first` lands. Returns // the page. async function firstAnswerLandsOnSecond(first, second) { const { page, answer } = await bootWithHeldRequest(first); open(first); await page.settle(); await page.click("btn-tx-back"); expect(page.visibleViews()).toEqual(["main"]); open(second); await page.settle(); expect(requestedUrls()).toEqual([requestUrl(first), requestUrl(second)]); answer(); await page.settle(); expect(page.visibleViews()).toEqual(["transaction"]); return page; } afterEach(() => { cleanupPopup(); }); test("an answer for the first transaction that lands on the second one's screen fills nothing", async () => { const page = await firstAnswerLandsOnSecond(FIRST, SECOND); const drawn = drawnDetails(page); for (const text of SECOND_SHOWN) expect(drawn).toContain(text); for (const text of FIRST_SHOWN) expect(drawn).not.toContain(text); }); test("an answer for a hash on mainnet that lands on the same hash's screen on Sepolia fills nothing", async () => { const page = await firstAnswerLandsOnSecond(FIRST, FIRST_ON_SEPOLIA); const drawn = drawnDetails(page); for (const text of FIRST_ON_SEPOLIA_SHOWN) expect(drawn).toContain(text); for (const text of FIRST_SHOWN) expect(drawn).not.toContain(text); }); test("an answer that lands while Settings is up still fills TransactionDetail", async () => { const { page, answer } = await bootWithHeldRequest(FIRST); open(FIRST); await page.settle(); await page.click("btn-settings"); expect(page.visibleViews()).toEqual(["settings"]); answer(); await page.settle(); await page.click("btn-settings"); expect(page.visibleViews()).toEqual(["transaction"]); expect(requestedUrls()).toEqual([requestUrl(FIRST)]); const drawn = drawnDetails(page); for (const text of FIRST_SHOWN) expect(drawn).toContain(text); });