mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-04 11:30:58 +08:00
fix: improve trader error feedback, stale balance cleanup, and claw402 warnings (#1452)
* fix: improve trader error handling and balance validation * fix: localize structured trader failure reasons --------- Co-authored-by: apple <apple@MacbookPro-zbh.local>
This commit is contained in:
@@ -21,6 +21,7 @@ import { ConfigStatusGrid } from './ConfigStatusGrid'
|
||||
import { TradersList } from './TradersList'
|
||||
import { BeginnerGuideCards } from './BeginnerGuideCards'
|
||||
import {
|
||||
AlertTriangle,
|
||||
Bot,
|
||||
Plus,
|
||||
MessageCircle,
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
setBeginnerWalletAddress as persistBeginnerWalletAddress,
|
||||
} from '../../lib/onboarding'
|
||||
import type { Strategy } from '../../types'
|
||||
import { ApiError } from '../../lib/httpClient'
|
||||
|
||||
interface AITradersPageProps {
|
||||
onTraderSelect?: (traderId: string) => void
|
||||
@@ -59,6 +61,189 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
const [quickSetupLoading, setQuickSetupLoading] = useState(false)
|
||||
const [beginnerWalletAddress, setBeginnerWalletAddress] = useState<string | null>(() => getBeginnerWalletAddress())
|
||||
const isBeginnerMode = getUserMode() === 'beginner'
|
||||
const getErrorMessage = (error: unknown, fallback: string) => {
|
||||
if (error instanceof Error && error.message.trim() !== '') {
|
||||
return error.message
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
const formatActionableDescriptionByKey = (
|
||||
errorKey: string,
|
||||
params: Record<string, string> = {},
|
||||
fallback: string
|
||||
) => {
|
||||
const traderName = params.trader_name || params.traderName || 'this trader'
|
||||
const modelName = params.model_name || params.modelName || 'selected model'
|
||||
const exchangeName = params.exchange_name || params.exchangeName || 'selected exchange account'
|
||||
const reason = localizeTraderReason(params.reason_key, params.reason || fallback)
|
||||
const symbol = params.symbol || ''
|
||||
|
||||
const zh = language === 'zh'
|
||||
|
||||
switch (errorKey) {
|
||||
case 'trader.create.invalid_request':
|
||||
return zh ? '提交的信息不完整,或者格式不正确。请检查后重新提交。' : 'The submitted information is incomplete or invalid. Please review it and try again.'
|
||||
case 'trader.create.invalid_btc_eth_leverage':
|
||||
return zh ? 'BTC/ETH 杠杆倍数需要在 1 到 50 倍之间。' : 'BTC/ETH leverage must be between 1x and 50x.'
|
||||
case 'trader.create.invalid_altcoin_leverage':
|
||||
return zh ? '山寨币杠杆倍数需要在 1 到 20 倍之间。' : 'Altcoin leverage must be between 1x and 20x.'
|
||||
case 'trader.create.invalid_symbol':
|
||||
return zh ? `交易对 ${symbol} 的格式不正确,目前只支持以 USDT 结尾的合约交易对。` : `Trading pair ${symbol} is invalid. Only perpetual pairs ending with USDT are supported.`
|
||||
case 'trader.create.model_not_found':
|
||||
return zh ? '还没有找到你选择的 AI 模型。请先到「设置 > 模型配置」添加并启用一个可用模型。' : 'The selected AI model was not found. Please add and enable a valid model in Settings > Model Config.'
|
||||
case 'trader.create.model_disabled':
|
||||
return zh ? `AI 模型「${modelName}」目前还没有启用。请先启用它再创建机器人。` : `AI model "${modelName}" is currently disabled. Please enable it before creating a trader.`
|
||||
case 'trader.create.model_missing_credentials':
|
||||
return zh ? `AI 模型「${modelName}」缺少 API Key 或支付凭证。请先补全模型配置。` : `AI model "${modelName}" is missing API credentials or payment setup. Please complete the model configuration first.`
|
||||
case 'trader.create.strategy_required':
|
||||
return zh ? '你还没有选择交易策略。请先选择一个策略,再继续创建机器人。' : 'No trading strategy is selected yet. Please choose a strategy before creating a trader.'
|
||||
case 'trader.create.strategy_not_found':
|
||||
return zh ? '你选择的策略不存在,或者已经被删除了。请重新选择一个可用策略。' : 'The selected strategy no longer exists. Please choose another available strategy.'
|
||||
case 'trader.create.exchange_not_found':
|
||||
return zh ? '还没有找到你选择的交易所账户。请先到「设置 > 交易所配置」添加一个可用账户。' : 'The selected exchange account was not found. Please add an exchange account in Settings > Exchange Config.'
|
||||
case 'trader.create.exchange_disabled':
|
||||
return zh ? `交易所账户「${exchangeName}」目前处于未启用状态。请先启用它。` : `Exchange account "${exchangeName}" is currently disabled. Please enable it first.`
|
||||
case 'trader.create.exchange_missing_fields':
|
||||
return zh ? `交易所账户「${exchangeName}」的配置还不完整。请先补全必填信息。` : `Exchange account "${exchangeName}" is incomplete. Please fill in the required fields first.`
|
||||
case 'trader.create.exchange_unsupported':
|
||||
return zh ? `交易所账户「${exchangeName}」当前类型暂不支持机器人创建。` : `Exchange account "${exchangeName}" uses a type that is not supported for trader creation.`
|
||||
case 'trader.create.exchange_probe_failed':
|
||||
return zh ? `交易所账户「${exchangeName}」没有通过初始化校验,原因是:${reason}` : `Exchange account "${exchangeName}" failed initialization checks: ${reason}`
|
||||
case 'trader.start.strategy_missing':
|
||||
return zh ? `机器人「${traderName}」缺少有效的交易策略配置。` : `Trader "${traderName}" does not have a valid strategy configuration.`
|
||||
case 'trader.start.model_not_found':
|
||||
return zh ? `机器人「${traderName}」关联的 AI 模型不存在。请检查模型配置。` : `Trader "${traderName}" references an AI model that no longer exists. Please check the model configuration.`
|
||||
case 'trader.start.model_disabled':
|
||||
return zh ? `机器人「${traderName}」关联的 AI 模型「${modelName}」目前还没有启用。` : `Trader "${traderName}" uses AI model "${modelName}", which is currently disabled.`
|
||||
case 'trader.start.exchange_not_found':
|
||||
return zh ? `机器人「${traderName}」关联的交易所账户不存在。请检查交易所配置。` : `Trader "${traderName}" references an exchange account that no longer exists. Please check the exchange configuration.`
|
||||
case 'trader.start.exchange_disabled':
|
||||
return zh ? `机器人「${traderName}」关联的交易所账户「${exchangeName}」目前还没有启用。` : `Trader "${traderName}" uses exchange account "${exchangeName}", which is currently disabled.`
|
||||
case 'trader.start.setup_invalid':
|
||||
case 'trader.start.load_failed':
|
||||
return zh ? `机器人「${traderName}」暂时还不能启动,原因是:${reason}` : `Trader "${traderName}" cannot be started yet because ${reason}`
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
const localizeTraderReason = (reasonKey?: string, fallback?: string) => {
|
||||
const zh = language === 'zh'
|
||||
|
||||
switch (reasonKey) {
|
||||
case 'trader.reason.strategy_config_invalid':
|
||||
return zh ? '当前策略配置内容已损坏,系统暂时无法解析' : 'the current strategy configuration is corrupted and cannot be parsed'
|
||||
case 'trader.reason.strategy_missing':
|
||||
return zh ? '当前机器人缺少有效的交易策略配置' : 'the trader is missing a valid strategy configuration'
|
||||
case 'trader.reason.private_key_invalid':
|
||||
return zh ? '私钥格式不正确,系统无法识别' : 'the private key format is invalid and cannot be recognized'
|
||||
case 'trader.reason.hyperliquid_init_failed':
|
||||
return zh ? 'Hyperliquid 账户初始化失败,请确认私钥、主钱包地址和 Agent Wallet 配置是否正确' : 'Hyperliquid account initialization failed. Please verify the private key, main wallet address, and Agent Wallet configuration'
|
||||
case 'trader.reason.aster_init_failed':
|
||||
return zh ? 'Aster 账户初始化失败,请确认 Aster User、Signer 和私钥是否正确' : 'Aster account initialization failed. Please verify the Aster User, Signer, and private key'
|
||||
case 'trader.reason.exchange_meta_unavailable':
|
||||
return zh ? '系统暂时无法从交易所读取账户元信息' : 'the system could not read account metadata from the exchange'
|
||||
case 'trader.reason.hyperliquid_agent_balance_too_high':
|
||||
return zh ? 'Hyperliquid Agent Wallet 余额过高,不符合当前安全要求' : 'the Hyperliquid Agent Wallet balance is too high for the current safety requirements'
|
||||
case 'trader.reason.exchange_account_init_failed':
|
||||
return zh ? '交易所账户初始化失败,请确认钱包地址和 API Key 是否匹配' : 'exchange account initialization failed. Please verify that the wallet address and API key match'
|
||||
case 'trader.reason.exchange_unsupported':
|
||||
return zh ? '当前交易所类型暂不支持机器人初始化' : 'the selected exchange type is not currently supported for trader initialization'
|
||||
case 'trader.reason.exchange_balance_unavailable':
|
||||
return zh ? '系统暂时无法从交易所读取账户余额' : 'the system could not read the account balance from the exchange'
|
||||
case 'trader.reason.exchange_service_unreachable':
|
||||
return zh ? '系统暂时无法连接交易所服务' : 'the system could not reach the exchange service right now'
|
||||
default:
|
||||
return fallback || (zh ? '系统返回了一个未知错误' : 'an unknown error was returned by the system')
|
||||
}
|
||||
}
|
||||
const normalizeActionableDescription = (error: unknown, message: string, title: string) => {
|
||||
if (error instanceof ApiError && error.errorKey) {
|
||||
return formatActionableDescriptionByKey(error.errorKey, error.errorParams, message)
|
||||
}
|
||||
|
||||
const prefixes = [
|
||||
'这次未能创建机器人:',
|
||||
'机器人创建失败:',
|
||||
'这次未能更新机器人:',
|
||||
'机器人更新失败:',
|
||||
'这次未能启动机器人:',
|
||||
'Failed to create trader:',
|
||||
'Failed to update trader:',
|
||||
'Unable to create trader:',
|
||||
'Unable to update trader:',
|
||||
'Unable to start trader:',
|
||||
]
|
||||
|
||||
let description = message.trim()
|
||||
if (description === title) return ''
|
||||
|
||||
for (const prefix of prefixes) {
|
||||
if (description.startsWith(prefix)) {
|
||||
description = description.slice(prefix.length).trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return description
|
||||
}
|
||||
const showActionableError = (title: string, error: unknown) => {
|
||||
const message = getErrorMessage(error, title)
|
||||
const description = normalizeActionableDescription(error, message, title)
|
||||
|
||||
if (description === '') {
|
||||
toast.error(title)
|
||||
return
|
||||
}
|
||||
|
||||
toast.error(title, {
|
||||
description,
|
||||
})
|
||||
}
|
||||
const parseBalanceUsdc = (balance?: string) => {
|
||||
if (!balance) return null
|
||||
const parsed = Number.parseFloat(balance)
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
}
|
||||
const getClaw402BalanceMessage = (balance: number, blocking: boolean) => {
|
||||
if (language === 'zh') {
|
||||
return blocking
|
||||
? `当前 Claw402 钱包余额为 ${balance.toFixed(6)} USDC,AI 调用无法执行。请先为这个钱包充值,再重新点击启动。`
|
||||
: `当前 Claw402 钱包余额仅剩 ${balance.toFixed(6)} USDC,虽然还能尝试启动,但很快可能因为 AI 调用费用不足而停止。建议先补一点 USDC。`
|
||||
}
|
||||
|
||||
return blocking
|
||||
? `Your Claw402 wallet balance is ${balance.toFixed(6)} USDC. AI calls cannot run with zero balance. Please top up this wallet before starting again.`
|
||||
: `Your Claw402 wallet balance is only ${balance.toFixed(6)} USDC. You can still try to start, but AI calls may stop soon due to insufficient funds.`
|
||||
}
|
||||
const getClaw402BalanceIssue = (traderId: string) => {
|
||||
const trader = traders?.find((item) => item.trader_id === traderId)
|
||||
if (!trader) return null
|
||||
|
||||
const model =
|
||||
allModels.find((item) => item.id === trader.ai_model) ||
|
||||
allModels.find((item) => item.provider === trader.ai_model)
|
||||
|
||||
if (!model || model.provider !== 'claw402') return null
|
||||
|
||||
const balance = parseBalanceUsdc(model.balanceUsdc)
|
||||
if (balance === null) return null
|
||||
if (balance <= 0) {
|
||||
return {
|
||||
blocking: true,
|
||||
title: language === 'zh' ? '启动失败' : 'Start failed',
|
||||
description: getClaw402BalanceMessage(balance, true),
|
||||
}
|
||||
}
|
||||
if (balance < 1) {
|
||||
return {
|
||||
blocking: false,
|
||||
title: language === 'zh' ? 'Claw402 余额偏低' : 'Low Claw402 balance',
|
||||
description: getClaw402BalanceMessage(balance, false),
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const navigateInApp = (path: string) => {
|
||||
navigate(path)
|
||||
@@ -180,6 +365,23 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}) || []
|
||||
|
||||
const enabledModels = allModels?.filter((m) => m.enabled) || []
|
||||
const enabledClaw402Model = enabledModels.find((model) => model.provider === 'claw402') || null
|
||||
const enabledClaw402Balance = parseBalanceUsdc(enabledClaw402Model?.balanceUsdc)
|
||||
const claw402BalanceAlert =
|
||||
enabledClaw402Model && enabledClaw402Balance !== null && enabledClaw402Balance < 1
|
||||
? {
|
||||
blocking: enabledClaw402Balance <= 0,
|
||||
title:
|
||||
language === 'zh'
|
||||
? enabledClaw402Balance <= 0
|
||||
? 'Claw402 钱包余额为 0'
|
||||
: 'Claw402 钱包余额偏低'
|
||||
: enabledClaw402Balance <= 0
|
||||
? 'Claw402 wallet balance is zero'
|
||||
: 'Claw402 wallet balance is low',
|
||||
description: getClaw402BalanceMessage(enabledClaw402Balance, enabledClaw402Balance <= 0),
|
||||
}
|
||||
: null
|
||||
const enabledExchanges =
|
||||
allExchanges?.filter((e) => {
|
||||
if (!e.enabled) return false
|
||||
@@ -237,26 +439,19 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
|
||||
const handleCreateTrader = async (data: CreateTraderRequest) => {
|
||||
try {
|
||||
const model = allModels?.find((m) => m.id === data.ai_model_id)
|
||||
const exchange = allExchanges?.find((e) => e.id === data.exchange_id)
|
||||
|
||||
if (!model?.enabled) {
|
||||
toast.error(t('modelNotConfigured', language))
|
||||
return
|
||||
const createdTrader = await api.createTrader(data)
|
||||
if (createdTrader.startup_warning) {
|
||||
toast.success(t('aiTradersToast.created', language), {
|
||||
description: createdTrader.startup_warning,
|
||||
})
|
||||
} else {
|
||||
toast.success(t('aiTradersToast.created', language))
|
||||
}
|
||||
|
||||
if (!exchange?.enabled) {
|
||||
toast.error(t('exchangeNotConfigured', language))
|
||||
return
|
||||
}
|
||||
|
||||
await api.createTrader(data)
|
||||
toast.success(t('aiTradersToast.created', language))
|
||||
setShowCreateModal(false)
|
||||
await mutateTraders()
|
||||
} catch (error) {
|
||||
console.error('Failed to create trader:', error)
|
||||
toast.error(t('createTraderFailed', language))
|
||||
showActionableError(t('createTraderFailed', language), error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,7 +501,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
await mutateTraders()
|
||||
} catch (error) {
|
||||
console.error('Failed to update trader:', error)
|
||||
toast.error(t('updateTraderFailed', language))
|
||||
showActionableError(t('updateTraderFailed', language), error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,16 +526,31 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
try {
|
||||
if (running) {
|
||||
await api.stopTrader(traderId)
|
||||
toast.success(t('aiTradersToast.stopped', language))
|
||||
toast.success(t('aiTradersToast.stopped', language))
|
||||
} else {
|
||||
const claw402Issue = getClaw402BalanceIssue(traderId)
|
||||
if (claw402Issue?.blocking) {
|
||||
toast.error(claw402Issue.title, {
|
||||
description: claw402Issue.description,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (claw402Issue && !claw402Issue.blocking) {
|
||||
toast.warning(claw402Issue.title, {
|
||||
description: claw402Issue.description,
|
||||
})
|
||||
}
|
||||
await api.startTrader(traderId)
|
||||
toast.success(t('aiTradersToast.started', language))
|
||||
toast.success(t('aiTradersToast.started', language))
|
||||
}
|
||||
|
||||
await mutateTraders()
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle trader:', error)
|
||||
toast.error(t('operationFailed', language))
|
||||
showActionableError(
|
||||
running ? t('aiTradersToast.stopFailed', language) : t('aiTradersToast.startFailed', language),
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,6 +980,52 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{claw402BalanceAlert ? (
|
||||
<div
|
||||
className="mb-6 rounded-xl border px-4 py-4 md:px-5 md:py-4 flex flex-col md:flex-row md:items-start md:justify-between gap-3"
|
||||
style={{
|
||||
borderColor: claw402BalanceAlert.blocking ? 'rgba(239, 68, 68, 0.55)' : 'rgba(245, 158, 11, 0.45)',
|
||||
background: claw402BalanceAlert.blocking ? 'rgba(127, 29, 29, 0.22)' : 'rgba(120, 53, 15, 0.18)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className="mt-0.5 rounded-full p-2"
|
||||
style={{
|
||||
background: claw402BalanceAlert.blocking ? 'rgba(239, 68, 68, 0.16)' : 'rgba(245, 158, 11, 0.14)',
|
||||
color: claw402BalanceAlert.blocking ? '#F87171' : '#FBBF24',
|
||||
}}
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: claw402BalanceAlert.blocking ? '#FCA5A5' : '#FDE68A' }}
|
||||
>
|
||||
{claw402BalanceAlert.title}
|
||||
</div>
|
||||
<div className="text-sm mt-1 leading-6" style={{ color: '#D4D4D8' }}>
|
||||
{claw402BalanceAlert.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => enabledClaw402Model && handleModelClick(enabledClaw402Model.id)}
|
||||
className="px-4 py-2 rounded text-xs font-mono uppercase tracking-wider border whitespace-nowrap self-start"
|
||||
style={{
|
||||
borderColor: claw402BalanceAlert.blocking ? 'rgba(248, 113, 113, 0.45)' : 'rgba(251, 191, 36, 0.35)',
|
||||
color: claw402BalanceAlert.blocking ? '#FCA5A5' : '#FDE68A',
|
||||
background: 'rgba(0, 0, 0, 0.18)',
|
||||
}}
|
||||
>
|
||||
{language === 'zh' ? '查看 AI 钱包' : 'Open AI wallet'}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Configuration Status Grid */}
|
||||
<ConfigStatusGrid
|
||||
configuredModels={configuredModels}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import type { AIModel, Exchange, CreateTraderRequest, Strategy } from '../../types'
|
||||
import type { AIModel, Exchange, CreateTraderRequest, ExchangeAccountStateResponse, Strategy } from '../../types'
|
||||
import { useLanguage } from '../../contexts/LanguageContext'
|
||||
import { t } from '../../i18n/translations'
|
||||
import { toast } from 'sonner'
|
||||
@@ -124,32 +124,62 @@ export function TraderConfigModal({
|
||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||
}
|
||||
|
||||
const handleExchangeChange = (exchangeId: string) => {
|
||||
setBalanceFetchError('')
|
||||
setFormData((prev) => {
|
||||
if (prev.exchange_id === exchangeId) {
|
||||
return prev
|
||||
}
|
||||
|
||||
const next: FormState = { ...prev, exchange_id: exchangeId }
|
||||
|
||||
// Exchange balance belongs to the selected exchange, not the trader record.
|
||||
// Clear the old baseline so we don't carry Exchange B's balance into Exchange A.
|
||||
if (isEditMode) {
|
||||
next.initial_balance = undefined
|
||||
}
|
||||
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const handleFetchCurrentBalance = async () => {
|
||||
if (!isEditMode || !traderData?.trader_id) {
|
||||
if (!isEditMode) {
|
||||
setBalanceFetchError(t('fetchBalanceEditModeOnly', language))
|
||||
return
|
||||
}
|
||||
|
||||
if (!formData.exchange_id) {
|
||||
setBalanceFetchError(t('balanceFetchFailed', language))
|
||||
return
|
||||
}
|
||||
|
||||
setIsFetchingBalance(true)
|
||||
setBalanceFetchError('')
|
||||
|
||||
try {
|
||||
const result = await httpClient.get<{
|
||||
total_equity?: number
|
||||
balance?: number
|
||||
}>(`/api/account?trader_id=${traderData.trader_id}`)
|
||||
const result = await httpClient.get<ExchangeAccountStateResponse>('/api/exchanges/account-state')
|
||||
|
||||
if (result.success && result.data) {
|
||||
const selectedState = result.data?.states?.[formData.exchange_id]
|
||||
if (result.success && selectedState?.status === 'ok') {
|
||||
const currentBalance =
|
||||
result.data.total_equity || result.data.balance || 0
|
||||
selectedState.total_equity ??
|
||||
selectedState.available_balance ??
|
||||
0
|
||||
setFormData((prev) => ({ ...prev, initial_balance: currentBalance }))
|
||||
toast.success(t('balanceFetched', language))
|
||||
} else {
|
||||
throw new Error(result.message || t('balanceFetchFailed', language))
|
||||
setBalanceFetchError(
|
||||
selectedState?.error_message || result.message || t('balanceFetchFailed', language)
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(t('balanceFetchFailed', language) + ':', error)
|
||||
setBalanceFetchError(t('balanceFetchNetworkError', language))
|
||||
setBalanceFetchError(
|
||||
error instanceof Error && error.message
|
||||
? error.message
|
||||
: t('balanceFetchNetworkError', language)
|
||||
)
|
||||
} finally {
|
||||
setIsFetchingBalance(false)
|
||||
}
|
||||
@@ -176,8 +206,6 @@ export function TraderConfigModal({
|
||||
}
|
||||
|
||||
await onSave(saveData)
|
||||
toast.success(t('saveSuccess', language))
|
||||
onClose()
|
||||
} catch (error) {
|
||||
console.error(t('saveFailed', language) + ':', error)
|
||||
} finally {
|
||||
@@ -269,9 +297,7 @@ export function TraderConfigModal({
|
||||
</label>
|
||||
<NofxSelect
|
||||
value={formData.exchange_id}
|
||||
onChange={(val) =>
|
||||
handleInputChange('exchange_id', val)
|
||||
}
|
||||
onChange={handleExchangeChange}
|
||||
className="w-full px-3 py-2 bg-[#0B0E11] border border-[#2B3139] rounded text-[#EAECEF]"
|
||||
options={availableExchanges.map((exchange) => ({
|
||||
value: exchange.id,
|
||||
|
||||
Reference in New Issue
Block a user