diff --git a/config/database.go b/config/database.go index a2fd5732..0ba3a4b6 100644 --- a/config/database.go +++ b/config/database.go @@ -398,11 +398,12 @@ type ExchangeConfig struct { Name string `json:"name"` Type string `json:"type"` Enabled bool `json:"enabled"` - APIKey string `json:"apiKey"` - SecretKey string `json:"secretKey"` + APIKey string `json:"apiKey"` // For Binance: API Key; For Hyperliquid: Agent Private Key (should have ~0 balance) + SecretKey string `json:"secretKey"` // For Binance: Secret Key; Not used for Hyperliquid Testnet bool `json:"testnet"` - // Hyperliquid 特定字段 - HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` + // Hyperliquid Agent Wallet configuration (following official best practices) + // Reference: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets + HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` // Main Wallet Address (holds funds, never expose private key) // Aster 特定字段 AsterUser string `json:"asterUser"` AsterSigner string `json:"asterSigner"` diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index 1c4ad954..1d32d822 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -39,17 +39,29 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) apiURL = hyperliquid.TestnetAPIURL } - // 从私钥生成钱包地址(如果未提供) + // Security enhancement: Implement Agent Wallet best practices + // Reference: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets + agentAddr := crypto.PubkeyToAddress(*privateKey.Public().(*ecdsa.PublicKey)).Hex() + if walletAddr == "" { - pubKey := privateKey.Public() - publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey) - if !ok { - return nil, fmt.Errorf("无法转换公钥") - } - walletAddr = crypto.PubkeyToAddress(*publicKeyECDSA).Hex() - log.Printf("✓ 从私钥自动生成钱包地址: %s", walletAddr) + return nil, fmt.Errorf("❌ Configuration error: Main wallet address (hyperliquid_wallet_addr) not provided\n" + + "🔐 Correct configuration pattern:\n" + + " 1. hyperliquid_private_key = Agent Private Key (for signing only, balance should be ~0)\n" + + " 2. hyperliquid_wallet_addr = Main Wallet Address (holds funds, never expose private key)\n" + + "💡 Please create an Agent Wallet on Hyperliquid official website and authorize it before configuration:\n" + + " https://app.hyperliquid.xyz/ → Settings → API Wallets") + } + + // Check if user accidentally uses main wallet private key (security risk) + if strings.EqualFold(walletAddr, agentAddr) { + log.Printf("⚠️⚠️⚠️ WARNING: Main wallet address (%s) matches Agent wallet address!", walletAddr) + log.Printf(" This indicates you may be using your main wallet private key, which poses extremely high security risks!") + log.Printf(" Recommendation: Immediately create a separate Agent Wallet on Hyperliquid official website") + log.Printf(" Reference: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets") } else { - log.Printf("✓ 使用提供的钱包地址: %s", walletAddr) + log.Printf("✓ Using Agent Wallet mode (secure)") + log.Printf(" └─ Agent wallet address: %s (for signing)", agentAddr) + log.Printf(" └─ Main wallet address: %s (holds funds)", walletAddr) } ctx := context.Background() @@ -73,6 +85,39 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) return nil, fmt.Errorf("获取meta信息失败: %w", err) } + // 🔍 Security check: Validate Agent wallet balance (should be close to 0) + // Only check if using separate Agent wallet (not when main wallet is used as agent) + if !strings.EqualFold(walletAddr, agentAddr) { + agentState, err := exchange.Info().UserState(ctx, agentAddr) + if err == nil && agentState != nil && agentState.CrossMarginSummary != nil { + // Parse Agent wallet balance + agentBalance, _ := strconv.ParseFloat(agentState.CrossMarginSummary.AccountValue, 64) + + if agentBalance > 100 { + // Critical: Agent wallet holds too much funds + log.Printf("🚨🚨🚨 CRITICAL SECURITY WARNING 🚨🚨🚨") + log.Printf(" Agent wallet balance: %.2f USDC (exceeds safe threshold of 100 USDC)", agentBalance) + log.Printf(" Agent wallet address: %s", agentAddr) + log.Printf(" ⚠️ Agent wallets should only be used for signing and hold minimal/zero balance") + log.Printf(" ⚠️ High balance in Agent wallet poses security risks") + log.Printf(" 📖 Reference: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets") + log.Printf(" 💡 Recommendation: Transfer funds to main wallet and keep Agent wallet balance near 0") + return nil, fmt.Errorf("security check failed: Agent wallet balance too high (%.2f USDC), exceeds 100 USDC threshold", agentBalance) + } else if agentBalance > 10 { + // Warning: Agent wallet has some balance (acceptable but not ideal) + log.Printf("⚠️ Notice: Agent wallet address (%s) has some balance: %.2f USDC", agentAddr, agentBalance) + log.Printf(" While not critical, it's recommended to keep Agent wallet balance near 0 for security") + } else { + // OK: Agent wallet balance is safe + log.Printf("✓ Agent wallet balance is safe: %.2f USDC (near zero as recommended)", agentBalance) + } + } else if err != nil { + // Failed to query agent balance - log warning but don't block initialization + log.Printf("⚠️ Could not verify Agent wallet balance (query failed): %v", err) + log.Printf(" Proceeding with initialization, but please manually verify Agent wallet balance is near 0") + } + } + return &HyperliquidTrader{ exchange: exchange, ctx: ctx, diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index c69956c6..aa19c715 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -140,9 +140,14 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { if (e.id === 'aster') { return e.asterUser && e.asterUser.trim() !== '' } - // Hyperliquid 只检查私钥 + // Hyperliquid 需要检查私钥和钱包地址 if (e.id === 'hyperliquid') { - return e.apiKey && e.apiKey.trim() !== '' + return ( + e.apiKey && + e.apiKey.trim() !== '' && + e.hyperliquidWalletAddr && + e.hyperliquidWalletAddr.trim() !== '' + ) } // 其他交易所检查 apiKey return e.apiKey && e.apiKey.trim() !== '' @@ -166,9 +171,14 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { ) } - // Hyperliquid 只需要私钥(作为apiKey),钱包地址会自动从私钥生成 + // Hyperliquid 需要私钥和钱包地址(Agent Wallet 模式) if (e.id === 'hyperliquid') { - return e.apiKey && e.apiKey.trim() !== '' + return ( + e.apiKey && + e.apiKey.trim() !== '' && + e.hyperliquidWalletAddr && + e.hyperliquidWalletAddr.trim() !== '' + ) } // Binance 等其他交易所需要 apiKey 和 secretKey @@ -1691,6 +1701,9 @@ function ExchangeConfigModal({ const [asterSigner, setAsterSigner] = useState('') const [asterPrivateKey, setAsterPrivateKey] = useState('') + // Hyperliquid 特定字段 + const [hyperliquidWalletAddr, setHyperliquidWalletAddr] = useState('') + // 获取当前编辑的交易所信息 const selectedExchange = allExchanges?.find( (e) => e.id === selectedExchangeId @@ -1708,6 +1721,9 @@ function ExchangeConfigModal({ setAsterUser(selectedExchange.asterUser || '') setAsterSigner(selectedExchange.asterSigner || '') setAsterPrivateKey('') // Don't load existing private key for security + + // Hyperliquid 字段 + setHyperliquidWalletAddr(selectedExchange.hyperliquidWalletAddr || '') } }, [editingExchangeId, selectedExchange]) @@ -1745,8 +1761,14 @@ function ExchangeConfigModal({ if (!apiKey.trim() || !secretKey.trim()) return await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet) } else if (selectedExchange?.id === 'hyperliquid') { - if (!apiKey.trim()) return // 只验证私钥,钱包地址自动从私钥生成 - await onSave(selectedExchangeId, apiKey.trim(), '', testnet, '') // 传空字符串,后端自动生成地址 + if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址 + await onSave( + selectedExchangeId, + apiKey.trim(), + '', + testnet, + hyperliquidWalletAddr.trim() + ) } else if (selectedExchange?.id === 'aster') { if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim()) return @@ -2106,18 +2128,48 @@ function ExchangeConfigModal({ {/* Hyperliquid 交易所的字段 */} {selectedExchange.id === 'hyperliquid' && ( <> + {/* 安全提示 banner */} +
+
+ + 🔐 + +
+
+ {t('hyperliquidAgentWalletTitle', language)} +
+
+ {t('hyperliquidAgentWalletDesc', language)} +
+
+
+
+ + {/* Agent Private Key 字段 */}
setApiKey(e.target.value)} - placeholder={t('enterPrivateKey', language)} + placeholder={t('enterHyperliquidAgentPrivateKey', language)} className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', @@ -2127,7 +2179,33 @@ function ExchangeConfigModal({ required />
- {t('hyperliquidPrivateKeyDesc', 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)}
@@ -2287,7 +2365,8 @@ function ExchangeConfigModal({ (!apiKey.trim() || !secretKey.trim() || !passphrase.trim())) || - (selectedExchange.id === 'hyperliquid' && !apiKey.trim()) || // 只验证私钥,钱包地址可选 + (selectedExchange.id === 'hyperliquid' && + (!apiKey.trim() || !hyperliquidWalletAddr.trim())) || // 验证私钥和钱包地址 (selectedExchange.id === 'aster' && (!asterUser.trim() || !asterSigner.trim() ||