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 }