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
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { ConnectButton } from '@rainbow-me/rainbowkit';
|
|
3
|
+
import { useAccount } from 'wagmi';
|
|
4
|
+
import toast from 'react-hot-toast';
|
|
5
|
+
import { Shield, ShieldOff, Send, Wallet, Lock, Coins } from 'lucide-react';
|
|
6
|
+
import { Card } from '../components/ui/Card';
|
|
7
|
+
import { Button } from '../components/ui/Button';
|
|
8
|
+
import { Input } from '../components/ui/Input';
|
|
9
|
+
import { TOKEN_CONFIGS, getConfTokenAddress, type TokenKey } from '../lib/contracts';
|
|
10
|
+
import { formatAmount, getUserFriendlyErrorMessage } from '../lib/utils';
|
|
11
|
+
import { useWrapToken } from '../hooks/useWrapToken';
|
|
12
|
+
import { useUnwrapToken } from '../hooks/useUnwrapToken';
|
|
13
|
+
import { useConfidentialBalance } from '../hooks/useConfidentialBalance';
|
|
14
|
+
import { useConfidentialTransfer } from '../hooks/useConfidentialTransfer';
|
|
15
|
+
|
|
16
|
+
function TokenSection({
|
|
17
|
+
tokenKey,
|
|
18
|
+
title,
|
|
19
|
+
description,
|
|
20
|
+
}: {
|
|
21
|
+
tokenKey: TokenKey;
|
|
22
|
+
title: string;
|
|
23
|
+
description: string;
|
|
24
|
+
}) {
|
|
25
|
+
const config = TOKEN_CONFIGS[tokenKey];
|
|
26
|
+
const { balance, balanceFormatted, wrap, isWriting: isWrapping, needsApproval, refetch: refetchUnderlying } = useWrapToken(tokenKey);
|
|
27
|
+
const { unwrap: doUnwrap, isUnwrapping, unwrapStepLabel, fheReady: fheReadyUnwrap } = useUnwrapToken(tokenKey);
|
|
28
|
+
const confBalance = useConfidentialBalance(getConfTokenAddress(tokenKey));
|
|
29
|
+
const [wrapAmount, setWrapAmount] = useState('');
|
|
30
|
+
const [unwrapAmount, setUnwrapAmount] = useState('');
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="relative overflow-hidden rounded-3xl bg-white border border-[var(--color-border-light)] shadow-[var(--shadow-md)] hover:shadow-[var(--shadow-lg)] transition-shadow duration-200">
|
|
34
|
+
{/* Orange gradient header strip */}
|
|
35
|
+
<div className="gradient-card-header relative overflow-hidden px-6 py-4">
|
|
36
|
+
<div className="absolute -top-1/2 -right-1/4 w-64 h-64 bg-white/10 rounded-full" />
|
|
37
|
+
<div className="absolute -bottom-1/2 -left-1/4 w-48 h-48 bg-white/10 rounded-full" />
|
|
38
|
+
<div className="relative flex items-center gap-3">
|
|
39
|
+
<img src={tokenKey === 'usdc' ? '/usdc.svg' : '/usdt.svg'} alt="" className="w-14 h-14 object-contain shrink-0" />
|
|
40
|
+
<div>
|
|
41
|
+
<h2 className="text-lg font-bold text-white">{title}</h2>
|
|
42
|
+
<p className="text-white/80 text-xs mt-0.5">{description}</p>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
{/* Content with orange left accent */}
|
|
47
|
+
<div className="border-l-4 border-l-[var(--color-primary)] p-6 flex flex-col gap-6">
|
|
48
|
+
<div className="grid grid-cols-2 gap-3">
|
|
49
|
+
<div className="p-4 rounded-2xl bg-gradient-to-br from-amber-50 to-orange-50 border border-amber-100/80">
|
|
50
|
+
<div className="text-[10px] font-semibold text-amber-700 uppercase tracking-wide">{config.underlyingSymbol}</div>
|
|
51
|
+
<div className="text-xl font-bold text-amber-800 mt-0.5">{balanceFormatted}</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div className="p-4 rounded-2xl bg-[var(--color-bg-light)] border border-[var(--color-border-light)]">
|
|
54
|
+
<div className="text-[10px] font-semibold text-[var(--color-text-secondary)] uppercase tracking-wide">{config.symbol}</div>
|
|
55
|
+
<div className="text-xl font-bold text-[var(--color-text-primary)] mt-0.5">
|
|
56
|
+
{confBalance.decryptedBalance !== null
|
|
57
|
+
? formatAmount(confBalance.decryptedBalance, config.decimals)
|
|
58
|
+
: confBalance.hasBalance
|
|
59
|
+
? 'Encrypted'
|
|
60
|
+
: '0.00'}
|
|
61
|
+
</div>
|
|
62
|
+
{confBalance.hasBalance && !confBalance.isDecrypted && confBalance.fheReady && (
|
|
63
|
+
<Button size="sm" variant="ghost" className="mt-2 shadow-md border border-[var(--color-border-light)] bg-white hover:bg-gray-50" onClick={() => confBalance.decrypt()} loading={confBalance.isDecrypting}>
|
|
64
|
+
Decrypt
|
|
65
|
+
</Button>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div>
|
|
70
|
+
<div className="flex items-center gap-2 mb-2">
|
|
71
|
+
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-lg bg-emerald-50 text-emerald-700 text-xs font-semibold border border-emerald-100">
|
|
72
|
+
Wrap {config.underlyingSymbol} → {config.symbol}
|
|
73
|
+
</span>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="flex gap-2 flex-wrap items-center">
|
|
76
|
+
<div className="relative flex-1 min-w-[120px]">
|
|
77
|
+
<Input
|
|
78
|
+
type="text"
|
|
79
|
+
inputMode="decimal"
|
|
80
|
+
placeholder="0.00"
|
|
81
|
+
value={wrapAmount}
|
|
82
|
+
onChange={(e) => setWrapAmount(e.target.value)}
|
|
83
|
+
/>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 text-xs font-bold text-[var(--color-primary)] hover:underline"
|
|
87
|
+
onClick={() => setWrapAmount(balanceFormatted)}
|
|
88
|
+
>
|
|
89
|
+
MAX
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
<Button
|
|
93
|
+
size="sm"
|
|
94
|
+
onClick={async () => {
|
|
95
|
+
if (!wrapAmount || Number(wrapAmount) <= 0) return;
|
|
96
|
+
try {
|
|
97
|
+
const hash = await wrap(wrapAmount);
|
|
98
|
+
if (hash) {
|
|
99
|
+
setWrapAmount('');
|
|
100
|
+
await Promise.all([refetchUnderlying(), confBalance.refetch()]);
|
|
101
|
+
toast.success(`Wrapped! ${config.symbol} received.`);
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
toast.error(getUserFriendlyErrorMessage(err, 'Wrap failed'));
|
|
105
|
+
}
|
|
106
|
+
}}
|
|
107
|
+
disabled={isWrapping || !wrapAmount || Number(wrapAmount) <= 0 || balance === 0n}
|
|
108
|
+
loading={isWrapping}
|
|
109
|
+
>
|
|
110
|
+
<Shield className="h-4 w-4" /> {wrapAmount && needsApproval(wrapAmount) ? 'Approve & Wrap/Shield' : 'Wrap/Shield'}
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="pt-2 border-t border-[var(--color-border-light)]">
|
|
115
|
+
<div className="flex items-center gap-2 mb-2">
|
|
116
|
+
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-lg bg-orange-50 text-orange-700 text-xs font-semibold border border-orange-100">
|
|
117
|
+
Unwrap {config.symbol} → {config.underlyingSymbol}
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
<div className="flex gap-2 flex-wrap items-center">
|
|
121
|
+
<div className="relative flex-1 min-w-[120px]">
|
|
122
|
+
<Input
|
|
123
|
+
type="text"
|
|
124
|
+
inputMode="decimal"
|
|
125
|
+
placeholder="0.00"
|
|
126
|
+
value={unwrapAmount}
|
|
127
|
+
onChange={(e) => setUnwrapAmount(e.target.value)}
|
|
128
|
+
/>
|
|
129
|
+
{confBalance.decryptedBalance !== null && confBalance.decryptedBalance > 0n && (
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 text-xs font-bold text-[var(--color-primary)] hover:underline"
|
|
133
|
+
onClick={() => setUnwrapAmount(formatAmount(confBalance.decryptedBalance!, config.decimals))}
|
|
134
|
+
>
|
|
135
|
+
MAX
|
|
136
|
+
</button>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
<Button
|
|
140
|
+
size="sm"
|
|
141
|
+
variant="secondary"
|
|
142
|
+
onClick={async () => {
|
|
143
|
+
if (!unwrapAmount || Number(unwrapAmount) <= 0) return;
|
|
144
|
+
try {
|
|
145
|
+
const hash = await doUnwrap(unwrapAmount);
|
|
146
|
+
if (hash) {
|
|
147
|
+
setUnwrapAmount('');
|
|
148
|
+
await Promise.all([refetchUnderlying(), confBalance.refetch()]);
|
|
149
|
+
toast.success(`Unwrapped! ${config.underlyingSymbol} received.`);
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
toast.error(getUserFriendlyErrorMessage(err, 'Unwrap failed'));
|
|
153
|
+
}
|
|
154
|
+
}}
|
|
155
|
+
disabled={!fheReadyUnwrap || isUnwrapping || !unwrapAmount || Number(unwrapAmount) <= 0}
|
|
156
|
+
loading={isUnwrapping}
|
|
157
|
+
>
|
|
158
|
+
<ShieldOff className="h-4 w-4" /> {unwrapStepLabel}
|
|
159
|
+
</Button>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function TransferSection({ tokenKey }: { tokenKey: TokenKey }) {
|
|
168
|
+
const config = TOKEN_CONFIGS[tokenKey];
|
|
169
|
+
const { transfer, isTransferring, fheReady } = useConfidentialTransfer(tokenKey);
|
|
170
|
+
const confBalance = useConfidentialBalance(getConfTokenAddress(tokenKey));
|
|
171
|
+
const [toAddress, setToAddress] = useState('');
|
|
172
|
+
const [amount, setAmount] = useState('');
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div className="relative overflow-hidden rounded-3xl bg-white border border-[var(--color-border-light)] shadow-[var(--shadow-md)] hover:shadow-[var(--shadow-lg)] transition-shadow duration-200">
|
|
176
|
+
<div className="gradient-card-header relative overflow-hidden px-6 py-4">
|
|
177
|
+
<div className="absolute -top-1/2 -right-1/4 w-64 h-64 bg-white/10 rounded-full" />
|
|
178
|
+
<div className="absolute -bottom-1/2 -left-1/4 w-48 h-48 bg-white/10 rounded-full" />
|
|
179
|
+
<div className="relative flex items-center gap-3">
|
|
180
|
+
<div className="w-11 h-11 rounded-xl bg-white/20 backdrop-blur-sm flex items-center justify-center">
|
|
181
|
+
<Send className="w-6 h-6 text-white" />
|
|
182
|
+
</div>
|
|
183
|
+
<div>
|
|
184
|
+
<h2 className="text-lg font-bold text-white">Send {config.symbol}</h2>
|
|
185
|
+
<p className="text-white/80 text-xs mt-0.5">
|
|
186
|
+
Transfer confidential {config.symbol}. Amount is encrypted on-chain.
|
|
187
|
+
</p>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
<div className="border-l-4 border-l-[var(--color-primary)] p-6 flex flex-col gap-4">
|
|
192
|
+
<Input
|
|
193
|
+
label="Recipient address"
|
|
194
|
+
placeholder="0x..."
|
|
195
|
+
value={toAddress}
|
|
196
|
+
onChange={(e) => setToAddress(e.target.value)}
|
|
197
|
+
/>
|
|
198
|
+
<div className="flex gap-2 items-center flex-wrap">
|
|
199
|
+
<div className="relative flex-1 min-w-[100px]">
|
|
200
|
+
<Input
|
|
201
|
+
type="text"
|
|
202
|
+
inputMode="decimal"
|
|
203
|
+
placeholder="0.00"
|
|
204
|
+
value={amount}
|
|
205
|
+
onChange={(e) => setAmount(e.target.value)}
|
|
206
|
+
/>
|
|
207
|
+
{confBalance.decryptedBalance !== null && confBalance.decryptedBalance > 0n && (
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 text-xs font-bold text-[var(--color-primary)] hover:underline"
|
|
211
|
+
onClick={() => setAmount(formatAmount(confBalance.decryptedBalance!, config.decimals))}
|
|
212
|
+
>
|
|
213
|
+
MAX
|
|
214
|
+
</button>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
<Button
|
|
218
|
+
size="sm"
|
|
219
|
+
onClick={async () => {
|
|
220
|
+
if (!toAddress.trim() || !amount || Number(amount) <= 0) return;
|
|
221
|
+
const addr = toAddress.trim() as `0x${string}`;
|
|
222
|
+
if (!addr.startsWith('0x') || addr.length !== 42) {
|
|
223
|
+
toast.error('Invalid recipient address');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const hash = await transfer(addr, amount);
|
|
228
|
+
if (hash) {
|
|
229
|
+
toast.success('Transfer sent.');
|
|
230
|
+
setAmount('');
|
|
231
|
+
setToAddress('');
|
|
232
|
+
confBalance.refetch();
|
|
233
|
+
}
|
|
234
|
+
} catch (err) {
|
|
235
|
+
toast.error(getUserFriendlyErrorMessage(err, 'Transfer failed'));
|
|
236
|
+
}
|
|
237
|
+
}}
|
|
238
|
+
disabled={!fheReady || isTransferring || !toAddress.trim() || !amount || Number(amount) <= 0}
|
|
239
|
+
loading={isTransferring}
|
|
240
|
+
>
|
|
241
|
+
<Send className="h-4 w-4" /> Transfer
|
|
242
|
+
</Button>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function ShieldUnshieldPage() {
|
|
250
|
+
const { isConnected } = useAccount();
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<div className="min-h-screen bg-[var(--color-bg-light)]">
|
|
254
|
+
<header className="border-b border-[var(--color-border-light)] bg-white/80 backdrop-blur sticky top-0 z-10">
|
|
255
|
+
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
|
|
256
|
+
<h1 className="text-xl font-bold text-[var(--color-text-primary)]">Shield / Unshield dApp</h1>
|
|
257
|
+
<ConnectButton />
|
|
258
|
+
</div>
|
|
259
|
+
</header>
|
|
260
|
+
|
|
261
|
+
<main className="max-w-4xl mx-auto px-4 py-8">
|
|
262
|
+
{!isConnected ? (
|
|
263
|
+
<div className="flex flex-col items-center">
|
|
264
|
+
{/* Hero card */}
|
|
265
|
+
<Card variant="elevated" padding="lg" className="w-full max-w-xl text-center overflow-hidden relative">
|
|
266
|
+
<div className="absolute inset-0 bg-gradient-to-br from-[var(--color-primary)]/5 via-transparent to-[var(--color-primary)]/10 pointer-events-none rounded-xl" />
|
|
267
|
+
<div className="relative">
|
|
268
|
+
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-[var(--color-primary)]/15 text-[var(--color-primary)] mb-6">
|
|
269
|
+
<Shield className="w-8 h-8" strokeWidth={2} />
|
|
270
|
+
</div>
|
|
271
|
+
<h2 className="text-2xl md:text-3xl font-bold text-[var(--color-text-primary)] tracking-tight mb-2">
|
|
272
|
+
Shield your tokens. Send privately.
|
|
273
|
+
</h2>
|
|
274
|
+
<p className="text-[var(--color-text-secondary)] text-base mb-8 max-w-md mx-auto">
|
|
275
|
+
Wrap USDC or USDT into confidential tokens, unwrap back anytime, and transfer without revealing amounts. Connect your wallet on Sepolia to get started.
|
|
276
|
+
</p>
|
|
277
|
+
<div className="flex flex-wrap justify-center gap-3 mb-8">
|
|
278
|
+
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-emerald-50 text-emerald-800 text-xs font-medium border border-emerald-100">
|
|
279
|
+
<Coins className="w-3.5 h-3.5" /> Wrap & unwrap
|
|
280
|
+
</span>
|
|
281
|
+
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[var(--color-info-bg)] text-[var(--color-info)] text-xs font-medium border border-blue-100">
|
|
282
|
+
<Lock className="w-3.5 h-3.5" /> Private transfers
|
|
283
|
+
</span>
|
|
284
|
+
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-amber-50 text-amber-800 text-xs font-medium border border-amber-100">
|
|
285
|
+
Sepolia
|
|
286
|
+
</span>
|
|
287
|
+
</div>
|
|
288
|
+
<div className="flex flex-col items-center gap-3">
|
|
289
|
+
<ConnectButton />
|
|
290
|
+
<p className="text-xs text-[var(--color-text-tertiary)]">
|
|
291
|
+
You’ll need a Sepolia wallet and some test USDC/USDT.
|
|
292
|
+
</p>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
</Card>
|
|
296
|
+
{/* Steps */}
|
|
297
|
+
<div className="mt-10 grid grid-cols-1 sm:grid-cols-3 gap-4 w-full max-w-2xl">
|
|
298
|
+
<div className="flex items-start gap-3 p-4 rounded-xl bg-white/80 border border-[var(--color-border-light)]">
|
|
299
|
+
<div className="flex-shrink-0 w-10 h-10 rounded-xl bg-[var(--color-primary)]/10 flex items-center justify-center">
|
|
300
|
+
<Wallet className="w-5 h-5 text-[var(--color-primary)]" />
|
|
301
|
+
</div>
|
|
302
|
+
<div>
|
|
303
|
+
<div className="font-semibold text-sm text-[var(--color-text-primary)]">1. Connect</div>
|
|
304
|
+
<div className="text-xs text-[var(--color-text-secondary)] mt-0.5">Connect your wallet on Sepolia.</div>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
<div className="flex items-start gap-3 p-4 rounded-xl bg-white/80 border border-[var(--color-border-light)]">
|
|
308
|
+
<div className="flex-shrink-0 w-10 h-10 rounded-xl bg-emerald-100 flex items-center justify-center">
|
|
309
|
+
<Shield className="w-5 h-5 text-emerald-700" />
|
|
310
|
+
</div>
|
|
311
|
+
<div>
|
|
312
|
+
<div className="font-semibold text-sm text-[var(--color-text-primary)]">2. Wrap or unwrap</div>
|
|
313
|
+
<div className="text-xs text-[var(--color-text-secondary)] mt-0.5">Convert between public and confidential tokens.</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
<div className="flex items-start gap-3 p-4 rounded-xl bg-white/80 border border-[var(--color-border-light)]">
|
|
317
|
+
<div className="flex-shrink-0 w-10 h-10 rounded-xl bg-[var(--color-info-bg)] flex items-center justify-center">
|
|
318
|
+
<Send className="w-5 h-5 text-[var(--color-info)]" />
|
|
319
|
+
</div>
|
|
320
|
+
<div>
|
|
321
|
+
<div className="font-semibold text-sm text-[var(--color-text-primary)]">3. Transfer</div>
|
|
322
|
+
<div className="text-xs text-[var(--color-text-secondary)] mt-0.5">Send confidential tokens to any address.</div>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
) : (
|
|
328
|
+
<div className="space-y-8">
|
|
329
|
+
<div className="rounded-2xl bg-gradient-to-r from-amber-50/80 via-orange-50/50 to-amber-50/80 border border-amber-100/60 px-5 py-3">
|
|
330
|
+
<p className="text-sm text-[var(--color-text-secondary)]">
|
|
331
|
+
Wrap USDC or USDT into confidential tokens (cUSDC, cUSDT), unwrap back to plain tokens, or send confidential tokens to another address.
|
|
332
|
+
</p>
|
|
333
|
+
</div>
|
|
334
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
335
|
+
<TokenSection
|
|
336
|
+
tokenKey="usdc"
|
|
337
|
+
title="USDC / cUSDC"
|
|
338
|
+
description="Wrap USDC to cUSDC or unwrap cUSDC back to USDC. Unwrap uses Zama gateway (two-step)."
|
|
339
|
+
/>
|
|
340
|
+
<TokenSection
|
|
341
|
+
tokenKey="usdt"
|
|
342
|
+
title="USDT / cUSDT"
|
|
343
|
+
description="Wrap USDT to cUSDT or unwrap cUSDT back to USDT. Unwrap uses Zama gateway (two-step)."
|
|
344
|
+
/>
|
|
345
|
+
</div>
|
|
346
|
+
<div className="grid gap-6 md:grid-cols-2">
|
|
347
|
+
<TransferSection tokenKey="usdc" />
|
|
348
|
+
<TransferSection tokenKey="usdt" />
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
</main>
|
|
353
|
+
</div>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
import type { FhevmInstance } from './FhevmProvider';
|
|
3
|
+
|
|
4
|
+
export interface FhevmContextType {
|
|
5
|
+
instance: FhevmInstance | null;
|
|
6
|
+
isReady: boolean;
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
error: Error | null;
|
|
9
|
+
initialize: () => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const FhevmContext = createContext<FhevmContextType>({
|
|
13
|
+
instance: null,
|
|
14
|
+
isReady: false,
|
|
15
|
+
isLoading: false,
|
|
16
|
+
error: null,
|
|
17
|
+
initialize: async () => {},
|
|
18
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FHEVM Provider - Zama RelayerSDK v0.3.0-8
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
|
6
|
+
import { useAccount, useChainId } from 'wagmi';
|
|
7
|
+
import { isMainnet } from '../lib/contracts';
|
|
8
|
+
import { FhevmContext } from './FhevmContext';
|
|
9
|
+
|
|
10
|
+
declare global {
|
|
11
|
+
interface Window {
|
|
12
|
+
RelayerSDK?: RelayerSDKType;
|
|
13
|
+
relayerSDK?: RelayerSDKType;
|
|
14
|
+
ethereum?: any;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface RelayerSDKType {
|
|
19
|
+
initSDK: () => Promise<void>;
|
|
20
|
+
createInstance: (config: FhevmInstanceConfig) => Promise<FhevmInstance>;
|
|
21
|
+
generateKeypair: () => { publicKey: string; privateKey: string };
|
|
22
|
+
SepoliaConfig: FhevmConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface FhevmConfig {
|
|
26
|
+
aclContractAddress: string;
|
|
27
|
+
kmsContractAddress: string;
|
|
28
|
+
inputVerifierContractAddress: string;
|
|
29
|
+
verifyingContractAddressDecryption: string;
|
|
30
|
+
verifyingContractAddressInputVerification: string;
|
|
31
|
+
chainId: number;
|
|
32
|
+
gatewayChainId: number;
|
|
33
|
+
network: string | unknown;
|
|
34
|
+
relayerUrl: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type FhevmInstanceConfig = Pick<
|
|
38
|
+
FhevmConfig,
|
|
39
|
+
| 'verifyingContractAddressDecryption'
|
|
40
|
+
| 'verifyingContractAddressInputVerification'
|
|
41
|
+
| 'kmsContractAddress'
|
|
42
|
+
| 'aclContractAddress'
|
|
43
|
+
| 'gatewayChainId'
|
|
44
|
+
> & { network: unknown; publicKey?: Uint8Array; auth?: unknown };
|
|
45
|
+
|
|
46
|
+
export interface FhevmInstance {
|
|
47
|
+
createEncryptedInput: (contractAddress: string, userAddress: string) => { add64: (n: bigint) => { encrypt: () => Promise<{ handles: unknown[]; inputProof: unknown }> } };
|
|
48
|
+
generateKeypair: () => { publicKey: string; privateKey: string };
|
|
49
|
+
createEIP712: (publicKey: string, contractAddresses: string[], startTimestamp: string, durationDays: string) => unknown;
|
|
50
|
+
userDecrypt: (...args: unknown[]) => Promise<Record<string, bigint | string>>;
|
|
51
|
+
publicDecrypt?: (handles: string[]) => Promise<{ decryptionProof?: string; clearValues?: Record<string, bigint | number> }>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const RELAYER_SDK_CDN = 'https://cdn.zama.org/relayer-sdk-js/0.3.0-8/relayer-sdk-js.umd.cjs';
|
|
55
|
+
const MAINNET_CHAIN_ID = 1;
|
|
56
|
+
const SEPOLIA_CHAIN_ID = 11155111;
|
|
57
|
+
|
|
58
|
+
const EXPECTED_CHAIN_ID = isMainnet ? MAINNET_CHAIN_ID : SEPOLIA_CHAIN_ID;
|
|
59
|
+
const EXPECTED_NETWORK_NAME = isMainnet ? 'Mainnet' : 'Sepolia';
|
|
60
|
+
|
|
61
|
+
interface FhevmProviderProps {
|
|
62
|
+
children: React.ReactNode;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function FhevmProvider({ children }: FhevmProviderProps) {
|
|
66
|
+
const { address, isConnected } = useAccount();
|
|
67
|
+
const chainId = useChainId();
|
|
68
|
+
const [instance, setInstance] = useState<FhevmInstance | null>(null);
|
|
69
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
70
|
+
const [error, setError] = useState<Error | null>(null);
|
|
71
|
+
|
|
72
|
+
const getSDK = useCallback((): RelayerSDKType | null => {
|
|
73
|
+
return window.relayerSDK || window.RelayerSDK || null;
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const loadSDK = useCallback(async (): Promise<void> => {
|
|
77
|
+
if (getSDK()) return;
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const script = document.createElement('script');
|
|
80
|
+
script.src = RELAYER_SDK_CDN;
|
|
81
|
+
script.async = true;
|
|
82
|
+
script.type = 'text/javascript';
|
|
83
|
+
script.onload = () => resolve();
|
|
84
|
+
script.onerror = () => reject(new Error('Failed to load FHEVM SDK from CDN.'));
|
|
85
|
+
document.head.appendChild(script);
|
|
86
|
+
});
|
|
87
|
+
}, [getSDK]);
|
|
88
|
+
|
|
89
|
+
const initialize = useCallback(async () => {
|
|
90
|
+
if (!isConnected || !address || !window.ethereum) return;
|
|
91
|
+
if (chainId !== EXPECTED_CHAIN_ID) {
|
|
92
|
+
setError(new Error(`Please connect to ${EXPECTED_NETWORK_NAME}`));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (instance) return;
|
|
96
|
+
if (isMainnet) {
|
|
97
|
+
setError(new Error('Mainnet relayer requires an API key. Contact the team for access.'));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
setIsLoading(true);
|
|
101
|
+
setError(null);
|
|
102
|
+
try {
|
|
103
|
+
await loadSDK();
|
|
104
|
+
const sdk = getSDK();
|
|
105
|
+
if (!sdk) throw new Error('FHEVM SDK not available after loading.');
|
|
106
|
+
await sdk.initSDK();
|
|
107
|
+
const fhevmInstance = await sdk.createInstance({ ...sdk.SepoliaConfig, network: window.ethereum });
|
|
108
|
+
setInstance(fhevmInstance as FhevmInstance);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
setError(err instanceof Error ? err : new Error('Failed to initialize FHEVM'));
|
|
111
|
+
} finally {
|
|
112
|
+
setIsLoading(false);
|
|
113
|
+
}
|
|
114
|
+
}, [isConnected, address, chainId, instance, loadSDK, getSDK]);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (isConnected && address && chainId === EXPECTED_CHAIN_ID && !isMainnet && !instance && !isLoading) {
|
|
118
|
+
void initialize();
|
|
119
|
+
}
|
|
120
|
+
}, [isConnected, address, chainId, instance, isLoading, initialize]);
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!isConnected || chainId !== EXPECTED_CHAIN_ID) {
|
|
124
|
+
setInstance(null);
|
|
125
|
+
setError(new Error(`Please connect to ${EXPECTED_NETWORK_NAME}`));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (isMainnet) {
|
|
129
|
+
setInstance(null);
|
|
130
|
+
setError(new Error('Mainnet relayer requires an API key. Contact the team for access.'));
|
|
131
|
+
}
|
|
132
|
+
}, [isConnected, chainId]);
|
|
133
|
+
|
|
134
|
+
const value = useMemo(
|
|
135
|
+
() => ({ instance, isReady: !!instance && isConnected, isLoading, error, initialize }),
|
|
136
|
+
[instance, isConnected, isLoading, error, initialize]
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return <FhevmContext.Provider value={value}>{children}</FhevmContext.Provider>;
|
|
140
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { getDefaultConfig, RainbowKitProvider, lightTheme } from '@rainbow-me/rainbowkit';
|
|
5
|
+
import { WagmiProvider, http } from 'wagmi';
|
|
6
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
7
|
+
import { CHAIN, CHAIN_RPC_URL } from '../lib/contracts';
|
|
8
|
+
import '@rainbow-me/rainbowkit/styles.css';
|
|
9
|
+
|
|
10
|
+
const queryClient = new QueryClient();
|
|
11
|
+
|
|
12
|
+
const WALLETCONNECT_PROJECT_ID =
|
|
13
|
+
import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? 'cf5d11022a642e528f427d4210e992db';
|
|
14
|
+
|
|
15
|
+
const config = getDefaultConfig({
|
|
16
|
+
appName: 'Shield / Unshield dApp',
|
|
17
|
+
projectId: WALLETCONNECT_PROJECT_ID,
|
|
18
|
+
chains: [CHAIN],
|
|
19
|
+
ssr: false,
|
|
20
|
+
transports: {
|
|
21
|
+
[CHAIN.id]: http(CHAIN_RPC_URL, { batch: true, retryCount: 3, retryDelay: 1000 }),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
interface WalletProviderProps {
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function WalletProvider({ children }: WalletProviderProps) {
|
|
30
|
+
return (
|
|
31
|
+
<WagmiProvider config={config}>
|
|
32
|
+
<QueryClientProvider client={queryClient}>
|
|
33
|
+
<RainbowKitProvider
|
|
34
|
+
modalSize="compact"
|
|
35
|
+
theme={lightTheme({
|
|
36
|
+
accentColor: '#FF8C00',
|
|
37
|
+
accentColorForeground: 'white',
|
|
38
|
+
})}
|
|
39
|
+
appInfo={{
|
|
40
|
+
appName: 'Shield / Unshield dApp',
|
|
41
|
+
learnMoreUrl: 'https://zama.ai',
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
</RainbowKitProvider>
|
|
46
|
+
</QueryClientProvider>
|
|
47
|
+
</WagmiProvider>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"verbatimModuleSyntax": false,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"jsx": "react-jsx",
|
|
16
|
+
"strict": true,
|
|
17
|
+
"noUnusedLocals": false,
|
|
18
|
+
"noUnusedParameters": false,
|
|
19
|
+
"noFallthroughCasesInSwitch": true,
|
|
20
|
+
"baseUrl": ".",
|
|
21
|
+
"paths": { "@/*": ["./src/*"] }
|
|
22
|
+
},
|
|
23
|
+
"include": ["src"]
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"files":[],"references":[{"path":"./tsconfig.app.json"},{"path":"./tsconfig.node.json"}]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"moduleDetection": "force",
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["vite.config.ts"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react(), tailwindcss()],
|
|
8
|
+
optimizeDeps: {
|
|
9
|
+
include: ['es-errors', 'call-bound', 'is-typed-array'],
|
|
10
|
+
exclude: ['lucide-react'],
|
|
11
|
+
},
|
|
12
|
+
resolve: {
|
|
13
|
+
alias: {
|
|
14
|
+
'@': path.resolve(__dirname, './src'),
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
server: {
|
|
18
|
+
headers: {
|
|
19
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
20
|
+
'Cross-Origin-Embedder-Policy': 'require-corp',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
preview: {
|
|
24
|
+
headers: {
|
|
25
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
26
|
+
'Cross-Origin-Embedder-Policy': 'require-corp',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
})
|