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 }