github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/PlatypusScraper.go (about) 1 package scrapers 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "math" 8 "math/big" 9 "strconv" 10 "strings" 11 "sync" 12 "time" 13 14 "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/platypusfinance" 15 platypusAssetABI "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/platypusfinance/asset" 16 platypusPoolABI "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/platypusfinance/pool" 17 platypusTokenABI "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/platypusfinance/token" 18 models "github.com/diadata-org/diadata/pkg/model" 19 "github.com/diadata-org/diadata/pkg/utils" 20 21 "github.com/diadata-org/diadata/pkg/dia" 22 "github.com/ethereum/go-ethereum/accounts/abi/bind" 23 "github.com/ethereum/go-ethereum/common" 24 "github.com/ethereum/go-ethereum/ethclient" 25 ) 26 27 const ( 28 // 30 days of blocks (avgBlocktimeTime = 2.5) 29 platypusLookBackBlocks = (60 / 2.5) * 60 * 24 * 30 30 platypusRestDialEth = "https://api.avax.network/ext/bc/C/rpc" 31 platypusWsDialEth = "wss://api.avax.network/ext/bc/C/ws" 32 platypusMasterRegV3Addr = "0x7125B4211357d7C3a90F796c956c12c681146EbB" 33 platypusMasterRegV2Addr = "0x68c5f4374228BEEdFa078e77b5ed93C28a2f713E" 34 platypusMasterRegV1Addr = "0xB0523f9F473812FB195Ee49BC7d2ab9873a98044" 35 ) 36 37 type platypusRegistry struct { 38 Address common.Address 39 Version int 40 } 41 42 type PlatypusCoin struct { 43 Symbol string 44 Decimals uint8 45 Address string 46 Name string 47 } 48 49 type PlatypusPools struct { 50 pools map[string]map[int]*PlatypusCoin 51 poolsLock sync.RWMutex 52 } 53 54 func (p *PlatypusPools) setPool(k string, v map[int]*PlatypusCoin) { 55 p.poolsLock.Lock() 56 defer p.poolsLock.Unlock() 57 p.pools[k] = v 58 } 59 func (p *PlatypusPools) getPool(k string) (map[int]*PlatypusCoin, bool) { 60 p.poolsLock.RLock() 61 defer p.poolsLock.RUnlock() 62 r, ok := p.pools[k] 63 return r, ok 64 } 65 66 func (p *PlatypusPools) poolsAddressNoLock() []string { 67 p.poolsLock.RLock() 68 defer p.poolsLock.RUnlock() 69 var values []string 70 for key := range p.pools { 71 values = append(values, key) 72 } 73 return values 74 } 75 76 // PlatypusScraper The scraper object for Platypus Finance 77 type PlatypusScraper struct { 78 exchangeName string 79 80 // channels to signal events 81 run bool 82 initDone chan nothing 83 shutdown chan nothing 84 shutdownDone chan nothing 85 resubscribe chan string 86 87 errorLock sync.RWMutex 88 error error 89 closed bool 90 91 pairScrapers map[string]*PlatypusPairScraper 92 chanTrades chan *dia.Trade 93 94 WsClient *ethclient.Client 95 RestClient *ethclient.Client 96 relDB *models.RelDB 97 platypusCoins map[string]*PlatypusCoin 98 pools *PlatypusPools 99 screenPools bool 100 basePoolRegistry platypusRegistry 101 } 102 103 // NewPlatypusScraper Returns a new exchange scraper 104 func NewPlatypusScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *PlatypusScraper { 105 106 registries := []platypusRegistry{ 107 {Version: 3, Address: common.HexToAddress(platypusMasterRegV3Addr)}, 108 {Version: 2, Address: common.HexToAddress(platypusMasterRegV2Addr)}, 109 {Version: 1, Address: common.HexToAddress(platypusMasterRegV1Addr)}, 110 } 111 112 log.Infof("init rest and ws client for %s", exchange.BlockChain.Name) 113 restClient, err := ethclient.Dial(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_REST", platypusRestDialEth)) 114 if err != nil { 115 log.Fatal("init rest client: ", err) 116 } 117 wsClient, err := ethclient.Dial(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_WS", platypusWsDialEth)) 118 if err != nil { 119 log.Fatal("init ws client: ", err) 120 } 121 122 // Only include pools with (minimum) liquidity bigger than given env var. 123 liquidityThreshold, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD", "0"), 64) 124 if err != nil { 125 liquidityThreshold = float64(0) 126 log.Warnf("parse liquidity threshold: %v. Set to default %v", err, liquidityThreshold) 127 } 128 // Only include pools with (minimum) liquidity USD value bigger than given env var. 129 liquidityThresholdUSD, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD_USD", "0"), 64) 130 if err != nil { 131 liquidityThresholdUSD = float64(0) 132 log.Warnf("parse liquidity threshold: %v. Set to default %v", err, liquidityThresholdUSD) 133 } 134 135 scraper := &PlatypusScraper{ 136 exchangeName: exchange.Name, 137 RestClient: restClient, 138 WsClient: wsClient, 139 initDone: make(chan nothing), 140 shutdown: make(chan nothing), 141 shutdownDone: make(chan nothing), 142 pairScrapers: make(map[string]*PlatypusPairScraper), 143 chanTrades: make(chan *dia.Trade), 144 platypusCoins: make(map[string]*PlatypusCoin), 145 resubscribe: make(chan string), 146 pools: &PlatypusPools{ 147 pools: make(map[string]map[int]*PlatypusCoin), 148 }, 149 } 150 151 scraper.relDB = relDB 152 // Load metadata from master registries 153 for _, registry := range registries { 154 err := scraper.loadPoolsAndCoins(registry, liquidityThreshold, liquidityThresholdUSD) 155 if err != nil { 156 log.Errorf("loadPoolsAndCoins error w %s registry (v%d): %s", registry.Address.Hex(), registry.Version, err) 157 } 158 log.Infof("metadata loaded, now scraper have %d pools data and %d coins", len(scraper.pools.pools), len(scraper.platypusCoins)) 159 } 160 161 scraper.basePoolRegistry = platypusRegistry{Version: 3, Address: common.HexToAddress(platypusMasterRegV3Addr)} 162 scraper.screenPools = true 163 164 if scrape { 165 go scraper.mainLoop() 166 } 167 return scraper 168 } 169 170 // Close Closes any existing API connections, as well as channels of 171 // pairScrapers from calls to ScrapePair 172 func (s *PlatypusScraper) Close() error { 173 s.run = false 174 for _, pairScraper := range s.pairScrapers { 175 pairScraper.closed = true 176 } 177 s.WsClient.Close() 178 s.RestClient.Close() 179 180 close(s.shutdown) 181 <-s.shutdownDone 182 return nil 183 } 184 185 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from the scraper 186 func (s *PlatypusScraper) ScrapePair(pair dia.ExchangePair) (ps PairScraper, err error) { 187 return 188 } 189 190 // Returns the list of all available trade pairs 191 func (s *PlatypusScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 192 return pairs, nil 193 } 194 195 // Channel returns a channel that can be used to receive trades 196 func (s *PlatypusScraper) Channel() chan *dia.Trade { 197 return s.chanTrades 198 } 199 200 // FillSymbolData adds the name to the asset underlying @symbol on scraper 201 func (s *PlatypusScraper) FillSymbolData(symbol string) (dia.Asset, error) { 202 return dia.Asset{Symbol: symbol}, nil 203 } 204 205 // NormalizePair accounts for the pair 206 func (s *PlatypusScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 207 return pair, nil 208 } 209 210 type PlatypusPairScraper struct { 211 parent *PlatypusScraper 212 pair dia.ExchangePair 213 closed bool 214 } 215 216 // Close stops listening for trades of the pair associated with the scraper 217 func (ps *PlatypusPairScraper) Close() error { 218 ps.parent.errorLock.RLock() 219 defer ps.parent.errorLock.RUnlock() 220 ps.closed = true 221 return nil 222 } 223 224 // Error returns an error when the channel Channel() is closed and nil otherwise 225 func (ps *PlatypusPairScraper) Error() error { 226 s := ps.parent 227 s.errorLock.RLock() 228 defer s.errorLock.RUnlock() 229 return s.error 230 } 231 232 // Pair returns the pair this scraper is subscribed to 233 func (ps *PlatypusPairScraper) Pair() dia.ExchangePair { 234 return ps.pair 235 } 236 237 // Load pools and coins metadata from master registry 238 func (s *PlatypusScraper) loadPoolsAndCoins(registry platypusRegistry, liquidityThreshold float64, liquidityThresholdUSD float64) (err error) { 239 log.Infof("loading master contract %s version %d and querying registry", registry.Address.Hex(), registry.Version) 240 contractMaster, err := platypusfinance.NewBaseMasterPlatypusCaller(registry.Address, s.RestClient) 241 if err != nil { 242 log.Error("NewBaseMasterPlatypusCaller: ", err) 243 return err 244 } 245 246 poolCount, err := contractMaster.PoolLength(&bind.CallOpts{}) 247 if err != nil { 248 log.Error("PoolLength: ", err) 249 return err 250 } 251 252 lowerBoundCount := 0 253 for i := 0; i < int(poolCount.Int64()); i++ { 254 asset, errPoolInfo := contractMaster.PoolInfo(&bind.CallOpts{}, big.NewInt(int64(i))) 255 if errPoolInfo != nil { 256 log.Error("PoolInfo: ", errPoolInfo) 257 return err 258 } 259 pool, errPool := s.relDB.GetPoolByAddress(dia.ETHEREUM, asset.LpToken.Hex()) 260 if errPool != nil { 261 log.Errorf("Get pool %s by address: %v", asset.LpToken.Hex(), errPool) 262 } 263 264 lowLiqui := false 265 for _, av := range pool.Assetvolumes { 266 if av.Volume < liquidityThreshold { 267 log.Warnf("low liquidity on %s: %v", pool.Address, av.Volume) 268 lowLiqui = true 269 break 270 } 271 } 272 if lowLiqui { 273 continue 274 } 275 276 liquidity, lowerBound := pool.GetPoolLiquidityUSD() 277 // Discard pool if complete USD liquidity is below threshold. 278 if !lowerBound && liquidity < liquidityThresholdUSD { 279 continue 280 } 281 if lowerBound { 282 lowerBoundCount++ 283 } 284 285 errPoolData := s.loadPoolData(asset.LpToken.Hex()) 286 if errPoolData != nil { 287 log.Errorf("loadPoolData error at asset %s: %s", asset.LpToken.Hex(), errPoolData) 288 return errPoolData 289 } 290 } 291 log.Info("lowerBound: ", lowerBoundCount) 292 293 return err 294 } 295 296 // Runs in a goroutine until scraper is closed 297 func (s *PlatypusScraper) mainLoop() { 298 299 s.run = true 300 for _, pool := range s.pools.poolsAddressNoLock() { 301 err := s.watchSwaps(pool) 302 if err != nil { 303 log.Error("watchSwaps: ", err) 304 } 305 } 306 if s.screenPools { 307 err := s.watchNewPools() 308 if err != nil { 309 log.Error("watchNewPools: ", err) 310 } 311 } 312 313 go func() { 314 defer func() { 315 log.Printf("Shutting down main work routine...\n") 316 if a := recover(); a != nil { 317 log.Errorf("work routine end. Recover msg: %+v", a) 318 } 319 }() 320 321 for s.run { 322 p := <-s.resubscribe 323 log.Info("resub to p: ", p) 324 if s.run { 325 if p == "NEW_POOLS" { 326 if s.screenPools { 327 log.Info("resubscribe to new pools") 328 err := s.watchNewPools() 329 if err != nil { 330 log.Error("watchNewPools in resubscribe: ", err) 331 } 332 } 333 } else { 334 log.Info("resubscribe to swaps from Pool: " + p) 335 err := s.watchSwaps(p) 336 if err != nil { 337 log.Error("watchSwaps in resubscribe: ", err) 338 } 339 } 340 } 341 } 342 }() 343 344 if s.run { 345 if len(s.pairScrapers) == 0 { 346 s.error = errors.New("no pairs to scrape provided") 347 log.Error(s.error.Error()) 348 } 349 } 350 351 if s.error == nil { 352 s.error = errors.New("main loop terminated by Close()") 353 } 354 s.cleanup(nil) 355 } 356 357 func (s *PlatypusScraper) cleanup(err error) { 358 s.errorLock.Lock() 359 defer s.errorLock.Unlock() 360 if err != nil { 361 s.error = err 362 } 363 s.closed = true 364 close(s.shutdownDone) 365 } 366 367 func (s *PlatypusScraper) watchSwaps(pool string) error { 368 contractPool, err := platypusPoolABI.NewPoolFilterer(common.HexToAddress(pool), s.WsClient) 369 if err != nil { 370 log.Fatal(err) 371 } 372 373 sink := make(chan *platypusPoolABI.PoolSwap) 374 header, err := s.RestClient.HeaderByNumber(context.Background(), nil) 375 if err != nil { 376 log.Fatal(err) 377 } 378 startblock := header.Number.Uint64() - uint64(20) 379 log.Infof("subscribing for trades at %s pool", pool) 380 sub, err := contractPool.WatchSwap(&bind.WatchOpts{Start: &startblock}, sink, nil, nil) 381 if err != nil { 382 log.Error("contractPool.WatchSwap: ", err) 383 return err 384 } 385 386 go func() { 387 defer log.Printf("Shutting down pool work routine %s ...\n", pool) 388 defer sub.Unsubscribe() 389 390 subscribed := true 391 for s.run && subscribed { 392 select { 393 case err = <-sub.Err(): 394 if err != nil { 395 log.Error("sub error: ", err) 396 } 397 subscribed = false 398 if s.run { 399 log.Warn("resubscribe pool: ", pool) 400 s.resubscribe <- pool 401 log.Info("scraper: ", s) 402 } 403 log.Warn("subscription error: ", err) 404 case swp := <-sink: 405 s.processSwap(pool, swp) 406 } 407 } 408 }() 409 410 return err 411 } 412 413 func (s *PlatypusScraper) processSwap(pool string, swap *platypusPoolABI.PoolSwap) { 414 foreignName, volume, price, baseToken, quoteToken, err := s.getSwapData(pool, swap) 415 if err != nil { 416 log.Error("getSwapDataPlatypus: ", err) 417 } 418 timestamp := time.Now().Unix() 419 420 trade := &dia.Trade{ 421 Symbol: quoteToken.Symbol, 422 Pair: foreignName, 423 BaseToken: baseToken, 424 QuoteToken: quoteToken, 425 Price: price, 426 Volume: volume, 427 Time: time.Unix(timestamp, 0), 428 ForeignTradeID: swap.Raw.TxHash.Hex() + "-" + fmt.Sprint(swap.Raw.Index), 429 PoolAddress: pool, 430 Source: s.exchangeName, 431 VerifiedPair: true, 432 } 433 434 log.Infof("got trade in pool %s with tx %s", pool, trade.ForeignTradeID) 435 log.Info("trade: ", trade) 436 s.chanTrades <- trade 437 } 438 439 // getSwapDataPlatypus returns the foreign name, volume and price of a swap 440 func (s *PlatypusScraper) getSwapData(pool string, swap *platypusPoolABI.PoolSwap) (foreignName string, volume float64, price float64, baseToken, quoteToken dia.Asset, err error) { 441 fromToken, ok := s.platypusCoins[swap.FromToken.Hex()] 442 if !ok { 443 log.Errorf("token not found: %s-%s", pool, swap.FromToken.Hex()) 444 return 445 } 446 baseToken = dia.Asset{ 447 Name: fromToken.Name, 448 Address: fromToken.Address, 449 Symbol: fromToken.Symbol, 450 Blockchain: Exchanges[s.exchangeName].BlockChain.Name, 451 } 452 453 toToken, ok := s.platypusCoins[swap.ToToken.Hex()] 454 if !ok { 455 log.Errorf("token not found: %s-%s", pool, swap.ToToken.Hex()) 456 return 457 } 458 quoteToken = dia.Asset{ 459 Name: toToken.Name, 460 Address: toToken.Address, 461 Symbol: toToken.Symbol, 462 Blockchain: Exchanges[s.exchangeName].BlockChain.Name, 463 } 464 465 // amountIn = AmountSold / math.Pow10( fromToken.Decimals ) 466 amountIn, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(swap.FromAmount), new(big.Float).SetFloat64(math.Pow10(int(fromToken.Decimals)))).Float64() 467 468 // amountOut = AmountBought / math.Pow10( toToken.Decimals ) 469 amountOut, _ := new(big.Float).Quo(big.NewFloat(0).SetInt(swap.ToAmount), new(big.Float).SetFloat64(math.Pow10(int(toToken.Decimals)))).Float64() 470 471 volume = amountOut 472 price = amountIn / amountOut 473 474 foreignName = toToken.Symbol + "-" + fromToken.Symbol 475 476 return 477 } 478 479 func (s *PlatypusScraper) watchNewPools() error { 480 contractPool, err := platypusPoolABI.NewPoolFilterer(s.basePoolRegistry.Address, s.WsClient) 481 if err != nil { 482 log.Error("NewPoolFilterer: ", err) 483 } 484 485 sink := make(chan *platypusPoolABI.PoolAssetAdded) 486 header, err := s.RestClient.HeaderByNumber(context.Background(), nil) 487 if err != nil { 488 log.Fatal(err) 489 } 490 startblock := header.Number.Uint64() - uint64(platypusLookBackBlocks) 491 sub, err := contractPool.WatchAssetAdded(&bind.WatchOpts{Start: &startblock}, sink, nil, nil) 492 if err != nil { 493 log.Error("WatchPoolAdded: ", err) 494 return err 495 } 496 497 go func() { 498 log.Infof("subscribing to new asset added events at latest registry") 499 defer log.Info("Unsubscribed to new pools") 500 defer sub.Unsubscribe() 501 subscribed := true 502 503 for s.run && subscribed { 504 505 select { 506 case err = <-sub.Err(): 507 if err != nil { 508 log.Error("subscription error in new pools: ", err) 509 } 510 subscribed = false 511 if s.run { 512 s.resubscribe <- "NEW_POOLS" 513 } 514 case vLog := <-sink: 515 516 if _, ok := s.pools.getPool(vLog.Asset.Hex()); !ok { 517 err = s.loadPoolData(vLog.Asset.Hex()) 518 if err != nil { 519 log.Error("loadPoolData in new pools: ", err) 520 } 521 err = s.watchSwaps(vLog.Asset.Hex()) 522 if err != nil { 523 log.Error("watchSwaps in new pools: ", err) 524 } 525 } 526 } 527 } 528 }() 529 530 return nil 531 } 532 533 func (s *PlatypusScraper) loadPoolData(asset string) error { 534 contractAsset, err := platypusAssetABI.NewAssetCaller(common.HexToAddress(asset), s.RestClient) 535 if err != nil { 536 log.Error("NewAssetCaller: ", err) 537 return err 538 } 539 540 pool, err := contractAsset.Pool(&bind.CallOpts{}) 541 if err != nil { 542 log.Error("Pool: ", err) 543 return err 544 } 545 546 contractPool, err := platypusPoolABI.NewPoolCaller(pool, s.RestClient) 547 if err != nil { 548 log.Error("NewPoolCaller: ", err) 549 return err 550 } 551 552 poolTokenAddresses, errGetTokens := contractPool.GetTokenAddresses(&bind.CallOpts{}) 553 if errGetTokens != nil { 554 symbol, err := contractAsset.Symbol(&bind.CallOpts{}) 555 if err != nil { 556 log.Error("contractAsset.Symbol: ", err) 557 } 558 log.Warnf("error calling GetTokenAddresses for %s %s asset: %s", symbol, asset, errGetTokens) 559 } 560 561 var poolCoinsMap = make(map[int]*PlatypusCoin) 562 for cIdx, c := range poolTokenAddresses { 563 var symbol string 564 var decimals *big.Int 565 var name string 566 if c == common.HexToAddress("0x0000000000000000000000000000000000000000") { 567 continue 568 } else { 569 contractToken, err := platypusTokenABI.NewTokenCaller(c, s.RestClient) 570 if err != nil { 571 log.Error("NewTokenCaller: ", err) 572 continue 573 } 574 575 symbol, err = contractToken.Symbol(&bind.CallOpts{}) 576 if err != nil { 577 log.Error("Symbol: ", err, c.Hex()) 578 continue 579 } 580 581 decimals, err = contractToken.Decimals(&bind.CallOpts{}) 582 if err != nil { 583 log.Error("Decimals: ", err) 584 continue 585 } 586 587 name, err = contractToken.Name(&bind.CallOpts{}) 588 if err != nil { 589 log.Error("Name: ", err) 590 continue 591 } 592 } 593 594 poolCoinsMap[cIdx] = &PlatypusCoin{ 595 Symbol: symbol, 596 Decimals: uint8(decimals.Uint64()), 597 Name: name, 598 Address: c.Hex(), 599 } 600 s.platypusCoins[c.Hex()] = &PlatypusCoin{ 601 Symbol: symbol, 602 Decimals: uint8(decimals.Uint64()), 603 Name: name, 604 Address: c.Hex(), 605 } 606 s.pools.setPool(pool.Hex(), poolCoinsMap) 607 } 608 609 return nil 610 }