github.com/status-im/status-go@v1.1.0/services/wallet/thirdparty/coingecko/client.go (about)

     1  package coingecko
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/url"
     8  	"strings"
     9  	"sync"
    10  
    11  	"golang.org/x/exp/maps"
    12  
    13  	"github.com/status-im/status-go/services/wallet/thirdparty"
    14  	"github.com/status-im/status-go/services/wallet/thirdparty/utils"
    15  )
    16  
    17  var coinGeckoMapping = map[string]string{
    18  	"STT":   "status",
    19  	"SNT":   "status",
    20  	"ETH":   "ethereum",
    21  	"AST":   "airswap",
    22  	"ABT":   "arcblock",
    23  	"BNB":   "binancecoin",
    24  	"BLT":   "bloom",
    25  	"COMP":  "compound-coin",
    26  	"EDG":   "edgeless",
    27  	"ENG":   "enigma",
    28  	"EOS":   "eos",
    29  	"GEN":   "daostack",
    30  	"MANA":  "decentraland-wormhole",
    31  	"LEND":  "ethlend",
    32  	"LRC":   "loopring",
    33  	"MET":   "metronome",
    34  	"POLY":  "polymath",
    35  	"PPT":   "populous",
    36  	"SAN":   "santiment-network-token",
    37  	"DNT":   "district0x",
    38  	"SPN":   "sapien",
    39  	"USDS":  "stableusd",
    40  	"STX":   "stox",
    41  	"SUB":   "substratum",
    42  	"PAY":   "tenx",
    43  	"GRT":   "the-graph",
    44  	"TNT":   "tierion",
    45  	"TRX":   "tron",
    46  	"RARE":  "superrare",
    47  	"UNI":   "uniswap",
    48  	"USDC":  "usd-coin",
    49  	"USDP":  "paxos-standard",
    50  	"USDT":  "tether",
    51  	"SHIB":  "shiba-inu",
    52  	"LINK":  "chainlink",
    53  	"MATIC": "matic-network",
    54  	"DAI":   "dai",
    55  	"ARB":   "arbitrum",
    56  	"OP":    "optimism",
    57  }
    58  
    59  const baseURL = "https://api.coingecko.com/api/v3"
    60  
    61  type HistoricalPriceContainer struct {
    62  	Prices [][]float64 `json:"prices"`
    63  }
    64  type GeckoMarketValues struct {
    65  	ID                                string  `json:"id"`
    66  	Symbol                            string  `json:"symbol"`
    67  	Name                              string  `json:"name"`
    68  	MarketCap                         float64 `json:"market_cap"`
    69  	High24h                           float64 `json:"high_24h"`
    70  	Low24h                            float64 `json:"low_24h"`
    71  	PriceChange24h                    float64 `json:"price_change_24h"`
    72  	PriceChangePercentage24h          float64 `json:"price_change_percentage_24h"`
    73  	PriceChangePercentage1hInCurrency float64 `json:"price_change_percentage_1h_in_currency"`
    74  }
    75  
    76  type GeckoToken struct {
    77  	ID          string `json:"id"`
    78  	Symbol      string `json:"symbol"`
    79  	Name        string `json:"name"`
    80  	EthPlatform bool
    81  }
    82  
    83  type Client struct {
    84  	httpClient       *thirdparty.HTTPClient
    85  	tokens           map[string][]GeckoToken
    86  	baseURL          string
    87  	fetchTokensMutex sync.Mutex
    88  }
    89  
    90  func NewClient() *Client {
    91  	return &Client{
    92  		httpClient: thirdparty.NewHTTPClient(),
    93  		tokens:     make(map[string][]GeckoToken),
    94  		baseURL:    baseURL,
    95  	}
    96  }
    97  
    98  func (gt *GeckoToken) UnmarshalJSON(data []byte) error {
    99  	// Define an auxiliary struct to hold the JSON data
   100  	var aux struct {
   101  		ID        string `json:"id"`
   102  		Symbol    string `json:"symbol"`
   103  		Name      string `json:"name"`
   104  		Platforms struct {
   105  			Ethereum string `json:"ethereum"`
   106  			// Other platforms can be added here if needed
   107  		} `json:"platforms"`
   108  	}
   109  
   110  	// Unmarshal the JSON data into the auxiliary struct
   111  	if err := json.Unmarshal(data, &aux); err != nil {
   112  		return err
   113  	}
   114  
   115  	// Set the fields of GeckoToken from the auxiliary struct
   116  	gt.ID = aux.ID
   117  	gt.Symbol = aux.Symbol
   118  	gt.Name = aux.Name
   119  
   120  	// Check if "ethereum" key exists in the platforms map
   121  	if aux.Platforms.Ethereum != "" {
   122  		gt.EthPlatform = true
   123  	} else {
   124  		gt.EthPlatform = false
   125  	}
   126  	return nil
   127  }
   128  
   129  func mapTokensToSymbols(tokens []GeckoToken, tokenMap map[string][]GeckoToken) {
   130  	for _, token := range tokens {
   131  		symbol := strings.ToUpper(token.Symbol)
   132  		if id, ok := coinGeckoMapping[symbol]; ok {
   133  			if id != token.ID {
   134  				continue
   135  			}
   136  		}
   137  		tokenMap[symbol] = append(tokenMap[symbol], token)
   138  	}
   139  }
   140  
   141  func getGeckoTokenFromSymbol(tokens map[string][]GeckoToken, symbol string) (GeckoToken, error) {
   142  	tokenList, ok := tokens[strings.ToUpper(symbol)]
   143  	if !ok {
   144  		return GeckoToken{}, fmt.Errorf("token not found for symbol %s", symbol)
   145  	}
   146  	for _, t := range tokenList {
   147  		if t.EthPlatform {
   148  			return t, nil
   149  		}
   150  	}
   151  	return tokenList[0], nil
   152  }
   153  
   154  func getIDFromSymbol(tokens map[string][]GeckoToken, symbol string) (string, error) {
   155  	token, err := getGeckoTokenFromSymbol(tokens, symbol)
   156  	if err != nil {
   157  		return "", err
   158  	}
   159  	return token.ID, nil
   160  }
   161  
   162  func (c *Client) getTokens() (map[string][]GeckoToken, error) {
   163  	c.fetchTokensMutex.Lock()
   164  	defer c.fetchTokensMutex.Unlock()
   165  
   166  	if len(c.tokens) > 0 {
   167  		return c.tokens, nil
   168  	}
   169  
   170  	params := url.Values{}
   171  	params.Add("include_platform", "true")
   172  
   173  	url := fmt.Sprintf("%s/coins/list", c.baseURL)
   174  	response, err := c.httpClient.DoGetRequest(context.Background(), url, params, nil)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	var tokens []GeckoToken
   180  	err = json.Unmarshal(response, &tokens)
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  
   185  	mapTokensToSymbols(tokens, c.tokens)
   186  	return c.tokens, nil
   187  }
   188  
   189  func (c *Client) mapSymbolsToIds(symbols []string) (map[string]string, error) {
   190  	tokens, err := c.getTokens()
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	ids := make(map[string]string, 0)
   195  	for _, symbol := range symbols {
   196  		id, err := getIDFromSymbol(tokens, utils.GetRealSymbol(symbol))
   197  		if err == nil {
   198  			ids[symbol] = id
   199  		}
   200  	}
   201  
   202  	return ids, nil
   203  }
   204  
   205  func (c *Client) FetchPrices(symbols []string, currencies []string) (map[string]map[string]float64, error) {
   206  	ids, err := c.mapSymbolsToIds(symbols)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  
   211  	params := url.Values{}
   212  	params.Add("ids", strings.Join(maps.Values(ids), ","))
   213  	params.Add("vs_currencies", strings.Join(currencies, ","))
   214  
   215  	url := fmt.Sprintf("%s/simple/price", c.baseURL)
   216  	response, err := c.httpClient.DoGetRequest(context.Background(), url, params, nil)
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  
   221  	prices := make(map[string]map[string]float64)
   222  	err = json.Unmarshal(response, &prices)
   223  	if err != nil {
   224  		return nil, fmt.Errorf("%s - %s", err, string(response))
   225  	}
   226  
   227  	result := make(map[string]map[string]float64)
   228  	for symbol, id := range ids {
   229  		result[symbol] = map[string]float64{}
   230  		for _, currency := range currencies {
   231  			result[symbol][currency] = prices[id][strings.ToLower(currency)]
   232  		}
   233  	}
   234  
   235  	return result, nil
   236  }
   237  
   238  func (c *Client) FetchTokenDetails(symbols []string) (map[string]thirdparty.TokenDetails, error) {
   239  	tokens, err := c.getTokens()
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  	result := make(map[string]thirdparty.TokenDetails)
   244  	for _, symbol := range symbols {
   245  		token, err := getGeckoTokenFromSymbol(tokens, utils.GetRealSymbol(symbol))
   246  		if err == nil {
   247  			result[symbol] = thirdparty.TokenDetails{
   248  				ID:     token.ID,
   249  				Name:   token.Name,
   250  				Symbol: symbol,
   251  			}
   252  		}
   253  	}
   254  	return result, nil
   255  }
   256  
   257  func (c *Client) FetchTokenMarketValues(symbols []string, currency string) (map[string]thirdparty.TokenMarketValues, error) {
   258  	ids, err := c.mapSymbolsToIds(symbols)
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	params := url.Values{}
   264  	params.Add("ids", strings.Join(maps.Values(ids), ","))
   265  	params.Add("vs_currency", currency)
   266  	params.Add("order", "market_cap_desc")
   267  	params.Add("per_page", "250")
   268  	params.Add("page", "1")
   269  	params.Add("sparkline", "false")
   270  	params.Add("price_change_percentage", "1h,24h")
   271  
   272  	url := fmt.Sprintf("%s/coins/markets", c.baseURL)
   273  	response, err := c.httpClient.DoGetRequest(context.Background(), url, params, nil)
   274  	if err != nil {
   275  		return nil, err
   276  	}
   277  
   278  	var marketValues []GeckoMarketValues
   279  	err = json.Unmarshal(response, &marketValues)
   280  	if err != nil {
   281  		return nil, fmt.Errorf("%s - %s", err, string(response))
   282  	}
   283  
   284  	result := make(map[string]thirdparty.TokenMarketValues)
   285  	for symbol, id := range ids {
   286  		for _, marketValue := range marketValues {
   287  			if id != marketValue.ID {
   288  				continue
   289  			}
   290  
   291  			result[symbol] = thirdparty.TokenMarketValues{
   292  				MKTCAP:          marketValue.MarketCap,
   293  				HIGHDAY:         marketValue.High24h,
   294  				LOWDAY:          marketValue.Low24h,
   295  				CHANGEPCTHOUR:   marketValue.PriceChangePercentage1hInCurrency,
   296  				CHANGEPCTDAY:    marketValue.PriceChangePercentage24h,
   297  				CHANGEPCT24HOUR: marketValue.PriceChangePercentage24h,
   298  				CHANGE24HOUR:    marketValue.PriceChange24h,
   299  			}
   300  		}
   301  	}
   302  
   303  	return result, nil
   304  }
   305  
   306  func (c *Client) FetchHistoricalHourlyPrices(symbol string, currency string, limit int, aggregate int) ([]thirdparty.HistoricalPrice, error) {
   307  	return []thirdparty.HistoricalPrice{}, nil
   308  }
   309  
   310  func (c *Client) FetchHistoricalDailyPrices(symbol string, currency string, limit int, allData bool, aggregate int) ([]thirdparty.HistoricalPrice, error) {
   311  	tokens, err := c.getTokens()
   312  	if err != nil {
   313  		return nil, err
   314  	}
   315  
   316  	id, err := getIDFromSymbol(tokens, utils.GetRealSymbol(symbol))
   317  	if err != nil {
   318  		return nil, err
   319  	}
   320  
   321  	params := url.Values{}
   322  	params.Add("vs_currency", currency)
   323  	params.Add("days", "30")
   324  
   325  	url := fmt.Sprintf("%s/coins/%s/market_chart", c.baseURL, id)
   326  	response, err := c.httpClient.DoGetRequest(context.Background(), url, params, nil)
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  
   331  	var container HistoricalPriceContainer
   332  	err = json.Unmarshal(response, &container)
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  
   337  	result := make([]thirdparty.HistoricalPrice, 0)
   338  	for _, price := range container.Prices {
   339  		result = append(result, thirdparty.HistoricalPrice{
   340  			Timestamp: int64(price[0]),
   341  			Value:     price[1],
   342  		})
   343  	}
   344  
   345  	return result, nil
   346  }
   347  
   348  func (c *Client) ID() string {
   349  	return "coingecko"
   350  }