// The approval screen for a site's Sepolia transfer of the contract at USDC's // mainnet address, while Sepolia is active. // // The bundled token list holds Ethereum Mainnet contracts. The contract at the // same address on Sepolia is another token, but the screen named it USDC and // scaled its amount at USDC's decimals // (https://git.eeqj.de/sneak/AutistMask/issues/632). The bundled list now names // a token only on Ethereum Mainnet. The same screen for a mainnet transfer of // USDC, while Sepolia is active, still names USDC. // // Driven through the real approval screen, against the minimal DOM stub // tests/approvalTokenNetwork.test.js uses. globalThis.chrome = { storage: { local: { get: async () => ({}), set: async () => {} } }, }; const { Interface } = require("ethers"); const { state } = require("../src/shared/state"); const { NETWORKS } = require("../src/shared/networks"); const approval = require("../src/popup/views/approval"); const FROM = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; const RECIPIENT = "0x711e691962E5c054b78071C789A0C8CE657377aD"; // USDC's address on Ethereum Mainnet, where the bundled list names it. const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // FROM sends 1234500000 base units to RECIPIENT. const TRANSFER_DATA = new Interface([ "function transfer(address to, uint256 amount)", ]).encodeFunctionData("transfer", [RECIPIENT, 1234500000n]); function makeElement(id) { const classes = new Set(); const el = { id, textContent: "", value: "", innerHTML: "", disabled: false, style: {}, dataset: {}, listeners: {}, 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: (name, fn) => { el.listeners[name] = el.listeners[name] || []; el.listeners[name].push(fn); }, 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); } // One line of the decoded details: its label and what is drawn under it. function decodedLine(label) { return node("approve-tx-decoded-details") .innerHTML.split('
') .find((line) => line.startsWith(`
${label}
`), ); } // Open the approval screen, with Sepolia active, for the site's transaction // sending TRANSFER_DATA to the contract at USDC's address on the network with // chain id `chainId`. async function showTransfer(chainId) { globalThis.document = makeDocument(); globalThis.window = { location: { search: "" }, close: () => {} }; globalThis.chrome.runtime = { connect: () => ({ postMessage: () => {} }), sendMessage: (msg, reply) => { if (!reply) return; reply({ type: "tx", origin: "https://dapp.example", isPhishingDomain: false, approvedFrom: FROM, approvedTx: { type: 2, from: FROM, chainId, nonce: "0x7", gasLimit: "0x30d40", maxPriorityFeePerGas: "0x3b9aca00", maxFeePerGas: "0x77359400", to: USDC, value: "0x0", data: TRANSFER_DATA, accessList: [], }, }); }, }; state.networkId = "sepolia"; state.trackedTokens = []; state.networkTrackedTokens = {}; state.activeAddress = FROM; state.wallets = [ { type: "hd", name: "Wallet 1", xpub: "xpub-wallet-1", encryptedSecret: "encrypted-secret-1", nextIndex: 1, addresses: [ { address: FROM, index: 0, balance: "0", tokenBalances: [] }, ], }, ]; state.currentView = null; approval.init({}); await approval.show(1); } test("names no token, and states the amount in base units", async () => { await showTransfer(NETWORKS.sepolia.chainId); expect(state.currentView).toBe("approve-tx"); expect(node("approve-tx-to").innerHTML).not.toContain("USDC"); expect(decodedLine("Token")).toContain( '
Unknown token
', ); expect(decodedLine("Amount")).toContain( '
1234500000 base units (decimals unknown)
', ); }); // A transaction is named for its own network, not the active one. test("a mainnet transfer of USDC is still named and scaled as USDC", async () => { await showTransfer(NETWORKS.mainnet.chainId); expect(state.currentView).toBe("approve-tx"); expect(decodedLine("Token")).toContain('
USDC
'); expect(decodedLine("Amount")).toContain( '
1234.5000 USDC
', ); });