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  }