2.0 RC docsView 1.x docs
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.
Vesting 2.0 · Flows

The vesting flows.

Six paths cover a confidential vesting integration on the 2.0 line, each with the hooks that ship it.

  1. 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.

    1. factory.createManagerAndGetAddress({ token, userSalt }) - a LibClone deploy whose address is parsed from the ManagerCreated event. Pass splitEnabled / pausableEnabled to route through createManagerWithOptions
    2. 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
    3. In React the hook's data is CreateManagerResult | PendingCreateManagerResult: narrow with if (data.pending) return; before reading data.manager
    4. A missing or repeated ManagerCreated event throws ReceiptEventNotFoundError / ReceiptEventAmbiguousError
  2. 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.

    1. setOperator({ publicClient, walletClient, token, spender: manager }) from @tokenops/sdk/fhe-vesting, or useEnsureOperator in React (sends only when the grant is missing)
    2. Optional: manager.preflightCreateVesting({ params, creator }) / usePreflightCreateVesting returns { ready, blockerErrors, hasCreatorRole, isOperatorSet, token }
    3. manager.createVesting({ params, amount }) - or { params, encryptedInput } when you encrypted yourself; never both
    4. batchCreateVesting({ items }) takes at most MAX_EUINT64_PER_INPUT_PROOF (32) items, since every amount shares one input proof, and also respects maxBatchSize
    5. Read the vestingId from the VestingCreated event in the receipt
  3. 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.

    1. Read the fee config once: useManagerFeeInfo({ address }) - immutable, cached with staleTime Infinity
    2. Full claim: { vestingId, feeType: FeeType.Gas, value } or { vestingId, feeType: FeeType.DistributionToken }
    3. Partial claim takes the same feeType discriminant plus amount (or encryptedInput): { vestingId, feeType: FeeType.Gas, value, amount }
    4. Headless preflightClaim({ vestingId, caller }) reports a wallet that cannot cover the gas fee as InsufficientBalanceError (balanceKind: "eth")
  4. 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.

    1. useAccessVestedAmount / useAccessClaimableAmount / useAccessTotalAllocation / useAccessSettledAmount resolve { handle, hash }; each accepts a per-call gas
    2. Decrypt the handle with useDecryptedHandle (pass account so a wallet switch re-decrypts)
    3. useDiscloseToParty({ vestingId, party, disclosureType }) for one view; useBatchDiscloseToParty for several, with handles[i] matching vestingIds[i]
    4. Encrypted views and disclosures have no receipt-free option: run them from an account that can user-decrypt in-session
  5. 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.

    1. 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
    2. useInitiateVestingTransfer({ vestingId, newRecipient, transferDurationSeconds }) by the current recipient
    3. The new recipient calls useAcceptVestingTransfer before expiry; only the current recipient (the initiator) can useCancelVestingTransfer; the offered recipient lets it expire
    4. usePendingVestingTransfer reads the offer; useDirectVestingTransfer is the current recipient's one-step path (for contract recipients that cannot accept), with no acceptance window
  6. 6

    Revoke, pause and recover

    Revoke revocable schedules, pause claims on a pausable clone, and withdraw what the manager holds beyond its reserves.

    1. useBatchRevokeVesting({ vestingIds }) - pass ids in any order; the SDK sorts them ascending and rejects duplicates with InvalidArgumentError
    2. The recipient keeps what vested; the unvested remainder is released from reserves and withdrawn with useWithdrawAdmin (WITHDRAWER_ROLE)
    3. useWithdrawAdmin / useWithdrawTokenFee take amount or encryptedInput (exactly one), a lazy per-call encryptor, and gas
    4. 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