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

     1  package scrapers
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"errors"
     7  	"fmt"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/diadata-org/diadata/pkg/dia"
    13  	bitflow "github.com/diadata-org/diadata/pkg/dia/helpers/bitflowhelper"
    14  	stacks "github.com/diadata-org/diadata/pkg/dia/helpers/stackshelper"
    15  	models "github.com/diadata-org/diadata/pkg/model"
    16  	"github.com/diadata-org/diadata/pkg/utils"
    17  	"github.com/sirupsen/logrus"
    18  	"github.com/zekroTJA/timedmap"
    19  )
    20  
    21  type bitflowSwapEvent struct {
    22  	txID        string
    23  	action      string
    24  	amountIn    string
    25  	amountOut   string
    26  	poolAddress string
    27  	blockTime   int
    28  }
    29  
    30  type BitflowScraper struct {
    31  	logger             *logrus.Entry
    32  	pairScrapers       map[string]*BitflowPairScraper // pc.ExchangePair -> pairScraperSet
    33  	swapContracts      map[string]nothing
    34  	shutdown           chan nothing
    35  	shutdownDone       chan nothing
    36  	errorLock          sync.RWMutex
    37  	error              error
    38  	closed             bool
    39  	ticker             *time.Ticker
    40  	exchangeName       string
    41  	blockchain         string
    42  	chanTrades         chan *dia.Trade
    43  	api                *stacks.StacksClient
    44  	db                 *models.RelDB
    45  	currentHeight      int
    46  	initialBlockHeight int
    47  }
    48  
    49  var (
    50  	singleDirectionPoolsBitflow *[]string
    51  )
    52  
    53  // NewBitflowScraper returns a new BitflowScraper initialized with default values.
    54  // The instance is asynchronously scraping as soon as it is created.
    55  // ENV values:
    56  //
    57  //	 	BITFLOW_SLEEP_TIMEOUT - (optional, millisecond), make timeout between API calls, default "stackshelper.DefaultSleepBetweenCalls" value
    58  //		BITFLOW_REFRESH_DELAY - (optional, millisecond) refresh data after each poll, default "stackshelper.DefaultRefreshDelay" value
    59  //		BITFLOW_HIRO_API_KEY - (optional, string), Hiro Stacks API key, improves scraping performance, default = ""
    60  //		BITFLOW_INITIAL_BLOCK_HEIGHT (optional, int), useful for debug, default = 0
    61  //		BITFLOW_DEBUG - (optional, bool), make stdout output with alephium client http call, default = false
    62  func NewBitflowScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BitflowScraper {
    63  	envPrefix := strings.ToUpper(exchange.Name)
    64  
    65  	sleepBetweenCalls := utils.GetTimeDurationFromIntAsMilliseconds(
    66  		utils.GetenvInt(
    67  			envPrefix+"_SLEEP_TIMEOUT",
    68  			stacks.DefaultSleepBetweenCalls,
    69  		),
    70  	)
    71  	refreshDelay := utils.GetTimeDurationFromIntAsMilliseconds(
    72  		utils.GetenvInt(envPrefix+"_REFRESH_DELAY", stacks.DefaultRefreshDelay),
    73  	)
    74  	hiroAPIKey := utils.Getenv(envPrefix+"_HIRO_API_KEY", "")
    75  	initialBlockHeight := utils.GetenvInt(envPrefix+"_INITIAL_BLOCK_HEIGHT", 0)
    76  	isDebug := utils.GetenvBool(envPrefix+"_DEBUG", false)
    77  
    78  	stacksClient := stacks.NewStacksClient(
    79  		log.WithContext(context.Background()).WithField("context", "StacksClient"),
    80  		sleepBetweenCalls,
    81  		hiroAPIKey,
    82  		isDebug,
    83  	)
    84  
    85  	swapContracts := make(map[string]nothing, len(bitflow.SwapContracts))
    86  
    87  	for _, contract := range bitflow.SwapContracts {
    88  		contractId := fmt.Sprintf("%s.%s", contract.DeployerAddress, contract.ContractRegistry)
    89  		swapContracts[contractId] = nothing{}
    90  	}
    91  
    92  	s := &BitflowScraper{
    93  		shutdown:           make(chan nothing),
    94  		shutdownDone:       make(chan nothing),
    95  		pairScrapers:       make(map[string]*BitflowPairScraper),
    96  		swapContracts:      swapContracts,
    97  		ticker:             time.NewTicker(refreshDelay),
    98  		chanTrades:         make(chan *dia.Trade),
    99  		api:                stacksClient,
   100  		db:                 relDB,
   101  		exchangeName:       exchange.Name,
   102  		blockchain:         exchange.BlockChain.Name,
   103  		initialBlockHeight: initialBlockHeight,
   104  	}
   105  
   106  	s.logger = logrus.
   107  		New().
   108  		WithContext(context.Background()).
   109  		WithField("context", "BitflowDEXScraper")
   110  
   111  	if scrape {
   112  		go s.mainLoop()
   113  	}
   114  	return s
   115  }
   116  
   117  func (s *BitflowScraper) mainLoop() {
   118  
   119  	var err error
   120  	singleDirectionPoolsBitflow, err = getReverseTokensFromConfig("bitflow/singleDirectionPools/" + s.exchangeName + "SingleDirectionPools")
   121  	if err != nil {
   122  		log.Error("error getting fullPools for which pairs should be reversed: ", err)
   123  	}
   124  	log.Info("singleDiration: ", *singleDirectionPoolsBitflow)
   125  
   126  	if s.initialBlockHeight <= 0 {
   127  		latestBlock, err := s.api.GetLatestBlock()
   128  		if err != nil {
   129  			s.logger.WithError(err).Error("failed to GetLatestBlock")
   130  			s.cleanup(err)
   131  			return
   132  		}
   133  		s.currentHeight = latestBlock.Height
   134  	} else {
   135  		s.currentHeight = s.initialBlockHeight
   136  	}
   137  
   138  	for {
   139  		select {
   140  		case <-s.ticker.C:
   141  			err := s.Update()
   142  			if err != nil {
   143  				s.logger.Error(err)
   144  			}
   145  		case <-s.shutdown:
   146  			s.logger.Println("shutting down")
   147  			s.cleanup(nil)
   148  			return
   149  		}
   150  	}
   151  }
   152  
   153  func (s *BitflowScraper) Update() error {
   154  	txs, err := s.api.GetAllBlockTransactions(s.currentHeight)
   155  	if err != nil {
   156  		s.logger.WithError(err).Error("failed to GetBlockTransactions")
   157  		return err
   158  	}
   159  
   160  	if len(txs) == 0 {
   161  		return nil
   162  	}
   163  	s.currentHeight += 1
   164  
   165  	swapEvents, err := s.fetchSwapEvents(txs)
   166  	if err != nil {
   167  		s.logger.WithError(err).Error("failed to fetchSwapEvents")
   168  		return err
   169  	}
   170  
   171  	if len(swapEvents) == 0 {
   172  		return nil
   173  	}
   174  
   175  	pools, err := s.getPools()
   176  	if err != nil {
   177  		s.logger.WithError(err).Error("failed to GetAllPoolsExchange")
   178  		return err
   179  	}
   180  
   181  	for _, pool := range pools {
   182  		tmFalseDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   183  		tmDuplicateTrades := timedmap.New(duplicateTradesScanFrequency)
   184  		if len(pool.Assetvolumes) != 2 {
   185  			s.logger.WithField("poolAddress", pool.Address).Error("pool is missing required asset volumes")
   186  			continue
   187  		}
   188  
   189  		for _, e := range swapEvents {
   190  			if e.poolAddress != pool.Address {
   191  				continue
   192  			}
   193  
   194  			diaTrade := s.handleTrade(&pool, &e)
   195  			log.Infof("got trade at height %v: %v -- %s -- %v --%v -- %s", s.currentHeight-1, diaTrade.Time, diaTrade.Pair, diaTrade.Price, diaTrade.Volume, diaTrade.ForeignTradeID)
   196  			discardTrade := diaTrade.IdentifyDuplicateFull(tmFalseDuplicateTrades, duplicateTradesMemory)
   197  			if discardTrade {
   198  				log.Warn("Identical trade already scraped: ", diaTrade)
   199  				continue
   200  			} else {
   201  				diaTrade.IdentifyDuplicateTagset(tmDuplicateTrades, duplicateTradesMemory)
   202  				s.chanTrades <- diaTrade
   203  				if !utils.Contains(singleDirectionPoolsBitflow, pool.Address) {
   204  					tSwapped, err := dia.SwapTrade(*diaTrade)
   205  					if err == nil {
   206  						if tSwapped.Price > 0 {
   207  							log.Infof("got trade at height %v: %v -- %s -- %v --%v -- %s", s.currentHeight-1, tSwapped.Time, tSwapped.Pair, tSwapped.Price, tSwapped.Volume, tSwapped.ForeignTradeID)
   208  							s.chanTrades <- &tSwapped
   209  						}
   210  					}
   211  				}
   212  			}
   213  		}
   214  	}
   215  
   216  	return nil
   217  }
   218  
   219  func (s *BitflowScraper) getPools() ([]dia.Pool, error) {
   220  	return s.db.GetAllPoolsExchange(s.exchangeName, 0)
   221  }
   222  
   223  func (s *BitflowScraper) fetchSwapEvents(transactions []stacks.Transaction) ([]bitflowSwapEvent, error) {
   224  	swapEvents := make([]bitflowSwapEvent, 0)
   225  
   226  	for _, tx := range transactions {
   227  		if tx.TxStatus != "success" || tx.TxType != "contract_call" {
   228  			continue
   229  		}
   230  
   231  		// This is a temporary workaround introduced due to a bug in hiro stacks API.
   232  		// Results returned from /blocks/{block_height}/transactions route have empty
   233  		// `name` field in `contract_call.function_args` list.
   234  		// TODO: remove this as soon as the issue is fixed.
   235  		normalizedTx, err := s.api.GetTransactionAt(tx.TxID)
   236  		if err != nil {
   237  			return nil, err
   238  		}
   239  
   240  		_, contractFound := s.swapContracts[tx.ContractCall.ContractID]
   241  		isStableSwapTransaction := contractFound &&
   242  			strings.Contains(tx.ContractCall.ContractID, "stableswap") &&
   243  			strings.HasPrefix(tx.ContractCall.FunctionName, "swap")
   244  
   245  		if isStableSwapTransaction {
   246  			event := bitflowSwapEvent{
   247  				txID:      tx.TxID,
   248  				action:    tx.ContractCall.FunctionName,
   249  				amountOut: normalizedTx.TxResult.Repr[5 : len(normalizedTx.TxResult.Repr)-1],
   250  				blockTime: tx.BlockTime,
   251  			}
   252  
   253  			for _, arg := range normalizedTx.ContractCall.FunctionArgs {
   254  				value := arg.Repr[1:]
   255  				switch arg.Name {
   256  				case "x-amount":
   257  					event.amountIn = value
   258  				case "y-amount":
   259  					event.amountIn = value
   260  				case "lp-token":
   261  					event.poolAddress = value
   262  				}
   263  			}
   264  
   265  			swapEvents = append(swapEvents, event)
   266  		} else {
   267  			for _, e := range normalizedTx.Events {
   268  				log := &e.ContractLog
   269  
   270  				isBitflowSwap := e.Type == "smart_contract_log" &&
   271  					log.Topic == "print" &&
   272  					(strings.HasPrefix(log.ContractID, bitflow.StableSwapDeployer) || strings.HasPrefix(log.ContractID, bitflow.XykDeployer)) &&
   273  					(strings.Contains(log.Value.Repr, "swap-x-for-y") || strings.Contains(log.Value.Repr, "swap-y-for-x"))
   274  
   275  				if !isBitflowSwap {
   276  					continue
   277  				}
   278  
   279  				bytes, err := hex.DecodeString(e.ContractLog.Value.Hex[2:])
   280  				if err != nil {
   281  					s.logger.WithError(err).Error("failed to decode contract log")
   282  					return nil, err
   283  				}
   284  
   285  				event, err := s.decodeXykSwapEvent(tx.TxID, tx.BlockTime, bytes)
   286  				if err != nil {
   287  					return nil, err
   288  				}
   289  				swapEvents = append(swapEvents, event)
   290  			}
   291  		}
   292  	}
   293  
   294  	return swapEvents, nil
   295  }
   296  
   297  func (s *BitflowScraper) decodeXykSwapEvent(txID string, blockTime int, src []byte) (bitflowSwapEvent, error) {
   298  	empty := bitflowSwapEvent{}
   299  
   300  	tuple, err := stacks.DeserializeCVTuple(src)
   301  	if err != nil {
   302  		s.logger.WithError(err).Error("failed to deserialize cv tuple")
   303  		return empty, err
   304  	}
   305  
   306  	action, err := stacks.DeserializeCVString(tuple["action"])
   307  	if err != nil {
   308  		s.logger.WithError(err).Error("failed to deserialize event action")
   309  		return empty, err
   310  	}
   311  
   312  	data, err := stacks.DeserializeCVTuple(tuple["data"])
   313  	if err != nil {
   314  		s.logger.WithError(err).Error("failed to deserialize cv tuple")
   315  		return empty, err
   316  	}
   317  
   318  	var keyIn, keyOut string
   319  	if action == "swap-x-for-y" {
   320  		keyIn = "x-amount"
   321  		keyOut = "dy"
   322  	} else {
   323  		keyIn = "y-amount"
   324  		keyOut = "dx"
   325  	}
   326  
   327  	amountIn, err := stacks.DeserializeCVUint(data[keyIn])
   328  	if err != nil {
   329  		s.logger.WithError(err).Error("failed to deserialize input amount")
   330  		return empty, err
   331  	}
   332  
   333  	amountOut, err := stacks.DeserializeCVUint(data[keyOut])
   334  	if err != nil {
   335  		s.logger.WithError(err).Error("failed to deserialize output amount")
   336  		return empty, err
   337  	}
   338  
   339  	poolContract, err := stacks.DeserializeCVPrincipal(data["pool-contract"])
   340  	if err != nil {
   341  		s.logger.WithError(err).Error("failed to deserialize pool contract address")
   342  		return empty, err
   343  	}
   344  
   345  	event := bitflowSwapEvent{
   346  		txID:        txID,
   347  		action:      action,
   348  		amountIn:    amountIn.String(),
   349  		amountOut:   amountOut.String(),
   350  		poolAddress: poolContract,
   351  		blockTime:   blockTime,
   352  	}
   353  
   354  	return event, nil
   355  }
   356  
   357  func (s *BitflowScraper) handleTrade(pool *dia.Pool, event *bitflowSwapEvent) *dia.Trade {
   358  	var volume, price float64
   359  
   360  	decimals0 := int64(pool.Assetvolumes[0].Asset.Decimals)
   361  	decimals1 := int64(pool.Assetvolumes[1].Asset.Decimals)
   362  
   363  	if event.action == "swap-x-for-y" {
   364  		amount0In, _ := utils.StringToFloat64(event.amountIn, decimals0)
   365  		amount1Out, _ := utils.StringToFloat64(event.amountOut, decimals1)
   366  		volume = amount0In
   367  		price = amount1Out / amount0In
   368  	} else {
   369  		amount1In, _ := utils.StringToFloat64(event.amountIn, decimals1)
   370  		amount0Out, _ := utils.StringToFloat64(event.amountOut, decimals0)
   371  		volume = -amount0Out
   372  		price = amount1In / amount0Out
   373  	}
   374  
   375  	symbolPair := fmt.Sprintf("%s-%s", pool.Assetvolumes[0].Asset.Symbol, pool.Assetvolumes[1].Asset.Symbol)
   376  
   377  	return &dia.Trade{
   378  		Time:           time.Unix(int64(event.blockTime), 0),
   379  		Symbol:         pool.Assetvolumes[0].Asset.Symbol,
   380  		Pair:           symbolPair,
   381  		ForeignTradeID: event.txID,
   382  		Source:         s.exchangeName,
   383  		Price:          price,
   384  		Volume:         volume,
   385  		VerifiedPair:   true,
   386  		BaseToken:      pool.Assetvolumes[1].Asset,
   387  		QuoteToken:     pool.Assetvolumes[0].Asset,
   388  		PoolAddress:    pool.Address,
   389  	}
   390  }
   391  
   392  func (s *BitflowScraper) FetchAvailablePairs() ([]dia.ExchangePair, error) {
   393  	return []dia.ExchangePair{}, nil
   394  }
   395  
   396  func (s *BitflowScraper) FillSymbolData(symbol string) (dia.Asset, error) {
   397  	return dia.Asset{Symbol: symbol}, nil
   398  }
   399  
   400  func (s *BitflowScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   401  	return pair, nil
   402  }
   403  
   404  func (s *BitflowScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   405  	s.errorLock.RLock()
   406  	defer s.errorLock.RUnlock()
   407  	if s.error != nil {
   408  		return nil, s.error
   409  	}
   410  	if s.closed {
   411  		return nil, errors.New("BitflowScraper: Call ScrapePair on closed scraper")
   412  	}
   413  	ps := &BitflowPairScraper{
   414  		parent:     s,
   415  		pair:       pair,
   416  		lastRecord: 0,
   417  	}
   418  
   419  	s.pairScrapers[pair.Symbol] = ps
   420  
   421  	return ps, nil
   422  }
   423  
   424  func (s *BitflowScraper) cleanup(err error) {
   425  	s.errorLock.Lock()
   426  	defer s.errorLock.Unlock()
   427  
   428  	s.ticker.Stop()
   429  
   430  	if err != nil {
   431  		s.error = err
   432  	}
   433  	s.closed = true
   434  	close(s.shutdownDone)
   435  }
   436  
   437  // Close gracefully shuts down the BitflowScraper.
   438  func (s *BitflowScraper) Close() error {
   439  	if s.closed {
   440  		return errors.New("BitflowScraper: Already closed")
   441  	}
   442  	close(s.shutdown)
   443  	<-s.shutdownDone
   444  	s.errorLock.RLock()
   445  	defer s.errorLock.RUnlock()
   446  	return s.error
   447  }
   448  
   449  // Channel returns the channel used to receive trades/pricing information.
   450  func (s *BitflowScraper) Channel() chan *dia.Trade {
   451  	return s.chanTrades
   452  }
   453  
   454  type BitflowPairScraper struct {
   455  	parent     *BitflowScraper
   456  	pair       dia.ExchangePair
   457  	closed     bool
   458  	lastRecord int64
   459  }
   460  
   461  func (ps *BitflowPairScraper) Pair() dia.ExchangePair {
   462  	return ps.pair
   463  }
   464  
   465  func (ps *BitflowPairScraper) Close() error {
   466  	ps.closed = true
   467  	return nil
   468  }
   469  
   470  // Error returns an error when the channel Channel() is closed
   471  // and nil otherwise
   472  func (ps *BitflowPairScraper) Error() error {
   473  	s := ps.parent
   474  	s.errorLock.RLock()
   475  	defer s.errorLock.RUnlock()
   476  	return s.error
   477  }