2.0 RC docsView 1.x docs
Upgrading · 1.x to 2.0

Upgrading from 1.x to 2.0

One module is replaced, a handful of deprecated names are removed, and the Zama peers move to 3.6.

v2.0.0-rc.1 release candidate
The API is frozen and later candidates carry fixes only, except the receipt-free and Safe create surface of /fhe-airdrop, which is @beta. Published on the next dist-tag; latest stays on 1.6.0 until 2.0.0.

2.0 is a major version because /fhe-airdrop now targets a different contract system. Everything else is the 1.x surface minus the names 1.6 already deprecated, plus the port to @zama-fhe/sdk 3.6. If you do not import /fhe-airdrop, expect a short upgrade: fix what typecheck reports, then re-check the encryptor and decryptor wiring.

Install and support#

The 2.0 line is published on the next dist-tag (v2.0.0-rc.1 today). latest stays on 1.6.0 until 2.0.0 ships, so a plain pnpm add @tokenops/sdk still installs 1.x.

pnpm add @tokenops/sdk@next
# Only if you encrypt or decrypt (the Zama peers are optional):
pnpm add @zama-fhe/sdk@~3.6.0
pnpm add @zama-fhe/react-sdk@~3.6.0 # React hooks only
LineSupport
1.xCritical security and correctness fixes only, for 90 days after 2.0.0 reaches latest. No feature work since 2026-09-05. Nothing in 1.x breaks or is withdrawn when 2.0.0 ships, and the v1 contracts stay live on-chain.
2.0 release candidatesPublished on next. The API is frozen: a later candidate carries fixes, not API changes, except the receipt-free and Safe create surface of /fhe-airdrop, which is @beta.
2.0.0 and laterMoves to latest after the release candidates, then follows normal SemVer.

Still running a v1 airdrop campaign? A 2.x client cannot address a v1 instance, so keep that code on 1.x until the campaign closes:

# Keep operating a v1 airdrop campaign on the 1.x line
pnpm add @tokenops/sdk@^1.6.0

What breaks, module by module#

SubpathWhat breaksWhere to go
/fhe-airdropReplaced. v2 is a different contract system behind the same subpath: separate ECDSA and Merkle clients, a new factory, a new EIP-712 claim signature signed by SIGNER_ROLE, renamed create params, and four new DEPLOYED_ADDRESSES keys. The v1 module is deleted.Airdrop v1 to v2
/fhe-vestinguseGet* encrypted-view hooks removed; CreateVestingPreflightReport.blockers removed; PredictManagerArgs moved to /advanced(/react); PartialClaimArgs discriminated by feeType; role hooks name the grantee holder; create and split hooks resolve Mined | Pending.Below
/fhe-disperseuseGetEncryptedFeeReserve removed; PreflightReport.blockers removed; a missing approval is OperatorNotApprovedError; transferredHandle always set; role hooks take holder; register, disperse and withdraw hooks resolve Mined | Pending.Below
/fheEncryptedInput64, EncryptedHandle and ExternalInputProof removed; createSepoliaEncryptorWeb moved to /fhe/web; Encryptor and UserDecryptor follow Zama 3.6; useDecryptedHandle drops relayerParams.Zama SDK 3.0 to 3.6
@tokenops/sdkDisperseId, asDisperseId, ClientPair and ContractRef removed.Below
/testnet-faucetNo removals. Hooks return a constructor error as resolutionError instead of throwing during render, and the unsupported-chain error names Sepolia. Additions: gas headroom, query options, telemetry on hooks.Testnet Faucet 2.0

/fhe-airdrop#

There is no in-place upgrade: v1 instances stay on v1 contracts, and a v2 campaign is created through the canonical v2 factory. The symbol-by-symbol delta, the new claim signature and the address registry reshape are in Migrating to airdrop v2. useIsOperator and useEnsureOperator are unaffected: they live on @tokenops/sdk/fhe/react.

/fhe-vesting#

  • useGetVestedAmount, useGetClaimableAmount, useGetTotalAllocation, useGetSettledAmount and their UseGet*Args are removed. Use the useAccess* names; they are the same functions.
  • CreateVestingPreflightReport.blockers is removed. Use blockerErrors; ready is unchanged.
  • PredictManagerArgs moved from /fhe-vesting/react to /fhe-vesting/advanced/react (still exported from /fhe-vesting/advanced).
  • PartialClaimArgs is discriminated by feeType: { feeType: FeeType.Gas, value } on a gas-fee manager, { feeType: FeeType.DistributionToken } otherwise. A mismatched msg.value now fails to compile.
  • useHasRole, useGrantRole and useRevokeRole take holder. account / accountTarget still work and are deprecated.
  • useCreateManager, useCreateManagerAndGetAddress and useSplitVesting resolve Mined | Pending; narrow before reading a receipt-derived field. Headless calls that never pass waitForReceipt keep the mined result type. See Receipt-free writes.
  • The fhe-vesting.react.hook.fired telemetry event is gone.
  • preflightClaim reports a claimant whose ETH balance is below the gas fee as InsufficientBalanceError (TOKENOPS_INSUFFICIENT_BALANCE, balanceKind: "eth"), not InsufficientFeeError. Typecheck does not catch this; update any branch on TOKENOPS_INSUFFICIENT_FEE.
  • batchCreateVesting takes at most 32 items, one input proof (MAX_EUINT64_PER_INPUT_PROOF), and refuses a larger batch with InvalidArgumentError before encrypting. Split bigger batches yourself.
// Before (1.x, deprecated in 1.6)
const access = useGetClaimableAmount({ address, encryptor });

// After (2.0): same mutation, the useAccess* name
const access = useAccessClaimableAmount({ address, encryptor });
access.mutate({ vestingId });
const create = useCreateManager();
const { data } = create;

// Receipt-derived fields type as X | undefined, because the hook accepts
// waitForReceipt: false. Narrow before reading them.
if (data && !data.pending) {
  console.log(data.manager);
}

/fhe-disperse#

  • useGetEncryptedFeeReserve is removed. Use useAccessEncryptedFeeReserve, the same function.
  • PreflightReport.blockers is removed. Use blockerErrors.
  • preflightDisperse checks the singleton approval in every mode and reports a missing one as OperatorNotApprovedError (TOKENOPS_OPERATOR_NOT_APPROVED). SingletonNotApprovedError is deprecated and never raised, and PreflightReport.hasApprovedSingleton narrows from boolean | null to boolean, so an === null check is dead code.
  • preflightDisperse reports a sender whose ETH balance is below the disperse fee as InsufficientBalanceError (TOKENOPS_INSUFFICIENT_BALANCE), not InsufficientFeeError. Typecheck does not catch this; update any branch on TOKENOPS_INSUFFICIENT_FEE.
  • disperse takes at most 32 recipients in "direct" mode and 30 in "wallet" / "wallet-token-fee", one input proof (MAX_EUINT64_PER_INPUT_PROOF). A larger batch is refused with InvalidArgumentError before encrypting, and preflightDisperse reports it as a blocker on recipients with batchOk: false.
  • WithdrawTokenFeeResult.transferredHandle is always set; disperse() and the disclosure methods throw ReceiptEventNotFoundError for a reverted transaction instead of resolving empty.
  • Role hooks take holder; useRegister, useDisperse and useWithdrawTokenFee resolve Mined | Pending.
// Before (1.x)
report.blockers.forEach((msg) => show(msg));

// After (2.0)
report.blockerErrors.forEach((err) => show(err.message)); // or branch on err.code

/fhe and the Zama peers#

The optional @zama-fhe/sdk and @zama-fhe/react-sdk peers move from ^3.0.0 to ~3.6.0. Upstream 3.6 removes RelayerNode, RelayerWeb and the SepoliaConfig / MainnetConfig presets in favour of createConfig plus ZamaSDK, and the SDK's Encryptor and UserDecryptor contracts follow. Product client signatures and the encryptUint64 outputs are unchanged.

  • createSepoliaEncryptorWeb moved to @tokenops/sdk/fhe/web; createSepoliaEncryptor and createMockEncryptor stay in /fhe.
  • createSepoliaEncryptor drops poolSize, fheArtifactStorage and fheArtifactCacheTTL, gains rpcUrl and auth; SepoliaEncryptorStorage is removed.
  • useDecryptedHandle takes userDecryptor: () => sdk.decryption and account, and drops relayerParams.
  • EncryptedInput64, EncryptedHandle and ExternalInputProof are removed from /fhe. Use EncryptedInput and Hex from viem.
  • A mined-but-reverted transaction is no longer a success. setOperator (and so revokeOperator, ensureOperator and useEnsureOperator) and mintMockERC7984 throw TokenOpsContractError on a reverted receipt instead of resolving. An out-of-range deadline / amount is now InvalidArgumentError, a missing account MissingAccountError and a rejected prompt WalletRejectedError, where all three used to be TokenOpsContractError (TOKENOPS_CONTRACT_REVERT).

Every before/after is in Upgrading the Zama peers to 3.6; the 2.0 encryptors are documented in Encryptors and Decryption.

Root and cross-cutting#

  • DisperseId and asDisperseId are removed; the disperse singleton emits no bytes32 id. Use Hex from viem for an identifier of your own.
  • ClientPair and ContractRef are removed; nothing in the SDK took or returned them. Declare the shape locally.
  • Gas headroom. Every write sends its gas estimate padded by DEFAULT_GAS_HEADROOM_PERCENT (25%), adjustable per client or hook with gasHeadroomPercent and per call with gas. See Gas headroom.
  • Wallet on the wrong chain.Every write refuses a wallet whose chain is not the public client's, with WalletChainMismatchError, before estimating.
  • Error codes. TokenOpsSdkErrorCode gains codes, including TOKENOPS_MISSING_PEER_DEPENDENCY and the airdrop codes. An exhaustive switch (err.code) with a never check needs the new cases.
  • No render-time throws.The vesting, disperse and faucet hooks return a client constructor's error (a malformed address, a bad gasHeadroomPercent) as resolutionError instead of throwing during render: queries stay disabled and mutations reject with it. An error boundary that caught the throw no longer sees it.
  • Faucet chain error. On an unsupported chain, TestnetFaucetClient and the faucet hooks throw UnsupportedChainError with a message that the faucet runs on Sepolia only, plus context.method and context.hint.
  • Immutable reads cache forever.The vesting manager's immutable reads and the disperse useWalletImplementation / useDeploymentBlockNumber default to staleTime: Infinity. Pass query: { staleTime: 0 } for the old behaviour.
  • Read hooks take query on every product. See Read-hook query options.

Upgrade checklist#

  1. Decide what stays on 1.x: anything that operates a v1 airdrop campaign.
  2. Install @tokenops/sdk@next and move the Zama peers to ~3.6.0 together.
  3. Run typecheck. Every removal and rename above fails to compile, so the error list is your work list. The error-code, batch-size and render-time changes do not fail to compile; check those by hand.
  4. Re-wire the encryptor and decryptor to the 3.6 shape and move any createSepoliaEncryptorWeb import to /fhe/web.
  5. Port /fhe-airdrop code with the airdrop guide, against the canonical v2 factory.
  6. Re-check any err.code switch (TOKENOPS_INSUFFICIENT_FEE, TOKENOPS_CONTRACT_REVERT), any UI that read blockers, and any error boundary around a vesting, disperse or faucet hook.

Untyped JavaScript#

Removed fields read as undefined
Without TypeScript, nothing flags a removed field: report.blockers is simply undefined. Search for the removed names listed on this page before shipping.

Related guides