Usage
Encryptor setup
The SDK accepts Zama's RelayerWeb, RelayerNode, or MockFhevmInstance directly — they satisfy the Encryptor interface structurally. Pass one to the manager at construction time or per-call.
When building UI that needs an FHE input proof, use @zama-fhe/sdk/viem's ViemSigner — don't write a custom bridge. The /viem subpath of @zama-fhe/sdk ships a complete GenericSigner over viem clients; writing your own duplicates ~50 lines and introduces subtle divergence. See Zama SDK v3 docs for the full userDecrypt flow.
Browser/frontend setup using RelayerWeb (same config shape as RelayerNode):
import { RelayerWeb, SepoliaConfig } from "@zama-fhe/sdk";
import { sepolia } from "viem/chains";
const encryptor = new RelayerWeb({
transports: { [SepoliaConfig.chainId]: { ...SepoliaConfig, network: "https://rpc.sepolia.org" } },
getChainId: async () => sepolia.id,
});
Lazy encryptor (React context that may not be ready at construction):
const manager = createConfidentialVestingManagerClient({
publicClient,
walletClient,
address: managerAddress,
encryptor: () => encryptorContext.current, // called per-encryption
});
Fee model
Each manager has an immutable feeType and fee baked into its clone bytecode. ClaimArgs is a discriminated union on feeType — the Gas branch requires value; the DistributionToken branch forbids it. The compiler enforces what the JSDoc used to merely document. Read feeType once, branch in the consumer, and the variant you didn't pick is unreachable.
const { feeType, fee } = await manager.getFeeInfo();
await manager.claim(
feeType === FeeType.Gas
? { vestingId, feeType, value: fee } // required — attached as msg.value
: { vestingId, feeType }, // `value` is a compile error here
);
Two-call form if you prefer explicit branches:
if (feeType === FeeType.Gas) {
await manager.claim({ vestingId, feeType: FeeType.Gas, value: fee });
} else {
// FeeType.DistributionToken: contract deducts its cut from the encrypted transfer.
await manager.claim({ vestingId, feeType: FeeType.DistributionToken });
}
Frontend usage
A typical React page: predict the manager address, then call createVesting once the encryptor is ready.
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";
// Outside the component: a read-only client and encryptor are safe to share.
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 });
// Deploy and learn the clone address via the ManagerCreated event in the
// receipt. predictManagerAddress is block-dependent and not safe to use
// before the deploy tx mines — factory.createManager always returns the
// authoritative parsed-from-receipt shape.
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 (Express / Next.js server actions): use RelayerNode from @zama-fhe/sdk/node and build viem clients from a private key.
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) });
// SepoliaConfig provides all required contract addresses and the public relayer URL.
const encryptor = new RelayerNode({
transports: { [SepoliaConfig.chainId]: { ...SepoliaConfig, network: rpcUrl } },
getChainId: async () => sepolia.id,
});
// Build clients once at startup; reuse across requests.
const factory = createConfidentialVestingFactoryClient({ publicClient, walletClient });
// Manager address comes from your database or the ManagerCreated event.
const manager = createConfidentialVestingManagerClient({
publicClient,
walletClient,
address: "0xYourManagerAddress",
encryptor,
});
// --- Operator: 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,
},
],
});
// --- Operator: get an encrypted handle for a vesting balance (requires tx) ---
// The handle is granted ACL access for msg.sender. Pass it to the Zama
// relayer's userDecrypt to obtain the plaintext.
// See https://docs.zama.ai/fhevm for the userDecrypt flow.
const vestingId = "0xabc..."; // from the VestingCreated event
const { handle } = await manager.getClaimableAmount({ vestingId });
// const plaintext = await relayer.userDecrypt(handle, ...);
// --- Operator: disclose a vesting amount to a third party (e.g. auditor) ---
await manager.discloseToParty({
vestingId,
party: "0xAuditorAddress",
disclosureType: DisclosureType.TotalAllocation,
});
// --- Operator: split a vesting (50% stays, 50% goes to a new recipient) ---
await manager.splitVesting({
vestingId,
numerator: 1n,
denominator: 2n, // SDK auto-scales to the privacy-preserving split constant (90_090_000)
newRecipient: "0xNewRecipient",
});
// --- Operator: revoke a vesting ---
await manager.revokeVesting(vestingId);
// --- Admin: withdraw accumulated token fee ---
await manager.withdrawTokenFee({ to: account.address, amount: 5_000n });