Skip to main content
These docs track the Aeneid release of @piplabs/cdr-sdk (v0.1.1). The SDK is not yet published to npm, so install it from source for now.

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 from Source

git clone https://github.com/piplabs/cdr-sdk.git --branch 0.1.1 --depth 1
cd cdr-sdk && pnpm install && pnpm build
cd ../your-project
npm install ../cdr-sdk/packages/sdk viem
viem (v2.21+) is a required peer dependency.
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";

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 client = useMemo(
    () => new CDRClient({ network: "testnet", publicClient }),
    [publicClient],
  );

  // 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,
    });
  };

  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,
});

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,
});

// 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 URLDescription
Aeneid"testnet"https://aeneid.storyrpc.ioCurrent supported release
Testnet
const publicClient = createPublicClient({
  transport: http("https://aeneid.storyrpc.io"),
});
const client = new CDRClient({ network: "testnet", publicClient });

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.
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,
});

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 NETWORK = (process.env.NETWORK ?? "testnet") as "testnet";

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

# Alternate Aeneid RPC
RPC_URL=https://your-aeneid-rpc.example.com
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 } from "@piplabs/cdr-sdk";
import {
  createPublicClient,
  createWalletClient,
  encodeAbiParameters,
  http,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";

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

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,
});

const ownerConditionData = encodeAbiParameters(
  [{ type: "address" }],
  [account.address],
);

const { uuid, txHashes } = await client.uploader.uploadCDR({
  dataKey: new TextEncoder().encode("hello from CDR"),
  globalPubKey: await client.observer.getGlobalPubKey(),
  updatable: false,
  writeConditionAddr: OWNER_CONDITION,
  readConditionAddr: OWNER_CONDITION,
  writeConditionData: ownerConditionData,
  readConditionData: ownerConditionData,
  accessAuxData: "0x",
});

console.log("Vault UUID:", uuid);
console.log("Allocate tx:", txHashes.allocate);
console.log("Write tx:", txHashes.write);

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().

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
ObserverRequiredErrorOBSERVER_REQUIREDAuto-querying globalPubKey / threshold without an observer
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
PartialCollectionTimeoutErrorPARTIAL_COLLECTION_TIMEOUTcollectPartials or accessCDR times out waiting for validator responses
CidIntegrityErrorCID_INTEGRITYDownloaded encrypted file does not match the vault CID
RpcConsensusErrorRPC_CONSENSUSValidation RPCs disagree about a DKG value
ContractRevertErrorCONTRACT_REVERTOn-chain transaction reverted
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}`);
  }
}