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

     1  package scrapers
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"errors"
     7  	"math"
     8  	"math/big"
     9  	"strconv"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	uniswapcontractv4 "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/uniswapv4"
    15  	models "github.com/diadata-org/diadata/pkg/model"
    16  
    17  	"github.com/diadata-org/diadata/pkg/utils"
    18  
    19  	"github.com/diadata-org/diadata/pkg/dia"
    20  	"github.com/ethereum/go-ethereum/accounts/abi/bind"
    21  	"github.com/ethereum/go-ethereum/common"
    22  	"github.com/ethereum/go-ethereum/ethclient"
    23  )
    24  
    25  type UniswapV4Swap struct {
    26  	ID        string
    27  	Timestamp int64
    28  	Pair      dia.Pair
    29  	Amount0   float64
    30  	Amount1   float64
    31  }
    32  
    33  type UniswapV4Scraper struct {
    34  	WsClient   *ethclient.Client
    35  	RestClient *ethclient.Client
    36  	relDB      *models.RelDB
    37  	// signaling channels for session initialization and finishing
    38  	//initDone     chan nothing
    39  	run          bool
    40  	shutdown     chan nothing
    41  	shutdownDone chan nothing
    42  	// error handling; to read error or closed, first acquire read lock
    43  	// only cleanup method should hold write lock
    44  	errorLock sync.RWMutex
    45  	error     error
    46  	closed    bool
    47  	// used to keep track of trading pairs that we subscribed to
    48  	pairScrapers map[string]*UniswapPairV4Scraper
    49  	pairRecieved chan *UniswapPair
    50  	poolMap      map[[32]byte]dia.Pool
    51  
    52  	exchangeName           string
    53  	startBlock             uint64
    54  	waitTime               int
    55  	chanTrades             chan *dia.Trade
    56  	factoryContractAddress common.Address
    57  	thresholdSlippage      float64
    58  }
    59  
    60  // NewUniswapV4Scraper returns a new UniswapV4Scraper
    61  func NewUniswapV4Scraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *UniswapV4Scraper {
    62  	log.Info("NewUniswapV4Scraper ", exchange.Name)
    63  	log.Info("NewUniswapV4Scraper Address ", exchange.Contract)
    64  
    65  	var (
    66  		s   *UniswapV4Scraper
    67  		err error
    68  	)
    69  
    70  	switch exchange.Name {
    71  	case dia.UniswapExchangeV4:
    72  		s = makeUniswapV4Scraper(exchange, "", "", "200", uint64(12369621))
    73  
    74  	}
    75  
    76  	s.relDB = relDB
    77  	s.poolMap = make(map[[32]byte]dia.Pool)
    78  
    79  	pingNodeInterval, err := strconv.ParseInt(utils.Getenv("PING_SERVER", "0"), 10, 64)
    80  	if err != nil {
    81  		log.Error("parse PING_SERVER: ", err)
    82  	}
    83  	if pingNodeInterval > 0 {
    84  		s.pingNode(pingNodeInterval)
    85  	}
    86  
    87  	if scrape {
    88  		go s.mainLoop()
    89  	}
    90  	return s
    91  }
    92  
    93  // makeUniswapV4Scraper returns a uniswap scraper as used in NewUniswapV4Scraper.
    94  func makeUniswapV4Scraper(exchange dia.Exchange, restDial string, wsDial string, waitMilliseconds string, startBlock uint64) *UniswapV4Scraper {
    95  	var restClient, wsClient *ethclient.Client
    96  	var err error
    97  	var s *UniswapV4Scraper
    98  
    99  	log.Infof("Init rest and ws client for %s.", exchange.BlockChain.Name)
   100  	restClient, err = ethclient.Dial(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_REST", restDial))
   101  	if err != nil {
   102  		log.Fatal("init rest client: ", err)
   103  	}
   104  	wsClient, err = ethclient.Dial(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_WS", wsDial))
   105  	if err != nil {
   106  		log.Fatal("init ws client: ", err)
   107  	}
   108  
   109  	var waitTime int
   110  	waitTimeString := utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_WAIT_TIME", waitMilliseconds)
   111  	waitTime, err = strconv.Atoi(waitTimeString)
   112  	if err != nil {
   113  		log.Error("could not parse wait time: ", err)
   114  		waitTime = 500
   115  	}
   116  
   117  	s = &UniswapV4Scraper{
   118  		WsClient:               wsClient,
   119  		RestClient:             restClient,
   120  		shutdown:               make(chan nothing),
   121  		shutdownDone:           make(chan nothing),
   122  		pairScrapers:           make(map[string]*UniswapPairV4Scraper),
   123  		exchangeName:           exchange.Name,
   124  		pairRecieved:           make(chan *UniswapPair),
   125  		error:                  nil,
   126  		chanTrades:             make(chan *dia.Trade),
   127  		waitTime:               waitTime,
   128  		startBlock:             startBlock,
   129  		factoryContractAddress: common.HexToAddress(exchange.Contract),
   130  	}
   131  
   132  	s.thresholdSlippage, err = strconv.ParseFloat(utils.Getenv(strings.ToUpper(s.exchangeName)+"_THRESHOLD_SLIPPAGE", "0.005"), 64)
   133  	if err != nil {
   134  		log.Error("Parse THRESHOLD_SLIPPAGE: ", err)
   135  		s.thresholdSlippage = 0.001
   136  	}
   137  
   138  	return s
   139  }
   140  
   141  // runs in a goroutine until s is closed
   142  func (s *UniswapV4Scraper) mainLoop() {
   143  
   144  	time.Sleep(4 * time.Second)
   145  	s.run = true
   146  
   147  	if len(s.pairScrapers) == 0 {
   148  		s.error = errors.New("uniswap: No pairs to scrape provided")
   149  		log.Error(s.error.Error())
   150  	}
   151  
   152  	sink, err := s.getSwapsChannel()
   153  	if err != nil {
   154  		log.Error("error fetching swaps channel: ", err)
   155  	}
   156  
   157  	go func() {
   158  		for {
   159  			rawSwap, ok := <-sink
   160  			if ok {
   161  
   162  				slippage := computeSlippage(rawSwap.SqrtPriceX96, rawSwap.Amount0, rawSwap.Amount1, rawSwap.Liquidity)
   163  				log.Infof("slippage: %v", slippage)
   164  
   165  				swap, err := s.normalizeRawSwap(rawSwap)
   166  				if err != nil {
   167  					log.Error("normalizeRawSwap: ", err)
   168  					continue
   169  				}
   170  				if slippage > s.thresholdSlippage {
   171  					log.Warn("slippage above threshold: ", slippage)
   172  					continue
   173  				}
   174  
   175  				s.sendTrade(swap, hex.EncodeToString(rawSwap.Id[:]))
   176  
   177  			}
   178  		}
   179  	}()
   180  
   181  }
   182  
   183  func (s *UniswapV4Scraper) getSwapsChannel() (chan *uniswapcontractv4.PoolmanagerSwap, error) {
   184  	contract, err := uniswapcontractv4.NewPoolmanagerFilterer(s.factoryContractAddress, s.WsClient)
   185  	if err != nil {
   186  		log.Error(err)
   187  	}
   188  	tradesSink := make(chan *uniswapcontractv4.PoolmanagerSwap)
   189  	_, err = contract.WatchSwap(&bind.WatchOpts{}, tradesSink, [][32]byte{}, []common.Address{})
   190  	if err != nil {
   191  		log.Fatal("WatchSwap: ", err)
   192  	}
   193  
   194  	return tradesSink, nil
   195  }
   196  
   197  func (s *UniswapV4Scraper) sendTrade(swap UniswapV4Swap, poolID string) {
   198  	price, volume := s.getSwapData(swap)
   199  
   200  	t := &dia.Trade{
   201  		Symbol:         swap.Pair.QuoteToken.Symbol,
   202  		Pair:           swap.Pair.QuoteToken.Symbol + "-" + swap.Pair.BaseToken.Symbol,
   203  		Price:          price,
   204  		Volume:         volume,
   205  		BaseToken:      swap.Pair.BaseToken,
   206  		QuoteToken:     swap.Pair.QuoteToken,
   207  		Time:           time.Unix(swap.Timestamp, 0),
   208  		ForeignTradeID: swap.ID,
   209  		PoolAddress:    poolID,
   210  		Source:         s.exchangeName,
   211  		VerifiedPair:   true,
   212  	}
   213  
   214  	if price > 0 {
   215  		log.Infof("Got trade on pair %s: %v", t.Pair, t)
   216  		log.Info("------")
   217  		s.chanTrades <- t
   218  	}
   219  }
   220  
   221  func (s *UniswapV4Scraper) getSwapData(swap UniswapV4Swap) (price float64, volume float64) {
   222  	volume = swap.Amount0
   223  	price = math.Abs(swap.Amount1 / swap.Amount0)
   224  	return
   225  }
   226  
   227  // normalizeUniswapSwap takes a raw swap as returned by the swap contract's channel and converts it to a UniswapSwap type
   228  func (s *UniswapV4Scraper) normalizeRawSwap(rawSwap *uniswapcontractv4.PoolmanagerSwap) (normalizedSwap UniswapV4Swap, err error) {
   229  
   230  	pool, ok := s.poolMap[rawSwap.Id]
   231  	if !ok {
   232  		pool, err = s.relDB.GetPoolByAddress(dia.ETHEREUM, hex.EncodeToString(rawSwap.Id[:]))
   233  		if err != nil {
   234  			return
   235  		}
   236  		if len(pool.Assetvolumes) != 2 {
   237  			err = errors.New("not enough assets in pool")
   238  			return
   239  		}
   240  		s.poolMap[rawSwap.Id] = pool
   241  	}
   242  
   243  	asset0 := pool.Assetvolumes[pool.Assetvolumes[0].Index].Asset
   244  	asset1 := pool.Assetvolumes[pool.Assetvolumes[1].Index].Asset
   245  	decimals0 := int(asset0.Decimals)
   246  	decimals1 := int(asset1.Decimals)
   247  	amount0Big := new(big.Float).Quo(big.NewFloat(0).SetInt(rawSwap.Amount0), new(big.Float).SetFloat64(math.Pow10(decimals0)))
   248  	amount1Big := new(big.Float).Quo(big.NewFloat(0).SetInt(rawSwap.Amount1), new(big.Float).SetFloat64(math.Pow10(decimals1)))
   249  	amount0, _ := amount0Big.Float64()
   250  	amount1, _ := amount1Big.Float64()
   251  
   252  	normalizedSwap = UniswapV4Swap{
   253  		ID:        rawSwap.Raw.TxHash.Hex(),
   254  		Timestamp: time.Now().Unix(),
   255  		Pair:      dia.Pair{QuoteToken: asset0, BaseToken: asset1},
   256  		Amount0:   amount0,
   257  		Amount1:   amount1,
   258  	}
   259  
   260  	return
   261  }
   262  
   263  // FetchAvailablePairs returns a list with all available trade pairs as dia.Pair for the pairDiscorvery service
   264  func (s *UniswapV4Scraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) {
   265  	return
   266  }
   267  
   268  func (s *UniswapV4Scraper) FillSymbolData(symbol string) (dia.Asset, error) {
   269  	return dia.Asset{Symbol: symbol}, nil
   270  }
   271  
   272  func (s *UniswapV4Scraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) {
   273  	return pair, nil
   274  }
   275  
   276  // Close closes any existing API connections, as well as channels of
   277  // PairScrapers from calls to ScrapePair
   278  func (s *UniswapV4Scraper) Close() error {
   279  
   280  	if s.closed {
   281  		return errors.New("UniswapScraper: Already closed")
   282  	}
   283  	s.WsClient.Close()
   284  	s.RestClient.Close()
   285  	close(s.shutdown)
   286  	<-s.shutdownDone
   287  	s.errorLock.RLock()
   288  	defer s.errorLock.RUnlock()
   289  	return s.error
   290  }
   291  
   292  // ScrapePair returns a PairScraper that can be used to get trades for a single pair from
   293  // this APIScraper
   294  func (s *UniswapV4Scraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) {
   295  
   296  	s.errorLock.RLock()
   297  	defer s.errorLock.RUnlock()
   298  	if s.error != nil {
   299  		return nil, s.error
   300  	}
   301  	if s.closed {
   302  		return nil, errors.New("UniswapScraper: Call ScrapePair on closed scraper")
   303  	}
   304  	ps := &UniswapPairV4Scraper{
   305  		parent: s,
   306  		pair:   pair,
   307  	}
   308  	s.pairScrapers[pair.ForeignName] = ps
   309  	return ps, nil
   310  }
   311  
   312  func (s *UniswapV4Scraper) pingNode(pingNodeInterval int64) {
   313  	ticker := time.NewTicker(time.Duration(pingNodeInterval) * time.Second)
   314  	go func() {
   315  		for range ticker.C {
   316  			blockNumber, err := s.WsClient.BlockNumber(context.Background())
   317  			if err != nil {
   318  				log.Error("pingNode: ", err)
   319  			} else {
   320  				log.Infof("%v -- blockNumber: %d", time.Now(), blockNumber)
   321  			}
   322  		}
   323  	}()
   324  }
   325  
   326  // UniswapPairScraper implements PairScraper for Uniswap
   327  type UniswapPairV4Scraper struct {
   328  	parent *UniswapV4Scraper
   329  	pair   dia.ExchangePair
   330  	//closed bool
   331  }
   332  
   333  // Close stops listening for trades of the pair associated with s
   334  func (ps *UniswapPairV4Scraper) Close() error {
   335  	return nil
   336  }
   337  
   338  // Channel returns a channel that can be used to receive trades
   339  func (s *UniswapV4Scraper) Channel() chan *dia.Trade {
   340  	return s.chanTrades
   341  }
   342  
   343  // Error returns an error when the channel Channel() is closed
   344  // and nil otherwise
   345  func (ps *UniswapPairV4Scraper) Error() error {
   346  	s := ps.parent
   347  	s.errorLock.RLock()
   348  	defer s.errorLock.RUnlock()
   349  	return s.error
   350  }
   351  
   352  // Pair returns the pair this scraper is subscribed to
   353  func (ps *UniswapPairV4Scraper) Pair() dia.ExchangePair {
   354  	return ps.pair
   355  }
   356  
   357  func computeSlippage(sqrtPriceX96 *big.Int, amount0 *big.Int, amount1 *big.Int, liquidity *big.Int) (slippage float64) {
   358  
   359  	price := new(big.Float).Quo(big.NewFloat(0).SetInt(sqrtPriceX96), new(big.Float).SetFloat64(math.Pow(2, 96)))
   360  
   361  	if amount0.Sign() < 0 {
   362  		// token0 -> token1
   363  		amount0Abs := big.NewInt(0).Abs(amount0)
   364  		numerator := big.NewFloat(0).Mul(big.NewFloat(0).SetInt(amount0Abs), price)
   365  		slippage, _ = new(big.Float).Quo(numerator, big.NewFloat(0).SetInt(liquidity)).Float64()
   366  		return
   367  	} else if amount1.Sign() < 0 {
   368  		// token1 -> token0
   369  		numerator := big.NewFloat(0).SetInt(big.NewInt(0).Abs(amount1))
   370  		denominator := big.NewFloat(0).Mul(big.NewFloat(0).SetInt(liquidity), price)
   371  		slippage, _ = new(big.Float).Quo(numerator, denominator).Float64()
   372  		return
   373  	}
   374  	log.Infof("sqrtPrice -- amount0 -- amount1 -- liquidity: %s -- %s -- %s -- %s", sqrtPriceX96.String(), amount0.String(), amount1.String(), liquidity.String())
   375  	return 0
   376  
   377  }