Skip to content

Commit

Permalink
Support specifying api key
Browse files Browse the repository at this point in the history
  • Loading branch information
polyrabbit committed Feb 20, 2021
1 parent 89f9915 commit 026e561
Show file tree
Hide file tree
Showing 19 changed files with 138 additions and 94 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Download from [release page](https://github.com/polyrabbit/my-token/releases/lat
```
$ mt --help
Usage: mt [Options] [Exchange1.Token1 Exchange2.Token2 ...]
Usage: mt [Options] [Exchange1.Token1 Exchange2.Token2.<api_key> ...]
Track token prices of your favorite exchanges in the terminal
Expand All @@ -80,7 +80,7 @@ Options:
-t, --timeout int HTTP request timeout in seconds (default 20)
Space-separated exchange.token pairs:
Specify which exchange and token pair to query, different exchanges use different forms to express tokens/trading pairs, refer to their URLs to find the format, eg. to get BitCoin price from Bitfinex and CoinMarketCap you should use query string "Bitfinex.BTCUSDT CoinMarketCap.Bitcoin"
Specify which exchange and token pair to query, different exchanges use different forms to express tokens/trading pairs, refer to their URLs to find the format (eg. "Bitfinex.BTCUSDT"). Optionally you can set api_key in the third place.
Find help/updates from here - https://github.com/polyrabbit/my-token
```
Expand Down
20 changes: 13 additions & 7 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,13 @@ func Parse() *Config {

func showUsageAndExit() {
// Print usage message and exit
fmt.Fprintf(os.Stderr, "\nUsage: %s [Options] [Exchange1.Token1 Exchange2.Token2 ...]\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\nUsage: %s [Options] [Exchange1.Token1 Exchange2.Token2.<api_key> ...]\n", os.Args[0])
fmt.Fprintln(os.Stderr, "\nTrack token prices of your favorite exchanges in the terminal")
fmt.Fprintln(os.Stderr, "\nOptions:")
pflag.PrintDefaults()
fmt.Fprintln(os.Stderr, "\nSpace-separated exchange.token pairs:")
fmt.Fprintln(os.Stderr, " Specify which exchange and token pair to query, different exchanges use different forms to express tokens/trading pairs, refer to their URLs to find the format"+
" (eg. to get BitCoin price from Bitfinex and CoinMarketCap you should use query string \"Bitfinex.BTCUSDT CoinMarketCap.Bitcoin\").")
" (eg. \"Bitfinex.BTCUSDT\"). Optionally you can set api_key in the third place.")
fmt.Fprintln(os.Stderr, "\nFind help/updates from here - https://github.com/polyrabbit/my-token")
os.Exit(0)
}
Expand Down Expand Up @@ -150,15 +150,16 @@ func ListExchangesAndExit(exchanges []string) {
os.Exit(0)
}

// CLI format exchange.token.<api_key> - api_key is optional
func parseQueryFromCLI(cliArgs []string) []PriceQuery {
var (
lastExchangeDef = PriceQuery{}
lastExchangeDef PriceQuery
exchangeList []PriceQuery
)
for _, arg := range cliArgs {
tokenDef := strings.SplitN(arg, ".", 2)
if len(tokenDef) != 2 {
logrus.Fatalf("Unrecognized token definition - %s, expecting {exchange}.{token}\n", arg)
tokenDef := strings.SplitN(arg, ".", -1)
if len(tokenDef) < 2 {
logrus.Fatalf("Unrecognized token definition - %s, expecting {exchange}.{token}.<api_key>\n", arg)
}
if lastExchangeDef.Name == tokenDef[0] {
// Merge consecutive exchange definitions
Expand All @@ -167,10 +168,15 @@ func parseQueryFromCLI(cliArgs []string) []PriceQuery {
} else {
exchangeDef := PriceQuery{
Name: tokenDef[0],
Tokens: []string{tokenDef[1]}}
Tokens: []string{tokenDef[1]},
}
lastExchangeDef = exchangeDef
exchangeList = append(exchangeList, exchangeDef)
}
// The third one is api key
if len(tokenDef) > 2 && len(exchangeList) > 0 {
exchangeList[len(exchangeList)-1].APIKey = tokenDef[2]
}
}
return exchangeList
}
4 changes: 3 additions & 1 deletion config/model.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package config

import "strings"

const (
ColumnSymbol = "Symbol"
ColumnPrice = "Price"
Expand Down Expand Up @@ -31,7 +33,7 @@ type Config struct {
func (c *Config) GroupQueryByExchange() map[string]PriceQuery {
exchangeMap := make(map[string]PriceQuery, len(c.Queries))
for _, query := range c.Queries {
exchangeMap[query.Name] = query
exchangeMap[strings.ToUpper(query.Name)] = query
}
return exchangeMap
}
3 changes: 2 additions & 1 deletion exchange/bigone.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"
"time"

"github.com/polyrabbit/my-token/config"
"github.com/polyrabbit/my-token/http"
"github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -47,7 +48,7 @@ type bigOneMarketResponse struct {
}
}

func NewBigOneClient(httpClient *http.Client) ExchangeClient {
func NewBigOneClient(queries map[string]config.PriceQuery, httpClient *http.Client) ExchangeClient {
return &bigOneClient{Client: httpClient}
}

Expand Down
11 changes: 5 additions & 6 deletions exchange/binance.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"
"time"

"github.com/polyrabbit/my-token/config"
"github.com/polyrabbit/my-token/http"
"github.com/sirupsen/logrus"
)
Expand All @@ -18,8 +19,6 @@ const binanceBaseApi = "https://api.binance.com"

type binanceClient struct {
*http.Client
AccessKey string
SecretKey string
}

type binanceErrorResponse struct {
Expand All @@ -44,7 +43,7 @@ type binance24hStatistics struct {
CloseTime int64
}

func NewBinanceClient(httpClient *http.Client) ExchangeClient {
func NewBinanceClient(queries map[string]config.PriceQuery, httpClient *http.Client) ExchangeClient {
return &binanceClient{Client: httpClient}
}

Expand Down Expand Up @@ -85,15 +84,15 @@ func (client *binanceClient) Get24hStatistics(symbol string) (*binance24hStatist

respBytes, err := client.Get(binanceBaseApi+"/api/v1/ticker/24hr", http.WithQuery(map[string]string{"symbol": strings.ToUpper(symbol)}))
if err != nil {
return &respJSON, err
return nil, err
}

if err := json.Unmarshal(respBytes, &respJSON); err != nil {
return &respJSON, err
return nil, err
}

if respJSON.Msg != nil {
return &respJSON, errors.New(*respJSON.Msg)
return nil, errors.New(*respJSON.Msg)
}
return &respJSON, nil
}
Expand Down
5 changes: 2 additions & 3 deletions exchange/bitfinex.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"
"time"

"github.com/polyrabbit/my-token/config"
"github.com/polyrabbit/my-token/http"
"github.com/sirupsen/logrus"
)
Expand All @@ -18,11 +19,9 @@ const bitfinixBaseApi = "https://api.bitfinex.com/v2/" //Need api v2 to get klin

type bitfinixClient struct {
*http.Client
AccessKey string
SecretKey string
}

func NewBitfinixClient(httpClient *http.Client) ExchangeClient {
func NewBitfinixClient(queries map[string]config.PriceQuery, httpClient *http.Client) ExchangeClient {
return &bitfinixClient{Client: httpClient}
}

Expand Down
5 changes: 2 additions & 3 deletions exchange/bittrex.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"time"

"github.com/polyrabbit/my-token/config"
"github.com/polyrabbit/my-token/http"
"github.com/sirupsen/logrus"
)
Expand All @@ -22,11 +23,9 @@ const bittrexV2BaseApi = "https://bittrex.com/Api/v2.0/pub/market/"

type bittrexClient struct {
*http.Client
AccessKey string
SecretKey string
}

func NewBittrexClient(httpClient *http.Client) ExchangeClient {
func NewBittrexClient(queries map[string]config.PriceQuery, httpClient *http.Client) ExchangeClient {
return &bittrexClient{Client: httpClient}
}

Expand Down
3 changes: 2 additions & 1 deletion exchange/coinbase.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strconv"
"time"

"github.com/polyrabbit/my-token/config"
"github.com/polyrabbit/my-token/http"
"github.com/preichenberger/go-coinbasepro/v2"
"github.com/sirupsen/logrus"
Expand All @@ -16,7 +17,7 @@ type coinbaseClient struct {
coinbasepro *coinbasepro.Client
}

func NewCoinBaseClient(httpClient *http.Client) ExchangeClient {
func NewCoinBaseClient(queries map[string]config.PriceQuery, httpClient *http.Client) ExchangeClient {
client := coinbasepro.NewClient()
client.HTTPClient = httpClient.StdClient
return &coinbaseClient{coinbasepro: client}
Expand Down
135 changes: 87 additions & 48 deletions exchange/coinmarketcap.go
Original file line number Diff line number Diff line change
@@ -1,86 +1,125 @@
package exchange

import (
"encoding/json"
"errors"
"fmt"
"time"
"strings"

"github.com/polyrabbit/my-token/config"
"github.com/polyrabbit/my-token/http"
"github.com/tidwall/gjson"
)

// https://coinmarketcap.com/api/
// https://coinmarketcap.com/api/documentation/v1/#operation/getV1CryptocurrencyQuotesLatest
const coinmarketcapBaseApi = "https://pro-api.coinmarketcap.com"

type coinMarketCapClient struct {
*http.Client
APIKey string
}

type coinMarketCapToken struct {
ID string
Name string
Symbol string
Rank int32 `json:",string"`
PriceUSD string `json:"price_usd"`
PriceBTC float64 `json:"price_btc,string"`
Volume24hUSD 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"`
MaxSupply float64 `json:"max_supply,string"`
PercentChange1h float64 `json:"percent_change_1h,string"`
PercentChange24h float64 `json:"percent_change_24h,string"`
PercentChange7d float64 `json:"percent_change_7d,string"`
LastUpdated int64 `json:"last_updated,string"`
}

type notFoundResponse struct {
Error string
}
// An example 200 response
// {
// "status": {
// "timestamp": "2021-02-20T13:18:48.729Z",
// "error_code": 0,
// "error_message": null,
// "elapsed": 17,
// "credit_count": 1,
// "notice": null
// },
// "data": {
// "BTC": {
// "id": 1,
// "name": "Bitcoin",
// "symbol": "BTC",
// "slug": "bitcoin",
// "num_market_pairs": 9713,
// "date_added": "2013-04-28T00:00:00.000Z",
// "tags": [
// "mineable",
// "pow",
// "sha-256",
// "store-of-value",
// "state-channels",
// "coinbase-ventures-portfolio",
// "three-arrows-capital-portfolio",
// "polychain-capital-portfolio"
// ],
// "max_supply": 21000000,
// "circulating_supply": 18633843,
// "total_supply": 18633843,
// "is_active": 1,
// "platform": null,
// "cmc_rank": 1,
// "is_fiat": 0,
// "last_updated": "2021-02-20T13:17:02.000Z",
// "quote": {
// "USD": {
// "price": 57088.920608781234,
// "volume_24h": 65358403259.270164,
// "percent_change_1h": 2.15396551,
// "percent_change_24h": 8.31814582,
// "percent_change_7d": 21.8879362,
// "percent_change_30d": 81.30631608,
// "market_cap": 1063785983663.4939,
// "last_updated": "2021-02-20T13:17:02.000Z"
// }
// }
// }
// }
// }

func NewCoinMarketCapClient(httpClient *http.Client) ExchangeClient {
return &coinMarketCapClient{Client: httpClient}
func NewCoinMarketCapClient(queries map[string]config.PriceQuery, httpClient *http.Client) ExchangeClient {
c := &coinMarketCapClient{Client: httpClient}
if query, ok := queries[strings.ToUpper(c.GetName())]; ok { // If user queries CoinMarketCap, then API key is required
c.APIKey = query.APIKey
if c.APIKey == "" {
panic(fmt.Errorf("%s now requires API key, get one from https://coinmarketcap.com/api/", c.GetName()))
}
}
return c
}

func (client *coinMarketCapClient) GetName() string {
return "CoinMarketCap"
}

func (client *coinMarketCapClient) Init() {
// TODO: coinmarketcap needs api key now, init it here.
func (client *coinMarketCapClient) HTTPHeader() map[string]string {
return map[string]string{
"X-CMC_PRO_API_KEY": client.APIKey,
}
}

func (client *coinMarketCapClient) GetSymbolPrice(symbol string) (*SymbolPrice, error) {
respBytes, err := client.Get(coinmarketcapBaseApi + symbol + "/")
respBytes, err := client.Get(coinmarketcapBaseApi+"/v1/cryptocurrency/quotes/latest",
http.WithQuery(map[string]string{"symbol": strings.ToUpper(symbol)}),
http.WithHeader(client.HTTPHeader()))
// If there is a more specific error
if errMsg := gjson.GetBytes(respBytes, "status.error_message"); errMsg.String() != "" {
return nil, errors.New(errMsg.String())
}
// Then throws a generic one
if err != nil {
if herr, ok := err.(*http.ResponseError); ok {
resp := &notFoundResponse{}
if err := json.Unmarshal(herr.Body, resp); err != nil {
return nil, err
}
return nil, errors.New(resp.Error)
}
return nil, err
}

var tokens []coinMarketCapToken
if err := json.Unmarshal(respBytes, &tokens); err != nil {
return nil, err
symbolInfo := gjson.GetBytes(respBytes, fmt.Sprintf("data.%s", strings.ToUpper(symbol)))
if !symbolInfo.Exists() {
return nil, fmt.Errorf("no symbol %q found in returned map", symbol)
}

if len(tokens) == 0 {
return nil, fmt.Errorf("cannot find symbol %s, got zero-sized array response", symbol)
usdQuote := gjson.GetBytes([]byte(symbolInfo.Raw), "quote.USD")
if !usdQuote.Exists() {
return nil, fmt.Errorf("quote.USD not found in %q", symbol)
}
token := tokens[0]

return &SymbolPrice{
Symbol: token.Symbol,
Price: token.PriceUSD,
Symbol: symbolInfo.Get("symbol").String(),
Price: usdQuote.Get("price").String(),
Source: client.GetName(),
UpdateAt: time.Unix(token.LastUpdated, 0),
PercentChange1h: token.PercentChange1h,
PercentChange24h: token.PercentChange24h}, nil
UpdateAt: usdQuote.Get("last_updated").Time(),
PercentChange1h: usdQuote.Get("percent_change_1h").Float(),
PercentChange24h: usdQuote.Get("percent_change_24h").Float()}, nil
}

func init() {
Expand Down
Loading

0 comments on commit 026e561

Please sign in to comment.