Files
nofx/web/src/components/traders/ExchangeConfigModal.tsx
tinkle-community 4725548a55 feat: fix Lighter V2 integration and improve error handling
- Fix Lighter API field name mismatches (position/size, avg_entry_price/entry_price, sign/side)
- Fix GetBalance return format to match standard fields (totalWalletBalance, totalUnrealizedProfit)
- Fix GetPositions return format to match standard fields (positionAmt, markPrice, unRealizedProfit)
- Add API Key Index field to frontend with explanation
- Update Lighter referral link
- Disable Lighter testnet (mainnet only)
- Add load error tracking for better error messages
- Remove old Lighter V1 implementation files
- Remove test credentials from test files
2025-12-14 20:50:10 +08:00

1288 lines
53 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<void>
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<WebCryptoCheckStatus>('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<string, { url: string; hasReferral?: boolean }> = {
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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 overflow-y-auto">
<div
className="bg-gray-800 rounded-lg w-full max-w-lg relative my-8"
style={{
background: '#1E2329',
maxHeight: 'calc(100vh - 4rem)',
}}
>
<div
className="flex items-center justify-between p-6 pb-4 sticky top-0 z-10"
style={{ background: '#1E2329' }}
>
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
{editingExchangeId
? t('editExchange', language)
: t('addExchange', language)}
</h3>
<div className="flex items-center gap-2">
{currentExchangeType === 'binance' && (
<button
type="button"
onClick={() => setShowGuide(true)}
className="px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105 flex items-center gap-2"
style={{
background: 'rgba(240, 185, 11, 0.1)',
color: '#F0B90B',
}}
>
<BookOpen className="w-4 h-4" />
{t('viewGuide', language)}
</button>
)}
{editingExchangeId && (
<button
type="button"
onClick={() => onDelete(editingExchangeId)}
className="p-2 rounded hover:bg-red-100 transition-colors"
style={{
background: 'rgba(246, 70, 93, 0.1)',
color: '#F6465D',
}}
title={t('delete', language)}
>
<Trash2 className="w-4 h-4" />
</button>
)}
</div>
</div>
<form onSubmit={handleSubmit} className="px-6 pb-6">
<div
className="space-y-4 overflow-y-auto"
style={{ maxHeight: 'calc(100vh - 16rem)' }}
>
{!editingExchangeId && (
<div className="space-y-3">
<div className="space-y-2">
<div
className="text-xs font-semibold uppercase tracking-wide"
style={{ color: '#F0B90B' }}
>
{t('environmentSteps.checkTitle', language)}
</div>
<WebCryptoEnvironmentCheck
language={language}
variant="card"
onStatusChange={setWebCryptoStatus}
/>
</div>
<div className="space-y-2">
<div
className="text-xs font-semibold uppercase tracking-wide"
style={{ color: '#F0B90B' }}
>
{t('environmentSteps.selectTitle', language)}
</div>
<select
value={selectedExchangeType}
onChange={(e) => setSelectedExchangeType(e.target.value)}
className="w-full px-3 py-2 rounded"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
aria-label={t('selectExchange', language)}
disabled={
webCryptoStatus !== 'secure' &&
webCryptoStatus !== 'disabled'
}
required
>
<option value="">
{t('pleaseSelectExchange', language)}
</option>
{SUPPORTED_EXCHANGE_TEMPLATES.map((template) => (
<option key={template.exchange_type} value={template.exchange_type}>
{getShortName(template.name)} (
{template.type.toUpperCase()})
</option>
))}
</select>
</div>
</div>
)}
{selectedTemplate && (
<div
className="p-4 rounded"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 flex items-center justify-center">
{getExchangeIcon(selectedTemplate.exchange_type, {
width: 32,
height: 32,
})}
</div>
<div>
<div className="font-semibold" style={{ color: '#EAECEF' }}>
{getShortName(selectedTemplate.name)}
{editingExchangeId && selectedExchange?.account_name && (
<span className="text-sm font-normal ml-2" style={{ color: '#848E9C' }}>
- {selectedExchange.account_name}
</span>
)}
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
{selectedTemplate.type.toUpperCase()} {' '}
{selectedTemplate.exchange_type}
</div>
</div>
</div>
{/* 账户名称输入 */}
<div className="mt-3">
<label
className="block text-sm font-semibold mb-2"
style={{ color: '#EAECEF' }}
>
{language === 'zh' ? '账户名称' : 'Account Name'} *
</label>
<input
type="text"
value={accountName}
onChange={(e) => 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
/>
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
{language === 'zh'
? '为此账户设置一个易于识别的名称,以便区分同一交易所的多个账户'
: 'Set an easily recognizable name for this account to distinguish multiple accounts on the same exchange'}
</div>
</div>
{/* 注册链接 */}
<a
href={exchangeRegistrationLinks[currentExchangeType || '']?.url || '#'}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between p-3 rounded-lg mt-3 transition-all hover:scale-[1.02]"
style={{
background: 'rgba(240, 185, 11, 0.08)',
border: '1px solid rgba(240, 185, 11, 0.2)',
}}
>
<div className="flex items-center gap-2">
<UserPlus className="w-4 h-4" style={{ color: '#F0B90B' }} />
<span className="text-sm" style={{ color: '#EAECEF' }}>
{language === 'zh' ? '还没有交易所账号?点击注册' : "No exchange account? Register here"}
</span>
{exchangeRegistrationLinks[currentExchangeType || '']?.hasReferral && (
<span
className="text-xs px-1.5 py-0.5 rounded"
style={{ background: 'rgba(14, 203, 129, 0.2)', color: '#0ECB81' }}
>
{language === 'zh' ? '折扣优惠' : 'Discount'}
</span>
)}
</div>
<ExternalLink className="w-4 h-4" style={{ color: '#848E9C' }} />
</a>
</div>
)}
{selectedTemplate && (
<>
{/* Binance/Bybit/OKX/Bitget 的输入字段 */}
{(currentExchangeType === 'binance' ||
currentExchangeType === 'bybit' ||
currentExchangeType === 'okx' ||
currentExchangeType === 'bitget') && (
<>
{/* 币安用户配置提示 (D1 方案) */}
{currentExchangeType === 'binance' && (
<div
className="mb-4 p-3 rounded cursor-pointer transition-colors"
style={{
background: '#1a3a52',
border: '1px solid #2b5278',
}}
onClick={() => setShowBinanceGuide(!showBinanceGuide)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span style={{ color: '#58a6ff' }}></span>
<span
className="text-sm font-medium"
style={{ color: '#EAECEF' }}
>
<strong></strong>
使API
API
</span>
</div>
<span style={{ color: '#8b949e' }}>
{showBinanceGuide ? '▲' : '▼'}
</span>
</div>
{/* 展开的详细说明 */}
{showBinanceGuide && (
<div
className="mt-3 pt-3"
style={{
borderTop: '1px solid #2b5278',
fontSize: '0.875rem',
color: '#c9d1d9',
}}
onClick={(e) => e.stopPropagation()}
>
<p className="mb-2" style={{ color: '#8b949e' }}>
<strong></strong> API
</p>
<p
className="font-semibold mb-1"
style={{ color: '#EAECEF' }}
>
</p>
<ol
className="list-decimal list-inside space-y-1 mb-3"
style={{ paddingLeft: '0.5rem' }}
>
<li>
{' '}
<strong>API </strong>
</li>
<li>
API
<strong> API </strong>
</li>
<li>
<strong></strong>
<span style={{ color: '#f85149' }}>
</span>
</li>
<li>
IP <strong></strong>
IP
</li>
</ol>
<p
className="mb-2 p-2 rounded"
style={{
background: '#3d2a00',
border: '1px solid #9e6a03',
}}
>
💡 <strong></strong>
使
</p>
<a
href="https://www.binance.com/zh-CN/support/faq/how-to-create-api-keys-on-binance-360002502072"
target="_blank"
rel="noopener noreferrer"
className="inline-block text-sm hover:underline"
style={{ color: '#58a6ff' }}
>
📖
</a>
</div>
)}
</div>
)}
<div>
<label
className="block text-sm font-semibold mb-2"
style={{ color: '#EAECEF' }}
>
{t('apiKey', language)}
</label>
<input
type="password"
value={apiKey}
onChange={(e) => 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
/>
</div>
<div>
<label
className="block text-sm font-semibold mb-2"
style={{ color: '#EAECEF' }}
>
{t('secretKey', language)}
</label>
<input
type="password"
value={secretKey}
onChange={(e) => 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
/>
</div>
{(currentExchangeType === 'okx' || currentExchangeType === 'bitget') && (
<div>
<label
className="block text-sm font-semibold mb-2"
style={{ color: '#EAECEF' }}
>
{t('passphrase', language)}
</label>
<input
type="password"
value={passphrase}
onChange={(e) => 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
/>
</div>
)}
{/* Binance 白名单IP提示 */}
{currentExchangeType === 'binance' && (
<div
className="p-4 rounded"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.2)',
}}
>
<div
className="text-sm font-semibold mb-2"
style={{ color: '#F0B90B' }}
>
{t('whitelistIP', language)}
</div>
<div
className="text-xs mb-3"
style={{ color: '#848E9C' }}
>
{t('whitelistIPDesc', language)}
</div>
{loadingIP ? (
<div
className="text-xs"
style={{ color: '#848E9C' }}
>
{t('loadingServerIP', language)}
</div>
) : serverIP && serverIP.public_ip ? (
<div
className="flex items-center gap-2 p-2 rounded"
style={{ background: '#0B0E11' }}
>
<code
className="flex-1 text-sm font-mono"
style={{ color: '#F0B90B' }}
>
{serverIP.public_ip}
</code>
<button
type="button"
onClick={() => handleCopyIP(serverIP.public_ip)}
className="px-3 py-1 rounded text-xs font-semibold transition-all hover:scale-105"
style={{
background: 'rgba(240, 185, 11, 0.2)',
color: '#F0B90B',
}}
>
{copiedIP
? t('ipCopied', language)
: t('copyIP', language)}
</button>
</div>
) : null}
</div>
)}
</>
)}
{/* Aster 交易所的字段 */}
{currentExchangeType === 'aster' && (
<>
{/* API Pro 代理钱包说明 banner */}
<div
className="p-3 rounded mb-4"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.3)',
}}
>
<div className="flex items-start gap-2">
<span style={{ color: '#F0B90B', fontSize: '16px' }}>
🔐
</span>
<div className="flex-1">
<div
className="text-sm font-semibold mb-1"
style={{ color: '#F0B90B' }}
>
{t('asterApiProTitle', language)}
</div>
<div
className="text-xs"
style={{ color: '#848E9C', lineHeight: '1.5' }}
>
{t('asterApiProDesc', language)}
</div>
</div>
</div>
</div>
{/* 主钱包地址 */}
<div>
<label
className="block text-sm font-semibold mb-2 flex items-center gap-2"
style={{ color: '#EAECEF' }}
>
{t('asterUserLabel', language)}
<Tooltip content={t('asterUserDesc', language)}>
<HelpCircle
className="w-4 h-4 cursor-help"
style={{ color: '#F0B90B' }}
/>
</Tooltip>
</label>
<input
type="text"
value={asterUser}
onChange={(e) => 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
/>
<div
className="text-xs mt-1"
style={{ color: '#848E9C' }}
>
{t('asterUserDesc', language)}
</div>
</div>
{/* API Pro 代理钱包地址 */}
<div>
<label
className="block text-sm font-semibold mb-2 flex items-center gap-2"
style={{ color: '#EAECEF' }}
>
{t('asterSignerLabel', language)}
<Tooltip content={t('asterSignerDesc', language)}>
<HelpCircle
className="w-4 h-4 cursor-help"
style={{ color: '#F0B90B' }}
/>
</Tooltip>
</label>
<input
type="text"
value={asterSigner}
onChange={(e) => 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
/>
<div
className="text-xs mt-1"
style={{ color: '#848E9C' }}
>
{t('asterSignerDesc', language)}
</div>
</div>
{/* API Pro 代理钱包私钥 */}
<div>
<label
className="block text-sm font-semibold mb-2 flex items-center gap-2"
style={{ color: '#EAECEF' }}
>
{t('asterPrivateKeyLabel', language)}
<Tooltip content={t('asterPrivateKeyDesc', language)}>
<HelpCircle
className="w-4 h-4 cursor-help"
style={{ color: '#F0B90B' }}
/>
</Tooltip>
</label>
<input
type="password"
value={asterPrivateKey}
onChange={(e) => 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
/>
<div
className="text-xs mt-1"
style={{ color: '#848E9C' }}
>
{t('asterPrivateKeyDesc', language)}
</div>
</div>
</>
)}
{/* Hyperliquid 交易所的字段 */}
{currentExchangeType === 'hyperliquid' && (
<>
{/* 安全提示 banner */}
<div
className="p-3 rounded mb-4"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.3)',
}}
>
<div className="flex items-start gap-2">
<span style={{ color: '#F0B90B', fontSize: '16px' }}>
🔐
</span>
<div className="flex-1">
<div
className="text-sm font-semibold mb-1"
style={{ color: '#F0B90B' }}
>
{t('hyperliquidAgentWalletTitle', language)}
</div>
<div
className="text-xs"
style={{ color: '#848E9C', lineHeight: '1.5' }}
>
{t('hyperliquidAgentWalletDesc', language)}
</div>
</div>
</div>
</div>
{/* Agent Private Key 字段 */}
<div>
<label
className="block text-sm font-semibold mb-2"
style={{ color: '#EAECEF' }}
>
{t('hyperliquidAgentPrivateKey', language)}
</label>
<div className="flex flex-col gap-2">
<div className="flex gap-2">
<input
type="text"
value={maskSecret(apiKey)}
readOnly
placeholder={t(
'enterHyperliquidAgentPrivateKey',
language
)}
className="w-full px-3 py-2 rounded"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
/>
<button
type="button"
onClick={() => setSecureInputTarget('hyperliquid')}
className="px-3 py-2 rounded text-xs font-semibold transition-all hover:scale-105"
style={{
background: '#F0B90B',
color: '#000',
whiteSpace: 'nowrap',
}}
>
{apiKey
? t('secureInputReenter', language)
: t('secureInputButton', language)}
</button>
{apiKey && (
<button
type="button"
onClick={() => setApiKey('')}
className="px-3 py-2 rounded text-xs font-semibold transition-all hover:scale-105"
style={{
background: '#1B1F2B',
color: '#848E9C',
whiteSpace: 'nowrap',
}}
>
{t('secureInputClear', language)}
</button>
)}
</div>
{apiKey && (
<div className="text-xs" style={{ color: '#848E9C' }}>
{t('secureInputHint', language)}
</div>
)}
</div>
<div
className="text-xs mt-1"
style={{ color: '#848E9C' }}
>
{t('hyperliquidAgentPrivateKeyDesc', language)}
</div>
</div>
{/* Main Wallet Address 字段 */}
<div>
<label
className="block text-sm font-semibold mb-2"
style={{ color: '#EAECEF' }}
>
{t('hyperliquidMainWalletAddress', language)}
</label>
<input
type="text"
value={hyperliquidWalletAddr}
onChange={(e) =>
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
/>
<div
className="text-xs mt-1"
style={{ color: '#848E9C' }}
>
{t('hyperliquidMainWalletAddressDesc', language)}
</div>
</div>
</>
)}
{/* LIGHTER 特定配置 */}
{currentExchangeType === 'lighter' && (
<>
{/* Info banner */}
<div
className="p-3 rounded mb-4"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.3)',
}}
>
<div className="flex items-start gap-2">
<span style={{ color: '#F0B90B', fontSize: '16px' }}>🔐</span>
<div className="flex-1">
<div className="text-sm font-semibold mb-1" style={{ color: '#F0B90B' }}>
{language === 'zh' ? 'Lighter API Key 配置' : 'Lighter API Key Setup'}
</div>
<div className="text-xs" style={{ color: '#848E9C', lineHeight: '1.5' }}>
{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.'}
</div>
</div>
</div>
</div>
{/* L1 Wallet Address */}
<div className="mb-4">
<label
className="block text-sm font-semibold mb-2"
style={{ color: '#EAECEF' }}
>
{t('lighterWalletAddress', language)} *
</label>
<input
type="text"
value={lighterWalletAddr}
onChange={(e) => 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
/>
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
{t('lighterWalletAddressDesc', language)}
</div>
</div>
{/* API Key Private Key */}
<div className="mb-4">
<label
className="block text-sm font-semibold mb-2"
style={{ color: '#EAECEF' }}
>
{t('lighterApiKeyPrivateKey', language)} *
<button
type="button"
onClick={() => setSecureInputTarget('lighter')}
className="ml-2 text-xs underline"
style={{ color: '#F0B90B' }}
>
{t('secureInputButton', language)}
</button>
</label>
<input
type="password"
value={lighterApiKeyPrivateKey}
onChange={(e) => 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
/>
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
{t('lighterApiKeyPrivateKeyDesc', language)}
</div>
</div>
{/* API Key Index */}
<div className="mb-4">
<label
className="block text-sm font-semibold mb-2 flex items-center gap-2"
style={{ color: '#EAECEF' }}
>
{language === 'zh' ? 'API Key 索引' : 'API Key Index'}
<Tooltip content={
language === 'zh'
? 'Lighter 允许每个账户创建多个 API Key最多256个。索引值对应您创建的第几个 API Key从0开始计数。如果您只创建了一个 API Key请使用默认值 0。'
: 'Lighter allows creating multiple API Keys per account (up to 256). The index corresponds to which API Key you created, starting from 0. If you only created one API Key, use the default value 0.'
}>
<HelpCircle
className="w-4 h-4 cursor-help"
style={{ color: '#F0B90B' }}
/>
</Tooltip>
</label>
<input
type="number"
min={0}
max={255}
value={lighterApiKeyIndex}
onChange={(e) => 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',
}}
/>
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
{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).'}
</div>
</div>
</>
)}
</>
)}
</div>
<div
className="flex gap-3 mt-6 pt-4 sticky bottom-0"
style={{ background: '#1E2329' }}
>
<button
type="button"
onClick={onClose}
className="flex-1 px-4 py-2 rounded text-sm font-semibold"
style={{ background: '#2B3139', color: '#848E9C' }}
>
{t('cancel', language)}
</button>
<button
type="submit"
disabled={
isSaving ||
!selectedTemplate ||
!accountName.trim() ||
(currentExchangeType === 'binance' &&
(!apiKey.trim() || !secretKey.trim())) ||
(currentExchangeType === 'okx' &&
(!apiKey.trim() ||
!secretKey.trim() ||
!passphrase.trim())) ||
(currentExchangeType === 'bitget' &&
(!apiKey.trim() ||
!secretKey.trim() ||
!passphrase.trim())) ||
(currentExchangeType === 'hyperliquid' &&
(!apiKey.trim() || !hyperliquidWalletAddr.trim())) || // 验证私钥和钱包地址
(currentExchangeType === 'aster' &&
(!asterUser.trim() ||
!asterSigner.trim() ||
!asterPrivateKey.trim())) ||
(currentExchangeType === 'lighter' &&
(!lighterWalletAddr.trim() || !lighterApiKeyPrivateKey.trim())) ||
(currentExchangeType === 'bybit' &&
(!apiKey.trim() || !secretKey.trim())) ||
(selectedTemplate?.type === 'cex' &&
currentExchangeType !== 'hyperliquid' &&
currentExchangeType !== 'aster' &&
currentExchangeType !== 'lighter' &&
currentExchangeType !== 'binance' &&
currentExchangeType !== 'bybit' &&
currentExchangeType !== 'okx' &&
currentExchangeType !== 'bitget' &&
(!apiKey.trim() || !secretKey.trim()))
}
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"
style={{ background: '#F0B90B', color: '#000' }}
>
{isSaving ? t('saving', language) || '保存中...' : t('saveConfig', language)}
</button>
</div>
</form>
</div>
{/* Binance Setup Guide Modal */}
{showGuide && (
<div
className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4"
onClick={() => setShowGuide(false)}
>
<div
className="bg-gray-800 rounded-lg p-6 w-full max-w-4xl relative"
style={{ background: '#1E2329' }}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-4">
<h3
className="text-xl font-bold flex items-center gap-2"
style={{ color: '#EAECEF' }}
>
<BookOpen className="w-6 h-6" style={{ color: '#F0B90B' }} />
{t('binanceSetupGuide', language)}
</h3>
<button
onClick={() => setShowGuide(false)}
className="px-4 py-2 rounded text-sm font-semibold transition-all hover:scale-105"
style={{ background: '#2B3139', color: '#848E9C' }}
>
{t('closeGuide', language)}
</button>
</div>
<div className="overflow-y-auto max-h-[80vh]">
<img
src="/images/guide.png"
alt={t('binanceSetupGuide', language)}
className="w-full h-auto rounded"
/>
</div>
</div>
</div>
)}
{/* Two Stage Key Modal */}
<TwoStageKeyModal
isOpen={secureInputTarget !== null}
language={language}
contextLabel={secureInputContextLabel}
expectedLength={64}
onCancel={handleSecureInputCancel}
onComplete={handleSecureInputComplete}
/>
</div>
)
}