2.0 RC docsView 1.x docs
Upgrading · 2.0 alpha to release candidate

Upgrading from a 2.0 alpha

The release candidate freezes the 2.x API. Most of the work is removals, one moved import and new airdrop addresses.

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.

v2.0.0-rc.1 is the release candidate for 2.0.0. Coming from any 2.0.0-alpha.x build, it removes every deprecated symbol, moves createSepoliaEncryptorWeb to @tokenops/sdk/fhe/web, moves /fhe-airdrop to the canonical deployment, moves the Zama peers to 3.6, and renames a set of /fhe-airdrop/react types. 2.0.0-beta.1 was prepared but never published, so everything it would have carried reaches you here. Coming from 1.x instead? Start with Upgrading from 1.x.

Install from the next dist-tag#

The alpha builds were published on the alpha dist-tag. The release candidates are on next, and latest stays on 1.x until 2.0.0. Replace any @tokenops/sdk@alpha or exact 2.0.0-alpha.N pin:

pnpm add @tokenops/sdk@next
# The optional Zama peers move with it:
pnpm add @zama-fhe/sdk@~3.6.0
pnpm add @zama-fhe/react-sdk@~3.6.0 # React hooks only

Addresses#

DEPLOYED_ADDRESSES.fheAirdrop now holds the canonical deployment of contracts commit 6b0edcc (AIRDROP_CONTRACTS_COMMIT), which carries the external-audit fixes. One CREATE3 deployer put it at the same addresses on mainnet and Sepolia, so chainsWithDeployment("fheAirdrop") is now [1, 11155111]. Every alpha address moved:

Keyalpha.4 (Sepolia only)Release candidate (mainnet + Sepolia)
airdropFactory0xD590…046a0x3c6a…470C
ecdsaImplementation0xE27A…43bA0xEed7…B823
merkleImplementation0xccea…505E0xBB4e…a501
complianceManagerImplementation0x0767…FADf0x3FC9…1c66
Alpha campaigns are not in the canonical registry
An instance created through an alpha factory is not in the new factory's registry: isAirdrop answers false for it, and a Merkle campaign planned against the old factory fails with PredictionDriftError. Create new campaigns through the canonical factory. The /fhe-vesting, /fhe-disperse and /testnet-faucet addresses are unchanged; vesting is still Sepolia only.

Removed#

Each removal fails at typecheck. Untyped JavaScript that still reads PreflightReport.blockers or CreateVestingPreflightReport.blockers gets undefined.

SubpathRemovedUse instead
/fhe-airdrop(/react)dedupMode on EcdsaAirdropClientConfig, the EcdsaAirdropClient.dedupMode field, EcdsaAirdropInstanceHookOptions, EcdsaAirdropInstanceClientOptions, UsePreflightClaimArgs and UseAirdropRoleConstantsArgsDrop the argument; no call branched on it. Read the policy with readDedupMode() or useDedupMode. Create-time params.dedupMode is unchanged.
/fhe-dispersePreflightReport.blockers: string[]blockerErrors: TokenOpsSdkError[]; branch on error.code or render error.message.
/fhe-vesting(/react)CreateVestingPreflightReport.blockers: string[]blockerErrors, as in /fhe-disperse.
/fhe-vesting/reactuseGetVestedAmount, useGetClaimableAmount, useGetTotalAllocation, useGetSettledAmount and their UseGet*ArgsuseAccessVestedAmount, useAccessClaimableAmount, useAccessTotalAllocation, useAccessSettledAmount. Same functions.
/fhe-disperse/reactuseGetEncryptedFeeReserveuseAccessEncryptedFeeReserve. Same function.
/fhe-vesting/reactPredictManagerArgsImport it from /fhe-vesting/advanced or /fhe-vesting/advanced/react, next to usePredictManagerAddress.
/fheEncryptedInput64, EncryptedHandle, ExternalInputProofEncryptedInput; Hex from viem (the root @tokenops/sdk EncryptedHandle and ExternalInputProof are unchanged).
@tokenops/sdkDisperseId, asDisperseIdHex from viem; the disperse singleton emits no bytes32 disperse id.
@tokenops/sdkClientPair, ContractRefNo replacement: no SDK API took or returned them. Declare the shape locally.
telemetryThe fhe-vesting.react.hook.fired eventDrop any dashboard keyed on it. The *.client.init events and the write spans are unchanged.

Moved#

createSepoliaEncryptorWeb and the types only it uses (CreateSepoliaEncryptorWebOpts, SepoliaEncryptorWeb, EncryptWorkerLike, EncryptWorkerMessageEvent, EncryptWorkerErrorEvent) moved to the new @tokenops/sdk/fhe/web. Options and behaviour are unchanged; createSepoliaEncryptor and createMockEncryptor stay in /fhe.

// Before: imported from the /fhe subpath. After:
import { createSepoliaEncryptorWeb, MAINNET_CHAIN_ID } from "@tokenops/sdk/fhe/web";

The move is what lets a browser app bundle @tokenops/sdk/fhe without the optional @zama-fhe/sdk peer. See Browser encryptor.

Zama 3.6#

The @zama-fhe/sdk and @zama-fhe/react-sdk peer ranges move to ~3.6.0. The changes most callers hit: pass the ZamaSDK itself as the encryptor (() => zamaSDK, not () => zamaSDK.relayer); pass userDecryptor: () => sdk.decryption and account to useDecryptedHandle and drop relayerParams; drop poolSize, fheArtifactStorage and fheArtifactCacheTTL from createSepoliaEncryptor; drop the /v2 suffix from an explicit Sepolia relayer URL; install @zama-fhe/relayer-sdk@0.4.1 explicitly if you use createMockEncryptor, since 3.6 no longer brings it in. The full list is in Upgrading the Zama peers to 3.6.

Changed in /fhe-airdrop#

  • extendClaimWindow refuses an end near the chain time. After the simulation passes, extendClaimWindow and useExtendClaimWindow read the latest block and throw InvalidArgumentError when newEndTime is less than 60 seconds after its timestamp. The contracts revert an end at or before the mining block with EndTimeInPast.
  • Extension errors name newEndTime. InvalidDuration and EndTimeInPast raised by extendClaimWindow report context.argument === "newEndTime", not "duration" / "endTime". Create errors are unchanged.
  • preflightCreate judges endTime against the chain clock (the latest block's timestamp plus 60 seconds) instead of the local clock. Both checks add one getBlock read, which a mocked PublicClient must answer.
  • Role-split steps name the grantee holder. RoleGrantStep.account and RoleGrantOutcome.account become holder (from planInstanceRoleSplit, grantInstanceRoles and useGrantInstanceRoles), and RoleGrantOutcome.role narrows to RoleName.
  • SignClaimAuthorizationArgs drops gas, which signing ignored.
  • preflightClaim reports a short wallet as InsufficientBalanceError (TOKENOPS_INSUFFICIENT_BALANCE, balanceKind: "eth") instead of InsufficientFeeError, and its blockers carry the entrypoint ("claim" or "claimAndUnwrap") as method.
  • Release tags. Everything is @public except the receipt-free and Safe create surface (the waitForReceipt: false create overloads, PendingCreateAirdropResult, encodeInstanceRoleSplit and the four useCreate* hooks with their variable types), which is @beta.

Renamed hook variable types#

/fhe-airdrop/react names a mutation's variables <Action>Args, or <Action>Variables where the headless subpath already exports <Action>Args:

BeforeAfter
UseDiscloseHandleToPartyArgsDiscloseHandleToPartyArgs
UseBatchDiscloseHandlesToPartyArgsBatchDiscloseHandlesToPartyArgs
UseAdminDiscloseBalanceToPartyArgsAdminDiscloseBalanceToPartyArgs
UseAdminBatchDiscloseBalanceToPartiesArgsAdminBatchDiscloseBalanceToPartiesArgs
UseCreateAndFundEcdsaAirdropArgsCreateAndFundEcdsaAirdropArgs
UseCreateAndFundMerkleAirdropArgsCreateAndFundMerkleAirdropArgs
UseBuildMerkleCampaignArgsBuildMerkleCampaignVariables
UseEncryptCampaignAmountsArgsEncryptCampaignAmountsVariables
UsePlanMerkleCampaignArgsPlanMerkleCampaignVariables
UseRotateMerkleRootArgsRotateMerkleRootVariables

Changed in /fhe-vesting#

PartialClaimArgs is discriminated by feeType, like ClaimArgs. A mismatched msg.value, which the contract already rejected, now fails to compile. partialClaim, adminPartialClaim, usePartialClaim and useAdminPartialClaim take the same shape:

import { FeeType } from "@tokenops/sdk/fhe-vesting";

const { feeType, fee } = await manager.getFeeInfo();

await manager.partialClaim(
  feeType === FeeType.Gas
    ? { vestingId, amount, feeType: FeeType.Gas, value: fee }
    : { vestingId, amount, feeType: FeeType.DistributionToken },
);

preflightClaim reports a short wallet as InsufficientBalanceError, as on /fhe-airdrop.

Changed in /fhe-disperse#

  • WithdrawTokenFeeResult.transferredHandle is always a Hex: withdrawTokenFee throws ReceiptEventNotFoundError when the receipt has no TokenFeeWithdrawn event. Drop any undefined check on a mined result.
  • preflightDisperse reports a missing singleton approval as OperatorNotApprovedError (TOKENOPS_OPERATOR_NOT_APPROVED) in every mode. SingletonNotApprovedError is deprecated and no longer raised, and PreflightReport.hasApprovedSingleton narrows to boolean.
  • disperse() and the disclosure methods throw ReceiptEventNotFoundError (or ReceiptEventAmbiguousError) on a receipt without their event, instead of resolving with distributions: [] or echoed inputs.
  • A sender whose ETH balance is below the disperse fee is reported as InsufficientBalanceError instead of InsufficientFeeError.

Changed in the React hooks#

  • Role hooks take holder on /fhe-vesting/react and /fhe-disperse/react. account and accountTarget still work, are deprecated, and cannot be combined with holder.
  • 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 value.
  • query on more read hooks. The /fhe-vesting, /fhe-disperse and /testnet-faucet read hooks take query (ReadHookQueryOptions), as the airdrop reads did. See Read-hook query options.
  • No render-time throws. The vesting, disperse and faucet hooks return a constructor error (a malformed address, a bad gasHeadroomPercent, an unsupported chain) as resolutionError: queries stay disabled and mutations reject with it.
// Vesting and disperse role hooks, as on /fhe-airdrop
const { data: isAdmin } = useHasRole({ address, role, holder });

const grant = useGrantRole({ address });
grant.mutate({ role, holder });

Behaviour changes#

  • Gas headroom on every write. Each write estimates its gas and sends it padded by DEFAULT_GAS_HEADROOM_PERCENT (25%) unless a client or hook sets gasHeadroomPercent; a per-call gas is sent as is. This fixes FHE writes that ran out of gas when an earlier transaction in the same block computed the same handles. See Gas headroom.
  • A wallet on the wrong chain is refused with WalletChainMismatchError before estimating, and a gas estimate that reverts throws the typed error instead of handing the call to the wallet (which could leave a nonce gap).
  • A mined-but-reverted transaction is no longer a success. setOperator (and so revokeOperator / ensureOperator) and mintMockERC7984 throw TokenOpsContractError on a reverted receipt; their input errors are now InvalidArgumentError / MissingAccountError, and a rejected prompt is WalletRejectedError.
  • Batches larger than one input proof are refused before encryption: MAX_EUINT64_PER_INPUT_PROOF (32) per proof, so batchCreateVesting takes at most 32 items and disperse 32 recipients in direct mode, 30 in the wallet modes.
  • New error codes. TokenOpsSdkErrorCode gains TOKENOPS_UNEXPECTED_CONTRACT_RESPONSE, TOKENOPS_MISSING_PEER_DEPENDENCY, TOKENOPS_ACL_NOT_PROPAGATED and more; an exhaustive switch (err.code) with a never check needs the new cases. A missing optional peer now throws MissingPeerDependencyError.
  • Faucet chain error. On mainnet, TestnetFaucetClient throws UnsupportedChainError with context.method and a context.hint that says the faucet runs on Sepolia only. See Testnet Faucet 2.0.

Also changed#

  • /fhe-vesting withdrawAdmin and withdrawTokenFee take the WithdrawAdminArgs / WithdrawTokenFeeArgs unions, so passing both amount and encryptedInput fails to compile.
  • batchRevokeVesting sorts the ids into the ascending order the contract requires and rejects duplicates with InvalidArgumentError.
  • planInstanceRoleSplit (and so grantInstanceRoles and encodeInstanceRoleSplit) refuses an assignment.admin equal to the caller with InvalidArgumentError. Omit admin to keep it.
  • The relayer's transient "ACL not propagated" failure is the retryable AclNotPropagatedError (TOKENOPS_ACL_NOT_PROPAGATED), not DecryptionFailedError. Retry on it instead of treating it as permanent.
  • /fhe-airdrop fundAirdrop, the createAndFund* creates, encryptCampaignAmounts and buildMerkleCampaign map Zama failures to RelayerUnreachableError / EncryptionFailedError instead of letting the raw @zama-fhe/sdk error escape.
  • A factory SaltAlreadyUsed revert is reported as SaltCollisionError, whose variant, mode, deployer and userSalt context fields are now optional in the type.
  • DeploymentAddressUnavailableError separates registry-not-deployed (a null entry) from registry-unknown-chain (no entry for the chain).

Related guides