// The Send screen refuses every amount the confirmation screen would refuse, // by the same check (https://git.eeqj.de/sneak/AutistMask/issues/532). // // Send used to accept anything parseFloat() read as above zero, so "1e-3", // "1abc" and an amount with more decimal places than the token has went on to // a confirmation screen that then refused them. Both screens now call // isValidAmount() in src/shared/txValidation.js. // // Driven through the real Send review handler and the real confirmation // screen, with only the node and the DOM stubbed. "use strict"; jest.mock("../src/shared/log", () => ({ log: { debugf: () => {}, infof: () => {}, warnf: () => {}, errorf: () => {}, }, debugFetch: jest.fn(), setRuntimeDebug: () => {}, isDebug: () => false, })); // What the confirmation screen asks the node for. const mockProvider = { getFeeData: async () => ({ maxFeePerGas: 2000000000n, gasPrice: 1000000000n, }), estimateGas: async () => 21000n, getCode: async () => "0x", getTransactionCount: async () => 1, getBalance: async () => 0n, }; jest.mock("../src/shared/balances", () => { const actual = jest.requireActual("../src/shared/balances"); return { ...actual, getProvider: () => mockProvider }; }); // The confirmation screen's Etherscan label lookup fails, as it does offline. global.fetch = jest.fn(() => { throw new Error("tests must not perform network requests"); }); const { makeStorageStub } = require("./support/storageStub"); global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } }; // A stub DOM: every id resolves to a recording element. const elements = new Map(); function makeEl(id) { const handlers = new Map(); return { id, textContent: "", innerHTML: "", value: "", disabled: false, style: {}, dataset: {}, classList: { add() {}, remove() {}, toggle() {}, contains: () => false, }, handlers, addEventListener(name, fn) { handlers.set(name, fn); }, appendChild(child) { return child; }, querySelectorAll: () => [], querySelector: () => null, remove() {}, focus() {}, }; } global.document = { getElementById(id) { if (!elements.has(id)) elements.set(id, makeEl(id)); return elements.get(id); }, createElement: (tag) => makeEl(tag), body: { prepend() {}, appendChild() {} }, addEventListener() {}, }; global.navigator = { clipboard: { writeText() {} } }; const { state } = require("../src/shared/state"); const send = require("../src/popup/views/send"); const confirmTx = require("../src/popup/views/confirmTx"); const HOLDER = "0x" + "a".repeat(40); const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe"; // A token with 6 decimal places, as USDC has. const TOKEN = "0x" + "d".repeat(40); const TOKEN_BY_SYMBOL = { ETH: "ETH", TOK: TOKEN }; function el(id) { return global.document.getElementById(id); } // Press Review on the Send screen with `amount` entered, for a send of `token` // ("ETH" or TOKEN), and return what it handed the confirmation screen, or null // when it handed nothing over. async function review(amount, token) { let handed = null; send.init({ showConfirmTx: (info) => (handed = info) }); state.selectedToken = token; el("send-to").value = RECIPIENT; el("send-amount").value = amount; await el("btn-send-review").handlers.get("click")(); return handed; } // Show the confirmation screen for the send Review hands over for 0.5, with // `amount` in its place, and let its fee estimate settle. async function confirm(amount, token) { const txInfo = await review("0.5", token); confirmTx.show({ ...txInfo, amount }); for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0)); } beforeEach(() => { elements.clear(); state.wallets = [ { name: "Wallet 1", addresses: [ { address: HOLDER, balance: "1.0", tokenBalances: [ { address: TOKEN, name: "Token", symbol: "TOK", decimals: 6, balance: "250.0", holders: 50000, }, ], }, ], }, ]; state.selectedWallet = 0; state.selectedAddress = 0; state.trackedTokens = [ { address: TOKEN, symbol: "TOK", name: "Token", decimals: 6 }, ]; state.fraudContracts = []; state.selectedToken = null; state.currentView = null; }); describe.each([ ["1e-3", "ETH"], ["1e-3", "TOK"], ["1abc", "ETH"], // 19 decimal places; ETH has 18. ["0.0000000000000000001", "ETH"], // 7 decimal places; the token has 6. ["0.0000001", "TOK"], ["0", "ETH"], ["-1", "ETH"], ])("%s %s", (amount, symbol) => { const token = TOKEN_BY_SYMBOL[symbol]; test("Send refuses it with a flash message and opens nothing", async () => { expect(await review(amount, token)).toBe(null); expect(el("flash-msg").textContent).toBe( "Please enter a valid amount.", ); }); test("the confirmation screen refuses it too", async () => { await confirm(amount, token); expect(el("confirm-errors").innerHTML).toContain( "Please enter a valid amount to send.", ); expect(el("btn-confirm-send").disabled).toBe(true); }); }); describe.each([ ["0.5", "ETH"], ["0.5", "TOK"], // Exactly the 6 decimal places the token has. ["0.000001", "TOK"], // Only zeros past the token's 6 places, and past ETH's 18. ["1.0000000", "TOK"], ["0.5000000000000000000", "ETH"], ])("%s %s", (amount, symbol) => { const token = TOKEN_BY_SYMBOL[symbol]; test("Send hands it to the confirmation screen", async () => { expect((await review(amount, token)).amount).toBe(amount); expect(el("flash-msg").textContent).toBe(""); }); test("the confirmation screen lets it be sent", async () => { await confirm(amount, token); expect(el("confirm-errors").innerHTML).toBe(""); expect(el("btn-confirm-send").disabled).toBe(false); }); });