github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/CurvefiScraper.go (about) 1 package scrapers 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "math" 8 "math/big" 9 "reflect" 10 "strconv" 11 "strings" 12 "sync" 13 "time" 14 15 "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/curvefi" 16 "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/curvefi/curvepool" 17 "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/curvefifactory" 18 "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/curvefimeta" 19 models "github.com/diadata-org/diadata/pkg/model" 20 "github.com/diadata-org/diadata/pkg/utils" 21 22 "github.com/diadata-org/diadata/pkg/dia" 23 "github.com/ethereum/go-ethereum/accounts/abi" 24 "github.com/ethereum/go-ethereum/accounts/abi/bind" 25 "github.com/ethereum/go-ethereum/common" 26 "github.com/ethereum/go-ethereum/ethclient" 27 ) 28 29 const ( 30 curveRestDialEth = "" 31 curveWsDialEth = "" 32 curveRestDialFantom = "" 33 curveWsDialFantom = "" 34 curveRestDialMoonbeam = "" 35 curveWsDialMoonbeam = "" 36 curveRestDialPolygon = "" 37 curveWsDialPolygon = "" 38 curveRestDialArbitrum = "" 39 curveWsDialArbitrum = "" 40 ) 41 42 type CurveCoin struct { 43 Symbol string 44 Decimals uint8 45 Address string 46 Name string 47 } 48 49 func assetToCoin(a dia.Asset) (c *CurveCoin) { 50 c = &CurveCoin{} 51 c.Address = a.Address 52 c.Decimals = a.Decimals 53 c.Name = a.Name 54 c.Symbol = a.Symbol 55 return 56 } 57 58 type curveRegistry struct { 59 Address common.Address 60 Type int 61 } 62 63 type Pools struct { 64 pools map[string]map[int]*CurveCoin 65 poolsLock sync.RWMutex 66 } 67 68 func (p *Pools) setPool(k string, v map[int]*CurveCoin) { 69 p.poolsLock.Lock() 70 defer p.poolsLock.Unlock() 71 p.pools[k] = v 72 } 73 74 func (p *Pools) getPool(k string) (map[int]*CurveCoin, bool) { 75 p.poolsLock.RLock() 76 defer p.poolsLock.RUnlock() 77 r, ok := p.pools[k] 78 return r, ok 79 } 80 81 func (p *Pools) getPoolCoin(poolk string, coink int) (*CurveCoin, bool) { 82 p.poolsLock.RLock() 83 defer p.poolsLock.RUnlock() 84 r, ok := p.pools[poolk][coink] 85 return r, ok 86 } 87 88 func (p *Pools) poolsAddressNoLock() []string { 89 p.poolsLock.RLock() 90 defer p.poolsLock.RUnlock() 91 var values []string 92 for key := range p.pools { 93 values = append(values, key) 94 } 95 return values 96 } 97 98 // CurveFIScraper is a curve finance scraper on a specific blockchain. 99 type CurveFIScraper struct { 100 exchangeName string 101 102 // channels to signal events 103 run bool 104 initDone chan nothing 105 shutdown chan nothing 106 shutdownDone chan nothing 107 108 errorLock sync.RWMutex 109 error error 110 closed bool 111 112 pairScrapers map[string]*CurveFIPairScraper 113 productPairIds map[string]int 114 chanTrades chan *dia.Trade 115 116 WsClient *ethclient.Client 117 RestClient *ethclient.Client 118 relDB *models.RelDB 119 curveCoins map[string]*CurveCoin 120 resubscribe chan string 121 pools *Pools 122 registriesUnderlying []curveRegistry 123 screenPools bool 124 } 125 126 // makeCurvefiScraper returns a curve finance scraper as used in NewCurvefiScraper. 127 func makeCurvefiScraper(exchange dia.Exchange, restDial string, wsDial string, relDB *models.RelDB) *CurveFIScraper { 128 var ( 129 restClient, wsClient *ethclient.Client 130 err error 131 scraper *CurveFIScraper 132 ) 133 134 log.Infof("Init rest and ws client for %s.", exchange.BlockChain.Name) 135 restClient, err = ethclient.Dial(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_REST", restDial)) 136 if err != nil { 137 log.Fatal("init rest client: ", err) 138 } 139 wsClient, err = ethclient.Dial(utils.Getenv(strings.ToUpper(exchange.BlockChain.Name)+"_URI_WS", wsDial)) 140 if err != nil { 141 log.Fatal("init ws client: ", err) 142 } 143 144 scraper = &CurveFIScraper{ 145 exchangeName: exchange.Name, 146 RestClient: restClient, 147 WsClient: wsClient, 148 relDB: relDB, 149 initDone: make(chan nothing), 150 shutdown: make(chan nothing), 151 shutdownDone: make(chan nothing), 152 productPairIds: make(map[string]int), 153 pairScrapers: make(map[string]*CurveFIPairScraper), 154 chanTrades: make(chan *dia.Trade), 155 curveCoins: make(map[string]*CurveCoin), 156 resubscribe: make(chan string), 157 pools: &Pools{ 158 pools: make(map[string]map[int]*CurveCoin), 159 }, 160 } 161 162 // Only include pools with (minimum) liquidity bigger than given env var. 163 liquidityThreshold, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD", "0"), 64) 164 if err != nil { 165 liquidityThreshold = float64(0) 166 log.Warnf("parse liquidity threshold: %v. Set to default %v", err, liquidityThreshold) 167 } 168 169 // Only include pools with (minimum) liquidity USD value bigger than given env var. 170 liquidityThresholdUSD, err := strconv.ParseFloat(utils.Getenv("LIQUIDITY_THRESHOLD_USD", "0"), 64) 171 if err != nil { 172 liquidityThresholdUSD = float64(0) 173 log.Warnf("parse liquidity threshold: %v. Set to default %v", err, liquidityThresholdUSD) 174 } 175 176 scraper.loadPools(liquidityThreshold, liquidityThresholdUSD) 177 178 return scraper 179 } 180 181 func NewCurveFIScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *CurveFIScraper { 182 183 var scraper *CurveFIScraper 184 185 switch exchange.Name { 186 case dia.CurveFIExchange: 187 scraper = makeCurvefiScraper(exchange, curveRestDialEth, curveWsDialEth, relDB) 188 basePoolRegistry := curveRegistry{Type: 1, Address: common.HexToAddress(exchange.Contract)} 189 cryptoswapPools := curveRegistry{Type: 1, Address: common.HexToAddress("0x8F942C20D02bEfc377D41445793068908E2250D0")} 190 metaPoolRegistry := curveRegistry{Type: 2, Address: common.HexToAddress("0xB9fC157394Af804a3578134A6585C0dc9cc990d4")} 191 factoryPools := curveRegistry{Type: 3, Address: common.HexToAddress("0xF18056Bbd320E96A48e3Fbf8bC061322531aac99")} 192 factory2Pools := curveRegistry{Type: 3, Address: common.HexToAddress("0x4F8846Ae9380B90d2E71D5e3D042dff3E7ebb40d")} 193 stableSwapRegistry := curveRegistry{Type: 3, Address: common.HexToAddress("0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf")} 194 scraper.registriesUnderlying = []curveRegistry{ 195 factoryPools, 196 factory2Pools, 197 metaPoolRegistry, 198 basePoolRegistry, 199 cryptoswapPools, 200 stableSwapRegistry} 201 scraper.screenPools = true 202 203 case dia.CurveFIExchangeFantom: 204 scraper = makeCurvefiScraper(exchange, curveRestDialFantom, curveWsDialFantom, relDB) 205 stableSwapFactory := curveRegistry{Type: 2, Address: common.HexToAddress("0x686d67265703d1f124c45e33d47d794c566889ba")} 206 scraper.registriesUnderlying = []curveRegistry{stableSwapFactory} 207 scraper.screenPools = false 208 209 case dia.CurveFIExchangeMoonbeam: 210 scraper = makeCurvefiScraper(exchange, curveRestDialMoonbeam, curveWsDialMoonbeam, relDB) 211 stableSwapFactory := curveRegistry{Type: 2, Address: common.HexToAddress("0x4244eB811D6e0Ef302326675207A95113dB4E1F8")} 212 scraper.registriesUnderlying = []curveRegistry{stableSwapFactory} 213 scraper.screenPools = false 214 215 case dia.CurveFIExchangePolygon: 216 scraper = makeCurvefiScraper(exchange, curveRestDialPolygon, curveWsDialPolygon, relDB) 217 stableSwapFactory := curveRegistry{Type: 2, Address: common.HexToAddress("0x722272D36ef0Da72FF51c5A65Db7b870E2e8D4ee")} 218 scraper.registriesUnderlying = []curveRegistry{stableSwapFactory} 219 scraper.screenPools = false 220 case dia.CurveFIExchangeArbitrum: 221 scraper = makeCurvefiScraper(exchange, curveRestDialArbitrum, curveWsDialArbitrum, relDB) 222 stableSwapFactory := curveRegistry{Type: 2, Address: common.HexToAddress("0xb17b674D9c5CB2e441F8e196a2f048A81355d031")} 223 scraper.registriesUnderlying = []curveRegistry{stableSwapFactory} 224 } 225 226 if scrape { 227 go scraper.mainLoop() 228 } 229 return scraper 230 } 231 232 func (scraper *CurveFIScraper) mainLoop() { 233 scraper.run = true 234 235 for _, pool := range scraper.pools.poolsAddressNoLock() { 236 err := scraper.watchSwaps(pool) 237 if err != nil { 238 log.Error("watchSwaps: ", err) 239 } 240 } 241 242 go func() { 243 for scraper.run { 244 p := <-scraper.resubscribe 245 log.Info("resub to p: ", p) 246 247 if scraper.run { 248 log.Info("resubscribe to swaps from Pool: " + p) 249 err := scraper.watchSwaps(p) 250 if err != nil { 251 log.Error("watchSwaps in resubscribe: ", err) 252 } 253 254 } 255 } 256 }() 257 258 if scraper.run { 259 if len(scraper.pools.pools) == 0 { 260 scraper.error = errors.New("no pairs to scrape provided") 261 log.Error(scraper.error.Error()) 262 } 263 } 264 265 time.Sleep(10 * time.Second) 266 267 if scraper.error == nil { 268 scraper.error = errors.New("main loop terminated by Close()") 269 } 270 scraper.cleanup(nil) 271 } 272 273 func (scraper *CurveFIScraper) watchSwaps(pool string) error { 274 275 filterer, err := curvepool.NewCurvepoolFilterer(common.HexToAddress(pool), scraper.WsClient) 276 if err != nil { 277 log.Fatal(err) 278 } 279 sink := make(chan *curvepool.CurvepoolTokenExchange) 280 281 filtererV2, err := curvefifactory.NewCurvefifactoryFilterer(common.HexToAddress(pool), scraper.WsClient) 282 if err != nil { 283 log.Fatal(err) 284 } 285 sinkV2 := make(chan *curvefifactory.CurvefifactoryTokenExchange) 286 287 header, err := scraper.RestClient.HeaderByNumber(context.Background(), nil) 288 if err != nil { 289 log.Fatal(err) 290 } 291 startblock := header.Number.Uint64() - uint64(20) 292 293 sub, err := filterer.WatchTokenExchange(&bind.WatchOpts{Start: &startblock}, sink, nil) 294 if err != nil { 295 log.Error("WatchTokenExchange: ", err) 296 } 297 298 subV2, err := filtererV2.WatchTokenExchange(&bind.WatchOpts{Start: &startblock}, sinkV2, nil) 299 if err != nil { 300 log.Error("WatchTokenExchange: ", err) 301 } 302 303 sinkUnderlying := make(chan *curvepool.CurvepoolTokenExchangeUnderlying) 304 subUnderlying, err := filterer.WatchTokenExchangeUnderlying(&bind.WatchOpts{Start: &startblock}, sinkUnderlying, nil) 305 if err != nil { 306 log.Error("WatchTokenExchangeUnderlying: ", err) 307 } 308 309 go func() { 310 fmt.Println("Curvefi Subscribed to pool: " + pool) 311 defer fmt.Printf("Curvefi UnSubscribed to pool %s with error: %v", pool, err) 312 defer sub.Unsubscribe() 313 defer subV2.Unsubscribe() 314 defer subUnderlying.Unsubscribe() 315 subscribed := true 316 317 for scraper.run && subscribed { 318 select { 319 case err = <-sub.Err(): 320 if err != nil { 321 log.Error("sub error: ", err) 322 } 323 subscribed = false 324 if scraper.run { 325 log.Warn("resubscribe pool: ", pool) 326 scraper.resubscribe <- pool 327 } 328 log.Warn("subscription error: ", err) 329 case swp := <-sink: 330 log.Info("got swap V1") 331 scraper.processSwap(pool, swp) 332 333 case swpV2 := <-sinkV2: 334 log.Info("got swap V2") 335 scraper.processSwap(pool, swpV2) 336 case swpUnderlying := <-sinkUnderlying: 337 log.Warn("got underlying swap: ", swpUnderlying) 338 // Only fetch trades from USDR pool until we have parsing TokenExchangeUnderlying resolved. 339 // if pool == common.HexToAddress("0xa138341185a9d0429b0021a11fb717b225e13e1f").Hex() && Exchanges[scraper.exchangeName].BlockChain.Name == dia.POLYGON { 340 scraper.processSwap(pool, swpUnderlying) 341 // } 342 } 343 } 344 }() 345 346 return err 347 348 } 349 350 func (scraper *CurveFIScraper) processSwap(pool string, swp interface{}) { 351 var ( 352 foreignName string 353 volume float64 354 price float64 355 baseToken dia.Asset 356 quoteToken dia.Asset 357 foreignTradeID string 358 err error 359 ) 360 361 switch s := swp.(type) { 362 case *curvepool.CurvepoolTokenExchange: 363 foreignName, volume, price, baseToken, quoteToken, err = scraper.getSwapDataCurve(pool, s) 364 if err != nil { 365 log.Error("getSwapDataCurve: ", err) 366 return 367 } 368 foreignTradeID = s.Raw.TxHash.Hex() + "-" + fmt.Sprint(s.Raw.Index) 369 case *curvefifactory.CurvefifactoryTokenExchange: 370 foreignName, volume, price, baseToken, quoteToken, err = scraper.getSwapDataCurve(pool, s) 371 if err != nil { 372 log.Error("getSwapDataCurve: ", err) 373 return 374 } 375 foreignTradeID = s.Raw.TxHash.Hex() + "-" + fmt.Sprint(s.Raw.Index) 376 case *curvepool.CurvepoolTokenExchangeUnderlying: 377 foreignName, volume, price, baseToken, quoteToken, err = scraper.getSwapDataCurve(pool, s) 378 if err != nil { 379 log.Error("getSwapDataCurve: ", err) 380 return 381 } 382 foreignTradeID = s.Raw.TxHash.Hex() + "-" + fmt.Sprint(s.Raw.Index) 383 } 384 385 // Hotfix for zero address bug. 386 // TO DO: Improve decoding. 387 if quoteToken.Address == "0x0000000000000000000000000000000000000000" || baseToken.Address == "0x0000000000000000000000000000000000000000" { 388 log.Errorf("got zero address on pool %s. discard trade.", pool) 389 return 390 } 391 392 timestamp := time.Now().Unix() 393 394 trade := &dia.Trade{ 395 Symbol: quoteToken.Symbol, 396 Pair: foreignName, 397 BaseToken: baseToken, 398 QuoteToken: quoteToken, 399 Price: price, 400 Volume: volume, 401 Time: time.Unix(timestamp, 0), 402 ForeignTradeID: foreignTradeID, 403 PoolAddress: pool, 404 Source: scraper.exchangeName, 405 VerifiedPair: true, 406 } 407 log.Infof("Got Trade in pool %s:\n %v", pool, trade) 408 409 scraper.chanTrades <- trade 410 411 } 412 413 // getSwapDataCurve returns the foreign name, volume and price of a swap 414 func (scraper *CurveFIScraper) getSwapDataCurve(pool string, swp interface{}) ( 415 foreignName string, 416 volume float64, 417 price float64, 418 baseToken, 419 quoteToken dia.Asset, 420 err error, 421 ) { 422 var ( 423 fromToken *CurveCoin 424 toToken *CurveCoin 425 amountIn float64 426 amountOut float64 427 ok bool 428 ) 429 430 switch s := swp.(type) { 431 case *curvepool.CurvepoolTokenExchange: 432 fromToken, ok = scraper.pools.getPoolCoin(pool, int(s.SoldId.Int64())) 433 if !ok { 434 err = fmt.Errorf("token not found: " + pool + "-" + s.SoldId.String()) 435 } 436 toToken, ok = scraper.pools.getPoolCoin(pool, int(s.BoughtId.Int64())) 437 if !ok { 438 err = fmt.Errorf("token not found: " + pool + "-" + s.SoldId.String()) 439 } 440 amountIn, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(s.TokensSold), new(big.Float).SetFloat64(math.Pow10(int(fromToken.Decimals)))).Float64() 441 amountOut, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(s.TokensBought), new(big.Float).SetFloat64(math.Pow10(int(toToken.Decimals)))).Float64() 442 case *curvefifactory.CurvefifactoryTokenExchange: 443 fromToken, ok = scraper.pools.getPoolCoin(pool, int(s.SoldId.Int64())) 444 if !ok { 445 err = fmt.Errorf("token not found: " + pool + "-" + s.SoldId.String()) 446 } 447 toToken, ok = scraper.pools.getPoolCoin(pool, int(s.BoughtId.Int64())) 448 if !ok { 449 err = fmt.Errorf("token not found: " + pool + "-" + s.SoldId.String()) 450 } 451 if toToken == nil || fromToken == nil { 452 err = errors.New("token not available, skip trade.") 453 return 454 } 455 amountIn, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(s.TokensSold), new(big.Float).SetFloat64(math.Pow10(int(fromToken.Decimals)))).Float64() 456 amountOut, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(s.TokensBought), new(big.Float).SetFloat64(math.Pow10(int(toToken.Decimals)))).Float64() 457 case *curvepool.CurvepoolTokenExchangeUnderlying: 458 log.Info("Got TokenExchangeUnderlying in pool: ", pool) 459 var ( 460 isMetaPool bool 461 underlyingCoins [8]common.Address 462 underlyingCoinCount *big.Int 463 metaCoinCount *big.Int 464 errNCoin error 465 errCoins error 466 ) 467 soldID := int(s.SoldId.Int64()) 468 boughtId := int(s.BoughtId.Int64()) 469 tokenSold := s.TokensSold 470 tokenBought := s.TokensBought 471 472 for _, registry := range scraper.registriesUnderlying { 473 if registry.Type == 2 { 474 metaRegistryContract, errMeta := curvefimeta.NewCurvefimetaCaller(registry.Address, scraper.RestClient) 475 if errMeta != nil { 476 log.Error("NewCurvefiCaller: ", errMeta) 477 } 478 // Get underlying coins from on-chain. soldID and boughtID are referring to these. 479 underlyingCoins, errCoins = metaRegistryContract.GetUnderlyingCoins(&bind.CallOpts{}, common.HexToAddress(pool)) 480 if errCoins != nil || (underlyingCoins[soldID] == (common.Address{}) && underlyingCoins[boughtId] == (common.Address{})) { 481 log.Warnf("Failed to call GetUnderlyingCoins: %v", errCoins) 482 continue 483 } else { 484 log.Warn("TokenExchangeUnderlying from meta type pool") 485 log.Warnf("bought id and sold id: %v -- %v", boughtId, soldID) 486 log.Warnf("tokens bought and tokens sold: %v -- %v ", tokenBought, tokenSold) 487 metaCoinCount, underlyingCoinCount, errNCoin = metaRegistryContract.GetMetaNCoins(&bind.CallOpts{}, common.HexToAddress(pool)) 488 if errNCoin != nil { 489 log.Errorf("calling GetMetaNCoins: %v", errCoins) 490 continue 491 } 492 log.Warnf("metaCoinCount: %v, underlyingCoinCount: %v", metaCoinCount, underlyingCoinCount) 493 isMetaPool = true 494 break 495 } 496 497 } 498 if registry.Type == 1 { 499 basepoolRegistryContract, errBase := curvefi.NewCurvefiCaller(registry.Address, scraper.RestClient) 500 if errBase != nil { 501 log.Error("NewCurvefiCaller: ", errBase) 502 } 503 // Get underlying coins from on-chain. soldID and boughtID are referring to these. 504 underlyingCoins, errCoins = basepoolRegistryContract.GetUnderlyingCoins(&bind.CallOpts{}, common.HexToAddress(pool)) 505 if errCoins != nil || (underlyingCoins[soldID] == (common.Address{}) && underlyingCoins[boughtId] == (common.Address{})) { 506 continue 507 } else { 508 log.Warn("TokenExchangeUnderlying from base type pool") 509 log.Warnf("bought id and sold id: %v -- %v", boughtId, soldID) 510 log.Warnf("tokens bought and tokens sold: %v -- %v ", tokenBought, tokenSold) 511 break 512 } 513 514 } 515 } 516 log.Warnf("meta pool : %v, soldID: %v", isMetaPool, soldID) 517 if isMetaPool && soldID > 0 && boughtId == 0 { 518 // This is the only case we need to look into the AddLiquidity event for finding the actual amount of sold token 519 // as this event contains the meta pool token amount in this case! 520 521 tokenAmount, errTokenAmounts := scraper.getTokenAmount(s.Raw.TxHash, common.HexToAddress(pool), soldID, metaCoinCount, underlyingCoinCount) 522 if err != nil { 523 log.Error("getting AddLiquidity event for tx: ", s.Raw.TxHash.Hex()) 524 err = errTokenAmounts 525 return 526 } 527 log.Warnf("token Amount %v", tokenAmount) 528 // Again we need to subtract MAX_COIN index from this value, (which is metaCoinCount minus one) 529 tokenSold = tokenAmount 530 } 531 log.Infof("token sold: %v", tokenSold) 532 533 fromTokenAddress := underlyingCoins[int(soldID)] 534 toTokenAddress := underlyingCoins[int(boughtId)] 535 536 fromTokenAsset, errToken := scraper.relDB.GetAsset(fromTokenAddress.Hex(), Exchanges[scraper.exchangeName].BlockChain.Name) 537 if errToken != nil { 538 err = errToken 539 return 540 } 541 toTokenAsset, errToken := scraper.relDB.GetAsset(toTokenAddress.Hex(), Exchanges[scraper.exchangeName].BlockChain.Name) 542 if errToken != nil { 543 err = errToken 544 return 545 } 546 log.Infof("fromToken address -- token: %s -- %v", fromTokenAddress.Hex(), fromTokenAsset) 547 log.Infof("toToken address -- token: %s -- %v", toTokenAddress.Hex(), toTokenAsset) 548 fromToken = assetToCoin(fromTokenAsset) 549 toToken = assetToCoin(toTokenAsset) 550 551 amountIn, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(tokenSold), new(big.Float).SetFloat64(math.Pow10(int(fromToken.Decimals)))).Float64() 552 amountOut, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(tokenBought), new(big.Float).SetFloat64(math.Pow10(int(toToken.Decimals)))).Float64() 553 } 554 555 baseToken = dia.Asset{ 556 Name: fromToken.Name, 557 Address: fromToken.Address, 558 Symbol: fromToken.Symbol, 559 Blockchain: Exchanges[scraper.exchangeName].BlockChain.Name, 560 } 561 quoteToken = dia.Asset{ 562 Name: toToken.Name, 563 Address: toToken.Address, 564 Symbol: toToken.Symbol, 565 Blockchain: Exchanges[scraper.exchangeName].BlockChain.Name, 566 } 567 568 volume = amountOut 569 price = amountIn / amountOut 570 571 foreignName = toToken.Symbol + "-" + fromToken.Symbol 572 573 return 574 } 575 576 func (scraper *CurveFIScraper) getTokenAmount(txHash common.Hash, poolAddress common.Address, soldID int, metaCoinCount, underlyingCoinCount *big.Int) (*big.Int, error) { 577 // Here we need to get the event log manually as the len of the token_amounts array is unknown! 578 // (fixed to 2 in the current curvepool contract, but there are cases when it's 3 like this pool: 579 // https://etherscan.io/address/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7) 580 // We get the value using reflection. 581 receipt, err := scraper.RestClient.TransactionReceipt(context.Background(), txHash) 582 if err != nil { 583 return nil, err 584 } 585 basePoolCoinCount := underlyingCoinCount.Int64() - 1 // We need to subtract that one meta pool coin here! 586 abiAddLiquidityJson := `[{"name":"AddLiquidity","inputs":[{"type":"address","name":"provider","indexed":true},{"type":"uint256[%d]","name":"token_amounts","indexed":false},{"type":"uint256[%d]","name":"fees","indexed":false},{"type":"uint256","name":"invariant","indexed":false},{"type":"uint256","name":"token_supply","indexed":false}],"anonymous":false,"type":"event"}]` 587 abiAddLiquidity, err := abi.JSON(strings.NewReader(fmt.Sprintf(abiAddLiquidityJson, basePoolCoinCount, basePoolCoinCount))) 588 if err != nil { 589 return nil, err 590 } 591 for _, log := range receipt.Logs { 592 if len(log.Topics) < 2 || log.Topics[1] != poolAddress.Hash() { 593 continue 594 } 595 valueMap := make(map[string]interface{}) 596 err := abiAddLiquidity.UnpackIntoMap(valueMap, "AddLiquidity", log.Data) 597 if err != nil { 598 continue 599 } 600 // Again we need to subtract MAX_COIN index from this value, (which is metaCoinCount minus one) 601 tokenBoughtIdx := soldID - (int(metaCoinCount.Int64()) - 1) 602 tokenAmount, ok := reflect.ValueOf(valueMap["token_amounts"]).Index(tokenBoughtIdx).Interface().(*big.Int) 603 if !ok { 604 return nil, errors.New("couldn't parse the AddLiquidity event") 605 } 606 return tokenAmount, nil 607 } 608 return nil, errors.New("AddLiquidity log couldn't be found") 609 610 } 611 612 func (scraper *CurveFIScraper) loadPools(liquidityThreshold float64, liquidityThresholdUSD float64) { 613 614 pools, err := scraper.relDB.GetAllPoolsExchange(scraper.exchangeName, liquidityThreshold) 615 if err != nil { 616 log.Fatal("fetch pools: ", err) 617 } 618 log.Infof("found %v pools to subscribe: ", len(pools)) 619 620 blacklistedPools, err := getAddressesFromConfig("curve/blacklist_pools/" + scraper.exchangeName) 621 if err != nil { 622 log.Fatal("blacklisted pools: ", err) 623 } 624 var blacklistedPoolsString []string 625 for _, bp := range blacklistedPools { 626 blacklistedPoolsString = append(blacklistedPoolsString, bp.Hex()) 627 } 628 log.Infof("remove %v blacklisted pools: %s", len(blacklistedPools), blacklistedPoolsString) 629 630 lowerBoundCount := 0 631 for _, pool := range pools { 632 if utils.Contains(&blacklistedPoolsString, pool.Address) { 633 continue 634 } 635 636 liquidity, lowerBound := pool.GetPoolLiquidityUSD() 637 // Discard pool if complete USD liquidity is below threshold. 638 if !lowerBound && liquidity < liquidityThresholdUSD { 639 continue 640 } 641 if lowerBound { 642 log.Info("lowerBound pool: ", pool.Address) 643 lowerBoundCount++ 644 } 645 646 var poolCoinsMap = make(map[int]*CurveCoin) 647 for _, av := range pool.Assetvolumes { 648 poolCoinsMap[int(av.Index)] = &CurveCoin{ 649 Symbol: av.Asset.Symbol, 650 Decimals: av.Asset.Decimals, 651 Name: av.Asset.Name, 652 Address: av.Asset.Address, 653 } 654 scraper.curveCoins[av.Asset.Address] = &CurveCoin{ 655 Symbol: av.Asset.Symbol, 656 Decimals: av.Asset.Decimals, 657 } 658 } 659 scraper.pools.setPool(pool.Address, poolCoinsMap) 660 } 661 log.Infof("Total number of %v pools to subscribe.", len(scraper.pools.pools)) 662 log.Infof("Number of lower bound pools: %v", lowerBoundCount) 663 } 664 665 func (scraper *CurveFIScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 666 667 return 668 } 669 670 func (scraper *CurveFIScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 671 return dia.ExchangePair{}, nil 672 } 673 674 func (scraper *CurveFIScraper) FillSymbolData(symbol string) (dia.Asset, error) { 675 return dia.Asset{}, nil 676 } 677 678 func (scraper *CurveFIScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 679 scraper.errorLock.RLock() 680 defer scraper.errorLock.RUnlock() 681 682 if scraper.error != nil { 683 return nil, scraper.error 684 } 685 686 if scraper.closed { 687 return nil, errors.New("CurveFIScraper is closed") 688 } 689 690 pairScraper := &CurveFIPairScraper{ 691 parent: scraper, 692 pair: pair, 693 } 694 695 scraper.pairScrapers[pair.ForeignName] = pairScraper 696 697 return pairScraper, nil 698 } 699 func (scraper *CurveFIScraper) cleanup(err error) { 700 scraper.errorLock.Lock() 701 defer scraper.errorLock.Unlock() 702 if err != nil { 703 scraper.error = err 704 } 705 scraper.closed = true 706 close(scraper.shutdownDone) 707 } 708 709 func (scraper *CurveFIScraper) Close() error { 710 // close the pair scraper channels 711 scraper.run = false 712 for _, pairScraper := range scraper.pairScrapers { 713 pairScraper.closed = true 714 } 715 scraper.WsClient.Close() 716 scraper.RestClient.Close() 717 718 close(scraper.shutdown) 719 <-scraper.shutdownDone 720 return nil 721 } 722 723 type CurveFIPairScraper struct { 724 parent *CurveFIScraper 725 pair dia.ExchangePair 726 closed bool 727 } 728 729 func (pairScraper *CurveFIPairScraper) Pair() dia.ExchangePair { 730 return pairScraper.pair 731 } 732 733 func (scraper *CurveFIScraper) Channel() chan *dia.Trade { 734 return scraper.chanTrades 735 } 736 737 func (pairScraper *CurveFIPairScraper) Error() error { 738 s := pairScraper.parent 739 s.errorLock.RLock() 740 defer s.errorLock.RUnlock() 741 return s.error 742 } 743 744 func (pairScraper *CurveFIPairScraper) Close() error { 745 pairScraper.parent.errorLock.RLock() 746 defer pairScraper.parent.errorLock.RUnlock() 747 pairScraper.closed = true 748 return nil 749 }