'use client' import Link from 'next/link' import { useState, useEffect, useCallback, useRef } from 'react' import { useLocale } from 'next-intl' import { useAccount, useSignMessage } from 'wagmi' import { getUserPublic, setAutoTrade, type UserPublic } from '@/lib/api' import { signRequest } from '@/lib/signedRequest' import { isUserRejection, walletErrorLabel } from '@/lib/walletError' import { confirmSign } from '@/components/wallet/SignConfirmSheet' /** * ONE control: the master Auto-Trade switch. * * OFF (default, safe) — signals are still scanned & shown in the feed, * but NO trade is opened. Pure monitoring. * ON — a qualifying signal auto-opens a trade (full * System-2 risk: dynamic leverage, staged de-risk, * ratchet, peak-trail). De-risk / stop-loss always * run on open positions regardless — never toggleable. * * Replaces the old scanner-toggle / timed manual-window / schedule / paper * trio. Circuit breaker is read-only; turning Auto-Trade ON acknowledges + * clears a tripped breaker. Per-trade pyramiding is the "Grow" switch on * each open position (Trades page), not here. */ const SYS = { trump: { idx: '', name: 'Trump Signal', accent: '#b45309', soft: 'rgba(180,83,9,0.08)' }, btc: { idx: '', name: 'Macro Vibes', accent: '#16a34a', soft: 'rgba(22,163,74,0.08)' }, } as const const ROW: React.CSSProperties = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, padding: '8px 0', flexWrap: 'wrap', } const LABEL: React.CSSProperties = { fontSize: 13, color: 'var(--ink-2)' } export default function SystemControl({ system }: { system: 'trump' | 'btc' }) { const locale = useLocale() const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json const s = SYS[system] const { address, isConnected } = useAccount() const { signMessageAsync } = useSignMessage() const [mounted, setMounted] = useState(false) const [pub, setPub] = useState(null) const [busy, setBusy] = useState(false) const [err, setErr] = useState('') useEffect(() => { setMounted(true) }, []) // Generation counter: each address change increments it so any in-flight // refresh from the previous address can detect it's stale. // `snap !== address` inside a useCallback is a stale-closure trap — both // refer to the same closed-over value so the check is always false. const genRef = useRef(0) useEffect(() => { genRef.current++ }, [address]) const refresh = useCallback(async () => { if (!address) { setPub(null); return } const gen = genRef.current const p = await getUserPublic(address.toLowerCase()).catch(() => null) if (gen !== genRef.current) return // wallet changed while in-flight setPub(p) }, [address]) useEffect(() => { refresh() const poll = setInterval(refresh, 30_000) return () => clearInterval(poll) }, [refresh]) const subscribed = !!pub?.active const paper = !!pub?.paper_mode const autoOn = !!pub?.auto_trade const cbTripped = !!pub?.circuit_breaker_tripped_at && (Date.now() - new Date(pub!.circuit_breaker_tripped_at!).getTime()) < 24 * 3600 * 1000 async function flipAuto(on: boolean) { if (busy) return if (!address) { setErr(isZh ? '请先连接右上角的钱包。' : 'Connect your wallet first (top-right).'); return } if (!subscribed) { setErr(isZh ? '钱包尚未订阅,请先去设置页完成订阅。' : 'Wallet not subscribed — subscribe on the Settings page first.'); return } // B51: live users without an HL API key cannot actually execute trades — // block the toggle so the ON state never silently becomes a no-op. if (on && !paper && !pub?.hl_api_key_set) { setErr(isZh ? '未绑定 Hyperliquid API Key。请先在设置页保存 API Key,Auto-Trade 才能真实下单。' : 'No Hyperliquid API key saved. Add your HL API key on the Settings page before enabling Auto-Trade.') return } if (autoOn === on) return // Claim the busy slot BEFORE awaiting confirmSign — otherwise a rapid // second click on the same pill (or the opposite pill) slips past the // `if (busy) return` guard and queues a second sheet + second signature. // We still clear it in `finally` below. setBusy(true); setErr('') // Show pre-signing confirmation sheet before triggering MetaMask const confirmed = await confirmSign(on ? { label: paper ? (isZh ? '开启 Auto-Trade(模拟模式)' : 'Enable Auto-Trade (paper mode)') : (isZh ? '开启 Auto-Trade(真实资金)' : 'Enable Auto-Trade (live capital)'), description: paper ? (isZh ? '机器人将在信号触发时自动开仓(Paper 模式,无真实资金)。钱包签名仅用于身份验证。' : 'The bot will auto-open positions when a qualifying signal fires in paper mode. No real capital is used. The wallet signature is only for identity verification.') : (isZh ? '机器人将在 Hyperliquid 上用真实资金自动开仓。止损与分级减仓始终生效保护仓位。' : 'The bot will auto-open real positions on Hyperliquid. Stop-loss and staged de-risking remain active to protect the position.'), danger: !paper, } : { label: isZh ? '关闭 Auto-Trade' : 'Disable Auto-Trade', description: isZh ? '停止自动开仓。已有持仓的止损与减仓逻辑仍会继续运行。' : 'Stop opening new trades automatically. Risk controls on existing positions will keep running.', danger: false, } ) if (!confirmed) { setBusy(false); return } // release the slot we claimed try { const env = await signRequest({ action: 'set_auto_trade', wallet: address, body: { enabled: on }, signMessageAsync, }) const r = await setAutoTrade(env, on) setPub(p => p ? { ...p, auto_trade: r.auto_trade, ...(r.circuit_breaker_cleared ? { circuit_breaker_tripped_at: null, circuit_breaker_reason: null } : {}), } : p) } catch (e) { if (isUserRejection(e)) { setErr(isZh ? '已取消' : 'Cancelled') } else { setErr(walletErrorLabel(e, isZh ? '已取消' : 'Cancelled', 110) || (isZh ? 'Auto-Trade 切换失败' : 'Auto-Trade toggle failed')) } } finally { setBusy(false) } } // SSR / pre-hydration: render nothing to avoid hydration mismatch with // wagmi state that resolves client-side. if (!mounted) return null const settingsHref = `/${locale}/settings#bot-config` const settingsLabel = system === 'trump' ? 'Trump Signal' : 'Macro Vibes' // Macro Vibes (System 2) is MANAGE-ONLY by design — it NEVER auto-opens a // trade. bot_engine.process_post early-returns for sys2 sources, so the // master Auto-Trade switch only governs Trump (System 1). Presenting an // ON/OFF auto-trade toggle here would (a) falsely imply Macro signals // auto-open and (b) be a confusing second copy of the SAME global switch // shown on the Trump page. Show the adopt-only flow instead. if (system === 'btc') { // Compact inline strip — signal fires → you open → bot manages. // No big card; the data panel is the main content. Settings link stays // for users who want to configure, but the explanation is one line. return (
You open · bot manages exit Signal → Telegram → open on Hyperliquid → /adopt {settingsLabel} settings ↗
) } // Wallet not connected: a previous version showed a giant full-width // "Connect a wallet..." card here, which was redundant with the navbar's // Connect button. The opposite extreme — rendering null — silently hid // the existence of Auto-Trade for unconnected users. Compromise: a SLIM // inline strip that simply announces the feature without pushing the // page around. Once connected, the full control panel renders below. if (!isConnected) { return (
{s.name} Auto-Trade · OFF — connect wallet (top-right) to enable
Settings {settingsLabel}
) } // ── Net result — the single sentence that matters ───────────────────────── let netOk = false let netMsg = '' if (!subscribed) { netMsg = isZh ? '钱包还没订阅,请先去设置页完成订阅。' : 'Wallet not subscribed — subscribe on the Settings page first.' } else if (cbTripped) { netMsg = isZh ? `熔断器已触发(${pub?.circuit_breaker_reason || '风险限制'})。重新打开 Auto-Trade 才会确认并恢复。` : `Circuit breaker tripped (${pub?.circuit_breaker_reason || 'risk limit'}). Turn Auto-Trade ON to acknowledge & resume.` } else if (!autoOn) { netMsg = isZh ? 'Auto-Trade 当前为关闭: Trump 信号仍会显示,但不会自动开仓。' : 'Auto-Trade is OFF — Trump signals are shown in the feed but NOT traded.' } else if (pub?.trump_enabled === false) { // Auto-Trade is ON but the Trump (System-1) gate is OFF, so the backend // skips every Trump signal (bot_engine: trump_enabled OFF → not traded). // Without this branch the UI falsely promised "next signal auto-opens". netMsg = isZh ? 'Auto-Trade 已开启,但 Trump 系统①未启用: 在设置页打开 Trump 开关后才会自动开仓。' : 'Auto-Trade is ON, but the Trump system is disabled — enable Trump in Settings before signals will trade.' } else { netOk = true netMsg = isZh ? `Auto-Trade 已开启(Trump 系统①): 下一条合格 Trump 信号会自动开出${paper ? '模拟' : '真实'}仓位。` : `Auto-Trade ON — will open a ${paper ? 'PAPER' : 'LIVE'} trade on the next qualifying Trump signal.` } const Pill = ({ active, onClick, children, tone }: { active: boolean; onClick: () => void; children: React.ReactNode tone: 'green' | 'idle' }) => { const bg = active ? (tone === 'green' ? 'var(--up, #16a34a)' : 'var(--ink-3)') : 'transparent' return ( ) } return (
{/* Header */}
{s.name} {isZh ? '控制面板' : '— Auto-Trade'}
{/* Master Auto-Trade switch */}
Auto-Trade {isZh ? '· 事件交易' : '· event-driven'}
{isZh ? 'OFF = 只监控信号 · ON = 自动开仓' : 'OFF = monitor only · ON = auto-open on qualifying Trump signals'}
flipAuto(true)} tone="green">ON flipAuto(false)} tone="idle">OFF
{/* Read-only status */}
{isZh ? '执行模式' : 'Execution mode'} {paper ? (isZh ? '📝 模拟' : '📝 PAPER') : (isZh ? '💰 真实资金' : '💰 LIVE')} {isZh ? '· 订阅时确定' : '· set when subscribing'}
{isZh ? '熔断器' : 'Circuit breaker'} {cbTripped ? (isZh ? `🚨 已触发(${pub?.circuit_breaker_reason})` : `🚨 tripped (${pub?.circuit_breaker_reason})`) : (isZh ? '✓ 正常' : '✓ clear')}
{err && (
● {err}
)}
{/* Net result */}
{isZh ? '⟹ 当前状态' : '⟹ Current status'}
{netOk ? '✅ ' : '⛔ '}{netMsg}
Settings {settingsLabel}
) }