|
|
|
@ -5,6 +5,7 @@ import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
apitypes "github.com/miguelmota/cointop/pkg/api/types"
|
|
|
|
@ -24,16 +25,20 @@ type Service struct {
|
|
|
|
|
client *gecko.Client
|
|
|
|
|
maxResultsPerPage int
|
|
|
|
|
maxPages int
|
|
|
|
|
cacheMap sync.Map
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewCoinGecko new service
|
|
|
|
|
func NewCoinGecko() *Service {
|
|
|
|
|
client := gecko.NewClient(nil)
|
|
|
|
|
return &Service{
|
|
|
|
|
svc := &Service{
|
|
|
|
|
client: client,
|
|
|
|
|
maxResultsPerPage: 250,
|
|
|
|
|
maxPages: 10,
|
|
|
|
|
cacheMap: sync.Map{},
|
|
|
|
|
}
|
|
|
|
|
svc.cacheCoinsIDList()
|
|
|
|
|
return svc
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ping ping API
|
|
|
|
@ -45,88 +50,6 @@ func (s *Service) Ping() error {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Service) getPaginatedCoinData(convert string, offset int, names []string) ([]apitypes.Coin, error) {
|
|
|
|
|
var ret []apitypes.Coin
|
|
|
|
|
page := offset + 1 // page starts at 1
|
|
|
|
|
sparkline := false
|
|
|
|
|
pcp := geckoTypes.PriceChangePercentageObject
|
|
|
|
|
priceChangePercentage := []string{
|
|
|
|
|
pcp.PCP1h,
|
|
|
|
|
pcp.PCP24h,
|
|
|
|
|
pcp.PCP7d,
|
|
|
|
|
pcp.PCP30d,
|
|
|
|
|
}
|
|
|
|
|
order := geckoTypes.OrderTypeObject.MarketCapDesc
|
|
|
|
|
convertTo := strings.ToLower(convert)
|
|
|
|
|
if convertTo == "" {
|
|
|
|
|
convertTo = "usd"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ids := make([]string, len(names))
|
|
|
|
|
for i, name := range names {
|
|
|
|
|
slug := util.NameToSlug(name)
|
|
|
|
|
ids[i] = slug
|
|
|
|
|
}
|
|
|
|
|
list, err := s.client.CoinsMarket(convertTo, ids, order, s.maxResultsPerPage, page, sparkline, priceChangePercentage)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if list != nil {
|
|
|
|
|
// for fetching "simple prices"
|
|
|
|
|
currencies := make([]string, len(*list))
|
|
|
|
|
for i, item := range *list {
|
|
|
|
|
currencies[i] = item.Name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, item := range *list {
|
|
|
|
|
price := item.CurrentPrice
|
|
|
|
|
var percentChange1H float64
|
|
|
|
|
var percentChange24H float64
|
|
|
|
|
var percentChange7D float64
|
|
|
|
|
var percentChange30D float64
|
|
|
|
|
|
|
|
|
|
if item.PriceChangePercentage1hInCurrency != nil {
|
|
|
|
|
percentChange1H = *item.PriceChangePercentage1hInCurrency
|
|
|
|
|
}
|
|
|
|
|
if item.PriceChangePercentage24hInCurrency != nil {
|
|
|
|
|
percentChange24H = *item.PriceChangePercentage24hInCurrency
|
|
|
|
|
}
|
|
|
|
|
if item.PriceChangePercentage7dInCurrency != nil {
|
|
|
|
|
percentChange7D = *item.PriceChangePercentage7dInCurrency
|
|
|
|
|
}
|
|
|
|
|
if item.PriceChangePercentage30dInCurrency != nil {
|
|
|
|
|
percentChange30D = *item.PriceChangePercentage30dInCurrency
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
availableSupply := item.CirculatingSupply
|
|
|
|
|
totalSupply := item.TotalSupply
|
|
|
|
|
if totalSupply == 0 {
|
|
|
|
|
totalSupply = availableSupply
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ret = append(ret, apitypes.Coin{
|
|
|
|
|
ID: util.FormatID(item.ID),
|
|
|
|
|
Name: util.FormatName(item.Name),
|
|
|
|
|
Symbol: util.FormatSymbol(item.Symbol),
|
|
|
|
|
Rank: util.FormatRank(item.MarketCapRank),
|
|
|
|
|
AvailableSupply: util.FormatSupply(availableSupply),
|
|
|
|
|
TotalSupply: util.FormatSupply(totalSupply),
|
|
|
|
|
MarketCap: util.FormatMarketCap(item.MarketCap),
|
|
|
|
|
Price: util.FormatPrice(price, convert),
|
|
|
|
|
PercentChange1H: util.FormatPercentChange(percentChange1H),
|
|
|
|
|
PercentChange24H: util.FormatPercentChange(percentChange24H),
|
|
|
|
|
PercentChange7D: util.FormatPercentChange(percentChange7D),
|
|
|
|
|
PercentChange30D: util.FormatPercentChange(percentChange30D),
|
|
|
|
|
Volume24H: util.FormatVolume(item.TotalVolume),
|
|
|
|
|
LastUpdated: util.FormatLastUpdated(item.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() {
|
|
|
|
@ -173,7 +96,7 @@ func (s *Service) GetCoinDataBatch(names []string, convert string) ([]apitypes.C
|
|
|
|
|
func (s *Service) GetCoinGraphData(convert, symbol, name string, start, end int64) (apitypes.CoinGraph, error) {
|
|
|
|
|
ret := apitypes.CoinGraph{}
|
|
|
|
|
days := strconv.Itoa(util.CalcDays(start, end))
|
|
|
|
|
chart, err := s.client.CoinsIDMarketChart(util.NameToSlug(name), convert, days)
|
|
|
|
|
chart, err := s.client.CoinsIDMarketChart(s.coinNameToID(name), convert, days)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ret, err
|
|
|
|
|
}
|
|
|
|
@ -259,18 +182,7 @@ func (s *Service) GetGlobalMarketData(convert string) (apitypes.GlobalMarketData
|
|
|
|
|
|
|
|
|
|
// Price returns the current price of the coin
|
|
|
|
|
func (s *Service) Price(name string, convert string) (float64, error) {
|
|
|
|
|
list, err := s.client.CoinsList()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, item := range *list {
|
|
|
|
|
if item.Symbol == strings.ToLower(name) {
|
|
|
|
|
name = item.Name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ids := []string{util.NameToSlug(name)}
|
|
|
|
|
ids := []string{s.coinNameToID(name)}
|
|
|
|
|
convert = strings.ToLower(convert)
|
|
|
|
|
currencies := []string{convert}
|
|
|
|
|
priceList, err := s.client.SimplePrice(ids, currencies)
|
|
|
|
@ -289,8 +201,8 @@ func (s *Service) Price(name string, convert string) (float64, error) {
|
|
|
|
|
|
|
|
|
|
// CoinLink returns the URL link for the coin
|
|
|
|
|
func (s *Service) CoinLink(name string) string {
|
|
|
|
|
slug := util.NameToSlug(name)
|
|
|
|
|
return fmt.Sprintf("https://www.coingecko.com/en/coins/%s", slug)
|
|
|
|
|
ID := s.coinNameToID(name)
|
|
|
|
|
return fmt.Sprintf("https://www.coingecko.com/en/coins/%s", ID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SupportedCurrencies returns a list of supported currencies
|
|
|
|
@ -350,3 +262,134 @@ func (s *Service) SupportedCurrencies() []string {
|
|
|
|
|
"ZAR",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// cacheCoinsIDList fetches list of all coin IDS by name and symbols and caches it in a map for fast lookups
|
|
|
|
|
func (s *Service) cacheCoinsIDList() error {
|
|
|
|
|
list, err := s.client.CoinsList()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if list == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
firstWords := [][]string{}
|
|
|
|
|
for _, item := range *list {
|
|
|
|
|
keys := []string{
|
|
|
|
|
strings.ToLower(item.Name),
|
|
|
|
|
strings.ToLower(item.Symbol),
|
|
|
|
|
util.NameToSlug(item.Name),
|
|
|
|
|
}
|
|
|
|
|
parts := strings.Split(strings.ToLower(item.Name), " ")
|
|
|
|
|
if len(parts) > 1 {
|
|
|
|
|
if parts[1] == "coin" {
|
|
|
|
|
keys = append(keys, parts[0])
|
|
|
|
|
} else {
|
|
|
|
|
firstWords = append(firstWords, []string{parts[0], item.ID})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, key := range keys {
|
|
|
|
|
_, exists := s.cacheMap.Load(key)
|
|
|
|
|
if !exists {
|
|
|
|
|
s.cacheMap.Store(key, item.ID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, parts := range firstWords {
|
|
|
|
|
_, exists := s.cacheMap.Load(parts[0])
|
|
|
|
|
if !exists {
|
|
|
|
|
s.cacheMap.Store(parts[0], parts[1])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// coinNameToID attempts to get coin ID based on coin name or coin symbol
|
|
|
|
|
func (s *Service) coinNameToID(name string) string {
|
|
|
|
|
id, ok := s.cacheMap.Load(strings.ToLower(strings.TrimSpace(name)))
|
|
|
|
|
if ok {
|
|
|
|
|
return id.(string)
|
|
|
|
|
}
|
|
|
|
|
return util.NameToSlug(name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getPaginatedCoinData fetches coin data from page offset
|
|
|
|
|
func (s *Service) getPaginatedCoinData(convert string, offset int, names []string) ([]apitypes.Coin, error) {
|
|
|
|
|
var ret []apitypes.Coin
|
|
|
|
|
page := offset + 1 // page starts at 1
|
|
|
|
|
sparkline := false
|
|
|
|
|
pcp := geckoTypes.PriceChangePercentageObject
|
|
|
|
|
priceChangePercentage := []string{
|
|
|
|
|
pcp.PCP1h,
|
|
|
|
|
pcp.PCP24h,
|
|
|
|
|
pcp.PCP7d,
|
|
|
|
|
pcp.PCP30d,
|
|
|
|
|
}
|
|
|
|
|
order := geckoTypes.OrderTypeObject.MarketCapDesc
|
|
|
|
|
convertTo := strings.ToLower(convert)
|
|
|
|
|
if convertTo == "" {
|
|
|
|
|
convertTo = "usd"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ids := make([]string, len(names))
|
|
|
|
|
for i, name := range names {
|
|
|
|
|
ids[i] = s.coinNameToID(name)
|
|
|
|
|
}
|
|
|
|
|
list, err := s.client.CoinsMarket(convertTo, ids, order, s.maxResultsPerPage, page, sparkline, priceChangePercentage)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if list != nil {
|
|
|
|
|
// for fetching "simple prices"
|
|
|
|
|
currencies := make([]string, len(*list))
|
|
|
|
|
for i, item := range *list {
|
|
|
|
|
currencies[i] = item.Name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, item := range *list {
|
|
|
|
|
price := item.CurrentPrice
|
|
|
|
|
var percentChange1H float64
|
|
|
|
|
var percentChange24H float64
|
|
|
|
|
var percentChange7D float64
|
|
|
|
|
var percentChange30D float64
|
|
|
|
|
|
|
|
|
|
if item.PriceChangePercentage1hInCurrency != nil {
|
|
|
|
|
percentChange1H = *item.PriceChangePercentage1hInCurrency
|
|
|
|
|
}
|
|
|
|
|
if item.PriceChangePercentage24hInCurrency != nil {
|
|
|
|
|
percentChange24H = *item.PriceChangePercentage24hInCurrency
|
|
|
|
|
}
|
|
|
|
|
if item.PriceChangePercentage7dInCurrency != nil {
|
|
|
|
|
percentChange7D = *item.PriceChangePercentage7dInCurrency
|
|
|
|
|
}
|
|
|
|
|
if item.PriceChangePercentage30dInCurrency != nil {
|
|
|
|
|
percentChange30D = *item.PriceChangePercentage30dInCurrency
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
availableSupply := item.CirculatingSupply
|
|
|
|
|
totalSupply := item.TotalSupply
|
|
|
|
|
if totalSupply == 0 {
|
|
|
|
|
totalSupply = availableSupply
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ret = append(ret, apitypes.Coin{
|
|
|
|
|
ID: util.FormatID(item.ID),
|
|
|
|
|
Name: util.FormatName(item.Name),
|
|
|
|
|
Symbol: util.FormatSymbol(item.Symbol),
|
|
|
|
|
Rank: util.FormatRank(item.MarketCapRank),
|
|
|
|
|
AvailableSupply: util.FormatSupply(availableSupply),
|
|
|
|
|
TotalSupply: util.FormatSupply(totalSupply),
|
|
|
|
|
MarketCap: util.FormatMarketCap(item.MarketCap),
|
|
|
|
|
Price: util.FormatPrice(price, convert),
|
|
|
|
|
PercentChange1H: util.FormatPercentChange(percentChange1H),
|
|
|
|
|
PercentChange24H: util.FormatPercentChange(percentChange24H),
|
|
|
|
|
PercentChange7D: util.FormatPercentChange(percentChange7D),
|
|
|
|
|
PercentChange30D: util.FormatPercentChange(percentChange30D),
|
|
|
|
|
Volume24H: util.FormatVolume(item.TotalVolume),
|
|
|
|
|
LastUpdated: util.FormatLastUpdated(item.LastUpdated),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ret, nil
|
|
|
|
|
}
|
|
|
|
|