diff --git a/api/crypto_handler.go b/api/crypto_handler.go index c78022bc11..86bd3f9817 100644 --- a/api/crypto_handler.go +++ b/api/crypto_handler.go @@ -1,10 +1,15 @@ package api import ( + "bytes" + "crypto/ed25519" + "encoding/json" "log" + "math/big" "net/http" "nofx/config" "nofx/crypto" + "strings" "github.com/gin-gonic/gin" ) @@ -84,10 +89,123 @@ func (h *CryptoHandler) HandleDecryptSensitiveData(c *gin.Context) { // isValidPrivateKey Validate private key format func isValidPrivateKey(key string) bool { - // EVM private key: 64 hex characters (optional 0x prefix) - if len(key) == 64 || (len(key) == 66 && key[:2] == "0x") { + trimmed := strings.TrimSpace(key) + if trimmed == "" { + return false + } + + if isValidEVMPrivateKey(trimmed) { + return true + } + if isValidSolanaPrivateKey(trimmed) { return true } // TODO: Add validation for other chains return false } + +func isValidEVMPrivateKey(key string) bool { + // EVM private key: 64 hex characters (optional 0x prefix) + trimmed := key + if len(trimmed) == 66 && strings.HasPrefix(trimmed, "0x") { + trimmed = trimmed[2:] + } + return len(trimmed) == 64 && isHexString(trimmed) +} + +func isValidSolanaPrivateKey(key string) bool { + keyBytes, ok := parseSolanaPrivateKeyBytes(key) + if !ok { + return false + } + + switch len(keyBytes) { + case ed25519.SeedSize: + return true + case ed25519.PrivateKeySize: + seed := keyBytes[:ed25519.SeedSize] + pub := keyBytes[ed25519.SeedSize:] + derived := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey) + return bytes.Equal(derived, pub) + default: + return false + } +} + +func parseSolanaPrivateKeyBytes(key string) ([]byte, bool) { + trimmed := strings.TrimSpace(key) + if strings.HasPrefix(trimmed, "[") { + return parseSolanaJSONKey(trimmed) + } + + decoded, ok := decodeBase58(trimmed) + if !ok { + return nil, false + } + if len(decoded) != ed25519.SeedSize && len(decoded) != ed25519.PrivateKeySize { + return nil, false + } + return decoded, true +} + +func parseSolanaJSONKey(key string) ([]byte, bool) { + var values []int + if err := json.Unmarshal([]byte(key), &values); err != nil { + return nil, false + } + if len(values) != ed25519.SeedSize && len(values) != ed25519.PrivateKeySize { + return nil, false + } + + out := make([]byte, len(values)) + for i, v := range values { + if v < 0 || v > 255 { + return nil, false + } + out[i] = byte(v) + } + return out, true +} + +const base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + +func decodeBase58(input string) ([]byte, bool) { + if input == "" { + return nil, false + } + + base := big.NewInt(58) + num := big.NewInt(0) + for i := 0; i < len(input); i++ { + idx := strings.IndexByte(base58Alphabet, input[i]) + if idx < 0 { + return nil, false + } + num.Mul(num, base) + num.Add(num, big.NewInt(int64(idx))) + } + + decoded := num.Bytes() + leadingZeros := 0 + for leadingZeros < len(input) && input[leadingZeros] == '1' { + leadingZeros++ + } + if leadingZeros > 0 { + decoded = append(make([]byte, leadingZeros), decoded...) + } + + return decoded, true +} + +func isHexString(s string) bool { + for _, c := range s { + switch { + case c >= '0' && c <= '9': + case c >= 'a' && c <= 'f': + case c >= 'A' && c <= 'F': + default: + return false + } + } + return true +} diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 1f79a75f83..4ad00e697e 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -101,6 +101,10 @@ For single-tenant/self-hosted usage, you can enable strict admin-only access: - 🔑 Restrict IP access - 🔑 Enable 2FA on exchanges +**Private Keys (Input Validation):** +- 🔐 EVM: 64 hex characters (optional `0x` prefix) +- 🔐 Solana/Ed25519: Base58 string or JSON array; supports 32-byte seed or 64-byte keypair (seed + public key) + --- ## 🆘 Troubleshooting diff --git a/docs/getting-started/README.zh-CN.md b/docs/getting-started/README.zh-CN.md index 9305422205..031ab25709 100644 --- a/docs/getting-started/README.zh-CN.md +++ b/docs/getting-started/README.zh-CN.md @@ -82,6 +82,10 @@ cp config.json.example config.json - 🔑 限制 IP 访问 - 🔑 在交易所启用 2FA +**私钥校验(输入格式):** +- 🔐 EVM:64 位十六进制(可带 `0x` 前缀) +- 🔐 Solana/Ed25519:Base58 字符串或 JSON 数组;支持 32 字节 seed 或 64 字节 keypair(seed + 公钥) + --- ## 🆘 故障排除 diff --git a/trader/lighter_trader_v2_orders.go b/trader/lighter_trader_v2_orders.go index c30783c202..b46fa4414a 100644 --- a/trader/lighter_trader_v2_orders.go +++ b/trader/lighter_trader_v2_orders.go @@ -9,6 +9,7 @@ import ( "net/http" "nofx/logger" "strconv" + "strings" "github.com/elliottech/lighter-go/types" ) @@ -185,8 +186,10 @@ func (t *LighterTraderV2) CancelStopOrders(symbol string) error { canceledCount := 0 for _, order := range orders { - // TODO: Check order type, only cancel stop orders - // For now, cancel all orders + // If order type is provided, only cancel stop-loss/take-profit orders. + if order.OrderType != "" && !isLighterStopOrder(order.OrderType) { + continue + } if err := t.CancelOrder(symbol, order.OrderID); err != nil { logger.Infof("⚠️ Failed to cancel order (ID: %s): %v", order.OrderID, err) } else { @@ -198,6 +201,15 @@ func (t *LighterTraderV2) CancelStopOrders(symbol string) error { return nil } +func isLighterStopOrder(orderType string) bool { + switch strings.ToLower(orderType) { + case "stop", "stop_loss", "take_profit", "stoploss", "takeprofit": + return true + default: + return false + } +} + // GetActiveOrders Get active orders func (t *LighterTraderV2) GetActiveOrders(symbol string) ([]OrderResponse, error) { if err := t.ensureAuthToken(); err != nil { @@ -237,9 +249,9 @@ func (t *LighterTraderV2) GetActiveOrders(symbol string) ([]OrderResponse, error // Parse response var apiResp struct { - Code int `json:"code"` - Message string `json:"message"` - Data []OrderResponse `json:"data"` + Code int `json:"code"` + Message string `json:"message"` + Data []OrderResponse `json:"data"` } if err := json.Unmarshal(body, &apiResp); err != nil {