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 '../common/ExchangeIcons' import { TwoStageKeyModal, type TwoStageKeyModalResult, } from '../modals/TwoStageKeyModal' import { WebCryptoEnvironmentCheck, type WebCryptoCheckStatus, } from '../common/WebCryptoEnvironmentCheck' import { BookOpen, Trash2, HelpCircle, ExternalLink, UserPlus, Key, Shield, ChevronLeft, Check, Copy, ArrowRight } from 'lucide-react' import { toast } from 'sonner' import { Tooltip } from './Tooltip' import { getShortName } from './utils' // Supported exchange templates 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: 'gate', name: 'Gate.io Futures', type: 'cex' as const }, { exchange_type: 'kucoin', name: 'KuCoin 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 }, { exchange_type: 'indodax', name: 'Indodax', type: 'cex' as const }, ] interface ExchangeConfigModalProps { allExchanges: Exchange[] editingExchangeId: string | null onSave: ( exchangeId: string | null, exchangeType: string, accountName: string, apiKey: string, secretKey?: string, passphrase?: string, 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 } // Step indicator component function StepIndicator({ currentStep, labels }: { currentStep: number; labels: string[] }) { return (
{labels.map((label, index) => (
{index < currentStep ? : index + 1}
{label}
{index < labels.length - 1 && (
)} ))}
) } // Exchange card component function ExchangeCard({ template, selected, onClick, disabled, }: { template: typeof SUPPORTED_EXCHANGE_TEMPLATES[0] selected: boolean onClick: () => void disabled?: boolean }) { return ( ) } export function ExchangeConfigModal({ allExchanges, editingExchangeId, onSave, onDelete, onClose, language, }: ExchangeConfigModalProps) { // Step: 0 = select exchange, 1 = configure const [currentStep, setCurrentStep] = useState(editingExchangeId ? 1 : 0) 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 fields const [asterUser, setAsterUser] = useState('') const [asterSigner, setAsterSigner] = useState('') const [asterPrivateKey, setAsterPrivateKey] = useState('') // Hyperliquid fields const [hyperliquidWalletAddr, setHyperliquidWalletAddr] = useState('') // Lighter fields const [lighterWalletAddr, setLighterWalletAddr] = useState('') const [lighterApiKeyPrivateKey, setLighterApiKeyPrivateKey] = useState('') const [lighterApiKeyIndex, setLighterApiKeyIndex] = useState(0) // Other state const [secureInputTarget, setSecureInputTarget] = useState(null) const [isSaving, setIsSaving] = useState(false) const [accountName, setAccountName] = useState('') const selectedExchange = editingExchangeId ? allExchanges?.find((e) => e.id === editingExchangeId) : null const selectedTemplate = editingExchangeId ? SUPPORTED_EXCHANGE_TEMPLATES.find((t) => t.exchange_type === selectedExchange?.exchange_type) : SUPPORTED_EXCHANGE_TEMPLATES.find((t) => t.exchange_type === selectedExchangeType) 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 }, gate: { url: 'https://www.gatenode.xyz/share/VQBGUAxY', hasReferral: true }, kucoin: { url: 'https://www.kucoin.com/r/broker/CXEV7XKK', 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 }, indodax: { url: 'https://indodax.com/ref/Saep23/1', hasReferral: true }, } // Initialize form when editing useEffect(() => { if (editingExchangeId && selectedExchange) { setAccountName(selectedExchange.account_name || '') setApiKey(selectedExchange.apiKey || '') setSecretKey(selectedExchange.secretKey || '') setPassphrase('') setTestnet(selectedExchange.testnet || false) setAsterUser(selectedExchange.asterUser || '') setAsterSigner(selectedExchange.asterSigner || '') setAsterPrivateKey('') setHyperliquidWalletAddr(selectedExchange.hyperliquidWalletAddr || '') setLighterWalletAddr(selectedExchange.lighterWalletAddr || '') setLighterApiKeyPrivateKey('') setLighterApiKeyIndex(selectedExchange.lighterApiKeyIndex || 0) } }, [editingExchangeId, selectedExchange]) // Load server IP for 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, serverIP]) const handleCopyIP = async (ip: string) => { try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(ip) setCopiedIP(true) setTimeout(() => setCopiedIP(false), 2000) toast.success(t('ipCopied', language)) } else { const textArea = document.createElement('textarea') textArea.value = ip textArea.style.position = 'fixed' textArea.style.left = '-999999px' document.body.appendChild(textArea) textArea.select() document.execCommand('copy') document.body.removeChild(textArea) setCopiedIP(true) setTimeout(() => setCopiedIP(false), 2000) toast.success(t('ipCopied', language)) } } catch { toast.error(t('copyIPFailed', language)) } } const secureInputContextLabel = secureInputTarget === 'aster' ? t('asterExchangeName', language) : secureInputTarget === 'hyperliquid' ? t('hyperliquidExchangeName', language) : undefined const handleSecureInputComplete = ({ value }: 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)) } 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 handleSelectExchange = (exchangeType: string) => { setSelectedExchangeType(exchangeType) setCurrentStep(1) } const handleBack = () => { if (editingExchangeId) { onClose() } else { setCurrentStep(0) setSelectedExchangeType('') } } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (isSaving) return if (!editingExchangeId && !selectedExchangeType) return const trimmedAccountName = accountName.trim() if (!trimmedAccountName) { toast.error(t('exchangeConfig.pleaseEnterAccountName', language)) return } const exchangeId = editingExchangeId || null const exchangeType = currentExchangeType || '' setIsSaving(true) try { if (currentExchangeType === 'binance' || currentExchangeType === 'bybit' || currentExchangeType === 'indodax') { if (!apiKey.trim() || !secretKey.trim()) return await onSave(exchangeId, exchangeType, trimmedAccountName, apiKey.trim(), secretKey.trim(), '', testnet) } else if (currentExchangeType === 'okx' || currentExchangeType === 'bitget' || currentExchangeType === 'kucoin') { 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, '', '', '', testnet, undefined, undefined, undefined, undefined, lighterWalletAddr.trim(), '', lighterApiKeyPrivateKey.trim(), lighterApiKeyIndex) } else { if (!apiKey.trim() || !secretKey.trim()) return await onSave(exchangeId, exchangeType, trimmedAccountName, apiKey.trim(), secretKey.trim(), '', testnet) } } finally { setIsSaving(false) } } const stepLabels = [t('exchangeConfig.selectExchange', language), t('exchangeConfig.configure', language)] const cexExchanges = SUPPORTED_EXCHANGE_TEMPLATES.filter(t => t.type === 'cex') const dexExchanges = SUPPORTED_EXCHANGE_TEMPLATES.filter(t => t.type === 'dex') return (
{/* Header */}
{currentStep > 0 && !editingExchangeId && ( )}

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

{currentExchangeType === 'binance' && currentStep === 1 && ( )} {editingExchangeId && ( )}
{/* Step Indicator */} {!editingExchangeId && (
)} {/* Content */}
{/* Step 0: Select Exchange */} {currentStep === 0 && !editingExchangeId && (
{/* WebCrypto Check */}
{t('environmentSteps.checkTitle', language)}
{/* Exchange Grid */}
{t('exchangeConfig.chooseExchange', language)}
{/* CEX */}
{t('exchangeConfig.centralizedExchanges', language)}
{cexExchanges.map((template) => ( handleSelectExchange(template.exchange_type)} disabled={webCryptoStatus !== 'secure' && webCryptoStatus !== 'disabled'} /> ))}
{/* DEX */}
{t('exchangeConfig.decentralizedExchanges', language)}
{dexExchanges.map((template) => ( handleSelectExchange(template.exchange_type)} disabled={webCryptoStatus !== 'secure' && webCryptoStatus !== 'disabled'} /> ))}
)} {/* Step 1: Configure */} {(currentStep === 1 || editingExchangeId) && selectedTemplate && (
{/* Selected Exchange Header */}
{getExchangeIcon(selectedTemplate.exchange_type, { width: 48, height: 48 })}
{getShortName(selectedTemplate.name)}
{selectedTemplate.type.toUpperCase()} • {selectedTemplate.exchange_type}
{t('exchangeConfig.register', language)} {exchangeRegistrationLinks[currentExchangeType || '']?.hasReferral && ( {t('exchangeConfig.bonus', language)} )}
{/* Account Name */}
setAccountName(e.target.value)} placeholder={t('exchangeConfig.accountNamePlaceholder', language)} className="w-full px-4 py-3 rounded-xl text-base" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
{/* CEX Fields */} {(currentExchangeType === 'binance' || currentExchangeType === 'bybit' || currentExchangeType === 'okx' || currentExchangeType === 'bitget' || currentExchangeType === 'gate' || currentExchangeType === 'kucoin' || currentExchangeType === 'indodax') && ( <> {currentExchangeType === 'binance' && (
setShowBinanceGuide(!showBinanceGuide)} >
ℹ️ {t('exchangeConfig.useBinanceFuturesApi', language)}
{showBinanceGuide ? '▲' : '▼'}
{showBinanceGuide && ( )}
)}
setApiKey(e.target.value)} placeholder={t('enterAPIKey', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
setSecretKey(e.target.value)} placeholder={t('enterSecretKey', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
{(currentExchangeType === 'okx' || currentExchangeType === 'bitget' || currentExchangeType === 'kucoin') && (
setPassphrase(e.target.value)} placeholder={t('enterPassphrase', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
)} {currentExchangeType === 'binance' && (
{t('whitelistIP', language)}
{t('whitelistIPDesc', language)}
{loadingIP ? (
{t('loadingServerIP', language)}
) : serverIP?.public_ip ? (
{serverIP.public_ip}
) : null}
)} )} {/* Aster Fields */} {currentExchangeType === 'aster' && ( <>
🔐
{t('asterApiProTitle', language)}
{t('asterApiProDesc', language)}
setAsterUser(e.target.value)} placeholder={t('enterAsterUser', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
setAsterSigner(e.target.value)} placeholder={t('enterAsterSigner', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
setAsterPrivateKey(e.target.value)} placeholder={t('enterAsterPrivateKey', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
)} {/* Hyperliquid Fields */} {currentExchangeType === 'hyperliquid' && ( <>
🔐
{t('hyperliquidAgentWalletTitle', language)}
{t('hyperliquidAgentWalletDesc', language)}
setHyperliquidWalletAddr(e.target.value)} placeholder={t('enterHyperliquidMainWalletAddress', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
)} {/* Lighter Fields */} {currentExchangeType === 'lighter' && ( <>
🔐
{t('exchangeConfig.lighterApiKeySetup', language)}
{t('exchangeConfig.lighterApiKeyDesc', language)}
setLighterWalletAddr(e.target.value)} placeholder={t('enterLighterWalletAddress', language)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
setLighterApiKeyPrivateKey(e.target.value)} placeholder={t('enterLighterApiKeyPrivateKey', language)} className="w-full px-4 py-3 rounded-xl font-mono" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} required />
setLighterApiKeyIndex(parseInt(e.target.value) || 0)} className="w-full px-4 py-3 rounded-xl" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} />
)} {/* Buttons */}
)}
{/* Binance Guide Modal */} {showGuide && (
setShowGuide(false)}>
e.stopPropagation()}>

{t('binanceSetupGuide', language)}

{t('binanceSetupGuide',
)} {/* Secure Input Modal */} setSecureInputTarget(null)} onComplete={handleSecureInputComplete} />
) }