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  }