Skip to content

pramadanif/macetz

Repository files navigation

πŸ” Macetz β€” The Confidential Wrapper Registry

The canonical interface for the Zama Wrappers Registry.
Browse, wrap, unwrap, and decrypt ERC-7984 confidential tokens with zero friction.

Live Demo Network License Built with Zama TypeScript


Built for the Zama Developer Program β€” Season 3 Β· Wrappers Registry Bounty Track


πŸ“‹ Table of Contents


🎯 Overview

Today, many developers spin up their own ERC-20 testnet tokens and ERC-7984 wrappers instead of using the ones that already exist in the official Zama Wrappers Registry. This fragments the ecosystem β€” every team ships against a slightly different set of tokens, integrations don't compose, and users end up with a wallet full of look-alike confidential assets that don't actually interoperate.

Macetz solves this by turning the official onchain registry into a complete, production-ready dApp. Every canonical ERC-20 ↔ ERC-7984 pair is easy to find, wrap, unwrap, decrypt, and extend β€” making the official registry the path of least resistance.

What makes Macetz different

Feature Macetz Typical DIY dApp
Registry source βœ… Onchain canonical registry ❌ Hardcoded / local only
Dual-network support βœ… Sepolia + Ethereum Mainnet ❌ Sepolia-only
Pair extensibility βœ… Hybrid: onchain + local config ❌ Redeployment required
EIP-712 decrypt βœ… Any ERC-7984, not just registry pairs ❌ Registry tokens only
cTokenMock faucet βœ… All 7 official Sepolia mocks (public mint) ❌ Limited / none
Registry integrity checks βœ… Auto-flags anomalies per pair ❌ Blind render
Error handling βœ… User-friendly, context-aware messages ❌ Raw contract reverts
UX βœ… Premium, step-by-step flows ❌ Minimal

πŸš€ Live Deployment

Environment URL Network
Production https://macetz.vercel.app Sepolia + Mainnet
App (dApp) https://macetz.vercel.app/app Sepolia + Mainnet
Local dev npm run dev β†’ http://localhost:3000/app Sepolia (recommended for judges)

Note for Judges: Connect MetaMask to Sepolia for the full bounty flow (registry β†’ faucet β†’ shield β†’ decrypt β†’ unshield β†’ arbitrary ERC-7984 decrypt). Ethereum mainnet is supported for browse and relayer-dependent shield/decrypt with a real-funds confirmation gate. Confidential Distribute (TokenOps) is Sepolia-only β€” the official disperse singleton is deployed on testnet.

The app is fully self-contained β€” it can also be run locally in one command (npm run dev) or redeployed anywhere with the included vercel.json.


🎬 Product Demo (GIF Walkthrough)

The complete bounty flow, captured live on Sepolia. Every GIF below maps 1:1 to a hard requirement of the Wrappers Registry bounty.

1. Browse the Registry β€” every onchain pair, badged

Live read of getTokenConfidentialTokenPairs() on Sepolia: βœ“ Docs-verified badges, integrity checks, Dev-pair labels, and one-click network switch to Mainnet and back.

2. Faucet β€” all 7 official cTokenMocks in one click

Mint All mints every public cTokenMock (cUSDC, cUSDT, cWETH, cBRON, cZAMA, ctGBP, cXAUt mocks) straight from the official Sepolia contracts.

3. Shield β€” ERC-20 β†’ confidential ERC-7984

Auto allowance detection, then a single Approve & Wrap with the live step indicator (Approving β†’ Wrapping β†’ Confirmed).

4. Decrypt β€” one EIP-712 signature, your balance only

The encrypted euint64 balance becomes readable after a single EIP-712 signature. The signature can only ever decrypt the signer's own balance.

5. Unshield β€” the honest two-phase unwrap

unwrap() β†’ Zama relayer decrypts β†’ finalizeUnwrap(). The UI shows a real Pending Finalization state (~30–90s) instead of faking instant success.

6. Universal Decrypt β€” ANY ERC-7984, not just registry pairs

Paste an arbitrary wrapper address (here: our own cMTUSD, deployed via the dev-guide and not in the official registry) β†’ validated onchain via ERC-165 β†’ decrypted.

7. Add a Pair β€” extensibility without redeploying

The in-app Admin UI validates a new pair onchain (ERC-165 + decimals), previews it instantly, and emits a copy-paste custom-pairs.json snippet.

8. Confidential Distribution β€” payroll nobody can read

CSV of recipients in, one TokenOps disperse tx out. Recipients are visible onchain; amounts never are. Each recipient decrypts only their own slice.

πŸŽ₯ Full narrated demo video: [VIDEO_LINK β€” add before submitting]


🌐 Dual-Network Support

Macetz is the first Wrappers Registry interface to explicitly support both networks listed in Zama's official documentation.

Chain Registry Address Wrapper Pairs Browse Decrypt Wrap/Unwrap Faucet Distribute
Sepolia 0x2f0750Bb...128e 8 docs-listed (7 mocks + ctGBP) βœ… βœ… βœ… βœ… βœ… (TokenOps, docs-verified only)
Ethereum Mainnet 0xeb5015fF...bBA0 9 docs-listed βœ… ⚠️ relayer-dependent ⚠️ relayer-dependent (real-funds confirmation) ❌ ❌

Network switching

  • A network switch control in the sidebar shows the active network with an animated indicator and lets you switch chains with a single click β€” this triggers a real MetaMask chain-switch request, not a UI state toggle.
  • The Faucet nav item animates in/out (height + opacity, 0.2s) reactively based on the actual connected chainId β€” visible on Sepolia, removed from the DOM on Mainnet.
  • Switching networks automatically reloads the registry from the correct contract address.

Mainnet real-funds safety

Wrap and unwrap on Ethereum mainnet show an explicit confirmation dialog before any transaction: "You are about to shield/unshield [amount] [token] on Ethereum mainnet β€” this uses real funds and cannot be undone." The user must click "I understand, proceed" to continue. This is exactly the kind of production-conscious UX that "could a real user trust it today" implies.

Mainnet FHE caveat: Wrap/decrypt on mainnet depend on Zama's mainnet relayer being provisioned (NEXT_PUBLIC_MAINNET_RELAYER_URL). Registry browsing works fully; FHE operations may return 403 until the relayer is live.


πŸ” Registry Integrity Detection

Macetz automatically flags registry anomalies rather than blindly rendering every entry β€” a feature no known competitor has shipped as a live product feature.

What is checked (per pair, on every registry load)

Check Pass Flag
Wrapper decimals ≀ 6 (per Zama ERC-7984 spec) > 6 decimals
Underlying address Valid non-zero address Zero address
Duplicate detection Official + Mock split is expected Two+ non-Mock-distinguishable entries share a base symbol

The Official/Mock distinction matters

Some symbols legitimately have two entries: an official production wrapper (e.g., ctGBP) AND a separate Mock testnet-only wrapper (ctGBPMock). Macetz correctly identifies this as intentional design and does NOT flag it as a duplicate β€” only genuinely suspicious duplicates (same base symbol, no official/Mock relationship) are flagged.

Visibility in the UI

  • Each pair in the Registry Browser shows a βœ“ Verified or ⚠ Flagged integrity badge.
  • Registry pairs also show βœ“ Docs-verified (wrapper in Zama's official docs table) or a neutral Registry badge with a caution note when listed onchain but not in the docs allowlist.
  • Flagged pairs remain fully functional for Shield/Decrypt when isValid β€” not hidden, just clearly marked.
  • The Registry header shows a count of flagged pairs and a legend explaining the checks.

✨ Features

πŸ—‚οΈ Browse the Registry

  • Displays every official ERC-20 ↔ ERC-7984 wrapper pair from the onchain Zama Wrappers Registry
  • Shows token metadata: symbol, name, decimals, and both contract addresses with Etherscan links
  • Network-aware: auto-switches between Sepolia and Mainnet registry contracts based on wallet connection
  • Integrity badges: each pair shows βœ“ Verified or ⚠ Flagged based on automatic anomaly checks
  • Clearly labels mock (testnet-only) vs. production tokens
  • Merges local dev-only pairs with a "Dev Pair" badge β€” configExample entries are registry display only (not offered in Shield/Decrypt/Distribute until real contracts are deployed)

πŸ“– In-App Documentation

  • Sidebar Docs tab with table of contents, copyable code blocks, and the full dev-guide/ deploy walkthrough
  • Structured content in src/lib/docs-content.ts β€” edit docs without touching layout code

πŸ’Ό Confidential Distribution (TokenOps β€” Sepolia)

  • 4-step sender wizard: token β†’ recipients (CSV) β†’ preflight review β†’ disperse tx
  • Recipient view: detect pending allocations β†’ EIP-712 decrypt own payroll amount
  • Uses official TokenOps singleton 0x710dD9885Cc9986EfD234E7719483147a6d8DBb4 on Sepolia

πŸ”’ Wrap (ERC-20 β†’ ERC-7984)

  • Auto-detects current ERC-20 allowance before submitting
  • One-click "Approve & Wrap" or a clean two-step flow for transparency
  • Shows real-time step indicator: Approving β†’ Wrapping β†’ Confirmed
  • Mainnet safety: explicit real-funds confirmation dialog before any mainnet transaction
  • Displays updated ERC-7984 balance immediately after wrap

πŸ”“ Unwrap (ERC-7984 β†’ ERC-20)

  • Full two-step async unwrap: unwrap() β†’ relayer decryption β†’ finalizeUnwrap()
  • Shows a "Pending Finalization" state with timer and human-readable explanation
  • Tracks unwrap request IDs onchain for auditability
  • Mainnet safety: explicit real-funds confirmation dialog before any mainnet transaction

πŸ”‘ Decrypt Balances (Universal)

  • Decrypts any ERC-7984 balance via EIP-712 user-decryption
  • Works for any ERC-7984 contract address β€” not only registry pairs
  • Works on both networks: decrypt is read-only/EIP-712 and fully supported on Ethereum mainnet
  • Two modes:
    • Registry mode: Select from the official pair dropdown
    • Universal mode: Paste any address β€” validated via supportsInterface(0x4958f2a4) before attempting decrypt

🚰 Testnet Faucet

  • Claims official cTokenMock test tokens (up to 1,000 per mint call)
  • Covers all 7 public-mint Sepolia cTokenMocks listed in the Zama docs
  • One-click mint with real-time transaction status feedback
  • Mainnet-aware: Faucet nav item is removed from sidebar on Mainnet (animates out with height + opacity transition); navigating to faucet on Mainnet shows an informational banner instead

πŸ›οΈ Architecture

System Overview

graph TD
    A["User / Browser"] --> B["Next.js 15 App Router"]
    B --> C["Landing Page /"]
    B --> D["dApp /app"]

    D --> E["Web3Provider"]
    E --> F["wagmi v3"]
    E --> G["@zama-fhe/react-sdk"]

    D --> H["Registry Tab"]
    D --> I["Wrap/Unwrap Tab"]
    D --> J["Decrypt Tab"]
    D --> K["Faucet Tab\n(Sepolia only)"]
    D --> L2["Distribute Tab\n(TokenOps Sepolia)"]
    D --> L3["Docs Tab"]

    H --> L["useRegistryPairs hook"]
    L --> M["fetchRegistryPairs()"]
    M --> N["Onchain Registry Contract\n0x2f0750Bb...128e"]
    M --> O["config/custom-pairs.json\nLocal Dev Pairs"]

    I --> P["useShield hook\n@zama-fhe/react-sdk"]
    I --> Q["useUnshield hook\n@zama-fhe/react-sdk"]

    J --> R["useConfidentialBalance hook\n@zama-fhe/react-sdk"]

    K --> S["useFaucet hook\nmint ERC-20 directly"]

    G --> T["Relayer Proxy (default)\n{origin}/api/relayer/{chainId}"]
    T --> U["Zama Public Relayer\n(testnet or mainnet)"]
Loading

Directory Structure

macetz/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ app/
β”‚   β”‚   β”œβ”€β”€ page.tsx                      # Landing page
β”‚   β”‚   β”œβ”€β”€ app/page.tsx                  # dApp shell (Registry/Shield/Decrypt/Faucet/Distribute/Docs)
β”‚   β”‚   └── api/relayer/[...path]/route.ts # Relayer proxy (Sepolia + Mainnet)
β”‚   β”œβ”€β”€ components/app/
β”‚   β”‚   β”œβ”€β”€ RegistryBrowser.tsx           # Browse pairs + Add a Pair admin UI
β”‚   β”‚   β”œβ”€β”€ WrapUnwrapPanel.tsx           # Shield / Unshield
β”‚   β”‚   β”œβ”€β”€ DecryptPanel.tsx              # Registry + Any ERC-7984 decrypt
β”‚   β”‚   β”œβ”€β”€ FaucetPanel.tsx               # cTokenMock mints (Sepolia)
β”‚   β”‚   β”œβ”€β”€ DistributePanel.tsx           # TokenOps payroll wizard (Sepolia)
β”‚   β”‚   └── DocsPanel.tsx                 # In-app documentation
β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”œβ”€β”€ useRegistryPairs.ts           # Onchain + custom + preview merge
β”‚   β”‚   └── useFaucet.ts
β”‚   β”œβ”€β”€ lib/
β”‚   β”‚   β”œβ”€β”€ config.ts                     # KNOWN_MOCK_PAIRS, registry addresses
β”‚   β”‚   β”œβ”€β”€ registry.ts                   # Onchain fetch + custom-pairs loader
β”‚   β”‚   β”œβ”€β”€ pair-utils.ts                 # Operational-pair gate (Shield/Decrypt/Distribute)
β”‚   β”‚   β”œβ”€β”€ pair-validation.ts            # Add Pair on-chain validation
β”‚   β”‚   β”œβ”€β”€ preview-pairs.ts              # Browser localStorage previews
β”‚   β”‚   β”œβ”€β”€ disperse.ts                   # TokenOps campaign helpers
β”‚   β”‚   └── docs-content.ts               # In-app docs (structured data)
β”‚   └── providers/Web3Provider.tsx        # wagmi + Zama SDK (shared config)
β”œβ”€β”€ config/custom-pairs.json              # Chain-keyed dev pairs (`configExample` = display-only)
β”œβ”€β”€ dev-guide/                            # Hardhat deploy your own pair
β”œβ”€β”€ scripts/verify-distribute.ts         # Autonomous registry + Distribute smoke tests
└── vercel.json

Data Flow

sequenceDiagram
    participant U as User
    participant A as App (Next.js)
    participant R as Registry Contract
    participant C as custom-pairs.json
    participant Z as Zama Relayer
    participant BC as Blockchain (Sepolia)

    Note over A: App Initialization
    A->>R: getTokenConfidentialTokenPairs()
    R-->>A: [{tokenAddress, confidentialTokenAddress, isValid}]
    A->>C: Load config/custom-pairs.json
    C-->>A: [{erc20, erc7984, symbol, decimals}]
    A->>A: filter(isValid) + merge + deduplicate
    A-->>U: Display unified registry

    Note over U,BC: Wrap Flow
    U->>A: Select pair + enter amount
    A->>BC: ERC20.allowance(user, wrapper)
    BC-->>A: currentAllowance
    alt Insufficient Allowance
        A->>BC: ERC20.approve(wrapper, amount)
        BC-->>A: tx confirmed
    end
    A->>Z: shield(wrapper, amount) via Zama SDK
    Z->>BC: wrapper.wrap(encryptedAmount)
    BC-->>A: ERC-7984 minted event
    A-->>U: Confirmed

    Note over U,BC: Decrypt Flow
    U->>A: Select or paste ERC-7984 address
    A->>BC: supportsInterface(0x4958f2a4)
    BC-->>A: isERC7984 = true
    A->>Z: requestUserDecrypt(addr, EIP-712 sig)
    Z->>BC: FHE.decrypt(encryptedBalance)
    BC-->>Z: plaintext balance
    Z-->>A: plaintext + proof
    A-->>U: Display decrypted balance
Loading

πŸ”— How the Registry is Sourced

Macetz uses a hybrid registry model β€” the onchain registry is always the authoritative source, while a local config allows dev-only extensions without fragmenting the canonical set.

1. Primary: Onchain Wrappers Registry

// src/lib/registry.ts
export async function fetchRegistryPairs(
  client: PublicClient,
  chainId: number
): Promise<TokenPair[]> {
  const registryAddress = getRegistryAddress(chainId);
  const officialAddresses = getOfficialAddresses(chainId);

  const rawPairs = await client.readContract({
    address: registryAddress,
    abi: REGISTRY_ABI,
    functionName: "getTokenConfidentialTokenPairs",
  });

  // All valid onchain pairs β€” registry is source of truth
  const validPairs = rawPairs.filter((p) => p.isValid);

  const pairs = await Promise.all(
    validPairs.map(async (raw) => {
      const [erc20Meta, erc7984Meta] = await Promise.all([
        fetchTokenMetadata(client, raw.tokenAddress),
        fetchTokenMetadata(client, raw.confidentialTokenAddress),
      ]);
      const wrapperInDocs = officialAddresses.has(
        raw.confidentialTokenAddress.toLowerCase()
      );
      return {
        erc20Address: raw.tokenAddress,
        erc7984Address: raw.confidentialTokenAddress,
        // ...metadata fields...
        source: "registry" as const,
        isValid: true,
        docsVerified: wrapperInDocs, // badge only β€” does not filter pairs out
      };
    })
  );

  return runIntegrityChecks(pairs);
}

2. Secondary: Local Config

// src/lib/registry.ts β€” chain-keyed local config
export function loadCustomPairs(chainId: number): TokenPair[] {
  const config = customPairsJson as CustomPairsConfig;
  const entries = config[String(chainId)] ?? [];
  return entries.map((entry) => mapCustomEntryToPair(entry, "local-dev"));
}

Custom pairs are tagged source: "local-dev". Entries with "configExample": true (seeded cDEMO1 / cDEMO2) appear in the Registry tab only β€” they are excluded from Shield, Decrypt, and Distribute until replaced with deployed contract addresses.

Registry Merge Logic

flowchart LR
    A["Onchain Registry\ngetTokenConfidentialTokenPairs()"] --> C["filter(isValid === true)"]
    B["config/custom-pairs.json\nsource: local-dev"] --> D["Tag as Dev Pair"]
    C --> E["Merge + Deduplicate\n(onchain > custom > preview)"]
    D --> E
    E --> F["Render Unified Registry UI\n+ docsVerified badge"]
Loading

βž• How to Add a New ERC-20 ↔ ERC-7984 Pair

Macetz supports four paths for adding pairs:


Path A β€” Official Registration (Recommended for Production)

Submit the pair to the onchain Zama Wrappers Registry. Once registered and isValid == true, Macetz will automatically surface it on next load β€” zero code changes required.

ConfidentialTokenWrappersRegistry (Sepolia)
0x2f0750Bbb0A246059d80e94c454586a7F27a128e

Follow the Zama Wrappers Registry documentation for the registration process.


Path B β€” Local Config (For Dev / Prototyping)

Ideal for developers iterating on new ERC-7984 tokens before official registration.

Live reference: open the Registry tab on Sepolia β€” you'll see pre-seeded config examples tagged Dev (cDEMO1, cDEMO2). These illustrate the JSON schema only; they are not deployed contracts and cannot be wrapped or decrypted.

Step 1: Open config/custom-pairs.json

Step 2: Add your entry under the correct chain key ("11155111" for Sepolia, "1" for Mainnet):

{
  "11155111": [
    {
      "erc20": "0xYourUnderlyingERC20Address",
      "erc7984": "0xYourERC7984WrapperAddress",
      "symbol": "cMYTOKEN",
      "decimals": 18,
      "source": "local-dev"
    }
  ],
  "1": []
}
Field Type Required Description
erc20 address βœ… Underlying ERC-20 address on the target network
erc7984 address βœ… ERC-7984 wrapper address on the target network
symbol string βœ… Ticker for the wrapper token (prefix with c)
decimals number βœ… ERC-20 decimals (max 18; wrapper auto-caps at 6)
source "local-dev" βœ… Must always be "local-dev"
configExample boolean Optional When true, pair is registry display-only (no Shield/Decrypt/Distribute)

Step 3: Restart dev server. Pair shows with Dev badge.

Path C β€” In-App Admin UI (Interactive extensibility demo)

  1. Open Registry β†’ Add a Pair
  2. Paste ERC-20 + ERC-7984 addresses (validated on the currently connected network)
  3. Pair appears instantly as Preview β€” browser-only, chain-scoped in localStorage
  4. Click Copy Config Snippet to paste into custom-pairs.json for a permanent entry

Works on both Sepolia and Mainnet β€” preview pairs never leak across networks.

Path D β€” Deploy your own pair (dev-guide/)

Cross-platform Hardhat guide (macOS + Windows): dev-guide/README.md.
npm run deploy:sepolia β†’ paste addresses into Path B or Path C.

Security note: Local-dev and preview pairs are visually separated from canonical on-chain registry pairs.


πŸͺ™ Official Sepolia cTokenMocks

All 8 official Sepolia confidential wrappers from the Zama docs are hardcoded in src/lib/config.ts (7 mocks + ctGBP). The Faucet tab shows the 7 public-mint mocks only:

Symbol ERC-7984 Wrapper Underlying ERC-20 Faucet
cUSDCMock 0x7c5BF43...3639 0x9b5Cd1...fF βœ… Public
cUSDTMock 0x4E7B06...491 0xa7dA08...b0 βœ… Public
cWETHMock 0x46208...158 0xff5473...3F βœ… Public
cBRONMock 0xaa5612...891 0xFf021f...5E βœ… Public
cZAMAMock 0xf2D628...bFB 0x75355a...57 βœ… Public
ctGBPMock 0xfCE5c7...7CC 0x93c931...42 βœ… Public
cXAUtMock 0xe4FcF8...0C7 0x24377A...40 βœ… Public
ctGBP 0x167DC9...208 0xf6Ef9A...f3 ⚠️ Restricted

πŸ”¬ Technical Deep Dive

Wrap Flow (ERC-20 β†’ ERC-7984)

stateDiagram-v2
    [*] --> Idle : User enters amount
    Idle --> CheckingAllowance : Submit clicked
    CheckingAllowance --> NeedsApproval : allowance < amount
    CheckingAllowance --> Wrapping : allowance >= amount
    NeedsApproval --> Approving : User confirms approve tx
    Approving --> Wrapping : Approval confirmed on-chain
    Wrapping --> Confirmed : Wrap tx mined + ERC-7984 minted
    Wrapping --> Error : Tx rejected or reverted
    Approving --> Error : Approval rejected
    Confirmed --> [*]
    Error --> [*] : User retries
Loading

Key implementation details:

  1. Allowance check β€” ERC20.allowance(user, wrapperAddress) via viem readContract
  2. ERC-20 Approve β€” ERC20.approve(wrapperAddress, rawAmount) standard tx
  3. Shield / Wrap β€” useShield() from @zama-fhe/react-sdk encrypts the amount client-side with TFHE and calls wrapper.wrap(encryptedInput) via the Zama SDK
  4. Rate conversion β€” wrapper.rate() converts between ERC-20 raw units and the 6-decimal encrypted ERC-7984 representation

Unwrap Flow (ERC-7984 β†’ ERC-20)

Unwrapping requires the Zama relayer to publicly decrypt the amount before the ERC-20 can be transferred back:

sequenceDiagram
    participant U as User
    participant A as App
    participant W as Wrapper Contract
    participant R as Zama Relayer
    participant BC as Blockchain

    U->>A: Click Unwrap + confirm amount
    A->>W: unwrap(encryptedAmount)
    W->>BC: emit UnwrapRequested(requestId, receiver, encAmt)
    BC-->>A: tx confirmed
    A-->>U: Show Pending Finalization state

    Note over R: Relayer monitors UnwrapRequested events
    R->>BC: FHE.decrypt(encAmt)
    BC-->>R: plaintext amount
    R->>W: finalizeUnwrap(requestId, plainAmt)
    W->>BC: ERC20.transfer(receiver, plainAmt)

    BC-->>A: UnwrapFinalized event
    A-->>U: Confirmed β€” ERC-20 received
Loading

The UI shows a two-phase state machine: requesting β†’ pending-finalization (~30–90s) β†’ confirmed, with a clear explanation of why finalization takes time.

EIP-712 User-Decryption Flow

sequenceDiagram
    participant U as User
    participant A as App
    participant W as Wallet (MetaMask)
    participant Z as Zama Relayer
    participant C as ERC-7984 Contract

    U->>A: Paste address or select from dropdown
    A->>C: supportsInterface(0x4958f2a4)
    C-->>A: isERC7984 = true

    A->>W: eth_signTypedData_v4 (EIP-712 permit)
    Note over W: Domain: ERC7984UserDecrypt, chainId: 11155111
    W-->>U: Signature request prompt
    U->>W: Sign
    W-->>A: ECDSA signature

    A->>Z: userDecrypt(contractAddr, signature, userPubKey)
    Z->>C: getEncryptedBalance(user)
    C-->>Z: ciphertext balance (euint64)
    Z->>Z: TFHE.decrypt(ciphertext)
    Z-->>A: plaintext balance

    A-->>U: Display Your Balance: X.XXXXX cTOKEN
Loading

The EIP-712 signature authorizes the relayer to decrypt only the signing user's balance β€” it is impossible to decrypt another user's balance with this flow.

Error Handling

Every wallet interaction routes through a centralized error formatter in src/lib/errors.ts:

export function formatWalletError(error: unknown, chainId?: number): string {
  const msg = (error instanceof Error ? error.message : String(error)).toLowerCase();

  if (msg.includes("user rejected") || msg.includes("user denied"))
    return "Transaction rejected by user.";
  if (msg.includes("wrong network") || msg.includes("chain mismatch"))
    return chainId === 1
      ? "Please switch to Ethereum mainnet to continue."
      : "Please switch to Sepolia testnet to continue.";
  if (msg.includes("gas"))
    return chainId === 1
      ? "Transaction requires more gas. Ensure you have enough ETH."
      : "Transaction requires more gas. Ensure you have enough Sepolia ETH.";
  // ...additional mappings...
}
Failure Scenario User-Facing Message
User cancels MetaMask prompt "Transaction rejected by user."
Insufficient ERC-20 balance "Insufficient balance for this transaction."
Missing ERC-20 allowance "Token approval required. Please approve first."
Wrong network connected "Please switch to Sepolia testnet to continue."
Not an ERC-7984 address "This address does not support ERC-7984 decryption."
Gas estimation failure "Ensure you have enough Sepolia ETH."
Contract reverts "The contract rejected this operation."
Relayer timeout "Transaction timed out. Please check your wallet."

πŸ› οΈ Tech Stack

Layer Technology Version Purpose
Framework Next.js 15 (App Router) SSR, API routes, file routing
Language TypeScript 5.x (strict) Full type safety across all modules
Styling Tailwind CSS v4 Utility-first, responsive styling
Animation Motion (Framer Motion) v12 Scroll reveals, micro-interactions
Web3 State wagmi v3 Wallet connection, tx lifecycle
EVM Client viem v2 Type-safe contract reads + writes
FHE React SDK @zama-fhe/react-sdk v3 useShield, useUnshield, useConfidentialBalance
FHE Core @zama-fhe/sdk v3 TFHE encryption primitives
Data Fetching TanStack React Query v5 Server state, smart caching
Wallet Support @wagmi/connectors v8 MetaMask, WalletConnect v2

πŸ’» Local Development

Prerequisites

  • Node.js β‰₯ 20.x
  • A wallet with Sepolia ETH (faucet: https://sepoliafaucet.com)
  • Sepolia RPC URL (Alchemy, Infura, or use the default public node)

Setup

# 1. Clone the repository
git clone https://github.com/pramadanif/macetz.git
cd macetz

# 2. Install dependencies
# (the repo-level .npmrc sets legacy-peer-deps for the TokenOps SDK,
#  so a plain npm install works out of the box)
npm install

# 3. Configure environment variables
cp .env.example .env.local
# Edit .env.local (see Environment Variables section below)

# 4. Start the development server
npm run dev
# Landing: http://localhost:3000
# dApp:    http://localhost:3000/app

Type Checking

# TypeScript strict mode β€” zero type errors expected
npm run lint

Production Build

npm run build
npm run start

🚒 Deployment

Vercel (Recommended)

# One-command deploy
npx vercel --prod

No backend is required. All FHEVM operations run client-side via the Zama SDK. By default the client routes relayer traffic through the same-origin Next.js proxy at /api/relayer/<chainId> (built from window.location.origin). Override with NEXT_PUBLIC_RELAYER_URL / NEXT_PUBLIC_MAINNET_RELAYER_URL to hit Zama relayers directly.

Environment Variables (Vercel Dashboard)

Set these in your Vercel project settings under Settings β†’ Environment Variables:

NEXT_PUBLIC_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_wc_project_id
# Optional β€” unset uses same-origin proxy automatically in the browser
# NEXT_PUBLIC_RELAYER_URL=https://your-app.vercel.app/api/relayer/11155111

πŸ” Environment Variables

Variable Required Default Description
NEXT_PUBLIC_RPC_URL Optional Public Sepolia node Sepolia JSON-RPC endpoint
NEXT_PUBLIC_MAINNET_RPC_URL Optional Public mainnet node Mainnet JSON-RPC endpoint
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID Optional "" Enables WalletConnect modal
NEXT_PUBLIC_RELAYER_URL Optional Same-origin /api/relayer/11155111 in browser Override Sepolia relayer URL (absolute http(s))
NEXT_PUBLIC_MAINNET_RELAYER_URL Optional Same-origin /api/relayer/1 proxy Override mainnet relayer URL (mainnet FHE ops are relayer-dependent)

Copy .env.example to .env.local to get started:

# .env.example

# Sepolia RPC (Alchemy, Infura, or public node)
NEXT_PUBLIC_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com

# WalletConnect Project ID β€” get one at cloud.walletconnect.com
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=

# Sepolia relayer β€” unset = same-origin proxy in browser ({origin}/api/relayer/11155111)
# NEXT_PUBLIC_RELAYER_URL=https://your-app.vercel.app/api/relayer/11155111

# Mainnet relayer β€” unset = same-origin proxy; upstream may 403 until Zama provisions mainnet
# NEXT_PUBLIC_MAINNET_RELAYER_URL=https://your-app.vercel.app/api/relayer/1

πŸ’Ό Special Bounty Track β€” Confidential Payroll (TokenOps)

Macetz integrates TokenOps Confidential Disperse (@tokenops/sdk/fhe-disperse) in the same app as the Wrappers Registry bounty β€” no separate deployment.

Use case

Corporate payroll on Sepolia: HR shields USDC (or any registry mock), batches encrypted salaries to employee wallets in one transaction. Each employee decrypts only their own allocation via EIP-712. Third parties see recipients but not individual amounts.

Smart contract layer (for judges)

There is no custom disperse contract in this repo. The app calls the official TokenOps singleton via SDK:

Network DisperseConfidential Singleton
Sepolia 0x710dD9885Cc9986EfD234E7719483147a6d8DBb4

Campaign clones are not used for disperse β€” the singleton + SDK useDisperse handles encryption, ACL grants, and confidentialTransferFrom in one tx (mode: "direct").

Distribute tab flows (Sepolia only)

  1. Sender wizard (4 steps): Select docs-verified operational shielded token β†’ Review preflight (usePreflightDisperse) β†’ Approve singleton operator + useDisperse tx β†’ Track claim status (pending/claimed via confidentialBalanceOf β€” amounts never shown to third parties)
  2. Recipient view: Scan operational registry tokens for pending handles β†’ Decrypt & Claim via EIP-712
  3. CSV upload: address,amount per line for payroll imports
  4. Mainnet: informational banner β€” disperse singleton is Sepolia-only

Payroll safety: Distribute (isDistributeOperationalPair) requires docsVerified === true for onchain registry pairs. Non-docs onchain pairs remain usable in Shield/Decrypt.

Autonomous smoke tests: npm run verify:distribute

TokenOps SDK wiring

npm install @tokenops/sdk --legacy-peer-deps

Encryptor uses the live Zama relayer from @zama-fhe/react-sdk (useZamaSDK().relayer), forwarded lazily into useDisperse({ encryptor: () => relayer }) per TokenOps docs.


βœ… Bounty Submission Checklist (Wrappers Registry Track)

Requirement Status Where
Public GitHub repo βœ… This repository
Live URL (wallet connect) βœ… https://macetz.vercel.app Live Deployment
Sepolia: browse registry βœ… Registry tab β€” all valid onchain pairs + docs-verified badge
Sepolia: all 7 cTokenMock faucet mints βœ… Faucet tab β€” KNOWN_MOCK_PAIRS + Mint All
Sepolia: wrap / unwrap every registry pair βœ… Shield tab β€” useShield / useUnshield
Sepolia: decrypt registry + arbitrary ERC-7984 βœ… Decrypt tab β€” Registry + Any ERC-7984 modes
Hybrid registry (onchain + local config) βœ… registry.ts + custom-pairs.json + Admin UI
Documented add-pair process (4 paths) βœ… README + in-app Docs + dev-guide/
EIP-712 user-decryption βœ… @zama-fhe/react-sdk useConfidentialBalance
Relayer SDK integration βœ… Web3Provider defaults to /api/relayer/<chainId> proxy
Error handling (approval, balance, network) βœ… src/lib/errors.ts + UI guards
Demo video + X thread πŸ”² Submitter delivers β€”

Judge quick path (Sepolia): Faucet mint cUSDCMock β†’ Shield wrap β†’ Decrypt balance β†’ Unshield β†’ Decrypt tab paste arbitrary wrapper from dev-guide deploy.

Run automated checks: npm run verify


🧾 Verified On-Chain Evidence (Sepolia)

Every hash below was produced by the reproducible E2E script scripts/sepolia-bounty-e2e.ts (run it with your own funded key to regenerate). The dev-guide deployment record lives in dev-guide/deployed-addresses.json.

Action Tx hash / address Etherscan
Faucet mint (cUSDCMock) 0x587c256bf57262a1f355aa724dcd1f115f0314a99c9954cc7a281b724c96da9b View
Wrap (shield cUSDCMock) 0xa17bdde962fcddcc9a63310110714ad846d3e36982782740339a4e51fc0fd6ac View
Decrypt balance EIP-712 relayer (no on-chain tx) β€” cUSDCMock + cMTUSD verified 2026-07-07 β€”
Unwrap (unshield phase 1) 0x8d90e5c42548484d9e55abe2c2be6b97b23973211402a902fe3f9e4f13e7069f View
Unwrap finalize (phase 2) 0xc6101c96c5c6ed3751b6586cad8f848ea64c9d5934142e6959da3ffa42889d65 View
TokenOps disperse 0x41d71ac3f1d8f701f9a85d8d0a3d4b58bae1ba238a6d0fae7ce1d16c3f74f9d1 View
dev-guide deploy (MTUSD) ERC-20 0x022D67AeE3a5f841CC0c422F0B849B366f2c59B7 View
dev-guide deploy (cMTUSD) Wrapper 0x3A1E3F5a8C5975078C587C73E80A916505538C4B View
dev-guide deployer 0xB4d186aF4d691dE665a36BDA1104067e069a15F8 View
dev-guide wrap tx 0xd767600c0d9e96f2c173d8c0b2596c57d5715c7ef31d685f969712303f52e976 View

⚠️ Known Limitations

  • Bounty E2E on Sepolia β€” Full wrap/unwrap/faucet/distribute flow is validated on Sepolia testnet. Mainnet supports registry browse; shield/decrypt are relayer-dependent and may fail until Zama provisions the mainnet relayer.
  • Config examples β€” configExample: true dev pairs are registry display-only until replaced with deployed addresses
  • Single-token batches β€” TokenOps Distribute processes one confidential token per payroll run
  • Injected wallet β€” Requires MetaMask or any EIP-1193 wallet
  • Relayer latency β€” Unwrap finalization depends on Zama's relayer (~30–90s on Sepolia)
  • Unaudited β€” Community project for the Zama Developer Program; do not use with real funds

πŸ“„ License

MIT


Built with ❀️ for the Zama Ecosystem

Live Demo Β· GitHub Β· Zama Docs

About

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors