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.
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
| Network | network param | Default RPC URL | Description |
|---|
| Aeneid | "testnet" | https://aeneid.storyrpc.io | Current supported release |
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:
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 });
# 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.
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 Class | Code | When |
|---|
CDRError | varies | Base class for all SDK-specific errors |
WalletClientRequiredError | WALLET_CLIENT_REQUIRED | Accessing uploader or consumer without a walletClient |
ObserverRequiredError | OBSERVER_REQUIRED | Auto-querying globalPubKey / threshold without an observer |
InvalidParamsError | INVALID_PARAMS | Invalid parameter combinations, such as only passing one keypair parameter |
InvalidConditionContractError | INVALID_CONDITION_CONTRACT | Condition address does not implement the required interface |
LabelMismatchError | LABEL_MISMATCH | Ciphertext label does not match the vault UUID |
ContentSizeExceededError | CONTENT_SIZE_EXCEEDED | Encrypted data exceeds maxEncryptedDataSize |
PartialCollectionTimeoutError | PARTIAL_COLLECTION_TIMEOUT | collectPartials or accessCDR times out waiting for validator responses |
CidIntegrityError | CID_INTEGRITY | Downloaded encrypted file does not match the vault CID |
RpcConsensusError | RPC_CONSENSUS | Validation RPCs disagree about a DKG value |
ContractRevertError | CONTRACT_REVERT | On-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}`);
}
}