Waldrop SDK · Recipe

Upload an encrypted blob

SEAL-encrypt before the publisher PUT. Only the BlobStore owner (and allowlisted viewers) can later decrypt — the ciphertext on Walrus is public-readable but unreadable.

One signature, not two

Encryption uses public key-server data only — no wallet prompt. The single wallet signature in the upload pipeline is the on-chain register tx at the end. Decryption later does require a signature (SEAL session key).

Prerequisites

npm install @waldrop/sdk @mysten/sui @mysten/seal

You also need an existing BlobStore — SEAL identities are scoped to a specific store id, so encrypted blobs can't be uploaded into a fresh PTB- created store in the same call.

// One-time: do a plaintext upload first to create your BlobStore.
const first = await client.blob.upload({ /* plaintext */ });
const blobStoreId = first.blobStoreId!;   // reuse this for all future uploads

Recipe

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 suiClient = new SuiGrpcClient({
  network: "testnet",
  baseUrl: "https://fullnode.testnet.sui.io:443",
});
const signer: TransactionSigner = {
  async signAndExecuteTransaction({ transaction }) {
    const r = await (suiClient as any).signAndExecuteTransaction({
      transaction: transaction as Transaction,
      signer: keypair,
      include: { effects: true },
    });
    return { digest: r?.digest, effects: r?.effects };
  },
};

const client = new WaldropClient({ network: "testnet", suiClient });

const plaintext = new TextEncoder().encode(
  "internal-only · do not log this · @waldrop/sdk demo",
);

const result = await client.blob.upload({
  data: plaintext,
  fileName: "secret.txt",
  contentType: "text/plain",
  epochs: 26,
  senderAddress: keypair.toSuiAddress(),
  subscriptionId: process.env.WALDROP_SUBSCRIPTION_ID!,
  signer,
  blobStoreId: process.env.WALDROP_BLOB_STORE_ID!,
  encrypted: true,                                 // ← the only difference
  onProgress: (e) => console.log(`[${e.stage}] ${e.percent}%`),
});

console.log(result.blobId);
console.log(result.sealMarker);                    // 32-char hex marker

What changes vs plaintext

[SHA-256]of original bytes (always)
[SEAL encrypt]NEW53-byte identity = store_id || marker || nonce
[publisher PUT]uploads CIPHERTEXT (larger than plaintext)
[register_blob]BlobRef.encrypted = true, BlobRef.seal_marker recorded

The marker is recorded on-chain so per-blob shares (add_blob_share) can key off it without needing to know the full SEAL identity.

Output

{
  blobId: "AmKt3…",
  sizeBytes: 50,                       // ORIGINAL bytes (pre-encryption)
  storedEpoch: 1098,
  expiryEpoch: 1124,
  transactionDigest: "DqW8…",
  blobStoreId: "0x…",
  sealMarker: "0xbde56d92087f77615e76856798ef6421",
}

Read it back

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

const { bytes } = await client.blob.fetch({ blobId: result.blobId });
// bytes is still SEAL ciphertext at this point.

try {
  const plaintext = await client.crypto.decrypt({
    bytes,
    blobStoreId: result.blobStoreId!,
    signer: keypair,                   // signs the SEAL session-key message
  });
  console.log(new TextDecoder().decode(plaintext));
} catch (e) {
  if (e instanceof DecryptionError) {
    // Wrong store id, not on viewer ACL, or malformed ciphertext.
  }
  throw e;
}
Session key caching

SEAL caches the session key in memory after the first prompt — subsequent decrypt() calls within ~10 minutes won't re-prompt. In a browser, also cached in sessionStorage so it survives page reloads but not new tabs.

Edit this page on GitHub ↗
Waldrop · 2026cryptokarigar