|
| 1 | +# @ocash/sdk |
| 2 | + |
| 3 | +> TypeScript ZKP SDK for privacy-preserving token operations via UTXO model and zk-SNARK proofs. |
| 4 | + |
| 5 | +## Install |
| 6 | + |
| 7 | +```bash |
| 8 | +pnpm add @ocash/sdk |
| 9 | +``` |
| 10 | + |
| 11 | +Three entry points: |
| 12 | +- `@ocash/sdk` — universal (MemoryStore) |
| 13 | +- `@ocash/sdk/browser` — browser (+ IndexedDbStore) |
| 14 | +- `@ocash/sdk/node` — Node.js (+ FileStore) |
| 15 | + |
| 16 | +## Quick Start |
| 17 | + |
| 18 | +```ts |
| 19 | +import { createSdk } from '@ocash/sdk'; |
| 20 | + |
| 21 | +const sdk = createSdk({ |
| 22 | + chains: [{ |
| 23 | + chainId: 11155111, |
| 24 | + entryUrl: 'https://entry.example.com', |
| 25 | + ocashContractAddress: '0x...', |
| 26 | + relayerUrl: 'https://relayer.example.com', |
| 27 | + merkleProofUrl: 'https://merkle.example.com', |
| 28 | + tokens: [], |
| 29 | + }], |
| 30 | + onEvent: (event) => console.log(event.type, event.payload), |
| 31 | +}); |
| 32 | + |
| 33 | +await sdk.core.ready(); // Load WASM & circuits |
| 34 | +await sdk.wallet.open({ seed: 'your-secret-seed' }); // Derive keys, init storage |
| 35 | +await sdk.sync.syncOnce(); // Sync memos, nullifiers, merkle |
| 36 | +const balance = await sdk.wallet.getBalance({ chainId: 11155111 }); |
| 37 | +await sdk.wallet.close(); // Release keys, flush storage |
| 38 | +``` |
| 39 | + |
| 40 | +## SDK Modules |
| 41 | + |
| 42 | +sdk.core — WASM & circuit initialization (ready, reset) |
| 43 | +sdk.keys — BabyJubjub key derivation (deriveKeyPair, userPkToAddress, addressToUserPk) |
| 44 | +sdk.crypto — Commitments, nullifiers, memo encryption (commitment, nullifier, createRecordOpening) |
| 45 | +sdk.assets — Chain/token/relayer configuration (getChains, getTokens, syncRelayerConfig) |
| 46 | +sdk.storage — Persistence adapter (upsertUtxos, listUtxos, markSpent, getSyncCursor) |
| 47 | +sdk.wallet — Session, UTXOs, balance (open, close, getUtxos, getBalance, markSpent) |
| 48 | +sdk.sync — Memo/nullifier/Merkle sync (start, stop, syncOnce, getStatus) |
| 49 | +sdk.merkle — Merkle proofs & membership witnesses (getProofByCids, buildInputSecretsFromUtxos) |
| 50 | +sdk.planner — Coin selection, fee estimation (estimate, estimateMax, plan) |
| 51 | +sdk.zkp — zk-SNARK proof generation (proveTransfer, proveWithdraw) |
| 52 | +sdk.tx — Relayer request builder (buildTransferCalldata, buildWithdrawCalldata) |
| 53 | +sdk.ops — End-to-end orchestration (prepareTransfer, prepareWithdraw, prepareDeposit, submitRelayerRequest) |
| 54 | + |
| 55 | +## Configuration |
| 56 | + |
| 57 | +```ts |
| 58 | +interface OCashSdkConfig { |
| 59 | + chains: ChainConfigInput[]; // Required: chain configurations |
| 60 | + assetsOverride?: AssetsOverride; // WASM/circuit file URLs (string or string[] for chunks) |
| 61 | + storage?: StorageAdapter; // Default: MemoryStore |
| 62 | + runtime?: 'auto' | 'browser' | 'node' | 'hybrid'; |
| 63 | + cacheDir?: string; // Node/hybrid: local asset cache directory |
| 64 | + merkle?: { mode?: 'remote' | 'local' | 'hybrid'; treeDepth?: number }; |
| 65 | + sync?: { pageSize?: number; pollMs?: number; requestTimeoutMs?: number; retry?: { attempts?: number; baseDelayMs?: number; maxDelayMs?: number } }; |
| 66 | + onEvent?: (event: SdkEvent) => void; // Event callback |
| 67 | +} |
| 68 | + |
| 69 | +interface ChainConfigInput { |
| 70 | + chainId: number; |
| 71 | + rpcUrl?: string; // JSON-RPC URL |
| 72 | + entryUrl?: string; // Entry Service (memo/nullifier sync) |
| 73 | + ocashContractAddress?: Address; // OCash contract |
| 74 | + relayerUrl?: string; // Relayer service |
| 75 | + merkleProofUrl?: string; // Merkle proof service |
| 76 | + tokens?: TokenMetadata[]; |
| 77 | +} |
| 78 | + |
| 79 | +interface TokenMetadata { |
| 80 | + id: string; symbol: string; decimals: number; wrappedErc20: Address; |
| 81 | + viewerPk: [string, string]; freezerPk: [string, string]; |
| 82 | + depositFeeBps?: number; withdrawFeeBps?: number; |
| 83 | + transferMaxAmount?: bigint | string; withdrawMaxAmount?: bigint | string; |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +## Transfer |
| 88 | + |
| 89 | +```ts |
| 90 | +const keyPair = sdk.keys.deriveKeyPair(seed, nonce); |
| 91 | + |
| 92 | +// Estimate fees first |
| 93 | +const estimate = await sdk.planner.estimate({ |
| 94 | + chainId: 11155111, assetId: 'token-id', action: 'transfer', amount: 500000n, |
| 95 | +}); |
| 96 | +// estimate.feeSummary.mergeCount / relayerFeeTotal / protocolFeeTotal |
| 97 | + |
| 98 | +// Max transferable |
| 99 | +const max = await sdk.planner.estimateMax({ |
| 100 | + chainId: 11155111, assetId: 'token-id', action: 'transfer', |
| 101 | +}); |
| 102 | +// max.maxSummary.outputAmount |
| 103 | + |
| 104 | +// Prepare (plan → merkle proof → witness → zk-SNARK proof → relayer request) |
| 105 | +const prepared = await sdk.ops.prepareTransfer({ |
| 106 | + chainId: 11155111, assetId: 'token-id', amount: 500000n, |
| 107 | + to: recipientViewingAddress, // Hex: BabyJubjub compressed address |
| 108 | + ownerKeyPair: keyPair, |
| 109 | + publicClient, // viem PublicClient |
| 110 | +}); |
| 111 | + |
| 112 | +// Submit to relayer |
| 113 | +const result = await sdk.ops.submitRelayerRequest({ prepared, publicClient }); |
| 114 | +const txHash = await result.waitRelayerTxHash; |
| 115 | +const receipt = await result.transactionReceipt; |
| 116 | +``` |
| 117 | + |
| 118 | +If more than 3 UTXOs needed, prepareTransfer returns `{ kind: 'merge' }` with merge steps. The planner handles this automatically with `autoMerge: true` (default). |
| 119 | + |
| 120 | +## Withdraw |
| 121 | + |
| 122 | +```ts |
| 123 | +const prepared = await sdk.ops.prepareWithdraw({ |
| 124 | + chainId: 11155111, assetId: 'token-id', amount: 500000n, |
| 125 | + recipient: '0x1234...abcd', // EVM address to receive tokens |
| 126 | + ownerKeyPair: keyPair, publicClient, |
| 127 | + gasDropValue: 10000000000000000n, // Optional: 0.01 ETH gas drop |
| 128 | +}); |
| 129 | +const result = await sdk.ops.submitRelayerRequest({ prepared, publicClient }); |
| 130 | +``` |
| 131 | + |
| 132 | +## Deposit |
| 133 | + |
| 134 | +```ts |
| 135 | +const ownerPub = sdk.keys.getPublicKeyBySeed(seed, nonce); |
| 136 | +const prepared = await sdk.ops.prepareDeposit({ |
| 137 | + chainId: 11155111, assetId: 'token-id', amount: 1000000n, |
| 138 | + ownerPublicKey: ownerPub, |
| 139 | + account: walletAddress, // Depositor's EOA |
| 140 | + publicClient, |
| 141 | +}); |
| 142 | + |
| 143 | +// ERC-20 approval if needed |
| 144 | +if (prepared.approveNeeded && prepared.approveRequest) { |
| 145 | + await walletClient.writeContract(prepared.approveRequest); |
| 146 | +} |
| 147 | +await walletClient.writeContract(prepared.depositRequest); |
| 148 | + |
| 149 | +// Or use submitDeposit for auto-approve: |
| 150 | +const result = await sdk.ops.submitDeposit({ |
| 151 | + prepared, walletClient, publicClient, autoApprove: true, |
| 152 | +}); |
| 153 | +``` |
| 154 | + |
| 155 | +## Key Management |
| 156 | + |
| 157 | +```ts |
| 158 | +// Derive key pair (seed must be >= 16 characters) |
| 159 | +const keyPair = sdk.keys.deriveKeyPair('my-secret-seed', 'optional-nonce'); |
| 160 | +// keyPair.user_sk.address_sk: bigint (secret key) |
| 161 | +// keyPair.user_pk.user_address: [bigint, bigint] (BabyJubjub public key) |
| 162 | + |
| 163 | +// Public key only (no secret key exposure) |
| 164 | +const pubKey = sdk.keys.getPublicKeyBySeed(seed, nonce); |
| 165 | + |
| 166 | +// Compress to viewing address |
| 167 | +const address = sdk.keys.userPkToAddress(pubKey); // 0x... (32 bytes) |
| 168 | + |
| 169 | +// Decompress back |
| 170 | +const pk = sdk.keys.addressToUserPk(address); |
| 171 | +``` |
| 172 | + |
| 173 | +## Sync |
| 174 | + |
| 175 | +```ts |
| 176 | +// One-shot sync |
| 177 | +await sdk.sync.syncOnce({ |
| 178 | + chainIds: [11155111], |
| 179 | + resources: ['memo', 'nullifier', 'merkle'], |
| 180 | + signal: abortController.signal, |
| 181 | +}); |
| 182 | + |
| 183 | +// Background polling |
| 184 | +await sdk.sync.start({ pollMs: 10_000 }); |
| 185 | +sdk.sync.stop(); // Stops polling + aborts in-flight sync |
| 186 | + |
| 187 | +// Check status |
| 188 | +const status = sdk.sync.getStatus(); |
| 189 | +// { 11155111: { memo: { status: 'synced', downloaded: 1291 }, ... } } |
| 190 | +``` |
| 191 | + |
| 192 | +## Storage Adapters |
| 193 | + |
| 194 | +```ts |
| 195 | +import { MemoryStore } from '@ocash/sdk'; |
| 196 | +import { IndexedDbStore } from '@ocash/sdk/browser'; |
| 197 | +import { FileStore } from '@ocash/sdk/node'; |
| 198 | + |
| 199 | +new MemoryStore({ maxOperations: 100 }) |
| 200 | +new IndexedDbStore({ dbName: 'myapp', maxOperations: 200 }) |
| 201 | +new FileStore({ baseDir: './data', maxOperations: 500 }) |
| 202 | +``` |
| 203 | + |
| 204 | +Required interface methods: |
| 205 | +- upsertUtxos(utxos: UtxoRecord[]): Promise<void> |
| 206 | +- listUtxos(query?: ListUtxosQuery): Promise<{ total: number; rows: UtxoRecord[] }> |
| 207 | +- markSpent(input: { chainId: number; nullifiers: Hex[] }): Promise<number> |
| 208 | +- getSyncCursor(chainId: number): Promise<SyncCursor | undefined> |
| 209 | +- setSyncCursor(chainId: number, cursor: SyncCursor): Promise<void> |
| 210 | + |
| 211 | +## Events |
| 212 | + |
| 213 | +All events via `onEvent` callback. Union type `SdkEvent`: |
| 214 | + |
| 215 | +- core:ready — { assetsVersion, durationMs } |
| 216 | +- core:progress — { stage: 'fetch'|'compile'|'init', loaded, total? } |
| 217 | +- sync:start — { chainId } |
| 218 | +- sync:progress — { chainId, resource: 'memo'|'nullifier'|'merkle', downloaded, total? } |
| 219 | +- sync:done — { chainId, cursor } |
| 220 | +- wallet:utxo:update — { chainId, added, spent, frozen } |
| 221 | +- zkp:start — { circuit: 'transfer'|'withdraw' } |
| 222 | +- zkp:done — { circuit, costMs } |
| 223 | +- error — { code: SdkErrorCode, message, detail?, cause? } |
| 224 | + |
| 225 | +Error codes: CONFIG | ASSETS | STORAGE | SYNC | CRYPTO | MERKLE | WITNESS | PROOF | RELAYER |
| 226 | + |
| 227 | +## Key Types |
| 228 | + |
| 229 | +```ts |
| 230 | +type Hex = `0x${string}`; |
| 231 | + |
| 232 | +interface UtxoRecord { |
| 233 | + chainId: number; assetId: string; amount: bigint; |
| 234 | + commitment: Hex; nullifier: Hex; mkIndex: number; |
| 235 | + isFrozen: boolean; isSpent: boolean; memo?: Hex; createdAt?: number; |
| 236 | +} |
| 237 | + |
| 238 | +interface CommitmentData { |
| 239 | + asset_id: bigint; asset_amount: bigint; |
| 240 | + user_pk: { user_address: [bigint, bigint] }; |
| 241 | + blinding_factor: bigint; is_frozen: boolean; |
| 242 | +} |
| 243 | + |
| 244 | +interface SyncCursor { memo: number; nullifier: number; merkle: number; } |
| 245 | + |
| 246 | +type OperationType = 'deposit' | 'transfer' | 'withdraw'; |
| 247 | +type OperationStatus = 'pending' | 'submitted' | 'confirmed' | 'failed'; |
| 248 | + |
| 249 | +interface ProofResult { proof: string; publicInputs: string[]; } |
| 250 | +interface RelayerRequest { kind: 'relayer'; method: 'POST'; path: string; body: Record<string, unknown>; } |
| 251 | +``` |
| 252 | + |
| 253 | +## Cryptography |
| 254 | + |
| 255 | +- Curve: BabyJubjub (twisted Edwards over BN254 scalar field) |
| 256 | +- Hash: Poseidon2 (commitments, nullifiers, Merkle nodes) |
| 257 | +- Encryption: ECDH + NaCl XSalsa20-Poly1305 (memo encryption) |
| 258 | +- Key derivation: HKDF-SHA256 (seed → spending key) |
| 259 | +- Proofs: Groth16 zk-SNARK (Go WASM, transfer & withdraw circuits) |
| 260 | +- commitment = Poseidon2(asset_id, amount, pk.x, pk.y, blinding_factor) |
| 261 | +- nullifier = Poseidon2(commitment, secret_key, merkle_index) |
| 262 | + |
| 263 | +## Static Utilities |
| 264 | + |
| 265 | +```ts |
| 266 | +import { MemoKit, CryptoToolkit, KeyManager, Utils } from '@ocash/sdk'; |
| 267 | + |
| 268 | +MemoKit.createMemo(recordOpening) // Encrypt record opening → Hex memo |
| 269 | +MemoKit.decodeMemoForOwner({ secretKey, memo }) // Decrypt memo → CommitmentData | null |
| 270 | + |
| 271 | +CryptoToolkit.commitment(data) // Poseidon2 commitment |
| 272 | +CryptoToolkit.nullifier(secretKey, commitment) // Nullifier derivation |
| 273 | +CryptoToolkit.createRecordOpening({ assetId, amount, userPk }) |
| 274 | + |
| 275 | +Utils.calcDepositFee(amount, feeBps) // Protocol fee calculation |
| 276 | +Utils.randomBytes32Bigint() // Cryptographic random bigint |
| 277 | +``` |
0 commit comments