diff --git a/README.md b/README.md index ecd50dc..fefd3b2 100644 --- a/README.md +++ b/README.md @@ -517,6 +517,10 @@ Frequently asked questions: - A: Run `cointop --clean` to delete the cache and then rerun cointop. If you're still not seeing any data, then please [submit an issue](https://github.com/miguelmota/cointop/issues/new). +- Q: How do I add my CoinMarketCap Pro API Key? + + - A: Export the environment variable `CMC_PRO_API_KEY` containing the API key. + - Q: I installed cointop without errors but the command is not found. - A: Make sure your `GOPATH` and `PATH` is set correctly. diff --git a/cointop/common/api/impl/coinmarketcap/coinmarketcap.go b/cointop/common/api/impl/coinmarketcap/coinmarketcap.go index bd340a3..cbb7f40 100644 --- a/cointop/common/api/impl/coinmarketcap/coinmarketcap.go +++ b/cointop/common/api/impl/coinmarketcap/coinmarketcap.go @@ -3,183 +3,117 @@ package coinmarketcap import ( "errors" "fmt" + "os" "strconv" "strings" "sync" "time" apitypes "github.com/miguelmota/cointop/cointop/common/api/types" - cmc "github.com/miguelmota/cointop/cointop/common/cmc" + cmc "github.com/miguelmota/go-coinmarketcap/pro/v1" + cmcv2 "github.com/miguelmota/go-coinmarketcap/v2" ) // Service service type Service struct { + client *cmc.Client } // New new service func New() *Service { - return &Service{} + client := cmc.NewClient(&cmc.Config{ + ProAPIKey: os.Getenv("CMC_PRO_API_KEY"), + }) + return &Service{ + client: client, + } } // Ping ping API func (s *Service) Ping() error { - ticker, err := cmc.Ticker(&cmc.TickerOptions{ - Symbol: "ETH", + info, err := s.client.Cryptocurrency.Info(&cmc.InfoOptions{ + Symbol: "BTC", }) if err != nil { return errors.New("failed to ping") } - if ticker == nil { + if info == nil { return errors.New("failed to ping") } return nil } -func getLimitedCoinDataV2(convert string, offset int) (map[string]apitypes.Coin, error) { +func (s *Service) getLimitedCoinData(convert string, offset int) (map[string]apitypes.Coin, error) { ret := make(map[string]apitypes.Coin) max := 100 - coins, err := cmc.Tickers(&cmc.TickersOptions{ - Convert: convert, - Start: max * offset, - Limit: max, - }) - if err != nil { - return ret, err - } - for _, v := range coins { - ret[v.Name] = apitypes.Coin{ - ID: v.Slug, - Name: v.Name, - Symbol: v.Symbol, - Rank: v.Rank, - AvailableSupply: v.CirculatingSupply, - TotalSupply: v.TotalSupply, - MarketCap: v.Quotes[convert].MarketCap, - Price: v.Quotes[convert].Price, - PercentChange1H: v.Quotes[convert].PercentChange1H, - PercentChange24H: v.Quotes[convert].PercentChange24H, - PercentChange7D: v.Quotes[convert].PercentChange7D, - Volume24H: formatVolume(v.Quotes[convert].Volume24H), - LastUpdated: strconv.Itoa(v.LastUpdated), - } - } - return ret, nil -} -func getLimitedCoinData(convert string, offset int) (map[string]apitypes.Coin, error) { - ret := make(map[string]apitypes.Coin) - max := 100 - coins, err := cmc.Tickers(&cmc.TickersOptions{ + listings, err := s.client.Cryptocurrency.LatestListings(&cmc.ListingOptions{ + Limit: max, Convert: convert, Start: max * offset, - Limit: max, }) if err != nil { - return ret, err + return nil, err } - for _, v := range coins { - price := formatPrice(v.Quotes[convert].Price, convert) - ret[v.Name] = apitypes.Coin{ - ID: strings.ToLower(v.Name), - Name: v.Name, - Symbol: v.Symbol, - Rank: v.Rank, - AvailableSupply: v.CirculatingSupply, - TotalSupply: v.TotalSupply, - MarketCap: v.Quotes[convert].MarketCap, - Price: price, - PercentChange1H: v.Quotes[convert].PercentChange1H, - PercentChange24H: v.Quotes[convert].PercentChange24H, - PercentChange7D: v.Quotes[convert].PercentChange7D, - Volume24H: formatVolume(v.Quotes[convert].Volume24H), - LastUpdated: strconv.Itoa(v.LastUpdated), + for _, v := range listings { + price := formatPrice(v.Quote[convert].Price, convert) + lastUpdated, err := time.Parse(time.RFC3339, v.LastUpdated) + if err != nil { + return nil, err } - } - return ret, nil -} - -// GetAllCoinData1 get all coin data -func (s *Service) GetAllCoinData1(convert string) (map[string]apitypes.Coin, error) { - ret := make(map[string]apitypes.Coin) - coins, err := cmc.V1Tickers(0, convert) - if err != nil { - return ret, err - } - for _, v := range coins { - price := formatPrice(v.Quotes[convert].Price, convert) ret[v.Name] = apitypes.Coin{ ID: strings.ToLower(v.Name), Name: v.Name, Symbol: v.Symbol, - Rank: v.Rank, - AvailableSupply: v.AvailableSupply, + Rank: int(v.CMCRank), + AvailableSupply: v.CirculatingSupply, TotalSupply: v.TotalSupply, - MarketCap: v.Quotes[convert].MarketCap, + MarketCap: float64(int(v.Quote[convert].MarketCap)), Price: price, - PercentChange1H: v.PercentChange1H, - PercentChange24H: v.PercentChange24H, - PercentChange7D: v.PercentChange7D, - Volume24H: formatVolume(v.Quotes[convert].Volume24H), - LastUpdated: strconv.Itoa(v.LastUpdated), + PercentChange1H: v.Quote[convert].PercentChange1H, + PercentChange24H: v.Quote[convert].PercentChange24H, + PercentChange7D: v.Quote[convert].PercentChange7D, + Volume24H: formatVolume(v.Quote[convert].Volume24H), + LastUpdated: strconv.Itoa(int(lastUpdated.Unix())), } } return ret, nil } -// GetAllCoinDataV2 gets all coin data -// V1 is currently better for fetching all the coins at once -func (s *Service) GetAllCoinDataV2(convert string) (map[string]apitypes.Coin, error) { - var wg sync.WaitGroup - ret := make(map[string]apitypes.Coin) - var mutex sync.Mutex - for i := 0; i < 5; i++ { - wg.Add(1) - go func(j int) { - defer wg.Done() - coins, err := getLimitedCoinDataV2(convert, j) - if err != nil { - return - } - mutex.Lock() - for k, v := range coins { - ret[k] = v - } - mutex.Unlock() - }(i) - } - wg.Wait() - return ret, nil -} - // GetAllCoinData gets all coin data. Need to paginate through all pages -func (s *Service) GetAllCoinData(convert string) (map[string]apitypes.Coin, error) { +func (s *Service) GetAllCoinData(convert string) (chan map[string]apitypes.Coin, error) { var wg sync.WaitGroup - ret := make(map[string]apitypes.Coin) - var mutex sync.Mutex - for i := 0; i < 17; i++ { - time.Sleep(500 * time.Millisecond) - wg.Add(1) - go func(j int) { - defer wg.Done() - coins, err := getLimitedCoinData(convert, j) - if err != nil { - return - } - mutex.Lock() - for k, v := range coins { - ret[k] = v - } - mutex.Unlock() - }(i) - } - wg.Wait() - return ret, nil + ch := make(chan map[string]apitypes.Coin) + go func() { + var mutex sync.Mutex + maxPages := 15 + for i := 0; i < maxPages; i++ { + time.Sleep(time.Duration(i) * time.Second) + wg.Add(1) + go func(j int) { + defer wg.Done() + coins, err := s.getLimitedCoinData(convert, j) + if err != nil { + return + } + mutex.Lock() + defer mutex.Unlock() + ret := make(map[string]apitypes.Coin) + for k, v := range coins { + ret[k] = v + } + ch <- ret + }(i) + } + wg.Wait() + }() + return ch, nil } // GetCoinGraphData gets coin graph data func (s *Service) GetCoinGraphData(coin string, start int64, end int64) (apitypes.CoinGraph, error) { ret := apitypes.CoinGraph{} - graphData, err := cmc.TickerGraph(&cmc.TickerGraphOptions{ + graphData, err := cmcv2.TickerGraph(&cmcv2.TickerGraphOptions{ Symbol: coin, Start: start, End: end, @@ -198,7 +132,7 @@ func (s *Service) GetCoinGraphData(coin string, start int64, end int64) (apitype // GetGlobalMarketGraphData gets global market graph data func (s *Service) GetGlobalMarketGraphData(start int64, end int64) (apitypes.MarketGraph, error) { ret := apitypes.MarketGraph{} - graphData, err := cmc.GlobalMarketGraph(&cmc.GlobalMarketGraphOptions{ + graphData, err := cmcv2.GlobalMarketGraph(&cmcv2.GlobalMarketGraphOptions{ Start: start, End: end, }) @@ -214,19 +148,20 @@ func (s *Service) GetGlobalMarketGraphData(start int64, end int64) (apitypes.Mar // GetGlobalMarketData gets global market data func (s *Service) GetGlobalMarketData(convert string) (apitypes.GlobalMarketData, error) { ret := apitypes.GlobalMarketData{} - market, err := cmc.GlobalMarket(&cmc.GlobalMarketOptions{ + market, err := s.client.GlobalMetrics.LatestQuotes(&cmc.QuoteOptions{ Convert: convert, }) + if err != nil { return ret, err } ret = apitypes.GlobalMarketData{ - TotalMarketCapUSD: market.Quotes[convert].TotalMarketCap, - Total24HVolumeUSD: market.Quotes[convert].TotalVolume24H, - BitcoinPercentageOfMarketCap: market.BitcoinPercentageOfMarketCap, - ActiveCurrencies: market.ActiveCurrencies, + TotalMarketCapUSD: market.Quote[convert].TotalMarketCap, + Total24HVolumeUSD: market.Quote[convert].TotalVolume24H, + BitcoinPercentageOfMarketCap: market.BTCDominance, + ActiveCurrencies: int(market.ActiveCryptocurrencies), ActiveAssets: 0, - ActiveMarkets: market.ActiveMarkets, + ActiveMarkets: int(market.ActiveMarketPairs), } return ret, nil } diff --git a/cointop/common/api/interface.go b/cointop/common/api/interface.go index e108cf5..2a4a6c7 100644 --- a/cointop/common/api/interface.go +++ b/cointop/common/api/interface.go @@ -7,7 +7,7 @@ import ( // Interface interface type Interface interface { Ping() error - GetAllCoinData(convert string) (map[string]types.Coin, error) + GetAllCoinData(convert string) (chan map[string]types.Coin, error) GetCoinGraphData(coin string, start int64, end int64) (types.CoinGraph, error) GetGlobalMarketGraphData(start int64, end int64) (types.MarketGraph, error) GetGlobalMarketData(convert string) (types.GlobalMarketData, error) diff --git a/cointop/common/cmc/v1.go b/cointop/common/cmc/v1.go deleted file mode 100644 index b1837af..0000000 --- a/cointop/common/cmc/v1.go +++ /dev/null @@ -1,63 +0,0 @@ -package coinmarketcap - -import ( - "encoding/json" - "fmt" - "strconv" - "strings" - - "github.com/miguelmota/cointop/cointop/common/cmc/types" -) - -// V1Tickers get information about all coins listed in Coin Market Cap -func V1Tickers(limit int, convert string) (map[string]*types.V1Ticker, error) { - var params []string - if limit >= 0 { - params = append(params, fmt.Sprintf("limit=%v", limit)) - } - if convert != "" { - params = append(params, fmt.Sprintf("convert=%v", convert)) - } - baseURL := "https://api.coinmarketcap.com/v1" - url := fmt.Sprintf("%s/ticker?%s", baseURL, strings.Join(params, "&")) - - resp, err := makeReq(url) - var data []*types.V1Ticker - err = json.Unmarshal(resp, &data) - if err != nil { - return nil, err - } - var mapstring []map[string]interface{} - err = json.Unmarshal(resp, &mapstring) - if err != nil { - return nil, err - } - - // creating map from the array - allCoins := make(map[string]*types.V1Ticker) - for i := 0; i < len(data); i++ { - allCoins[data[i].ID] = data[i] - } - - for _, item := range mapstring { - id, _ := item["id"].(string) - priceifc := item[fmt.Sprintf("price_%s", strings.ToLower(convert))] - pricestr, _ := priceifc.(string) - price, _ := strconv.ParseFloat(pricestr, 64) - marketcapifc := item[fmt.Sprintf("market_cap_%s", strings.ToLower(convert))] - marketcapstr, _ := marketcapifc.(string) - marketcap, _ := strconv.ParseFloat(marketcapstr, 64) - volumeifc := item[fmt.Sprintf("24h_volume_%s", strings.ToLower(convert))] - volumestr, _ := volumeifc.(string) - volume, _ := strconv.ParseFloat(volumestr, 64) - quotes := &types.TickerQuote{ - Price: price, - Volume24H: volume, - MarketCap: marketcap, - } - allCoins[id].Quotes = map[string]*types.TickerQuote{} - allCoins[id].Quotes[strings.ToUpper(convert)] = quotes - } - - return allCoins, nil -} diff --git a/cointop/common/cmc/v1_test.go b/cointop/common/cmc/v1_test.go deleted file mode 100644 index 08bc4a2..0000000 --- a/cointop/common/cmc/v1_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package coinmarketcap - -import "testing" - -func TestV1Tickers(t *testing.T) { - coins, err := V1Tickers(10, "EUR") - if err != nil { - t.FailNow() - } - - if len(coins) != 10 { - t.FailNow() - } -} diff --git a/cointop/list.go b/cointop/list.go index 345a2e1..4a99e4b 100644 --- a/cointop/list.go +++ b/cointop/list.go @@ -2,10 +2,8 @@ package cointop import ( "sync" - "time" types "github.com/miguelmota/cointop/cointop/common/api/types" - "github.com/miguelmota/cointop/cointop/common/filecache" ) var coinslock sync.Mutex @@ -13,10 +11,8 @@ var coinslock sync.Mutex func (ct *Cointop) updateCoins() error { coinslock.Lock() defer coinslock.Unlock() - list := []*coin{} cachekey := "allcoinsslugmap" - var err error var allcoinsslugmap map[string]types.Coin cached, found := ct.cache.Get(cachekey) if found { @@ -28,16 +24,34 @@ func (ct *Cointop) updateCoins() error { // cache miss if allcoinsslugmap == nil { ct.debuglog("cache miss") - allcoinsslugmap, err = ct.api.GetAllCoinData(ct.currencyconversion) + ch, err := ct.api.GetAllCoinData(ct.currencyconversion) if err != nil { return err } - ct.cache.Set(cachekey, allcoinsslugmap, 10*time.Second) - go func() { - filecache.Set(cachekey, allcoinsslugmap, 24*time.Hour) - }() + + for { + coins, ok := <-ch + if !ok { + break + } + ct.updateCoinsMap(coins, true) + ct.updateTable() + } + + /* + ct.cache.Set(cachekey, allcoinsslugmap, 10*time.Second) + go func() { + filecache.Set(cachekey, allcoinsslugmap, 24*time.Hour) + }() + */ + } else { + ct.updateCoinsMap(allcoinsslugmap, false) } + return nil +} + +func (ct *Cointop) updateCoinsMap(allcoinsslugmap map[string]types.Coin, b bool) { if len(ct.allcoinsslugmap) == 0 { ct.allcoinsslugmap = map[string]*coin{} } @@ -61,15 +75,17 @@ func (ct *Cointop) updateCoins() error { if last != nil { ct.allcoinsslugmap[k].Favorite = last.Favorite } - } - if len(ct.allcoins) == 0 { - for i := range ct.allcoinsslugmap { - coin := ct.allcoinsslugmap[i] - list = append(list, coin) + + if b { + ct.allcoins = append(ct.allcoins, ct.allcoinsslugmap[k]) } - ct.allcoins = list - ct.sort(ct.sortby, ct.sortdesc, ct.allcoins) - } else { + } + + //if len(ct.allcoins) == 0 { + if b { + //ct.sort(ct.sortby, ct.sortdesc, ct.allcoins) + } + if !b { // update list in place without changing order for i := range ct.allcoinsslugmap { cm := ct.allcoinsslugmap[i] @@ -95,5 +111,4 @@ func (ct *Cointop) updateCoins() error { } } } - return nil } diff --git a/cointop/marketbar.go b/cointop/marketbar.go index ef8a0ec..dd4168f 100644 --- a/cointop/marketbar.go +++ b/cointop/marketbar.go @@ -67,6 +67,7 @@ func (ct *Cointop) updateMarketbar() error { var err error cachekey := "market" cached, found := ct.cache.Get(cachekey) + if found { // cache hit var ok bool @@ -93,14 +94,12 @@ func (ct *Cointop) updateMarketbar() error { } content = fmt.Sprintf( - "[ Chart: %s %s ] Global ▶ Market Cap: $%s • 24H Volume: $%s • BTC Dominance: %.2f%% • Active Currencies: %s • Active Markets: %s", + "[ Chart: %s %s ] Global ▶ Market Cap: %s • 24H Volume: %s • BTC Dominance: %.2f%%", color.Cyan(chartname), timeframe, - humanize.Commaf(market.TotalMarketCapUSD), - humanize.Commaf(market.Total24HVolumeUSD), + fmt.Sprintf("%s%s", ct.currencySymbol(), humanize.Commaf(market.TotalMarketCapUSD)), + fmt.Sprintf("%s%s", ct.currencySymbol(), humanize.Commaf(market.Total24HVolumeUSD)), market.BitcoinPercentageOfMarketCap, - humanize.Commaf(float64(market.ActiveCurrencies)), - humanize.Commaf(float64(market.ActiveMarkets)), ) } diff --git a/cointop/sort.go b/cointop/sort.go index f0f2395..ac0e9bb 100644 --- a/cointop/sort.go +++ b/cointop/sort.go @@ -15,6 +15,12 @@ func (ct *Cointop) sort(sortby string, desc bool, list []*coin) { } a := list[i] b := list[j] + if a == nil { + return true + } + if b == nil { + return false + } switch sortby { case "rank": return a.Rank < b.Rank diff --git a/cointop/table.go b/cointop/table.go index f786af1..faa1de0 100644 --- a/cointop/table.go +++ b/cointop/table.go @@ -83,6 +83,9 @@ func (ct *Cointop) refreshTable() error { ct.table.AddCol("") ct.table.AddCol("") for _, coin := range ct.coins { + if coin == nil { + continue + } unix, _ := strconv.ParseInt(coin.LastUpdated, 10, 64) lastUpdated := time.Unix(unix, 0).Format("15:04:05 Jan 02") namecolor := color.White diff --git a/go.mod b/go.mod index 7810cda..f29cdd8 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/miguelmota/cointop require ( github.com/BurntSushi/toml v0.3.1 - github.com/anaskhan96/soup v1.1.1 + github.com/anaskhan96/soup v1.1.1 // indirect github.com/fatih/color v1.7.0 github.com/gizak/termui v2.3.0+incompatible github.com/jroimartin/gocui v0.4.0 @@ -11,9 +11,12 @@ require ( github.com/mattn/go-isatty v0.0.6 // indirect github.com/mattn/go-runewidth v0.0.4 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/miguelmota/go-coinmarketcap v0.1.3 github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect github.com/patrickmn/go-cache v2.1.0+incompatible - golang.org/x/net v0.0.0-20181220203305-927f97764cc3 // indirect + golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a // indirect + golang.org/x/net v0.0.0-20190415214537-1da14a5a36f2 // indirect + golang.org/x/sys v0.0.0-20190416152802-12500544f89f // indirect golang.org/x/text v0.3.0 ) diff --git a/go.sum b/go.sum index 77e5665..39fc880 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,10 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anaskhan96/soup v1.0.1/go.mod h1:pT5vs4HXDwA5y4KQCsKvnkpQd3D+joP7IqpiGskfWW0= github.com/anaskhan96/soup v1.1.1 h1:Duux/0htS2Va7XLJ9qIakCSey790hg9OFRm2FwlMTy0= github.com/anaskhan96/soup v1.1.1/go.mod h1:pT5vs4HXDwA5y4KQCsKvnkpQd3D+joP7IqpiGskfWW0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/gizak/termui v2.3.0+incompatible h1:S8wJoNumYfc/rR5UezUM4HsPEo3RJh0LKdiuDWQpjqw= @@ -21,16 +24,31 @@ github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/ github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/miguelmota/go-coinmarketcap v0.1.1 h1:4meaHf411shSIUZDqzaVNU+sgjTa7O9F8KjJqhsP1AI= +github.com/miguelmota/go-coinmarketcap v0.1.1/go.mod h1:Jdv/kqtKclIElmoNAZMMJn0DSQv+j7p/H1te/GGnxhA= +github.com/miguelmota/go-coinmarketcap v0.1.2 h1:XGhLhzruXD14sVS3kuXAtAinvwJK3m1apRase/vfr88= +github.com/miguelmota/go-coinmarketcap v0.1.2/go.mod h1:Jdv/kqtKclIElmoNAZMMJn0DSQv+j7p/H1te/GGnxhA= +github.com/miguelmota/go-coinmarketcap v0.1.3 h1:6/TvCnvq6tNVa8NG33X5uiIfIHI55mRmmArnUQ7Hdeg= +github.com/miguelmota/go-coinmarketcap v0.1.3/go.mod h1:Jdv/kqtKclIElmoNAZMMJn0DSQv+j7p/H1te/GGnxhA= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840= github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/net v0.0.0-20180215212450-dc948dff8834/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190415214537-1da14a5a36f2 h1:iC0Y6EDq+rhnAePxGvJs2kzUAYcwESqdcGRPzEUfzTU= +golang.org/x/net v0.0.0-20190415214537-1da14a5a36f2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190416152802-12500544f89f h1:1ZH9RnjNgLzh6YrsRp/c6ddZ8Lq0fq9xztNOoWJ2sz4= +golang.org/x/sys v0.0.0-20190416152802-12500544f89f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/vendor/github.com/miguelmota/go-coinmarketcap/LICENSE.md b/vendor/github.com/miguelmota/go-coinmarketcap/LICENSE.md new file mode 100644 index 0000000..eb0736f --- /dev/null +++ b/vendor/github.com/miguelmota/go-coinmarketcap/LICENSE.md @@ -0,0 +1,21 @@ +MIT license + +Copyright (C) 2015 Miguel Mota + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/miguelmota/go-coinmarketcap/pro/v1/coinmarketcap.go b/vendor/github.com/miguelmota/go-coinmarketcap/pro/v1/coinmarketcap.go new file mode 100644 index 0000000..8b53181 --- /dev/null +++ b/vendor/github.com/miguelmota/go-coinmarketcap/pro/v1/coinmarketcap.go @@ -0,0 +1,753 @@ +// Package coinmarketcap Coin Market Cap API client for Go +package coinmarketcap + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "os" + "strings" +) + +// Client the CoinMarketCap client +type Client struct { + proAPIKey string + Cryptocurrency *CryptocurrencyService + Exchange *ExchangeService + GlobalMetrics *GlobalMetricsService + Tools *ToolsService + common service +} + +// Config the client config structure +type Config struct { + ProAPIKey string +} + +// CryptocurrencyService ... +type CryptocurrencyService service + +// ExchangeService ... +type ExchangeService service + +// GlobalMetricsService ... +type GlobalMetricsService service + +// ToolsService ... +type ToolsService service + +// Status is the status structure +type Status struct { + Timestamp string `json:"timestamp"` + ErrorCode int `json:"error_code"` + ErrorMessage *string `json:"error_message"` + Elapsed int `json:"elapsed"` + CreditCount int `json:"credit_count"` +} + +// Response is the response structure +type Response struct { + Status Status `json:"status"` + Data interface{} `json:"data"` +} + +// Listing is the listing structure +type Listing struct { + ID float64 `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Slug string `json:"slug"` + CirculatingSupply float64 `json:"circulating_supply"` + TotalSupply float64 `json:"total_supply"` + MaxSupply float64 `json:"max_supply"` + DateAdded string `json:"date_added"` + NumMarketPairs float64 `json:"num_market_pairs"` + CMCRank float64 `json:"cmc_rank"` + LastUpdated string `json:"last_updated"` + Quote map[string]*Quote `json:"quote"` +} + +// MapListing is the structure of a map listing +type MapListing struct { + ID float64 `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Slug string `json:"slug"` + IsActive int `json:"is_active"` + FirstHistoricalData string `json:"first_historical_data"` + LastHistoricalData string `json:"last_historical_data"` + Platform *string +} + +// ConvertListing is the converted listing structure +type ConvertListing struct { + ID string `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Amount float64 `json:"amount"` + LastUpdated string `json:"last_updated"` + Quote map[string]*ConvertQuote `json:"quote"` +} + +// ConvertQuote is the converted listing structure +type ConvertQuote struct { + Price float64 `json:"price"` + LastUpdated string `json:"last_updated"` +} + +// QuoteLatest is the quotes structure +type QuoteLatest struct { + ID float64 `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Slug string `json:"slug"` + CirculatingSupply float64 `json:"circulating_supply"` + TotalSupply float64 `json:"total_supply"` + MaxSupply float64 `json:"max_supply"` + DateAdded string `json:"date_added"` + NumMarketPairs float64 `json:"num_market_pairs"` + CMCRank float64 `json:"cmc_rank"` + LastUpdated string `json:"last_updated"` + Quote map[string]*Quote `json:"quote"` +} + +// Quote is the quote structure +type Quote struct { + Price float64 `json:"price"` + Volume24H float64 `json:"volume_24h"` + PercentChange1H float64 `json:"percent_change_1h"` + PercentChange24H float64 `json:"percent_change_24h"` + PercentChange7D float64 `json:"percent_change_7d"` + MarketCap float64 `json:"market_cap"` + LastUpdated string `json:"last_updated"` +} + +// MarketMetrics is the market metrics structure +type MarketMetrics struct { + BTCDominance float64 `json:"btc_dominance"` + ETHDominance float64 `json:"eth_dominance"` + ActiveCryptocurrencies float64 `json:"active_cryptocurrencies"` + ActiveMarketPairs float64 `json:"active_market_pairs"` + ActiveExchanges float64 `json:"active_exchanges"` + LastUpdated string `json:"last_updated"` + Quote map[string]*MarketMetricsQuote `json:"quote"` +} + +// MarketMetricsQuote is the quote structure +type MarketMetricsQuote struct { + TotalMarketCap float64 `json:"total_market_cap"` + TotalVolume24H float64 `json:"total_volume_24h"` + LastUpdated string `json:"last_updated"` +} + +// CryptocurrencyInfo options +type CryptocurrencyInfo struct { + ID float64 `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + Category string `json:"category"` + Slug string `json:"slug"` + Logo string `json:"logo"` + Tags []string `json:"tags"` + Urls map[string]interface{} `json:"urls"` +} + +// InfoOptions options +type InfoOptions struct { + ID string + Symbol string +} + +// ListingOptions options +type ListingOptions struct { + Start int + Limit int + Convert string + Sort string +} + +// MapOptions options +type MapOptions struct { + ListingStatus string + Start int + Limit int + Symbol string +} + +// QuoteOptions options +type QuoteOptions struct { + // Covert suppots multiple currencies command separated. eg. "BRL,USD" + Convert string + // Symbols suppots multiple tickers command separated. eg. "BTC,ETH,XRP" + Symbol string +} + +// ConvertOptions options +type ConvertOptions struct { + Amount float64 + ID string + Symbol string + Time int + Convert string +} + +// MarketPairOptions options +type MarketPairOptions struct { + ID int + Symbol string + Start int + Limit int + Convert string +} + +// service is abstraction for individual endpoint resources +type service struct { + client *Client +} + +// SortOptions sort options +var SortOptions sortOptions + +type sortOptions struct { + Name string + Symbol string + DateAdded string + MarketCap string + Price string + CirculatingSupply string + TotalSupply string + MaxSupply string + NumMarketPairs string + Volume24H string + PercentChange1H string + PercentChange24H string + PercentChange7D string +} + +var ( + // ErrTypeAssertion is type assertion error + ErrTypeAssertion = errors.New("type assertion error") +) + +var ( + siteURL = "https://coinmarketcap.com" + baseURL = "https://pro-api.coinmarketcap.com/v1" + coinGraphURL = "https://graphs2.coinmarketcap.com/currencies" + globalMarketGraphURL = "https://graphs2.coinmarketcap.com/global/marketcap-total" + altcoinMarketGraphURL = "https://graphs2.coinmarketcap.com/global/marketcap-altcoin" +) + +// NewClient initializes a new client +func NewClient(cfg *Config) *Client { + if cfg == nil { + cfg = new(Config) + } + + if cfg.ProAPIKey == "" { + cfg.ProAPIKey = os.Getenv("CMC_PRO_API_KEY") + } + + if cfg.ProAPIKey == "" { + log.Fatal("Pro API Key is required") + } + + c := &Client{ + proAPIKey: cfg.ProAPIKey, + } + + c.common.client = c + c.Cryptocurrency = (*CryptocurrencyService)(&c.common) + c.Exchange = (*ExchangeService)(&c.common) + c.GlobalMetrics = (*GlobalMetricsService)(&c.common) + c.Tools = (*ToolsService)(&c.common) + + return c +} + +// Info returns all static metadata for one or more cryptocurrencies including name, symbol, logo, and its various registered URLs. +func (s *CryptocurrencyService) Info(options *InfoOptions) (map[string]*CryptocurrencyInfo, error) { + var params []string + if options == nil { + options = new(InfoOptions) + } + if options.ID != "" { + params = append(params, fmt.Sprintf("id=%s", options.ID)) + } + if options.Symbol != "" { + params = append(params, fmt.Sprintf("symbol=%s", options.Symbol)) + } + + url := fmt.Sprintf("%s/cryptocurrency/info?%s", baseURL, strings.Join(params, "&")) + + body, err := s.client.makeReq(url) + resp := new(Response) + err = json.Unmarshal(body, &resp) + if err != nil { + return nil, err + } + + var result = make(map[string]*CryptocurrencyInfo) + ifcs, ok := resp.Data.(map[string]interface{}) + if !ok { + return nil, ErrTypeAssertion + } + + for k, v := range ifcs { + info := new(CryptocurrencyInfo) + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + err = json.Unmarshal(b, info) + if err != nil { + return nil, err + } + result[k] = info + } + + return result, nil +} + +// LatestListings gets a paginated list of all cryptocurrencies with latest market data. You can configure this call to sort by market cap or another market ranking field. Use the "convert" option to return market values in multiple fiat and cryptocurrency conversions in the same call. +func (s *CryptocurrencyService) LatestListings(options *ListingOptions) ([]*Listing, error) { + var params []string + if options == nil { + options = new(ListingOptions) + } + if options.Start != 0 { + params = append(params, fmt.Sprintf("start=%v", options.Start)) + } + if options.Limit != 0 { + params = append(params, fmt.Sprintf("limit=%v", options.Limit)) + } + if options.Convert != "" { + params = append(params, fmt.Sprintf("convert=%s", options.Convert)) + } + if options.Sort != "" { + params = append(params, fmt.Sprintf("sort=%s", options.Sort)) + } + + url := fmt.Sprintf("%s/cryptocurrency/listings/latest?%s", baseURL, strings.Join(params, "&")) + + body, err := s.client.makeReq(url) + resp := new(Response) + err = json.Unmarshal(body, &resp) + if err != nil { + return nil, fmt.Errorf("JSON Error: [%s]. Response body: [%s]", err.Error(), string(body)) + } + + var listings []*Listing + ifcs, ok := resp.Data.([]interface{}) + if !ok { + return nil, ErrTypeAssertion + } + + for i := range ifcs { + ifc := ifcs[i] + listing := new(Listing) + b, err := json.Marshal(ifc) + if err != nil { + return nil, err + } + err = json.Unmarshal(b, listing) + if err != nil { + return nil, err + } + listings = append(listings, listing) + } + + return listings, nil +} + +// Map returns a paginated list of all cryptocurrencies by CoinMarketCap ID. +func (s *CryptocurrencyService) Map(options *MapOptions) ([]*MapListing, error) { + var params []string + if options == nil { + options = new(MapOptions) + } + + if options.ListingStatus != "" { + params = append(params, fmt.Sprintf("listing_status=%s", options.ListingStatus)) + } + + if options.Start != 0 { + params = append(params, fmt.Sprintf("start=%d", options.Start)) + } + + if options.Limit != 0 { + params = append(params, fmt.Sprintf("limit=%d", options.Limit)) + } + + if options.Symbol != "" { + params = append(params, fmt.Sprintf("symbol=%s", options.Symbol)) + } + + url := fmt.Sprintf("%s/cryptocurrency/map?%s", baseURL, strings.Join(params, "&")) + + body, err := s.client.makeReq(url) + resp := new(Response) + err = json.Unmarshal(body, &resp) + if err != nil { + return nil, fmt.Errorf("JSON Error: [%s]. Response body: [%s]", err.Error(), string(body)) + } + + var result []*MapListing + ifcs, ok := resp.Data.(interface{}) + if !ok { + return nil, ErrTypeAssertion + } + + for _, item := range ifcs.([]interface{}) { + value := new(MapListing) + b, err := json.Marshal(item) + if err != nil { + return nil, err + } + + err = json.Unmarshal(b, value) + if err != nil { + return nil, err + } + + result = append(result, value) + } + + return result, nil +} + +// Exchange ... +type Exchange struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` +} + +// MarketPairBase ... +type MarketPairBase struct { + CurrencyID int `json:"currency_id"` + CurrencySymbol string `json:"currency_symbol"` + CurrencyType string `json:"currency_type"` +} + +// MarketPairQuote ... +type MarketPairQuote struct { + CurrencyID int `json:"currency_id"` + CurrencySymbol string `json:"currency_symbol"` + CurrencyType string `json:"currency_type"` +} + +// ExchangeQuote ... +type ExchangeQuote struct { + Price float64 `json:"price"` + Volume24 float64 `json:"volume_24h"` + Volume24HBase float64 `json:"volume_24h_base"` // for 'exchange_reported' + Volume24HQuote float64 `json:"volume_24h_quote"` // for 'exchange_reported' + LastUpdated string `json:"last_updated"` +} + +// ExchangeQuotes ... +type ExchangeQuotes map[string]*ExchangeQuote + +// ExchangeReported ... +type ExchangeReported struct { + Price float64 `json:"price"` + Volume24HBase float64 `json:"volume_24h_base"` + Volume24HQuote float64 `json:"volume_24h_quote"` + LastUpdated string `json:"last_updated"` +} + +// MarketPairs ... +type MarketPairs struct { + ID int `json:"id"` + Name string `json:"name"` + Symbol string `json:"symbol"` + NumMarketPairs int `json:"num_market_pairs"` + MarketPairs []*MarketPair `json:"market_pairs"` +} + +// MarketPair ... +type MarketPair struct { + Exchange *Exchange + MarketPair string `json:"market_pair"` + MarketPairBase *MarketPairBase `json:"market_pair_base"` + MarketPairQuote *MarketPairQuote `json:"market_pair_quote"` + Quote ExchangeQuotes `json:"quote"` + ExchangeReported *ExchangeReported `json:"exchange_reported"` +} + +// LatestMarketPairs Lists all market pairs across all exchanges for the specified cryptocurrency with associated stats. Use the "convert" option to return market values in multiple fiat and cryptocurrency conversions in the same call. +func (s *CryptocurrencyService) LatestMarketPairs(options *MarketPairOptions) (*MarketPairs, error) { + var params []string + if options == nil { + options = new(MarketPairOptions) + } + + if options.ID != 0 { + params = append(params, fmt.Sprintf("id=%v", options.ID)) + } + + if options.Symbol != "" { + params = append(params, fmt.Sprintf("symbol=%s", options.Symbol)) + } + + if options.Start != 0 { + params = append(params, fmt.Sprintf("start=%v", options.Start)) + } + + if options.Limit != 0 { + params = append(params, fmt.Sprintf("limit=%v", options.Limit)) + } + + if options.Convert != "" { + params = append(params, fmt.Sprintf("convert=%s", options.Convert)) + } + + url := fmt.Sprintf("%s/cryptocurrency/market-pairs/latest?%s", baseURL, strings.Join(params, "&")) + + body, err := s.client.makeReq(url) + resp := new(Response) + + err = json.Unmarshal(body, &resp) + if err != nil { + return nil, fmt.Errorf("JSON Error: [%s]. Response body: [%s]", err.Error(), string(body)) + } + + data, ok := resp.Data.(interface{}) + if !ok { + return nil, ErrTypeAssertion + } + + marketPairs := new(MarketPairs) + b, err := json.Marshal(data) + if err != nil { + return nil, err + } + + err = json.Unmarshal(b, marketPairs) + if err != nil { + return nil, err + } + + for _, pair := range marketPairs.MarketPairs { + reported, ok := pair.Quote["exchange_reported"] + if ok { + pair.ExchangeReported = &ExchangeReported{ + Price: reported.Price, + Volume24HBase: reported.Volume24HBase, + Volume24HQuote: reported.Volume24HQuote, + LastUpdated: reported.LastUpdated, + } + + delete(pair.Quote, "exchange_reported") + } + } + + return marketPairs, nil +} + +// HistoricalOHLCV NOT IMPLEMENTED +func (s *CryptocurrencyService) HistoricalOHLCV() error { + return nil +} + +// LatestQuotes gets latest quote for each specified symbol. Use the "convert" option to return market values in multiple fiat and cryptocurrency conversions in the same call. +func (s *CryptocurrencyService) LatestQuotes(options *QuoteOptions) ([]*QuoteLatest, error) { + var params []string + if options == nil { + options = new(QuoteOptions) + } + + if options.Symbol != "" { + params = append(params, fmt.Sprintf("symbol=%s", options.Symbol)) + } + + if options.Convert != "" { + params = append(params, fmt.Sprintf("convert=%s", options.Convert)) + } + + url := fmt.Sprintf("%s/cryptocurrency/quotes/latest?%s", baseURL, strings.Join(params, "&")) + + body, err := s.client.makeReq(url) + resp := new(Response) + err = json.Unmarshal(body, &resp) + if err != nil { + return nil, fmt.Errorf("JSON Error: [%s]. Response body: [%s]", err.Error(), string(body)) + } + + var quotesLatest []*QuoteLatest + ifcs, ok := resp.Data.(interface{}) + if !ok { + return nil, ErrTypeAssertion + } + + for _, coinObj := range ifcs.(map[string]interface{}) { + quoteLatest := new(QuoteLatest) + b, err := json.Marshal(coinObj) + if err != nil { + return nil, err + } + + err = json.Unmarshal(b, quoteLatest) + if err != nil { + return nil, err + } + + quotesLatest = append(quotesLatest, quoteLatest) + } + return quotesLatest, nil +} + +// HistoricalQuotes NOT IMPLEMENTED +func (s *CryptocurrencyService) HistoricalQuotes() error { + return nil +} + +// Info NOT IMPLEMENTED +func (s *ExchangeService) Info() error { + return nil +} + +// Map NOT IMPLEMENTED +func (s *ExchangeService) Map() error { + return nil +} + +// LatestListings NOT IMPLEMENTED +func (s *ExchangeService) LatestListings() error { + return nil +} + +// LatestMarketPairs NOT IMPLEMENTED +func (s *ExchangeService) LatestMarketPairs() error { + return nil +} + +// LatestQuotes NOT IMPLEMENTED +func (s *ExchangeService) LatestQuotes() error { + return nil +} + +// HistoricalQuotes NOT IMPLEMENTED +func (s *ExchangeService) HistoricalQuotes() error { + return nil +} + +// LatestQuotes Get the latest quote of aggregate market metrics. Use the "convert" option to return market values in multiple fiat and cryptocurrency conversions in the same call. +func (s *GlobalMetricsService) LatestQuotes(options *QuoteOptions) (*MarketMetrics, error) { + var params []string + if options == nil { + options = new(QuoteOptions) + } + + if options.Convert != "" { + params = append(params, fmt.Sprintf("convert=%s", options.Convert)) + } + + url := fmt.Sprintf("%s/global-metrics/quotes/latest?%s", baseURL, strings.Join(params, "&")) + + body, err := s.client.makeReq(url) + resp := new(Response) + + err = json.Unmarshal(body, &resp) + if err != nil { + return nil, fmt.Errorf("JSON Error: [%s]. Response body: [%s]", err.Error(), string(body)) + } + + data, ok := resp.Data.(interface{}) + if !ok { + return nil, ErrTypeAssertion + } + + marketMetrics := new(MarketMetrics) + b, err := json.Marshal(data) + if err != nil { + return nil, err + } + + err = json.Unmarshal(b, marketMetrics) + if err != nil { + return nil, err + } + + return marketMetrics, nil +} + +// HistoricalQuotes NOT IMPLEMENTED +func (s *GlobalMetricsService) HistoricalQuotes() error { + return nil +} + +// PriceConversion Convert an amount of one currency into multiple cryptocurrencies or fiat currencies at the same time using the latest market averages. Optionally pass a historical timestamp to convert values based on historic averages. +func (s *ToolsService) PriceConversion(options *ConvertOptions) (*ConvertListing, error) { + var params []string + if options == nil { + options = new(ConvertOptions) + } + + if options.Amount != 0 { + params = append(params, fmt.Sprintf("amount=%f", options.Amount)) + } + + if options.ID != "" { + params = append(params, fmt.Sprintf("id=%s", options.ID)) + } + + if options.Symbol != "" { + params = append(params, fmt.Sprintf("symbol=%s", options.Symbol)) + } + + if options.Time != 0 { + params = append(params, fmt.Sprintf("time=%d", options.Time)) + } + + if options.Convert != "" { + params = append(params, fmt.Sprintf("convert=%s", options.Convert)) + } + + url := fmt.Sprintf("%s/tools/price-conversion?%s", baseURL, strings.Join(params, "&")) + + body, err := s.client.makeReq(url) + + resp := new(Response) + err = json.Unmarshal(body, &resp) + if err != nil { + return nil, fmt.Errorf("JSON Error: [%s]. Response body: [%s]", err.Error(), string(body)) + } + + ifc, ok := resp.Data.(interface{}) + if !ok { + return nil, ErrTypeAssertion + } + + listing := new(ConvertListing) + b, err := json.Marshal(ifc) + if err != nil { + return nil, err + } + + err = json.Unmarshal(b, listing) + if err != nil { + return nil, err + } + + return listing, nil +} + +func init() { + SortOptions = sortOptions{ + Name: "name", + Symbol: "symbol", + DateAdded: "date_added", + MarketCap: "market_cap", + Price: "price", + CirculatingSupply: "circulating_supply", + TotalSupply: "total_supply", + MaxSupply: "max_supply", + NumMarketPairs: "num_market_pairs", + Volume24H: "volume_24h", + PercentChange1H: "percent_change_1h", + PercentChange24H: "percent_change_24h", + PercentChange7D: "percent_change_7d", + } +} diff --git a/vendor/github.com/miguelmota/go-coinmarketcap/pro/v1/util.go b/vendor/github.com/miguelmota/go-coinmarketcap/pro/v1/util.go new file mode 100644 index 0000000..9df1752 --- /dev/null +++ b/vendor/github.com/miguelmota/go-coinmarketcap/pro/v1/util.go @@ -0,0 +1,59 @@ +package coinmarketcap + +import ( + "fmt" + "io/ioutil" + "net/http" + "strconv" + "strings" + "time" +) + +// toInt helper for parsing strings to int +func toInt(rawInt string) int { + parsed, _ := strconv.Atoi(strings.Replace(strings.Replace(rawInt, "$", "", -1), ",", "", -1)) + return parsed +} + +// toFloat helper for parsing strings to float +func toFloat(rawFloat string) float64 { + parsed, _ := strconv.ParseFloat(strings.Replace(strings.Replace(strings.Replace(rawFloat, "$", "", -1), ",", "", -1), "%", "", -1), 64) + return parsed +} + +// doReq HTTP client +func doReq(req *http.Request) ([]byte, error) { + requestTimeout := time.Duration(5 * time.Second) + client := &http.Client{ + Timeout: requestTimeout, + } + 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 200 != resp.StatusCode { + return nil, fmt.Errorf("%s", body) + } + + return body, nil +} + +// makeReq HTTP request helper +func (s *Client) makeReq(url string) ([]byte, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Add("X-CMC_PRO_API_KEY", s.proAPIKey) + resp, err := doReq(req) + if err != nil { + return nil, err + } + + return resp, err +} diff --git a/cointop/common/cmc/types/types.go b/vendor/github.com/miguelmota/go-coinmarketcap/v2/types/types.go similarity index 75% rename from cointop/common/cmc/types/types.go rename to vendor/github.com/miguelmota/go-coinmarketcap/v2/types/types.go index 7701812..67bbd34 100644 --- a/cointop/common/cmc/types/types.go +++ b/vendor/github.com/miguelmota/go-coinmarketcap/v2/types/types.go @@ -71,23 +71,3 @@ type MarketGraph struct { MarketCapByAvailableSupply [][]float64 `json:"market_cap_by_available_supply"` VolumeUSD [][]float64 `json:"volume_usd"` } - -// V1Ticker struct -type V1Ticker struct { - ID string `json:"id"` - Name string `json:"name"` - Symbol string `json:"symbol"` - Rank int `json:"rank,string"` - PriceUSD float64 `json:"price_usd,string"` - PriceBTC float64 `json:"price_btc,string"` - USD24HVolume float64 `json:"24h_volume_usd,string"` - MarketCapUSD float64 `json:"market_cap_usd,string"` - AvailableSupply float64 `json:"available_supply,string"` - TotalSupply float64 `json:"total_supply,string"` - PercentChange1H float64 `json:"percent_change_1h,string"` - PercentChange24H float64 `json:"percent_change_24h,string"` - PercentChange7D float64 `json:"percent_change_7d,string"` - LastUpdated int `json:"last_updated,string"` - - Quotes map[string]*TickerQuote `json:"quotes"` -} diff --git a/cointop/common/cmc/cmc.go b/vendor/github.com/miguelmota/go-coinmarketcap/v2/v2.go similarity index 89% rename from cointop/common/cmc/cmc.go rename to vendor/github.com/miguelmota/go-coinmarketcap/v2/v2.go index 4267192..5ec6892 100644 --- a/cointop/common/cmc/cmc.go +++ b/vendor/github.com/miguelmota/go-coinmarketcap/v2/v2.go @@ -10,8 +10,10 @@ import ( "strconv" "strings" + "sort" + "github.com/anaskhan96/soup" - "github.com/miguelmota/cointop/cointop/common/cmc/types" + "github.com/miguelmota/go-coinmarketcap/v2/types" ) var ( @@ -25,7 +27,7 @@ var ( // Interface interface type Interface interface { Listings() ([]*types.Listing, error) - Tickers(options *TickersOptions) (map[string]*types.Ticker, error) + Tickers(options *TickersOptions) ([]*types.Ticker, error) Ticker(options *TickerOptions) (*types.Ticker, error) TickerGraph(options *TickerGraphOptions) (*types.TickerGraph, error) GlobalMarket(options *GlobalMarketOptions) (*types.GlobalMarket, error) @@ -59,15 +61,21 @@ type TickersOptions struct { Start int Limit int Convert string + Sort string } // tickerMedia tickers response media type tickersMedia struct { - Data map[string]*types.Ticker `json:"data"` + Data map[string]*types.Ticker `json:"data,omitempty"` + Metadata struct { + Timestamp int64 + NumCryptoCurrencies int `json:"num_cryptocurrencies,omitempty"` + Error string `json:",omitempty"` + } } // Tickers gets ticker information on coins -func Tickers(options *TickersOptions) (map[string]*types.Ticker, error) { +func Tickers(options *TickersOptions) ([]*types.Ticker, error) { var params []string if options.Start >= 0 { params = append(params, fmt.Sprintf("start=%v", options.Start)) @@ -78,6 +86,9 @@ func Tickers(options *TickersOptions) (map[string]*types.Ticker, error) { if options.Convert != "" { params = append(params, fmt.Sprintf("convert=%v", options.Convert)) } + if options.Sort != "" { + params = append(params, fmt.Sprintf("sort=%v", options.Sort)) + } url := fmt.Sprintf("%s/ticker?%s", baseURL, strings.Join(params, "&")) resp, err := makeReq(url) var body tickersMedia @@ -85,11 +96,20 @@ func Tickers(options *TickersOptions) (map[string]*types.Ticker, error) { if err != nil { return nil, err } - tickers := make(map[string]*types.Ticker) data := body.Data + var tickers []*types.Ticker for _, v := range data { - tickers[strings.ToUpper(string(v.Symbol))] = v + tickers = append(tickers, v) + } + + if body.Metadata.Error != "" { + return nil, errors.New(body.Metadata.Error) } + + sort.Slice(tickers, func(i, j int) bool { + return tickers[i].Rank < tickers[j].Rank + }) + return tickers, nil } @@ -269,13 +289,14 @@ type PriceOptions struct { // Price gets price of a cryptocurrency func Price(options *PriceOptions) (float64, error) { - coins, err := Tickers(&TickersOptions{ + coin, err := Ticker(&TickerOptions{ Convert: options.Convert, + Symbol: options.Symbol, }) if err != nil { return 0, err } - coin := coins[options.Symbol] + if coin == nil { return 0, errors.New("coin not found") } @@ -285,25 +306,30 @@ func Price(options *PriceOptions) (float64, error) { // CoinID gets the ID for the cryptocurrency func CoinID(symbol string) (int, error) { symbol = strings.ToUpper(strings.TrimSpace(symbol)) - coins, err := Tickers(&TickersOptions{}) + listings, err := Listings() if err != nil { return 0, err } - coin := coins[symbol] - if coin == nil { - return 0, errors.New("coin not found") + + for _, l := range listings { + if l.Symbol == symbol { + return l.ID, nil + } } - return coin.ID, nil + //returns error as default + return 0, errors.New("coin not found") } // CoinSlug gets the slug for the cryptocurrency func CoinSlug(symbol string) (string, error) { symbol = strings.ToUpper(strings.TrimSpace(symbol)) - coins, err := Tickers(&TickersOptions{}) + coin, err := Ticker(&TickerOptions{ + Symbol: symbol, + }) if err != nil { return "", err } - coin := coins[symbol] + if coin == nil { return "", errors.New("coin not found") } diff --git a/vendor/golang.org/x/net/html/node.go b/vendor/golang.org/x/net/html/node.go index 2c1cade..633ee15 100644 --- a/vendor/golang.org/x/net/html/node.go +++ b/vendor/golang.org/x/net/html/node.go @@ -177,7 +177,7 @@ func (s *nodeStack) index(n *Node) int { // contains returns whether a is within s. func (s *nodeStack) contains(a atom.Atom) bool { for _, n := range *s { - if n.DataAtom == a { + if n.DataAtom == a && n.Namespace == "" { return true } } diff --git a/vendor/golang.org/x/net/html/parse.go b/vendor/golang.org/x/net/html/parse.go index 64a5793..1d3c198 100644 --- a/vendor/golang.org/x/net/html/parse.go +++ b/vendor/golang.org/x/net/html/parse.go @@ -439,9 +439,6 @@ func (p *parser) resetInsertionMode() { case a.Select: if !last { for ancestor, first := n, p.oe[0]; ancestor != first; { - if ancestor == first { - break - } ancestor = p.oe[p.oe.index(ancestor)-1] switch ancestor.DataAtom { case a.Template: @@ -904,7 +901,7 @@ func inBodyIM(p *parser) bool { case a.A: for i := len(p.afe) - 1; i >= 0 && p.afe[i].Type != scopeMarkerNode; i-- { if n := p.afe[i]; n.Type == ElementNode && n.DataAtom == a.A { - p.inBodyEndTagFormatting(a.A) + p.inBodyEndTagFormatting(a.A, "a") p.oe.remove(n) p.afe.remove(n) break @@ -918,7 +915,7 @@ func inBodyIM(p *parser) bool { case a.Nobr: p.reconstructActiveFormattingElements() if p.elementInScope(defaultScope, a.Nobr) { - p.inBodyEndTagFormatting(a.Nobr) + p.inBodyEndTagFormatting(a.Nobr, "nobr") p.reconstructActiveFormattingElements() } p.addFormattingElement() @@ -1126,7 +1123,7 @@ func inBodyIM(p *parser) bool { case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6: p.popUntil(defaultScope, a.H1, a.H2, a.H3, a.H4, a.H5, a.H6) case a.A, a.B, a.Big, a.Code, a.Em, a.Font, a.I, a.Nobr, a.S, a.Small, a.Strike, a.Strong, a.Tt, a.U: - p.inBodyEndTagFormatting(p.tok.DataAtom) + p.inBodyEndTagFormatting(p.tok.DataAtom, p.tok.Data) case a.Applet, a.Marquee, a.Object: if p.popUntil(defaultScope, p.tok.DataAtom) { p.clearActiveFormattingElements() @@ -1137,7 +1134,7 @@ func inBodyIM(p *parser) bool { case a.Template: return inHeadIM(p) default: - p.inBodyEndTagOther(p.tok.DataAtom) + p.inBodyEndTagOther(p.tok.DataAtom, p.tok.Data) } case CommentToken: p.addChild(&Node{ @@ -1164,7 +1161,7 @@ func inBodyIM(p *parser) bool { return true } -func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) { +func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom, tagName string) { // This is the "adoption agency" algorithm, described at // https://html.spec.whatwg.org/multipage/syntax.html#adoptionAgency @@ -1186,7 +1183,7 @@ func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) { } } if formattingElement == nil { - p.inBodyEndTagOther(tagAtom) + p.inBodyEndTagOther(tagAtom, tagName) return } feIndex := p.oe.index(formattingElement) @@ -1291,9 +1288,17 @@ func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) { // inBodyEndTagOther performs the "any other end tag" algorithm for inBodyIM. // "Any other end tag" handling from 12.2.6.5 The rules for parsing tokens in foreign content // https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inforeign -func (p *parser) inBodyEndTagOther(tagAtom a.Atom) { +func (p *parser) inBodyEndTagOther(tagAtom a.Atom, tagName string) { for i := len(p.oe) - 1; i >= 0; i-- { - if p.oe[i].DataAtom == tagAtom { + // Two element nodes have the same tag if they have the same Data (a + // string-typed field). As an optimization, for common HTML tags, each + // Data string is assigned a unique, non-zero DataAtom (a uint32-typed + // field), since integer comparison is faster than string comparison. + // Uncommon (custom) tags get a zero DataAtom. + // + // The if condition here is equivalent to (p.oe[i].Data == tagName). + if (p.oe[i].DataAtom == tagAtom) && + ((tagAtom != 0) || (p.oe[i].Data == tagName)) { p.oe = p.oe[:i] break } @@ -1719,8 +1724,12 @@ func inSelectIM(p *parser) bool { } p.addElement() case a.Select: - p.tok.Type = EndTagToken - return false + if p.popUntil(selectScope, a.Select) { + p.resetInsertionMode() + } else { + // Ignore the token. + return true + } case a.Input, a.Keygen, a.Textarea: if p.elementInScope(selectScope, a.Select) { p.parseImpliedToken(EndTagToken, a.Select, a.Select.String()) @@ -1750,6 +1759,9 @@ func inSelectIM(p *parser) bool { case a.Select: if p.popUntil(selectScope, a.Select) { p.resetInsertionMode() + } else { + // Ignore the token. + return true } case a.Template: return inHeadIM(p) @@ -1775,13 +1787,22 @@ func inSelectInTableIM(p *parser) bool { case StartTagToken, EndTagToken: switch p.tok.DataAtom { case a.Caption, a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr, a.Td, a.Th: - if p.tok.Type == StartTagToken || p.elementInScope(tableScope, p.tok.DataAtom) { - p.parseImpliedToken(EndTagToken, a.Select, a.Select.String()) - return false - } else { + if p.tok.Type == EndTagToken && !p.elementInScope(tableScope, p.tok.DataAtom) { // Ignore the token. return true } + // This is like p.popUntil(selectScope, a.Select), but it also + // matches , not just