Cloudinary-like API for decentralized media storage on Sui blockchain.
📦 NPM Package: @walbucket/sdk
Walbucket SDK provides a simple, developer-friendly interface for storing and managing media files on the Sui blockchain using Walrus decentralized storage. With built-in encryption support via Seal, automatic URL generation, and flexible gas payment strategies, it's designed to make decentralized storage as easy as using traditional cloud storage services.
pnpm add @walbucket/sdk
# or
npm install @walbucket/sdk
# or
yarn add @walbucket/sdkimport { Walbucket } from '@walbucket/sdk';
// Initialize SDK
const walbucket = new Walbucket({
apiKey: 'your-api-key',
network: 'testnet',
sponsorPrivateKey: 'your-private-key',
});
// Upload a file
const result = await walbucket.upload(file, {
name: 'my-image.jpg',
folder: 'products',
});
console.log(result.assetId); // Sui object ID
console.log(result.url); // Automatically generated file URL
// Retrieve a file
const retrieveResult = await walbucket.retrieve(result.assetId);
console.log('File data:', retrieveResult.data);
console.log('File URL:', retrieveResult.url);
console.log('Metadata:', retrieveResult.metadata);
// Delete a file
await walbucket.delete(result.assetId);apiKey: Your API key for authenticationsponsorPrivateKey: Required if usinggasStrategy: 'developer-sponsored'(default)userSigner: Required if usinggasStrategy: 'user-pays'
network: Sui network -'testnet'(default),'mainnet','devnet', or'localnet'- Package ID is automatically selected based on network
encryption: Enable encryption -true(default) orfalsegasStrategy: Choose who pays gas fees:'developer-sponsored'(default) - Developer pays gas fees'user-pays'- Users pay their own gas fees
packageId: Override auto-detected package ID (usually not needed)walrusPublisherUrl: Override auto-detected Walrus publisher URLwalrusAggregatorUrl: Override auto-detected Walrus aggregator URLcacheTTL: Cache TTL in seconds (default: 3600)
interface WalbucketConfig {
// Required
apiKey: string;
// Required based on gasStrategy choice
sponsorPrivateKey?: string; // Required if gasStrategy is 'developer-sponsored'
userSigner?: Signer; // Required if gasStrategy is 'user-pays'
// Optional with smart defaults
network?: 'testnet' | 'mainnet' | 'devnet' | 'localnet'; // Default: 'testnet'
encryption?: boolean; // Default: true
gasStrategy?: 'developer-sponsored' | 'user-pays'; // Default: 'developer-sponsored'
// Auto-detected (usually don't need to set)
packageId?: string; // Auto-detected from network
walrusPublisherUrl?: string; // Auto-detected from network
walrusAggregatorUrl?: string; // Auto-detected from network
sealServerIds?: string[]; // Auto-detected from network
cacheTTL?: number; // Default: 3600 seconds
}Walbucket supports two gas payment strategies, giving you flexibility in how transactions are paid for:
The developer pays for all gas fees. This provides the best user experience as users don't need to approve gas payments.
Benefits:
- ✅ Better user experience (no wallet popups for gas)
- ✅ Users don't need SUI tokens
- ✅ Simplified onboarding
Requirements:
sponsorPrivateKey: Your private key for sponsoring transactions
Example:
const walbucket = new Walbucket({
apiKey: 'your-api-key',
network: 'testnet',
gasStrategy: 'developer-sponsored', // Default - can omit
sponsorPrivateKey: 'your-private-key', // Required
});Users pay their own gas fees. This is more decentralized and puts users in control.
Benefits:
- ✅ More decentralized
- ✅ Users control their own transactions
- ✅ No developer gas costs
Requirements:
userSigner: Signer from the user's connected wallet extension- Users need SUI tokens for gas
- Users must connect their wallet (Sui Wallet, Ethos, etc.)
Getting the User Signer:
Users connect their wallets through wallet extensions, and you get the signer from the connected wallet. Here are examples for different integration methods:
Using @mysten/dapp-kit (React Apps):
import { useCurrentWallet } from '@mysten/dapp-kit';
import { Walbucket } from '@walbucket/sdk';
import { useEffect, useState } from 'react';
function MyComponent() {
const { currentWallet, isConnected } = useCurrentWallet();
const [walbucket, setWalbucket] = useState<Walbucket | null>(null);
useEffect(() => {
if (isConnected && currentWallet?.account) {
// Get signer from connected wallet
const walbucket = new Walbucket({
apiKey: 'your-api-key',
network: 'testnet',
gasStrategy: 'user-pays',
userSigner: currentWallet.account, // Signer from wallet extension
});
setWalbucket(walbucket);
}
}, [isConnected, currentWallet]);
// When upload is called, wallet popup appears for user to sign
const handleUpload = async (file: File) => {
if (!walbucket) return;
const result = await walbucket.upload(file);
console.log('Uploaded!', result.url);
};
}Using @mysten/wallet-standard:
import { getWallets } from '@mysten/wallet-standard';
import { Walbucket } from '@walbucket/sdk';
// Get available wallets
const wallets = getWallets();
const wallet = wallets[0]; // User's wallet
// Connect wallet (shows popup to user)
await wallet.features['standard:connect'].connect();
// Get signer from wallet
const accounts = await wallet.features['standard:connect'].getAccounts();
const signer = accounts[0]; // User's account signer
// Create SDK instance
const walbucket = new Walbucket({
apiKey: 'your-api-key',
network: 'testnet',
gasStrategy: 'user-pays',
userSigner: signer, // Signer from wallet extension
});
// When SDK methods are called, wallet popups appear automatically
const result = await walbucket.upload(file);Important Notes:
- ✅ The
userSignercomes from a connected wallet extension (Sui Wallet, Ethos, etc.) - ✅ Wallet popups appear automatically when SDK methods are called
- ✅ Never ask users for their private keys - always use wallet extensions
- ✅ The signer is provided by the wallet after the user connects
Note: The gasStrategy field is optional and defaults to 'developer-sponsored'. You can omit it if using the default strategy.
- ✅ Standalone SDK - No backend dependency
- ✅ Direct Sui Integration - Uses Sui gRPC and JSON-RPC clients
- ✅ Walrus Storage - Direct blob storage integration
- ✅ Seal Encryption - Optional client-side encryption
- ✅ API Key Authentication - On-chain API key validation
- ✅ Gas Strategies - Developer-sponsored or user-pays
- ✅ Type-Safe - Full TypeScript support
- ✅ Cloudinary-like API - Familiar developer experience
- ✅ Automatic URL Generation - File URLs automatically generated and returned
- ✅ Network Auto-Detection - Package IDs automatically selected based on network
Upload a file to Walbucket storage.
const result = await walbucket.upload(file, options);Parameters:
file: File input (File, Blob, Buffer, Uint8Array, or file path string)options: Upload options (optional)
Options:
name?: string- Asset namefolder?: string- Folder/collection IDencryption?: boolean- Enable encryption (default: true)policy?: EncryptionPolicy- Encryption policy (see Encryption Policies below)tags?: string[]- Tags for categorizationdescription?: string- Asset descriptioncategory?: string- Asset categorywidth?: number- Image/video width in pixelsheight?: number- Image/video height in pixels
Returns:
UploadResultobject with:assetId: string- Sui object IDblobId: string- Walrus blob IDurl: string- Automatically generated file URLencrypted: boolean- Whether encryptedpolicyId?: string- Policy ID if encryptedsize: number- File size in bytescontentType: string- MIME typecreatedAt: number- Timestamp
Example:
const result = await walbucket.upload(file, {
name: 'photo.jpg',
folder: 'gallery',
encryption: true,
policy: {
type: 'wallet-gated',
addresses: ['0x...']
}
});
console.log('Uploaded!', result.url);Retrieve a file from Walbucket storage.
const result = await walbucket.retrieve(assetId, options);Parameters:
assetId: Asset ID to retrieveoptions: Retrieve options (optional)
Options:
decrypt?: boolean- Decrypt the file (default: true if encrypted)password?: string- Password for password-protected assetssessionKey?: SessionKey- SessionKey from @mysten/seal (required for decryption)
Returns:
RetrieveResultobject with:data: Buffer- File dataurl: string- Automatically generated file URLmetadata: AssetMetadata- Complete asset metadata
Example:
// Basic retrieve
const result = await walbucket.retrieve(assetId);
console.log('File URL:', result.url);
console.log('File size:', result.metadata.size);
// Retrieve with decryption
import { SealClient } from '@mysten/seal';
const sealClient = new SealClient({ suiClient });
const sessionKey = await sealClient.getSessionKey(policyId);
const result = await walbucket.retrieve(assetId, {
sessionKey,
});Delete an asset from Walbucket storage.
await walbucket.delete(assetId);Parameters:
assetId: Asset ID to delete
Example:
await walbucket.delete(assetId);Get asset metadata without retrieving the file.
const asset = await walbucket.getAsset(assetId);Parameters:
assetId: Asset ID to query
Returns:
AssetMetadata | null- Asset metadata including:url: string- Automatically generated file URLassetId: string- Asset IDblobId: string- Blob ID from Walrusname: string- Asset namesize: number- File size in bytescontentType: string- MIME typecreatedAt: number- Creation timestampupdatedAt: number- Last update timestamptags: string[]- Tagsdescription: string- Descriptioncategory: string- Category- And more...
Example:
const asset = await walbucket.getAsset(assetId);
if (asset) {
console.log('Asset URL:', asset.url);
console.log('Name:', asset.name);
console.log('Size:', asset.size);
}Walbucket supports multiple encryption policy types:
Only specific wallet addresses can access the file:
policy: {
type: 'wallet-gated',
addresses: ['0x...', '0x...']
}File access expires at a specific timestamp:
policy: {
type: 'time-limited',
expiration: Date.now() + 86400000 // 24 hours from now
}File requires a password for access:
policy: {
type: 'password-protected',
password: 'my-secret-password'
}No access restrictions:
policy: {
type: 'public'
}The SDK provides typed error classes for better error handling:
import {
WalbucketError,
ValidationError,
NetworkError,
EncryptionError,
BlockchainError,
ConfigurationError
} from '@walbucket/sdk';
try {
await walbucket.upload(file);
} catch (error) {
if (error instanceof ValidationError) {
console.error('Validation error:', error.message);
} else if (error instanceof NetworkError) {
console.error('Network error:', error.message);
} else if (error instanceof EncryptionError) {
console.error('Encryption error:', error.message);
} else if (error instanceof BlockchainError) {
console.error('Blockchain error:', error.message);
} else if (error instanceof ConfigurationError) {
console.error('Configuration error:', error.message);
} else if (error instanceof WalbucketError) {
console.error('Error code:', error.code);
}
}React App with @mysten/dapp-kit:
import { useCurrentWallet, ConnectButton } from '@mysten/dapp-kit';
import { Walbucket } from '@walbucket/sdk';
import { useState, useEffect } from 'react';
function UploadComponent() {
const { currentWallet, isConnected } = useCurrentWallet();
const [walbucket, setWalbucket] = useState<Walbucket | null>(null);
useEffect(() => {
if (isConnected && currentWallet?.account) {
// Create SDK instance when wallet is connected
const sdk = new Walbucket({
apiKey: 'your-api-key',
network: 'testnet',
gasStrategy: 'user-pays',
userSigner: currentWallet.account, // Signer from connected wallet
});
setWalbucket(sdk);
}
}, [isConnected, currentWallet]);
const handleUpload = async (file: File) => {
if (!walbucket) {
alert('Please connect your wallet first');
return;
}
// Wallet popup will appear for user to sign transaction
try {
const result = await walbucket.upload(file, {
name: file.name,
});
console.log('Uploaded!', result.url);
} catch (error) {
if (error.message.includes('User rejected') || error.message.includes('rejected')) {
console.log('User cancelled transaction');
}
}
};
return (
<div>
<ConnectButton />
{!isConnected && <p>Connect your wallet to upload files</p>}
{walbucket && (
<input
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleUpload(file);
}}
/>
)}
</div>
);
}Vanilla JavaScript/TypeScript:
import { getWallets } from '@mysten/wallet-standard';
import { Walbucket } from '@walbucket/sdk';
async function setupWallet() {
// Get available wallets
const wallets = getWallets();
const wallet = wallets.find(w => w.name === 'Sui Wallet');
if (!wallet) {
throw new Error('Sui Wallet not found. Please install Sui Wallet extension.');
}
// Connect wallet (shows popup to user)
await wallet.features['standard:connect'].connect();
// Get user's account/signer from wallet
const accounts = await wallet.features['standard:connect'].getAccounts();
const signer = accounts[0];
// Create SDK with user signer
const walbucket = new Walbucket({
apiKey: 'your-api-key',
network: 'testnet',
gasStrategy: 'user-pays',
userSigner: signer, // User will sign transactions via wallet popup
});
return walbucket;
}
// Usage
const walbucket = await setupWallet();
// When uploading, wallet popup appears automatically for user to sign
const result = await walbucket.upload(file);
console.log('Uploaded!', result.url);How It Works:
- User connects wallet → Wallet extension provides a
Signerinterface - Developer passes signer to SDK → SDK stores it for transaction signing
- SDK calls transaction methods → Wallet popup appears automatically
- User approves in wallet → Transaction is signed and executed
const result = await walbucket.upload(file, {
name: 'secret-document.pdf',
encryption: true,
policy: {
type: 'wallet-gated',
addresses: ['0x123...', '0x456...']
}
});
console.log('Encrypted asset:', result.assetId);
console.log('Policy ID:', result.policyId);import { SealClient } from '@mysten/seal';
import { SuiClient, getFullnodeUrl } from '@mysten/sui/client';
const suiClient = new SuiClient({
url: getFullnodeUrl('testnet'),
});
const sealClient = new SealClient({ suiClient });
const sessionKey = await sealClient.getSessionKey(policyId);
const result = await walbucket.retrieve(assetId, {
sessionKey,
});
// Use the file data
const fileContent = result.data.toString('utf8');// Upload returns URL automatically
const uploadResult = await walbucket.upload(file);
console.log('File URL:', uploadResult.url);
// Retrieve also returns URL
const retrieveResult = await walbucket.retrieve(assetId);
console.log('File URL:', retrieveResult.url);
// Get asset metadata includes URL
const asset = await walbucket.getAsset(assetId);
if (asset) {
console.log('Asset URL:', asset.url);
// Use URL directly in your app
// <img src={asset.url} />
}When using gasStrategy: 'user-pays', users sign transactions through their wallet extensions. Here's the complete flow:
- User Connects Wallet: User connects their Sui wallet (Sui Wallet, Ethos, etc.) to your app
- Get Signer from Wallet: The wallet provides a
Signerinterface after connection - Pass Signer to SDK: Provide the signer to the SDK configuration
- Transactions Trigger Wallet Popups: When SDK methods are called, wallet popups appear for user approval
Step 1: Install Wallet Dependencies
pnpm add @mysten/dapp-kit @mysten/sui @tanstack/react-queryStep 2: Setup Wallet Provider (React)
import { createNetworkConfig, SuiClientProvider, WalletProvider } from '@mysten/dapp-kit';
import { getFullnodeUrl } from '@mysten/sui/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const { networkConfig } = createNetworkConfig({
testnet: { url: getFullnodeUrl('testnet') },
mainnet: { url: getFullnodeUrl('mainnet') },
});
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<SuiClientProvider networks={networkConfig} defaultNetwork="testnet">
<WalletProvider>
<YourApp />
</WalletProvider>
</SuiClientProvider>
</QueryClientProvider>
);
}Step 3: Use SDK with Wallet Signer
import { useCurrentWallet, ConnectButton } from '@mysten/dapp-kit';
import { Walbucket } from '@walbucket/sdk';
import { useEffect, useState } from 'react';
function YourApp() {
const { currentWallet, isConnected } = useCurrentWallet();
const [walbucket, setWalbucket] = useState<Walbucket | null>(null);
useEffect(() => {
if (isConnected && currentWallet?.account) {
// Create SDK instance with user's wallet signer
const sdk = new Walbucket({
apiKey: 'your-api-key',
network: 'testnet',
gasStrategy: 'user-pays',
userSigner: currentWallet.account, // Signer from connected wallet
});
setWalbucket(sdk);
}
}, [isConnected, currentWallet]);
const handleUpload = async (file: File) => {
if (!walbucket) {
alert('Please connect your wallet');
return;
}
// Wallet popup will appear for user to approve transaction
try {
const result = await walbucket.upload(file, {
name: file.name,
});
console.log('Uploaded!', result.url);
} catch (error) {
if (error.message.includes('User rejected')) {
console.log('User cancelled transaction');
}
}
};
return (
<div>
<ConnectButton />
{!isConnected && <p>Connect your wallet to continue</p>}
{walbucket && (
<input
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleUpload(file);
}}
/>
)}
</div>
);
}- Never ask for private keys: Always use wallet extensions to get signers
- Wallet popups are automatic: When SDK methods are called, the wallet extension will show popups for user approval
- User must have SUI tokens: For
user-paysstrategy, users need SUI tokens in their wallet for gas - Signer comes from wallet: The
userSigneris provided by the wallet after connection, not from private keys - Multiple transactions: Each SDK method call (upload, delete, etc.) will trigger a wallet popup for user approval
The SDK is fully typed with TypeScript:
import type {
WalbucketConfig,
UploadResult,
RetrieveResult,
AssetMetadata,
EncryptionPolicy
} from '@walbucket/sdk';
const config: WalbucketConfig = {
apiKey: 'your-api-key',
network: 'testnet',
sponsorPrivateKey: 'your-private-key',
};
const walbucket = new Walbucket(config);ISC