github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/MaverickScraper.go (about) 1 package scrapers 2 3 import ( 4 "context" 5 "errors" 6 "math" 7 "math/big" 8 "strconv" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/diadata-org/diadata/pkg/dia" 14 "github.com/diadata-org/diadata/pkg/dia/helpers" 15 "github.com/diadata-org/diadata/pkg/dia/helpers/ethhelper" 16 pairfactorycontract "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/maverick/pairfactory" 17 18 poolcontract "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/maverick/pool" 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 maverickPoolMap = make(map[string]MaverickPair) 27 28 const ( 29 factoryContractAddressDeploymentBlockEth = uint64(17210221) 30 maverickWaitMilliseconds = "25" 31 ) 32 33 type MaverickScraper struct { 34 WsClient *ethclient.Client 35 RestClient *ethclient.Client 36 relDB *models.RelDB 37 // signaling channels for session initialization and finishing 38 run bool 39 shutdown chan nothing 40 shutdownDone chan nothing 41 // error handling; to read error or closed, first acquire read lock 42 // only cleanup method should hold write lock 43 errorLock sync.RWMutex 44 error error 45 closed bool 46 // used to keep track of trading pairs that we subscribed to 47 pairScrapers map[string]*MaverickPairScraper 48 exchangeName string 49 blockchain string 50 poolFactoryContractAddress string 51 poolFactoryContractCreationBlock uint64 52 chanTrades chan *dia.Trade 53 waitTime int 54 // If true, only pairs given in config file are scraped. Default is false. 55 listenByAddress bool 56 fetchPoolsFromDB bool 57 } 58 59 type Token struct { 60 Address common.Address 61 Symbol string 62 Decimals uint8 63 Name string 64 } 65 66 type MaverickPair struct { 67 Token0 Token 68 Token1 Token 69 ForeignName string 70 Address common.Address 71 } 72 73 // NewMaverickScraper returns a new MaverickScraper for the given pair 74 func NewMaverickScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *MaverickScraper { 75 log.Info("NewMaverickScraper: ", exchange.Name) 76 var ( 77 s *MaverickScraper 78 listenByAddress bool 79 fetchPoolsFromDB bool 80 err error 81 ) 82 83 listenByAddress, err = strconv.ParseBool(utils.Getenv("LISTEN_BY_ADDRESS", "")) 84 if err != nil { 85 log.Fatal("parse LISTEN_BY_ADDRESS: ", err) 86 } 87 88 fetchPoolsFromDB, err = strconv.ParseBool(utils.Getenv("FETCH_POOLS_FROM_DB", "")) 89 if err != nil { 90 log.Fatal("parse FETCH_POOLS_FROM_DB: ", err) 91 } 92 93 switch exchange.Name { 94 case dia.MaverickExchange: 95 s = makeMaverickScraper(exchange, listenByAddress, fetchPoolsFromDB, restDialEth, wsDialEth, maverickWaitMilliseconds, factoryContractAddressDeploymentBlockEth) 96 //case dia.MaverickExchangeBNB: 97 // s = makeMaverickScraper(exchange, listenByAddress, fetchPoolsFromDB, restDialEth, wsDialEth, maverickWaitMilliseconds) 98 //case dia.MaverickExchangeZKSync: 99 // s = makeMaverickScraper(exchange, listenByAddress, fetchPoolsFromDB, restDialEth, wsDialEth, maverickWaitMilliseconds) 100 } 101 102 s.relDB = relDB 103 104 // Only include pools with (minimum) liquidity bigger than given env var. 105 liquidityThreshold, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD", ""), 64) 106 if err != nil { 107 liquidityThreshold = float64(0) 108 log.Warnf("parse liquidity threshold: %v. Set to default %v", err, liquidityThreshold) 109 } 110 // Only include pools with (minimum) liquidity USD value bigger than given env var. 111 liquidityThresholdUSD, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD_USD", ""), 64) 112 if err != nil { 113 liquidityThresholdUSD = float64(0) 114 log.Warnf("parse liquidity threshold: %v. Set to default %v", err, liquidityThresholdUSD) 115 } 116 117 // Fetch all pool with given liquidity threshold from database. 118 maverickPoolMap, err = s.makePoolMap(liquidityThreshold, liquidityThresholdUSD) 119 if err != nil { 120 log.Fatal("build poolMap: ", err) 121 } 122 123 if scrape { 124 go s.mainLoop() 125 } 126 return s 127 } 128 129 // makeMaverickScraper returns a maverick scraper as used in NewUniswapScraper. 130 func makeMaverickScraper(exchange dia.Exchange, listenByAddress bool, fetchPoolsFromDB bool, restDial string, wsDial string, waitMilliseconds string, factoryContractDeploymentBlock uint64) *MaverickScraper { 131 var ( 132 restClient, wsClient *ethclient.Client 133 err error 134 s *MaverickScraper 135 waitTime int 136 ) 137 138 log.Infof("Init rest and ws client for %s.", exchange.BlockChain.Name) 139 restClient, err = ethclient.Dial(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_REST", restDial)) 140 if err != nil { 141 log.Fatal("init rest client: ", err) 142 } 143 wsClient, err = ethclient.Dial(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_WS", wsDial)) 144 if err != nil { 145 log.Fatal("init ws client: ", err) 146 } 147 148 waitTime, err = strconv.Atoi(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_WAIT_TIME", waitMilliseconds)) 149 if err != nil { 150 log.Error("could not parse wait time: ", err) 151 waitTime = 5000 152 } 153 154 s = &MaverickScraper{ 155 WsClient: wsClient, 156 RestClient: restClient, 157 shutdown: make(chan nothing), 158 shutdownDone: make(chan nothing), 159 pairScrapers: make(map[string]*MaverickPairScraper), 160 exchangeName: exchange.Name, 161 error: nil, 162 chanTrades: make(chan *dia.Trade), 163 waitTime: waitTime, 164 listenByAddress: listenByAddress, 165 fetchPoolsFromDB: fetchPoolsFromDB, 166 blockchain: exchange.BlockChain.Name, 167 poolFactoryContractAddress: exchange.Contract, 168 poolFactoryContractCreationBlock: factoryContractDeploymentBlock, 169 } 170 return s 171 } 172 173 func (s *MaverickScraper) mainLoop() { 174 175 // Import tokens which appear as base token and we need a quotation for 176 var err error 177 reverseBasetokens, err = getReverseTokensFromConfig("maverick/reverse_tokens/" + s.exchangeName + "Basetoken") 178 if err != nil { 179 log.Error("error getting tokens for which pairs should be reversed: ", err) 180 } 181 log.Info("reverse basetokens: ", reverseBasetokens) 182 reverseQuotetokens, err = getReverseTokensFromConfig("maverick/reverse_tokens/" + s.exchangeName + "Quotetoken") 183 if err != nil { 184 log.Error("error getting tokens for which pairs should be reversed: ", err) 185 } 186 log.Info("reverse quotetokens: ", reverseQuotetokens) 187 188 // wait for all pairs have added into s.PairScrapers 189 time.Sleep(4 * time.Second) 190 s.run = true 191 192 if s.listenByAddress || s.fetchPoolsFromDB { 193 var wg sync.WaitGroup 194 for address := range maverickPoolMap { 195 time.Sleep(time.Duration(s.waitTime) * time.Millisecond) 196 wg.Add(1) 197 go func(address common.Address, w *sync.WaitGroup) { 198 defer w.Done() 199 s.ListenToPair(address) 200 }(common.HexToAddress(address), &wg) 201 } 202 203 } else { 204 addresses, err := s.getAllPairsAddress() 205 if err != nil { 206 log.Fatal(err) 207 } 208 log.Info("Found ", len(addresses), " pairs") 209 log.Info("Found ", len(s.pairScrapers), " pairScrapers") 210 211 if len(s.pairScrapers) == 0 { 212 s.error = errors.New("maverick: No pairs to scrap provided") 213 log.Error(s.error.Error()) 214 } 215 216 var wg sync.WaitGroup 217 for _, address := range addresses { 218 time.Sleep(time.Duration(s.waitTime) * time.Millisecond) 219 wg.Add(1) 220 go func(address common.Address, w *sync.WaitGroup) { 221 defer w.Done() 222 s.ListenToPair(address) 223 }(address, &wg) 224 } 225 wg.Wait() 226 227 } 228 } 229 230 // ListenToPair subscribes to a uniswap pool. 231 // If @byAddress is true, it listens by pool address, otherwise by index. 232 func (s *MaverickScraper) ListenToPair(address common.Address) { 233 var ( 234 pair MaverickPair 235 err error 236 ) 237 pair = maverickPoolMap[address.Hex()] 238 if !s.listenByAddress && !s.fetchPoolsFromDB { 239 //Get pool info from on-chain. @poolMap is empty. 240 pair, err = s.getPairByAddress(address) 241 if err != nil { 242 log.Error("error fetching pair: ", err) 243 return 244 } 245 } else { 246 // Relevant pool info is retrieved from @poolMap. 247 pair = maverickPoolMap[address.Hex()] 248 } 249 250 if len(pair.Token0.Symbol) < 2 || len(pair.Token1.Symbol) < 2 { 251 log.Info("skip pair: ", pair.ForeignName) 252 return 253 } 254 255 if helpers.AddressIsBlacklisted(pair.Token0.Address) || helpers.AddressIsBlacklisted(pair.Token1.Address) { 256 log.Info("skip pair ", pair.ForeignName, ", address is blacklisted") 257 return 258 } 259 if helpers.PoolIsBlacklisted(pair.Address) { 260 log.Info("skip blacklisted pool ", pair.Address) 261 return 262 } 263 264 log.Info("add pair scraper for: ", pair.ForeignName, " with address ", pair.Address.Hex()) 265 sink, err := s.GetSwapsChannel(pair.Address) 266 if err != nil { 267 log.Error("error fetching swaps channel: ", err) 268 } 269 270 go func() { 271 for { 272 rawSwap, ok := <-sink 273 if ok { 274 price, volume := getPriceAndVolumeFromRawSwapData(rawSwap, pair) 275 token0 := dia.Asset{ 276 Address: pair.Token0.Address.Hex(), 277 Symbol: pair.Token0.Symbol, 278 Name: pair.Token0.Name, 279 Decimals: pair.Token0.Decimals, 280 Blockchain: Exchanges[s.exchangeName].BlockChain.Name, 281 } 282 token1 := dia.Asset{ 283 Address: pair.Token1.Address.Hex(), 284 Symbol: pair.Token1.Symbol, 285 Name: pair.Token1.Name, 286 Decimals: pair.Token1.Decimals, 287 Blockchain: Exchanges[s.exchangeName].BlockChain.Name, 288 } 289 t := &dia.Trade{ 290 Symbol: pair.Token0.Symbol, 291 Pair: pair.ForeignName, 292 Price: price, 293 Volume: volume, 294 BaseToken: token1, 295 QuoteToken: token0, 296 Time: time.Unix(time.Now().Unix(), 0), 297 PoolAddress: rawSwap.Raw.Address.Hex(), 298 ForeignTradeID: rawSwap.Raw.TxHash.Hex(), 299 Source: s.exchangeName, 300 VerifiedPair: true, 301 } 302 303 // TO DO: Refactor approach for reversing pairs. 304 switch { 305 case utils.Contains(reverseBasetokens, pair.Token1.Address.Hex()): 306 // If we need quotation of a base token, reverse pair 307 tSwapped, err := dia.SwapTrade(*t) 308 if err == nil { 309 t = &tSwapped 310 } 311 case utils.Contains(reverseQuotetokens, pair.Token0.Address.Hex()): 312 // If we don't need quotation of quote token, reverse pair. 313 tSwapped, err := dia.SwapTrade(*t) 314 if err == nil { 315 t = &tSwapped 316 } 317 } 318 if price > 0 { 319 log.Info("tx hash: ", rawSwap.Raw.TxHash.Hex()) 320 log.Infof("Got trade at time %v - symbol: %s, pair: %s, price: %v, volume:%v", t.Time, t.Symbol, t.Pair, t.Price, t.Volume) 321 s.chanTrades <- t 322 } 323 } 324 } 325 }() 326 } 327 328 func (s *MaverickScraper) getPairByAddress(pairAddress common.Address) (MaverickPair, error) { 329 var ( 330 poolContractInstance *poolcontract.PoolCaller 331 token0 dia.Asset 332 token1 dia.Asset 333 ) 334 335 connection := s.RestClient 336 poolContractInstance, err := poolcontract.NewPoolCaller(pairAddress, connection) 337 if err != nil { 338 log.Error(err) 339 return MaverickPair{}, err 340 } 341 342 // Getting tokens from pair 343 address0, _ := poolContractInstance.TokenA(&bind.CallOpts{}) 344 address1, _ := poolContractInstance.TokenB(&bind.CallOpts{}) 345 346 // Only fetch assets from on-chain in case they are not in our DB. 347 token0, err = s.relDB.GetAsset(address0.Hex(), s.blockchain) 348 if err != nil { 349 token0, err = ethhelper.ETHAddressToAsset(address0, s.RestClient, s.blockchain) 350 if err != nil { 351 return MaverickPair{}, err 352 } 353 } 354 token1, err = s.relDB.GetAsset(address1.Hex(), s.blockchain) 355 if err != nil { 356 token1, err = ethhelper.ETHAddressToAsset(address1, s.RestClient, s.blockchain) 357 if err != nil { 358 return MaverickPair{}, err 359 } 360 } 361 362 pair := MaverickPair{ 363 Token0: Token{ 364 Address: common.HexToAddress(token0.Address), 365 Symbol: token0.Symbol, 366 Decimals: token0.Decimals, 367 Name: token0.Name, 368 }, 369 Token1: Token{ 370 Address: common.HexToAddress(token1.Address), 371 Symbol: token1.Symbol, 372 Decimals: token1.Decimals, 373 Name: token1.Name, 374 }, 375 ForeignName: token0.Symbol + "-" + token1.Symbol, 376 Address: pairAddress, 377 } 378 379 return pair, nil 380 } 381 382 // getVolumeAndPriceFromRawSwapData returns price, volume and sell/buy information of @swap 383 func getPriceAndVolumeFromRawSwapData(swap *poolcontract.PoolSwap, pair MaverickPair) (price, volume float64) { 384 decimals0 := int(pair.Token0.Decimals) 385 decimals1 := int(pair.Token1.Decimals) 386 387 if swap.TokenAIn { 388 amount0In, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(swap.AmountIn), new(big.Float).SetFloat64(math.Pow10(decimals0))).Float64() 389 amount1Out, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(swap.AmountOut), new(big.Float).SetFloat64(math.Pow10(decimals1))).Float64() 390 volume = -amount0In 391 price = amount1Out / amount0In 392 return 393 } 394 395 amount1In, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(swap.AmountIn), new(big.Float).SetFloat64(math.Pow10(decimals1))).Float64() 396 amount0Out, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(swap.AmountOut), new(big.Float).SetFloat64(math.Pow10(decimals0))).Float64() 397 volume = amount0Out 398 price = amount1In / amount0Out 399 return 400 } 401 402 // makePoolMap returns a map with pool addresses as keys and the underlying MaverickPool as values. 403 // If s.listenByAddress is true, it only loads the corresponding assets from the list. 404 func (s *MaverickScraper) makePoolMap(liquiThreshold float64, liquidityThresholdUSD float64) (map[string]MaverickPair, error) { 405 pm := make(map[string]MaverickPair) 406 var ( 407 pools []dia.Pool 408 err error 409 ) 410 411 if s.listenByAddress { 412 // Only load pool info for addresses from json file. 413 poolAddresses, errAddr := getAddressesFromConfig("maverick/subscribe_pools/" + s.exchangeName) 414 if errAddr != nil { 415 log.Error("fetch pool addresses from config file: ", errAddr) 416 } 417 for _, address := range poolAddresses { 418 pool, errPool := s.relDB.GetPoolByAddress(Exchanges[s.exchangeName].BlockChain.Name, address.Hex()) 419 if errPool != nil { 420 log.Fatalf("Get pool with address %s: %v", address.Hex(), errPool) 421 } 422 pools = append(pools, pool) 423 } 424 } else if s.fetchPoolsFromDB { 425 // Load all pools above liqui threshold. 426 pools, err = s.relDB.GetAllPoolsExchange(s.exchangeName, liquiThreshold) 427 if err != nil { 428 return pm, err 429 } 430 } else { 431 // Pool info will be fetched from on-chain and poolMap is not needed. 432 return pm, nil 433 } 434 435 log.Info("Found ", len(pools), " pools.") 436 log.Info("make pool map...") 437 lowerBoundCount := 0 438 for _, pool := range pools { 439 if len(pool.Assetvolumes) != 2 { 440 log.Warn("not enough assets in pool with address: ", pool.Address) 441 continue 442 } 443 444 liquidity, lowerBound := pool.GetPoolLiquidityUSD() 445 // Discard pool if complete USD liquidity is below threshold. 446 if !lowerBound && liquidity < liquidityThresholdUSD { 447 continue 448 } 449 if lowerBound { 450 lowerBoundCount++ 451 } 452 453 maverickPair := MaverickPair{ 454 Address: common.HexToAddress(pool.Address), 455 } 456 if pool.Assetvolumes[0].Index == 0 { 457 maverickPair.Token0 = asset2MaverickAsset(pool.Assetvolumes[0].Asset) 458 maverickPair.Token1 = asset2MaverickAsset(pool.Assetvolumes[1].Asset) 459 } else { 460 maverickPair.Token0 = asset2MaverickAsset(pool.Assetvolumes[1].Asset) 461 maverickPair.Token1 = asset2MaverickAsset(pool.Assetvolumes[0].Asset) 462 } 463 maverickPair.ForeignName = maverickPair.Token0.Symbol + "-" + maverickPair.Token1.Symbol 464 pm[pool.Address] = maverickPair 465 } 466 467 log.Infof("found %v subscribable pools.", len(pm)) 468 log.Infof("%v pools with lowerBound=true.", lowerBoundCount) 469 return pm, err 470 } 471 472 func asset2MaverickAsset(asset dia.Asset) Token { 473 return Token{ 474 Address: common.HexToAddress(asset.Address), 475 Decimals: asset.Decimals, 476 Symbol: asset.Symbol, 477 Name: asset.Name, 478 } 479 } 480 481 // GetSwapsChannel returns a channel for swaps of the pair with address @pairAddress 482 func (s *MaverickScraper) GetSwapsChannel(pairAddress common.Address) (chan *poolcontract.PoolSwap, error) { 483 484 sink := make(chan *poolcontract.PoolSwap) 485 var poolFiltererContract *poolcontract.PoolFilterer 486 poolFiltererContract, err := poolcontract.NewPoolFilterer(pairAddress, s.WsClient) 487 if err != nil { 488 log.Fatal(err) 489 } 490 491 _, err = poolFiltererContract.WatchSwap(&bind.WatchOpts{}, sink) 492 if err != nil { 493 log.Error("error in get swaps channel: ", err) 494 } 495 496 return sink, nil 497 498 } 499 500 func (s *MaverickScraper) getAllPairsAddress() ([]common.Address, error) { 501 pools := make([]common.Address, 0) 502 503 var factoryContractInstance *pairfactorycontract.PairfactoryFilterer 504 factoryContractInstance, err := pairfactorycontract.NewPairfactoryFilterer(common.HexToAddress(s.poolFactoryContractAddress), s.RestClient) 505 if err != nil { 506 log.Error(err) 507 return pools, err 508 } 509 510 currBlock, err := s.RestClient.BlockNumber(context.Background()) 511 if err != nil { 512 return nil, err 513 } 514 515 var offset uint64 = 2500 516 startBlock := s.poolFactoryContractCreationBlock 517 var endBlock = startBlock + offset 518 519 for { 520 if endBlock > currBlock { 521 endBlock = currBlock 522 } 523 log.Infof("startblock - endblock: %v --- %v ", startBlock, endBlock) 524 525 it, err := factoryContractInstance.FilterPoolCreated( 526 &bind.FilterOpts{ 527 Start: startBlock, 528 End: &endBlock, 529 }) 530 if err != nil { 531 log.Error(err) 532 //return pools, err 533 if endBlock == currBlock { 534 break 535 } 536 537 startBlock = endBlock + 1 538 endBlock = endBlock + offset 539 continue 540 } 541 542 for it.Next() { 543 pools = append(pools, it.Event.PoolAddress) 544 } 545 if err := it.Close(); err != nil { 546 log.Warn("closing iterator: ", it) 547 } 548 549 if endBlock == currBlock { 550 break 551 } 552 553 startBlock = endBlock + 1 554 endBlock = endBlock + offset 555 } 556 557 return pools, err 558 } 559 560 func (s *MaverickScraper) getAllPairs() ([]MaverickPair, error) { 561 pairs := make([]MaverickPair, 0) 562 addresses, err := s.getAllPairsAddress() 563 if err != nil { 564 log.Fatal(err) 565 } 566 for _, address := range addresses { 567 pair, err := s.getPairByAddress(address) 568 if err != nil { 569 log.Warn(err) 570 continue 571 } 572 pairs = append(pairs, pair) 573 } 574 return pairs, nil 575 } 576 577 // Close closes any existing API connections, as well as channels of 578 // PairScrapers from calls to ScrapePair 579 func (s *MaverickScraper) Close() error { 580 if s.closed { 581 return errors.New("UniswapScraper: Already closed") 582 } 583 s.WsClient.Close() 584 s.RestClient.Close() 585 close(s.shutdown) 586 <-s.shutdownDone 587 s.errorLock.RLock() 588 defer s.errorLock.RUnlock() 589 return s.error 590 } 591 592 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from 593 // this APIScraper 594 func (s *MaverickScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 595 s.errorLock.RLock() 596 defer s.errorLock.RUnlock() 597 if s.error != nil { 598 return nil, s.error 599 } 600 if s.closed { 601 return nil, errors.New("UniswapScraper: Call ScrapePair on closed scraper") 602 } 603 ps := &MaverickPairScraper{ 604 parent: s, 605 pair: pair, 606 } 607 s.pairScrapers[pair.ForeignName] = ps 608 return ps, nil 609 } 610 611 // FetchAvailablePairs returns a list with all available trade pairs as dia.ExchangePair for the pairDiscorvery service 612 func (s *MaverickScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 613 time.Sleep(100 * time.Millisecond) 614 maverickPairs, err := s.getAllPairs() 615 if err != nil { 616 return 617 } 618 for _, pair := range maverickPairs { 619 quotetoken := dia.Asset{ 620 Symbol: pair.Token0.Symbol, 621 Name: pair.Token0.Name, 622 Address: pair.Token0.Address.Hex(), 623 Decimals: pair.Token0.Decimals, 624 Blockchain: Exchanges[s.exchangeName].BlockChain.Name, 625 } 626 basetoken := dia.Asset{ 627 Symbol: pair.Token1.Symbol, 628 Name: pair.Token1.Name, 629 Address: pair.Token1.Address.Hex(), 630 Decimals: pair.Token1.Decimals, 631 Blockchain: Exchanges[s.exchangeName].BlockChain.Name, 632 } 633 pairToNormalise := dia.ExchangePair{ 634 Symbol: pair.Token0.Symbol, 635 ForeignName: pair.ForeignName, 636 Exchange: s.exchangeName, 637 Verified: true, 638 UnderlyingPair: dia.Pair{BaseToken: basetoken, QuoteToken: quotetoken}, 639 } 640 normalizedPair, _ := s.NormalizePair(pairToNormalise) 641 pairs = append(pairs, normalizedPair) 642 } 643 return 644 } 645 646 func (s *MaverickScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 647 return pair, nil 648 } 649 650 // FillSymbolData is not used by DEX scrapers. 651 func (s *MaverickScraper) FillSymbolData(symbol string) (dia.Asset, error) { 652 return dia.Asset{}, nil 653 } 654 655 // MaverickPairScraper implements PairScraper for Uniswap 656 type MaverickPairScraper struct { 657 parent *MaverickScraper 658 pair dia.ExchangePair 659 closed bool 660 } 661 662 // Close stops listening for trades of the pair associated with s 663 func (ps *MaverickPairScraper) Close() error { 664 ps.closed = true 665 return nil 666 } 667 668 // Channel returns a channel that can be used to receive trades 669 func (s *MaverickScraper) Channel() chan *dia.Trade { 670 return s.chanTrades 671 } 672 673 // Error returns an error when the channel Channel() is closed 674 // and nil otherwise 675 func (ps *MaverickPairScraper) Error() error { 676 s := ps.parent 677 s.errorLock.RLock() 678 defer s.errorLock.RUnlock() 679 return s.error 680 } 681 682 // Pair returns the pair this scraper is subscribed to 683 func (ps *MaverickPairScraper) Pair() dia.ExchangePair { 684 return ps.pair 685 }