// The USD value on the approval screen's Value and Network fee lines, for a // site's ETH send. // // It was worked out from the four-decimal figure the line shows, at the price // on the active network, so the same value read a few cents different here than // on the confirmation screen, which prices the exact amount at the price on the // transaction's own network, and a transaction on another network than the // active one was priced at the active one's rate, its fee too // (https://git.eeqj.de/sneak/AutistMask/issues/614). // // Driven through the real approval screen, against a minimal DOM stub in the // shape tests/approvalTokenNetwork.test.js uses. globalThis.chrome = { storage: { local: { get: async () => ({}), set: async () => {} } }, }; const { state } = require("../src/shared/state"); const { NETWORKS } = require("../src/shared/networks"); const { prices, clearPrices } = require("../src/shared/prices"); const approval = require("../src/popup/views/approval"); const FROM = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; const RECIPIENT = "0x711e691962E5c054b78071C789A0C8CE657377aD"; 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); } // Open the approval screen, with the network `networkId` active, for the // site's transaction sending 1.23414 ETH to RECIPIENT on the chain `chainId`. async function showEthSend(chainId, networkId) { 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: "0x5208", maxPriorityFeePerGas: "0x3b9aca00", maxFeePerGas: "0x77359400", to: RECIPIENT, value: "1234140000000000000", data: "0x", accessList: [], }, }); }, }; state.networkId = networkId; 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" }], }, ]; state.trackedTokens = []; state.networkTrackedTokens = {}; state.currentView = null; approval.init({}); await approval.show(1); } beforeEach(() => { prices.ETH = 2000; }); afterEach(() => { clearPrices(); }); // 1.23414 ETH reads 1.2341 ETH. At $2,000 the exact value is $2,468.28, the // figure the confirmation screen shows for it; the shown figure would be // $2,468.20. test.each([ [ "a mainnet transaction is priced from its exact value", NETWORKS.mainnet.chainId, "mainnet", "1.2341 ETH ($2,468.28)", ], [ "a mainnet transaction shown with Sepolia active is priced at the mainnet price", NETWORKS.mainnet.chainId, "sepolia", "1.2341 ETH ($2,468.28)", ], [ "a Sepolia transaction shown with mainnet active has no USD value", NETWORKS.sepolia.chainId, "mainnet", "1.2341 SepoliaETH", ], // Chain id 1337 names no network here, so nothing knows its price. [ "a transaction on a chain id that names no network has no USD value", "0x539", "mainnet", "1.2341 ETH", ], ])("%s", async (_label, chainId, networkId, expected) => { await showEthSend(chainId, networkId); expect(state.currentView).toBe("approve-tx"); expect(node("approve-tx-value").textContent).toBe(expected); }); // The Network fee line is priced on the transaction's network too. 21000 gas at // up to 2 gwei is 0.000042 ETH, $0.08 at $2,000. test.each([ [ "the fee of a mainnet transaction shown with Sepolia active is priced at the mainnet price", NETWORKS.mainnet.chainId, "sepolia", "0.00004 ETH ($0.08)", ], [ "the fee of a Sepolia transaction shown with mainnet active has no USD value", NETWORKS.sepolia.chainId, "mainnet", "0.00004 SepoliaETH", ], ])("%s", async (_label, chainId, networkId, expected) => { await showEthSend(chainId, networkId); expect(state.currentView).toBe("approve-tx"); expect(node("approve-tx-fee").textContent).toBe(expected); });