github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/AyinScraper.go (about) 1 package scrapers 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 "sync" 9 "time" 10 11 "github.com/diadata-org/diadata/pkg/dia" 12 alephiumhelper "github.com/diadata-org/diadata/pkg/dia/helpers/alephium-helper" 13 models "github.com/diadata-org/diadata/pkg/model" 14 "github.com/diadata-org/diadata/pkg/utils" 15 "github.com/sirupsen/logrus" 16 ) 17 18 type AyinScraper struct { 19 logger *logrus.Entry 20 // signaling channels 21 shutdown chan nothing 22 shutdownDone chan nothing 23 // error handling; to read error or closed, first acquire read lock 24 // only cleanup method should hold write lock 25 errorLock sync.RWMutex 26 error error 27 closed bool 28 pairScrapers map[string]*AyinPairScraper // pc.ExchangePair -> pairScraperSet 29 api *alephiumhelper.AlephiumClient 30 ticker *time.Ticker 31 exchangeName string 32 blockchain string 33 currentHeight int 34 eventsLimit int 35 swapContractsLimit int 36 targetSwapContract string 37 chanTrades chan *dia.Trade 38 db *models.RelDB 39 refreshDelay time.Duration 40 sleepBetweenContractCalls time.Duration 41 reverseBasetokens []string 42 reverseQuotetokens []string 43 } 44 45 // NewAyinScraper returns a new AyinScraper initialized with default values. 46 // The instance is asynchronously scraping as soon as it is created. 47 // ENV values: 48 // 49 // AYIN_REFRESH_DELAY - (optional,millisecond) refresh data after each poll, default "alephiumhelper.DefaultRefreshDelay" value 50 // AYIN_SLEEP_TIMEOUT - (optional,millisecond), make timeout between API calls, default "alephiumhelper.DefaultSleepBetweenContractCalls" value 51 // AYIN_SWAP_CONTRACTS_LIMIT - (optional, int), limit to get swap contact addresses, default "alephiumhelper.DefaultSwapContractsLimit" value 52 // AYIN_TARGET_SWAP_CONTRACT - (optional, string), useful for debug, default = "" 53 // AYIN_DEBUG - (optional, bool), make stdout output with alephium client http call, default = false 54 func NewAyinScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *AyinScraper { 55 ayinRefreshDelay := utils.GetTimeDurationFromIntAsMilliseconds( 56 utils.GetenvInt(strings.ToUpper(exchange.Name)+"_REFRESH_DELAY", alephiumhelper.DefaultRefreshDelay), 57 ) 58 sleepBetweenContractCalls := utils.GetTimeDurationFromIntAsMilliseconds( 59 utils.GetenvInt(strings.ToUpper(exchange.Name)+"_SLEEP_TIMEOUT", alephiumhelper.DefaultSleepBetweenContractCalls), 60 ) 61 isDebug := utils.GetenvBool(strings.ToUpper(exchange.Name)+"_DEBUG", false) 62 eventsLimit := utils.GetenvInt(strings.ToUpper(exchange.Name)+"_REFRESH_DELAY", alephiumhelper.DefaultEventsLimit) 63 swapContractsLimit := utils.GetenvInt(strings.ToUpper(exchange.Name)+"_SWAP_CONTRACTS_LIMIT", alephiumhelper.DefaultSwapContractsLimit) 64 targetSwapContract := utils.Getenv(strings.ToUpper(exchange.Name)+"_TARGET_SWAP_CONTRACT", "") 65 66 alephiumClient := alephiumhelper.NewAlephiumClient( 67 log.WithContext(context.Background()).WithField("context", "AlephiumClient"), 68 sleepBetweenContractCalls, 69 isDebug, 70 ) 71 s := &AyinScraper{ 72 shutdown: make(chan nothing), 73 shutdownDone: make(chan nothing), 74 pairScrapers: make(map[string]*AyinPairScraper), 75 api: alephiumClient, 76 ticker: time.NewTicker(ayinRefreshDelay), 77 exchangeName: exchange.Name, 78 blockchain: exchange.BlockChain.Name, 79 currentHeight: 0, 80 error: nil, 81 chanTrades: make(chan *dia.Trade), 82 db: relDB, 83 refreshDelay: ayinRefreshDelay, 84 sleepBetweenContractCalls: sleepBetweenContractCalls, 85 eventsLimit: eventsLimit, 86 swapContractsLimit: swapContractsLimit, 87 targetSwapContract: targetSwapContract, 88 } 89 90 // Import tokens which appear as base token and we need a quotation for 91 var err error 92 reverseBasetokens, err = getReverseTokensFromConfig("ayin/reverse_tokens/" + s.exchangeName + "Basetoken") 93 if err != nil { 94 log.Error("error getting tokens for which pairs should be reversed: ", err) 95 } 96 log.Info("reverse basetokens: ", reverseBasetokens) 97 reverseQuotetokens, err = getReverseTokensFromConfig("ayin/reverse_tokens/" + s.exchangeName + "Quotetoken") 98 if err != nil { 99 log.Error("error getting tokens for which pairs should be reversed: ", err) 100 } 101 log.Info("reverse quotetokens: ", reverseQuotetokens) 102 s.reverseBasetokens = *reverseBasetokens 103 s.reverseQuotetokens = *reverseQuotetokens 104 105 s.logger = logrus. 106 New(). 107 WithContext(context.Background()). 108 WithField("context", "AyinDEXScraper") 109 110 if scrape { 111 go s.mainLoop() 112 } 113 return s 114 } 115 116 // mainLoop runs in a goroutine until channel s is closed. 117 func (s *AyinScraper) mainLoop() { 118 currentHeight, err := s.api.GetCurrentHeight() 119 if err != nil { 120 s.logger.WithError(err).Error("failed to GetCurrentHeight") 121 s.cleanup(err) 122 return 123 } 124 s.currentHeight = currentHeight 125 126 err = s.Update() 127 if err != nil { 128 s.logger.Error(err) 129 } 130 for { 131 select { 132 case <-s.ticker.C: 133 err := s.Update() 134 if err != nil { 135 s.logger.Error(err) 136 } 137 case <-s.shutdown: // user requested shutdown 138 s.logger.Println("shutting down") 139 s.cleanup(nil) 140 return 141 } 142 } 143 } 144 145 func (s *AyinScraper) FillSymbolData(symbol string) (dia.Asset, error) { 146 return dia.Asset{Symbol: symbol}, nil 147 } 148 149 func (s *AyinScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 150 return pair, nil 151 } 152 153 func (s *AyinScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 154 s.errorLock.RLock() 155 defer s.errorLock.RUnlock() 156 if s.error != nil { 157 return nil, s.error 158 } 159 if s.closed { 160 return nil, errors.New("AlephiumScraper: Call ScrapePair on closed scraper") 161 } 162 ps := &AyinPairScraper{ 163 parent: s, 164 pair: pair, 165 lastRecord: 0, 166 } 167 168 s.pairScrapers[pair.Symbol] = ps 169 170 return ps, nil 171 } 172 173 func (s *AyinScraper) getPools() ([]dia.Pool, error) { 174 if s.targetSwapContract != "" { 175 result := make([]dia.Pool, 1) 176 pool, err := s.db.GetPoolByAddress(s.blockchain, s.targetSwapContract) 177 result[0] = pool 178 return result, err 179 } 180 return s.db.GetAllPoolsExchange(s.exchangeName, 0) 181 } 182 183 func (s *AyinScraper) Update() error { 184 logger := s.logger.WithFields(logrus.Fields{ 185 "function": "Update", 186 }) 187 188 blockHashes, err := s.api.GetBlockHashes(s.currentHeight) 189 if err != nil { 190 s.logger.WithError(err).Error("failed to GetBlockHashes") 191 return err 192 } 193 if len(blockHashes) == 0 { 194 // logger.Info("no new blocks in the network, waiting...") 195 return nil 196 } 197 198 allEvents, err := s.fetchEvents(blockHashes) 199 if err != nil { 200 logger.WithError(err).Error("failed to fetch events") 201 return err 202 } 203 s.currentHeight += 1 204 205 if len(allEvents) == 0 { 206 logger.WithField("currentHeight", s.currentHeight).Info("no events, skipping to the next block...") 207 return nil 208 } 209 210 pools, err := s.getPools() 211 if err != nil { 212 logger. 213 WithError(err). 214 Error("failed to GetAllPoolsExchange") 215 return err 216 } 217 218 for _, pool := range pools { 219 if len(pool.Assetvolumes) != 2 { 220 logger.WithField("poolAddress", pool.Address).Error("pool is missing required asset volumes") 221 continue 222 } 223 224 poolEvents := make([]alephiumhelper.ContractEvent, 0) 225 226 for _, event := range allEvents { 227 if event.ContractAddress == pool.Address { 228 poolEvents = append(poolEvents, event) 229 } 230 } 231 232 if len(poolEvents) == 0 { 233 continue 234 } 235 236 for _, event := range poolEvents { 237 logger.WithField("event", event).WithField("currentHeight", s.currentHeight).Info("event") 238 transactionDetails, err := s.api.GetTransactionDetails(event.TxID) 239 if err != nil { 240 logger. 241 WithError(err). 242 Error("failed to GetTransactionDetails") 243 continue 244 } 245 246 var timestamp time.Time 247 if transactionDetails.Timestamp > 0 { 248 timestamp = time.UnixMilli(transactionDetails.Timestamp) 249 } else { 250 timestamp = time.Now() 251 } 252 253 diaTrade := s.handleTrade(&pool, &event, timestamp) 254 logger. 255 WithField("parentAddress", pool.Address). 256 WithField("diaTrade", diaTrade). 257 Info("diaTrade") 258 s.chanTrades <- diaTrade 259 } 260 } 261 262 return nil 263 } 264 265 func (s *AyinScraper) fetchEvents(blockHashes []string) ([]alephiumhelper.ContractEvent, error) { 266 allEvents := make([]alephiumhelper.ContractEvent, 0) 267 268 for _, hash := range blockHashes { 269 events, err := s.api.GetBlockEvents(hash) 270 if err != nil { 271 return allEvents, err 272 } 273 274 filtered := s.api.FilterEvents(events, alephiumhelper.SwapEventIndex) 275 allEvents = append(allEvents, filtered...) 276 } 277 278 return allEvents, nil 279 } 280 281 func (s *AyinScraper) handleTrade(pool *dia.Pool, event *alephiumhelper.ContractEvent, time time.Time) *dia.Trade { 282 var volume, price float64 283 284 decimals0 := int64(pool.Assetvolumes[0].Asset.Decimals) 285 decimals1 := int64(pool.Assetvolumes[1].Asset.Decimals) 286 287 if event.Fields[1].Value != "0" { 288 // if we are swapping from ALPH(asset0) to USDT(asset1), - default behaviour 289 // then amount0In ((fields[1]) will be the amount for ALPH 290 // and amount1Out (fields[4]) will be the amount for USDT. 291 amount0In, _ := utils.StringToFloat64(event.Fields[1].Value, decimals0) 292 amount1Out, _ := utils.StringToFloat64(event.Fields[4].Value, decimals1) 293 volume = amount0In 294 price = amount1Out / amount0In 295 } else { 296 // If we are swapping from USDT(asset1) to ALPH(asset0), 297 // then amount1In ((fields[2]) will be the amount for USDT 298 // and amount0Out (fields[3]) will be the amount for ALPH. 299 amount1In, _ := utils.StringToFloat64(event.Fields[2].Value, decimals1) 300 amount0Out, _ := utils.StringToFloat64(event.Fields[3].Value, decimals0) 301 volume = -amount0Out 302 price = amount1In / amount0Out 303 } 304 305 symbolPair := fmt.Sprintf("%s-%s", pool.Assetvolumes[0].Asset.Symbol, pool.Assetvolumes[1].Asset.Symbol) 306 307 diaTrade := &dia.Trade{ 308 Time: time, 309 Symbol: pool.Assetvolumes[0].Asset.Symbol, 310 Pair: symbolPair, 311 ForeignTradeID: event.TxID, 312 Source: s.exchangeName, 313 Price: price, 314 Volume: volume, 315 VerifiedPair: true, 316 QuoteToken: pool.Assetvolumes[0].Asset, 317 BaseToken: pool.Assetvolumes[1].Asset, 318 PoolAddress: pool.Address, 319 } 320 321 // Reverse the order of the trade for selected assets. 322 switch { 323 case utils.Contains(reverseQuotetokens, diaTrade.QuoteToken.Address): 324 // If we don't need quotation of quote token, reverse pair. 325 tSwapped, err := dia.SwapTrade(*diaTrade) 326 if err == nil { 327 diaTrade = &tSwapped 328 } 329 case utils.Contains(reverseBasetokens, diaTrade.BaseToken.Address): 330 // If we need quotation of a base token, reverse pair 331 tSwapped, err := dia.SwapTrade(*diaTrade) 332 if err == nil { 333 diaTrade = &tSwapped 334 } 335 } 336 337 return diaTrade 338 } 339 340 // closes all connected PairScrapers 341 // must only be called from mainLoop 342 func (s *AyinScraper) cleanup(err error) { 343 344 s.errorLock.Lock() 345 defer s.errorLock.Unlock() 346 347 s.ticker.Stop() 348 349 if err != nil { 350 s.error = err 351 } 352 s.closed = true 353 354 close(s.shutdownDone) // signal that shutdown is complete 355 } 356 357 // Close closes any existing API connections, as well as channels of 358 // PairScrapers from calls to ScrapePair 359 func (s *AyinScraper) Close() error { 360 if s.closed { 361 return errors.New("AlephiumScraper: Already closed") 362 } 363 close(s.shutdown) 364 <-s.shutdownDone 365 s.errorLock.RLock() 366 defer s.errorLock.RUnlock() 367 return s.error 368 } 369 370 // Channel returns a channel that can be used to receive trades/pricing information 371 func (s *AyinScraper) Channel() chan *dia.Trade { 372 return s.chanTrades 373 } 374 375 // FetchAvailablePairs returns a list with all available trade pairs as dia.ExchangePair for the pairDiscorvery service 376 func (s *AyinScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 377 logger := s.logger.WithFields(logrus.Fields{ 378 "function": "FetchAvailablePairs", 379 }) 380 contractAddresses, err := s.api.GetSwapPairsContractAddresses(s.swapContractsLimit) 381 if err != nil { 382 logger.WithError(err).Error("failed to get swap contract addresses") 383 return 384 } 385 for _, contractAddress := range contractAddresses.SubContracts { 386 tokenPairs, err := s.api.GetTokenPairAddresses(contractAddress) 387 388 if err != nil { 389 logger. 390 WithField("contractAddress", contractAddress). 391 WithError(err). 392 Error("failed to get GetTokenPairAddresses") 393 continue 394 } 395 396 token0, err := s.api.GetTokenInfoForContractDecoded(tokenPairs[0], s.blockchain) 397 if err != nil { 398 logger. 399 WithField("contractAddress", contractAddress). 400 WithError(err). 401 Error("failed to get GetTokenInfoForContractDecoded for token0") 402 continue 403 } 404 405 token1, err := s.api.GetTokenInfoForContractDecoded(tokenPairs[1], s.blockchain) 406 if err != nil { 407 logger. 408 WithField("contractAddress", contractAddress). 409 WithError(err). 410 Error("failed to get GetTokenInfoForContractDecoded for token1") 411 continue 412 } 413 pair := dia.ExchangePair{ 414 Symbol: token0.Symbol, 415 ForeignName: fmt.Sprintf("%s-%s", token0.Symbol, token1.Symbol), 416 Exchange: s.exchangeName, 417 } 418 419 pairs = append(pairs, pair) 420 421 time.Sleep(s.sleepBetweenContractCalls) 422 } 423 return pairs, nil 424 } 425 426 type AyinPairScraper struct { 427 parent *AyinScraper 428 pair dia.ExchangePair 429 closed bool 430 lastRecord int64 431 } 432 433 func (ps *AyinPairScraper) Pair() dia.ExchangePair { 434 return ps.pair 435 } 436 437 func (ps *AyinPairScraper) Close() error { 438 ps.closed = true 439 return nil 440 } 441 442 // Error returns an error when the channel Channel() is closed 443 // and nil otherwise 444 func (ps *AyinPairScraper) Error() error { 445 s := ps.parent 446 s.errorLock.RLock() 447 defer s.errorLock.RUnlock() 448 return s.error 449 }