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:
tinkle-community
2025-12-14 20:50:10 +08:00
parent abaffaddb9
commit 4725548a55
22 changed files with 749 additions and 1774 deletions

View File

@@ -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