'use client'
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { useAccount, useSignMessage } from 'wagmi'
import { useDashboardStore } from '@/store/dashboard'
import { getUser, getUserPublic, setUserSettings, type UserSettings } from '@/lib/api'
import { getOrCreateViewEnvelope, signRequest } from '@/lib/signedRequest'
import BotPanel from '@/components/dashboard/BotPanel'
const ACTION_SET_SETTINGS = 'set_settings'
const ACTION_VIEW_USER = 'view_user'
const DEFAULT_SETTINGS: UserSettings = {
leverage: 3,
position_size_usd: 20,
take_profit_pct: null,
stop_loss_pct: null,
min_confidence: 80,
}
function LockedCard({ label }: { label: string }) {
return (
)
}
function BotSettings({
connected, subscribed, wallet, initial,
}: {
connected: boolean
subscribed: boolean
wallet?: string
initial: UserSettings
}) {
if (!connected) {
return (
Connect wallet to configure
Your wallet is used to verify access and sign bot commands. We never custody your funds.
document.querySelector('.connect-btn')?.click()}>
Connect wallet · free
)
}
if (!subscribed) {
return (
Subscribe to unlock the bot
Get automated execution, real-time alerts, and advanced configuration.
14-day free trial
{['Auto-execute trades on your exchange', 'SMS + Telegram alerts within 2s', 'Custom confidence + position-size rules', 'Backtest any strategy on 2y of data'].map(f => (
✓ {f}
))}
Activate on dashboard →
)
}
return
}
function BotSettingsForm({ wallet, initial }: { wallet: string; initial: UserSettings }) {
const { signMessageAsync } = useSignMessage()
const [s, setS] = useState(initial)
const [useTp, setUseTp] = useState(initial.take_profit_pct != null)
const [useSl, setUseSl] = useState(initial.stop_loss_pct != null)
const [state, setState] = useState<'idle' | 'signing' | 'saving' | 'ok' | 'err'>('idle')
const [err, setErr] = useState('')
// Re-sync when initial changes (after /user fetch resolves)
useEffect(() => {
setS(initial)
setUseTp(initial.take_profit_pct != null)
setUseSl(initial.stop_loss_pct != null)
}, [initial])
const dirty = JSON.stringify(s) !== JSON.stringify(initial) ||
useTp !== (initial.take_profit_pct != null) ||
useSl !== (initial.stop_loss_pct != null)
async function save() {
setErr('')
try {
const payload: UserSettings = {
...s,
take_profit_pct: useTp ? s.take_profit_pct ?? 2 : null,
stop_loss_pct: useSl ? s.stop_loss_pct ?? 1.5 : null,
}
setState('signing')
const env = await signRequest({
action: ACTION_SET_SETTINGS,
wallet,
body: payload,
signMessageAsync,
})
setState('saving')
await setUserSettings(env, payload)
setS(payload)
setState('ok')
setTimeout(() => setState('idle'), 2000)
} catch (e: unknown) {
const m = e instanceof Error ? e.message : 'Save failed'
setErr(m.includes('User rejected') || m.includes('denied') ? 'Signature cancelled' : m.slice(0, 140))
setState('err')
}
}
const label = state === 'signing' ? 'Waiting for signature…' : state === 'saving' ? 'Saving…' : state === 'ok' ? '✓ Saved' : 'Save changes'
return (
Execution
Position size (USD)
Fixed notional per trade. Margin = size / leverage.
setS({ ...s, position_size_usd: Math.max(5, +e.target.value || 0) })}
style={{ width: 140 }}
/>
margin ≈ ${(s.position_size_usd / s.leverage).toFixed(2)}
Leverage
Isolated margin, 1–50×. Higher = bigger PnL swings.
Minimum AI confidence
Platform floor is 80%. Raise to be pickier; can't lower.
{label}
{!dirty && state === 'idle' && No changes to save }
{state === 'err' && {err} }
)
}
function ExchangeSettings({ subscribed }: { subscribed: boolean }) {
if (!subscribed) return
return
}
function AccountSettings({ address }: { address?: string }) {
return (
)
}
export default function SettingsClient() {
const [section, setSection] = useState('bot')
const { address, isConnected } = useAccount()
const { signMessageAsync } = useSignMessage()
const { isSubscribed, setSubscribed, setHlApiKeySet } = useDashboardStore()
const [settings, setSettings] = useState(DEFAULT_SETTINGS)
const [loadErr, setLoadErr] = useState('')
useEffect(() => {
if (!isConnected || !address) return
let cancelled = false
// First fetch public state (no sig). Only request a view signature if user
// is actually subscribed — otherwise there's nothing private to fetch.
;(async () => {
try {
const pub = await getUserPublic(address.toLowerCase())
if (cancelled) return
setSubscribed(pub.active)
setHlApiKeySet(pub.hl_api_key_set)
if (!pub.active) return
const env = await getOrCreateViewEnvelope({
action: ACTION_VIEW_USER,
wallet: address,
signMessageAsync,
})
if (cancelled) return
const u = await getUser(address, env)
if (cancelled) return
setSubscribed(u.active)
setHlApiKeySet(u.hl_api_key_set, u.hl_api_key_masked ?? undefined)
if (u.settings) setSettings(u.settings)
} catch (e: unknown) {
const m = e instanceof Error ? e.message : 'Failed to load settings'
// Swallow cancelled signatures; surface real failures
if (!m.includes('User rejected') && !m.includes('denied')) {
setLoadErr(m.slice(0, 140))
}
}
})()
return () => { cancelled = true }
}, [address, isConnected, setSubscribed, setHlApiKeySet, signMessageAsync])
const sections = [
{ key: 'bot', label: 'Trading bot' },
{ key: 'exchange', label: 'Exchange keys' },
{ key: 'account', label: 'Account' },
]
return (
{sections.map(s => (
setSection(s.key)}>
{s.label}
))}
{loadErr && (
{loadErr}
)}
{section === 'bot' &&
}
{section === 'exchange' &&
}
{section === 'account' &&
}
)
}