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
349 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|