github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/HydrationScraper.go (about) 1 package scrapers 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strconv" 8 "strings" 9 "sync" 10 "time" 11 12 "github.com/diadata-org/diadata/pkg/dia" 13 models "github.com/diadata-org/diadata/pkg/model" 14 15 substratehelper "github.com/diadata-org/diadata/pkg/dia/helpers/substrate-helper" 16 "github.com/diadata-org/diadata/pkg/dia/helpers/substrate-helper/gsrpc/registry/parser" 17 "github.com/diadata-org/diadata/pkg/utils" 18 "github.com/sirupsen/logrus" 19 ) 20 21 type HydrationScraper struct { 22 logger *logrus.Entry 23 pairScrapers map[string]*HydrationPairScraper // pc.ExchangePair -> pairScraperSet 24 shutdown chan nothing 25 shutdownDone chan nothing 26 errorLock sync.RWMutex 27 error error 28 closed bool 29 chanTrades chan *dia.Trade 30 db *models.RelDB 31 wsApi *substratehelper.SubstrateEventHelper 32 exchangeName string 33 blockchain string 34 currentBlock uint64 35 } 36 37 func NewHydrationScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *HydrationScraper { 38 logger := logrus. 39 New(). 40 WithContext(context.Background()). 41 WithField("context", "HydrationScraper") 42 43 wsApi, err := substratehelper.NewSubstrateEventHelper(exchange.WsAPI, logger) 44 if err != nil { 45 logrus.WithError(err).Error("Failed to create Hydration Substrate event helper") 46 return nil 47 } 48 49 startBlock := utils.Getenv(strings.ToUpper(exchange.Name)+"_START_BLOCK", "0") 50 startBlockUint64, err := strconv.ParseUint(startBlock, 10, 64) 51 if err != nil { 52 logrus.WithError(err).Error("Failed to parse start block, using default value of 0") 53 startBlockUint64 = 0 54 } 55 56 s := &HydrationScraper{ 57 logger: logger, // Ensure logger is initialized 58 shutdown: make(chan nothing), 59 shutdownDone: make(chan nothing), 60 chanTrades: make(chan *dia.Trade), 61 db: relDB, 62 wsApi: wsApi, 63 exchangeName: exchange.Name, 64 blockchain: "Hydration", 65 currentBlock: startBlockUint64, 66 } 67 68 s.logger.Info("WS API", s.wsApi) 69 if scrape { 70 go s.mainLoop() 71 } 72 return s 73 } 74 75 // processNewBlock processes new blocks and filters SellExecuted events. 76 // https://hydration.subscan.io/event?block=6148977&page=1&time_dimension=date&module=xyk&event_id=sellexecuted 77 // pool = 7KKXieLDbfJPUaVohYTbbib97LdC1URmZuMNFq9rvTudmDMv 78 // block = 0xfd38c9dc2c95278fd3015f73b48a01e804320865a1a6153e31471cb782be92f0 79 // blocknumber = 6148977 80 func (s *HydrationScraper) mainLoop() { 81 s.logger.Info("Listening for new blocks") 82 defer s.cleanup(nil) 83 84 for { 85 select { 86 case <-s.shutdown: 87 s.logger.Println("shutting down") 88 return 89 default: 90 s.logger.Info("Processing block:", s.currentBlock) 91 92 if s.currentBlock == 0 { 93 s.wsApi.ListenForNewBlocks(s.processEvents) 94 } else { 95 s.wsApi.ListenForSpecificBlock(s.currentBlock, s.processEvents) 96 s.currentBlock++ 97 time.Sleep(time.Second) 98 latestBlock, err := s.wsApi.API.RPC.Chain.GetBlockLatest() 99 if err != nil { 100 s.logger.WithError(err).Error("Failed to get latest block") 101 return 102 } 103 104 if s.currentBlock > uint64(latestBlock.Block.Header.Number) { 105 s.logger.Info("Reached the latest block") 106 s.wsApi.ListenForNewBlocks(s.processEvents) 107 } 108 } 109 } 110 } 111 } 112 113 func (s *HydrationScraper) processEvents(events []*parser.Event, blockNumber uint64) { 114 for _, e := range events { 115 parsedEvent := s.parseRouterEvent(e) 116 if parsedEvent == nil { 117 continue 118 } 119 if e.Phase.IsApplyExtrinsic { 120 parsedEvent.ExtrinsicID = fmt.Sprintf("%d-%d", blockNumber, e.Phase.AsApplyExtrinsic) 121 } 122 123 pools, err := s.db.GetAllPoolsExchange(s.exchangeName, 0) 124 if err != nil { 125 s.logger.Error("Failed to get pools from database") 126 continue 127 } 128 129 pool := s.filterPools(pools, parsedEvent) 130 131 if len(pool.Assetvolumes) < 2 { 132 // look for pool address in other events 133 secundaryEvent := s.parseSecundaryEvent(events, parsedEvent, blockNumber) 134 secundaryPool := s.filterPools(pools, secundaryEvent) 135 136 // Manually fill asset from routerEvent 137 assetIn, err := s.db.GetAsset(parsedEvent.AssetIn, s.blockchain) 138 if err != nil { 139 s.logger.Errorf("Failed to get assetIn for asset address %s", parsedEvent.AssetIn) 140 continue 141 142 } 143 assetOut, err := s.db.GetAsset(parsedEvent.AssetOut, s.blockchain) 144 if err != nil { 145 s.logger.Errorf("Failed to get assetOut for asset address %s", parsedEvent.AssetOut) 146 continue 147 } 148 pool.Address = secundaryPool.Address 149 pool.Assetvolumes = []dia.AssetVolume{ 150 {Asset: assetIn}, 151 {Asset: assetOut}, 152 } 153 if len(pool.Assetvolumes) < 2 { 154 s.logger.WithField("poolAddress", pool.Address).Error("Pool has fewer than 2 asset volumes") 155 continue 156 } 157 } 158 159 diaTrade, err := s.handleTrade(pool, *parsedEvent, time.Now()) 160 if err != nil { 161 s.logger.WithError(err).Error("Failed to handle trade") 162 continue 163 } 164 165 s.logger.WithFields(logrus.Fields{ 166 "Pair": diaTrade.Pair, 167 "Price": diaTrade.Price, 168 "Volume": diaTrade.Volume, 169 }).Info("Trade processed") 170 171 s.chanTrades <- diaTrade 172 } 173 } 174 175 func (s *HydrationScraper) filterPools(pools []dia.Pool, event *HydrationParsedEvent) dia.Pool { 176 for _, pool := range pools { 177 assetInFound := false 178 assetOutFound := false 179 180 for _, assetVolume := range pool.Assetvolumes { 181 if assetVolume.Asset.Address == event.AssetIn { 182 assetInFound = true 183 } 184 if assetVolume.Asset.Address == event.AssetOut { 185 assetOutFound = true 186 } 187 if assetInFound && assetOutFound { 188 return pool 189 } 190 } 191 } 192 return dia.Pool{} 193 } 194 195 func (s *HydrationScraper) parseSecundaryEvent(events []*parser.Event, routerEvent *HydrationParsedEvent, blockNumber uint64) *HydrationParsedEvent { 196 parsedEvent := &HydrationParsedEvent{} 197 for _, e := range events { 198 if strings.EqualFold(e.Name, "Omnipool.SellExecuted") || 199 strings.EqualFold(e.Name, "Omnipool.BuyExecuted") || 200 strings.EqualFold(e.Name, "Stableswap.BuyExecuted") || 201 strings.EqualFold(e.Name, "Stableswap.SellExecuted") { 202 if e.Phase.IsApplyExtrinsic { 203 extrinsicId := fmt.Sprintf("%d-%d", blockNumber, e.Phase.AsApplyExtrinsic) 204 if extrinsicId == routerEvent.ExtrinsicID { 205 parsedEvent := &HydrationParsedEvent{} 206 parsedEvent = s.parseFields(e) 207 parsedEvent.AmountIn = routerEvent.AmountIn 208 parsedEvent.AmountOut = routerEvent.AmountOut 209 parsedEvent.ExtrinsicID = extrinsicId 210 return parsedEvent 211 } 212 } 213 } 214 } 215 216 return parsedEvent 217 } 218 219 func (s *HydrationScraper) parseRouterEvent(event *parser.Event) *HydrationParsedEvent { 220 if strings.EqualFold(event.Name, "Router.Executed") { 221 return s.parseFields(event) 222 } 223 224 return nil 225 } 226 227 func (s *HydrationScraper) parseFields(event *parser.Event) *HydrationParsedEvent { 228 parsedEvent := &HydrationParsedEvent{} 229 for _, v := range event.Fields { 230 switch v.Name { 231 case "asset_in": 232 parsedEvent.AssetIn = fmt.Sprint(v.Value) 233 case "asset_out": 234 parsedEvent.AssetOut = fmt.Sprint(v.Value) 235 case "amount_in": 236 parsedEvent.AmountIn = fmt.Sprint(v.Value) 237 case "amount_out": 238 parsedEvent.AmountOut = fmt.Sprint(v.Value) 239 } 240 } 241 return parsedEvent 242 } 243 244 func (s *HydrationScraper) cleanup(err error) { 245 s.errorLock.Lock() 246 defer s.errorLock.Unlock() 247 248 if err != nil { 249 s.error = err 250 } 251 s.closed = true 252 close(s.shutdownDone) 253 } 254 255 func (s *HydrationScraper) Close() error { 256 if s.closed { 257 return errors.New("HydrationScraper: Already closed") 258 } 259 close(s.shutdown) 260 <-s.shutdownDone 261 s.errorLock.RLock() 262 defer s.errorLock.RUnlock() 263 return s.error 264 } 265 266 func (s *HydrationScraper) Channel() chan *dia.Trade { 267 return s.chanTrades 268 } 269 270 type HydrationPairScraper struct { 271 parent *HydrationScraper 272 pair dia.ExchangePair 273 closed bool 274 lastRecord int64 275 } 276 277 func (ps *HydrationPairScraper) Pair() dia.ExchangePair { 278 return ps.pair 279 } 280 281 func (ps *HydrationPairScraper) Close() error { 282 ps.closed = true 283 return nil 284 } 285 286 // Error returns an error when the channel Channel() is closed 287 // and nil otherwise 288 func (ps *HydrationPairScraper) Error() error { 289 s := ps.parent 290 s.errorLock.RLock() 291 defer s.errorLock.RUnlock() 292 return s.error 293 } 294 295 // Channel returns the channel used to receive trades/pricing information. 296 func (s *HydrationScraper) handleTrade(pool dia.Pool, event HydrationParsedEvent, time time.Time) (*dia.Trade, error) { 297 var volume, price float64 298 var decimalsIn, decimalsOut int64 299 var quoteToken, baseToken dia.Asset 300 301 // Determine which asset is being sold (this is the base asset) 302 for _, assetVolume := range pool.Assetvolumes { 303 if event.AssetIn == assetVolume.Asset.Address { 304 baseToken = assetVolume.Asset 305 } 306 if event.AssetOut == assetVolume.Asset.Address { 307 quoteToken = assetVolume.Asset 308 } 309 if baseToken.Address != "" && quoteToken.Address != "" { 310 break 311 } 312 } 313 // Check if both baseToken and quoteToken have been assigned 314 if baseToken.Address == "" || quoteToken.Address == "" { 315 return &dia.Trade{}, errors.New("Failed to determine baseToken or quoteToken") 316 } 317 318 decimalsIn = int64(baseToken.Decimals) 319 decimalsOut = int64(quoteToken.Decimals) 320 amountIn, _ := utils.StringToFloat64(event.AmountIn, decimalsIn) 321 amountOut, _ := utils.StringToFloat64(event.AmountOut, decimalsOut) 322 323 volume = amountOut 324 325 price = amountIn / amountOut 326 327 symbolPair := fmt.Sprintf("%s-%s", quoteToken.Symbol, baseToken.Symbol) 328 329 return &dia.Trade{ 330 Time: time, 331 Symbol: quoteToken.Symbol, 332 Pair: symbolPair, 333 ForeignTradeID: event.ExtrinsicID, 334 Source: s.exchangeName, 335 Price: price, 336 Volume: volume, 337 VerifiedPair: true, 338 QuoteToken: quoteToken, 339 BaseToken: baseToken, 340 PoolAddress: pool.Address, 341 }, nil 342 } 343 344 func (s *HydrationScraper) FetchAvailablePairs() ([]dia.ExchangePair, error) { 345 return []dia.ExchangePair{}, nil 346 } 347 348 func (s *HydrationScraper) FillSymbolData(symbol string) (dia.Asset, error) { 349 return dia.Asset{Symbol: symbol}, nil 350 } 351 352 func (s *HydrationScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 353 return pair, nil 354 } 355 356 func (s *HydrationScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 357 s.errorLock.RLock() 358 defer s.errorLock.RUnlock() 359 if s.error != nil { 360 return nil, s.error 361 } 362 if s.closed { 363 return nil, errors.New("HydrationScraper: Call ScrapePair on closed scraper") 364 } 365 ps := &HydrationPairScraper{ 366 parent: s, 367 pair: pair, 368 lastRecord: 0, 369 } 370 371 s.pairScrapers[pair.Symbol] = ps 372 373 return ps, nil 374 } 375 376 type HydrationParsedEvent struct { 377 Name string 378 ExtrinsicID string 379 AssetIn string 380 AssetOut string 381 AmountIn string 382 AmountOut string 383 }