// 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, the token's name and scale come from Blockscout's report of // the user's holding, 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( `