A Next.js app for browsing the Zama Confidential Token Wrappers Registry and interacting with ERC-20 to ERC-7984 wrapper pairs: wrap, unwrap, and decrypt confidential balances.
Built for the Zama Developer Program Season 3 Bounty Track.
- Lists every registered ERC-20 to ERC-7984 wrapper pair on Sepolia and Ethereum mainnet, read directly from the on-chain registry contract, including revoked pairs.
- Wraps and unwraps any listed pair on Sepolia (mainnet is read-only), with per-transaction status steps and explorer links.
- Decrypts your confidential ERC-7984 balance client side using an EIP-712 user-decryption permit. Only your wallet can read your balance.
- Provides a Sepolia faucet that mints the official Zama mock underlying tokens (USDCMock, USDTMock, WETHMock, and others) so the wrap flow can be tried without holding any tokens.
- Sends ERC-7984 wrapper balance to another address confidentially, keeping the amount and balances encrypted on-chain, using the Zama SDK client-side.
Registry on Sepolia:
Registry on mainnet (read-only, write actions disabled):
flowchart LR
User([User wallet]) -->|connect, sign, send tx| Wagmi[wagmi + viem]
Wagmi -->|read pairs via multicall| Registry[ConfidentialTokenWrappersRegistry]
Wagmi -->|approve, wrap, unwrap, mint| Tokens[ERC-20 and ERC-7984 contracts]
User -->|EIP-712 permit signature| SDK[Zama react-sdk]
SDK -->|userDecrypt request| Relayer[Zama relayer]
Relayer -->|decrypted balance| SDK
SDK -->|plaintext balance, client only| UI[Next.js UI]
Wagmi --> UI
The app has no backend. All reads go through public RPC endpoints, all writes go through the connected wallet, and decryption goes through the Zama relayer configured per chain in app/providers.tsx.
app/
layout.tsx Root layout, restores wagmi state from cookies for SSR
page.tsx Home page composing ConnectWallet and RegistryExplorer
providers.tsx WagmiProvider, QueryClientProvider, ZamaProvider with per-chain relayers
config.ts wagmi config: Sepolia + mainnet, injected connector, cookie storage
connect-wallet.tsx Connect and disconnect button with the injected connector
components/
RegistryExplorer.tsx Network tabs (Sepolia, Mainnet), mounts Faucet and RegistryTable
RegistryTable.tsx Pair table with revoked filter, skeletons, and expandable rows
WrapPanel.tsx Wrap and unwrap form with approval detection and tx step list
DecryptBalance.tsx Permit grant plus on-demand confidential balance decryption
TransferPanel.tsx Confidential transfer form for sending ERC-7984 tokens
Faucet.tsx Mint buttons for the official Sepolia mock underlying tokens
lib/
registry.ts Registry ABI, chain config, slice pagination, metadata multicall
format.ts Address shortening helper
The wrap form checks the current ERC-20 allowance toward the wrapper with useUnderlyingAllowance. When the entered amount exceeds the allowance, useShield first submits an approve transaction, then the wrap transaction, and reports both hashes through callbacks that the UI renders as a step list. Unwrap uses useUnshield, which submits the unwrap, waits for the decryption proof, and then submits the finalize transaction. Before unwrapping, the panel verifies the user has a decryption permit, since the SDK reads the confidential balance to validate the amount.
On mainnet the same panels render but stay disabled, so pairs can be inspected without any risk of sending a transaction:
ERC-7984 balances are stored as ciphertext handles, so a plain balanceOf returns nothing readable. The user first signs a gas-free EIP-712 permit with useGrantPermit; nothing is sent on chain. useHasPermit gates the UI so the permit is only requested once per wrapper contract. After that, clicking Decrypt runs useConfidentialBalance, which sends a userDecrypt request to the Zama relayer and returns the plaintext amount to the browser only. The decrypted value never leaves the client.
The useConfidentialTransfer hook encrypts the amount on the client side. The transfer requires a decryption permit, reusing the existing permit logic. Confidential transfer is only active on Sepolia.
lib/registry.ts reads getTokenConfidentialTokenPairsLength and then pages through getTokenConfidentialTokenPairsSlice in batches of 50, so the app keeps working as the registry grows. Token name, symbol, and decimals for both sides of every pair are fetched in a single viem multicall with allowFailure: true, so one broken token contract cannot break the whole table. Results are cached for 60 seconds with TanStack Query.
| Hook | Purpose |
|---|---|
useShield |
Wraps ERC-20 into ERC-7984, submitting the approve transaction first when the allowance is insufficient |
useUnshield |
Unwraps ERC-7984 back to ERC-20, including the decryption proof wait and the finalize transaction |
useGrantPermit |
Signs the gas-free EIP-712 user-decryption permit for a wrapper contract |
useHasPermit |
Checks whether a valid permit already exists, used to gate decrypt and unwrap UI |
useConfidentialBalance |
Decrypts the caller's own ERC-7984 balance through the relayer |
useUnderlyingAllowance |
Reads the ERC-20 allowance granted to the wrapper, used for approval detection |
useConfidentialTransfer |
Transfers the sender's ERC-7984 balance to another address confidentially |
| Network | Address |
|---|---|
| Sepolia | 0x2f0750Bbb0A246059d80e94c454586a7F27a128e |
| Ethereum mainnet | 0xeb5015fF021DB115aCe010f23F55C2591059bBA0 |
Source: Zama protocol addresses. They match the registryAddress constants shipped in @zama-fhe/sdk/chains.
- Next.js 15 (App Router) with TypeScript
- wagmi v3 and viem for wallet connection and contract reads and writes
- @zama-fhe/sdk and @zama-fhe/react-sdk for wrapping, permits, and user decryption
- TanStack Query for caching and request state
- Tailwind CSS 4 for styling
MIT







