StakeVerification.tsx
1 import { useState } from 'react' 2 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' 3 import { useAuthStore } from '../../store/auth' 4 import * as chainService from '../../services/chain' 5 6 interface StakeInfo { 7 address: string 8 stakedAmount: number 9 lockedUntil: number | null 10 isEligible: boolean 11 } 12 13 async function fetchStakeInfo(address: string): Promise<StakeInfo | null> { 14 try { 15 const res = await fetch(`/api/cache/stakes/${address}`) 16 if (!res.ok) return null 17 return res.json() 18 } catch { 19 return null 20 } 21 } 22 23 export default function StakeVerification() { 24 const { address, isConnected } = useAuthStore() 25 const queryClient = useQueryClient() 26 const [isRegistering, setIsRegistering] = useState(false) 27 const [error, setError] = useState<string | null>(null) 28 29 const { data: stakeInfo, isLoading } = useQuery({ 30 queryKey: ['stakeInfo', address], 31 queryFn: () => fetchStakeInfo(address!), 32 enabled: !!address && isConnected, 33 }) 34 35 const registerMutation = useMutation({ 36 mutationFn: async () => { 37 if (!stakeInfo || !stakeInfo.isEligible) { 38 throw new Error('Insufficient stake') 39 } 40 return chainService.registerCodeGovernor(stakeInfo.stakedAmount) 41 }, 42 onSuccess: () => { 43 queryClient.invalidateQueries({ queryKey: ['governors'] }) 44 setIsRegistering(false) 45 }, 46 onError: (err) => { 47 setError(err instanceof Error ? err.message : 'Registration failed') 48 }, 49 }) 50 51 if (!isConnected) { 52 return ( 53 <div className="bg-gray-50 border border-gray-200 rounded-lg p-6"> 54 <p className="text-gray-600 text-center"> 55 Connect your wallet to verify your DX stake. 56 </p> 57 </div> 58 ) 59 } 60 61 if (isLoading) { 62 return ( 63 <div className="bg-delta-50 border border-delta-200 rounded-lg p-6"> 64 <div className="flex items-center justify-center gap-3"> 65 <div className="animate-spin h-5 w-5 border-2 border-delta-500 border-t-transparent rounded-full" /> 66 <span className="text-delta-700">Verifying stake on Delta chain...</span> 67 </div> 68 </div> 69 ) 70 } 71 72 return ( 73 <div className="bg-delta-50 border border-delta-200 rounded-lg p-6"> 74 <h3 className="text-lg font-semibold text-delta-800 mb-4"> 75 DX Stake Verification 76 </h3> 77 78 {stakeInfo ? ( 79 <div className="space-y-4"> 80 {/* Stake Summary */} 81 <div className="bg-white rounded-lg p-4 border border-delta-200"> 82 <dl className="grid grid-cols-2 gap-4"> 83 <div> 84 <dt className="text-sm text-gray-500">Your Staked Amount</dt> 85 <dd className="text-2xl font-bold text-delta-700"> 86 {stakeInfo.stakedAmount.toLocaleString()} DX 87 </dd> 88 </div> 89 <div> 90 <dt className="text-sm text-gray-500">Minimum Required</dt> 91 <dd className="text-2xl font-bold text-gray-500">10,000 DX</dd> 92 </div> 93 </dl> 94 95 {/* Progress bar */} 96 <div className="mt-4"> 97 <div className="flex justify-between text-xs text-gray-500 mb-1"> 98 <span>Progress to eligibility</span> 99 <span> 100 {Math.min(100, (stakeInfo.stakedAmount / 10000) * 100).toFixed(1)}% 101 </span> 102 </div> 103 <div className="h-2 bg-gray-200 rounded-full overflow-hidden"> 104 <div 105 className={`h-full rounded-full transition-all ${ 106 stakeInfo.isEligible ? 'bg-green-500' : 'bg-delta-500' 107 }`} 108 style={{ 109 width: `${Math.min(100, (stakeInfo.stakedAmount / 10000) * 100)}%`, 110 }} 111 /> 112 </div> 113 </div> 114 </div> 115 116 {/* Status */} 117 {stakeInfo.isEligible ? ( 118 <div className="bg-green-50 border border-green-200 rounded-lg p-4"> 119 <div className="flex items-center gap-2 text-green-700"> 120 <CheckIcon className="h-5 w-5" /> 121 <span className="font-medium">Eligible to become a Code Governor</span> 122 </div> 123 <p className="text-sm text-green-600 mt-2"> 124 You have staked enough DX to register as a Delta Code Governor. 125 </p> 126 127 {!isRegistering ? ( 128 <button 129 onClick={() => setIsRegistering(true)} 130 className="mt-4 w-full py-2 px-4 bg-delta-600 text-white rounded-lg font-medium hover:bg-delta-700 transition-colors" 131 > 132 Register as Code Governor 133 </button> 134 ) : ( 135 <div className="mt-4 space-y-3"> 136 <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3"> 137 <p className="text-sm text-yellow-700"> 138 <strong>Important:</strong> By registering, you commit to 139 participating in governance votes. Missing 3 consecutive votes 140 will result in automatic deregistration. 141 </p> 142 </div> 143 <div className="flex gap-3"> 144 <button 145 onClick={() => setIsRegistering(false)} 146 className="flex-1 py-2 px-4 border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50" 147 > 148 Cancel 149 </button> 150 <button 151 onClick={() => registerMutation.mutate()} 152 disabled={registerMutation.isPending} 153 className="flex-1 py-2 px-4 bg-delta-600 text-white rounded-lg font-medium hover:bg-delta-700 disabled:opacity-50" 154 > 155 {registerMutation.isPending ? 'Registering...' : 'Confirm Registration'} 156 </button> 157 </div> 158 </div> 159 )} 160 </div> 161 ) : ( 162 <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4"> 163 <div className="flex items-center gap-2 text-yellow-700"> 164 <WarningIcon className="h-5 w-5" /> 165 <span className="font-medium">Insufficient Stake</span> 166 </div> 167 <p className="text-sm text-yellow-600 mt-2"> 168 You need to stake at least{' '} 169 <strong>{(10000 - stakeInfo.stakedAmount).toLocaleString()} more DX</strong>{' '} 170 to become eligible as a Code Governor. 171 </p> 172 <a 173 href="https://dex.ac-dc.network/stake" 174 target="_blank" 175 rel="noopener noreferrer" 176 className="mt-3 inline-block text-sm text-delta-600 hover:text-delta-700 font-medium" 177 > 178 Stake more DX → 179 </a> 180 </div> 181 )} 182 183 {stakeInfo.lockedUntil && ( 184 <div className="text-xs text-gray-500"> 185 Stake locked until:{' '} 186 {new Date(stakeInfo.lockedUntil * 1000).toLocaleDateString()} 187 </div> 188 )} 189 190 {error && ( 191 <div className="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-700"> 192 {error} 193 </div> 194 )} 195 </div> 196 ) : ( 197 <div className="text-center py-4"> 198 <p className="text-gray-600 mb-4">No stake found for your address.</p> 199 <a 200 href="https://dex.ac-dc.network/stake" 201 target="_blank" 202 rel="noopener noreferrer" 203 className="inline-block px-4 py-2 bg-delta-600 text-white rounded-lg font-medium hover:bg-delta-700" 204 > 205 Stake DX to Participate 206 </a> 207 </div> 208 )} 209 </div> 210 ) 211 } 212 213 function CheckIcon({ className }: { className?: string }) { 214 return ( 215 <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"> 216 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> 217 </svg> 218 ) 219 } 220 221 function WarningIcon({ className }: { className?: string }) { 222 return ( 223 <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"> 224 <path 225 strokeLinecap="round" 226 strokeLinejoin="round" 227 strokeWidth={2} 228 d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" 229 /> 230 </svg> 231 ) 232 }