Mysten Labs SDKs

Walrus SDK

Installation

npm install --save @mysten/walrus @mysten/sui

Setup

To use the walrus SDK you will need to create an instance of the SuiClient from the typescript SDK, and an instance of the walrus SDK.

import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';
import { WalrusClient } from '@mysten/walrus';

const suiClient = new SuiClient({
	url: getFullnodeUrl('testnet'),
});

const walrusClient = new WalrusClient({
	network: 'testnet',
	suiClient,
});

The walrus SDK currently includes all the relevant package and object IDs needed for connecting to testnet. You can also manually configure the walrusClient to use a different set of ids, allowing you to connect to a different network or updated deployment of the walrus contracts.

const walrusClient = new WalrusClient({
	suiClient,
	packageConfig: {
		systemObjectId: '0x98ebc47370603fe81d9e15491b2f1443d619d1dab720d586e429ed233e1255c1',
		stakingPoolId: '0x20266a17b4f1a216727f3eef5772f8d486a9e3b5e319af80a5b75809c035561d',
	},
});

For some environments you may need to customize how data is fetched:

const walrusClient = new WalrusClient({
	network: 'testnet',
	suiClient,
	storageNodeClientOptions: {
		fetch: (url, options) => {
			console.log('fetching', url);
			return fetch(url, options);
		},
		timeout: 60_000,
	},
});

This can be used to implement a fetch function with custom timeouts, rate limits, retry logic, or any other desired behavior.

SDK Overview

The Walrus TS SDK is designed to work directly with walrus storage nodes, or with the walrus upload relay. When using the walrus SDK without an upload relay, it is important to understand that reading and writing walrus blobs requires a lot of requests (~2200 to write a blob, ~335 to read a blob). The upload relay will reduce the number of requests needed to write a blob, but reads through the walrus SDK will still require a lot of requests. For many applications, using publishers and aggregators is recommended, but the TS SDK can be useful when building applications where the application needs to directly interact with walrus or users need to pay for their own storage directly.

The WalrusClient exposes high level methods for reading and writing blobs, as well as lower level methods for the individual steps in the process that can be used to implement more complex flows when you want more control to implement more optimized implementations.

WalrusFiles

The WalrusFile API provides a higher level abstraction so that applications don't need to worry about how data is stored in walrus. Today it handles data stored directly in blobs, and data stored in Quilts, but may be expanded to cover other storage patterns in the future.

Reading files

To read files, you can use the getFiles method. This method accepts both Blob IDs and Quilt IDs, and will return a WalrusFile.

It is encouraged to always read files in batches when possible, which will allow the client to be more efficient when loading multiple files from the same quilt.

const [file1, file2] = await walrusClient.getFiles({ ids: [anyBlobId, orQuiltId] });

A WalrusFile works like a Response object from the fetch API:

// get contents as a Uint8Array
const bytes = await file1.bytes();
// Parse the contents as a `utf-8` string
const text = await file1.text();
// Parse the contents as JSON
const json = await file2.json();

A WalrusFile may also have and identifier and tags properties if the file was stored in a quilt.

const identifier: string | null = await file1.getIdentifier();
const tags: Record<string, string> = await file1.getTags();

WalrusBlobs

You can also get a WalrusBlob instead of a WalrusFile if you have the blobId:

const blob = await walrusClient.getBlob({ blobId });

If the blob is a quilt, you can read the files in the quilt:

// Get all files:
const files = await blob.files();
// Get files by identifier
const [readme] = await blob.files({ identifiers: ['README.md'] });
// Get files by tag
const textFiles: WalrusFile[] = await blob.files({ tags: [{ 'content-type': 'text/plain' }] });
// Get files by quilt id
const filesById = await blob.files({ ids: [quiltID] });

Writing files

You can also construct a WalrusFile from a Uint8Array, Blob, or a string which can then be stored on walrus:

const file1 = WalrusFile.from(new Uint8Array([1, 2, 3]));
const file2 = WalrusFile.from(new Blob([new Uint8Array([1, 2, 3])]));
const file3 = WalrusFile.from('Hello from the TS SDK!!!\n', {
	identifier: 'README.md',
	tags: {
		'content-type': 'text/plain',
	},
});

Once you have your files you can use the writeFiles method to write them to walrus.

Along with the files, you will also need to provide a Signer instance that signs and pays for the transaction/storage fees. The signer's address will need to have sufficient SUI to cover the transactions that register the blob, and certify its availability after it's been uploaded. The Signer must own sufficient WAL to pay to store the blob for the specified number of epochs, as well as the write fee for writing the blob.

The exact costs will depend on the size of the blobs, as well as the current gas and storage prices.

const results: {
	id: string;
	blobId: string;
	blobObject: Blob.$inferType;
}[] = walrusClient.writeFiles({
	files: [file1, file2, file3],
	epochs: 3,
	deletable: true,
	signer: keypair,
});

Currently the provided files will all be written into a single quilt. Future versions of the SDK may optimize how files are stored to be more efficient by splitting files into multiple quilts.

The current quilt encoding is less efficient for single files, so writing multiple files together is recommended when possible. Writing raw blobs directly is also possible using the writeBlob API (described below)

Writing files in browser environments

When the transactions to upload a blob are signed by a wallet in a browser, some wallets will use popups to prompt the user for a signature. If the popups are not opened in direct response to a user interaction, they may be blocked by the browser.

To avoid this, we need to ensure that we execute the transactions that register and certify the blob in separate events handlers by creating separate buttons for the user to click for each step.

The client.writeFilesFlow method returns an object with a set of methods that break the write flow into several smaller steps:

  1. encode - Encodes the files and generates a blobId
  2. register - returns a transaction that will register the blob on-chain
  3. upload - Uploads the data to storage nodes
  4. certify - returns a transaction that will certify the blob on-chain
  5. listFiles - returns a list of the created files

Here's a simplified example showing the core API usage with separate user interactions:

// Step 1: Create and encode the flow (can be done immediately when file is selected)
const flow = walrusClient.writeFilesFlow({
	files: [
		WalrusFile.from({
			contents: new Uint8Array(fileData),
			identifier: 'my-file.txt',
		}),
	],
});

await flow.encode();

// Step 2: Register the blob (triggered by user clicking a register button after the encode step)
async function handleRegister() {
	const registerTx = flow.register({
		epochs: 3,
		owner: currentAccount.address,
		deletable: true,
	});
	const { digest } = await signAndExecuteTransaction({ transaction: registerTx });
	// Step 3: Upload the data to storage nodes
	// This can be done immediately after the register step, or as a separate step the user initiates
	await flow.upload({ digest });
}

// Step 4: Certify the blob (triggered by user clicking a certify button after the blob is uploaded)
async function handleCertify() {
	const certifyTx = flow.certify();

	await signAndExecuteTransaction({ transaction: certifyTx });

	// Step 5: Get the new files
	const files = await flow.listFiles();
	console.log('Uploaded files', files);
}

This approach ensures that each transaction signing step is separated into different user interactions, allowing wallet popups to work properly without being blocked by the browser.

Writing blobs with an upload relay

Writing blobs directly from a client requires a lot of requests to write data to all of the storage nodes. An upload relay can be used to offload the work of these writes to a server, reducing complexity for the client.

To use an upload relay, you can add the uploadRelay option when creating your WalrusClient.

const client = new SuiClient({
	url: getFullnodeUrl('testnet'),
	network: 'testnet',
}).$extend(
	WalrusClient.experimental_asClientExtension({
		uploadRelay: {
			host: 'https://upload-relay.testnet.walrus.space',
			sendTip: {
				max: 1_000,
			},
		},
	}),
);

The host option is required, and indicates the url for your upload relay. Upload relays may require a tip to be included to cover the cost of writing the blob. You can configure a maximum tip (paid in MIST) and the WalrusClient will automatically determine the required tip for your upload relay, or you can manually configure the tip configuration as shown below.

The tip required by an upload relay can be found using the tip-config endpoint: (eg. https://upload-relay.testnet.walrus.space/v1/tip-config) and may either be a const or a linear tip.

const tip

A const will send a fixed amount for each blob written to the upload relay.

const client = new SuiClient({
	url: getFullnodeUrl('testnet'),
	network: 'testnet',
}).$extend(
	WalrusClient.experimental_asClientExtension({
		uploadRelay: {
			host: 'https://upload-relay.testnet.walrus.space',
			sendTip: {
				address: '0x123...',
				kind: {
					const: 105,
				},
			},
		},
	}),
);

linear tip

A linear tip will send a fixed amount for each blob written to the fan out proxy, plus a multiplier based on the size of the blob.

const client = new SuiClient({
	url: getFullnodeUrl('testnet'),
	network: 'testnet',
}).$extend(
	WalrusClient.experimental_asClientExtension({
		uploadRelay: {
			host: 'https://upload-relay.testnet.walrus.space',
			sendTip: {
				address: '0x123...',
				kind: {
					linear: {
						base: 105,
						perEncodedKib: 10,
					},
				},
			},
		},
	}),
);

Interacting with blobs directly

In case you do not want to use the WalrusFile abstractions, you can use the readBlob and writeBlob APIs directly

Reading blobs

The readBlob method will read a blob given the blobId and return Uint8Array containing the blobs content:

const blob = await walrusClient.readBlob({ blobId });

Writing Blobs

Thw writeBlob method can be used to write a blob (as a Uint8Array) to walrus. You will need to specify how long the blob should be stored for, and if the blob should be deletable.

const file = new TextEncoder().encode('Hello from the TS SDK!!!\n');

const { blobId } = await walrusClient.writeBlob({
	blob: file,
	deletable: false,
	epochs: 3,
	signer: keypair,
});

Full API

For a complete overview of the available methods on the WalrusClient you can reference type TypeDocs

Examples

There are a number of simple examples you can reference in the ts-sdks repo that show things like building simple aggregators and publishers with the walrus SDK

Error handling

The SDK exports all the error classes for different types of errors that can be thrown. Walrus is a fault tolerant distributed system, where many types of errors can be recovered from. During epoch changes there may be times when the data cached in the WalrusClient can become invalid. Errors that result from this situation will extend the RetryableWalrusClientError class.

You can check for these errors, and reset the client before retrying:

import { RetryableWalrusClientError } from '@mysten/walrus';

if (error instanceof RetryableWalrusClientError) {
	walrusClient.reset();

	/* retry your operation */
}

RetryableWalrusClientError are not guaranteed to succeed after resetting the client and retrying, but this pattern can be used to handle some edge cases.

High level methods like readBlob already handle various error cases and will automatically retry when hitting these methods, as well as handling cases where only a subset of nodes need to respond successfully to read or publish a blob.

When using the lower level methods to build your own read or publish flows, it is recommended to understand the number of shards/sliver that need to be successfully written or read for you operation to succeed, and gracefully handle cases where some nodes may be in a bad state.

Network errors

Walrus is designed to be handle some nodes being down, and the SDK will only throw errors when it can't read from or write to enough storage nodes. When trying to troubleshoot problems, it can be challenging to figure out whats going wrong when you don't see all the individual network errors.

You can pass an onError option in the storageNodeClientOptions to get the individual errors from failed requests:

const walrusClient = new WalrusClient({
	network: 'testnet',
	suiClient,
	storageNodeClientOptions: {
		onError: (error) => console.log(error),
	},
});

Configuring network requests

Reading and writing blobs directly from storage nodes requires a lot of requests. The walrus SDK will issue all requests needed to complete these operations, but does not handling all the complexities a robust aggregator or publisher might encounter.

By default all requests are issued using the global fetch for whatever runtime the SDK is running in.

This will not impose any limitations on concurrency, and will be subject to default timeouts and behavior defined by your runtime. To customize how requests are made, you can provide a custom fetch method:

import type { RequestInfo, RequestInit } from 'undici';
import { Agent, fetch, setGlobalDispatcher } from 'undici';

const walrusClient = new WalrusClient({
	network: 'testnet',
	suiClient,
	storageNodeClientOptions: {
		timeout: 60_000,
		fetch: (url, init) => {
			// Some casting may be required because undici types may not exactly match the @node/types types
			return fetch(url as RequestInfo, {
				...(init as RequestInit),
				dispatcher: new Agent({
					connectTimeout: 60_000,
				}),
			}) as unknown as Promise<Response>;
		},
	},
});

Loading the wasm module in vite or client side apps

The walrus SDK requires wasm bindings to encode and decode blobs. When running in node or bun and some bundlers this will work without any additional configuration.

In some cases you may need to manually specify where the SDK loads the wasm bindings from.

In vite you can get the url for the wasm bindings by importing the wasm file with a ?url suffix, and then passed into the walrus client:

import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url';

const walrusClient = new WalrusClient({
	network: 'testnet',
	suiClient,
	wasmUrl: walrusWasmUrl,
});

If you are unable to get a url for the wasm file in your bundler or build system, you can self host the wasm bindings, or load them from a CDN:

const walrusClient = new WalrusClient({
	network: 'testnet',
	suiClient,
	wasmUrl: 'https://unpkg.com/@mysten/walrus-wasm@latest/web/walrus_wasm_bg.wasm',
});

In next.js when using walrus in API routes, you may need to tell next.js to skip bundling for the walrus packages:

// next.config.ts
const nextConfig: NextConfig = {
	serverExternalPackages: ['@mysten/walrus', '@mysten/walrus-wasm'],
};

Known fetch limitations you might run into

  • Some nodes can be slow to respond. When running in node, the default connectTimeout is 10 seconds and can cause request timeouts
  • In bun the abort signal will stop requests from responding, but they still wait for completion before their promises reject