// Creating a wallet from a recovery phrase or an extended private key does not // report an address scan request the popup's own closing cancelled, and still // reports one that failed while the popup was open // (https://git.eeqj.de/sneak/AutistMask/issues/475). // // In the popup a cancelled request fails with the same "Failed to fetch" as a // server that cannot be reached, so every RPC request here fails that way, and // only the signal the popup aborts on pagehide tells the two cases apart. // Driven against the fake elements tests/tokenLookupCancelled.test.js uses. const { FetchRequest, HDNodeWallet, Mnemonic } = require("ethers"); const PHRASE = "abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon about"; const PASSWORD = "correct horse battery staple"; let elements; function fakeElement() { return { value: "", textContent: "", style: {}, classList: { toggle: () => {} }, listeners: {}, addEventListener(event, handler) { this.listeners[event] = handler; }, }; } // Stands in for document.getElementById(): one fake element per id. function element(id) { return (elements[id] ||= fakeElement()); } jest.doMock("../src/popup/views/helpers", () => ({ $: element, showView: () => {}, showFlash: () => {}, goBack: () => {}, clearViewStack: () => {}, onViewLeave: () => {}, })); // state.js reads chrome.storage.local at load, and saveState() writes it. globalThis.chrome = { storage: { local: { get: async () => ({}), set: async () => {} } }, }; const { state } = require("../src/shared/state"); const addWallet = require("../src/popup/views/addWallet"); let logged; beforeEach(() => { elements = {}; logged = []; state.wallets = []; state.rpcUrl = "https://rpc.example.invalid"; state.networkId = "mainnet"; for (const method of ["warn", "error"]) { jest.spyOn(console, method).mockImplementation((...args) => { logged.push(args.map(String).join(" ")); }); } // What the scan found is logged at info level, which is not under test. jest.spyOn(console, "log").mockImplementation(() => {}); FetchRequest.registerGetUrl(async () => { throw new TypeError("Failed to fetch"); }); }); afterEach(() => { FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc()); jest.restoreAllMocks(); }); describe.each([ ["a recovery phrase", "mnemonic", "wallet-mnemonic", PHRASE], [ "an extended private key", "xprv", "import-xprv-key", HDNodeWallet.fromSeed(Mnemonic.fromPhrase(PHRASE).computeSeed()) .extendedKey, ], ])("creating a wallet from %s", (_name, mode, field, secret) => { // Enters `secret` on the add-wallet screen, presses its button and waits // for the address scan that follows. async function create(pageClosed) { addWallet.init({ renderWalletList: () => {}, doRefreshAndRender: () => {}, pageClosed, }); element("tab-" + mode).listeners.click(); element(field).value = secret; element("add-wallet-password").value = PASSWORD; element("add-wallet-password-confirm").value = PASSWORD; await element("btn-add-wallet-confirm").listeners.click(); } test("a scan failure while the popup is open is reported", async () => { await create(new AbortController().signal); expect(state.wallets).toHaveLength(1); expect(logged).not.toEqual([]); for (const line of logged) { expect(line).toContain("scanForAddresses check failed"); } }); test("a scan failure once the popup has closed is not", async () => { const pageClosed = new AbortController(); pageClosed.abort(); await create(pageClosed.signal); expect(state.wallets).toHaveLength(1); expect(logged).toEqual([]); }); });