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 }