synor/apps/desktop-wallet/src/pages/Decoy/DecoyDashboard.tsx
2026-02-02 15:16:29 +05:30

265 lines
9.7 KiB
TypeScript

import { useEffect, useState } from 'react';
import {
EyeOff,
Plus,
Trash2,
RefreshCw,
AlertCircle,
ShieldAlert,
Info,
Wallet,
} from 'lucide-react';
import { useDecoyStore, DecoyWallet } from '../../store/decoy';
function SetupDecoyModal({ onClose }: { onClose: () => void }) {
const { setup, isLoading } = useDecoyStore();
const [password, setPassword] = useState('');
const [confirm, setConfirm] = useState('');
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (password !== confirm) {
setError('Passwords do not match');
return;
}
if (password.length < 8) {
setError('Password must be at least 8 characters');
return;
}
try {
await setup(password);
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : 'Setup failed');
}
};
return (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div className="bg-gray-900 rounded-xl p-6 max-w-md w-full mx-4 border border-gray-800">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<ShieldAlert className="text-synor-400" />
Setup Decoy Wallets
</h2>
<p className="text-gray-400 text-sm mb-4">
Create a "duress password" that opens decoy wallets instead of your real wallet.
This provides plausible deniability if forced to unlock your wallet.
</p>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-1">Duress Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 text-white"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Confirm Password</label>
<input
type="password"
value={confirm}
onChange={(e) => setConfirm(e.target.value)}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 text-white"
/>
</div>
{error && <p className="text-red-400 text-sm">{error}</p>}
<div className="flex gap-3">
<button type="button" onClick={onClose} className="flex-1 px-4 py-2 bg-gray-700 rounded-lg">
Cancel
</button>
<button type="submit" disabled={isLoading} className="flex-1 px-4 py-2 bg-synor-600 rounded-lg">
{isLoading ? 'Setting up...' : 'Enable'}
</button>
</div>
</form>
</div>
</div>
);
}
function CreateDecoyModal({ onClose }: { onClose: () => void }) {
const { createDecoy, isLoading } = useDecoyStore();
const [name, setName] = useState('');
const [balance, setBalance] = useState('0.1');
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await createDecoy(name, parseFloat(balance));
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create');
}
};
return (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
<div className="bg-gray-900 rounded-xl p-6 max-w-md w-full mx-4 border border-gray-800">
<h2 className="text-xl font-bold mb-4">Create Decoy Wallet</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-1">Wallet Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., Savings"
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 text-white"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Fake Balance (SYN)</label>
<input
type="number"
step="0.001"
value={balance}
onChange={(e) => setBalance(e.target.value)}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 text-white"
/>
</div>
{error && <p className="text-red-400 text-sm">{error}</p>}
<div className="flex gap-3">
<button type="button" onClick={onClose} className="flex-1 px-4 py-2 bg-gray-700 rounded-lg">
Cancel
</button>
<button type="submit" disabled={isLoading} className="flex-1 px-4 py-2 bg-synor-600 rounded-lg">
Create
</button>
</div>
</form>
</div>
</div>
);
}
function DecoyCard({ decoy }: { decoy: DecoyWallet }) {
const { deleteDecoy } = useDecoyStore();
const [showConfirm, setShowConfirm] = useState(false);
return (
<div className="bg-gray-800 rounded-lg p-4">
<div className="flex items-start justify-between mb-2">
<div>
<h4 className="font-medium">{decoy.name}</h4>
<p className="text-xs text-gray-500 font-mono">{decoy.address.slice(0, 20)}...</p>
</div>
{!showConfirm ? (
<button onClick={() => setShowConfirm(true)} className="text-gray-500 hover:text-red-400">
<Trash2 size={16} />
</button>
) : (
<div className="flex gap-2">
<button onClick={() => setShowConfirm(false)} className="px-2 py-1 text-xs bg-gray-700 rounded">
No
</button>
<button onClick={() => deleteDecoy(decoy.id)} className="px-2 py-1 text-xs bg-red-600 rounded">
Yes
</button>
</div>
)}
</div>
<p className="text-xl font-bold text-synor-400">{decoy.balanceHuman}</p>
</div>
);
}
export default function DecoyDashboard() {
const { isEnabled, decoys, isLoading, error, checkEnabled, fetchDecoys } = useDecoyStore();
const [showSetup, setShowSetup] = useState(false);
const [showCreate, setShowCreate] = useState(false);
useEffect(() => {
checkEnabled();
fetchDecoys();
}, [checkEnabled, fetchDecoys]);
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold flex items-center gap-3">
<EyeOff className="text-synor-400" />
Decoy Wallets
</h1>
<p className="text-gray-400 mt-1">Plausible deniability for your crypto</p>
</div>
<div className="flex gap-3">
<button onClick={fetchDecoys} className="p-2 bg-gray-800 rounded-lg">
<RefreshCw size={18} className={isLoading ? 'animate-spin' : ''} />
</button>
{isEnabled && (
<button onClick={() => setShowCreate(true)} className="px-4 py-2 bg-synor-600 rounded-lg flex items-center gap-2">
<Plus size={18} />
Add Decoy
</button>
)}
</div>
</div>
{error && (
<div className="bg-red-500/20 border border-red-500/30 rounded-lg p-4 flex items-center gap-3">
<AlertCircle className="text-red-400" />
<span className="text-red-200">{error}</span>
</div>
)}
{!isEnabled ? (
<div className="bg-gray-900 rounded-xl p-8 text-center border border-gray-800">
<ShieldAlert size={48} className="mx-auto text-gray-600 mb-4" />
<h3 className="text-lg font-medium mb-2">Decoy Wallets Not Enabled</h3>
<p className="text-gray-500 mb-4 max-w-md mx-auto">
Set up a duress password that opens fake wallets to protect your real funds
under coercion.
</p>
<button onClick={() => setShowSetup(true)} className="px-6 py-2 bg-synor-600 rounded-lg">
Enable Decoy Wallets
</button>
</div>
) : (
<>
<div className="bg-green-500/10 border border-green-500/30 rounded-xl p-4 flex items-center gap-3">
<ShieldAlert className="text-green-400" />
<div>
<p className="font-medium">Decoy Protection Active</p>
<p className="text-sm text-gray-400">
Using your duress password will show decoy wallets instead of real funds
</p>
</div>
</div>
{decoys.length === 0 ? (
<div className="bg-gray-900 rounded-xl p-8 text-center border border-gray-800">
<Wallet size={32} className="mx-auto text-gray-600 mb-2" />
<p className="text-gray-500">No decoy wallets created yet</p>
</div>
) : (
<div className="grid grid-cols-2 gap-4">
{decoys.map((d) => (
<DecoyCard key={d.id} decoy={d} />
))}
</div>
)}
</>
)}
<div className="bg-gray-900/50 rounded-lg p-4 border border-gray-800 flex items-start gap-3">
<Info className="text-gray-500 mt-0.5" size={18} />
<div className="text-sm text-gray-400">
<p className="font-medium text-gray-300 mb-1">How Decoy Wallets Work</p>
<p>
When unlocking with your duress password, decoy wallets are shown instead of your
real wallet. The decoys appear legitimate but contain minimal funds, protecting your
actual holdings.
</p>
</div>
</div>
{showSetup && <SetupDecoyModal onClose={() => setShowSetup(false)} />}
{showCreate && <CreateDecoyModal onClose={() => setShowCreate(false)} />}
</div>
);
}