'use client' import { useEffect, useState } from 'react' import { useAccount, useConnect, useSignMessage } from 'wagmi' import type { BotPerformance } from '@/types' import { useDashboardStore } from '@/store/dashboard' import { getUserPublic, setHlApiKey, subscribe } from '@/lib/api' import { signRequest } from '@/lib/signedRequest' import { isUserRejection, walletErrorLabel } from '@/lib/walletError' import { getFirstReadyConnector, walletConnectErrorLabel } from '@/lib/walletConnect' // Action names must match backend/app/api/{user,subscribe}.py const ACTION_SET_API_KEY = 'set_hl_api_key' const ACTION_SUBSCRIBE = 'subscribe' interface Props { performance?: BotPerformance | null } type SaveState = 'idle' | 'signing' | 'saving' | 'success' | 'error' function fmtHold(s: number) { if (s < 60) return s + 's' const m = Math.floor(s / 60) if (m < 60) return m + 'm' return Math.floor(m / 60) + 'h ' + (m % 60) + 'm' } export default function BotPanel({ performance }: Props) { const { isSubscribed, hlApiKeySet, hlApiKeyMasked, botReadiness, setBotReadiness, setHlApiKeySet, setSubscribed } = useDashboardStore() const { address, isConnected } = useAccount() const { connectAsync, connectors } = useConnect() const { signMessageAsync } = useSignMessage() const [mounted, setMounted] = useState(false) const [apiKey, setApiKey] = useState('') const [saveState, setSaveState] = useState('idle') const [errorMsg, setErrorMsg] = useState('') const [subState, setSubState] = useState<'idle' | 'signing' | 'saving' | 'error'>('idle') const [subError, setSubError] = useState('') const [connectError, setConnectError] = useState('') useEffect(() => { setMounted(true) }, []) useEffect(() => { if (!isConnected || !address) { setSubscribed(false) setHlApiKeySet(false) setBotReadiness('unknown') return } setSubscribed(false) setHlApiKeySet(false) setBotReadiness('unknown') getUserPublic(address.toLowerCase()) .then((user) => { setSubscribed(user.active) setHlApiKeySet(user.hl_api_key_set) setBotReadiness(user.hl_api_key_set ? 'saved' : 'unknown') }) .catch(() => {}) }, [address, isConnected, setHlApiKeySet, setSubscribed, setBotReadiness]) 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 } async function handleSubscribe() { if (!address) return setSubError('') try { setSubState('signing') const env = await signRequest({ action: ACTION_SUBSCRIBE, wallet: address, body: null, signMessageAsync, }) setSubState('saving') await subscribe(env) await refreshUserState(address) setSubState('idle') } catch (err: unknown) { setSubError(walletErrorLabel(err, 'Signature cancelled', 120)) setSubState('error') } } async function handleSaveKey() { if (!address || !apiKey.trim()) return if (!apiKey.trim().startsWith('0x') || apiKey.trim().length !== 66) { setErrorMsg('Key must start with 0x and be 66 characters') setSaveState('error') return } setErrorMsg('') try { setSaveState('signing') const trimmed = apiKey.trim() const env = await signRequest({ action: ACTION_SET_API_KEY, wallet: address, body: { api_key: trimmed }, signMessageAsync, }) setSaveState('saving') const res = await setHlApiKey(env, trimmed) const pub = await refreshUserState(address) setHlApiKeySet(pub.hl_api_key_set, res.masked_key) setApiKey('') setSaveState('success') } catch (err: unknown) { if (isUserRejection(err)) { setErrorMsg('Signature cancelled') } else { setErrorMsg(walletErrorLabel(err, 'Signature cancelled', 120)) } setSaveState('error') } } const saveLabel = saveState === 'signing' ? 'Waiting for signature…' : saveState === 'saving' ? 'Saving…' : saveState === 'success' ? '✓ Saved' : 'Save key' async function handleConnectWallet() { setConnectError('') try { const connector = await getFirstReadyConnector(connectors) if (!connector) { setConnectError('No wallet connector is available right now.') return } await connectAsync({ connector }) } catch (err: unknown) { setConnectError(walletConnectErrorLabel(err)) } } return ( <> {/* Bot status card — dark design */}

Auto-trader

30 days
Net P&L
{performance ? (performance.net_pnl_usd >= 0 ? '+$' : '-$') + Math.abs(performance.net_pnl_usd).toLocaleString('en-US', { maximumFractionDigits: 0 }) : '—'}
Win rate
{performance ? (performance.win_rate * 100).toFixed(1) + '%' : '—'}
Trades
{performance?.total_trades ?? '—'}
Avg hold
{performance ? fmtHold(performance.avg_hold_seconds) : '—'}
{(!mounted || !isConnected) && ( <> {connectError && (

{connectError}

)} )} {mounted && isConnected && !isSubscribed && (
{subState === 'error' && subError && (

{subError}

)}
)} {mounted && isConnected && isSubscribed && !hlApiKeySet && (
↓ Paste your Hyperliquid API key below to finish setup
)} {mounted && isConnected && isSubscribed && hlApiKeySet && (
✓ Setup saved · verification still depends on backend
)}
{/* HL API Key card — only when subscribed */} {mounted && isConnected && isSubscribed && (

Hyperliquid API key

{hlApiKeySet && ✓ Connected}
{hlApiKeySet && !apiKey && (
{hlApiKeyMasked ?? '···'}
)} {(!hlApiKeySet || apiKey) && ( <>
{ setApiKey(e.target.value) if (saveState === 'error' || saveState === 'success') setSaveState('idle') }} placeholder="0x…" />
From{' '} app.hyperliquid.xyz/API
{saveState === 'error' && errorMsg && (

{errorMsg}

)} )} {!hlApiKeySet && (
How to get your API key
  1. Deposit USDC at app.hyperliquid.xyz
  2. Go to app.hyperliquid.xyz/API
  3. Click Generate API Wallet
  4. Sign with MetaMask (no gas)
  5. Copy the private key → paste above
)}
)} ) }