feat(hyperliquid): Auto-generate wallet address from private key

Enable automatic wallet address generation from private key for Hyperliquid
exchange, simplifying user onboarding and reducing configuration errors.

Backend Changes (trader/hyperliquid_trader.go):
- Import crypto/ecdsa package for ECDSA public key operations
- Enable wallet address auto-generation when walletAddr is empty
- Use crypto.PubkeyToAddress() to derive address from private key
- Add logging for both auto-generated and manually provided addresses

Frontend Changes (web/src/components/AITradersPage.tsx):
- Remove wallet address required validation (only private key required)
- Update button disabled state to only check private key
- Add "Optional" label to wallet address field
- Add dynamic placeholder with bilingual hint
- Show context-aware helper text based on input state
- Remove HTML required attribute from input field

Translation Updates (web/src/i18n/translations.ts):
- Add 'optional' translation (EN: "Optional", ZH: "可选")
- Add 'hyperliquidWalletAddressAutoGenerate' translation
  EN: "Leave blank to automatically generate wallet address from private key"
  ZH: "留空将自动从私钥生成钱包地址"

Benefits:
 Simplified UX - Users only need to provide private key
 Error prevention - Auto-generated address always matches private key
 Backward compatible - Manual address input still supported
 Better UX - Clear visual indicators for optional fields

Technical Details:
- Uses Ethereum standard ECDSA public key to address conversion
- Implementation was already present but commented out (lines 37-43)
- No database schema changes required (hyperliquid_wallet_addr already nullable)
- Fallback behavior: manual input > auto-generation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tangmengqiu
2025-11-03 23:15:38 -05:00
parent ff4cd0e3ca
commit eea26d755d
3 changed files with 27 additions and 13 deletions

View File

@@ -2,6 +2,7 @@ package trader
import (
"context"
"crypto/ecdsa"
"encoding/json"
"fmt"
"log"
@@ -34,13 +35,18 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool)
apiURL = hyperliquid.TestnetAPIURL
}
// // 从私钥生成钱包地址
// pubKey := privateKey.Public()
// publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey)
// if !ok {
// return nil, fmt.Errorf("无法转换公钥")
// }
// walletAddr := crypto.PubkeyToAddress(*publicKeyECDSA).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)
} else {
log.Printf("✓ 使用提供的钱包地址: %s", walletAddr)
}
ctx := context.Background()

View File

@@ -1201,7 +1201,7 @@ function ExchangeConfigModal({
if (!apiKey.trim() || !secretKey.trim()) return;
await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet);
} else if (selectedExchange?.id === 'hyperliquid') {
if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return;
if (!apiKey.trim()) return; // 只验证私钥,钱包地址可选(会自动生成)
await onSave(selectedExchangeId, apiKey.trim(), '', testnet, hyperliquidWalletAddr.trim());
} else if (selectedExchange?.id === 'aster') {
if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim()) return;
@@ -1360,18 +1360,22 @@ function ExchangeConfigModal({
<div>
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
{t('walletAddress', language)}
<span className="text-xs font-normal ml-2" style={{ color: '#848E9C' }}>
({t('optional', language)})
</span>
</label>
<input
type="text"
value={hyperliquidWalletAddr}
onChange={(e) => setHyperliquidWalletAddr(e.target.value)}
placeholder={t('enterWalletAddress', language)}
placeholder="0x... (留空将自动从私钥生成 / Leave blank to auto-generate)"
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('hyperliquidWalletAddressDesc', language)}
{hyperliquidWalletAddr.trim()
? t('hyperliquidWalletAddressDesc', language)
: t('hyperliquidWalletAddressAutoGenerate', language)}
</div>
</div>
</>
@@ -1468,10 +1472,10 @@ function ExchangeConfigModal({
<button
type="submit"
disabled={
!selectedExchange ||
!selectedExchange ||
(selectedExchange.id === 'binance' && (!apiKey.trim() || !secretKey.trim())) ||
(selectedExchange.id === 'okx' && (!apiKey.trim() || !secretKey.trim() || !passphrase.trim())) ||
(selectedExchange.id === 'hyperliquid' && (!apiKey.trim() || !hyperliquidWalletAddr.trim())) ||
(selectedExchange.id === 'hyperliquid' && !apiKey.trim()) || // 只验证私钥,钱包地址可选
(selectedExchange.id === 'aster' && (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim())) ||
(selectedExchange.type === 'cex' && selectedExchange.id !== 'hyperliquid' && selectedExchange.id !== 'aster' && selectedExchange.id !== 'binance' && selectedExchange.id !== 'okx' && (!apiKey.trim() || !secretKey.trim()))
}

View File

@@ -194,6 +194,8 @@ export const translations = {
enterPassphrase: 'Enter Passphrase (Required for OKX)',
hyperliquidPrivateKeyDesc: 'Hyperliquid uses private key for trading authentication',
hyperliquidWalletAddressDesc: 'Wallet address corresponding to the private key',
hyperliquidWalletAddressAutoGenerate: 'Leave blank to automatically generate wallet address from private key',
optional: 'Optional',
testnetDescription: 'Enable to connect to exchange test environment for simulated trading',
securityWarning: 'Security Warning',
saveConfiguration: 'Save Configuration',
@@ -608,6 +610,8 @@ export const translations = {
enterPassphrase: '输入Passphrase (OKX必填)',
hyperliquidPrivateKeyDesc: 'Hyperliquid 使用私钥进行交易认证',
hyperliquidWalletAddressDesc: '与私钥对应的钱包地址',
hyperliquidWalletAddressAutoGenerate: '留空将自动从私钥生成钱包地址',
optional: '可选',
testnetDescription: '启用后将连接到交易所测试环境,用于模拟交易',
securityWarning: '安全提示',
saveConfiguration: '保存配置',