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