github.com/diadata-org/diadata@v1.4.593/pkg/dia/scraper/exchange-scrapers/AnyswapEthScraper.go (about) 1 package scrapers 2 3 import ( 4 "encoding/json" 5 "errors" 6 "math" 7 "math/big" 8 "strconv" 9 "strings" 10 "sync" 11 "time" 12 13 anyswap "github.com/diadata-org/diadata/pkg/dia/scraper/exchange-scrapers/anyswap" 14 models "github.com/diadata-org/diadata/pkg/model" 15 16 "github.com/diadata-org/diadata/pkg/dia" 17 "github.com/diadata-org/diadata/pkg/utils" 18 "github.com/ethereum/go-ethereum/accounts/abi/bind" 19 "github.com/ethereum/go-ethereum/common" 20 "github.com/ethereum/go-ethereum/ethclient" 21 ) 22 23 var ( 24 // anyswapEthereumContractAddress = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f" 25 anyswapAPIURL = "https://bridgeapi.anyswap.exchange/v3/serverinfoV3?chainId=all&version=STABLEV3" 26 // maps chainID to chain name. 27 // TO DO: import from postgres. 28 chainMap map[string]string 29 // maps chainID+address to the corresponding dia.Asset. 30 assetMap map[string]dia.Asset 31 ) 32 33 const ( 34 anyswapWaitMilliseconds = "200" 35 ) 36 37 type AnyswapToken struct { 38 Address common.Address 39 Symbol string 40 Decimals uint8 41 Name string 42 } 43 44 type AnyswapPair struct { 45 Token0 UniswapToken 46 Token1 UniswapToken 47 ForeignName string 48 Address common.Address 49 } 50 51 type AnyswapSwap struct { 52 ID string 53 Timestamp int64 54 Pair UniswapPair 55 Amount0In float64 56 Amount0Out float64 57 Amount1In float64 58 Amount1Out float64 59 } 60 61 type AnyswapScraper struct { 62 WsClientMap map[string]*ethclient.Client 63 RestClientMap map[string]*ethclient.Client 64 db *models.RelDB 65 // signaling channels for session initialization and finishing 66 //initDone chan nothing 67 run bool 68 shutdown chan nothing 69 shutdownDone chan nothing 70 // error handling; to read error or closed, first acquire read lock 71 // only cleanup method should hold write lock 72 errorLock sync.RWMutex 73 error error 74 closed bool 75 // used to keep track of trading pairs that we subscribed to 76 pairScrapers map[string]*AnyswapPairScraper 77 exchangeName string 78 chanTrades chan *dia.Trade 79 waitTime int 80 anyswapAssetInfo map[string]map[string]interface{} 81 } 82 83 // NewUniswapScraper returns a new UniswapScraper for the given pair 84 func NewAnyswapScraper(exchange dia.Exchange, scrape bool, relDB *models.RelDB) *AnyswapScraper { 85 log.Info("NewUniswapScraper: ", exchange.Name) 86 var wsClientMap, restClientMap map[string]*ethclient.Client 87 var waitTime int 88 var err error 89 assetMap = make(map[string]dia.Asset) 90 91 switch exchange.Name { 92 case dia.AnyswapExchange: 93 exchangeFactoryContractAddress = exchange.Contract 94 waitTimeString := utils.Getenv("UNISWAP_WAIT_TIME", anyswapWaitMilliseconds) 95 waitTime, err = strconv.Atoi(waitTimeString) 96 if err != nil { 97 log.Error("could not parse wait time: ", err) 98 waitTime = 100 99 } 100 101 } 102 103 chainMap = make(map[string]string) 104 chainMap["1"] = dia.ETHEREUM 105 chainMap["56"] = dia.BINANCESMARTCHAIN 106 chainMap["137"] = dia.POLYGON 107 chainMap["250"] = dia.FANTOM 108 chainMap["1284"] = dia.MOONBEAM 109 chainMap["1285"] = dia.MOONRIVER 110 chainMap["42161"] = dia.ARBITRUM 111 chainMap["43114"] = dia.AVALANCHE 112 113 restClientMap, wsClientMap, err = getClientMaps() 114 if err != nil { 115 log.Fatal("get client map: ", err) 116 } 117 118 allAssetsAllChains, err := fetchEndpoint(anyswapAPIURL) 119 if err != nil { 120 log.Fatal("fetch asset info from anyswap API endpoint: ", err) 121 } 122 123 s := &AnyswapScraper{ 124 RestClientMap: restClientMap, 125 WsClientMap: wsClientMap, 126 db: relDB, 127 shutdown: make(chan nothing), 128 shutdownDone: make(chan nothing), 129 pairScrapers: make(map[string]*AnyswapPairScraper), 130 exchangeName: exchange.Name, 131 error: nil, 132 chanTrades: make(chan *dia.Trade), 133 waitTime: waitTime, 134 anyswapAssetInfo: allAssetsAllChains, 135 } 136 137 if scrape { 138 go s.mainLoop() 139 } 140 return s 141 } 142 143 // runs in a goroutine until s is closed 144 func (s *AnyswapScraper) mainLoop() { 145 146 // wait for all pairs have added into s.PairScrapers 147 time.Sleep(4 * time.Second) 148 s.run = true 149 150 var wg sync.WaitGroup 151 for key := range chainMap { 152 time.Sleep(time.Duration(s.waitTime) * time.Millisecond) 153 wg.Add(1) 154 go func(chainID string, w *sync.WaitGroup) { 155 defer w.Done() 156 s.ListenToChainOut(chainID) 157 }(key, &wg) 158 } 159 wg.Wait() 160 161 } 162 163 // ListenToChainOut screens swaps out of the chain with @chainID to any other chain 164 // offered by Anyswap. 165 func (s *AnyswapScraper) ListenToChainOut(chainID string) { 166 log.Info("listen to chain: ", chainMap[chainID]) 167 var err error 168 169 // Fetch all addresses that can be bridged on chain with @chainID. 170 addresses, err := getAddressesByChain(chainID) 171 if err != nil { 172 log.Error("") 173 } 174 175 // Switch from anyToken to underlying token. 176 anyTokenMap, err := getAnyTokenMap() 177 if err != nil { 178 log.Error("get anyToken map: ", err) 179 } 180 181 // Listen to swaps out of current chain. 182 sink, err := s.GetSwapOutChannel(addresses, chainID) 183 if err != nil { 184 log.Error("error fetching swaps channel: ", err) 185 } 186 187 go func() { 188 for { 189 rawSwap, ok := <-sink 190 if ok { 191 192 swap, err := s.processSwap(*rawSwap, anyTokenMap) 193 if err != nil { 194 log.Error("process swap: ", err) 195 } else { 196 log.Infof("got swap -- %v", swap) 197 s.chanTrades <- &swap 198 } 199 } 200 } 201 }() 202 } 203 204 // processSwap returns a dia.Trade object from a rawSwap as emitted in LogAnySwapOut. 205 func (s *AnyswapScraper) processSwap(rawSwap anyswap.AnyswapV4RouterLogAnySwapOut, anyTokenMap map[string]string) (trade dia.Trade, err error) { 206 207 // Get Basetoken 208 fromChainID := rawSwap.FromChainID.String() 209 basetokenaddress := rawSwap.Token.Hex() 210 211 // If outToken is an anyToken, switch to the underlying asset. 212 if underlyingToken, ok := anyTokenMap[fromChainID+"-"+basetokenaddress]; ok { 213 basetokenaddress = underlyingToken 214 } 215 216 if basetoken, ok := assetMap[fromChainID+basetokenaddress]; ok { 217 trade.BaseToken = basetoken 218 } else { 219 basetoken, err = s.db.GetAsset(common.HexToAddress(basetokenaddress).Hex(), chainMap[fromChainID]) 220 if err != nil { 221 log.Errorf("get base asset %s on chainID %v: %v", basetokenaddress, rawSwap.FromChainID, err) 222 return 223 } 224 trade.BaseToken = basetoken 225 assetMap[fromChainID+basetokenaddress] = basetoken 226 } 227 228 // Get Quotetoken 229 toChainID := rawSwap.ToChainID.String() 230 toAddress := s.anyswapAssetInfo[fromChainID][strings.ToLower(basetokenaddress)].(map[string]interface{})["destChains"].(map[string]interface{})[toChainID].(map[string]interface{})["address"].(string) 231 if quotetoken, ok := assetMap[toChainID+toAddress]; ok { 232 trade.QuoteToken = quotetoken 233 } else { 234 quotetoken, err = s.db.GetAsset(common.HexToAddress(toAddress).Hex(), chainMap[toChainID]) 235 if err != nil { 236 log.Errorf("get quote asset %s on chainID %s: %v", toAddress, rawSwap.ToChainID, err) 237 return 238 } 239 trade.QuoteToken = quotetoken 240 assetMap[fromChainID+toAddress] = quotetoken 241 } 242 243 trade.Symbol = trade.QuoteToken.Symbol 244 trade.Pair = trade.QuoteToken.Symbol + "-" + trade.BaseToken.Symbol 245 trade.Price = float64(1) 246 trade.Volume, _ = new(big.Float).Quo(big.NewFloat(0).SetInt(rawSwap.Amount), new(big.Float).SetFloat64(math.Pow10(int(trade.BaseToken.Decimals)))).Float64() 247 trade.ForeignTradeID = rawSwap.Raw.TxHash.String() 248 trade.Time = time.Now() 249 trade.Source = dia.AnyswapExchange 250 trade.VerifiedPair = true 251 return 252 } 253 254 // GetSwapOutChannel returns the channel @sink delivering the events LogAnySwapOut. 255 func (s *AnyswapScraper) GetSwapOutChannel(tokens []common.Address, chainID string) (chan *anyswap.AnyswapV4RouterLogAnySwapOut, error) { 256 sink := make(chan *anyswap.AnyswapV4RouterLogAnySwapOut) 257 anyswapRouterContractAddress := s.anyswapAssetInfo[chainID][strings.ToLower(tokens[0].Hex())].(map[string]interface{})["router"].(string) 258 outFiltererContract, err := anyswap.NewAnyswapV4RouterFilterer(common.HexToAddress(anyswapRouterContractAddress), s.WsClientMap[chainID]) 259 if err != nil { 260 log.Fatal(err) 261 } 262 _, err = outFiltererContract.WatchLogAnySwapOut(&bind.WatchOpts{}, sink, tokens, []common.Address{}, []common.Address{}) 263 if err != nil { 264 return sink, err 265 } 266 return sink, nil 267 } 268 269 // getClientMaps returns maps for rest and ws clients. Keys are the corresponding chain IDs. 270 func getClientMaps() (map[string]*ethclient.Client, map[string]*ethclient.Client, error) { 271 272 restClientMap := make(map[string]*ethclient.Client) 273 wsClientMap := make(map[string]*ethclient.Client) 274 for key := range chainMap { 275 restClient, err := ethclient.Dial(utils.Getenv("ETH_URI_REST_"+key, "")) 276 if err != nil { 277 return restClientMap, wsClientMap, err 278 } 279 restClientMap[key] = restClient 280 wsClient, err := ethclient.Dial(utils.Getenv("ETH_URI_WS_"+key, "")) 281 if err != nil { 282 return restClientMap, wsClientMap, err 283 } 284 wsClientMap[key] = wsClient 285 286 } 287 288 return restClientMap, wsClientMap, nil 289 } 290 291 // getAddressesByChain returns all addresses of assets which can be bridged away from the chain with @chainID. 292 // This includes anyTokens. 293 func getAddressesByChain(chainID string) (addresses []common.Address, err error) { 294 allAssetsAllChains, err := fetchEndpoint(anyswapAPIURL) 295 if err != nil { 296 return 297 } 298 for address := range allAssetsAllChains[chainID] { 299 addresses = append(addresses, common.HexToAddress(address)) 300 anyAddress := allAssetsAllChains[chainID][address].(map[string]interface{})["anyToken"].(map[string]interface{})["address"].(string) 301 addresses = append(addresses, common.HexToAddress(anyAddress)) 302 } 303 304 return 305 } 306 307 // getAnyTokenMap maps an anyToken to its underlying asset. 308 func getAnyTokenMap() (map[string]string, error) { 309 anyTokenMap := make(map[string]string) 310 allAssetsAllChains, err := fetchEndpoint(anyswapAPIURL) 311 if err != nil { 312 return anyTokenMap, err 313 } 314 for chainID := range allAssetsAllChains { 315 for address := range allAssetsAllChains[chainID] { 316 anyAddress := allAssetsAllChains[chainID][address].(map[string]interface{})["anyToken"].(map[string]interface{})["address"].(string) 317 anyTokenMap[chainID+"-"+common.HexToAddress(anyAddress).Hex()] = common.HexToAddress(address).Hex() 318 } 319 } 320 return anyTokenMap, nil 321 } 322 323 // fetchEndpoint returns all assets available in the Anyswap bridge obtained through an API endpoint. 324 func fetchEndpoint(url string) (response map[string]map[string]interface{}, err error) { 325 // @response is of type map[chainID]map[assetAddress]interface{} 326 data, _, err := utils.GetRequest(url) 327 if err != nil { 328 return 329 } 330 331 err = json.Unmarshal(data, &response) 332 if err != nil { 333 return 334 } 335 return 336 } 337 338 // FetchAvailablePairs returns a list with all available trade pairs as dia.ExchangePair for the pairDiscorvery service 339 func (s *AnyswapScraper) FetchAvailablePairs() (pairs []dia.ExchangePair, err error) { 340 // TO DO: Use API in order to fetch available pairs 341 // https://bridgeapi.anyswap.exchange/v3/serverinfoV3?chainId=all&version=STABLEV3 342 343 return 344 } 345 346 // FillSymbolData is not used by DEX scrapers. 347 func (s *AnyswapScraper) FillSymbolData(symbol string) (dia.Asset, error) { 348 return dia.Asset{Symbol: symbol}, nil 349 } 350 351 func (up *AnyswapScraper) NormalizePair(pair dia.ExchangePair) (dia.ExchangePair, error) { 352 return pair, nil 353 } 354 355 // Close closes any existing API connections, as well as channels of 356 // PairScrapers from calls to ScrapePair 357 func (s *AnyswapScraper) Close() error { 358 if s.closed { 359 return errors.New("UniswapScraper: Already closed") 360 } 361 for i := range s.RestClientMap { 362 s.WsClientMap[i].Close() 363 s.RestClientMap[i].Close() 364 } 365 close(s.shutdown) 366 <-s.shutdownDone 367 s.errorLock.RLock() 368 defer s.errorLock.RUnlock() 369 return s.error 370 } 371 372 // ScrapePair returns a PairScraper that can be used to get trades for a single pair from 373 // this APIScraper 374 func (s *AnyswapScraper) ScrapePair(pair dia.ExchangePair) (PairScraper, error) { 375 s.errorLock.RLock() 376 defer s.errorLock.RUnlock() 377 if s.error != nil { 378 return nil, s.error 379 } 380 if s.closed { 381 return nil, errors.New("UniswapScraper: Call ScrapePair on closed scraper") 382 } 383 ps := &AnyswapPairScraper{ 384 parent: s, 385 pair: pair, 386 } 387 s.pairScrapers[pair.ForeignName] = ps 388 return ps, nil 389 } 390 391 // UniswapPairScraper implements PairScraper for Uniswap 392 type AnyswapPairScraper struct { 393 parent *AnyswapScraper 394 pair dia.ExchangePair 395 closed bool 396 } 397 398 // Close stops listening for trades of the pair associated with s 399 func (ps *AnyswapPairScraper) Close() error { 400 ps.closed = true 401 return nil 402 } 403 404 // Channel returns a channel that can be used to receive trades 405 func (ps *AnyswapScraper) Channel() chan *dia.Trade { 406 return ps.chanTrades 407 } 408 409 // Error returns an error when the channel Channel() is closed 410 // and nil otherwise 411 func (ps *AnyswapPairScraper) Error() error { 412 s := ps.parent 413 s.errorLock.RLock() 414 defer s.errorLock.RUnlock() 415 return s.error 416 } 417 418 // Pair returns the pair this scraper is subscribed to 419 func (ps *AnyswapPairScraper) Pair() dia.ExchangePair { 420 return ps.pair 421 }