github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/TraderJoeScraper.go (about) 1 package scrapers 2 3 import ( 4 "encoding/json" 5 "errors" 6 "io" 7 "math" 8 "math/big" 9 "os" 10 "strconv" 11 "strings" 12 "sync" 13 "time" 14 15 "github.com/diadata-org/diadata/pkg/dia" 16 "github.com/diadata-org/diadata/pkg/dia/helpers" 17 "github.com/diadata-org/diadata/pkg/dia/helpers/configCollectors" 18 "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/traderjoe2.1/traderjoeILBPair" 19 models "github.com/diadata-org/diadata/pkg/model" 20 "github.com/diadata-org/diadata/pkg/utils" 21 "github.com/ethereum/go-ethereum/accounts/abi/bind" 22 "github.com/ethereum/go-ethereum/common" 23 "github.com/ethereum/go-ethereum/ethclient" 24 ) 25 26 var ( 27 TraderJoeExchangeFactoryContractAddress = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f" 28 29 MapOfPools = make(map[string]TraderJoePair) 30 ) 31 32 type TraderJoeTokens struct { 33 Address common.Address 34 Symbol string 35 Decimals uint8 36 Name string 37 } 38 39 type TraderJoePair struct { 40 Token0 TraderJoeTokens 41 Token1 TraderJoeTokens 42 ForeignName string 43 Address common.Address 44 } 45 46 type TraderJoeSwap struct { 47 ID string 48 Timestamp int64 49 Pair TraderJoePair 50 Amount0 float64 51 Amount1 float64 52 } 53 54 type TraderJoeScraper struct { 55 // Ethereum WebSocket client for real-time data. 56 WsClient *ethclient.Client 57 // Ethereum REST client for querying historical data. 58 RestClient *ethclient.Client 59 // Relational database connection. 60 relDB *models.RelDB 61 // Signaling channels for managing session start and shutdown. 62 run bool 63 shutdown chan nothing 64 shutdownDone chan nothing 65 // Error handling; read lock for error or closed status. 66 errorLock sync.RWMutex 67 error error 68 closed bool 69 // Map of active TraderJoeTradeScraper instances for trading pairs. 70 pairScrapers map[string]*TraderJoeTradeScraper 71 // Channel to receive new trading pairs for scraping. 72 pairReceived chan *TraderJoePair 73 // Name of the exchange. 74 exchangeName string 75 // Time interval for waiting between actions. 76 waitTime int 77 // Option to listen for trading pairs by address. 78 listenByAddress bool 79 // Channel for receiving trade data. 80 chanTrades chan *dia.Trade 81 // Address of the factory contract for the exchange. 82 factoryContractAddress common.Address 83 } 84 85 // NewTraderJoeScraper initializes a Trader Joe scraper instance with the provided exchange information, 86 // scraping flag, and relational database connection. It configures parameters, sets up pool maps, 87 // and starts the scraping process if requested. 88 func NewTraderJoeScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *TraderJoeScraper { 89 log.Info("NewTraderJoeScraper ", exchange.Name) 90 log.Info("NewTraderJoeScraper Address ", exchange.Contract) 91 92 var ( 93 tjs *TraderJoeScraper 94 listenByAddress bool 95 err error 96 ) 97 98 listenByAddress, err = strconv.ParseBool(utils.Getenv("LISTEN_BY_ADDRESS", "")) 99 if err != nil { 100 log.Fatal("parse LISTEN_BY_ADDRESS: ", err) 101 } 102 103 switch exchange.Name { 104 case dia.TraderJoeExchangeV2_1: 105 tjs = makeTraderJoeScraper(exchange, listenByAddress, "", "", "200") 106 case dia.TraderJoeExchangeV2_1Arbitrum: 107 tjs = makeTraderJoeScraper(exchange, listenByAddress, "", "", "200") 108 case dia.TraderJoeExchangeV2_1Avalanche: 109 tjs = makeTraderJoeScraper(exchange, listenByAddress, "", "", "200") 110 case dia.TraderJoeExchangeV2_1BNB: 111 tjs = makeTraderJoeScraper(exchange, listenByAddress, "", "", "200") 112 case dia.TraderJoeExchangeV2_2Avalanche: 113 tjs = makeTraderJoeScraper(exchange, listenByAddress, "", "", "200") 114 115 } 116 117 tjs.relDB = relDB 118 119 // Only include pools with (minimum) liquidity bigger than given env var. 120 liquidityThreshold, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD", "0"), 64) 121 if err != nil { 122 liquidityThreshold = float64(0) 123 log.Warnf("parse liquidity threshold: %v. Set to default %v", err, liquidityThreshold) 124 } 125 liquidityThresholdUSD, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD_USD", "0"), 64) 126 if err != nil { 127 liquidityThresholdUSD = float64(0) 128 log.Warnf("parse liquidity threshold: %v. Set to default %v", err, liquidityThresholdUSD) 129 } 130 131 MapOfPools, err = tjs.makeTraderJoePoolMap(liquidityThreshold, liquidityThresholdUSD) 132 if err != nil { 133 log.Fatal("build poolMap: ", err) 134 } 135 136 if scrape { 137 go tjs.mainLoop() 138 } 139 return tjs 140 } 141 142 // makeTraderJoeScraper creates and initializes a Trader Joe scraper instance with the given exchange information, 143 // connection details, and configuration parameters. It establishes REST and WebSocket clients for the blockchain, 144 // determines wait time, and sets up various channels and data structures for scraping tasks. 145 func makeTraderJoeScraper(exchange dia.Exchange, listenByAddress bool, restDial string, wsDial string, waitMilliseconds string) *TraderJoeScraper { 146 var ( 147 restClient, wsClient *ethclient.Client 148 s *TraderJoeScraper 149 err error 150 ) 151 152 log.Infof("Initialize rest and ws client for %s.", exchange.BlockChain.Name) 153 restClient, err = ethclient.Dial(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_REST", restDial)) 154 if err != nil { 155 log.Fatal("init rest client: ", err) 156 } 157 wsClient, err = ethclient.Dial(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_WS", wsDial)) 158 if err != nil { 159 log.Fatal("init ws client: ", err) 160 } 161 162 var waitTime int 163 waitTimeString := utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_WAIT_TIME", waitMilliseconds) 164 waitTime, err = strconv.Atoi(waitTimeString) 165 if err != nil { 166 log.Error("could not parse wait time: ", err) 167 waitTime = 500 168 } 169 170 s = &TraderJoeScraper{ 171 WsClient: wsClient, 172 RestClient: restClient, 173 shutdown: make(chan nothing), 174 shutdownDone: make(chan nothing), 175 pairScrapers: make(map[string]*TraderJoeTradeScraper), 176 exchangeName: exchange.Name, 177 pairReceived: make(chan *TraderJoePair), 178 error: nil, 179 chanTrades: make(chan *dia.Trade), 180 waitTime: waitTime, 181 listenByAddress: listenByAddress, 182 factoryContractAddress: common.HexToAddress(exchange.Contract), 183 } 184 185 return s 186 } 187 188 // GetSwapsChannel returns a channel for swaps of the pair with pair address. 189 func (tjs *TraderJoeScraper) GetSwapsChannel(pairAddress common.Address) (chan *traderjoeILBPair.ILBPairSwap, error) { 190 sink := make(chan *traderjoeILBPair.ILBPairSwap) 191 192 pairFiltererContract, err := traderjoeILBPair.NewILBPairFilterer(pairAddress, tjs.WsClient) 193 if err != nil { 194 log.Fatal(err) 195 } 196 _, err = pairFiltererContract.WatchSwap(&bind.WatchOpts{}, sink, []common.Address{}, []common.Address{}) 197 if err != nil { 198 log.Error("error in get swaps channel: ", err) 199 } 200 201 return sink, nil 202 } 203 204 func (tjs *TraderJoeScraper) normalizeTraderJoeSwap(swap traderjoeILBPair.ILBPairSwap) (normalizedSwap TraderJoeSwap) { 205 206 pair := MapOfPools[swap.Raw.Address.Hex()] 207 decimals0 := int(pair.Token0.Decimals) 208 decimals1 := int(pair.Token1.Decimals) 209 210 amount1In := new(big.Int).SetBytes(swap.AmountsIn[:16]) 211 amount0In := new(big.Int).SetBytes(swap.AmountsIn[16:]) 212 amount1Out := new(big.Int).SetBytes(swap.AmountsOut[:16]) 213 amount0Out := new(big.Int).SetBytes(swap.AmountsOut[16:]) 214 var amount0, amount1 float64 215 216 if amount0In.Cmp(big.NewInt(0)) == 1 { 217 amount0, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(amount0In), new(big.Float).SetFloat64(math.Pow10(decimals0))).Float64() 218 } else { 219 amount0, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(amount0Out), new(big.Float).SetFloat64(math.Pow10(decimals0))).Float64() 220 } 221 if amount1In.Cmp(big.NewInt(0)) == 1 { 222 amount1, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(amount1In), new(big.Float).SetFloat64(math.Pow10(decimals1))).Float64() 223 } else { 224 amount1, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(amount1Out), new(big.Float).SetFloat64(math.Pow10(decimals1))).Float64() 225 } 226 227 normalizedSwap = TraderJoeSwap{ 228 ID: swap.Raw.TxHash.Hex(), 229 Timestamp: time.Now().UnixNano(), 230 Pair: pair, 231 Amount0: amount0, 232 Amount1: amount1, 233 } 234 return 235 } 236 237 // mainLoop is the central loop of the Trader Joe scraper that manages the subscription and scraping of pairs. 238 // It initializes the process by retrieving reverse base tokens and quote tokens from configuration. After a brief 239 // initial delay, it sets the `run` flag to true and kicks off a goroutine to feed pools to subscriptions. 240 // The function then listens for incoming pairs from the `pairReceived` channel and subscribes to and scrapes data 241 // for each pair. It performs various checks to skip pairs that don't meet certain criteria, such as blacklisted 242 // tokens or pools. It also logs relevant information about the progress of the loop. 243 func (tjs *TraderJoeScraper) mainLoop() { 244 var err error 245 reverseBasetokens, err = getReverseTokensFromConfig("traderjoe/reverse_tokens/" + tjs.exchangeName + "Basetoken") 246 if err != nil { 247 log.Error("error getting base tokens for which pairs should be reversed: ", err) 248 } 249 log.Infof("reverse the following basetokens on %s: %v", tjs.exchangeName, reverseBasetokens) 250 reverseQuotetokens, err = getReverseTokensFromConfig("traderjoe/reverse_tokens/" + tjs.exchangeName + "Quotetoken") 251 if err != nil { 252 log.Error("error getting quote tokens for which pairs should be reversed: ", err) 253 } 254 log.Infof("reverse the following quotetokens on %s: %v", tjs.exchangeName, reverseQuotetokens) 255 256 time.Sleep(4 * time.Second) 257 tjs.run = true 258 259 go func() { 260 pools := tjs.feedPoolsToSubscriptions() 261 log.Info("Found ", len(pools), " pairs") 262 log.Info("Found ", len(tjs.pairScrapers), " pairScrapers") 263 }() 264 265 if len(tjs.pairScrapers) == 0 { 266 tjs.error = errors.New("traderjoe scraper: No pairs to scrape provided") 267 log.Error(tjs.error.Error()) 268 } 269 270 count := 0 271 for { 272 pool := <-tjs.pairReceived 273 log.Infoln("Subscribing for pair: ", pool) 274 275 if len(pool.Token0.Symbol) < 2 || len(pool.Token1.Symbol) < 2 { 276 log.Info("skip pair: ", pool.ForeignName) 277 continue 278 } 279 if helpers.AddressIsBlacklisted(pool.Token0.Address) || helpers.AddressIsBlacklisted(pool.Token1.Address) { 280 log.Info("skip pair ", pool.ForeignName, ", address is blacklisted") 281 continue 282 } 283 if helpers.PoolIsBlacklisted(pool.Address) { 284 log.Info("skip blacklisted pool ", pool.Address) 285 continue 286 } 287 log.Infof("%v found pair scraper for: %s with address %s", count, pool.ForeignName, pool.Address.Hex()) 288 count++ 289 290 sink, err := tjs.GetSwapsChannel(pool.Address) 291 if err != nil { 292 log.Error("error fetching swaps channel: ", err) 293 } 294 go func() { 295 for { 296 rawSwap, ok := <-sink 297 if ok { 298 swap := tjs.normalizeTraderJoeSwap(*rawSwap) 299 tjs.sendTrade(swap, pool) 300 } 301 } 302 }() 303 304 } 305 } 306 307 // makeTraderJoePoolMap generates a map of Trader Joe pool pairs based on the provided liquidity thresholds and configuration. 308 // It retrieves pool information either by specific addresses from a JSON file or by querying the database for all pools above 309 // the liquidity threshold. The resulting pool map includes pairs with sufficient liquidity and handles lower-bound checks. 310 // It returns the generated pool map and any error encountered during the process. 311 func (tjs *TraderJoeScraper) makeTraderJoePoolMap(liquidityThreshold, liquidityThresholdUSD float64) (map[string]TraderJoePair, error) { 312 poolMap := make(map[string]TraderJoePair) 313 var ( 314 pools []dia.Pool 315 err error 316 ) 317 318 if tjs.listenByAddress { 319 // Only load pool info for addresses from json file. 320 poolAddresses, errAddr := getTradeAddressesFromConfig("traderjoe/subscribe_pools/" + tjs.exchangeName) 321 if errAddr != nil { 322 log.Error("fetch pool addresses from config file: ", errAddr) 323 } 324 for _, address := range poolAddresses { 325 pool, errPool := tjs.relDB.GetPoolByAddress(Exchanges[tjs.exchangeName].BlockChain.Name, address.Hex()) 326 if errPool != nil { 327 log.Fatalf("Get pool with address %s: %v", address.Hex(), errPool) 328 } 329 pools = append(pools, pool) 330 } 331 } else { 332 // Load all pools above liquidity threshold. 333 pools, err = tjs.relDB.GetAllPoolsExchange(tjs.exchangeName, liquidityThreshold) 334 if err != nil { 335 return poolMap, err 336 } 337 } 338 339 log.Info("Found ", len(pools), " pools.") 340 log.Info("make pool map...") 341 lowerBoundCount := 0 342 for _, pool := range pools { 343 if len(pool.Assetvolumes) != 2 { 344 continue 345 } 346 liquidity, lowerBound := pool.GetPoolLiquidityUSD() 347 // Discard pool if complete USD liquidity is below threshold. 348 if !lowerBound && liquidity < liquidityThresholdUSD { 349 continue 350 } 351 if lowerBound { 352 lowerBoundCount++ 353 } 354 355 up := TraderJoePair{ 356 Address: common.HexToAddress(pool.Address), 357 } 358 if pool.Assetvolumes[0].Index == 0 { 359 up.Token0 = asset2TraderJoeAsset(pool.Assetvolumes[0].Asset) 360 up.Token1 = asset2TraderJoeAsset(pool.Assetvolumes[1].Asset) 361 } else { 362 up.Token0 = asset2TraderJoeAsset(pool.Assetvolumes[1].Asset) 363 up.Token1 = asset2TraderJoeAsset(pool.Assetvolumes[0].Asset) 364 } 365 up.ForeignName = up.Token0.Symbol + "-" + up.Token1.Symbol 366 poolMap[pool.Address] = up 367 } 368 369 log.Infof("found %v subscribable pools.", len(poolMap)) 370 log.Infof("%v pools with lowerBound=true.", lowerBoundCount) 371 372 return poolMap, err 373 } 374 375 // sendTrade receives Trader Joe trade data and transforms it into a standardized dia.Trade 376 // structure for further analysis and publication. 377 func (tjs *TraderJoeScraper) sendTrade(traderjoeswap TraderJoeSwap, pool *TraderJoePair) { 378 price, volume := tjs.getTradeData(traderjoeswap) 379 token0 := dia.Asset{ 380 Address: pool.Token0.Address.Hex(), 381 Symbol: pool.Token0.Symbol, 382 Name: pool.Token0.Name, 383 Decimals: pool.Token0.Decimals, 384 Blockchain: Exchanges[tjs.exchangeName].BlockChain.Name, 385 } 386 token1 := dia.Asset{ 387 Address: pool.Token1.Address.Hex(), 388 Symbol: pool.Token1.Symbol, 389 Name: pool.Token1.Name, 390 Decimals: pool.Token1.Decimals, 391 Blockchain: Exchanges[tjs.exchangeName].BlockChain.Name, 392 } 393 394 t := &dia.Trade{ 395 Symbol: pool.Token0.Symbol, 396 Pair: pool.ForeignName, 397 QuoteToken: token0, 398 BaseToken: token1, 399 Price: price, 400 Volume: volume, 401 Time: time.Unix(0, traderjoeswap.Timestamp), 402 PoolAddress: pool.Address.Hex(), 403 ForeignTradeID: traderjoeswap.ID, 404 //EstimatedUSDPrice: 0, 405 Source: tjs.exchangeName, 406 VerifiedPair: true, 407 } 408 409 switch { 410 case utils.Contains(reverseBasetokens, pool.Token1.Address.Hex()): 411 // If we need quotation of a base token, reverse pair 412 tSwapped, err := dia.SwapTrade(*t) 413 if err == nil { 414 t = &tSwapped 415 } 416 case utils.Contains(reverseQuotetokens, pool.Token0.Address.Hex()): 417 // If we need quotation of a base token, reverse pair 418 tSwapped, err := dia.SwapTrade(*t) 419 if err == nil { 420 t = &tSwapped 421 } 422 } 423 if price > 0 { 424 log.Infof("Got trade on pool %s: %v", pool.Address.Hex(), t) 425 tjs.chanTrades <- t 426 } 427 } 428 429 // TraderJoeTradeScraper represents a scraper for collecting trade data associated with a specific dia.ExchangePair 430 // within the Trader Joe exchange. 431 type TraderJoeTradeScraper struct { 432 parent *TraderJoeScraper 433 pair dia.ExchangePair 434 } 435 436 func (tjs *TraderJoeScraper) FetchAvailablePairs() ([]dia.ExchangePair, error) { 437 return []dia.ExchangePair{}, nil 438 } 439 440 func (tjs *TraderJoeScraper) FillSymbolData(symbol string) (dia.Asset, error) { 441 return dia.Asset{Symbol: symbol}, nil 442 } 443 444 func (tjs *TraderJoeScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 445 return pair, nil 446 } 447 448 // Close closes any existing API connections, as well as channels of PairScrapers from calls to ScrapePair 449 func (tjs *TraderJoeScraper) Close() error { 450 if tjs.closed { 451 return errors.New("TraderJoeScraper: Already closed") 452 } 453 tjs.WsClient.Close() 454 tjs.RestClient.Close() 455 close(tjs.shutdown) 456 <-tjs.shutdownDone 457 tjs.errorLock.RLock() 458 defer tjs.errorLock.RUnlock() 459 return tjs.error 460 } 461 462 // getTradeData extracts price and volume data from TraderJoe trade information. 463 func (tjs *TraderJoeScraper) getTradeData(swap TraderJoeSwap) (price, volume float64) { 464 volume = swap.Amount0 465 price = math.Abs(swap.Amount1 / swap.Amount0) 466 return 467 } 468 469 // ScrapePair initiates a new scraping process for the specified dia.ExchangePair within the Trader Joe scraper. 470 // It checks for any previously encountered errors using a read lock on the error lock. If an error is present, 471 // it returns that error. Additionally, if the Trader Joe scraper has been closed, it returns an error indicating 472 // that ScrapePair cannot be called on a closed pair. Otherwise, it creates a new TraderJoeTradeScraper instance 473 // associated with the provided ExchangePair, adds it to the list of active pair scrapers, and returns it along 474 // with a nil error to indicate successful initiation. 475 func (tjs *TraderJoeScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 476 tjs.errorLock.RLock() 477 defer tjs.errorLock.RUnlock() 478 if tjs.error != nil { 479 return nil, tjs.error 480 } 481 482 if tjs.closed { 483 return nil, errors.New("TraderJoeScraper: Call Scrape Pair on closed pair") 484 } 485 486 pairScraper := &TraderJoeTradeScraper{ 487 parent: tjs, 488 pair: pair, 489 } 490 491 tjs.pairScrapers[pair.ForeignName] = pairScraper 492 493 return pairScraper, nil 494 } 495 496 // Close closes the TraderJoeTradeScraper instance. 497 func (ps TraderJoeTradeScraper) Close() error { 498 return nil 499 } 500 501 // Error returns the error associated with the parent Trader Joe scraper. It retrieves the error from the parent scraper's state 502 // using a read lock on the error lock. This function is useful for obtaining any error that occurred during scraping tasks. 503 func (ps TraderJoeTradeScraper) Error() error { 504 tjs := ps.parent 505 tjs.errorLock.RLock() 506 defer tjs.errorLock.RUnlock() 507 return tjs.error 508 } 509 510 // Pair returns the dia.ExchangePair associated with the current Trader Joe trade scraper. 511 // It simply retrieves and returns the ExchangePair stored within the scraper's state. 512 func (ps TraderJoeTradeScraper) Pair() dia.ExchangePair { 513 return ps.pair 514 } 515 516 // Channel returns a channel that can be used to receive trades 517 func (tjs *TraderJoeScraper) Channel() chan *dia.Trade { 518 return tjs.chanTrades 519 } 520 521 // asset2TraderJoeAsset converts a dia.Asset into a TraderJoeTokens structure. 522 // It takes the provided asset's address, decimals, symbol, and name, 523 // and returns a TraderJoeTokens representation containing the same information. 524 func asset2TraderJoeAsset(asset dia.Asset) TraderJoeTokens { 525 return TraderJoeTokens{ 526 Address: common.HexToAddress(asset.Address), 527 Decimals: asset.Decimals, 528 Symbol: asset.Symbol, 529 Name: asset.Name, 530 } 531 } 532 533 // getTradeAddressesFromConfig reads a JSON configuration file specified by the provided filename and retrieves 534 // trading pair addresses. The function opens and reads the file, unmarshals the data to extract pairs' addresses 535 // and foreign names, and returns a slice of common.Address containing the extracted addresses. In case of any 536 func getTradeAddressesFromConfig(filename string) (pairAddresses []common.Address, err error) { 537 538 // Load file and read data 539 fileHandle := configCollectors.ConfigFileConnectors(filename, ".json") 540 jsonFile, err := os.Open(fileHandle) 541 if err != nil { 542 return 543 } 544 defer func() { 545 err = jsonFile.Close() 546 if err != nil { 547 log.Error(err) 548 } 549 }() 550 551 byteData, err := io.ReadAll(jsonFile) 552 if err != nil { 553 return 554 } 555 556 // Unmarshal read data 557 type scrapedPair struct { 558 Address string `json:"Address"` 559 ForeignName string `json:"ForeignName"` 560 } 561 type scrapedPairList struct { 562 AllPairs []scrapedPair `json:"Pools"` 563 } 564 var allPairs scrapedPairList 565 err = json.Unmarshal(byteData, &allPairs) 566 if err != nil { 567 return 568 } 569 570 // Extract addresses 571 for _, token := range allPairs.AllPairs { 572 pairAddresses = append(pairAddresses, common.HexToAddress(token.Address)) 573 } 574 575 return 576 } 577 578 // feedPoolsToSubscriptions sends a list of TraderJoePairs to subscription channels. 579 func (tjs *TraderJoeScraper) feedPoolsToSubscriptions() (pairs []TraderJoePair) { 580 for i := range MapOfPools { 581 up := MapOfPools[i] 582 pairs = append(pairs, up) 583 tjs.pairReceived <- &up 584 } 585 return 586 }