메인 콘텐츠로 건너뛰기
이 문서는 @piplabs/cdr-sdk의 Aeneid 릴리스(v0.2.1)를 추적합니다.

사전 요구 사항

  • Node.js 18+ 및 npm 8+
  • HeliaProvider를 사용할 계획이라면 Node.js 22+
  • Aeneid 테스트넷의 자금이 충전된 지갑
  • 블록체인 상호 작용을 위한 viem (v2.21+)

설치

npm install @piplabs/cdr-sdk viem
viem (v2.21+)은 필수 피어 종속성입니다.
스토리지 프로바이더는 선택 사항이며 자체 피어 종속성을 가져옵니다. 사용하는 백엔드에 대해서만 설치하세요: HeliaProvider의 경우 helia, multiformats@helia/unixfs, StorachaProvider의 경우 @storacha/client, 또는 SynapseProvider의 경우 @filoz/synapse-sdk.
IP 게이트 흐름에서 DATA Foundation 라이선스 토큰을 발행할 계획이라면 @story-protocol/core-sdk도 설치하세요.

WASM 초기화

CDR SDK는 임계값 암호학을 위해 WebAssembly 모듈을 사용합니다. 암호화 또는 복호화 작업을 수행하기 전에 한 번 초기화해야 합니다.
import { initWasm } from "@piplabs/cdr-sdk";

// Call once at application startup
await initWasm();
React 애플리케이션에서는 CDR 작업이 시도되기 전에 준비되도록 프로바이더 컴포넌트 또는 최상위 효과에서 WASM을 초기화하세요.

브라우저 및 번들러 가이드

  • Vite / webpack - 일반 ESM 애플리케이션 코드에서 SDK를 가져오고 첫 번째 암호화 또는 복호화 전에 initWasm()을 호출하세요. SSR 빌드가 서버 측에서 SDK를 평가하려고 하면 클라이언트 전용 경계 뒤로 가져오기를 이동하세요.
  • Next.js / SSR - 브라우저 지갑 흐름을 "use client" 컴포넌트에 유지하세요. CDR 암호학을 사용하는 라우트 핸들러 또는 스크립트는 Edge 대신 Node 런타임에서 실행하세요.
  • Edge 런타임 - 현재 릴리스는 Edge 런타임용으로 문서화되지 않았습니다. Aeneid에서는 브라우저 또는 Node.js 런타임을 선호하세요.
  • TypeScript - 최신 ESM 해석을 사용하세요. moduleResolution: "Bundler"는 브라우저 앱에 좋은 기본값이며; moduleResolution: "NodeNext"는 순수 Node ESM 프로젝트에 적합합니다.
// Next.js route handlers / server actions
export const runtime = "nodejs";

DATA Foundation API REST 엔드포인트

모든 CDRClient에는 apiUrl이 필요합니다: DATA Foundation API REST 엔드포인트의 기본 URL입니다. SDK는 모든 DKG 상태(활성 라운드, 글로벌 공개 키, 임계값, 참가자 수, 등록된 검증자 및 검증자 증명)를 이 REST API를 통해 읽습니다. 볼트 및 수수료와 같은 컨트랙트 상태는 여전히 EVM publicClient를 통해 읽습니다.
네트워크DATA Foundation API REST URL비고
Aeneidhttp://172.192.41.96:1317일반 HTTP. 배포 간에 변경될 수 있습니다.
프로덕션 배포의 경우 공유 엔드포인트 대신 자체 Story 노드의 REST 게이트웨이를 apiUrl로 지정할 수 있습니다. 쉽게 교체할 수 있도록 환경 변수를 통해 구성하세요.

CDR 클라이언트 생성

CDRClient는 세 가지 서브 클라이언트를 제공합니다:
  • observer - 읽기 전용 쿼리(수수료, 볼트 데이터, DKG 상태). 항상 사용 가능합니다.
  • uploader - 암호화 및 볼트 할당. walletClient가 필요합니다.
  • consumer - 복호화 및 읽기 요청. walletClient가 필요합니다.

React에서 (지갑 커넥터)

React 앱에서는 일반적으로 Privy, RainbowKit 또는 wagmi와 같은 커넥터에서 지갑을 가져옵니다. 미리 읽기 전용 CDRClient를 생성하고, 지갑의 프로바이더에서 필요에 따라 쓰기 가능한 클라이언트를 빌드하세요.
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_DATAFDN_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 };
}
그런 다음 컴포넌트에서:
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 });

프라이빗 키 사용 (백엔드 / 스크립트)

서버 측 코드, 스크립트 또는 CLI 도구의 경우 프라이빗 키를 직접 사용할 수 있습니다:
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.DATAFDN_API_URL!,
});

읽기 전용 (지갑 없이)

볼트 데이터 또는 DKG 상태만 쿼리해야 하는 경우 walletClient를 생략할 수 있습니다:
const client = new CDRClient({
  network: "testnet",
  publicClient,
  apiUrl: process.env.DATAFDN_API_URL!,
});

// observer methods work without a wallet
const vault = await client.observer.getVault(123);
const allocateFee = await client.observer.getAllocateFee();
walletClient 없이 client.uploader 또는 client.consumer를 사용하려고 시도하면 WalletClientRequiredError가 발생합니다.

네트워크 구성

지원되는 네트워크

네트워크network 매개변수기본 RPC URLDATA Foundation API REST URL설명
Aeneid"testnet"https://aeneid.datarpc.iohttp://172.192.41.96:1317현재 지원되는 릴리스
Testnet
const publicClient = createPublicClient({
  transport: http("https://aeneid.datarpc.io"),
});
const client = new CDRClient({
  network: "testnet",
  publicClient,
  apiUrl: "http://172.192.41.96:1317",
});

사용자 정의 RPC URL

http() 전송 URL을 변경하여 SDK를 모든 Aeneid 호환 RPC 엔드포인트로 지정할 수 있습니다. 이는 더 높은 속도 제한이 있는 타사 RPC 프로바이더에 유용합니다. apiUrl은 독립적으로 구성되며, 공유 DATA Foundation API 엔드포인트 또는 자체 Story 노드의 REST 게이트웨이로 지정하세요.
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",
});

환경 변수 사용

일반적인 패턴은 환경 변수를 통해 네트워크를 구성하는 것입니다:
config.ts
const RPC_URL = process.env.RPC_URL ?? "https://aeneid.datarpc.io";
const DATAFDN_API_URL = process.env.DATAFDN_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: DATAFDN_API_URL,
});
.env
# Testnet (default)
RPC_URL=https://aeneid.datarpc.io
DATAFDN_API_URL=http://172.192.41.96:1317
NETWORK=testnet

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

빠른 시작: 엔드 투 엔드 비밀 예제

아래 스크립트는 소유자 전용 볼트를 생성하고, 작은 비밀을 작성한 다음, 같은 지갑으로 다시 읽어옵니다. WALLET_PRIVATE_KEY가 설정되면 완전히 실행 가능합니다.
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.datarpc.io";
const DATAFDN_API_URL = process.env.DATAFDN_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: DATAFDN_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));
이 예제는 총 세 개의 트랜잭션을 전송합니다: allocate(), write()read(). 더 큰 페이로드의 경우 배포된 조건 컨트랙트와 함께 uploadFile() / downloadFile()로 전환하세요(예: IP Asset Vaults의 DATA Foundation 라이선스 게이트 패턴).
어떤 EOA 주소든 쓰기 또는 읽기 조건으로 작동하며, 해당 EOA만이 매칭되는 동작을 수행할 수 있습니다. 고수준 uploadCDR() / uploadFile() 헬퍼는 조건 주소가 배포된 컨트랙트를 가리키는지 검증하므로, EOA 조건은 skipConditionValidation: true와 함께 저수준 allocate() 호출을 통해 진행됩니다.

다음 단계

오류 처리

SDK는 잡아서 처리할 수 있는 형식화된 오류를 던집니다:
오류 클래스코드발생 시점
CDRError다양모든 SDK 특정 오류의 기본 클래스
WalletClientRequiredErrorWALLET_CLIENT_REQUIREDwalletClient 없이 uploader 또는 consumer에 접근
InvalidParamsErrorINVALID_PARAMS잘못된 매개변수 조합(예: 키 쌍 매개변수 하나만 전달)
InvalidConditionContractErrorINVALID_CONDITION_CONTRACT조건 주소가 필요한 인터페이스를 구현하지 않음
LabelMismatchErrorLABEL_MISMATCH암호문 레이블이 볼트 UUID와 일치하지 않음
ContentSizeExceededErrorCONTENT_SIZE_EXCEEDED암호화된 데이터가 maxEncryptedDataSize를 초과함
EmptyVaultErrorEMPTY_VAULT한 번도 쓰여진 적 없는 볼트를 읽음
PartialCollectionTimeoutErrorPARTIAL_COLLECTION_TIMEOUTcollectPartials 또는 accessCDR이 검증자 응답 대기 중 타임아웃됨
CidIntegrityErrorCID_INTEGRITY다운로드한 암호화 파일이 볼트 CID와 일치하지 않음
온체인 트랜잭션 되돌리기(예: 실패한 조건 검사)는 CDR 특정 오류 클래스가 아닌 기본 viem 컨트랙트 오류로 나타납니다.
모든 오류는 프로그래밍 처리를 위한 code 속성을 가진 CDRError를 확장합니다:
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}`);
  }
}