@zama-fhe/sdk 1.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,763 @@
1
+ # @zama-fhe/sdk
2
+
3
+ A TypeScript SDK for building privacy-preserving token applications using Fully Homomorphic Encryption (FHE). It abstracts the complexity of encrypted ERC-20 operations — shielding, unshielding, confidential transfers, and balance decryption — behind a clean, high-level API. Works with any Web3 library (viem, ethers, or custom signers).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @zama-fhe/sdk
9
+ ```
10
+
11
+ ### Peer dependencies
12
+
13
+ | Package | Version | Required? |
14
+ | ----------------------- | ------- | -------------------------------------------------- |
15
+ | `viem` | >= 2 | Optional — for the `@zama-fhe/sdk/viem` adapter |
16
+ | `ethers` | >= 6 | Optional — for the `@zama-fhe/sdk/ethers` adapter |
17
+ | `@zama-fhe/relayer-sdk` | >= 0.4 | Optional — only for `@zama-fhe/sdk/node` (Node.js) |
18
+
19
+ ## Quick Start
20
+
21
+ ### Browser
22
+
23
+ ```ts
24
+ import { ZamaSDK, RelayerWeb, IndexedDBStorage } from "@zama-fhe/sdk";
25
+ import { ViemSigner } from "@zama-fhe/sdk/viem";
26
+ import { mainnet, sepolia } from "viem/chains";
27
+
28
+ // 1. Create signer and relayer
29
+ const signer = new ViemSigner({ walletClient, publicClient });
30
+
31
+ const sdk = new ZamaSDK({
32
+ relayer: new RelayerWeb({
33
+ getChainId: () => signer.getChainId(),
34
+ transports: {
35
+ [mainnet.id]: {
36
+ relayerUrl: "https://your-app.com/api/relayer/1",
37
+ network: "https://mainnet.infura.io/v3/YOUR_KEY",
38
+ },
39
+ [sepolia.id]: {
40
+ relayerUrl: "https://your-app.com/api/relayer/11155111",
41
+ network: "https://sepolia.infura.io/v3/YOUR_KEY",
42
+ },
43
+ },
44
+ }),
45
+ signer,
46
+ storage: new IndexedDBStorage(),
47
+ });
48
+
49
+ // 2. Create a token instance (wrapper is auto-discovered if omitted)
50
+ const token = sdk.createToken("0xEncryptedERC20Address");
51
+ // Or provide the wrapper explicitly:
52
+ // const token = sdk.createToken("0xEncryptedERC20Address", "0xWrapperAddress");
53
+
54
+ // 3. Shield (wrap) public tokens into confidential tokens
55
+ const { txHash } = await token.shield(1000n);
56
+
57
+ // 4. Check decrypted balance
58
+ const balance = await token.balanceOf();
59
+ console.log("Confidential balance:", balance);
60
+
61
+ // 5. Transfer confidential tokens
62
+ const transferTx = await token.confidentialTransfer("0xRecipient", 500n);
63
+ ```
64
+
65
+ ### Node.js
66
+
67
+ ```ts
68
+ import { ZamaSDK } from "@zama-fhe/sdk";
69
+ import { RelayerNode, asyncLocalStorage } from "@zama-fhe/sdk/node";
70
+ import { ViemSigner } from "@zama-fhe/sdk/viem";
71
+ import { mainnet, sepolia } from "viem/chains";
72
+
73
+ const signer = new ViemSigner({ walletClient, publicClient });
74
+
75
+ const sdk = new ZamaSDK({
76
+ relayer: new RelayerNode({
77
+ getChainId: () => signer.getChainId(),
78
+ poolSize: 4, // number of worker threads (default: min(CPUs, 4))
79
+ transports: {
80
+ [mainnet.id]: {
81
+ network: "https://mainnet.infura.io/v3/YOUR_KEY",
82
+ auth: { __type: "ApiKeyHeader", value: process.env.RELAYER_API_KEY },
83
+ },
84
+ [sepolia.id]: {
85
+ network: "https://sepolia.infura.io/v3/YOUR_KEY",
86
+ auth: { __type: "ApiKeyHeader", value: process.env.RELAYER_API_KEY },
87
+ },
88
+ },
89
+ }),
90
+ signer,
91
+ storage: asyncLocalStorage,
92
+ });
93
+
94
+ const token = sdk.createToken("0xEncryptedERC20Address");
95
+ const balance = await token.balanceOf();
96
+ ```
97
+
98
+ ## Core Concepts
99
+
100
+ ### ZamaSDK
101
+
102
+ Entry point to the SDK. Composes a relayer backend with a signer and storage layer. Acts as a factory for token instances.
103
+
104
+ ```ts
105
+ const sdk = new ZamaSDK({
106
+ relayer, // RelayerSDK — either RelayerWeb (browser) or RelayerNode (Node.js)
107
+ signer, // GenericSigner
108
+ storage, // GenericStringStorage
109
+ });
110
+
111
+ // Read-only — balances, metadata, decryption. No wrapper needed.
112
+ const readonlyToken = sdk.createReadonlyToken("0xTokenAddress");
113
+
114
+ // Full read/write — shield, unshield, transfer, approve.
115
+ // The token address IS the wrapper (encrypted ERC20 = wrapper contract).
116
+ const token = sdk.createToken("0xTokenAddress");
117
+ // Override wrapper if it differs from the token address (rare):
118
+ // const token = sdk.createToken("0xTokenAddress", "0xWrapperAddress");
119
+ ```
120
+
121
+ The `relayer`, `signer`, and `storage` properties are public and accessible after construction. Low-level FHE operations (`encrypt`, `userDecrypt`, `publicDecrypt`, `generateKeypair`, etc.) are available via `sdk.relayer`. Call `sdk.terminate()` to clean up resources when done.
122
+
123
+ ### Relayer Backends
124
+
125
+ The `RelayerSDK` interface defines the FHE operations contract. Two implementations are provided:
126
+
127
+ | Backend | Import | Environment | How it works |
128
+ | ------------- | -------------------- | ----------- | ------------------------------------------ |
129
+ | `RelayerWeb` | `@zama-fhe/sdk` | Browser | Runs WASM in a Web Worker via CDN |
130
+ | `RelayerNode` | `@zama-fhe/sdk/node` | Node.js | Uses `@zama-fhe/relayer-sdk/node` directly |
131
+
132
+ The `/node` sub-path also exports `NodeWorkerClient` and `NodeWorkerClientConfig` for running FHE operations in a Node.js worker thread.
133
+
134
+ You can also implement the `RelayerSDK` interface for custom backends.
135
+
136
+ ### Token
137
+
138
+ Full read/write interface for a single confidential ERC-20. Extends `ReadonlyToken`. The encrypted ERC-20 contract IS the wrapper, so `wrapper` defaults to the token `address`. Pass an explicit `wrapper` only if they differ.
139
+
140
+ | Method | Description |
141
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
142
+ | `shield(amount, options?)` | Shield (wrap) public ERC-20 tokens. Handles approval automatically. Options: `{ approvalStrategy: "max" \| "exact" \| "skip" }` (default `"exact"`). `"skip"` bypasses approval (use when already approved). |
143
+ | `shieldETH(amount, value?)` | Shield (wrap) native ETH. `value` defaults to `amount`. Use this when the underlying token is the zero address (native ETH). |
144
+ | `unshield(amount, callbacks?)` | Unwrap a specific amount and finalize in one call. Orchestrates: unwrap → wait receipt → parse event → finalizeUnwrap. Optional `UnshieldCallbacks` for progress tracking. |
145
+ | `unshieldAll(callbacks?)` | Unwrap the entire balance and finalize in one call. Orchestrates: unwrapAll → wait receipt → parse event → finalizeUnwrap. Optional `UnshieldCallbacks` for progress tracking. |
146
+ | `unwrap(amount)` | Request unwrap for a specific amount (low-level, requires manual finalization). |
147
+ | `unwrapAll()` | Request unwrap for the entire balance (low-level, requires manual finalization). |
148
+ | `resumeUnshield(unwrapTxHash, callbacks?)` | Resume an interrupted unshield from an existing unwrap tx hash. Goes straight to wait receipt → finalize. |
149
+ | `finalizeUnwrap(burnAmountHandle)` | Complete unwrap with public decryption proof. |
150
+ | `confidentialTransfer(to, amount)` | Encrypted transfer. Encrypts amount, then calls the contract. |
151
+ | `confidentialTransferFrom(from, to, amt)` | Operator encrypted transfer. |
152
+ | `approve(spender, until?)` | Set operator approval. `until` defaults to now + 1 hour. |
153
+ | `isApproved(spender)` | Check if a spender is an approved operator. |
154
+ | `approveUnderlying(amount?)` | Approve wrapper to spend underlying ERC-20. Default: max uint256. |
155
+ | `balanceOf(owner?)` | Decrypt and return the plaintext balance. |
156
+ | `decryptHandles(handles, owner?)` | Batch-decrypt arbitrary encrypted handles. |
157
+
158
+ All write methods return a `TransactionResult` object:
159
+
160
+ ```ts
161
+ interface TransactionResult {
162
+ txHash: Hex;
163
+ receipt: TransactionReceipt;
164
+ }
165
+ ```
166
+
167
+ ### ReadonlyToken
168
+
169
+ Read-only subset. No wrapper address needed.
170
+
171
+ | Method | Description |
172
+ | ------------------------------------- | ----------------------------------------------------------------- |
173
+ | `balanceOf(owner?)` | Decrypt and return the plaintext balance. |
174
+ | `confidentialBalanceOf(owner?)` | Return the raw encrypted balance handle (no decryption). |
175
+ | `decryptBalance(handle, owner?)` | Decrypt a single encrypted handle. |
176
+ | `decryptHandles(handles, owner?)` | Batch-decrypt handles in a single relayer call. |
177
+ | `authorize()` | Ensure FHE decrypt credentials exist (generates/signs if needed). |
178
+ | `authorizeAll(tokens)` _(static)_ | Pre-authorize multiple tokens with a single wallet signature. |
179
+ | `isConfidential()` | ERC-165 check for ERC-7984 support. |
180
+ | `isWrapper()` | ERC-165 check for wrapper interface. |
181
+ | `discoverWrapper(coordinatorAddress)` | Look up a wrapper for this token via the deployment coordinator. |
182
+ | `underlyingToken()` | Read the underlying ERC-20 address from a wrapper. |
183
+ | `allowance(wrapper, owner?)` | Read ERC-20 allowance of the underlying token. |
184
+ | `isZeroHandle(handle)` | Returns `true` if the handle is the zero sentinel. |
185
+ | `name()` / `symbol()` / `decimals()` | Read token metadata. |
186
+
187
+ Static methods for multi-token operations:
188
+
189
+ ```ts
190
+ // Pre-authorize all tokens with a single wallet signature
191
+ const tokens = addresses.map((a) => sdk.createReadonlyToken(a));
192
+ await ReadonlyToken.authorizeAll(tokens);
193
+ // All subsequent decrypts reuse cached credentials — no more wallet prompts
194
+
195
+ // Decrypt balances for multiple tokens in parallel
196
+ const balances = await ReadonlyToken.batchDecryptBalances(tokens, { owner });
197
+
198
+ // Decrypt pre-fetched handles for multiple tokens
199
+ const balances = await ReadonlyToken.batchDecryptBalances(tokens, { handles, owner });
200
+ ```
201
+
202
+ ### Pending Unshield Persistence
203
+
204
+ The unshield flow is two-phase: unwrap tx, then finalize. If the page reloads between phases, the unwrap tx hash is lost. Use these utilities to persist it:
205
+
206
+ ```ts
207
+ import { savePendingUnshield, loadPendingUnshield, clearPendingUnshield } from "@zama-fhe/sdk";
208
+
209
+ // Save the unwrap hash before finalization
210
+ await savePendingUnshield(storage, wrapperAddress, unwrapTxHash);
211
+
212
+ // On next load, check for pending unshields
213
+ const pending = await loadPendingUnshield(storage, wrapperAddress);
214
+ if (pending) {
215
+ await token.resumeUnshield(pending);
216
+ await clearPendingUnshield(storage, wrapperAddress);
217
+ }
218
+ ```
219
+
220
+ ### Storage
221
+
222
+ FHE credentials (keypair + EIP-712 signature) are persisted to storage. Three options:
223
+
224
+ | Storage | Use case |
225
+ | ------------------- | -------------------------------------------------------- |
226
+ | `indexedDBStorage` | Browser apps — persists across page reloads and sessions |
227
+ | `memoryStorage` | Tests, scripts, throwaway sessions |
228
+ | `asyncLocalStorage` | Node.js servers — isolate credentials per request |
229
+ | Custom | Implement the `GenericStringStorage` interface |
230
+
231
+ ```ts
232
+ interface GenericStringStorage {
233
+ getItem(key: string): string | Promise<string | null> | null;
234
+ setItem(key: string, value: string): void | Promise<void>;
235
+ removeItem(key: string): void | Promise<void>;
236
+ }
237
+ ```
238
+
239
+ ## Configuration Reference
240
+
241
+ ### `ZamaSDKConfig`
242
+
243
+ | Field | Type | Description |
244
+ | ------------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
245
+ | `relayer` | `RelayerSDK` | Relayer backend (`RelayerWeb` or `RelayerNode` instance) |
246
+ | `signer` | `GenericSigner` | Wallet signer interface. |
247
+ | `storage` | `GenericStringStorage` | Credential storage backend. |
248
+ | `credentialDurationDays` | `number` | Optional. Days FHE credentials remain valid. Default: 1. Set `0` to require a wallet signature on every decrypt (high-security mode). |
249
+ | `onEvent` | `ZamaSDKEventListener` | Optional. Structured event listener for debugging. |
250
+
251
+ #### Structured Event Listener
252
+
253
+ The `onEvent` callback receives typed events at key lifecycle points. Event payloads never contain sensitive data (amounts, keys, proofs) — only metadata useful for debugging and telemetry.
254
+
255
+ ```ts
256
+ const sdk = new ZamaSDK({
257
+ relayer,
258
+ signer,
259
+ storage,
260
+ onEvent: ({ type, tokenAddress, ...event }) => {
261
+ console.debug(`[Zama] ${type}`, {
262
+ tokenAddress: tokenAddress?.slice(0, 10),
263
+ ...event,
264
+ });
265
+ },
266
+ });
267
+ ```
268
+
269
+ **Event types:**
270
+
271
+ | Category | Events | Key fields |
272
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
273
+ | Credentials | `credentials:loading`, `credentials:cached`, `credentials:expired`, `credentials:creating`, `credentials:created` | `contractAddresses` |
274
+ | Encryption | `encrypt:start`, `encrypt:end`, `encrypt:error` | `durationMs` (end/error), `error` (error) |
275
+ | Decryption | `decrypt:start`, `decrypt:end`, `decrypt:error` | `durationMs` (end/error), `error` (error) |
276
+ | Transactions | `transaction:error` | `operation` (`"transfer"`, `"wrap"`, `"approve"`, etc.), `error` |
277
+ | Write confirmations | `wrap:submitted`, `transfer:submitted`, `transferFrom:submitted`, `approve:submitted`, `approveUnderlying:submitted`, `unwrap:submitted`, `finalizeUnwrap:submitted` | `txHash` |
278
+ | Unshield orchestration | `unshield:phase1_submitted`, `unshield:phase2_started`, `unshield:phase2_submitted` | `txHash`, `operationId` |
279
+
280
+ All events carry `tokenAddress`, `timestamp`, and an optional `operationId` (set on unshield phase events to correlate multi-step operations).
281
+
282
+ **Dispatching events to other systems:**
283
+
284
+ The `onEvent` callback is a simple function — you can bridge it to any event system:
285
+
286
+ ```ts
287
+ // Fan out to multiple listeners with EventEmitter
288
+ import { EventEmitter } from "events";
289
+ const emitter = new EventEmitter();
290
+ const sdk = new ZamaSDK({
291
+ // ...
292
+ onEvent: (event) => emitter.emit(event.type, event),
293
+ });
294
+ emitter.on("encrypt:start", (e) => {
295
+ /* listener A */
296
+ });
297
+ emitter.on("encrypt:start", (e) => {
298
+ /* listener B */
299
+ });
300
+
301
+ // Bridge to DOM CustomEvent (e.g. for cross-framework communication)
302
+ const sdk = new ZamaSDK({
303
+ // ...
304
+ onEvent: (event) => window.dispatchEvent(new CustomEvent(event.type, { detail: event })),
305
+ });
306
+
307
+ // Collect into React state
308
+ const [events, setEvents] = useState<ZamaSDKEvent[]>([]);
309
+ const sdk = new ZamaSDK({
310
+ // ...
311
+ onEvent: (event) => setEvents((prev) => [...prev, event]),
312
+ });
313
+ ```
314
+
315
+ ### `RelayerWebConfig` (browser)
316
+
317
+ | Field | Type | Description |
318
+ | ------------ | ------------------------------------- | -------------------------------------------------------------------------------------------- |
319
+ | `getChainId` | `() => Promise<number>` | Resolve the current chain ID. Called lazily; the worker is re-initialized on chain change. |
320
+ | `transports` | `Record<number, FhevmInstanceConfig>` | Chain-specific configs keyed by chain ID (includes relayerUrl, network, contract addresses). |
321
+ | `security` | `RelayerWebSecurityConfig` | Optional. Security options (see below). |
322
+ | `logger` | `GenericLogger` | Optional. Logger for worker lifecycle and request timing. |
323
+
324
+ #### `RelayerWebSecurityConfig`
325
+
326
+ | Field | Type | Description |
327
+ | ---------------- | -------------- | ------------------------------------------------------------------------------------------------ |
328
+ | `getCsrfToken` | `() => string` | Optional. Resolve the CSRF token before each authenticated network request. |
329
+ | `integrityCheck` | `boolean` | Optional. Verify SHA-384 integrity of the CDN bundle. Defaults to `true`. Set `false` for tests. |
330
+
331
+ > **Security note:** `RelayerWeb` loads FHE WASM from a CDN at runtime. The `integrityCheck` option (enabled by default) verifies the SHA-384 hash of the bundle before execution, protecting against CDN compromise or MITM attacks. Only disable it in local development or testing.
332
+
333
+ ### `RelayerNodeConfig` (Node.js)
334
+
335
+ | Field | Type | Description |
336
+ | ------------ | ------------------------------------- | -------------------------------------------------------------------------------------------------- |
337
+ | `getChainId` | `() => Promise<number>` | Resolve the current chain ID. Called lazily; the pool is re-initialized on chain change. |
338
+ | `transports` | `Record<number, FhevmInstanceConfig>` | Chain-specific configs keyed by chain ID (includes relayerUrl, network, auth, contract addresses). |
339
+
340
+ ### Network Preset Configs
341
+
342
+ Both the main entry (`@zama-fhe/sdk`) and the `/node` sub-path re-export preset configs so you don't need to import from `@zama-fhe/relayer-sdk` directly:
343
+
344
+ | Config | Chain ID | Description |
345
+ | --------------- | -------- | ----------------------------------- |
346
+ | `SepoliaConfig` | 11155111 | Sepolia testnet contract addresses. |
347
+ | `MainnetConfig` | 1 | Mainnet contract addresses. |
348
+ | `HardhatConfig` | 31337 | Local Hardhat node addresses. |
349
+
350
+ Each preset provides contract addresses and default relayer URL. Override `network` (RPC URL) for your environment. Browser apps should override `relayerUrl` with a proxy; server-side apps add `auth`:
351
+
352
+ ```ts
353
+ import { SepoliaConfig, MainnetConfig } from "@zama-fhe/sdk";
354
+
355
+ // Browser — proxy through your backend
356
+ const transports = {
357
+ [SepoliaConfig.chainId]: {
358
+ ...SepoliaConfig,
359
+ relayerUrl: "https://your-app.com/api/relayer/11155111",
360
+ network: "https://sepolia.infura.io/v3/KEY",
361
+ },
362
+ };
363
+
364
+ // Node.js — auth is safe server-side
365
+ const transports = {
366
+ [SepoliaConfig.chainId]: {
367
+ ...SepoliaConfig,
368
+ network: "https://sepolia.infura.io/v3/KEY",
369
+ auth: { __type: "ApiKeyHeader", value: process.env.RELAYER_API_KEY },
370
+ },
371
+ };
372
+ ```
373
+
374
+ ## GenericSigner Interface
375
+
376
+ The `GenericSigner` interface has six methods. Any Web3 library can back it.
377
+
378
+ ```ts
379
+ interface GenericSigner {
380
+ getChainId(): Promise<number>;
381
+ getAddress(): Promise<Address>;
382
+ signTypedData(typedData: EIP712TypedData): Promise<Hex>;
383
+ writeContract(config: ContractCallConfig): Promise<Hex>;
384
+ readContract(config: ContractCallConfig): Promise<unknown>;
385
+ waitForTransactionReceipt(hash: Hex): Promise<TransactionReceipt>;
386
+ }
387
+ ```
388
+
389
+ ### Built-in Adapters
390
+
391
+ **viem** — `@zama-fhe/sdk/viem`
392
+
393
+ ```ts
394
+ import { ViemSigner } from "@zama-fhe/sdk/viem";
395
+
396
+ const signer = new ViemSigner({ walletClient, publicClient });
397
+ ```
398
+
399
+ **ethers** — `@zama-fhe/sdk/ethers`
400
+
401
+ ```ts
402
+ import { EthersSigner } from "@zama-fhe/sdk/ethers";
403
+
404
+ const signer = new EthersSigner({ signer: ethersSigner });
405
+ ```
406
+
407
+ ## Contract Call Builders
408
+
409
+ Every function returns a `ContractCallConfig` object (address, ABI, function name, args) that can be used with any Web3 library. These are the low-level building blocks — they map 1:1 to on-chain contract calls without any orchestration. Use them when the high-level `Token` API doesn't cover your use case.
410
+
411
+ > **High-level vs low-level:** `token.shield()` / `token.unshield()` handle the full flow (approval, encryption, receipt waiting, finalization). The contract call builders (`wrapContract()`, `unwrapContract()`, etc.) produce raw call configs for a single contract interaction.
412
+
413
+ ```ts
414
+ interface ContractCallConfig {
415
+ readonly address: Address;
416
+ readonly abi: readonly unknown[];
417
+ readonly functionName: string;
418
+ readonly args: readonly unknown[];
419
+ readonly value?: bigint;
420
+ readonly gas?: bigint;
421
+ }
422
+ ```
423
+
424
+ ### ERC-20
425
+
426
+ | Function | Description |
427
+ | ------------------------------------------ | ------------------------ |
428
+ | `nameContract(token)` | Read token name. |
429
+ | `symbolContract(token)` | Read token symbol. |
430
+ | `decimalsContract(token)` | Read token decimals. |
431
+ | `balanceOfContract(token, owner)` | Read ERC-20 balance. |
432
+ | `allowanceContract(token, owner, spender)` | Read ERC-20 allowance. |
433
+ | `approveContract(token, spender, value)` | Approve ERC-20 spending. |
434
+
435
+ ### Encryption (Confidential ERC-20)
436
+
437
+ | Function | Description |
438
+ | ----------------------------------------------------------------------- | ----------------------------------------- |
439
+ | `confidentialBalanceOfContract(token, user)` | Read encrypted balance handle. |
440
+ | `confidentialTransferContract(token, to, handle, inputProof)` | Encrypted transfer. |
441
+ | `confidentialTransferFromContract(token, from, to, handle, inputProof)` | Operator encrypted transfer. |
442
+ | `isOperatorContract(token, holder, spender)` | Check operator approval. |
443
+ | `setOperatorContract(token, spender, timestamp?)` | Set operator approval (default: +1 hour). |
444
+ | `confidentialTotalSupplyContract(token)` | Read encrypted total supply handle. |
445
+ | `totalSupplyContract(token)` | Read plaintext total supply. |
446
+ | `rateContract(token)` | Read conversion rate. |
447
+ | `deploymentCoordinatorContract(token)` | Read deployment coordinator address. |
448
+ | `isFinalizeUnwrapOperatorContract(token, holder, operator)` | Check finalize-unwrap operator status. |
449
+ | `setFinalizeUnwrapOperatorContract(token, operator, timestamp?)` | Set finalize-unwrap operator. |
450
+
451
+ ### Wrapper
452
+
453
+ | Function | Description |
454
+ | ---------------------------------------------------------------- | --------------------------------------------- |
455
+ | `wrapContract(wrapper, to, amount)` | Wrap ERC-20 tokens. |
456
+ | `wrapETHContract(wrapper, to, amount, value)` | Wrap native ETH. |
457
+ | `unwrapContract(token, from, to, encryptedAmount, inputProof)` | Request unwrap with encrypted amount. |
458
+ | `unwrapFromBalanceContract(token, from, to, encryptedBalance)` | Request unwrap using on-chain balance handle. |
459
+ | `finalizeUnwrapContract(wrapper, burntAmount, cleartext, proof)` | Finalize unwrap with decryption proof. |
460
+ | `underlyingContract(wrapper)` | Read underlying ERC-20 address. |
461
+
462
+ ### Deployment Coordinator
463
+
464
+ | Function | Description |
465
+ | ------------------------------------------- | ---------------------------- |
466
+ | `getWrapperContract(coordinator, token)` | Look up wrapper for a token. |
467
+ | `wrapperExistsContract(coordinator, token)` | Check if wrapper exists. |
468
+
469
+ ### ERC-165
470
+
471
+ | Function | Description |
472
+ | ----------------------------------------------- | ------------------------ |
473
+ | `supportsInterfaceContract(token, interfaceId)` | ERC-165 interface check. |
474
+
475
+ ### Fee Manager
476
+
477
+ | Function | Description |
478
+ | ---------------------------------------------------- | -------------------------- |
479
+ | `getWrapFeeContract(feeManager, amount, from, to)` | Calculate wrap fee. |
480
+ | `getUnwrapFeeContract(feeManager, amount, from, to)` | Calculate unwrap fee. |
481
+ | `getBatchTransferFeeContract(feeManager)` | Get batch transfer fee. |
482
+ | `getFeeRecipientContract(feeManager)` | Get fee recipient address. |
483
+
484
+ ### Transfer Batcher
485
+
486
+ | Function | Description |
487
+ | -------------------------------------------------------------------------- | ----------------------------------- |
488
+ | `confidentialBatchTransferContract(batcher, token, from, transfers, fees)` | Batch multiple encrypted transfers. |
489
+
490
+ ## Library-Specific Contract Helpers
491
+
492
+ Both the `/viem` and `/ethers` sub-paths export convenience wrappers that execute contract calls directly with library-native clients.
493
+
494
+ ### viem (`@zama-fhe/sdk/viem`)
495
+
496
+ ```ts
497
+ import {
498
+ readConfidentialBalanceOfContract,
499
+ writeConfidentialTransferContract,
500
+ writeWrapContract,
501
+ // ... more
502
+ } from "@zama-fhe/sdk/viem";
503
+
504
+ // Read: pass a PublicClient
505
+ const handle = await readConfidentialBalanceOfContract(publicClient, tokenAddress, userAddress);
506
+
507
+ // Write: pass a WalletClient
508
+ const txHash = await writeConfidentialTransferContract(
509
+ walletClient,
510
+ tokenAddress,
511
+ to,
512
+ handle,
513
+ inputProof,
514
+ );
515
+ ```
516
+
517
+ **Read helpers:** `readConfidentialBalanceOfContract`, `readWrapperForTokenContract`, `readUnderlyingTokenContract`, `readWrapperExistsContract`, `readSupportsInterfaceContract`.
518
+
519
+ **Write helpers:** `writeConfidentialTransferContract`, `writeConfidentialBatchTransferContract`, `writeUnwrapContract`, `writeUnwrapFromBalanceContract`, `writeFinalizeUnwrapContract`, `writeSetOperatorContract`, `writeWrapContract`, `writeWrapETHContract`.
520
+
521
+ ### ethers (`@zama-fhe/sdk/ethers`)
522
+
523
+ Same set of functions, but read helpers take `Provider | Signer` and write helpers take `Signer`.
524
+
525
+ ```ts
526
+ import {
527
+ readConfidentialBalanceOfContract,
528
+ writeConfidentialTransferContract,
529
+ } from "@zama-fhe/sdk/ethers";
530
+
531
+ const handle = await readConfidentialBalanceOfContract(provider, tokenAddress, userAddress);
532
+ const txHash = await writeConfidentialTransferContract(
533
+ signer,
534
+ tokenAddress,
535
+ to,
536
+ handle,
537
+ inputProof,
538
+ );
539
+ ```
540
+
541
+ ## Event Decoders
542
+
543
+ Decode raw log entries from `eth_getLogs` into typed event objects.
544
+
545
+ ### Topics
546
+
547
+ Use `TOKEN_TOPICS` as the `topics[0]` filter for `getLogs` to capture all confidential token events:
548
+
549
+ ```ts
550
+ import { TOKEN_TOPICS } from "@zama-fhe/sdk";
551
+
552
+ const logs = await publicClient.getLogs({
553
+ address: tokenAddress,
554
+ topics: [TOKEN_TOPICS],
555
+ });
556
+ ```
557
+
558
+ Individual topic hashes are accessible via the `Topics` object: `Topics.ConfidentialTransfer`, `Topics.Wrapped`, `Topics.UnwrapRequested`, `Topics.UnwrappedFinalized`, `Topics.UnwrappedStarted`.
559
+
560
+ ### Decoders
561
+
562
+ | Function | Returns |
563
+ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
564
+ | `decodeConfidentialTransfer(log)` | `ConfidentialTransferEvent \| null` — `{ from, to, encryptedAmountHandle }` |
565
+ | `decodeWrapped(log)` | `WrappedEvent \| null` — `{ mintAmount, amountIn, feeAmount, to, mintTxId }` |
566
+ | `decodeUnwrapRequested(log)` | `UnwrapRequestedEvent \| null` — `{ receiver, encryptedAmount }` |
567
+ | `decodeUnwrappedFinalized(log)` | `UnwrappedFinalizedEvent \| null` — `{ burntAmountHandle, finalizeSuccess, burnAmount, unwrapAmount, feeAmount, ... }` |
568
+ | `decodeUnwrappedStarted(log)` | `UnwrappedStartedEvent \| null` — `{ returnVal, requestId, txId, to, refund, requestedAmount, burnAmount }` |
569
+ | `decodeOnChainEvent(log)` | `OnChainEvent \| null` — tries all decoders |
570
+ | `decodeOnChainEvents(logs)` | `OnChainEvent[]` — batch decode, skips unrecognized logs |
571
+
572
+ ### Finder Helpers
573
+
574
+ Convenience functions that decode a logs array and return the first matching event:
575
+
576
+ ```ts
577
+ import { findWrapped, findUnwrapRequested } from "@zama-fhe/sdk";
578
+
579
+ const wrappedEvent = findWrapped(receipt.logs);
580
+ const unwrapEvent = findUnwrapRequested(receipt.logs);
581
+ ```
582
+
583
+ ## Activity Feed Helpers
584
+
585
+ Transform raw event logs into a user-friendly activity feed with decrypted amounts.
586
+
587
+ ### Pipeline
588
+
589
+ ```ts
590
+ import {
591
+ parseActivityFeed,
592
+ extractEncryptedHandles,
593
+ applyDecryptedValues,
594
+ sortByBlockNumber,
595
+ } from "@zama-fhe/sdk";
596
+
597
+ // 1. Parse raw logs into classified activity items
598
+ const items = parseActivityFeed(logs, userAddress);
599
+
600
+ // 2. Extract encrypted handles that need decryption
601
+ const handles = extractEncryptedHandles(items);
602
+
603
+ // 3. Decrypt handles (using your token instance)
604
+ const decryptedMap = await token.decryptHandles(handles);
605
+
606
+ // 4. Apply decrypted values back to activity items
607
+ const enrichedItems = applyDecryptedValues(items, decryptedMap);
608
+
609
+ // 5. Sort by block number (most recent first)
610
+ const sorted = sortByBlockNumber(enrichedItems);
611
+ ```
612
+
613
+ ### Types
614
+
615
+ ```ts
616
+ type ActivityDirection = "incoming" | "outgoing" | "self";
617
+
618
+ type ActivityType =
619
+ | "transfer"
620
+ | "shield"
621
+ | "unshield_requested"
622
+ | "unshield_started"
623
+ | "unshield_finalized";
624
+
625
+ type ActivityAmount =
626
+ | { type: "clear"; value: bigint }
627
+ | { type: "encrypted"; handle: string; decryptedValue?: bigint };
628
+
629
+ interface ActivityItem {
630
+ type: ActivityType;
631
+ direction: ActivityDirection;
632
+ amount: ActivityAmount;
633
+ from?: string;
634
+ to?: string;
635
+ fee?: ActivityAmount;
636
+ success?: boolean;
637
+ metadata: ActivityLogMetadata;
638
+ rawEvent: OnChainEvent;
639
+ }
640
+
641
+ interface ActivityLogMetadata {
642
+ transactionHash?: string;
643
+ blockNumber?: bigint | number;
644
+ logIndex?: number;
645
+ }
646
+ ```
647
+
648
+ ## Error Handling
649
+
650
+ All SDK errors extend `ZamaError`. Use `instanceof` to catch specific error types:
651
+
652
+ ```ts
653
+ import { ZamaError, SigningRejectedError, EncryptionFailedError } from "@zama-fhe/sdk";
654
+
655
+ try {
656
+ await token.confidentialTransfer(to, amount);
657
+ } catch (error) {
658
+ if (error instanceof SigningRejectedError) {
659
+ // User rejected wallet signature
660
+ }
661
+ if (error instanceof EncryptionFailedError) {
662
+ // FHE encryption failed
663
+ }
664
+ if (error instanceof ZamaError) {
665
+ // Any other SDK error — check error.code for details
666
+ }
667
+ }
668
+ ```
669
+
670
+ ### Error Classes
671
+
672
+ | Error Class | Code | Description |
673
+ | --------------------------- | ------------------------ | ------------------------------------------------------------------------- |
674
+ | `SigningRejectedError` | `SIGNING_REJECTED` | User rejected the wallet signature request. |
675
+ | `SigningFailedError` | `SIGNING_FAILED` | Wallet signature failed for a non-rejection reason. |
676
+ | `EncryptionFailedError` | `ENCRYPTION_FAILED` | FHE encryption operation failed. |
677
+ | `DecryptionFailedError` | `DECRYPTION_FAILED` | FHE decryption operation failed. |
678
+ | `ApprovalFailedError` | `APPROVAL_FAILED` | ERC-20 approval transaction failed. |
679
+ | `TransactionRevertedError` | `TRANSACTION_REVERTED` | On-chain transaction reverted. |
680
+ | `InvalidCredentialsError` | `INVALID_CREDENTIALS` | Relayer rejected credentials (stale or expired). |
681
+ | `NoCiphertextError` | `NO_CIPHERTEXT` | No FHE ciphertext exists for this account (e.g. never shielded). |
682
+ | `RelayerRequestFailedError` | `RELAYER_REQUEST_FAILED` | Relayer HTTP error. Carries a `statusCode` property with the HTTP status. |
683
+
684
+ ### `matchZamaError`
685
+
686
+ Pattern-match on error codes without `instanceof` chains. Falls through to the `_` wildcard if no handler matches. Returns `undefined` for non-SDK errors when no `_` handler is provided.
687
+
688
+ ```ts
689
+ import { matchZamaError } from "@zama-fhe/sdk";
690
+
691
+ matchZamaError(error, {
692
+ SIGNING_REJECTED: () => toast("Please approve in wallet"),
693
+ TRANSACTION_REVERTED: (e) => toast(`Tx failed: ${e.message}`),
694
+ _: () => toast("Unknown error"),
695
+ });
696
+ ```
697
+
698
+ **Distinguishing "no ciphertext" from "zero balance":**
699
+
700
+ ```ts
701
+ import { NoCiphertextError, RelayerRequestFailedError } from "@zama-fhe/sdk";
702
+
703
+ try {
704
+ const balance = await token.balanceOf();
705
+ } catch (error) {
706
+ if (error instanceof NoCiphertextError) {
707
+ // Account has never shielded — show "no confidential balance" in UI
708
+ }
709
+ if (error instanceof RelayerRequestFailedError) {
710
+ console.error(`Relayer returned HTTP ${error.statusCode}`);
711
+ }
712
+ }
713
+ ```
714
+
715
+ ### Unshield Progress Callbacks
716
+
717
+ `unshield()`, `unshieldAll()`, and `resumeUnshield()` accept optional callbacks for tracking progress through the two-phase unshield flow:
718
+
719
+ ```ts
720
+ import type { UnshieldCallbacks } from "@zama-fhe/sdk";
721
+
722
+ const callbacks: UnshieldCallbacks = {
723
+ onUnwrapSubmitted: (txHash) => console.log("Unwrap tx:", txHash),
724
+ onFinalizing: () => console.log("Waiting for decryption proof..."),
725
+ onFinalizeSubmitted: (txHash) => console.log("Finalize tx:", txHash),
726
+ };
727
+
728
+ await token.unshield(500n, callbacks);
729
+ ```
730
+
731
+ Callbacks are safe — a throwing callback will not interrupt the unshield flow.
732
+
733
+ ## RelayerSDK (Low-Level FHE)
734
+
735
+ Low-level FHE operations are available on the relayer backend via `sdk.relayer`:
736
+
737
+ | Method | Description |
738
+ | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
739
+ | `encrypt(params)` | Encrypt values for smart contract calls. Returns `{ handles, inputProof }`. |
740
+ | `userDecrypt(params)` | Decrypt ciphertext handles with the user's FHE private key. |
741
+ | `publicDecrypt(handles)` | Public decryption (no private key needed). Returns `{ clearValues, decryptionProof }`. |
742
+ | `generateKeypair()` | Generate an FHE keypair. Returns `{ publicKey, privateKey }`. |
743
+ | `createEIP712(publicKey, contractAddresses, startTimestamp, durationDays?)` | Create EIP-712 typed data for decrypt authorization. Default duration: 7 days. |
744
+ | `createDelegatedUserDecryptEIP712(...)` | Create EIP-712 for delegated decryption. |
745
+ | `delegatedUserDecrypt(params)` | Decrypt via delegation. |
746
+ | `requestZKProofVerification(zkProof)` | Submit a ZK proof for on-chain verification. |
747
+ | `getPublicKey()` | Get the TFHE compact public key. |
748
+ | `getPublicParams(bits)` | Get public parameters for encryption capacity. |
749
+ | `terminate()` | Terminate the backend and clean up resources. |
750
+
751
+ ## Constants
752
+
753
+ | Constant | Value | Description |
754
+ | ------------------------------ | --------------------------------- | --------------------------------------------- |
755
+ | `ZERO_HANDLE` | `"0x0000...0000"` (32 zero bytes) | Sentinel for empty/zero encrypted values. |
756
+ | `ERC7984_INTERFACE_ID` | `"0x4958f2a4"` | ERC-165 interface ID for confidential tokens. |
757
+ | `ERC7984_WRAPPER_INTERFACE_ID` | `"0xd04584ba"` | ERC-165 interface ID for wrapper contracts. |
758
+
759
+ ## Exported ABIs
760
+
761
+ For direct use with viem, ethers, or any ABI-compatible library:
762
+
763
+ `ERC20_ABI`, `ERC20_METADATA_ABI`, `ENCRYPTION_ABI`, `WRAPPER_ABI`, `DEPLOYMENT_COORDINATOR_ABI`, `ERC165_ABI`, `FEE_MANAGER_ABI`, `TRANSFER_BATCHER_ABI`, `BATCH_SWAP_ABI`.