feat: add exchange account states and refine beginner trader creation flow (#1450)

* feat: implement exchange account state management and UI updates

- Added functionality to invalidate exchange account state cache on exchange config updates, creation, and deletion.
- Introduced new API endpoint to fetch exchange account states.
- Updated frontend components to display exchange account states, including status and balance information.
- Enhanced user experience by refreshing exchange account states after relevant actions.

* feat: enhance trader creation readiness in AITradersPage and BeginnerGuideCards

---------

Co-authored-by: Dean <afei.wuhao@gmail.com>
This commit is contained in:
deanokk
2026-04-01 16:26:04 +08:00
committed by GitHub
parent 9937542020
commit 9a80f1d88d
10 changed files with 551 additions and 186 deletions

View File

@@ -7,6 +7,7 @@ import type {
CreateTraderRequest,
AIModel,
Exchange,
ExchangeAccountState,
} from '../../types'
import { useLanguage } from '../../contexts/LanguageContext'
import { t } from '../../i18n/translations'
@@ -106,6 +107,18 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
api.getTraders,
{ refreshInterval: 5000 }
)
const {
data: exchangeAccountStateData,
mutate: mutateExchangeAccountStates,
isLoading: isExchangeAccountStatesLoading,
} = useSWR<{ states: Record<string, ExchangeAccountState> }>(
user && token ? 'exchange-account-state' : null,
api.getExchangeAccountState,
{
refreshInterval: 30000,
shouldRetryOnError: false,
}
)
const { data: strategies } = useSWR<Strategy[]>(
user && token ? 'strategies' : null,
api.getStrategies,
@@ -537,6 +550,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const refreshedExchanges = await api.getExchangeConfigs()
setAllExchanges(refreshedExchanges)
await mutateExchangeAccountStates()
setShowExchangeModal(false)
setEditingExchange(null)
@@ -618,6 +632,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const refreshedExchanges = await api.getExchangeConfigs()
setAllExchanges(refreshedExchanges)
await mutateExchangeAccountStates()
setShowExchangeModal(false)
setEditingExchange(null)
@@ -665,6 +680,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const claw402Configured = configuredModels.some((model) => model.provider === 'claw402')
const hasStrategies = (strategies?.length || 0) > 0
const hasCreatedTrader = (traders?.length || 0) > 0
const canCreateTrader = configuredModels.length > 0 && configuredExchanges.length > 0
return (
@@ -744,6 +760,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
claw402Ready={claw402Configured}
exchangeReady={configuredExchanges.length > 0}
strategyReady={hasStrategies}
traderReady={hasCreatedTrader}
canCreateTrader={canCreateTrader}
walletAddress={beginnerWalletAddress}
onQuickSetupClaw402={handleQuickSetupClaw402}
@@ -757,6 +774,8 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<ConfigStatusGrid
configuredModels={configuredModels}
configuredExchanges={configuredExchanges}
exchangeAccountStates={exchangeAccountStateData?.states}
isExchangeAccountStatesLoading={isExchangeAccountStatesLoading}
visibleExchangeAddresses={visibleExchangeAddresses}
copiedId={copiedId}
language={language}

View File

@@ -5,6 +5,7 @@ interface BeginnerGuideCardsProps {
claw402Ready: boolean
exchangeReady: boolean
strategyReady: boolean
traderReady: boolean
canCreateTrader: boolean
walletAddress?: string | null
onQuickSetupClaw402: () => void
@@ -23,6 +24,7 @@ export function BeginnerGuideCards({
claw402Ready,
exchangeReady,
strategyReady,
traderReady,
canCreateTrader,
walletAddress,
onQuickSetupClaw402,
@@ -109,15 +111,25 @@ export function BeginnerGuideCards({
desc: isZh
? '最后一步,把模型和交易所绑在一起,就能开始运行。'
: 'Last step: bind your model and exchange, then start running.',
meta: canCreateTrader
meta: traderReady
? isZh
? '已经可以创建'
: 'Ready to create'
? '已创建 Trader可继续添加'
: 'Trader created, you can add more'
: canCreateTrader
? isZh
? '已经可以创建'
: 'Ready to create'
: isZh
? '先完成前步'
: 'Finish the first two steps first',
ready: canCreateTrader,
actionLabel: isZh ? '立即创建' : 'Create now',
? '先完成前步'
: 'Finish the first three steps first',
ready: traderReady,
actionLabel: traderReady
? isZh
? '继续创建'
: 'Create another'
: isZh
? '立即创建'
: 'Create now',
onAction: onCreateTrader,
disabled: !canCreateTrader,
},

View File

@@ -6,7 +6,7 @@ import {
Copy,
Check,
} from 'lucide-react'
import type { AIModel, Exchange } from '../../types'
import type { AIModel, Exchange, ExchangeAccountState } from '../../types'
import type { Language } from '../../i18n/translations'
import { t } from '../../i18n/translations'
import { getModelIcon } from '../common/ModelIcons'
@@ -25,6 +25,8 @@ interface UsageInfo {
interface ConfigStatusGridProps {
configuredModels: AIModel[]
configuredExchanges: Exchange[]
exchangeAccountStates?: Record<string, ExchangeAccountState>
isExchangeAccountStatesLoading?: boolean
visibleExchangeAddresses: Set<string>
copiedId: string | null
language: Language
@@ -41,6 +43,8 @@ interface ConfigStatusGridProps {
export function ConfigStatusGrid({
configuredModels,
configuredExchanges,
exchangeAccountStates,
isExchangeAccountStatesLoading,
visibleExchangeAddresses,
copiedId,
language,
@@ -53,6 +57,48 @@ export function ConfigStatusGrid({
onToggleExchangeAddress,
onCopyAddress,
}: ConfigStatusGridProps) {
const getExchangeStateMeta = (state: ExchangeAccountState | undefined) => {
if (!state) {
return {
label: language === 'zh' ? '未检查' : 'NOT CHECKED',
className: 'text-zinc-400 border-zinc-700/80 bg-zinc-900/40',
}
}
switch (state.status) {
case 'ok':
return {
label: state.display_balance || '0',
className: 'text-emerald-300 border-emerald-500/20 bg-emerald-500/10',
}
case 'disabled':
return {
label: language === 'zh' ? '已禁用' : 'DISABLED',
className: 'text-zinc-400 border-zinc-700/80 bg-zinc-900/40',
}
case 'missing_credentials':
return {
label: language === 'zh' ? '配置不完整' : 'INCOMPLETE',
className: 'text-amber-300 border-amber-500/20 bg-amber-500/10',
}
case 'invalid_credentials':
return {
label: language === 'zh' ? '密钥无效' : 'INVALID KEYS',
className: 'text-rose-300 border-rose-500/20 bg-rose-500/10',
}
case 'permission_denied':
return {
label: language === 'zh' ? '无余额权限' : 'NO PERMISSION',
className: 'text-orange-300 border-orange-500/20 bg-orange-500/10',
}
default:
return {
label: language === 'zh' ? '暂时无法获取' : 'UNAVAILABLE',
className: 'text-zinc-300 border-zinc-600/60 bg-zinc-800/50',
}
}
}
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* AI Models Card */}
@@ -149,6 +195,8 @@ export function ConfigStatusGrid({
{configuredExchanges.map((exchange) => {
const inUse = isExchangeInUse(exchange.id)
const usageInfo = getExchangeUsageInfo(exchange.id)
const state = exchangeAccountStates?.[exchange.id]
const stateMeta = getExchangeStateMeta(state)
return (
<div
key={exchange.id}
@@ -174,6 +222,18 @@ export function ConfigStatusGrid({
<div className="text-[10px] text-zinc-500 font-mono flex items-center gap-2">
{exchange.type?.toUpperCase() || 'CEX'}
</div>
<div className="mt-1 flex flex-wrap items-center gap-2 text-[10px] font-mono">
<span className={`rounded border px-1.5 py-0.5 ${stateMeta.className}`}>
{isExchangeAccountStatesLoading && !state
? (language === 'zh' ? '检查中...' : 'CHECKING...')
: stateMeta.label}
</span>
{state?.status !== 'ok' && state?.error_message ? (
<span className="text-zinc-500 truncate max-w-[220px]">
{state.error_message}
</span>
) : null}
</div>
</div>
</div>

View File

@@ -1,6 +1,7 @@
import type {
AIModel,
Exchange,
ExchangeAccountStateResponse,
UpdateModelConfigRequest,
UpdateExchangeConfigRequest,
CreateExchangeRequest,
@@ -73,6 +74,16 @@ export const configApi = {
return result.data!
},
async getExchangeAccountState(): Promise<ExchangeAccountStateResponse> {
const result = await httpClient.get<ExchangeAccountStateResponse>(
`${API_BASE}/exchanges/account-state`
)
if (!result.success || !result.data) {
throw new Error('Failed to fetch exchange account states')
}
return result.data
},
async getSupportedExchanges(): Promise<Exchange[]> {
const result = await httpClient.get<Exchange[]>(
`${API_BASE}/supported-exchanges`

View File

@@ -41,6 +41,30 @@ export interface Exchange {
lighterApiKeyIndex?: number
}
export type ExchangeAccountStatus =
| 'ok'
| 'disabled'
| 'missing_credentials'
| 'invalid_credentials'
| 'permission_denied'
| 'unavailable'
export interface ExchangeAccountState {
exchange_id: string
status: ExchangeAccountStatus
display_balance?: string
asset?: string
total_equity?: number
available_balance?: number
checked_at: string
error_code?: string
error_message?: string
}
export interface ExchangeAccountStateResponse {
states: Record<string, ExchangeAccountState>
}
export interface CreateExchangeRequest {
exchange_type: string // "binance", "bybit", "okx", "hyperliquid", "aster", "lighter"
account_name: string // User-defined account name