'use client' import { useState, useEffect, useCallback } from 'react' import { useAccount } from 'wagmi' import { useWsSubscribe } from '@/lib/wsContext' interface TradeAlert { id: number event: 'execution_failed' | 'insufficient_balance' | 'budget_reached' | 'reconcile_drift' | 'circuit_breaker_tripped' asset?: string reason?: string balance_usd?: number required_usd?: number spent_usd?: number cap_usd?: number // reconcile_drift fields (from reconciler.py broadcast) // orphan_hl is a list of {asset, trade_id} dicts, NOT a number orphan_hl?: unknown[] marked_closed?: number ghost_positions?: unknown[] // circuit_breaker_tripped fields (from circuit_breaker.py broadcast) system?: string cb_reason?: string unlock_at?: string } // How long each alert stays visible before auto-dismissing. const AUTO_DISMISS_MS = 10_000 function alertMessage(alert: TradeAlert): string { switch (alert.event) { case 'execution_failed': return `Auto-trade failed${alert.asset ? ` on ${alert.asset}` : ''}${alert.reason ? `: ${alert.reason}` : ''}. Check your HL API key and account status.` case 'insufficient_balance': return `Insufficient balance to open${alert.asset ? ` ${alert.asset}` : ''}: account has $${alert.balance_usd?.toFixed(2) ?? '?'}, trade requires $${alert.required_usd?.toFixed(2) ?? '?'}. Top up your Hyperliquid account.` case 'budget_reached': return `Daily budget reached${alert.asset ? ` (${alert.asset})` : ''}: $${alert.spent_usd?.toFixed(0) ?? '?'} spent of $${alert.cap_usd?.toFixed(0) ?? '?'} cap. No more trades today.` // B47: reconciler detects DB ↔ HL mismatch (ghost/orphan position) case 'reconcile_drift': { const parts = [] const orphanCount = (alert.orphan_hl as unknown[])?.length ?? 0 if (orphanCount > 0) parts.push(`${orphanCount} orphan(s) on HL`) if (alert.marked_closed) parts.push(`${alert.marked_closed} auto-closed`) if ((alert.ghost_positions as unknown[])?.length) parts.push(`${(alert.ghost_positions as unknown[]).length} ghost(s)`) return `Position drift detected: ${parts.join(', ') || 'mismatch between DB and HL'}. Check your Hyperliquid account.` } // B53: circuit breaker fired — Auto-Trade suspended case 'circuit_breaker_tripped': return `Circuit breaker tripped${alert.system ? ` (${alert.system})` : ''}${alert.cb_reason ? `: ${alert.cb_reason}` : ''}. Auto-Trade suspended — turn it back ON on the Trump page to acknowledge and resume.` default: return 'Auto-trade event — check your account.' } } let _seq = 0 export default function TradeAlertBanner() { const { address } = useAccount() const [alerts, setAlerts] = useState([]) const dismiss = useCallback((id: number) => { setAlerts(prev => prev.filter(a => a.id !== id)) }, []) // Auto-dismiss each alert after AUTO_DISMISS_MS. // Effect re-runs whenever the alert list changes. For each alert we schedule // a timeout; cleanup cancels all pending timers when the list updates or // the component unmounts — prevents a dismissed-then-re-added alert from // triggering a ghost dismiss. useEffect(() => { if (alerts.length === 0) return const timers = alerts.map(a => window.setTimeout(() => dismiss(a.id), AUTO_DISMISS_MS) ) return () => timers.forEach(clearTimeout) }, [alerts, dismiss]) useWsSubscribe('trade_alert', useCallback((raw) => { const msg = raw as { wallet?: string; event?: string } & Record if (!address || msg.wallet?.toLowerCase() !== address.toLowerCase()) return const alert = { id: ++_seq, ...msg } as TradeAlert setAlerts(prev => [alert, ...prev].slice(0, 3)) }, [address])) // B47: reconcile_drift — DB↔HL position mismatch flagged by the reconciler. useWsSubscribe('reconcile_drift', useCallback((raw) => { const msg = raw as { wallet?: string } & Record if (!address || msg.wallet?.toLowerCase() !== address.toLowerCase()) return const alert = { id: ++_seq, event: 'reconcile_drift' as const, ...msg } as TradeAlert setAlerts(prev => [alert, ...prev].slice(0, 3)) }, [address])) // B53: circuit_breaker_tripped — Auto-Trade suspended by the CB. useWsSubscribe('circuit_breaker_tripped', useCallback((raw) => { const msg = raw as { wallet?: string; reason?: string } & Record if (!address || msg.wallet?.toLowerCase() !== address.toLowerCase()) return // Rename 'reason' → 'cb_reason' to avoid collision with the trade_alert 'reason' field. const { reason: cb_reason, ...rest } = msg const alert = { id: ++_seq, event: 'circuit_breaker_tripped' as const, cb_reason, ...rest } as TradeAlert setAlerts(prev => [alert, ...prev].slice(0, 3)) }, [address])) if (alerts.length === 0) return null return ( <>
{alerts.map(alert => (
⚠️
Auto-trade blocked {alertMessage(alert)}
))}
) }