mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-11 14:56:57 +08:00
feat(hyperliquid): enhance Agent Wallet security model (#717)
## Background Hyperliquid official documentation recommends using Agent Wallet pattern for API trading: - Agent Wallet is used for signing only - Main Wallet Address is used for querying account data - Agent Wallet should not hold significant funds Reference: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets ## Current Implementation Current implementation allows auto-generating wallet address from private key, which simplifies user configuration but may lead to potential security concerns if users accidentally use their main wallet private key. ## Enhancement Following the proven pattern already used in Aster exchange implementation (which uses dual-address mode), this enhancement upgrades Hyperliquid to Agent Wallet mode: ### Core Changes 1. **Mandatory dual-address configuration** - Agent Private Key (for signing) - Main Wallet Address (holds funds) 2. **Multi-layer security checks** - Detect if user accidentally uses main wallet private key - Validate Agent wallet balance (reject if > 100 USDC) - Provide detailed configuration guidance 3. **Design consistency** - Align with Aster's dual-address pattern - Follow Hyperliquid official best practices ### Code Changes **config/database.go**: - Add inline comments clarifying Agent Wallet security model **trader/hyperliquid_trader.go**: - Require explicit main wallet address (no auto-generation) - Check if agent address matches main wallet address (security risk indicator) - Query agent wallet balance and block if excessive - Display both agent and main wallet addresses for transparency **web/src/components/AITradersPage.tsx**: - Add security alert banner explaining Agent Wallet mode - Separate required inputs for Agent Private Key and Main Wallet Address - Add field descriptions and validation ### Benefits - ✅ Aligns with Hyperliquid official security recommendations - ✅ Maintains design consistency with Aster implementation - ✅ Multi-layer protection against configuration mistakes - ✅ Detailed logging for troubleshooting ### Breaking Change Users must now explicitly provide main wallet address (hyperliquid_wallet_addr). Old configurations will receive clear error messages with migration guidance. ### Migration Guide **Before** (single private key): ```json { "hyperliquid_private_key": "0x..." } ``` **After** (Agent Wallet mode): ```json { "hyperliquid_private_key": "0x...", // Agent Wallet private key "hyperliquid_wallet_addr": "0x..." // Main Wallet address } ``` Users can create Agent Wallet on Hyperliquid official website: https://app.hyperliquid.xyz/ → Settings → API Wallets 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
28a63f4d48
commit
b48dfe7bfd
@@ -398,11 +398,12 @@ type ExchangeConfig struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
APIKey string `json:"apiKey"`
|
APIKey string `json:"apiKey"` // For Binance: API Key; For Hyperliquid: Agent Private Key (should have ~0 balance)
|
||||||
SecretKey string `json:"secretKey"`
|
SecretKey string `json:"secretKey"` // For Binance: Secret Key; Not used for Hyperliquid
|
||||||
Testnet bool `json:"testnet"`
|
Testnet bool `json:"testnet"`
|
||||||
// Hyperliquid 特定字段
|
// Hyperliquid Agent Wallet configuration (following official best practices)
|
||||||
HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"`
|
// 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 特定字段
|
// Aster 特定字段
|
||||||
AsterUser string `json:"asterUser"`
|
AsterUser string `json:"asterUser"`
|
||||||
AsterSigner string `json:"asterSigner"`
|
AsterSigner string `json:"asterSigner"`
|
||||||
|
|||||||
@@ -39,17 +39,29 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool)
|
|||||||
apiURL = hyperliquid.TestnetAPIURL
|
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 == "" {
|
if walletAddr == "" {
|
||||||
pubKey := privateKey.Public()
|
return nil, fmt.Errorf("❌ Configuration error: Main wallet address (hyperliquid_wallet_addr) not provided\n" +
|
||||||
publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey)
|
"🔐 Correct configuration pattern:\n" +
|
||||||
if !ok {
|
" 1. hyperliquid_private_key = Agent Private Key (for signing only, balance should be ~0)\n" +
|
||||||
return nil, fmt.Errorf("无法转换公钥")
|
" 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" +
|
||||||
walletAddr = crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
|
" https://app.hyperliquid.xyz/ → Settings → API Wallets")
|
||||||
log.Printf("✓ 从私钥自动生成钱包地址: %s", walletAddr)
|
}
|
||||||
|
|
||||||
|
// 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 {
|
} 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()
|
ctx := context.Background()
|
||||||
@@ -73,6 +85,39 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool)
|
|||||||
return nil, fmt.Errorf("获取meta信息失败: %w", err)
|
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{
|
return &HyperliquidTrader{
|
||||||
exchange: exchange,
|
exchange: exchange,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
|
|||||||
@@ -140,9 +140,14 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
if (e.id === 'aster') {
|
if (e.id === 'aster') {
|
||||||
return e.asterUser && e.asterUser.trim() !== ''
|
return e.asterUser && e.asterUser.trim() !== ''
|
||||||
}
|
}
|
||||||
// Hyperliquid 只检查私钥
|
// Hyperliquid 需要检查私钥和钱包地址
|
||||||
if (e.id === 'hyperliquid') {
|
if (e.id === 'hyperliquid') {
|
||||||
return e.apiKey && e.apiKey.trim() !== ''
|
return (
|
||||||
|
e.apiKey &&
|
||||||
|
e.apiKey.trim() !== '' &&
|
||||||
|
e.hyperliquidWalletAddr &&
|
||||||
|
e.hyperliquidWalletAddr.trim() !== ''
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// 其他交易所检查 apiKey
|
// 其他交易所检查 apiKey
|
||||||
return e.apiKey && e.apiKey.trim() !== ''
|
return e.apiKey && e.apiKey.trim() !== ''
|
||||||
@@ -166,9 +171,14 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hyperliquid 只需要私钥(作为apiKey),钱包地址会自动从私钥生成
|
// Hyperliquid 需要私钥和钱包地址(Agent Wallet 模式)
|
||||||
if (e.id === 'hyperliquid') {
|
if (e.id === 'hyperliquid') {
|
||||||
return e.apiKey && e.apiKey.trim() !== ''
|
return (
|
||||||
|
e.apiKey &&
|
||||||
|
e.apiKey.trim() !== '' &&
|
||||||
|
e.hyperliquidWalletAddr &&
|
||||||
|
e.hyperliquidWalletAddr.trim() !== ''
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Binance 等其他交易所需要 apiKey 和 secretKey
|
// Binance 等其他交易所需要 apiKey 和 secretKey
|
||||||
@@ -1691,6 +1701,9 @@ function ExchangeConfigModal({
|
|||||||
const [asterSigner, setAsterSigner] = useState('')
|
const [asterSigner, setAsterSigner] = useState('')
|
||||||
const [asterPrivateKey, setAsterPrivateKey] = useState('')
|
const [asterPrivateKey, setAsterPrivateKey] = useState('')
|
||||||
|
|
||||||
|
// Hyperliquid 特定字段
|
||||||
|
const [hyperliquidWalletAddr, setHyperliquidWalletAddr] = useState('')
|
||||||
|
|
||||||
// 获取当前编辑的交易所信息
|
// 获取当前编辑的交易所信息
|
||||||
const selectedExchange = allExchanges?.find(
|
const selectedExchange = allExchanges?.find(
|
||||||
(e) => e.id === selectedExchangeId
|
(e) => e.id === selectedExchangeId
|
||||||
@@ -1708,6 +1721,9 @@ function ExchangeConfigModal({
|
|||||||
setAsterUser(selectedExchange.asterUser || '')
|
setAsterUser(selectedExchange.asterUser || '')
|
||||||
setAsterSigner(selectedExchange.asterSigner || '')
|
setAsterSigner(selectedExchange.asterSigner || '')
|
||||||
setAsterPrivateKey('') // Don't load existing private key for security
|
setAsterPrivateKey('') // Don't load existing private key for security
|
||||||
|
|
||||||
|
// Hyperliquid 字段
|
||||||
|
setHyperliquidWalletAddr(selectedExchange.hyperliquidWalletAddr || '')
|
||||||
}
|
}
|
||||||
}, [editingExchangeId, selectedExchange])
|
}, [editingExchangeId, selectedExchange])
|
||||||
|
|
||||||
@@ -1745,8 +1761,14 @@ function ExchangeConfigModal({
|
|||||||
if (!apiKey.trim() || !secretKey.trim()) return
|
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 === 'hyperliquid') {
|
} else if (selectedExchange?.id === 'hyperliquid') {
|
||||||
if (!apiKey.trim()) return // 只验证私钥,钱包地址自动从私钥生成
|
if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址
|
||||||
await onSave(selectedExchangeId, apiKey.trim(), '', testnet, '') // 传空字符串,后端自动生成地址
|
await onSave(
|
||||||
|
selectedExchangeId,
|
||||||
|
apiKey.trim(),
|
||||||
|
'',
|
||||||
|
testnet,
|
||||||
|
hyperliquidWalletAddr.trim()
|
||||||
|
)
|
||||||
} else if (selectedExchange?.id === 'aster') {
|
} else if (selectedExchange?.id === 'aster') {
|
||||||
if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim())
|
if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim())
|
||||||
return
|
return
|
||||||
@@ -2106,18 +2128,48 @@ function ExchangeConfigModal({
|
|||||||
{/* Hyperliquid 交易所的字段 */}
|
{/* Hyperliquid 交易所的字段 */}
|
||||||
{selectedExchange.id === 'hyperliquid' && (
|
{selectedExchange.id === '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>
|
<div>
|
||||||
<label
|
<label
|
||||||
className="block text-sm font-semibold mb-2"
|
className="block text-sm font-semibold mb-2"
|
||||||
style={{ color: '#EAECEF' }}
|
style={{ color: '#EAECEF' }}
|
||||||
>
|
>
|
||||||
{t('privateKey', language)}
|
{t('hyperliquidAgentPrivateKey', language)}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={apiKey}
|
value={apiKey}
|
||||||
onChange={(e) => setApiKey(e.target.value)}
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
placeholder={t('enterPrivateKey', language)}
|
placeholder={t('enterHyperliquidAgentPrivateKey', language)}
|
||||||
className="w-full px-3 py-2 rounded"
|
className="w-full px-3 py-2 rounded"
|
||||||
style={{
|
style={{
|
||||||
background: '#0B0E11',
|
background: '#0B0E11',
|
||||||
@@ -2127,7 +2179,33 @@ function ExchangeConfigModal({
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||||
{t('hyperliquidPrivateKeyDesc', language)}
|
{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>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -2287,7 +2365,8 @@ function ExchangeConfigModal({
|
|||||||
(!apiKey.trim() ||
|
(!apiKey.trim() ||
|
||||||
!secretKey.trim() ||
|
!secretKey.trim() ||
|
||||||
!passphrase.trim())) ||
|
!passphrase.trim())) ||
|
||||||
(selectedExchange.id === 'hyperliquid' && !apiKey.trim()) || // 只验证私钥,钱包地址可选
|
(selectedExchange.id === 'hyperliquid' &&
|
||||||
|
(!apiKey.trim() || !hyperliquidWalletAddr.trim())) || // 验证私钥和钱包地址
|
||||||
(selectedExchange.id === 'aster' &&
|
(selectedExchange.id === 'aster' &&
|
||||||
(!asterUser.trim() ||
|
(!asterUser.trim() ||
|
||||||
!asterSigner.trim() ||
|
!asterSigner.trim() ||
|
||||||
|
|||||||
Reference in New Issue
Block a user