Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.story.foundation/llms.txt

Use this file to discover all available pages before exploring further.

These docs track the Aeneid release of @piplabs/cdr-sdk (v0.2.1).

Prerequisites

  • Node.js 18+ and npm 8+
  • Node.js 22+ if you plan to use HeliaProvider
  • A funded wallet on Aeneid testnet
  • viem (v2.21+) for blockchain interactions

Install

npm install @piplabs/cdr-sdk viem
viem (v2.21+) is a required peer dependency.
Storage providers are optional and pull their own peer dependencies. Install them only for the backend you use: helia, multiformats, and @helia/unixfs for HeliaProvider, @storacha/client for StorachaProvider, or @filoz/synapse-sdk for SynapseProvider.
If you plan to mint Story license tokens in an IP-gated flow, also install @story-protocol/core-sdk.

Initialize WASM

The CDR SDK uses a WebAssembly module for threshold cryptography. You must initialize it once before performing any encryption or decryption operations.
import { initWasm } from "@piplabs/cdr-sdk";

// Call once at application startup
await initWasm();
In a React application, initialize WASM in a provider component or top-level effect so it’s ready before any CDR operations are attempted.

Browser and Bundler Guidance

  • Vite / webpack - Import the SDK from normal ESM application code and call initWasm() before the first encryption or decryption. If your SSR build tries to evaluate the SDK server-side, move the import behind a client-only boundary.
  • Next.js / SSR - Keep browser wallet flows in "use client" components. For route handlers or scripts that use CDR cryptography, run them in the Node runtime instead of Edge.
  • Edge runtime - The current release is not documented for Edge runtimes. Prefer the browser or Node.js runtime on Aeneid.
  • TypeScript - Use modern ESM resolution. moduleResolution: "Bundler" is a good default for browser apps; moduleResolution: "NodeNext" fits pure Node ESM projects.
// Next.js route handlers / server actions
export const runtime = "nodejs";

Story-API REST Endpoint

Every CDRClient requires an apiUrl — the base URL of a Story-API REST endpoint. The SDK reads all DKG state (active round, global public key, threshold, participant count, registered validators, and validator attestations) over this REST API. Contract state such as vaults and fees is still read over the EVM publicClient.
NetworkStory-API REST URLNotes
Aeneidhttp://172.192.41.96:1317Plain HTTP. May change between deployments.
For production deployments you can point apiUrl at your own Story node’s REST gateway instead of the shared endpoint. Configure it through an environment variable so it is easy to swap.

Create the CDR Client

The CDRClient provides three sub-clients:
  • observer - Read-only queries (fees, vault data, DKG state). Always available.
  • uploader - Encryption and vault allocation. Requires a walletClient.
  • consumer - Decryption and read requests. Requires a walletClient.

In React (Wallet Connector)

In a React app, you typically get the wallet from a connector like Privy, RainbowKit, or wagmi. Create a read-only CDRClient up front, and build a write-capable client on demand from the wallet’s provider.
hooks/use-cdr-client.ts
import { useMemo } from "react";
import { createPublicClient, createWalletClient, custom, http } from "viem";
import { CDRClient } from "@piplabs/cdr-sdk";

// Example using Privy — adapt for your wallet connector
import { usePrivy, useWallets } from "@privy-io/react-auth";

export function useCDRClient() {
  const { authenticated } = usePrivy();
  const { wallets } = useWallets();
  const wallet = wallets[0];

  // Read-only client — always available
  const publicClient = useMemo(
    () =>
      createPublicClient({ transport: http(process.env.NEXT_PUBLIC_RPC_URL) }),
    [],
  );

  const apiUrl = process.env.NEXT_PUBLIC_STORY_API_URL!;

  const client = useMemo(
    () => new CDRClient({ network: "testnet", publicClient, apiUrl }),
    [publicClient, apiUrl],
  );

  // Write client — created on demand from the wallet's provider
  const getWriteClient = async () => {
    if (!wallet) throw new Error("No wallet connected");
    const provider = await wallet.getEthereumProvider();
    const walletClient = createWalletClient({
      transport: custom(provider),
      account: wallet.address as `0x${string}`,
    });
    return new CDRClient({
      network: "testnet",
      publicClient,
      walletClient,
      apiUrl,
    });
  };

  return { client, publicClient, getWriteClient, address: wallet?.address };
}
Then in your components:
const { client, getWriteClient } = useCDRClient();

// Read-only operations work immediately
const vault = await client.observer.getVault(42);

// Write operations — get a write client first
const writeClient = await getWriteClient();
await writeClient.uploader.write({ uuid, accessAuxData: "0x", encryptedData });

With Private Key (Backend / Scripts)

For server-side code, scripts, or CLI tools, you can use a private key directly:
import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { CDRClient } from "@piplabs/cdr-sdk";

const account = privateKeyToAccount(`0x${process.env.WALLET_PRIVATE_KEY}`);

const publicClient = createPublicClient({
  transport: http(process.env.RPC_PROVIDER_URL),
});

const walletClient = createWalletClient({
  account,
  transport: http(process.env.RPC_PROVIDER_URL),
});

const client = new CDRClient({
  network: "testnet",
  publicClient,
  walletClient,
  apiUrl: process.env.STORY_API_URL!,
});

Read-Only (No Wallet)

If you only need to query vault data or DKG state, you can omit the walletClient:
const client = new CDRClient({
  network: "testnet",
  publicClient,
  apiUrl: process.env.STORY_API_URL!,
});

// observer methods work without a wallet
const vault = await client.observer.getVault(123);
const allocateFee = await client.observer.getAllocateFee();
Attempting to use client.uploader or client.consumer without a walletClient will throw a WalletClientRequiredError.

Network Configuration

Supported Network

Networknetwork paramDefault RPC URLStory-API REST URLDescription
Aeneid"testnet"https://aeneid.storyrpc.iohttp://172.192.41.96:1317Current supported release
Testnet
const publicClient = createPublicClient({
  transport: http("https://aeneid.storyrpc.io"),
});
const client = new CDRClient({
  network: "testnet",
  publicClient,
  apiUrl: "http://172.192.41.96:1317",
});

Custom RPC URL

You can point the SDK to any Aeneid-compatible RPC endpoint by changing the http() transport URL. This is useful for third-party RPC providers with higher rate limits. The apiUrl is configured independently — point it at the shared Story-API endpoint or your own Story node’s REST gateway.
const publicClient = createPublicClient({
  transport: http("https://your-aeneid-rpc.example.com"),
});
const walletClient = createWalletClient({
  account,
  transport: http("https://your-aeneid-rpc.example.com"),
});

// Use "testnet"
const client = new CDRClient({
  network: "testnet",
  publicClient,
  walletClient,
  apiUrl: "http://172.192.41.96:1317",
});

Using Environment Variables

A common pattern is to configure the network via environment variables:
config.ts
const RPC_URL = process.env.RPC_URL ?? "https://aeneid.storyrpc.io";
const STORY_API_URL = process.env.STORY_API_URL ?? "http://172.192.41.96:1317";
const NETWORK = (process.env.NETWORK ?? "testnet") as "testnet";

const publicClient = createPublicClient({ transport: http(RPC_URL) });
const client = new CDRClient({
  network: NETWORK,
  publicClient,
  apiUrl: STORY_API_URL,
});
.env
# Testnet (default)
RPC_URL=https://aeneid.storyrpc.io
STORY_API_URL=http://172.192.41.96:1317
NETWORK=testnet

# Alternate Aeneid RPC
RPC_URL=https://your-aeneid-rpc.example.com
STORY_API_URL=http://your-story-node:1317
NETWORK=testnet

Quick Start: End-to-End Secret Example

The script below creates an owner-only vault, writes a small secret, then reads it back with the same wallet. It is fully runnable once WALLET_PRIVATE_KEY is set.
quickstart-cdr.ts
import { CDRClient, initWasm, uuidToLabel } from "@piplabs/cdr-sdk";
import {
  createPublicClient,
  createWalletClient,
  http,
  toHex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";

const RPC_URL = process.env.RPC_URL ?? "https://aeneid.storyrpc.io";
const STORY_API_URL = process.env.STORY_API_URL ?? "http://172.192.41.96:1317";
const PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY as `0x${string}` | undefined;

if (!PRIVATE_KEY) {
  throw new Error("Set WALLET_PRIVATE_KEY before running this script.");
}

const account = privateKeyToAccount(PRIVATE_KEY);
const publicClient = createPublicClient({ transport: http(RPC_URL) });
const walletClient = createWalletClient({
  account,
  transport: http(RPC_URL),
});

await initWasm();

const client = new CDRClient({
  network: "testnet",
  publicClient,
  walletClient,
  apiUrl: STORY_API_URL,
});

// Use the wallet (EOA) address as both write and read condition. Only this
// EOA can encrypt to or decrypt from the vault.
const { uuid, txHash: allocateTx } = await client.uploader.allocate({
  updatable: false,
  writeConditionAddr: account.address,
  readConditionAddr: account.address,
  writeConditionData: "0x",
  readConditionData: "0x",
  skipConditionValidation: true,
});

const globalPubKey = await client.observer.getGlobalPubKey();
const ciphertext = await client.uploader.encryptDataKey({
  dataKey: new TextEncoder().encode("hello from CDR"),
  globalPubKey,
  label: uuidToLabel(uuid),
});

const { txHash: writeTx } = await client.uploader.write({
  uuid,
  accessAuxData: "0x",
  encryptedData: toHex(ciphertext.raw),
});

console.log("Vault UUID:", uuid);
console.log("Allocate tx:", allocateTx);
console.log("Write tx:", writeTx);

const { dataKey, txHash } = await client.consumer.accessCDR({
  uuid,
  accessAuxData: "0x",
  timeoutMs: 120_000,
});

console.log("Read tx:", txHash);
console.log("Recovered secret:", new TextDecoder().decode(dataKey));
This example sends three transactions total: allocate(), write(), and read(). For larger payloads, switch to uploadFile() / downloadFile() with deployed condition contracts (such as the Story license-gated pattern in IP Asset Vaults).
Any EOA address works as a write or read condition — only that EOA can perform the matching action. The high-level uploadCDR() / uploadFile() helpers validate that condition addresses point at deployed contracts, so EOA conditions go through the low-level allocate() call with skipConditionValidation: true.

Next Steps

Error Handling

The SDK throws typed errors you can catch and handle:
Error ClassCodeWhen
CDRErrorvariesBase class for all SDK-specific errors
WalletClientRequiredErrorWALLET_CLIENT_REQUIREDAccessing uploader or consumer without a walletClient
InvalidParamsErrorINVALID_PARAMSInvalid parameter combinations, such as only passing one keypair parameter
InvalidConditionContractErrorINVALID_CONDITION_CONTRACTCondition address does not implement the required interface
LabelMismatchErrorLABEL_MISMATCHCiphertext label does not match the vault UUID
ContentSizeExceededErrorCONTENT_SIZE_EXCEEDEDEncrypted data exceeds maxEncryptedDataSize
EmptyVaultErrorEMPTY_VAULTReading a vault that has never been written to
PartialCollectionTimeoutErrorPARTIAL_COLLECTION_TIMEOUTcollectPartials or accessCDR times out waiting for validator responses
CidIntegrityErrorCID_INTEGRITYDownloaded encrypted file does not match the vault CID
On-chain transaction reverts (for example, a failed condition check) surface as the underlying viem contract errors, not a CDR-specific error class.
All errors extend CDRError, which has a code property for programmatic handling:
import { CDRError, PartialCollectionTimeoutError } from "@piplabs/cdr-sdk";

try {
  const { dataKey } = await client.consumer.accessCDR({ ... });
} catch (err) {
  if (err instanceof PartialCollectionTimeoutError) {
    console.error("Not enough validators responded in time. Try increasing timeoutMs.");
  } else if (err instanceof CDRError) {
    console.error(`CDR error [${err.code}]: ${err.message}`);
  }
}