// Browser-level network interception for the end-to-end suite. // // Every http(s) request the extension makes is fulfilled from these // fixtures, so the suite is deterministic and runs entirely offline. The // probe that motivated this harness (see issue #181) observed live calls // to Blockscout returning 401 inside the container, which would make any // assertion about rendered transaction data worthless. // // Anything not explicitly stubbed here is aborted AND reported to the // error collector, so a newly added outbound call shows up as a test // failure rather than as intermittent flakiness. "use strict"; // Fictional ERC-20 used to seed the transaction-detail test. The symbol // must not collide with any entry in src/shared/tokenList.js, or // isSpoofedSymbol() in src/shared/transactions.js drops the transfer as a // symbol-spoofing attempt; holders_count must be >= 1000 or the default // hideLowHolderTokens filter drops it. Either would make the test pass // vacuously by never rendering a row at all. const STUB_TOKEN = { address: "0xe2e0000000000000000000000000000000000e2e", symbol: "E2E", name: "End To End Test Token", decimals: "6", holders: "12345", }; const STUB_COUNTERPARTY = "0xc0ffee0000000000000000000000000000c0ffee"; const STUB_TX_HASH = "0xe2e0000000000000000000000000000000000000000000000000000000000e2e"; const STUB_BLOCK_NUMBER = 21000000; // Fixed instant so timeAgo() output is stable across runs. const STUB_TX_TIMESTAMP = "2026-01-02T03:04:05.000000Z"; // A 32-byte zero word. Returned for every eth_call, which is what makes // ethers' ENS reverse lookup resolve to "no resolver set" and return null // instead of throwing. A throw would be logged by src/shared/ens.js via // log.errorf(), i.e. console.error, which fails the run on its own. const ZERO_WORD = "0x" + "0".repeat(64); const RPC_RESULTS = { eth_chainId: "0x1", net_version: "1", eth_blockNumber: "0x1406f40", eth_getBalance: "0x0", eth_call: ZERO_WORD, eth_gasPrice: "0x3b9aca00", eth_estimateGas: "0x5208", eth_getTransactionCount: "0x0", eth_maxPriorityFeePerGas: "0x3b9aca00", }; function tokenObject() { return { address_hash: STUB_TOKEN.address, address: STUB_TOKEN.address, symbol: STUB_TOKEN.symbol, name: STUB_TOKEN.name, decimals: STUB_TOKEN.decimals, holders_count: STUB_TOKEN.holders, type: "ERC-20", }; } // One received ERC-20 transfer of 1.5 E2E to the address under test. function tokenTransferItems(address) { return [ { transaction_hash: STUB_TX_HASH, block_number: STUB_BLOCK_NUMBER, timestamp: STUB_TX_TIMESTAMP, from: { hash: STUB_COUNTERPARTY }, to: { hash: address }, total: { decimals: STUB_TOKEN.decimals, value: "1500000" }, token: tokenObject(), }, ]; } // Full details for STUB_TX_HASH. raw_input is "0x" so the calldata // decoder short-circuits; the on-chain detail fields still populate. function transactionDetails() { return { hash: STUB_TX_HASH, block_number: STUB_BLOCK_NUMBER, nonce: 7, gas_used: "51000", gas_price: "1000000000", fee: { value: "51000000000000" }, raw_input: "0x", status: "ok", }; } function jsonResponse(route, body) { return route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(body), }); } // Extract the address from a Blockscout /addresses//... path. function blockscoutAddress(pathname) { const m = pathname.match(/\/addresses\/(0x[0-9a-fA-F]{40})\//); return m ? m[1] : null; } function handleRpc(route, postData, report) { let payload; try { payload = JSON.parse(postData || "null"); } catch { report("unstubbed RPC: unparseable body " + String(postData)); return route.abort(); } // ethers batches by default, so the body may be an array. const batch = Array.isArray(payload) ? payload : [payload]; const replies = batch.map((req) => { const result = RPC_RESULTS[req.method]; if (result === undefined) { report("unstubbed RPC method: " + req.method); return { jsonrpc: "2.0", id: req.id, error: { code: -32601, message: "unstubbed in e2e harness" }, }; } return { jsonrpc: "2.0", id: req.id, result }; }); return jsonResponse(route, Array.isArray(payload) ? replies : replies[0]); } /** * Route every http(s) request through local fixtures. * * @param {import("playwright-core").BrowserContext} ctx * @param {object} opts * @param {(text: string) => void} opts.report called for unstubbed traffic * @param {boolean} [opts.seedTokenTransfer] serve the stubbed ERC-20 * transfer. Read at request time, so a test can flip it on the same * options object without re-registering the route. */ async function installNetworkStubs(ctx, opts) { const report = opts.report; // Regex rather than a glob so chrome-extension:// resource loads are // never touched — routing those would break the popup itself. await ctx.route(/^https?:\/\//, async (route) => { const req = route.request(); const url = new URL(req.url()); const p = url.pathname; // JSON-RPC endpoint (any host): a POST with a JSON-RPC body. if (req.method() === "POST") { return handleRpc(route, req.postData(), report); } // Blockscout v2 if (p.includes("/api/v2/")) { if (/\/addresses\/0x[0-9a-fA-F]{40}\/transactions$/.test(p)) { return jsonResponse(route, { items: [] }); } if (/\/addresses\/0x[0-9a-fA-F]{40}\/token-transfers$/.test(p)) { const addr = blockscoutAddress(p); return jsonResponse(route, { items: opts.seedTokenTransfer && addr ? tokenTransferItems(addr) : [], }); } if (/\/addresses\/0x[0-9a-fA-F]{40}\/token-balances$/.test(p)) { return jsonResponse(route, []); } if (p.endsWith("/transactions/" + STUB_TX_HASH)) { return jsonResponse(route, transactionDetails()); } } // CoinDesk price tick if (url.hostname.endsWith("coindesk.com")) { return jsonResponse(route, { Data: {} }); } // MetaMask phishing blocklist if ( url.hostname === "raw.githubusercontent.com" || p.endsWith("/eth-phishing-detect/main/src/config.json") ) { return jsonResponse(route, { version: 2, tolerance: 2, fuzzylist: [], whitelist: [], blacklist: [], }); } // Best-effort Etherscan address labels: served as an empty page. if (url.hostname.endsWith("etherscan.io")) { return route.fulfill({ status: 200, contentType: "text/html", body: "", }); } report("unstubbed request: " + req.method() + " " + req.url()); return route.abort(); }); } module.exports = { installNetworkStubs, STUB_TOKEN, STUB_COUNTERPARTY, STUB_TX_HASH, };