github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/BifrostScraper.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  	substratehelper "github.com/diadata-org/diadata/pkg/dia/helpers/substrate-helper"
    14  	"github.com/diadata-org/diadata/pkg/dia/helpers/substrate-helper/gsrpc/registry"
    15  	"github.com/diadata-org/diadata/pkg/dia/helpers/substrate-helper/gsrpc/registry/parser"
    16  
    17  	models "github.com/diadata-org/diadata/pkg/model"
    18  
    19  	"github.com/diadata-org/diadata/pkg/utils"
    20  	"github.com/sirupsen/logrus"
    21  )
    22  
    23  type BifrostScraper struct {
    24  	logger       *logrus.Entry
    25  	pairScrapers map[string]*BifrostPairScraper // pc.ExchangePair -> pairScraperSet
    26  	shutdown     chan nothing
    27  	shutdownDone chan nothing
    28  	errorLock    sync.RWMutex
    29  	error        error
    30  	closed       bool
    31  	ticker       *time.Ticker
    32  	chanTrades   chan *dia.Trade
    33  	db           *models.RelDB
    34  	wsApi        *substratehelper.SubstrateEventHelper
    35  	exchangeName string
    36  	blockchain   string
    37  	currentBlock uint64
    38  }
    39  
    40  func NewBifrostScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BifrostScraper {
    41  	logger := logrus.
    42  		New().
    43  		WithContext(context.Background()).
    44  		WithField("context", "BifrostScraper")
    45  
    46  	wsApi, err := substratehelper.NewSubstrateEventHelper(exchange.WsAPI, logger)
    47  	if err != nil {
    48  		logrus.WithError(err).Error("Failed to create Bifrost Substrate event helper")
    49  		return nil
    50  	}
    51  
    52  	startBlock := utils.Getenv(strings.ToUpper(exchange.Name)+"_START_BLOCK", "0")
    53  	startBlockUint64, err := strconv.ParseUint(startBlock, 10, 64)
    54  	if err != nil {
    55  		logrus.WithError(err).Error("Failed to parse start block, using default value of 10")
    56  		startBlockUint64 = 10
    57  	}
    58  
    59  	s := &BifrostScraper{
    60  		shutdown:     make(chan nothing),
    61  		shutdownDone: make(chan nothing),
    62  		chanTrades:   make(chan *dia.Trade),
    63  		db:           relDB,
    64  		wsApi:        wsApi,
    65  		exchangeName: exchange.Name,
    66  		blockchain:   "Bifrost",
    67  		currentBlock: startBlockUint64,
    68  	}
    69  
    70  	s.logger = logger
    71  
    72  	s.logger.Info("Initialized BifrostScraper")
    73  
    74  	if scrape {
    75  		go s.mainLoop()
    76  	}
    77  	return s
    78  }
    79  
    80  func (s *BifrostScraper) 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 *BifrostScraper) processEvents(events []*parser.Event, blockNumber uint64) {
   114  	s.logger.Info("Processing events")
   115  
   116  	for _, e := range events {
   117  		if e.Name == "StableAsset.TokenSwapped" {
   118  			parsedEvent := parseFields(e)
   119  			parsedEvent.ExtrinsicID = fmt.Sprintf("%d-%d", blockNumber, e.Phase.AsApplyExtrinsic)
   120  			pool, err := s.db.GetPoolByAddress(s.blockchain, parsedEvent.PoolId)
   121  
   122  			if len(pool.Assetvolumes) < 2 {
   123  				s.logger.WithField("poolAddress", pool.Address).Error("Pool has fewer than 2 asset volumes")
   124  				continue
   125  			}
   126  			if err != nil {
   127  				continue
   128  			}
   129  
   130  			diaTrade := s.handleTrade(pool, parsedEvent, time.Now())
   131  
   132  			s.logger.WithFields(logrus.Fields{
   133  				"Pair":   diaTrade.Pair,
   134  				"Price":  diaTrade.Price,
   135  				"Volume": diaTrade.Volume,
   136  			}).Info("Trade processed")
   137  
   138  			s.chanTrades <- diaTrade
   139  		}
   140  	}
   141  }
   142  
   143  type ParsedEvent struct {
   144  	InputAsset   string
   145  	OutputAsset  string
   146  	InputAmount  string
   147  	OutputAmount string
   148  	PoolId       string
   149  	ExtrinsicID  string
   150  }
   151  
   152  type TokenValue struct {
   153  	Token *string `json:"Token,omitempty"` // Token variant, e.g., "KSM"
   154  }
   155  type VTokenValue struct {
   156  	VToken string `json:"VToken,omitempty"` // Token variant, e.g., "KSM"
   157  }
   158  
   159  // TokenSymbol represents the token symbols as an enum
   160  type TokenSymbol int
   161  
   162  const (
   163  	ASG TokenSymbol = iota
   164  	BNC
   165  	KUSD
   166  	DOT
   167  	KSM
   168  	ETH
   169  	KAR
   170  	ZLK
   171  	PHA
   172  	RMRK
   173  	MOVR
   174  )
   175  
   176  // String returns the string representation of the TokenSymbol
   177  func (ts TokenSymbol) String() string {
   178  	return [...]string{"ASG", "BNC", "KUSD", "DOT", "KSM", "ETH", "KAR", "ZLK", "PHA", "RMRK", "MOVR"}[ts]
   179  }
   180  
   181  func parseFields(event *parser.Event) ParsedEvent {
   182  	var parsedEvent ParsedEvent
   183  	for _, v := range event.Fields {
   184  		switch v.Name {
   185  		case "bifrost_primitives.currency.CurrencyId.input_asset":
   186  			if result, ok := v.Value.(registry.VariantDecoderResult); ok {
   187  				if decodedFields, ok := result.Value.(registry.DecodedFields); ok {
   188  					if len(decodedFields) > 0 {
   189  						parsedEvent.InputAsset = strings.ToLower(result.FieldName) + "-" + strings.ToLower(fmt.Sprint(decodedFields[0].Value))
   190  					}
   191  				}
   192  			}
   193  		case "bifrost_primitives.currency.CurrencyId.output_asset":
   194  			if result, ok := v.Value.(registry.VariantDecoderResult); ok {
   195  				if decodedFields, ok := result.Value.(registry.DecodedFields); ok {
   196  					if len(decodedFields) > 0 {
   197  						if len(decodedFields) > 0 {
   198  							parsedEvent.OutputAsset = strings.ToLower(result.FieldName) + "-" + strings.ToLower(fmt.Sprint(decodedFields[0].Value))
   199  						}
   200  					}
   201  				}
   202  			}
   203  
   204  		case "input_amount":
   205  			parsedEvent.InputAmount = fmt.Sprint(v.Value)
   206  		case "output_amount":
   207  			parsedEvent.OutputAmount = fmt.Sprint(v.Value)
   208  		case "pool_id":
   209  			parsedEvent.PoolId = fmt.Sprint(v.Value)
   210  		}
   211  	}
   212  	return parsedEvent
   213  }
   214  
   215  // handleTrade processes a swap event and converts it into a dia.Trade object.
   216  //
   217  // This function takes a pool and a corresponding stable swap event, calculates
   218  // the trade volume and price based on the asset amounts in the event, and returns
   219  // a `dia.Trade` object representing the processed trade. The price is calculated
   220  // as the ratio of the input to output amounts, and the volume is set as the
   221  // negative of the input amount to indicate the amount being swapped.
   222  //
   223  // The `dia.Trade` object includes metadata such as the trade timestamp, the trading pair,
   224  // the pool address, and the exchange source.
   225  //
   226  // Parameters:
   227  //   - pool: A `dia.Pool` object representing the liquidity pool where the swap occurred.
   228  //   - event: A `ParsedEvent` containing the swap details such as asset amounts and event ID.
   229  //   - time: The timestamp for the trade event.
   230  //
   231  // Returns:
   232  //   - *dia.Trade: A pointer to the constructed `dia.Trade` object containing the trade details.
   233  
   234  func (s *BifrostScraper) handleTrade(pool dia.Pool, event ParsedEvent, time time.Time) *dia.Trade {
   235  	var volume, price float64
   236  	var baseToken, quoteToken dia.Asset
   237  	var decimalsIn, decimalsOut int64
   238  
   239  	if fmt.Sprint(event.InputAsset) == pool.Assetvolumes[0].Asset.Address {
   240  		baseToken = pool.Assetvolumes[0].Asset
   241  		quoteToken = pool.Assetvolumes[1].Asset
   242  	} else {
   243  		baseToken = pool.Assetvolumes[1].Asset
   244  		quoteToken = pool.Assetvolumes[0].Asset
   245  	}
   246  
   247  	decimalsIn = int64(baseToken.Decimals)
   248  	decimalsOut = int64(quoteToken.Decimals)
   249  	amountIn, _ := utils.StringToFloat64(event.InputAmount, decimalsIn)
   250  	amountOut, _ := utils.StringToFloat64(event.OutputAmount, decimalsOut)
   251  
   252  	volume = amountOut
   253  
   254  	price = amountIn / amountOut
   255  
   256  	symbolPair := fmt.Sprintf("%s-%s", quoteToken.Symbol, baseToken.Symbol)
   257  
   258  	return &dia.Trade{
   259  		Time:           time,
   260  		Symbol:         quoteToken.Symbol,
   261  		Pair:           symbolPair,
   262  		ForeignTradeID: event.ExtrinsicID,
   263  		Source:         s.exchangeName,
   264  		Price:          price,
   265  		Volume:         volume,
   266  		VerifiedPair:   true,
   267  		QuoteToken:     quoteToken,
   268  		BaseToken:      baseToken,
   269  		PoolAddress:    pool.Address,
   270  	}
   271  }
   272  
   273  // FetchAvailablePairs returns a list with all trading pairs available on
   274  // the exchange associated to the APIScraper. The format is such that it can
   275  // be used by the corr. pairScraper in order to fetch trades.
   276  func (s *BifrostScraper) FetchAvailablePairs() ([]dia.ExchangePair, error) {
   277  	return []dia.ExchangePair{}, nil
   278  }
   279  
   280  func (s *BifrostScraper) FillSymbolData(symbol string) (dia.Asset, error) {
   281  	return dia.Asset{Symbol: symbol}, nil
   282  }
   283  
   284  func (s *BifrostScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   285  	return pair, nil
   286  }
   287  
   288  // ScrapePair initializes and returns a `BifrostPairScraper`.
   289  //
   290  // Parameters:
   291  //   - pair: The `dia.ExchangePair` representing the trading pair (e.g: `BifrostPairScraper`) to be scraped.
   292  //
   293  // Returns:
   294  //   - PairScraper: A `PairScraper` (specifically a `BifrostPairScraper`) for the given exchange pair.
   295  //   - error: An error if the scraper is closed or if an error has occurred, otherwise `nil`.
   296  func (s *BifrostScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   297  	s.errorLock.RLock()
   298  	defer s.errorLock.RUnlock()
   299  	if s.error != nil {
   300  		return nil, s.error
   301  	}
   302  	if s.closed {
   303  		return nil, errors.New("BifrostScraper: Call ScrapePair on closed scraper")
   304  	}
   305  	ps := &BifrostPairScraper{
   306  		parent:     s,
   307  		pair:       pair,
   308  		lastRecord: 0,
   309  	}
   310  
   311  	s.pairScrapers[pair.Symbol] = ps
   312  
   313  	return ps, nil
   314  }
   315  
   316  // cleanup handles the shutdown procedure.
   317  func (s *BifrostScraper) cleanup(err error) {
   318  	s.errorLock.Lock()
   319  	defer s.errorLock.Unlock()
   320  
   321  	s.ticker.Stop()
   322  
   323  	if err != nil {
   324  		s.error = err
   325  	}
   326  	s.closed = true
   327  	close(s.shutdownDone)
   328  }
   329  
   330  // Close gracefully shuts down the BifrostScraper.
   331  func (s *BifrostScraper) Close() error {
   332  	if s.closed {
   333  		return errors.New("BifrostScraper: Already closed")
   334  	}
   335  	close(s.shutdown)
   336  	<-s.shutdownDone
   337  	s.errorLock.RLock()
   338  	defer s.errorLock.RUnlock()
   339  	return s.error
   340  }
   341  
   342  // Channel returns the channel used to receive trades/pricing information.
   343  func (s *BifrostScraper) Channel() chan *dia.Trade {
   344  	return s.chanTrades
   345  }
   346  
   347  type BifrostPairScraper struct {
   348  	parent     *BifrostScraper
   349  	pair       dia.ExchangePair
   350  	closed     bool
   351  	lastRecord int64
   352  }
   353  
   354  func (ps *BifrostPairScraper) Pair() dia.ExchangePair {
   355  	return ps.pair
   356  }
   357  
   358  func (ps *BifrostPairScraper) Close() error {
   359  	ps.closed = true
   360  	return nil
   361  }
   362  
   363  // Error returns an error when the channel Channel() is closed
   364  // and nil otherwise
   365  func (ps *BifrostPairScraper) Error() error {
   366  	s := ps.parent
   367  	s.errorLock.RLock()
   368  	defer s.errorLock.RUnlock()
   369  	return s.error
   370  }