github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/BalancerV2Scraper.go (about) 1 package scrapers 2 3 import ( 4 "context" 5 "fmt" 6 "math" 7 "math/big" 8 "sort" 9 "strconv" 10 "sync" 11 "time" 12 13 "golang.org/x/sync/errgroup" 14 15 "github.com/ethereum/go-ethereum/accounts/abi/bind" 16 "github.com/ethereum/go-ethereum/common" 17 "github.com/ethereum/go-ethereum/ethclient" 18 "github.com/pkg/errors" 19 "go.uber.org/ratelimit" 20 21 balancervault "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/balancerv2/vault" 22 models "github.com/diadata-org/diadata/pkg/model" 23 "github.com/diadata-org/diadata/pkg/utils" 24 25 "github.com/diadata-org/diadata/pkg/dia" 26 "github.com/diadata-org/diadata/pkg/dia/helpers/ethhelper" 27 ) 28 29 const ( 30 balancerV2RateLimitPerSec = 50 31 balancerV2FilterPageSize = 5000 32 balancerV2RestDial = "" 33 balancerV2WSDial = "" 34 ) 35 36 var ( 37 balancerV2VaultContract = "" 38 balancerV2StartBlockPoolRegister = 16896080 39 reverseBasetokensBalancer *[]string 40 reverseQuotetokensBalancer *[]string 41 ) 42 43 // BalancerV2Swap is a swap information 44 type BalancerV2Swap struct { 45 SellToken string 46 BuyToken string 47 SellVolume float64 48 BuyVolume float64 49 ID string 50 Timestamp int64 51 } 52 53 // BalancerV2Scraper is a scraper for Balancer V2 54 type BalancerV2Scraper struct { 55 rest *ethclient.Client 56 ws *ethclient.Client 57 rl ratelimit.Limiter 58 relDB *models.RelDB 59 60 // signaling channels for session initialization and finishing 61 shutdown chan nothing 62 shutdownDone chan nothing 63 signalShutdown sync.Once 64 signalShutdownDone sync.Once 65 66 // error handling; err should be read from error(), closed should be read from isClosed() 67 // those two methods implement RW lock 68 errMutex sync.RWMutex 69 err error 70 closedMutex sync.RWMutex 71 closed bool 72 73 // used to keep track of trading pairs that we subscribed to 74 pairScrapers map[string]*BalancerV2PairScraper 75 exchangeName string 76 chanTrades chan *dia.Trade 77 78 tokensMap map[string]dia.Asset 79 poolsMap map[[32]byte]common.Address 80 admissiblePools map[common.Address]struct{} 81 cachedAssets sync.Map // map[string]dia.Asset 82 } 83 84 // NewBalancerV2Scraper returns a Balancer V2 scraper 85 func NewBalancerV2Scraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BalancerV2Scraper { 86 balancerV2VaultContract = exchange.Contract 87 scraper := &BalancerV2Scraper{ 88 exchangeName: exchange.Name, 89 err: nil, 90 shutdown: make(chan nothing), 91 shutdownDone: make(chan nothing), 92 pairScrapers: make(map[string]*BalancerV2PairScraper), 93 chanTrades: make(chan *dia.Trade), 94 tokensMap: make(map[string]dia.Asset), 95 poolsMap: make(map[[32]byte]common.Address), 96 admissiblePools: make(map[common.Address]struct{}), 97 } 98 99 switch exchange.Name { 100 case dia.BalancerV2Exchange: 101 balancerV2StartBlockPoolRegister = 12272146 102 case dia.BalancerV2ExchangeArbitrum: 103 balancerV2StartBlockPoolRegister = 222832 104 case dia.BeetsExchange: 105 balancerV2StartBlockPoolRegister = 16896080 106 case dia.BalancerV2ExchangePolygon: 107 balancerV2StartBlockPoolRegister = 15832990 108 } 109 110 var err error 111 112 ws, err := ethclient.Dial(utils.Getenv("ETH_URI_WS", balancerV2WSDial)) 113 if err != nil { 114 log.Fatal("init ws client: ", err) 115 } 116 117 rest, err := ethclient.Dial(utils.Getenv("ETH_URI_REST", balancerV2RestDial)) 118 if err != nil { 119 log.Fatal("init rest client: ", err) 120 } 121 122 scraper.relDB = relDB 123 124 // Only include pools with (minimum) liquidity bigger than given env var. 125 liquidityThreshold, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD", "0"), 64) 126 if err != nil { 127 liquidityThreshold = float64(0) 128 log.Warnf("parse liquidity threshold: %v. Set to default %v", err, liquidityThreshold) 129 } 130 131 // Only include pools with (minimum) liquidity USD value bigger than given env var. 132 liquidityThresholdUSD, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD_USD", "0"), 64) 133 if err != nil { 134 liquidityThresholdUSD = float64(0) 135 log.Warnf("parse liquidity threshold: %v. Set to default %v", err, liquidityThresholdUSD) 136 } 137 138 scraper.fetchAdmissiblePools(liquidityThreshold, liquidityThresholdUSD) 139 140 scraper.ws = ws 141 scraper.rest = rest 142 scraper.rl = ratelimit.New(balancerV2RateLimitPerSec) 143 144 if scrape { 145 go scraper.mainLoop() 146 } 147 148 return scraper 149 } 150 151 func (s *BalancerV2Scraper) mainLoop() { 152 153 // Import tokens which appear as base token and we need a quotation for 154 var err error 155 reverseBasetokensBalancer, err = getReverseTokensFromConfig("balancer/reverse_tokens/" + s.exchangeName + "Basetoken") 156 if err != nil { 157 log.Error("error getting tokens for which pairs should be reversed: ", err) 158 } 159 log.Info("reverse basetokens: ", reverseBasetokensBalancer) 160 reverseQuotetokensBalancer, err = getReverseTokensFromConfig("balancer/reverse_tokens/" + s.exchangeName + "Quotetoken") 161 if err != nil { 162 log.Error("error getting tokens for which pairs should be reversed: ", err) 163 } 164 log.Info("reverse quotetokens: ", reverseQuotetokensBalancer) 165 166 defer s.cleanup() 167 168 filterer, err := balancervault.NewBalancerVaultFilterer(common.HexToAddress(balancerV2VaultContract), s.ws) 169 if err != nil { 170 s.setError(err) 171 log.Fatalf("%s: Cannot create vault filter, err=%s", s.exchangeName, err.Error()) 172 } 173 174 balancerVaultCaller, err := balancervault.NewBalancerVaultCaller(common.HexToAddress(balancerV2VaultContract), s.rest) 175 if err != nil { 176 log.Error("balancer vault caller: ", err) 177 } 178 179 currBlock, err := s.rest.BlockNumber(context.Background()) 180 if err != nil { 181 s.setError(err) 182 log.Fatalf("%s: Cannot get a current block number, err=%s", s.exchangeName, err.Error()) 183 } 184 185 sink := make(chan *balancervault.BalancerVaultSwap) 186 sub, err := filterer.WatchSwap(&bind.WatchOpts{Start: &currBlock}, sink, nil, nil, nil) 187 if err != nil { 188 s.setError(err) 189 log.Fatalf("%s: Cannot watch swap events, err=%s", s.exchangeName, err.Error()) 190 } 191 192 defer sub.Unsubscribe() 193 194 for { 195 select { 196 case <-s.shutdown: 197 log.Println("BalancerV2Scraper: Shutting down main loop") 198 case err := <-sub.Err(): 199 s.setError(err) 200 log.Errorf("BalancerV2Scraper: Subscription error, err=%s", err.Error()) 201 case event := <-sink: 202 203 // Fetch pool address in order to check admissibility. 204 poolAddress, ok := s.poolsMap[event.PoolId] 205 if !ok { 206 poolAddress, _, err = balancerVaultCaller.GetPool(&bind.CallOpts{}, event.PoolId) 207 if err != nil { 208 log.Error("get pool: ", err) 209 } 210 } 211 if _, ok = s.admissiblePools[poolAddress]; !ok { 212 log.Warnf("pool %s not admissible, skip trade.", poolAddress) 213 continue 214 } 215 216 assetIn, ok := s.tokensMap[event.TokenIn.Hex()] 217 if !ok { 218 asset, err := s.assetFromToken(event.TokenIn) 219 if err != nil { 220 log.Warnf("%s: Retrieving asset-in %s, err=%s", s.exchangeName, event.TokenIn.Hex(), err.Error()) 221 continue 222 } 223 s.tokensMap[asset.Address] = asset 224 assetIn = asset 225 } 226 227 assetOut, ok := s.tokensMap[event.TokenOut.Hex()] 228 if !ok { 229 asset, err := s.assetFromToken(event.TokenOut) 230 if err != nil { 231 log.Warnf("%s: Retrieving asset-out %s, err=%s", s.exchangeName, event.TokenOut.Hex(), err.Error()) 232 continue 233 } 234 s.tokensMap[asset.Address] = asset 235 assetOut = asset 236 } 237 238 decimalsIn := int(assetIn.Decimals) 239 decimalsOut := int(assetOut.Decimals) 240 amountIn, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(event.AmountIn), new(big.Float).SetFloat64(math.Pow10(decimalsIn))).Float64() 241 amountOut, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(event.AmountOut), new(big.Float).SetFloat64(math.Pow10(decimalsOut))).Float64() 242 swap := BalancerV2Swap{ 243 SellToken: assetIn.Symbol, 244 BuyToken: assetOut.Symbol, 245 SellVolume: amountIn, 246 BuyVolume: amountOut, 247 ID: event.Raw.TxHash.String() + "-" + fmt.Sprint(event.Raw.Index), 248 Timestamp: time.Now().Unix(), 249 } 250 251 foreignName := swap.BuyToken + "-" + swap.SellToken 252 volume := swap.BuyVolume 253 trade := &dia.Trade{ 254 Symbol: swap.BuyToken, 255 Pair: foreignName, 256 Price: swap.SellVolume / swap.BuyVolume, 257 Volume: volume, 258 Time: time.Unix(swap.Timestamp, 0), 259 PoolAddress: poolAddress.Hex(), 260 ForeignTradeID: swap.ID, 261 Source: s.exchangeName, 262 BaseToken: assetIn, 263 QuoteToken: assetOut, 264 VerifiedPair: true, 265 } 266 switch { 267 case utils.Contains(reverseBasetokensBalancer, trade.BaseToken.Address): 268 // If we need quotation of a base token, reverse pair 269 tSwapped, err := dia.SwapTrade(*trade) 270 if err == nil { 271 trade = &tSwapped 272 } 273 case utils.Contains(reverseQuotetokensBalancer, trade.QuoteToken.Address): 274 // If we don't need quotation of quote token, reverse pair. 275 tSwapped, err := dia.SwapTrade(*trade) 276 if err == nil { 277 trade = &tSwapped 278 } 279 } 280 281 select { 282 case <-s.shutdown: 283 case s.chanTrades <- trade: 284 // Take into account reversed trade as well in either of both cases 285 // 1. Base asset is not bluechip 286 // 2. Both assets are bluechip 287 if !utils.Contains(reverseQuotetokensBalancer, trade.BaseToken.Address) || 288 (utils.Contains(reverseQuotetokensBalancer, trade.BaseToken.Address) && utils.Contains(reverseQuotetokensBalancer, trade.QuoteToken.Address)) { 289 tSwapped, err := dia.SwapTrade(*trade) 290 if err == nil { 291 s.chanTrades <- &tSwapped 292 } 293 } 294 log.Info("got trade: ", trade) 295 } 296 } 297 } 298 } 299 300 // Close unsubscribes data and closes any existing WebSocket connections, as well as channels of BalancerV2Scraper 301 func (s *BalancerV2Scraper) Close() error { 302 if s.isClosed() { 303 return errors.New("BalancerV2Scraper: Already closed") 304 } 305 306 s.signalShutdown.Do(func() { 307 close(s.shutdown) 308 }) 309 310 <-s.shutdownDone 311 312 return s.error() 313 } 314 315 // Channel returns a channel that can be used to receive trades 316 func (s *BalancerV2Scraper) Channel() chan *dia.Trade { 317 return s.chanTrades 318 } 319 320 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from the BalancerV2 scraper 321 func (s *BalancerV2Scraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 322 if err := s.error(); err != nil { 323 return nil, err 324 } 325 if s.isClosed() { 326 return nil, errors.New("BalancerV2Scraper: Call ScrapePair on closed scraper") 327 } 328 329 ps := &BalancerV2PairScraper{ 330 parent: s, 331 pair: pair, 332 } 333 334 s.pairScrapers[pair.ForeignName] = ps 335 336 return ps, nil 337 } 338 339 // fetchAdmissiblePools fetches all pools from postgres with native liquidity > liquidityThreshold and 340 // (if available) liquidity in USD > liquidityThresholdUSD. 341 func (s *BalancerV2Scraper) fetchAdmissiblePools(liquidityThreshold float64, liquidityThresholdUSD float64) { 342 poolsPreselection, err := s.relDB.GetAllPoolsExchange(s.exchangeName, liquidityThreshold) 343 if err != nil { 344 log.Error("fetch all admissible pools: ", err) 345 } 346 log.Infof("Found %v pools after preselection.", len(poolsPreselection)) 347 348 for _, pool := range poolsPreselection { 349 liquidity, lowerBound := pool.GetPoolLiquidityUSD() 350 // Discard pool if complete USD liquidity is below threshold. 351 if !lowerBound && liquidity < liquidityThresholdUSD { 352 continue 353 } else { 354 s.admissiblePools[common.HexToAddress(pool.Address)] = struct{}{} 355 } 356 } 357 log.Infof("Found %v pools after USD liquidity filtering.", len(s.admissiblePools)) 358 } 359 360 func (s *BalancerV2Scraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 361 pools, err := s.listPools() 362 if err != nil { 363 log.Warn("list pools: ", err) 364 // return nil, err 365 } 366 367 log.Infof("%s: Total pools are %v", s.exchangeName, len(pools)) 368 369 pp, err := s.listPairs(pools) 370 if err != nil { 371 return nil, err 372 } 373 374 existingPair := make(map[string]struct{}) 375 for _, p := range pp { 376 quoteAddr := p.UnderlyingPair.QuoteToken.Address 377 baseAddr := p.UnderlyingPair.BaseToken.Address 378 if _, ok := existingPair[baseAddr+":"+quoteAddr]; !ok { 379 pairs = append(pairs, p) 380 existingPair[baseAddr+":"+quoteAddr] = struct{}{} 381 } 382 } 383 384 log.Infof("%s: Total pairs are %v", s.exchangeName, len(pairs)) 385 386 return 387 } 388 389 func (s *BalancerV2Scraper) assetFromToken(token common.Address) (dia.Asset, error) { 390 cached, ok := s.cachedAssets.Load(token.Hex()) 391 if !ok { 392 asset, err := ethhelper.ETHAddressToAsset(token, s.rest, Exchanges[s.exchangeName].BlockChain.Name) 393 if err != nil { 394 return dia.Asset{}, err 395 } 396 397 s.cachedAssets.Store(token.Hex(), asset) 398 399 return asset, nil 400 } 401 402 asset := cached.(dia.Asset) 403 404 return asset, nil 405 } 406 407 func (s *BalancerV2Scraper) makePair(token0, token1 common.Address) (dia.ExchangePair, error) { 408 asset0, err := s.assetFromToken(token0) 409 if err != nil { 410 return dia.ExchangePair{}, err 411 } 412 asset1, err := s.assetFromToken(token1) 413 if err != nil { 414 return dia.ExchangePair{}, err 415 } 416 417 var pair dia.ExchangePair 418 pair.UnderlyingPair.QuoteToken = asset0 419 pair.UnderlyingPair.BaseToken = asset1 420 pair.ForeignName = asset0.Symbol + "-" + asset1.Symbol 421 pair.Verified = true 422 pair.Exchange = s.exchangeName 423 pair.Symbol = asset0.Symbol 424 425 return pair, nil 426 } 427 428 func (s *BalancerV2Scraper) listPairs(pools [][]common.Address) (pairs []dia.ExchangePair, err error) { 429 pairCount := 0 430 pairMap := make(map[int]dia.ExchangePair) 431 var g errgroup.Group 432 var mu sync.Mutex 433 for _, tokens := range pools { 434 if len(tokens) < 2 { 435 continue 436 } 437 438 for i := 0; i < len(tokens); i++ { 439 for j := i + 1; j < len(tokens); j++ { 440 pairCount++ 441 i := i 442 j := j 443 pairCount := pairCount 444 tokens := tokens 445 g.Go(func() error { 446 s.rl.Take() 447 pair, err := s.makePair(tokens[i], tokens[j]) 448 if err != nil { 449 log.Warn(err) 450 451 return nil 452 } 453 454 mu.Lock() 455 defer mu.Unlock() 456 457 pairMap[pairCount] = pair 458 459 return nil 460 }) 461 } 462 } 463 } 464 465 if err := g.Wait(); err != nil { 466 return nil, err 467 } 468 469 keys := make([]int, 0, len(pairMap)) 470 for k := range pairMap { 471 keys = append(keys, k) 472 } 473 474 sort.Ints(keys) 475 476 for _, k := range keys { 477 pairs = append(pairs, pairMap[k]) 478 } 479 480 return 481 } 482 483 func (s *BalancerV2Scraper) listPools() ([][]common.Address, error) { 484 events, err := s.allRegisteredPools() 485 if err != nil { 486 return nil, err 487 } 488 489 caller, err := balancervault.NewBalancerVaultCaller(common.HexToAddress(balancerV2VaultContract), s.rest) 490 if err != nil { 491 return nil, err 492 } 493 494 var g errgroup.Group 495 var mu sync.Mutex 496 pools := make([][]common.Address, len(events)) 497 for idx, evt := range events { 498 idx := idx 499 evt := evt 500 g.Go(func() error { 501 s.rl.Take() 502 pool, err := caller.GetPoolTokens(&bind.CallOpts{}, evt.PoolId) 503 if err != nil { 504 log.Warn("get pool tokens: ", err) 505 return err 506 } 507 508 mu.Lock() 509 defer mu.Unlock() 510 511 pools[idx] = pool.Tokens 512 513 return nil 514 }) 515 } 516 517 if err := g.Wait(); err != nil { 518 return nil, err 519 } 520 521 return pools, nil 522 } 523 524 func (s *BalancerV2Scraper) allRegisteredPools() ([]*balancervault.BalancerVaultPoolRegistered, error) { 525 filterer, err := balancervault.NewBalancerVaultFilterer(common.HexToAddress(balancerV2VaultContract), s.rest) 526 if err != nil { 527 return nil, err 528 } 529 530 currBlock, err := s.rest.BlockNumber(context.Background()) 531 if err != nil { 532 return nil, err 533 } 534 535 var offset uint64 = balancerV2FilterPageSize 536 var startBlock uint64 = uint64(balancerV2StartBlockPoolRegister) 537 var endBlock = startBlock + offset 538 var events []*balancervault.BalancerVaultPoolRegistered 539 for { 540 if endBlock > currBlock { 541 endBlock = currBlock 542 } 543 log.Infof("startblock - endblock: %v --- %v ", startBlock, endBlock) 544 545 it, err := filterer.FilterPoolRegistered(&bind.FilterOpts{ 546 Start: startBlock, 547 End: &endBlock, 548 }, nil, nil) 549 if err != nil { 550 log.Warn("filterpoolregistered: ", err) 551 continue 552 } 553 554 for it.Next() { 555 events = append(events, it.Event) 556 } 557 if err := it.Close(); err != nil { 558 log.Warn("closing iterator: ", it) 559 } 560 561 if endBlock == currBlock { 562 break 563 } 564 565 startBlock = endBlock + 1 566 endBlock = endBlock + offset 567 } 568 569 return events, nil 570 } 571 572 // FillSymbolData adds the name to the asset underlying @symbol on BalancerV2 573 func (s *BalancerV2Scraper) FillSymbolData(symbol string) (dia.Asset, error) { 574 return dia.Asset{Symbol: symbol}, nil 575 } 576 577 func (s *BalancerV2Scraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 578 return pair, nil 579 } 580 581 func (s *BalancerV2Scraper) cleanup() { 582 close(s.chanTrades) 583 s.ws.Close() 584 s.rest.Close() 585 s.close() 586 s.signalShutdownDone.Do(func() { 587 close(s.shutdownDone) 588 }) 589 } 590 591 func (s *BalancerV2Scraper) error() error { 592 s.errMutex.RLock() 593 defer s.errMutex.RUnlock() 594 595 return s.err 596 } 597 598 func (s *BalancerV2Scraper) setError(err error) { 599 s.errMutex.Lock() 600 defer s.errMutex.Unlock() 601 602 s.err = err 603 } 604 605 func (s *BalancerV2Scraper) isClosed() bool { 606 s.closedMutex.RLock() 607 defer s.closedMutex.RUnlock() 608 609 return s.closed 610 } 611 612 func (s *BalancerV2Scraper) close() { 613 s.closedMutex.Lock() 614 defer s.closedMutex.Unlock() 615 616 s.closed = true 617 } 618 619 // BalancerV2PairScraper implements PairScraper for BalancerV2 620 type BalancerV2PairScraper struct { 621 parent *BalancerV2Scraper 622 pair dia.ExchangePair 623 closed bool 624 } 625 626 // Error returns an error when the channel Channel() is closed 627 // and nil otherwise 628 func (p *BalancerV2PairScraper) Error() error { 629 return p.parent.error() 630 } 631 632 // Pair returns the pair this scraper is subscribed to 633 func (p *BalancerV2PairScraper) Pair() dia.ExchangePair { 634 return p.pair 635 } 636 637 // Close stops listening for trades of the pair associated with the BalancerV2Scraper 638 func (p *BalancerV2PairScraper) Close() error { 639 if err := p.parent.error(); err != nil { 640 return err 641 } 642 if p.closed { 643 return errors.New("BalancerV2Scraper: Already closed") 644 } 645 646 p.closed = true 647 648 return nil 649 }