// 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 a second transaction before // Blockscout answers for the first. The first answer must then fill nothing on // the second transaction's screen. If the first request fails instead, its // error must not be logged. // // 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 BLOCKSCOUT = "https://eth.blockscout.com/api/v2"; const RECIPIENT = "0x2222222222222222222222222222222222222222"; // A history entry for a send of 1 ETH from ADDRESS to RECIPIENT on mainnet. function historyEntry(hash) { return { hash, from: ADDRESS, to: RECIPIENT, value: "1.0000", exactValue: "1.0", symbol: "ETH", timestamp: 1790000000, isError: false, direction: "sent", directionLabel: "Sent", contractAddress: null, chainId: "0x1", }; } const FIRST = historyEntry("0x" + "aa".repeat(32)); const SECOND = historyEntry("0x" + "bb".repeat(32)); // What Blockscout reports for each transaction. const ANSWERS = { [FIRST.hash]: { block_number: 21000001, nonce: 4441, fee: { value: "21000000000000" }, raw_input: "0x11111111", method: "firstMethod", }, [SECOND.hash]: { block_number: 21000002, nonce: 4442, fee: { value: "42000000000000" }, raw_input: "0x22222222", method: "secondMethod", }, }; // The text each answer puts on screen: its block, nonce, fee, method name and // raw data. No text from one answer appears in the other. const FIRST_SHOWN = [ "21000001", "4441", "0.000021 ETH", "firstMethod", "0x11111111", ]; const SECOND_SHOWN = [ "21000002", "4442", "0.000042 ETH", "secondMethod", "0x22222222", ]; // The error the first request fails with in the second test. const FIRST_FAILURE = "Blockscout did not answer the first request"; // 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); } // Boots the popup on Home with "Use Blockscout" on. The user opens FIRST, // whose request to Blockscout is held. Then the user presses Back and opens // SECOND, whose request is answered at once. Returns the page and the held // request's two outcomes: answer() lets FIRST's answer land, and fail() makes // the request fail. async function secondOpenedWhileFirstIsOut() { const page = await bootPopup( unversionedValidProfile({ useBlockscout: true }), ); let answer; let fail; const firstHeld = new Promise((resolve, reject) => { answer = resolve; fail = reject; }); globalThis.fetch = jest.fn(async (url) => { const hash = String(url).slice((BLOCKSCOUT + "/transactions/").length); if (hash === FIRST.hash) await firstHeld; return { ok: true, status: 200, json: async () => ANSWERS[hash] }; }); open(FIRST); await page.settle(); await page.click("btn-tx-back"); expect(page.visibleViews()).toEqual(["main"]); open(SECOND); await page.settle(); expect(globalThis.fetch.mock.calls.map(([url]) => url)).toEqual([ BLOCKSCOUT + "/transactions/" + FIRST.hash, BLOCKSCOUT + "/transactions/" + SECOND.hash, ]); return { page, answer: () => answer(), fail: () => fail(new Error(FIRST_FAILURE)), }; } beforeEach(() => { jest.spyOn(console, "error").mockImplementation(() => {}); }); afterEach(() => { cleanupPopup(); jest.restoreAllMocks(); }); test("an answer for the first transaction that lands on the second one's screen fills nothing", async () => { const { page, answer } = await secondOpenedWhileFirstIsOut(); answer(); await page.settle(); expect(page.visibleViews()).toEqual(["transaction"]); 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("a request for the first transaction that fails on the second one's screen logs nothing", async () => { const { page, fail } = await secondOpenedWhileFirstIsOut(); fail(); await page.settle(); expect(page.visibleViews()).toEqual(["transaction"]); const drawn = drawnDetails(page); for (const text of SECOND_SHOWN) expect(drawn).toContain(text); expect(console.error.mock.calls.flat()).not.toContain(FIRST_FAILURE); });