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,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FHEVM Encryption Hook - encrypt amounts for confidential transactions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback, useState } from 'react';
|
|
6
|
+
import { useAccount } from 'wagmi';
|
|
7
|
+
import { toHex } from 'viem';
|
|
8
|
+
import { useFhevm } from '../providers/useFhevmContext';
|
|
9
|
+
|
|
10
|
+
export interface EncryptedAmount {
|
|
11
|
+
handles: `0x${string}`[];
|
|
12
|
+
inputProof: `0x${string}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function uint8ArrayToHex(arr: Uint8Array): `0x${string}` {
|
|
16
|
+
return toHex(arr);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useFhevmEncrypt() {
|
|
20
|
+
const { instance, isReady, error } = useFhevm();
|
|
21
|
+
const { address, isConnected } = useAccount();
|
|
22
|
+
const [isEncrypting, setIsEncrypting] = useState(false);
|
|
23
|
+
const [encryptError, setEncryptError] = useState<Error | null>(null);
|
|
24
|
+
|
|
25
|
+
const MAX_UINT64 = 18446744073709551615n;
|
|
26
|
+
|
|
27
|
+
const encryptAmount = useCallback(
|
|
28
|
+
async (amount: bigint, contractAddress: string): Promise<EncryptedAmount | null> => {
|
|
29
|
+
if (!instance || !address || !isReady || !isConnected) {
|
|
30
|
+
setEncryptError(new Error('FHEVM not ready or wallet not connected'));
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (amount < 0n || amount > MAX_UINT64) {
|
|
34
|
+
setEncryptError(new Error('Amount out of range for uint64 encryption'));
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
setIsEncrypting(true);
|
|
38
|
+
setEncryptError(null);
|
|
39
|
+
try {
|
|
40
|
+
const encryptedInput = instance.createEncryptedInput(contractAddress, address);
|
|
41
|
+
const result = await encryptedInput.add64(amount).encrypt();
|
|
42
|
+
const { handles, inputProof } = result;
|
|
43
|
+
const hexHandles = handles.map((h: unknown) => {
|
|
44
|
+
if (typeof h === 'string' && h.startsWith('0x')) return h as `0x${string}`;
|
|
45
|
+
return uint8ArrayToHex(h as Uint8Array);
|
|
46
|
+
});
|
|
47
|
+
const hexProof =
|
|
48
|
+
typeof inputProof === 'string' && inputProof.startsWith('0x')
|
|
49
|
+
? (inputProof as `0x${string}`)
|
|
50
|
+
: uint8ArrayToHex(inputProof as unknown as Uint8Array);
|
|
51
|
+
return { handles: hexHandles, inputProof: hexProof };
|
|
52
|
+
} catch (err) {
|
|
53
|
+
setEncryptError(err instanceof Error ? err : new Error('Encryption failed'));
|
|
54
|
+
return null;
|
|
55
|
+
} finally {
|
|
56
|
+
setIsEncrypting(false);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
[instance, address, isReady, isConnected]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return { encryptAmount, isEncrypting, error: error || encryptError, isReady };
|
|
63
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ERC-7984 two-step unwrap: unwrap() then finalizeUnwrap() after tx is confirmed.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback, useState } from 'react';
|
|
6
|
+
import { createPublicClient, http, decodeEventLog, type TransactionReceipt } from 'viem';
|
|
7
|
+
import { useAccount, useWriteContract } from 'wagmi';
|
|
8
|
+
import { CONF_TOKEN_ABI, TOKEN_CONFIGS, getConfTokenAddress, CHAIN, CHAIN_RPC_URL, type TokenKey } from '../lib/contracts';
|
|
9
|
+
import { parseAmount } from '../lib/utils';
|
|
10
|
+
import { useFhevm } from '../providers/useFhevmContext';
|
|
11
|
+
import { useFhevmEncrypt } from './useFhevmEncrypt';
|
|
12
|
+
|
|
13
|
+
const publicClient = createPublicClient({
|
|
14
|
+
chain: CHAIN,
|
|
15
|
+
transport: http(CHAIN_RPC_URL, { retryCount: 3, timeout: 10_000 }),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
type PublicDecryptInstance = {
|
|
19
|
+
publicDecrypt: (handles: string[]) => Promise<{ decryptionProof?: string; clearValues?: Record<string, bigint | number> }>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function parseBurntHandleFromReceipt(receipt: TransactionReceipt, tokenAddress: `0x${string}`): `0x${string}` | null {
|
|
23
|
+
const tokenLower = tokenAddress.toLowerCase();
|
|
24
|
+
for (const log of receipt.logs) {
|
|
25
|
+
if (log.address?.toLowerCase() !== tokenLower) continue;
|
|
26
|
+
try {
|
|
27
|
+
const decoded = decodeEventLog({ abi: CONF_TOKEN_ABI, data: log.data, topics: log.topics });
|
|
28
|
+
if (decoded.eventName === 'UnwrapRequested' && decoded.args && 'amount' in decoded.args) {
|
|
29
|
+
const amount = decoded.args.amount;
|
|
30
|
+
if (typeof amount === 'string' && amount.startsWith('0x') && amount.length === 66) return amount as `0x${string}`;
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function getDecryptionProof(
|
|
40
|
+
instance: PublicDecryptInstance,
|
|
41
|
+
handle: string,
|
|
42
|
+
maxAttempts = 8,
|
|
43
|
+
delayMs = 2000
|
|
44
|
+
): Promise<{ cleartext: bigint; decryptionProof: `0x${string}` } | null> {
|
|
45
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
46
|
+
try {
|
|
47
|
+
const result = await instance.publicDecrypt([handle]);
|
|
48
|
+
const proof = result?.decryptionProof ?? (result as { decryptionProof?: string })?.decryptionProof;
|
|
49
|
+
const clearValues = result?.clearValues ?? (result as { clearValues?: Record<string, bigint | number> })?.clearValues;
|
|
50
|
+
if (proof && clearValues != null && handle in clearValues) {
|
|
51
|
+
const val = clearValues[handle];
|
|
52
|
+
const cleartext = typeof val === 'bigint' ? val : BigInt(Number(val));
|
|
53
|
+
return {
|
|
54
|
+
cleartext,
|
|
55
|
+
decryptionProof: proof.startsWith('0x') ? (proof as `0x${string}`) : (`0x${proof}` as `0x${string}`),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
/* gateway not ready */
|
|
60
|
+
}
|
|
61
|
+
if (i < maxAttempts - 1) await new Promise((r) => setTimeout(r, delayMs));
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type UnwrapStep = 'idle' | 'encrypting' | 'confirming' | 'getting_proof' | 'finalizing';
|
|
67
|
+
|
|
68
|
+
export function useUnwrapToken(tokenKey: TokenKey) {
|
|
69
|
+
const { address, isConnected } = useAccount();
|
|
70
|
+
const { instance, isReady: fheReady } = useFhevm();
|
|
71
|
+
const { encryptAmount } = useFhevmEncrypt();
|
|
72
|
+
const { writeContractAsync, isPending } = useWriteContract();
|
|
73
|
+
const confToken = getConfTokenAddress(tokenKey);
|
|
74
|
+
const config = TOKEN_CONFIGS[tokenKey];
|
|
75
|
+
|
|
76
|
+
const [isUnwrapping, setIsUnwrapping] = useState(false);
|
|
77
|
+
const [unwrapStep, setUnwrapStep] = useState<UnwrapStep>('idle');
|
|
78
|
+
|
|
79
|
+
const unwrapStepLabels: Record<UnwrapStep, string> = {
|
|
80
|
+
idle: 'Unwrap/Unshield',
|
|
81
|
+
encrypting: 'Preparing…',
|
|
82
|
+
confirming: 'Confirming unwrap…',
|
|
83
|
+
getting_proof: 'Getting decryption proof…',
|
|
84
|
+
finalizing: `Finalizing (receiving ${config.underlyingSymbol})…`,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const unwrap = useCallback(
|
|
88
|
+
async (amountHuman: string) => {
|
|
89
|
+
if (!address || !isConnected || !fheReady || !instance) return null;
|
|
90
|
+
const amount = parseAmount(amountHuman, config.decimals);
|
|
91
|
+
if (amount <= 0n) return null;
|
|
92
|
+
setIsUnwrapping(true);
|
|
93
|
+
setUnwrapStep('encrypting');
|
|
94
|
+
try {
|
|
95
|
+
const encrypted = await encryptAmount(amount, confToken);
|
|
96
|
+
if (!encrypted?.handles?.[0]) throw new Error('Encryption failed');
|
|
97
|
+
const handle = encrypted.handles[0];
|
|
98
|
+
setUnwrapStep('confirming');
|
|
99
|
+
const hash = await writeContractAsync({
|
|
100
|
+
address: confToken,
|
|
101
|
+
abi: CONF_TOKEN_ABI,
|
|
102
|
+
functionName: 'unwrap',
|
|
103
|
+
args: [address, address, handle, encrypted.inputProof as `0x${string}`],
|
|
104
|
+
});
|
|
105
|
+
if (!hash) throw new Error('Unwrap tx failed');
|
|
106
|
+
const receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
107
|
+
if (receipt.status !== 'success') throw new Error('Unwrap transaction reverted');
|
|
108
|
+
const burntHandle = parseBurntHandleFromReceipt(receipt, confToken) ?? handle;
|
|
109
|
+
setUnwrapStep('getting_proof');
|
|
110
|
+
const proofResult = await getDecryptionProof(instance as unknown as PublicDecryptInstance, burntHandle);
|
|
111
|
+
if (!proofResult) throw new Error('Decryption proof not ready. Try again in a few seconds.');
|
|
112
|
+
setUnwrapStep('finalizing');
|
|
113
|
+
const cleartextU64 = proofResult.cleartext <= 0xffff_ffff_ffff_ffffn ? proofResult.cleartext : 0n;
|
|
114
|
+
const finalizeHash = await writeContractAsync({
|
|
115
|
+
address: confToken,
|
|
116
|
+
abi: CONF_TOKEN_ABI,
|
|
117
|
+
functionName: 'finalizeUnwrap',
|
|
118
|
+
args: [burntHandle, cleartextU64, proofResult.decryptionProof],
|
|
119
|
+
});
|
|
120
|
+
if (finalizeHash) {
|
|
121
|
+
await publicClient.waitForTransactionReceipt({ hash: finalizeHash });
|
|
122
|
+
}
|
|
123
|
+
return finalizeHash ?? hash;
|
|
124
|
+
} finally {
|
|
125
|
+
setIsUnwrapping(false);
|
|
126
|
+
setUnwrapStep('idle');
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
[address, isConnected, fheReady, instance, encryptAmount, writeContractAsync, confToken, config.decimals, config.underlyingSymbol]
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
unwrap,
|
|
134
|
+
isUnwrapping: isUnwrapping || isPending,
|
|
135
|
+
unwrapStep,
|
|
136
|
+
unwrapStepLabel: unwrapStepLabels[unwrapStep],
|
|
137
|
+
fheReady: !!instance && fheReady,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { createPublicClient, http, formatUnits, maxUint256 } from 'viem';
|
|
3
|
+
import { useAccount, useWriteContract } from 'wagmi';
|
|
4
|
+
import { ERC20_ABI, CONF_TOKEN_ABI, TOKEN_CONFIGS, getUnderlyingAddress, getConfTokenAddress, CHAIN, CHAIN_RPC_URL, type TokenKey } from '../lib/contracts';
|
|
5
|
+
import { parseAmount } from '../lib/utils';
|
|
6
|
+
|
|
7
|
+
const client = createPublicClient({
|
|
8
|
+
chain: CHAIN,
|
|
9
|
+
transport: http(CHAIN_RPC_URL, { retryCount: 3, timeout: 10_000 }),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export function useWrapToken(tokenKey: TokenKey) {
|
|
13
|
+
const { address, isConnected } = useAccount();
|
|
14
|
+
const { writeContractAsync, isPending } = useWriteContract();
|
|
15
|
+
const underlying = getUnderlyingAddress(tokenKey);
|
|
16
|
+
const confToken = getConfTokenAddress(tokenKey);
|
|
17
|
+
const config = TOKEN_CONFIGS[tokenKey];
|
|
18
|
+
|
|
19
|
+
const [balance, setBalance] = useState<bigint>(0n);
|
|
20
|
+
const [allowance, setAllowance] = useState<bigint>(0n);
|
|
21
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
22
|
+
const [isWrappingFlow, setIsWrappingFlow] = useState(false);
|
|
23
|
+
|
|
24
|
+
const fetchBalances = useCallback(async () => {
|
|
25
|
+
if (!address) return;
|
|
26
|
+
setIsLoading(true);
|
|
27
|
+
try {
|
|
28
|
+
const [bal, allow] = await Promise.all([
|
|
29
|
+
client.readContract({ address: underlying, abi: ERC20_ABI, functionName: 'balanceOf', args: [address] }),
|
|
30
|
+
client.readContract({ address: underlying, abi: ERC20_ABI, functionName: 'allowance', args: [address, confToken] }),
|
|
31
|
+
]);
|
|
32
|
+
setBalance(bal);
|
|
33
|
+
setAllowance(allow);
|
|
34
|
+
} catch {
|
|
35
|
+
setBalance(0n);
|
|
36
|
+
setAllowance(0n);
|
|
37
|
+
} finally {
|
|
38
|
+
setIsLoading(false);
|
|
39
|
+
}
|
|
40
|
+
}, [address, underlying, confToken]);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (isConnected && address) void fetchBalances();
|
|
44
|
+
}, [isConnected, address, fetchBalances]);
|
|
45
|
+
|
|
46
|
+
const approve = useCallback(async (amountHuman?: string) => {
|
|
47
|
+
if (!address || !isConnected) return null;
|
|
48
|
+
const spendAmount = amountHuman ? parseAmount(amountHuman, config.decimals) : maxUint256;
|
|
49
|
+
const hash = await writeContractAsync({
|
|
50
|
+
address: underlying,
|
|
51
|
+
abi: ERC20_ABI,
|
|
52
|
+
functionName: 'approve',
|
|
53
|
+
args: [confToken, spendAmount],
|
|
54
|
+
});
|
|
55
|
+
await client.waitForTransactionReceipt({ hash });
|
|
56
|
+
await fetchBalances();
|
|
57
|
+
return hash;
|
|
58
|
+
}, [address, isConnected, underlying, confToken, config.decimals, writeContractAsync, fetchBalances]);
|
|
59
|
+
|
|
60
|
+
const wrap = useCallback(async (amountHuman: string) => {
|
|
61
|
+
if (!address || !isConnected) return null;
|
|
62
|
+
const amount = parseAmount(amountHuman, config.decimals);
|
|
63
|
+
setIsWrappingFlow(true);
|
|
64
|
+
try {
|
|
65
|
+
if (allowance < amount) await approve();
|
|
66
|
+
const hash = await writeContractAsync({
|
|
67
|
+
address: confToken,
|
|
68
|
+
abi: CONF_TOKEN_ABI,
|
|
69
|
+
functionName: 'wrap',
|
|
70
|
+
args: [address, amount],
|
|
71
|
+
});
|
|
72
|
+
await client.waitForTransactionReceipt({ hash });
|
|
73
|
+
await fetchBalances();
|
|
74
|
+
return hash;
|
|
75
|
+
} finally {
|
|
76
|
+
setIsWrappingFlow(false);
|
|
77
|
+
}
|
|
78
|
+
}, [address, isConnected, allowance, approve, confToken, config.decimals, writeContractAsync, fetchBalances]);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
balance,
|
|
82
|
+
balanceFormatted: formatUnits(balance, config.decimals),
|
|
83
|
+
allowance,
|
|
84
|
+
needsApproval: (amount: string) => {
|
|
85
|
+
try { return allowance < parseAmount(amount, config.decimals); } catch { return true; }
|
|
86
|
+
},
|
|
87
|
+
isLoading,
|
|
88
|
+
isWriting: isPending || isWrappingFlow,
|
|
89
|
+
approve,
|
|
90
|
+
wrap,
|
|
91
|
+
refetch: fetchBalances,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--color-primary: #FF8C00;
|
|
5
|
+
--color-primary-light: #FFB347;
|
|
6
|
+
--color-primary-dark: #CC7000;
|
|
7
|
+
--color-bg-white: #FFFFFF;
|
|
8
|
+
--color-bg-light: #f8f7f5;
|
|
9
|
+
--color-text-primary: #1d150c;
|
|
10
|
+
--color-text-secondary: #a17745;
|
|
11
|
+
--color-text-tertiary: #c4a882;
|
|
12
|
+
--color-text-on-primary: #FFFFFF;
|
|
13
|
+
--color-success: #10B981;
|
|
14
|
+
--color-warning: #F59E0B;
|
|
15
|
+
--color-error: #EF4444;
|
|
16
|
+
--color-info: #3B82F6;
|
|
17
|
+
--color-border-light: #eaddcd;
|
|
18
|
+
--color-border-input: #d4c4a8;
|
|
19
|
+
--color-border-focus: #FF8C00;
|
|
20
|
+
--color-success-bg: #ECFDF5;
|
|
21
|
+
--color-warning-bg: #FFFBEB;
|
|
22
|
+
--color-error-bg: #FEF2F2;
|
|
23
|
+
--color-info-bg: #EFF6FF;
|
|
24
|
+
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
|
25
|
+
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.07), 0 2px 4px -2px rgb(0 0 0 / 0.05);
|
|
26
|
+
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.08), 0 4px 6px -4px rgb(0 0 0 / 0.04);
|
|
27
|
+
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.08), 0 8px 10px -6px rgb(0 0 0 / 0.04);
|
|
28
|
+
--shadow-glow: 0 0 40px rgb(255 140 0 / 0.12);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Payroll-style gradient card header */
|
|
32
|
+
.gradient-card-header {
|
|
33
|
+
background: linear-gradient(135deg, var(--color-primary) 0%, #f97316 50%, #d97706 100%);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
html { scroll-behavior: smooth; }
|
|
37
|
+
body {
|
|
38
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
39
|
+
color: var(--color-text-primary);
|
|
40
|
+
background-color: var(--color-bg-light);
|
|
41
|
+
-webkit-font-smoothing: antialiased;
|
|
42
|
+
}
|
|
43
|
+
button:disabled { cursor: not-allowed; }
|
|
44
|
+
:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
|
|
45
|
+
|
|
46
|
+
@theme {
|
|
47
|
+
--color-payroll-orange: #FF8C00;
|
|
48
|
+
--color-success: #10B981;
|
|
49
|
+
--color-warning: #F59E0B;
|
|
50
|
+
--color-error: #EF4444;
|
|
51
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import { parseAbi } from 'viem';
|
|
3
|
+
import { mainnet, sepolia } from 'viem/chains';
|
|
4
|
+
|
|
5
|
+
const env = import.meta.env;
|
|
6
|
+
|
|
7
|
+
/** Set to "true" for mainnet, anything else (or unset) = testnet (Sepolia) */
|
|
8
|
+
export const isMainnet = env.VITE_MAINNET === 'true';
|
|
9
|
+
|
|
10
|
+
export const CHAIN = isMainnet ? mainnet : sepolia;
|
|
11
|
+
|
|
12
|
+
/** Mainnet: Valid Token–Wrapper pairs (Ethereum mainnet) */
|
|
13
|
+
const MAINNET_CONTRACTS = {
|
|
14
|
+
USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as `0x${string}`,
|
|
15
|
+
CONF_USDC: '0xe978F22157048E5DB8E5d0797136e86671672B2' as `0x${string}`,
|
|
16
|
+
USDT: '0xdAC17F958D2ee523a2206206994597C130831ec7' as `0x${string}`,
|
|
17
|
+
CONF_USDT: '0xAe0207C757Aa2B4019Ad96edD0092ddc63EF0c50' as `0x${string}`,
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
/** Testnet: from env (Sepolia) */
|
|
21
|
+
const SEPOLIA_CONTRACTS = {
|
|
22
|
+
USDC: (env.VITE_USDC_ADDRESS || '0x0000000000000000000000000000000000000000') as `0x${string}`,
|
|
23
|
+
CONF_USDC: (env.VITE_CONF_USDC_ADDRESS || '0x0000000000000000000000000000000000000000') as `0x${string}`,
|
|
24
|
+
USDT: (env.VITE_USDT_ADDRESS || '0x0000000000000000000000000000000000000000') as `0x${string}`,
|
|
25
|
+
CONF_USDT: (env.VITE_CONF_USDT_ADDRESS || '0x0000000000000000000000000000000000000000') as `0x${string}`,
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export const CONTRACTS = isMainnet ? MAINNET_CONTRACTS : SEPOLIA_CONTRACTS;
|
|
29
|
+
|
|
30
|
+
/** RPC URL for the active chain (used by hooks and WalletProvider) */
|
|
31
|
+
export const CHAIN_RPC_URL = isMainnet
|
|
32
|
+
? (env.VITE_MAINNET_RPC_URL || 'https://eth.llamarpc.com')
|
|
33
|
+
: (env.VITE_SEPOLIA_RPC_URL || 'https://ethereum-sepolia-rpc.publicnode.com');
|
|
34
|
+
|
|
35
|
+
export type TokenKey = 'usdc' | 'usdt';
|
|
36
|
+
|
|
37
|
+
export const TOKEN_CONFIGS: Record<TokenKey, { symbol: string; underlyingSymbol: string; decimals: number }> = {
|
|
38
|
+
usdc: { symbol: 'cUSDC', underlyingSymbol: 'USDC', decimals: 6 },
|
|
39
|
+
usdt: { symbol: 'cUSDT', underlyingSymbol: 'USDT', decimals: 6 },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function getUnderlyingAddress(key: TokenKey): `0x${string}` {
|
|
43
|
+
return key === 'usdc' ? CONTRACTS.USDC : CONTRACTS.USDT;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getConfTokenAddress(key: TokenKey): `0x${string}` {
|
|
47
|
+
return key === 'usdc' ? CONTRACTS.CONF_USDC : CONTRACTS.CONF_USDT;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const ERC20_ABI = parseAbi([
|
|
51
|
+
'function balanceOf(address account) view returns (uint256)',
|
|
52
|
+
'function allowance(address owner, address spender) view returns (uint256)',
|
|
53
|
+
'function approve(address spender, uint256 amount) returns (bool)',
|
|
54
|
+
'function decimals() view returns (uint8)',
|
|
55
|
+
'function symbol() view returns (string)',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/** ERC-7984 confidential token (wrap, unwrap, finalizeUnwrap, confidentialTransfer) */
|
|
59
|
+
export const CONF_TOKEN_ABI = parseAbi([
|
|
60
|
+
'function wrap(address to, uint256 amount)',
|
|
61
|
+
'function unwrap(address from, address to, bytes32 encryptedAmount, bytes inputProof)',
|
|
62
|
+
'function finalizeUnwrap(bytes32 burntAmount, uint64 burntAmountCleartext, bytes decryptionProof)',
|
|
63
|
+
'function confidentialBalanceOf(address account) view returns (bytes32)',
|
|
64
|
+
'function confidentialTransfer(address to, bytes32 encryptedAmount, bytes inputProof)',
|
|
65
|
+
'function setOperator(address operator, uint48 until)',
|
|
66
|
+
'function isOperator(address account, address operator) view returns (bool)',
|
|
67
|
+
'function underlying() view returns (address)',
|
|
68
|
+
'function rate() view returns (uint256)',
|
|
69
|
+
'event UnwrapRequested(address indexed receiver, bytes32 amount)',
|
|
70
|
+
'event UnwrapFinalized(address indexed receiver, bytes32 encryptedAmount, uint64 cleartextAmount)',
|
|
71
|
+
]);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatAddress(address: string, chars = 4): string {
|
|
9
|
+
if (!address) return '';
|
|
10
|
+
return `${address.slice(0, chars + 2)}...${address.slice(-chars)}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatAmount(amount: bigint, decimals = 6): string {
|
|
14
|
+
const divisor = BigInt(10 ** decimals);
|
|
15
|
+
const integerPart = amount / divisor;
|
|
16
|
+
const fractionalPart = amount % divisor;
|
|
17
|
+
const fractionalStr = fractionalPart.toString().padStart(decimals, '0');
|
|
18
|
+
const trimmedFractional = fractionalStr.slice(0, 2);
|
|
19
|
+
return `${integerPart.toLocaleString()}.${trimmedFractional}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseAmount(amount: string, decimals = 6): bigint {
|
|
23
|
+
const [integer, fraction = ''] = amount.split('.');
|
|
24
|
+
const paddedFraction = fraction.padEnd(decimals, '0').slice(0, decimals);
|
|
25
|
+
return BigInt(integer + paddedFraction);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getUserFriendlyErrorMessage(err: unknown, fallback = 'Something went wrong. Please try again.'): string {
|
|
29
|
+
const msg = err instanceof Error ? err.message : String(err ?? '');
|
|
30
|
+
const lower = msg.toLowerCase();
|
|
31
|
+
if (
|
|
32
|
+
lower.includes('user rejected') ||
|
|
33
|
+
lower.includes('user denied') ||
|
|
34
|
+
lower.includes('rejected the request') ||
|
|
35
|
+
lower.includes('denied transaction') ||
|
|
36
|
+
lower.includes('tx signature: user denied') ||
|
|
37
|
+
lower.includes('request rejected')
|
|
38
|
+
) {
|
|
39
|
+
return "You cancelled the transaction. You can try again when you're ready.";
|
|
40
|
+
}
|
|
41
|
+
if (msg && msg.length < 120) return msg;
|
|
42
|
+
if (msg) return msg.slice(0, 100) + '…';
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React, { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { Toaster } from 'react-hot-toast';
|
|
4
|
+
import { WalletProvider } from './providers/WalletProvider';
|
|
5
|
+
import { FhevmProvider } from './providers/FhevmProvider';
|
|
6
|
+
import './index.css';
|
|
7
|
+
import App from './App';
|
|
8
|
+
|
|
9
|
+
createRoot(document.getElementById('root') as HTMLElement).render(
|
|
10
|
+
<StrictMode>
|
|
11
|
+
<WalletProvider>
|
|
12
|
+
<FhevmProvider>
|
|
13
|
+
<App />
|
|
14
|
+
<Toaster
|
|
15
|
+
position="top-right"
|
|
16
|
+
toastOptions={{
|
|
17
|
+
duration: 4000,
|
|
18
|
+
style: { background: '#FF8C00', color: '#fff' },
|
|
19
|
+
}}
|
|
20
|
+
/>
|
|
21
|
+
</FhevmProvider>
|
|
22
|
+
</WalletProvider>
|
|
23
|
+
</StrictMode>
|
|
24
|
+
);
|