diff --git a/api/client/client.go b/api/client/client.go new file mode 100644 index 0000000..15da414 --- /dev/null +++ b/api/client/client.go @@ -0,0 +1,62 @@ +package client + +import ( + "net/http" + + "github.com/JulianToledano/goingecko/api" + + "github.com/JulianToledano/goingecko/api/coins" + geckohttp "github.com/JulianToledano/goingecko/http" +) + +// proApiHeader returns a function that sets the Pro API key header on requests +func proApiHeader(apiKey string) func(r *http.Request) { + return func(r *http.Request) { + r.Header.Set("x-cg-pro-api-key", apiKey) + } +} + +// demoApiHeader returns a function that sets the Demo API key header on requests +func demoApiHeader(apiKey string) func(r *http.Request) { + return func(r *http.Request) { + r.Header.Set("x-cg-demo-api-key", apiKey) + } +} + +// Client wraps the CoinGecko API client functionality +type Client struct { + *coins.Client + + url string +} + +// NewDefaultClient creates a new Client using the default HTTP client and base URL +func NewDefaultClient() *Client { + return newClient( + geckohttp.NewClient(geckohttp.WithHttpClient(http.DefaultClient)), + api.BaseURL, + ) +} + +// NewDemoApiClient creates a new Client configured for the Demo API with the provided API key and HTTP client +func NewDemoApiClient(apiKey string, c *http.Client) *Client { + return newClient( + geckohttp.NewClient(geckohttp.WithHttpClient(c), geckohttp.WithApiHeaderFn(demoApiHeader(apiKey))), + api.BaseURL, + ) +} + +// NewProApiClient creates a new Client configured for the Pro API with the provided API key and HTTP client +func NewProApiClient(apiKey string, c *http.Client) *Client { + return newClient( + geckohttp.NewClient(geckohttp.WithHttpClient(c), geckohttp.WithApiHeaderFn(proApiHeader(apiKey))), + api.ProBaseURL, + ) +} + +// newClient creates a new Client with the provided HTTP client and base URL +func newClient(c *geckohttp.Client, url string) *Client { + return &Client{ + Client: coins.NewCoinsClient(c, url), + } +} diff --git a/api/client/client_test.go b/api/client/client_test.go new file mode 100644 index 0000000..040d806 --- /dev/null +++ b/api/client/client_test.go @@ -0,0 +1,18 @@ +package client + +import ( + "context" + "testing" +) + +func TestClient_CoinsList(t *testing.T) { + c := NewDefaultClient() + + got, err := c.CoinsList(context.Background()) + if err != nil { + t.Fatal(err) + } + if got != nil { + t.Fatal("nil response") + } +} diff --git a/api/coins/client.go b/api/coins/client.go new file mode 100644 index 0000000..92eba88 --- /dev/null +++ b/api/coins/client.go @@ -0,0 +1,22 @@ +package coins + +import ( + geckohttp "github.com/JulianToledano/goingecko/http" +) + +type Client struct { + *geckohttp.Client + + url string +} + +func NewCoinsClient(c *geckohttp.Client, url string) *Client { + return &Client{ + c, + url, + } +} + +func (c *Client) coinsUrl() string { + return c.url + "/coins" +} diff --git a/api/coins/id.go b/api/coins/id.go new file mode 100644 index 0000000..a41f913 --- /dev/null +++ b/api/coins/id.go @@ -0,0 +1,121 @@ +package coins + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strconv" + + "github.com/JulianToledano/goingecko/api" + "github.com/JulianToledano/goingecko/api/coins/types" +) + +// coinsIdOption is specific to the CoinsId function +type coinsIdOption interface { + api.Option + isCoinsIdOption() +} + +// WithLocalization includes localized data in the response if true. +// Default: true +func WithLocalization(localization bool) coinsIdOption { return localizationOption{localization} } + +// WithTickers includes tickers data in the response if true. +// Default: true +func WithTickers(tickers bool) coinsIdOption { return tickersOption{tickers} } + +// WithMarketData includes market data in the response if true. +// Default: true +func WithMarketData(marketData bool) coinsIdOption { return marketDataOption{marketData} } + +// WithCommunityData includes community data in the response if true. +// Default: true +func WithCommunityData(communityData bool) coinsIdOption { + return communityDataOption{communityData} +} + +// WithDeveloperData includes developer data in the response if true. +// Default: true +func WithDeveloperData(developerData bool) coinsIdOption { + return developerDataOption{developerData} +} + +// WithCoinSparkline includes sparkline data in the response if true. +// Default: false +func WithCoinSparkline(sparkline bool) coinsIdOption { return coinSparklineOption{sparkline} } + +// CoinsId allows you to query all the coin data of a coin (name, price, market .... including exchange tickers) on +// CoinGecko coin page based on a particular coin id. +// +// ๐Ÿ‘ Tips +// +// You may obtain the coin id (api id) via several ways: +// refers to respective coin page and find โ€˜api idโ€™ +// refers to /coins/list endpoint +// refers to google sheets here +// You may also flag to include more data such as tickers, market data, community data, developer data and sparkline +// You may refer to last_updated in the endpoint response to check whether the price is stale +// +// ๐Ÿ“˜ Notes +// +// Tickers are limited to 100 items, to get more tickers, please go to /coins/{id}/tickers +// Cache/Update Frequency: +// Every 60 seconds for all the API plans +// Community data for Twitter and Telegram will be updated on weekly basis (Reddit community data is no longer supported) +func (c *Client) CoinsId(ctx context.Context, id string, options ...coinsIdOption) (*types.CoinID, error) { + params := url.Values{} + + // Apply all the options + for _, opt := range options { + opt.Apply(¶ms) + } + + rUrl := fmt.Sprintf("%s/%s?%s", c.coinsUrl(), id, params.Encode()) + resp, err := c.MakeReq(ctx, rUrl) + if err != nil { + return nil, err + } + + var data *types.CoinID + err = json.Unmarshal(resp, &data) + if err != nil { + return nil, err + } + + return data, nil +} + +// Define option types +type localizationOption struct{ localization bool } +type tickersOption struct{ tickers bool } +type marketDataOption struct{ marketData bool } +type communityDataOption struct{ communityData bool } +type developerDataOption struct{ developerData bool } +type coinSparklineOption struct{ sparkline bool } + +// Implement Option interface +func (o localizationOption) Apply(v *url.Values) { + v.Set("localization", strconv.FormatBool(o.localization)) +} +func (o tickersOption) Apply(v *url.Values) { v.Set("tickers", strconv.FormatBool(o.tickers)) } +func (o marketDataOption) Apply(v *url.Values) { + v.Set("market_data", strconv.FormatBool(o.marketData)) +} +func (o communityDataOption) Apply(v *url.Values) { + v.Set("community_data", strconv.FormatBool(o.communityData)) +} +func (o developerDataOption) Apply(v *url.Values) { + v.Set("developer_data", strconv.FormatBool(o.developerData)) +} +func (o coinSparklineOption) Apply(v *url.Values) { + v.Set("sparkline", strconv.FormatBool(o.sparkline)) +} + +// Implement CoinsIdOption interface +func (localizationOption) isCoinsIdOption() {} +func (tickersOption) isCoinsIdOption() {} +func (marketDataOption) isCoinsIdOption() {} +func (communityDataOption) isCoinsIdOption() {} +func (developerDataOption) isCoinsIdOption() {} +func (coinSparklineOption) isCoinsIdOption() {} diff --git a/api/coins/id_history.go b/api/coins/id_history.go new file mode 100644 index 0000000..48f863b --- /dev/null +++ b/api/coins/id_history.go @@ -0,0 +1,72 @@ +package coins + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strconv" + + "github.com/JulianToledano/goingecko/api" + "github.com/JulianToledano/goingecko/api/coins/types" +) + +// idHistoryOption is an interface that extends api.Option to provide +// specific options for the CoinsIdHistory endpoint. It includes a marker +// method isIdHistoryOption() to ensure type safety for history-specific options. +type idHistoryOption interface { + api.Option + isIdHistoryOption() +} + +// WithLocalizationIdHistoryOption sets whether to include localized data. +// If true, returns localized data in response (name, description, etc.) +// If false, returns data in English. +func WithLocalizationIdHistoryOption(loc bool) idHistoryOption { + return localizationIdHistoryOption{loc} +} + +// CoinsIdHistory allows you to query the historical data (price, market cap, 24hrs volume, etc) at a given date for a +// coin based on a particular coin id. +// +// ๐Ÿ‘ Tips +// +// You may obtain the coin id (api id) via several ways: +// refers to respective coin page and find โ€˜api idโ€™ +// refers to /coins/list endpoint +// refers to google sheets here +// +// ๐Ÿ“˜ Notes +// +// The data returned is at 00:00:00 UTC +// The last completed UTC day (00:00) is available 35 minutes after midnight on the next UTC day (00:35) +func (c *Client) CoinsIdHistory(ctx context.Context, id, date string, options ...idHistoryOption) (*types.History, error) { + params := url.Values{} + params.Set("date", date) + + // Apply all the options + for _, opt := range options { + opt.Apply(¶ms) + } + + rUrl := fmt.Sprintf("%s/%s/%s?%s", c.coinsUrl(), id, "history", params.Encode()) + resp, err := c.MakeReq(ctx, rUrl) + if err != nil { + return nil, err + } + + var data *types.History + err = json.Unmarshal(resp, &data) + if err != nil { + return nil, err + } + + return data, nil +} + +type localizationIdHistoryOption struct{ localization bool } + +func (o localizationIdHistoryOption) Apply(v *url.Values) { + v.Set("localization", strconv.FormatBool(o.localization)) +} +func (localizationIdHistoryOption) isIdHistoryOption() {} diff --git a/api/coins/id_market_chart.go b/api/coins/id_market_chart.go new file mode 100644 index 0000000..c92f868 --- /dev/null +++ b/api/coins/id_market_chart.go @@ -0,0 +1,88 @@ +package coins + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + + "github.com/JulianToledano/goingecko/api" + "github.com/JulianToledano/goingecko/types" +) + +// idMarketChartOption is an interface that extends api.Option to provide +// specific options for the CoinsIdMarketChart endpoint. It includes a marker +// method isIdMarketChartOptions() to ensure type safety for market chart-specific options. +type idMarketChartOption interface { + api.Option + isIdMarketChartOption() +} + +// WithIntervalIdMarketChart sets the interval between data points in the response. +// Valid values: 5m, hourly, daily +func WithIntervalIdMarketChart(interval string) idMarketChartOption { + return intervalIdMarketChartOptions{interval} +} + +// WithPrecisionIdMarketChart sets the number of decimal places in the response data. +// Valid values: from 1 to 18 +func WithPrecisionIdMarketChart(precision string) idMarketChartOption { + return precisionIdMarketChartOptions{precision} +} + +// CoinsIdMarketChart allows you to get the historical chart data of a coin including time in UNIX, price, market cap +// and 24hrs volume based on particular coin id. +// +// ๐Ÿ‘Tips +// +// You may obtain the coin id (api id) via several ways: +// refers to respective coin page and find โ€˜api idโ€™ +// refers to /coins/list endpoint +// refers to google sheets here +// You may use tools like epoch converter to convert human readable date to UNIX timestamp +// +// ๐Ÿ“˜Notes +// You may leave the interval params as empty for automatic granularity: +// 1 day from current time = 5-minutely data +// 2 - 90 days from current time = hourly data +// above 90 days from current time = daily data (00:00 UTC) +// For non-Enterprise plan subscribers who would like to get hourly data, please leave the interval params empty for auto granularity +// The 5-minutely and hourly interval params are also exclusively available to Enterprise plan subscribers, bypassing auto-granularity: +// interval=5m: 5-minutely historical data (responses include information from the past 10 days, up until 2 days ago) +// interval=hourly: hourly historical dataโ€จ(responses include information from the past 100 days, up until now) +// Cache / Update Frequency: +// every 30 seconds for all the API plans (for last data point) +// The last completed UTC day (00:00) data is available 10 minutes after midnight on the next UTC day (00:10). +func (c *Client) CoinsIdMarketChart(ctx context.Context, id, vsCurrency, days string, options ...idMarketChartOption) (*types.MarketChart, error) { + params := url.Values{} + params.Add("vs_currency", vsCurrency) + params.Add("days", days) + params.Add("interval", "daily") + + for _, opt := range options { + opt.Apply(¶ms) + } + + rUrl := fmt.Sprintf("%s/%s/%s?%s", c.coinsUrl(), id, "market_chart", params.Encode()) + resp, err := c.MakeReq(ctx, rUrl) + if err != nil { + return nil, err + } + + var data *types.MarketChart + err = json.Unmarshal(resp, &data) + if err != nil { + return nil, err + } + + return data, nil +} + +type intervalIdMarketChartOptions struct{ interval string } +type precisionIdMarketChartOptions struct{ precision string } + +func (o intervalIdMarketChartOptions) Apply(v *url.Values) { v.Set("interval", o.interval) } +func (o precisionIdMarketChartOptions) Apply(v *url.Values) { v.Set("precision", o.precision) } + +func (o intervalIdMarketChartOptions) isIdMarketChartOption() {} +func (o precisionIdMarketChartOptions) isIdMarketChartOption() {} diff --git a/api/coins/id_market_chart_range.go b/api/coins/id_market_chart_range.go new file mode 100644 index 0000000..023a5f8 --- /dev/null +++ b/api/coins/id_market_chart_range.go @@ -0,0 +1,65 @@ +package coins + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + + "github.com/JulianToledano/goingecko/api" + "github.com/JulianToledano/goingecko/types" +) + +// idMarketChartRangeOption is an interface that extends api.Option to provide +// specific options for the CoinsIdMarketChartRange endpoint. It includes a marker +// method isIdMarketChartRangeOption() to ensure type safety for market chart range-specific options. +type idMarketChartRangeOption interface { + api.Option + isIdMarketChartRangeOption() +} + +// WithIntervalIdMarketChartRange sets the interval between data points in the response. +// Valid values: 5m, hourly, daily +func WithIntervalIdMarketChartRange(interval string) idMarketChartRangeOption { + return intervalIdMarketChartRangeOption{interval} +} + +// WithPrecisionIdMarketChartRange sets the number of decimal places in the response data. +// Valid values: from 1 to 18 +func WithPrecisionIdMarketChartRange(precision string) idMarketChartRangeOption { + return precisionIdMarketChartRangeOption{precision} +} + +func (c *Client) CoinsIdMarketChartRange(ctx context.Context, id, currency, from, to string, options ...idMarketChartRangeOption) (*types.MarketChart, error) { + params := url.Values{} + params.Add("vs_currency", currency) + params.Add("from", from) + params.Add("to", to) + + for _, opt := range options { + opt.Apply(¶ms) + } + + rUrl := fmt.Sprintf("%s/%s/%s?%s", c.coinsUrl(), id, "market_chart/range", params.Encode()) + resp, err := c.MakeReq(ctx, rUrl) + if err != nil { + return nil, err + } + + var data *types.MarketChart + err = json.Unmarshal(resp, &data) + if err != nil { + return nil, err + } + + return data, nil +} + +type intervalIdMarketChartRangeOption struct{ interval string } +type precisionIdMarketChartRangeOption struct{ precision string } + +func (o intervalIdMarketChartRangeOption) Apply(v *url.Values) { v.Set("interval", o.interval) } +func (o precisionIdMarketChartRangeOption) Apply(v *url.Values) { v.Set("precision", o.precision) } + +func (o intervalIdMarketChartRangeOption) isIdMarketChartRangeOption() {} +func (o precisionIdMarketChartRangeOption) isIdMarketChartRangeOption() {} diff --git a/api/coins/id_tickers.go b/api/coins/id_tickers.go new file mode 100644 index 0000000..4332477 --- /dev/null +++ b/api/coins/id_tickers.go @@ -0,0 +1,106 @@ +package coins + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strconv" + + "github.com/JulianToledano/goingecko/api" + "github.com/JulianToledano/goingecko/api/coins/types" +) + +// idTickersOption is an interface that extends api.Option to provide +// specific options for the CoinsIdTickers endpoint. It includes a marker +// method isCoinsIdTickersOption() to ensure type safety for tickers-specific options. +type idTickersOption interface { + api.Option + isCoinsIdTickersOption() +} + +// WithExchangeId filters tickers by exchange id. +// Multiple exchange ids can be provided as a comma-separated string. +// Refers to /exchanges/list. +func WithExchangeId(exchangeIds string) idTickersOption { return exchangeIdsOption{exchangeIds} } + +// WithIncludeExchangeLogo includes exchange logo URLs in the response if true. +// Default: false +func WithIncludeExchangeLogo(includeLogo bool) idTickersOption { + return includeExchangeLogoOption{includeLogo} +} + +// WithIdTickersPage specifies which page of results to return. +func WithIdTickersPage(page int64) idTickersOption { return pageIdTickersOption{page} } + +// WithIdTickersOrder specifies the ordering of results. +// Valid values: "trust_score_desc", "trust_score_asc", "volume_desc", "volume_asc +func WithIdTickersOrder(order string) idTickersOption { return orderIdTickersOption{order} } + +// WithDepth includes 2% orderbook depth info if "cost_to_move_up_usd" or "cost_to_move_down_usd". +// Valid values: "", "cost_to_move_up_usd", "cost_to_move_down_usd" +func WithDepth(depth string) idTickersOption { return depthOption{depth} } + +// CoinsIdTickers allows you to query the coin tickers on both centralized exchange (cex) and decentralized exchange +// (dex) based on a particular coin id. +// +// ๐Ÿ‘ Tips +// +// You may obtain the coin id (api id) via several ways: +// refers to respective coin page and find โ€˜api idโ€™ +// refers to /coins/list endpoint +// refers to google sheets here +// You may specify the exchange_ids if you want to retrieve tickers for specific exchange only +// You may include values such as page to specify which page of responses you would like to show +// You may also flag to include more data such as exchange logo and depth +// +// ๐Ÿ“˜ Notes +// +// The tickers are paginated to 100 items +// Cache / Update Frequency: every 2 minutes for all the API plans +// When order is sorted by 'volume', converted_volume will be used instead of volume +func (c *Client) CoinsIdTickers(ctx context.Context, id string, options ...idTickersOption) (*types.Tickers, error) { + params := url.Values{} + + // Apply all the options + for _, opt := range options { + opt.Apply(¶ms) + } + + rUrl := fmt.Sprintf("%s/%s/%s?%s", c.coinsUrl(), id, "tickers", params.Encode()) + resp, err := c.MakeReq(ctx, rUrl) + if err != nil { + return nil, err + } + + var data *types.Tickers + err = json.Unmarshal(resp, &data) + if err != nil { + return nil, err + } + + return data, nil +} + +// Define option types +type exchangeIdsOption struct{ exchangeIds string } +type includeExchangeLogoOption struct{ includeLogo bool } +type pageIdTickersOption struct{ page int64 } +type orderIdTickersOption struct{ order string } +type depthOption struct{ depth string } + +// Implement Option interface +func (o exchangeIdsOption) Apply(v *url.Values) { v.Set("exchange_ids", o.exchangeIds) } +func (o includeExchangeLogoOption) Apply(v *url.Values) { + v.Set("include_exchange_logo", strconv.FormatBool(o.includeLogo)) +} +func (o pageIdTickersOption) Apply(v *url.Values) { v.Set("page", strconv.FormatInt(o.page, 10)) } +func (o orderIdTickersOption) Apply(v *url.Values) { v.Set("order", o.order) } +func (o depthOption) Apply(v *url.Values) { v.Set("depth", o.depth) } + +// Implement CoinsIdTickersOption interface +func (exchangeIdsOption) isCoinsIdTickersOption() {} +func (includeExchangeLogoOption) isCoinsIdTickersOption() {} +func (pageIdTickersOption) isCoinsIdTickersOption() {} +func (orderIdTickersOption) isCoinsIdTickersOption() {} +func (depthOption) isCoinsIdTickersOption() {} diff --git a/api/coins/list.go b/api/coins/list.go new file mode 100644 index 0000000..87fbd58 --- /dev/null +++ b/api/coins/list.go @@ -0,0 +1,76 @@ +package coins + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strconv" + + "github.com/JulianToledano/goingecko/api" + "github.com/JulianToledano/goingecko/api/coins/types" +) + +// listOption is an interface that extends api.Option to provide +// specific options for the CoinsList endpoint. It includes a marker +// method isListOption() to ensure type safety for list-specific options. +type listOption interface { + api.Option + isListOption() +} + +// WithIncludePlatform include platform and token's contract addresses, default: false. +func WithIncludePlatform(include bool) listOption { return includePlatformOption{include: include} } + +// WithStatus filter by status of coins, default: active +// valid values: active and inactive. +func WithStatus(status string) listOption { return statusOption{status: status} } + +// CoinsList allows you to query all the supported coins on CoinGecko with coins id, name and symbol. +// +// ๐Ÿ‘ Tips +// You may use this endpoint to query the list of coins with coin id for other endpoints that contain params like id or +// ids (coin id). +// By default, this endpoint returns full list of active coins that are currently listed on CoinGecko.com , you can also +// flag status=inactive to retrieve coins that are no longer available on CoinGecko.com . The inactive coin ids can also +// be used with selected historical data endpoints. +// +// ๐Ÿ“˜ Notes +// There is no pagination required for this endpoint +// Cache/Update Frequency: Every 5 minutes for all the API plans +func (c *Client) CoinsList(ctx context.Context, options ...listOption) ([]*types.CoinInfo, error) { + params := url.Values{} + + // Apply all the options + for _, opt := range options { + opt.Apply(¶ms) + } + + rUrl := fmt.Sprintf("%s/%s?%s", c.coinsUrl(), "list", params.Encode()) + resp, err := c.MakeReq(ctx, rUrl) + if err != nil { + return nil, err + } + + var data []*types.CoinInfo + err = json.Unmarshal(resp, &data) + if err != nil { + return nil, err + } + + return data, nil +} + +type includePlatformOption struct{ include bool } + +func (o includePlatformOption) isListOption() {} +func (o includePlatformOption) Apply(v *url.Values) { + v.Set("include_platform", strconv.FormatBool(o.include)) +} + +type statusOption struct{ status string } + +func (o statusOption) isListOption() {} +func (o statusOption) Apply(v *url.Values) { + v.Set("status", o.status) +} diff --git a/api/coins/list_test.go b/api/coins/list_test.go new file mode 100644 index 0000000..213de11 --- /dev/null +++ b/api/coins/list_test.go @@ -0,0 +1,35 @@ +package coins + +import ( + "context" + "net/http" + "testing" + + "github.com/JulianToledano/goingecko/api" + geckohttp "github.com/JulianToledano/goingecko/http" +) + +func TestCoinsClient_CoinsList(t *testing.T) { + tests := []struct { + name string + }{ + { + name: "test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Client{ + Client: geckohttp.NewClient(geckohttp.WithHttpClient(http.DefaultClient)), + url: api.BaseURL, + } + got, err := c.CoinsList(context.Background(), WithIncludePlatform(true)) + if err != nil { + t.Errorf("CoinsList() error = %v", err) + } + if got == nil { + t.Errorf("CoinsList() got = nil, want not nil") + } + }) + } +} diff --git a/api/coins/markets.go b/api/coins/markets.go new file mode 100644 index 0000000..3a217f2 --- /dev/null +++ b/api/coins/markets.go @@ -0,0 +1,114 @@ +package coins + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strconv" + "strings" + + "github.com/JulianToledano/goingecko/api" + "github.com/JulianToledano/goingecko/api/coins/types" +) + +// marketsOption is an interface that extends api.Option to provide +// specific options for the CoinsMarket endpoint. It includes a marker +// method isCoinsMarketOption() to ensure type safety for market-specific options. +type marketsOption interface { + api.Option + isMarketsOption() +} + +// WithIDs specifies the coin ids to filter results by. +// Multiple ids can be provided as a string slice. +func WithIDs(ids []string) marketsOption { return idsOption{ids} } + +// WithCategory filters results by coin category. +// Valid values include: "decentralized_finance_defi", "stablecoins", etc. +func WithCategory(category string) marketsOption { return categoryOption{category} } + +// WithOrder specifies the ordering of results. +// Valid values: "market_cap_desc", "market_cap_asc", "volume_desc", "volume_asc", +// "id_desc", "id_asc", "gecko_desc", "gecko_asc" +func WithOrder(order string) marketsOption { return orderOption{order} } + +// WithPerPage sets the number of results per page. +// Valid values: 1-250, default: 100 +func WithPerPage(perPage int) marketsOption { return perPageOption{perPage} } + +// WithPage specifies which page of results to return. +// Default: 1 +func WithPage(page int) marketsOption { return pageOption{page} } + +// WithSparkline includes sparkline data in results if true. +// Default: false +func WithSparkline(sparkline bool) marketsOption { return sparklineOption{sparkline} } + +// WithPriceChangePercentage includes price change percentage for specified intervals. +// Valid intervals: "1h", "24h", "7d", "14d", "30d", "200d", "1y" +func WithPriceChangePercentage(intervals []string) marketsOption { + return priceChangePercentageOption{intervals} +} + +// CoinsMarket allows you to query all the supported coins with price, market cap, volume and market related data. +// ๐Ÿ‘ Tips +// You may specify the coinsโ€™ ids in ids parameter if you want to retrieve market data for specific coins only instead of the whole list +// You may also provide value in category to filter the responses based on coin's category +// You can use per_page and page values to control the number of results per page and specify which page of results you want to display in the responses +// +// ๐Ÿ“˜ Notes +// If you provide values for both category and ids parameters, the category parameter will be prioritized over the ids parameter +// Cache/Update Frequency: Every 45 seconds for all the API plans +func (c *Client) CoinsMarket(ctx context.Context, currency string, options ...marketsOption) ([]*types.Market, error) { + params := url.Values{} + params.Add("vs_currency", currency) + + // Apply all the options + for _, opt := range options { + opt.Apply(¶ms) + } + + rUrl := fmt.Sprintf("%s/%s?%s", c.coinsUrl(), "markets", params.Encode()) + resp, err := c.MakeReq(ctx, rUrl) + if err != nil { + return nil, err + } + + var data []*types.Market + err = json.Unmarshal(resp, &data) + if err != nil { + return nil, err + } + + return data, nil +} + +// Define option types +type idsOption struct{ ids []string } +type categoryOption struct{ category string } +type orderOption struct{ order string } +type perPageOption struct{ perPage int } +type pageOption struct{ page int } +type sparklineOption struct{ sparkline bool } +type priceChangePercentageOption struct{ intervals []string } + +// Implement Option interface +func (o idsOption) Apply(v *url.Values) { v.Set("ids", strings.Join(o.ids, ",")) } +func (o categoryOption) Apply(v *url.Values) { v.Set("category", o.category) } +func (o orderOption) Apply(v *url.Values) { v.Set("order", o.order) } +func (o perPageOption) Apply(v *url.Values) { v.Set("per_page", strconv.Itoa(o.perPage)) } +func (o pageOption) Apply(v *url.Values) { v.Set("page", strconv.Itoa(o.page)) } +func (o sparklineOption) Apply(v *url.Values) { v.Set("sparkline", strconv.FormatBool(o.sparkline)) } +func (o priceChangePercentageOption) Apply(v *url.Values) { + v.Set("price_change_percentage", strings.Join(o.intervals, ",")) +} + +// Implement CoinsMarketOption interface +func (idsOption) isMarketsOption() {} +func (categoryOption) isMarketsOption() {} +func (orderOption) isMarketsOption() {} +func (perPageOption) isMarketsOption() {} +func (pageOption) isMarketsOption() {} +func (sparklineOption) isMarketsOption() {} +func (priceChangePercentageOption) isMarketsOption() {} diff --git a/api/coins/ohlc.go b/api/coins/ohlc.go new file mode 100644 index 0000000..1abb608 --- /dev/null +++ b/api/coins/ohlc.go @@ -0,0 +1,87 @@ +package coins + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + + "github.com/JulianToledano/goingecko/api" + "github.com/JulianToledano/goingecko/api/coins/types" +) + +// ohlcOption is an interface that extends api.Option to provide +// specific options for the CoinsOhlc endpoint. It includes a marker +// method isOhlcOption() to ensure type safety for OHLC-specific options. +type ohlcOption interface { + api.Option + isOhlcOption() +} + +// WithOhlcIntervalOption sets the interval between data points in the response. +// Valid values: hourly, daily +func WithOhlcIntervalOption(interval string) ohlcOption { + return intervalOhlcOption{interval: interval} +} + +// WithOhlcPrecisionOption sets the number of decimal places in the response data. +// Valid values: from 1 to 18 +func WithOhlcPrecisionOption(precision string) ohlcOption { + return precisionOhlcOption{precision: precision} +} + +// CoinsOhlc allows you to get the OHLC chart (Open, High, Low, Close) of a coin based on particular coin id. +// +// ๐Ÿ‘Tips +// +// You may obtain the coin id (api id) via several ways: +// refers to respective coin page and find โ€˜api idโ€™ +// refers to /coins/list endpoint +// refers to google sheets here +// For historical chart data with better granularity, you may consider using /coins/{id}/market_chart endpoint +// +// ๐Ÿ“˜ Notes +// +// The timestamp displayed in the payload (response) indicates the end (or close) time of the OHLC data +// Data granularity (candle's body) is automatic: +// 1 - 2 days: 30 minutes +// 3 - 30 days: 4 hours +// 31 days and beyond: 4 days +// Cache / Update Frequency: +// every 15 minutes for all the API plans +// The last completed UTC day (00:00) is available 35 minutes after midnight on the next UTC day (00:35) +// Exclusive daily and hourly candle interval parameter for all paid plan subscribers (interval = daily, interval=hourly) +// 'daily' interval is available for 1 / 7 / 14 / 30 / 90 / 180 days only. +// 'hourly' interval is available for 1 /7 / 14 / 30 / 90 days only. +func (c *Client) CoinsOhlc(ctx context.Context, id, vsCurrency, days string, options ...ohlcOption) (*types.Ohlc, error) { + params := url.Values{} + params.Add("vs_currency", vsCurrency) + params.Add("days", days) + + for _, opt := range options { + opt.Apply(¶ms) + } + + rUrl := fmt.Sprintf("%s/%s/%s?%s", c.coinsUrl(), id, "ohlc", params.Encode()) + resp, err := c.MakeReq(ctx, rUrl) + if err != nil { + return nil, err + } + + var data *types.Ohlc + err = json.Unmarshal(resp, &data) + if err != nil { + return nil, err + } + + return data, nil +} + +type intervalOhlcOption struct{ interval string } +type precisionOhlcOption struct{ precision string } + +func (o intervalOhlcOption) Apply(v *url.Values) { v.Set("interval", o.interval) } +func (o precisionOhlcOption) Apply(v *url.Values) { v.Set("precision", o.precision) } + +func (o intervalOhlcOption) isOhlcOption() {} +func (o precisionOhlcOption) isOhlcOption() {} diff --git a/coins/history.go b/api/coins/types/history.go similarity index 98% rename from coins/history.go rename to api/coins/types/history.go index 9aa8f45..9ba89c4 100644 --- a/coins/history.go +++ b/api/coins/types/history.go @@ -1,4 +1,4 @@ -package coins +package types import "github.com/JulianToledano/goingecko/types" diff --git a/coins/id.go b/api/coins/types/id.go similarity index 99% rename from coins/id.go rename to api/coins/types/id.go index 3463281..926d880 100644 --- a/coins/id.go +++ b/api/coins/types/id.go @@ -1,4 +1,4 @@ -package coins +package types import "github.com/JulianToledano/goingecko/types" diff --git a/coins/list.go b/api/coins/types/list.go similarity index 88% rename from coins/list.go rename to api/coins/types/list.go index 08afbbe..840bd63 100644 --- a/coins/list.go +++ b/api/coins/types/list.go @@ -1,4 +1,4 @@ -package coins +package types type CoinInfo struct { ID string `json:"id"` diff --git a/coins/markets.go b/api/coins/types/markets.go similarity index 99% rename from coins/markets.go rename to api/coins/types/markets.go index 3f49df7..97cdaaa 100644 --- a/coins/markets.go +++ b/api/coins/types/markets.go @@ -1,4 +1,4 @@ -package coins +package types import "github.com/JulianToledano/goingecko/types" diff --git a/coins/ohlc.go b/api/coins/types/ohlc.go similarity index 62% rename from coins/ohlc.go rename to api/coins/types/ohlc.go index 61dd046..e426cae 100644 --- a/coins/ohlc.go +++ b/api/coins/types/ohlc.go @@ -1,3 +1,3 @@ -package coins +package types type Ohlc [][]float64 diff --git a/coins/statusUpdates.go b/api/coins/types/statusUpdates.go similarity index 98% rename from coins/statusUpdates.go rename to api/coins/types/statusUpdates.go index 8fafa7e..87eaa06 100644 --- a/coins/statusUpdates.go +++ b/api/coins/types/statusUpdates.go @@ -1,4 +1,4 @@ -package coins +package types import "github.com/JulianToledano/goingecko/types" diff --git a/coins/tickers.go b/api/coins/types/tickers.go similarity index 91% rename from coins/tickers.go rename to api/coins/types/tickers.go index f3c90a8..2b4a64f 100644 --- a/coins/tickers.go +++ b/api/coins/types/tickers.go @@ -1,4 +1,4 @@ -package coins +package types import "github.com/JulianToledano/goingecko/types" diff --git a/api/endpoins.go b/api/endpoins.go new file mode 100644 index 0000000..5fc39f2 --- /dev/null +++ b/api/endpoins.go @@ -0,0 +1,12 @@ +package api + +import "fmt" + +var ( + // Version represents the CoinGecko API version used by this client + Version = "v3" + // BaseURL is the base URL for the CoinGecko public API + BaseURL = fmt.Sprintf("https://api.coingecko.com/api/%s", Version) + // ProBaseURL is the base URL for the CoinGecko Pro API which requires authentication + ProBaseURL = fmt.Sprintf("https://pro-api.coingecko.com/api/%s", Version) +) diff --git a/api/option.go b/api/option.go new file mode 100644 index 0000000..8fd12bf --- /dev/null +++ b/api/option.go @@ -0,0 +1,8 @@ +package api + +import "net/url" + +// Option is a generic option type +type Option interface { + Apply(*url.Values) +} diff --git a/client.go b/client.go index 650105a..93cce3c 100644 --- a/client.go +++ b/client.go @@ -10,8 +10,10 @@ import ( "github.com/JulianToledano/goingecko/ping" ) -const apiHeader = "x-cg-demo-api-key" -const proApiHeader = "x-cg-pro-api-key" +const ( + apiHeader = "x-cg-demo-api-key" + proApiHeader = "x-cg-pro-api-key" +) type Client struct { httpClient *http.Client diff --git a/coins.go b/coins.go deleted file mode 100644 index 19fd698..0000000 --- a/coins.go +++ /dev/null @@ -1,182 +0,0 @@ -package goingecko - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "strconv" - "strings" - - "github.com/JulianToledano/goingecko/coins" - "github.com/JulianToledano/goingecko/types" -) - -func (c *Client) CoinsList(ctx context.Context) ([]*coins.CoinInfo, error) { - rUrl := fmt.Sprintf("%s/%s", c.getCoinsURL(), "list") - resp, err := c.MakeReq(ctx, rUrl) - if err != nil { - return nil, err - } - - var data []*coins.CoinInfo - err = json.Unmarshal([]byte(resp), &data) - if err != nil { - return nil, err - } - return data, nil -} - -func (c *Client) CoinsMarket(ctx context.Context, currency string, ids []string, category string, order string, perPage, page string, sparkline bool, priceChange []string) ([]*coins.Market, error) { - params := url.Values{} - idsParam := strings.Join(ids[:], ",") - pChange := strings.Join(priceChange[:], ",") - // vsCurrenciesParam := strings.Join(vsCurrencies[:], ",") - - params.Add("vs_currency", currency) - params.Add("ids", idsParam) - if category != "" { - params.Add("category", category) - } - params.Add("order", order) - params.Add("per_page", perPage) - params.Add("page", page) - params.Add("sparkline", strconv.FormatBool(sparkline)) - params.Add("price_change_percentage", pChange) - - rUrl := fmt.Sprintf("%s/%s?%s", c.getCoinsURL(), "markets", params.Encode()) - resp, err := c.MakeReq(ctx, rUrl) - if err != nil { - return nil, err - } - - var data []*coins.Market - err = json.Unmarshal([]byte(resp), &data) - if err != nil { - return nil, err - } - return data, nil -} - -func (c *Client) CoinsId(ctx context.Context, id string, localization, tickers, marketData, communityData, developerData, sparkline bool) (*coins.CoinID, error) { - params := url.Values{} - - params.Add("localization", strconv.FormatBool(localization)) - params.Add("tickers", strconv.FormatBool(tickers)) - params.Add("market_data", strconv.FormatBool(marketData)) - params.Add("community_data", strconv.FormatBool(communityData)) - params.Add("developer_data", strconv.FormatBool(developerData)) - params.Add("sparkline", strconv.FormatBool(sparkline)) - - rUrl := fmt.Sprintf("%s/%s?%s", c.getCoinsURL(), id, params.Encode()) - resp, err := c.MakeReq(ctx, rUrl) - if err != nil { - return nil, err - } - var data *coins.CoinID - err = json.Unmarshal(resp, &data) - if err != nil { - return nil, err - } - return data, nil -} - -func (c *Client) CoinsIdTickers(ctx context.Context, id, exchangeId, includeExchangeLogo, page, order, depth string) (*coins.Tickers, error) { - params := url.Values{} - - params.Add("exchange_ids", exchangeId) - params.Add("include_exchange_logo", includeExchangeLogo) - params.Add("page", page) - params.Add("order", order) - params.Add("depth", depth) - - rUrl := fmt.Sprintf("%s/%s/%s?%s", c.getCoinsURL(), id, "tickers", params.Encode()) - resp, err := c.MakeReq(ctx, rUrl) - if err != nil { - return nil, err - } - var data *coins.Tickers - err = json.Unmarshal(resp, &data) - if err != nil { - return nil, err - } - return data, nil -} - -func (c *Client) CoinsIdHistory(ctx context.Context, id, date string, localization bool) (*coins.History, error) { - params := url.Values{} - - params.Add("date", date) - params.Add("localization", strconv.FormatBool(localization)) - - rUrl := fmt.Sprintf("%s/%s/%s?%s", c.getCoinsURL(), id, "history", params.Encode()) - resp, err := c.MakeReq(ctx, rUrl) - if err != nil { - return nil, err - } - var data *coins.History - err = json.Unmarshal(resp, &data) - if err != nil { - return nil, err - } - return data, nil -} - -func (c *Client) CoinsIdMarketChart(ctx context.Context, id, currency, days, interval string) (*types.MarketChart, error) { - params := url.Values{} - params.Add("vs_currency", currency) - params.Add("days", days) - - if interval != "" { - params.Add("interval", interval) - } - - rUrl := fmt.Sprintf("%s/%s/%s?%s", c.getCoinsURL(), id, "market_chart", params.Encode()) - resp, err := c.MakeReq(ctx, rUrl) - if err != nil { - return nil, err - } - var data *types.MarketChart - err = json.Unmarshal(resp, &data) - if err != nil { - return nil, err - } - return data, nil -} - -func (c *Client) CoinsIdMarketChartRange(ctx context.Context, id, currency, from, to string) (*types.MarketChart, error) { - params := url.Values{} - params.Add("vs_currency", currency) - params.Add("from", from) - params.Add("to", to) - - rUrl := fmt.Sprintf("%s/%s/%s?%s", c.getCoinsURL(), id, "market_chart/range", params.Encode()) - resp, err := c.MakeReq(ctx, rUrl) - if err != nil { - return nil, err - } - var data *types.MarketChart - err = json.Unmarshal(resp, &data) - if err != nil { - return nil, err - } - return data, nil -} - -func (c *Client) CoinsOhlc(ctx context.Context, id, currency, days string) (*coins.Ohlc, error) { - params := url.Values{} - params.Add("vs_currency", currency) - params.Add("days", days) - - rUrl := fmt.Sprintf("%s/%s/%s?%s", c.getCoinsURL(), id, "ohlc", params.Encode()) - resp, err := c.MakeReq(ctx, rUrl) - if err != nil { - return nil, err - } - var data *coins.Ohlc - err = json.Unmarshal(resp, &data) - if err != nil { - return nil, err - } - return data, nil -} diff --git a/examples/bitcoinPrice.go b/examples/bitcoinPrice.go index 53a3ffd..498bbc7 100644 --- a/examples/bitcoinPrice.go +++ b/examples/bitcoinPrice.go @@ -10,7 +10,7 @@ func main() { cgClient := goingecko.NewClient(nil, "") defer cgClient.Close() - data, err := cgClient.CoinsId("bitcoin", true, true, true, false, false, false) + data, err := cgClient.CoinsId("bitcoin") if err != nil { fmt.Print("Somethig went wrong...") return diff --git a/examples/checkTrending.go b/examples/checkTrending.go index d6b9a03..4a9f6a9 100644 --- a/examples/checkTrending.go +++ b/examples/checkTrending.go @@ -16,7 +16,7 @@ func main() { return } for _, coin := range treding.Coins { - coinData, err := cgClient.CoinsId(coin.Item.ID, false, false, true, false, false, false) + coinData, err := cgClient.CoinsId(coin.Item.ID) if err != nil { fmt.Printf("Error: %v", err) } diff --git a/http/client.go b/http/client.go new file mode 100644 index 0000000..90649a8 --- /dev/null +++ b/http/client.go @@ -0,0 +1,86 @@ +package http + +import ( + "context" + "io" + "net/http" +) + +type ( + // ApiHeaderFn is a function type that sets headers on HTTP requests + ApiHeaderFn func(r *http.Request) + + // option is a function type that configures a Client instance + option func(*Client) *Client +) + +// WithHttpClient returns an option that sets the HTTP client used by the Client +func WithHttpClient(client *http.Client) option { + return func(c *Client) *Client { + c.httpClient = client + return c + } +} + +// WithApiHeaderFn returns an option that sets a function to add API headers to requests +func WithApiHeaderFn(fn ApiHeaderFn) option { + return func(c *Client) *Client { + c.apiHeaderSetter = fn + return c + } +} + +// Client is an HTTP client that can make API requests with optional headers +type Client struct { + // httpClient is the underlying HTTP client used to make requests + httpClient *http.Client + // apiHeaderSetter is an optional function to set API headers on requests + apiHeaderSetter ApiHeaderFn +} + +// NewClient creates a new Client with the given options +func NewClient(opts ...option) *Client { + c := &Client{} + + for _, opt := range opts { + c = opt(c) + } + + return c +} + +// MakeReq makes a GET request to the given URL with the configured client +// It returns the response body as bytes or an error if the request fails +func (c *Client) MakeReq(ctx context.Context, url string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + + if c.apiHeaderSetter != nil { + c.apiHeaderSetter(req) + } + + _, resp, err := c.do(req) + if err != nil { + return nil, err + } + + return resp, err +} + +// do executes an HTTP request and returns the status code, response body and any error +func (c *Client) do(req *http.Request) (int, []byte, error) { + resp, err := c.httpClient.Do(req) + if err != nil { + return -1, nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return -1, nil, err + } + + return resp.StatusCode, body, nil +} diff --git a/test/coins_test.go b/test/coins_test.go index 181eb2a..0d98d78 100644 --- a/test/coins_test.go +++ b/test/coins_test.go @@ -27,7 +27,11 @@ func TestCoinsMarket(t *testing.T) { priceChange := make([]string, 0) priceChange = append(priceChange, "24h") +<<<<<<< HEAD + coinData, _ := cgClient.CoinsMarket("usd", goingecko.WithIDs(ids)) +======= coinData, _ := cgClient.CoinsMarket(context.Background(), "usd", ids, "", "", "100", "1", true, priceChange) +>>>>>>> main if coinData == nil { t.Errorf("Error") }