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.
- package/README.md +31 -0
- package/bin/cli.js +70 -0
- package/package.json +26 -0
- package/template/.env.example +16 -0
- package/template/README.md +91 -0
- package/template/index.html +12 -0
- package/template/package.json +45 -0
- package/template/public/usdc.svg +5 -0
- package/template/public/usdt.svg +1 -0
- package/template/src/App.tsx +6 -0
- package/template/src/components/ui/Badge.tsx +23 -0
- package/template/src/components/ui/Button.tsx +45 -0
- package/template/src/components/ui/Card.tsx +22 -0
- package/template/src/components/ui/Input.tsx +33 -0
- package/template/src/hooks/useConfidentialBalance.ts +85 -0
- package/template/src/hooks/useConfidentialTransfer.ts +50 -0
- package/template/src/hooks/useFhevmDecrypt.ts +67 -0
- package/template/src/hooks/useFhevmEncrypt.ts +63 -0
- package/template/src/hooks/useUnwrapToken.ts +139 -0
- package/template/src/hooks/useWrapToken.ts +93 -0
- package/template/src/index.css +51 -0
- package/template/src/lib/contracts.ts +71 -0
- package/template/src/lib/utils.ts +44 -0
- package/template/src/main.tsx +24 -0
- package/template/src/pages/ShieldUnshieldPage.tsx +355 -0
- package/template/src/providers/FhevmContext.ts +18 -0
- package/template/src/providers/FhevmProvider.tsx +140 -0
- package/template/src/providers/WalletProvider.tsx +49 -0
- package/template/src/providers/useFhevmContext.ts +8 -0
- package/template/tsconfig.app.json +24 -0
- package/template/tsconfig.json +1 -0
- package/template/tsconfig.node.json +20 -0
- 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,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
|
+
}
|