'use client' import { useState, useEffect, useCallback } from 'react' import { useLocale } from 'next-intl' import { useAccount, useConnect, useSignMessage } from 'wagmi' import { getUserPublic, getUser, setUserSettings, setHlApiKey, setManualWindow, subscribe, type UserSettings, } from '@/lib/api' import { useDashboardStore } from '@/store/dashboard' import { signRequest, getOrCreateViewEnvelope, getCachedViewEnvelope, } from '@/lib/signedRequest' import { walletErrorLabel } from '@/lib/walletError' import { getFirstReadyConnector, walletConnectErrorLabel } from '@/lib/walletConnect' import { confirmSign } from '@/components/wallet/SignConfirmSheet' const DEFAULT_SETTINGS: UserSettings = { leverage: 3, position_size_usd: 20, take_profit_pct: 2, stop_loss_pct: 1.5, min_confidence: 70, daily_budget_usd: 15, sys2_leverage: 2, sys2_mode: 'standard', active_from: null, active_until: null, trump_enabled: false, macro_enabled: false, } function isoToLocalInput(iso: string | null): string { if (!iso) return '' const d = new Date(iso) if (isNaN(d.getTime())) return '' const pad = (n: number) => String(n).padStart(2, '0') return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}` } function localInputToIso(v: string): string | null { if (!v) return null const d = new Date(v) if (isNaN(d.getTime())) return null return d.toISOString() } export default function BotConfigPanel() { useLocale() // keep next-intl context hydrated even though i18n is shelved const { address, isConnected } = useAccount() const { connectAsync, connectors } = useConnect() const { signMessageAsync } = useSignMessage() const { isSubscribed, hlApiKeySet, hlApiKeyMasked, setSubscribed, setBotReadiness, setHlApiKeySet, } = useDashboardStore() const [settings, setSettings] = useState(DEFAULT_SETTINGS) const [tpConfigured, setTpConfigured] = useState(false) const [slConfigured, setSlConfigured] = useState(false) const [useBudget, setUseBudget] = useState(false) const [useSchedule, setUseSchedule] = useState(false) const [fromLocal, setFromLocal] = useState('') const [untilLocal, setUntilLocal] = useState('') const [dirty, setDirty] = useState(false) const [saveState, setSaveState] = useState<'idle'|'signing'|'saving'|'ok'|'err'>('idle') const [saveErr, setSaveErr] = useState('') const [apiKey, setApiKey] = useState('') const [keyState, setKeyState] = useState<'idle'|'signing'|'saving'|'ok'|'err'>('idle') const [keyErr, setKeyErr] = useState('') const [subState, setSubState] = useState<'idle'|'signing'|'saving'|'err'>('idle') const [subErr, setSubErr] = useState('') const [subPaperChoice, setSubPaperChoice] = useState<'paper' | 'live'>('paper') const [manualUntil, setManualUntil] = useState(null) const [mwState, setMwState] = useState<'idle'|'signing'|'saving'|'err'>('idle') const [mwErr, setMwErr] = useState('') const [mwTick, setMwTick] = useState(0) const [paperMode, setPaperMode] = useState(false) const [showAdvanced, setShowAdvanced] = useState(false) const [mounted, setMounted] = useState(false) const [loadState, setLoadState] = useState<'idle'|'loading'|'loaded'|'err'>('idle') const [loadErr, setLoadErr] = useState('') const [connectErr, setConnectErr] = useState('') useEffect(() => { setMounted(true) }, []) function applyUserPayload(u: { active: boolean hl_api_key_set: boolean hl_api_key_masked: string | null paper_mode?: boolean settings: UserSettings manual_window_until?: string | null }) { setSubscribed(u.active) setPaperMode(!!u.paper_mode) setHlApiKeySet(u.hl_api_key_set, u.hl_api_key_masked ?? undefined) setManualUntil(u.manual_window_until ?? null) if (u.settings) { const s = u.settings setTpConfigured(s.take_profit_pct != null) setSlConfigured(s.stop_loss_pct != null) const hasBudget = s.daily_budget_usd != null setUseBudget(hasBudget) setBotReadiness(u.hl_api_key_set ? 'saved' : 'unknown') setSettings({ ...s, take_profit_pct: s.take_profit_pct ?? 2, stop_loss_pct: s.stop_loss_pct ?? 1.5, daily_budget_usd: s.daily_budget_usd ?? 15, trump_enabled: s.trump_enabled ?? false, macro_enabled: s.macro_enabled ?? false, }) const hasSched = s.active_from != null && s.active_until != null setUseSchedule(hasSched) setFromLocal(isoToLocalInput(s.active_from)) setUntilLocal(isoToLocalInput(s.active_until)) } } useEffect(() => { if (!mounted || !isConnected || !address) return let cancelled = false ;(async () => { const pub = await getUserPublic(address.toLowerCase()).catch(() => null) if (!pub || cancelled) return setSubscribed(pub.active) setHlApiKeySet(pub.hl_api_key_set) setBotReadiness(pub.hl_api_key_set ? 'saved' : 'unknown') if (!pub.active) return const cached = getCachedViewEnvelope('view_user', address) if (!cached) return const u = await getUser(address, cached).catch(() => null) if (!u || cancelled) return applyUserPayload(u) setLoadState('loaded') })() return () => { cancelled = true } }, [address, isConnected, mounted]) // eslint-disable-line react-hooks/exhaustive-deps async function handleLoadSettings() { if (!address) return setLoadErr(''); setLoadState('loading') try { const ok = await confirmSign({ label: 'View account settings', description: 'Read your Trump Alpha subscription state, risk settings, and API key status. The signature is only for identity verification — no on-chain action is performed.', }) if (!ok) { setLoadState('idle'); return } const env = await getOrCreateViewEnvelope({ action: 'view_user', wallet: address, signMessageAsync }) const u = await getUser(address, env) applyUserPayload(u) setLoadState('loaded') } catch (e: unknown) { setLoadErr(walletErrorLabel(e, 'Cancelled', 120)) setLoadState('err') } } async function refreshUserState(wallet: string) { const pub = await getUserPublic(wallet.toLowerCase()) setSubscribed(pub.active) setHlApiKeySet(pub.hl_api_key_set) setBotReadiness(pub.hl_api_key_set ? 'saved' : 'unknown') return pub } function updateSettings(patch: Partial) { setSettings(s => ({ ...s, ...patch })) setDirty(true) } async function handleSubscribe() { if (!address) return const paperMode = subPaperChoice === 'paper' const ok = await confirmSign({ label: paperMode ? 'Start in paper mode' : 'Start live trading', description: paperMode ? 'Trades are simulated end-to-end — no funds are at risk. You can switch to live at any time from the settings panel.' : 'Start live trading. You\'ll need to add a Hyperliquid API key (next step) before the bot can open any real position.', danger: !paperMode, }) if (!ok) return setSubErr(''); setSubState('signing') try { const signedBody = paperMode ? { paper_mode: true } : null const env = await signRequest({ action: 'subscribe', wallet: address, body: signedBody, signMessageAsync }) setSubState('saving') await subscribe(env, { paper_mode: paperMode }) setSubscribed(true) setPaperMode(paperMode) void refreshUserState(address) setSubState('idle') setTpConfigured(false); setSlConfigured(false) setUseBudget(false); setLoadState('loaded') } catch (e: unknown) { setSubErr(walletErrorLabel(e, 'Cancelled', 100)) setSubState('err') } } async function handleSaveSettings() { if (!address) return const ok = await confirmSign({ label: 'Save trading settings', description: 'Settings are saved to the server and apply to all new trades from this point forward. Open positions are not affected.', }) if (!ok) return setSaveErr(''); setSaveState('signing') try { let schedFrom: string | null = null let schedUntil: string | null = null if (useSchedule) { schedFrom = localInputToIso(fromLocal) schedUntil = localInputToIso(untilLocal) if (!schedFrom || !schedUntil) { setSaveErr('Pick both a start and end time'); setSaveState('err'); return } if (new Date(schedUntil).getTime() <= new Date(schedFrom).getTime()) { setSaveErr('End must be after start'); setSaveState('err'); return } } const tp = settings.take_profit_pct, sl = settings.stop_loss_pct, bd = settings.daily_budget_usd if (trumpOn) { if (tp == null || tp < 0.1 || tp > 50) { setSaveErr('Take profit must be 0.1–50%'); setSaveState('err'); return } if (sl == null || sl < 0.1 || sl > 50) { setSaveErr('Stop loss must be 0.1–50%'); setSaveState('err'); return } } if (useBudget && (bd == null || bd <= 0 || bd > 100000)) { setSaveErr('Daily budget must be > $0'); setSaveState('err'); return } const payload: UserSettings = { ...settings, daily_budget_usd: useBudget ? bd : null, active_from: schedFrom, active_until: schedUntil } const env = await signRequest({ action: 'set_settings', wallet: address, body: payload, signMessageAsync }) setSaveState('saving') await setUserSettings(env, payload) setSettings(payload) setTpConfigured(tp != null); setSlConfigured(sl != null) setBotReadiness('saved'); setDirty(false) setSaveState('ok') setTimeout(() => setSaveState('idle'), 2500) } catch (e: unknown) { setSaveErr(walletErrorLabel(e, 'Cancelled', 100)) setSaveState('err') } } async function handleSaveKey() { if (!address || !apiKey.trim()) return const trimmed = apiKey.trim() if (!trimmed.startsWith('0x') || trimmed.length !== 66) { setKeyErr('Must be 0x + 64 hex chars'); setKeyState('err'); return } const ok = await confirmSign({ label: 'Link Hyperliquid API key', description: 'The API key is stored encrypted on the server and used by the bot to place trades on Hyperliquid for you.', danger: true, }) if (!ok) return setKeyErr(''); setKeyState('signing') try { const env = await signRequest({ action: 'set_hl_api_key', wallet: address, body: { api_key: trimmed }, signMessageAsync }) setKeyState('saving') const res = await setHlApiKey(env, trimmed) setHlApiKeySet(true, res.masked_key) setBotReadiness('saved') void refreshUserState(address) setApiKey(''); setKeyState('ok') } catch (e: unknown) { setKeyErr(walletErrorLabel(e, 'Cancelled', 100)) setKeyState('err') } } const handleConnectWallet = useCallback(async () => { setConnectErr('') try { const connector = await getFirstReadyConnector(connectors) if (!connector) { setConnectErr('No wallet connector is available right now.'); return } await connectAsync({ connector }) } catch (err: unknown) { setConnectErr(walletConnectErrorLabel(err)) } }, [connectAsync, connectors]) async function handleManualWindow(hours: number) { if (!address) return const ok = await confirmSign(hours === 0 ? { label: 'Cancel override window', description: 'The bot will stop accepting new trades from this override. Any open position risk controls (stop-loss, de-risk) remain active.', } : { label: `Open ${hours}h override window`, description: `The bot will accept qualifying signals for the next ${hours} hour${hours > 1 ? 's' : ''}, regardless of your trading schedule. Use this for high-conviction catalysts (e.g. CPI, FOMC). Stop-loss is always active.`, danger: true, } ) if (!ok) return setMwErr(''); setMwState('signing') try { const env = await signRequest({ action: 'set_manual_window', wallet: address, body: { hours }, signMessageAsync }) setMwState('saving') const r = await setManualWindow(env, hours) setManualUntil(r.manual_window_until); setMwState('idle') } catch (e: unknown) { setMwErr(walletErrorLabel(e, 'Cancelled', 100)) setMwState('err') } } async function handleUpgradeToLive() { if (!address) return const ok = await confirmSign({ label: 'Switch to live trading', description: 'Your subscription will change from paper mode to live. No trade opens immediately — you still need to add a Hyperliquid API key and enable a signal module before the bot can act.', danger: true, }) if (!ok) return setSubErr(''); setSubState('signing') try { // paper_mode=false → signedBody=null (same as original live subscribe) const env = await signRequest({ action: 'subscribe', wallet: address, body: null, signMessageAsync }) setSubState('saving') await subscribe(env, { paper_mode: false }) setPaperMode(false) void refreshUserState(address) setSubState('idle') } catch (e: unknown) { setSubErr(walletErrorLabel(e, 'Cancelled', 100)) setSubState('err') } } useEffect(() => { if (!manualUntil) return const id = setInterval(() => setMwTick(t => t + 1), 15000) return () => clearInterval(id) }, [manualUntil]) // ── Not connected ────────────────────────────────────────────────────────── if (!mounted || !isConnected) { return (
Connect your wallet to continue
Your subscription, API key, and trade settings are wallet-bound. No account needed — just sign a message.
{connectErr &&

{connectErr}

}
) } // ── helpers ──────────────────────────────────────────────────────────────── const Switch = ({ on, onChange, tone = 'amber' }: { on: boolean; onChange: (v: boolean) => void; tone?: 'amber' | 'up' }) => ( ) const trumpOn = !!settings.trump_enabled const macroOn = !!settings.macro_enabled // ── Not subscribed ───────────────────────────────────────────────────────── if (!isSubscribed) { return (
Set up your trading bot
The bot places trades on Hyperliquid with a trade-only API key — it can never withdraw funds. Start in paper mode to try it safely, or choose Live if you already have a Hyperliquid API key.
{/* Paper / Live choice */}
{subPaperChoice === 'paper' ? '📝 Paper mode — simulated trades, no real funds' : '💰 Live trading — real positions on Hyperliquid'}
{subPaperChoice === 'paper' ? 'Nothing is at risk. No Hyperliquid API key needed. You can switch to live at any time.' : 'Real positions will be opened and managed on Hyperliquid. A trade-only API key is required before the first trade.'}
Paper { setSubPaperChoice(v ? 'live' : 'paper') // Clear any stale subscribe-error from a previous attempt so // the user sees a clean state for the new paper/live choice. if (subState === 'err') { setSubState('idle'); setSubErr('') } }} tone="amber" /> Live
{subState === 'err' && {subErr}}
) } // ── Subscribed but settings not loaded yet ───────────────────────────────── if (loadState !== 'loaded') { return (
Sign in to view your settings
Your settings are private and wallet-bound. Sign once to load them — the permission is cached for 4 minutes so you won't be prompted again while you browse.
{loadState === 'err' && {loadErr}}
) } // ── Main config (subscribed + loaded) ────────────────────────────────────── // Readiness check — shown at bottom near save button const missingItems: string[] = [] if (!paperMode && !hlApiKeySet) missingItems.push('link a Hyperliquid API key') if (!trumpOn && !macroOn) missingItems.push('enable Trump Signal or Macro Vibes below') if (trumpOn && !tpConfigured) missingItems.push('set a Trump Signal take-profit %') if (trumpOn && !slConfigured) missingItems.push('set a Trump Signal stop-loss %') const botReady = missingItems.length === 0 // Manual window countdown void mwTick const untilMs = manualUntil ? new Date(manualUntil).getTime() : 0 const remainingMin = untilMs ? Math.max(0, Math.round((untilMs - Date.now()) / 60000)) : 0 const armed = remainingMin > 0 const remainingLabel = remainingMin >= 60 ? `${Math.floor(remainingMin / 60)}h ${remainingMin % 60}m` : `${remainingMin}m` const mwBusy = mwState === 'signing' || mwState === 'saving' const saveBtnLabel = saveState === 'signing' ? 'Sign in wallet…' : saveState === 'saving' ? 'Saving…' : saveState === 'ok' ? '✓ Saved' : 'Save changes' return (
{/* ── HL API Key ─────────────────────────────────────────────────────── */}
{/* Status dot */}
{(paperMode || hlApiKeySet) ? '✓' : '◇'}
Hyperliquid API wallet
{paperMode ? 📝 Paper mode — no real money involved. Add an API key below to switch to live. : hlApiKeySet && !apiKey ? <>{hlApiKeyMasked ?? '···'} · trade-only · cannot withdraw : 'Generate at app.hyperliquid.xyz/API — paste the private key here. Never your MetaMask key.'}
{/* Paper mode: show upgrade path instead of key input */} {paperMode && ( )} {/* Live mode: show key input */} {!paperMode && (hlApiKeySet && !apiKey ? ( ) : (
{ setApiKey(e.target.value); if (keyState !== 'idle') setKeyState('idle') }} placeholder="0x…" style={{ width: 200, maxWidth: '100%', fontFamily: 'var(--mono)', fontSize: 12, padding: '8px 10px' }} />
))}
{keyState === 'err' &&
{keyErr}
} {keyState === 'ok' &&
Saved. Run one small test trade to confirm the live path end-to-end.
} {subState === 'err' && subErr &&
{subErr}
}
{/* ── Trump Signal ───────────────────────────────────────────────────── */}
{/* Header */}
Trump Signal
{trumpOn ? 'Opens positions when a Trump Truth Social post fires a signal.' : 'Disabled — bot ignores Trump posts.'}
updateSettings({ trump_enabled: v })} tone="amber" />
{/* Form — only when enabled */} {trumpOn && (
Per-trade size Notional in USD — margin ≈ ${(settings.position_size_usd / settings.leverage).toFixed(2)} at {settings.leverage}×
$ updateSettings({ position_size_usd: Math.max(5, +e.target.value || 0) })} />
$5 – $10,000
Leverage Event-driven scalp — use lower leverage if unsure
updateSettings({ leverage: +e.target.value })} />
25×50×
{settings.leverage}×
Min AI confidence Skip signals below this score (0 = take all, 100 = only the highest-conviction)
updateSettings({ min_confidence: +e.target.value })} />
050100
{settings.min_confidence}%
Take profit * Auto-close when unrealised gain hits this target. Required.
updateSettings({ take_profit_pct: e.target.value === '' ? null : +e.target.value })} /> % gain
{!tpConfigured && Required — not saved}
Stop loss * Auto-close when drawdown hits this limit. Required.
updateSettings({ stop_loss_pct: e.target.value === '' ? null : +e.target.value })} /> % loss
{!slConfigured && Required — not saved}
)}
{/* ── Macro Vibes ────────────────────────────────────────────────────── */}
{/* Header */}
Macro Vibes
{macroOn ? 'You open a BTC long on Hyperliquid, then use /adopt in the Telegram bot to hand it to the bot for exit management.' : 'Disabled — Macro Vibes alerts will not include bot management instructions.'}
updateSettings({ macro_enabled: v })} tone="up" />
{/* Form — only when enabled */} {macroOn && (() => { const aggressive = settings.sys2_mode === 'aggressive' const defLev = aggressive ? 8 : 2 const lev = Math.max(1, Math.min(10, settings.sys2_leverage ?? defLev)) const prot = Math.min(35, 0.85 * 100 / lev) const liq = 100 / lev const risky = lev > 2 return (
BTC strategy mode Standard is calmer. Aggressive uses higher leverage and wider pyramiding.
{aggressive && (
High-risk sleeve. More leverage, harder pyramiding, wider peak-trail. Treat as a separate risk bucket.
)}
BTC bottom leverage Separate from Trump leverage. The bot de-risks in stages before exchange liquidation.
updateSettings({ sys2_leverage: +e.target.value })} />
10×
{lev}×
At {lev}× it sheds ⅓ near −{(prot * 0.6).toFixed(0)}%, ⅓ near −{(prot * 0.8).toFixed(0)}%, fully out by −{prot.toFixed(0)}%. Exchange liquidation ≈ −{liq.toFixed(0)}%. {risky ? ' Above 2×, a normal correction can push you out early.' : ' Wide enough to survive a normal correction.'}
) })()}
{/* ── Advanced / Global controls ─────────────────────────────────────── */}
{showAdvanced && (
{/* Daily spend cap */}
Daily trading cap Optional — bot stops opening new trades once total notional crosses this limit in a UTC day.
{ setUseBudget(v); setDirty(true) }} /> {useBudget ? (
$ updateSettings({ daily_budget_usd: e.target.value === '' ? null : Math.max(0, +e.target.value) })} /> /day
) : ( No cap )}
{/* Schedule */}
Trading schedule Bot only accepts signals inside this window. Times in your browser's local timezone. Leave off to trade anytime.
{ setUseSchedule(v); setDirty(true) }} /> {useSchedule && (
From { setFromLocal(e.target.value); setDirty(true) }} style={{ width: 190 }} />
Until { setUntilLocal(e.target.value); setDirty(true) }} style={{ width: 190 }} />
)}
{/* Manual window */}
Override window Open a timed override so the bot accepts signals for the next 1–24 hours — useful for high-conviction catalysts like CPI or FOMC releases.
{armed ? ( <> ● Override active — {remainingLabel} remaining ) : ( [1, 4, 24].map(h => ( )) )} {mwState === 'err' && {mwErr}}
)}
{/* ── Save ───────────────────────────────────────────────────────────── */}
{/* Status */}
{saveState === 'err' ? ( ⚠ {saveErr} ) : botReady ? ( ✓ Bot is configured and ready ) : ( Still needed: {missingItems.join(' · ')} )}
{/* Button */}
) }