This document describes the Walrus integration in the project: all post content (text, images) and profile images (avatars, banners) are stored on Walrus, a decentralized blob-storage layer built on SUI.
- What is Walrus?
- How It Is Used — Overview
- Architecture in the Project
- Detailed Flow (Code)
- Configuration
- Binary Data Blob Format
- Usage in the App
- Troubleshooting
- References
-
Walrus is a decentralized blob storage protocol on Sui.
-
Blobs are addressed by a content-derived blobId (content-addressable).
-
Data is replicated across storage nodes and can be fetched from any aggregator.
-
In this project, Walrus stores two kinds of data:
- Encrypted blobs — post content (text + images) encrypted with Seal before upload.
- Public blobs — profile images (avatars, banners) and public posts sent in the clear.
-
Official docs: docs.walrus.site
- Post text + images are serialized into two blobs (metadata JSON + binary image pack).
- Both blobs are encrypted with Seal (see SEAL.md).
- The encrypted bytes are uploaded to Walrus via the Publisher API (
PUT). - The resulting
blobIds are stored on-chain in theServiceobject viapublish_post. - To read: download raw bytes from the Aggregator → decrypt with Seal → parse.
- Avatar/banner image files are uploaded to Walrus without encryption.
- The
blobIdis stored on-chain (inService.avatar_blob_id/Service.banner_blob_id). - To display: build an aggregator URL from the
blobIdor download the raw bytes.
Frontend (app/)
├── lib/
│ ├── walrus.ts ← uploadToWalrus(), downloadFromWalrus(), uploadPublicImage()
│ ├── post-service.ts ← packImages(), unpackImages(), serializeMetadata()
│ └── contract.ts ← buildPublishPost() — stores blobIds on-chain
├── hooks/
│ ├── usePublishPost.ts ← Full pipeline: validate → encrypt → upload → TX
│ ├── usePostContent.ts ← Download → decrypt → parse for viewing
│ └── useCreatorBlobUrl.ts ← Resolve profile image blobId → displayable URL
└── components/
└── ... ← Displays resolved blob URLs
const url = `${WALRUS_PUBLISHER_URL}/v1/blobs?epochs=${epochs}`;
const response = await fetch(url, {
method: "PUT",
headers: { "Content-Type": "application/octet-stream" },
body: data,
});Returns either newlyCreated.blobObject.blobId or alreadyCertified.blobId (idempotent).
const url = `${WALRUS_AGGREGATOR_URL}/v1/blobs/${blobId}`;
const response = await fetch(url);
return new Uint8Array(await response.arrayBuffer());Returns raw bytes — still encrypted for gated posts, plain bytes for public content.
uploadPublicImage(file: File) reads the file as Uint8Array and calls uploadToWalrus directly (no encryption). Used for avatars and banners.
getWalrusImageUrl(blobId) returns ${WALRUS_AGGREGATOR_URL}/v1/blobs/${blobId} for direct <img src> use.
| Constant | Value (testnet) | Purpose |
|---|---|---|
WALRUS_PUBLISHER_URL |
https://publisher.walrus-testnet.walrus.space |
Upload endpoint |
WALRUS_AGGREGATOR_URL |
https://aggregator.walrus-testnet.walrus.space |
Download endpoint |
WALRUS_EPOCHS |
5 |
Storage duration (1 epoch ≈ 1 day on testnet) |
No API key or secret is needed. Walrus uploads are permissionless on testnet.
Images are packed into a single binary blob before encryption:
[4 bytes: image count (u32 LE)]
For each image:
[4 bytes: header JSON length (u32 LE)]
[N bytes: header JSON → { index, mimeType, fileName, size, width?, height?, alt? }]
[4 bytes: image data length (u32 LE)]
[M bytes: raw image bytes]
- Pack:
packImages()inlib/post-service.ts - Unpack:
unpackImages()inlib/post-service.ts— creates Object URLs for display (caller must callrevokeImageUrls()on unmount).
The metadata blob is a simple JSON:
{ "version": 1, "text": "...", "images": [{ "index": 0, "mimeType": "image/jpeg", ... }] }Serialized via serializeMetadata() and parsed via deserializeMetadata().
The hook orchestrates the full pipeline with progress tracking:
- Validate inputs (title, text, images).
- Pack images into binary blob.
- Get
next_post_idfrom chain (needed for Seal identity). - If
requiredTier > 0: encrypt both blobs with Seal →uploadEncryptedContent. - If
requiredTier === 0: upload both blobs directly (no encryption). - Execute
publish_posttransaction with the twoblobIds.
- Public posts (
requiredTier === 0): auto-loads on mount —downloadFromWalrus→deserializeMetadata/unpackImages. - Encrypted posts (
requiredTier > 0): waits forunlock()— downloads then decrypts with Seal. - Creator's own posts: auto-unlocked (creators always have access).
Resolves a blobId to a displayable URL:
- If already an HTTP URL → use as-is.
- Download from Walrus.
- If raw image bytes (JPEG/PNG/WebP magic) → Object URL.
- If encrypted → decrypt with Seal (postId = 0 for profile blobs) → Object URL.
| Issue | What to check |
|---|---|
Walrus upload failed (4xx/5xx) |
Check WALRUS_PUBLISHER_URL is correct for your network (testnet vs mainnet). Verify the publisher node is reachable. |
Walrus download failed |
Verify WALRUS_AGGREGATOR_URL. If blob was uploaded with few epochs, it may have expired. |
Unexpected Walrus response format |
The Walrus API response doesn't contain newlyCreated or alreadyCertified. Check for API changes. |
| Images not displaying | Ensure unpackImages Object URLs are not revoked prematurely. Check console for download errors. |
| Large uploads failing | Total upload is capped at MAX_TOTAL_UPLOAD_BYTES. Individual images at MAX_IMAGE_SIZE_BYTES. |
- Walrus — Documentation
- Walrus — Testnet Publisher
- Walrus — Testnet Aggregator
- Seal integration (this project) — Encryption layer used before Walrus upload
- Enoki integration (this project) — Sponsored transactions for
publish_post