import React, { useState, useEffect } from 'react' import type { Exchange } from '../../types' import { t, type Language } from '../../i18n/translations' import { api } from '../../lib/api' import { getExchangeIcon } from '../ExchangeIcons' import { TwoStageKeyModal, type TwoStageKeyModalResult, } from '../TwoStageKeyModal' import { WebCryptoEnvironmentCheck, type WebCryptoCheckStatus, } from '../WebCryptoEnvironmentCheck' import { BookOpen, Trash2, HelpCircle, ExternalLink, UserPlus } from 'lucide-react' import { toast } from 'sonner' import { Tooltip } from './Tooltip' import { getShortName } from './utils' // Supported exchange templates for creating new accounts const SUPPORTED_EXCHANGE_TEMPLATES = [ { exchange_type: 'binance', name: 'Binance Futures', type: 'cex' as const }, { exchange_type: 'bybit', name: 'Bybit Futures', type: 'cex' as const }, { exchange_type: 'okx', name: 'OKX Futures', type: 'cex' as const }, { exchange_type: 'bitget', name: 'Bitget Futures', type: 'cex' as const }, { exchange_type: 'hyperliquid', name: 'Hyperliquid', type: 'dex' as const }, { exchange_type: 'aster', name: 'Aster DEX', type: 'dex' as const }, { exchange_type: 'lighter', name: 'Lighter', type: 'dex' as const }, ] interface ExchangeConfigModalProps { allExchanges: Exchange[] editingExchangeId: string | null onSave: ( exchangeId: string | null, // null for creating new account exchangeType: string, accountName: string, apiKey: string, secretKey?: string, passphrase?: string, // OKX专用 testnet?: boolean, hyperliquidWalletAddr?: string, asterUser?: string, asterSigner?: string, asterPrivateKey?: string, lighterWalletAddr?: string, lighterPrivateKey?: string, lighterApiKeyPrivateKey?: string, lighterApiKeyIndex?: number ) => Promise onDelete: (exchangeId: string) => void onClose: () => void language: Language } export function ExchangeConfigModal({ allExchanges, editingExchangeId, onSave, onDelete, onClose, language, }: ExchangeConfigModalProps) { // Selected exchange type for creating new accounts const [selectedExchangeType, setSelectedExchangeType] = useState('') const [apiKey, setApiKey] = useState('') const [secretKey, setSecretKey] = useState('') const [passphrase, setPassphrase] = useState('') const [testnet, setTestnet] = useState(false) const [showGuide, setShowGuide] = useState(false) const [serverIP, setServerIP] = useState<{ public_ip: string message: string } | null>(null) const [loadingIP, setLoadingIP] = useState(false) const [copiedIP, setCopiedIP] = useState(false) const [webCryptoStatus, setWebCryptoStatus] = useState('idle') // 币安配置指南展开状态 const [showBinanceGuide, setShowBinanceGuide] = useState(false) // Aster 特定字段 const [asterUser, setAsterUser] = useState('') const [asterSigner, setAsterSigner] = useState('') const [asterPrivateKey, setAsterPrivateKey] = useState('') // Hyperliquid 特定字段 const [hyperliquidWalletAddr, setHyperliquidWalletAddr] = useState('') // LIGHTER 特定字段 const [lighterWalletAddr, setLighterWalletAddr] = useState('') const [lighterApiKeyPrivateKey, setLighterApiKeyPrivateKey] = useState('') const [lighterApiKeyIndex, setLighterApiKeyIndex] = useState(0) // 安全输入状态 const [secureInputTarget, setSecureInputTarget] = useState< null | 'hyperliquid' | 'aster' | 'lighter' >(null) // 保存中状态 const [isSaving, setIsSaving] = useState(false) // 账户名称 const [accountName, setAccountName] = useState('') // 获取当前编辑的交易所信息或模板 // For editing: find the existing account by id (UUID) // For creating: use the selected exchange template const selectedExchange = editingExchangeId ? allExchanges?.find((e) => e.id === editingExchangeId) : null // Get the exchange template for displaying UI fields const selectedTemplate = editingExchangeId ? SUPPORTED_EXCHANGE_TEMPLATES.find((t) => t.exchange_type === selectedExchange?.exchange_type) : SUPPORTED_EXCHANGE_TEMPLATES.find((t) => t.exchange_type === selectedExchangeType) // Get the current exchange type (from existing account or selected template) const currentExchangeType = editingExchangeId ? selectedExchange?.exchange_type : selectedExchangeType // 交易所注册链接配置 const exchangeRegistrationLinks: Record = { binance: { url: 'https://www.binance.com/join?ref=NOFXENG', hasReferral: true }, okx: { url: 'https://www.okx.com/join/1865360', hasReferral: true }, bybit: { url: 'https://partner.bybit.com/b/83856', hasReferral: true }, bitget: { url: 'https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172', hasReferral: true }, hyperliquid: { url: 'https://app.hyperliquid.xyz/join/AITRADING', hasReferral: true }, aster: { url: 'https://www.asterdex.com/en/referral/fdfc0e', hasReferral: true }, lighter: { url: 'https://app.lighter.xyz/?referral=68151432', hasReferral: true }, } // 如果是编辑现有交易所,初始化表单数据 useEffect(() => { if (editingExchangeId && selectedExchange) { setAccountName(selectedExchange.account_name || '') setApiKey(selectedExchange.apiKey || '') setSecretKey(selectedExchange.secretKey || '') setPassphrase('') // Don't load existing passphrase for security setTestnet(selectedExchange.testnet || false) // Aster 字段 setAsterUser(selectedExchange.asterUser || '') setAsterSigner(selectedExchange.asterSigner || '') setAsterPrivateKey('') // Don't load existing private key for security // Hyperliquid 字段 setHyperliquidWalletAddr(selectedExchange.hyperliquidWalletAddr || '') // LIGHTER 字段 setLighterWalletAddr(selectedExchange.lighterWalletAddr || '') setLighterApiKeyPrivateKey('') // Don't load existing API key for security setLighterApiKeyIndex(selectedExchange.lighterApiKeyIndex || 0) } }, [editingExchangeId, selectedExchange]) // 加载服务器IP(当选择binance时) useEffect(() => { if (currentExchangeType === 'binance' && !serverIP) { setLoadingIP(true) api .getServerIP() .then((data) => { setServerIP(data) }) .catch((err) => { console.error('Failed to load server IP:', err) }) .finally(() => { setLoadingIP(false) }) } }, [currentExchangeType]) const handleCopyIP = async (ip: string) => { try { // 优先使用现代 Clipboard API if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(ip) setCopiedIP(true) setTimeout(() => setCopiedIP(false), 2000) toast.success(t('ipCopied', language)) } else { // 降级方案: 使用传统的 execCommand 方法 const textArea = document.createElement('textarea') textArea.value = ip textArea.style.position = 'fixed' textArea.style.left = '-999999px' textArea.style.top = '-999999px' document.body.appendChild(textArea) textArea.focus() textArea.select() try { const successful = document.execCommand('copy') if (successful) { setCopiedIP(true) setTimeout(() => setCopiedIP(false), 2000) toast.success(t('ipCopied', language)) } else { throw new Error('复制命令执行失败') } } finally { document.body.removeChild(textArea) } } } catch (err) { console.error('复制失败:', err) // 显示错误提示 toast.error( t('copyIPFailed', language) || `复制失败: ${ip}\n请手动复制此IP地址` ) } } // 安全输入处理函数 const secureInputContextLabel = secureInputTarget === 'aster' ? t('asterExchangeName', language) : secureInputTarget === 'hyperliquid' ? t('hyperliquidExchangeName', language) : undefined const handleSecureInputCancel = () => { setSecureInputTarget(null) } const handleSecureInputComplete = ({ value, obfuscationLog, }: TwoStageKeyModalResult) => { const trimmed = value.trim() if (secureInputTarget === 'hyperliquid') { setApiKey(trimmed) } if (secureInputTarget === 'aster') { setAsterPrivateKey(trimmed) } if (secureInputTarget === 'lighter') { setLighterApiKeyPrivateKey(trimmed) toast.success(t('lighterApiKeyImported', language)) } // 仅在开发环境输出调试信息 if (import.meta.env.DEV) { console.log('Secure input obfuscation log:', obfuscationLog) } setSecureInputTarget(null) } // 掩盖敏感数据显示 const maskSecret = (secret: string) => { if (!secret || secret.length === 0) return '' if (secret.length <= 8) return '*'.repeat(secret.length) return ( secret.slice(0, 4) + '*'.repeat(Math.max(secret.length - 8, 4)) + secret.slice(-4) ) } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (isSaving) return // For creating, we need the exchange type if (!editingExchangeId && !selectedExchangeType) return // Validate account name const trimmedAccountName = accountName.trim() if (!trimmedAccountName) { toast.error(language === 'zh' ? '请输入账户名称' : 'Please enter account name') return } const exchangeId = editingExchangeId || null const exchangeType = currentExchangeType || '' setIsSaving(true) try { // 根据交易所类型验证不同字段 if (currentExchangeType === 'binance') { if (!apiKey.trim() || !secretKey.trim()) return await onSave(exchangeId, exchangeType, trimmedAccountName, apiKey.trim(), secretKey.trim(), '', testnet) } else if (currentExchangeType === 'okx') { if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return await onSave(exchangeId, exchangeType, trimmedAccountName, apiKey.trim(), secretKey.trim(), passphrase.trim(), testnet) } else if (currentExchangeType === 'bitget') { if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return await onSave(exchangeId, exchangeType, trimmedAccountName, apiKey.trim(), secretKey.trim(), passphrase.trim(), testnet) } else if (currentExchangeType === 'hyperliquid') { if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址 await onSave( exchangeId, exchangeType, trimmedAccountName, apiKey.trim(), '', '', testnet, hyperliquidWalletAddr.trim() ) } else if (currentExchangeType === 'aster') { if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim()) return await onSave( exchangeId, exchangeType, trimmedAccountName, '', '', '', testnet, undefined, asterUser.trim(), asterSigner.trim(), asterPrivateKey.trim() ) } else if (currentExchangeType === 'lighter') { if (!lighterWalletAddr.trim() || !lighterApiKeyPrivateKey.trim()) return await onSave( exchangeId, exchangeType, trimmedAccountName, '', // apiKey not used for Lighter '', '', testnet, undefined, // hyperliquidWalletAddr undefined, // asterUser undefined, // asterSigner undefined, // asterPrivateKey lighterWalletAddr.trim(), '', // lighterPrivateKey (L1) no longer needed lighterApiKeyPrivateKey.trim(), lighterApiKeyIndex ) } else { // 默认情况(其他CEX交易所) if (!apiKey.trim() || !secretKey.trim()) return await onSave(exchangeId, exchangeType, trimmedAccountName, apiKey.trim(), secretKey.trim(), '', testnet) } } finally { setIsSaving(false) } } return (

{editingExchangeId ? t('editExchange', language) : t('addExchange', language)}

{currentExchangeType === 'binance' && ( )} {editingExchangeId && ( )}
{!editingExchangeId && (
{t('environmentSteps.checkTitle', language)}
{t('environmentSteps.selectTitle', language)}
)} {selectedTemplate && (
{getExchangeIcon(selectedTemplate.exchange_type, { width: 32, height: 32, })}
{getShortName(selectedTemplate.name)} {editingExchangeId && selectedExchange?.account_name && ( - {selectedExchange.account_name} )}
{selectedTemplate.type.toUpperCase()} •{' '} {selectedTemplate.exchange_type}
{/* 账户名称输入 */}
setAccountName(e.target.value)} placeholder={language === 'zh' ? '例如:主账户、套利账户' : 'e.g., Main Account, Arbitrage Account'} className="w-full px-3 py-2 rounded" style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF', }} required />
{language === 'zh' ? '为此账户设置一个易于识别的名称,以便区分同一交易所的多个账户' : 'Set an easily recognizable name for this account to distinguish multiple accounts on the same exchange'}
{/* 注册链接 */}
{language === 'zh' ? '还没有交易所账号?点击注册' : "No exchange account? Register here"} {exchangeRegistrationLinks[currentExchangeType || '']?.hasReferral && ( {language === 'zh' ? '折扣优惠' : 'Discount'} )}
)} {selectedTemplate && ( <> {/* Binance/Bybit/OKX/Bitget 的输入字段 */} {(currentExchangeType === 'binance' || currentExchangeType === 'bybit' || currentExchangeType === 'okx' || currentExchangeType === 'bitget') && ( <> {/* 币安用户配置提示 (D1 方案) */} {currentExchangeType === 'binance' && (
setShowBinanceGuide(!showBinanceGuide)} >
ℹ️ 币安用户必读: 使用「现货与合约交易」API,不要用「统一账户 API」
{showBinanceGuide ? '▲' : '▼'}
{/* 展开的详细说明 */} {showBinanceGuide && (
e.stopPropagation()} >

原因:统一账户 API 权限结构不同,会导致订单提交失败

正确配置步骤:

  1. 登录币安 → 个人中心 →{' '} API 管理
  2. 创建 API → 选择「 系统生成的 API 密钥
  3. 勾选「现货与合约交易」( 不选统一账户
  4. IP 限制选「无限制 」或添加服务器 IP

💡 多资产模式用户注意: 如果您开启了多资产模式,将强制使用全仓模式。建议关闭多资产模式以支持逐仓交易。

📖 查看币安官方教程 ↗
)}
)}
setApiKey(e.target.value)} placeholder={t('enterAPIKey', language)} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} required />
setSecretKey(e.target.value)} placeholder={t('enterSecretKey', language)} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} required />
{(currentExchangeType === 'okx' || currentExchangeType === 'bitget') && (
setPassphrase(e.target.value)} placeholder={t('enterPassphrase', language)} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} required />
)} {/* Binance 白名单IP提示 */} {currentExchangeType === 'binance' && (
{t('whitelistIP', language)}
{t('whitelistIPDesc', language)}
{loadingIP ? (
{t('loadingServerIP', language)}
) : serverIP && serverIP.public_ip ? (
{serverIP.public_ip}
) : null}
)} )} {/* Aster 交易所的字段 */} {currentExchangeType === 'aster' && ( <> {/* API Pro 代理钱包说明 banner */}
🔐
{t('asterApiProTitle', language)}
{t('asterApiProDesc', language)}
{/* 主钱包地址 */}
setAsterUser(e.target.value)} placeholder={t('enterAsterUser', language)} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} required />
{t('asterUserDesc', language)}
{/* API Pro 代理钱包地址 */}
setAsterSigner(e.target.value)} placeholder={t('enterAsterSigner', language)} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} required />
{t('asterSignerDesc', language)}
{/* API Pro 代理钱包私钥 */}
setAsterPrivateKey(e.target.value)} placeholder={t('enterAsterPrivateKey', language)} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} required />
{t('asterPrivateKeyDesc', language)}
)} {/* Hyperliquid 交易所的字段 */} {currentExchangeType === 'hyperliquid' && ( <> {/* 安全提示 banner */}
🔐
{t('hyperliquidAgentWalletTitle', language)}
{t('hyperliquidAgentWalletDesc', language)}
{/* Agent Private Key 字段 */}
{apiKey && ( )}
{apiKey && (
{t('secureInputHint', language)}
)}
{t('hyperliquidAgentPrivateKeyDesc', language)}
{/* Main Wallet Address 字段 */}
setHyperliquidWalletAddr(e.target.value) } placeholder={t( 'enterHyperliquidMainWalletAddress', language )} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} required />
{t('hyperliquidMainWalletAddressDesc', language)}
)} {/* LIGHTER 特定配置 */} {currentExchangeType === 'lighter' && ( <> {/* Info banner */}
🔐
{language === 'zh' ? 'Lighter API Key 配置' : 'Lighter API Key Setup'}
{language === 'zh' ? '请在 Lighter 网站生成 API Key,然后填写钱包地址、API Key 私钥和索引。' : 'Generate an API Key on the Lighter website, then enter your wallet address, API Key private key, and index.'}
{/* L1 Wallet Address */}
setLighterWalletAddr(e.target.value)} placeholder={t('enterLighterWalletAddress', language)} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} required />
{t('lighterWalletAddressDesc', language)}
{/* API Key Private Key */}
setLighterApiKeyPrivateKey(e.target.value)} placeholder={t('enterLighterApiKeyPrivateKey', language)} className="w-full px-3 py-2 rounded font-mono text-sm" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} required />
{t('lighterApiKeyPrivateKeyDesc', language)}
{/* API Key Index */}
setLighterApiKeyIndex(parseInt(e.target.value) || 0)} placeholder="0" className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF', }} />
{language === 'zh' ? '默认为 0。如果您在 Lighter 创建了多个 API Key,请填写对应的索引号(0-255)。' : 'Default is 0. If you created multiple API Keys on Lighter, enter the corresponding index (0-255).'}
)} )}
{/* Binance Setup Guide Modal */} {showGuide && (
setShowGuide(false)} >
e.stopPropagation()} >

{t('binanceSetupGuide', language)}

{t('binanceSetupGuide',
)} {/* Two Stage Key Modal */}
) }