A compact Solidity registry maps an ENS name to a Walrus blob pointer (blobId + Sui object id + content-type
shortcode). Updates are authorized by the ENS registry's owner or an ENSIP-1 operator. A small
TypeScript keeper extends the underlying Sui Blob object's end_epoch on a cron so the bytes
survive indefinitely — and anyone can run a keeper for any name they're willing to pay WAL for.
"Pinata is now openly pitching its smart-contract-based IPCM (IPFS CID Mapping) as a replacement for IPNS — a remarkable position from inside the IPFS commercial ecosystem."— Pinata blog, Jan 2025
eth_call, one aggregator GET, no DHT walk.
setWalrusBlob(node, blobId, suiObjectId, contentType) is gated on
ensRegistry.owner(node) == msg.sender || isApprovedForAll(owner, msg.sender), so ENSIP-1
operator approvals work out of the box. Three storage slots per name, one SSTORE-set per update.
walrusBlob(node) is a single eth_call. The reader follows the returned
blobId to <aggregator>/v1/blobs/<blobId> for the bytes. The
suiObjectId in the record is what a keeper extends to keep the blob alive forever.
The leverage over IPFS is not the trust model — it's that the pointer resolves deterministically in < 200ms instead of a DHT random walk.
// contract surface function setWalrusBlob( bytes32 node, bytes32 blobId, bytes32 suiObjectId, bytes8 contentType // e.g. "app/json" ) external; function walrusBlob(bytes32 node) external view returns ( bytes32 blobId, bytes32 suiObjectId, bytes8 contentType ); event WalrusBlobChanged( bytes32 indexed node, bytes32 blobId, bytes32 suiObjectId, bytes8 contentType, uint64 at );
vitalik.eth's resolver at WalrusResolver would lose every existing address /
text / contenthash record on that name. Dedicate a subdomain such as blog.vitalik.eth to
Walrus content and leave the apex name on its existing PublicResolver.
cd showcases/contracts forge install --no-git foundry-rs/forge-std openzeppelin/openzeppelin-contracts forge build forge test --match-contract WalrusResolver -vv # 8 tests, all pass
# Sepolia / mainnet / any L2 with an ENS-shaped registry forge create src/WalrusResolver.sol:WalrusResolver \ --rpc-url $EVM_RPC_URL \ --private-key $DEPLOYER_PRIVATE_KEY \ --constructor-args $ENS_REGISTRY # mainnet ENS registry: 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e
Via the ENS Manager on a name you own: create blog.vitalik.eth as a subdomain, then set its resolver to your WalrusResolver deployment address.
Programmatic: call ENSRegistry.setSubnodeOwner followed by ENSRegistry.setResolver.
# upload via @mysten/walrus (backend mode) — you must own the resulting Blob object # to use the keeper. The publisher route works but the publisher owns the Blob, not you. # once you have (blobId, suiObjectId), call: cast send $RESOLVER_ADDRESS "setWalrusBlob(bytes32,bytes32,bytes32,bytes8)" \ $(cast namehash blog.vitalik.eth) \ $BLOB_ID \ $SUI_OBJECT_ID \ 0x746578742f6d6400 \ # bytes8 = ASCII "text/md" + 1 null pad (16 hex chars) --rpc-url $EVM_RPC_URL \ --private-key $OWNER_PRIVATE_KEY
cd showcases/03-walrus-resolver/keeper pnpm install pnpm keeper
The keeper, for every name in KEEPER_NAMES, reads the on-chain pointer, fetches the linked Sui Blob's end_epoch, and calls executeExtendBlobTransaction when the remaining window drops below EXTENSION_THRESHOLD. Per-name failures are caught locally so one bad name does not abort the cron tick.
| Env | Required? | Purpose |
|---|---|---|
| EVM_RPC_URL | yes | JSON-RPC for the chain holding WalrusResolver. |
| EVM_CHAIN_ID | default 1 | EVM chain id (1 = mainnet, 11155111 = Sepolia, etc.). |
| RESOLVER_ADDRESS | yes | Deployed WalrusResolver address. |
| SUI_RPC_URL | yes | Sui fullnode (e.g. https://fullnode.testnet.sui.io:443). |
| WALRUS_NETWORK | default testnet | testnet or mainnet. |
| SUI_PRIVATE_KEY | yes | bech32 keypair holding WAL to fund extensions. |
| KEEPER_NAMES | yes | Comma-separated ENS names to keep alive. |
| EXTENSION_THRESHOLD | default 5 | Refresh when remaining epochs < this. |
| EXTEND_BY_EPOCHS | default 50 | Epochs to add per extension. |
cast call $RESOLVER_ADDRESS "walrusBlob(bytes32)" $(cast namehash blog.vitalik.eth) returns the blobId + suiObjectId + contentType you just set.$AGGREGATOR/v1/blobs/$blobId returns the original bytes — the content type matches the on-chain shortcode.[ok] <name>: N epochs left, no action; after enough time it prints [extend] <name>: N epochs left → +50 and a Walruscan check on the Sui Blob object shows the new end_epoch.IResolver implementation. Only walrusBlob is exposed — no addr, no text, no contenthash. Consumers should staticcall walrusBlob(bytes32) directly, not via ENSIP resolver-record interface ids.setWalrusBlob / clearWalrusBlob / walrusBlob / WalrusBlobChanged event.
contracts/test/WalrusResolver.t.sol8 Foundry tests covering ownership, operator approval, ENS-owner-transfer, ERC-165.
03-walrus-resolver/keeper/keeper.ts~150 lines; per-name extension loop with per-name error isolation.
showcases/03-walrus-resolver/README.mdFull deployment walkthrough + IPNS-vs-WalrusResolver comparison table.