Recipe · Setup · 5 min

Your first encrypted tx

Three steps: get a wallet client, get an encryptor, open a vesting. Every value above is real Sepolia, copy, paste a real ERC-7984 token, run.

1. Viem clients#

Anything you build with viem already has these, bring your own. Otherwise: stand them up like this. Sepolia for free; mainnet only for /fhe-disperse today.

lib/viem.ts
ts
import { createPublicClient, createWalletClient, custom, http } from "viem";
import { sepolia } from "viem/chains";

export const publicClient = createPublicClient({
  chain: sepolia,
  transport: http(),
});

export const walletClient = createWalletClient({
  chain: sepolia,
  transport: custom(window.ethereum),
});

2. Encryptor#

The browser encryptor wraps Zama's RelayerWeb. The user signs an authorization once; subsequent encryptions reuse that signature for the relayer.

lib/encryptor.ts
ts
import { createSepoliaEncryptorWeb } from "@tokenops/sdk/fhe";
import { publicClient, walletClient } from "./viem";

// Browser-flavour encryptor wraps Zama RelayerWeb. Resolves the user's
// wallet to sign the per-encrypt authorization.
export const encryptor = await createSepoliaEncryptorWeb({
  publicClient,
  walletClient,
});

3. Deploy + vest#

Two SDK calls, one tx each. The amount is encrypted client-side; the contract stores a 32-byte handle and grants ACL to the operator + the recipient atomically.

components/firstVesting.ts
ts
import {
  createConfidentialVestingFactoryClient,
  createConfidentialVestingManagerClient,
  FeeType,
} from "@tokenops/sdk/fhe-vesting";

// Pre-deployed Sepolia factory, DEPLOYED_ADDRESSES resolves it by chainId.
const factory = createConfidentialVestingFactoryClient({
  publicClient,
  walletClient,
});

// 1. Deploy a manager clone (one tx).
const { hash: deployHash, manager: managerAddress } =
  await factory.createManagerAndGetAddress({
    token: "0xYOUR_ERC7984_TOKEN_ON_SEPOLIA",
    userSalt: "0x".padEnd(66, "0").slice(0, 65) + "a", // any unique 32-byte salt
  });

// 2. Mount a per-clone manager client.
const manager = createConfidentialVestingManagerClient({
  publicClient,
  walletClient,
  address: managerAddress,
  encryptor,
});

// 3. Create a vesting, the amount is encrypted client-side before submit.
const { hash: vestingHash, vestingId } = await manager.createVesting({
  params: {
    recipient: "0xRECIPIENT_ADDRESS",
    startTimestamp: Math.floor(Date.now() / 1000),
    endTimestamp: Math.floor(Date.now() / 1000) + 365 * 24 * 3600,
    cliffSeconds: 0,
    releaseIntervalSecs: 86_400,  // unlock per-day
    timelockSeconds: 0,
    initialUnlockBps: 0,
    cliffAmountBps: 0,
    isRevocable: false,
  },
  amount: 100_000n,                // bigint, encrypted to euint64 before submit
});

console.log({ deployHash, managerAddress, vestingHash, vestingId });

Run it live#

Connect your wallet (top-right pill) on Sepolia, edit the token field if you have your own ERC-7984 deployment, and click Run on Sepolia. The same Monaco editor + runner that powers the stories, the tx broadcasts for real.

Interactive · live Sepolia tx
Loading editor…
Edit any input above, then press to run.
Console0 lines
No output yet. Run the snippet to see logs stream in.

What you have now#

deployHash + vestingHash on Etherscan, a vestingId you can pass to useGetVestedAmount / useClaim, and a manager clone you can keep opening vestings inside without re-deploying. The recipient can now call useDecryptedHandle against the vesting's encrypted view handles to see their balance.

See also