@zama-fhe/react-sdk 1.0.0 → 1.0.1

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 CHANGED
@@ -6,6 +6,10 @@ React hooks for confidential token operations, built on [React Query](https://ta
6
6
 
7
7
  ```bash
8
8
  pnpm add @zama-fhe/react-sdk @tanstack/react-query
9
+ # or
10
+ npm install @zama-fhe/react-sdk @tanstack/react-query
11
+ # or
12
+ yarn add @zama-fhe/react-sdk @tanstack/react-query
9
13
  ```
10
14
 
11
15
  `@zama-fhe/sdk` is included as a direct dependency — no need to install it separately.
@@ -28,11 +32,9 @@ pnpm add @zama-fhe/react-sdk @tanstack/react-query
28
32
  import { WagmiProvider, createConfig, http } from "wagmi";
29
33
  import { mainnet, sepolia } from "wagmi/chains";
30
34
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
31
- import { TokenSDKProvider, RelayerWeb, indexedDBStorage } from "@zama-fhe/react-sdk";
35
+ import { ZamaProvider, RelayerWeb, indexedDBStorage } from "@zama-fhe/react-sdk";
32
36
  import { WagmiSigner } from "@zama-fhe/react-sdk/wagmi";
33
37
 
34
- const queryClient = new QueryClient();
35
-
36
38
  const wagmiConfig = createConfig({
37
39
  chains: [mainnet, sepolia],
38
40
  transports: {
@@ -41,36 +43,38 @@ const wagmiConfig = createConfig({
41
43
  },
42
44
  });
43
45
 
44
- const signer = new WagmiSigner(wagmiConfig);
46
+ const signer = new WagmiSigner({ config: wagmiConfig });
45
47
 
46
48
  const relayer = new RelayerWeb({
47
49
  getChainId: () => signer.getChainId(),
48
50
  transports: {
49
- [1]: {
50
- relayerUrl: "https://relayer.zama.ai",
51
+ [mainnet.id]: {
52
+ relayerUrl: "https://your-app.com/api/relayer/1",
51
53
  network: "https://mainnet.infura.io/v3/YOUR_KEY",
52
54
  },
53
- [11155111]: {
54
- relayerUrl: "https://relayer.zama.ai",
55
+ [sepolia.id]: {
56
+ relayerUrl: "https://your-app.com/api/relayer/11155111",
55
57
  network: "https://sepolia.infura.io/v3/YOUR_KEY",
56
58
  },
57
59
  },
58
60
  });
59
61
 
62
+ const queryClient = new QueryClient();
63
+
60
64
  function App() {
61
65
  return (
62
66
  <WagmiProvider config={wagmiConfig}>
63
67
  <QueryClientProvider client={queryClient}>
64
- <TokenSDKProvider relayer={relayer} signer={signer} storage={indexedDBStorage}>
68
+ <ZamaProvider relayer={relayer} signer={signer} storage={indexedDBStorage}>
65
69
  <TokenBalance />
66
- </TokenSDKProvider>
70
+ </ZamaProvider>
67
71
  </QueryClientProvider>
68
72
  </WagmiProvider>
69
73
  );
70
74
  }
71
75
 
72
76
  function TokenBalance() {
73
- const { data: balance, isLoading } = useConfidentialBalance("0xTokenAddress");
77
+ const { data: balance, isLoading } = useConfidentialBalance({ tokenAddress: "0xTokenAddress" });
74
78
 
75
79
  if (isLoading) return <p>Decrypting balance...</p>;
76
80
  return <p>Balance: {balance?.toString()}</p>;
@@ -81,42 +85,43 @@ function TokenBalance() {
81
85
 
82
86
  ```tsx
83
87
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
88
+ import { mainnet, sepolia } from "wagmi/chains"; // or define your own chain IDs
84
89
  import {
85
- TokenSDKProvider,
90
+ ZamaProvider,
86
91
  RelayerWeb,
87
92
  useConfidentialBalance,
88
93
  useConfidentialTransfer,
89
- MemoryStorage,
94
+ memoryStorage,
90
95
  } from "@zama-fhe/react-sdk";
91
96
 
92
- const queryClient = new QueryClient();
93
-
94
97
  const relayer = new RelayerWeb({
95
98
  getChainId: () => yourCustomSigner.getChainId(),
96
99
  transports: {
97
- [1]: {
98
- relayerUrl: "https://relayer.zama.ai",
100
+ [mainnet.id]: {
101
+ relayerUrl: "https://your-app.com/api/relayer/1",
99
102
  network: "https://mainnet.infura.io/v3/YOUR_KEY",
100
103
  },
101
- [11155111]: {
102
- relayerUrl: "https://relayer.zama.ai",
104
+ [sepolia.id]: {
105
+ relayerUrl: "https://your-app.com/api/relayer/11155111",
103
106
  network: "https://sepolia.infura.io/v3/YOUR_KEY",
104
107
  },
105
108
  },
106
109
  });
107
110
 
111
+ const queryClient = new QueryClient();
112
+
108
113
  function App() {
109
114
  return (
110
115
  <QueryClientProvider client={queryClient}>
111
- <TokenSDKProvider relayer={relayer} signer={yourCustomSigner} storage={new MemoryStorage()}>
116
+ <ZamaProvider relayer={relayer} signer={yourCustomSigner} storage={memoryStorage}>
112
117
  <TransferForm />
113
- </TokenSDKProvider>
118
+ </ZamaProvider>
114
119
  </QueryClientProvider>
115
120
  );
116
121
  }
117
122
 
118
123
  function TransferForm() {
119
- const { data: balance } = useConfidentialBalance("0xTokenAddress");
124
+ const { data: balance } = useConfidentialBalance({ tokenAddress: "0xTokenAddress" });
120
125
  const { mutateAsync: transfer, isPending } = useConfidentialTransfer({
121
126
  tokenAddress: "0xTokenAddress",
122
127
  });
@@ -139,32 +144,60 @@ function TransferForm() {
139
144
 
140
145
  ## Provider Setup
141
146
 
142
- All setups use `TokenSDKProvider`. Create a signer with the adapter for your library, then pass it directly.
147
+ All setups use `ZamaProvider`. Create a signer with the adapter for your library, then pass it directly.
143
148
 
144
149
  ```tsx
145
- import { TokenSDKProvider } from "@zama-fhe/react-sdk";
150
+ import { ZamaProvider } from "@zama-fhe/react-sdk";
146
151
 
147
- <TokenSDKProvider
152
+ <ZamaProvider
148
153
  relayer={relayer} // RelayerSDK (RelayerWeb or RelayerNode instance)
149
154
  signer={signer} // GenericSigner (WagmiSigner, ViemSigner, EthersSigner, or custom)
150
- storage={storage} // GenericStringStorage
155
+ storage={storage} // GenericStorage
156
+ sessionStorage={sessionStorage} // Optional. Session storage for wallet signatures. Default: in-memory (lost on reload).
157
+ keypairTTL={86400} // Optional. Seconds the ML-KEM keypair remains valid. Default: 86400 (1 day).
158
+ sessionTTL={2592000} // Optional. Seconds the session signature remains valid. Default: 2592000 (30 days). 0 = re-sign every operation.
159
+ onEvent={(event) => console.debug(event)} // Optional. Structured event listener for debugging.
151
160
  >
152
161
  {children}
153
- </TokenSDKProvider>;
162
+ </ZamaProvider>;
163
+ ```
164
+
165
+ ## Which Hooks Should I Use?
166
+
167
+ The React SDK exports hooks from two layers. **Pick one layer per operation — never mix them.**
168
+
169
+ **Use the main import** (`@zama-fhe/react-sdk`) when you have a `ZamaProvider` in your component tree. These hooks handle FHE encryption, cache invalidation, and error wrapping automatically:
170
+
171
+ ```tsx
172
+ import { useShield, useConfidentialTransfer } from "@zama-fhe/react-sdk";
173
+
174
+ const { mutateAsync: shield } = useShield({ tokenAddress });
175
+ await shield({ amount: 1000n }); // encryption + approval handled for you
176
+ ```
177
+
178
+ ```tsx
179
+ import { ViemSigner } from "@zama-fhe/sdk/viem";
180
+ import { EthersSigner } from "@zama-fhe/sdk/ethers";
181
+ ```
182
+
183
+ The `WagmiSigner` is the only adapter in the react-sdk since wagmi is React-specific:
184
+
185
+ ```tsx
186
+ import { WagmiSigner } from "@zama-fhe/react-sdk/wagmi";
154
187
  ```
155
188
 
156
189
  ## Hooks Reference
157
190
 
158
- All hooks require a `TokenSDKProvider` (or one of its variants) in the component tree.
191
+ All hooks require a `ZamaProvider` (or one of its variants) in the component tree.
159
192
 
160
193
  ### SDK Access
161
194
 
162
- #### `useTokenSDK`
195
+ #### `useZamaSDK`
163
196
 
164
- Returns the `TokenSDK` instance from context. Use this when you need direct access to the SDK (e.g. for low-level relayer operations).
197
+ Returns the `ZamaSDK` instance from context. Use this when you need direct access to the SDK (e.g. for low-level relayer operations).
165
198
 
166
199
  ```ts
167
- function useTokenSDK(): TokenSDK;
200
+ function useZamaSDK(): ZamaSDK;
168
201
  ```
169
202
 
170
203
  #### `useToken`
@@ -191,28 +224,27 @@ Single-token balance with automatic decryption. Uses two-phase polling: polls th
191
224
 
192
225
  ```ts
193
226
  function useConfidentialBalance(
194
- tokenAddress: Address,
195
- owner?: Address, // defaults to connected wallet
227
+ config: UseConfidentialBalanceConfig,
196
228
  options?: UseConfidentialBalanceOptions,
197
229
  ): UseQueryResult<bigint, Error>;
198
- ```
199
230
 
200
- Options extend `UseQueryOptions` and add:
231
+ interface UseConfidentialBalanceConfig {
232
+ tokenAddress: Address;
233
+ handleRefetchInterval?: number; // default: 10000ms
234
+ }
235
+ ```
201
236
 
202
- | Option | Type | Default | Description |
203
- | ----------------------- | -------- | ------- | ----------------------------------------------- |
204
- | `handleRefetchInterval` | `number` | `10000` | Polling interval (ms) for the encrypted handle. |
237
+ Options extend `UseQueryOptions`.
205
238
 
206
239
  ```tsx
207
240
  const {
208
241
  data: balance,
209
242
  isLoading,
210
243
  error,
211
- } = useConfidentialBalance(
212
- "0xTokenAddress",
213
- undefined, // use connected wallet
214
- { handleRefetchInterval: 5_000 },
215
- );
244
+ } = useConfidentialBalance({
245
+ tokenAddress: "0xTokenAddress",
246
+ handleRefetchInterval: 5_000,
247
+ });
216
248
  ```
217
249
 
218
250
  #### `useConfidentialBalances`
@@ -221,14 +253,21 @@ Multi-token batch balance. Same two-phase polling pattern.
221
253
 
222
254
  ```ts
223
255
  function useConfidentialBalances(
224
- tokenAddresses: Address[],
225
- owner?: Address,
256
+ config: UseConfidentialBalancesConfig,
226
257
  options?: UseConfidentialBalancesOptions,
227
258
  ): UseQueryResult<Map<Address, bigint>, Error>;
259
+
260
+ interface UseConfidentialBalancesConfig {
261
+ tokenAddresses: Address[];
262
+ handleRefetchInterval?: number;
263
+ maxConcurrency?: number;
264
+ }
228
265
  ```
229
266
 
230
267
  ```tsx
231
- const { data: balances } = useConfidentialBalances(["0xTokenA", "0xTokenB", "0xTokenC"]);
268
+ const { data: balances } = useConfidentialBalances({
269
+ tokenAddresses: ["0xTokenA", "0xTokenB", "0xTokenC"],
270
+ });
232
271
 
233
272
  // balances is a Map<Address, bigint>
234
273
  const tokenABalance = balances?.get("0xTokenA");
@@ -236,24 +275,55 @@ const tokenABalance = balances?.get("0xTokenA");
236
275
 
237
276
  ### Authorization
238
277
 
239
- #### `useAuthorizeAll`
278
+ #### `useTokenAllow`
240
279
 
241
280
  Pre-authorize FHE decrypt credentials for a list of token addresses with a single wallet signature. Call this early (e.g. after loading the token list) so that subsequent individual decrypt operations reuse cached credentials without prompting the wallet again.
242
281
 
243
282
  ```ts
244
- function useAuthorizeAll(): UseMutationResult<void, Error, Address[]>;
283
+ function useTokenAllow(): UseMutationResult<void, Error, Address[]>;
245
284
  ```
246
285
 
247
286
  ```tsx
248
- const { mutateAsync: authorizeAll, isPending } = useAuthorizeAll();
287
+ const { mutateAsync: tokenAllow, isPending } = useTokenAllow();
249
288
 
250
289
  // Pre-authorize all known tokens up front
251
- await authorizeAll(allTokenAddresses);
290
+ await tokenAllow(allTokenAddresses);
252
291
 
253
292
  // Individual balance decrypts now reuse cached credentials
254
293
  const { data: balance } = useConfidentialBalance("0xTokenA");
255
294
  ```
256
295
 
296
+ #### `useIsTokenAllowed`
297
+
298
+ Check whether a session signature is cached for a given token. Returns `true` if decrypt operations can proceed without a wallet prompt. Use this to conditionally enable UI elements (e.g. a "Reveal Balances" button).
299
+
300
+ ```ts
301
+ function useIsTokenAllowed(tokenAddress: Address): UseQueryResult<boolean, Error>;
302
+ ```
303
+
304
+ ```tsx
305
+ const { data: allowed } = useIsTokenAllowed("0xTokenAddress");
306
+
307
+ <button disabled={!allowed}>Reveal Balance</button>;
308
+ ```
309
+
310
+ Automatically invalidated when `useTokenAllow` or `useTokenRevoke` succeed.
311
+
312
+ #### `useTokenRevoke`
313
+
314
+ Revoke the session signature for the connected wallet. Stored credentials remain intact, but the next decrypt operation will require a fresh wallet signature.
315
+
316
+ ```ts
317
+ function useTokenRevoke(): UseMutationResult<void, Error, Address[]>;
318
+ ```
319
+
320
+ ```tsx
321
+ const { mutate: tokenRevoke } = useTokenRevoke();
322
+
323
+ // Revoke session — addresses are included in the credentials:revoked event
324
+ tokenRevoke(["0xTokenA", "0xTokenB"]);
325
+ ```
326
+
257
327
  ### Transfer Hooks
258
328
 
259
329
  #### `useConfidentialTransfer`
@@ -262,7 +332,7 @@ Encrypted transfer. Encrypts the amount and calls the contract. Automatically in
262
332
 
263
333
  ```ts
264
334
  function useConfidentialTransfer(
265
- config: UseTokenConfig,
335
+ config: UseZamaConfig,
266
336
  options?: UseMutationOptions<Address, Error, ConfidentialTransferParams>,
267
337
  ): UseMutationResult<Address, Error, ConfidentialTransferParams>;
268
338
 
@@ -286,7 +356,7 @@ Operator transfer on behalf of another address.
286
356
 
287
357
  ```ts
288
358
  function useConfidentialTransferFrom(
289
- config: UseTokenConfig,
359
+ config: UseZamaConfig,
290
360
  options?: UseMutationOptions<Address, Error, ConfidentialTransferFromParams>,
291
361
  ): UseMutationResult<Address, Error, ConfidentialTransferFromParams>;
292
362
 
@@ -299,13 +369,13 @@ interface ConfidentialTransferFromParams {
299
369
 
300
370
  ### Shield Hooks
301
371
 
302
- #### `useShield` (alias: `useWrap`)
372
+ #### `useShield`
303
373
 
304
374
  Shield public ERC-20 tokens into confidential tokens. Handles ERC-20 approval automatically.
305
375
 
306
376
  ```ts
307
377
  function useShield(
308
- config: UseTokenConfig,
378
+ config: UseZamaConfig,
309
379
  options?: UseMutationOptions<Address, Error, ShieldParams>,
310
380
  ): UseMutationResult<Address, Error, ShieldParams>;
311
381
 
@@ -325,13 +395,13 @@ await shield({ amount: 1000n });
325
395
  await shield({ amount: 1000n, approvalStrategy: "max" });
326
396
  ```
327
397
 
328
- #### `useShieldETH` (alias: `useWrapETH`)
398
+ #### `useShieldETH`
329
399
 
330
400
  Shield native ETH into confidential tokens. Use when the underlying token is the zero address (native ETH).
331
401
 
332
402
  ```ts
333
403
  function useShieldETH(
334
- config: UseTokenConfig,
404
+ config: UseZamaConfig,
335
405
  options?: UseMutationOptions<Address, Error, ShieldETHParams>,
336
406
  ): UseMutationResult<Address, Error, ShieldETHParams>;
337
407
 
@@ -347,16 +417,17 @@ These hooks orchestrate the full unshield flow in a single call: unwrap → wait
347
417
 
348
418
  #### `useUnshield`
349
419
 
350
- Unshield a specific amount. Handles the entire unwrap + finalize flow.
420
+ Unshield a specific amount. Handles the entire unwrap + finalize flow. Supports optional progress callbacks to track each step.
351
421
 
352
422
  ```ts
353
423
  function useUnshield(
354
- config: UseTokenConfig,
424
+ config: UseZamaConfig,
355
425
  options?: UseMutationOptions<Address, Error, UnshieldParams>,
356
426
  ): UseMutationResult<Address, Error, UnshieldParams>;
357
427
 
358
428
  interface UnshieldParams {
359
429
  amount: bigint;
430
+ callbacks?: UnshieldCallbacks;
360
431
  }
361
432
  ```
362
433
 
@@ -365,18 +436,29 @@ const { mutateAsync: unshield, isPending } = useUnshield({
365
436
  tokenAddress: "0xTokenAddress",
366
437
  });
367
438
 
368
- const finalizeTxHash = await unshield({ amount: 500n });
439
+ const finalizeTxHash = await unshield({
440
+ amount: 500n,
441
+ callbacks: {
442
+ onUnwrapSubmitted: (txHash) => console.log("Unwrap tx:", txHash),
443
+ onFinalizing: () => console.log("Finalizing..."),
444
+ onFinalizeSubmitted: (txHash) => console.log("Finalize tx:", txHash),
445
+ },
446
+ });
369
447
  ```
370
448
 
371
449
  #### `useUnshieldAll`
372
450
 
373
- Unshield the entire balance. Handles the entire unwrap + finalize flow.
451
+ Unshield the entire balance. Handles the entire unwrap + finalize flow. Supports optional progress callbacks.
374
452
 
375
453
  ```ts
376
454
  function useUnshieldAll(
377
- config: UseTokenConfig,
378
- options?: UseMutationOptions<Address, Error, void>,
379
- ): UseMutationResult<Address, Error, void>;
455
+ config: UseZamaConfig,
456
+ options?: UseMutationOptions<Address, Error, UnshieldAllParams | void>,
457
+ ): UseMutationResult<Address, Error, UnshieldAllParams | void>;
458
+
459
+ interface UnshieldAllParams {
460
+ callbacks?: UnshieldCallbacks;
461
+ }
380
462
  ```
381
463
 
382
464
  ```tsx
@@ -387,6 +469,58 @@ const { mutateAsync: unshieldAll } = useUnshieldAll({
387
469
  const finalizeTxHash = await unshieldAll();
388
470
  ```
389
471
 
472
+ #### `useResumeUnshield`
473
+
474
+ Resume an interrupted unshield from a saved unwrap tx hash. Useful when the user submitted the unwrap but the finalize step was interrupted (e.g. page reload, network error). Pair with the `savePendingUnshield`/`loadPendingUnshield`/`clearPendingUnshield` utilities for persistence.
475
+
476
+ ```ts
477
+ function useResumeUnshield(
478
+ config: UseZamaConfig,
479
+ options?: UseMutationOptions<Address, Error, ResumeUnshieldParams>,
480
+ ): UseMutationResult<Address, Error, ResumeUnshieldParams>;
481
+
482
+ interface ResumeUnshieldParams {
483
+ unwrapTxHash: Hex;
484
+ callbacks?: UnshieldCallbacks;
485
+ }
486
+ ```
487
+
488
+ ```tsx
489
+ import { loadPendingUnshield, clearPendingUnshield } from "@zama-fhe/react-sdk";
490
+
491
+ const { mutateAsync: resumeUnshield } = useResumeUnshield({
492
+ tokenAddress: "0xTokenAddress",
493
+ });
494
+
495
+ // On mount, check for interrupted unshields
496
+ const pending = await loadPendingUnshield(storage, wrapperAddress);
497
+ if (pending) {
498
+ await resumeUnshield({ unwrapTxHash: pending });
499
+ await clearPendingUnshield(storage, wrapperAddress);
500
+ }
501
+ ```
502
+
503
+ #### Pending Unshield Persistence
504
+
505
+ Save the unwrap tx hash before finalization so interrupted unshields can be resumed after page reloads:
506
+
507
+ ```ts
508
+ import {
509
+ savePendingUnshield,
510
+ loadPendingUnshield,
511
+ clearPendingUnshield,
512
+ } from "@zama-fhe/react-sdk";
513
+
514
+ // Save before the finalize step
515
+ await savePendingUnshield(storage, wrapperAddress, unwrapTxHash);
516
+
517
+ // Load on next visit
518
+ const pending = await loadPendingUnshield(storage, wrapperAddress);
519
+
520
+ // Clear after successful finalization
521
+ await clearPendingUnshield(storage, wrapperAddress);
522
+ ```
523
+
390
524
  ### Unwrap Hooks (Low-Level)
391
525
 
392
526
  These hooks expose the individual unwrap steps. Use them when you need fine-grained control over the flow.
@@ -397,7 +531,7 @@ Request unwrap for a specific amount (requires manual finalization via `useFinal
397
531
 
398
532
  ```ts
399
533
  function useUnwrap(
400
- config: UseTokenConfig,
534
+ config: UseZamaConfig,
401
535
  options?: UseMutationOptions<Address, Error, UnwrapParams>,
402
536
  ): UseMutationResult<Address, Error, UnwrapParams>;
403
537
 
@@ -412,7 +546,7 @@ Request unwrap for the entire balance (requires manual finalization).
412
546
 
413
547
  ```ts
414
548
  function useUnwrapAll(
415
- config: UseTokenConfig,
549
+ config: UseZamaConfig,
416
550
  options?: UseMutationOptions<Address, Error, void>,
417
551
  ): UseMutationResult<Address, Error, void>;
418
552
  ```
@@ -423,7 +557,7 @@ Complete an unwrap by providing the decryption proof.
423
557
 
424
558
  ```ts
425
559
  function useFinalizeUnwrap(
426
- config: UseTokenConfig,
560
+ config: UseZamaConfig,
427
561
  options?: UseMutationOptions<Address, Error, FinalizeUnwrapParams>,
428
562
  ): UseMutationResult<Address, Error, FinalizeUnwrapParams>;
429
563
 
@@ -440,7 +574,7 @@ Set operator approval for the confidential token.
440
574
 
441
575
  ```ts
442
576
  function useConfidentialApprove(
443
- config: UseTokenConfig,
577
+ config: UseZamaConfig,
444
578
  options?: UseMutationOptions<Address, Error, ConfidentialApproveParams>,
445
579
  ): UseMutationResult<Address, Error, ConfidentialApproveParams>;
446
580
 
@@ -456,7 +590,7 @@ Check if a spender is an approved operator. Enabled only when `spender` is defin
456
590
 
457
591
  ```ts
458
592
  function useConfidentialIsApproved(
459
- config: UseTokenConfig,
593
+ config: UseZamaConfig,
460
594
  spender: Address | undefined,
461
595
  options?: Omit<UseQueryOptions<boolean, Error>, "queryKey" | "queryFn">,
462
596
  ): UseQueryResult<boolean, Error>;
@@ -464,7 +598,7 @@ function useConfidentialIsApproved(
464
598
 
465
599
  #### `useUnderlyingAllowance`
466
600
 
467
- Read the ERC-20 allowance of the underlying token for the wrapper.
601
+ Read the underlying ERC-20 allowance granted to the wrapper.
468
602
 
469
603
  ```ts
470
604
  function useUnderlyingAllowance(
@@ -550,6 +684,61 @@ feed?.forEach((item) => {
550
684
  });
551
685
  ```
552
686
 
687
+ ### Fee Hooks
688
+
689
+ #### `useShieldFee`
690
+
691
+ Read the shield (wrap) fee for a given amount and address pair.
692
+
693
+ ```ts
694
+ function useShieldFee(
695
+ config: UseFeeConfig,
696
+ options?: Omit<UseQueryOptions<bigint, Error>, "queryKey" | "queryFn">,
697
+ ): UseQueryResult<bigint, Error>;
698
+
699
+ interface UseFeeConfig {
700
+ feeManagerAddress: Address;
701
+ amount: bigint;
702
+ from: Address;
703
+ to: Address;
704
+ }
705
+ ```
706
+
707
+ ```tsx
708
+ const { data: fee } = useShieldFee({
709
+ feeManagerAddress: "0xFeeManager",
710
+ amount: 1000n,
711
+ from: "0xSender",
712
+ to: "0xReceiver",
713
+ });
714
+ ```
715
+
716
+ #### `useUnshieldFee`
717
+
718
+ Read the unshield (unwrap) fee for a given amount and address pair. Same signature as `useShieldFee`.
719
+
720
+ #### `useBatchTransferFee`
721
+
722
+ Read the batch transfer fee from the fee manager.
723
+
724
+ ```ts
725
+ function useBatchTransferFee(
726
+ feeManagerAddress: Address,
727
+ options?: Omit<UseQueryOptions<bigint, Error>, "queryKey" | "queryFn">,
728
+ ): UseQueryResult<bigint, Error>;
729
+ ```
730
+
731
+ #### `useFeeRecipient`
732
+
733
+ Read the fee recipient address from the fee manager.
734
+
735
+ ```ts
736
+ function useFeeRecipient(
737
+ feeManagerAddress: Address,
738
+ options?: Omit<UseQueryOptions<Address, Error>, "queryKey" | "queryFn">,
739
+ ): UseQueryResult<Address, Error>;
740
+ ```
741
+
553
742
  ### Low-Level FHE Hooks
554
743
 
555
744
  These hooks expose the raw `RelayerSDK` operations as React Query mutations.
@@ -605,42 +794,36 @@ const { data: value } = useUserDecryptedValue("0xHandleHash");
605
794
 
606
795
  ## Query Keys
607
796
 
608
- Exported query key factories for manual cache management (invalidation, prefetching, removal).
797
+ Use `zamaQueryKeys` for manual cache management (invalidation, prefetching, removal).
609
798
 
610
799
  ```ts
611
- import {
612
- confidentialBalanceQueryKeys,
613
- confidentialBalancesQueryKeys,
614
- confidentialHandleQueryKeys,
615
- confidentialHandlesQueryKeys,
616
- underlyingAllowanceQueryKeys,
617
- activityFeedQueryKeys,
618
- decryptionKeys,
619
- } from "@zama-fhe/react-sdk";
800
+ import { zamaQueryKeys, decryptionKeys } from "@zama-fhe/react-sdk";
620
801
  ```
621
802
 
622
- | Factory | Keys | Description |
623
- | ------------------------------- | --------------------------------------------------- | ----------------------------------- |
624
- | `confidentialBalanceQueryKeys` | `.all`, `.token(address)`, `.owner(address, owner)` | Single-token decrypted balance. |
625
- | `confidentialBalancesQueryKeys` | `.all`, `.tokens(addresses, owner)` | Multi-token batch balances. |
626
- | `confidentialHandleQueryKeys` | `.all`, `.token(address)`, `.owner(address, owner)` | Single-token encrypted handle. |
627
- | `confidentialHandlesQueryKeys` | `.all`, `.tokens(addresses, owner)` | Multi-token batch handles. |
628
- | `underlyingAllowanceQueryKeys` | `.all`, `.token(address, wrapper)` | Underlying ERC-20 allowance. |
629
- | `activityFeedQueryKeys` | `.all`, `.token(address)` | Activity feed items. |
630
- | `decryptionKeys` | `.value(handle)` | Individual decrypted handle values. |
803
+ | Factory | Keys | Description |
804
+ | ------------------------------------ | ---------------------------------------------------------------------------------------- | ----------------------------------- |
805
+ | `zamaQueryKeys.confidentialBalance` | `.all`, `.token(address)`, `.owner(address, owner)` | Single-token decrypted balance. |
806
+ | `zamaQueryKeys.confidentialBalances` | `.all`, `.tokens(addresses, owner)` | Multi-token batch balances. |
807
+ | `zamaQueryKeys.confidentialHandle` | `.all`, `.token(address)`, `.owner(address, owner)` | Single-token encrypted handle. |
808
+ | `zamaQueryKeys.confidentialHandles` | `.all`, `.tokens(addresses, owner)` | Multi-token batch handles. |
809
+ | `zamaQueryKeys.isAllowed` | `.all` | Session signature status. |
810
+ | `zamaQueryKeys.underlyingAllowance` | `.all`, `.token(address)`, `.scope(address, owner, wrapper)` | Underlying ERC-20 allowance. |
811
+ | `zamaQueryKeys.activityFeed` | `.all`, `.token(address)`, `.scope(address, userAddress, logsKey, decrypt)` | Activity feed items. |
812
+ | `zamaQueryKeys.fees` | `.shieldFee(...)`, `.unshieldFee(...)`, `.batchTransferFee(addr)`, `.feeRecipient(addr)` | Fee manager queries. |
813
+ | `decryptionKeys` | `.value(handle)` | Individual decrypted handle values. |
631
814
 
632
815
  ```tsx
633
816
  import { useQueryClient } from "@tanstack/react-query";
634
- import { confidentialBalanceQueryKeys } from "@zama-fhe/react-sdk";
817
+ import { zamaQueryKeys } from "@zama-fhe/react-sdk";
635
818
 
636
819
  const queryClient = useQueryClient();
637
820
 
638
821
  // Invalidate all balances
639
- queryClient.invalidateQueries({ queryKey: confidentialBalanceQueryKeys.all });
822
+ queryClient.invalidateQueries({ queryKey: zamaQueryKeys.confidentialBalance.all });
640
823
 
641
824
  // Invalidate a specific token's balance
642
825
  queryClient.invalidateQueries({
643
- queryKey: confidentialBalanceQueryKeys.token("0xTokenAddress"),
826
+ queryKey: zamaQueryKeys.confidentialBalance.token("0xTokenAddress"),
644
827
  });
645
828
  ```
646
829
 
@@ -673,62 +856,148 @@ All write hooks return `{ mutate, mutateAsync, ...mutation }` from wagmi's `useW
673
856
  | `useUnwrapFromBalance()` | `(token, from, to, encryptedBalance)` | Unwrap using on-chain handle. |
674
857
  | `useFinalizeUnwrap()` | `(wrapper, burntAmount, cleartext, proof)` | Finalize unwrap. |
675
858
  | `useSetOperator()` | `(token, spender, timestamp?)` | Set operator approval. |
676
- | `useWrap()` | `(wrapper, to, amount)` | Wrap ERC-20 tokens. |
677
- | `useWrapETH()` | `(wrapper, to, amount, value)` | Wrap native ETH. |
859
+ | `useShield()` | `(wrapper, to, amount)` | Shield ERC-20 tokens. |
860
+ | `useShieldETH()` | `(wrapper, to, amount, value)` | Shield native ETH. |
678
861
 
679
862
  ### Wagmi Signer Adapter
680
863
 
681
864
  ```ts
682
865
  import { WagmiSigner } from "@zama-fhe/react-sdk/wagmi";
683
866
 
684
- const signer = new WagmiSigner(wagmiConfig);
867
+ const signer = new WagmiSigner({ config: wagmiConfig });
685
868
  ```
686
869
 
687
- ## Viem & Ethers Adapter Hooks
870
+ ## Signer Adapters
688
871
 
689
- Both `@zama-fhe/react-sdk/viem` and `@zama-fhe/react-sdk/ethers` export the same set of read/write hooks, but typed for their respective libraries. They also include `Suspense` variants of all read hooks.
872
+ Signer adapters are provided by the core SDK package:
690
873
 
691
- ### Read hooks
874
+ ```ts
875
+ import { ViemSigner } from "@zama-fhe/sdk/viem";
876
+ import { EthersSigner } from "@zama-fhe/sdk/ethers";
877
+ ```
692
878
 
693
- `useConfidentialBalanceOf`, `useWrapperForToken`, `useUnderlyingToken`, `useWrapperExists`, `useSupportsInterface` — plus `*Suspense` variants.
879
+ ## Wallet Integration Guide
694
880
 
695
- - **viem:** First parameter is `PublicClient`.
696
- - **ethers:** First parameter is `Provider | Signer`.
881
+ ### SSR / Next.js
697
882
 
698
- ### Write hooks
883
+ All components using SDK hooks must be client components. Add `"use client"` at the top of files that import from `@zama-fhe/react-sdk`. FHE operations (encryption, decryption) run in a Web Worker and require browser APIs — they cannot execute on the server.
699
884
 
700
- `useConfidentialTransfer`, `useConfidentialBatchTransfer`, `useUnwrap`, `useUnwrapFromBalance`, `useFinalizeUnwrap`, `useSetOperator`, `useShield`, `useShieldETH`.
885
+ ```tsx
886
+ "use client";
701
887
 
702
- - **viem:** Mutation params include `client: WalletClient`.
703
- - **ethers:** Mutation params include `signer: Signer`.
888
+ import { useConfidentialBalance } from "@zama-fhe/react-sdk";
889
+ ```
704
890
 
705
- ### Signer adapters
891
+ Place `ZamaProvider` inside your client-only layout. Do **not** create the relayer or signer at the module level in a server component — wrap them in a client component or use lazy initialization.
706
892
 
707
- ```ts
708
- // Re-exported for convenience
709
- import { ViemSigner } from "@zama-fhe/react-sdk/viem";
710
- import { EthersSigner } from "@zama-fhe/react-sdk/ethers";
893
+ ### FHE Credentials Lifecycle
894
+
895
+ FHE decrypt credentials are generated once per wallet + token set and cached in the storage backend you provide (e.g. `IndexedDBStorage`). The wallet signature is kept **in memory only** — never persisted to disk. The lifecycle:
896
+
897
+ 1. **First decrypt** — SDK generates an FHE keypair, creates EIP-712 typed data, and prompts the wallet to sign. The encrypted credential is stored; the signature is cached in memory.
898
+ 2. **Same session** — Cached credentials and session signature are reused silently (no wallet prompt).
899
+ 3. **Page reload** — Encrypted credentials are loaded from storage; the wallet is prompted once to re-sign for the session.
900
+ 4. **Expiry** — Credentials expire based on `keypairTTL` (default: 86400s = 1 day). After expiry, the next decrypt regenerates and re-prompts.
901
+ 5. **Pre-authorization** — Call `useTokenAllow(tokenAddresses)` early to batch-authorize all tokens in one wallet prompt, avoiding repeated popups.
902
+ 6. **Check status** — Use `useIsTokenAllowed(tokenAddress)` to conditionally enable UI elements (e.g. disable "Reveal" until allowed).
903
+ 7. **Disconnect** — Call `useTokenRevoke(tokenAddresses)` or `await credentials.revoke()` to clear the session signature from memory.
904
+
905
+ ### Web Extension Support
906
+
907
+ By default, wallet signatures are stored in memory and lost on page reload (or service worker restart). For MV3 web extensions, use the built-in `chromeSessionStorage` singleton so signatures survive service worker restarts and are shared across popup, background, and content script contexts:
908
+
909
+ ```tsx
910
+ import { chromeSessionStorage } from "@zama-fhe/react-sdk";
911
+
912
+ <ZamaProvider
913
+ relayer={relayer}
914
+ signer={signer}
915
+ storage={indexedDBStorage}
916
+ sessionStorage={chromeSessionStorage}
917
+ >
918
+ <App />
919
+ </ZamaProvider>;
920
+ ```
921
+
922
+ This keeps the encrypted credentials in IndexedDB (persistent) while the unlock signature lives in `chrome.storage.session` (ephemeral, cleared when the browser closes).
923
+
924
+ ### Error-to-User-Message Mapping
925
+
926
+ Map SDK errors to user-friendly messages in your UI:
927
+
928
+ ```tsx
929
+ import {
930
+ SigningRejectedError,
931
+ EncryptionFailedError,
932
+ DecryptionFailedError,
933
+ TransactionRevertedError,
934
+ ApprovalFailedError,
935
+ } from "@zama-fhe/react-sdk";
936
+
937
+ function getUserMessage(error: Error): string {
938
+ if (error instanceof SigningRejectedError)
939
+ return "Transaction cancelled — please approve in your wallet.";
940
+ if (error instanceof EncryptionFailedError) return "Encryption failed — please try again.";
941
+ if (error instanceof DecryptionFailedError) return "Decryption failed — please try again.";
942
+ if (error instanceof ApprovalFailedError) return "Token approval failed — please try again.";
943
+ if (error instanceof TransactionRevertedError)
944
+ return "Transaction failed on-chain — check your balance.";
945
+ return "An unexpected error occurred.";
946
+ }
947
+ ```
948
+
949
+ Or use `matchZamaError` for a more concise pattern:
950
+
951
+ ```tsx
952
+ import { matchZamaError } from "@zama-fhe/react-sdk";
953
+
954
+ const message = matchZamaError(error, {
955
+ SIGNING_REJECTED: () => "Transaction cancelled — please approve in your wallet.",
956
+ ENCRYPTION_FAILED: () => "Encryption failed — please try again.",
957
+ DECRYPTION_FAILED: () => "Decryption failed — please try again.",
958
+ APPROVAL_FAILED: () => "Token approval failed — please try again.",
959
+ TRANSACTION_REVERTED: () => "Transaction failed on-chain — check your balance.",
960
+ _: () => "An unexpected error occurred.",
961
+ });
962
+ ```
963
+
964
+ ### Balance Caching and Refresh
965
+
966
+ Balance queries use two-phase polling:
967
+
968
+ 1. **Phase 1 (cheap)** — Polls the encrypted balance handle via a read-only RPC call at `handleRefetchInterval` (default: 10s).
969
+ 2. **Phase 2 (expensive)** — Only when the handle changes (i.e. balance updated on-chain), triggers an FHE decryption via the relayer.
970
+
971
+ This means balances update within `handleRefetchInterval` ms of any on-chain change, without wasting decryption resources. Mutation hooks (`useConfidentialTransfer`, `useShield`, `useUnshield`, etc.) automatically invalidate the relevant caches on success, so the UI updates immediately after user actions.
972
+
973
+ To force a refresh:
974
+
975
+ ```tsx
976
+ const queryClient = useQueryClient();
977
+ queryClient.invalidateQueries({ queryKey: zamaQueryKeys.confidentialBalance.all });
711
978
  ```
712
979
 
713
980
  ## Re-exports from Core SDK
714
981
 
715
982
  All public exports from `@zama-fhe/sdk` are re-exported from the main entry point. You never need to import from the core package directly.
716
983
 
717
- **Classes:** `RelayerWeb`, `TokenSDK`, `Token`, `ReadonlyToken`, `MemoryStorage`, `IndexedDBStorage`, `indexedDBStorage`, `CredentialsManager`.
984
+ **Classes:** `RelayerWeb`, `ZamaSDK`, `Token`, `ReadonlyToken`, `MemoryStorage`, `memoryStorage`, `IndexedDBStorage`, `indexedDBStorage`, `CredentialsManager`.
718
985
 
719
986
  **Network configs:** `SepoliaConfig`, `MainnetConfig`, `HardhatConfig`.
720
987
 
721
- **Types:** `Address`, `TokenSDKConfig`, `TokenConfig`, `ReadonlyTokenConfig`, `NetworkType`, `RelayerSDK`, `RelayerSDKStatus`, `EncryptResult`, `EncryptParams`, `UserDecryptParams`, `PublicDecryptResult`, `FHEKeypair`, `EIP712TypedData`, `DelegatedUserDecryptParams`, `KmsDelegatedUserDecryptEIP712Type`, `ZKProofLike`, `InputProofBytesType`, `BatchTransferData`, `StoredCredentials`, `GenericSigner`, `GenericStringStorage`, `ContractCallConfig`, `TransactionReceipt`, `UnshieldCallbacks`.
988
+ **Pending unshield:** `savePendingUnshield`, `loadPendingUnshield`, `clearPendingUnshield`.
989
+
990
+ **Types:** `Address`, `ZamaSDKConfig`, `ZamaConfig`, `ReadonlyTokenConfig`, `NetworkType`, `RelayerSDK`, `RelayerSDKStatus`, `EncryptResult`, `EncryptParams`, `UserDecryptParams`, `PublicDecryptResult`, `FHEKeypair`, `EIP712TypedData`, `DelegatedUserDecryptParams`, `KmsDelegatedUserDecryptEIP712Type`, `ZKProofLike`, `InputProofBytesType`, `BatchTransferData`, `StoredCredentials`, `GenericSigner`, `GenericStorage`, `ContractCallConfig`, `TransactionReceipt`, `TransactionResult`, `UnshieldCallbacks`.
722
991
 
723
- **Errors:** `TokenError`, `TokenErrorCode`, `SigningRejectedError`, `SigningFailedError`, `EncryptionFailedError`, `DecryptionFailedError`, `ApprovalFailedError`, `TransactionRevertedError`, `InvalidCredentialsError`, `NoCiphertextError`, `RelayerRequestFailedError`.
992
+ **Errors:** `ZamaError`, `ZamaErrorCode`, `SigningRejectedError`, `SigningFailedError`, `EncryptionFailedError`, `DecryptionFailedError`, `ApprovalFailedError`, `TransactionRevertedError`, `InvalidKeypairError`, `NoCiphertextError`, `RelayerRequestFailedError`, `matchZamaError`.
724
993
 
725
994
  **Constants:** `ZERO_HANDLE`, `ERC7984_INTERFACE_ID`, `ERC7984_WRAPPER_INTERFACE_ID`.
726
995
 
727
996
  **ABIs:** `ERC20_ABI`, `ERC20_METADATA_ABI`, `DEPLOYMENT_COORDINATOR_ABI`, `ERC165_ABI`, `ENCRYPTION_ABI`, `FEE_MANAGER_ABI`, `TRANSFER_BATCHER_ABI`, `WRAPPER_ABI`, `BATCH_SWAP_ABI`.
728
997
 
729
- **Events:** `RawLog`, `ConfidentialTransferEvent`, `WrappedEvent`, `UnwrapRequestedEvent`, `UnwrappedFinalizedEvent`, `UnwrappedStartedEvent`, `TokenEvent`, `Topics`, `TOKEN_TOPICS`.
998
+ **Events:** `RawLog`, `ConfidentialTransferEvent`, `WrappedEvent`, `UnwrapRequestedEvent`, `UnwrappedFinalizedEvent`, `UnwrappedStartedEvent`, `OnChainEvent`, `Topics`, `TOKEN_TOPICS`.
730
999
 
731
- **Event decoders:** `decodeConfidentialTransfer`, `decodeWrapped`, `decodeUnwrapRequested`, `decodeUnwrappedFinalized`, `decodeUnwrappedStarted`, `decodeTokenEvent`, `decodeTokenEvents`, `findUnwrapRequested`, `findWrapped`.
1000
+ **Event decoders:** `decodeConfidentialTransfer`, `decodeWrapped`, `decodeUnwrapRequested`, `decodeUnwrappedFinalized`, `decodeUnwrappedStarted`, `decodeOnChainEvent`, `decodeOnChainEvents`, `findUnwrapRequested`, `findWrapped`.
732
1001
 
733
1002
  **Activity feed:** `ActivityDirection`, `ActivityType`, `ActivityAmount`, `ActivityLogMetadata`, `ActivityItem`, `parseActivityFeed`, `extractEncryptedHandles`, `applyDecryptedValues`, `sortByBlockNumber`.
734
1003