Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 120 additions & 2 deletions api/crypto_handler.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand Down Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions docs/getting-started/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/getting-started/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ cp config.json.example config.json
- 🔑 限制 IP 访问
- 🔑 在交易所启用 2FA

**私钥校验(输入格式):**
- 🔐 EVM:64 位十六进制(可带 `0x` 前缀)
- 🔐 Solana/Ed25519:Base58 字符串或 JSON 数组;支持 32 字节 seed 或 64 字节 keypair(seed + 公钥)

---

## 🆘 故障排除
Expand Down
22 changes: 17 additions & 5 deletions trader/lighter_trader_v2_orders.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"nofx/logger"
"strconv"
"strings"

"github.com/elliottech/lighter-go/types"
)
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down