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 for history
and token lookups, and each recipient to Etherscan for its label. Two settings,
"Use Blockscout" and "Check recipients on Etherscan", now gate those requests
where they are made. Both are off in a new profile and offered unticked on
Welcome and in Settings. While one is off, the screens that relied on it say so
in a plain line naming the setting; after either changes in Settings, Back
draws the screen it lands on again. ENS lookups ask only the RPC endpoint, so a
name resolved off-chain does not resolve. README and the user guide say what is
contacted by default and what each setting adds.

Model: opus-5-5
This commit is contained in:
2026-10-21 16:11:02 +00:00
parent c316d9ee78
commit d7f0e241ff
50 changed files with 1476 additions and 143 deletions
+111
View File
@@ -0,0 +1,111 @@
// ENS lookups ask nothing but the RPC endpoint. A resolver can answer that a
// name is resolved off-chain, naming a web address to fetch the answer from;
// ethers fetches it unless the provider has `disableCcipRead` set, and that
// request would carry the name to a server other than the RPC endpoint
// (https://git.eeqj.de/sneak/AutistMask/issues/668). With it set, such a name
// does not resolve.
//
// The provider getProvider() builds runs against a fake JSON-RPC node,
// answered through ethers' own request hook, which also records the address of
// every request ethers makes, the off-chain fetch included.
const { FetchRequest, Interface } = require("ethers");
const { getProvider } = require("../src/shared/balances");
const RPC_URL = "https://rpc.example.invalid";
const RESOLVER = "0x5555555555555555555555555555555555555555";
const USER = "0x1111111111111111111111111111111111111111";
const NAME = "offchain.eth";
// The ENS registry's resolver(), and the resolver's functions ethers calls: the
// registry names RESOLVER for every name, the resolver says it resolves names
// through resolve(), gives NAME as the reverse record of any address, and
// answers resolve() with the OffchainLookup error that says the name is
// resolved off-chain.
const ens = new Interface([
"function resolver(bytes32) view returns (address)",
"function supportsInterface(bytes4) view returns (bool)",
"function name(bytes32) view returns (string)",
"function resolve(bytes, bytes) view returns (bytes)",
"error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData)",
]);
let asked;
// One JSON-RPC answer. Anything but a call to one of the functions above is
// refused.
function reply(req) {
const envelope = { jsonrpc: "2.0", id: req.id };
const call =
req.method === "eth_call" ? ens.parseTransaction(req.params[0]) : null;
if (call === null) {
return {
...envelope,
error: { code: -32601, message: "method not found" },
};
}
if (call.name === "resolve") {
const offchain = ens.encodeErrorResult("OffchainLookup", [
RESOLVER,
["https://gateway.example.invalid/{sender}/{data}.json"],
"0x1234",
"0x12345678",
"0x",
]);
return {
...envelope,
error: { code: 3, message: "execution reverted", data: offchain },
};
}
const answers = {
resolver: [RESOLVER],
supportsInterface: [true],
name: [NAME],
};
return {
...envelope,
result: ens.encodeFunctionResult(call.name, answers[call.name]),
};
}
beforeEach(() => {
asked = [];
FetchRequest.registerGetUrl(async (req) => {
asked.push(req.url);
// Any other address is the off-chain fetch, answered as not found.
if (req.url !== RPC_URL) {
return {
statusCode: 404,
statusMessage: "Not Found",
headers: {},
body: null,
};
}
const body = JSON.parse(Buffer.from(req.body).toString("utf8"));
const answer = Array.isArray(body) ? body.map(reply) : reply(body);
return {
statusCode: 200,
statusMessage: "OK",
headers: { "content-type": "application/json" },
body: Buffer.from(JSON.stringify(answer)),
};
});
});
afterEach(() => {
FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc());
});
test("a name typed into Send that is resolved off-chain does not resolve, and only the RPC endpoint is asked", async () => {
const provider = getProvider(RPC_URL, "mainnet");
const [outcome] = await Promise.allSettled([provider.resolveName(NAME)]);
expect(asked.filter((url) => url !== RPC_URL)).toEqual([]);
expect(outcome).toEqual({ status: "fulfilled", value: null });
});
test("the reverse lookup of a user's address whose name is resolved off-chain finds no name, and only the RPC endpoint is asked", async () => {
const provider = getProvider(RPC_URL, "mainnet");
const [outcome] = await Promise.allSettled([provider.lookupAddress(USER)]);
expect(asked.filter((url) => url !== RPC_URL)).toEqual([]);
expect(outcome).toEqual({ status: "fulfilled", value: null });
});