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  }