Confidential Vesting
@tokenops/sdk/fhe-vesting is a typed wrapper for the ConfidentialVesting contract system. The factory deploys per-user LibClone manager instances; each manager holds all vesting schedules for one token. Amounts are kept confidential on-chain as euint64 ciphertexts — the SDK encrypts plaintext bigint inputs before submitting transactions.
Sepolia factory: 0xA87701CE9A52D43681600583a99c85b50DbE3150, resolved automatically when publicClient.chain.id === 11155111. Mainnet is not yet deployed; pass address explicitly on mainnet until the factory ships.
30-second example
import { createPublicClient, createWalletClient, http, parseEventLogs } from "viem";
import { sepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { RelayerNode, SepoliaConfig } from "@zama-fhe/sdk/node";
import {
createConfidentialVestingFactoryClient,
createConfidentialVestingManagerClient,
confidentialVestingManagerAbi,
} from "@tokenops/sdk/fhe-vesting";
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const rpcUrl = process.env.RPC_URL!;
const publicClient = createPublicClient({ chain: sepolia, transport: http(rpcUrl) });
const walletClient = createWalletClient({ account, chain: sepolia, transport: http(rpcUrl) });
const encryptor = new RelayerNode({
transports: { [SepoliaConfig.chainId]: { ...SepoliaConfig, network: rpcUrl } },
getChainId: async () => sepolia.id,
});
const factory = createConfidentialVestingFactoryClient({ publicClient, walletClient });
const { manager: managerAddress } = await factory.createManager({
token: process.env.TOKEN as `0x${string}`,
userSalt: "0x0000000000000000000000000000000000000000000000000000000000000001",
});
const manager = createConfidentialVestingManagerClient({
publicClient,
walletClient,
address: managerAddress,
encryptor,
});
// amount is raw token units: 1_000_000n = 1 USDC at 6 decimals
const vestingHash = await manager.createVesting({
params: {
recipient: "0xRecipientAddress" as `0x${string}`,
startTimestamp: Math.floor(Date.now() / 1000),
endTimestamp: Math.floor(Date.now() / 1000) + 365 * 86400,
cliffSeconds: 90 * 86400,
releaseIntervalSecs: 86400,
timelockSeconds: 0,
initialUnlockBps: 0,
cliffAmountBps: 0,
isRevocable: true,
},
amount: 1_000_000n,
});
// Extract vestingId from the VestingCreated event
const receipt = await publicClient.waitForTransactionReceipt({ hash: vestingHash });
const events = parseEventLogs({
abi: confidentialVestingManagerAbi,
eventName: "VestingCreated",
logs: receipt.logs,
});
const vestingId = events[0]!.args.vestingId;
// Claim vested tokens
await manager.claim({ vestingId });
Frontend usage
A typical React component using wagmi — the factory address resolves automatically on Sepolia from DEPLOYED_ADDRESSES.
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
import { useWalletClient } from "wagmi";
import { RelayerWeb, SepoliaConfig } from "@zama-fhe/sdk";
import {
createConfidentialVestingFactoryClient,
createConfidentialVestingManagerClient,
} from "@tokenops/sdk/fhe-vesting";
const publicClient = createPublicClient({ chain: sepolia, transport: http() });
const encryptor = new RelayerWeb({
transports: { [SepoliaConfig.chainId]: { ...SepoliaConfig, network: "https://rpc.sepolia.org" } },
getChainId: async () => sepolia.id,
});
export function CreateVestingButton({
token,
userSalt,
recipient,
}: {
token: `0x${string}`;
userSalt: `0x${string}`;
recipient: `0x${string}`;
}) {
const { data: walletClient } = useWalletClient();
async function handleCreate() {
if (!walletClient) return;
const factory = createConfidentialVestingFactoryClient({ publicClient, walletClient });
const { manager: managerAddress } = await factory.createManager({ token, userSalt });
const manager = createConfidentialVestingManagerClient({
publicClient,
walletClient,
address: managerAddress,
encryptor,
});
await manager.createVesting({
params: {
recipient,
startTimestamp: Math.floor(Date.now() / 1000),
endTimestamp: Math.floor(Date.now() / 1000) + 365 * 86400,
cliffSeconds: 0,
releaseIntervalSecs: 86400,
timelockSeconds: 0,
initialUnlockBps: 0,
cliffAmountBps: 0,
isRevocable: false,
},
amount: 500_000n,
});
}
return <button onClick={handleCreate}>Create vesting</button>;
}
Backend usage
Server-side operations using RelayerNode:
import { createPublicClient, createWalletClient, http } from "viem";
import { sepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { RelayerNode, SepoliaConfig } from "@zama-fhe/sdk/node";
import {
createConfidentialVestingFactoryClient,
createConfidentialVestingManagerClient,
FeeType,
DisclosureType,
} from "@tokenops/sdk/fhe-vesting";
const account = privateKeyToAccount(process.env.OPERATOR_PRIVATE_KEY as `0x${string}`);
const rpcUrl = process.env.RPC_URL!;
const publicClient = createPublicClient({ chain: sepolia, transport: http(rpcUrl) });
const walletClient = createWalletClient({ account, chain: sepolia, transport: http(rpcUrl) });
const encryptor = new RelayerNode({
transports: { [SepoliaConfig.chainId]: { ...SepoliaConfig, network: rpcUrl } },
getChainId: async () => sepolia.id,
});
const manager = createConfidentialVestingManagerClient({
publicClient,
walletClient,
address: "0xYourManagerAddress",
encryptor,
});
// Batch-create multiple schedules in one tx
await manager.batchCreateVesting({
items: [
{
params: {
recipient: "0xAlice",
startTimestamp: 1700000000,
endTimestamp: 1731536000,
cliffSeconds: 0,
releaseIntervalSecs: 86400,
timelockSeconds: 0,
initialUnlockBps: 0,
cliffAmountBps: 0,
isRevocable: false,
},
amount: 100_000n,
},
{
params: {
recipient: "0xBob",
startTimestamp: 1700000000,
endTimestamp: 1731536000,
cliffSeconds: 0,
releaseIntervalSecs: 86400,
timelockSeconds: 0,
initialUnlockBps: 0,
cliffAmountBps: 0,
isRevocable: false,
},
amount: 200_000n,
},
],
});
// Get an encrypted handle for a vesting balance (requires tx)
const vestingId = "0xabc..." as `0x${string}`;
const { handle } = await manager.getClaimableAmount({ vestingId });
// Pass handle to the Zama relayer's userDecrypt to obtain the plaintext
// Disclose a vesting amount to a third party
await manager.discloseToParty({
vestingId,
party: "0xAuditorAddress",
disclosureType: DisclosureType.TotalAllocation,
});
// Split a vesting (50% stays, 50% goes to a new recipient)
await manager.splitVesting({
vestingId,
numerator: 1n,
denominator: 2n,
newRecipient: "0xNewRecipient",
});
// Revoke a vesting
await manager.revokeVesting(vestingId);
Fee model
Each manager has an immutable feeType and fee baked into its clone bytecode. ClaimArgs is a discriminated union on feeType:
const { feeType, fee } = await manager.getFeeInfo();
await manager.claim(
feeType === FeeType.Gas
? { vestingId, feeType, value: fee }
: { vestingId, feeType },
);
Encryptor setup
See Getting Started for RelayerNode, RelayerWeb, mock encryptor, and lazy vs eager injection patterns.
For the full FHE ACL specification and userDecrypt flow, see docs.zama.ai/fhevm.