265 lines
9.7 KiB
TypeScript
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>
|
|
);
|
|
}
|