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  }