package main import ( "encoding/json" "fmt" "io" "net/http" "strings" "sync" "time" ) const SOL_MINT = "So11111111111111111111111111111111111111112" type ApiKeyWorker struct { apiKey string lastRequestTime time.Time mutex sync.Mutex } var ( apiKeyWorkers []*ApiKeyWorker workerIndex int workerMutex sync.Mutex ) func initApiKeyWorkers(cfg *Config) { apiKeyWorkers = make([]*ApiKeyWorker, len(cfg.JupiterApiKeys)) for i, key := range cfg.JupiterApiKeys { apiKeyWorkers[i] = &ApiKeyWorker{ apiKey: key, lastRequestTime: time.Time{}, } } workerIndex = 0 } func getNextApiKeyWorker() *ApiKeyWorker { workerMutex.Lock() defer workerMutex.Unlock() worker := apiKeyWorkers[workerIndex] workerIndex = (workerIndex + 1) % len(apiKeyWorkers) return worker } func (w *ApiKeyWorker) rateLimit(cfg *Config) { w.mutex.Lock() defer w.mutex.Unlock() elapsed := time.Since(w.lastRequestTime) waitTime := time.Duration(cfg.JupiterRateLimitMs)*time.Millisecond - elapsed if waitTime > 0 { time.Sleep(waitTime) } w.lastRequestTime = time.Now() } type PortfolioValue struct { Value float64 TokenCount int } type Balance struct { UiAmount float64 `json:"uiAmount"` } type PriceData struct { UsdPrice float64 `json:"usdPrice"` } func getPortfolioValue(address string, cfg *Config) (*PortfolioValue, error) { const MAX_RETRIES = 2 // Get a worker for this request worker := getNextApiKeyWorker() for attempt := 0; attempt < MAX_RETRIES; attempt++ { result, err := attemptGetPortfolio(address, cfg, worker) if err == nil { return result, nil } if attempt == MAX_RETRIES-1 { return &PortfolioValue{Value: 0, TokenCount: 0}, nil } } return &PortfolioValue{Value: 0, TokenCount: 0}, nil } func attemptGetPortfolio(address string, cfg *Config, worker *ApiKeyWorker) (*PortfolioValue, error) { const BATCH_SIZE = 100 const MAX_TOKENS = 100 worker.rateLimit(cfg) transport, err := getNextProxy() if err != nil { return nil, fmt.Errorf("failed to get proxy: %w", err) } client := &http.Client{ Transport: transport, Timeout: 10 * time.Second, } // Get balances req, err := http.NewRequest("GET", fmt.Sprintf("https://api.jup.ag/ultra/v1/balances/%s", address), nil) if err != nil { return nil, err } req.Header.Set("x-api-key", worker.apiKey) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("balances request failed: %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var balances map[string]Balance if err := json.Unmarshal(body, &balances); err != nil { return nil, err } if len(balances) == 0 { return &PortfolioValue{Value: 0, TokenCount: 0}, nil } // Build token list var tokens []string for addr := range balances { if addr != "SOL" { tokens = append(tokens, addr) } } // Add SOL mint if SOL balance exists if _, hasSol := balances["SOL"]; hasSol { tokens = append(tokens, SOL_MINT) } if len(tokens) > MAX_TOKENS { tokens = tokens[:MAX_TOKENS] } if len(tokens) == 0 { return &PortfolioValue{Value: 0, TokenCount: 0}, nil } // Split into batches var batches [][]string for i := 0; i < len(tokens); i += BATCH_SIZE { end := i + BATCH_SIZE if end > len(tokens) { end = len(tokens) } batches = append(batches, tokens[i:end]) } worker.rateLimit(cfg) // Fetch prices for all batches allPrices := make(map[string]PriceData) for _, batch := range batches { ids := strings.Join(batch, "%2C") url := fmt.Sprintf("https://api.jup.ag/price/v3?ids=%s", ids) req, err := http.NewRequest("GET", url, nil) if err != nil { continue } req.Header.Set("x-api-key", worker.apiKey) resp, err := client.Do(req) if err != nil { continue } if resp.StatusCode == http.StatusOK { body, err := io.ReadAll(resp.Body) resp.Body.Close() if err == nil { var prices map[string]PriceData if json.Unmarshal(body, &prices) == nil { for k, v := range prices { allPrices[k] = v } } } } else { resp.Body.Close() } } // Calculate total value var total float64 var count int for addr, bal := range balances { mint := addr if addr == "SOL" { mint = SOL_MINT } price := 0.0 if priceData, ok := allPrices[mint]; ok { price = priceData.UsdPrice } val := bal.UiAmount * price if val > 0.01 { total += val count++ } } return &PortfolioValue{Value: total, TokenCount: count}, nil }