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 }