create-shield-unshield-dapp 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +31 -0
  2. package/bin/cli.js +70 -0
  3. package/package.json +26 -0
  4. package/template/.env.example +16 -0
  5. package/template/README.md +91 -0
  6. package/template/index.html +12 -0
  7. package/template/package.json +45 -0
  8. package/template/public/usdc.svg +5 -0
  9. package/template/public/usdt.svg +1 -0
  10. package/template/src/App.tsx +6 -0
  11. package/template/src/components/ui/Badge.tsx +23 -0
  12. package/template/src/components/ui/Button.tsx +45 -0
  13. package/template/src/components/ui/Card.tsx +22 -0
  14. package/template/src/components/ui/Input.tsx +33 -0
  15. package/template/src/hooks/useConfidentialBalance.ts +85 -0
  16. package/template/src/hooks/useConfidentialTransfer.ts +50 -0
  17. package/template/src/hooks/useFhevmDecrypt.ts +67 -0
  18. package/template/src/hooks/useFhevmEncrypt.ts +63 -0
  19. package/template/src/hooks/useUnwrapToken.ts +139 -0
  20. package/template/src/hooks/useWrapToken.ts +93 -0
  21. package/template/src/index.css +51 -0
  22. package/template/src/lib/contracts.ts +71 -0
  23. package/template/src/lib/utils.ts +44 -0
  24. package/template/src/main.tsx +24 -0
  25. package/template/src/pages/ShieldUnshieldPage.tsx +355 -0
  26. package/template/src/providers/FhevmContext.ts +18 -0
  27. package/template/src/providers/FhevmProvider.tsx +140 -0
  28. package/template/src/providers/WalletProvider.tsx +49 -0
  29. package/template/src/providers/useFhevmContext.ts +8 -0
  30. package/template/tsconfig.app.json +24 -0
  31. package/template/tsconfig.json +1 -0
  32. package/template/tsconfig.node.json +20 -0
  33. package/template/vite.config.ts +29 -0
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # create-shield-unshield-dapp
2
+
3
+ Scaffold a **Shield/Unshield dApp** for wrapping and unwrapping USDC ↔ cUSDC and USDT ↔ cUSDT, and for confidential transfers. Uses ERC-7984 confidential tokens and the Zama relayer.
4
+
5
+ ## Install and run
6
+
7
+ ```bash
8
+ npx create-shield-unshield-dapp my-app
9
+ cd my-app
10
+ cp .env.example .env
11
+ # Edit .env (see below)
12
+ npm run dev
13
+ ```
14
+
15
+ Open the URL shown (e.g. http://localhost:5173) and connect your wallet.
16
+
17
+ ## Switching environment (mainnet vs testnet)
18
+
19
+ - **Mainnet:** In `.env` set `VITE_MAINNET=true`. Contract addresses are built-in. Optionally set `VITE_MAINNET_RPC_URL`. Connect your wallet to Ethereum Mainnet.
20
+ - **Testnet (Sepolia):** Leave `VITE_MAINNET=false` or omit it. Set the four Sepolia contract addresses in `.env`: `VITE_USDC_ADDRESS`, `VITE_CONF_USDC_ADDRESS`, `VITE_USDT_ADDRESS`, `VITE_CONF_USDT_ADDRESS`. Connect your wallet to Sepolia.
21
+
22
+ Full details, all environment variables, and where to get Sepolia addresses are in the **scaffolded project’s README** (`README.md` in the created folder).
23
+
24
+ ## Options
25
+
26
+ - `npx create-shield-unshield-dapp my-app --force` — Overwrite existing files in `my-app` if it already exists.
27
+ - `npx create-shield-unshield-dapp my-app --no-install` — Skip running `npm install` after copying (run it yourself).
28
+
29
+ ## License
30
+
31
+ MIT.
package/bin/cli.js ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+
7
+ const TEMPLATE_DIR = path.join(__dirname, '..', 'template');
8
+
9
+ function parseArgs() {
10
+ const args = process.argv.slice(2);
11
+ const force = args.includes('--force') || args.includes('-f');
12
+ const noInstall = args.includes('--no-install');
13
+ const target = args.find((a) => !a.startsWith('-')) || 'shield-unshield-app';
14
+ return { target, force, noInstall };
15
+ }
16
+
17
+ function copyRecursive(src, dest, force) {
18
+ if (!fs.existsSync(src)) {
19
+ console.error('Template not found at', src);
20
+ process.exit(1);
21
+ }
22
+ const stat = fs.statSync(src);
23
+ if (stat.isDirectory()) {
24
+ if (!fs.existsSync(dest)) {
25
+ fs.mkdirSync(dest, { recursive: true });
26
+ }
27
+ for (const entry of fs.readdirSync(src)) {
28
+ copyRecursive(path.join(src, entry), path.join(dest, entry), force);
29
+ }
30
+ } else {
31
+ if (fs.existsSync(dest) && !force) return;
32
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
33
+ fs.copyFileSync(src, dest);
34
+ }
35
+ }
36
+
37
+ function main() {
38
+ const { target, force, noInstall } = parseArgs();
39
+ const targetPath = path.resolve(process.cwd(), target);
40
+
41
+ if (fs.existsSync(targetPath) && fs.readdirSync(targetPath).length > 0 && !force) {
42
+ console.error(`Directory "${target}" already exists and is not empty. Use --force to overwrite.`);
43
+ process.exit(1);
44
+ }
45
+
46
+ console.log('Creating Shield/Unshield dApp in', targetPath);
47
+ if (!fs.existsSync(targetPath)) {
48
+ fs.mkdirSync(targetPath, { recursive: true });
49
+ }
50
+ copyRecursive(TEMPLATE_DIR, targetPath, force);
51
+ console.log('Template copied.');
52
+
53
+ if (!noInstall) {
54
+ console.log('Running npm install...');
55
+ try {
56
+ execSync('npm install', { cwd: targetPath, stdio: 'inherit' });
57
+ } catch (e) {
58
+ console.warn('npm install failed. Run "npm install" in the project folder manually.');
59
+ }
60
+ }
61
+
62
+ console.log('\nDone! Next steps:\n');
63
+ console.log(` cd ${target}`);
64
+ console.log(' cp .env.example .env');
65
+ console.log(' # Edit .env: set VITE_MAINNET=true for mainnet, or set Sepolia contract addresses for testnet.');
66
+ console.log(' # See README.md for full environment and mainnet/testnet docs.');
67
+ console.log(' npm run dev\n');
68
+ }
69
+
70
+ main();
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "create-shield-unshield-dapp",
3
+ "version": "1.0.1",
4
+ "description": "Scaffold a Shield/Unshield dApp for USDC/cUSDC and USDT/cUSDT (wrap, unwrap, confidential transfer)",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "create-shield-unshield-dapp": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "template"
12
+ ],
13
+ "scripts": {
14
+ "copy-template": "node scripts/copy-template.js"
15
+ },
16
+ "keywords": [
17
+ "shield",
18
+ "unshield",
19
+ "confidential",
20
+ "usdc",
21
+ "usdt",
22
+ "erc-7984",
23
+ "zama"
24
+ ],
25
+ "license": "MIT"
26
+ }
@@ -0,0 +1,16 @@
1
+ # Network: "true" = Ethereum Mainnet, anything else (or unset) = Sepolia testnet
2
+ VITE_MAINNET=false
3
+
4
+ # ---- Testnet (Sepolia) ----
5
+ VITE_SEPOLIA_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
6
+ VITE_USDC_ADDRESS=0x...
7
+ VITE_CONF_USDC_ADDRESS=0x...
8
+ VITE_USDT_ADDRESS=0x...
9
+ VITE_CONF_USDT_ADDRESS=0x...
10
+
11
+ # ---- Mainnet (only when VITE_MAINNET=true) ----
12
+ # Mainnet contract addresses are built-in. Optional: custom RPC
13
+ # VITE_MAINNET_RPC_URL=https://eth.llamarpc.com
14
+
15
+ # WalletConnect project ID (optional)
16
+ # VITE_WALLETCONNECT_PROJECT_ID=your_project_id
@@ -0,0 +1,91 @@
1
+ # Shield / Unshield dApp
2
+
3
+ A frontend for wrapping and unwrapping **USDC ↔ cUSDC** and **USDT ↔ cUSDT**, and for **transferring** confidential tokens. Supports **Ethereum Mainnet** and **Sepolia** testnet. Uses ERC-7984 confidential token contracts and the Zama relayer for unwrap decryption.
4
+
5
+ ## Features
6
+
7
+ - **Wrap (shield):** USDC → cUSDC, USDT → cUSDT.
8
+ - **Unwrap (unshield):** cUSDC → USDC, cUSDT → USDT (two-step flow via Zama gateway).
9
+ - **Transfer:** Send cUSDC or cUSDT to any address; amount is encrypted on-chain.
10
+
11
+ ## Prerequisites
12
+
13
+ - Node.js 18+
14
+ - A wallet (e.g. MetaMask) on the network you use (Mainnet or Sepolia).
15
+
16
+ ## Quick start
17
+
18
+ 1. Install dependencies (if not already done):
19
+
20
+ ```bash
21
+ npm install
22
+ ```
23
+
24
+ 2. Copy the example env file and edit it:
25
+
26
+ ```bash
27
+ cp .env.example .env
28
+ ```
29
+
30
+ 3. Configure for your target network:
31
+ - **Testnet (Sepolia):** Set the four contract addresses in `.env` (see [Environment variables](#environment-variables)).
32
+ - **Mainnet:** Set only `VITE_MAINNET=true` in `.env`; contract addresses are built-in.
33
+
34
+ 4. Run the app:
35
+
36
+ ```bash
37
+ npm run dev
38
+ ```
39
+
40
+ 5. Open the URL (e.g. http://localhost:5173), connect your wallet to the correct network (Sepolia or Mainnet), and use the app.
41
+
42
+ ## Switching environment (mainnet vs testnet)
43
+
44
+ The app uses a single env flag to choose the network.
45
+
46
+ | Mode | Set in `.env` | Contract addresses |
47
+ |----------|-----------------------------------|--------------------|
48
+ | Testnet | `VITE_MAINNET=false` or omit | **Required:** `VITE_USDC_ADDRESS`, `VITE_CONF_USDC_ADDRESS`, `VITE_USDT_ADDRESS`, `VITE_CONF_USDT_ADDRESS` (Sepolia). |
49
+ | Mainnet | `VITE_MAINNET=true` | **Built-in.** No address env vars needed. Optional: `VITE_MAINNET_RPC_URL`. |
50
+
51
+ - **After changing** `.env`, restart the dev server (`npm run dev`) or rebuild for production (`npm run build`).
52
+ - **Mainnet:** Wrap and transfer work with built-in addresses. Unwrap/decrypt on mainnet may require a relayer API key (contact the Zama team for access).
53
+
54
+ ## Environment variables
55
+
56
+ | Variable | Required | Description |
57
+ |----------|----------|-------------|
58
+ | `VITE_MAINNET` | No | Set to `"true"` for Ethereum Mainnet; anything else or unset = Sepolia testnet. |
59
+ | `VITE_SEPOLIA_RPC_URL` | No | Sepolia RPC URL (default: public node). |
60
+ | `VITE_USDC_ADDRESS` | Yes (testnet) | USDC (underlying) contract on Sepolia. |
61
+ | `VITE_CONF_USDC_ADDRESS` | Yes (testnet) | cUSDC (wrapper) contract on Sepolia. |
62
+ | `VITE_USDT_ADDRESS` | Yes (testnet) | USDT (underlying) contract on Sepolia. |
63
+ | `VITE_CONF_USDT_ADDRESS` | Yes (testnet) | cUSDT (wrapper) contract on Sepolia. |
64
+ | `VITE_MAINNET_RPC_URL` | No | Mainnet RPC URL when `VITE_MAINNET=true` (default: public RPC). |
65
+ | `VITE_WALLETCONNECT_PROJECT_ID` | No | WalletConnect project ID (optional). |
66
+
67
+ ## Where to get Sepolia addresses
68
+
69
+ Obtain the USDC, cUSDC, USDT, and cUSDT contract addresses from your deployment or from the Zama/contract documentation. The app does not ship valid default addresses for Sepolia.
70
+
71
+ ## Build and preview
72
+
73
+ ```bash
74
+ npm run build
75
+ ```
76
+
77
+ Output is in `dist/`. To serve it locally:
78
+
79
+ ```bash
80
+ npm run preview
81
+ ```
82
+
83
+ ## Architecture
84
+
85
+ - **Frontend only:** No backend or indexer; reads from the chain and uses the Zama relayer for unwrap decryption proof.
86
+ - **Contracts:** ERC-7984 confidential token wrappers with `wrap`, `unwrap`, `finalizeUnwrap`, and `confidentialTransfer`.
87
+ - **Two token pairs:** USDC/cUSDC and USDT/cUSDT; on testnet you supply all four addresses via env; on mainnet they are built-in.
88
+
89
+ ## License
90
+
91
+ MIT.
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Shield / Unshield dApp</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "shield-unshield-dapp",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "call-bound": "^1.0.2",
14
+ "es-errors": "^1.3.0",
15
+ "is-typed-array": "^1.1.13",
16
+ "@rainbow-me/rainbowkit": "^2.2.10",
17
+ "@tanstack/react-query": "^5.90.12",
18
+ "@tailwindcss/vite": "^4.1.18",
19
+ "@zama-fhe/relayer-sdk": "^0.3.0-8",
20
+ "clsx": "^2.1.1",
21
+ "lucide-react": "^0.562.0",
22
+ "react": "^19.2.0",
23
+ "react-dom": "^19.2.0",
24
+ "react-hot-toast": "^2.6.0",
25
+ "react-router-dom": "^7.11.0",
26
+ "tailwind-merge": "^3.4.0",
27
+ "tailwindcss": "^4.1.18",
28
+ "viem": "^2.43.2",
29
+ "wagmi": "^2.19.5"
30
+ },
31
+ "devDependencies": {
32
+ "@eslint/js": "^9.39.1",
33
+ "@types/node": "^24.10.1",
34
+ "@types/react": "^19.2.5",
35
+ "@types/react-dom": "^19.2.3",
36
+ "@vitejs/plugin-react": "^5.1.1",
37
+ "eslint": "^9.39.1",
38
+ "eslint-plugin-react-hooks": "^7.0.1",
39
+ "eslint-plugin-react-refresh": "^0.4.24",
40
+ "globals": "^16.5.0",
41
+ "typescript": "~5.9.3",
42
+ "typescript-eslint": "^8.46.4",
43
+ "vite": "7.2.4"
44
+ }
45
+ }
@@ -0,0 +1,5 @@
1
+ <svg data-name="86977684-12db-4850-8f30-233a7c267d11" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 2000">
2
+ <path d="M1000 2000c554.17 0 1000-445.83 1000-1000S1554.17 0 1000 0 0 445.83 0 1000s445.83 1000 1000 1000z" fill="#2775ca"/>
3
+ <path d="M1275 1158.33c0-145.83-87.5-195.83-262.5-216.66-125-16.67-150-50-150-108.34s41.67-95.83 125-95.83c75 0 116.67 25 137.5 87.5 4.17 12.5 16.67 20.83 29.17 20.83h66.66c16.67 0 29.17-12.5 29.17-29.16v-4.17c-16.67-91.67-91.67-162.5-187.5-170.83v-100c0-16.67-12.5-29.17-33.33-33.34h-62.5c-16.67 0-29.17 12.5-33.34 33.34v95.83c-125 16.67-204.16 100-204.16 204.17 0 137.5 83.33 191.66 258.33 212.5 116.67 20.83 154.17 45.83 154.17 112.5s-58.34 112.5-137.5 112.5c-108.34 0-145.84-45.84-158.34-108.34-4.16-16.66-16.66-25-29.16-25h-70.84c-16.66 0-29.16 12.5-29.16 29.17v4.17c16.66 104.16 83.33 179.16 220.83 200v100c0 16.66 12.5 29.16 33.33 33.33h62.5c16.67 0 29.17-12.5 33.34-33.33v-100c125-20.84 208.33-108.34 208.33-220.84z" fill="#fff"/>
4
+ <path d="M787.5 1595.83c-325-116.66-491.67-479.16-370.83-800 62.5-175 200-308.33 370.83-370.83 16.67-8.33 25-20.83 25-41.67V325c0-16.67-8.33-29.17-25-33.33-4.17 0-12.5 0-16.67 4.16-395.83 125-612.5 545.84-487.5 941.67 75 233.33 254.17 412.5 487.5 487.5 16.67 8.33 33.34 0 37.5-16.67 4.17-4.16 4.17-8.33 4.17-16.66v-58.34c0-12.5-12.5-29.16-25-37.5zM1229.17 295.83c-16.67-8.33-33.34 0-37.5 16.67-4.17 4.17-4.17 8.33-4.17 16.67v58.33c0 16.67 12.5 33.33 25 41.67 325 116.66 491.67 479.16 370.83 800-62.5 175-200 308.33-370.83 370.83-16.67 8.33-25 20.83-25 41.67V1700c0 16.67 8.33 29.17 25 33.33 4.17 0 12.5 0 16.67-4.16 395.83-125 612.5-545.84 487.5-941.67-75-237.5-258.34-416.67-487.5-491.67z" fill="#fff"/>
5
+ </svg>
@@ -0,0 +1 @@
1
+ <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 339.43 295.27"><title>tether-usdt-logo</title><path d="M62.15,1.45l-61.89,130a2.52,2.52,0,0,0,.54,2.94L167.95,294.56a2.55,2.55,0,0,0,3.53,0L338.63,134.4a2.52,2.52,0,0,0,.54-2.94l-61.89-130A2.5,2.5,0,0,0,275,0H64.45a2.5,2.5,0,0,0-2.3,1.45h0Z" style="fill:#50af95;fill-rule:evenodd"/><path d="M191.19,144.8v0c-1.2.09-7.4,0.46-21.23,0.46-11,0-18.81-.33-21.55-0.46v0c-42.51-1.87-74.24-9.27-74.24-18.13s31.73-16.25,74.24-18.15v28.91c2.78,0.2,10.74.67,21.74,0.67,13.2,0,19.81-.55,21-0.66v-28.9c42.42,1.89,74.08,9.29,74.08,18.13s-31.65,16.24-74.08,18.12h0Zm0-39.25V79.68h59.2V40.23H89.21V79.68H148.4v25.86c-48.11,2.21-84.29,11.74-84.29,23.16s36.18,20.94,84.29,23.16v82.9h42.78V151.83c48-2.21,84.12-11.73,84.12-23.14s-36.09-20.93-84.12-23.15h0Zm0,0h0Z" style="fill:#fff;fill-rule:evenodd"/></svg>
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { ShieldUnshieldPage } from './pages/ShieldUnshieldPage';
3
+
4
+ export default function App() {
5
+ return <ShieldUnshieldPage />;
6
+ }
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { cn } from '../../lib/utils';
3
+
4
+ interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
5
+ variant?: 'default' | 'primary' | 'success' | 'warning' | 'error';
6
+ size?: 'sm' | 'md';
7
+ }
8
+
9
+ export function Badge({ className, variant = 'default', size = 'sm', children, ...props }: BadgeProps) {
10
+ const base = 'inline-flex items-center font-bold rounded-full';
11
+ const sizes: Record<string, string> = {
12
+ sm: 'px-2.5 py-0.5 text-[11px]',
13
+ md: 'px-3 py-1 text-xs',
14
+ };
15
+ const variants: Record<string, string> = {
16
+ default: 'bg-[#f4eee6] text-[var(--color-text-primary)]',
17
+ primary: 'bg-[var(--color-primary)]/10 text-[var(--color-primary)]',
18
+ success: 'bg-[var(--color-success-bg)] text-emerald-700',
19
+ warning: 'bg-[var(--color-warning-bg)] text-amber-700',
20
+ error: 'bg-[var(--color-error-bg)] text-red-700',
21
+ };
22
+ return <span className={cn(base, sizes[size], variants[variant], className)} {...props}>{children}</span>;
23
+ }
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import { cn } from '../../lib/utils';
3
+
4
+ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
5
+ variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'success';
6
+ size?: 'sm' | 'md' | 'lg';
7
+ loading?: boolean;
8
+ }
9
+
10
+ export function Button({
11
+ className,
12
+ variant = 'primary',
13
+ size = 'md',
14
+ loading = false,
15
+ disabled,
16
+ children,
17
+ ...props
18
+ }: ButtonProps) {
19
+ const base =
20
+ 'relative inline-flex items-center justify-center font-bold rounded-xl transition-all duration-200 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';
21
+ const sizes: Record<string, string> = {
22
+ sm: 'px-3 py-1.5 text-xs gap-1.5',
23
+ md: 'px-5 py-2.5 text-sm gap-2',
24
+ lg: 'px-7 py-3.5 text-base gap-2.5',
25
+ };
26
+ const variants: Record<string, string> = {
27
+ primary: 'bg-[var(--color-primary)] text-white shadow-lg hover:bg-orange-600 active:scale-[0.98] focus-visible:ring-[var(--color-primary)]',
28
+ secondary: 'border-2 border-[var(--color-border-light)] text-[var(--color-text-primary)] bg-white hover:bg-[#f4eee6] active:scale-[0.98] focus-visible:ring-[var(--color-primary)]',
29
+ ghost: 'text-[var(--color-text-primary)] hover:bg-[#f4eee6]',
30
+ danger: 'bg-[var(--color-error)] text-white hover:bg-red-600 active:scale-[0.98]',
31
+ success: 'bg-[var(--color-success)] text-white hover:bg-emerald-600 active:scale-[0.98]',
32
+ };
33
+
34
+ return (
35
+ <button className={cn(base, sizes[size], variants[variant], className)} disabled={disabled || loading} {...props}>
36
+ {loading && (
37
+ <svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
38
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
39
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
40
+ </svg>
41
+ )}
42
+ {children}
43
+ </button>
44
+ );
45
+ }
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { cn } from '../../lib/utils';
3
+
4
+ interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ variant?: 'default' | 'elevated' | 'bordered';
6
+ padding?: 'sm' | 'md' | 'lg';
7
+ }
8
+
9
+ export function Card({ className, variant = 'default', padding = 'md', ...props }: CardProps) {
10
+ const base = 'rounded-xl transition-all duration-200';
11
+ const variants: Record<string, string> = {
12
+ default: 'bg-white border border-[var(--color-border-light)] shadow-sm',
13
+ elevated: 'bg-white border border-[var(--color-border-light)] shadow-sm hover:shadow-md',
14
+ bordered: 'bg-white border-2 border-[var(--color-border-light)]',
15
+ };
16
+ const paddings: Record<string, string> = {
17
+ sm: 'p-4',
18
+ md: 'p-6',
19
+ lg: 'p-8',
20
+ };
21
+ return <div className={cn(base, variants[variant], paddings[padding], className)} {...props} />;
22
+ }
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { cn } from '../../lib/utils';
3
+
4
+ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
5
+ label?: string;
6
+ hint?: string;
7
+ error?: string;
8
+ }
9
+
10
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
11
+ ({ className, label, hint, error, ...props }, ref) => (
12
+ <div className="space-y-1.5">
13
+ {label && (
14
+ <label className="block text-xs font-semibold text-[var(--color-text-secondary)] uppercase tracking-wide">
15
+ {label}
16
+ </label>
17
+ )}
18
+ <input
19
+ ref={ref}
20
+ className={cn(
21
+ 'w-full rounded-xl border bg-white px-4 py-2.5 text-sm text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)]',
22
+ 'border-[var(--color-border-input)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20 focus:border-[var(--color-primary)]',
23
+ error && 'border-[var(--color-error)]',
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ {hint && !error && <p className="text-[11px] text-[var(--color-text-tertiary)]">{hint}</p>}
29
+ {error && <p className="text-[11px] text-[var(--color-error)]">{error}</p>}
30
+ </div>
31
+ )
32
+ );
33
+ Input.displayName = 'Input';
@@ -0,0 +1,85 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { createPublicClient, http } from 'viem';
3
+ import { useAccount } from 'wagmi';
4
+ import { CONF_TOKEN_ABI, CHAIN, CHAIN_RPC_URL } from '../lib/contracts';
5
+ import { useFhevmDecrypt } from './useFhevmDecrypt';
6
+
7
+ const client = createPublicClient({
8
+ chain: CHAIN,
9
+ transport: http(CHAIN_RPC_URL, { retryCount: 3, timeout: 10_000 }),
10
+ });
11
+
12
+ const ZERO_HANDLE = '0x' + '0'.repeat(64);
13
+
14
+ export function useConfidentialBalance(confTokenAddress: `0x${string}`) {
15
+ const { address, isConnected } = useAccount();
16
+ const { decryptHandle, isDecrypting, isReady: fheReady } = useFhevmDecrypt();
17
+
18
+ const [handle, setHandle] = useState<string | null>(null);
19
+ const [decryptedBalance, setDecryptedBalance] = useState<bigint | null>(null);
20
+ const [isLoadingHandle, setIsLoadingHandle] = useState(false);
21
+
22
+ const fetchHandle = useCallback(async (): Promise<string | null> => {
23
+ if (!address) {
24
+ setHandle(null);
25
+ setDecryptedBalance(null);
26
+ return null;
27
+ }
28
+ setIsLoadingHandle(true);
29
+ try {
30
+ const result = await client.readContract({
31
+ address: confTokenAddress,
32
+ abi: CONF_TOKEN_ABI,
33
+ functionName: 'confidentialBalanceOf',
34
+ args: [address],
35
+ });
36
+ const h = result as string;
37
+ const newHandle = h && h !== ZERO_HANDLE ? h : null;
38
+ setHandle(newHandle);
39
+ setDecryptedBalance(null);
40
+ return newHandle;
41
+ } catch {
42
+ setHandle(null);
43
+ setDecryptedBalance(null);
44
+ return null;
45
+ } finally {
46
+ setIsLoadingHandle(false);
47
+ }
48
+ }, [address, confTokenAddress]);
49
+
50
+ useEffect(() => {
51
+ if (isConnected && address) void fetchHandle();
52
+ else {
53
+ setHandle(null);
54
+ setDecryptedBalance(null);
55
+ }
56
+ }, [isConnected, address, fetchHandle]);
57
+
58
+ const decrypt = useCallback(async () => {
59
+ if (!handle || !fheReady) return null;
60
+ const value = await decryptHandle(handle, confTokenAddress);
61
+ if (value !== null) setDecryptedBalance(value);
62
+ return value;
63
+ }, [handle, fheReady, decryptHandle, confTokenAddress]);
64
+
65
+ const refetchAndDecrypt = useCallback(async () => {
66
+ const newHandle = await fetchHandle();
67
+ if (newHandle && fheReady) {
68
+ const value = await decryptHandle(newHandle, confTokenAddress);
69
+ if (value !== null) setDecryptedBalance(value);
70
+ }
71
+ }, [fetchHandle, fheReady, decryptHandle, confTokenAddress]);
72
+
73
+ return {
74
+ handle,
75
+ hasBalance: !!handle,
76
+ decryptedBalance,
77
+ isDecrypted: decryptedBalance !== null,
78
+ isLoadingHandle,
79
+ isDecrypting,
80
+ fheReady,
81
+ decrypt,
82
+ refetch: fetchHandle,
83
+ refetchAndDecrypt,
84
+ };
85
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Confidential transfer: encrypt amount and call confidentialTransfer(to, handle, inputProof).
3
+ * No relayer; SDK only.
4
+ */
5
+
6
+ import { useCallback, useState } from 'react';
7
+ import { useAccount, useWriteContract } from 'wagmi';
8
+ import { CONF_TOKEN_ABI, TOKEN_CONFIGS, getConfTokenAddress, type TokenKey } from '../lib/contracts';
9
+ import { parseAmount } from '../lib/utils';
10
+ import { useFhevmEncrypt } from './useFhevmEncrypt';
11
+
12
+ export function useConfidentialTransfer(tokenKey: TokenKey) {
13
+ const { address, isConnected } = useAccount();
14
+ const { encryptAmount, isReady: fheReady } = useFhevmEncrypt();
15
+ const { writeContractAsync, isPending } = useWriteContract();
16
+ const confToken = getConfTokenAddress(tokenKey);
17
+ const config = TOKEN_CONFIGS[tokenKey];
18
+
19
+ const [isTransferring, setIsTransferring] = useState(false);
20
+
21
+ const transfer = useCallback(
22
+ async (toAddress: `0x${string}`, amountHuman: string) => {
23
+ if (!address || !isConnected || !fheReady) return null;
24
+ const amount = parseAmount(amountHuman, config.decimals);
25
+ if (amount <= 0n) return null;
26
+ if (!toAddress || toAddress.length !== 42) return null;
27
+ setIsTransferring(true);
28
+ try {
29
+ const encrypted = await encryptAmount(amount, confToken);
30
+ if (!encrypted?.handles?.[0]) throw new Error('Encryption failed');
31
+ const hash = await writeContractAsync({
32
+ address: confToken,
33
+ abi: CONF_TOKEN_ABI,
34
+ functionName: 'confidentialTransfer',
35
+ args: [toAddress, encrypted.handles[0], encrypted.inputProof as `0x${string}`],
36
+ });
37
+ return hash;
38
+ } finally {
39
+ setIsTransferring(false);
40
+ }
41
+ },
42
+ [address, isConnected, fheReady, encryptAmount, writeContractAsync, confToken, config.decimals]
43
+ );
44
+
45
+ return {
46
+ transfer,
47
+ isTransferring: isTransferring || isPending,
48
+ fheReady,
49
+ };
50
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * FHEVM Decryption Hook - decrypt encrypted values using signature-based auth
3
+ */
4
+
5
+ import { useCallback, useState } from 'react';
6
+ import { useAccount, useSignTypedData } from 'wagmi';
7
+ import { useFhevm } from '../providers/useFhevmContext';
8
+
9
+ export function useFhevmDecrypt() {
10
+ const { instance, isReady, error } = useFhevm();
11
+ const { address, isConnected } = useAccount();
12
+ const { signTypedDataAsync } = useSignTypedData();
13
+ const [isDecrypting, setIsDecrypting] = useState(false);
14
+ const [decryptError, setDecryptError] = useState<Error | null>(null);
15
+
16
+ const decryptHandle = useCallback(
17
+ async (handle: string, contractAddress: string): Promise<bigint | null> => {
18
+ if (!instance || !address || !isReady || !isConnected) {
19
+ setDecryptError(new Error('FHEVM not ready or wallet not connected'));
20
+ return null;
21
+ }
22
+ if (!handle || handle === '0x' + '0'.repeat(64)) return 0n;
23
+ setIsDecrypting(true);
24
+ setDecryptError(null);
25
+ try {
26
+ const keypair = instance.generateKeypair();
27
+ const handleContractPairs = [{ handle, contractAddress }];
28
+ const startTimestamp = Math.floor(Date.now() / 1000).toString();
29
+ const durationDays = '10';
30
+ const contractAddresses = [contractAddress];
31
+ const eip712 = instance.createEIP712(
32
+ keypair.publicKey,
33
+ contractAddresses,
34
+ startTimestamp,
35
+ durationDays
36
+ );
37
+ const sigPayload = eip712 as { domain: unknown; types: Record<string, unknown>; message: unknown };
38
+ const signature = await signTypedDataAsync({
39
+ domain: sigPayload.domain as Record<string, unknown>,
40
+ types: (sigPayload.types || {}) as Record<string, unknown>,
41
+ message: sigPayload.message as Record<string, unknown>,
42
+ primaryType: 'UserDecryptRequestVerification',
43
+ });
44
+ const result = await instance.userDecrypt(
45
+ handleContractPairs,
46
+ keypair.privateKey,
47
+ keypair.publicKey,
48
+ signature.replace('0x', ''),
49
+ contractAddresses,
50
+ address,
51
+ startTimestamp,
52
+ durationDays
53
+ );
54
+ const value = result[handle];
55
+ return typeof value === 'bigint' ? value : BigInt(value ?? 0);
56
+ } catch (err) {
57
+ setDecryptError(err instanceof Error ? err : new Error('Decryption failed'));
58
+ return null;
59
+ } finally {
60
+ setIsDecrypting(false);
61
+ }
62
+ },
63
+ [instance, address, isReady, isConnected, signTypedDataAsync]
64
+ );
65
+
66
+ return { decryptHandle, isDecrypting, error: error || decryptError, isReady };
67
+ }