/ frontend / src / services / emergency.ts
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  }