github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/BalancerV3Scraper.go (about) 1 package scrapers 2 3 import ( 4 "context" 5 "fmt" 6 "math" 7 "math/big" 8 "strconv" 9 "sync" 10 "time" 11 12 "github.com/ethereum/go-ethereum/accounts/abi/bind" 13 "github.com/ethereum/go-ethereum/common" 14 "github.com/ethereum/go-ethereum/ethclient" 15 "github.com/pkg/errors" 16 "go.uber.org/ratelimit" 17 18 vault "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/balancerv3/vault" 19 models "github.com/diadata-org/diadata/pkg/model" 20 "github.com/diadata-org/diadata/pkg/utils" 21 22 "github.com/diadata-org/diadata/pkg/dia" 23 "github.com/diadata-org/diadata/pkg/dia/helpers/ethhelper" 24 ) 25 26 const ( 27 balancerV3RateLimitPerSec = 50 28 balancerV3FilterPageSize = 5000 29 balancerV3RestDial = "" 30 balancerV3WSDial = "" 31 ) 32 33 var ( 34 balancerV3VaultContract = "" 35 reverseBasetokensBalancerV3 *[]string 36 reverseQuotetokensBalancerV3 *[]string 37 ) 38 39 // BalancerV3Swap is a swap information 40 type BalancerV3Swap struct { 41 SellToken string 42 BuyToken string 43 SellVolume float64 44 BuyVolume float64 45 ID string 46 Timestamp int64 47 } 48 49 // BalancerV3Scraper is a scraper for Balancer V3 50 type BalancerV3Scraper struct { 51 rest *ethclient.Client 52 ws *ethclient.Client 53 rl ratelimit.Limiter 54 relDB *models.RelDB 55 56 // signaling channels for session initialization and finishing 57 shutdown chan nothing 58 shutdownDone chan nothing 59 signalShutdown sync.Once 60 signalShutdownDone sync.Once 61 62 // error handling; err should be read from error(), closed should be read from isClosed() 63 // those two methods implement RW lock 64 errMutex sync.RWMutex 65 err error 66 closedMutex sync.RWMutex 67 closed bool 68 69 // used to keep track of trading pairs that we subscribed to 70 pairScrapers map[string]*BalancerV3PairScraper 71 exchangeName string 72 chanTrades chan *dia.Trade 73 74 tokensMap map[string]dia.Asset 75 admissiblePools map[common.Address]struct{} 76 cachedAssets sync.Map // map[string]dia.Asset 77 } 78 79 // NewBalancerV3Scraper returns a Balancer V3 scraper 80 func NewBalancerV3Scraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *BalancerV3Scraper { 81 balancerV3VaultContract = exchange.Contract 82 scraper := &BalancerV3Scraper{ 83 exchangeName: exchange.Name, 84 err: nil, 85 shutdown: make(chan nothing), 86 shutdownDone: make(chan nothing), 87 pairScrapers: make(map[string]*BalancerV3PairScraper), 88 chanTrades: make(chan *dia.Trade), 89 tokensMap: make(map[string]dia.Asset), 90 admissiblePools: make(map[common.Address]struct{}), 91 } 92 93 var err error 94 95 ws, err := ethclient.Dial(utils.Getenv("ETH_URI_WS", balancerV3WSDial)) 96 if err != nil { 97 log.Fatal("init ws client: ", err) 98 } 99 100 rest, err := ethclient.Dial(utils.Getenv("ETH_URI_REST", balancerV3RestDial)) 101 if err != nil { 102 log.Fatal("init rest client: ", err) 103 } 104 105 scraper.relDB = relDB 106 107 // Only include pools with (minimum) liquidity bigger than given env var. 108 liquidityThreshold, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD", "0"), 64) 109 if err != nil { 110 liquidityThreshold = float64(0) 111 log.Warnf("parse liquidity threshold: %v. Set to default %v", err, liquidityThreshold) 112 } 113 114 // Only include pools with (minimum) liquidity USD value bigger than given env var. 115 liquidityThresholdUSD, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD_USD", "0"), 64) 116 if err != nil { 117 liquidityThresholdUSD = float64(0) 118 log.Warnf("parse liquidity threshold: %v. Set to default %v", err, liquidityThresholdUSD) 119 } 120 121 scraper.fetchAdmissiblePools(liquidityThreshold, liquidityThresholdUSD) 122 123 scraper.ws = ws 124 scraper.rest = rest 125 scraper.rl = ratelimit.New(balancerV3RateLimitPerSec) 126 127 if scrape { 128 go scraper.mainLoop() 129 } 130 131 return scraper 132 } 133 134 func (s *BalancerV3Scraper) mainLoop() { 135 136 // Import tokens which appear as base token and we need a quotation for 137 var err error 138 reverseBasetokensBalancerV3, err = getReverseTokensFromConfig("balancer/reverse_tokens/" + s.exchangeName + "Basetoken") 139 if err != nil { 140 log.Error("error getting tokens for which pairs should be reversed: ", err) 141 } 142 log.Info("reverse basetokens: ", reverseBasetokensBalancerV3) 143 reverseQuotetokensBalancerV3, err = getReverseTokensFromConfig("balancer/reverse_tokens/" + s.exchangeName + "Quotetoken") 144 if err != nil { 145 log.Error("error getting tokens for which pairs should be reversed: ", err) 146 } 147 log.Info("reverse quotetokens: ", reverseQuotetokensBalancerV3) 148 149 defer s.cleanup() 150 151 filterer, err := vault.NewVaultFilterer(common.HexToAddress(balancerV3VaultContract), s.ws) 152 if err != nil { 153 s.setError(err) 154 log.Fatalf("%s: Cannot create vault filter, err=%s", s.exchangeName, err.Error()) 155 } 156 157 currBlock, err := s.rest.BlockNumber(context.Background()) 158 if err != nil { 159 s.setError(err) 160 log.Fatalf("%s: Cannot get a current block number, err=%s", s.exchangeName, err.Error()) 161 } 162 163 sink := make(chan *vault.VaultSwap) 164 sub, err := filterer.WatchSwap(&bind.WatchOpts{Start: &currBlock}, sink, nil, nil, nil) 165 if err != nil { 166 s.setError(err) 167 log.Fatalf("%s: Cannot watch swap events, err=%s", s.exchangeName, err.Error()) 168 } 169 170 defer sub.Unsubscribe() 171 172 for { 173 select { 174 case <-s.shutdown: 175 log.Println("BalancerV3Scraper: Shutting down main loop") 176 case err := <-sub.Err(): 177 s.setError(err) 178 log.Errorf("BalancerV3Scraper: Subscription error, err=%s", err.Error()) 179 case event := <-sink: 180 181 if _, ok := s.admissiblePools[event.Pool]; !ok { 182 log.Warnf("pool %s not admissible, skip trade.", event.Pool) 183 continue 184 } 185 186 assetIn, ok := s.tokensMap[event.TokenIn.Hex()] 187 if !ok { 188 asset, err := s.assetFromToken(event.TokenIn) 189 if err != nil { 190 log.Warnf("%s: Retrieving asset-in %s, err=%s", s.exchangeName, event.TokenIn.Hex(), err.Error()) 191 continue 192 } 193 s.tokensMap[asset.Address] = asset 194 assetIn = asset 195 } 196 197 assetOut, ok := s.tokensMap[event.TokenOut.Hex()] 198 if !ok { 199 asset, err := s.assetFromToken(event.TokenOut) 200 if err != nil { 201 log.Warnf("%s: Retrieving asset-out %s, err=%s", s.exchangeName, event.TokenOut.Hex(), err.Error()) 202 continue 203 } 204 s.tokensMap[asset.Address] = asset 205 assetOut = asset 206 } 207 208 decimalsIn := int(assetIn.Decimals) 209 decimalsOut := int(assetOut.Decimals) 210 amountIn, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(event.AmountIn), new(big.Float).SetFloat64(math.Pow10(decimalsIn))).Float64() 211 amountOut, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(event.AmountOut), new(big.Float).SetFloat64(math.Pow10(decimalsOut))).Float64() 212 swap := BalancerV3Swap{ 213 SellToken: assetIn.Symbol, 214 BuyToken: assetOut.Symbol, 215 SellVolume: amountIn, 216 BuyVolume: amountOut, 217 ID: event.Raw.TxHash.String() + "-" + fmt.Sprint(event.Raw.Index), 218 Timestamp: time.Now().Unix(), 219 } 220 221 foreignName := swap.BuyToken + "-" + swap.SellToken 222 volume := swap.BuyVolume 223 trade := &dia.Trade{ 224 Symbol: swap.BuyToken, 225 Pair: foreignName, 226 Price: swap.SellVolume / swap.BuyVolume, 227 Volume: volume, 228 Time: time.Unix(swap.Timestamp, 0), 229 PoolAddress: event.Pool.Hex(), 230 ForeignTradeID: swap.ID, 231 Source: s.exchangeName, 232 BaseToken: assetIn, 233 QuoteToken: assetOut, 234 VerifiedPair: true, 235 } 236 switch { 237 case utils.Contains(reverseBasetokensBalancer, trade.BaseToken.Address): 238 // If we need quotation of a base token, reverse pair 239 tSwapped, err := dia.SwapTrade(*trade) 240 if err == nil { 241 trade = &tSwapped 242 } 243 case utils.Contains(reverseQuotetokensBalancer, trade.QuoteToken.Address): 244 // If we don't need quotation of quote token, reverse pair. 245 tSwapped, err := dia.SwapTrade(*trade) 246 if err == nil { 247 trade = &tSwapped 248 } 249 } 250 251 select { 252 case <-s.shutdown: 253 case s.chanTrades <- trade: 254 // Take into account reversed trade as well in either of both cases 255 // 1. Base asset is not bluechip 256 // 2. Both assets are bluechip 257 if !utils.Contains(reverseQuotetokensBalancer, trade.BaseToken.Address) || 258 (utils.Contains(reverseQuotetokensBalancer, trade.BaseToken.Address) && utils.Contains(reverseQuotetokensBalancer, trade.QuoteToken.Address)) { 259 tSwapped, err := dia.SwapTrade(*trade) 260 if err == nil { 261 s.chanTrades <- &tSwapped 262 } 263 } 264 log.Info("got trade: ", trade) 265 } 266 } 267 } 268 } 269 270 // Close unsubscribes data and closes any existing WebSocket connections, as well as channels of BalancerV3Scraper 271 func (s *BalancerV3Scraper) Close() error { 272 if s.isClosed() { 273 return errors.New("BalancerV3Scraper: Already closed") 274 } 275 276 s.signalShutdown.Do(func() { 277 close(s.shutdown) 278 }) 279 280 <-s.shutdownDone 281 282 return s.error() 283 } 284 285 // Channel returns a channel that can be used to receive trades 286 func (s *BalancerV3Scraper) Channel() chan *dia.Trade { 287 return s.chanTrades 288 } 289 290 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from the BalancerV3 scraper 291 func (s *BalancerV3Scraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 292 if err := s.error(); err != nil { 293 return nil, err 294 } 295 if s.isClosed() { 296 return nil, errors.New("BalancerV3Scraper: Call ScrapePair on closed scraper") 297 } 298 299 ps := &BalancerV3PairScraper{ 300 parent: s, 301 pair: pair, 302 } 303 304 s.pairScrapers[pair.ForeignName] = ps 305 306 return ps, nil 307 } 308 309 // fetchAdmissiblePools fetches all pools from postgres with native liquidity > liquidityThreshold and 310 // (if available) liquidity in USD > liquidityThresholdUSD. 311 func (s *BalancerV3Scraper) fetchAdmissiblePools(liquidityThreshold float64, liquidityThresholdUSD float64) { 312 poolsPreselection, err := s.relDB.GetAllPoolsExchange(s.exchangeName, liquidityThreshold) 313 if err != nil { 314 log.Error("fetch all admissible pools: ", err) 315 } 316 log.Infof("Found %v pools after preselection.", len(poolsPreselection)) 317 318 for _, pool := range poolsPreselection { 319 liquidity, lowerBound := pool.GetPoolLiquidityUSD() 320 // Discard pool if complete USD liquidity is below threshold. 321 if !lowerBound && liquidity < liquidityThresholdUSD { 322 continue 323 } else { 324 s.admissiblePools[common.HexToAddress(pool.Address)] = struct{}{} 325 } 326 } 327 log.Infof("Found %v pools after USD liquidity filtering.", len(s.admissiblePools)) 328 } 329 330 func (s *BalancerV3Scraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 331 return 332 } 333 334 func (s *BalancerV3Scraper) assetFromToken(token common.Address) (dia.Asset, error) { 335 cached, ok := s.cachedAssets.Load(token.Hex()) 336 if !ok { 337 asset, err := ethhelper.ETHAddressToAsset(token, s.rest, Exchanges[s.exchangeName].BlockChain.Name) 338 if err != nil { 339 return dia.Asset{}, err 340 } 341 342 s.cachedAssets.Store(token.Hex(), asset) 343 344 return asset, nil 345 } 346 347 asset := cached.(dia.Asset) 348 349 return asset, nil 350 } 351 352 func (s *BalancerV3Scraper) makePair(token0, token1 common.Address) (dia.ExchangePair, error) { 353 asset0, err := s.assetFromToken(token0) 354 if err != nil { 355 return dia.ExchangePair{}, err 356 } 357 asset1, err := s.assetFromToken(token1) 358 if err != nil { 359 return dia.ExchangePair{}, err 360 } 361 362 var pair dia.ExchangePair 363 pair.UnderlyingPair.QuoteToken = asset0 364 pair.UnderlyingPair.BaseToken = asset1 365 pair.ForeignName = asset0.Symbol + "-" + asset1.Symbol 366 pair.Verified = true 367 pair.Exchange = s.exchangeName 368 pair.Symbol = asset0.Symbol 369 370 return pair, nil 371 } 372 373 // FillSymbolData adds the name to the asset underlying @symbol on BalancerV3 374 func (s *BalancerV3Scraper) FillSymbolData(symbol string) (dia.Asset, error) { 375 return dia.Asset{Symbol: symbol}, nil 376 } 377 378 func (s *BalancerV3Scraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 379 return pair, nil 380 } 381 382 func (s *BalancerV3Scraper) cleanup() { 383 close(s.chanTrades) 384 s.ws.Close() 385 s.rest.Close() 386 s.close() 387 s.signalShutdownDone.Do(func() { 388 close(s.shutdownDone) 389 }) 390 } 391 392 func (s *BalancerV3Scraper) error() error { 393 s.errMutex.RLock() 394 defer s.errMutex.RUnlock() 395 396 return s.err 397 } 398 399 func (s *BalancerV3Scraper) setError(err error) { 400 s.errMutex.Lock() 401 defer s.errMutex.Unlock() 402 403 s.err = err 404 } 405 406 func (s *BalancerV3Scraper) isClosed() bool { 407 s.closedMutex.RLock() 408 defer s.closedMutex.RUnlock() 409 410 return s.closed 411 } 412 413 func (s *BalancerV3Scraper) close() { 414 s.closedMutex.Lock() 415 defer s.closedMutex.Unlock() 416 417 s.closed = true 418 } 419 420 // BalancerV3PairScraper implements PairScraper for BalancerV3 421 type BalancerV3PairScraper struct { 422 parent *BalancerV3Scraper 423 pair dia.ExchangePair 424 closed bool 425 } 426 427 // Error returns an error when the channel Channel() is closed 428 // and nil otherwise 429 func (p *BalancerV3PairScraper) Error() error { 430 return p.parent.error() 431 } 432 433 // Pair returns the pair this scraper is subscribed to 434 func (p *BalancerV3PairScraper) Pair() dia.ExchangePair { 435 return p.pair 436 } 437 438 // Close stops listening for trades of the pair associated with the BalancerV3Scraper 439 func (p *BalancerV3PairScraper) Close() error { 440 if err := p.parent.error(); err != nil { 441 return err 442 } 443 if p.closed { 444 return errors.New("BalancerV3Scraper: Already closed") 445 } 446 447 p.closed = true 448 449 return nil 450 }