github.com/diadata-org/diadata@v1.4.593/pkg/dia/service/assetservice/source/hydration.go (about) 1 package source 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 // HydrationAssetSource asset collector object - which serves assetCollector command 20 type HydrationAssetSource struct { 21 // client - interaction with hydration REST API services 22 hydrationClient *hydrationhelper.HydrationClient 23 // channel to store received asset info 24 assetChannel chan dia.Asset 25 // channel which informs about work is finished 26 doneChannel chan bool 27 // blockchain name 28 blockchain string 29 // exchange name 30 exchange string 31 // DB connector to interact with databases 32 relDB *models.RelDB 33 // logs all events here 34 logger *logrus.Entry 35 // swap contracts count limitation in hydration REST API 36 swapContractsLimit int 37 sleepTimeout time.Duration 38 targetSwapContract string 39 api string 40 } 41 42 // NewHydrationAssetSource creates object to get hydration assets 43 // ENV values: 44 // 45 // HYDRATION_ASSETS_SLEEP_TIMEOUT - (optional,millisecond), make timeout between API calls, default "hydrationhelper.DefaultSleepBetweenContractCalls" value 46 // HYDRATION_SWAP_CONTRACTS_LIMIT - (optional, int), limit to get swap contact addresses, default "hydrationhelper.DefaultSwapContractsLimit" value 47 // HYDRATION_TARGET_SWAP_CONTRACT - (optional, string), useful for debug, default = "" 48 // HYDRATION_DEBUG - (optional, bool), make stdout output with hydration client http call, default = false 49 func NewHydrationAssetSource(exchange dia.Exchange, relDB *models.RelDB) *HydrationAssetSource { 50 sleepBetweenContractCalls := utils.GetTimeDurationFromIntAsMilliseconds( 51 utils.GetenvInt(strings.ToUpper(exchange.Name)+"_SLEEP_TIMEOUT", hydrationhelper.DefaultSleepBetweenContractCalls), 52 ) 53 swapContractsLimit := utils.GetenvInt( 54 strings.ToUpper(exchange.Name)+"_SWAP_CONTRACTS_LIMIT", 55 hydrationhelper.DefaultSwapContractsLimit, 56 ) 57 targetSwapContract := utils.Getenv(strings.ToUpper(exchange.Name)+"_TARGET_SWAP_CONTRACT", "") 58 isDebug := utils.GetenvBool(strings.ToUpper(exchange.Name)+"_DEBUG", false) 59 var ( 60 assetChannel = make(chan dia.Asset) 61 doneChannel = make(chan bool) 62 ) 63 hydrationClient := hydrationhelper.NewHydrationClient( 64 log.WithContext(context.Background()).WithField("context", "HydrationClient"), 65 sleepBetweenContractCalls, 66 isDebug, 67 ) 68 logger := log. 69 WithContext(context.Background()). 70 WithField("service", "assetCollector"). 71 WithField("network", "Hydration") 72 apiURL := utils.Getenv(strings.ToUpper(exchange.Name)+"_API_URL", "http://localhost:3000/hydration/v1") 73 scraper := &HydrationAssetSource{ 74 hydrationClient: hydrationClient, 75 assetChannel: assetChannel, 76 doneChannel: doneChannel, 77 blockchain: "Hydration", 78 exchange: "Hydration", 79 relDB: relDB, 80 logger: logger, 81 swapContractsLimit: swapContractsLimit, 82 sleepTimeout: sleepBetweenContractCalls, 83 targetSwapContract: targetSwapContract, 84 api: apiURL, 85 } 86 go scraper.fetchAssets() 87 return scraper 88 } 89 90 // fetchAssets scrapes asset data and sends it through channels for processing. 91 // 92 // This method logs the start of the asset scraping process, fetches assets using 93 // the `scrapAssets` method, and handles any errors that occur during the scraping. 94 // If successful, it sends each scraped asset into the `assetChannel` for further 95 // processing. Once all assets have been sent, a signal is sent to the `doneChannel` 96 // indicating the completion of the process. 97 // 98 // No return values. 99 func (s *HydrationAssetSource) fetchAssets() { 100 s.logger.Info("Scraping assets...") 101 assets, err := s.scrapAssets() 102 if err != nil { 103 s.logger.Error("Error when scraping assets: ", err) 104 return 105 } 106 for _, asset := range assets { 107 s.assetChannel <- *asset 108 } 109 s.doneChannel <- true 110 } 111 112 // scrapAssets fetches asset metadata from the API and processes it into dia.Asset objects. 113 // 114 // This method performs an HTTP request to retrieve asset data from the Hydration API. 115 // It retries the request up to 3 times if necessary. After successfully fetching 116 // the data, it decodes the JSON response into a slice of `HydrationAssetMetadata` objects 117 // and parses them into `dia.Asset` objects using the `parseAssets` method. 118 // 119 // Parameters: 120 // - None. 121 // 122 // Returns: 123 // - []*dia.Asset: A slice of pointers to `dia.Asset` objects representing the processed assets. 124 // - error: An error if the API request or JSON decoding fails, otherwise `nil`. 125 func (s *HydrationAssetSource) scrapAssets() ([]*dia.Asset, error) { 126 endpoint := fmt.Sprintf("%s/assets", s.api) 127 resp, err := s.fetchWithRetry(endpoint, "application/json", 3) 128 if err != nil { 129 return nil, fmt.Errorf("failed to fetch swap events after retries: %w", err) 130 } 131 defer resp.Body.Close() 132 var assets []hydrationhelper.HydrationAssetMetadata 133 if err := json.NewDecoder(resp.Body).Decode(&assets); err != nil { 134 return nil, fmt.Errorf("failed to decode swap events: %w", err) 135 } 136 return s.parseAssets(assets), nil 137 } 138 func (s *HydrationAssetSource) Asset() chan dia.Asset { 139 return s.assetChannel 140 } 141 func (s *HydrationAssetSource) Done() chan bool { 142 return s.doneChannel 143 } 144 145 // fetchWithRetry performs an HTTP GET request to the specified endpoint with retries. 146 // 147 // This function attempts to fetch data from the provided API endpoint, retrying 148 // the request up to the specified number of times (`retries`) in case of failure. 149 // The retry logic handles both network errors and server-side errors (HTTP 5xx). 150 // 151 // For each attempt, the function logs the attempt number and the endpoint being requested. 152 // If a request fails due to a server-side error (HTTP 5xx), the function retries 153 // after a short delay. Client-side errors (HTTP 4xx) and other non-retryable errors 154 // are returned immediately. 155 // 156 // Parameters: 157 // - endpoint: The URL of the API endpoint to request data from. 158 // - contentType: The content type to set in the request header (e.g., "application/json"). 159 // - retries: The number of retry attempts in case of failure. 160 // 161 // Returns: 162 // - *http.Response: The response object if the request is successful. 163 // - error: An error if all retry attempts fail, or if a non-retryable error occurs. 164 func (s *HydrationAssetSource) fetchWithRetry(endpoint string, contentType string, retries int) (*http.Response, error) { 165 client := &http.Client{ 166 Timeout: 20 * time.Second, 167 } 168 var resp *http.Response 169 var err error 170 for i := 0; i < retries; i++ { 171 s.logger.WithField("attempt", i+1).Info("Fetching data from API") 172 s.logger.WithField("endpoint", endpoint).Info("Requesting data") 173 req, err := http.NewRequest("GET", endpoint, bytes.NewBuffer([]byte(""))) 174 if err != nil { 175 s.logger.WithError(err).Error("Error creating request") 176 return nil, err 177 } 178 req.Header.Set("Content-Type", contentType) 179 resp, err = client.Do(req) 180 if err == nil && resp != nil { 181 if resp.StatusCode >= 200 && resp.StatusCode < 300 { 182 return resp, nil 183 } 184 resp.Body.Close() 185 if resp.StatusCode >= 500 && resp.StatusCode < 600 { 186 s.logger.WithField("status", resp.StatusCode).Warn("Server error. Retrying...") 187 } else { 188 // Client error or other non-retryable response 189 return nil, fmt.Errorf("request failed with status code: %d", resp.StatusCode) 190 } 191 } else if err != nil { 192 s.logger.WithError(err).Warn("Failed to fetch data from API. Retrying...") 193 } 194 time.Sleep(time.Second * 2) 195 } 196 return nil, err 197 } 198 func (s *HydrationAssetSource) parseAssets(assetsMetadata []hydrationhelper.HydrationAssetMetadata) []*dia.Asset { 199 assets := make([]*dia.Asset, 0, len(assetsMetadata)) 200 for _, assetMetadata := range assetsMetadata { 201 asset := s.parseAsset(assetMetadata) 202 assets = append(assets, asset) 203 } 204 return assets 205 } 206 207 // parseAsset converts a HydrationAssetMetadata object into a dia.Asset object. 208 // 209 // This method takes metadata about an asset, such as its name, symbol, and decimals, 210 // and converts it into a `dia.Asset` object. The blockchain name is added from the 211 // `HydrationAssetSource` instance, and the asset's address is constructed by appending 212 // the asset ID (in lowercase) to a fixed prefix. 213 // 214 // Parameters: 215 // - assetMetadata: A `HydrationAssetMetadata` object containing the raw asset data. 216 // 217 // Returns: 218 // - *dia.Asset: A pointer to the newly created `dia.Asset` object. 219 func (s *HydrationAssetSource) parseAsset(assetMetadata hydrationhelper.HydrationAssetMetadata) *dia.Asset { 220 return &dia.Asset{ 221 Name: assetMetadata.Name, 222 Symbol: assetMetadata.Symbol, 223 Decimals: uint8(assetMetadata.Decimals), 224 Blockchain: "Hydration", 225 Address: assetMetadata.Id, 226 } 227 }