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.
cointop/pkg/api/impl/coinmarketcap/coinmarketcap.go

441 lines
10 KiB
Go

package coinmarketcap
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"sort"
"strings"
"time"
apitypes "github.com/cointop-sh/cointop/pkg/api/types"
"github.com/cointop-sh/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) {
var 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
}
// GetExchangeRate gets the current excange rate between two currencies
func (s *Service) GetExchangeRate(convertFrom, convertTo string, cached bool) (float64, error) {
if convertFrom == convertTo {
return 1.0, nil
}
return 0, fmt.Errorf("unsupported currency conversion: %s => %s", convertFrom, convertTo)
}