github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/BifrostScraper.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 substratehelper "github.com/diadata-org/diadata/pkg/dia/helpers/substrate-helper" 14 "github.com/diadata-org/diadata/pkg/dia/helpers/substrate-helper/gsrpc/registry" 15 "github.com/diadata-org/diadata/pkg/dia/helpers/substrate-helper/gsrpc/registry/parser" 16 17 models "github.com/diadata-org/diadata/pkg/model" 18 19 "github.com/diadata-org/diadata/pkg/utils" 20 "github.com/sirupsen/logrus" 21 ) 22 23 type BifrostScraper struct { 24 logger *logrus.Entry 25 pairScrapers map[string]*BifrostPairScraper // pc.ExchangePair -> pairScraperSet 26 shutdown chan nothing 27 shutdownDone chan nothing 28 errorLock sync.RWMutex 29 error error 30 closed bool 31 ticker *time.Ticker 32 chanTrades chan *dia.Trade 33 db *models.RelDB 34 wsApi *substratehelper.SubstrateEventHelper 35 exchangeName string 36 blockchain string 37 currentBlock uint64 38 } 39 40 func NewBifrostScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BifrostScraper { 41 logger := logrus. 42 New(). 43 WithContext(context.Background()). 44 WithField("context", "BifrostScraper") 45 46 wsApi, err := substratehelper.NewSubstrateEventHelper(exchange.WsAPI, logger) 47 if err != nil { 48 logrus.WithError(err).Error("Failed to create Bifrost Substrate event helper") 49 return nil 50 } 51 52 startBlock := utils.Getenv(strings.ToUpper(exchange.Name)+"_START_BLOCK", "0") 53 startBlockUint64, err := strconv.ParseUint(startBlock, 10, 64) 54 if err != nil { 55 logrus.WithError(err).Error("Failed to parse start block, using default value of 10") 56 startBlockUint64 = 10 57 } 58 59 s := &BifrostScraper{ 60 shutdown: make(chan nothing), 61 shutdownDone: make(chan nothing), 62 chanTrades: make(chan *dia.Trade), 63 db: relDB, 64 wsApi: wsApi, 65 exchangeName: exchange.Name, 66 blockchain: "Bifrost", 67 currentBlock: startBlockUint64, 68 } 69 70 s.logger = logger 71 72 s.logger.Info("Initialized BifrostScraper") 73 74 if scrape { 75 go s.mainLoop() 76 } 77 return s 78 } 79 80 func (s *BifrostScraper) 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 *BifrostScraper) processEvents(events []*parser.Event, blockNumber uint64) { 114 s.logger.Info("Processing events") 115 116 for _, e := range events { 117 if e.Name == "StableAsset.TokenSwapped" { 118 parsedEvent := parseFields(e) 119 parsedEvent.ExtrinsicID = fmt.Sprintf("%d-%d", blockNumber, e.Phase.AsApplyExtrinsic) 120 pool, err := s.db.GetPoolByAddress(s.blockchain, parsedEvent.PoolId) 121 122 if len(pool.Assetvolumes) < 2 { 123 s.logger.WithField("poolAddress", pool.Address).Error("Pool has fewer than 2 asset volumes") 124 continue 125 } 126 if err != nil { 127 continue 128 } 129 130 diaTrade := s.handleTrade(pool, parsedEvent, time.Now()) 131 132 s.logger.WithFields(logrus.Fields{ 133 "Pair": diaTrade.Pair, 134 "Price": diaTrade.Price, 135 "Volume": diaTrade.Volume, 136 }).Info("Trade processed") 137 138 s.chanTrades <- diaTrade 139 } 140 } 141 } 142 143 type ParsedEvent struct { 144 InputAsset string 145 OutputAsset string 146 InputAmount string 147 OutputAmount string 148 PoolId string 149 ExtrinsicID string 150 } 151 152 type TokenValue struct { 153 Token *string `json:"Token,omitempty"` // Token variant, e.g., "KSM" 154 } 155 type VTokenValue struct { 156 VToken string `json:"VToken,omitempty"` // Token variant, e.g., "KSM" 157 } 158 159 // TokenSymbol represents the token symbols as an enum 160 type TokenSymbol int 161 162 const ( 163 ASG TokenSymbol = iota 164 BNC 165 KUSD 166 DOT 167 KSM 168 ETH 169 KAR 170 ZLK 171 PHA 172 RMRK 173 MOVR 174 ) 175 176 // String returns the string representation of the TokenSymbol 177 func (ts TokenSymbol) String() string { 178 return [...]string{"ASG", "BNC", "KUSD", "DOT", "KSM", "ETH", "KAR", "ZLK", "PHA", "RMRK", "MOVR"}[ts] 179 } 180 181 func parseFields(event *parser.Event) ParsedEvent { 182 var parsedEvent ParsedEvent 183 for _, v := range event.Fields { 184 switch v.Name { 185 case "bifrost_primitives.currency.CurrencyId.input_asset": 186 if result, ok := v.Value.(registry.VariantDecoderResult); ok { 187 if decodedFields, ok := result.Value.(registry.DecodedFields); ok { 188 if len(decodedFields) > 0 { 189 parsedEvent.InputAsset = strings.ToLower(result.FieldName) + "-" + strings.ToLower(fmt.Sprint(decodedFields[0].Value)) 190 } 191 } 192 } 193 case "bifrost_primitives.currency.CurrencyId.output_asset": 194 if result, ok := v.Value.(registry.VariantDecoderResult); ok { 195 if decodedFields, ok := result.Value.(registry.DecodedFields); ok { 196 if len(decodedFields) > 0 { 197 if len(decodedFields) > 0 { 198 parsedEvent.OutputAsset = strings.ToLower(result.FieldName) + "-" + strings.ToLower(fmt.Sprint(decodedFields[0].Value)) 199 } 200 } 201 } 202 } 203 204 case "input_amount": 205 parsedEvent.InputAmount = fmt.Sprint(v.Value) 206 case "output_amount": 207 parsedEvent.OutputAmount = fmt.Sprint(v.Value) 208 case "pool_id": 209 parsedEvent.PoolId = fmt.Sprint(v.Value) 210 } 211 } 212 return parsedEvent 213 } 214 215 // handleTrade processes a swap event and converts it into a dia.Trade object. 216 // 217 // This function takes a pool and a corresponding stable swap event, calculates 218 // the trade volume and price based on the asset amounts in the event, and returns 219 // a `dia.Trade` object representing the processed trade. The price is calculated 220 // as the ratio of the input to output amounts, and the volume is set as the 221 // negative of the input amount to indicate the amount being swapped. 222 // 223 // The `dia.Trade` object includes metadata such as the trade timestamp, the trading pair, 224 // the pool address, and the exchange source. 225 // 226 // Parameters: 227 // - pool: A `dia.Pool` object representing the liquidity pool where the swap occurred. 228 // - event: A `ParsedEvent` containing the swap details such as asset amounts and event ID. 229 // - time: The timestamp for the trade event. 230 // 231 // Returns: 232 // - *dia.Trade: A pointer to the constructed `dia.Trade` object containing the trade details. 233 234 func (s *BifrostScraper) handleTrade(pool dia.Pool, event ParsedEvent, time time.Time) *dia.Trade { 235 var volume, price float64 236 var baseToken, quoteToken dia.Asset 237 var decimalsIn, decimalsOut int64 238 239 if fmt.Sprint(event.InputAsset) == pool.Assetvolumes[0].Asset.Address { 240 baseToken = pool.Assetvolumes[0].Asset 241 quoteToken = pool.Assetvolumes[1].Asset 242 } else { 243 baseToken = pool.Assetvolumes[1].Asset 244 quoteToken = pool.Assetvolumes[0].Asset 245 } 246 247 decimalsIn = int64(baseToken.Decimals) 248 decimalsOut = int64(quoteToken.Decimals) 249 amountIn, _ := utils.StringToFloat64(event.InputAmount, decimalsIn) 250 amountOut, _ := utils.StringToFloat64(event.OutputAmount, decimalsOut) 251 252 volume = amountOut 253 254 price = amountIn / amountOut 255 256 symbolPair := fmt.Sprintf("%s-%s", quoteToken.Symbol, baseToken.Symbol) 257 258 return &dia.Trade{ 259 Time: time, 260 Symbol: quoteToken.Symbol, 261 Pair: symbolPair, 262 ForeignTradeID: event.ExtrinsicID, 263 Source: s.exchangeName, 264 Price: price, 265 Volume: volume, 266 VerifiedPair: true, 267 QuoteToken: quoteToken, 268 BaseToken: baseToken, 269 PoolAddress: pool.Address, 270 } 271 } 272 273 // FetchAvailablePairs returns a list with all trading pairs available on 274 // the exchange associated to the APIScraper. The format is such that it can 275 // be used by the corr. pairScraper in order to fetch trades. 276 func (s *BifrostScraper) FetchAvailablePairs() ([]dia.ExchangePair, error) { 277 return []dia.ExchangePair{}, nil 278 } 279 280 func (s *BifrostScraper) FillSymbolData(symbol string) (dia.Asset, error) { 281 return dia.Asset{Symbol: symbol}, nil 282 } 283 284 func (s *BifrostScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 285 return pair, nil 286 } 287 288 // ScrapePair initializes and returns a `BifrostPairScraper`. 289 // 290 // Parameters: 291 // - pair: The `dia.ExchangePair` representing the trading pair (e.g: `BifrostPairScraper`) to be scraped. 292 // 293 // Returns: 294 // - PairScraper: A `PairScraper` (specifically a `BifrostPairScraper`) for the given exchange pair. 295 // - error: An error if the scraper is closed or if an error has occurred, otherwise `nil`. 296 func (s *BifrostScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 297 s.errorLock.RLock() 298 defer s.errorLock.RUnlock() 299 if s.error != nil { 300 return nil, s.error 301 } 302 if s.closed { 303 return nil, errors.New("BifrostScraper: Call ScrapePair on closed scraper") 304 } 305 ps := &BifrostPairScraper{ 306 parent: s, 307 pair: pair, 308 lastRecord: 0, 309 } 310 311 s.pairScrapers[pair.Symbol] = ps 312 313 return ps, nil 314 } 315 316 // cleanup handles the shutdown procedure. 317 func (s *BifrostScraper) cleanup(err error) { 318 s.errorLock.Lock() 319 defer s.errorLock.Unlock() 320 321 s.ticker.Stop() 322 323 if err != nil { 324 s.error = err 325 } 326 s.closed = true 327 close(s.shutdownDone) 328 } 329 330 // Close gracefully shuts down the BifrostScraper. 331 func (s *BifrostScraper) Close() error { 332 if s.closed { 333 return errors.New("BifrostScraper: Already closed") 334 } 335 close(s.shutdown) 336 <-s.shutdownDone 337 s.errorLock.RLock() 338 defer s.errorLock.RUnlock() 339 return s.error 340 } 341 342 // Channel returns the channel used to receive trades/pricing information. 343 func (s *BifrostScraper) Channel() chan *dia.Trade { 344 return s.chanTrades 345 } 346 347 type BifrostPairScraper struct { 348 parent *BifrostScraper 349 pair dia.ExchangePair 350 closed bool 351 lastRecord int64 352 } 353 354 func (ps *BifrostPairScraper) Pair() dia.ExchangePair { 355 return ps.pair 356 } 357 358 func (ps *BifrostPairScraper) Close() error { 359 ps.closed = true 360 return nil 361 } 362 363 // Error returns an error when the channel Channel() is closed 364 // and nil otherwise 365 func (ps *BifrostPairScraper) Error() error { 366 s := ps.parent 367 s.errorLock.RLock() 368 defer s.errorLock.RUnlock() 369 return s.error 370 }