← Contract Hero · WorkLearnPlugins
acc-evm-wal
Lesson 05 · TypeScript

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.

Slug: 05-verifiable-manifest Stack: TS · viem · vitest Sections: 4 Final gate: pnpm vitest run · 15 tests

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 const is what makes viem type-check the return shape. Without as const, the ABI is widened to any[] and readContract returns unknown — the destructuring [blobId, , contentTypeRaw] would silently lose type safety. The as const is 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. The bytes8 on-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 wrapper

    resolveTokenList = (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.