synapsecns/sanguine

View on GitHub
services/explorer/consumer/fetcher/pricefetcher.go

Summary

Maintainability
B
6 hrs
Test Coverage
package fetcher

import (
    "context"
    "encoding/json"
    "fmt"
    "github.com/jpillora/backoff"
    "net/http"
    "strconv"
    "time"
)

// PriceService --output=mocks --case=underscore.
type PriceService interface {
    // GetDefiLlamaData historical price data
    GetDefiLlamaData(context.Context, int, string) *float64
}

// tokenMetadataMaxRetry is the maximum number of times to retry requesting token metadata
// from the DeFi llama API.
const tokenMetadataMaxRetry = 20
const tokenMetadataMaxRetrySecondary = 1

// GetDefiLlamaData does a get request to defi llama for the symbol and price for a token.
//
//nolint:cyclop,gocognit
func GetDefiLlamaData(ctx context.Context, timestamp int, coinGeckoID string) *float64 {
    zero := float64(0)

    if coinGeckoID == "NO_TOKEN" || coinGeckoID == "NO_PRICE" {
        // if there is no data on the token, the amount returned will be 1:1 (price will be same as the amount of token
        // and the token  symbol will say "no symbol"
        return &zero
    }
    client := http.Client{
        Timeout: 10 * time.Second,
        Transport: &http.Transport{
            ResponseHeaderTimeout: 10 * time.Second,
        },
    }
    b := &backoff.Backoff{
        Factor: 2,
        Jitter: true,
        Min:    10 * time.Millisecond,
        Max:    1 * time.Second,
    }
    timeout := time.Duration(0)
    retries := 0
    retriesSecondary := 0
RETRY:
    select {
    case <-ctx.Done():
        logger.Errorf("context canceled %s, %v", coinGeckoID, ctx.Err())

        return nil
    case <-time.After(timeout):
        if retriesSecondary > tokenMetadataMaxRetrySecondary {
            retriesSecondary = 0
            retries++
        }
        if retries >= tokenMetadataMaxRetry {
            logger.Errorf("Max retries reached, could not get token metadata for %s", coinGeckoID)
            return nil
        }
        var granularityStr string
        if retries > 1 {
            granularityStr = fmt.Sprintf("?searchWidth=%dh", 15*(retries-2)+24)
        }

        url := fmt.Sprintf("https://coins.llama.fi/prices/historical/%d/coingecko:%s%s", timestamp, coinGeckoID, granularityStr)
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
        if err != nil {
            timeout = b.Duration()
            logger.Errorf("error getting response from defi llama %v", err)
            retries++
            goto RETRY
        }
        resRaw, err := client.Do(req)

        if err != nil {
            timeout = b.Duration()
            logger.Errorf("error getting response from defi llama %v", err)
            retries++
            goto RETRY
        }

        res := make(map[string]map[string]map[string]interface{})
        err = json.NewDecoder(resRaw.Body).Decode(&res)
        if err != nil {
            if retriesSecondary > retriesSecondary-2 && retries > 18 {
                logger.Errorf("Failed decoding defillama data after retries %d, retries secondary: %d  id %s: timestamp: %d error %v %s", retries, retriesSecondary, coinGeckoID, timestamp, err, url)
            }
            timeout = b.Duration()
            retriesSecondary++
            goto RETRY
        }

        priceRaw := res["coins"][fmt.Sprintf("coingecko:%s", coinGeckoID)]["price"]
        if priceRaw == nil {
            if retries >= 6 {
                logger.Errorf("error getting price from defi llama: retries: %d %s %d %v %s", retries, coinGeckoID, timestamp, res, url)
            }
            timeout = b.Duration()
            retries++

            goto RETRY
        }
        priceStr := fmt.Sprintf("%.10f", priceRaw)
        priceFloat, err := strconv.ParseFloat(priceStr, 64)
        if err != nil {
            timeout = b.Duration()
            logger.Errorf("error unwrapping price from defi llama %v", err)
            retries++
            goto RETRY
        }

        price := &priceFloat

        if resRaw.Body.Close() != nil {
            logger.Error("Error closing http connection.")
        }
        return price
    }
}