diff --git a/README.md b/README.md index 353348d..d3a0899 100644 --- a/README.md +++ b/README.md @@ -988,6 +988,7 @@ Frequently asked questions: - A: Cointop does not do any kind of mining. + - Q: How can I run the cointop SSH server on port 22? - A: Port 22 is a privileged port so you need to run with `sudo`: @@ -996,6 +997,14 @@ Frequently asked questions: sudo cointop server -p 22 ``` +- Q: Why doesn't the version number work when I install with `go get`? + + - A: The version number is read from the git tag during the build process but this requires the `GO111MODULE` environment variable to be set in order for Go to read the build information: + + ```bash + GO111MODULE=on go get github.com/miguelmota/cointop + ``` + ## Mentioned in Cointop has been mentioned in: diff --git a/cointop/cmd/holdings.go b/cointop/cmd/holdings.go index 00cb6b7..2199d72 100644 --- a/cointop/cmd/holdings.go +++ b/cointop/cmd/holdings.go @@ -19,7 +19,6 @@ func HoldingsCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { ct, err := cointop.NewCointop(&cointop.Config{ ConfigFilepath: config, - APIChoice: cointop.CoinGecko, CacheDir: cointop.DefaultCacheDir, }) if err != nil { diff --git a/cointop/cmd/root.go b/cointop/cmd/root.go index 6b16255..e654348 100644 --- a/cointop/cmd/root.go +++ b/cointop/cmd/root.go @@ -100,7 +100,7 @@ See git.io/cointop for more info.`, rootCmd.Flags().UintVarP(&perPage, "per-page", "", perPage, "Per page") rootCmd.Flags().StringVarP(&config, "config", "c", "", fmt.Sprintf("Config filepath. (default %s)", cointop.DefaultConfigFilepath)) rootCmd.Flags().StringVarP(&cmcAPIKey, "coinmarketcap-api-key", "", "", "Set the CoinMarketCap API key") - rootCmd.Flags().StringVarP(&apiChoice, "api", "", cointop.CoinGecko, "API choice. Available choices are \"coinmarketcap\" and \"coingecko\"") + rootCmd.Flags().StringVarP(&apiChoice, "api", "", "", "API choice. Available choices are \"coinmarketcap\" and \"coingecko\"") rootCmd.Flags().StringVarP(&colorscheme, "colorscheme", "", "", "Colorscheme to use (default \"cointop\"). To install standard themes, do:\n\ngit clone git@github.com:cointop-sh/colors.git ~/.config/cointop/colors\n\nSee git.io/cointop#colorschemes for more info.") rootCmd.Flags().StringVarP(&cacheDir, "cache-dir", "", cacheDir, "Cache directory") diff --git a/cointop/cointop.go b/cointop/cointop.go index 332e6fd..33688ed 100644 --- a/cointop/cointop.go +++ b/cointop/cointop.go @@ -507,13 +507,6 @@ func Reset(config *ResetConfig) error { configDeleted := false - possibleConfigPaths := []string{ - "~/.config/cointop/config.toml", - "~/.config/cointop/config", - "~/.cointop/config", - "~/.cointop/config.toml", - } - for _, configPath := range possibleConfigPaths { normalizedPath := pathutil.NormalizePath(configPath) if _, err := os.Stat(normalizedPath); !os.IsNotExist(err) { diff --git a/cointop/common/api/impl/coingecko/coingecko.go b/cointop/common/api/impl/coingecko/coingecko.go index 7b3c99c..c0d9223 100644 --- a/cointop/common/api/impl/coingecko/coingecko.go +++ b/cointop/common/api/impl/coingecko/coingecko.go @@ -45,9 +45,8 @@ func (s *Service) Ping() error { return nil } -func (s *Service) getLimitedCoinData(convert string, offset int) ([]apitypes.Coin, error) { +func (s *Service) getPaginatedCoinData(convert string, offset int, names []string) ([]apitypes.Coin, error) { var ret []apitypes.Coin - ids := []string{} page := offset sparkline := false pcp := geckoTypes.PriceChangePercentageObject @@ -57,6 +56,12 @@ func (s *Service) getLimitedCoinData(convert string, offset int) ([]apitypes.Coi 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 @@ -124,7 +129,7 @@ func (s *Service) GetAllCoinData(convert string, ch chan []apitypes.Coin) error time.Sleep(1 * time.Second) } - coins, err := s.getLimitedCoinData(convert, i) + coins, err := s.getPaginatedCoinData(convert, i, []string{}) if err != nil { return } @@ -135,6 +140,27 @@ func (s *Service) GetAllCoinData(convert string, ch chan []apitypes.Coin) error return nil } +// GetCoinData gets all data of a coin. +func (s *Service) GetCoinData(name string, convert string) (apitypes.Coin, error) { + ret := apitypes.Coin{} + ids := []string{name} + coins, err := s.getPaginatedCoinData(convert, 0, ids) + if err != nil { + return ret, err + } + + if len(coins) > 0 { + ret = coins[0] + } + + return ret, nil +} + +// GetCoinDataBatch gets all data of specified coins. +func (s *Service) GetCoinDataBatch(names []string, convert string) ([]apitypes.Coin, error) { + return s.getPaginatedCoinData(convert, 0, names) +} + // GetCoinGraphData gets coin graph data func (s *Service) GetCoinGraphData(convert, symbol, name string, start, end int64) (apitypes.CoinGraph, error) { ret := apitypes.CoinGraph{} diff --git a/cointop/common/api/impl/coinmarketcap/coinmarketcap.go b/cointop/common/api/impl/coinmarketcap/coinmarketcap.go index 3b6d844..a6c91ac 100644 --- a/cointop/common/api/impl/coinmarketcap/coinmarketcap.go +++ b/cointop/common/api/impl/coinmarketcap/coinmarketcap.go @@ -51,7 +51,7 @@ func (s *Service) Ping() error { return nil } -func (s *Service) getLimitedCoinData(convert string, offset int) ([]apitypes.Coin, error) { +func (s *Service) getPaginatedCoinData(convert string, offset int) ([]apitypes.Coin, error) { var ret []apitypes.Coin max := 100 @@ -98,7 +98,7 @@ func (s *Service) GetAllCoinData(convert string, ch chan []apitypes.Coin) error time.Sleep(1 * time.Second) } - coins, err := s.getLimitedCoinData(convert, i) + coins, err := s.getPaginatedCoinData(convert, i) if err != nil { return } @@ -109,6 +109,43 @@ func (s *Service) GetAllCoinData(convert string, ch chan []apitypes.Coin) error 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{} diff --git a/cointop/common/api/interface.go b/cointop/common/api/interface.go index 536cb34..db908e3 100644 --- a/cointop/common/api/interface.go +++ b/cointop/common/api/interface.go @@ -11,7 +11,8 @@ type Interface interface { GetCoinGraphData(convert string, symbol string, name string, start int64, end int64) (types.CoinGraph, error) GetGlobalMarketGraphData(convert string, start int64, end int64) (types.MarketGraph, error) GetGlobalMarketData(convert string) (types.GlobalMarketData, error) - //GetCoinData(coin string) (types.Coin, error) + GetCoinData(name string, convert string) (types.Coin, error) + GetCoinDataBatch(names []string, convert string) ([]types.Coin, error) //GetAltcoinMarketGraphData(start int64, end int64) (types.MarketGraph, error) //GetCoinPriceUSD(coin string) (float64, error) //GetCoinMarkets(coin string) ([]types.Market, error) diff --git a/cointop/config.go b/cointop/config.go index 4855267..5d27b46 100644 --- a/cointop/config.go +++ b/cointop/config.go @@ -15,6 +15,14 @@ import ( var fileperm = os.FileMode(0644) +// NOTE: this is to support previous default config filepaths +var possibleConfigPaths = []string{ + "~/.config/cointop/config.toml", + "~/.config/cointop/config", + "~/.cointop/config", + "~/.cointop/config.toml", +} + type config struct { Shortcuts map[string]interface{} `toml:"shortcuts"` Favorites map[string][]interface{} `toml:"favorites"` @@ -42,9 +50,6 @@ func (ct *Cointop) SetupConfig() error { if err := ct.loadFavoritesFromConfig(); err != nil { return err } - if err := ct.loadPortfolioFromConfig(); err != nil { - return err - } if err := ct.loadCurrencyFromConfig(); err != nil { return err } @@ -63,6 +68,9 @@ func (ct *Cointop) SetupConfig() error { if err := ct.loadRefreshRateFromConfig(); err != nil { return err } + if err := ct.loadPortfolioFromConfig(); err != nil { + return err + } return nil } @@ -71,14 +79,8 @@ func (ct *Cointop) SetupConfig() error { func (ct *Cointop) CreateConfigIfNotExists() error { ct.debuglog("createConfigIfNotExists()") - // NOTE: this is to support previous default config filepaths - previousDefaultConfigPaths := []string{ - "~/.cointop/config", - "~/.cointop/config.toml", - } - - for _, previousConfigFilepath := range previousDefaultConfigPaths { - normalizedPath := pathutil.NormalizePath(previousConfigFilepath) + for _, configPath := range possibleConfigPaths { + normalizedPath := pathutil.NormalizePath(configPath) if _, err := os.Stat(normalizedPath); err == nil { ct.configFilepath = normalizedPath return nil @@ -218,8 +220,8 @@ func (ct *Cointop) configToToml() ([]byte, error) { cmcIfc := map[string]interface{}{ "pro_api_key": ct.apiKeys.cmc, } - var apiChoiceIfc interface{} = ct.apiChoice + var apiChoiceIfc interface{} = ct.apiChoice var inputs = &config{ API: apiChoiceIfc, Colorscheme: colorschemeIfc, diff --git a/cointop/portfolio.go b/cointop/portfolio.go index 235ba28..1eec448 100644 --- a/cointop/portfolio.go +++ b/cointop/portfolio.go @@ -184,7 +184,7 @@ func (ct *Cointop) SetPortfolioHoldings() error { // PortfolioEntry returns a portfolio entry func (ct *Cointop) PortfolioEntry(c *Coin) (*PortfolioEntry, bool) { - //ct.debuglog("portfolioEntry()") + //ct.debuglog("portfolioEntry()") // too many if c == nil { return &PortfolioEntry{}, true } @@ -297,20 +297,28 @@ func (ct *Cointop) GetPortfolioTotal() float64 { return total } -// NormalizeFloatString normalizes a float as a string -func normalizeFloatString(input string) string { - re := regexp.MustCompile(`(\d+\.\d+|\.\d+|\d+)`) - result := re.FindStringSubmatch(input) - if len(result) > 0 { - return result[0] +// RefreshPortfolioCoins refreshes portfolio entry coin data +func (ct *Cointop) RefreshPortfolioCoins() error { + ct.debuglog("refreshPortfolioCoins()") + holdings := ct.GetPortfolioSlice() + holdingCoins := make([]string, len(holdings)) + for i, entry := range holdings { + holdingCoins[i] = entry.Name } - return "" + coins, err := ct.api.GetCoinDataBatch(holdingCoins, ct.State.currencyConversion) + ct.processCoins(coins) + if err != nil { + return err + } + + return nil } // PrintHoldingsTable prints the holdings in an ASCII table func (ct *Cointop) PrintHoldingsTable() error { - ct.UpdateCoins() // fetches latest data + ct.debuglog("printHoldingsTable()") + ct.RefreshPortfolioCoins() holdings := ct.GetPortfolioSlice() total := ct.GetPortfolioTotal() data := make([][]string, len(holdings)) @@ -347,10 +355,22 @@ func (ct *Cointop) PrintHoldingsTable() error { // PrintTotalHoldings prints the total holdings amount func (ct *Cointop) PrintTotalHoldings() error { - ct.UpdateCoins() // fetches latest data + ct.debuglog("printTotalHoldings()") + ct.RefreshPortfolioCoins() total := ct.GetPortfolioTotal() symbol := ct.CurrencySymbol() fmt.Fprintf(os.Stdout, "%s%s\n", symbol, humanize.Commaf(total)) return nil } + +// NormalizeFloatString normalizes a float as a string +func normalizeFloatString(input string) string { + re := regexp.MustCompile(`(\d+\.\d+|\.\d+|\d+)`) + result := re.FindStringSubmatch(input) + if len(result) > 0 { + return result[0] + } + + return "" +}