// What reaches the console when the RPC endpoint answers with an HTTP error. // // RPC providers put the API key in the endpoint URL's path or query string. // When the endpoint answers with an HTTP error (a wrong or expired key, a rate // limit, a server error), the error ethers throws carries the full request URL // in its message, so a line logging that message printed the key // (https://git.eeqj.de/sneak/AutistMask/issues/410). Those lines log the // error's short message, which names the HTTP status and not the URL. // // The real ethers provider runs; only its HTTP transport is replaced, by one // that answers every request with 401 Unauthorized. Debug mode is on, so // every log level is printed. const { FetchRequest } = require("ethers"); const { getProvider, lookupTokenInfo, refreshBalances, } = require("../src/shared/balances"); const { getFullWarnings } = require("../src/shared/addressWarnings"); const { resolveEnsName } = require("../src/shared/ens"); const { setRuntimeDebug } = require("../src/shared/log"); const RPC_URL = "https://rpc.example.invalid/v3/PATHKEY123?token=QUERYTOKEN456"; const ADDRESS = "0x1111111111111111111111111111111111111111"; const realFetch = globalThis.fetch; let printed; beforeEach(() => { setRuntimeDebug(true); printed = []; for (const method of ["log", "warn", "error"]) { jest.spyOn(console, method).mockImplementation((...args) => { printed.push(args.map(String).join(" ")); }); } FetchRequest.registerGetUrl(async () => ({ statusCode: 401, statusMessage: "Unauthorized", headers: {}, body: new Uint8Array(), })); // The explorer requests the balance refresh makes go nowhere. globalThis.fetch = jest.fn(async () => { throw new Error("tests must not perform network requests"); }); }); afterEach(() => { FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc()); globalThis.fetch = realFetch; setRuntimeDebug(false); jest.restoreAllMocks(); }); // The line carrying `label` was printed and names the HTTP status, and nothing // printed carries the key. function expectFailureLoggedWithoutKey(label) { const line = printed.find((text) => text.includes(label)); expect(line).toContain("401"); const all = printed.join("\n"); expect(all).not.toContain("PATHKEY123"); expect(all).not.toContain("QUERYTOKEN456"); } test("ethers puts the URL in the error message, not in the short message", async () => { const provider = getProvider(RPC_URL, "mainnet"); const error = await provider.getCode(ADDRESS).catch((e) => e); expect(error.message).toContain("PATHKEY123"); expect(error.shortMessage).not.toContain("PATHKEY123"); }); test("the recipient checks before a send", async () => { await getFullWarnings(ADDRESS, getProvider(RPC_URL, "mainnet")); expectFailureLoggedWithoutKey("contract check failed"); expectFailureLoggedWithoutKey("tx count check failed"); }); test("the ENS reverse lookup", async () => { expect(await resolveEnsName(ADDRESS, RPC_URL, "mainnet")).toBeNull(); expectFailureLoggedWithoutKey("ENS reverse lookup failed"); }); test("the balance refresh", async () => { const wallets = [{ addresses: [{ address: ADDRESS }] }]; await refreshBalances( wallets, RPC_URL, "https://explorer.example.invalid/api/v2", [], "mainnet", ); expectFailureLoggedWithoutKey("ETH balance failed"); expectFailureLoggedWithoutKey("ENS reverse failed"); }); // The lookup's first line, at debug level, names the RPC endpoint; the check // of everything printed covers it too. test("the token lookup", async () => { await expect(lookupTokenInfo(ADDRESS, RPC_URL, "mainnet")).rejects.toThrow( "Not a valid ERC-20 token", ); expectFailureLoggedWithoutKey("symbol() failed:"); });