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  }