Out of the box the wallet sent the user's addresses to Blockscout and each recipient's address to Etherscan. Two settings, "Use Blockscout" and "Check recipients on Etherscan", now gate every request that sends the user's data to those services. Both start off and are offered unticked on Welcome and in Settings. Each request reads its setting from storage before it asks. An answer that lands after the setting went off is dropped. While a setting is off, the screens that rely on it say so. ENS lookups ask only the RPC endpoint, so a name resolved off-chain does not resolve. RULES.md's External Communication item is now out of date. Its new wording will be proposed on #211 once this lands. Model: opus-5-5 (implementation, summary); fable-5-1 (prose)
115 lines
4.4 KiB
JavaScript
115 lines
4.4 KiB
JavaScript
// 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 that address unless the provider has `disableCcipRead` set.
|
|
// That request would carry the name to a server other than the RPC endpoint
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/668). With the flag set, such a
|
|
// name does not resolve.
|
|
//
|
|
// The test builds its provider with getProvider() and runs it against a fake
|
|
// JSON-RPC node. The fake node answers through ethers' own request hook. The
|
|
// hook 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()
|
|
// - it gives NAME as the reverse record of any address
|
|
// - it answers resolve() with the OffchainLookup error, which 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 });
|
|
});
|