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

     1  package liquidityscrapers
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"errors"
     7  	"math/big"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/diadata-org/diadata/pkg/dia"
    12  	"github.com/diadata-org/diadata/pkg/dia/helpers/bitflowhelper"
    13  	"github.com/diadata-org/diadata/pkg/dia/helpers/stackshelper"
    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 BitflowLiquidityScraper struct {
    20  	logger             *logrus.Entry
    21  	api                *stackshelper.StacksClient
    22  	poolChannel        chan dia.Pool
    23  	doneChannel        chan bool
    24  	blockchain         string
    25  	exchangeName       string
    26  	relDB              *models.RelDB
    27  	datastore          *models.DB
    28  	handlerType        string
    29  	targetSwapContract string
    30  }
    31  
    32  // NewBitflowLiquidityScraper returns a new BitflowLiquidityScraper initialized with default values.
    33  // The instance is asynchronously scraping as soon as it is created.
    34  // ENV values:
    35  //
    36  //	 	BITFLOW_SLEEP_TIMEOUT - (optional, millisecond), make timeout between API calls, default "stackshelper.DefaultSleepBetweenCalls" value
    37  //		BITFLOW_TARGET_SWAP_CONTRACT - (optional, string), useful for debug, default = ""
    38  //		BITFLOW_HIRO_API_KEY - (optional, string), Hiro Stacks API key, improves scraping performance, default = ""
    39  //		BITFLOW_DEBUG - (optional, bool), make stdout output with bitflow client http call, default = false
    40  func NewBitflowLiquidityScraper(exchange dia.Exchange, relDB *models.RelDB, datastore *models.DB) *BitflowLiquidityScraper {
    41  	envPrefix := strings.ToUpper(exchange.Name)
    42  
    43  	sleepBetweenCalls := utils.GetenvInt(envPrefix+"_SLEEP_TIMEOUT", stackshelper.DefaultSleepBetweenCalls)
    44  	targetSwapContract := utils.Getenv(envPrefix+"_TARGET_SWAP_CONTRACT", "")
    45  	hiroAPIKey := utils.Getenv(envPrefix+"_HIRO_API_KEY", "")
    46  	isDebug := utils.GetenvBool(envPrefix+"_DEBUG", false)
    47  
    48  	stacksClient := stackshelper.NewStacksClient(
    49  		log.WithContext(context.Background()).WithField("context", "StacksClient"),
    50  		utils.GetTimeDurationFromIntAsMilliseconds(sleepBetweenCalls),
    51  		hiroAPIKey,
    52  		isDebug,
    53  	)
    54  
    55  	s := &BitflowLiquidityScraper{
    56  		poolChannel:        make(chan dia.Pool),
    57  		doneChannel:        make(chan bool),
    58  		exchangeName:       exchange.Name,
    59  		blockchain:         exchange.BlockChain.Name,
    60  		api:                stacksClient,
    61  		relDB:              relDB,
    62  		datastore:          datastore,
    63  		handlerType:        "liquidity",
    64  		targetSwapContract: targetSwapContract,
    65  	}
    66  
    67  	s.logger = logrus.
    68  		New().
    69  		WithContext(context.Background()).
    70  		WithField("handlerType", s.handlerType).
    71  		WithField("context", "BitflowLiquidityScraper")
    72  
    73  	go s.fetchPools()
    74  
    75  	return s
    76  }
    77  
    78  func (s *BitflowLiquidityScraper) fetchPools() {
    79  	swapContracts := bitflowhelper.SwapContracts[:]
    80  
    81  	if s.targetSwapContract != "" {
    82  		address := strings.Split(s.targetSwapContract, ".")
    83  
    84  		contractType := 0
    85  		if strings.HasPrefix(address[1], "xyk") {
    86  			contractType = 1
    87  		}
    88  
    89  		swapContracts = []bitflowhelper.SwapContract{
    90  			{
    91  				ContractType:     contractType,
    92  				DeployerAddress:  address[0],
    93  				ContractRegistry: address[1],
    94  			},
    95  		}
    96  	}
    97  
    98  	for _, contract := range swapContracts {
    99  		s.logger.Infof("Fetching pools of %s", contract.ContractRegistry)
   100  		contractID := contract.DeployerAddress + "." + contract.ContractRegistry
   101  
   102  		switch contract.ContractType {
   103  		case 0:
   104  			total := stackshelper.MaxPageLimit
   105  
   106  			for offset := 0; offset < total; offset += stackshelper.MaxPageLimit {
   107  				resp, err := s.api.GetAddressTransactions(contractID, stackshelper.MaxPageLimit, offset)
   108  				if err != nil {
   109  					s.logger.WithError(err).Error("failed to GetAddressTransactions")
   110  					continue
   111  				}
   112  
   113  				total = resp.Total
   114  				filtered := s.fetchPoolTransactions(resp.Results, contract.ContractType)
   115  				for _, tx := range filtered {
   116  					pool, err := s.parseTx(tx)
   117  					if err != nil {
   118  						continue
   119  					}
   120  					// s.logger.WithField("pool", pool).Info("sending pool to poolChannel")
   121  					s.poolChannel <- pool
   122  				}
   123  			}
   124  		case 1:
   125  			lastID, err := s.api.GetDataVar(contract.DeployerAddress, contract.ContractRegistry, "last-pool-id")
   126  			if err != nil {
   127  				s.logger.WithError(err).Error("failed to get last pool ID")
   128  				continue
   129  			}
   130  
   131  			total, err := stackshelper.DeserializeCVUint(lastID)
   132  			if err != nil {
   133  				s.logger.WithError(err).Error("failed to deserialize CV uint")
   134  				continue
   135  			}
   136  
   137  			xykContract := contract.DeployerAddress + "." + contract.ContractRegistry
   138  			one := big.NewInt(1)
   139  
   140  			for id := new(big.Int).Set(one); id.Cmp(total) <= 0; id.Add(id, one) {
   141  				poolContract, err := s.getXykPoolContractAddress(xykContract, id)
   142  				if err != nil {
   143  					s.logger.Error("failed to get xyk pool contract address")
   144  					continue
   145  				}
   146  
   147  				pool, err := s.fetchXykPool(poolContract)
   148  				if err != nil {
   149  					continue
   150  				}
   151  
   152  				s.logger.WithField("pool", pool).Info("sending pool to poolChannel")
   153  				s.poolChannel <- pool
   154  			}
   155  
   156  		}
   157  	}
   158  
   159  	s.doneChannel <- true
   160  }
   161  
   162  func (s *BitflowLiquidityScraper) parseTx(tx stackshelper.Transaction) (dia.Pool, error) {
   163  	args := make(map[string]stackshelper.FunctionArg, len(tx.ContractCall.FunctionArgs))
   164  	for _, item := range tx.ContractCall.FunctionArgs {
   165  		args[item.Name] = item
   166  	}
   167  
   168  	tokens := [...]string{"", args["y-token"].Repr[1:]}
   169  	if xToken, ok := args["x-token"]; ok {
   170  		tokens[0] = xToken.Repr[1:]
   171  	}
   172  
   173  	dbAssets, err := s.fetchAssets(tokens[:])
   174  	if err != nil {
   175  		return dia.Pool{}, err
   176  	}
   177  
   178  	balances, err := s.fetchStableswapPoolBalances(
   179  		tx.ContractCall.ContractID,
   180  		args["x-token"].Hex,
   181  		args["y-token"].Hex,
   182  		args["lp-token"].Hex,
   183  	)
   184  
   185  	if err != nil {
   186  		return dia.Pool{}, errors.New("failed to fetch bitflow pool balances")
   187  	}
   188  
   189  	assetVolumes := make([]dia.AssetVolume, len(balances))
   190  
   191  	for i, balance := range balances {
   192  		assetVolumes[i] = dia.AssetVolume{
   193  			Index:  uint8(i),
   194  			Asset:  dbAssets[i],
   195  			Volume: balance,
   196  		}
   197  	}
   198  
   199  	pool := dia.Pool{
   200  		Exchange:     dia.Exchange{Name: s.exchangeName},
   201  		Blockchain:   dia.BlockChain{Name: s.blockchain},
   202  		Time:         time.Now(),
   203  		Assetvolumes: assetVolumes,
   204  		Address:      args["lp-token"].Repr[1:],
   205  	}
   206  
   207  	if pool.SufficientNativeBalance(GLOBAL_NATIVE_LIQUIDITY_THRESHOLD) {
   208  		s.datastore.GetPoolLiquiditiesUSD(&pool, priceCache)
   209  	}
   210  	return pool, nil
   211  }
   212  
   213  func (s *BitflowLiquidityScraper) fetchStableswapPoolBalances(contract, xToken, yToken, lpToken string) ([]float64, error) {
   214  	yTokenBytes, _ := hex.DecodeString(yToken[2:])
   215  	lpTokenBytes, _ := hex.DecodeString(lpToken[2:])
   216  	pairKey := stackshelper.CVTuple{"lp-token": lpTokenBytes, "y-token": yTokenBytes}
   217  
   218  	if xToken != "" {
   219  		xTokenBytes, _ := hex.DecodeString(xToken[2:])
   220  		pairKey["x-token"] = xTokenBytes
   221  	}
   222  
   223  	encodedKey := "0x" + hex.EncodeToString(stackshelper.SerializeCVTuple(pairKey))
   224  	entry, err := s.api.GetDataMapEntry(contract, "PairsDataMap", encodedKey)
   225  	if err != nil {
   226  		s.logger.WithError(err).Error("failed to GetDataMapEntry")
   227  		return nil, err
   228  	}
   229  
   230  	tuple, err := stackshelper.DeserializeCVTuple(entry)
   231  	if err != nil {
   232  		s.logger.WithError(err).Error("failed to deserialize cv tuple")
   233  		return nil, err
   234  	}
   235  
   236  	balanceX, _ := stackshelper.DeserializeCVUint(tuple["balance-x"])
   237  	decimalsX, _ := stackshelper.DeserializeCVUint(tuple["x-decimals"])
   238  
   239  	balanceY, _ := stackshelper.DeserializeCVUint(tuple["balance-y"])
   240  	decimalsY, _ := stackshelper.DeserializeCVUint(tuple["y-decimals"])
   241  
   242  	balances := make([]float64, 2)
   243  	balances[0], _ = utils.StringToFloat64(balanceX.String(), decimalsX.Int64())
   244  	balances[1], _ = utils.StringToFloat64(balanceY.String(), decimalsY.Int64())
   245  
   246  	return balances, nil
   247  }
   248  
   249  func (s *BitflowLiquidityScraper) getXykPoolContractAddress(contractID string, poolID *big.Int) (string, error) {
   250  	encodedPoolID := hex.EncodeToString(stackshelper.SerializeCVUint(poolID))
   251  
   252  	result, err := s.api.GetDataMapEntry(contractID, "pools", encodedPoolID)
   253  	if err != nil {
   254  		log.WithError(err).Error("failed to get pool by ID")
   255  		return "", err
   256  	}
   257  
   258  	tuple, err := stackshelper.DeserializeCVTuple(result)
   259  	if err != nil {
   260  		log.WithError(err).Error("failed to deserialize cv tuple")
   261  		return "", err
   262  	}
   263  
   264  	return stackshelper.DeserializeCVPrincipal(tuple["pool-contract"])
   265  }
   266  
   267  func (s *BitflowLiquidityScraper) fetchXykPool(poolContract string) (dia.Pool, error) {
   268  	address := strings.Split(poolContract, ".")
   269  	args := stackshelper.ContractCallArgs{Sender: address[0]}
   270  
   271  	result, err := s.api.CallContractFunction(address[0], address[1], "get-pool", args)
   272  	if err != nil {
   273  		return dia.Pool{}, err
   274  	}
   275  
   276  	data, ok := stackshelper.DeserializeCVResponse(result)
   277  	if !ok {
   278  		return dia.Pool{}, errors.New("failed to deserialize CV response")
   279  	}
   280  	poolInfo, err := stackshelper.DeserializeCVTuple(data)
   281  	if err != nil {
   282  		return dia.Pool{}, err
   283  	}
   284  
   285  	xToken, _ := stackshelper.DeserializeCVPrincipal(poolInfo["x-token"])
   286  	yToken, _ := stackshelper.DeserializeCVPrincipal(poolInfo["y-token"])
   287  
   288  	xDecimals, err := s.fetchTokenDecimals(xToken)
   289  	if err != nil {
   290  		return dia.Pool{}, err
   291  	}
   292  	yDecimals, err := s.fetchTokenDecimals(yToken)
   293  	if err != nil {
   294  		return dia.Pool{}, err
   295  	}
   296  
   297  	dbAssets, err := s.fetchAssets([]string{xToken, yToken})
   298  	if err != nil {
   299  		return dia.Pool{}, err
   300  	}
   301  
   302  	xBalance, _ := stackshelper.DeserializeCVUint(poolInfo["x-balance"])
   303  	yBalance, _ := stackshelper.DeserializeCVUint(poolInfo["y-balance"])
   304  
   305  	balances := make([]float64, 2)
   306  	balances[0], _ = utils.StringToFloat64(xBalance.String(), xDecimals)
   307  	balances[1], _ = utils.StringToFloat64(yBalance.String(), yDecimals)
   308  
   309  	assetVolumes := make([]dia.AssetVolume, len(balances))
   310  	for i, balance := range balances {
   311  		assetVolumes[i] = dia.AssetVolume{
   312  			Index:  uint8(i),
   313  			Asset:  dbAssets[i],
   314  			Volume: balance,
   315  		}
   316  	}
   317  
   318  	pool := dia.Pool{
   319  		Exchange:     dia.Exchange{Name: s.exchangeName},
   320  		Blockchain:   dia.BlockChain{Name: s.blockchain},
   321  		Time:         time.Now(),
   322  		Assetvolumes: assetVolumes,
   323  		Address:      poolContract,
   324  	}
   325  
   326  	if pool.SufficientNativeBalance(GLOBAL_NATIVE_LIQUIDITY_THRESHOLD) {
   327  		s.datastore.GetPoolLiquiditiesUSD(&pool, priceCache)
   328  	}
   329  	return pool, nil
   330  }
   331  
   332  func (s *BitflowLiquidityScraper) fetchTokenDecimals(tokenContract string) (int64, error) {
   333  	address := strings.Split(tokenContract, ".")
   334  	args := stackshelper.ContractCallArgs{Sender: address[0]}
   335  
   336  	result, err := s.api.CallContractFunction(address[0], address[1], "get-decimals", args)
   337  	if err != nil {
   338  		return 0, err
   339  	}
   340  
   341  	data, ok := stackshelper.DeserializeCVResponse(result)
   342  	if !ok {
   343  		return 0, errors.New("failed to deserialize CV response")
   344  	}
   345  
   346  	decimals, err := stackshelper.DeserializeCVUint(data)
   347  	if err != nil {
   348  		return 0, err
   349  	}
   350  	return decimals.Int64(), nil
   351  }
   352  
   353  func (s *BitflowLiquidityScraper) fetchAssets(tokens []string) ([]dia.Asset, error) {
   354  	dbAssets := make([]dia.Asset, 0, len(tokens))
   355  
   356  	for _, address := range tokens {
   357  		// Workaround to fetch the native STX token data from DB
   358  		key := address
   359  		if address == "" {
   360  			key = "0x0000000000000000000000000000000000000000"
   361  		}
   362  
   363  		assset, err := s.relDB.GetAsset(key, s.blockchain)
   364  		if err != nil {
   365  			s.logger.WithError(err).Errorf("failed to GetAsset with key: %s", key)
   366  			continue
   367  		}
   368  		dbAssets = append(dbAssets, assset)
   369  	}
   370  
   371  	if len(dbAssets) != len(tokens) {
   372  		return nil, errors.New("found less than 2 assets for the pool pair")
   373  	}
   374  	return dbAssets, nil
   375  }
   376  
   377  func (s *BitflowLiquidityScraper) fetchPoolTransactions(txs []stackshelper.AddressTransaction, poolType int) []stackshelper.Transaction {
   378  	poolTxs := make([]stackshelper.Transaction, 0)
   379  
   380  	for _, item := range txs {
   381  		var isCreatePairCall bool
   382  		if poolType == 0 {
   383  			isCreatePairCall = item.Tx.TxType == "contract_call" &&
   384  				item.Tx.ContractCall.FunctionName == "create-pair"
   385  		} else if poolType == 1 {
   386  			isCreatePairCall = item.Tx.TxType == "contract_call" &&
   387  				item.Tx.ContractCall.FunctionName == "create-pool"
   388  		}
   389  
   390  		if isCreatePairCall && item.Tx.TxStatus == "success" {
   391  			// This is a temporary workaround introduced due to a bug in hiro stacks API.
   392  			// Results returned from /addresses/{address}/transactions route have empty
   393  			// `name` field in `contract_call.function_args` list.
   394  			// TODO: remove this as soon as the issue is fixed.
   395  			normalizedTx, err := s.api.GetTransactionAt(item.Tx.TxID)
   396  			if err != nil {
   397  				s.logger.WithError(err).Error("failed to GetTransactionAt")
   398  				continue
   399  			}
   400  			poolTxs = append(poolTxs, normalizedTx)
   401  		}
   402  	}
   403  
   404  	return poolTxs
   405  }
   406  
   407  func (s *BitflowLiquidityScraper) Pool() chan dia.Pool {
   408  	return s.poolChannel
   409  }
   410  
   411  func (s *BitflowLiquidityScraper) Done() chan bool {
   412  	return s.doneChannel
   413  }