Waldrop SDK · Integration

Next.js + dapp-kit

Browser dapp setup. Reuse the dapp-kit SuiGrpcClient for both wallet ops and Waldrop reads — one connection, one wallet picker, one signer. This is the path the Waldrop dapp itself uses.

Install

npm install @waldrop/sdk @mysten/sui @mysten/dapp-kit-react
# Optional — only if you'll encrypt / decrypt:
npm install @mysten/seal

Provider setup

Wrap your app in DAppKitProvider. The SDK will pick up the same client your wallets do.

import { createDAppKit } from "@mysten/dapp-kit-react";
import { SuiGrpcClient } from "@mysten/sui/grpc";

export const dAppKit = createDAppKit({
  networks: ["testnet", "mainnet"],
  createClient: (network) =>
    new SuiGrpcClient({
      network,
      baseUrl:
        network === "testnet"
          ? "https://fullnode.testnet.sui.io:443"
          : "https://fullnode.mainnet.sui.io:443",
    }),
});

declare module "@mysten/dapp-kit-react" {
  interface Register {
    dAppKit: typeof dAppKit;
  }
}
"use client";

import { DAppKitProvider } from "@mysten/dapp-kit-react";
import { dAppKit } from "@/config/dapp-kit";

export function Providers({ children }: { children: React.ReactNode }) {
  return <DAppKitProvider dAppKit={dAppKit}>{children}</DAppKitProvider>;
}
import { Providers } from "./providers";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

useWaldrop hook

One memoized client per dapp-kit client change:

"use client";

import { useMemo } from "react";
import { useCurrentClient } from "@mysten/dapp-kit-react";
import { WaldropClient } from "@waldrop/sdk";

export function useWaldrop() {
  const client = useCurrentClient();
  return useMemo(
    () => new WaldropClient({ network: "testnet", suiClient: client }),
    [client],
  );
}

useCurrentClient() returns the active network's SuiGrpcClient — when the user switches networks, the WaldropClient is recreated automatically.

Connect + upload component

"use client";

import { useState } from "react";
import {
  ConnectButton,
  useCurrentAccount,
  useDAppKit,
} from "@mysten/dapp-kit-react";
import {
  RegistrationError,
  InsufficientGasError,
  type UploadProgressEvent,
} from "@waldrop/sdk";
import { useWaldrop } from "@/hooks/useWaldrop";

export function Upload({
  subscriptionId,
  blobStoreId,
}: {
  subscriptionId: string;
  blobStoreId?: string;
}) {
  const account = useCurrentAccount();
  const dAppKit = useDAppKit();
  const waldrop = useWaldrop();

  const [stage, setStage] = useState<UploadProgressEvent | null>(null);
  const [blobId, setBlobId] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);

  if (!account) {
    return <ConnectButton />;
  }

  async function handleFile(file: File) {
    setError(null);
    setBlobId(null);
    try {
      const result = await waldrop.blob.upload({
        data: new Uint8Array(await file.arrayBuffer()),
        fileName: file.name,
        contentType: file.type || "application/octet-stream",
        epochs: 26,
        senderAddress: account!.address,
        subscriptionId,
        blobStoreId,
        signer: dAppKit,                       // ← dAppKit is a TransactionSigner
        onProgress: setStage,
      });
      setBlobId(result.blobId);
    } catch (err) {
      if (err instanceof InsufficientGasError) {
        setError(`Wallet needs ~${(err.requiredMist! / 1e9).toFixed(4)} SUI for gas.`);
      } else if (err instanceof RegistrationError) {
        // bytes are safe on Walrus — persist checkpoint for retry
        localStorage.setItem(
          "waldrop:checkpoint",
          JSON.stringify({
            ...err.checkpoint,
            contentHash: Array.from(err.checkpoint.contentHash),
          }),
        );
        setError("Registration failed — checkpoint saved for retry.");
      } else {
        setError(err instanceof Error ? err.message : "Upload failed");
      }
    }
  }

  return (
    <div>
      <input
        type="file"
        onChange={(e) => e.target.files?.[0] && handleFile(e.target.files[0])}
      />
      {stage && <p>[{stage.stage}] {stage.percent}%</p>}
      {blobId && <p>✓ uploaded as {blobId}</p>}
      {error && <p style={{ color: "var(--red-tx)" }}>{error}</p>}
    </div>
  );
}

The key line: signer: dAppKit. dapp-kit's useDAppKit() returns an object that already matches the SDK's TransactionSigner interface — no adapter needed.

Reading without a connected wallet

Read operations don't need an account — drop them in any component:

"use client";

import { useEffect, useState } from "react";
import { useWaldrop } from "@/hooks/useWaldrop";
import type { BlobRef } from "@waldrop/sdk";

export function BlobList({ owner }: { owner: string }) {
  const waldrop = useWaldrop();
  const [blobs, setBlobs] = useState<BlobRef[] | null>(null);

  useEffect(() => {
    waldrop.blob.list({ owner }).then(setBlobs);
  }, [waldrop, owner]);

  if (!blobs) return <p>Loading…</p>;
  return (
    <ul>
      {blobs.map((b) => (
        <li key={b.blobId}>
          {b.originalName} · {b.sizeDisplay} {b.encrypted && "🔒"}
        </li>
      ))}
    </ul>
  );
}

Decrypt with the user's wallet

The signer for client.crypto.decrypt is whatever @mysten/seal expects — dapp-kit's signer works as-is:

"use client";

import {
  useCurrentAccount,
  useCurrentClient,
  useDAppKit,
} from "@mysten/dapp-kit-react";
import { useWaldrop } from "@/hooks/useWaldrop";

export function useDecrypt() {
  const waldrop = useWaldrop();
  const client = useCurrentClient() as any;  // SealCompatibleClient

  return async (blob: { blobId: string; sealPolicyId: string }) => {
    const { bytes } = await waldrop.blob.fetch({ blobId: blob.blobId });
    return await waldrop.crypto.decrypt({
      bytes,
      blobStoreId: blob.sealPolicyId,
      signer: client,                         // SEAL needs the client, not dAppKit
    });
  };
}

The first decrypt prompts a personal-message signature for the session key. Subsequent decrypts within ~10 minutes reuse the cached session.

Server-side rendering caveats

The SDK is client-only for upload + decrypt — those need a wallet. For read flows in a Server Component, instantiate a standalone client:

import { WaldropClient } from "@waldrop/sdk";

export default async function BlobsPage() {
  const waldrop = new WaldropClient({ network: "testnet" });
  const blobs = await waldrop.blob.list({
    owner: "0x9d655392521726d0…",
  });

  return (
    <ul>
      {blobs.map((b) => (
        <li key={b.blobId}>{b.originalName}</li>
      ))}
    </ul>
  );
}

This is a Server Component — runs at request time on your Next server. Good for SEO + public profile pages.

Don't mix server and client clients

Don't pass WaldropClient instances across the server/client boundary. They're not serialisable. Construct them where they're used — once per Server Component request, once per useWaldrop() call client-side.

Vercel deployment

The SDK has no Node-only APIs in the hot paths (uses fetch, crypto.subtle, no fs / child_process). Deploys to Vercel Edge or Node runtimes both work out of the box.

If you're calling SDK methods from a Server Component or Route Handler, they run server-side on Node — no special config needed.

Edit this page on GitHub ↗
Waldrop · 2026cryptokarigar