fix: Blockscout and Etherscan lookups are off until the user turns them on (closes #668)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

Out of the box the wallet sent the user's addresses to Blockscout and each
recipient to Etherscan. Two settings, "Use Blockscout" and "Check recipients on
Etherscan", now gate those requests. Both are off in a new install and offered
unticked on Welcome and in Settings. While one is off, the screens that relied
on it say so in a plain line. Each gated request reads its setting from storage
before it asks and when the answer lands, and drops a late answer. An open popup
takes another page's change and draws its screens again. While "Use Blockscout"
is off, no address keeps an untracked token, and no screen names a token from
Blockscout's report. ENS lookups ask only the RPC endpoint.

Model: opus-5-5
This commit is contained in:
2026-10-26 08:58:03 +00:00
parent c316d9ee78
commit e66aac60e2
62 changed files with 2649 additions and 231 deletions
+262
View File
@@ -0,0 +1,262 @@
// While "Use Blockscout" is off, the approval screens and the screens after
// TxApproval name a token, and scale its amount, from the bundled list and the
// tracked tokens alone (https://git.eeqj.de/sneak/AutistMask/issues/668). A
// token they lack gets a line saying it was not looked up on Blockscout. While
// the setting is on, what Blockscout reported for a token the user holds names
// and scales it, as before.
//
// Booted through the real popup entry point, as the approval window or as a
// popup reopened onto SuccessTx. 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 =
'This token was not looked up on Blockscout, 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("says a token it cannot name was not looked up on Blockscout 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(
`<div class="text-xs text-muted">${LINE}</div>`,
);
},
);
});
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("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);
});