The vesting flows.
Six paths cover a confidential vesting integration on the 2.0 line, each with the hooks that ship it.
- 1
Deploy a manager
The operator's wallet deploys a per-user manager clone through the Sepolia factory and keeps DEFAULT_ADMIN_ROLE on it. Every vesting the operator opens lives inside that clone.
- factory.createManagerAndGetAddress({ token, userSalt }) - a LibClone deploy whose address is parsed from the ManagerCreated event. Pass splitEnabled / pausableEnabled to route through createManagerWithOptions
- Safe or multisig signer: pass waitForReceipt: false. The call returns { hash, pending: true, manager: undefined }; read the executed transaction's ManagerCreated log for the address
- In React the hook's data is CreateManagerResult | PendingCreateManagerResult: narrow with if (data.pending) return; before reading data.manager
- A missing or repeated ManagerCreated event throws ReceiptEventNotFoundError / ReceiptEventAmbiguousError
Hooks - 2
Approve the manager and open a vesting
The manager clone pulls the amount from the operator's confidential balance, so it must be an ERC-7984 operator on the token. Then the SDK encrypts the plaintext amount and submits it with the plaintext schedule.
- setOperator({ publicClient, walletClient, token, spender: manager }) from @tokenops/sdk/fhe-vesting, or useEnsureOperator in React (sends only when the grant is missing)
- Optional: manager.preflightCreateVesting({ params, creator }) / usePreflightCreateVesting returns { ready, blockerErrors, hasCreatorRole, isOperatorSet, token }
- manager.createVesting({ params, amount }) - or { params, encryptedInput } when you encrypted yourself; never both
- batchCreateVesting({ items }) takes at most MAX_EUINT64_PER_INPUT_PROOF (32) items, since every amount shares one input proof, and also respects maxBatchSize
- Read the vestingId from the VestingCreated event in the receipt
- 3
Claim what is vested
The recipient pulls the vested portion, or a CLAIMER_ROLE holder does it for them. Full and partial claims are both discriminated by the manager's fee model.
- Read the fee config once: useManagerFeeInfo({ address }) - immutable, cached with staleTime Infinity
- Full claim: { vestingId, feeType: FeeType.Gas, value } or { vestingId, feeType: FeeType.DistributionToken }
- Partial claim takes the same feeType discriminant plus amount (or encryptedInput): { vestingId, feeType: FeeType.Gas, value, amount }
- Headless preflightClaim({ vestingId, caller }) reports a wallet that cannot cover the gas fee as InsufficientBalanceError (balanceKind: "eth")
- 4
Read and disclose encrypted amounts
Encrypted views submit a transaction that grants the caller ACL on a fresh handle, then the caller user-decrypts it. Disclosure grants a third party ACL on a handle without moving funds.
- useAccessVestedAmount / useAccessClaimableAmount / useAccessTotalAllocation / useAccessSettledAmount resolve { handle, hash }; each accepts a per-call gas
- Decrypt the handle with useDecryptedHandle (pass account so a wallet switch re-decrypts)
- useDiscloseToParty({ vestingId, party, disclosureType }) for one view; useBatchDiscloseToParty for several, with handles[i] matching vestingIds[i]
- Encrypted views and disclosures have no receipt-free option: run them from an account that can user-decrypt in-session
Hooks - 5
Split or transfer a vesting
Split one position into two with an encrypted share, or hand a position to a new recipient through a two-step offer.
- useSplitVesting({ vestingId, numerator, denominator, newRecipient }) - the SDK scales to FHE_SPLIT_DENOMINATOR and encrypts the numerator; for exact ratios pass share.fraction(m, n) or share.basisPoints(bps) from @tokenops/sdk/fhe-vesting with preScaled: true; waitForReceipt: false returns newVestingId: undefined
- useInitiateVestingTransfer({ vestingId, newRecipient, transferDurationSeconds }) by the current recipient
- The new recipient calls useAcceptVestingTransfer before expiry; only the current recipient (the initiator) can useCancelVestingTransfer; the offered recipient lets it expire
- usePendingVestingTransfer reads the offer; useDirectVestingTransfer is the current recipient's one-step path (for contract recipients that cannot accept), with no acceptance window
- 6
Revoke, pause and recover
Revoke revocable schedules, pause claims on a pausable clone, and withdraw what the manager holds beyond its reserves.
- useBatchRevokeVesting({ vestingIds }) - pass ids in any order; the SDK sorts them ascending and rejects duplicates with InvalidArgumentError
- The recipient keeps what vested; the unvested remainder is released from reserves and withdrawn with useWithdrawAdmin (WITHDRAWER_ROLE)
- useWithdrawAdmin / useWithdrawTokenFee take amount or encryptedInput (exactly one), a lazy per-call encryptor, and gas
- usePause / useUnpause (PAUSER_ROLE) block recipient claims, splits and initiate/accept/direct transfers; adminClaim (CLAIMER_ROLE), cancels, revokes, withdrawals and creates are not pause-gated