mirror of https://github.com/miguelmota/cointop
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
433 lines
9.8 KiB
Go
433 lines
9.8 KiB
Go
package coinmarketcap
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
apitypes "github.com/miguelmota/cointop/pkg/api/types"
|
|
util "github.com/miguelmota/cointop/pkg/api/util"
|
|
cmc "github.com/miguelmota/go-coinmarketcap/pro/v1"
|
|
cmcv2 "github.com/miguelmota/go-coinmarketcap/v2"
|
|
)
|
|
|
|
// ErrQuoteNotFound is the error for when a quote is not found
|
|
var ErrQuoteNotFound = errors.New("quote not found")
|
|
|
|
// ErrPingFailed is the error for when pinging the API fails
|
|
var ErrPingFailed = errors.New("failed to ping")
|
|
|
|
// ErrFetchGraphData is the error for when fetching graph data fails
|
|
var ErrFetchGraphData = errors.New("graph data fetch error")
|
|
|
|
// Service service
|
|
type Service struct {
|
|
client *cmc.Client
|
|
}
|
|
|
|
// NewCMC new service
|
|
func NewCMC(apiKey string) *Service {
|
|
if apiKey == "" {
|
|
apiKey = os.Getenv("CMC_PRO_API_KEY")
|
|
}
|
|
client := cmc.NewClient(&cmc.Config{
|
|
ProAPIKey: apiKey,
|
|
})
|
|
return &Service{
|
|
client: client,
|
|
}
|
|
}
|
|
|
|
// Ping ping API
|
|
func (s *Service) Ping() error {
|
|
info, err := s.client.Cryptocurrency.Info(&cmc.InfoOptions{
|
|
Symbol: "BTC",
|
|
})
|
|
if err != nil {
|
|
return ErrPingFailed
|
|
}
|
|
if info == nil {
|
|
return ErrPingFailed
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) getPaginatedCoinData(convert string, offset int) ([]apitypes.Coin, error) {
|
|
var ret []apitypes.Coin
|
|
max := 100
|
|
|
|
listings, err := s.client.Cryptocurrency.LatestListings(&cmc.ListingOptions{
|
|
Limit: max,
|
|
Convert: convert,
|
|
Start: max * offset,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, v := range listings {
|
|
quote, ok := v.Quote[convert]
|
|
if !ok {
|
|
return nil, ErrQuoteNotFound
|
|
}
|
|
|
|
ret = append(ret, apitypes.Coin{
|
|
ID: util.FormatID(v.Name),
|
|
Name: util.FormatName(v.Name),
|
|
Symbol: util.FormatSymbol(v.Symbol),
|
|
Rank: util.FormatRank(v.CMCRank),
|
|
AvailableSupply: util.FormatSupply(v.CirculatingSupply),
|
|
TotalSupply: util.FormatSupply(v.TotalSupply),
|
|
MarketCap: util.FormatMarketCap(quote.MarketCap),
|
|
Price: util.FormatPrice(v.Quote[convert].Price, convert),
|
|
PercentChange1H: util.FormatPercentChange(quote.PercentChange1H),
|
|
PercentChange24H: util.FormatPercentChange(quote.PercentChange24H),
|
|
PercentChange7D: util.FormatPercentChange(quote.PercentChange7D),
|
|
Volume24H: util.FormatVolume(v.Quote[convert].Volume24H),
|
|
LastUpdated: util.FormatLastUpdated(v.LastUpdated),
|
|
})
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// GetAllCoinData gets all coin data. Need to paginate through all pages
|
|
func (s *Service) GetAllCoinData(convert string, ch chan []apitypes.Coin) error {
|
|
go func() {
|
|
maxPages := 10
|
|
defer close(ch)
|
|
for i := 0; i < maxPages; i++ {
|
|
if i > 0 {
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
|
|
coins, err := s.getPaginatedCoinData(convert, i)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
ch <- coins
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
// GetCoinData gets all data of a coin.
|
|
func (s *Service) GetCoinData(name string, convert string) (apitypes.Coin, error) {
|
|
ret := apitypes.Coin{}
|
|
coins, err := s.getPaginatedCoinData(convert, 0)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
|
|
for _, coin := range coins {
|
|
if coin.Name == name {
|
|
return coin, nil
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// GetCoinDataBatch gets all data of specified coins.
|
|
func (s *Service) GetCoinDataBatch(names []string, convert string) ([]apitypes.Coin, error) {
|
|
ret := []apitypes.Coin{}
|
|
coins, err := s.getPaginatedCoinData(convert, 0)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
|
|
for _, coin := range coins {
|
|
for _, name := range names {
|
|
if coin.Name == name {
|
|
ret = append(ret, coin)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// GetCoinGraphData gets coin graph data
|
|
func (s *Service) GetCoinGraphData(convert, symbol string, name string, start int64, end int64) (apitypes.CoinGraph, error) {
|
|
ret := apitypes.CoinGraph{}
|
|
symbol = strings.ToUpper(symbol)
|
|
info, err := s.client.Cryptocurrency.Info(&cmc.InfoOptions{
|
|
Slug: name,
|
|
})
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
var coinID string
|
|
if len(info) == 0 {
|
|
return ret, ErrFetchGraphData
|
|
}
|
|
for k := range info {
|
|
coinID = fmt.Sprintf("%v", info[k].ID)
|
|
}
|
|
if convert == "" {
|
|
convert = "usd"
|
|
}
|
|
convert = strings.ToUpper(convert)
|
|
interval := getChartInterval(start, end)
|
|
params := []string{
|
|
fmt.Sprintf("convert=%s,%s", convert, symbol),
|
|
"format=chart_crypto_details",
|
|
fmt.Sprintf("id=%s", coinID),
|
|
fmt.Sprintf("interval=%s", interval),
|
|
fmt.Sprintf("time_start=%v", start),
|
|
fmt.Sprintf("time_end=%v", end),
|
|
}
|
|
baseURL := "https://web-api.coinmarketcap.com/v1.1"
|
|
url := fmt.Sprintf("%s/cryptocurrency/quotes/historical?%s", baseURL, strings.Join(params, "&"))
|
|
resp, err := makeReq(url)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
var result map[string]interface{}
|
|
err = json.Unmarshal(resp, &result)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
data, ok := result["data"]
|
|
if !ok {
|
|
return ret, ErrFetchGraphData
|
|
}
|
|
ifcs, ok := data.(map[string]interface{})
|
|
if !ok {
|
|
return ret, ErrFetchGraphData
|
|
}
|
|
var prices [][]float64
|
|
for datetime, item := range ifcs {
|
|
ifc, ok := item.(map[string]interface{})
|
|
if !ok {
|
|
return ret, ErrFetchGraphData
|
|
}
|
|
for key, obj := range ifc {
|
|
if key != convert {
|
|
continue
|
|
}
|
|
arrIfc, ok := obj.([]interface{})
|
|
if !ok {
|
|
return ret, ErrFetchGraphData
|
|
}
|
|
if len(arrIfc) == 0 {
|
|
return ret, ErrFetchGraphData
|
|
}
|
|
val := arrIfc[0].(float64)
|
|
t, err := time.Parse(time.RFC3339, datetime)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
prices = append(prices, []float64{float64(t.Unix()), val})
|
|
}
|
|
}
|
|
sort.Slice(prices[:], func(i, j int) bool {
|
|
return prices[i][0] < prices[j][0]
|
|
})
|
|
ret.Price = prices
|
|
return ret, nil
|
|
}
|
|
|
|
// GetGlobalMarketGraphData gets global market graph data
|
|
func (s *Service) GetGlobalMarketGraphData(convert string, start int64, end int64) (apitypes.MarketGraph, error) {
|
|
ret := apitypes.MarketGraph{}
|
|
if convert == "" {
|
|
convert = "usd"
|
|
}
|
|
convert = strings.ToUpper(convert)
|
|
interval := getChartInterval(start, end)
|
|
params := []string{
|
|
fmt.Sprintf("convert=%s", convert),
|
|
"format=chart",
|
|
fmt.Sprintf("interval=%s", interval),
|
|
fmt.Sprintf("time_start=%v", start),
|
|
fmt.Sprintf("time_end=%v", end),
|
|
}
|
|
baseURL := "https://web-api.coinmarketcap.com/v1.1"
|
|
url := fmt.Sprintf("%s/global-metrics/quotes/historical?%s", baseURL, strings.Join(params, "&"))
|
|
resp, err := makeReq(url)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
var result map[string]interface{}
|
|
err = json.Unmarshal(resp, &result)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
data, ok := result["data"]
|
|
if !ok {
|
|
return ret, ErrFetchGraphData
|
|
}
|
|
mapIfc, ok := data.(map[string]interface{})
|
|
if !ok {
|
|
return ret, ErrFetchGraphData
|
|
}
|
|
var marketCap [][]float64
|
|
for datetime, item := range mapIfc {
|
|
arrIfc, ok := item.([]interface{})
|
|
if !ok {
|
|
return ret, ErrFetchGraphData
|
|
}
|
|
if len(arrIfc) == 0 {
|
|
return ret, ErrFetchGraphData
|
|
}
|
|
val := arrIfc[0].(float64)
|
|
t, err := time.Parse(time.RFC3339, datetime)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
marketCap = append(marketCap, []float64{float64(t.Unix()), val})
|
|
}
|
|
sort.Slice(marketCap[:], func(i, j int) bool {
|
|
return marketCap[i][0] < marketCap[j][0]
|
|
})
|
|
ret.MarketCapByAvailableSupply = marketCap
|
|
return ret, nil
|
|
}
|
|
|
|
// GetGlobalMarketData gets global market data
|
|
func (s *Service) GetGlobalMarketData(convert string) (apitypes.GlobalMarketData, error) {
|
|
ret := apitypes.GlobalMarketData{}
|
|
market, err := s.client.GlobalMetrics.LatestQuotes(&cmc.QuoteOptions{
|
|
Convert: convert,
|
|
})
|
|
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
ret = apitypes.GlobalMarketData{
|
|
TotalMarketCapUSD: market.Quote[convert].TotalMarketCap,
|
|
Total24HVolumeUSD: market.Quote[convert].TotalVolume24H,
|
|
BitcoinPercentageOfMarketCap: market.BTCDominance,
|
|
ActiveCurrencies: int(market.ActiveCryptocurrencies),
|
|
ActiveAssets: 0,
|
|
ActiveMarkets: int(market.ActiveMarketPairs),
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// Price returns the current price of the coin
|
|
func (s *Service) Price(name string, convert string) (float64, error) {
|
|
convert = strings.ToUpper(convert)
|
|
symbol, err := cmcv2.CoinSymbol(util.NameToSlug(name))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
price, err := cmcv2.Price(&cmcv2.PriceOptions{
|
|
Symbol: symbol,
|
|
Convert: convert,
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return util.FormatPrice(price, convert), nil
|
|
}
|
|
|
|
// CoinLink returns the URL link for the coin
|
|
func (s *Service) CoinLink(name string) string {
|
|
slug := util.NameToSlug(name)
|
|
return fmt.Sprintf("https://coinmarketcap.com/currencies/%s", slug)
|
|
}
|
|
|
|
// SupportedCurrencies returns a list of supported currencies
|
|
func (s *Service) SupportedCurrencies() []string {
|
|
// keep these in alphabetical order
|
|
return []string{
|
|
"AUD",
|
|
"BRL",
|
|
"BTC",
|
|
"CAD",
|
|
"CFH",
|
|
"CLP",
|
|
"CNY",
|
|
"CZK",
|
|
"DKK",
|
|
"ETH",
|
|
"EUR",
|
|
"GBP",
|
|
"HKD",
|
|
"HUF",
|
|
"IDR",
|
|
"ILS",
|
|
"INR",
|
|
"JPY",
|
|
"KRW",
|
|
"MXN",
|
|
"MYR",
|
|
"NOK",
|
|
"NZD",
|
|
"PHP",
|
|
"PKR",
|
|
"PLN",
|
|
"RUB",
|
|
"SEK",
|
|
"SGD",
|
|
"THB",
|
|
"TRY",
|
|
"TWD",
|
|
"USD",
|
|
"VND",
|
|
"ZAR",
|
|
}
|
|
}
|
|
|
|
// doReq does HTTP request with client
|
|
func doReq(req *http.Request) ([]byte, error) {
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
return nil, fmt.Errorf("%s", body)
|
|
}
|
|
|
|
return body, nil
|
|
}
|
|
|
|
// makeReq is an HTTP GET request helper
|
|
func makeReq(url string) ([]byte, error) {
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := doReq(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return resp, err
|
|
}
|
|
|
|
// getChartInterval returns the interval to use for given time range
|
|
func getChartInterval(start, end int64) string {
|
|
interval := "15m"
|
|
delta := end - start
|
|
if delta >= 604800 {
|
|
interval = "1h"
|
|
}
|
|
if delta >= 2629746 {
|
|
interval = "1d"
|
|
}
|
|
if delta >= 604800 {
|
|
interval = "1h"
|
|
}
|
|
if delta >= 2592000 {
|
|
interval = "1d"
|
|
}
|
|
return interval
|
|
}
|