mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
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:
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user