Files
AutistMask/src/shared/ens.js
T
clawbot bd0a626e7b
check / check (push) Successful in 33s
e2e / e2e-chrome (push) Successful in 1m45s
e2e / e2e-firefox (push) Successful in 31s
harden: stop the background reading the shared state singleton, and enforce it at build time (closes #324)
Five defects, one of which destroyed every wallet, came from src/background reading and writing the module-level state singleton the MV3 worker never populates, which silently served DEFAULT_STATE. Each point fix created the next defect. The background now has its own per-call getState() and a queued read-modify-write updateState(); the singleton is unreachable from it, and an unpopulated read throws instead of serving defaults.

The prohibition is enforced by the build, not by review: build.js asserts over esbuild's own metafile that no forbidden module is an input of a background bundle, so every specifier syntax esbuild resolves is covered, and both halves of the table are checked for rot -- a stale key, a stale module, an empty list, or an unlisted entry point under src/background/ all fail the build. The ESLint rule remains as fast local feedback and reads the same shared table. Known bounds are documented where the table lives.

Also closes #320: getProvider() now requires a validated network id, so a cold worker no longer prepares a non-mainnet dApp transaction for mainnet and gets refused by the wallet's own verifier. backgroundRefresh() no longer mutates address objects across a network round trip, the broadcast path takes its endpoint and chain id from one snapshot, and eight test storage stubs now structured-clone on get as the real chrome.storage.local does.

closes #320
2026-08-23 17:57:30 +02:00

62 lines
1.9 KiB
JavaScript

// Cached ENS reverse resolution.
// Resolves addresses to ENS names via ethers provider.lookupAddress(),
// caching results in localStorage with a 12-hour TTL.
//
// POPUP ONLY. localStorage does not exist in the Chrome MV3 service worker,
// so this module must not be pulled into src/background/. Anything the
// background context needs to cache goes in extension storage instead.
const { getProvider } = require("./balances");
const { log } = require("./log");
const CACHE_TTL_MS = 43200000; // 12 hours
const CACHE_PREFIX = "ens:";
function getCached(address) {
const key = CACHE_PREFIX + address.toLowerCase();
try {
const raw = localStorage.getItem(key);
if (!raw) return undefined;
const entry = JSON.parse(raw);
if (Date.now() - entry.ts < CACHE_TTL_MS) {
return entry.name;
}
} catch {
// Corrupt cache entry — treat as miss.
}
return undefined;
}
function setCache(address, name) {
const key = CACHE_PREFIX + address.toLowerCase();
localStorage.setItem(key, JSON.stringify({ name, ts: Date.now() }));
}
async function resolveEnsName(address, rpcUrl, networkId) {
const cached = getCached(address);
if (cached !== undefined) return cached;
const provider = getProvider(rpcUrl, networkId);
try {
const name = (await provider.lookupAddress(address)) || null;
setCache(address, name);
return name;
} catch (e) {
log.errorf("ENS reverse lookup failed", address, e.message);
// Don't cache failures — let subsequent lookups retry
return null;
}
}
async function resolveEnsNames(addresses, rpcUrl, networkId) {
const results = new Map();
await Promise.all(
addresses.map(async (addr) => {
results.set(addr, await resolveEnsName(addr, rpcUrl, networkId));
}),
);
return results;
}
module.exports = { resolveEnsName, resolveEnsNames };