2023-02-21 09:05:16 +00:00
|
|
|
package coingecko
|
|
|
|
|
|
|
|
import (
|
2024-05-22 11:56:30 +00:00
|
|
|
"context"
|
2023-02-21 09:05:16 +00:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2024-05-22 11:56:30 +00:00
|
|
|
"net/url"
|
2023-02-21 09:05:16 +00:00
|
|
|
"strings"
|
2023-03-14 18:59:10 +00:00
|
|
|
"sync"
|
2023-02-21 09:05:16 +00:00
|
|
|
|
2024-08-01 14:32:18 +00:00
|
|
|
"golang.org/x/exp/maps"
|
|
|
|
|
2023-02-21 09:05:16 +00:00
|
|
|
"github.com/status-im/status-go/services/wallet/thirdparty"
|
|
|
|
"github.com/status-im/status-go/services/wallet/thirdparty/utils"
|
|
|
|
)
|
|
|
|
|
|
|
|
var coinGeckoMapping = map[string]string{
|
2024-04-25 12:15:07 +00:00
|
|
|
"STT": "status",
|
|
|
|
"SNT": "status",
|
|
|
|
"ETH": "ethereum",
|
|
|
|
"AST": "airswap",
|
|
|
|
"ABT": "arcblock",
|
|
|
|
"BNB": "binancecoin",
|
|
|
|
"BLT": "bloom",
|
|
|
|
"COMP": "compound-coin",
|
|
|
|
"EDG": "edgeless",
|
|
|
|
"ENG": "enigma",
|
|
|
|
"EOS": "eos",
|
|
|
|
"GEN": "daostack",
|
|
|
|
"MANA": "decentraland-wormhole",
|
|
|
|
"LEND": "ethlend",
|
|
|
|
"LRC": "loopring",
|
|
|
|
"MET": "metronome",
|
|
|
|
"POLY": "polymath",
|
|
|
|
"PPT": "populous",
|
|
|
|
"SAN": "santiment-network-token",
|
|
|
|
"DNT": "district0x",
|
|
|
|
"SPN": "sapien",
|
|
|
|
"USDS": "stableusd",
|
|
|
|
"STX": "stox",
|
|
|
|
"SUB": "substratum",
|
|
|
|
"PAY": "tenx",
|
|
|
|
"GRT": "the-graph",
|
|
|
|
"TNT": "tierion",
|
|
|
|
"TRX": "tron",
|
|
|
|
"RARE": "superrare",
|
|
|
|
"UNI": "uniswap",
|
|
|
|
"USDC": "usd-coin",
|
|
|
|
"USDP": "paxos-standard",
|
|
|
|
"USDT": "tether",
|
|
|
|
"SHIB": "shiba-inu",
|
|
|
|
"LINK": "chainlink",
|
|
|
|
"MATIC": "matic-network",
|
|
|
|
"DAI": "dai",
|
|
|
|
"ARB": "arbitrum",
|
|
|
|
"OP": "optimism",
|
2023-02-21 09:05:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const baseURL = "https://api.coingecko.com/api/v3/"
|
|
|
|
|
|
|
|
type HistoricalPriceContainer struct {
|
|
|
|
Prices [][]float64 `json:"prices"`
|
|
|
|
}
|
|
|
|
type GeckoMarketValues struct {
|
|
|
|
ID string `json:"id"`
|
|
|
|
Symbol string `json:"symbol"`
|
|
|
|
Name string `json:"name"`
|
|
|
|
MarketCap float64 `json:"market_cap"`
|
|
|
|
High24h float64 `json:"high_24h"`
|
|
|
|
Low24h float64 `json:"low_24h"`
|
|
|
|
PriceChange24h float64 `json:"price_change_24h"`
|
|
|
|
PriceChangePercentage24h float64 `json:"price_change_percentage_24h"`
|
|
|
|
PriceChangePercentage1hInCurrency float64 `json:"price_change_percentage_1h_in_currency"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type GeckoToken struct {
|
2024-05-15 05:33:12 +00:00
|
|
|
ID string `json:"id"`
|
|
|
|
Symbol string `json:"symbol"`
|
|
|
|
Name string `json:"name"`
|
|
|
|
EthPlatform bool
|
2023-02-21 09:05:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type Client struct {
|
2024-05-22 11:56:30 +00:00
|
|
|
httpClient *thirdparty.HTTPClient
|
2024-05-15 05:33:12 +00:00
|
|
|
tokens map[string][]GeckoToken
|
2023-03-14 18:59:10 +00:00
|
|
|
tokensURL string
|
|
|
|
fetchTokensMutex sync.Mutex
|
2023-02-21 09:05:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewClient() *Client {
|
2024-05-22 11:56:30 +00:00
|
|
|
return &Client{
|
|
|
|
httpClient: thirdparty.NewHTTPClient(),
|
|
|
|
tokens: make(map[string][]GeckoToken),
|
|
|
|
tokensURL: fmt.Sprintf("%scoins/list", baseURL),
|
|
|
|
}
|
2024-05-15 05:33:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (gt *GeckoToken) UnmarshalJSON(data []byte) error {
|
|
|
|
// Define an auxiliary struct to hold the JSON data
|
|
|
|
var aux struct {
|
|
|
|
ID string `json:"id"`
|
|
|
|
Symbol string `json:"symbol"`
|
|
|
|
Name string `json:"name"`
|
|
|
|
Platforms struct {
|
|
|
|
Ethereum string `json:"ethereum"`
|
|
|
|
// Other platforms can be added here if needed
|
|
|
|
} `json:"platforms"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unmarshal the JSON data into the auxiliary struct
|
|
|
|
if err := json.Unmarshal(data, &aux); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the fields of GeckoToken from the auxiliary struct
|
|
|
|
gt.ID = aux.ID
|
|
|
|
gt.Symbol = aux.Symbol
|
|
|
|
gt.Name = aux.Name
|
|
|
|
|
|
|
|
// Check if "ethereum" key exists in the platforms map
|
|
|
|
if aux.Platforms.Ethereum != "" {
|
|
|
|
gt.EthPlatform = true
|
|
|
|
} else {
|
|
|
|
gt.EthPlatform = false
|
|
|
|
}
|
|
|
|
return nil
|
2023-02-21 09:05:16 +00:00
|
|
|
}
|
|
|
|
|
2024-05-15 05:33:12 +00:00
|
|
|
func mapTokensToSymbols(tokens []GeckoToken, tokenMap map[string][]GeckoToken) {
|
2023-03-14 18:59:10 +00:00
|
|
|
for _, token := range tokens {
|
2024-05-15 05:33:12 +00:00
|
|
|
symbol := strings.ToUpper(token.Symbol)
|
|
|
|
if id, ok := coinGeckoMapping[symbol]; ok {
|
2023-03-14 18:59:10 +00:00
|
|
|
if id != token.ID {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
2024-05-15 05:33:12 +00:00
|
|
|
tokenMap[symbol] = append(tokenMap[symbol], token)
|
2023-03-14 18:59:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-15 05:33:12 +00:00
|
|
|
func getGeckoTokenFromSymbol(tokens map[string][]GeckoToken, symbol string) (GeckoToken, error) {
|
|
|
|
tokenList, ok := tokens[strings.ToUpper(symbol)]
|
|
|
|
if !ok {
|
|
|
|
return GeckoToken{}, fmt.Errorf("token not found for symbol %s", symbol)
|
|
|
|
}
|
|
|
|
for _, t := range tokenList {
|
|
|
|
if t.EthPlatform {
|
|
|
|
return t, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return tokenList[0], nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getIDFromSymbol(tokens map[string][]GeckoToken, symbol string) (string, error) {
|
|
|
|
token, err := getGeckoTokenFromSymbol(tokens, symbol)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return token.ID, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) getTokens() (map[string][]GeckoToken, error) {
|
2023-03-14 18:59:10 +00:00
|
|
|
c.fetchTokensMutex.Lock()
|
|
|
|
defer c.fetchTokensMutex.Unlock()
|
|
|
|
|
2023-02-21 09:05:16 +00:00
|
|
|
if len(c.tokens) > 0 {
|
|
|
|
return c.tokens, nil
|
|
|
|
}
|
|
|
|
|
2024-05-22 11:56:30 +00:00
|
|
|
params := url.Values{}
|
|
|
|
params.Add("include_platform", "true")
|
2023-02-21 09:05:16 +00:00
|
|
|
|
2024-05-22 11:56:30 +00:00
|
|
|
url := c.tokensURL
|
2024-07-30 13:48:22 +00:00
|
|
|
response, err := c.httpClient.DoGetRequest(context.Background(), url, params, nil)
|
2023-02-21 09:05:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var tokens []GeckoToken
|
2024-05-22 11:56:30 +00:00
|
|
|
err = json.Unmarshal(response, &tokens)
|
2023-02-21 09:05:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-03-14 18:59:10 +00:00
|
|
|
mapTokensToSymbols(tokens, c.tokens)
|
2023-02-21 09:05:16 +00:00
|
|
|
return c.tokens, nil
|
|
|
|
}
|
|
|
|
|
2024-08-01 14:32:18 +00:00
|
|
|
func (c *Client) mapSymbolsToIds(symbols []string) (map[string]string, error) {
|
2023-02-21 09:05:16 +00:00
|
|
|
tokens, err := c.getTokens()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2024-08-01 14:32:18 +00:00
|
|
|
ids := make(map[string]string, 0)
|
|
|
|
for _, symbol := range symbols {
|
|
|
|
id, err := getIDFromSymbol(tokens, utils.GetRealSymbol(symbol))
|
2024-05-15 05:33:12 +00:00
|
|
|
if err == nil {
|
2024-08-01 14:32:18 +00:00
|
|
|
ids[symbol] = id
|
2023-02-21 09:05:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-15 05:33:12 +00:00
|
|
|
return ids, nil
|
2023-02-21 09:05:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) FetchPrices(symbols []string, currencies []string) (map[string]map[string]float64, error) {
|
|
|
|
ids, err := c.mapSymbolsToIds(symbols)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-05-22 11:56:30 +00:00
|
|
|
params := url.Values{}
|
2024-08-01 14:32:18 +00:00
|
|
|
params.Add("ids", strings.Join(maps.Values(ids), ","))
|
2024-05-22 11:56:30 +00:00
|
|
|
params.Add("vs_currencies", strings.Join(currencies, ","))
|
|
|
|
|
|
|
|
url := fmt.Sprintf("%ssimple/price", baseURL)
|
2024-07-30 13:48:22 +00:00
|
|
|
response, err := c.httpClient.DoGetRequest(context.Background(), url, params, nil)
|
2023-02-21 09:05:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
prices := make(map[string]map[string]float64)
|
2024-05-22 11:56:30 +00:00
|
|
|
err = json.Unmarshal(response, &prices)
|
2023-02-21 09:05:16 +00:00
|
|
|
if err != nil {
|
2024-05-22 11:56:30 +00:00
|
|
|
return nil, fmt.Errorf("%s - %s", err, string(response))
|
2023-02-21 09:05:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
result := make(map[string]map[string]float64)
|
2024-08-01 14:32:18 +00:00
|
|
|
for symbol, id := range ids {
|
2023-02-21 09:05:16 +00:00
|
|
|
result[symbol] = map[string]float64{}
|
|
|
|
for _, currency := range currencies {
|
|
|
|
result[symbol][currency] = prices[id][strings.ToLower(currency)]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) FetchTokenDetails(symbols []string) (map[string]thirdparty.TokenDetails, error) {
|
|
|
|
tokens, err := c.getTokens()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
result := make(map[string]thirdparty.TokenDetails)
|
|
|
|
for _, symbol := range symbols {
|
2024-05-15 05:33:12 +00:00
|
|
|
token, err := getGeckoTokenFromSymbol(tokens, utils.GetRealSymbol(symbol))
|
|
|
|
if err == nil {
|
2023-02-21 09:05:16 +00:00
|
|
|
result[symbol] = thirdparty.TokenDetails{
|
2024-05-15 05:33:12 +00:00
|
|
|
ID: token.ID,
|
|
|
|
Name: token.Name,
|
2023-02-21 09:05:16 +00:00
|
|
|
Symbol: symbol,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) FetchTokenMarketValues(symbols []string, currency string) (map[string]thirdparty.TokenMarketValues, error) {
|
|
|
|
ids, err := c.mapSymbolsToIds(symbols)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-03-20 13:02:09 +00:00
|
|
|
|
2024-05-22 11:56:30 +00:00
|
|
|
params := url.Values{}
|
2024-08-01 14:32:18 +00:00
|
|
|
params.Add("ids", strings.Join(maps.Values(ids), ","))
|
2024-05-22 11:56:30 +00:00
|
|
|
params.Add("vs_currency", currency)
|
|
|
|
params.Add("order", "market_cap_desc")
|
|
|
|
params.Add("per_page", "250")
|
|
|
|
params.Add("page", "1")
|
|
|
|
params.Add("sparkline", "false")
|
|
|
|
params.Add("price_change_percentage", "1h,24h")
|
2023-02-21 09:05:16 +00:00
|
|
|
|
2024-05-22 11:56:30 +00:00
|
|
|
url := fmt.Sprintf("%scoins/markets", baseURL)
|
2024-07-30 13:48:22 +00:00
|
|
|
response, err := c.httpClient.DoGetRequest(context.Background(), url, params, nil)
|
2023-02-21 09:05:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var marketValues []GeckoMarketValues
|
2024-05-22 11:56:30 +00:00
|
|
|
err = json.Unmarshal(response, &marketValues)
|
2023-02-21 09:05:16 +00:00
|
|
|
if err != nil {
|
2024-05-22 11:56:30 +00:00
|
|
|
return nil, fmt.Errorf("%s - %s", err, string(response))
|
2023-02-21 09:05:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
result := make(map[string]thirdparty.TokenMarketValues)
|
2024-08-01 14:32:18 +00:00
|
|
|
for symbol, id := range ids {
|
2023-02-21 09:05:16 +00:00
|
|
|
for _, marketValue := range marketValues {
|
|
|
|
if id != marketValue.ID {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
result[symbol] = thirdparty.TokenMarketValues{
|
|
|
|
MKTCAP: marketValue.MarketCap,
|
|
|
|
HIGHDAY: marketValue.High24h,
|
|
|
|
LOWDAY: marketValue.Low24h,
|
|
|
|
CHANGEPCTHOUR: marketValue.PriceChangePercentage1hInCurrency,
|
|
|
|
CHANGEPCTDAY: marketValue.PriceChangePercentage24h,
|
|
|
|
CHANGEPCT24HOUR: marketValue.PriceChangePercentage24h,
|
|
|
|
CHANGE24HOUR: marketValue.PriceChange24h,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) FetchHistoricalHourlyPrices(symbol string, currency string, limit int, aggregate int) ([]thirdparty.HistoricalPrice, error) {
|
|
|
|
return []thirdparty.HistoricalPrice{}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) FetchHistoricalDailyPrices(symbol string, currency string, limit int, allData bool, aggregate int) ([]thirdparty.HistoricalPrice, error) {
|
2024-05-15 05:33:12 +00:00
|
|
|
tokens, err := c.getTokens()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
id, err := getIDFromSymbol(tokens, utils.GetRealSymbol(symbol))
|
2023-02-21 09:05:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-05-22 11:56:30 +00:00
|
|
|
params := url.Values{}
|
|
|
|
params.Add("vs_currency", currency)
|
|
|
|
params.Add("days", "30")
|
2023-02-21 09:05:16 +00:00
|
|
|
|
2024-05-22 11:56:30 +00:00
|
|
|
url := fmt.Sprintf("%scoins/%s/market_chart", baseURL, id)
|
2024-07-30 13:48:22 +00:00
|
|
|
response, err := c.httpClient.DoGetRequest(context.Background(), url, params, nil)
|
2023-02-21 09:05:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2024-05-22 11:56:30 +00:00
|
|
|
|
2023-02-21 09:05:16 +00:00
|
|
|
var container HistoricalPriceContainer
|
2024-05-22 11:56:30 +00:00
|
|
|
err = json.Unmarshal(response, &container)
|
2023-02-21 09:05:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
result := make([]thirdparty.HistoricalPrice, 0)
|
|
|
|
for _, price := range container.Prices {
|
|
|
|
result = append(result, thirdparty.HistoricalPrice{
|
|
|
|
Timestamp: int64(price[0]),
|
|
|
|
Value: price[1],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
2024-07-02 17:58:55 +00:00
|
|
|
|
|
|
|
func (c *Client) ID() string {
|
|
|
|
return "coingecko"
|
|
|
|
}
|