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 }