diff --git a/cmd/commands/root.go b/cmd/commands/root.go index 53a7ee6..fe01669 100644 --- a/cmd/commands/root.go +++ b/cmd/commands/root.go @@ -31,6 +31,7 @@ func RootCmd() *cobra.Command { config := os.Getenv("COINTOP_CONFIG") apiChoice := os.Getenv("COINTOP_API") cmcAPIKey := os.Getenv("CMC_PRO_API_KEY") + coingeckoAPIKey := os.Getenv("COINGECKO_PRO_API_KEY") perPage := cointop.DefaultPerPage maxPages := cointop.DefaultMaxPages @@ -102,6 +103,7 @@ See git.io/cointop for more info.`, NoCache: noCache, ConfigFilepath: config, CoinMarketCapAPIKey: cmcAPIKey, + CoinGeckoAPIKey: coingeckoAPIKey, APIChoice: apiChoice, Colorscheme: colorscheme, HideMarketbar: hideMarketbar, @@ -140,7 +142,8 @@ See git.io/cointop for more info.`, rootCmd.Flags().UintVarP(&perPage, "per-page", "", perPage, "Per page") rootCmd.Flags().UintVarP(&maxPages, "max-pages", "", maxPages, "Max number of pages") rootCmd.Flags().StringVarP(&config, "config", "c", config, fmt.Sprintf("Config filepath. (default %s)", cointop.DefaultConfigFilepath)) - rootCmd.Flags().StringVarP(&cmcAPIKey, "coinmarketcap-api-key", "", cmcAPIKey, "Set the CoinMarketCap API key") + rootCmd.Flags().StringVarP(&cmcAPIKey, "coinmarketcap-api-key", "", cmcAPIKey, "Set the CoinMarketCap Pro API key") + rootCmd.Flags().StringVarP(&coingeckoAPIKey, "coingecko-api-key", "", coingeckoAPIKey, "Set the CoinGecko Pro API key") rootCmd.Flags().StringVarP(&apiChoice, "api", "", apiChoice, "API choice. Available choices are \"coinmarketcap\" and \"coingecko\"") rootCmd.Flags().StringVarP(&colorscheme, "colorscheme", "", colorscheme, fmt.Sprintf("Colorscheme to use (default \"cointop\").\n%s", cointop.ColorschemeHelpString())) rootCmd.Flags().StringVarP(&cacheDir, "cache-dir", "", cacheDir, fmt.Sprintf("Cache directory (default %s)", cointop.DefaultCacheDir)) diff --git a/cointop/cointop.go b/cointop/cointop.go index 6af281b..bcb1b79 100644 --- a/cointop/cointop.go +++ b/cointop/cointop.go @@ -169,6 +169,7 @@ type Config struct { Colorscheme string ConfigFilepath string CoinMarketCapAPIKey string + CoinGeckoAPIKey string NoPrompts bool HideMarketbar bool HideChart bool @@ -185,7 +186,8 @@ type Config struct { // APIKeys is api keys structure type APIKeys struct { - cmc string + cmc string + coingecko string } // DefaultCurrency ... @@ -383,6 +385,14 @@ func NewCointop(config *Config) (*Cointop, error) { } } + // prompt for CoinGecko api key if not found + if config.CoinGeckoAPIKey != "" { + ct.apiKeys.coingecko = config.CoinGeckoAPIKey + if err := ct.SaveConfig(); err != nil { + return nil, err + } + } + if config.Colorscheme != "" { ct.colorschemeName = config.Colorscheme } @@ -420,10 +430,34 @@ func NewCointop(config *Config) (*Cointop, error) { } } + if ct.apiChoice == CoinGecko && ct.apiKeys.coingecko == "" { + apiKey := os.Getenv("COINGECKO_PRO_API_KEY") + if apiKey == "" { + // if !config.NoPrompts { + // apiKey, err = ct.ReadAPIKeyFromStdin("CoinGecko Pro") + // if err != nil { + // return nil, err + // } + + // ct.apiKeys.coingecko = apiKey + // } + } else { + ct.apiKeys.coingecko = apiKey + } + + if err := ct.SaveConfig(); err != nil { + return nil, err + } + } + if ct.apiChoice == CoinMarketCap { ct.api = api.NewCMC(ct.apiKeys.cmc) } else if ct.apiChoice == CoinGecko { - ct.api = api.NewCG(perPage, maxPages) + ct.api = api.NewCG(&api.CoinGeckoConfig{ + PerPage: perPage, + MaxPages: maxPages, + ApiKey: ct.apiKeys.coingecko, + }) } else { return nil, ErrInvalidAPIChoice } diff --git a/cointop/config.go b/cointop/config.go index 10e42ce..5297d84 100644 --- a/cointop/config.go +++ b/cointop/config.go @@ -44,6 +44,7 @@ type ConfigFileConfig struct { DefaultView interface{} `toml:"default_view"` DefaultChartRange interface{} `toml:"default_chart_range"` CoinMarketCap map[string]interface{} `toml:"coinmarketcap"` + CoinGecko map[string]interface{} `toml:"coingecko"` API interface{} `toml:"api"` Colorscheme interface{} `toml:"colorscheme"` RefreshRate interface{} `toml:"refresh_rate"` @@ -253,6 +254,10 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) { "pro_api_key": ct.apiKeys.cmc, } + coingeckoIfc := map[string]interface{}{ + "pro_api_key": ct.apiKeys.coingecko, + } + var priceAlertsIfc []interface{} for _, priceAlert := range ct.State.priceAlerts.Entries { if priceAlert.Expired { @@ -287,6 +292,7 @@ func (ct *Cointop) ConfigToToml() ([]byte, error) { API: ct.apiChoice, Colorscheme: ct.colorschemeName, CoinMarketCap: cmcIfc, + CoinGecko: coingeckoIfc, Currency: ct.State.currencyConversion, DefaultView: ct.State.defaultView, DefaultChartRange: ct.State.defaultChartRange, @@ -476,6 +482,12 @@ func (ct *Cointop) loadAPIKeysFromConfig() error { ct.apiKeys.cmc = value.(string) } } + for key, value := range ct.config.CoinGecko { + k := strings.TrimSpace(strings.ToLower(key)) + if k == "pro_api_key" { + ct.apiKeys.coingecko = value.(string) + } + } return nil } @@ -541,7 +553,7 @@ func (ct *Cointop) loadAltCoinLinkFromConfig() error { // LoadAPIChoiceFromConfig loads API choices from config file to struct func (ct *Cointop) loadAPIChoiceFromConfig() error { - log.Debug("loadAPIKeysFromConfig()") + log.Debug("loadAPIChoiceFromConfig()") apiChoice, ok := ct.config.API.(string) if ok { apiChoice = strings.TrimSpace(strings.ToLower(apiChoice)) diff --git a/cointop/dominance.go b/cointop/dominance.go index a76b247..5bb3d3f 100644 --- a/cointop/dominance.go +++ b/cointop/dominance.go @@ -2,6 +2,7 @@ package cointop import ( "fmt" + "os" "github.com/cointop-sh/cointop/pkg/api" ) @@ -22,7 +23,9 @@ func PrintBitcoinDominance(config *DominanceConfig) error { if config.APIChoice == CoinMarketCap { coinAPI = api.NewCMC("") } else if config.APIChoice == CoinGecko { - coinAPI = api.NewCG(0, 0) + coinAPI = api.NewCG(&api.CoinGeckoConfig{ + ApiKey: os.Getenv("COINGECKO_PRO_API_KEY"), + }) } else { return ErrInvalidAPIChoice } diff --git a/cointop/price.go b/cointop/price.go index 203afdc..540b86b 100644 --- a/cointop/price.go +++ b/cointop/price.go @@ -3,6 +3,7 @@ package cointop import ( "fmt" "math" + "os" "strings" "github.com/cointop-sh/cointop/pkg/api" @@ -57,7 +58,9 @@ func GetCoinPrices(config *PricesConfig) ([]string, error) { if config.APIChoice == CoinMarketCap { priceAPI = api.NewCMC("") } else if config.APIChoice == CoinGecko { - priceAPI = api.NewCG(0, 0) + priceAPI = api.NewCG(&api.CoinGeckoConfig{ + ApiKey: os.Getenv("COINGECKO_PRO_API_KEY"), + }) } else { return nil, ErrInvalidAPIChoice } diff --git a/docs/content/faq.md b/docs/content/faq.md index 865fd7f..f835ec6 100644 --- a/docs/content/faq.md +++ b/docs/content/faq.md @@ -122,6 +122,27 @@ draft: false cointop --coinmarketcap-api-key=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ``` +## How do I add my CoinGecko Pro API key? + + Add the API key in the cointop config file: + + ```toml + [coingecko] + pro_api_key = "CG-xxxxxxxxxxxxxxxxxxxxxxxx" + ``` + + Alternatively, you can export the environment variable `COINGECKO_PRO_API_KEY` containing the API key in your `~/.bashrc` + + ```bash + export COINGECKO_PRO_API_KEY=CG-xxxxxxxxxxxxxxxxxxxxxxxx + ``` + + You may also set the API key on start: + + ```bash + cointop --coingecko-api-key=CG-xxxxxxxxxxxxxxxxxxxxxxxx + ``` + ## I can I add my own API to cointop? Fork cointop and add the API that implements the API [interface](https://github.com/cointop-sh/cointop/blob/master/cointop/common/api/interface.go) to [`cointop/cointop/common/api/impl/`](https://github.com/cointop-sh/cointop/tree/master/cointop/common/api/impl). You can use the CoinGecko [implementation](https://github.com/cointop-sh/cointop/blob/master/cointop/common/api/impl/coingecko/coingecko.go) as reference. diff --git a/pkg/api/api.go b/pkg/api/api.go index 29cd450..7b192ff 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -5,6 +5,12 @@ import ( cmc "github.com/cointop-sh/cointop/pkg/api/impl/coinmarketcap" ) +type CoinGeckoConfig struct { + PerPage uint + MaxPages uint + ApiKey string +} + // NewCMC new CoinMarketCap API func NewCMC(apiKey string) Interface { return cmc.NewCMC(apiKey) @@ -16,9 +22,10 @@ func NewCC() { } // NewCG new CoinGecko API -func NewCG(perPage, maxPages uint) Interface { +func NewCG(config *CoinGeckoConfig) Interface { return cg.NewCoinGecko(&cg.Config{ - PerPage: perPage, - MaxPages: maxPages, + PerPage: config.PerPage, + MaxPages: config.MaxPages, + ApiKey: config.ApiKey, }) } diff --git a/pkg/api/impl/coingecko/coingecko.go b/pkg/api/impl/coingecko/coingecko.go index ea22bfb..268daf6 100644 --- a/pkg/api/impl/coingecko/coingecko.go +++ b/pkg/api/impl/coingecko/coingecko.go @@ -26,6 +26,7 @@ var ErrNotFound = errors.New("not found") type Config struct { PerPage uint MaxPages uint + ApiKey string } // Service service @@ -52,7 +53,7 @@ func NewCoinGecko(config *Config) *Service { maxPages = uint(math.Ceil(math.Max(float64(maxResults)/float64(maxResultsPerPage), 1))) } - client := gecko.NewClient(nil) + client := gecko.NewClient(nil, config.ApiKey) svc := &Service{ client: client, maxResultsPerPage: uint(math.Min(float64(maxResults), float64(maxResultsPerPage))), diff --git a/pkg/api/vendors/coingecko/v3/v3.go b/pkg/api/vendors/coingecko/v3/v3.go index 1bb4891..57f556e 100644 --- a/pkg/api/vendors/coingecko/v3/v3.go +++ b/pkg/api/vendors/coingecko/v3/v3.go @@ -16,19 +16,18 @@ import ( log "github.com/sirupsen/logrus" ) -var baseURL = "https://api.coingecko.com/api/v3" - // Client struct type Client struct { httpClient *http.Client + apiKey string } // NewClient create new client object -func NewClient(httpClient *http.Client) *Client { +func NewClient(httpClient *http.Client, apiKey string) *Client { if httpClient == nil { httpClient = http.DefaultClient } - return &Client{httpClient: httpClient} + return &Client{httpClient: httpClient, apiKey: apiKey} } // helper @@ -48,7 +47,7 @@ func doReq(req *http.Request, client *http.Client) ([]byte, error) { if err != nil { return nil, err } - if 200 != resp.StatusCode { + if resp.StatusCode != 200 { if debugHttp { log.Warnf("doReq Got Status '%s' from %s %s", resp.Status, req.Method, req.URL) log.Debugf("doReq Got Body: %s", body) @@ -58,6 +57,20 @@ func doReq(req *http.Request, client *http.Client) ([]byte, error) { return body, nil } +func (c *Client) getApiUrl(path string, params *url.Values) string { + urlParams := url.Values{} + subdomain := "api" + if params != nil { + urlParams = *params + } + if c.apiKey != "" { + subdomain = "pro-api" + urlParams.Add("x_cg_pro_api_key", c.apiKey) + } + url := fmt.Sprintf("https://%s.coingecko.com/api/v3%s?%s", subdomain, path, urlParams.Encode()) + return url +} + // MakeReq HTTP request helper func (c *Client) MakeReq(url string) ([]byte, error) { req, err := http.NewRequest("GET", url, nil) @@ -76,7 +89,7 @@ func (c *Client) MakeReq(url string) ([]byte, error) { // Ping /ping endpoint func (c *Client) Ping() (*types.Ping, error) { - url := fmt.Sprintf("%s/ping", baseURL) + url := c.getApiUrl("/ping", nil) resp, err := c.MakeReq(url) if err != nil { return nil, err @@ -105,14 +118,14 @@ func (c *Client) SimpleSinglePrice(id string, vsCurrency string) (*types.SimpleS // SimplePrice /simple/price Multiple ID and Currency (ids, vs_currencies) func (c *Client) SimplePrice(ids []string, vsCurrencies []string) (*map[string]map[string]float32, error) { - params := url.Values{} + params := &url.Values{} idsParam := strings.Join(ids[:], ",") vsCurrenciesParam := strings.Join(vsCurrencies[:], ",") params.Add("ids", idsParam) params.Add("vs_currencies", vsCurrenciesParam) - url := fmt.Sprintf("%s/simple/price?%s", baseURL, params.Encode()) + url := c.getApiUrl("/simple/price", params) resp, err := c.MakeReq(url) if err != nil { return nil, err @@ -129,7 +142,7 @@ func (c *Client) SimplePrice(ids []string, vsCurrencies []string) (*map[string]m // SimpleSupportedVSCurrencies /simple/supported_vs_currencies func (c *Client) SimpleSupportedVSCurrencies() (*types.SimpleSupportedVSCurrencies, error) { - url := fmt.Sprintf("%s/simple/supported_vs_currencies", baseURL) + url := c.getApiUrl("/simple/supported_vs_currencies", nil) resp, err := c.MakeReq(url) if err != nil { return nil, err @@ -145,7 +158,7 @@ func (c *Client) SimpleSupportedVSCurrencies() (*types.SimpleSupportedVSCurrenci // CoinsList /coins/list func (c *Client) CoinsList() (*types.CoinList, error) { - url := fmt.Sprintf("%s/coins/list", baseURL) + url := c.getApiUrl("/coins/list", nil) resp, err := c.MakeReq(url) if err != nil { return nil, err @@ -164,7 +177,7 @@ func (c *Client) CoinsMarket(vsCurrency string, ids []string, order string, perP if len(vsCurrency) == 0 { return nil, fmt.Errorf("vsCurrency is required") } - params := url.Values{} + params := &url.Values{} // vsCurrency params.Add("vs_currency", vsCurrency) // order @@ -190,7 +203,7 @@ func (c *Client) CoinsMarket(vsCurrency string, ids []string, order string, perP priceChangePercentageParam := strings.Join(priceChangePercentage[:], ",") params.Add("price_change_percentage", priceChangePercentageParam) } - url := fmt.Sprintf("%s/coins/markets?%s", baseURL, params.Encode()) + url := c.getApiUrl("/coins/markets", params) resp, err := c.MakeReq(url) if err != nil { return nil, err @@ -209,14 +222,14 @@ func (c *Client) CoinsID(id string, localization bool, tickers bool, marketData if len(id) == 0 { return nil, fmt.Errorf("id is required") } - params := url.Values{} + params := &url.Values{} params.Add("localization", format.Bool2String(localization)) params.Add("tickers", format.Bool2String(tickers)) params.Add("market_data", format.Bool2String(marketData)) params.Add("community_data", format.Bool2String(communityData)) params.Add("developer_data", format.Bool2String(developerData)) params.Add("sparkline", format.Bool2String(sparkline)) - url := fmt.Sprintf("%s/coins/%s?%s", baseURL, id, params.Encode()) + url := c.getApiUrl(fmt.Sprintf("/coins/%s", id), params) resp, err := c.MakeReq(url) if err != nil { return nil, err @@ -235,11 +248,11 @@ func (c *Client) CoinsIDTickers(id string, page int) (*types.CoinsIDTickers, err if len(id) == 0 { return nil, fmt.Errorf("id is required") } - params := url.Values{} + params := &url.Values{} if page > 0 { params.Add("page", format.Int2String(page)) } - url := fmt.Sprintf("%s/coins/%s/tickers?%s", baseURL, id, params.Encode()) + url := c.getApiUrl(fmt.Sprintf("/coins/%s/tickers", id), params) resp, err := c.MakeReq(url) if err != nil { return nil, err @@ -257,11 +270,11 @@ func (c *Client) CoinsIDHistory(id string, date string, localization bool) (*typ if len(id) == 0 || len(date) == 0 { return nil, fmt.Errorf("id and date is required") } - params := url.Values{} + params := &url.Values{} params.Add("date", date) params.Add("localization", format.Bool2String(localization)) - url := fmt.Sprintf("%s/coins/%s/history?%s", baseURL, id, params.Encode()) + url := c.getApiUrl(fmt.Sprintf("/coins/%s/history", id), params) resp, err := c.MakeReq(url) if err != nil { return nil, err @@ -280,11 +293,11 @@ func (c *Client) CoinsIDMarketChart(id string, vsCurrency string, days string) ( return nil, fmt.Errorf("id, vsCurrency, and days is required") } - params := url.Values{} + params := &url.Values{} params.Add("vs_currency", vsCurrency) params.Add("days", days) - url := fmt.Sprintf("%s/coins/%s/market_chart?%s", baseURL, id, params.Encode()) + url := c.getApiUrl(fmt.Sprintf("/coins/%s/market_chart", id), params) resp, err := c.MakeReq(url) if err != nil { return nil, err @@ -303,7 +316,7 @@ func (c *Client) CoinsIDMarketChart(id string, vsCurrency string, days string) ( // CoinsIDContractAddress https://api.coingecko.com/api/v3/coins/{id}/contract/{contract_address} // func CoinsIDContractAddress(id string, address string) (nil, error) { -// url := fmt.Sprintf("%s/coins/%s/contract/%s", baseURL, id, address) +// url := c.getApiUrl(fmt.Sprintf("/coins/%s/contract/%s", id, address), nil) // resp, err := request.MakeReq(url) // if err != nil { // return nil, err @@ -312,7 +325,7 @@ func (c *Client) CoinsIDMarketChart(id string, vsCurrency string, days string) ( // EventsCountries https://api.coingecko.com/api/v3/events/countries func (c *Client) EventsCountries() ([]types.EventCountryItem, error) { - url := fmt.Sprintf("%s/events/countries", baseURL) + url := c.getApiUrl("/events/countries", nil) resp, err := c.MakeReq(url) if err != nil { return nil, err @@ -328,7 +341,7 @@ func (c *Client) EventsCountries() ([]types.EventCountryItem, error) { // EventsTypes https://api.coingecko.com/api/v3/events/types func (c *Client) EventsTypes() (*types.EventsTypes, error) { - url := fmt.Sprintf("%s/events/types", baseURL) + url := c.getApiUrl("/events/types", nil) resp, err := c.MakeReq(url) if err != nil { return nil, err @@ -344,7 +357,7 @@ func (c *Client) EventsTypes() (*types.EventsTypes, error) { // ExchangeRates https://api.coingecko.com/api/v3/exchange_rates func (c *Client) ExchangeRates() (*types.ExchangeRatesItem, error) { - url := fmt.Sprintf("%s/exchange_rates", baseURL) + url := c.getApiUrl("/exchange_rates", nil) resp, err := c.MakeReq(url) if err != nil { return nil, err @@ -359,7 +372,7 @@ func (c *Client) ExchangeRates() (*types.ExchangeRatesItem, error) { // Global https://api.coingecko.com/api/v3/global func (c *Client) Global() (*types.Global, error) { - url := fmt.Sprintf("%s/global", baseURL) + url := c.getApiUrl("/global", nil) resp, err := c.MakeReq(url) if err != nil { return nil, err