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

     1  package scrapers
     2  
     3  import (
     4  	"errors"
     5  	"math"
     6  	"math/big"
     7  	"strconv"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/diadata-org/diadata/pkg/dia"
    13  	"github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/velodrome"
    14  	models "github.com/diadata-org/diadata/pkg/model"
    15  	"github.com/diadata-org/diadata/pkg/utils"
    16  	"github.com/ethereum/go-ethereum/accounts/abi/bind"
    17  	"github.com/ethereum/go-ethereum/common"
    18  	"github.com/ethereum/go-ethereum/ethclient"
    19  )
    20  
    21  const (
    22  	velodromeRestDial  = ""
    23  	velodromeWsDial    = ""
    24  	baseRestDial       = ""
    25  	baseWsDial         = ""
    26  	swellchainRestDial = ""
    27  	swellchainWsDial   = ""
    28  )
    29  
    30  type VelodromeSwap struct {
    31  	ID         string
    32  	Timestamp  int64
    33  	IndexIn    int
    34  	IndexOut   int
    35  	Amount0In  float64
    36  	Amount0Out float64
    37  	Amount1In  float64
    38  	Amount1Out float64
    39  }
    40  
    41  type VelodromeScraper struct {
    42  	RestClient *ethclient.Client
    43  	WsClient   *ethclient.Client
    44  	relDB      *models.RelDB
    45  	// error handling; to read error or closed, first acquire read lock
    46  	// only cleanup method should hold write lock
    47  	errorLock          sync.RWMutex
    48  	error              error
    49  	closed             bool
    50  	pools              []dia.Pool
    51  	listenByAddress    bool
    52  	reverseQuotetokens *[]string
    53  	reverseBasetokens  *[]string
    54  	fullPools          *[]string
    55  	// used to keep track of trading pairs that we subscribed to
    56  	pairScrapers map[string]*VelodromePairScraper
    57  	exchangeName string
    58  	chanTrades   chan *dia.Trade
    59  }
    60  
    61  func NewVelodromeScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *VelodromeScraper {
    62  	log.Info("NewVelodromeScraper: ", exchange.Name)
    63  	var (
    64  		s               *VelodromeScraper
    65  		listenByAddress bool
    66  		err             error
    67  	)
    68  
    69  	switch exchange.Name {
    70  	case dia.VelodromeExchange:
    71  		s = makeVelodromeScraper(exchange, velodromeRestDial, velodromeWsDial, relDB)
    72  	case dia.VelodromeExchangeSwellchain:
    73  		s = makeVelodromeScraper(exchange, swellchainRestDial, swellchainWsDial, relDB)
    74  	case dia.AerodromeV1Exchange:
    75  		s = makeVelodromeScraper(exchange, baseRestDial, baseWsDial, relDB)
    76  	}
    77  
    78  	// Only include pools with (minimum) liquidity bigger than given env var.
    79  	liquidityThreshold, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD", "0"), 64)
    80  	if err != nil {
    81  		liquidityThreshold = float64(0)
    82  		log.Warnf("parse liquidity threshold:  %v. Set to default %v", err, liquidityThreshold)
    83  	}
    84  
    85  	// Only include pools with (minimum) liquidity USD value bigger than given env var.
    86  	liquidityThresholdUSD, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD_USD", "0"), 64)
    87  	if err != nil {
    88  		liquidityThresholdUSD = float64(0)
    89  		log.Warnf("parse liquidity threshold:  %v. Set to default %v", err, liquidityThresholdUSD)
    90  	}
    91  
    92  	listenByAddress, err = strconv.ParseBool(utils.Getenv("LISTEN_BY_ADDRESS", ""))
    93  	if err != nil {
    94  		log.Fatal("parse LISTEN_BY_ADDRESS: ", err)
    95  	}
    96  	s.listenByAddress = listenByAddress
    97  
    98  	s.reverseBasetokens, err = getReverseTokensFromConfig("velodrome/reverse_tokens/" + s.exchangeName + "Basetoken")
    99  	if err != nil {
   100  		log.Error("error getting basetokens for which pairs should be reversed: ", err)
   101  	}
   102  	log.Infof("reverse the following basetokens on %s: %v", s.exchangeName, s.reverseBasetokens)
   103  
   104  	s.reverseQuotetokens, err = getReverseTokensFromConfig("velodrome/reverse_tokens/" + s.exchangeName + "Quotetoken")
   105  	if err != nil {
   106  		log.Error("error getting quotetokens for which pairs should be reversed: ", err)
   107  	}
   108  	log.Infof("reverse the following quotetokens on %s: %v", s.exchangeName, s.reverseQuotetokens)
   109  
   110  	s.fullPools, err = getReverseTokensFromConfig("velodrome/fullPools/" + s.exchangeName + "FullPools")
   111  	if err != nil {
   112  		log.Error("error getting fullPools for which pairs should be reversed: ", err)
   113  	}
   114  	log.Infof("Take into account both directions of a trade on the following pools: %v", s.fullPools)
   115  
   116  	err = s.loadPools(liquidityThreshold, liquidityThresholdUSD)
   117  	if err != nil {
   118  		log.Fatal("load pools: ", err)
   119  	}
   120  
   121  	if scrape {
   122  		go s.mainLoop()
   123  	}
   124  
   125  	return s
   126  
   127  }
   128  
   129  func makeVelodromeScraper(exchange dia.Exchange, restDial string, wsDial string, relDB *models.RelDB) *VelodromeScraper {
   130  	var (
   131  		restClient, wsClient *ethclient.Client
   132  		err                  error
   133  		s                    *VelodromeScraper
   134  	)
   135  
   136  	log.Infof("Init rest and ws client for %s.", exchange.BlockChain.Name)
   137  	restClient, err = ethclient.Dial(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_REST", restDial))
   138  	if err != nil {
   139  		log.Fatal("init rest client: ", err)
   140  	}
   141  	wsClient, err = ethclient.Dial(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_WS", wsDial))
   142  	if err != nil {
   143  		log.Fatal("init ws client: ", err)
   144  	}
   145  
   146  	s = &VelodromeScraper{
   147  		RestClient:   restClient,
   148  		WsClient:     wsClient,
   149  		relDB:        relDB,
   150  		pairScrapers: make(map[string]*VelodromePairScraper),
   151  		exchangeName: exchange.Name,
   152  		error:        nil,
   153  		chanTrades:   make(chan *dia.Trade),
   154  	}
   155  
   156  	return s
   157  }
   158  
   159  func (s *VelodromeScraper) mainLoop() {
   160  
   161  	for _, pool := range s.pools {
   162  		s.WatchSwaps(pool)
   163  	}
   164  
   165  }
   166  
   167  func (s *VelodromeScraper) WatchSwaps(pool dia.Pool) {
   168  	sink, err := s.GetSwapsChannel(common.HexToAddress(pool.Address))
   169  	if err != nil {
   170  		log.Error("error fetching swaps channel: ", err)
   171  	}
   172  
   173  	go func() {
   174  		for {
   175  			rawSwap, ok := <-sink
   176  			if ok {
   177  				swap, indexmap := s.normalizeSwap(*rawSwap, pool)
   178  				price, volume := s.getSwapData(swap)
   179  				token0 := pool.Assetvolumes[indexmap[0]].Asset
   180  				token1 := pool.Assetvolumes[indexmap[1]].Asset
   181  
   182  				t := &dia.Trade{
   183  					Symbol:         token0.Symbol,
   184  					Pair:           token0.Symbol + "-" + token1.Symbol,
   185  					Price:          price,
   186  					Volume:         volume,
   187  					BaseToken:      token1,
   188  					QuoteToken:     token0,
   189  					Time:           time.Unix(swap.Timestamp, 0),
   190  					PoolAddress:    rawSwap.Raw.Address.Hex(),
   191  					ForeignTradeID: swap.ID,
   192  					Source:         s.exchangeName,
   193  					VerifiedPair:   true,
   194  				}
   195  
   196  				switch {
   197  				case utils.Contains(s.reverseBasetokens, token1.Address):
   198  					// If we need quotation of a base token, reverse pair
   199  					tSwapped, err := dia.SwapTrade(*t)
   200  					if err == nil {
   201  						t = &tSwapped
   202  					}
   203  				case utils.Contains(s.reverseQuotetokens, token0.Address):
   204  					// If we need quotation of a base token, reverse pair
   205  					tSwapped, err := dia.SwapTrade(*t)
   206  					if err == nil {
   207  						t = &tSwapped
   208  					}
   209  				}
   210  
   211  				if utils.Contains(s.fullPools, pool.Address) {
   212  					tSwapped, err := dia.SwapTrade(*t)
   213  					if err == nil {
   214  						if tSwapped.Price > 0 {
   215  							s.chanTrades <- &tSwapped
   216  						}
   217  					}
   218  				}
   219  
   220  				if price > 0 {
   221  					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)
   222  					s.chanTrades <- t
   223  				}
   224  			}
   225  		}
   226  	}()
   227  }
   228  
   229  // GetSwapsChannel returns a channel for swaps of the pair with address @pairAddress
   230  func (s *VelodromeScraper) GetSwapsChannel(pairAddress common.Address) (chan *velodrome.IPoolSwap, error) {
   231  
   232  	sink := make(chan *velodrome.IPoolSwap)
   233  	var pairFiltererContract *velodrome.IPoolFilterer
   234  	pairFiltererContract, err := velodrome.NewIPoolFilterer(pairAddress, s.WsClient)
   235  	if err != nil {
   236  		log.Fatal(err)
   237  	}
   238  
   239  	_, err = pairFiltererContract.WatchSwap(&bind.WatchOpts{}, sink, []common.Address{}, []common.Address{})
   240  	if err != nil {
   241  		log.Error("error in get swaps channel: ", err)
   242  	}
   243  
   244  	return sink, nil
   245  
   246  }
   247  
   248  // normalizeSwap takes a swap as returned by the swap contract's channel and converts it to a VelodromeSwap type
   249  func (s *VelodromeScraper) normalizeSwap(swap velodrome.IPoolSwap, pool dia.Pool) (normalizedSwap VelodromeSwap, indexmap map[uint8]int) {
   250  
   251  	// map the on-chain index of the pair's tokens onto their position in @Assetvolumes slice.
   252  	indexmap = make(map[uint8]int)
   253  	indexmap[pool.Assetvolumes[0].Index] = 0
   254  	indexmap[pool.Assetvolumes[1].Index] = 1
   255  
   256  	decimals0 := int(pool.Assetvolumes[indexmap[0]].Asset.Decimals)
   257  	decimals1 := int(pool.Assetvolumes[indexmap[1]].Asset.Decimals)
   258  
   259  	amount0In, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(swap.Amount0In), new(big.Float).SetFloat64(math.Pow10(decimals0))).Float64()
   260  	amount0Out, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(swap.Amount0Out), new(big.Float).SetFloat64(math.Pow10(decimals0))).Float64()
   261  	amount1In, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(swap.Amount1In), new(big.Float).SetFloat64(math.Pow10(decimals1))).Float64()
   262  	amount1Out, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(swap.Amount1Out), new(big.Float).SetFloat64(math.Pow10(decimals1))).Float64()
   263  
   264  	normalizedSwap = VelodromeSwap{
   265  		ID:         swap.Raw.TxHash.Hex(),
   266  		Timestamp:  time.Now().Unix(),
   267  		Amount0In:  amount0In,
   268  		Amount0Out: amount0Out,
   269  		Amount1In:  amount1In,
   270  		Amount1Out: amount1Out,
   271  	}
   272  
   273  	if amount0In > 0 {
   274  		normalizedSwap.IndexIn = 0
   275  		normalizedSwap.IndexOut = 1
   276  	} else {
   277  		normalizedSwap.IndexIn = 1
   278  		normalizedSwap.IndexOut = 0
   279  	}
   280  	return
   281  }
   282  
   283  func (s *VelodromeScraper) getSwapData(swap VelodromeSwap) (price float64, volume float64) {
   284  	if swap.Amount0In == float64(0) {
   285  		volume = swap.Amount0Out
   286  		price = swap.Amount1In / swap.Amount0Out
   287  		return
   288  	}
   289  	volume = -swap.Amount0In
   290  	price = swap.Amount1Out / swap.Amount0In
   291  	return
   292  }
   293  
   294  func (s *VelodromeScraper) Channel() chan *dia.Trade {
   295  	return s.chanTrades
   296  }
   297  
   298  func (s *VelodromeScraper) Close() error {
   299  	s.closed = true
   300  	return nil
   301  }
   302  
   303  func (s *VelodromeScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   304  	return pairs, nil
   305  }
   306  
   307  func (s *VelodromeScraper) FillSymbolData(symbol string) (dia.Asset, error) {
   308  	return dia.Asset{}, nil
   309  }
   310  
   311  func (up *VelodromeScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   312  	return pair, nil
   313  }
   314  
   315  type VelodromePairScraper struct {
   316  	parent *VelodromeScraper
   317  	pair   dia.ExchangePair
   318  	closed bool
   319  }
   320  
   321  func (s *VelodromeScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   322  	s.errorLock.RLock()
   323  	defer s.errorLock.RUnlock()
   324  	if s.error != nil {
   325  		return nil, s.error
   326  	}
   327  	if s.closed {
   328  		return nil, errors.New("Velodrome: Call ScrapePair on closed scraper")
   329  	}
   330  	ps := &VelodromePairScraper{
   331  		parent: s,
   332  		pair:   pair,
   333  	}
   334  	s.pairScrapers[pair.ForeignName] = ps
   335  	return ps, nil
   336  }
   337  
   338  func (ps *VelodromePairScraper) Close() error {
   339  	ps.closed = true
   340  	return nil
   341  }
   342  
   343  func (ps *VelodromePairScraper) Error() error {
   344  	s := ps.parent
   345  	s.errorLock.RLock()
   346  	defer s.errorLock.RUnlock()
   347  	return s.error
   348  }
   349  
   350  // Pair returns the pair this scraper is subscribed to
   351  func (ps *VelodromePairScraper) Pair() dia.ExchangePair {
   352  	return ps.pair
   353  }
   354  
   355  // loadPools loads all pools with sufficient liquidity from postgres.
   356  func (s *VelodromeScraper) loadPools(liquiThreshold float64, liquidityThresholdUSD float64) (err error) {
   357  	var pools []dia.Pool
   358  
   359  	if s.listenByAddress {
   360  
   361  		// Only load pool info for addresses from json file.
   362  		poolAddresses, errAddr := getAddressesFromConfig("velodrome/subscribe_pools/" + s.exchangeName)
   363  		if errAddr != nil {
   364  			log.Error("fetch pool addresses from config file: ", errAddr)
   365  		}
   366  		for _, address := range poolAddresses {
   367  			pool, errPool := s.relDB.GetPoolByAddress(Exchanges[s.exchangeName].BlockChain.Name, address.Hex())
   368  			if errPool != nil {
   369  				log.Fatalf("Get pool with address %s: %v", address.Hex(), errPool)
   370  			}
   371  			s.pools = append(s.pools, pool)
   372  		}
   373  
   374  	} else {
   375  
   376  		// Load all pools above liqui threshold.
   377  		pools, err = s.relDB.GetAllPoolsExchange(s.exchangeName, liquiThreshold)
   378  		if err != nil {
   379  			return
   380  		}
   381  
   382  		log.Info("Found ", len(pools), " pools.")
   383  		log.Info("make pool map...")
   384  		lowerBoundCount := 0
   385  		for _, pool := range pools {
   386  			if len(pool.Assetvolumes) != 2 {
   387  				log.Warn("not enough assets in pool with address: ", pool.Address)
   388  				continue
   389  			}
   390  
   391  			liquidity, lowerBound := pool.GetPoolLiquidityUSD()
   392  			// Discard pool if complete USD liquidity is below threshold.
   393  			if !lowerBound && liquidity < liquidityThresholdUSD {
   394  				continue
   395  			}
   396  			if lowerBound {
   397  				lowerBoundCount++
   398  			}
   399  			s.pools = append(s.pools, pool)
   400  		}
   401  		log.Infof("found %v subscribable pools.", len(s.pools))
   402  		log.Infof("%v pools with lowerBound=true.", lowerBoundCount)
   403  	}
   404  
   405  	return
   406  }