emergency.ts
1 /** 2 * Emergency system service - handles DEQ operations and emergency actions 3 */ 4 5 import type { Chain } from '../types/vote' 6 import type { 7 DEQMember, 8 EmergencyAction, 9 EmergencyActionType, 10 EmergencyConfig, 11 OfflineSigningPayload, 12 SignedOfflinePayload, 13 EmergencySignature, 14 } from '../types/emergency' 15 import { getChainEndpoint } from './chain' 16 17 /** 18 * Get DEQ members for a chain 19 */ 20 export async function getDEQMembers(chain: Chain): Promise<DEQMember[]> { 21 try { 22 const res = await fetch(`/api/cache/deq-members?chain=${chain}`) 23 if (!res.ok) return [] 24 return res.json() 25 } catch { 26 return [] 27 } 28 } 29 30 /** 31 * Get active DEQ members count 32 */ 33 export async function getActiveDEQCount(chain: Chain): Promise<number> { 34 const members = await getDEQMembers(chain) 35 return members.filter((m) => m.status === 'active').length 36 } 37 38 /** 39 * Get emergency system configuration 40 */ 41 export async function getEmergencyConfig(chain: Chain): Promise<EmergencyConfig> { 42 try { 43 const res = await fetch(`/api/cache/emergency-config?chain=${chain}`) 44 if (!res.ok) throw new Error('Config not found') 45 return res.json() 46 } catch { 47 // Default configuration 48 return { 49 chain, 50 requiredSignatures: 3, 51 totalDEQMembers: 5, 52 actionTimeout: 24 * 60 * 60, // 24 hours 53 cooldownPeriod: 60 * 60, // 1 hour 54 lastEmergencyAction: null, 55 } 56 } 57 } 58 59 /** 60 * Get pending emergency actions 61 */ 62 export async function getPendingEmergencyActions( 63 chain: Chain 64 ): Promise<EmergencyAction[]> { 65 try { 66 const res = await fetch(`/api/cache/emergency-actions?chain=${chain}&status=pending`) 67 if (!res.ok) return [] 68 return res.json() 69 } catch { 70 return [] 71 } 72 } 73 74 /** 75 * Get all emergency actions (for history) 76 */ 77 export async function getEmergencyActionHistory( 78 chain: Chain, 79 limit = 20 80 ): Promise<EmergencyAction[]> { 81 try { 82 const res = await fetch( 83 `/api/cache/emergency-actions?chain=${chain}&limit=${limit}` 84 ) 85 if (!res.ok) return [] 86 return res.json() 87 } catch { 88 return [] 89 } 90 } 91 92 /** 93 * Request a new emergency action 94 */ 95 export async function requestEmergencyAction( 96 actionType: EmergencyActionType, 97 chain: Chain, 98 params: Record<string, unknown>, 99 reason: string 100 ): Promise<EmergencyAction> { 101 // Chain endpoint for future integration 102 void getChainEndpoint(chain) 103 104 console.log(`[Emergency] Requesting ${actionType} on ${chain}:`, { params, reason }) 105 106 const now = Math.floor(Date.now() / 1000) 107 const config = await getEmergencyConfig(chain) 108 109 // In production, this would create the action on-chain 110 return { 111 id: `emergency-${Date.now()}`, 112 actionType, 113 chain, 114 params, 115 requestedBy: 'ax1...placeholder', 116 requestedAt: now, 117 reason, 118 signatures: [], 119 requiredSignatures: config.requiredSignatures, 120 status: 'pending', 121 executedAt: null, 122 expiresAt: now + config.actionTimeout, 123 } 124 } 125 126 /** 127 * Sign an emergency action (online) 128 */ 129 export async function signEmergencyAction( 130 actionId: string, 131 chain: Chain 132 ): Promise<EmergencySignature> { 133 void getChainEndpoint(chain) 134 135 console.log(`[Emergency] Signing action ${actionId} on ${chain}`) 136 137 const now = Math.floor(Date.now() / 1000) 138 139 // In production, this would: 140 // 1. Verify caller is a DEQ member 141 // 2. Create and submit signature on-chain 142 return { 143 signer: 'ax1...placeholder', 144 signature: `sig-${Date.now()}`, 145 signedAt: now, 146 method: 'online', 147 } 148 } 149 150 /** 151 * Generate offline signing payload 152 */ 153 export function generateOfflinePayload( 154 action: EmergencyAction 155 ): OfflineSigningPayload { 156 const nonce = crypto.randomUUID() 157 158 return { 159 actionId: action.id, 160 actionType: action.actionType, 161 chain: action.chain, 162 params: action.params, 163 nonce, 164 expiresAt: action.expiresAt, 165 message: formatActionMessage(action), 166 } 167 } 168 169 /** 170 * Format action for human-readable display 171 */ 172 function formatActionMessage(action: EmergencyAction): string { 173 const actionDescriptions: Record<EmergencyActionType, string> = { 174 pause_minting: 'PAUSE all minting operations', 175 resume_minting: 'RESUME minting operations', 176 emergency_rollback: 'EMERGENCY ROLLBACK to checkpoint', 177 freeze_account: 'FREEZE account', 178 pause_governance: 'PAUSE governance voting', 179 emergency_upgrade: 'EMERGENCY contract upgrade', 180 } 181 182 return ` 183 EMERGENCY ACTION REQUEST 184 ======================== 185 Action: ${actionDescriptions[action.actionType]} 186 Chain: ${action.chain.toUpperCase()} 187 Reason: ${action.reason} 188 Requested: ${new Date(action.requestedAt * 1000).toISOString()} 189 Expires: ${new Date(action.expiresAt * 1000).toISOString()} 190 Action ID: ${action.id} 191 192 Parameters: 193 ${JSON.stringify(action.params, null, 2)} 194 195 BY SIGNING THIS MESSAGE, YOU AUTHORIZE THE ABOVE EMERGENCY ACTION. 196 `.trim() 197 } 198 199 /** 200 * Submit offline signature 201 */ 202 export async function submitOfflineSignature( 203 signedPayload: SignedOfflinePayload, 204 chain: Chain 205 ): Promise<EmergencySignature> { 206 void getChainEndpoint(chain) 207 208 console.log(`[Emergency] Submitting offline signature for ${signedPayload.payload.actionId}`) 209 210 // In production, this would: 211 // 1. Verify signature against payload 212 // 2. Verify signer is a DEQ member 213 // 3. Submit signature on-chain 214 return { 215 signer: signedPayload.publicKey, 216 signature: signedPayload.signature, 217 signedAt: Math.floor(Date.now() / 1000), 218 method: 'offline', 219 } 220 } 221 222 /** 223 * Execute an approved emergency action 224 */ 225 export async function executeEmergencyAction( 226 actionId: string, 227 chain: Chain 228 ): Promise<{ success: boolean; transactionHash: string }> { 229 void getChainEndpoint(chain) 230 231 console.log(`[Emergency] Executing action ${actionId} on ${chain}`) 232 233 // In production, this would: 234 // 1. Verify action has required signatures 235 // 2. Execute the emergency action on-chain 236 // 3. Return transaction hash 237 return { 238 success: true, 239 transactionHash: `0x${Date.now().toString(16)}`, 240 } 241 } 242 243 /** 244 * Check if current user is a DEQ member 245 */ 246 export async function isDEQMember( 247 address: string, 248 chain: Chain 249 ): Promise<boolean> { 250 const members = await getDEQMembers(chain) 251 return members.some((m) => m.address === address && m.status === 'active') 252 } 253 254 /** 255 * Calculate action status 256 */ 257 export function calculateActionStatus( 258 action: EmergencyAction, 259 userAddress: string | undefined, 260 isDEQ: boolean 261 ): { 262 signaturesNeeded: number 263 timeRemaining: number 264 canSign: boolean 265 canExecute: boolean 266 isExpired: boolean 267 } { 268 const now = Math.floor(Date.now() / 1000) 269 const signaturesNeeded = action.requiredSignatures - action.signatures.length 270 const timeRemaining = Math.max(0, action.expiresAt - now) 271 const isExpired = timeRemaining === 0 272 273 const hasAlreadySigned = action.signatures.some( 274 (s) => s.signer === userAddress 275 ) 276 277 return { 278 signaturesNeeded, 279 timeRemaining, 280 canSign: isDEQ && !hasAlreadySigned && !isExpired && action.status === 'pending', 281 canExecute: signaturesNeeded <= 0 && !isExpired && action.status === 'pending', 282 isExpired, 283 } 284 } 285 286 /** 287 * Format action type for display 288 */ 289 export function formatActionType(actionType: EmergencyActionType): { 290 label: string 291 severity: 'low' | 'medium' | 'high' | 'critical' 292 icon: string 293 } { 294 const types: Record< 295 EmergencyActionType, 296 { label: string; severity: 'low' | 'medium' | 'high' | 'critical'; icon: string } 297 > = { 298 pause_minting: { 299 label: 'Pause Minting', 300 severity: 'high', 301 icon: 'pause', 302 }, 303 resume_minting: { 304 label: 'Resume Minting', 305 severity: 'medium', 306 icon: 'play', 307 }, 308 emergency_rollback: { 309 label: 'Emergency Rollback', 310 severity: 'critical', 311 icon: 'rewind', 312 }, 313 freeze_account: { 314 label: 'Freeze Account', 315 severity: 'high', 316 icon: 'lock', 317 }, 318 pause_governance: { 319 label: 'Pause Governance', 320 severity: 'critical', 321 icon: 'stop', 322 }, 323 emergency_upgrade: { 324 label: 'Emergency Upgrade', 325 severity: 'critical', 326 icon: 'upload', 327 }, 328 } 329 330 return types[actionType] 331 } 332 333 /** 334 * Generate CLI command for offline signing 335 */ 336 export function generateCLICommand(payload: OfflineSigningPayload): string { 337 const base64Payload = btoa(JSON.stringify(payload)) 338 339 return `acdc-forge sign-emergency --payload "${base64Payload}"` 340 } 341 342 /** 343 * Parse CLI output (signed payload) 344 */ 345 export function parseCLIOutput(output: string): SignedOfflinePayload | null { 346 try { 347 // Expected format: JSON object with payload, signature, publicKey 348 const parsed = JSON.parse(output) 349 350 if (parsed.payload && parsed.signature && parsed.publicKey) { 351 return parsed as SignedOfflinePayload 352 } 353 354 return null 355 } catch { 356 return null 357 } 358 }