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:
tinkle-community
2025-12-06 18:04:59 +08:00
committed by GitHub
parent 5d1d0b6842
commit 1e5ece947c
11 changed files with 1489 additions and 68 deletions

View File

@@ -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' }}

View File

@@ -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} />

View File

@@ -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' && (