@zama-fhe/sdk 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Zama
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,649 @@
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 { TokenSDK, RelayerWeb, IndexedDBStorage } from "@zama-fhe/sdk";
25
+ import { ViemSigner } from "@zama-fhe/sdk/viem";
26
+
27
+ // 1. Create signer and relayer
28
+ const signer = new ViemSigner(walletClient, publicClient);
29
+
30
+ const sdk = new TokenSDK({
31
+ relayer: new RelayerWeb({
32
+ getChainId: () => signer.getChainId(),
33
+ transports: {
34
+ [1]: {
35
+ relayerUrl: "https://relayer.zama.ai",
36
+ network: "https://mainnet.infura.io/v3/YOUR_KEY",
37
+ },
38
+ [11155111]: {
39
+ relayerUrl: "https://relayer.zama.ai",
40
+ network: "https://sepolia.infura.io/v3/YOUR_KEY",
41
+ },
42
+ },
43
+ }),
44
+ signer,
45
+ storage: new IndexedDBStorage(),
46
+ });
47
+
48
+ // 2. Create a token instance (wrapper is auto-discovered if omitted)
49
+ const token = sdk.createToken("0xEncryptedERC20Address");
50
+ // Or provide the wrapper explicitly:
51
+ // const token = sdk.createToken("0xEncryptedERC20Address", "0xWrapperAddress");
52
+
53
+ // 3. Shield (wrap) public tokens into confidential tokens
54
+ const wrapTx = await token.wrap(1000n);
55
+
56
+ // 4. Check decrypted balance
57
+ const balance = await token.balanceOf();
58
+ console.log("Confidential balance:", balance);
59
+
60
+ // 5. Transfer confidential tokens
61
+ const transferTx = await token.confidentialTransfer("0xRecipient", 500n);
62
+ ```
63
+
64
+ ### Node.js
65
+
66
+ ```ts
67
+ import { TokenSDK, MemoryStorage } from "@zama-fhe/sdk";
68
+ import { RelayerNode } from "@zama-fhe/sdk/node";
69
+ import { ViemSigner } from "@zama-fhe/sdk/viem";
70
+
71
+ const signer = new ViemSigner(walletClient, publicClient);
72
+
73
+ const sdk = new TokenSDK({
74
+ relayer: new RelayerNode({
75
+ getChainId: () => signer.getChainId(),
76
+ poolSize: 4, // number of worker threads (default: min(CPUs, 4))
77
+ transports: {
78
+ [1]: {
79
+ relayerUrl: "https://relayer.zama.ai",
80
+ network: "https://mainnet.infura.io/v3/YOUR_KEY",
81
+ },
82
+ [11155111]: {
83
+ relayerUrl: "https://relayer.zama.ai",
84
+ network: "https://sepolia.infura.io/v3/YOUR_KEY",
85
+ },
86
+ },
87
+ }),
88
+ signer,
89
+ storage: new MemoryStorage(),
90
+ });
91
+
92
+ const token = sdk.createToken("0xEncryptedERC20Address");
93
+ const balance = await token.balanceOf();
94
+ ```
95
+
96
+ ## Core Concepts
97
+
98
+ ### TokenSDK
99
+
100
+ Entry point to the SDK. Composes a relayer backend with a signer and storage layer. Acts as a factory for token instances.
101
+
102
+ ```ts
103
+ const sdk = new TokenSDK({
104
+ relayer, // RelayerSDK — either RelayerWeb (browser) or RelayerNode (Node.js)
105
+ signer, // GenericSigner
106
+ storage, // GenericStringStorage
107
+ });
108
+
109
+ // Read-only — balances, metadata, decryption. No wrapper needed.
110
+ const readonlyToken = sdk.createReadonlyToken("0xTokenAddress");
111
+
112
+ // Full read/write — shield, unshield, transfer, approve.
113
+ // The token address IS the wrapper (encrypted ERC20 = wrapper contract).
114
+ const token = sdk.createToken("0xTokenAddress");
115
+ // Override wrapper if it differs from the token address (rare):
116
+ // const token = sdk.createToken("0xTokenAddress", "0xWrapperAddress");
117
+ ```
118
+
119
+ 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.
120
+
121
+ ### Relayer Backends
122
+
123
+ The `RelayerSDK` interface defines the FHE operations contract. Two implementations are provided:
124
+
125
+ | Backend | Import | Environment | How it works |
126
+ | ------------- | -------------------- | ----------- | ------------------------------------------ |
127
+ | `RelayerWeb` | `@zama-fhe/sdk` | Browser | Runs WASM in a Web Worker via CDN |
128
+ | `RelayerNode` | `@zama-fhe/sdk/node` | Node.js | Uses `@zama-fhe/relayer-sdk/node` directly |
129
+
130
+ The `/node` sub-path also exports `NodeWorkerClient` and `NodeWorkerClientConfig` for running FHE operations in a Node.js worker thread.
131
+
132
+ You can also implement the `RelayerSDK` interface for custom backends.
133
+
134
+ ### Token
135
+
136
+ 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.
137
+
138
+ | Method | Description |
139
+ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
140
+ | `wrap(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). |
141
+ | `wrapETH(amount, value?)` | Shield (wrap) native ETH. `value` defaults to `amount`. Use this when the underlying token is the zero address (native ETH). |
142
+ | `unshield(amount, callbacks?)` | Unwrap a specific amount and finalize in one call. Orchestrates: unwrap → wait receipt → parse event → finalizeUnwrap. Optional `UnshieldCallbacks` for progress tracking. |
143
+ | `unshieldAll(callbacks?)` | Unwrap the entire balance and finalize in one call. Orchestrates: unwrapAll → wait receipt → parse event → finalizeUnwrap. Optional `UnshieldCallbacks` for progress tracking. |
144
+ | `unwrap(amount)` | Request unwrap for a specific amount (low-level, requires manual finalization). |
145
+ | `unwrapAll()` | Request unwrap for the entire balance (low-level, requires manual finalization). |
146
+ | `resumeUnshield(unwrapTxHash, callbacks?)` | Resume an interrupted unshield from an existing unwrap tx hash. Goes straight to wait receipt → finalize. |
147
+ | `finalizeUnwrap(burnAmountHandle)` | Complete unwrap with public decryption proof. |
148
+ | `confidentialTransfer(to, amount)` | Encrypted transfer. Encrypts amount, then calls the contract. |
149
+ | `confidentialTransferFrom(from, to, amt)` | Operator encrypted transfer. |
150
+ | `approve(spender, until?)` | Set operator approval. `until` defaults to now + 1 hour. |
151
+ | `isApproved(spender)` | Check if a spender is an approved operator. |
152
+ | `approveUnderlying(amount?)` | Approve wrapper to spend underlying ERC-20. Default: max uint256. |
153
+ | `balanceOf(owner?)` | Decrypt and return the plaintext balance. |
154
+ | `decryptHandles(handles, owner?)` | Batch-decrypt arbitrary encrypted handles. |
155
+
156
+ All write methods return the transaction hash (`Address`).
157
+
158
+ ### ReadonlyToken
159
+
160
+ Read-only subset. No wrapper address needed.
161
+
162
+ | Method | Description |
163
+ | ------------------------------------- | ----------------------------------------------------------------- |
164
+ | `balanceOf(owner?)` | Decrypt and return the plaintext balance. |
165
+ | `confidentialBalanceOf(owner?)` | Return the raw encrypted balance handle (no decryption). |
166
+ | `decryptBalance(handle, owner?)` | Decrypt a single encrypted handle. |
167
+ | `decryptHandles(handles, owner?)` | Batch-decrypt handles in a single relayer call. |
168
+ | `authorize()` | Ensure FHE decrypt credentials exist (generates/signs if needed). |
169
+ | `authorizeAll(tokens)` _(static)_ | Pre-authorize multiple tokens with a single wallet signature. |
170
+ | `isConfidential()` | ERC-165 check for ERC-7984 support. |
171
+ | `isWrapper()` | ERC-165 check for wrapper interface. |
172
+ | `discoverWrapper(coordinatorAddress)` | Look up a wrapper for this token via the deployment coordinator. |
173
+ | `underlyingToken()` | Read the underlying ERC-20 address from a wrapper. |
174
+ | `allowance(wrapper, owner?)` | Read ERC-20 allowance of the underlying token. |
175
+ | `isZeroHandle(handle)` | Returns `true` if the handle is the zero sentinel. |
176
+ | `name()` / `symbol()` / `decimals()` | Read token metadata. |
177
+
178
+ Static methods for multi-token operations:
179
+
180
+ ```ts
181
+ // Pre-authorize all tokens with a single wallet signature
182
+ const tokens = addresses.map((a) => sdk.createReadonlyToken(a));
183
+ await ReadonlyToken.authorizeAll(tokens);
184
+ // All subsequent decrypts reuse cached credentials — no more wallet prompts
185
+
186
+ // Decrypt balances for multiple tokens in parallel
187
+ const balances = await ReadonlyToken.batchBalanceOf(tokens, owner);
188
+
189
+ // Decrypt pre-fetched handles for multiple tokens
190
+ const balances = await ReadonlyToken.batchDecryptBalances(tokens, handles, owner);
191
+ ```
192
+
193
+ ### Storage
194
+
195
+ FHE credentials (keypair + EIP-712 signature) are persisted to storage. Three options:
196
+
197
+ | Storage | Use case |
198
+ | ------------------ | ------------------------------------------------- |
199
+ | `MemoryStorage` | Testing. In-memory `Map`, lost on page reload. |
200
+ | `IndexedDBStorage` | Browser production. IndexedDB-backed, persistent. |
201
+ | `indexedDBStorage` | Pre-built singleton `IndexedDBStorage` instance. |
202
+ | Custom | Implement the `GenericStringStorage` interface. |
203
+
204
+ ```ts
205
+ interface GenericStringStorage {
206
+ getItem(key: string): string | Promise<string | null> | null;
207
+ setItem(key: string, value: string): void | Promise<void>;
208
+ removeItem(key: string): void | Promise<void>;
209
+ }
210
+ ```
211
+
212
+ ## Configuration Reference
213
+
214
+ ### `TokenSDKConfig`
215
+
216
+ | Field | Type | Description |
217
+ | --------- | ---------------------- | -------------------------------------------------------- |
218
+ | `relayer` | `RelayerSDK` | Relayer backend (`RelayerWeb` or `RelayerNode` instance) |
219
+ | `signer` | `GenericSigner` | Wallet signer interface. |
220
+ | `storage` | `GenericStringStorage` | Credential storage backend. |
221
+
222
+ ### `RelayerWebConfig` (browser)
223
+
224
+ | Field | Type | Description |
225
+ | ------------ | ------------------------------------- | -------------------------------------------------------------------------------------------- |
226
+ | `getChainId` | `() => Promise<number>` | Resolve the current chain ID. Called lazily; the worker is re-initialized on chain change. |
227
+ | `transports` | `Record<number, FhevmInstanceConfig>` | Chain-specific configs keyed by chain ID (includes relayerUrl, network, contract addresses). |
228
+ | `security` | `RelayerWebSecurityConfig` | Optional. Security options (see below). |
229
+ | `logger` | `GenericLogger` | Optional. Logger for worker lifecycle and request timing. |
230
+
231
+ #### `RelayerWebSecurityConfig`
232
+
233
+ | Field | Type | Description |
234
+ | ---------------- | -------------- | ------------------------------------------------------------------------------------------------ |
235
+ | `getCsrfToken` | `() => string` | Optional. Resolve the CSRF token before each authenticated network request. |
236
+ | `integrityCheck` | `boolean` | Optional. Verify SHA-384 integrity of the CDN bundle. Defaults to `true`. Set `false` for tests. |
237
+
238
+ ### `RelayerNodeConfig` (Node.js)
239
+
240
+ | Field | Type | Description |
241
+ | ------------ | ------------------------------------- | -------------------------------------------------------------------------------------------------- |
242
+ | `getChainId` | `() => Promise<number>` | Resolve the current chain ID. Called lazily; the pool is re-initialized on chain change. |
243
+ | `transports` | `Record<number, FhevmInstanceConfig>` | Chain-specific configs keyed by chain ID (includes relayerUrl, network, auth, contract addresses). |
244
+
245
+ ### Network Preset Configs
246
+
247
+ 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:
248
+
249
+ | Config | Chain ID | Description |
250
+ | --------------- | -------- | ----------------------------------- |
251
+ | `SepoliaConfig` | 11155111 | Sepolia testnet contract addresses. |
252
+ | `MainnetConfig` | 1 | Mainnet contract addresses. |
253
+ | `HardhatConfig` | 31337 | Local Hardhat node addresses. |
254
+
255
+ Each preset provides contract addresses and default values. Override `relayerUrl` and `network` (RPC URL) for your environment:
256
+
257
+ ```ts
258
+ import { SepoliaConfig, MainnetConfig } from "@zama-fhe/sdk";
259
+
260
+ const transports = {
261
+ [11155111]: {
262
+ ...SepoliaConfig,
263
+ relayerUrl: "/api/proxy",
264
+ network: "https://sepolia.infura.io/v3/KEY",
265
+ },
266
+ [1]: {
267
+ ...MainnetConfig,
268
+ relayerUrl: "/api/proxy",
269
+ network: "https://mainnet.infura.io/v3/KEY",
270
+ },
271
+ };
272
+ ```
273
+
274
+ ## GenericSigner Interface
275
+
276
+ The `GenericSigner` interface has six methods. Any Web3 library can back it.
277
+
278
+ ```ts
279
+ interface GenericSigner {
280
+ getChainId(): Promise<number>;
281
+ getAddress(): Promise<Address>;
282
+ signTypedData(typedData: EIP712TypedData): Promise<Address>;
283
+ writeContract(config: ContractCallConfig): Promise<Address>;
284
+ readContract(config: ContractCallConfig): Promise<unknown>;
285
+ waitForTransactionReceipt(hash: Address): Promise<TransactionReceipt>;
286
+ }
287
+ ```
288
+
289
+ ### Built-in Adapters
290
+
291
+ **viem** — `@zama-fhe/sdk/viem`
292
+
293
+ ```ts
294
+ import { ViemSigner } from "@zama-fhe/sdk/viem";
295
+
296
+ const signer = new ViemSigner(walletClient, publicClient);
297
+ ```
298
+
299
+ **ethers** — `@zama-fhe/sdk/ethers`
300
+
301
+ ```ts
302
+ import { EthersSigner } from "@zama-fhe/sdk/ethers";
303
+
304
+ const signer = new EthersSigner(ethersSigner);
305
+ ```
306
+
307
+ ## Contract Call Builders
308
+
309
+ 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.
310
+
311
+ > **High-level vs low-level:** `token.wrap()` / `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.
312
+
313
+ ```ts
314
+ interface ContractCallConfig {
315
+ readonly address: Address;
316
+ readonly abi: readonly unknown[];
317
+ readonly functionName: string;
318
+ readonly args: readonly unknown[];
319
+ readonly value?: bigint;
320
+ readonly gas?: bigint;
321
+ }
322
+ ```
323
+
324
+ ### ERC-20
325
+
326
+ | Function | Description |
327
+ | ------------------------------------------ | ------------------------ |
328
+ | `nameContract(token)` | Read token name. |
329
+ | `symbolContract(token)` | Read token symbol. |
330
+ | `decimalsContract(token)` | Read token decimals. |
331
+ | `balanceOfContract(token, owner)` | Read ERC-20 balance. |
332
+ | `allowanceContract(token, owner, spender)` | Read ERC-20 allowance. |
333
+ | `approveContract(token, spender, value)` | Approve ERC-20 spending. |
334
+
335
+ ### Encryption (Confidential ERC-20)
336
+
337
+ | Function | Description |
338
+ | ----------------------------------------------------------------------- | ----------------------------------------- |
339
+ | `confidentialBalanceOfContract(token, user)` | Read encrypted balance handle. |
340
+ | `confidentialTransferContract(token, to, handle, inputProof)` | Encrypted transfer. |
341
+ | `confidentialTransferFromContract(token, from, to, handle, inputProof)` | Operator encrypted transfer. |
342
+ | `isOperatorContract(token, holder, spender)` | Check operator approval. |
343
+ | `setOperatorContract(token, spender, timestamp?)` | Set operator approval (default: +1 hour). |
344
+ | `confidentialTotalSupplyContract(token)` | Read encrypted total supply handle. |
345
+ | `totalSupplyContract(token)` | Read plaintext total supply. |
346
+ | `rateContract(token)` | Read conversion rate. |
347
+ | `deploymentCoordinatorContract(token)` | Read deployment coordinator address. |
348
+ | `isFinalizeUnwrapOperatorContract(token, holder, operator)` | Check finalize-unwrap operator status. |
349
+ | `setFinalizeUnwrapOperatorContract(token, operator, timestamp?)` | Set finalize-unwrap operator. |
350
+
351
+ ### Wrapper
352
+
353
+ | Function | Description |
354
+ | ---------------------------------------------------------------- | --------------------------------------------- |
355
+ | `wrapContract(wrapper, to, amount)` | Wrap ERC-20 tokens. |
356
+ | `wrapETHContract(wrapper, to, amount, value)` | Wrap native ETH. |
357
+ | `unwrapContract(token, from, to, encryptedAmount, inputProof)` | Request unwrap with encrypted amount. |
358
+ | `unwrapFromBalanceContract(token, from, to, encryptedBalance)` | Request unwrap using on-chain balance handle. |
359
+ | `finalizeUnwrapContract(wrapper, burntAmount, cleartext, proof)` | Finalize unwrap with decryption proof. |
360
+ | `underlyingContract(wrapper)` | Read underlying ERC-20 address. |
361
+
362
+ ### Deployment Coordinator
363
+
364
+ | Function | Description |
365
+ | ------------------------------------------- | ---------------------------- |
366
+ | `getWrapperContract(coordinator, token)` | Look up wrapper for a token. |
367
+ | `wrapperExistsContract(coordinator, token)` | Check if wrapper exists. |
368
+
369
+ ### ERC-165
370
+
371
+ | Function | Description |
372
+ | ----------------------------------------------- | ------------------------ |
373
+ | `supportsInterfaceContract(token, interfaceId)` | ERC-165 interface check. |
374
+
375
+ ### Fee Manager
376
+
377
+ | Function | Description |
378
+ | ---------------------------------------------------- | -------------------------- |
379
+ | `getWrapFeeContract(feeManager, amount, from, to)` | Calculate wrap fee. |
380
+ | `getUnwrapFeeContract(feeManager, amount, from, to)` | Calculate unwrap fee. |
381
+ | `getBatchTransferFeeContract(feeManager)` | Get batch transfer fee. |
382
+ | `getFeeRecipientContract(feeManager)` | Get fee recipient address. |
383
+
384
+ ### Transfer Batcher
385
+
386
+ | Function | Description |
387
+ | -------------------------------------------------------------------------- | ----------------------------------- |
388
+ | `confidentialBatchTransferContract(batcher, token, from, transfers, fees)` | Batch multiple encrypted transfers. |
389
+
390
+ ## Library-Specific Contract Helpers
391
+
392
+ Both the `/viem` and `/ethers` sub-paths export convenience wrappers that execute contract calls directly with library-native clients.
393
+
394
+ ### viem (`@zama-fhe/sdk/viem`)
395
+
396
+ ```ts
397
+ import {
398
+ readConfidentialBalanceOfContract,
399
+ writeConfidentialTransferContract,
400
+ writeWrapContract,
401
+ // ... more
402
+ } from "@zama-fhe/sdk/viem";
403
+
404
+ // Read: pass a PublicClient
405
+ const handle = await readConfidentialBalanceOfContract(publicClient, tokenAddress, userAddress);
406
+
407
+ // Write: pass a WalletClient
408
+ const txHash = await writeConfidentialTransferContract(
409
+ walletClient,
410
+ tokenAddress,
411
+ to,
412
+ handle,
413
+ inputProof,
414
+ );
415
+ ```
416
+
417
+ **Read helpers:** `readConfidentialBalanceOfContract`, `readWrapperForTokenContract`, `readUnderlyingTokenContract`, `readWrapperExistsContract`, `readSupportsInterfaceContract`.
418
+
419
+ **Write helpers:** `writeConfidentialTransferContract`, `writeConfidentialBatchTransferContract`, `writeUnwrapContract`, `writeUnwrapFromBalanceContract`, `writeFinalizeUnwrapContract`, `writeSetOperatorContract`, `writeWrapContract`, `writeWrapETHContract`.
420
+
421
+ ### ethers (`@zama-fhe/sdk/ethers`)
422
+
423
+ Same set of functions, but read helpers take `Provider | Signer` and write helpers take `Signer`.
424
+
425
+ ```ts
426
+ import {
427
+ readConfidentialBalanceOfContract,
428
+ writeConfidentialTransferContract,
429
+ } from "@zama-fhe/sdk/ethers";
430
+
431
+ const handle = await readConfidentialBalanceOfContract(provider, tokenAddress, userAddress);
432
+ const txHash = await writeConfidentialTransferContract(
433
+ signer,
434
+ tokenAddress,
435
+ to,
436
+ handle,
437
+ inputProof,
438
+ );
439
+ ```
440
+
441
+ ## Event Decoders
442
+
443
+ Decode raw log entries from `eth_getLogs` into typed event objects.
444
+
445
+ ### Topics
446
+
447
+ Use `TOKEN_TOPICS` as the `topics[0]` filter for `getLogs` to capture all confidential token events:
448
+
449
+ ```ts
450
+ import { TOKEN_TOPICS } from "@zama-fhe/sdk";
451
+
452
+ const logs = await publicClient.getLogs({
453
+ address: tokenAddress,
454
+ topics: [TOKEN_TOPICS],
455
+ });
456
+ ```
457
+
458
+ Individual topic hashes are accessible via the `Topics` object: `Topics.ConfidentialTransfer`, `Topics.Wrapped`, `Topics.UnwrapRequested`, `Topics.UnwrappedFinalized`, `Topics.UnwrappedStarted`.
459
+
460
+ ### Decoders
461
+
462
+ | Function | Returns |
463
+ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
464
+ | `decodeConfidentialTransfer(log)` | `ConfidentialTransferEvent \| null` — `{ from, to, encryptedAmountHandle }` |
465
+ | `decodeWrapped(log)` | `WrappedEvent \| null` — `{ mintAmount, amountIn, feeAmount, to, mintTxId }` |
466
+ | `decodeUnwrapRequested(log)` | `UnwrapRequestedEvent \| null` — `{ receiver, encryptedAmount }` |
467
+ | `decodeUnwrappedFinalized(log)` | `UnwrappedFinalizedEvent \| null` — `{ burntAmountHandle, finalizeSuccess, burnAmount, unwrapAmount, feeAmount, ... }` |
468
+ | `decodeUnwrappedStarted(log)` | `UnwrappedStartedEvent \| null` — `{ returnVal, requestId, txId, to, refund, requestedAmount, burnAmount }` |
469
+ | `decodeTokenEvent(log)` | `TokenEvent \| null` — tries all decoders |
470
+ | `decodeTokenEvents(logs)` | `TokenEvent[]` — batch decode, skips unrecognized logs |
471
+
472
+ ### Finder Helpers
473
+
474
+ Convenience functions that decode a logs array and return the first matching event:
475
+
476
+ ```ts
477
+ import { findWrapped, findUnwrapRequested } from "@zama-fhe/sdk";
478
+
479
+ const wrappedEvent = findWrapped(receipt.logs);
480
+ const unwrapEvent = findUnwrapRequested(receipt.logs);
481
+ ```
482
+
483
+ ## Activity Feed Helpers
484
+
485
+ Transform raw event logs into a user-friendly activity feed with decrypted amounts.
486
+
487
+ ### Pipeline
488
+
489
+ ```ts
490
+ import {
491
+ parseActivityFeed,
492
+ extractEncryptedHandles,
493
+ applyDecryptedValues,
494
+ sortByBlockNumber,
495
+ } from "@zama-fhe/sdk";
496
+
497
+ // 1. Parse raw logs into classified activity items
498
+ const items = parseActivityFeed(logs, userAddress);
499
+
500
+ // 2. Extract encrypted handles that need decryption
501
+ const handles = extractEncryptedHandles(items);
502
+
503
+ // 3. Decrypt handles (using your token instance)
504
+ const decryptedMap = await token.decryptHandles(handles);
505
+
506
+ // 4. Apply decrypted values back to activity items
507
+ const enrichedItems = applyDecryptedValues(items, decryptedMap);
508
+
509
+ // 5. Sort by block number (most recent first)
510
+ const sorted = sortByBlockNumber(enrichedItems);
511
+ ```
512
+
513
+ ### Types
514
+
515
+ ```ts
516
+ type ActivityDirection = "incoming" | "outgoing" | "self";
517
+
518
+ type ActivityType =
519
+ | "transfer"
520
+ | "shield"
521
+ | "unshield_requested"
522
+ | "unshield_started"
523
+ | "unshield_finalized";
524
+
525
+ type ActivityAmount =
526
+ | { type: "clear"; value: bigint }
527
+ | { type: "encrypted"; handle: string; decryptedValue?: bigint };
528
+
529
+ interface ActivityItem {
530
+ type: ActivityType;
531
+ direction: ActivityDirection;
532
+ amount: ActivityAmount;
533
+ from?: string;
534
+ to?: string;
535
+ fee?: ActivityAmount;
536
+ success?: boolean;
537
+ metadata: ActivityLogMetadata;
538
+ rawEvent: TokenEvent;
539
+ }
540
+
541
+ interface ActivityLogMetadata {
542
+ transactionHash?: string;
543
+ blockNumber?: bigint | number;
544
+ logIndex?: number;
545
+ }
546
+ ```
547
+
548
+ ## Error Handling
549
+
550
+ All SDK errors extend `TokenError`. Use `instanceof` to catch specific error types:
551
+
552
+ ```ts
553
+ import { TokenError, SigningRejectedError, EncryptionFailedError } from "@zama-fhe/sdk";
554
+
555
+ try {
556
+ await token.confidentialTransfer(to, amount);
557
+ } catch (error) {
558
+ if (error instanceof SigningRejectedError) {
559
+ // User rejected wallet signature
560
+ }
561
+ if (error instanceof EncryptionFailedError) {
562
+ // FHE encryption failed
563
+ }
564
+ if (error instanceof TokenError) {
565
+ // Any other SDK error — check error.code for details
566
+ }
567
+ }
568
+ ```
569
+
570
+ ### Error Classes
571
+
572
+ | Error Class | Code | Description |
573
+ | --------------------------- | ------------------------ | ------------------------------------------------------------------------- |
574
+ | `SigningRejectedError` | `SIGNING_REJECTED` | User rejected the wallet signature request. |
575
+ | `SigningFailedError` | `SIGNING_FAILED` | Wallet signature failed for a non-rejection reason. |
576
+ | `EncryptionFailedError` | `ENCRYPTION_FAILED` | FHE encryption operation failed. |
577
+ | `DecryptionFailedError` | `DECRYPTION_FAILED` | FHE decryption operation failed. |
578
+ | `ApprovalFailedError` | `APPROVAL_FAILED` | ERC-20 approval transaction failed. |
579
+ | `TransactionRevertedError` | `TRANSACTION_REVERTED` | On-chain transaction reverted. |
580
+ | `InvalidCredentialsError` | `INVALID_CREDENTIALS` | Relayer rejected credentials (stale or expired). |
581
+ | `NoCiphertextError` | `NO_CIPHERTEXT` | No FHE ciphertext exists for this account (e.g. never shielded). |
582
+ | `RelayerRequestFailedError` | `RELAYER_REQUEST_FAILED` | Relayer HTTP error. Carries a `statusCode` property with the HTTP status. |
583
+
584
+ **Distinguishing "no ciphertext" from "zero balance":**
585
+
586
+ ```ts
587
+ import { NoCiphertextError, RelayerRequestFailedError } from "@zama-fhe/sdk";
588
+
589
+ try {
590
+ const balance = await token.balanceOf();
591
+ } catch (error) {
592
+ if (error instanceof NoCiphertextError) {
593
+ // Account has never shielded — show "no confidential balance" in UI
594
+ }
595
+ if (error instanceof RelayerRequestFailedError) {
596
+ console.error(`Relayer returned HTTP ${error.statusCode}`);
597
+ }
598
+ }
599
+ ```
600
+
601
+ ### Unshield Progress Callbacks
602
+
603
+ `unshield()`, `unshieldAll()`, and `resumeUnshield()` accept optional callbacks for tracking progress through the two-phase unshield flow:
604
+
605
+ ```ts
606
+ import type { UnshieldCallbacks } from "@zama-fhe/sdk";
607
+
608
+ const callbacks: UnshieldCallbacks = {
609
+ onUnwrapSubmitted: (txHash) => console.log("Unwrap tx:", txHash),
610
+ onFinalizing: () => console.log("Waiting for decryption proof..."),
611
+ onFinalizeSubmitted: (txHash) => console.log("Finalize tx:", txHash),
612
+ };
613
+
614
+ await token.unshield(500n, callbacks);
615
+ ```
616
+
617
+ Callbacks are safe — a throwing callback will not interrupt the unshield flow.
618
+
619
+ ## RelayerSDK (Low-Level FHE)
620
+
621
+ Low-level FHE operations are available on the relayer backend via `sdk.relayer`:
622
+
623
+ | Method | Description |
624
+ | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
625
+ | `encrypt(params)` | Encrypt values for smart contract calls. Returns `{ handles, inputProof }`. |
626
+ | `userDecrypt(params)` | Decrypt ciphertext handles with the user's FHE private key. |
627
+ | `publicDecrypt(handles)` | Public decryption (no private key needed). Returns `{ clearValues, decryptionProof }`. |
628
+ | `generateKeypair()` | Generate an FHE keypair. Returns `{ publicKey, privateKey }`. |
629
+ | `createEIP712(publicKey, contractAddresses, startTimestamp, durationDays?)` | Create EIP-712 typed data for decrypt authorization. Default duration: 7 days. |
630
+ | `createDelegatedUserDecryptEIP712(...)` | Create EIP-712 for delegated decryption. |
631
+ | `delegatedUserDecrypt(params)` | Decrypt via delegation. |
632
+ | `requestZKProofVerification(zkProof)` | Submit a ZK proof for on-chain verification. |
633
+ | `getPublicKey()` | Get the TFHE compact public key. |
634
+ | `getPublicParams(bits)` | Get public parameters for encryption capacity. |
635
+ | `terminate()` | Terminate the backend and clean up resources. |
636
+
637
+ ## Constants
638
+
639
+ | Constant | Value | Description |
640
+ | ------------------------------ | --------------------------------- | --------------------------------------------- |
641
+ | `ZERO_HANDLE` | `"0x0000...0000"` (32 zero bytes) | Sentinel for empty/zero encrypted values. |
642
+ | `ERC7984_INTERFACE_ID` | `"0x4958f2a4"` | ERC-165 interface ID for confidential tokens. |
643
+ | `ERC7984_WRAPPER_INTERFACE_ID` | `"0xd04584ba"` | ERC-165 interface ID for wrapper contracts. |
644
+
645
+ ## Exported ABIs
646
+
647
+ For direct use with viem, ethers, or any ABI-compatible library:
648
+
649
+ `ERC20_ABI`, `ERC20_METADATA_ABI`, `ENCRYPTION_ABI`, `WRAPPER_ABI`, `DEPLOYMENT_COORDINATOR_ABI`, `ERC165_ABI`, `FEE_MANAGER_ABI`, `TRANSFER_BATCHER_ABI`, `BATCH_SWAP_ABI`.