Waldrop SDK · Integration
Node.js / CLI
Backend scripts, CLIs, GitHub Actions, cron jobs — any non-browser context
with a keypair available. Wrap the keypair in a TransactionSigner adapter
once and the SDK works identically to the dapp case.
Install
npm install @waldrop/sdk @mysten/sui
# Optional:
npm install @mysten/seal
Runtime: Node 18+, Deno (with Node compat), or Bun. The SDK uses fetch
and crypto.subtle — both built-in on those runtimes.
The signer adapter
TransactionSigner is a 1-method interface matching dapp-kit's shape. For
Node, wrap your keypair once and reuse:
import { Ed25519Keypair } from " @mysten/sui/keypairs/ed25519 " ;
import { SuiGrpcClient } from " @mysten/sui/grpc " ;
import type { Transaction } from " @mysten/sui/transactions " ;
import { WaldropClient , type TransactionSigner } from " @waldrop/sdk " ;
const keypair = Ed25519Keypair . fromSecretKey ( process . env . WALDROP_PRIVATE_KEY ! );
const senderAddress = keypair . toSuiAddress ();
const suiClient = new SuiGrpcClient ({
network : " testnet " ,
baseUrl : " https://fullnode.testnet.sui.io:443 " ,
});
const signer : TransactionSigner = {
async signAndExecuteTransaction ({ transaction }) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const r = await ( suiClient as any ). signAndExecuteTransaction ({
transaction : transaction as Transaction ,
signer : keypair ,
include : { effects : true }, // needed to read created BlobStore id
});
const digest = r ?. digest ?? r ?. effects ?. transactionDigest ?? "" ;
if ( digest ) {
// Wait for fullnode indexing so the digest is queryable immediately.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await ( suiClient as any ). waitForTransaction ?.({ digest });
}
return { digest , effects : r ?. effects };
},
};
export const waldrop = new WaldropClient ({ network : " testnet " , suiClient });
export { keypair , senderAddress , signer };
That's the only Node-specific code. Every other example below imports
waldrop, senderAddress, and signer from this file.
A simple CLI
#!/usr/bin/env bun
import { readFileSync , writeFileSync , existsSync } from " node:fs " ;
import { basename } from " node:path " ;
import {
waldrop , signer , senderAddress , keypair ,
} from " ../src/lib/waldrop " ;
import { InsufficientGasError , RegistrationError } from " @waldrop/sdk " ;
const [ cmd , ... args ] = process . argv . slice ( 2 );
switch ( cmd ) {
case " upload " : {
const [ path ] = args ;
if ( ! path || ! existsSync ( path )) {
console . error ( " usage: waldrop-cli upload <file> " );
process . exit ( 1 );
}
const data = readFileSync ( path );
try {
const r = await waldrop . blob . upload ({
data : new Uint8Array ( data ),
fileName : basename ( path ),
contentType : guessMime ( path ),
epochs : 26 ,
senderAddress ,
subscriptionId : process . env . WALDROP_SUBSCRIPTION_ID ! ,
signer ,
onProgress : ( e ) =>
process . stdout . write ( ` \r [ ${ e . stage } ] ${ e . percent } % ` ),
});
console . log ( ` \n ✓ ${ r . blobId } ` );
console . log ( ` tx: ${ r . transactionDigest } ` );
} catch ( err ) {
if ( err instanceof InsufficientGasError ) {
console . error ( ` \n ✗ wallet ${ err . address } needs ${ err . requiredMist } MIST ` );
} else if ( err instanceof RegistrationError ) {
// persist for `waldrop-cli resume`
writeFileSync (
" .waldrop-resume.json " ,
JSON . stringify ({
... err . checkpoint ,
contentHash : Array . from ( err . checkpoint . contentHash ),
}),
);
console . error ( " \n ✗ register failed — checkpoint saved. run `resume`. " );
} else throw err ;
}
break ;
}
case " list " : {
const owner = args [ 0 ] ?? senderAddress ;
const blobs = await waldrop . blob . list ({ owner });
for ( const b of blobs ) {
console . log (
b . blobId ,
b . sizeDisplay . padEnd ( 10 ),
b . encrypted ? " 🔒 " : " " ,
b . originalName ,
);
}
break ;
}
case " fetch " : {
const [ blobId , out ] = args ;
if ( ! blobId ) {
console . error ( " usage: waldrop-cli fetch <blob-id> [out-path] " );
process . exit ( 1 );
}
const { bytes , contentType } = await waldrop . blob . fetch ({ blobId });
const outPath = out ?? ` ${ blobId } .bin ` ;
writeFileSync ( outPath , bytes );
console . log ( ` ✓ ${ bytes . byteLength } bytes → ${ outPath } ( ${ contentType } ) ` );
break ;
}
case " cost " : {
const sizeMb = Number ( args [ 0 ] ?? " 10 " );
const epochs = Number ( args [ 1 ] ?? " 26 " );
const r = await waldrop . cost . estimate ({
bytesPerBlob : sizeMb * 1024 * 1024 ,
epochs ,
});
console . log ( ` ${ r . totalWal . toFixed ( 6 ) } WAL · ${ r . totalUnits } units × ${ epochs } epochs ` );
break ;
}
case " resume " : {
const raw = JSON . parse ( readFileSync ( " .waldrop-resume.json " , " utf8 " ));
const checkpoint = { ... raw , contentHash : new Uint8Array ( raw . contentHash ) };
const r = await waldrop . blob . registerOnly ({
checkpoint ,
epochs : 26 ,
senderAddress ,
subscriptionId : process . env . WALDROP_SUBSCRIPTION_ID ! ,
signer ,
});
console . log ( ` ✓ resumed: ${ r . transactionDigest } ` );
break ;
}
default :
console . error ( " usage: waldrop-cli [upload|list|fetch|cost|resume] " );
process . exit ( 1 );
}
function guessMime ( path : string ) {
const ext = path . split ( " . " ). pop ()?. toLowerCase () ?? "" ;
return ({
txt : " text/plain " , md : " text/markdown " ,
json : " application/json " , csv : " text/csv " ,
png : " image/png " , jpg : " image/jpeg " , jpeg : " image/jpeg " ,
pdf : " application/pdf " ,
} as Record < string , string >)[ ext ] ?? " application/octet-stream " ;
}
Run it:
export WALDROP_PRIVATE_KEY = suiprivkey1 ...
export WALDROP_SUBSCRIPTION_ID = 0x ...
bun bin/waldrop-cli.ts upload ./README.md
bun bin/waldrop-cli.ts list
bun bin/waldrop-cli.ts fetch < blob-i d > ./out.md
bun bin/waldrop-cli.ts cost 100 26 # 100 MiB · 26 epochs
GitHub Actions / CI
For automated uploads (release artifacts, dataset snapshots):
name : upload-to-waldrop
on :
release :
types : [ published ]
jobs :
upload :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- uses : oven-sh/setup-bun@v1
- run : bun install
- run : bun run scripts/upload-release.ts
env :
WALDROP_PRIVATE_KEY : ${{ secrets.WALDROP_PRIVATE_KEY }}
WALDROP_SUBSCRIPTION_ID : ${{ secrets.WALDROP_SUBSCRIPTION_ID }}
Inside scripts/upload-release.ts, build a tar bundle and call
waldrop.blob.uploadBundle({ … }). One on-chain tx per release.
Never commit private keys
Use GitHub Actions secrets, Doppler, Vercel env vars — anything but git.
The SDK accepts the Bech32 suiprivkey… format directly via
Ed25519Keypair.fromSecretKey.
Bun considerations
Bun runs everything above natively — no Node-specific shims needed. Two
notes:
crypto.subtle works the same as in Node 19+.
Bun's bundled SQLite makes it easy to track checkpoints across many
concurrent uploads — wire the resume pattern into a bun:sqlite
table instead of one JSON file per upload.
Backend service pattern
For an HTTP service that uploads on behalf of users (e.g. "drop a file in
S3, we mirror to Walrus"), keep the keypair server-side and expose a thin
API:
// app.ts (Hono / Express / Fastify)
import { Hono } from " hono " ;
import { waldrop , senderAddress , signer } from " ./lib/waldrop " ;
const app = new Hono ();
app . post ( " /upload " , async ( c ) => {
const file = await c . req . arrayBuffer ();
const filename = c . req . header ( " x-filename " ) ?? " untitled " ;
const contentType = c . req . header ( " content-type " ) ?? " application/octet-stream " ;
const r = await waldrop . blob . upload ({
data : new Uint8Array ( file ),
fileName : filename , contentType ,
epochs : 26 , senderAddress ,
subscriptionId : process . env . WALDROP_SUBSCRIPTION_ID ! ,
signer ,
});
return c . json ({ blobId : r . blobId , tx : r . transactionDigest });
});
export default app ;
The Walrus blob ownership goes to your server's wallet — fine for
service-managed scenarios. For user-owned blobs, the user has to sign the
register tx themselves (front-end flow).
← Integrations
Next.js + dapp-kit
Integrations →
Migrate from S3
Waldrop · 2026 cryptokarigar