Skip to main content

Usage

Encryptor setup

Pass a Zama RelayerWeb (browser) or RelayerNode (server) instance. Both satisfy the Encryptor interface structurally.

Browser:

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 client = createConfidentialDisperseClient({
publicClient,
walletClient,
address: singletonAddress,
encryptor: () => encryptorContext.current, // resolved per-call
});

For details on the Zama SDK v3 encryptor setup and userDecrypt, see docs.zama.ai/fhevm.

Frontend usage

A typical React flow: check readiness with preflight, then submit the disperse.

import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";
import { useWalletClient } from "wagmi";
import { RelayerWeb, SepoliaConfig } from "@zama-fhe/sdk";
import {
createConfidentialDisperseClient,
type PreflightReport,
} from "@tokenops/sdk/fhe-disperse";

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 DisperseButton({
singletonAddress,
token,
recipients,
amounts,
}: {
singletonAddress: `0x${string}`;
token: `0x${string}`;
recipients: `0x${string}`[];
amounts: bigint[];
}) {
const { data: walletClient } = useWalletClient();

async function handleDisperse() {
if (!walletClient?.account) return;

const client = createConfidentialDisperseClient({
publicClient,
walletClient,
address: singletonAddress,
encryptor,
});

const report: PreflightReport = await client.preflightDisperse({
user: walletClient.account.address,
token,
recipients,
amounts,
mode: "wallet",
});

if (!report.ready) {
throw new Error(
`Disperse not ready: ${report.blockerErrors.map((e) => e.message).join("; ")}`,
);
}

const { hash } = await client.disperse({
token,
mode: "wallet",
recipients,
amounts,
});
console.log("Disperse tx:", hash);
}

return <button onClick={handleDisperse}>Disperse tokens</button>;
}

Backend usage

Server-side (Express / Next.js server actions): operator runs the disperse on behalf of users, handles registration, manages admin tasks.

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 { createConfidentialDisperseClient } from "@tokenops/sdk/fhe-disperse";

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: () => Promise.resolve(sepolia.id),
});

// Build the client once at startup; reuse across requests.
const client = createConfidentialDisperseClient({
publicClient,
walletClient,
encryptor,
address: process.env.SINGLETON_ADDRESS as `0x${string}`,
});

const token = process.env.TOKEN as `0x${string}`;

// ── Operator: register if not already done ──
const isRegistered = await client.isRegistered(account.address);
if (!isRegistered) {
const { wallets } = await client.register({ token });
console.log("Registered wallets:", wallets);
}

// ── Operator: preflight + disperse ──
const recipients = process.env.RECIPIENTS!.split(",") as `0x${string}`[];
const amounts = recipients.map(() => 1_000_000n); // uniform distribution for this example

const report = await client.preflightDisperse({
user: account.address,
token,
recipients,
amounts,
mode: "wallet",
});
if (!report.ready) {
throw new Error(report.blockerErrors.map((e) => e.message).join("; "));
}

const { hash } = await client.disperse({ token, mode: "wallet", recipients, amounts });
console.log("Disperse tx:", hash);

// ── Fee collector: read the encrypted token fee reserve ──
// getEncryptedFeeReserve is a WRITE transaction — it grants you ACL on the handle.
const { handle } = await client.getEncryptedFeeReserve({ token });
console.log("Encrypted fee reserve handle:", handle);
// Pass handle to Zama relayer userDecrypt to read the plaintext total.

// ── Admin: pause the singleton ──
await client.pause();

// ── Admin: update fee config ──
await client.setFeeConfig({
gasFeeEnabled: true,
tokenFeeEnabled: false,
defaultGasFee: 1_000_000_000_000n, // 0.000001 ETH per recipient
defaultTokenFee: 0,
});