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:
0xYYBB | ZYY | Bobo
2025-11-07 23:26:56 +08:00
committed by GitHub
parent 28a63f4d48
commit b48dfe7bfd
3 changed files with 148 additions and 23 deletions

View File

@@ -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 */}
<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('privateKey', language)}
{t('hyperliquidAgentPrivateKey', language)}
</label>
<input
type="password"
value={apiKey}
onChange={(e) => 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
/>
<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>
</>
@@ -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() ||