sui-walrus — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited sui-walrus (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Integrate Walrus — Mysten Labs' decentralized blob storage layer. Walrus stores immutable byte arrays ("blobs"); Sui handles coordination, payments, metadata, and object ownership. Storage is provided by erasure-coded slivers across ~1000 shards with Byzantine fault tolerance up to 1/3 malicious nodes. Follow these rules precisely.
Critical: All blobs on Walrus are public and discoverable by anyone. Deletion does not remove cached copies. If the data is sensitive, encrypt before uploading (see the sui-seal skill for the canonical pattern).A successful Walrus upload is three Sui transactions plus distributed storage work:
Blob object).Reading a blob is ~335 requests to reconstruct from slivers. These numbers are why most apps do not call storage nodes directly — they use one of:
@mysten/walrus) — full control; carries the request overhead. Best for backends with custom retry logic.Blob identity and lifecycle:
BlobId → no duplicate storage cost.Blob object on Sui carries the BlobId plus ownership/deletability flags. Burn the object to reclaim a small SUI rebate; burning does not delete storage.Two costs on every write: SUI (gas for up to 3 transactions) and WAL (storage + write fee). Use costcalculator.wal.app or walrus info.
| Path | When | Tradeoffs |
|---|---|---|
| HTTP API via publisher/aggregator | Scripts, demos, apps where a trusted gateway is fine | Simplest. Publisher holds the signing keys and pays SUI/WAL, so pick who to trust. Public publishers on Testnet; self-host for Mainnet. |
| SDK + upload relay | Browsers, mobile, any client that can't open thousands of outbound connections | One request per upload. User still pays SUI/WAL directly. Relay may charge a tip. |
| SDK direct | Server-side services that want full control, custom retry, or to avoid relay fees | Opens ~2200 connections per write. Fine on servers, awful in browsers. |
| `walrus` CLI | One-off ops, debugging, scripting from a shell | Not an integration path — a tool. |
Default recommendation for a Sui dApp that lets users upload: SDK + upload relay. For read-only browsing: aggregator HTTP API.
# Raw bytes inline
curl -X PUT "$PUBLISHER/v1/blobs" -d "some string"
# File upload
curl -X PUT "$PUBLISHER/v1/blobs?epochs=5" --upload-file ./photo.jpg
# Permanent (non-deletable), transfer Blob object to an address
curl -X PUT "$PUBLISHER/v1/blobs?epochs=10&permanent=true&send_object_to=0xabc..." --upload-file ./archive.tarQuery parameters:
| Param | Default | Meaning |
|---|---|---|
epochs | 1 | Number of epochs to store. |
deletable | true | Mints a deletable blob (owner can delete early). |
permanent | false | Mints a permanent blob. Mutually exclusive with deletable=true. |
send_object_to | caller | Recipient address for the Blob Sui object. |
Response shape: { newlyCreated: { blobObject, ... } } for new blobs, or { alreadyCertified: { blobId, endEpoch, ... } } when a certified blob with the same id already exists. Always handle both cases.
# By BlobId
curl "$AGGREGATOR/v1/blobs/$BLOB_ID" -o file.bin
# By Sui object id — lets you set HTTP headers via blob attributes
curl "$AGGREGATOR/v1/blobs/by-object-id/$OBJECT_ID"Aggregators limit content-type sniffing to prevent scripts/stylesheets from being served with inferred MIME types. Set headers via walrus set-blob-attribute (key-value pairs like content-type, content-disposition) — they are returned when reading by object id.
For v1.35+: add strict_consistency_check=true to verify slivers cryptographically. For v1.36+ with a trusted uploader: skip_consistency_check=true for a speedup.
Store many small blobs as one Quilt; retrieve individual files by identifier or by QuiltPatchId. The aggregator returns raw bytes with metadata in HTTP headers. Identifiers must be unique within a quilt and must not start with _ (reserved for Walrus metadata).
Do not hardcode specific public endpoints in production code — they rotate. Current lists:
@mysten/walrusnpm install @mysten/walrus @mysten/suiThe SDK extends a SuiGrpcClient:
import { SuiGrpcClient } from '@mysten/sui/grpc';
import { walrus } from '@mysten/walrus';
const client = new SuiGrpcClient({
network: 'testnet',
baseUrl: 'https://fullnode.testnet.sui.io:443',
}).$extend(walrus());
// Now: client.walrus.getFiles({ ... }), client.walrus.writeFiles({ ... }), etc.walrus() with no args uses the built-in Testnet package config. For Mainnet or a pinned deployment, import MAINNET_WALRUS_PACKAGE_CONFIG / TESTNET_WALRUS_PACKAGE_CONFIG or pass packageConfig: { systemObjectId, stakingPoolId }..walrus namespace on the extended client.Useful exports:
import {
walrus, WalrusClient,
WalrusFile, WalrusBlob,
RetryableWalrusClientError,
blobIdFromInt, blobIdToInt,
encodeQuilt,
TESTNET_WALRUS_PACKAGE_CONFIG, MAINNET_WALRUS_PACKAGE_CONFIG,
} from '@mysten/walrus';WalrusFile — the high-level APIRead (works for both plain blobs and files inside a Quilt — always batch):
const [readme, logo] = await client.walrus.getFiles({ ids: [blobIdOrQuiltId1, id2] });
const bytes: Uint8Array = await readme.bytes();
const text: string = await readme.text();
const json: unknown = await logo.json();
const identifier: string | null = await readme.getIdentifier();
const tags: Record<string,string> = await readme.getTags();Write (files are bundled into a single Quilt by the SDK):
const file = WalrusFile.from({
contents: new TextEncoder().encode('Hello from Walrus\n'),
identifier: 'README.md',
tags: { 'content-type': 'text/plain' },
});
const results = await client.walrus.writeFiles({
files: [file],
epochs: 3,
deletable: true,
signer: keypair, // must own enough SUI for tx + WAL for storage
});
// results: Array<{ id: string; blobId: string; blobObject }>The Quilt encoding is inefficient for a single file — prefer writing multiple files together. For a single file, consider writeBlob (§4.5) instead.
WalrusBlob — read a Quilt and query its contentsconst blob = await client.walrus.getBlob({ blobId });
const all = await blob.files();
const [readme] = await blob.files({ identifiers: ['README.md'] });
const textFiles = await blob.files({ tags: [{ 'content-type': 'text/plain' }] });writeFilesFlow — the browser workflow (important)Browsers block wallet-popup prompts that aren't in direct response to a click. A Walrus write needs two signatures (register + certify), so they must be split into separate click handlers. Use writeFilesFlow:
// On file-select: start encoding immediately.
const flow = client.walrus.writeFilesFlow({
files: [WalrusFile.from({ contents: fileBytes, identifier: 'upload.bin' })],
});
await flow.encode();
// Click 1 — register + upload slivers
async function handleRegister() {
const registerTx = flow.register({
epochs: 3,
owner: currentAccount.address,
deletable: true,
});
const result = await signAndExecuteTransaction({ transaction: registerTx });
if (result.$kind === 'FailedTransaction') throw new Error(result.FailedTransaction.status.error?.message);
await flow.upload({ digest: result.Transaction.digest });
}
// Click 2 — certify
async function handleCertify() {
const certifyTx = flow.certify();
const result = await signAndExecuteTransaction({ transaction: certifyTx });
if (result.$kind === 'FailedTransaction') throw new Error(result.FailedTransaction.status.error?.message);
const files = await flow.listFiles(); // [{ id, blobId, blobObject }]
}Always check result.$kind === 'FailedTransaction' after each signed step. Don't collapse both signatures into one handler — the second popup will be blocked in Safari and some Chrome profiles.
writeBlob / writeBlobFlow — raw bytes, with crash recoveryFor a single blob without Quilt packaging:
const { blobId } = await client.walrus.writeBlob({
blob: fileBytes,
deletable: false,
epochs: 10,
signer: keypair,
});For resumable uploads (long-running backends, unreliable networks):
const saved = await db.load(fileId); // undefined on first attempt
const { blobId } = await client.walrus.writeBlob({
blob: fileBytes,
epochs: 3,
deletable: true,
signer: keypair,
onStep: (step) => db.save(fileId, step), // persist each step
resume: saved, // skip completed steps on retry
});Or use the async-iterator flow when per-step control is needed:
const flow = client.walrus.writeBlobFlow({ blob: fileBytes });
for await (const step of flow.run({ signer, epochs: 3, deletable: true })) {
await db.save(fileId, step);
}flow.executeRegister(...) and flow.executeCertify(...) are the typed step-by-step equivalents.
readBlob — raw bytesconst bytes = await client.walrus.readBlob({ blobId });readBlob already handles storage-node failures and retries internally. Implement additional retry only for RetryableWalrusClientError (§4.8).
When to use: browsers, mobile, any client that cannot comfortably open thousands of outbound HTTP requests. Without a relay the SDK opens ~2200 connections to storage nodes per write.
Configure at walrus({}) init:
const client = new SuiGrpcClient({ network: 'testnet', baseUrl: '...' }).$extend(
walrus({
uploadRelay: {
host: 'https://upload-relay.testnet.walrus.space',
sendTip: { max: 1_000 }, // max MIST per blob; SDK auto-discovers actual tip
},
}),
);For explicit tip modes instead of auto-discovery:
// Constant tip — flat MIST per blob.
sendTip: { address: '0x...', kind: { const: 105 } }
// Linear tip — base + per-encoded-KiB.
sendTip: { address: '0x...', kind: { linear: { base: 105, perEncodedKib: 10 } } }The relay fetches the tip config from $host/v1/tip-config. Failures manifest as NotEnoughBlobConfirmationsError.
When storing lots of files under ~1 MiB each (NFT thumbnails, avatars, small JSON), use a Quilt, not individual blobs. A Quilt packs up to 666 blobs (QuiltV1) into one storage unit and amortizes the fixed metadata cost. Reported savings: ~409× for 10 KiB files and ~238× on Sui fees vs. individual blobs.
Tradeoffs:
QuiltPatchId, which changes if moved to another quilt._-prefixed names are reserved.writeFiles already packs into a Quilt. To encode one offline:
import { encodeQuilt } from '@mysten/walrus';walrus CLIwalrus store file.txt --epochs 2 --context testnetwalrus read <BLOB_ID> --out file.txtwalrus extend --blob-obj-id <ID> --epochs-ahead 10 — extends a blob's life. Anyone can extend a shared blob; only the owner can extend an owned one.walrus delete --blob-id <BLOB_ID> — only the owner, only if deletable, does not remove cached copies (blob is public).walrus burn-blobs --blob-obj-id <ID> — destroys the Sui object (no storage refund); the ability to delete/extend is lost but some SUI storage rebate is freed.walrus set-blob-attribute --blob-obj-id <ID> --attr content-type text/html — set HTTP headers for aggregator responses.A shared blob is a Walrus Blob object wrapped into a Sui shared object. Shared blobs must be permanent (cannot be deleted before expiry), but anyone can fund extending them. Use for community-maintained assets.
walrus infoPrint live network state — system object id, current epoch, price per unit, active committee. Always verify integrations against walrus info output rather than hardcoded numbers.
Walrus provides availability and integrity (blob bytes returned match what was uploaded, verifiable via content hash). It does not provide confidentiality — blobs are public.
For confidentiality, combine with Seal using envelope encryption:
// 1. Generate a data-encryption key and encrypt locally with AES-GCM.
const dek = crypto.getRandomValues(new Uint8Array(32));
const ciphertext = await aesGcmEncrypt(dek, plaintext);
// 2. Seal-wrap the DEK against your seal_approve* policy.
const { encryptedObject: wrappedDek } = await seal.encrypt({
threshold: 2, packageId, id: keyIdBytes, data: dek,
});
// 3. Upload ciphertext + wrappedDek to Walrus as two files in a quilt,
// or upload ciphertext to Walrus and store wrappedDek on Sui.
await client.walrus.writeFiles({
files: [
WalrusFile.from({ contents: ciphertext, identifier: 'content.bin' }),
WalrusFile.from({ contents: wrappedDek, identifier: 'wrapped.dek' }),
],
epochs: 10, deletable: false, signer,
});Why envelope encryption: Seal key servers can be rotated by re-encrypting only the small DEK — the large ciphertext on Walrus stays put. See the sui-seal skill for details.
For TEE-based off-chain computation over Walrus data, see Nautilus (out of scope for this skill).
import { RetryableWalrusClientError } from '@mysten/walrus';
try {
await client.walrus.writeBlob({ /* ... */ });
} catch (err) {
if (err instanceof RetryableWalrusClientError) {
client.walrus.reset(); // clear cached committee/epoch state
// retry — most often fires during epoch transitions
} else {
throw err;
}
}RetryableWalrusClientError is the critical one — cached committee data is invalidated at epoch boundaries. Retry after reset() is not guaranteed to succeed but usually does.
For deeper debugging, surface individual node errors:
walrus({
storageNodeClientOptions: {
onError: (e) => console.warn('node error:', e),
timeout: 60_000,
},
})High-level methods (readBlob, writeBlob, writeFiles) already tolerate some node failures — they only throw when quorum cannot be reached. Do not implement loop-retries inside these calls yourself.
The SDK requires WASM bindings to encode/decode blobs. Node, Bun, and many bundlers work out of the box. When they don't:
Vite — import the wasm file as a URL and pass it:
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url';
walrus({ wasmUrl: walrusWasmUrl })CDN fallback (any bundler that can't resolve the wasm path):
walrus({ wasmUrl: 'https://unpkg.com/@mysten/walrus-wasm@latest/web/walrus_wasm_bg.wasm' })Next.js API routes — exclude from server bundling:
// next.config.ts
const nextConfig: NextConfig = {
serverExternalPackages: ['@mysten/walrus', '@mysten/walrus-wasm'],
};Memory is the main constraint: upload requires ~4.5× blob size in RAM. Checklist:
blob_size × 4.5 × concurrent_uploads.{blobId, txDigest} between steps; use onStep/resume for crash recovery.walrus info or costcalculator.wal.app; Mainnet epoch = 14 days, not 7.| Mainnet | Testnet | |
|---|---|---|
| Sui network | Mainnet | Testnet |
| Epoch | 14 days | 1 day |
| Max epochs per registration | 53 (~2 years) | 53 (~53 days) |
| Tokens | WAL + SUI (real value) | tWAL + tSUI (no value) |
| Reset? | No | Can be wiped without warning |
Get Testnet WAL after funding a Sui Testnet wallet: walrus get-wal (default swaps 0.5 SUI → 0.5 WAL).
Blob is a Sui object. A Move package can accept, store, and verify Blob objects — e.g., an NFT module that requires an attached Walrus blob for its metadata. Reference: wrapped_blob.move in the Walrus repo. Load the move skill for object handling (abilities, transfer, shared objects).
To verify that a given blob's content matches expected bytes on-chain, use the content-addressed blob_id field — anyone who has the bytes can recompute the hash and compare.
Walrus Sites host decentralized static websites on Sui + Walrus. Site files are stored as Quilts on Walrus; a Sui object holds the URL-path-to-blob routing. Use the site-builder CLI to deploy and update sites; use a portal to browse them.
For the full site-builder CLI reference, portal deployment, local tunnel setup for sharing localhost:3000 externally, portal-config.yaml schema, ws-resources.json field reference (including redirects), restrictions, site-specific anti-patterns, and troubleshooting, load `references/walrus-sites.md`. For custom domain setup (DNS, HTTPS, reverse proxy), load `references/walrus-sites-custom-domains.md`. For CI/CD deployment (GitHub Actions, GitLab CI, CircleCI, Bitbucket), load `references/walrus-sites-ci-cd.md`.
| Task | Also load |
|---|---|
| Encrypting before upload | sui-seal skill |
Building transactions that handle Blob objects | sui-ts-sdk |
| React/browser upload UI (wallet popups, state) | sui-frontend + sui-ts-sdk |
A Move package that accepts Walrus Blob objects | sui-move skill |
| Deploying a static website to Walrus Sites | See section 14, then references/walrus-sites.md |
| Custom domain for a Walrus Site | references/walrus-sites-custom-domains.md |
| CI/CD deployment (GitHub Actions, GitLab, etc.) | references/walrus-sites-ci-cd.md |
register and certify signing inside a single event handler in a browser — wallet popup blocked on the second.RetryableWalrusClientError without calling client.walrus.reset() — retries will hit stale committee data.writeBlob / readBlob in custom retry loops — they already retry. Only retry at the Retryable* boundary.result.$kind === 'FailedTransaction' after signing register/certify — a failed on-chain step silently breaks the flow.epochs: 1 for production data — Testnet gives a day; Mainnet two weeks; factor in renewal cost and risk.encode. Set wasmUrl up front.writeFiles with just one file when writeBlob would be cheaper — Quilt encoding overhead dominates for single small files.Walrus Sites-specific anti-patterns are listed in `references/walrus-sites.md`.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.