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,
});