mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-12 15:26:55 +08:00
fix: division-by-zero guard, rate-limit wallet endpoints, safer JSON parse, Lighter HTTP checks
- Guard GridCount-1 division by zero in grid spacing calculation (both grid.go and grid_levels.go) - Rate-limit /wallet/validate and /wallet/generate endpoints (prevent DoS on key generation) - Wrap JSON.parse(localStorage) in try-catch in AuthContext (corrupted data → graceful re-login) - Wrap handleJSONResponse JSON.parse in try-catch with descriptive error - Add HTTP status code checks for Lighter GetActiveOrders and sendTx - Replace deprecated strings.Title with cases.Title (golang.org/x/text) - Reuse HTTP client in checkClaw402Health (was creating new client per call)
This commit is contained in:
@@ -4,9 +4,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/language"
|
||||||
"nofx/store"
|
"nofx/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var titleCaser = cases.Title(language.English)
|
||||||
|
|
||||||
// Onboard handles first-time setup through natural language.
|
// Onboard handles first-time setup through natural language.
|
||||||
// When there's no trader configured, the agent guides the user.
|
// When there's no trader configured, the agent guides the user.
|
||||||
|
|
||||||
@@ -176,9 +180,9 @@ func (a *Agent) handleExchangeChoice(userID int64, text string, state *SetupStat
|
|||||||
a.saveSetupState(userID, state)
|
a.saveSetupState(userID, state)
|
||||||
|
|
||||||
if L == "zh" {
|
if L == "zh" {
|
||||||
return fmt.Sprintf("✅ 选择了 *%s*\n\n请发送你的 API Key:", strings.Title(ex)), true
|
return fmt.Sprintf("✅ 选择了 *%s*\n\n请发送你的 API Key:", titleCaser.String(ex)), true
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("✅ Selected *%s*\n\nPlease send your API Key:", strings.Title(ex)), true
|
return fmt.Sprintf("✅ Selected *%s*\n\nPlease send your API Key:", titleCaser.String(ex)), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) handleAIChoice(userID int64, text string, state *SetupState, L string) (string, bool) {
|
func (a *Agent) handleAIChoice(userID int64, text string, state *SetupState, L string) (string, bool) {
|
||||||
@@ -240,7 +244,7 @@ func (a *Agent) finishSetup(userID int64, state *SetupState, L string) (string,
|
|||||||
result = fmt.Sprintf("🎉 *配置完成!*\n\n"+
|
result = fmt.Sprintf("🎉 *配置完成!*\n\n"+
|
||||||
"• 交易所: %s\n"+
|
"• 交易所: %s\n"+
|
||||||
"• API Key: %s\n",
|
"• API Key: %s\n",
|
||||||
strings.Title(state.Exchange), maskedKey)
|
titleCaser.String(state.Exchange), maskedKey)
|
||||||
if state.AIModel != "" {
|
if state.AIModel != "" {
|
||||||
result += fmt.Sprintf("• AI 模型: %s\n", state.AIModel)
|
result += fmt.Sprintf("• AI 模型: %s\n", state.AIModel)
|
||||||
}
|
}
|
||||||
@@ -249,7 +253,7 @@ func (a *Agent) finishSetup(userID int64, state *SetupState, L string) (string,
|
|||||||
result = fmt.Sprintf("🎉 *Setup Complete!*\n\n"+
|
result = fmt.Sprintf("🎉 *Setup Complete!*\n\n"+
|
||||||
"• Exchange: %s\n"+
|
"• Exchange: %s\n"+
|
||||||
"• API Key: %s\n",
|
"• API Key: %s\n",
|
||||||
strings.Title(state.Exchange), maskedKey)
|
titleCaser.String(state.Exchange), maskedKey)
|
||||||
if state.AIModel != "" {
|
if state.AIModel != "" {
|
||||||
result += fmt.Sprintf("• AI Model: %s\n", state.AIModel)
|
result += fmt.Sprintf("• AI Model: %s\n", state.AIModel)
|
||||||
}
|
}
|
||||||
@@ -329,7 +333,7 @@ func (a *Agent) createTraderFromSetup(state *SetupState) error {
|
|||||||
|
|
||||||
// Create trader config
|
// Create trader config
|
||||||
trader := &store.Trader{
|
trader := &store.Trader{
|
||||||
Name: fmt.Sprintf("NOFXi-%s", strings.Title(state.Exchange)),
|
Name: fmt.Sprintf("NOFXi-%s", titleCaser.String(state.Exchange)),
|
||||||
UserID: "default",
|
UserID: "default",
|
||||||
ExchangeID: exchangeID,
|
ExchangeID: exchangeID,
|
||||||
AIModelID: aiModelID,
|
AIModelID: aiModelID,
|
||||||
|
|||||||
@@ -115,9 +115,10 @@ func (s *Server) handleWalletGenerate(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var walletHTTPClient = &http.Client{Timeout: 5 * time.Second}
|
||||||
|
|
||||||
func checkClaw402Health() string {
|
func checkClaw402Health() string {
|
||||||
client := &http.Client{Timeout: 5 * time.Second}
|
resp, err := walletHTTPClient.Get("https://claw402.ai/health")
|
||||||
resp, err := client.Get("https://claw402.ai/health")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "unreachable"
|
return "unreachable"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,8 +144,10 @@ func (s *Server) setupRoutes() {
|
|||||||
s.route(api, "GET", "/config", "Get system configuration", s.handleGetSystemConfig)
|
s.route(api, "GET", "/config", "Get system configuration", s.handleGetSystemConfig)
|
||||||
|
|
||||||
// Wallet validation (no authentication required — used by frontend config form)
|
// Wallet validation (no authentication required — used by frontend config form)
|
||||||
api.POST("/wallet/validate", s.handleWalletValidate)
|
// Rate-limited to prevent DoS (key generation is computationally expensive)
|
||||||
api.POST("/wallet/generate", s.handleWalletGenerate)
|
walletLimited := api.Group("/", LoginRateLimitMiddleware(s.loginLimiter))
|
||||||
|
walletLimited.POST("/wallet/validate", s.handleWalletValidate)
|
||||||
|
walletLimited.POST("/wallet/generate", s.handleWalletGenerate)
|
||||||
|
|
||||||
// Crypto public key & config (needed by frontend before login for transport encryption)
|
// Crypto public key & config (needed by frontend before login for transport encryption)
|
||||||
api.GET("/crypto/config", s.cryptoHandler.HandleGetCryptoConfig)
|
api.GET("/crypto/config", s.cryptoHandler.HandleGetCryptoConfig)
|
||||||
|
|||||||
@@ -326,8 +326,12 @@ func (at *AutoTrader) InitializeGrid() error {
|
|||||||
at.gridState.LowerPrice = gridConfig.LowerPrice
|
at.gridState.LowerPrice = gridConfig.LowerPrice
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate grid spacing
|
// Calculate grid spacing (guard against division by zero when GridCount <= 1)
|
||||||
at.gridState.GridSpacing = (at.gridState.UpperPrice - at.gridState.LowerPrice) / float64(gridConfig.GridCount-1)
|
if gridConfig.GridCount > 1 {
|
||||||
|
at.gridState.GridSpacing = (at.gridState.UpperPrice - at.gridState.LowerPrice) / float64(gridConfig.GridCount-1)
|
||||||
|
} else {
|
||||||
|
at.gridState.GridSpacing = 0
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize grid levels
|
// Initialize grid levels
|
||||||
at.initializeGridLevels(price, gridConfig)
|
at.initializeGridLevels(price, gridConfig)
|
||||||
|
|||||||
@@ -295,7 +295,12 @@ func (at *AutoTrader) autoAdjustGrid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Recalculate grid spacing based on new bounds
|
// Recalculate grid spacing based on new bounds
|
||||||
at.gridState.GridSpacing = (at.gridState.UpperPrice - at.gridState.LowerPrice) / float64(gridConfig.GridCount-1)
|
// Guard against division by zero when GridCount <= 1
|
||||||
|
if gridConfig.GridCount > 1 {
|
||||||
|
at.gridState.GridSpacing = (at.gridState.UpperPrice - at.gridState.LowerPrice) / float64(gridConfig.GridCount-1)
|
||||||
|
} else {
|
||||||
|
at.gridState.GridSpacing = 0
|
||||||
|
}
|
||||||
|
|
||||||
logger.Infof("[Grid] New bounds: $%.2f - $%.2f, spacing: $%.2f",
|
logger.Infof("[Grid] New bounds: $%.2f - $%.2f, spacing: $%.2f",
|
||||||
at.gridState.LowerPrice, at.gridState.UpperPrice, at.gridState.GridSpacing)
|
at.gridState.LowerPrice, at.gridState.UpperPrice, at.gridState.GridSpacing)
|
||||||
|
|||||||
@@ -246,6 +246,14 @@ func (t *LighterTraderV2) GetActiveOrders(symbol string) ([]OrderResponse, error
|
|||||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
snippet := string(body)
|
||||||
|
if len(snippet) > 512 {
|
||||||
|
snippet = snippet[:512] + "..."
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("GetActiveOrders API HTTP error (status %d): %s", resp.StatusCode, snippet)
|
||||||
|
}
|
||||||
|
|
||||||
logger.Debugf("📋 LIGHTER GetActiveOrders raw response: %s", string(body))
|
logger.Debugf("📋 LIGHTER GetActiveOrders raw response: %s", string(body))
|
||||||
|
|
||||||
// Parse response - Lighter API uses "orders" field, not "data"
|
// Parse response - Lighter API uses "orders" field, not "data"
|
||||||
|
|||||||
@@ -394,6 +394,14 @@ func (t *LighterTraderV2) submitOrder(txType int, txInfo string) (map[string]int
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
snippet := string(respBody)
|
||||||
|
if len(snippet) > 512 {
|
||||||
|
snippet = snippet[:512] + "..."
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Lighter sendTx HTTP error (status %d): %s", resp.StatusCode, snippet)
|
||||||
|
}
|
||||||
|
|
||||||
// Parse response
|
// Parse response
|
||||||
var sendResp SendTxResponse
|
var sendResp SendTxResponse
|
||||||
if err := json.Unmarshal(respBody, &sendResp); err != nil {
|
if err := json.Unmarshal(respBody, &sendResp); err != nil {
|
||||||
|
|||||||
@@ -52,8 +52,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const savedToken = localStorage.getItem('auth_token')
|
const savedToken = localStorage.getItem('auth_token')
|
||||||
const savedUser = localStorage.getItem('auth_user')
|
const savedUser = localStorage.getItem('auth_user')
|
||||||
if (savedToken && savedUser) {
|
if (savedToken && savedUser) {
|
||||||
setToken(savedToken)
|
try {
|
||||||
setUser(JSON.parse(savedUser))
|
setToken(savedToken)
|
||||||
|
setUser(JSON.parse(savedUser))
|
||||||
|
} catch {
|
||||||
|
// Corrupted localStorage data — clear and force re-login
|
||||||
|
localStorage.removeItem('auth_token')
|
||||||
|
localStorage.removeItem('auth_user')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
@@ -65,8 +71,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const savedUser = localStorage.getItem('auth_user')
|
const savedUser = localStorage.getItem('auth_user')
|
||||||
|
|
||||||
if (savedToken && savedUser) {
|
if (savedToken && savedUser) {
|
||||||
setToken(savedToken)
|
try {
|
||||||
setUser(JSON.parse(savedUser))
|
setToken(savedToken)
|
||||||
|
setUser(JSON.parse(savedUser))
|
||||||
|
} catch {
|
||||||
|
localStorage.removeItem('auth_token')
|
||||||
|
localStorage.removeItem('auth_user')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -36,5 +36,9 @@ export async function handleJSONResponse<T>(res: Response): Promise<T> {
|
|||||||
if (!text) {
|
if (!text) {
|
||||||
return {} as T
|
return {} as T
|
||||||
}
|
}
|
||||||
return JSON.parse(text) as T
|
try {
|
||||||
|
return JSON.parse(text) as T
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Invalid JSON response: ${text.slice(0, 200)}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user