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-id> ./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).

Edit this page on GitHub ↗
Waldrop · 2026cryptokarigar