diff --git a/README.md b/README.md index fed1b2a..debfd89 100644 --- a/README.md +++ b/README.md @@ -1008,6 +1008,12 @@ Frequently asked questions: - A: Run the command `cointop reset` to delete the config files and cache. Cointop will generate a new config when starting up. You can run `cointop --reset` to reset before running cointop. +- Q: Why aren't Home or End keys working for me? + + - A: Make sure to not manually set `TERM` in your `~/.bashrc`, `~/.zshrc`, or any where else. The `TERM` environment variable should be automatically set by your terminal, otherwise this may cause the terminal emulator to send escape codes when these keys are pressed. See [this Arch wiki](https://wiki.archlinux.org/index.php/Home_and_End_keys_not_working) for more info. + + - A: Use the `cointop price` command. Here are some examples: + - Q: What is the size of the binary? - A: The Go build size is ~8MB but packed with UPX it's only a ~3MB executable binary. @@ -1040,8 +1046,7 @@ Frequently asked questions: - Q: Does cointop do mining? - - A: Cointop does not do any kind of mining. - + - A: Cointop does not do any kind of cryptocurrency mining. - Q: How can I run the cointop SSH server on port 22? diff --git a/cointop/chart.go b/cointop/chart.go index 7716322..1326b10 100644 --- a/cointop/chart.go +++ b/cointop/chart.go @@ -2,6 +2,7 @@ package cointop import ( "fmt" + "sort" "strings" "sync" "time" @@ -142,7 +143,7 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error { } for i := range graphData.MarketCapByAvailableSupply { price := graphData.MarketCapByAvailableSupply[i][1] - data = append(data, price/1e9) + data = append(data, price) } } else { convert := ct.State.currencyConversion @@ -150,9 +151,12 @@ func (ct *Cointop) ChartPoints(symbol string, name string) error { if err != nil { return nil } - - for i := range graphData.Price { - price := graphData.Price[i][1] + sorted := graphData.Price + sort.Slice(sorted[:], func(i, j int) bool { + return sorted[i][0] < sorted[j][0] + }) + for i := range sorted { + price := sorted[i][1] data = append(data, price) } } @@ -230,8 +234,12 @@ func (ct *Cointop) PortfolioChart() error { if err != nil { return err } - for i := range apiGraphData.Price { - price := apiGraphData.Price[i][1] + sorted := apiGraphData.Price + sort.Slice(sorted[:], func(i, j int) bool { + return sorted[i][0] < sorted[j][0] + }) + for i := range sorted { + price := sorted[i][1] graphData = append(graphData, price) } } @@ -358,6 +366,7 @@ func (ct *Cointop) ToggleCoinChart() error { ct.UpdateChart() }() + // TODO: not do this (SoC) go ct.UpdateMarketbar() return nil diff --git a/cointop/cointop.go b/cointop/cointop.go index 27d2b60..0e54335 100644 --- a/cointop/cointop.go +++ b/cointop/cointop.go @@ -225,7 +225,7 @@ func NewCointop(config *Config) (*Cointop, error) { marketBarHeight: 1, onlyTable: config.OnlyTable, refreshRate: 60 * time.Second, - selectedChartRange: "7D", + selectedChartRange: "1Y", shortcutKeys: DefaultShortcuts(), sortBy: "rank", page: 0, @@ -340,10 +340,6 @@ func NewCointop(config *Config) (*Cointop, error) { } } - if ct.apiChoice == CoinGecko { - ct.State.selectedChartRange = "1Y" - } - if ct.apiChoice == CoinMarketCap { ct.api = api.NewCMC(ct.apiKeys.cmc) } else if ct.apiChoice == CoinGecko { diff --git a/go.mod b/go.mod index 10e616c..379455a 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/maruel/panicparse v1.5.0 github.com/mattn/go-colorable v0.1.7 // indirect github.com/mattn/go-runewidth v0.0.9 - github.com/miguelmota/go-coinmarketcap v0.1.6 + github.com/miguelmota/go-coinmarketcap v0.1.7 github.com/miguelmota/gocui v0.4.2 github.com/miguelmota/termbox-go v0.0.0-20191229070316-58d4fcbce2a7 github.com/mitchellh/go-wordwrap v1.0.0 diff --git a/go.sum b/go.sum index f55aa25..41dcdab 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/Qd github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/miguelmota/go-coinmarketcap v0.1.6 h1:YIe+VdFhEgyGESfmkL7BHRDIdf6CUOAjJisml01AFqs= -github.com/miguelmota/go-coinmarketcap v0.1.6/go.mod h1:Jdv/kqtKclIElmoNAZMMJn0DSQv+j7p/H1te/GGnxhA= +github.com/miguelmota/go-coinmarketcap v0.1.7 h1:9kTFWMom73IuGXqacD/LYPiUeX1qLpuLH8BhceHXYt0= +github.com/miguelmota/go-coinmarketcap v0.1.7/go.mod h1:hBjej1IiB5+pfj+0cZhnxRkAc2bgky8qWLhCJTQ3zjw= github.com/miguelmota/gocui v0.4.2 h1:nMYnYn3RjV7FlWFcidQa9eAkX3kT7XMI6yJMxEkAz6s= github.com/miguelmota/gocui v0.4.2/go.mod h1:wVtmhuLR+VAS9VRBIJZBNJS9IgH+9QOZ/m/MvRarOZ4= github.com/miguelmota/termbox-go v0.0.0-20191229070316-58d4fcbce2a7 h1:sZmjSV25xMXIGAaATVuOtC9VtGHMydXpd9OejNaTxQE= diff --git a/pkg/api/impl/coinmarketcap/coinmarketcap.go b/pkg/api/impl/coinmarketcap/coinmarketcap.go index d6b7ed9..9d03230 100644 --- a/pkg/api/impl/coinmarketcap/coinmarketcap.go +++ b/pkg/api/impl/coinmarketcap/coinmarketcap.go @@ -1,9 +1,13 @@ package coinmarketcap import ( + "encoding/json" "errors" "fmt" + "io/ioutil" + "net/http" "os" + "sort" "strings" "time" @@ -19,6 +23,9 @@ 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 @@ -149,35 +156,138 @@ func (s *Service) GetCoinDataBatch(names []string, convert string) ([]apitypes.C // GetCoinGraphData gets coin graph data func (s *Service) GetCoinGraphData(convert, symbol string, name string, start int64, end int64) (apitypes.CoinGraph, error) { ret := apitypes.CoinGraph{} - graphData, err := cmcv2.TickerGraph(&cmcv2.TickerGraphOptions{ - Symbol: symbol, - Start: start, - End: end, + symbol = strings.ToUpper(symbol) + info, err := s.client.Cryptocurrency.Info(&cmc.InfoOptions{ + Slug: name, }) if err != nil { return ret, err } - - ret.MarketCapByAvailableSupply = graphData.MarketCapByAvailableSupply - ret.PriceBTC = graphData.PriceBTC - ret.Price = graphData.PriceUSD - ret.Volume = graphData.VolumeUSD + 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{} - graphData, err := cmcv2.GlobalMarketGraph(&cmcv2.GlobalMarketGraphOptions{ - Start: start, - End: end, - }) + 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 } - - ret.MarketCapByAvailableSupply = graphData.MarketCapByAvailableSupply - ret.VolumeUSD = graphData.VolumeUSD + 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 } @@ -229,7 +339,6 @@ func (s *Service) CoinLink(name string) string { // SupportedCurrencies returns a list of supported currencies func (s *Service) SupportedCurrencies() []string { - // keep these in alphabetical order return []string{ "AUD", @@ -269,3 +378,55 @@ func (s *Service) SupportedCurrencies() []string { "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 +} diff --git a/pkg/termui/linechart.go b/pkg/termui/linechart.go index 0e76f5e..ab26670 100644 --- a/pkg/termui/linechart.go +++ b/pkg/termui/linechart.go @@ -198,7 +198,16 @@ func (lc *LineChart) calcLabelX() { } func shortenFloatVal(x float64) string { - s := fmt.Sprintf("%.2f", x) + if x > 1e12 { + return fmt.Sprintf("%.2fT", x/1e12) + } + if x > 1e9 { + return fmt.Sprintf("%.2fB", x/1e9) + } + if x > 1e6 { + return fmt.Sprintf("%.2fB", x/1e6) + } + //if len(s)-3 > 3 { //s = fmt.Sprintf("%.2e", x) //} @@ -206,7 +215,7 @@ func shortenFloatVal(x float64) string { //if x < 0 { //s = fmt.Sprintf("%.2f", x) //} - return s + return fmt.Sprintf("%.2f", x) } func (lc *LineChart) calcLabelY() { @@ -217,7 +226,8 @@ func (lc *LineChart) calcLabelY() { lc.labelY = make([][]rune, n) maxLen := 0 for i := 0; i < n; i++ { - s := str2runes(shortenFloatVal(lc.bottomValue + float64(i)*span/float64(n))) + val := lc.bottomValue + float64(i)*span/float64(n) + s := str2runes(shortenFloatVal(val)) if len(s) > maxLen { maxLen = len(s) }