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 }