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

     1  package scrapers
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strconv"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/diadata-org/diadata/pkg/dia"
    13  	models "github.com/diadata-org/diadata/pkg/model"
    14  
    15  	substratehelper "github.com/diadata-org/diadata/pkg/dia/helpers/substrate-helper"
    16  	"github.com/diadata-org/diadata/pkg/dia/helpers/substrate-helper/gsrpc/registry/parser"
    17  	"github.com/diadata-org/diadata/pkg/utils"
    18  	"github.com/sirupsen/logrus"
    19  )
    20  
    21  type HydrationScraper struct {
    22  	logger       *logrus.Entry
    23  	pairScrapers map[string]*HydrationPairScraper // pc.ExchangePair -> pairScraperSet
    24  	shutdown     chan nothing
    25  	shutdownDone chan nothing
    26  	errorLock    sync.RWMutex
    27  	error        error
    28  	closed       bool
    29  	chanTrades   chan *dia.Trade
    30  	db           *models.RelDB
    31  	wsApi        *substratehelper.SubstrateEventHelper
    32  	exchangeName string
    33  	blockchain   string
    34  	currentBlock uint64
    35  }
    36  
    37  func NewHydrationScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *HydrationScraper {
    38  	logger := logrus.
    39  		New().
    40  		WithContext(context.Background()).
    41  		WithField("context", "HydrationScraper")
    42  
    43  	wsApi, err := substratehelper.NewSubstrateEventHelper(exchange.WsAPI, logger)
    44  	if err != nil {
    45  		logrus.WithError(err).Error("Failed to create Hydration Substrate event helper")
    46  		return nil
    47  	}
    48  
    49  	startBlock := utils.Getenv(strings.ToUpper(exchange.Name)+"_START_BLOCK", "0")
    50  	startBlockUint64, err := strconv.ParseUint(startBlock, 10, 64)
    51  	if err != nil {
    52  		logrus.WithError(err).Error("Failed to parse start block, using default value of 0")
    53  		startBlockUint64 = 0
    54  	}
    55  
    56  	s := &HydrationScraper{
    57  		logger:       logger, // Ensure logger is initialized
    58  		shutdown:     make(chan nothing),
    59  		shutdownDone: make(chan nothing),
    60  		chanTrades:   make(chan *dia.Trade),
    61  		db:           relDB,
    62  		wsApi:        wsApi,
    63  		exchangeName: exchange.Name,
    64  		blockchain:   "Hydration",
    65  		currentBlock: startBlockUint64,
    66  	}
    67  
    68  	s.logger.Info("WS API", s.wsApi)
    69  	if scrape {
    70  		go s.mainLoop()
    71  	}
    72  	return s
    73  }
    74  
    75  // processNewBlock processes new blocks and filters SellExecuted events.
    76  // https://hydration.subscan.io/event?block=6148977&page=1&time_dimension=date&module=xyk&event_id=sellexecuted
    77  // pool = 7KKXieLDbfJPUaVohYTbbib97LdC1URmZuMNFq9rvTudmDMv
    78  // block = 0xfd38c9dc2c95278fd3015f73b48a01e804320865a1a6153e31471cb782be92f0
    79  // blocknumber = 6148977
    80  func (s *HydrationScraper) mainLoop() {
    81  	s.logger.Info("Listening for new blocks")
    82  	defer s.cleanup(nil)
    83  
    84  	for {
    85  		select {
    86  		case <-s.shutdown:
    87  			s.logger.Println("shutting down")
    88  			return
    89  		default:
    90  			s.logger.Info("Processing block:", s.currentBlock)
    91  
    92  			if s.currentBlock == 0 {
    93  				s.wsApi.ListenForNewBlocks(s.processEvents)
    94  			} else {
    95  				s.wsApi.ListenForSpecificBlock(s.currentBlock, s.processEvents)
    96  				s.currentBlock++
    97  				time.Sleep(time.Second)
    98  				latestBlock, err := s.wsApi.API.RPC.Chain.GetBlockLatest()
    99  				if err != nil {
   100  					s.logger.WithError(err).Error("Failed to get latest block")
   101  					return
   102  				}
   103  
   104  				if s.currentBlock > uint64(latestBlock.Block.Header.Number) {
   105  					s.logger.Info("Reached the latest block")
   106  					s.wsApi.ListenForNewBlocks(s.processEvents)
   107  				}
   108  			}
   109  		}
   110  	}
   111  }
   112  
   113  func (s *HydrationScraper) processEvents(events []*parser.Event, blockNumber uint64) {
   114  	for _, e := range events {
   115  		parsedEvent := s.parseRouterEvent(e)
   116  		if parsedEvent == nil {
   117  			continue
   118  		}
   119  		if e.Phase.IsApplyExtrinsic {
   120  			parsedEvent.ExtrinsicID = fmt.Sprintf("%d-%d", blockNumber, e.Phase.AsApplyExtrinsic)
   121  		}
   122  
   123  		pools, err := s.db.GetAllPoolsExchange(s.exchangeName, 0)
   124  		if err != nil {
   125  			s.logger.Error("Failed to get pools from database")
   126  			continue
   127  		}
   128  
   129  		pool := s.filterPools(pools, parsedEvent)
   130  
   131  		if len(pool.Assetvolumes) < 2 {
   132  			// look for pool address in other events
   133  			secundaryEvent := s.parseSecundaryEvent(events, parsedEvent, blockNumber)
   134  			secundaryPool := s.filterPools(pools, secundaryEvent)
   135  
   136  			// Manually fill asset from routerEvent
   137  			assetIn, err := s.db.GetAsset(parsedEvent.AssetIn, s.blockchain)
   138  			if err != nil {
   139  				s.logger.Errorf("Failed to get assetIn for asset address %s", parsedEvent.AssetIn)
   140  				continue
   141  
   142  			}
   143  			assetOut, err := s.db.GetAsset(parsedEvent.AssetOut, s.blockchain)
   144  			if err != nil {
   145  				s.logger.Errorf("Failed to get assetOut for asset address %s", parsedEvent.AssetOut)
   146  				continue
   147  			}
   148  			pool.Address = secundaryPool.Address
   149  			pool.Assetvolumes = []dia.AssetVolume{
   150  				{Asset: assetIn},
   151  				{Asset: assetOut},
   152  			}
   153  			if len(pool.Assetvolumes) < 2 {
   154  				s.logger.WithField("poolAddress", pool.Address).Error("Pool has fewer than 2 asset volumes")
   155  				continue
   156  			}
   157  		}
   158  
   159  		diaTrade, err := s.handleTrade(pool, *parsedEvent, time.Now())
   160  		if err != nil {
   161  			s.logger.WithError(err).Error("Failed to handle trade")
   162  			continue
   163  		}
   164  
   165  		s.logger.WithFields(logrus.Fields{
   166  			"Pair":   diaTrade.Pair,
   167  			"Price":  diaTrade.Price,
   168  			"Volume": diaTrade.Volume,
   169  		}).Info("Trade processed")
   170  
   171  		s.chanTrades <- diaTrade
   172  	}
   173  }
   174  
   175  func (s *HydrationScraper) filterPools(pools []dia.Pool, event *HydrationParsedEvent) dia.Pool {
   176  	for _, pool := range pools {
   177  		assetInFound := false
   178  		assetOutFound := false
   179  
   180  		for _, assetVolume := range pool.Assetvolumes {
   181  			if assetVolume.Asset.Address == event.AssetIn {
   182  				assetInFound = true
   183  			}
   184  			if assetVolume.Asset.Address == event.AssetOut {
   185  				assetOutFound = true
   186  			}
   187  			if assetInFound && assetOutFound {
   188  				return pool
   189  			}
   190  		}
   191  	}
   192  	return dia.Pool{}
   193  }
   194  
   195  func (s *HydrationScraper) parseSecundaryEvent(events []*parser.Event, routerEvent *HydrationParsedEvent, blockNumber uint64) *HydrationParsedEvent {
   196  	parsedEvent := &HydrationParsedEvent{}
   197  	for _, e := range events {
   198  		if strings.EqualFold(e.Name, "Omnipool.SellExecuted") ||
   199  			strings.EqualFold(e.Name, "Omnipool.BuyExecuted") ||
   200  			strings.EqualFold(e.Name, "Stableswap.BuyExecuted") ||
   201  			strings.EqualFold(e.Name, "Stableswap.SellExecuted") {
   202  			if e.Phase.IsApplyExtrinsic {
   203  				extrinsicId := fmt.Sprintf("%d-%d", blockNumber, e.Phase.AsApplyExtrinsic)
   204  				if extrinsicId == routerEvent.ExtrinsicID {
   205  					parsedEvent := &HydrationParsedEvent{}
   206  					parsedEvent = s.parseFields(e)
   207  					parsedEvent.AmountIn = routerEvent.AmountIn
   208  					parsedEvent.AmountOut = routerEvent.AmountOut
   209  					parsedEvent.ExtrinsicID = extrinsicId
   210  					return parsedEvent
   211  				}
   212  			}
   213  		}
   214  	}
   215  
   216  	return parsedEvent
   217  }
   218  
   219  func (s *HydrationScraper) parseRouterEvent(event *parser.Event) *HydrationParsedEvent {
   220  	if strings.EqualFold(event.Name, "Router.Executed") {
   221  		return s.parseFields(event)
   222  	}
   223  
   224  	return nil
   225  }
   226  
   227  func (s *HydrationScraper) parseFields(event *parser.Event) *HydrationParsedEvent {
   228  	parsedEvent := &HydrationParsedEvent{}
   229  	for _, v := range event.Fields {
   230  		switch v.Name {
   231  		case "asset_in":
   232  			parsedEvent.AssetIn = fmt.Sprint(v.Value)
   233  		case "asset_out":
   234  			parsedEvent.AssetOut = fmt.Sprint(v.Value)
   235  		case "amount_in":
   236  			parsedEvent.AmountIn = fmt.Sprint(v.Value)
   237  		case "amount_out":
   238  			parsedEvent.AmountOut = fmt.Sprint(v.Value)
   239  		}
   240  	}
   241  	return parsedEvent
   242  }
   243  
   244  func (s *HydrationScraper) cleanup(err error) {
   245  	s.errorLock.Lock()
   246  	defer s.errorLock.Unlock()
   247  
   248  	if err != nil {
   249  		s.error = err
   250  	}
   251  	s.closed = true
   252  	close(s.shutdownDone)
   253  }
   254  
   255  func (s *HydrationScraper) Close() error {
   256  	if s.closed {
   257  		return errors.New("HydrationScraper: Already closed")
   258  	}
   259  	close(s.shutdown)
   260  	<-s.shutdownDone
   261  	s.errorLock.RLock()
   262  	defer s.errorLock.RUnlock()
   263  	return s.error
   264  }
   265  
   266  func (s *HydrationScraper) Channel() chan *dia.Trade {
   267  	return s.chanTrades
   268  }
   269  
   270  type HydrationPairScraper struct {
   271  	parent     *HydrationScraper
   272  	pair       dia.ExchangePair
   273  	closed     bool
   274  	lastRecord int64
   275  }
   276  
   277  func (ps *HydrationPairScraper) Pair() dia.ExchangePair {
   278  	return ps.pair
   279  }
   280  
   281  func (ps *HydrationPairScraper) Close() error {
   282  	ps.closed = true
   283  	return nil
   284  }
   285  
   286  // Error returns an error when the channel Channel() is closed
   287  // and nil otherwise
   288  func (ps *HydrationPairScraper) Error() error {
   289  	s := ps.parent
   290  	s.errorLock.RLock()
   291  	defer s.errorLock.RUnlock()
   292  	return s.error
   293  }
   294  
   295  // Channel returns the channel used to receive trades/pricing information.
   296  func (s *HydrationScraper) handleTrade(pool dia.Pool, event HydrationParsedEvent, time time.Time) (*dia.Trade, error) {
   297  	var volume, price float64
   298  	var decimalsIn, decimalsOut int64
   299  	var quoteToken, baseToken dia.Asset
   300  
   301  	// Determine which asset is being sold (this is the base asset)
   302  	for _, assetVolume := range pool.Assetvolumes {
   303  		if event.AssetIn == assetVolume.Asset.Address {
   304  			baseToken = assetVolume.Asset
   305  		}
   306  		if event.AssetOut == assetVolume.Asset.Address {
   307  			quoteToken = assetVolume.Asset
   308  		}
   309  		if baseToken.Address != "" && quoteToken.Address != "" {
   310  			break
   311  		}
   312  	}
   313  	// Check if both baseToken and quoteToken have been assigned
   314  	if baseToken.Address == "" || quoteToken.Address == "" {
   315  		return &dia.Trade{}, errors.New("Failed to determine baseToken or quoteToken")
   316  	}
   317  
   318  	decimalsIn = int64(baseToken.Decimals)
   319  	decimalsOut = int64(quoteToken.Decimals)
   320  	amountIn, _ := utils.StringToFloat64(event.AmountIn, decimalsIn)
   321  	amountOut, _ := utils.StringToFloat64(event.AmountOut, decimalsOut)
   322  
   323  	volume = amountOut
   324  
   325  	price = amountIn / amountOut
   326  
   327  	symbolPair := fmt.Sprintf("%s-%s", quoteToken.Symbol, baseToken.Symbol)
   328  
   329  	return &dia.Trade{
   330  		Time:           time,
   331  		Symbol:         quoteToken.Symbol,
   332  		Pair:           symbolPair,
   333  		ForeignTradeID: event.ExtrinsicID,
   334  		Source:         s.exchangeName,
   335  		Price:          price,
   336  		Volume:         volume,
   337  		VerifiedPair:   true,
   338  		QuoteToken:     quoteToken,
   339  		BaseToken:      baseToken,
   340  		PoolAddress:    pool.Address,
   341  	}, nil
   342  }
   343  
   344  func (s *HydrationScraper) FetchAvailablePairs() ([]dia.ExchangePair, error) {
   345  	return []dia.ExchangePair{}, nil
   346  }
   347  
   348  func (s *HydrationScraper) FillSymbolData(symbol string) (dia.Asset, error) {
   349  	return dia.Asset{Symbol: symbol}, nil
   350  }
   351  
   352  func (s *HydrationScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   353  	return pair, nil
   354  }
   355  
   356  func (s *HydrationScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   357  	s.errorLock.RLock()
   358  	defer s.errorLock.RUnlock()
   359  	if s.error != nil {
   360  		return nil, s.error
   361  	}
   362  	if s.closed {
   363  		return nil, errors.New("HydrationScraper: Call ScrapePair on closed scraper")
   364  	}
   365  	ps := &HydrationPairScraper{
   366  		parent:     s,
   367  		pair:       pair,
   368  		lastRecord: 0,
   369  	}
   370  
   371  	s.pairScrapers[pair.Symbol] = ps
   372  
   373  	return ps, nil
   374  }
   375  
   376  type HydrationParsedEvent struct {
   377  	Name        string
   378  	ExtrinsicID string
   379  	AssetIn     string
   380  	AssetOut    string
   381  	AmountIn    string
   382  	AmountOut   string
   383  }