An 80-line TypeScript client that resolves an ENS name (e.g. tokens.uniswap.eth) to a Walrus blob via
the WalrusResolver contract from showcase 03, then fetches the manifest
body from any Walrus aggregator by its content-addressed blob id. The pointer lookup is deterministic; the
bytes are content-addressed; opt-in client-side hash verification is one Walrus SDK call away.
"ipfs.io 85–90% of the time, it resolves … sometimes takes 5 min" — and Uniswap's interface release notes broke because public gateways flake. Token lists and dApp manifests are exactly the small, high-read, cache-friendly artifact that public-gateway flakiness ruins.— 2026 builder-sentiment survey
The trust boundary is the aggregator — same model as fetching by CID from an IPFS gateway. The leverage over IPFS is that the pointer lookup is now an EVM read, not an IPNS lottery.
The client validates that the on-chain contentType shortcode starts with app/json
before parsing, so non-JSON pointers fail loud instead of silently corrupting the schema.
For trustless retrieval, re-encode the returned bytes with @mysten/walrus and assert the hash
matches the on-chain blobId. The 80-line client doesn't do this by default — it's the showcase's job to
demonstrate the pointer swap; verification is one composition away.
// the entire public surface export interface ResolvedManifest<T> { manifest: T; blobId: `0x${string}`; contentType: string; } export async function resolveManifest<T>( ensName: string, opts: ResolveOpts, ): Promise<ResolvedManifest<T>>; // + a typed wrapper for the Uniswap TokenList shape export const resolveTokenList = (ens, opts) => resolveManifest<UniswapTokenList>(ens, opts);
tokens.uniswap.eth (or your own ENS-pointed name).This client depends on the WalrusResolver from showcase 03. Either:
setWalrusBlob (content-type app/json), orsetWalrusBlob with the blobId of a JSON manifest you've already uploaded to Walrus.cd showcases/05-verifiable-manifest pnpm install
| Env | Required? | Purpose |
|---|---|---|
| EVM_RPC_URL | yes | Mainnet (or wherever your WalrusResolver lives + the ENS registry it points at). |
| RESOLVER_ADDRESS | yes | Deployed WalrusResolver address. |
| ENS_NAME | default tokens.uniswap.eth | The ENS name whose subdomain holds the manifest pointer. |
| AGGREGATOR | default testnet | Walrus aggregator URL (any will do — same blobId, same bytes). |
pnpm tsx example-usage.ts # → Resolved tokens.uniswap.eth → blobId=0x... (app/json) # → Token list "Uniswap Default" contains 423 tokens. # → - USDC 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 # → - DAI 0x6b175474e89094c44da98b954eedeac495271d0f # → - ...
Or use it from your own code:
import { resolveTokenList } from "walrus-verifiable-manifest"; const { manifest, blobId, contentType } = await resolveTokenList("tokens.uniswap.eth", { rpcUrl: EVM_RPC_URL, resolverAddress: RESOLVER_ADDRESS, aggregator: "https://aggregator.walrus-testnet.walrus.space", });
If your threat model requires the bytes to be trustless against the aggregator, run the returned manifest through @mysten/walrus's Reed-Solomon encoder and assert the recomputed blob id matches the on-chain one. The 80-line client deliberately keeps this opt-in to stay minimal.
blobId and content-type shortcode (e.g. app/json).AGGREGATOR=https://aggregator.walrus-testnet.walrus.space → some other operator) returns the same JSON byte-for-byte.manifest: pointer unset for <ens> — no silent empty manifest.@mysten/walrus.EVM_RPC_URL — it doesn't proxy across chains. Run one resolver per chain you care about, or wrap with a thin multi-chain dispatcher.app/json. The contentType shortcode is checked at the start. Other content types are out of grammar for this client — fork it if you want PNG-pointer or markdown-pointer manifests.resolveManifest<T> generic, resolveTokenList Uniswap shape, hex↔base64url helpers.
05-verifiable-manifest/example-usage.tsMinimal CLI demonstrating the import + first-five-tokens print.
contracts/src/WalrusResolver.solThe on-chain pointer this client reads from (see showcase 03 for the walkthrough).