Resolve an ENS name to a typed JSON manifest in one async call.
The off-chain side of the WalrusResolver pattern. An ENS name in, a typed JSON manifest out — one
eth_call to the resolver, one HTTP GET to the aggregator, no IPNS round-trip in between. Generic
over the manifest shape so the same function serves Uniswap token lists, dApp configs, and DAO metadata.
What you'll write
Two modules: src/encoding.ts (pure helpers — bytes32 → base64url, bytes8 →
ASCII) and src/manifest.ts (the on-chain read + aggregator fetch + generic typed wrapper).
Dependency-injected fakes for the RPC + HTTP boundaries keep the suite fully offline.
-
s01 · encoding
Encoding helpers — bytes32 → base64url and bytes8 → ASCII
base64url is NOT base64. The aggregator URL must use
-instead of+,_instead of/, and DROP the=padding.btoa()produces standard base64; getting the substitutions wrong by one character returns 404 silently — the aggregator interprets the wrong slug as an unknown blob, not as a malformed request. -
s02 · on-chain read
The on-chain read — viem PublicClient + ABI
Declaring
WALRUS_RESOLVER_ABI as constis what makes viem type-check the return shape. Withoutas const, the ABI is widened toany[]andreadContractreturnsunknown— the destructuring[blobId, , contentTypeRaw]would silently lose type safety. Theas constis load-bearing TypeScript, not aesthetic style. -
s03 · aggregator fetch
Aggregator GET + content-type gating
The
contentType.startsWith("app/json")check before the fetch is what prevents a misconfigured pointer (say, one that points at a PNG) from being parsed as JSON downstream. Thebytes8on-chain field is intentionally a shortcode grammar ("app/json", "text/md", "image/p"), not a full MIME. The client mirrors the contract's design by accepting the shortcode form only. -
s04 · generic + typed
Generic
resolveManifest+ typed convenience wrapperresolveTokenList = (name, opts) => resolveManifest<UniswapTokenList>(name, opts)— the curried specialization is one line and zero runtime overhead. The TYPE narrows at the call site; the implementation stays generic over any JSON shape. Scales to any number of well-known manifest shapes without forcing every caller to specify<T>inline.
Verification
Two vitest suites, 15 tests total. Run them inside the workspace:
pnpm install
pnpm vitest run
encoding.test.ts (7) covers the base64url substitutions and the null-trim grammar. manifest.test.ts (8) covers the full pipeline against injected fakes — unset pointer error, content-type rejection, non-2xx aggregator error, JSON parse error, and the typed-wrapper convenience.
Sources
Read the reference-app on GitHub, or follow the upstream walkthrough for the production trust-model discussion.
- Reference-app: lessons/05-verifiable-manifest/reference-app/ — vitest workspace with DI seams for RPC + HTTP.
- Lesson sections: lessons/05-verifiable-manifest/sections/ — the four section prompts.
- evm-sui walkthrough: 05-verifiable-manifest — verifiable token-list resolution — the production-shape reference.
- viem: viem.sh — for the
readContract+ ABI inference dance. - @mysten/walrus: @mysten/walrus — for trustless retrieval (recomputes the Walrus blob id over fetched bytes).