Unify agent routing and tighten exchange config

This commit is contained in:
lky-spec
2026-04-28 11:58:58 +08:00
parent d481b3d88c
commit 30a703a827
12 changed files with 679 additions and 77 deletions

View File

@@ -319,29 +319,23 @@ func accountAssetForExchange(exchangeType string) string {
}
func missingExchangeCredentials(exchangeCfg *store.Exchange) (status string, code string, message string, missing bool) {
switch exchangeCfg.ExchangeType {
case "binance", "bybit", "gate", "indodax":
if exchangeCfg.APIKey == "" || exchangeCfg.SecretKey == "" {
return exchangeAccountStatusMissingCredentials, "MISSING_REQUIRED_FIELDS", "API key and secret key are required", true
missingFields := store.MissingRequiredExchangeCredentialFields(
exchangeCfg.ExchangeType,
string(exchangeCfg.APIKey),
string(exchangeCfg.SecretKey),
string(exchangeCfg.Passphrase),
exchangeCfg.HyperliquidWalletAddr,
exchangeCfg.AsterUser,
exchangeCfg.AsterSigner,
string(exchangeCfg.AsterPrivateKey),
exchangeCfg.LighterWalletAddr,
string(exchangeCfg.LighterAPIKeyPrivateKey),
)
if len(missingFields) > 0 {
if len(missingFields) == 1 && missingFields[0] == "exchange_type" {
return exchangeAccountStatusUnavailable, "UNSUPPORTED_EXCHANGE", "Unsupported exchange type", true
}
case "okx", "bitget", "kucoin":
if exchangeCfg.APIKey == "" || exchangeCfg.SecretKey == "" || exchangeCfg.Passphrase == "" {
return exchangeAccountStatusMissingCredentials, "MISSING_REQUIRED_FIELDS", "API key, secret key, and passphrase are required", true
}
case "hyperliquid":
if exchangeCfg.APIKey == "" || exchangeCfg.HyperliquidWalletAddr == "" {
return exchangeAccountStatusMissingCredentials, "MISSING_REQUIRED_FIELDS", "Private key and wallet address are required", true
}
case "aster":
if exchangeCfg.AsterUser == "" || exchangeCfg.AsterSigner == "" || exchangeCfg.AsterPrivateKey == "" {
return exchangeAccountStatusMissingCredentials, "MISSING_REQUIRED_FIELDS", "Aster user, signer, and private key are required", true
}
case "lighter":
if exchangeCfg.LighterWalletAddr == "" || exchangeCfg.LighterAPIKeyPrivateKey == "" {
return exchangeAccountStatusMissingCredentials, "MISSING_REQUIRED_FIELDS", "Wallet address and API key private key are required", true
}
default:
return exchangeAccountStatusUnavailable, "UNSUPPORTED_EXCHANGE", "Unsupported exchange type", true
return exchangeAccountStatusMissingCredentials, "MISSING_REQUIRED_FIELDS", "Missing required fields: " + strings.Join(missingFields, ", "), true
}
return "", "", "", false

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strings"
"nofx/config"
"nofx/crypto"
@@ -37,9 +38,9 @@ type SafeExchangeConfig struct {
Testnet bool `json:"testnet,omitempty"`
HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` // Hyperliquid wallet address (not sensitive)
HasAsterPrivateKey bool `json:"has_aster_private_key"`
AsterUser string `json:"asterUser"` // Aster username (not sensitive)
AsterSigner string `json:"asterSigner"` // Aster signer (not sensitive)
LighterWalletAddr string `json:"lighterWalletAddr"` // LIGHTER wallet address (not sensitive)
AsterUser string `json:"asterUser"` // Aster username (not sensitive)
AsterSigner string `json:"asterSigner"` // Aster signer (not sensitive)
LighterWalletAddr string `json:"lighterWalletAddr"` // LIGHTER wallet address (not sensitive)
HasLighterPrivateKey bool `json:"has_lighter_private_key"`
HasLighterAPIKey bool `json:"has_lighter_api_key_private_key"`
}
@@ -199,13 +200,73 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {
// Update each exchange's configuration and track traders that need reload
tradersToReload := make(map[string]bool)
for exchangeID, exchangeData := range req.Exchanges {
existing, err := s.store.Exchange().GetByID(userID, exchangeID)
if err != nil {
SafeInternalError(c, fmt.Sprintf("Load exchange %s", exchangeID), err)
return
}
effectiveAPIKey := strings.TrimSpace(exchangeData.APIKey)
if effectiveAPIKey == "" {
effectiveAPIKey = strings.TrimSpace(string(existing.APIKey))
}
effectiveSecretKey := strings.TrimSpace(exchangeData.SecretKey)
if effectiveSecretKey == "" {
effectiveSecretKey = strings.TrimSpace(string(existing.SecretKey))
}
effectivePassphrase := strings.TrimSpace(exchangeData.Passphrase)
if effectivePassphrase == "" {
effectivePassphrase = strings.TrimSpace(string(existing.Passphrase))
}
effectiveAsterPrivateKey := strings.TrimSpace(exchangeData.AsterPrivateKey)
if effectiveAsterPrivateKey == "" {
effectiveAsterPrivateKey = strings.TrimSpace(string(existing.AsterPrivateKey))
}
effectiveLighterAPIKeyPrivateKey := strings.TrimSpace(exchangeData.LighterAPIKeyPrivateKey)
if effectiveLighterAPIKeyPrivateKey == "" {
effectiveLighterAPIKeyPrivateKey = strings.TrimSpace(string(existing.LighterAPIKeyPrivateKey))
}
effectiveHyperliquidWalletAddr := strings.TrimSpace(exchangeData.HyperliquidWalletAddr)
if effectiveHyperliquidWalletAddr == "" {
effectiveHyperliquidWalletAddr = strings.TrimSpace(existing.HyperliquidWalletAddr)
}
effectiveAsterUser := strings.TrimSpace(exchangeData.AsterUser)
if effectiveAsterUser == "" {
effectiveAsterUser = strings.TrimSpace(existing.AsterUser)
}
effectiveAsterSigner := strings.TrimSpace(exchangeData.AsterSigner)
if effectiveAsterSigner == "" {
effectiveAsterSigner = strings.TrimSpace(existing.AsterSigner)
}
effectiveLighterWalletAddr := strings.TrimSpace(exchangeData.LighterWalletAddr)
if effectiveLighterWalletAddr == "" {
effectiveLighterWalletAddr = strings.TrimSpace(existing.LighterWalletAddr)
}
if missing := store.MissingRequiredExchangeCredentialFields(
existing.ExchangeType,
effectiveAPIKey,
effectiveSecretKey,
effectivePassphrase,
effectiveHyperliquidWalletAddr,
effectiveAsterUser,
effectiveAsterSigner,
effectiveAsterPrivateKey,
effectiveLighterWalletAddr,
effectiveLighterAPIKeyPrivateKey,
); len(missing) > 0 {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Missing required exchange fields: %s", strings.Join(missing, ", ")),
"missing_fields": missing,
})
return
}
// Find traders using this exchange BEFORE updating
traders, _ := s.store.Trader().ListByExchangeID(userID, exchangeID)
for _, t := range traders {
tradersToReload[t.ID] = true
}
err := s.store.Exchange().Update(userID, exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Passphrase, exchangeData.Testnet, exchangeData.HyperliquidWalletAddr, exchangeData.HyperliquidUnifiedAcct, exchangeData.AsterUser, exchangeData.AsterSigner, exchangeData.AsterPrivateKey, exchangeData.LighterWalletAddr, exchangeData.LighterPrivateKey, exchangeData.LighterAPIKeyPrivateKey, exchangeData.LighterAPIKeyIndex)
err = s.store.Exchange().Update(userID, exchangeID, true, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Passphrase, exchangeData.Testnet, effectiveHyperliquidWalletAddr, exchangeData.HyperliquidUnifiedAcct, effectiveAsterUser, effectiveAsterSigner, exchangeData.AsterPrivateKey, effectiveLighterWalletAddr, exchangeData.LighterPrivateKey, exchangeData.LighterAPIKeyPrivateKey, exchangeData.LighterAPIKeyIndex)
if err != nil {
SafeInternalError(c, fmt.Sprintf("Update exchange %s", exchangeID), err)
return
@@ -291,10 +352,28 @@ func (s *Server) handleCreateExchange(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid exchange type: %s", req.ExchangeType)})
return
}
if missing := store.MissingRequiredExchangeCredentialFields(
req.ExchangeType,
req.APIKey,
req.SecretKey,
req.Passphrase,
req.HyperliquidWalletAddr,
req.AsterUser,
req.AsterSigner,
req.AsterPrivateKey,
req.LighterWalletAddr,
req.LighterAPIKeyPrivateKey,
); len(missing) > 0 {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Missing required exchange fields: %s", strings.Join(missing, ", ")),
"missing_fields": missing,
})
return
}
// Create new exchange account
// Exchange configs only persist once complete; persisted configs are always enabled.
id, err := s.store.Exchange().Create(
userID, req.ExchangeType, req.AccountName, req.Enabled,
userID, req.ExchangeType, req.AccountName, true,
req.APIKey, req.SecretKey, req.Passphrase, req.Testnet,
req.HyperliquidWalletAddr, req.HyperliquidUnifiedAcct,
req.AsterUser, req.AsterSigner, req.AsterPrivateKey,