1// Package coinmarketcap Coin Market Cap API client for Go
2package coinmarketcap
3
4import (
5	"encoding/json"
6	"errors"
7	"fmt"
8	"io/ioutil"
9	"net/http"
10	"strconv"
11	"strings"
12
13	"sort"
14
15	"github.com/anaskhan96/soup"
16	"github.com/miguelmota/go-coinmarketcap/v2/types"
17)
18
19var (
20	siteURL               = "https://coinmarketcap.com"
21	baseURL               = "https://api.coinmarketcap.com/v2"
22	coinGraphURL          = "https://graphs2.coinmarketcap.com/currencies"
23	globalMarketGraphURL  = "https://graphs2.coinmarketcap.com/global/marketcap-total"
24	altcoinMarketGraphURL = "https://graphs2.coinmarketcap.com/global/marketcap-altcoin"
25)
26
27// Interface interface
28type Interface interface {
29	Listings() ([]*types.Listing, error)
30	Tickers(options *TickersOptions) ([]*types.Ticker, error)
31	Ticker(options *TickerOptions) (*types.Ticker, error)
32	TickerGraph(options *TickerGraphOptions) (*types.TickerGraph, error)
33	GlobalMarket(options *GlobalMarketOptions) (*types.GlobalMarket, error)
34	GlobalMarketGraph(options *GlobalMarketGraphOptions) (*types.MarketGraph, error)
35	GlobalAltcoinMarketGraph(options *GlobalAltcoinMarketGraphOptions) (*types.MarketGraph, error)
36	Markets(options *MarketsOptions) ([]*types.Market, error)
37	Price(options *PriceOptions) (float64, error)
38	CoinID(symbol string) (int, error)
39	CoinSlug(symbol string) (string, error)
40	CoinSymbol(slug string) (string, error)
41}
42
43// listingsMedia listings response media
44type listingsMedia struct {
45	Data []*types.Listing `json:"data"`
46}
47
48// Listings gets all coin listings
49func Listings() ([]*types.Listing, error) {
50	url := fmt.Sprintf("%s/listings", baseURL)
51	resp, err := makeReq(url)
52	var body listingsMedia
53	err = json.Unmarshal(resp, &body)
54	if err != nil {
55		return nil, err
56	}
57	return body.Data, nil
58}
59
60// TickersOptions options for tickers method
61type TickersOptions struct {
62	Start   int
63	Limit   int
64	Convert string
65	Sort    string
66}
67
68// tickerMedia tickers response media
69type tickersMedia struct {
70	Data     map[string]*types.Ticker `json:"data,omitempty"`
71	Metadata struct {
72		Timestamp           int64
73		NumCryptoCurrencies int    `json:"num_cryptocurrencies,omitempty"`
74		Error               string `json:",omitempty"`
75	}
76}
77
78// Tickers gets ticker information on coins
79func Tickers(options *TickersOptions) ([]*types.Ticker, error) {
80	var params []string
81	if options.Start >= 0 {
82		params = append(params, fmt.Sprintf("start=%v", options.Start))
83	}
84	if options.Limit >= 0 {
85		params = append(params, fmt.Sprintf("limit=%v", options.Limit))
86	}
87	if options.Convert != "" {
88		params = append(params, fmt.Sprintf("convert=%v", options.Convert))
89	}
90	if options.Sort != "" {
91		params = append(params, fmt.Sprintf("sort=%v", options.Sort))
92	}
93	url := fmt.Sprintf("%s/ticker?%s", baseURL, strings.Join(params, "&"))
94	resp, err := makeReq(url)
95	var body tickersMedia
96	err = json.Unmarshal(resp, &body)
97	if err != nil {
98		return nil, err
99	}
100	data := body.Data
101	var tickers []*types.Ticker
102	for _, v := range data {
103		tickers = append(tickers, v)
104	}
105
106	if body.Metadata.Error != "" {
107		return nil, errors.New(body.Metadata.Error)
108	}
109
110	sort.Slice(tickers, func(i, j int) bool {
111		return tickers[i].Rank < tickers[j].Rank
112	})
113
114	return tickers, nil
115}
116
117// TickerOptions options for ticker method
118type TickerOptions struct {
119	Symbol  string
120	Convert string
121}
122
123type tickerMedia struct {
124	Data *types.Ticker `json:"data"`
125}
126
127// Ticker gets ticker information about a cryptocurrency
128func Ticker(options *TickerOptions) (*types.Ticker, error) {
129	var params []string
130	if options.Convert != "" {
131		params = append(params, fmt.Sprintf("convert=%v", options.Convert))
132	}
133	id, err := CoinID(options.Symbol)
134	if err != nil {
135		return nil, err
136	}
137	url := fmt.Sprintf("%s/ticker/%v?%s", baseURL, id, strings.Join(params, "&"))
138	resp, err := makeReq(url)
139	if err != nil {
140		return nil, err
141	}
142	var body tickerMedia
143	err = json.Unmarshal(resp, &body)
144	if err != nil {
145		return nil, err
146	}
147	return body.Data, nil
148}
149
150// TickerGraphOptions options for ticker graph
151type TickerGraphOptions struct {
152	Symbol string
153	Start  int64
154	End    int64
155}
156
157// TickerGraph gets graph data points for a cryptocurrency
158func TickerGraph(options *TickerGraphOptions) (*types.TickerGraph, error) {
159	slug, err := CoinSlug(options.Symbol)
160	if err != nil {
161		return nil, err
162	}
163	url := fmt.Sprintf("%s/%s/%d/%d", coinGraphURL, slug, options.Start*1000, options.End*1000)
164	resp, err := makeReq(url)
165	if err != nil {
166		return nil, err
167	}
168	var data *types.TickerGraph
169	err = json.Unmarshal(resp, &data)
170	if err != nil {
171		return nil, err
172	}
173	return data, nil
174}
175
176// GlobalMarketOptions options for global data method
177type GlobalMarketOptions struct {
178	Convert string
179}
180
181// globalMedia global data response media
182type globalMarketMedia struct {
183	Data *types.GlobalMarket `json:"data"`
184}
185
186// GlobalMarket gets information about the global market of the cryptocurrencies
187func GlobalMarket(options *GlobalMarketOptions) (*types.GlobalMarket, error) {
188	var params []string
189	if options.Convert != "" {
190		params = append(params, fmt.Sprintf("convert=%v", options.Convert))
191	}
192	url := fmt.Sprintf("%s/global?%s", baseURL, strings.Join(params, "&"))
193	resp, err := makeReq(url)
194	var body globalMarketMedia
195	err = json.Unmarshal(resp, &body)
196	if err != nil {
197		return nil, err
198	}
199	return body.Data, nil
200}
201
202// GlobalMarketGraphOptions options for global market graph method
203type GlobalMarketGraphOptions struct {
204	Start int64
205	End   int64
206}
207
208// GlobalMarketGraph get graph data points of global market
209func GlobalMarketGraph(options *GlobalMarketGraphOptions) (*types.MarketGraph, error) {
210	url := fmt.Sprintf("%s/%d/%d", globalMarketGraphURL, options.Start*1000, options.End*1000)
211	resp, err := makeReq(url)
212	if err != nil {
213		return nil, err
214	}
215	var data *types.MarketGraph
216	err = json.Unmarshal(resp, &data)
217	if err != nil {
218		return nil, err
219	}
220	return data, nil
221}
222
223// GlobalAltcoinMarketGraphOptions options for global altcoin market graph method
224type GlobalAltcoinMarketGraphOptions struct {
225	Start int64
226	End   int64
227}
228
229// GlobalAltcoinMarketGraph gets graph data points of altcoin market
230func GlobalAltcoinMarketGraph(options *GlobalAltcoinMarketGraphOptions) (*types.MarketGraph, error) {
231	url := fmt.Sprintf("%s/%d/%d", altcoinMarketGraphURL, options.Start*1000, options.End*1000)
232	resp, err := makeReq(url)
233	if err != nil {
234		return nil, err
235	}
236	var data *types.MarketGraph
237	err = json.Unmarshal(resp, &data)
238	if err != nil {
239		return nil, err
240	}
241	return data, nil
242}
243
244// MarketsOptions options for markets method
245type MarketsOptions struct {
246	Symbol string
247}
248
249// Markets get market data for a cryptocurrency
250func Markets(options *MarketsOptions) ([]*types.Market, error) {
251	slug, err := CoinSlug(options.Symbol)
252	if err != nil {
253		return nil, err
254	}
255	url := fmt.Sprintf("%s/currencies/%s/#markets", siteURL, slug)
256	var markets []*types.Market
257	response, err := soup.Get(url)
258	if err != nil {
259		return nil, err
260	}
261	rows := soup.HTMLParse(response).Find("table", "id", "markets-table").Find("tbody").FindAll("tr")
262	for _, row := range rows {
263		var data []string
264		for _, column := range row.FindAll("td") {
265			attrs := column.Attrs()
266			if attrs["data-sort"] != "" {
267				data = append(data, attrs["data-sort"])
268			} else {
269				data = append(data, column.Text())
270			}
271		}
272		markets = append(markets, &types.Market{
273			Rank:          toInt(data[0]),
274			Exchange:      data[1],
275			Pair:          data[2],
276			VolumeUSD:     toFloat(data[3]),
277			Price:         toFloat(data[4]),
278			VolumePercent: toFloat(data[5]),
279			Updated:       data[6],
280		})
281	}
282	return markets, nil
283}
284
285// PriceOptions options for price method
286type PriceOptions struct {
287	Symbol  string
288	Convert string
289}
290
291// Price gets price of a cryptocurrency
292func Price(options *PriceOptions) (float64, error) {
293	coin, err := Ticker(&TickerOptions{
294		Convert: options.Convert,
295		Symbol:  options.Symbol,
296	})
297	if err != nil {
298		return 0, err
299	}
300
301	if coin == nil {
302		return 0, errors.New("coin not found")
303	}
304	return coin.Quotes[options.Convert].Price, nil
305}
306
307// CoinID gets the ID for the cryptocurrency
308func CoinID(symbol string) (int, error) {
309	symbol = strings.ToUpper(strings.TrimSpace(symbol))
310	listings, err := Listings()
311	if err != nil {
312		return 0, err
313	}
314
315	for _, l := range listings {
316		if l.Symbol == symbol {
317			return l.ID, nil
318		}
319
320		if l.Slug == strings.ToLower(symbol) {
321			return l.ID, nil
322		}
323	}
324
325	return 0, errors.New("coin not found")
326}
327
328// CoinSlug gets the slug for the cryptocurrency
329func CoinSlug(symbol string) (string, error) {
330	symbol = strings.ToUpper(strings.TrimSpace(symbol))
331	coin, err := Ticker(&TickerOptions{
332		Symbol: symbol,
333	})
334	if err != nil {
335		return "", err
336	}
337
338	if coin == nil {
339		return "", errors.New("coin not found")
340	}
341	return coin.Slug, nil
342}
343
344// CoinSymbol gets the symbol for the cryptocurrency
345func CoinSymbol(slug string) (string, error) {
346	slug = strings.ToLower(strings.TrimSpace(slug))
347	coin, err := Ticker(&TickerOptions{
348		Symbol: slug,
349	})
350	if err != nil {
351		return "", err
352	}
353
354	if coin == nil {
355		return "", errors.New("coin not found")
356	}
357
358	return coin.Symbol, nil
359}
360
361// toInt helper for parsing strings to int
362func toInt(rawInt string) int {
363	parsed, _ := strconv.Atoi(strings.Replace(strings.Replace(rawInt, "$", "", -1), ",", "", -1))
364	return parsed
365}
366
367// toFloat helper for parsing strings to float
368func toFloat(rawFloat string) float64 {
369	parsed, _ := strconv.ParseFloat(strings.Replace(strings.Replace(strings.Replace(rawFloat, "$", "", -1), ",", "", -1), "%", "", -1), 64)
370	return parsed
371}
372
373// doReq HTTP client
374func doReq(req *http.Request) ([]byte, error) {
375	client := &http.Client{}
376	resp, err := client.Do(req)
377	if err != nil {
378		return nil, err
379	}
380	defer resp.Body.Close()
381	body, err := ioutil.ReadAll(resp.Body)
382	if err != nil {
383		return nil, err
384	}
385	if 200 != resp.StatusCode {
386		return nil, fmt.Errorf("%s", body)
387	}
388
389	return body, nil
390}
391
392// makeReq HTTP request helper
393func makeReq(url string) ([]byte, error) {
394	req, err := http.NewRequest("GET", url, nil)
395	if err != nil {
396		return nil, err
397	}
398	resp, err := doReq(req)
399	if err != nil {
400		return nil, err
401	}
402
403	return resp, err
404}
405