// While "Use Blockscout" is off, the approval screens, and the screens that // follow TxApproval, take a token's name and scale from the bundled list and // the tracked tokens alone (https://git.eeqj.de/sneak/AutistMask/issues/668). // A token missing from both gets a line under it. The line says Blockscout's // report of the user's balances is not used to name the token. While the // setting is on, Blockscout's report of the user's holding is a source too, as // before. // // Booted through the real popup entry point, as the approval window or as a // popup reopened onto SuccessTx, WaitTx or ErrorTx. The vault is mocked so that // no password has to be hashed: the stored secret decrypts to the test key // below. jest.mock("../src/shared/vault", () => ({ decryptWithPassword: jest.fn(), encryptWithPassword: jest.fn(), })); const { Interface, Wallet } = require("ethers"); const { bootPopup, cleanupPopup, unversionedValidProfile, } = require("./support/popupBoot"); // A published test key, never used for real funds. const SIGNER_KEY = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; const signer = new Wallet(SIGNER_KEY); const ORIGIN = "https://dapp.example"; const RECIPIENT = "0x2222222222222222222222222222222222222222"; const TX_HASH = "0x" + "ab".repeat(32); // Outside the bundled list, and not tracked. const TOKEN = "0xE2E0000000000000000000000000000000000E2e"; // In the bundled list, at 6 decimals. const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; const LINE = 'Blockscout\'s report of your balances is not used to name this token, because "Use Blockscout" is off in Settings.'; // TOKEN as Blockscout reports the user's holding of it. const TOKEN_ROW = { address: TOKEN, name: "Novel", symbol: "NOVEL", decimals: "6", balance: "5000.0", holders: 5000, }; // A transfer of 5,000 of a 6-decimal token to RECIPIENT. const TRANSFER_DATA = new Interface([ "function transfer(address to, uint256 amount)", ]).encodeFunctionData("transfer", [RECIPIENT, 5000000000n]); // An EIP-2612 permit letting RECIPIENT take 5,000 of TOKEN. const PERMIT = { types: { EIP712Domain: [ { name: "name", type: "string" }, { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" }, ], Permit: [ { name: "owner", type: "address" }, { name: "spender", type: "address" }, { name: "value", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" }, ], }, primaryType: "Permit", domain: { name: "Novel", chainId: 1, verifyingContract: TOKEN }, message: { owner: signer.address, spender: RECIPIENT, value: "5000000000", nonce: "0", deadline: "1790000000", }, }; // The stored profile: the user's one address holds `tokenBalances`. function profile(useBlockscout, tokenBalances, extra) { return unversionedValidProfile({ useBlockscout, wallets: [ { type: "key", name: "Wallet 1", encryptedSecret: "encrypted-key", addresses: [ { address: signer.address, balance: "1.0", tokenBalances, }, ], }, ], activeAddress: signer.address, allowedSites: { [signer.address]: [ORIGIN] }, ...extra, }); } // The approval window for the site's request: a transfer sent to `token`, or // with `signParams`, a signature. The background answers the signed // transaction with `txResponse`. function bootApproval(stored, { token = TOKEN, signParams, txResponse } = {}) { return bootPopup(stored, { search: "?approval=approval-1", runtime: { connect: () => ({ postMessage: () => {} }), sendMessage: (msg, reply) => { if (msg.type === "AUTISTMASK_GET_APPROVAL") { reply({ type: signParams ? "sign" : "tx", origin: ORIGIN, isPhishingDomain: false, approvedFrom: signer.address, signParams, approvedTx: { type: 2, from: signer.address, chainId: "0x1", nonce: "0x7", gasLimit: "0x30d40", maxPriorityFeePerGas: "0x3b9aca00", maxFeePerGas: "0x77359400", to: token, value: "0x0", data: TRANSFER_DATA, accessList: [], }, }); } else if (msg.type === "AUTISTMASK_TX_RESPONSE") { reply(txResponse); } else { reply(undefined); } }, }, }); } beforeEach(() => { // The page logs its failed RPC reads; the stub node in popupBoot.js // answers none. jest.spyOn(console, "error").mockImplementation(() => {}); }); afterEach(() => { cleanupPopup(); jest.restoreAllMocks(); }); describe("TxApproval", () => { test("has the line under a token it cannot name while Use Blockscout is off", async () => { const page = await bootApproval(profile(false, [])); const details = page.node("approve-tx-decoded-details").innerHTML; expect(details).toContain("Unknown token"); expect(details).toContain(LINE); expect(details).toContain("5000000000 base units (decimals unknown)"); cleanupPopup(); // The bundled list names USDC, so there is nothing to say. const bundled = await bootApproval(profile(false, []), { token: USDC, }); expect( bundled.node("approve-tx-decoded-details").innerHTML, ).not.toContain(LINE); cleanupPopup(); const on = await bootApproval(profile(true, [TOKEN_ROW])); const named = on.node("approve-tx-decoded-details").innerHTML; expect(named).toContain("5000.0000 NOVEL"); expect(named).not.toContain(LINE); }); test("names nothing from a Blockscout entry still in the balances while Use Blockscout is off", async () => { const page = await bootApproval(profile(false, [TOKEN_ROW])); const details = page.node("approve-tx-decoded-details").innerHTML; expect(details).not.toContain("NOVEL"); expect(details).toContain("Unknown token"); expect(details).toContain(LINE); }); test.each([ ["WaitTx", { txHash: TX_HASH }, "wait-tx"], [ "ErrorTx", { error: "The transaction could not be sent.", retryable: false }, "error-tx", ], ])( "%s after it says so under the amount while Use Blockscout is off", async (_name, txResponse, view) => { const page = await bootApproval(profile(false, []), { txResponse, }); const { decryptWithPassword } = require("../src/shared/vault"); decryptWithPassword.mockResolvedValue(SIGNER_KEY); page.node("approve-tx-password").value = "any password"; await page.click("btn-approve-tx"); expect(page.visibleViews()).toEqual([view]); expect(page.node(view + "-token-note").innerHTML).toBe( `
${LINE}
`, ); }, ); }); test("SuccessTx says so under the decoded token while Use Blockscout is off", async () => { const page = await bootPopup( profile(false, [], { currentView: "success-tx", viewStack: ["main"], viewData: { amount: "5000000000 base units (decimals unknown)", symbol: "?", to: RECIPIENT, hash: TX_HASH, blockNumber: 21000000, chainId: "0x1", decoded: { name: "Token Transfer", description: "Transfer ERC-20 token", details: [ { label: "Token", value: TOKEN, address: TOKEN, isToken: true, }, ], }, }, }), ); expect(page.visibleViews()).toEqual(["success-tx"]); const decoded = page.node("success-tx-decoded").innerHTML; expect(decoded).toContain("Unknown token"); expect(decoded).toContain(LINE); }); test.each([ [ "wait-tx", { pendingWait: { hash: TX_HASH, txInfo: { to: RECIPIENT, amount: "1", token: 42, chainId: "0x1", }, broadcastTime: Date.now(), }, }, ], [ "error-tx", { amount: "1", symbol: "?", token: 42, to: RECIPIENT, hash: TX_HASH, message: "The transaction failed.", chainId: "0x1", }, ], ])( "a popup reopened onto %s shows no line for a stored token that is not text", async (view, viewData) => { const page = await bootPopup( profile(false, [], { currentView: view, viewStack: ["main"], viewData, }), ); expect(page.pageErrors).toEqual([]); expect(page.visibleViews()).toEqual([view]); expect(page.node(view + "-token-note").innerHTML).toBe(""); }, ); test("SignApproval says so under a permit's token while Use Blockscout is off", async () => { const signParams = { method: "eth_signTypedData_v4", typedData: JSON.stringify(PERMIT), from: signer.address, }; const page = await bootApproval(profile(false, []), { signParams }); const message = page.node("approve-sign-message").innerHTML; expect(message).toContain("Unknown token"); expect(message).toContain(LINE); cleanupPopup(); const on = await bootApproval(profile(true, [TOKEN_ROW]), { signParams }); const named = on.node("approve-sign-message").innerHTML; expect(named).toContain("5000.0000 NOVEL"); expect(named).not.toContain(LINE); });