231 lines
4.5 KiB
Go
231 lines
4.5 KiB
Go
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
|
|
}
|