EmergencyPanel.tsx
1 import { useState } from 'react' 2 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' 3 import { useAuthStore } from '../../store/auth' 4 import type { Chain } from '../../types/vote' 5 import type { EmergencyAction, EmergencyActionType } from '../../types/emergency' 6 import * as emergencyService from '../../services/emergency' 7 8 interface EmergencyPanelProps { 9 chain: Chain 10 } 11 12 const ACTION_TYPES: { type: EmergencyActionType; label: string; description: string }[] = [ 13 { 14 type: 'pause_minting', 15 label: 'Pause Minting', 16 description: 'Temporarily halt all token minting operations', 17 }, 18 { 19 type: 'resume_minting', 20 label: 'Resume Minting', 21 description: 'Restore normal minting operations', 22 }, 23 { 24 type: 'emergency_rollback', 25 label: 'Emergency Rollback', 26 description: 'Rollback to a previous checkpoint without governance vote', 27 }, 28 { 29 type: 'freeze_account', 30 label: 'Freeze Account', 31 description: 'Temporarily freeze a specific account', 32 }, 33 { 34 type: 'pause_governance', 35 label: 'Pause Governance', 36 description: 'Halt all governance voting temporarily', 37 }, 38 { 39 type: 'emergency_upgrade', 40 label: 'Emergency Upgrade', 41 description: 'Deploy critical security patch without standard timelock', 42 }, 43 ] 44 45 export default function EmergencyPanel({ chain }: EmergencyPanelProps) { 46 const { address, isConnected } = useAuthStore() 47 const queryClient = useQueryClient() 48 49 const [selectedAction, setSelectedAction] = useState<EmergencyActionType | null>(null) 50 const [reason, setReason] = useState('') 51 const [params, setParams] = useState<Record<string, string>>({}) 52 53 // Check if user is DEQ member 54 const { data: isDEQ = false } = useQuery({ 55 queryKey: ['isDEQMember', address, chain], 56 queryFn: () => emergencyService.isDEQMember(address!, chain), 57 enabled: !!address, 58 }) 59 60 // Get pending actions 61 const { data: pendingActions = [] } = useQuery({ 62 queryKey: ['pendingEmergencyActions', chain], 63 queryFn: () => emergencyService.getPendingEmergencyActions(chain), 64 }) 65 66 // Get emergency config 67 const { data: config } = useQuery({ 68 queryKey: ['emergencyConfig', chain], 69 queryFn: () => emergencyService.getEmergencyConfig(chain), 70 }) 71 72 // Request action mutation 73 const requestMutation = useMutation({ 74 mutationFn: () => 75 emergencyService.requestEmergencyAction(selectedAction!, chain, params, reason), 76 onSuccess: () => { 77 queryClient.invalidateQueries({ queryKey: ['pendingEmergencyActions', chain] }) 78 setSelectedAction(null) 79 setReason('') 80 setParams({}) 81 }, 82 }) 83 84 const actionInfo = selectedAction 85 ? emergencyService.formatActionType(selectedAction) 86 : null 87 88 return ( 89 <div className="space-y-6"> 90 {/* Header */} 91 <div className="bg-red-50 border border-red-200 rounded-lg p-4"> 92 <div className="flex items-center gap-2 text-red-800"> 93 <WarningIcon className="h-5 w-5" /> 94 <span className="font-semibold">Emergency Actions Panel</span> 95 </div> 96 <p className="text-sm text-red-700 mt-1"> 97 Emergency actions require {config?.requiredSignatures || 3} of{' '} 98 {config?.totalDEQMembers || 5} DEQ member signatures. 99 </p> 100 </div> 101 102 {/* DEQ Status */} 103 {isConnected && ( 104 <div 105 className={`rounded-lg p-4 ${ 106 isDEQ 107 ? 'bg-green-50 border border-green-200' 108 : 'bg-gray-50 border border-gray-200' 109 }`} 110 > 111 <div className="flex items-center gap-2"> 112 {isDEQ ? ( 113 <> 114 <CheckIcon className="h-5 w-5 text-green-600" /> 115 <span className="text-green-800 font-medium"> 116 You are a DEQ member 117 </span> 118 </> 119 ) : ( 120 <> 121 <LockIcon className="h-5 w-5 text-gray-500" /> 122 <span className="text-gray-600"> 123 You are not a DEQ member. Contact governance to request access. 124 </span> 125 </> 126 )} 127 </div> 128 </div> 129 )} 130 131 {/* Pending Actions */} 132 {pendingActions.length > 0 && ( 133 <div className="bg-white border rounded-lg"> 134 <div className="p-4 border-b"> 135 <h3 className="font-medium">Pending Actions ({pendingActions.length})</h3> 136 </div> 137 <div className="divide-y"> 138 {pendingActions.map((action) => ( 139 <PendingActionCard 140 key={action.id} 141 action={action} 142 userAddress={address ?? undefined} 143 isDEQ={isDEQ} 144 chain={chain} 145 /> 146 ))} 147 </div> 148 </div> 149 )} 150 151 {/* Request New Action */} 152 {isDEQ && ( 153 <div className="bg-white border rounded-lg"> 154 <div className="p-4 border-b"> 155 <h3 className="font-medium">Request Emergency Action</h3> 156 </div> 157 158 <div className="p-4 space-y-4"> 159 {/* Action Type Selection */} 160 <div> 161 <label className="block text-sm font-medium text-gray-700 mb-2"> 162 Action Type 163 </label> 164 <div className="grid grid-cols-2 gap-2"> 165 {ACTION_TYPES.map(({ type, label, description }) => { 166 const info = emergencyService.formatActionType(type) 167 return ( 168 <button 169 key={type} 170 onClick={() => setSelectedAction(type)} 171 className={`p-3 rounded-lg border text-left transition-colors ${ 172 selectedAction === type 173 ? 'border-red-500 bg-red-50' 174 : 'border-gray-200 hover:border-gray-300' 175 }`} 176 > 177 <div className="flex items-center gap-2"> 178 <span 179 className={`text-xs px-2 py-0.5 rounded ${ 180 info.severity === 'critical' 181 ? 'bg-red-100 text-red-700' 182 : info.severity === 'high' 183 ? 'bg-orange-100 text-orange-700' 184 : 'bg-yellow-100 text-yellow-700' 185 }`} 186 > 187 {info.severity} 188 </span> 189 </div> 190 <div className="font-medium text-sm mt-1">{label}</div> 191 <div className="text-xs text-gray-500 mt-1">{description}</div> 192 </button> 193 ) 194 })} 195 </div> 196 </div> 197 198 {/* Action-specific params */} 199 {selectedAction === 'freeze_account' && ( 200 <div> 201 <label className="block text-sm font-medium text-gray-700 mb-1"> 202 Account Address 203 </label> 204 <input 205 type="text" 206 value={params.address || ''} 207 onChange={(e) => setParams({ ...params, address: e.target.value })} 208 placeholder="ax1... or dx1..." 209 className="w-full px-3 py-2 border rounded-lg text-sm" 210 /> 211 </div> 212 )} 213 214 {selectedAction === 'emergency_rollback' && ( 215 <div> 216 <label className="block text-sm font-medium text-gray-700 mb-1"> 217 Target Checkpoint Hash 218 </label> 219 <input 220 type="text" 221 value={params.checkpoint || ''} 222 onChange={(e) => setParams({ ...params, checkpoint: e.target.value })} 223 placeholder="abc123..." 224 className="w-full px-3 py-2 border rounded-lg text-sm" 225 /> 226 </div> 227 )} 228 229 {selectedAction === 'emergency_upgrade' && ( 230 <div> 231 <label className="block text-sm font-medium text-gray-700 mb-1"> 232 Program ID 233 </label> 234 <input 235 type="text" 236 value={params.programId || ''} 237 onChange={(e) => setParams({ ...params, programId: e.target.value })} 238 placeholder="program.adl" 239 className="w-full px-3 py-2 border rounded-lg text-sm" 240 /> 241 </div> 242 )} 243 244 {/* Reason */} 245 {selectedAction && ( 246 <div> 247 <label className="block text-sm font-medium text-gray-700 mb-1"> 248 Reason for Emergency Action 249 </label> 250 <textarea 251 value={reason} 252 onChange={(e) => setReason(e.target.value)} 253 rows={3} 254 placeholder="Describe the emergency situation and why this action is necessary..." 255 className="w-full px-3 py-2 border rounded-lg text-sm" 256 /> 257 </div> 258 )} 259 260 {/* Submit */} 261 {selectedAction && ( 262 <div className="flex items-center gap-4"> 263 <button 264 onClick={() => requestMutation.mutate()} 265 disabled={!reason || requestMutation.isPending} 266 className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-medium disabled:opacity-50 hover:bg-red-700" 267 > 268 {requestMutation.isPending ? 'Submitting...' : 'Request Action'} 269 </button> 270 271 {actionInfo && ( 272 <span className="text-sm text-gray-500"> 273 Severity: {actionInfo.severity} 274 </span> 275 )} 276 </div> 277 )} 278 </div> 279 </div> 280 )} 281 </div> 282 ) 283 } 284 285 function PendingActionCard({ 286 action, 287 userAddress, 288 isDEQ, 289 chain, 290 }: { 291 action: EmergencyAction 292 userAddress: string | undefined 293 isDEQ: boolean 294 chain: Chain 295 }) { 296 const queryClient = useQueryClient() 297 const status = emergencyService.calculateActionStatus(action, userAddress, isDEQ) 298 const actionInfo = emergencyService.formatActionType(action.actionType) 299 300 const signMutation = useMutation({ 301 mutationFn: () => emergencyService.signEmergencyAction(action.id, chain), 302 onSuccess: () => { 303 queryClient.invalidateQueries({ queryKey: ['pendingEmergencyActions', chain] }) 304 }, 305 }) 306 307 const executeMutation = useMutation({ 308 mutationFn: () => emergencyService.executeEmergencyAction(action.id, chain), 309 onSuccess: () => { 310 queryClient.invalidateQueries({ queryKey: ['pendingEmergencyActions', chain] }) 311 }, 312 }) 313 314 return ( 315 <div className="p-4"> 316 <div className="flex items-start justify-between"> 317 <div> 318 <div className="flex items-center gap-2"> 319 <span 320 className={`text-xs px-2 py-0.5 rounded ${ 321 actionInfo.severity === 'critical' 322 ? 'bg-red-100 text-red-700' 323 : actionInfo.severity === 'high' 324 ? 'bg-orange-100 text-orange-700' 325 : 'bg-yellow-100 text-yellow-700' 326 }`} 327 > 328 {actionInfo.severity} 329 </span> 330 <span className="font-medium">{actionInfo.label}</span> 331 </div> 332 <p className="text-sm text-gray-600 mt-1">{action.reason}</p> 333 </div> 334 335 <div className="text-right"> 336 <div className="text-sm"> 337 {action.signatures.length} / {action.requiredSignatures} signatures 338 </div> 339 {status.timeRemaining > 0 && ( 340 <div className="text-xs text-gray-500"> 341 {formatTimeRemaining(status.timeRemaining)} remaining 342 </div> 343 )} 344 </div> 345 </div> 346 347 {/* Signature progress */} 348 <div className="mt-3"> 349 <div className="h-2 bg-gray-100 rounded-full overflow-hidden"> 350 <div 351 className="h-full bg-red-500 transition-all" 352 style={{ 353 width: `${(action.signatures.length / action.requiredSignatures) * 100}%`, 354 }} 355 /> 356 </div> 357 </div> 358 359 {/* Actions */} 360 <div className="mt-3 flex items-center gap-2"> 361 {status.canSign && ( 362 <button 363 onClick={() => signMutation.mutate()} 364 disabled={signMutation.isPending} 365 className="px-3 py-1.5 bg-red-600 text-white rounded text-sm disabled:opacity-50 hover:bg-red-700" 366 > 367 {signMutation.isPending ? 'Signing...' : 'Sign'} 368 </button> 369 )} 370 371 {status.canExecute && ( 372 <button 373 onClick={() => executeMutation.mutate()} 374 disabled={executeMutation.isPending} 375 className="px-3 py-1.5 bg-green-600 text-white rounded text-sm disabled:opacity-50 hover:bg-green-700" 376 > 377 {executeMutation.isPending ? 'Executing...' : 'Execute'} 378 </button> 379 )} 380 381 {status.isExpired && ( 382 <span className="text-sm text-red-600">Expired</span> 383 )} 384 </div> 385 </div> 386 ) 387 } 388 389 function formatTimeRemaining(seconds: number): string { 390 const hours = Math.floor(seconds / 3600) 391 const minutes = Math.floor((seconds % 3600) / 60) 392 393 if (hours > 0) { 394 return `${hours}h ${minutes}m` 395 } 396 return `${minutes}m` 397 } 398 399 function WarningIcon({ className }: { className?: string }) { 400 return ( 401 <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"> 402 <path 403 strokeLinecap="round" 404 strokeLinejoin="round" 405 strokeWidth={2} 406 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" 407 /> 408 </svg> 409 ) 410 } 411 412 function CheckIcon({ className }: { className?: string }) { 413 return ( 414 <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"> 415 <path 416 strokeLinecap="round" 417 strokeLinejoin="round" 418 strokeWidth={2} 419 d="M5 13l4 4L19 7" 420 /> 421 </svg> 422 ) 423 } 424 425 function LockIcon({ className }: { className?: string }) { 426 return ( 427 <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"> 428 <path 429 strokeLinecap="round" 430 strokeLinejoin="round" 431 strokeWidth={2} 432 d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" 433 /> 434 </svg> 435 ) 436 }