// The approval screen for a site's Sepolia token transfer while Ethereum // Mainnet is active (https://git.eeqj.de/sneak/AutistMask/issues/629). // // The screen named the token, and scaled its amount, from the active network's // tracked tokens and balances, so a token the wallet tracks on Sepolia read as // an unknown token with its amount in base units, and the screens after it // labelled the amount `?`. It now uses what the wallet knows of the tokens on // the network the transaction is for. // // Driven through the real approval screen, password and Confirm included, // against a minimal DOM stub in the shape tests/swapStatusAmount.test.js uses. // The vault is mocked so that no password has to be hashed. jest.mock("../src/shared/vault", () => ({ decryptWithPassword: jest.fn(), })); 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 { decryptWithPassword } = require("../src/shared/vault"); const approval = require("../src/popup/views/approval"); const txStatus = require("../src/popup/views/txStatus"); // A well-known test phrase, and its first address. const PHRASE = "test test test test test test test test test test test junk"; const FROM = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; const RECIPIENT = "0x711e691962E5c054b78071C789A0C8CE657377aD"; // A token the bundled token list does not name. const TOKEN = "0x74de5d4FCbf63E00296fd95d33236B9794016631"; // FROM sends 1234.5 of the 6-decimal token 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); } function click(id) { return Promise.all((node(id).listeners.click || []).map((fn) => fn())); } // 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('