SDK Structure
The SDK is a TypeScript library that exposes the encrypted-balance protocol as viem action extensions. It owns key-derived account types, ZK proof generation, ElGamal decryption, and ERC-4337 submission. Persistent storage, key management UX, and account discovery are the application's responsibility.
Architecture decisions: ADR-016, ADR-017.
Layers
Runtime (decryption + proving services; WASM-backed; long-lived)
│
Account (keys in closures: decrypt, prove, sign)
│
Client (viem PublicClient or BundlerClient + ZKP action extensions)
│
Chain (EVM JSON-RPC + ERC-4337 bundler)
One runtime per process; accounts derive from it; clients compose via factories or manual .extend() chains.
Action extensions
The public surface — each one a plain viem extension:
| Extension | Required client | Hoists | Purpose |
|---|---|---|---|
zkpPublicActions | PublicClient | — | Chain reads — ciphertexts, registration state, balance logs |
zkpEvmActions | WalletClient or BundlerLike | — | EVM-side writes by EOA — sendPublicToEncryptedTransfer, sendSetAutoEncrypt, signPermit |
zkpPreparedActions | WalletClient or BundlerLike | — | Submit pre-built PreparedTransactions |
zkpDecryptActions | zkpPublicActions | ZkpReadAccount | Decrypt balances and history |
zkpWalletActions | zkpPublicActions + zkpPreparedActions | ZkpAccount | Orchestrated sendEncryptedTransfer, sendEncryptedToPublicTransfer, sendRegisterEpk |
erc3009Actions | bundler | optional account | EIP-3009 transferWithAuthorization for plain ERC-20 |
TypeScript enforces composition order at compile time. ERC-20 reads and publicTransfer are not wrapped — viem handles them natively.
Account
Three capability interfaces split so that decrypt / prove / sign can be granted independently — e.g. a proving service holds the ESK while custody (Fireblocks, Safe, etc.) holds the CSK and signs (ADR-014).
interface ZkpReadAccount { zkpAddress; decrypt(ct) }
interface ZkpProvingAccount extends ZkpReadAccount { prove(req) }
interface ZkpSigningAccount { controllerAddress; signControllerAuth(typed) }
interface ZkpAccount extends ZkpProvingAccount, ZkpSigningAccount {}
Runtime
A runtime composes a solver (and optionally a prover) with account factories. Two variants:
ReadRuntimeholds aSolverand exposescreateReadAccount,createReadAccountFromMnemonic,createReadAccountFromSeed.ZkpRuntime extends ReadRuntimeadditionally holds aProverand exposescreateAccount,createAccountFromMnemonic,createAccountFromSeed,prewarmProver.
One per process. Pick ReadRuntime or ZkpRuntime at startup; you don't upgrade between them. Both expose destroy() to release WASM threads at shutdown.
@cardinal-cryptography/sdk exposes createReadRuntime and createRuntime. @cardinal-cryptography/core ships assembleRuntime(solver, prover) and assembleReadRuntime(solver) for tests and custom backends.
Convenience factories
| Factory | Backed by | Account binding | Use |
|---|---|---|---|
createDecryptClient | PublicClient | ZkpReadAccount | Read + decrypt; no submission |
createEvmClient | BundlerClient | LocalAccount (EOA permit signer) | Public-to-encrypted transfer, auto-encrypt toggle |
createFullClient | BundlerClient | ZkpAccount | Full encrypted-balance flows |
Custom token addresses and bespoke compositions drop down to manual .extend() chains.
Transaction submission
Default routing is paymaster → ERC-4337 bundler → classic eth_sendTransaction, picked by capability detection on the client. Through the bundler, msg.sender is the SharedAccount contract; security comes from the ZK proof and EIP-712 controller signature in calldata, not from tx.origin. The user holds no ETH, and the controller key never appears on-chain as the gas-payer.
The classic WalletClient path bypasses the bundler — msg.sender is the user's EOA and the unlinkability above does not hold. No convenience factory; assemble manually.
See Relayer for the wire protocol with the paymaster backend.
Packages and platform targets
@cardinal-cryptography/sdk ships a single npm package with conditional exports resolving per platform — Node and browser today, React Native when added.
| Condition | Entry | Solver | Prover |
|---|---|---|---|
node | index.node.js | DlogSolver (Node WASM build) | NoirProver (NoirJS + bb.js, worker pool) |
browser / default | index.browser.js | DlogSolver (browser WASM, multi-threaded if crossOriginIsolated) | NoirProver (NoirJS + bb.js) |
@cardinal-cryptography/core holds the platform-agnostic surface (extensions, account types, runtime interfaces, client factories, EIP-712 builders, sponsor middleware) and depends only on viem; @cardinal-cryptography/sdk re-exports it alongside the WASM-backed backends.