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 onlyAddresses#
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:
| Key | alpha.4 (Sepolia only) | Release candidate (mainnet + Sepolia) |
|---|---|---|
airdropFactory | 0xD590…046a | 0x3c6a…470C |
ecdsaImplementation | 0xE27A…43bA | 0xEed7…B823 |
merkleImplementation | 0xccea…505E | 0xBB4e…a501 |
complianceManagerImplementation | 0x0767…FADf | 0x3FC9…1c66 |
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.
| Subpath | Removed | Use instead |
|---|---|---|
/fhe-airdrop(/react) | dedupMode on EcdsaAirdropClientConfig, the EcdsaAirdropClient.dedupMode field, EcdsaAirdropInstanceHookOptions, EcdsaAirdropInstanceClientOptions, UsePreflightClaimArgs and UseAirdropRoleConstantsArgs | Drop the argument; no call branched on it. Read the policy with readDedupMode() or useDedupMode. Create-time params.dedupMode is unchanged. |
/fhe-disperse | PreflightReport.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/react | useGetVestedAmount, useGetClaimableAmount, useGetTotalAllocation, useGetSettledAmount and their UseGet*Args | useAccessVestedAmount, useAccessClaimableAmount, useAccessTotalAllocation, useAccessSettledAmount. Same functions. |
/fhe-disperse/react | useGetEncryptedFeeReserve | useAccessEncryptedFeeReserve. Same function. |
/fhe-vesting/react | PredictManagerArgs | Import it from /fhe-vesting/advanced or /fhe-vesting/advanced/react, next to usePredictManagerAddress. |
/fhe | EncryptedInput64, EncryptedHandle, ExternalInputProof | EncryptedInput; Hex from viem (the root @tokenops/sdk EncryptedHandle and ExternalInputProof are unchanged). |
@tokenops/sdk | DisperseId, asDisperseId | Hex from viem; the disperse singleton emits no bytes32 disperse id. |
@tokenops/sdk | ClientPair, ContractRef | No replacement: no SDK API took or returned them. Declare the shape locally. |
telemetry | The fhe-vesting.react.hook.fired event | Drop 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,
extendClaimWindowanduseExtendClaimWindowread the latest block and throwInvalidArgumentErrorwhennewEndTimeis less than 60 seconds after its timestamp. The contracts revert an end at or before the mining block withEndTimeInPast. - Extension errors name newEndTime.
InvalidDurationandEndTimeInPastraised byextendClaimWindowreportcontext.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
getBlockread, which a mockedPublicClientmust answer. - Role-split steps name the grantee holder.
RoleGrantStep.accountandRoleGrantOutcome.accountbecomeholder(fromplanInstanceRoleSplit,grantInstanceRolesanduseGrantInstanceRoles), andRoleGrantOutcome.rolenarrows toRoleName. - SignClaimAuthorizationArgs drops gas, which signing ignored.
- preflightClaim reports a short wallet as InsufficientBalanceError (
TOKENOPS_INSUFFICIENT_BALANCE,balanceKind: "eth") instead ofInsufficientFeeError, and its blockers carry the entrypoint ("claim"or"claimAndUnwrap") asmethod. - Release tags. Everything is
@publicexcept the receipt-free and Safe create surface (thewaitForReceipt: falsecreate overloads,PendingCreateAirdropResult,encodeInstanceRoleSplitand the fouruseCreate*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:
| Before | After |
|---|---|
UseDiscloseHandleToPartyArgs | DiscloseHandleToPartyArgs |
UseBatchDiscloseHandlesToPartyArgs | BatchDiscloseHandlesToPartyArgs |
UseAdminDiscloseBalanceToPartyArgs | AdminDiscloseBalanceToPartyArgs |
UseAdminBatchDiscloseBalanceToPartiesArgs | AdminBatchDiscloseBalanceToPartiesArgs |
UseCreateAndFundEcdsaAirdropArgs | CreateAndFundEcdsaAirdropArgs |
UseCreateAndFundMerkleAirdropArgs | CreateAndFundMerkleAirdropArgs |
UseBuildMerkleCampaignArgs | BuildMerkleCampaignVariables |
UseEncryptCampaignAmountsArgs | EncryptCampaignAmountsVariables |
UsePlanMerkleCampaignArgs | PlanMerkleCampaignVariables |
UseRotateMerkleRootArgs | RotateMerkleRootVariables |
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.transferredHandleis always aHex:withdrawTokenFeethrowsReceiptEventNotFoundErrorwhen the receipt has noTokenFeeWithdrawnevent. Drop anyundefinedcheck on a mined result.preflightDispersereports a missing singleton approval asOperatorNotApprovedError(TOKENOPS_OPERATOR_NOT_APPROVED) in every mode.SingletonNotApprovedErroris deprecated and no longer raised, andPreflightReport.hasApprovedSingletonnarrows toboolean.disperse()and the disclosure methods throwReceiptEventNotFoundError(orReceiptEventAmbiguousError) on a receipt without their event, instead of resolving withdistributions: []or echoed inputs.- A sender whose ETH balance is below the disperse fee is reported as
InsufficientBalanceErrorinstead ofInsufficientFeeError.
Changed in the React hooks#
- Role hooks take holder on
/fhe-vesting/reactand/fhe-disperse/react.accountandaccountTargetstill work, are deprecated, and cannot be combined withholder. - Immutable reads cache forever.The vesting manager's immutable reads and the disperse
useWalletImplementation/useDeploymentBlockNumberdefault tostaleTime: Infinity. Passquery: { staleTime: 0 }for the old value. - query on more read hooks. The
/fhe-vesting,/fhe-disperseand/testnet-faucetread hooks takequery(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 badgasHeadroomPercent, an unsupported chain) asresolutionError: 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 setsgasHeadroomPercent; a per-callgasis 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
WalletChainMismatchErrorbefore 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 sorevokeOperator/ensureOperator) andmintMockERC7984throwTokenOpsContractErroron a reverted receipt; their input errors are nowInvalidArgumentError/MissingAccountError, and a rejected prompt isWalletRejectedError. - Batches larger than one input proof are refused before encryption:
MAX_EUINT64_PER_INPUT_PROOF(32) per proof, sobatchCreateVestingtakes at most 32 items anddisperse32 recipients in direct mode, 30 in the wallet modes. - New error codes.
TokenOpsSdkErrorCodegainsTOKENOPS_UNEXPECTED_CONTRACT_RESPONSE,TOKENOPS_MISSING_PEER_DEPENDENCY,TOKENOPS_ACL_NOT_PROPAGATEDand more; an exhaustiveswitch (err.code)with anevercheck needs the new cases. A missing optional peer now throwsMissingPeerDependencyError. - Faucet chain error. On mainnet,
TestnetFaucetClientthrowsUnsupportedChainErrorwithcontext.methodand acontext.hintthat says the faucet runs on Sepolia only. See Testnet Faucet 2.0.
Also changed#
/fhe-vestingwithdrawAdminandwithdrawTokenFeetake theWithdrawAdminArgs/WithdrawTokenFeeArgsunions, so passing bothamountandencryptedInputfails to compile.batchRevokeVestingsorts the ids into the ascending order the contract requires and rejects duplicates withInvalidArgumentError.planInstanceRoleSplit(and sograntInstanceRolesandencodeInstanceRoleSplit) refuses anassignment.adminequal to the caller withInvalidArgumentError. Omitadminto keep it.- The relayer's transient "ACL not propagated" failure is the retryable
AclNotPropagatedError(TOKENOPS_ACL_NOT_PROPAGATED), notDecryptionFailedError. Retry on it instead of treating it as permanent. /fhe-airdropfundAirdrop, thecreateAndFund*creates,encryptCampaignAmountsandbuildMerkleCampaignmap Zama failures toRelayerUnreachableError/EncryptionFailedErrorinstead of letting the raw@zama-fhe/sdkerror escape.- A factory
SaltAlreadyUsedrevert is reported asSaltCollisionError, whosevariant,mode,deployeranduserSaltcontext fields are now optional in the type. DeploymentAddressUnavailableErrorseparatesregistry-not-deployed(anullentry) fromregistry-unknown-chain(no entry for the chain).