mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-16 09:11:03 +08:00
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:
@@ -2,6 +2,7 @@ package trader
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@@ -34,13 +35,18 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool)
|
|||||||
apiURL = hyperliquid.TestnetAPIURL
|
apiURL = hyperliquid.TestnetAPIURL
|
||||||
}
|
}
|
||||||
|
|
||||||
// // 从私钥生成钱包地址
|
// 从私钥生成钱包地址(如果未提供)
|
||||||
// pubKey := privateKey.Public()
|
if walletAddr == "" {
|
||||||
// publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey)
|
pubKey := privateKey.Public()
|
||||||
// if !ok {
|
publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey)
|
||||||
// return nil, fmt.Errorf("无法转换公钥")
|
if !ok {
|
||||||
// }
|
return nil, fmt.Errorf("无法转换公钥")
|
||||||
// walletAddr := crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
|
}
|
||||||
|
walletAddr = crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
|
||||||
|
log.Printf("✓ 从私钥自动生成钱包地址: %s", walletAddr)
|
||||||
|
} else {
|
||||||
|
log.Printf("✓ 使用提供的钱包地址: %s", walletAddr)
|
||||||
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|||||||
@@ -1201,7 +1201,7 @@ 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() || !hyperliquidWalletAddr.trim()) return;
|
if (!apiKey.trim()) return; // 只验证私钥,钱包地址可选(会自动生成)
|
||||||
await onSave(selectedExchangeId, apiKey.trim(), '', testnet, hyperliquidWalletAddr.trim());
|
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()) return;
|
if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim()) return;
|
||||||
@@ -1360,18 +1360,22 @@ function ExchangeConfigModal({
|
|||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
<label className="block text-sm font-semibold mb-2" style={{ color: '#EAECEF' }}>
|
||||||
{t('walletAddress', language)}
|
{t('walletAddress', language)}
|
||||||
|
<span className="text-xs font-normal ml-2" style={{ color: '#848E9C' }}>
|
||||||
|
({t('optional', language)})
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={hyperliquidWalletAddr}
|
value={hyperliquidWalletAddr}
|
||||||
onChange={(e) => setHyperliquidWalletAddr(e.target.value)}
|
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"
|
className="w-full px-3 py-2 rounded"
|
||||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||||
{t('hyperliquidWalletAddressDesc', language)}
|
{hyperliquidWalletAddr.trim()
|
||||||
|
? t('hyperliquidWalletAddressDesc', language)
|
||||||
|
: t('hyperliquidWalletAddressAutoGenerate', language)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -1468,10 +1472,10 @@ function ExchangeConfigModal({
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={
|
disabled={
|
||||||
!selectedExchange ||
|
!selectedExchange ||
|
||||||
(selectedExchange.id === 'binance' && (!apiKey.trim() || !secretKey.trim())) ||
|
(selectedExchange.id === 'binance' && (!apiKey.trim() || !secretKey.trim())) ||
|
||||||
(selectedExchange.id === 'okx' && (!apiKey.trim() || !secretKey.trim() || !passphrase.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.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()))
|
(selectedExchange.type === 'cex' && selectedExchange.id !== 'hyperliquid' && selectedExchange.id !== 'aster' && selectedExchange.id !== 'binance' && selectedExchange.id !== 'okx' && (!apiKey.trim() || !secretKey.trim()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ export const translations = {
|
|||||||
enterPassphrase: 'Enter Passphrase (Required for OKX)',
|
enterPassphrase: 'Enter Passphrase (Required for OKX)',
|
||||||
hyperliquidPrivateKeyDesc: 'Hyperliquid uses private key for trading authentication',
|
hyperliquidPrivateKeyDesc: 'Hyperliquid uses private key for trading authentication',
|
||||||
hyperliquidWalletAddressDesc: 'Wallet address corresponding to the private key',
|
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',
|
testnetDescription: 'Enable to connect to exchange test environment for simulated trading',
|
||||||
securityWarning: 'Security Warning',
|
securityWarning: 'Security Warning',
|
||||||
saveConfiguration: 'Save Configuration',
|
saveConfiguration: 'Save Configuration',
|
||||||
@@ -608,6 +610,8 @@ export const translations = {
|
|||||||
enterPassphrase: '输入Passphrase (OKX必填)',
|
enterPassphrase: '输入Passphrase (OKX必填)',
|
||||||
hyperliquidPrivateKeyDesc: 'Hyperliquid 使用私钥进行交易认证',
|
hyperliquidPrivateKeyDesc: 'Hyperliquid 使用私钥进行交易认证',
|
||||||
hyperliquidWalletAddressDesc: '与私钥对应的钱包地址',
|
hyperliquidWalletAddressDesc: '与私钥对应的钱包地址',
|
||||||
|
hyperliquidWalletAddressAutoGenerate: '留空将自动从私钥生成钱包地址',
|
||||||
|
optional: '可选',
|
||||||
testnetDescription: '启用后将连接到交易所测试环境,用于模拟交易',
|
testnetDescription: '启用后将连接到交易所测试环境,用于模拟交易',
|
||||||
securityWarning: '安全提示',
|
securityWarning: '安全提示',
|
||||||
saveConfiguration: '保存配置',
|
saveConfiguration: '保存配置',
|
||||||
|
|||||||
Reference in New Issue
Block a user