mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-09 14:00:57 +08:00
Feature/okx trading (#1177)
* feat: add OKX exchange trading support - Add OKX trader client with full trading API integration - Support API Key, Secret Key, and Passphrase authentication - Add OKX icon and frontend configuration modal - Update exchange store and types for OKX fields * fix: add passphrase column migration and fix exchange type mapping * fix: show OKX input fields in exchange config modal * fix: ensure all supported exchanges exist for user when listing * fix: simplify exchange type check condition for OKX * debug: add visible debug info for exchange id * fix: remove debug info from exchange config modal * fix: add OKX to exchange type condition in AITradersPage * feat: complete OKX trading support and fix exchange config issues - Add LIGHTER exchange UI support in AITradersPage - Add passphrase field to UpdateExchangeConfigRequest type - Fix OKX HTTP client to bypass proxy (disable system proxy) - Auto-fetch initial balance from exchange when not set - Support multiple balance field names for different exchanges - Add detailed error messages when trader fails to load - Add lighter_api_key_private_key field to exchange store
This commit is contained in:
@@ -601,11 +601,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
exchangeId: string,
|
||||
apiKey: string,
|
||||
secretKey?: string,
|
||||
passphrase?: string,
|
||||
testnet?: boolean,
|
||||
hyperliquidWalletAddr?: string,
|
||||
asterUser?: string,
|
||||
asterSigner?: string,
|
||||
asterPrivateKey?: string
|
||||
asterPrivateKey?: string,
|
||||
lighterWalletAddr?: string,
|
||||
lighterPrivateKey?: string,
|
||||
lighterApiKeyPrivateKey?: string
|
||||
) => {
|
||||
try {
|
||||
// 找到要配置的交易所(从supportedExchanges中)
|
||||
@@ -630,11 +634,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
...e,
|
||||
apiKey,
|
||||
secretKey,
|
||||
passphrase,
|
||||
testnet,
|
||||
hyperliquidWalletAddr,
|
||||
asterUser,
|
||||
asterSigner,
|
||||
asterPrivateKey,
|
||||
lighterWalletAddr,
|
||||
lighterPrivateKey,
|
||||
lighterApiKeyPrivateKey,
|
||||
enabled: true,
|
||||
}
|
||||
: e
|
||||
@@ -645,11 +653,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
...exchangeToUpdate,
|
||||
apiKey,
|
||||
secretKey,
|
||||
passphrase,
|
||||
testnet,
|
||||
hyperliquidWalletAddr,
|
||||
asterUser,
|
||||
asterSigner,
|
||||
asterPrivateKey,
|
||||
lighterWalletAddr,
|
||||
lighterPrivateKey,
|
||||
lighterApiKeyPrivateKey,
|
||||
enabled: true,
|
||||
}
|
||||
updatedExchanges = [...(allExchanges || []), newExchange]
|
||||
@@ -663,11 +675,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
enabled: exchange.enabled,
|
||||
api_key: exchange.apiKey || '',
|
||||
secret_key: exchange.secretKey || '',
|
||||
passphrase: exchange.passphrase || '',
|
||||
testnet: exchange.testnet || false,
|
||||
hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '',
|
||||
aster_user: exchange.asterUser || '',
|
||||
aster_signer: exchange.asterSigner || '',
|
||||
aster_private_key: exchange.asterPrivateKey || '',
|
||||
lighter_wallet_addr: exchange.lighterWalletAddr || '',
|
||||
lighter_private_key: exchange.lighterPrivateKey || '',
|
||||
lighter_api_key_private_key: exchange.lighterApiKeyPrivateKey || '',
|
||||
},
|
||||
])
|
||||
),
|
||||
@@ -1758,11 +1774,15 @@ function ExchangeConfigModal({
|
||||
exchangeId: string,
|
||||
apiKey: string,
|
||||
secretKey?: string,
|
||||
passphrase?: string,
|
||||
testnet?: boolean,
|
||||
hyperliquidWalletAddr?: string,
|
||||
asterUser?: string,
|
||||
asterSigner?: string,
|
||||
asterPrivateKey?: string
|
||||
asterPrivateKey?: string,
|
||||
lighterWalletAddr?: string,
|
||||
lighterPrivateKey?: string,
|
||||
lighterApiKeyPrivateKey?: string
|
||||
) => Promise<void>
|
||||
onDelete: (exchangeId: string) => void
|
||||
onClose: () => void
|
||||
@@ -1796,9 +1816,14 @@ function ExchangeConfigModal({
|
||||
// Hyperliquid 特定字段
|
||||
const [hyperliquidWalletAddr, setHyperliquidWalletAddr] = useState('')
|
||||
|
||||
// LIGHTER 特定字段
|
||||
const [lighterWalletAddr, setLighterWalletAddr] = useState('')
|
||||
const [lighterPrivateKey, setLighterPrivateKey] = useState('')
|
||||
const [lighterApiKeyPrivateKey, setLighterApiKeyPrivateKey] = useState('')
|
||||
|
||||
// 安全输入状态
|
||||
const [secureInputTarget, setSecureInputTarget] = useState<
|
||||
null | 'hyperliquid' | 'aster'
|
||||
null | 'hyperliquid' | 'aster' | 'lighter'
|
||||
>(null)
|
||||
|
||||
// 获取当前编辑的交易所信息
|
||||
@@ -1821,6 +1846,11 @@ function ExchangeConfigModal({
|
||||
|
||||
// Hyperliquid 字段
|
||||
setHyperliquidWalletAddr(selectedExchange.hyperliquidWalletAddr || '')
|
||||
|
||||
// LIGHTER 字段
|
||||
setLighterWalletAddr(selectedExchange.lighterWalletAddr || '')
|
||||
setLighterPrivateKey('') // Don't load existing private key for security
|
||||
setLighterApiKeyPrivateKey('') // Don't load existing API key for security
|
||||
}
|
||||
}, [editingExchangeId, selectedExchange])
|
||||
|
||||
@@ -1926,15 +1956,19 @@ function ExchangeConfigModal({
|
||||
if (!selectedExchangeId) return
|
||||
|
||||
// 根据交易所类型验证不同字段
|
||||
if (selectedExchange?.id === 'binance') {
|
||||
if (selectedExchange?.id === 'binance' || selectedExchange?.id === 'bybit') {
|
||||
if (!apiKey.trim() || !secretKey.trim()) return
|
||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
|
||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), '', testnet)
|
||||
} else if (selectedExchange?.id === 'okx') {
|
||||
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return
|
||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), passphrase.trim(), testnet)
|
||||
} else if (selectedExchange?.id === 'hyperliquid') {
|
||||
if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址
|
||||
if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return
|
||||
await onSave(
|
||||
selectedExchangeId,
|
||||
apiKey.trim(),
|
||||
'',
|
||||
'',
|
||||
testnet,
|
||||
hyperliquidWalletAddr.trim()
|
||||
)
|
||||
@@ -1945,19 +1979,33 @@ function ExchangeConfigModal({
|
||||
selectedExchangeId,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
testnet,
|
||||
undefined,
|
||||
asterUser.trim(),
|
||||
asterSigner.trim(),
|
||||
asterPrivateKey.trim()
|
||||
)
|
||||
} else if (selectedExchange?.id === 'okx') {
|
||||
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return
|
||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
|
||||
} else if (selectedExchange?.id === 'lighter') {
|
||||
if (!lighterWalletAddr.trim() || !lighterPrivateKey.trim()) return
|
||||
await onSave(
|
||||
selectedExchangeId,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
testnet,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
lighterWalletAddr.trim(),
|
||||
lighterPrivateKey.trim(),
|
||||
lighterApiKeyPrivateKey.trim()
|
||||
)
|
||||
} else {
|
||||
// 默认情况(其他CEX交易所)
|
||||
if (!apiKey.trim() || !secretKey.trim()) return
|
||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
|
||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), '', testnet)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2095,9 +2143,10 @@ function ExchangeConfigModal({
|
||||
|
||||
{selectedExchange && (
|
||||
<>
|
||||
{/* Binance/Bybit 和其他 CEX 交易所的字段 */}
|
||||
{/* Binance/Bybit/OKX 和其他 CEX 交易所的字段 */}
|
||||
{(selectedExchange.id === 'binance' ||
|
||||
selectedExchange.id === 'bybit' ||
|
||||
selectedExchange.id === 'okx' ||
|
||||
selectedExchange.type === 'cex') &&
|
||||
selectedExchange.id !== 'hyperliquid' &&
|
||||
selectedExchange.id !== 'aster' && (
|
||||
@@ -2553,6 +2602,104 @@ function ExchangeConfigModal({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* LIGHTER 交易所的字段 */}
|
||||
{selectedExchange.id === 'lighter' && (
|
||||
<>
|
||||
<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>
|
||||
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="block text-sm font-semibold mb-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{t('lighterPrivateKey', language)}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={lighterPrivateKey}
|
||||
onChange={(e) => setLighterPrivateKey(e.target.value)}
|
||||
placeholder={t('enterLighterPrivateKey', 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('lighterPrivateKeyDesc', language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label
|
||||
className="block text-sm font-semibold mb-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{t('lighterApiKeyPrivateKey', language)} ⭐
|
||||
</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',
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||
{t('lighterApiKeyPrivateKeyDesc', language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 p-3 rounded" style={{
|
||||
background: lighterApiKeyPrivateKey ? '#0F3F2E' : '#3F2E0F',
|
||||
border: '1px solid ' + (lighterApiKeyPrivateKey ? '#10B981' : '#F59E0B')
|
||||
}}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm font-semibold" style={{
|
||||
color: lighterApiKeyPrivateKey ? '#10B981' : '#F59E0B'
|
||||
}}>
|
||||
{lighterApiKeyPrivateKey ? '✅ LIGHTER V2' : '⚠️ LIGHTER V1'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||
{lighterApiKeyPrivateKey
|
||||
? t('lighterV2Description', language)
|
||||
: t('lighterV1Description', language)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -2575,25 +2722,20 @@ function ExchangeConfigModal({
|
||||
!selectedExchange ||
|
||||
(selectedExchange.id === 'binance' &&
|
||||
(!apiKey.trim() || !secretKey.trim())) ||
|
||||
(selectedExchange.id === 'bybit' &&
|
||||
(!apiKey.trim() || !secretKey.trim())) ||
|
||||
(selectedExchange.id === 'okx' &&
|
||||
(!apiKey.trim() ||
|
||||
!secretKey.trim() ||
|
||||
!passphrase.trim())) ||
|
||||
(selectedExchange.id === 'hyperliquid' &&
|
||||
(!apiKey.trim() || !hyperliquidWalletAddr.trim())) || // 验证私钥和钱包地址
|
||||
(!apiKey.trim() || !hyperliquidWalletAddr.trim())) ||
|
||||
(selectedExchange.id === 'aster' &&
|
||||
(!asterUser.trim() ||
|
||||
!asterSigner.trim() ||
|
||||
!asterPrivateKey.trim())) ||
|
||||
(selectedExchange.id === 'bybit' &&
|
||||
(!apiKey.trim() || !secretKey.trim())) ||
|
||||
(selectedExchange.type === 'cex' &&
|
||||
selectedExchange.id !== 'hyperliquid' &&
|
||||
selectedExchange.id !== 'aster' &&
|
||||
selectedExchange.id !== 'binance' &&
|
||||
selectedExchange.id !== 'bybit' &&
|
||||
selectedExchange.id !== 'okx' &&
|
||||
(!apiKey.trim() || !secretKey.trim()))
|
||||
(selectedExchange.id === 'lighter' &&
|
||||
(!lighterWalletAddr.trim() || !lighterPrivateKey.trim()))
|
||||
}
|
||||
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"
|
||||
style={{ background: '#F0B90B', color: '#000' }}
|
||||
|
||||
@@ -80,6 +80,28 @@ const BybitIcon: React.FC<IconProps> = ({
|
||||
</svg>
|
||||
)
|
||||
|
||||
// OKX SVG 图标组件
|
||||
const OKXIcon: React.FC<IconProps> = ({
|
||||
width = 24,
|
||||
height = 24,
|
||||
className,
|
||||
}) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 200 200"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="200" height="200" rx="24" fill="#000"/>
|
||||
<rect x="40" y="40" width="50" height="50" rx="8" fill="#fff"/>
|
||||
<rect x="110" y="40" width="50" height="50" rx="8" fill="#fff"/>
|
||||
<rect x="40" y="110" width="50" height="50" rx="8" fill="#fff"/>
|
||||
<rect x="110" y="110" width="50" height="50" rx="8" fill="#fff"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
// Aster SVG 图标组件
|
||||
const AsterIcon: React.FC<IconProps> = ({
|
||||
width = 24,
|
||||
@@ -168,11 +190,13 @@ export const getExchangeIcon = (
|
||||
? 'binance'
|
||||
: exchangeType.toLowerCase().includes('bybit')
|
||||
? 'bybit'
|
||||
: exchangeType.toLowerCase().includes('hyperliquid')
|
||||
? 'hyperliquid'
|
||||
: exchangeType.toLowerCase().includes('aster')
|
||||
? 'aster'
|
||||
: exchangeType.toLowerCase()
|
||||
: exchangeType.toLowerCase().includes('okx')
|
||||
? 'okx'
|
||||
: exchangeType.toLowerCase().includes('hyperliquid')
|
||||
? 'hyperliquid'
|
||||
: exchangeType.toLowerCase().includes('aster')
|
||||
? 'aster'
|
||||
: exchangeType.toLowerCase()
|
||||
|
||||
const iconProps = {
|
||||
width: props.width || 24,
|
||||
@@ -185,6 +209,8 @@ export const getExchangeIcon = (
|
||||
return <BinanceIcon {...iconProps} />
|
||||
case 'bybit':
|
||||
return <BybitIcon {...iconProps} />
|
||||
case 'okx':
|
||||
return <OKXIcon {...iconProps} />
|
||||
case 'hyperliquid':
|
||||
case 'dex':
|
||||
return <HyperliquidIcon {...iconProps} />
|
||||
|
||||
@@ -23,6 +23,7 @@ interface ExchangeConfigModalProps {
|
||||
exchangeId: string,
|
||||
apiKey: string,
|
||||
secretKey?: string,
|
||||
passphrase?: string, // OKX专用
|
||||
testnet?: boolean,
|
||||
hyperliquidWalletAddr?: string,
|
||||
asterUser?: string,
|
||||
@@ -222,13 +223,17 @@ export function ExchangeConfigModal({
|
||||
// 根据交易所类型验证不同字段
|
||||
if (selectedExchange?.id === 'binance') {
|
||||
if (!apiKey.trim() || !secretKey.trim()) return
|
||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
|
||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), '', testnet)
|
||||
} else if (selectedExchange?.id === 'okx') {
|
||||
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return
|
||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), passphrase.trim(), testnet)
|
||||
} else if (selectedExchange?.id === 'hyperliquid') {
|
||||
if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址
|
||||
await onSave(
|
||||
selectedExchangeId,
|
||||
apiKey.trim(),
|
||||
'',
|
||||
'',
|
||||
testnet,
|
||||
hyperliquidWalletAddr.trim()
|
||||
)
|
||||
@@ -239,6 +244,7 @@ export function ExchangeConfigModal({
|
||||
selectedExchangeId,
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
testnet,
|
||||
undefined,
|
||||
asterUser.trim(),
|
||||
@@ -251,6 +257,7 @@ export function ExchangeConfigModal({
|
||||
selectedExchangeId,
|
||||
lighterPrivateKey.trim(),
|
||||
'',
|
||||
'',
|
||||
testnet,
|
||||
lighterWalletAddr.trim(),
|
||||
undefined,
|
||||
@@ -260,13 +267,10 @@ export function ExchangeConfigModal({
|
||||
lighterPrivateKey.trim(),
|
||||
lighterApiKeyPrivateKey.trim()
|
||||
)
|
||||
} else if (selectedExchange?.id === 'okx') {
|
||||
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return
|
||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
|
||||
} else {
|
||||
// 默认情况(其他CEX交易所)
|
||||
if (!apiKey.trim() || !secretKey.trim()) return
|
||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
|
||||
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), '', testnet)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,12 +408,10 @@ export function ExchangeConfigModal({
|
||||
|
||||
{selectedExchange && (
|
||||
<>
|
||||
{/* Binance/Bybit 和其他 CEX 交易所的字段 */}
|
||||
{/* Binance/Bybit/OKX 的输入字段 */}
|
||||
{(selectedExchange.id === 'binance' ||
|
||||
selectedExchange.id === 'bybit' ||
|
||||
selectedExchange.type === 'cex') &&
|
||||
selectedExchange.id !== 'hyperliquid' &&
|
||||
selectedExchange.id !== 'aster' && (
|
||||
selectedExchange.id === 'okx') && (
|
||||
<>
|
||||
{/* 币安用户配置提示 (D1 方案) */}
|
||||
{selectedExchange.id === 'binance' && (
|
||||
|
||||
Reference in New Issue
Block a user