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.
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:
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:
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.