mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-18 09:54:35 +08:00
feat: fix Lighter V2 integration and improve error handling
- Fix Lighter API field name mismatches (position/size, avg_entry_price/entry_price, sign/side) - Fix GetBalance return format to match standard fields (totalWalletBalance, totalUnrealizedProfit) - Fix GetPositions return format to match standard fields (positionAmt, markPrice, unRealizedProfit) - Add API Key Index field to frontend with explanation - Update Lighter referral link - Disable Lighter testnet (mainnet only) - Add load error tracking for better error messages - Remove old Lighter V1 implementation files - Remove test credentials from test files
This commit is contained in:
@@ -2,12 +2,11 @@ package trader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"nofx/logger"
|
||||
"net/http"
|
||||
"nofx/logger"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -15,21 +14,47 @@ import (
|
||||
lighterClient "github.com/elliottech/lighter-go/client"
|
||||
lighterHTTP "github.com/elliottech/lighter-go/client/http"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
// AccountInfo LIGHTER account information
|
||||
type AccountInfo struct {
|
||||
AccountIndex int64 `json:"account_index"`
|
||||
L1Address string `json:"l1_address"`
|
||||
// Other fields can be added based on actual API response
|
||||
AccountIndex int64 `json:"account_index"`
|
||||
Index int64 `json:"index"` // Same as account_index
|
||||
L1Address string `json:"l1_address"`
|
||||
AvailableBalance string `json:"available_balance"`
|
||||
Collateral string `json:"collateral"`
|
||||
CrossAssetValue string `json:"cross_asset_value"`
|
||||
TotalEquity string `json:"total_equity"`
|
||||
UnrealizedPnl string `json:"unrealized_pnl"`
|
||||
Positions []LighterPositionInfo `json:"positions"`
|
||||
}
|
||||
|
||||
// LighterPositionInfo Position info from Lighter account API
|
||||
type LighterPositionInfo struct {
|
||||
MarketID int `json:"market_id"`
|
||||
Symbol string `json:"symbol"`
|
||||
Sign int `json:"sign"` // 1 = long, -1 = short
|
||||
Position string `json:"position"` // Position size
|
||||
AvgEntryPrice string `json:"avg_entry_price"` // Entry price
|
||||
PositionValue string `json:"position_value"` // Position value in USD
|
||||
LiquidationPrice string `json:"liquidation_price"`
|
||||
UnrealizedPnl string `json:"unrealized_pnl"`
|
||||
RealizedPnl string `json:"realized_pnl"`
|
||||
InitialMarginFraction string `json:"initial_margin_fraction"` // e.g. "5.00" means 5% = 20x leverage
|
||||
AllocatedMargin string `json:"allocated_margin"`
|
||||
MarginMode int `json:"margin_mode"` // 0 = cross, 1 = isolated
|
||||
}
|
||||
|
||||
// AccountResponse LIGHTER account API response
|
||||
type AccountResponse struct {
|
||||
Code int `json:"code"`
|
||||
Accounts []AccountInfo `json:"accounts"`
|
||||
}
|
||||
|
||||
// LighterTraderV2 New implementation using official lighter-go SDK
|
||||
type LighterTraderV2 struct {
|
||||
ctx context.Context
|
||||
privateKey *ecdsa.PrivateKey // L1 wallet private key (for account identification)
|
||||
walletAddr string // Ethereum wallet address
|
||||
walletAddr string // Ethereum wallet address
|
||||
|
||||
client *http.Client
|
||||
baseURL string
|
||||
@@ -55,36 +80,34 @@ type LighterTraderV2 struct {
|
||||
precisionMutex sync.RWMutex
|
||||
|
||||
// Market index cache
|
||||
marketIndexMap map[string]uint8 // symbol -> market_id
|
||||
marketIndexMap map[string]uint16 // symbol -> market_id
|
||||
marketMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewLighterTraderV2 Create new LIGHTER trader (using official SDK)
|
||||
// Parameters:
|
||||
// - l1PrivateKeyHex: L1 wallet private key (32 bytes, standard Ethereum private key)
|
||||
// - walletAddr: Ethereum wallet address (optional, will be derived from private key if empty)
|
||||
// - apiKeyPrivateKeyHex: API Key private key (40 bytes, for signing transactions) - needs generation if empty
|
||||
// - walletAddr: Ethereum wallet address (required)
|
||||
// - apiKeyPrivateKeyHex: API Key private key (40 bytes, for signing transactions)
|
||||
// - apiKeyIndex: API Key index (0-255)
|
||||
// - testnet: Whether to use testnet
|
||||
func NewLighterTraderV2(l1PrivateKeyHex, walletAddr, apiKeyPrivateKeyHex string, testnet bool) (*LighterTraderV2, error) {
|
||||
// 1. Parse L1 private key
|
||||
l1PrivateKeyHex = strings.TrimPrefix(strings.ToLower(l1PrivateKeyHex), "0x")
|
||||
l1PrivateKey, err := crypto.HexToECDSA(l1PrivateKeyHex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid L1 private key: %w", err)
|
||||
func NewLighterTraderV2(walletAddr, apiKeyPrivateKeyHex string, apiKeyIndex int, testnet bool) (*LighterTraderV2, error) {
|
||||
// 1. Validate wallet address
|
||||
if walletAddr == "" {
|
||||
return nil, fmt.Errorf("wallet address is required")
|
||||
}
|
||||
|
||||
// 2. If wallet address not provided, derive from private key
|
||||
if walletAddr == "" {
|
||||
walletAddr = crypto.PubkeyToAddress(*l1PrivateKey.Public().(*ecdsa.PublicKey)).Hex()
|
||||
logger.Infof("✓ Derived wallet address from private key: %s", walletAddr)
|
||||
// 2. Validate API Key
|
||||
if apiKeyPrivateKeyHex == "" {
|
||||
return nil, fmt.Errorf("API Key private key is required")
|
||||
}
|
||||
|
||||
// 3. Determine API URL and Chain ID
|
||||
// Note: Python SDK uses 304 for mainnet, 300 for testnet (not the L1 chain IDs)
|
||||
baseURL := "https://mainnet.zklighter.elliot.ai"
|
||||
chainID := uint32(42766) // Mainnet Chain ID
|
||||
chainID := uint32(304) // Mainnet Lighter Chain ID (from Python SDK)
|
||||
if testnet {
|
||||
baseURL = "https://testnet.zklighter.elliot.ai"
|
||||
chainID = uint32(42069) // Testnet Chain ID
|
||||
chainID = uint32(300) // Testnet Lighter Chain ID (from Python SDK)
|
||||
}
|
||||
|
||||
// 4. Create HTTP client
|
||||
@@ -92,7 +115,6 @@ func NewLighterTraderV2(l1PrivateKeyHex, walletAddr, apiKeyPrivateKeyHex string,
|
||||
|
||||
trader := &LighterTraderV2{
|
||||
ctx: context.Background(),
|
||||
privateKey: l1PrivateKey,
|
||||
walletAddr: walletAddr,
|
||||
client: &http.Client{Timeout: 30 * time.Second},
|
||||
baseURL: baseURL,
|
||||
@@ -100,9 +122,9 @@ func NewLighterTraderV2(l1PrivateKeyHex, walletAddr, apiKeyPrivateKeyHex string,
|
||||
chainID: chainID,
|
||||
httpClient: httpClient,
|
||||
apiKeyPrivateKey: apiKeyPrivateKeyHex,
|
||||
apiKeyIndex: 0, // Default to index 0
|
||||
apiKeyIndex: uint8(apiKeyIndex),
|
||||
symbolPrecision: make(map[string]SymbolPrecision),
|
||||
marketIndexMap: make(map[string]uint8),
|
||||
marketIndexMap: make(map[string]uint16),
|
||||
}
|
||||
|
||||
// 5. Initialize account (get account index)
|
||||
@@ -110,14 +132,7 @@ func NewLighterTraderV2(l1PrivateKeyHex, walletAddr, apiKeyPrivateKeyHex string,
|
||||
return nil, fmt.Errorf("failed to initialize account: %w", err)
|
||||
}
|
||||
|
||||
// 6. If no API Key, prompt user to generate one
|
||||
if apiKeyPrivateKeyHex == "" {
|
||||
logger.Infof("⚠️ No API Key private key provided, please call GenerateAndRegisterAPIKey() to generate")
|
||||
logger.Infof(" Or get an existing API Key from LIGHTER website")
|
||||
return trader, nil
|
||||
}
|
||||
|
||||
// 7. Create TxClient (for signing transactions)
|
||||
// 6. Create TxClient (for signing transactions)
|
||||
txClient, err := lighterClient.NewTxClient(
|
||||
httpClient,
|
||||
apiKeyPrivateKeyHex,
|
||||
@@ -131,11 +146,10 @@ func NewLighterTraderV2(l1PrivateKeyHex, walletAddr, apiKeyPrivateKeyHex string,
|
||||
|
||||
trader.txClient = txClient
|
||||
|
||||
// 8. Verify API Key is correct
|
||||
// 7. Verify API Key is correct
|
||||
if err := trader.checkClient(); err != nil {
|
||||
logger.Infof("⚠️ API Key verification failed: %v", err)
|
||||
logger.Infof(" You may need to regenerate API Key or check configuration")
|
||||
return trader, err
|
||||
logger.Warnf("⚠️ API Key verification failed: %v", err)
|
||||
// Don't fail here, allow trader to continue (may work with some operations)
|
||||
}
|
||||
|
||||
logger.Infof("✓ LIGHTER trader initialized successfully (account=%d, apiKey=%d, testnet=%v)",
|
||||
@@ -162,7 +176,7 @@ func (t *LighterTraderV2) initializeAccount() error {
|
||||
|
||||
// getAccountByL1Address Get LIGHTER account info by L1 wallet address
|
||||
func (t *LighterTraderV2) getAccountByL1Address() (*AccountInfo, error) {
|
||||
endpoint := fmt.Sprintf("%s/api/v1/account?by=address&value=%s", t.baseURL, t.walletAddr)
|
||||
endpoint := fmt.Sprintf("%s/api/v1/account?by=l1_address&value=%s", t.baseURL, t.walletAddr)
|
||||
|
||||
req, err := http.NewRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
@@ -184,12 +198,23 @@ func (t *LighterTraderV2) getAccountByL1Address() (*AccountInfo, error) {
|
||||
return nil, fmt.Errorf("failed to get account (status %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var accountInfo AccountInfo
|
||||
if err := json.Unmarshal(body, &accountInfo); err != nil {
|
||||
// Parse response - Lighter returns {"accounts": [...]}
|
||||
var accountResp AccountResponse
|
||||
if err := json.Unmarshal(body, &accountResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse account response: %w", err)
|
||||
}
|
||||
|
||||
return &accountInfo, nil
|
||||
if len(accountResp.Accounts) == 0 {
|
||||
return nil, fmt.Errorf("no account found for wallet address: %s", t.walletAddr)
|
||||
}
|
||||
|
||||
account := &accountResp.Accounts[0]
|
||||
// Use index field if account_index is 0
|
||||
if account.AccountIndex == 0 && account.Index != 0 {
|
||||
account.AccountIndex = account.Index
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// checkClient Verify if API Key is correct
|
||||
|
||||
Reference in New Issue
Block a user