github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/CurvefiScraper.go (about)

     1  package scrapers
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"math"
     8  	"math/big"
     9  	"reflect"
    10  	"strconv"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/curvefi"
    16  	"github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/curvefi/curvepool"
    17  	"github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/curvefifactory"
    18  	"github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/curvefimeta"
    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/ethereum/go-ethereum/accounts/abi"
    24  	"github.com/ethereum/go-ethereum/accounts/abi/bind"
    25  	"github.com/ethereum/go-ethereum/common"
    26  	"github.com/ethereum/go-ethereum/ethclient"
    27  )
    28  
    29  const (
    30  	curveRestDialEth      = ""
    31  	curveWsDialEth        = ""
    32  	curveRestDialFantom   = ""
    33  	curveWsDialFantom     = ""
    34  	curveRestDialMoonbeam = ""
    35  	curveWsDialMoonbeam   = ""
    36  	curveRestDialPolygon  = ""
    37  	curveWsDialPolygon    = ""
    38  	curveRestDialArbitrum = ""
    39  	curveWsDialArbitrum   = ""
    40  )
    41  
    42  type CurveCoin struct {
    43  	Symbol   string
    44  	Decimals uint8
    45  	Address  string
    46  	Name     string
    47  }
    48  
    49  func assetToCoin(a dia.Asset) (c *CurveCoin) {
    50  	c = &CurveCoin{}
    51  	c.Address = a.Address
    52  	c.Decimals = a.Decimals
    53  	c.Name = a.Name
    54  	c.Symbol = a.Symbol
    55  	return
    56  }
    57  
    58  type curveRegistry struct {
    59  	Address common.Address
    60  	Type    int
    61  }
    62  
    63  type Pools struct {
    64  	pools     map[string]map[int]*CurveCoin
    65  	poolsLock sync.RWMutex
    66  }
    67  
    68  func (p *Pools) setPool(k string, v map[int]*CurveCoin) {
    69  	p.poolsLock.Lock()
    70  	defer p.poolsLock.Unlock()
    71  	p.pools[k] = v
    72  }
    73  
    74  func (p *Pools) getPool(k string) (map[int]*CurveCoin, bool) {
    75  	p.poolsLock.RLock()
    76  	defer p.poolsLock.RUnlock()
    77  	r, ok := p.pools[k]
    78  	return r, ok
    79  }
    80  
    81  func (p *Pools) getPoolCoin(poolk string, coink int) (*CurveCoin, bool) {
    82  	p.poolsLock.RLock()
    83  	defer p.poolsLock.RUnlock()
    84  	r, ok := p.pools[poolk][coink]
    85  	return r, ok
    86  }
    87  
    88  func (p *Pools) poolsAddressNoLock() []string {
    89  	p.poolsLock.RLock()
    90  	defer p.poolsLock.RUnlock()
    91  	var values []string
    92  	for key := range p.pools {
    93  		values = append(values, key)
    94  	}
    95  	return values
    96  }
    97  
    98  // CurveFIScraper is a curve finance scraper on a specific blockchain.
    99  type CurveFIScraper struct {
   100  	exchangeName string
   101  
   102  	// channels to signal events
   103  	run          bool
   104  	initDone     chan nothing
   105  	shutdown     chan nothing
   106  	shutdownDone chan nothing
   107  
   108  	errorLock sync.RWMutex
   109  	error     error
   110  	closed    bool
   111  
   112  	pairScrapers   map[string]*CurveFIPairScraper
   113  	productPairIds map[string]int
   114  	chanTrades     chan *dia.Trade
   115  
   116  	WsClient             *ethclient.Client
   117  	RestClient           *ethclient.Client
   118  	relDB                *models.RelDB
   119  	curveCoins           map[string]*CurveCoin
   120  	resubscribe          chan string
   121  	pools                *Pools
   122  	registriesUnderlying []curveRegistry
   123  	screenPools          bool
   124  }
   125  
   126  // makeCurvefiScraper returns a curve finance scraper as used in NewCurvefiScraper.
   127  func makeCurvefiScraper(exchange dia.Exchange, restDial string, wsDial string, relDB *models.RelDB) *CurveFIScraper {
   128  	var (
   129  		restClient, wsClient *ethclient.Client
   130  		err                  error
   131  		scraper              *CurveFIScraper
   132  	)
   133  
   134  	log.Infof("Init rest and ws client for %s.", exchange.BlockChain.Name)
   135  	restClient, err = ethclient.Dial(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_REST", restDial))
   136  	if err != nil {
   137  		log.Fatal("init rest client: ", err)
   138  	}
   139  	wsClient, err = ethclient.Dial(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_WS", wsDial))
   140  	if err != nil {
   141  		log.Fatal("init ws client: ", err)
   142  	}
   143  
   144  	scraper = &CurveFIScraper{
   145  		exchangeName:   exchange.Name,
   146  		RestClient:     restClient,
   147  		WsClient:       wsClient,
   148  		relDB:          relDB,
   149  		initDone:       make(chan nothing),
   150  		shutdown:       make(chan nothing),
   151  		shutdownDone:   make(chan nothing),
   152  		productPairIds: make(map[string]int),
   153  		pairScrapers:   make(map[string]*CurveFIPairScraper),
   154  		chanTrades:     make(chan *dia.Trade),
   155  		curveCoins:     make(map[string]*CurveCoin),
   156  		resubscribe:    make(chan string),
   157  		pools: &Pools{
   158  			pools: make(map[string]map[int]*CurveCoin),
   159  		},
   160  	}
   161  
   162  	// Only include pools with (minimum) liquidity bigger than given env var.
   163  	liquidityThreshold, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD", "0"), 64)
   164  	if err != nil {
   165  		liquidityThreshold = float64(0)
   166  		log.Warnf("parse liquidity threshold:  %v. Set to default %v", err, liquidityThreshold)
   167  	}
   168  
   169  	// Only include pools with (minimum) liquidity USD value bigger than given env var.
   170  	liquidityThresholdUSD, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD_USD", "0"), 64)
   171  	if err != nil {
   172  		liquidityThresholdUSD = float64(0)
   173  		log.Warnf("parse liquidity threshold:  %v. Set to default %v", err, liquidityThresholdUSD)
   174  	}
   175  
   176  	scraper.loadPools(liquidityThreshold, liquidityThresholdUSD)
   177  
   178  	return scraper
   179  }
   180  
   181  func NewCurveFIScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *CurveFIScraper {
   182  
   183  	var scraper *CurveFIScraper
   184  
   185  	switch exchange.Name {
   186  	case dia.CurveFIExchange:
   187  		scraper = makeCurvefiScraper(exchange, curveRestDialEth, curveWsDialEth, relDB)
   188  		basePoolRegistry := curveRegistry{Type: 1, Address: common.HexToAddress(exchange.Contract)}
   189  		cryptoswapPools := curveRegistry{Type: 1, Address: common.HexToAddress("0x8F942C20D02bEfc377D41445793068908E2250D0")}
   190  		metaPoolRegistry := curveRegistry{Type: 2, Address: common.HexToAddress("0xB9fC157394Af804a3578134A6585C0dc9cc990d4")}
   191  		factoryPools := curveRegistry{Type: 3, Address: common.HexToAddress("0xF18056Bbd320E96A48e3Fbf8bC061322531aac99")}
   192  		factory2Pools := curveRegistry{Type: 3, Address: common.HexToAddress("0x4F8846Ae9380B90d2E71D5e3D042dff3E7ebb40d")}
   193  		stableSwapRegistry := curveRegistry{Type: 3, Address: common.HexToAddress("0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf")}
   194  		scraper.registriesUnderlying = []curveRegistry{
   195  			factoryPools,
   196  			factory2Pools,
   197  			metaPoolRegistry,
   198  			basePoolRegistry,
   199  			cryptoswapPools,
   200  			stableSwapRegistry}
   201  		scraper.screenPools = true
   202  
   203  	case dia.CurveFIExchangeFantom:
   204  		scraper = makeCurvefiScraper(exchange, curveRestDialFantom, curveWsDialFantom, relDB)
   205  		stableSwapFactory := curveRegistry{Type: 2, Address: common.HexToAddress("0x686d67265703d1f124c45e33d47d794c566889ba")}
   206  		scraper.registriesUnderlying = []curveRegistry{stableSwapFactory}
   207  		scraper.screenPools = false
   208  
   209  	case dia.CurveFIExchangeMoonbeam:
   210  		scraper = makeCurvefiScraper(exchange, curveRestDialMoonbeam, curveWsDialMoonbeam, relDB)
   211  		stableSwapFactory := curveRegistry{Type: 2, Address: common.HexToAddress("0x4244eB811D6e0Ef302326675207A95113dB4E1F8")}
   212  		scraper.registriesUnderlying = []curveRegistry{stableSwapFactory}
   213  		scraper.screenPools = false
   214  
   215  	case dia.CurveFIExchangePolygon:
   216  		scraper = makeCurvefiScraper(exchange, curveRestDialPolygon, curveWsDialPolygon, relDB)
   217  		stableSwapFactory := curveRegistry{Type: 2, Address: common.HexToAddress("0x722272D36ef0Da72FF51c5A65Db7b870E2e8D4ee")}
   218  		scraper.registriesUnderlying = []curveRegistry{stableSwapFactory}
   219  		scraper.screenPools = false
   220  	case dia.CurveFIExchangeArbitrum:
   221  		scraper = makeCurvefiScraper(exchange, curveRestDialArbitrum, curveWsDialArbitrum, relDB)
   222  		stableSwapFactory := curveRegistry{Type: 2, Address: common.HexToAddress("0xb17b674D9c5CB2e441F8e196a2f048A81355d031")}
   223  		scraper.registriesUnderlying = []curveRegistry{stableSwapFactory}
   224  	}
   225  
   226  	if scrape {
   227  		go scraper.mainLoop()
   228  	}
   229  	return scraper
   230  }
   231  
   232  func (scraper *CurveFIScraper) mainLoop() {
   233  	scraper.run = true
   234  
   235  	for _, pool := range scraper.pools.poolsAddressNoLock() {
   236  		err := scraper.watchSwaps(pool)
   237  		if err != nil {
   238  			log.Error("watchSwaps: ", err)
   239  		}
   240  	}
   241  
   242  	go func() {
   243  		for scraper.run {
   244  			p := <-scraper.resubscribe
   245  			log.Info("resub to p: ", p)
   246  
   247  			if scraper.run {
   248  				log.Info("resubscribe to swaps from Pool: " + p)
   249  				err := scraper.watchSwaps(p)
   250  				if err != nil {
   251  					log.Error("watchSwaps in resubscribe: ", err)
   252  				}
   253  
   254  			}
   255  		}
   256  	}()
   257  
   258  	if scraper.run {
   259  		if len(scraper.pools.pools) == 0 {
   260  			scraper.error = errors.New("no pairs to scrape provided")
   261  			log.Error(scraper.error.Error())
   262  		}
   263  	}
   264  
   265  	time.Sleep(10 * time.Second)
   266  
   267  	if scraper.error == nil {
   268  		scraper.error = errors.New("main loop terminated by Close()")
   269  	}
   270  	scraper.cleanup(nil)
   271  }
   272  
   273  func (scraper *CurveFIScraper) watchSwaps(pool string) error {
   274  
   275  	filterer, err := curvepool.NewCurvepoolFilterer(common.HexToAddress(pool), scraper.WsClient)
   276  	if err != nil {
   277  		log.Fatal(err)
   278  	}
   279  	sink := make(chan *curvepool.CurvepoolTokenExchange)
   280  
   281  	filtererV2, err := curvefifactory.NewCurvefifactoryFilterer(common.HexToAddress(pool), scraper.WsClient)
   282  	if err != nil {
   283  		log.Fatal(err)
   284  	}
   285  	sinkV2 := make(chan *curvefifactory.CurvefifactoryTokenExchange)
   286  
   287  	header, err := scraper.RestClient.HeaderByNumber(context.Background(), nil)
   288  	if err != nil {
   289  		log.Fatal(err)
   290  	}
   291  	startblock := header.Number.Uint64() - uint64(20)
   292  
   293  	sub, err := filterer.WatchTokenExchange(&bind.WatchOpts{Start: &startblock}, sink, nil)
   294  	if err != nil {
   295  		log.Error("WatchTokenExchange: ", err)
   296  	}
   297  
   298  	subV2, err := filtererV2.WatchTokenExchange(&bind.WatchOpts{Start: &startblock}, sinkV2, nil)
   299  	if err != nil {
   300  		log.Error("WatchTokenExchange: ", err)
   301  	}
   302  
   303  	sinkUnderlying := make(chan *curvepool.CurvepoolTokenExchangeUnderlying)
   304  	subUnderlying, err := filterer.WatchTokenExchangeUnderlying(&bind.WatchOpts{Start: &startblock}, sinkUnderlying, nil)
   305  	if err != nil {
   306  		log.Error("WatchTokenExchangeUnderlying: ", err)
   307  	}
   308  
   309  	go func() {
   310  		fmt.Println("Curvefi Subscribed to pool: " + pool)
   311  		defer fmt.Printf("Curvefi UnSubscribed to pool %s with error: %v", pool, err)
   312  		defer sub.Unsubscribe()
   313  		defer subV2.Unsubscribe()
   314  		defer subUnderlying.Unsubscribe()
   315  		subscribed := true
   316  
   317  		for scraper.run && subscribed {
   318  			select {
   319  			case err = <-sub.Err():
   320  				if err != nil {
   321  					log.Error("sub error: ", err)
   322  				}
   323  				subscribed = false
   324  				if scraper.run {
   325  					log.Warn("resubscribe pool: ", pool)
   326  					scraper.resubscribe <- pool
   327  				}
   328  				log.Warn("subscription error: ", err)
   329  			case swp := <-sink:
   330  				log.Info("got swap V1")
   331  				scraper.processSwap(pool, swp)
   332  
   333  			case swpV2 := <-sinkV2:
   334  				log.Info("got swap V2")
   335  				scraper.processSwap(pool, swpV2)
   336  			case swpUnderlying := <-sinkUnderlying:
   337  				log.Warn("got underlying swap: ", swpUnderlying)
   338  				// Only fetch trades from USDR pool until we have parsing TokenExchangeUnderlying resolved.
   339  				// if pool == common.HexToAddress("0xa138341185a9d0429b0021a11fb717b225e13e1f").Hex() && Exchanges[scraper.exchangeName].BlockChain.Name == dia.POLYGON {
   340  				scraper.processSwap(pool, swpUnderlying)
   341  				// }
   342  			}
   343  		}
   344  	}()
   345  
   346  	return err
   347  
   348  }
   349  
   350  func (scraper *CurveFIScraper) processSwap(pool string, swp interface{}) {
   351  	var (
   352  		foreignName    string
   353  		volume         float64
   354  		price          float64
   355  		baseToken      dia.Asset
   356  		quoteToken     dia.Asset
   357  		foreignTradeID string
   358  		err            error
   359  	)
   360  
   361  	switch s := swp.(type) {
   362  	case *curvepool.CurvepoolTokenExchange:
   363  		foreignName, volume, price, baseToken, quoteToken, err = scraper.getSwapDataCurve(pool, s)
   364  		if err != nil {
   365  			log.Error("getSwapDataCurve: ", err)
   366  			return
   367  		}
   368  		foreignTradeID = s.Raw.TxHash.Hex() + "-" + fmt.Sprint(s.Raw.Index)
   369  	case *curvefifactory.CurvefifactoryTokenExchange:
   370  		foreignName, volume, price, baseToken, quoteToken, err = scraper.getSwapDataCurve(pool, s)
   371  		if err != nil {
   372  			log.Error("getSwapDataCurve: ", err)
   373  			return
   374  		}
   375  		foreignTradeID = s.Raw.TxHash.Hex() + "-" + fmt.Sprint(s.Raw.Index)
   376  	case *curvepool.CurvepoolTokenExchangeUnderlying:
   377  		foreignName, volume, price, baseToken, quoteToken, err = scraper.getSwapDataCurve(pool, s)
   378  		if err != nil {
   379  			log.Error("getSwapDataCurve: ", err)
   380  			return
   381  		}
   382  		foreignTradeID = s.Raw.TxHash.Hex() + "-" + fmt.Sprint(s.Raw.Index)
   383  	}
   384  
   385  	// Hotfix for zero address bug.
   386  	// TO DO: Improve decoding.
   387  	if quoteToken.Address == "0x0000000000000000000000000000000000000000" || baseToken.Address == "0x0000000000000000000000000000000000000000" {
   388  		log.Errorf("got zero address on pool %s. discard trade.", pool)
   389  		return
   390  	}
   391  
   392  	timestamp := time.Now().Unix()
   393  
   394  	trade := &dia.Trade{
   395  		Symbol:         quoteToken.Symbol,
   396  		Pair:           foreignName,
   397  		BaseToken:      baseToken,
   398  		QuoteToken:     quoteToken,
   399  		Price:          price,
   400  		Volume:         volume,
   401  		Time:           time.Unix(timestamp, 0),
   402  		ForeignTradeID: foreignTradeID,
   403  		PoolAddress:    pool,
   404  		Source:         scraper.exchangeName,
   405  		VerifiedPair:   true,
   406  	}
   407  	log.Infof("Got Trade in pool %s:\n %v", pool, trade)
   408  
   409  	scraper.chanTrades <- trade
   410  
   411  }
   412  
   413  // getSwapDataCurve returns the foreign name, volume and price of a swap
   414  func (scraper *CurveFIScraper) getSwapDataCurve(pool string, swp interface{}) (
   415  	foreignName string,
   416  	volume float64,
   417  	price float64,
   418  	baseToken,
   419  	quoteToken dia.Asset,
   420  	err error,
   421  ) {
   422  	var (
   423  		fromToken *CurveCoin
   424  		toToken   *CurveCoin
   425  		amountIn  float64
   426  		amountOut float64
   427  		ok        bool
   428  	)
   429  
   430  	switch s := swp.(type) {
   431  	case *curvepool.CurvepoolTokenExchange:
   432  		fromToken, ok = scraper.pools.getPoolCoin(pool, int(s.SoldId.Int64()))
   433  		if !ok {
   434  			err = fmt.Errorf("token not found: " + pool + "-" + s.SoldId.String())
   435  		}
   436  		toToken, ok = scraper.pools.getPoolCoin(pool, int(s.BoughtId.Int64()))
   437  		if !ok {
   438  			err = fmt.Errorf("token not found: " + pool + "-" + s.SoldId.String())
   439  		}
   440  		amountIn, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(s.TokensSold), new(big.Float).SetFloat64(math.Pow10(int(fromToken.Decimals)))).Float64()
   441  		amountOut, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(s.TokensBought), new(big.Float).SetFloat64(math.Pow10(int(toToken.Decimals)))).Float64()
   442  	case *curvefifactory.CurvefifactoryTokenExchange:
   443  		fromToken, ok = scraper.pools.getPoolCoin(pool, int(s.SoldId.Int64()))
   444  		if !ok {
   445  			err = fmt.Errorf("token not found: " + pool + "-" + s.SoldId.String())
   446  		}
   447  		toToken, ok = scraper.pools.getPoolCoin(pool, int(s.BoughtId.Int64()))
   448  		if !ok {
   449  			err = fmt.Errorf("token not found: " + pool + "-" + s.SoldId.String())
   450  		}
   451  		if toToken == nil || fromToken == nil {
   452  			err = errors.New("token not available, skip trade.")
   453  			return
   454  		}
   455  		amountIn, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(s.TokensSold), new(big.Float).SetFloat64(math.Pow10(int(fromToken.Decimals)))).Float64()
   456  		amountOut, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(s.TokensBought), new(big.Float).SetFloat64(math.Pow10(int(toToken.Decimals)))).Float64()
   457  	case *curvepool.CurvepoolTokenExchangeUnderlying:
   458  		log.Info("Got TokenExchangeUnderlying in pool: ", pool)
   459  		var (
   460  			isMetaPool          bool
   461  			underlyingCoins     [8]common.Address
   462  			underlyingCoinCount *big.Int
   463  			metaCoinCount       *big.Int
   464  			errNCoin            error
   465  			errCoins            error
   466  		)
   467  		soldID := int(s.SoldId.Int64())
   468  		boughtId := int(s.BoughtId.Int64())
   469  		tokenSold := s.TokensSold
   470  		tokenBought := s.TokensBought
   471  
   472  		for _, registry := range scraper.registriesUnderlying {
   473  			if registry.Type == 2 {
   474  				metaRegistryContract, errMeta := curvefimeta.NewCurvefimetaCaller(registry.Address, scraper.RestClient)
   475  				if errMeta != nil {
   476  					log.Error("NewCurvefiCaller: ", errMeta)
   477  				}
   478  				// Get underlying coins from on-chain. soldID and boughtID are referring to these.
   479  				underlyingCoins, errCoins = metaRegistryContract.GetUnderlyingCoins(&bind.CallOpts{}, common.HexToAddress(pool))
   480  				if errCoins != nil || (underlyingCoins[soldID] == (common.Address{}) && underlyingCoins[boughtId] == (common.Address{})) {
   481  					log.Warnf("Failed to call GetUnderlyingCoins: %v", errCoins)
   482  					continue
   483  				} else {
   484  					log.Warn("TokenExchangeUnderlying from meta type pool")
   485  					log.Warnf("bought id and sold id: %v -- %v", boughtId, soldID)
   486  					log.Warnf("tokens bought and tokens sold: %v -- %v ", tokenBought, tokenSold)
   487  					metaCoinCount, underlyingCoinCount, errNCoin = metaRegistryContract.GetMetaNCoins(&bind.CallOpts{}, common.HexToAddress(pool))
   488  					if errNCoin != nil {
   489  						log.Errorf("calling GetMetaNCoins: %v", errCoins)
   490  						continue
   491  					}
   492  					log.Warnf("metaCoinCount: %v, underlyingCoinCount: %v", metaCoinCount, underlyingCoinCount)
   493  					isMetaPool = true
   494  					break
   495  				}
   496  
   497  			}
   498  			if registry.Type == 1 {
   499  				basepoolRegistryContract, errBase := curvefi.NewCurvefiCaller(registry.Address, scraper.RestClient)
   500  				if errBase != nil {
   501  					log.Error("NewCurvefiCaller: ", errBase)
   502  				}
   503  				// Get underlying coins from on-chain. soldID and boughtID are referring to these.
   504  				underlyingCoins, errCoins = basepoolRegistryContract.GetUnderlyingCoins(&bind.CallOpts{}, common.HexToAddress(pool))
   505  				if errCoins != nil || (underlyingCoins[soldID] == (common.Address{}) && underlyingCoins[boughtId] == (common.Address{})) {
   506  					continue
   507  				} else {
   508  					log.Warn("TokenExchangeUnderlying from base type pool")
   509  					log.Warnf("bought id and sold id: %v -- %v", boughtId, soldID)
   510  					log.Warnf("tokens bought and tokens sold: %v -- %v ", tokenBought, tokenSold)
   511  					break
   512  				}
   513  
   514  			}
   515  		}
   516  		log.Warnf("meta pool : %v, soldID: %v", isMetaPool, soldID)
   517  		if isMetaPool && soldID > 0 && boughtId == 0 {
   518  			// This is the only case we need to look into the AddLiquidity event for finding the actual amount of sold token
   519  			// as this event contains the meta pool token amount in this case!
   520  
   521  			tokenAmount, errTokenAmounts := scraper.getTokenAmount(s.Raw.TxHash, common.HexToAddress(pool), soldID, metaCoinCount, underlyingCoinCount)
   522  			if err != nil {
   523  				log.Error("getting AddLiquidity event for tx: ", s.Raw.TxHash.Hex())
   524  				err = errTokenAmounts
   525  				return
   526  			}
   527  			log.Warnf("token Amount %v", tokenAmount)
   528  			// Again we need to subtract MAX_COIN index from this value, (which is metaCoinCount minus one)
   529  			tokenSold = tokenAmount
   530  		}
   531  		log.Infof("token sold: %v", tokenSold)
   532  
   533  		fromTokenAddress := underlyingCoins[int(soldID)]
   534  		toTokenAddress := underlyingCoins[int(boughtId)]
   535  
   536  		fromTokenAsset, errToken := scraper.relDB.GetAsset(fromTokenAddress.Hex(), Exchanges[scraper.exchangeName].BlockChain.Name)
   537  		if errToken != nil {
   538  			err = errToken
   539  			return
   540  		}
   541  		toTokenAsset, errToken := scraper.relDB.GetAsset(toTokenAddress.Hex(), Exchanges[scraper.exchangeName].BlockChain.Name)
   542  		if errToken != nil {
   543  			err = errToken
   544  			return
   545  		}
   546  		log.Infof("fromToken address -- token: %s -- %v", fromTokenAddress.Hex(), fromTokenAsset)
   547  		log.Infof("toToken address -- token: %s -- %v", toTokenAddress.Hex(), toTokenAsset)
   548  		fromToken = assetToCoin(fromTokenAsset)
   549  		toToken = assetToCoin(toTokenAsset)
   550  
   551  		amountIn, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(tokenSold), new(big.Float).SetFloat64(math.Pow10(int(fromToken.Decimals)))).Float64()
   552  		amountOut, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(tokenBought), new(big.Float).SetFloat64(math.Pow10(int(toToken.Decimals)))).Float64()
   553  	}
   554  
   555  	baseToken = dia.Asset{
   556  		Name:       fromToken.Name,
   557  		Address:    fromToken.Address,
   558  		Symbol:     fromToken.Symbol,
   559  		Blockchain: Exchanges[scraper.exchangeName].BlockChain.Name,
   560  	}
   561  	quoteToken = dia.Asset{
   562  		Name:       toToken.Name,
   563  		Address:    toToken.Address,
   564  		Symbol:     toToken.Symbol,
   565  		Blockchain: Exchanges[scraper.exchangeName].BlockChain.Name,
   566  	}
   567  
   568  	volume = amountOut
   569  	price = amountIn / amountOut
   570  
   571  	foreignName = toToken.Symbol + "-" + fromToken.Symbol
   572  
   573  	return
   574  }
   575  
   576  func (scraper *CurveFIScraper) getTokenAmount(txHash common.Hash, poolAddress common.Address, soldID int, metaCoinCount, underlyingCoinCount *big.Int) (*big.Int, error) {
   577  	// Here we need to get the event log manually as the len of the token_amounts array is unknown!
   578  	// (fixed to 2 in the current curvepool contract, but there are cases when it's 3 like this pool:
   579  	// https://etherscan.io/address/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7)
   580  	// We get the value using reflection.
   581  	receipt, err := scraper.RestClient.TransactionReceipt(context.Background(), txHash)
   582  	if err != nil {
   583  		return nil, err
   584  	}
   585  	basePoolCoinCount := underlyingCoinCount.Int64() - 1 // We need to subtract that one meta pool coin here!
   586  	abiAddLiquidityJson := `[{"name":"AddLiquidity","inputs":[{"type":"address","name":"provider","indexed":true},{"type":"uint256[%d]","name":"token_amounts","indexed":false},{"type":"uint256[%d]","name":"fees","indexed":false},{"type":"uint256","name":"invariant","indexed":false},{"type":"uint256","name":"token_supply","indexed":false}],"anonymous":false,"type":"event"}]`
   587  	abiAddLiquidity, err := abi.JSON(strings.NewReader(fmt.Sprintf(abiAddLiquidityJson, basePoolCoinCount, basePoolCoinCount)))
   588  	if err != nil {
   589  		return nil, err
   590  	}
   591  	for _, log := range receipt.Logs {
   592  		if len(log.Topics) < 2 || log.Topics[1] != poolAddress.Hash() {
   593  			continue
   594  		}
   595  		valueMap := make(map[string]interface{})
   596  		err := abiAddLiquidity.UnpackIntoMap(valueMap, "AddLiquidity", log.Data)
   597  		if err != nil {
   598  			continue
   599  		}
   600  		// Again we need to subtract MAX_COIN index from this value, (which is metaCoinCount minus one)
   601  		tokenBoughtIdx := soldID - (int(metaCoinCount.Int64()) - 1)
   602  		tokenAmount, ok := reflect.ValueOf(valueMap["token_amounts"]).Index(tokenBoughtIdx).Interface().(*big.Int)
   603  		if !ok {
   604  			return nil, errors.New("couldn't parse the AddLiquidity event")
   605  		}
   606  		return tokenAmount, nil
   607  	}
   608  	return nil, errors.New("AddLiquidity log couldn't be found")
   609  
   610  }
   611  
   612  func (scraper *CurveFIScraper) loadPools(liquidityThreshold float64, liquidityThresholdUSD float64) {
   613  
   614  	pools, err := scraper.relDB.GetAllPoolsExchange(scraper.exchangeName, liquidityThreshold)
   615  	if err != nil {
   616  		log.Fatal("fetch pools: ", err)
   617  	}
   618  	log.Infof("found %v pools to subscribe: ", len(pools))
   619  
   620  	blacklistedPools, err := getAddressesFromConfig("curve/blacklist_pools/" + scraper.exchangeName)
   621  	if err != nil {
   622  		log.Fatal("blacklisted pools: ", err)
   623  	}
   624  	var blacklistedPoolsString []string
   625  	for _, bp := range blacklistedPools {
   626  		blacklistedPoolsString = append(blacklistedPoolsString, bp.Hex())
   627  	}
   628  	log.Infof("remove %v blacklisted pools: %s", len(blacklistedPools), blacklistedPoolsString)
   629  
   630  	lowerBoundCount := 0
   631  	for _, pool := range pools {
   632  		if utils.Contains(&blacklistedPoolsString, pool.Address) {
   633  			continue
   634  		}
   635  
   636  		liquidity, lowerBound := pool.GetPoolLiquidityUSD()
   637  		// Discard pool if complete USD liquidity is below threshold.
   638  		if !lowerBound && liquidity < liquidityThresholdUSD {
   639  			continue
   640  		}
   641  		if lowerBound {
   642  			log.Info("lowerBound pool: ", pool.Address)
   643  			lowerBoundCount++
   644  		}
   645  
   646  		var poolCoinsMap = make(map[int]*CurveCoin)
   647  		for _, av := range pool.Assetvolumes {
   648  			poolCoinsMap[int(av.Index)] = &CurveCoin{
   649  				Symbol:   av.Asset.Symbol,
   650  				Decimals: av.Asset.Decimals,
   651  				Name:     av.Asset.Name,
   652  				Address:  av.Asset.Address,
   653  			}
   654  			scraper.curveCoins[av.Asset.Address] = &CurveCoin{
   655  				Symbol:   av.Asset.Symbol,
   656  				Decimals: av.Asset.Decimals,
   657  			}
   658  		}
   659  		scraper.pools.setPool(pool.Address, poolCoinsMap)
   660  	}
   661  	log.Infof("Total number of %v pools to subscribe.", len(scraper.pools.pools))
   662  	log.Infof("Number of lower bound pools: %v", lowerBoundCount)
   663  }
   664  
   665  func (scraper *CurveFIScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   666  
   667  	return
   668  }
   669  
   670  func (scraper *CurveFIScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   671  	return dia.ExchangePair{}, nil
   672  }
   673  
   674  func (scraper *CurveFIScraper) FillSymbolData(symbol string) (dia.Asset, error) {
   675  	return dia.Asset{}, nil
   676  }
   677  
   678  func (scraper *CurveFIScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   679  	scraper.errorLock.RLock()
   680  	defer scraper.errorLock.RUnlock()
   681  
   682  	if scraper.error != nil {
   683  		return nil, scraper.error
   684  	}
   685  
   686  	if scraper.closed {
   687  		return nil, errors.New("CurveFIScraper is closed")
   688  	}
   689  
   690  	pairScraper := &CurveFIPairScraper{
   691  		parent: scraper,
   692  		pair:   pair,
   693  	}
   694  
   695  	scraper.pairScrapers[pair.ForeignName] = pairScraper
   696  
   697  	return pairScraper, nil
   698  }
   699  func (scraper *CurveFIScraper) cleanup(err error) {
   700  	scraper.errorLock.Lock()
   701  	defer scraper.errorLock.Unlock()
   702  	if err != nil {
   703  		scraper.error = err
   704  	}
   705  	scraper.closed = true
   706  	close(scraper.shutdownDone)
   707  }
   708  
   709  func (scraper *CurveFIScraper) Close() error {
   710  	// close the pair scraper channels
   711  	scraper.run = false
   712  	for _, pairScraper := range scraper.pairScrapers {
   713  		pairScraper.closed = true
   714  	}
   715  	scraper.WsClient.Close()
   716  	scraper.RestClient.Close()
   717  
   718  	close(scraper.shutdown)
   719  	<-scraper.shutdownDone
   720  	return nil
   721  }
   722  
   723  type CurveFIPairScraper struct {
   724  	parent *CurveFIScraper
   725  	pair   dia.ExchangePair
   726  	closed bool
   727  }
   728  
   729  func (pairScraper *CurveFIPairScraper) Pair() dia.ExchangePair {
   730  	return pairScraper.pair
   731  }
   732  
   733  func (scraper *CurveFIScraper) Channel() chan *dia.Trade {
   734  	return scraper.chanTrades
   735  }
   736  
   737  func (pairScraper *CurveFIPairScraper) Error() error {
   738  	s := pairScraper.parent
   739  	s.errorLock.RLock()
   740  	defer s.errorLock.RUnlock()
   741  	return s.error
   742  }
   743  
   744  func (pairScraper *CurveFIPairScraper) Close() error {
   745  	pairScraper.parent.errorLock.RLock()
   746  	defer pairScraper.parent.errorLock.RUnlock()
   747  	pairScraper.closed = true
   748  	return nil
   749  }