Mysten Incubation

Walrus

Local Walrus clusters, known deployments, and WAL funding.

Store and retrieve blobs from a Walrus cluster running on your local network. Add walrus() to your stack to start the cluster, then use the generated publisher, aggregator, and upload relay URLs to publish and read blobs through the release Walrus services.

Local mode starts the cluster. Use walCoin(localWalrus) with account funding when an account needs WAL.

devstack.config.ts
import { account, defineDevstack, sui, walCoin, walrus } from '@mysten-incubation/devstack';

const localnet = sui();
// `nodeCount` is the number of storage nodes in the committee (default 1). The
// committee's shards are split across them — `shards` (default 100) is the total
// shard count and must be >= `nodeCount`; raise `nodeCount` for a multi-node
// committee.
const localWalrus = walrus({
	local: {
		nodeCount: 4,
	},
});
const wal = walCoin(localWalrus);

const alice = account('alice', {
	funding: [
		{ coin: 'sui', amount: 1_000_000_000n },
		{ coin: wal, amount: 500_000_000n },
	],
});

export default defineDevstack({ members: [localnet, localWalrus, alice] });

Known mode points at an existing deployment. The per-network methods — walrusFor(network).testnet({ nodes }) / .mainnet({ nodes }) — are the common path: the on-chain ids (including systemObjectId and stakingPoolId) default from the @mysten/walrus SDK package config, so you only supply the storage-node list. (Walrus testnet runs 100+ dynamic nodes, so the node list can't be derived and stays required.) Use walrusFor(network).known({ systemObjectId, stakingPoolId, nodes }) to override the ids for a deployment outside the SDK constants. Fork stacks use the same walrusFor(forkNetwork).testnet({ nodes }) methods; local Walrus clusters run only on local networks.

walCoin(walrusMember) returns the WAL funding coin for that Walrus member. Pass it to account funding like any other coin ref to give an account WAL.

In a bare stack you can add the cluster directly to members, as the snippet above does. In a real app, put only localnet, your app host-service, and dashboard() in members, and order the cluster via the consuming server's after: list so it boots before the app starts — see examples/private-content/devstack.config.ts (after: [localnet, vault, walrusCluster, sealKeyServer, devWallet] on the hostService).

Local publisher, aggregator, and upload relay

Local mode starts app-facing publisher, aggregator, and upload relay HTTP endpoints by default. These endpoints let an app publish and read blobs through one URL instead of talking directly to every storage node. The generated Walrus binding exposes them as publisherUrl, aggregatorUrl, and uploadRelayUrl. If you disable a service, that URL resolves to null.

if (!w.publisherUrl || !w.aggregatorUrl || !w.uploadRelayUrl) {
	throw new Error(
		'Walrus publisher, aggregator, or upload relay services are disabled for this stack',
	);
}

const publish = await fetch(`${w.publisherUrl}/v1/blobs?epochs=3&deletable=false`, {
	method: 'PUT',
	body: new TextEncoder().encode('hello walrus'),
});
const receipt = await publish.json();
const blobId = receipt.newlyCreated.blobObject.blob_id;

const read = await fetch(`${w.aggregatorUrl}/v1/blobs/${blobId}`);
const bytes = new Uint8Array(await read.arrayBuffer());

Devstack runs the release-provided walrus publisher, walrus aggregator, and walrus-upload-relay binaries in managed containers. The publisher uses the local deploy-generated wallet and native Walrus publisher behavior, including support for send_object_to=<address> on the publish URL. The local upload relay uses a free !no_tip relay configuration and exposes /v1/tip-config plus /v1/blob-upload-relay.

Tune or disable the services through local.aggregator, local.publisher, and local.uploadRelay:

devstack.config.ts
const localWalrus = walrus({
	local: {
		aggregator: { port: 40100 },
		publisher: { port: 40101 },
		uploadRelay: { port: 40102 },
	},
});

const storageNodesOnly = walrus({
	local: { aggregator: false, publisher: false, uploadRelay: false },
});

Direct SDK access

The config above runs the cluster; the generated bindings carry its resolved ids into your app. Walrus is a per-network service: walrus.forNetwork(network) returns the config for the network your wallet is connected to, so a runtime switchNetwork flips the cluster ids in lockstep.

For local clusters, prefer the generated publisherUrl, aggregatorUrl, and uploadRelayUrl shown above. The local storage-node committee records use Docker-network hostnames so the Rust publisher, aggregator, and upload relay containers can reach the nodes directly; those hostnames are not a browser API. Build a direct WalrusClient only for known/live deployments, or for advanced host-side tooling that deliberately manages storage-node routing itself:

src/lib/walrus.ts
import { useCurrentNetwork } from '@mysten/dapp-kit-react';
import { WalrusClient } from '@mysten/walrus';
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url';

import { walrus } from '@generated/walrus.js';

const network = useCurrentNetwork();
const w = walrus.forNetwork(network);

if (w.mode === 'local') {
	throw new Error(
		'Use w.publisherUrl, w.aggregatorUrl, and w.uploadRelayUrl for local Walrus clusters',
	);
}

const client = new WalrusClient({
	suiClient,
	packageConfig: {
		systemObjectId: w.packageConfig.systemObjectId,
		stakingPoolId: w.packageConfig.stakingPoolId,
		// `exchangeIds` is optional on the resolved config — only spread it
		// when present so the shape matches `WalrusClient`'s expected type.
		...(w.packageConfig.exchangeIds ? { exchangeIds: [...w.packageConfig.exchangeIds] } : {}),
	},
	storageNodeUrlScheme: 'https',
	wasmUrl: walrusWasmUrl,
});

w.packageConfig holds systemObjectId, stakingPoolId, and an optional exchangeIds resolved at boot — build the client's packageConfig field-by-field as above, spreading exchangeIds only when it's present. See examples/private-content for an endpoint-backed local write/read helper with direct-SDK fallback for non-local networks.

On this page