For the complete documentation index, see llms.txt. This page is also available as Markdown.

Migrate from v2 to v3

Step-by-step guide to upgrade an app from @zama-fhe/sdk 2.x to 3.x — config factory, relayer factories, permits, operators, encrypt/decrypt glossary.

This guide upgrades an application that uses @zama-fhe/sdk and @zama-fhe/react-sdk from 2.5.0 (the last 2.x release) to the 3.x line. Each step has an explicit Before (2.x) / After (3.x) pair and a find/replace rule, so it works whether you migrate by hand or hand it to an AI coding agent.

The happy path: for most apps it's Step 1 (config) plus mechanical renames — the high-level Token flow API keeps its signatures. Only three surfaces actually moved: approve → operators (Step 4), token delegation → sdk.delegations.* (Step 3), and balanceOf now takes the holder (Step 4).

Before you start. This assumes a working app on @zama-fhe/sdk / @zama-fhe/react-sdk 2.5.0 (upgrade to 2.5.0 first if you're below it), a clean git tree so you can review the migration as a diff, and Node 22+. The API here is complete as of 3.1.x; the 3.0 major bump was an on-chain wrapper/registry upgrade (Step 6), not a TypeScript change. It's a code-only migration — to roll back, discard the diff and reinstall @^2 (the deployed Step 6 contract upgrade isn't something your app reverts).

Migrate with an AI coding agent

This guide is built to be executed by an AI coding agent (Claude Code, Cursor, Copilot, …). The fastest path is to point an agent at your repository with the prompt below. Read the rest of this page if you'd rather migrate by hand — or to review what the agent is doing.

Upgrade this repository from @zama-fhe/sdk and @zama-fhe/react-sdk v2.x to v3.x.

SOURCE OF TRUTH — follow it exactly:
https://docs.zama.org/protocol/sdk/migration/migrate-v2-to-v3.md

Rules:
1. Fetch and read that guide BEFORE doing anything. It is authoritative. Do NOT
   rely on prior knowledge of the "Zama SDK": this is the high-level
   @zama-fhe/sdk, NOT the legacy @zama-fhe/relayer-sdk (createInstance/initSDK).
   For any symbol you're unsure about, use the guide's symbol-mapping table; for
   anything it doesn't cover, fetch
   https://raw.githubusercontent.com/zama-ai/sdk/main/llms.txt and follow its links.
2. First print a short PLAN: list every file importing @zama-fhe/sdk or
   @zama-fhe/react-sdk and which guide Steps apply to each. Then proceed.
3. Apply the Steps IN ORDER, starting with Step 1 (configuration) — it unblocks
   the rest. Use the symbol table for renames. Respect the per-symbol notes: the
   React hook calling convention is MIXED — see the Step 4 convention table (most
   hooks are positional `useX(address)`, the rest take `{ address }`, and the old
   `{ tokenAddress }` field is gone), and several signatures changed (e.g.
   balanceOf(owner), isOperator(holder, spender)).
4. Do NOT change app logic or unrelated code. The high-level Token flow API
   (shield, confidentialTransfer, unshield, …) is unchanged except where the
   guide says otherwise.
5. Bump the @zama-fhe/* deps to ^3 using this repo's package manager.
6. VALIDATE: run the type checker and the guide's final leftover-symbol `rg`
   sweep; fix until both are clean. Inspect the false-positive cases the guide
   calls out (.chainId on viem/EIP-1193 objects, token.approve on the underlying
   ERC-20, tokenAddress in your own variable names) instead of blind-replacing.
7. Show the result as a diff and flag anything ambiguous for human review.

No repo access (a plain chat assistant)? Paste this page as the source of truth, then the contents of your SDK-using files, and ask for the v3 rewrite of each under the same rules.

Step 0 — Install

Symbol mapping (quick reference)

The tables below are the authoritative list of renames. Skim for the symbol you need and jump to the cited Step, or read Steps 1→7 in order for a full migration.

@zama-fhe/sdk (core)

2.x
3.x
Step

ZamaSDKConfig

ZamaConfig (+ ZamaConfigViem/ZamaConfigEthers/ZamaConfigWagmi)

new ZamaSDK({ relayer, signer, storage })

new ZamaSDK(createConfig({ chains, …client, relayers, storage }))

SepoliaConfig / MainnetConfig / HardhatConfig (from @zama-fhe/sdk)

sepolia / mainnet / hardhat (+ hoodi, anvil, ingenTestnet, bscTestnet) from @zama-fhe/sdk/chains

<chainConfig>.chainId

<chain>.id

ViemSigner / EthersSigner (constructed)

pass publicClient/walletClient (or ethers provider/signer) to createConfig

keypairTTL (config option)

transportKeyPairTTL

new RelayerWeb(...)

web() from @zama-fhe/sdk/web

new RelayerNode(...)

node() from @zama-fhe/sdk/node

CredentialsManager / DelegatedCredentialsManager

Permits / Delegations / Decryption

CredentialsManagerConfig, Credentials*Event, StoredCredentials, StoredKeypair

Permission, StoredTransportKeyPair (+ permit events)

token.approve(spender[, expiry])

token.setOperator(operator[, expiry])

token.isApproved(spender[, owner])

token.isOperator(holder, spender)

token.balanceOf() (self default)

token.balanceOf(owner) — owner address now required

EncryptResult.handles (bytes)

EncryptResult.encryptedValues (hex; inputProof is now hex too)

extractEncryptedHandles(...)

removed — read result.encryptedValues

Handle (type), ClearValueType

EncryptedValue (term), ClearValue

ZERO_HANDLE / isZeroHandle()

ZERO_ENCRYPTED_VALUE / isEncryptedValueZero()

UserDecryptParams, PublicDecryptResult, DelegatedUserDecryptParams, DecryptHandle

DecryptValuesParams, DecryptPublicValuesResult, DelegatedDecryptValuesParams, DecryptInput

applyDecryptedValues, DecryptCache

removed — handled by the SDK's internal cache

KeypairType / Keypair; generateKeypair() / warmKeypair()

TransportKeyPair; generateTransportKeyPair() / warmTransportKeyPair()

relayer getPublicKey(); PublicKeyData

fetchFheEncryptionKeyBytes(); FheEncryptionKey

KeypairExpiredError / InvalidKeypairError

TransportKeyPairExpiredError / InvalidTransportKeyPairError

sdk.createReadonlyToken(addr); sdk.createToken(addr, wrapper?)

sdk.createToken(addr) (read/transfer); sdk.createWrappedToken(addr) (wrap/shield/unshield)

ReadonlyToken

Token (read/transfer) / WrappedToken (wrap)

decodeUnwrappedFinalized, UnwrappedFinalizedEvent

decodeUnwrapFinalized, UnwrapFinalizedEvent

decodeUnwrappedStarted, UnwrappedStartedEvent

removed

parseActivityFeed, ActivityItem, ActivityAmount, ActivityType

removed (activity feed dropped)

totalSupplyContract, matchAclRevert, sortByBlockNumber

removed

@zama-fhe/react-sdk (hooks)

2.x
3.x
Step

<ZamaProvider relayer signer storage sessionStorage onEvent>

<ZamaProvider config={createConfig({…})}> (no sessionStorage)

new WagmiSigner({ config })

createConfig from @zama-fhe/react-sdk/wagmi

useAllow / useIsAllowed

useGrantPermit / useHasPermit

useGenerateKeypair

removed — permits are managed by the SDK

useCreateEIP712 / useCreateDelegatedUserDecryptEIP712

removed — use useGrantPermit

useDelegatedUserDecrypt

useDelegatedDecryptValues

useRevoke

useRevokePermits — revoke permits, keep the transport key pair

useRevokeSession

useClearCredentials — full logout (also wipes the transport key pair)

useConfidentialApprove

useConfidentialSetOperator

useConfidentialIsApproved (+ Suspense)

useConfidentialIsOperator (+ Suspense)

token hooks taking { tokenAddress }

positional (address) or { address } — see the convention table

useUserDecrypt({ handles })

useDecryptValues(inputs) — renamed + arg shape change, see Step 5

usePublicDecrypt

useDecryptPublicValues — public-decrypt mutation, see Step 5

usePublicKey, usePublicParams, useRequestZKProofVerification

removed — low-level key/proof hooks; the SDK handles these

useReadonlyToken

useWrappedToken

useActivityFeed

removed (activity feed dropped)


Step 1 — Migrate the SDK configuration

This is the central change and affects every integration. The imperative "construct a Signer, construct a Relayer, pass them in" pattern is replaced by a single declarative createConfig({ chains, …client, relayers, storage }).

Why: v2 bound one relayer to one active chain (a second chain meant a second ZamaSDK); createConfig declares every chain and its relayer once, making multichain first-class.

Key shifts:

  • Chain presets move to @zama-fhe/sdk/chains and expose .id (not .chainId).

  • You no longer construct ViemSigner / EthersSigner / WagmiSigner. You pass the underlying clients (publicClient + walletClient, ethers provider + signer, or wagmiConfig) to createConfig.

  • Relayers become factories (web() / node()) placed in a relayers map keyed by each chain's id. See Step 2.

  • new ZamaSDK(config) / <ZamaProvider config={config}> take the object returned by createConfig.

Node / backend (viem)

If you also construct viem clients here (createPublicClient / createWalletClient), import viem's own sepolia under an alias (e.g. sepolia as viemSepolia) to avoid colliding with the sepolia preset from @zama-fhe/sdk/chains.

React (wagmi)

Notes:

  • The wagmi adapter creates the SDK signer/provider and subscribes to wagmi connection changes internally — no useMemo for the relayer and no walletKey remount pattern needed.

  • The 2.x sessionStorage prop is removed. There is now a single storage option (permits reuse it via permitStorage, which defaults to storage), so the separate new IndexedDBStorage("SessionStore") is no longer required.

  • All wiring (relayer, signer, storage, onEvent) moves into createConfig; <ZamaProvider> takes a single config prop.

Other adapters

Adapter

createConfig import

Clients to pass

viem

@zama-fhe/sdk/viem

publicClient, walletClient

ethers

@zama-fhe/sdk/ethers

provider, signer

wagmi (React)

@zama-fhe/react-sdk/wagmi

wagmiConfig

generic

@zama-fhe/sdk

provider, signer (GenericProvider/BaseSigner)

Step 2 — Migrate the relayer

Relayers are no longer classes you instantiate; they are factories placed in a relayers map keyed by each chain's id, inside createConfig.

2.x
3.x
Import

new RelayerWeb({...})

web()

@zama-fhe/sdk/web

new RelayerNode({...})

node()

@zama-fhe/sdk/node

(new in v3)

cleartext()

@zama-fhe/sdk

cleartext() is new in v3 — the relayer for cleartext-mode chains (no FHE, KMS, or gateway): local dev (hardhat) and the cleartext testnets (hoodi, ingenTestnet, bscTestnet).

The getChainId / transports plumbing is gone: the network endpoint, relayer URL and auth are configured on the FheChain object (network, relayerUrl, auth) and the SDK resolves the right relayer per chain via RelayerDispatcher.

Imported the relayer config types directly? They followed the constructor → factory move: node() / web() / cleartext() return NodeRelayerConfig / WebRelayerConfig / CleartextRelayerConfig (all extend RelayerConfig). The relayer-sdk-level RelayerWebConfig / RelayerWebSecurityConfig are unchanged but now live under @zama-fhe/sdk/web.

Relayer auth (FheChain.auth). Still ApiKeyHeader | ApiKeyCookie | BearerToken. The Zama-hosted relayer requires ApiKeyHeader (sent as x-api-key; Bearer and cookie are rejected at the edge). Field names differ — ApiKeyHeader uses value, BearerToken uses token. See Relayer API keys.

Step 3 — Permits & delegated decryption

The "credentials/session" vocabulary is replaced by the permit model. A permit is a reusable EIP-712 signature granting your app decrypt rights for a set of contracts. See the Permit model concept page.

The mental model changed, not just the names:

  • 2.x: your app held a session — a decrypt transport key pair (useGenerateKeypair) plus per-contract credentials (the grants), all under one TTL. The key pair was chain-scoped, so switching chains threw it away.

  • 3.x: the two are decoupled — one identity transport key pair (owned by the SDK, shared across all chains, surviving chain switches) backs many independent permits (the grants). Adding a contract signs just an incremental permit rather than re-issuing the whole set, and the two revocation hooks split along that seam: useRevokePermits drops grants but keeps the key pair, useClearCredentials is a full logout.

In most apps you do not manage permits manually — decrypt hooks (useDecryptValues, useConfidentialBalance) trigger the permit signature automatically on first use. The explicit hooks are for gating that prompt and for revocation. The full hook renames are in the react-sdk symbol table.

Recommended pattern — gate any decrypt UI on useHasPermit so users don't get an unsolicited wallet popup on render:

SDK-level delegation moved off the Token instance onto the sdk.delegations namespace, and now takes the contract address explicitly. Only decryptBalanceAs stays on Token.

2.x
3.x

token.delegateDecryption({ delegateAddress })

sdk.delegations.delegateDecryption({ contractAddress: token.address, delegateAddress })

token.revokeDelegation({ delegateAddress })

sdk.delegations.revokeDelegation({ contractAddress: token.address, delegateAddress })

token.getDelegationExpiry({ delegatorAddress, … })

sdk.delegations.getExpiry({ contractAddress, delegatorAddress, delegateAddress })

(no Token-level status check)

sdk.delegations.isActive({ contractAddress, delegatorAddress, delegateAddress })

token.decryptBalanceAs(...)

unchanged — stays on Token

Step 4 — Approvals → operators

ERC-7984 uses an operator model instead of ERC-20-style allowances.

Why: approve borrowed ERC-20's verb, but a confidential balance is encrypted — there's no cleartext amount to cap. What you grant is time-boxed authority to move your tokens, which is what the on-chain setOperator (ERC-7984) does.

The write side is a pure rename — v2's token.approve() already called the on-chain setOperator, so behaviour is unchanged. The read side has one trap:

The hook calling convention changed for every single-token hook

This is the easiest thing to under-migrate. The 2.x UseZamaConfig type ({ tokenAddress }) was removed. Every single-token hook either takes the token address positionally as its first argument, or keeps a config object with the field renamed tokenAddressaddress. Don't assume a hook is unchanged just because its name is:

Calling convention
Hooks

Positional — useX(address, options?)

useConfidentialSetOperator, useConfidentialTransferFrom, useApproveUnderlying, useUnshield, useUnshieldAll, useUnwrap, useUnwrapAll, useResumeUnshield, useFinalizeUnwrap, useDelegateDecryption, useRevokeDelegation, useDecryptBalanceAs, useToken, useWrappedToken

Config object — useX({ address, … }) (was { tokenAddress })

useShield, useConfidentialTransfer, useConfidentialBalance, useConfidentialBalances, useConfidentialIsOperator, useUnderlyingAllowance, useDelegationStatus

Read hooks that target a holder (useConfidentialBalance, useConfidentialBalances) now also require an explicit account.

Balance reads now take the holder explicitly

The 2.x convenience of defaulting balance reads to the connected account is gone — pass the holder address:

The React hook config changed to match: useConfidentialBalance({ tokenAddress }) becomes useConfidentialBalance({ address, account })address is the token, account is the holder to read.

Step 5 — Encrypt (hex) & decrypt glossary

Encrypt returns contract-ready hex

encrypt results are now hex strings ready to pass straight to a contract call — no more bytesToHex(...). The field handles is renamed encryptedValues (and inputProof is hex too), and extractEncryptedHandles(...) is removed — read result.encryptedValues directly.

sdk.signer may be undefined in 3.x. In 2.x the signer was passed at construction and always present; in 3.x it is undefined in read-only mode (no wallet connected) — hence the if (!sdk.signer) throw … guard above. Prefer that over asserting sdk.signer!, which only hides the undefined until it crashes at the call site. Reads never need the signer — use sdk.provider.

Decrypt glossary: handleencryptedValue

useUserDecrypt was renamed useDecryptValues, and its argument changed: from an object { handles } to a positional array of { encryptedValue, contractAddress }. Result objects are keyed by encryptedValue (not handle). Reads also move from sdk.signer.readContract to sdk.provider.readContractsdk.provider is always available, whereas in 2.x reads went through the signer.

Public (non-permit) decryption follows the same rename: usePublicDecryptuseDecryptPublicValues. Both are mutations, so only the hook name changes — no permit is involved since the values are already publicly decryptable. The verb now says who reads — decryptValues (you, via your permit) vs decryptPublicValues (everyone).

Key glossary: transport key pair & FHE encryption key

The glossary pass split two keys the old names blurred: the transport key pair (your locally-held decrypt keys) and the FHE encryption key (the network's input-encryption key). The core symbol table lists every rename — most apps touch none of them, since createConfig, the Token API, and the hooks manage keys internally.

Error codes are stable. Only the error class names and ZamaErrorCode enum keys changed (KeypairExpiredErrorTransportKeyPairExpiredError, enum key KeypairExpiredTransportKeyPairExpired, etc.). The string code values (KEYPAIR_EXPIRED / INVALID_KEYPAIR) are unchanged, so matchZamaError and err.code === "KEYPAIR_EXPIRED" checks keep working.

Step 6 — Token / WrappedToken & upgraded contracts

  • ReadonlyToken was the read-only base in 2.x. In 3.x the base read/transfer class is Token, and WrappedToken extends it with wrap/shield/unshield. Build them via sdk.createToken(addr) (read/transfer) or sdk.createWrappedToken(addr) — the 2.x sdk.createReadonlyToken(addr) is removed and sdk.createToken(addr, wrapper?) lost its second argument. The hook useReadonlyTokenuseWrappedToken.

  • The wrapper/registry contracts were upgraded in 3.0. If you read registry results, check the new isValid flag before using a wrapper:

  • Unwrap events/results now carry a new optional unwrapRequestId field. If you decode unwrap events directly: decodeUnwrappedFinalizeddecodeUnwrapFinalized (and UnwrappedFinalizedEventUnwrapFinalizedEvent), and the "started" decoder/event (decodeUnwrappedStarted / UnwrappedStartedEvent) were removed. If you only use unshield/unshieldAll/useUnshield, no change is needed.

  • If you call Token.finalizeUnwrap directly (an escape hatch — most apps use unshield), its argument changed from burnAmountHandle to unwrapRequestId: EncryptedValue (the id returned by the request phase).

  • If you read decoded transfer events directly, the field ConfidentialTransferEvent.encryptedAmountHandle was renamed encryptedAmount (part of the handleencryptedValue glossary shift, Step 5).

  • If you hardcoded ERC7984_WRAPPER_INTERFACE_ID, its value changed; import the constant instead of inlining it.

Step 7 — Removed with no replacement

  • Activity feed is gone: useActivityFeed, parseActivityFeed, ActivityItem, ActivityAmount, ActivityType, activityFeedQueryOptions, deriveActivityFeedLogsKey. It was a prebuilt transaction-history view — and what that history shows and how it's grouped is your app's call, not the SDK's. You keep every building block: decode events with decodeOnChainEvent and reveal amounts with decryptValues / decryptPublicValues, or read from your own indexer.

  • Utility exports totalSupplyContract, matchAclRevert, sortByBlockNumber are removed.

Validation checklist

After applying the steps:

  1. Run your type-checker (pnpm typecheck, tsc --noEmit, …) — the SDK is strongly typed; most missed renames surface here.

  2. Search your codebase for leftover 2.x symbols:

    A few atoms can still produce hits that don't need migrating — inspect rather than blind-replace: .chainId is legitimate on viem/EIP-1193 objects (only chain-preset accesses like SepoliaConfig.chainId migrate to .id), token.approve( only matters for the Zama confidential token (approving an underlying ERC-20 before a manual wrap is unchanged), and tokenAddress appears in plenty of your own variable names — only the hook config field migrates to address.

  3. Verify the SDK is built once via createConfig and <ZamaProvider> / new ZamaSDK receive its result.

  4. Run a smoke flow (shield → transfer → unshield, or encrypt → store → decrypt) against a local cleartext() chain first, then a testnet.

  5. Hitting a renamed error class (TransportKeyPairExpiredError, …) or the now-nullable sdk.signer? See Handle errors.

Next steps

Help center

Stuck on the migration, or spotted a step or rename this guide is missing? Open an issue in the SDK repository — migration gaps are useful feedback. For general questions, ask the community:

Last updated