github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/liquidity-scrapers/HydrationScraper.go (about)

     1  package liquidityscrapers
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"net/http"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/diadata-org/diadata/pkg/dia"
    13  	hydrationhelper "github.com/diadata-org/diadata/pkg/dia/helpers/hydration-helper"
    14  	models "github.com/diadata-org/diadata/pkg/model"
    15  	"github.com/diadata-org/diadata/pkg/utils"
    16  	"github.com/sirupsen/logrus"
    17  )
    18  
    19  type HydrationLiquidityScraper struct {
    20  	logger                    *logrus.Entry
    21  	poolChannel               chan dia.Pool
    22  	doneChannel               chan bool
    23  	blockchain                string
    24  	exchangeName              string
    25  	relDB                     *models.RelDB
    26  	datastore                 *models.DB
    27  	targetSwapContract        string
    28  	swapContractsLimit        int
    29  	handlerType               string
    30  	sleepBetweenContractCalls time.Duration
    31  	apiURL                    string
    32  }
    33  
    34  // NewHydrationLiquidityScraper returns a new HydrationLiquidityScraper initialized with default values.
    35  // The instance is asynchronously scraping as soon as it is created.
    36  // ENV values:
    37  //
    38  //	 	BIFROST_SLEEP_TIMEOUT - (optional,millisecond), make timeout between API calls, default "Hydrationhelper.DefaultSleepBetweenContractCalls" value
    39  //		BIFROST_TARGET_SWAP_CONTRACT - (optional, string), useful for debug, default = ""
    40  //		BIFROST_DEBUG - (optional, bool), make stdout output with Hydration client http call, default = false
    41  func NewHydrationLiquidityScraper(exchange dia.Exchange, relDB *models.RelDB, datastore *models.DB) *HydrationLiquidityScraper {
    42  	targetSwapContract := utils.Getenv(
    43  		strings.ToUpper(exchange.Name)+"_TARGET_SWAP_CONTRACT",
    44  		"",
    45  	)
    46  	sleepBetweenContractCalls := utils.GetTimeDurationFromIntAsMilliseconds(
    47  		utils.GetenvInt(
    48  			strings.ToUpper(exchange.Name)+"_SLEEP_TIMEOUT",
    49  			hydrationhelper.DefaultSleepBetweenContractCalls,
    50  		),
    51  	)
    52  	swapContractsLimit := utils.GetenvInt(
    53  		strings.ToUpper(exchange.Name)+"_SWAP_CONTRACTS_LIMIT",
    54  		hydrationhelper.DefaultSwapContractsLimit,
    55  	)
    56  	var (
    57  		poolChannel = make(chan dia.Pool)
    58  		doneChannel = make(chan bool)
    59  		scraper     *HydrationLiquidityScraper
    60  	)
    61  	apiURL := utils.Getenv(strings.ToUpper(exchange.Name)+"_API_URL", "http://localhost:3000/hydration/v1")
    62  	scraper = &HydrationLiquidityScraper{
    63  		poolChannel:               poolChannel,
    64  		doneChannel:               doneChannel,
    65  		exchangeName:              exchange.Name,
    66  		blockchain:                "Hydration",
    67  		relDB:                     relDB,
    68  		datastore:                 datastore,
    69  		targetSwapContract:        targetSwapContract,
    70  		swapContractsLimit:        swapContractsLimit,
    71  		handlerType:               "liquidity",
    72  		sleepBetweenContractCalls: sleepBetweenContractCalls,
    73  		apiURL:                    apiURL,
    74  	}
    75  	scraper.logger = logrus.
    76  		New().
    77  		WithContext(context.Background()).
    78  		WithField("handlerType", scraper.handlerType).
    79  		WithField("context", "HydrationLiquidityScraper")
    80  	go scraper.fetchPools()
    81  	return scraper
    82  }
    83  
    84  // fetches liquidity pool data from the API and processes it into dia.Pool objects.
    85  //
    86  // This method performs an HTTP GET request to the API endpoint to retrieve pool metadata.
    87  // It retries the request up to 3 times in case of failure. Once the response is successfully
    88  // fetched, it decodes the JSON response into a slice of `HydrationPoolMetadata` objects and
    89  // processes them into `dia.Pool` objects using the `parseAssets` method.
    90  //
    91  // Returns:
    92  //   - []*dia.Pool: A slice of pointers to `dia.Pool` objects representing the parsed liquidity pools.
    93  //   - error: An error if the API request or JSON decoding fails, otherwise `nil`.
    94  func (s *HydrationLiquidityScraper) getPools() ([]*dia.Pool, error) {
    95  	endpoint := fmt.Sprintf("%s/pools", s.apiURL)
    96  	resp, err := s.fetchWithRetry(endpoint, "application/json", 3)
    97  	if err != nil {
    98  		return nil, fmt.Errorf("failed to fetch swap events after retries: %w", err)
    99  	}
   100  	defer resp.Body.Close()
   101  	var assets []hydrationhelper.HydrationPoolMetada
   102  	if err := json.NewDecoder(resp.Body).Decode(&assets); err != nil {
   103  		return nil, fmt.Errorf("failed to decode swap events: %w", err)
   104  	}
   105  	return s.parseAssets(assets), nil
   106  }
   107  
   108  // converts a slice of HydrationPoolMetadata into dia.Pool objects.
   109  //
   110  // This method processes liquidity pool metadata by retrieving associated assets from the database,
   111  // calculating their volumes, and constructing `dia.Pool` objects. Each pool is assigned a unique
   112  // address based on its tokens, and the asset volumes are populated with the appropriate balance
   113  // and USD equivalent for each asset. The function skips any pools that do not contain exactly two
   114  // asset types.
   115  //
   116  // Parameters:
   117  //   - poolMetadata: A slice of `HydrationPoolMetadata` containing the raw metadata of liquidity pools.
   118  //
   119  // Returns:
   120  //   - []*dia.Pool: A slice of pointers to `dia.Pool` objects representing the processed pools.
   121  func (s *HydrationLiquidityScraper) parseAssets(poolMetadata []hydrationhelper.HydrationPoolMetada) []*dia.Pool {
   122  	pools := make([]*dia.Pool, 0)
   123  	for _, metadataPair := range poolMetadata {
   124  		pair := &dia.Pool{
   125  			Assetvolumes: []dia.AssetVolume{},
   126  			Time:         time.Now(),
   127  		}
   128  		var tokenNames []string
   129  		for _, token := range metadataPair.Tokens {
   130  			assetKey := token.ID
   131  			dbAsset, err := s.relDB.GetAsset(assetKey, s.blockchain)
   132  			if err != nil {
   133  				s.logger.WithError(err).Warn("Failed to GetAsset with key: ", assetKey)
   134  				continue
   135  			}
   136  			balance, _ := utils.StringToFloat64(token.Balance, int64(token.Decimals))
   137  			usdBalance, _ := utils.StringToFloat64(token.UsdBalance, int64(6))
   138  			pair.Assetvolumes = append(pair.Assetvolumes, dia.AssetVolume{
   139  				Index:     uint8(token.Index),
   140  				Asset:     dbAsset,
   141  				Volume:    balance,
   142  				VolumeUSD: usdBalance,
   143  			})
   144  			tokenNames = append(tokenNames, strings.ToLower(token.ID))
   145  		}
   146  		if len(pair.Assetvolumes) < 2 {
   147  			s.logger.Warn("Found less than 2 asset types for the pool")
   148  			continue
   149  		}
   150  		pair.Address = metadataPair.Address
   151  		pair.Exchange = dia.Exchange{Name: s.exchangeName}
   152  		pair.Blockchain = dia.BlockChain{Name: s.blockchain}
   153  		pools = append(pools, pair)
   154  	}
   155  	return pools
   156  }
   157  
   158  // performs an HTTP GET request to the specified endpoint with retries.
   159  //
   160  // This function attempts to fetch data from the provided API endpoint, retrying
   161  // the request up to the specified number of times (`retries`) in case of failure.
   162  // The retry logic handles both network errors and server-side errors (HTTP 5xx).
   163  //
   164  // For each attempt, the function logs the attempt number and the endpoint being requested.
   165  // If a request fails due to a server-side error (HTTP 5xx), the function retries
   166  // after a short delay. Client-side errors (HTTP 4xx) and other non-retryable errors
   167  // are returned immediately.
   168  //
   169  // Parameters:
   170  //   - endpoint: The URL of the API endpoint to request data from.
   171  //   - contentType: The content type to set in the request header (e.g., "application/json").
   172  //   - retries: The number of retry attempts in case of failure.
   173  //
   174  // Returns:
   175  //   - *http.Response: The response object if the request is successful.
   176  //   - error: An error if all retry attempts fail, or if a non-retryable error occurs.
   177  func (s *HydrationLiquidityScraper) fetchWithRetry(endpoint string, contentType string, retries int) (*http.Response, error) {
   178  	client := &http.Client{
   179  		Timeout: 20 * time.Second,
   180  	}
   181  	var resp *http.Response
   182  	var err error
   183  	for i := 0; i < retries; i++ {
   184  		s.logger.WithField("attempt", i+1).Info("Fetching data from API")
   185  		s.logger.WithField("endpoint", endpoint).Info("Requesting data")
   186  		req, err := http.NewRequest("GET", endpoint, bytes.NewBuffer([]byte("")))
   187  		if err != nil {
   188  			s.logger.WithError(err).Error("Error creating request")
   189  			return nil, err
   190  		}
   191  		req.Header.Set("Content-Type", contentType)
   192  		resp, err = client.Do(req)
   193  		if err == nil && resp != nil {
   194  			if resp.StatusCode >= 200 && resp.StatusCode < 300 {
   195  				return resp, nil
   196  			}
   197  			resp.Body.Close()
   198  			if resp.StatusCode >= 500 && resp.StatusCode < 600 {
   199  				s.logger.WithField("status", resp.StatusCode).Warn("Server error. Retrying...")
   200  			} else {
   201  				// Client error or other non-retryable response
   202  				return nil, fmt.Errorf("request failed with status code: %d", resp.StatusCode)
   203  			}
   204  		} else if err != nil {
   205  			s.logger.WithError(err).Warn("Failed to fetch data from API. Retrying...")
   206  		}
   207  		time.Sleep(time.Second * 2)
   208  	}
   209  	return nil, err
   210  }
   211  func (s *HydrationLiquidityScraper) fetchPools() {
   212  	// Fetch all pair tokens pool entries from api
   213  	pools, err := s.getPools()
   214  	if err != nil {
   215  		s.logger.WithError(err).Error("failed to GetAllPoolAssets")
   216  	}
   217  	s.logger.Infof("Found %d pools", len(pools))
   218  	for _, pool := range pools {
   219  		s.poolChannel <- *pool
   220  	}
   221  	s.doneChannel <- true
   222  }
   223  func (s *HydrationLiquidityScraper) Pool() chan dia.Pool {
   224  	return s.poolChannel
   225  }
   226  func (s *HydrationLiquidityScraper) Done() chan bool {
   227  	return s.doneChannel
   228  }