github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/VelarScraper.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  	stacks "github.com/diadata-org/diadata/pkg/dia/helpers/stackshelper"
    13  	velar "github.com/diadata-org/diadata/pkg/dia/helpers/velarhelper"
    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 VelarScraper struct {
    20  	logger             *logrus.Entry
    21  	pairScrapers       map[string]*VelarPairScraper // pc.ExchangePair -> pairScraperSet
    22  	shutdown           chan nothing
    23  	shutdownDone       chan nothing
    24  	errorLock          sync.RWMutex
    25  	error              error
    26  	closed             bool
    27  	ticker             *time.Ticker
    28  	exchangeName       string
    29  	blockchain         string
    30  	chanTrades         chan *dia.Trade
    31  	api                *stacks.StacksClient
    32  	db                 *models.RelDB
    33  	currentHeight      int
    34  	initialBlockHeight int
    35  }
    36  
    37  // NewVelarScraper returns a new VelarScraper initialized with default values.
    38  // The instance is asynchronously scraping as soon as it is created.
    39  // ENV values:
    40  //
    41  //	 	VELAR_SLEEP_TIMEOUT - (optional, millisecond), make timeout between API calls, default "stackshelper.DefaultSleepBetweenCalls" value
    42  //		VELAR_REFRESH_DELAY - (optional, millisecond) refresh data after each poll, default "stackshelper.DefaultRefreshDelay" value
    43  //		VELAR_HIRO_API_KEY - (optional, string), Hiro Stacks API key, improves scraping performance, default = ""
    44  //		VELAR_INITIAL_BLOCK_HEIGHT (optional, int), useful for debug, default = 0
    45  //		VELAR_DEBUG - (optional, bool), make stdout output with alephium client http call, default = false
    46  func NewVelarScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *VelarScraper {
    47  	envPrefix := strings.ToUpper(exchange.Name)
    48  
    49  	sleepBetweenCalls := utils.GetTimeDurationFromIntAsMilliseconds(
    50  		utils.GetenvInt(
    51  			envPrefix+"_SLEEP_TIMEOUT",
    52  			stacks.DefaultSleepBetweenCalls,
    53  		),
    54  	)
    55  	refreshDelay := utils.GetTimeDurationFromIntAsMilliseconds(
    56  		utils.GetenvInt(envPrefix+"_REFRESH_DELAY", stacks.DefaultRefreshDelay),
    57  	)
    58  	hiroAPIKey := utils.Getenv(envPrefix+"_HIRO_API_KEY", "")
    59  	initialBlockHeight := utils.GetenvInt(envPrefix+"_INITIAL_BLOCK_HEIGHT", 0)
    60  	isDebug := utils.GetenvBool(envPrefix+"_DEBUG", false)
    61  
    62  	stacksClient := stacks.NewStacksClient(
    63  		log.WithContext(context.Background()).WithField("context", "StacksClient"),
    64  		sleepBetweenCalls,
    65  		hiroAPIKey,
    66  		isDebug,
    67  	)
    68  
    69  	s := &VelarScraper{
    70  		shutdown:           make(chan nothing),
    71  		shutdownDone:       make(chan nothing),
    72  		pairScrapers:       make(map[string]*VelarPairScraper),
    73  		ticker:             time.NewTicker(refreshDelay),
    74  		chanTrades:         make(chan *dia.Trade),
    75  		api:                stacksClient,
    76  		db:                 relDB,
    77  		exchangeName:       exchange.Name,
    78  		blockchain:         exchange.BlockChain.Name,
    79  		initialBlockHeight: initialBlockHeight,
    80  	}
    81  
    82  	s.logger = logrus.
    83  		New().
    84  		WithContext(context.Background()).
    85  		WithField("context", "VelarDEXScraper")
    86  
    87  	if scrape {
    88  		go s.mainLoop()
    89  	}
    90  	return s
    91  }
    92  
    93  func (s *VelarScraper) mainLoop() {
    94  	if s.initialBlockHeight <= 0 {
    95  		latestBlock, err := s.api.GetLatestBlock()
    96  		if err != nil {
    97  			s.logger.WithError(err).Error("failed to GetLatestBlock")
    98  			s.cleanup(err)
    99  			return
   100  		}
   101  		s.currentHeight = latestBlock.Height
   102  	} else {
   103  		s.currentHeight = s.initialBlockHeight
   104  	}
   105  
   106  	for {
   107  		select {
   108  		case <-s.ticker.C:
   109  			err := s.Update()
   110  			if err != nil {
   111  				s.logger.WithError(err).Error("failed to run Update")
   112  			}
   113  		case <-s.shutdown:
   114  			s.logger.Println("shutting down")
   115  			s.cleanup(nil)
   116  			return
   117  		}
   118  	}
   119  }
   120  
   121  func (s *VelarScraper) Update() error {
   122  	txs, err := s.api.GetAllBlockTransactions(s.currentHeight)
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	if len(txs) == 0 {
   128  		return nil
   129  	}
   130  	s.currentHeight += 1
   131  
   132  	swapEvents, err := s.getSwapEvents(txs)
   133  	if err != nil {
   134  		return err
   135  	}
   136  	if len(swapEvents) == 0 {
   137  		return nil
   138  	}
   139  
   140  	pools, err := s.getPools()
   141  	if err != nil {
   142  		s.logger.WithError(err).Error("failed to GetAllPoolsExchange")
   143  		return err
   144  	}
   145  
   146  	for _, pool := range pools {
   147  		if len(pool.Assetvolumes) != 2 {
   148  			s.logger.WithField("poolAddress", pool.Address).Error("pool is missing required asset volumes")
   149  			continue
   150  		}
   151  
   152  		for _, e := range swapEvents {
   153  			if pool.Address != e.TickerID {
   154  				continue
   155  			}
   156  
   157  			diaTrade := s.handleTrade(&pool, &e)
   158  			s.logger.
   159  				WithField("parentAddress", pool.Address).
   160  				WithField("height", s.currentHeight-1).
   161  				WithField("diaTrade", diaTrade).
   162  				Info("trade")
   163  			s.chanTrades <- diaTrade
   164  		}
   165  	}
   166  
   167  	return nil
   168  }
   169  
   170  func (s *VelarScraper) getPools() ([]dia.Pool, error) {
   171  	return s.db.GetAllPoolsExchange(s.exchangeName, 0)
   172  }
   173  
   174  func (s *VelarScraper) getSwapEvents(txs []stacks.Transaction) ([]velar.SwapEvent, error) {
   175  	result := make([]velar.SwapEvent, 0)
   176  
   177  	for _, tx := range txs {
   178  		if tx.TxStatus != "success" || tx.TxType != "contract_call" || !s.isSwapTransaction(&tx) {
   179  			continue
   180  		}
   181  
   182  		// This is a temporary workaround introduced due to a bug in hiro stacks API.
   183  		// Results returned from /blocks/{block_height}/transactions route have empty
   184  		// `name` field in `contract_call.function_args` list.
   185  		// TODO: remove this as soon as the issue is fixed.
   186  		normalizedTx, err := s.api.GetTransactionAt(tx.TxID)
   187  		if err != nil {
   188  			return nil, err
   189  		}
   190  
   191  		events, err := velar.DecodeSwapEvents(normalizedTx)
   192  		if err != nil {
   193  			return nil, err
   194  		}
   195  		result = append(result, events...)
   196  	}
   197  
   198  	return result, nil
   199  }
   200  
   201  func (s *VelarScraper) isSwapTransaction(tx *stacks.Transaction) bool {
   202  	return strings.HasPrefix(tx.ContractCall.FunctionName, "swap") ||
   203  		tx.ContractCall.FunctionName == "apply" ||
   204  		tx.ContractCall.FunctionName == "r4"
   205  }
   206  
   207  func (s *VelarScraper) handleTrade(pool *dia.Pool, event *velar.SwapEvent) *dia.Trade {
   208  	var volume, price float64
   209  
   210  	token0 := pool.Assetvolumes[0].Asset
   211  	token1 := pool.Assetvolumes[1].Asset
   212  
   213  	amountIn := event.AmountIn.String()
   214  	amountOut := event.AmountOut.String()
   215  
   216  	var trade dia.Trade
   217  
   218  	if event.TokenIn == token0.Address {
   219  		trade.Pair = fmt.Sprintf("%s-%s", token1.Symbol, token0.Symbol)
   220  		trade.Symbol = token1.Symbol
   221  		trade.BaseToken = token0
   222  		trade.QuoteToken = token1
   223  
   224  		amount0In, _ := utils.StringToFloat64(amountIn, int64(token0.Decimals))
   225  		amount1Out, _ := utils.StringToFloat64(amountOut, int64(token1.Decimals))
   226  		volume = amount1Out
   227  		price = amount0In / amount1Out
   228  	} else {
   229  		trade.Pair = fmt.Sprintf("%s-%s", token0.Symbol, token1.Symbol)
   230  		trade.Symbol = token0.Symbol
   231  		trade.BaseToken = token1
   232  		trade.QuoteToken = token0
   233  
   234  		amount1In, _ := utils.StringToFloat64(amountIn, int64(token1.Decimals))
   235  		amount0Out, _ := utils.StringToFloat64(amountOut, int64(token0.Decimals))
   236  		volume = amount0Out
   237  		price = amount1In / amount0Out
   238  	}
   239  
   240  	trade.Time = time.Unix(int64(event.Timestamp), 0)
   241  	trade.ForeignTradeID = event.TxID
   242  	trade.Source = s.exchangeName
   243  	trade.Price = price
   244  	trade.Volume = volume
   245  	trade.VerifiedPair = true
   246  
   247  	trade.PoolAddress = pool.Address
   248  	return &trade
   249  }
   250  
   251  func (s *VelarScraper) FetchAvailablePairs() ([]dia.ExchangePair, error) {
   252  	return []dia.ExchangePair{}, nil
   253  }
   254  
   255  func (s *VelarScraper) FillSymbolData(symbol string) (dia.Asset, error) {
   256  	return dia.Asset{Symbol: symbol}, nil
   257  }
   258  
   259  func (s *VelarScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   260  	return pair, nil
   261  }
   262  
   263  func (s *VelarScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   264  	s.errorLock.RLock()
   265  	defer s.errorLock.RUnlock()
   266  	if s.error != nil {
   267  		return nil, s.error
   268  	}
   269  	if s.closed {
   270  		return nil, errors.New("VelarScraper: Call ScrapePair on closed scraper")
   271  	}
   272  	ps := &VelarPairScraper{
   273  		parent:     s,
   274  		pair:       pair,
   275  		lastRecord: 0,
   276  	}
   277  
   278  	s.pairScrapers[pair.Symbol] = ps
   279  
   280  	return ps, nil
   281  }
   282  
   283  func (s *VelarScraper) cleanup(err error) {
   284  	s.errorLock.Lock()
   285  	defer s.errorLock.Unlock()
   286  
   287  	s.ticker.Stop()
   288  
   289  	if err != nil {
   290  		s.error = err
   291  	}
   292  	s.closed = true
   293  	close(s.shutdownDone)
   294  }
   295  
   296  // Close gracefully shuts down the VelarScraper.
   297  func (s *VelarScraper) Close() error {
   298  	if s.closed {
   299  		return errors.New("VelarScraper: Already closed")
   300  	}
   301  	close(s.shutdown)
   302  	<-s.shutdownDone
   303  	s.errorLock.RLock()
   304  	defer s.errorLock.RUnlock()
   305  	return s.error
   306  }
   307  
   308  // Channel returns the channel used to receive trades/pricing information.
   309  func (s *VelarScraper) Channel() chan *dia.Trade {
   310  	return s.chanTrades
   311  }
   312  
   313  type VelarPairScraper struct {
   314  	parent     *VelarScraper
   315  	pair       dia.ExchangePair
   316  	closed     bool
   317  	lastRecord int64
   318  }
   319  
   320  func (ps *VelarPairScraper) Pair() dia.ExchangePair {
   321  	return ps.pair
   322  }
   323  
   324  func (ps *VelarPairScraper) Close() error {
   325  	ps.closed = true
   326  	return nil
   327  }
   328  
   329  // Error returns an error when the channel Channel() is closed
   330  // and nil otherwise
   331  func (ps *VelarPairScraper) Error() error {
   332  	s := ps.parent
   333  	s.errorLock.RLock()
   334  	defer s.errorLock.RUnlock()
   335  	return s.error
   336  }