Waldrop SDK · Guide

Resume failed uploads

Uploads are a two-step thing — first the bytes land on Walrus, then a Sui transaction registers them on-chain. The most expensive step (PUT) is content-addressed and idempotent; the cheap step (tx) is where most failures actually happen. The SDK lets you retry just the tx without re-uploading bytes.

Where uploads break

The pipeline:

[SHA-256]millisecond, no network
[SEAL encrypt]if encrypted=true; pure crypto, no network
[publisher PUT]seconds to minutesslow, network-bound
[register_blob]~2swallet signature + Sui tx

The slow stage is the publisher PUT. The most failure-prone stage is the register tx — wallet rejections, plan-tier checks, network blips, gas shortage. About 95% of failures hit the register step after PUT succeeded, which means your bytes are already on Walrus.

That's the case worth optimizing.

What you get from the SDK

When register fails after PUT, the SDK throws RegistrationError with a .checkpoint field that carries everything needed to retry:

class RegistrationError extends WaldropError {
  readonly checkpoint: {
    blobId: string;
    sizeBytes: number;
    contentHash: Uint8Array;
    fileName: string;
    contentType: string;
    encrypted: boolean;
    sealMarker?: string;
  };
}

InsufficientGasError extends RegistrationError for the most common cause — wallet out of SUI. Same .checkpoint, but also .requiredMist and .address so you can surface a faucet prompt.

The recovery pattern

try {
  return await client.blob.upload({ … });
} catch (err) {
  if (!(err instanceof RegistrationError)) throw err;

  // 1. Stash the checkpoint somewhere durable.
  persistCheckpoint(err.checkpoint);

  // 2. Show the user what went wrong + how to fix it.
  if (err instanceof InsufficientGasError) {
    showFaucetPrompt(err.address, err.requiredMist);
  }

  // 3. Once the cause is fixed, retry register only.
  const result = await client.blob.registerOnly({
    checkpoint: loadCheckpoint()!,
    epochs, senderAddress, subscriptionId, signer, blobStoreId,
  });

  clearCheckpoint();
  return result;
}

registerOnly is idempotent on success — Walrus storage is paid at PUT time and survives any number of failed register attempts. Retry as often as needed.

Persisting the checkpoint

The checkpoint is plain JSON except contentHash, which is a Uint8Array. Serialize as a number array and reconstitute on load.

Node — filesystem

import { writeFileSync, readFileSync, existsSync, unlinkSync } from "node:fs";

const PATH = ".waldrop-checkpoint.json";

function persistCheckpoint(c: UploadCheckpoint) {
  writeFileSync(PATH, JSON.stringify(
    { ...c, contentHash: Array.from(c.contentHash) },
  ));
}
function loadCheckpoint(): UploadCheckpoint | null {
  if (!existsSync(PATH)) return null;
  const raw = JSON.parse(readFileSync(PATH, "utf8"));
  return { ...raw, contentHash: new Uint8Array(raw.contentHash) };
}
function clearCheckpoint() {
  if (existsSync(PATH)) unlinkSync(PATH);
}

Browser — localStorage

const KEY = "waldrop:checkpoint";

function persistCheckpoint(c: UploadCheckpoint) {
  localStorage.setItem(KEY, JSON.stringify(
    { ...c, contentHash: Array.from(c.contentHash) },
  ));
}
function loadCheckpoint(): UploadCheckpoint | null {
  const raw = localStorage.getItem(KEY);
  return raw
    ? { ...JSON.parse(raw), contentHash: new Uint8Array(JSON.parse(raw).contentHash) }
    : null;
}
function clearCheckpoint() {
  localStorage.removeItem(KEY);
}

Server-side — database row

For backend uploads, store the checkpoint as a JSON column on whatever "upload job" record you have. Keep the JSON shape minimal — blobId is the primary key you'll be querying on.

What about partial PUT failures?

Less common, but real. If the publisher PUT itself fails partway:

  • Network drop mid-PUT — restart from byte 0. Walrus is content-addressed, so if any of your prior partial PUT actually landed on storage nodes, the retry hits the alreadyCertified fast path and returns immediately.
  • Publisher timeout — same, restart from byte 0.

There's no chunked-resume primitive for a single PUT in this SDK. True chunked resume would require @mysten/walrus's direct-to-node writeBlobFlow — a heavier dep we deliberately don't pull in. The content-addressing trick gets you most of the way there for free.

The InsufficientGasError UX

Real-world testing surfaced this as the #1 first-run failure. The SDK detects it specifically:

catch (err) {
  if (err instanceof InsufficientGasError) {
    // err.address       → "0xd248…c759"
    // err.requiredMist  → 7694984
    // err.checkpoint    → resume payload
    return showFaucetCard({
      address: err.address,
      neededSui: (err.requiredMist / 1e9).toFixed(4),    // "0.0077"
      checkpoint: err.checkpoint,
    });
  }
}

Common message in your UI:

Your wallet needs ~0.008 SUI to record this upload on-chain. Top up via the testnet faucet and click Retry — your bytes are already on Walrus.

The 0.008 SUI cost is fixed per register tx (regardless of blob size).

Idempotency at the publisher

Worth knowing: re-uploading the same bytes returns the same blob_id and hits the publisher's alreadyCertified fast path. So even if you lose the checkpoint, re-running upload() with the same data is safe — you won't double-pay storage. The publisher just returns the existing blob_id in milliseconds.

This is why the SDK can guarantee blobId is stable across retries even without a checkpoint — Walrus content-addressing does the deduplication.

The mental model

The "checkpoint" is really just a memo to your application that you've already paid for storage. The bytes themselves are findable by re-PUTing or by knowing the blob_id. Lose the checkpoint, you'll re-PUT (cheap); keep it, you skip straight to register (cheaper).

Edit this page on GitHub ↗
Waldrop · 2026cryptokarigar