Developer Guide

Build on Zama confidential tokens.

CipherOps is a production reference implementation for Zama ERC-7984 workflows — from registry discovery through private reveal, unwrap, and TokenOps Disperse.

Zama RegistryERC-7984Private RevealTokenOps SDKSepolia Verified

1 — Architecture Overview

Full lifecycle demonstrated by CipherOps

1
Official Registry

Read live ERC-20 ↔ ERC-7984 pairs from Zama's on-chain registry contracts

2
ERC-20 Faucet

Mint Sepolia test tokens via ERC20.mint(address, uint256) on official mock tokens

3
Approve

ERC20.approve(wrapper, amount) — grant the wrapper spending rights

4
Wrap

wrapper.wrap(address, uint256) — seal ERC-20 into an FHE-encrypted ERC-7984 token

5
Private Reveal

confidentialBalanceOf → EIP-712 user-decrypt via Zama relayer → plaintext in browser only

6
Unwrap

wrapper.unwrap(…, encHandle, inputProof) + publicDecrypt → finalizeUnwrap — two-step Zama Gateway flow

7
TokenOps Disperse

register(token) + setOperator(singleton) + FHE-encrypt amounts → Disperse singleton on Sepolia + Mainnet

2 — Key SDKs & Packages

wagmi v2 + viem

wagmi@^2.15 / viem@^2.31

Contract reads, writes, wallet connection, multicall, chain config. All registry reads and lifecycle write calls use wagmi hooks.

@zama-fhe/relayer-sdk

^0.4.3

Original Zama FHE SDK used for Private Reveal and Unwrap flows. Provides userDecrypt / publicDecrypt / createFhevmInstance.

@zama-fhe/sdk

3.0.1

Newer Zama unified SDK required by @tokenops/sdk peer dep. Provides RelayerWeb (browser Web Worker), ViemSigner, and SepoliaConfig.

@tokenops/sdk

1.0.0

TokenOps typed viem-first SDK. Provides ConfidentialDisperseClient, useDisperse, useRegister, usePreflightDisperse, and all React hooks for fhe-disperse / fhe-airdrop / fhe-vesting.

RainbowKit v2

^2.2.5

Wallet connection UI (MetaMask, Coinbase Wallet, WalletConnect). Custom dark-themed ConnectButton.

Next.js 15

^15.3

App Router, server components, static prerendering. All write-flow components are client components; informational pages are static.

Full analysis in docs/TOKENOPS_CLIENT_ANALYSIS.md

3 — Implementation Notes

Read registry pairsts
// From src/hooks/useRegistryPairs.ts
const { data } = useReadContract({
  address: REGISTRY_ADDRESS,       // Zama on-chain registry
  abi: REGISTRY_ABI,
  functionName: "getTokenConfidentialTokenPairs",
  chainId: 11155111,               // Sepolia
});
// Returns { tokenAddress, confidentialTokenAddress, isValid }[]
Read encrypted balance handlets
// From src/components/registry/TokenActionPanel.tsx
const { data: handle } = useReadContract({
  address: wrapperAddress,         // ERC-7984 confidential token
  abi: WRAPPER_ABI,
  functionName: "confidentialBalanceOf",
  args: [userAddress],
});
// Returns bytes32 — FHE-encrypted handle, NOT plaintext
Decrypt balance (Private Reveal)ts
// From src/lib/fhevm/react/useFHEDecrypt.ts
const { decrypt, results } = useFHEDecrypt({
  instance,                        // FhevmInstance from useFhevm()
  ethersSigner,                    // ethers.JsonRpcSigner (from wagmi)
  fhevmDecryptionSignatureStorage, // in-memory storage
  chainId,
  requests: [{ handle, contractAddress }],
});
decrypt(); // prompts EIP-712 sign → Zama relayer → plaintext in results[handle]
TokenOps Disperse — register + sendts
// From src/components/operations/DisperseForm.tsx
// 1. One-time registration per token
useRegister().mutate({ token: erc7984Address });

// 2. Approve singleton as ERC-7984 operator
writeContract({ abi: erc7984OperatorAbi, functionName: "setOperator",
  args: [DISPERSE_SINGLETON, ERC7984_OPERATOR_MAX_DEADLINE] });

// 3. FHE-encrypt amounts and disperse
useDisperse({ encryptor: encryptorFactory }).mutate({
  token: erc7984Address,
  mode: "direct",
  recipients: ["0xAlice", "0xBob"],
  amounts: [parseUnits("10", 18), parseUnits("20", 18)],
});
// SDK encrypts each amount locally before broadcast
Unwrap two-step lifecyclets
// From src/hooks/useUnwrapAction.ts
// Step 1: encrypt amount + submit unwrap
const { handles, inputProof } = await instance
  .createEncryptedInput(wrapperAddress, userAddress).add64(amount).encrypt();
writeContract({ functionName: "unwrap",
  args: [from, to, handles[0], inputProof] });

// Step 2: Zama Gateway public decrypt
const { clearValues, decryptionProof } =
  await instance.publicDecrypt([unwrapRequestId]);

// Step 3: finalize — releases underlying ERC-20
writeContract({ functionName: "finalizeUnwrap",
  args: [unwrapRequestId, clearValues[unwrapRequestId], decryptionProof] });

4 — Routes

/

Product showcase — hero video, lifecycle strip, protocol coverage, feature menu

/registry

Live lifecycle app — registry explorer, Faucet/Approve/Wrap/Private Reveal/Unwrap

/operations

TokenOps Disperse — register, allow, CSV import, send FHE-encrypted payouts

/recipient

Recipient education — how to reveal an encrypted payout via Private Reveal

/verification

Proof center — verified tx receipts, lifecycle checklist, privacy guarantees

/developers

This page — architecture, packages, code snippets, route map, limitations

5 — Safety & Limitations

  • ·All write flows (Faucet, Wrap, Private Reveal, Unwrap, Disperse) are Sepolia-only in the current UI.
  • ·Ethereum Mainnet: registry read and pair discovery only. Write actions are gated by chainId === 11155111.
  • ·TokenOps Confidential Disperse is verified on Sepolia (2 confirmed transactions). Airdrop and Vesting SDKs are installed but not integrated yet.
  • ·Private Reveal and Unwrap require standard EOA wallets (MetaMask). Smart wallets may not support EIP-712 signing.
  • ·FHE encryption requires connectivity to Zama's public Sepolia relayer (relayer.testnet.zama.org/v2). Relayer downtime affects all FHE flows.
  • ·No fake balances, invented tx hashes, or mocked SDK calls. All verified transactions are real Sepolia on-chain events.

6 — External Resources

Explore the implementation

Every feature is live on Sepolia. All source is in this repository.