synor/apps/desktop-wallet/src/pages/CreateWallet.tsx
Gulshan Yadav 6b5a232a5e feat: Desktop wallet, gas estimator UI, and 30-day monitoring stack
Security (Desktop Wallet):
- Implement BIP39 mnemonic generation with cryptographic RNG
- Add Argon2id password-based key derivation (64MB, 3 iterations)
- Add ChaCha20-Poly1305 authenticated encryption for seed storage
- Add mnemonic auto-clear (60s timeout) and clipboard auto-clear (30s)
- Add sanitized error logging to prevent credential leaks
- Strengthen CSP with object-src, base-uri, form-action, frame-ancestors
- Clear sensitive state on component unmount

Explorer (Gas Estimator):
- Add Gas Estimation page with from/to/amount/data inputs
- Add bech32 address validation (synor1/tsynor1 prefix)
- Add BigInt-based amount parsing to avoid floating point errors
- Add production guard for mock mode (cannot enable in prod builds)

Monitoring (30-day Testnet):
- Add Prometheus config with 30-day retention
- Add comprehensive alert rules for node health, consensus, network, mempool
- Add Alertmanager with severity-based routing and inhibition rules
- Add Grafana with auto-provisioned datasource and dashboard
- Add Synor testnet dashboard with uptime SLA tracking

Docker:
- Update docker-compose.testnet.yml with monitoring profile
- Fix node-exporter for macOS Docker Desktop compatibility
- Change Grafana port to 3001 to avoid conflict
2026-01-10 04:38:09 +05:30

349 lines
11 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft, Copy, Check, AlertTriangle, Clock } from 'lucide-react';
import { useWalletStore } from '../store/wallet';
/** Seconds before mnemonic is automatically cleared from display */
const MNEMONIC_DISPLAY_TIMEOUT = 60;
/** Seconds before clipboard is automatically cleared after copying */
const CLIPBOARD_CLEAR_TIMEOUT = 30;
type Step = 'password' | 'mnemonic' | 'verify';
export default function CreateWallet() {
const navigate = useNavigate();
const { createWallet } = useWalletStore();
const [step, setStep] = useState<Step>('password');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [mnemonic, setMnemonic] = useState('');
const [copied, setCopied] = useState(false);
const [verifyWord, setVerifyWord] = useState('');
const [verifyIndex, setVerifyIndex] = useState(0);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [mnemonicHidden, setMnemonicHidden] = useState(false);
const [countdown, setCountdown] = useState(MNEMONIC_DISPLAY_TIMEOUT);
const [clipboardTimer, setClipboardTimer] = useState<number | null>(null);
/**
* Clear the mnemonic from state for security
*/
const clearMnemonic = useCallback(() => {
setMnemonic('');
setMnemonicHidden(true);
}, []);
/**
* Auto-clear mnemonic after timeout when viewing recovery phrase
*/
useEffect(() => {
if (step !== 'mnemonic' || !mnemonic || mnemonicHidden) return;
// Countdown timer
const interval = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearMnemonic();
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(interval);
}, [step, mnemonic, mnemonicHidden, clearMnemonic]);
/**
* Reset countdown when entering mnemonic step
*/
useEffect(() => {
if (step === 'mnemonic' && mnemonic && !mnemonicHidden) {
setCountdown(MNEMONIC_DISPLAY_TIMEOUT);
}
}, [step, mnemonic, mnemonicHidden]);
/**
* Clear sensitive state on unmount
*/
useEffect(() => {
return () => {
// Clear password and mnemonic from state on unmount
setPassword('');
setConfirmPassword('');
setMnemonic('');
setVerifyWord('');
};
}, []);
const handleCreateWallet = async () => {
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
if (password.length < 8) {
setError('Password must be at least 8 characters');
return;
}
setLoading(true);
setError('');
try {
const result = await createWallet(password);
setMnemonic(result.mnemonic);
// Random word to verify (1-24)
setVerifyIndex(Math.floor(Math.random() * 24));
setStep('mnemonic');
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create wallet');
} finally {
setLoading(false);
}
};
const handleCopyMnemonic = async () => {
await navigator.clipboard.writeText(mnemonic);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
// Clear any existing clipboard timer
if (clipboardTimer) {
clearTimeout(clipboardTimer);
}
// Auto-clear clipboard after timeout for security
const timer = window.setTimeout(async () => {
try {
// Clear clipboard by writing empty string
await navigator.clipboard.writeText('');
setClipboardTimer(null);
} catch {
// Clipboard API may fail if window not focused
}
}, CLIPBOARD_CLEAR_TIMEOUT * 1000);
setClipboardTimer(timer);
};
const handleVerify = () => {
const words = mnemonic.split(' ');
if (verifyWord.toLowerCase().trim() === words[verifyIndex]) {
navigate('/dashboard');
} else {
setError(`Incorrect word. Please check word #${verifyIndex + 1}`);
}
};
const words = mnemonic.split(' ');
return (
<div className="h-full flex flex-col items-center justify-center p-8">
<div className="w-full max-w-md">
{/* Back button */}
<button
onClick={() =>
step === 'password'
? navigate('/')
: setStep(step === 'verify' ? 'mnemonic' : 'password')
}
className="flex items-center gap-2 text-gray-400 hover:text-white mb-8 transition-colors"
>
<ArrowLeft size={20} />
Back
</button>
{step === 'password' && (
<div className="card">
<h2 className="text-xl font-semibold text-white mb-6">
Create Password
</h2>
<p className="text-gray-400 text-sm mb-6">
This password will encrypt your wallet. You'll need it every time
you open the app.
</p>
<div className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-2">
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="input"
placeholder="Enter password (min 8 characters)"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-2">
Confirm Password
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="input"
placeholder="Confirm password"
/>
</div>
{error && (
<p className="text-red-400 text-sm flex items-center gap-2">
<AlertTriangle size={16} />
{error}
</p>
)}
<button
onClick={handleCreateWallet}
disabled={loading || !password || !confirmPassword}
className="btn btn-primary w-full"
>
{loading ? 'Creating...' : 'Create Wallet'}
</button>
</div>
</div>
)}
{step === 'mnemonic' && (
<div className="card">
<h2 className="text-xl font-semibold text-white mb-2">
Recovery Phrase
</h2>
<p className="text-gray-400 text-sm mb-4">
Write down these 24 words in order. This is the ONLY way to
recover your wallet if you lose access.
</p>
{/* Security countdown timer */}
{!mnemonicHidden && (
<div className="flex items-center gap-2 text-sm text-amber-400 mb-4">
<Clock size={16} />
<span>
Auto-hiding in {countdown}s for security
</span>
</div>
)}
{mnemonicHidden ? (
/* Mnemonic has been auto-cleared */
<div className="bg-gray-950 rounded-lg p-6 mb-4 text-center">
<AlertTriangle className="text-amber-500 mx-auto mb-3" size={32} />
<p className="text-gray-300 mb-2">
Recovery phrase hidden for security
</p>
<p className="text-gray-500 text-sm mb-4">
If you haven't saved it, go back and create a new wallet.
</p>
<button
onClick={() => navigate('/')}
className="btn btn-secondary"
>
Start Over
</button>
</div>
) : (
/* Show mnemonic words */
<div className="bg-gray-950 rounded-lg p-4 mb-4">
<div className="grid grid-cols-3 gap-2">
{words.map((word, i) => (
<div
key={i}
className="flex items-center gap-2 text-sm py-1"
>
<span className="text-gray-500 w-6">{i + 1}.</span>
<span className="text-white font-mono">{word}</span>
</div>
))}
</div>
</div>
)}
{!mnemonicHidden && (
<>
<button
onClick={handleCopyMnemonic}
className="btn btn-secondary w-full mb-4"
>
{copied ? (
<>
<Check size={18} className="text-green-400" />
Copied! (auto-clears in {CLIPBOARD_CLEAR_TIMEOUT}s)
</>
) : (
<>
<Copy size={18} />
Copy to Clipboard
</>
)}
</button>
<div className="bg-yellow-900/30 border border-yellow-700 rounded-lg p-4 mb-6">
<div className="flex gap-3">
<AlertTriangle className="text-yellow-500 shrink-0" size={20} />
<p className="text-sm text-yellow-200">
Never share your recovery phrase. Anyone with these words can
steal your funds.
</p>
</div>
</div>
<button
onClick={() => setStep('verify')}
className="btn btn-primary w-full"
>
I've Written It Down
</button>
</>
)}
</div>
)}
{step === 'verify' && (
<div className="card">
<h2 className="text-xl font-semibold text-white mb-2">
Verify Recovery Phrase
</h2>
<p className="text-gray-400 text-sm mb-6">
Enter word #{verifyIndex + 1} from your recovery phrase to verify
you've saved it correctly.
</p>
<div>
<label className="block text-sm text-gray-400 mb-2">
Word #{verifyIndex + 1}
</label>
<input
type="text"
value={verifyWord}
onChange={(e) => setVerifyWord(e.target.value)}
className="input"
placeholder={`Enter word #${verifyIndex + 1}`}
autoFocus
/>
</div>
{error && (
<p className="text-red-400 text-sm flex items-center gap-2 mt-4">
<AlertTriangle size={16} />
{error}
</p>
)}
<button
onClick={handleVerify}
disabled={!verifyWord}
className="btn btn-primary w-full mt-6"
>
Verify & Continue
</button>
</div>
)}
</div>
</div>
);
}