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

     1  package scrapers
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/diadata-org/diadata/pkg/dia"
    12  	alephiumhelper "github.com/diadata-org/diadata/pkg/dia/helpers/alephium-helper"
    13  	models "github.com/diadata-org/diadata/pkg/model"
    14  	"github.com/diadata-org/diadata/pkg/utils"
    15  	"github.com/sirupsen/logrus"
    16  )
    17  
    18  type AyinScraper struct {
    19  	logger *logrus.Entry
    20  	// signaling channels
    21  	shutdown     chan nothing
    22  	shutdownDone chan nothing
    23  	// error handling; to read error or closed, first acquire read lock
    24  	// only cleanup method should hold write lock
    25  	errorLock                 sync.RWMutex
    26  	error                     error
    27  	closed                    bool
    28  	pairScrapers              map[string]*AyinPairScraper // pc.ExchangePair -> pairScraperSet
    29  	api                       *alephiumhelper.AlephiumClient
    30  	ticker                    *time.Ticker
    31  	exchangeName              string
    32  	blockchain                string
    33  	currentHeight             int
    34  	eventsLimit               int
    35  	swapContractsLimit        int
    36  	targetSwapContract        string
    37  	chanTrades                chan *dia.Trade
    38  	db                        *models.RelDB
    39  	refreshDelay              time.Duration
    40  	sleepBetweenContractCalls time.Duration
    41  	reverseBasetokens         []string
    42  	reverseQuotetokens        []string
    43  }
    44  
    45  // NewAyinScraper returns a new AyinScraper initialized with default values.
    46  // The instance is asynchronously scraping as soon as it is created.
    47  // ENV values:
    48  //
    49  //		AYIN_REFRESH_DELAY - (optional,millisecond) refresh data after each poll, default "alephiumhelper.DefaultRefreshDelay" value
    50  //	 	AYIN_SLEEP_TIMEOUT - (optional,millisecond), make timeout between API calls, default "alephiumhelper.DefaultSleepBetweenContractCalls" value
    51  //		AYIN_SWAP_CONTRACTS_LIMIT - (optional, int), limit to get swap contact addresses, default "alephiumhelper.DefaultSwapContractsLimit" value
    52  //		AYIN_TARGET_SWAP_CONTRACT - (optional, string), useful for debug, default = ""
    53  //		AYIN_DEBUG - (optional, bool), make stdout output with alephium client http call, default = false
    54  func NewAyinScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *AyinScraper {
    55  	ayinRefreshDelay := utils.GetTimeDurationFromIntAsMilliseconds(
    56  		utils.GetenvInt(strings.ToUpper(exchange.Name)+"_REFRESH_DELAY", alephiumhelper.DefaultRefreshDelay),
    57  	)
    58  	sleepBetweenContractCalls := utils.GetTimeDurationFromIntAsMilliseconds(
    59  		utils.GetenvInt(strings.ToUpper(exchange.Name)+"_SLEEP_TIMEOUT", alephiumhelper.DefaultSleepBetweenContractCalls),
    60  	)
    61  	isDebug := utils.GetenvBool(strings.ToUpper(exchange.Name)+"_DEBUG", false)
    62  	eventsLimit := utils.GetenvInt(strings.ToUpper(exchange.Name)+"_REFRESH_DELAY", alephiumhelper.DefaultEventsLimit)
    63  	swapContractsLimit := utils.GetenvInt(strings.ToUpper(exchange.Name)+"_SWAP_CONTRACTS_LIMIT", alephiumhelper.DefaultSwapContractsLimit)
    64  	targetSwapContract := utils.Getenv(strings.ToUpper(exchange.Name)+"_TARGET_SWAP_CONTRACT", "")
    65  
    66  	alephiumClient := alephiumhelper.NewAlephiumClient(
    67  		log.WithContext(context.Background()).WithField("context", "AlephiumClient"),
    68  		sleepBetweenContractCalls,
    69  		isDebug,
    70  	)
    71  	s := &AyinScraper{
    72  		shutdown:                  make(chan nothing),
    73  		shutdownDone:              make(chan nothing),
    74  		pairScrapers:              make(map[string]*AyinPairScraper),
    75  		api:                       alephiumClient,
    76  		ticker:                    time.NewTicker(ayinRefreshDelay),
    77  		exchangeName:              exchange.Name,
    78  		blockchain:                exchange.BlockChain.Name,
    79  		currentHeight:             0,
    80  		error:                     nil,
    81  		chanTrades:                make(chan *dia.Trade),
    82  		db:                        relDB,
    83  		refreshDelay:              ayinRefreshDelay,
    84  		sleepBetweenContractCalls: sleepBetweenContractCalls,
    85  		eventsLimit:               eventsLimit,
    86  		swapContractsLimit:        swapContractsLimit,
    87  		targetSwapContract:        targetSwapContract,
    88  	}
    89  
    90  	// Import tokens which appear as base token and we need a quotation for
    91  	var err error
    92  	reverseBasetokens, err = getReverseTokensFromConfig("ayin/reverse_tokens/" + s.exchangeName + "Basetoken")
    93  	if err != nil {
    94  		log.Error("error getting tokens for which pairs should be reversed: ", err)
    95  	}
    96  	log.Info("reverse basetokens: ", reverseBasetokens)
    97  	reverseQuotetokens, err = getReverseTokensFromConfig("ayin/reverse_tokens/" + s.exchangeName + "Quotetoken")
    98  	if err != nil {
    99  		log.Error("error getting tokens for which pairs should be reversed: ", err)
   100  	}
   101  	log.Info("reverse quotetokens: ", reverseQuotetokens)
   102  	s.reverseBasetokens = *reverseBasetokens
   103  	s.reverseQuotetokens = *reverseQuotetokens
   104  
   105  	s.logger = logrus.
   106  		New().
   107  		WithContext(context.Background()).
   108  		WithField("context", "AyinDEXScraper")
   109  
   110  	if scrape {
   111  		go s.mainLoop()
   112  	}
   113  	return s
   114  }
   115  
   116  // mainLoop runs in a goroutine until channel s is closed.
   117  func (s *AyinScraper) mainLoop() {
   118  	currentHeight, err := s.api.GetCurrentHeight()
   119  	if err != nil {
   120  		s.logger.WithError(err).Error("failed to GetCurrentHeight")
   121  		s.cleanup(err)
   122  		return
   123  	}
   124  	s.currentHeight = currentHeight
   125  
   126  	err = s.Update()
   127  	if err != nil {
   128  		s.logger.Error(err)
   129  	}
   130  	for {
   131  		select {
   132  		case <-s.ticker.C:
   133  			err := s.Update()
   134  			if err != nil {
   135  				s.logger.Error(err)
   136  			}
   137  		case <-s.shutdown: // user requested shutdown
   138  			s.logger.Println("shutting down")
   139  			s.cleanup(nil)
   140  			return
   141  		}
   142  	}
   143  }
   144  
   145  func (s *AyinScraper) FillSymbolData(symbol string) (dia.Asset, error) {
   146  	return dia.Asset{Symbol: symbol}, nil
   147  }
   148  
   149  func (s *AyinScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   150  	return pair, nil
   151  }
   152  
   153  func (s *AyinScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   154  	s.errorLock.RLock()
   155  	defer s.errorLock.RUnlock()
   156  	if s.error != nil {
   157  		return nil, s.error
   158  	}
   159  	if s.closed {
   160  		return nil, errors.New("AlephiumScraper: Call ScrapePair on closed scraper")
   161  	}
   162  	ps := &AyinPairScraper{
   163  		parent:     s,
   164  		pair:       pair,
   165  		lastRecord: 0,
   166  	}
   167  
   168  	s.pairScrapers[pair.Symbol] = ps
   169  
   170  	return ps, nil
   171  }
   172  
   173  func (s *AyinScraper) getPools() ([]dia.Pool, error) {
   174  	if s.targetSwapContract != "" {
   175  		result := make([]dia.Pool, 1)
   176  		pool, err := s.db.GetPoolByAddress(s.blockchain, s.targetSwapContract)
   177  		result[0] = pool
   178  		return result, err
   179  	}
   180  	return s.db.GetAllPoolsExchange(s.exchangeName, 0)
   181  }
   182  
   183  func (s *AyinScraper) Update() error {
   184  	logger := s.logger.WithFields(logrus.Fields{
   185  		"function": "Update",
   186  	})
   187  
   188  	blockHashes, err := s.api.GetBlockHashes(s.currentHeight)
   189  	if err != nil {
   190  		s.logger.WithError(err).Error("failed to GetBlockHashes")
   191  		return err
   192  	}
   193  	if len(blockHashes) == 0 {
   194  		// logger.Info("no new blocks in the network, waiting...")
   195  		return nil
   196  	}
   197  
   198  	allEvents, err := s.fetchEvents(blockHashes)
   199  	if err != nil {
   200  		logger.WithError(err).Error("failed to fetch events")
   201  		return err
   202  	}
   203  	s.currentHeight += 1
   204  
   205  	if len(allEvents) == 0 {
   206  		logger.WithField("currentHeight", s.currentHeight).Info("no events, skipping to the next block...")
   207  		return nil
   208  	}
   209  
   210  	pools, err := s.getPools()
   211  	if err != nil {
   212  		logger.
   213  			WithError(err).
   214  			Error("failed to GetAllPoolsExchange")
   215  		return err
   216  	}
   217  
   218  	for _, pool := range pools {
   219  		if len(pool.Assetvolumes) != 2 {
   220  			logger.WithField("poolAddress", pool.Address).Error("pool is missing required asset volumes")
   221  			continue
   222  		}
   223  
   224  		poolEvents := make([]alephiumhelper.ContractEvent, 0)
   225  
   226  		for _, event := range allEvents {
   227  			if event.ContractAddress == pool.Address {
   228  				poolEvents = append(poolEvents, event)
   229  			}
   230  		}
   231  
   232  		if len(poolEvents) == 0 {
   233  			continue
   234  		}
   235  
   236  		for _, event := range poolEvents {
   237  			logger.WithField("event", event).WithField("currentHeight", s.currentHeight).Info("event")
   238  			transactionDetails, err := s.api.GetTransactionDetails(event.TxID)
   239  			if err != nil {
   240  				logger.
   241  					WithError(err).
   242  					Error("failed to GetTransactionDetails")
   243  				continue
   244  			}
   245  
   246  			var timestamp time.Time
   247  			if transactionDetails.Timestamp > 0 {
   248  				timestamp = time.UnixMilli(transactionDetails.Timestamp)
   249  			} else {
   250  				timestamp = time.Now()
   251  			}
   252  
   253  			diaTrade := s.handleTrade(&pool, &event, timestamp)
   254  			logger.
   255  				WithField("parentAddress", pool.Address).
   256  				WithField("diaTrade", diaTrade).
   257  				Info("diaTrade")
   258  			s.chanTrades <- diaTrade
   259  		}
   260  	}
   261  
   262  	return nil
   263  }
   264  
   265  func (s *AyinScraper) fetchEvents(blockHashes []string) ([]alephiumhelper.ContractEvent, error) {
   266  	allEvents := make([]alephiumhelper.ContractEvent, 0)
   267  
   268  	for _, hash := range blockHashes {
   269  		events, err := s.api.GetBlockEvents(hash)
   270  		if err != nil {
   271  			return allEvents, err
   272  		}
   273  
   274  		filtered := s.api.FilterEvents(events, alephiumhelper.SwapEventIndex)
   275  		allEvents = append(allEvents, filtered...)
   276  	}
   277  
   278  	return allEvents, nil
   279  }
   280  
   281  func (s *AyinScraper) handleTrade(pool *dia.Pool, event *alephiumhelper.ContractEvent, time time.Time) *dia.Trade {
   282  	var volume, price float64
   283  
   284  	decimals0 := int64(pool.Assetvolumes[0].Asset.Decimals)
   285  	decimals1 := int64(pool.Assetvolumes[1].Asset.Decimals)
   286  
   287  	if event.Fields[1].Value != "0" {
   288  		// if we are swapping from ALPH(asset0) to USDT(asset1), - default behaviour
   289  		//	then amount0In ((fields[1]) will be the amount for ALPH
   290  		//	and amount1Out (fields[4]) will be the amount for USDT.
   291  		amount0In, _ := utils.StringToFloat64(event.Fields[1].Value, decimals0)
   292  		amount1Out, _ := utils.StringToFloat64(event.Fields[4].Value, decimals1)
   293  		volume = amount0In
   294  		price = amount1Out / amount0In
   295  	} else {
   296  		// If we are swapping from USDT(asset1) to ALPH(asset0),
   297  		//	then amount1In ((fields[2]) will be the amount for USDT
   298  		//  and amount0Out (fields[3]) will be the amount for ALPH.
   299  		amount1In, _ := utils.StringToFloat64(event.Fields[2].Value, decimals1)
   300  		amount0Out, _ := utils.StringToFloat64(event.Fields[3].Value, decimals0)
   301  		volume = -amount0Out
   302  		price = amount1In / amount0Out
   303  	}
   304  
   305  	symbolPair := fmt.Sprintf("%s-%s", pool.Assetvolumes[0].Asset.Symbol, pool.Assetvolumes[1].Asset.Symbol)
   306  
   307  	diaTrade := &dia.Trade{
   308  		Time:           time,
   309  		Symbol:         pool.Assetvolumes[0].Asset.Symbol,
   310  		Pair:           symbolPair,
   311  		ForeignTradeID: event.TxID,
   312  		Source:         s.exchangeName,
   313  		Price:          price,
   314  		Volume:         volume,
   315  		VerifiedPair:   true,
   316  		QuoteToken:     pool.Assetvolumes[0].Asset,
   317  		BaseToken:      pool.Assetvolumes[1].Asset,
   318  		PoolAddress:    pool.Address,
   319  	}
   320  
   321  	// Reverse the order of the trade for selected assets.
   322  	switch {
   323  	case utils.Contains(reverseQuotetokens, diaTrade.QuoteToken.Address):
   324  		// If we don't need quotation of quote token, reverse pair.
   325  		tSwapped, err := dia.SwapTrade(*diaTrade)
   326  		if err == nil {
   327  			diaTrade = &tSwapped
   328  		}
   329  	case utils.Contains(reverseBasetokens, diaTrade.BaseToken.Address):
   330  		// If we need quotation of a base token, reverse pair
   331  		tSwapped, err := dia.SwapTrade(*diaTrade)
   332  		if err == nil {
   333  			diaTrade = &tSwapped
   334  		}
   335  	}
   336  
   337  	return diaTrade
   338  }
   339  
   340  // closes all connected PairScrapers
   341  // must only be called from mainLoop
   342  func (s *AyinScraper) cleanup(err error) {
   343  
   344  	s.errorLock.Lock()
   345  	defer s.errorLock.Unlock()
   346  
   347  	s.ticker.Stop()
   348  
   349  	if err != nil {
   350  		s.error = err
   351  	}
   352  	s.closed = true
   353  
   354  	close(s.shutdownDone) // signal that shutdown is complete
   355  }
   356  
   357  // Close closes any existing API connections, as well as channels of
   358  // PairScrapers from calls to ScrapePair
   359  func (s *AyinScraper) Close() error {
   360  	if s.closed {
   361  		return errors.New("AlephiumScraper: Already closed")
   362  	}
   363  	close(s.shutdown)
   364  	<-s.shutdownDone
   365  	s.errorLock.RLock()
   366  	defer s.errorLock.RUnlock()
   367  	return s.error
   368  }
   369  
   370  // Channel returns a channel that can be used to receive trades/pricing information
   371  func (s *AyinScraper) Channel() chan *dia.Trade {
   372  	return s.chanTrades
   373  }
   374  
   375  // FetchAvailablePairs returns a list with all available trade pairs as dia.ExchangePair for the pairDiscorvery service
   376  func (s *AyinScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   377  	logger := s.logger.WithFields(logrus.Fields{
   378  		"function": "FetchAvailablePairs",
   379  	})
   380  	contractAddresses, err := s.api.GetSwapPairsContractAddresses(s.swapContractsLimit)
   381  	if err != nil {
   382  		logger.WithError(err).Error("failed to get swap contract addresses")
   383  		return
   384  	}
   385  	for _, contractAddress := range contractAddresses.SubContracts {
   386  		tokenPairs, err := s.api.GetTokenPairAddresses(contractAddress)
   387  
   388  		if err != nil {
   389  			logger.
   390  				WithField("contractAddress", contractAddress).
   391  				WithError(err).
   392  				Error("failed to get GetTokenPairAddresses")
   393  			continue
   394  		}
   395  
   396  		token0, err := s.api.GetTokenInfoForContractDecoded(tokenPairs[0], s.blockchain)
   397  		if err != nil {
   398  			logger.
   399  				WithField("contractAddress", contractAddress).
   400  				WithError(err).
   401  				Error("failed to get GetTokenInfoForContractDecoded for token0")
   402  			continue
   403  		}
   404  
   405  		token1, err := s.api.GetTokenInfoForContractDecoded(tokenPairs[1], s.blockchain)
   406  		if err != nil {
   407  			logger.
   408  				WithField("contractAddress", contractAddress).
   409  				WithError(err).
   410  				Error("failed to get GetTokenInfoForContractDecoded for token1")
   411  			continue
   412  		}
   413  		pair := dia.ExchangePair{
   414  			Symbol:      token0.Symbol,
   415  			ForeignName: fmt.Sprintf("%s-%s", token0.Symbol, token1.Symbol),
   416  			Exchange:    s.exchangeName,
   417  		}
   418  
   419  		pairs = append(pairs, pair)
   420  
   421  		time.Sleep(s.sleepBetweenContractCalls)
   422  	}
   423  	return pairs, nil
   424  }
   425  
   426  type AyinPairScraper struct {
   427  	parent     *AyinScraper
   428  	pair       dia.ExchangePair
   429  	closed     bool
   430  	lastRecord int64
   431  }
   432  
   433  func (ps *AyinPairScraper) Pair() dia.ExchangePair {
   434  	return ps.pair
   435  }
   436  
   437  func (ps *AyinPairScraper) Close() error {
   438  	ps.closed = true
   439  	return nil
   440  }
   441  
   442  // Error returns an error when the channel Channel() is closed
   443  // and nil otherwise
   444  func (ps *AyinPairScraper) Error() error {
   445  	s := ps.parent
   446  	s.errorLock.RLock()
   447  	defer s.errorLock.RUnlock()
   448  	return s.error
   449  }