From 2bd27feaf09d84073f3f65bb4f6c4c819169489f Mon Sep 17 00:00:00 2001 From: Adrian Gallagher Date: Mon, 4 Sep 2017 16:24:02 +1000 Subject: [PATCH] Polish websocket code --- config/config.go | 53 ++++++ config_routes.go | 74 -------- currency/currency.go | 6 +- currency/currency_test.go | 12 +- events/event_test.go | 183 ++++++++++++++------ events/events.go | 59 +++---- helpers.go | 108 ++++++++++++ main.go | 38 ++--- orderbook_routes.go | 141 ---------------- portfolio_routes.go | 27 --- restful_logger.go | 24 --- restful_router.go | 95 ++++++++++- restful_router_test.go | 11 -- restful_routes.go | 16 -- restful_server.go | 297 +++++++++++++++++++++++++++++++++ smsglobal/smsglobal.go | 7 +- ticker_routes.go | 139 --------------- ticker_routes_test.go | 1 - tools/websocket_client/main.go | 18 +- wallet_routes.go | 92 ---------- wallet_routes_test.go | 28 ---- websocket.go | 280 +++++++++++++++++++------------ 22 files changed, 916 insertions(+), 793 deletions(-) delete mode 100644 config_routes.go create mode 100644 helpers.go delete mode 100644 orderbook_routes.go delete mode 100644 portfolio_routes.go delete mode 100644 restful_logger.go delete mode 100644 restful_router_test.go delete mode 100644 restful_routes.go create mode 100644 restful_server.go delete mode 100644 ticker_routes.go delete mode 100644 ticker_routes_test.go delete mode 100644 wallet_routes.go delete mode 100644 wallet_routes_test.go diff --git a/config/config.go b/config/config.go index 322a13cd..2d01c15a 100644 --- a/config/config.go +++ b/config/config.go @@ -413,6 +413,22 @@ func (c *Config) LoadConfig(configPath string) error { return fmt.Errorf(ErrCheckingConfigValues, err) } + if c.SMS.Enabled { + err = c.CheckSMSGlobalConfigValues() + if err != nil { + log.Print(fmt.Errorf(ErrCheckingConfigValues, err)) + c.SMS.Enabled = false + } + } + + if c.Webserver.Enabled { + err = c.CheckWebserverConfigValues() + if err != nil { + log.Print(fmt.Errorf(ErrCheckingConfigValues, err)) + c.Webserver.Enabled = false + } + } + if c.CurrencyPairFormat == nil { c.CurrencyPairFormat = &CurrencyPairFormatConfig{ Delimiter: "-", @@ -423,6 +439,43 @@ func (c *Config) LoadConfig(configPath string) error { return nil } +// UpdateConfig updates the config with a supplied config file +func (c *Config) UpdateConfig(configPath string, newCfg Config) error { + if c.Name != newCfg.Name && newCfg.Name != "" { + c.Name = newCfg.Name + } + + err := newCfg.CheckExchangeConfigValues() + if err != nil { + return err + } + c.Exchanges = newCfg.Exchanges + + if c.CurrencyPairFormat != newCfg.CurrencyPairFormat { + c.CurrencyPairFormat = newCfg.CurrencyPairFormat + } + + c.Portfolio = newCfg.Portfolio + + err = newCfg.CheckSMSGlobalConfigValues() + if err != nil { + return err + } + c.SMS = newCfg.SMS + + err = c.SaveConfig(configPath) + if err != nil { + return err + } + + err = c.LoadConfig(configPath) + if err != nil { + return err + } + + return nil +} + // GetConfig returns a pointer to a confiuration object func GetConfig() *Config { return &Cfg diff --git a/config_routes.go b/config_routes.go deleted file mode 100644 index 8de3474e..00000000 --- a/config_routes.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "encoding/json" - "net/http" - - "github.com/thrasher-/gocryptotrader/config" -) - -// GetAllSettings replies to a request with an encoded JSON response about the -// trading bots configuration. -func GetAllSettings(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(bot.config); err != nil { - panic(err) - } -} - -// SaveAllSettings saves all current settings from request body as a JSON -// document then reloads state and returns the settings -func SaveAllSettings(w http.ResponseWriter, r *http.Request) { - //Get the data from the request - decoder := json.NewDecoder(r.Body) - var responseData config.Post - jsonerr := decoder.Decode(&responseData) - if jsonerr != nil { - panic(jsonerr) - } - //Save change the settings - for x := range bot.config.Exchanges { - for i := 0; i < len(responseData.Data.Exchanges); i++ { - if responseData.Data.Exchanges[i].Name == bot.config.Exchanges[x].Name { - bot.config.Exchanges[x].Enabled = responseData.Data.Exchanges[i].Enabled - bot.config.Exchanges[x].APIKey = responseData.Data.Exchanges[i].APIKey - bot.config.Exchanges[x].APISecret = responseData.Data.Exchanges[i].APISecret - bot.config.Exchanges[x].EnabledPairs = responseData.Data.Exchanges[i].EnabledPairs - } - } - } - //Reload the configuration - err := bot.config.SaveConfig(bot.configFile) - if err != nil { - panic(err) - } - err = bot.config.LoadConfig(bot.configFile) - if err != nil { - panic(err) - } - setupBotExchanges() - //Return response status - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(bot.config); err != nil { - panic(err) - } -} - -// ConfigRoutes declares the current routes for config_routes.go -var ConfigRoutes = Routes{ - Route{ - "GetAllSettings", - "GET", - "/config/all", - GetAllSettings, - }, - - Route{ - "SaveAllSettings", - "POST", - "/config/all/save", - SaveAllSettings, - }, -} diff --git a/currency/currency.go b/currency/currency.go index e29d8d7c..fe4a2b5b 100644 --- a/currency/currency.go +++ b/currency/currency.go @@ -68,7 +68,7 @@ var ( ErrCurrencyNotFound = errors.New("unable to find specified currency") ErrQueryingYahoo = errors.New("unable to query Yahoo currency values") ErrQueryingYahooZeroCount = errors.New("yahoo returned zero currency data") - yahooEnabled = false + YahooEnabled = false ) // IsDefaultCurrency checks if the currency passed in matches the default @@ -182,7 +182,7 @@ func SeedCurrencyData(fiatCurrencies string) error { fiatCurrencies = DefaultCurrencies } - if yahooEnabled { + if YahooEnabled { return QueryYahooCurrencyValues(fiatCurrencies) } @@ -215,7 +215,7 @@ func ConvertCurrency(amount float64, from, to string) (float64, error) { return amount, nil } - if yahooEnabled { + if YahooEnabled { currency := from + to _, ok := CurrencyStore[currency] if !ok { diff --git a/currency/currency_test.go b/currency/currency_test.go index e7a0ad00..3f354e01 100644 --- a/currency/currency_test.go +++ b/currency/currency_test.go @@ -264,7 +264,7 @@ func TestCheckAndAddCurrency(t *testing.T) { } func TestSeedCurrencyData(t *testing.T) { - if yahooEnabled { + if YahooEnabled { currencyRequestDefault := "" currencyRequestUSDAUD := "USD,AUD" currencyRequestObtuse := "WigWham" @@ -292,7 +292,7 @@ func TestSeedCurrencyData(t *testing.T) { } } - yahooEnabled = false + YahooEnabled = false err := SeedCurrencyData("") if err != nil { t.Errorf("Test failed. SeedCurrencyData via Fixer. Error: %s", err) @@ -313,7 +313,7 @@ func TestMakecurrencyPairs(t *testing.T) { } func TestConvertCurrency(t *testing.T) { - if yahooEnabled { + if YahooEnabled { fiatCurrencies := DefaultCurrencies for _, currencyFrom := range common.SplitStrings(fiatCurrencies, ",") { for _, currencyTo := range common.SplitStrings(fiatCurrencies, ",") { @@ -336,7 +336,7 @@ func TestConvertCurrency(t *testing.T) { } } - yahooEnabled = false + YahooEnabled = false _, err := ConvertCurrency(1000, "USD", "AUD") if err != nil { t.Errorf("Test failed. ConvertCurrency USD -> AUD. Error %s", err) @@ -383,7 +383,7 @@ func TestFetchFixerCurrencyData(t *testing.T) { } func TestFetchYahooCurrencyData(t *testing.T) { - if !yahooEnabled { + if !YahooEnabled { return } @@ -407,7 +407,7 @@ func TestFetchYahooCurrencyData(t *testing.T) { } func TestQueryYahooCurrencyValues(t *testing.T) { - if !yahooEnabled { + if !YahooEnabled { return } diff --git a/events/event_test.go b/events/event_test.go index c33c37bd..a57ef5d0 100644 --- a/events/event_test.go +++ b/events/event_test.go @@ -2,40 +2,42 @@ package events import ( "testing" + + "github.com/thrasher-/gocryptotrader/currency/pair" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" ) func TestAddEvent(t *testing.T) { - eventID, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", "SPOT", actionTest) + pair := pair.NewCurrencyPair("BTC", "USD") + eventID, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) if err != nil && eventID != 0 { t.Errorf("Test Failed. AddEvent: Error, %s", err) } - eventID, err = AddEvent("ANXX", "price", ">,==", "BTC", "LTC", "SPOT", actionTest) + eventID, err = AddEvent("ANXX", "price", ">,==", pair, "SPOT", actionTest) if err == nil && eventID == 0 { t.Error("Test Failed. AddEvent: Error, error not captured in Exchange") } - eventID, err = AddEvent("ANX", "prices", ">,==", "BTC", "LTC", "SPOT", actionTest) + eventID, err = AddEvent("ANX", "prices", ">,==", pair, "SPOT", actionTest) if err == nil && eventID == 0 { t.Error("Test Failed. AddEvent: Error, error not captured in Item") } - eventID, err = AddEvent("ANX", "price", "3===D", "BTC", "LTC", "SPOT", actionTest) + eventID, err = AddEvent("ANX", "price", "3===D", pair, "SPOT", actionTest) if err == nil && eventID == 0 { t.Error("Test Failed. AddEvent: Error, error not captured in Condition") } - eventID, err = AddEvent("ANX", "price", ">,==", "BTC", "LTC", "SPOT", "console_prints") - if err == nil && eventID == 0 { - t.Error("Test Failed. AddEvent: Error, error not captured in Action") - } - eventID, err = AddEvent("ANX", "price", ">,==", "BATMAN", "ROBIN", "SPOT", actionTest) + eventID, err = AddEvent("ANX", "price", ">,==", pair, "SPOT", "console_prints") if err == nil && eventID == 0 { t.Error("Test Failed. AddEvent: Error, error not captured in Action") } + if !RemoveEvent(eventID) { t.Error("Test Failed. RemoveEvent: Error, error removing event") } } func TestRemoveEvent(t *testing.T) { - eventID, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", "SPOT", actionTest) + pair := pair.NewCurrencyPair("BTC", "USD") + eventID, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) if err != nil && eventID != 0 { t.Errorf("Test Failed. RemoveEvent: Error, %s", err) } @@ -48,15 +50,16 @@ func TestRemoveEvent(t *testing.T) { } func TestGetEventCounter(t *testing.T) { - one, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", "SPOT", actionTest) + pair := pair.NewCurrencyPair("BTC", "USD") + one, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) if err != nil { t.Errorf("Test Failed. GetEventCounter: Error, %s", err) } - two, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", "SPOT", actionTest) + two, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) if err != nil { t.Errorf("Test Failed. GetEventCounter: Error, %s", err) } - three, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", "SPOT", actionTest) + three, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) if err != nil { t.Errorf("Test Failed. GetEventCounter: Error, %s", err) } @@ -84,9 +87,10 @@ func TestGetEventCounter(t *testing.T) { } func TestExecuteAction(t *testing.T) { - one, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", "SPOT", actionTest) + pair := pair.NewCurrencyPair("BTC", "USD") + one, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) if err != nil { - t.Errorf("Test Failed. ExecuteAction: Error, %s", err) + t.Fatalf("Test Failed. ExecuteAction: Error, %s", err) } isExecuted := Events[one].ExecuteAction() if !isExecuted { @@ -96,17 +100,46 @@ func TestExecuteAction(t *testing.T) { t.Error("Test Failed. ExecuteAction: Error, error removing event") } + action := actionSMSNotify + "," + "ALL" + one, err = AddEvent("ANX", "price", ">,==", pair, "SPOT", action) + if err != nil { + t.Fatalf("Test Failed. ExecuteAction: Error, %s", err) + } + + isExecuted = Events[one].ExecuteAction() + if !isExecuted { + t.Error("Test Failed. ExecuteAction: Error, error removing event") + } + if !RemoveEvent(one) { + t.Error("Test Failed. ExecuteAction: Error, error removing event") + } + + action = actionSMSNotify + "," + "StyleGherkin" + one, err = AddEvent("ANX", "price", ">,==", pair, "SPOT", action) + if err != nil { + t.Fatalf("Test Failed. ExecuteAction: Error, %s", err) + } + + isExecuted = Events[one].ExecuteAction() + if !isExecuted { + t.Error("Test Failed. ExecuteAction: Error, error removing event") + } + if !RemoveEvent(one) { + t.Error("Test Failed. ExecuteAction: Error, error removing event") + } + // More tests when ExecuteAction is expanded } func TestEventToString(t *testing.T) { - one, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", "SPOT", actionTest) + pair := pair.NewCurrencyPair("BTC", "USD") + one, err := AddEvent("ANX", "price", ">,==", pair, "SPOT", actionTest) if err != nil { t.Errorf("Test Failed. EventToString: Error, %s", err) } - eventString := Events[one].EventToString() - if eventString != "If the BTCLTC [SPOT] price on ANX is > == then ACTION_TEST." { + eventString := Events[one].String() + if eventString != "If the BTCUSD [SPOT] price on ANX is > == then ACTION_TEST." { t.Error("Test Failed. EventToString: Error, incorrect return string") } @@ -115,64 +148,118 @@ func TestEventToString(t *testing.T) { } } -func TestCheckCondition(t *testing.T) { //error handling needs to be implemented - one, err := AddEvent("ANX", "price", ">,==", "BTC", "LTC", "SPOT", actionTest) +func TestCheckCondition(t *testing.T) { + // Test invalid currency pair + newPair := pair.NewCurrencyPair("A", "B") + one, err := AddEvent("ANX", "price", ">=,10", newPair, "SPOT", actionTest) if err != nil { - t.Errorf("Test Failed. EventToString: Error, %s", err) + t.Errorf("Test Failed. CheckCondition: Error, %s", err) + } + conditionBool := Events[one].CheckCondition() + if conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") } - conditionBool := Events[one].CheckCondition() - if conditionBool { //check once error handling is implemented - t.Error("Test Failed. EventToString: Error, wrong conditional.") + // Test last price == 0 + var tickerNew ticker.Price + tickerNew.Last = 0 + newPair = pair.NewCurrencyPair("BTC", "USD") + ticker.ProcessTicker("ANX", newPair, tickerNew, ticker.Spot) + Events[one].Pair = newPair + conditionBool = Events[one].CheckCondition() + if conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") + } + + // Test last pricce > 0 and conditional logic + tickerNew.Last = 11 + ticker.ProcessTicker("ANX", newPair, tickerNew, ticker.Spot) + Events[one].Condition = ">,10" + conditionBool = Events[one].CheckCondition() + if !conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") + } + + // Test last price >= 10 + Events[one].Condition = ">=,10" + conditionBool = Events[one].CheckCondition() + if !conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") + } + + // Test last price <= 10 + Events[one].Condition = "<,100" + conditionBool = Events[one].CheckCondition() + if !conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") + } + + // Test last price <= 10 + Events[one].Condition = "<=,100" + conditionBool = Events[one].CheckCondition() + if !conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") + } + + Events[one].Condition = "==,11" + conditionBool = Events[one].CheckCondition() + if !conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") + } + + Events[one].Condition = "^,11" + conditionBool = Events[one].CheckCondition() + if conditionBool { + t.Error("Test Failed. CheckCondition: Error, wrong conditional.") } if !RemoveEvent(one) { - t.Error("Test Failed. EventToString: Error, error removing event") + t.Error("Test Failed. CheckCondition: Error, error removing event") } } func TestIsValidEvent(t *testing.T) { err := IsValidEvent("ANX", "price", ">,==", actionTest) if err != nil { - t.Errorf("Test Failed. IsValidExchange: Error %s", err) + t.Errorf("Test Failed. IsValidEvent: %s", err) } err = IsValidEvent("ANX", "price", ">,", actionTest) if err == nil { - t.Errorf("Test Failed. IsValidExchange: Error") + t.Errorf("Test Failed. IsValidEvent: %s", err) } err = IsValidEvent("ANX", "Testy", ">,==", actionTest) if err == nil { - t.Errorf("Test Failed. IsValidExchange: Error") + t.Errorf("Test Failed. IsValidEvent: %s", err) } err = IsValidEvent("Testys", "price", ">,==", actionTest) if err == nil { - t.Errorf("Test Failed. IsValidExchange: Error") + t.Errorf("Test Failed. IsValidEvent: %s", err) + } + + action := "blah,blah" + err = IsValidEvent("ANX", "price", ">=,10", action) + if err == nil { + t.Errorf("Test Failed. IsValidEvent: %s", err) + } + + action = "SMS,blah" + err = IsValidEvent("ANX", "price", ">=,10", action) + if err == nil { + t.Errorf("Test Failed. IsValidEvent: %s", err) } //Function tests need to appended to this function when more actions are //implemented } -func TestCheckEvents(t *testing.T) { //Add error handling - //CheckEvents() //check once error handling is implemented -} +func TestCheckEvents(t *testing.T) { + pair := pair.NewCurrencyPair("BTC", "USD") + _, err := AddEvent("ANX", "price", ">=,10", pair, "SPOT", actionTest) + if err != nil { + t.Fatal("Test failed. TestChcheckEvents add event") + } -func TestIsValidCurrency(t *testing.T) { - if !IsValidCurrency("BTC") { - t.Error("Test Failed - Event_test.go TestIsValidCurrency Error") - } - if !IsValidCurrency("USD") { - t.Error("Test Failed - Event_test.go TestIsValidCurrency Error") - } - if IsValidCurrency("testy") { - t.Error("Test Failed - Event_test.go TestIsValidCurrency Error") - } - if !IsValidCurrency("USD", "BTC", "USD") { - t.Error("Test Failed - Event_test.go TestIsValidCurrency Error") - } - if IsValidCurrency("USD", "USD", "Wigwham") { - t.Error("Test Failed - Event_test.go TestIsValidCurrency Error") - } + go CheckEvents() } func TestIsValidExchange(t *testing.T) { diff --git a/events/events.go b/events/events.go index 9105ec06..e14a11c3 100644 --- a/events/events.go +++ b/events/events.go @@ -8,7 +8,6 @@ import ( "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" - "github.com/thrasher-/gocryptotrader/currency" "github.com/thrasher-/gocryptotrader/currency/pair" "github.com/thrasher-/gocryptotrader/exchanges/ticker" "github.com/thrasher-/gocryptotrader/smsglobal" @@ -32,20 +31,18 @@ var ( errInvalidCondition = errors.New("invalid conditional option") errInvalidAction = errors.New("invalid action") errExchangeDisabled = errors.New("desired exchange is disabled") - errCurrencyInvalid = errors.New("invalid currency") ) // Event struct holds the event variables type Event struct { - ID int - Exchange string - Item string - Condition string - FirstCurrency string - Asset string - SecondCurrency string - Action string - Executed bool + ID int + Exchange string + Item string + Condition string + Pair pair.CurrencyPair + Asset string + Action string + Executed bool } // Events variable is a pointer array to the event structures that will be @@ -54,16 +51,12 @@ var Events []*Event // AddEvent adds an event to the Events chain and returns an index/eventID // and an error -func AddEvent(Exchange, Item, Condition, FirstCurrency, SecondCurrency, Asset, Action string) (int, error) { +func AddEvent(Exchange, Item, Condition string, CurrencyPair pair.CurrencyPair, Asset, Action string) (int, error) { err := IsValidEvent(Exchange, Item, Condition, Action) if err != nil { return 0, err } - if !IsValidCurrency(FirstCurrency, SecondCurrency) { - return 0, errCurrencyInvalid - } - Event := &Event{} if len(Events) == 0 { @@ -75,8 +68,7 @@ func AddEvent(Exchange, Item, Condition, FirstCurrency, SecondCurrency, Asset, A Event.Exchange = Exchange Event.Item = Item Event.Condition = Condition - Event.FirstCurrency = FirstCurrency - Event.SecondCurrency = SecondCurrency + Event.Pair = CurrencyPair Event.Asset = Asset Event.Action = Action Event.Executed = false @@ -114,7 +106,7 @@ func (e *Event) ExecuteAction() bool { if common.StringContains(e.Action, ",") { action := common.SplitStrings(e.Action, ",") if action[0] == actionSMSNotify { - message := fmt.Sprintf("Event triggered: %s", e.EventToString()) + message := fmt.Sprintf("Event triggered: %s", e.String()) if action[1] == "ALL" { smsglobal.SMSSendToAll(message, config.Cfg) } else { @@ -124,17 +116,17 @@ func (e *Event) ExecuteAction() bool { } } } else { - log.Printf("Event triggered: %s", e.EventToString()) + log.Printf("Event triggered: %s", e.String()) } return true } // EventToString turns the structure event into a string -func (e *Event) EventToString() string { +func (e *Event) String() string { condition := common.SplitStrings(e.Condition, ",") return fmt.Sprintf( - "If the %s%s [%s] %s on %s is %s then %s.", e.FirstCurrency, e.SecondCurrency, - e.Asset, e.Item, e.Exchange, condition[0]+" "+condition[1], e.Action, + "If the %s%s [%s] %s on %s is %s then %s.", e.Pair.FirstCurrency.String(), + e.Pair.SecondCurrency.String(), e.Asset, e.Item, e.Exchange, condition[0]+" "+condition[1], e.Action, ) } @@ -144,12 +136,12 @@ func (e *Event) CheckCondition() bool { condition := common.SplitStrings(e.Condition, ",") targetPrice, _ := strconv.ParseFloat(condition[1], 64) - ticker, err := ticker.GetTickerByExchange(e.Exchange) + t, err := ticker.GetTicker(e.Exchange, e.Pair, e.Asset) if err != nil { return false } - lastPrice := ticker.Price[pair.CurrencyItem(e.FirstCurrency)][pair.CurrencyItem(e.SecondCurrency)][e.Asset].Last + lastPrice := t.Last if lastPrice == 0 { return false @@ -226,8 +218,9 @@ func IsValidEvent(Exchange, Item, Condition, Action string) error { return errInvalidAction } + cfg := config.GetConfig() if action[1] != "ALL" && smsglobal.SMSGetNumberByName( - action[1], config.Cfg.SMS) == smsglobal.ErrSMSContactNotFound { + action[1], cfg.SMS) == smsglobal.ErrSMSContactNotFound { return errInvalidAction } } else { @@ -260,20 +253,6 @@ func CheckEvents() { } } -// IsValidCurrency takes in CRYPTO or FIAT currency strings and returns if valid -func IsValidCurrency(currencies ...string) bool { - for index, whatIsIt := range currencies { - whatIsIt = common.StringToUpper(whatIsIt) - if currency.IsDefaultCryptocurrency(whatIsIt) || currency.IsDefaultCurrency(whatIsIt) { - if len(currencies)-1 == index { - return true - } - continue - } - } - return false -} - // IsValidExchange validates the exchange func IsValidExchange(Exchange, configPath string) bool { Exchange = common.StringToUpper(Exchange) diff --git a/helpers.go b/helpers.go new file mode 100644 index 00000000..554f66ff --- /dev/null +++ b/helpers.go @@ -0,0 +1,108 @@ +package main + +import ( + "errors" + "fmt" + + "github.com/thrasher-/gocryptotrader/exchanges/stats" + + "github.com/thrasher-/gocryptotrader/currency/pair" + exchange "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" +) + +// GetSpecificOrderbook returns a specific orderbook given the currency, +// exchangeName and assetType +func GetSpecificOrderbook(currency, exchangeName, assetType string) (orderbook.Base, error) { + var specificOrderbook orderbook.Base + var err error + for i := 0; i < len(bot.exchanges); i++ { + if bot.exchanges[i] != nil { + if bot.exchanges[i].IsEnabled() && bot.exchanges[i].GetName() == exchangeName { + specificOrderbook, err = bot.exchanges[i].GetOrderbookEx( + pair.NewCurrencyPairFromString(currency), + assetType, + ) + break + } + } + } + return specificOrderbook, err +} + +// GetSpecificTicker returns a specific ticker given the currency, +// exchangeName and assetType +func GetSpecificTicker(currency, exchangeName, assetType string) (ticker.Price, error) { + var specificTicker ticker.Price + var err error + for i := 0; i < len(bot.exchanges); i++ { + if bot.exchanges[i] != nil { + if bot.exchanges[i].IsEnabled() && bot.exchanges[i].GetName() == exchangeName { + specificTicker, err = bot.exchanges[i].GetTickerPrice( + pair.NewCurrencyPairFromString(currency), + assetType, + ) + break + } + } + } + return specificTicker, err +} + +// GetCollatedExchangeAccountInfoByCoin collates individual exchange account +// information and turns into into a map string of +// exchange.AccountCurrencyInfo +func GetCollatedExchangeAccountInfoByCoin(accounts []exchange.AccountInfo) map[string]exchange.AccountCurrencyInfo { + result := make(map[string]exchange.AccountCurrencyInfo) + for i := 0; i < len(accounts); i++ { + for j := 0; j < len(accounts[i].Currencies); j++ { + currencyName := accounts[i].Currencies[j].CurrencyName + avail := accounts[i].Currencies[j].TotalValue + onHold := accounts[i].Currencies[j].Hold + + info, ok := result[currencyName] + if !ok { + accountInfo := exchange.AccountCurrencyInfo{CurrencyName: currencyName, Hold: onHold, TotalValue: avail} + result[currencyName] = accountInfo + } else { + info.Hold += onHold + info.TotalValue += avail + result[currencyName] = info + } + } + } + return result +} + +// GetAccountCurrencyInfoByExchangeName returns info for an exchange +func GetAccountCurrencyInfoByExchangeName(accounts []exchange.AccountInfo, exchangeName string) (exchange.AccountInfo, error) { + for i := 0; i < len(accounts); i++ { + if accounts[i].ExchangeName == exchangeName { + return accounts[i], nil + } + } + return exchange.AccountInfo{}, errors.New(exchange.ErrExchangeNotFound) +} + +// GetExchangeHighestPriceByCurrencyPair returns the exchange with the highest +// price for a given currency pair and asset type +func GetExchangeHighestPriceByCurrencyPair(p pair.CurrencyPair, assetType string) (string, error) { + result := stats.SortExchangesByPrice(p, assetType, true) + if len(result) != 1 { + return "", fmt.Errorf("no stats for supplied currency pair and asset type") + } + + return result[0].Exchange, nil +} + +// GetExchangeLowestPriceByCurrencyPair returns the exchange with the lowest +// price for a given currency pair and asset type +func GetExchangeLowestPriceByCurrencyPair(p pair.CurrencyPair, assetType string) (string, error) { + result := stats.SortExchangesByPrice(p, assetType, false) + if len(result) != 1 { + return "", fmt.Errorf("no stats for supplied currency pair and asset type") + } + + return result[0].Exchange, nil +} diff --git a/main.go b/main.go index 08a6b9d8..818b2081 100644 --- a/main.go +++ b/main.go @@ -119,16 +119,10 @@ func main() { AdjustGoMaxProcs() if bot.config.SMS.Enabled { - err = bot.config.CheckSMSGlobalConfigValues() - if err != nil { - log.Println(err) // non fatal event - bot.config.SMS.Enabled = false - } else { - log.Printf( - "SMS support enabled. Number of SMS contacts %d.\n", - smsglobal.GetEnabledSMSContacts(bot.config.SMS), - ) - } + log.Printf( + "SMS support enabled. Number of SMS contacts %d.\n", + smsglobal.GetEnabledSMSContacts(bot.config.SMS), + ) } else { log.Println("SMS support disabled.") } @@ -174,7 +168,6 @@ func main() { setupBotExchanges() bot.config.RetrieveConfigCurrencyPairs() - err = currency.SeedCurrencyData(currency.BaseCurrencies) if err != nil { log.Fatalf("Fatal error retrieving config currencies. Error: %s", err) @@ -194,21 +187,14 @@ func main() { go OrderbookUpdaterRoutine() if bot.config.Webserver.Enabled { - err := bot.config.CheckWebserverConfigValues() - if err != nil { - log.Println(err) // non fatal event - //bot.config.Webserver.Enabled = false - } else { - listenAddr := bot.config.Webserver.ListenAddress - log.Printf( - "HTTP RESTful Webserver support enabled. Listen URL: http://%s:%d/\n", - common.ExtractHost(listenAddr), common.ExtractPort(listenAddr), - ) - router := NewRouter(bot.exchanges) - log.Fatal(http.ListenAndServe(listenAddr, router)) - } - } - if !bot.config.Webserver.Enabled { + listenAddr := bot.config.Webserver.ListenAddress + log.Printf( + "HTTP Webserver support enabled. Listen URL: http://%s:%d/\n", + common.ExtractHost(listenAddr), common.ExtractPort(listenAddr), + ) + router := NewRouter(bot.exchanges) + log.Fatal(http.ListenAndServe(listenAddr, router)) + } else { log.Println("HTTP RESTful Webserver support disabled.") } diff --git a/orderbook_routes.go b/orderbook_routes.go deleted file mode 100644 index 6a708739..00000000 --- a/orderbook_routes.go +++ /dev/null @@ -1,141 +0,0 @@ -package main - -import ( - "encoding/json" - "log" - "net/http" - - "github.com/gorilla/mux" - "github.com/thrasher-/gocryptotrader/currency/pair" - exchange "github.com/thrasher-/gocryptotrader/exchanges" - "github.com/thrasher-/gocryptotrader/exchanges/orderbook" -) - -// GetSpecificOrderbook returns a specific orderbook given the currency, -// exchangeName and assetType -func GetSpecificOrderbook(currency, exchangeName, assetType string) (orderbook.Base, error) { - var specificOrderbook orderbook.Base - var err error - for i := 0; i < len(bot.exchanges); i++ { - if bot.exchanges[i] != nil { - if bot.exchanges[i].IsEnabled() && bot.exchanges[i].GetName() == exchangeName { - specificOrderbook, err = bot.exchanges[i].GetOrderbookEx( - pair.NewCurrencyPairFromString(currency), - assetType, - ) - break - } - } - } - return specificOrderbook, err -} - -func jsonOrderbookResponse(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - currency := vars["currency"] - exchange := vars["exchangeName"] - assetType := vars["assetType"] - - if assetType == "" { - assetType = orderbook.Spot - } - - response, err := GetSpecificOrderbook(currency, exchange, assetType) - if err != nil { - log.Printf("Failed to fetch orderbook for %s currency: %s\n", exchange, - currency) - return - } - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(response); err != nil { - panic(err) - } -} - -// AllEnabledExchangeOrderbooks holds the enabled exchange orderbooks -type AllEnabledExchangeOrderbooks struct { - Data []EnabledExchangeOrderbooks `json:"data"` -} - -// EnabledExchangeOrderbooks is a sub type for singular exchanges and respective -// orderbooks -type EnabledExchangeOrderbooks struct { - ExchangeName string `json:"exchangeName"` - ExchangeValues []orderbook.Base `json:"exchangeValues"` -} - -// GetAllActiveOrderbooks returns all enabled exchanges orderbooks -func GetAllActiveOrderbooks() []EnabledExchangeOrderbooks { - var orderbookData []EnabledExchangeOrderbooks - - for _, individualBot := range bot.exchanges { - if individualBot != nil && individualBot.IsEnabled() { - var individualExchange EnabledExchangeOrderbooks - exchangeName := individualBot.GetName() - individualExchange.ExchangeName = exchangeName - currencies := individualBot.GetEnabledCurrencies() - assetTypes, err := exchange.GetExchangeAssetTypes(exchangeName) - if err != nil { - log.Printf("failed to get %s exchange asset types. Error: %s", - exchangeName, err) - continue - } - for _, x := range currencies { - currency := x - - var ob orderbook.Base - if len(assetTypes) > 1 { - for y := range assetTypes { - ob, err = individualBot.UpdateOrderbook(currency, - assetTypes[y]) - } - } else { - ob, err = individualBot.UpdateOrderbook(currency, - assetTypes[0]) - } - - if err != nil { - log.Printf("failed to get %s %s orderbook. Error: %s", - currency.Pair().String(), - exchangeName, - err) - continue - } - - individualExchange.ExchangeValues = append( - individualExchange.ExchangeValues, ob, - ) - } - orderbookData = append(orderbookData, individualExchange) - } - } - return orderbookData -} - -func getAllActiveOrderbooksResponse(w http.ResponseWriter, r *http.Request) { - var response AllEnabledExchangeOrderbooks - response.Data = GetAllActiveOrderbooks() - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(response); err != nil { - panic(err) - } -} - -// OrderbookRoutes denotes the current exchange orderbook routes -var OrderbookRoutes = Routes{ - Route{ - "AllActiveExchangesAndOrderbooks", - "GET", - "/exchanges/orderbook/latest/all", - getAllActiveOrderbooksResponse, - }, - Route{ - "IndividualExchangeOrderbook", - "GET", - "/exchanges/{exchangeName}/orderbook/latest/{currency}", - jsonOrderbookResponse, - }, -} diff --git a/portfolio_routes.go b/portfolio_routes.go deleted file mode 100644 index d2117160..00000000 --- a/portfolio_routes.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "encoding/json" - "net/http" -) - -// RESTGetPortfolio replies to a request with an encoded JSON response of the -// portfolio -func RESTGetPortfolio(w http.ResponseWriter, r *http.Request) { - result := bot.portfolio.GetPortfolioSummary() - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(result); err != nil { - panic(err) - } -} - -// PortfolioRoutes declares the current routes for config_routes.go -var PortfolioRoutes = Routes{ - Route{ - "GetPortfolio", - "GET", - "/portfolio/all", - RESTGetPortfolio, - }, -} diff --git a/restful_logger.go b/restful_logger.go deleted file mode 100644 index 2025c5a3..00000000 --- a/restful_logger.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "log" - "net/http" - "time" -) - -// Logger logs the requests internally -func Logger(inner http.Handler, name string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - - inner.ServeHTTP(w, r) - - log.Printf( - "%s\t%s\t%s\t%s", - r.Method, - r.RequestURI, - name, - time.Since(start), - ) - }) -} diff --git a/restful_router.go b/restful_router.go index f151bb3d..bd335797 100644 --- a/restful_router.go +++ b/restful_router.go @@ -2,27 +2,104 @@ package main import ( "fmt" + "log" "net/http" + "time" "github.com/gorilla/mux" "github.com/thrasher-/gocryptotrader/exchanges" ) +// RESTLogger logs the requests internally +func RESTLogger(inner http.Handler, name string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + inner.ServeHTTP(w, r) + + log.Printf( + "%s\t%s\t%s\t%s", + r.Method, + r.RequestURI, + name, + time.Since(start), + ) + }) +} + +// Route is a sub type that holds the request routes +type Route struct { + Name string + Method string + Pattern string + HandlerFunc http.HandlerFunc +} + +// Routes is an array of all the registered routes +type Routes []Route + +var routes = Routes{} + // NewRouter takes in the exchange interfaces and returns a new multiplexor // router func NewRouter(exchanges []exchange.IBotExchange) *mux.Router { router := mux.NewRouter().StrictSlash(true) - allRoutes := append(routes, ExchangeRoutes...) - allRoutes = append(allRoutes, ConfigRoutes...) - allRoutes = append(allRoutes, PortfolioRoutes...) - allRoutes = append(allRoutes, WalletRoutes...) - allRoutes = append(allRoutes, IndexRoute...) - allRoutes = append(allRoutes, WebsocketRoutes...) - allRoutes = append(allRoutes, OrderbookRoutes...) - for _, route := range allRoutes { + + routes = Routes{ + Route{ + "GetAllSettings", + "GET", + "/config/all", + RESTGetAllSettings, + }, + Route{ + "SaveAllSettings", + "POST", + "/config/all/save", + RESTSaveAllSettings, + }, + Route{ + "AllEnabledAccountInfo", + "GET", + "/exchanges/enabled/accounts/all", + RESTGetAllEnabledAccountInfo, + }, + Route{ + "AllActiveExchangesAndCurrencies", + "GET", + "/exchanges/enabled/latest/all", + RESTGetAllActiveTickers, + }, + Route{ + "IndividualExchangeAndCurrency", + "GET", + "/exchanges/{exchangeName}/latest/{currency}", + RESTGetTicker, + }, + Route{ + "GetPortfolio", + "GET", + "/portfolio/all", + RESTGetPortfolio, + }, + Route{ + "AllActiveExchangesAndOrderbooks", + "GET", + "/exchanges/orderbook/latest/all", + RESTGetAllActiveOrderbooks, + }, + Route{ + "IndividualExchangeOrderbook", + "GET", + "/exchanges/{exchangeName}/orderbook/latest/{currency}", + RESTGetOrderbook, + }, + } + + for _, route := range routes { var handler http.Handler handler = route.HandlerFunc - handler = Logger(handler, route.Name) + handler = RESTLogger(handler, route.Name) router. Methods(route.Method). diff --git a/restful_router_test.go b/restful_router_test.go deleted file mode 100644 index e7fd9cf8..00000000 --- a/restful_router_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -import ( - "testing" -) - -func TestNewRouter(t *testing.T) { - if value := NewRouter(bot.exchanges); value.KeepContext { - t.Error("Test Failed - Restful_Router_Test.go - NewRouter Error") - } -} diff --git a/restful_routes.go b/restful_routes.go deleted file mode 100644 index 4728f9be..00000000 --- a/restful_routes.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import "net/http" - -// Route is a sub type that holds the request routes -type Route struct { - Name string - Method string - Pattern string - HandlerFunc http.HandlerFunc -} - -// Routes is an array of all the registered routes -type Routes []Route - -var routes = Routes{} diff --git a/restful_server.go b/restful_server.go new file mode 100644 index 00000000..f5103227 --- /dev/null +++ b/restful_server.go @@ -0,0 +1,297 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/gorilla/mux" + "github.com/thrasher-/gocryptotrader/config" + exchange "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" +) + +// AllEnabledExchangeOrderbooks holds the enabled exchange orderbooks +type AllEnabledExchangeOrderbooks struct { + Data []EnabledExchangeOrderbooks `json:"data"` +} + +// EnabledExchangeOrderbooks is a sub type for singular exchanges and respective +// orderbooks +type EnabledExchangeOrderbooks struct { + ExchangeName string `json:"exchangeName"` + ExchangeValues []orderbook.Base `json:"exchangeValues"` +} + +// AllEnabledExchangeCurrencies holds the enabled exchange currencies +type AllEnabledExchangeCurrencies struct { + Data []EnabledExchangeCurrencies `json:"data"` +} + +// EnabledExchangeCurrencies is a sub type for singular exchanges and respective +// currencies +type EnabledExchangeCurrencies struct { + ExchangeName string `json:"exchangeName"` + ExchangeValues []ticker.Price `json:"exchangeValues"` +} + +// AllEnabledExchangeAccounts holds all enabled accounts info +type AllEnabledExchangeAccounts struct { + Data []exchange.AccountInfo `json:"data"` +} + +// RESTfulJSONResponse outputs a JSON response of the req interface +func RESTfulJSONResponse(w http.ResponseWriter, r *http.Request, req interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(req); err != nil { + return err + } + return nil +} + +// RESTfulError prints the REST method and error +func RESTfulError(method string, err error) { + log.Printf("RESTful %s: server failed to send JSON response. Error %s", + method, err) +} + +// RESTGetAllSettings replies to a request with an encoded JSON response about the +// trading bots configuration. +func RESTGetAllSettings(w http.ResponseWriter, r *http.Request) { + err := RESTfulJSONResponse(w, r, bot.config) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// RESTSaveAllSettings saves all current settings from request body as a JSON +// document then reloads state and returns the settings +func RESTSaveAllSettings(w http.ResponseWriter, r *http.Request) { + //Get the data from the request + decoder := json.NewDecoder(r.Body) + var responseData config.Post + err := decoder.Decode(&responseData) + if err != nil { + RESTfulError(r.Method, err) + } + //Save change the settings + err = bot.config.UpdateConfig(bot.configFile, responseData.Data) + if err != nil { + RESTfulError(r.Method, err) + } + + err = RESTfulJSONResponse(w, r, bot.config) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// RESTGetOrderbook returns orderbook info for a given currency, exchange and +// asset type +func RESTGetOrderbook(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + currency := vars["currency"] + exchange := vars["exchangeName"] + assetType := vars["assetType"] + + if assetType == "" { + assetType = orderbook.Spot + } + + response, err := GetSpecificOrderbook(currency, exchange, assetType) + if err != nil { + log.Printf("Failed to fetch orderbook for %s currency: %s\n", exchange, + currency) + return + } + + err = RESTfulJSONResponse(w, r, response) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// GetAllActiveOrderbooks returns all enabled exchanges orderbooks +func GetAllActiveOrderbooks() []EnabledExchangeOrderbooks { + var orderbookData []EnabledExchangeOrderbooks + + for _, individualBot := range bot.exchanges { + if individualBot != nil && individualBot.IsEnabled() { + var individualExchange EnabledExchangeOrderbooks + exchangeName := individualBot.GetName() + individualExchange.ExchangeName = exchangeName + currencies := individualBot.GetEnabledCurrencies() + assetTypes, err := exchange.GetExchangeAssetTypes(exchangeName) + if err != nil { + log.Printf("failed to get %s exchange asset types. Error: %s", + exchangeName, err) + continue + } + for _, x := range currencies { + currency := x + + var ob orderbook.Base + if len(assetTypes) > 1 { + for y := range assetTypes { + ob, err = individualBot.GetOrderbookEx(currency, + assetTypes[y]) + } + } else { + ob, err = individualBot.GetOrderbookEx(currency, + assetTypes[0]) + } + + if err != nil { + log.Printf("failed to get %s %s orderbook. Error: %s", + currency.Pair().String(), + exchangeName, + err) + continue + } + + individualExchange.ExchangeValues = append( + individualExchange.ExchangeValues, ob, + ) + } + orderbookData = append(orderbookData, individualExchange) + } + } + return orderbookData +} + +// RESTGetAllActiveOrderbooks returns all enabled exchange orderbooks +func RESTGetAllActiveOrderbooks(w http.ResponseWriter, r *http.Request) { + var response AllEnabledExchangeOrderbooks + response.Data = GetAllActiveOrderbooks() + + err := RESTfulJSONResponse(w, r, response) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// RESTGetPortfolio returns the bot portfolio +func RESTGetPortfolio(w http.ResponseWriter, r *http.Request) { + result := bot.portfolio.GetPortfolioSummary() + err := RESTfulJSONResponse(w, r, result) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// RESTGetTicker returns ticker info for a given currency, exchange and +// asset type +func RESTGetTicker(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + currency := vars["currency"] + exchange := vars["exchangeName"] + assetType := vars["assetType"] + + if assetType == "" { + assetType = ticker.Spot + } + response, err := GetSpecificTicker(currency, exchange, assetType) + if err != nil { + log.Printf("Failed to fetch ticker for %s currency: %s\n", exchange, + currency) + return + } + err = RESTfulJSONResponse(w, r, response) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// GetAllActiveTickers returns all enabled exchange tickers +func GetAllActiveTickers() []EnabledExchangeCurrencies { + var tickerData []EnabledExchangeCurrencies + + for _, individualBot := range bot.exchanges { + if individualBot != nil && individualBot.IsEnabled() { + var individualExchange EnabledExchangeCurrencies + exchangeName := individualBot.GetName() + individualExchange.ExchangeName = exchangeName + log.Println( + "Getting enabled currencies for '" + exchangeName + "'", + ) + currencies := individualBot.GetEnabledCurrencies() + for _, x := range currencies { + currency := x + assetTypes, err := exchange.GetExchangeAssetTypes(exchangeName) + if err != nil { + log.Printf("failed to get %s exchange asset types. Error: %s", + exchangeName, err) + continue + } + var tickerPrice ticker.Price + if len(assetTypes) > 1 { + for y := range assetTypes { + tickerPrice, err = individualBot.GetTickerPrice(currency, + assetTypes[y]) + } + } else { + tickerPrice, err = individualBot.GetTickerPrice(currency, + assetTypes[0]) + } + + if err != nil { + log.Printf("failed to get %s %s ticker. Error: %s", + currency.Pair().String(), + exchangeName, + err) + continue + } + + individualExchange.ExchangeValues = append( + individualExchange.ExchangeValues, tickerPrice, + ) + } + tickerData = append(tickerData, individualExchange) + } + } + return tickerData +} + +// RESTGetAllActiveTickers returns all active tickers +func RESTGetAllActiveTickers(w http.ResponseWriter, r *http.Request) { + var response AllEnabledExchangeCurrencies + response.Data = GetAllActiveTickers() + + err := RESTfulJSONResponse(w, r, response) + if err != nil { + RESTfulError(r.Method, err) + } +} + +// GetAllEnabledExchangeAccountInfo returns all the current enabled exchanges +func GetAllEnabledExchangeAccountInfo() AllEnabledExchangeAccounts { + var response AllEnabledExchangeAccounts + for _, individualBot := range bot.exchanges { + if individualBot != nil && individualBot.IsEnabled() { + if !individualBot.GetAuthenticatedAPISupport() { + log.Printf("GetAllEnabledExchangeAccountInfo: Skippping %s due to disabled authenticated API support.", individualBot.GetName()) + continue + } + individualExchange, err := individualBot.GetExchangeAccountInfo() + if err != nil { + log.Printf("Error encountered retrieving exchange account info for %s. Error %s", + individualBot.GetName(), err) + continue + } + response.Data = append(response.Data, individualExchange) + } + } + return response +} + +// RESTGetAllEnabledAccountInfo via get request returns JSON response of account +// info +func RESTGetAllEnabledAccountInfo(w http.ResponseWriter, r *http.Request) { + response := GetAllEnabledExchangeAccountInfo() + err := RESTfulJSONResponse(w, r, response) + if err != nil { + RESTfulError(r.Method, err) + } +} diff --git a/smsglobal/smsglobal.go b/smsglobal/smsglobal.go index 803407ab..55fa03ca 100644 --- a/smsglobal/smsglobal.go +++ b/smsglobal/smsglobal.go @@ -2,6 +2,7 @@ package smsglobal import ( "errors" + "flag" "log" "net/url" "strings" @@ -44,7 +45,7 @@ func SMSSendToAll(message string, cfg config.Config) { // SMSGetNumberByName returns contact number by supplied name func SMSGetNumberByName(name string, smsCfg config.SMSGlobalConfig) string { for _, contact := range smsCfg.Contacts { - if contact.Name == name { + if common.StringToUpper(contact.Name) == common.StringToUpper(name) { return contact.Number } } @@ -53,6 +54,10 @@ func SMSGetNumberByName(name string, smsCfg config.SMSGlobalConfig) string { // SMSNotify sends a message to an individual contact func SMSNotify(to, message string, cfg config.Config) error { + if flag.Lookup("test.v") != nil { + return nil + } + values := url.Values{} values.Set("action", "sendsms") values.Set("user", cfg.SMS.Username) diff --git a/ticker_routes.go b/ticker_routes.go deleted file mode 100644 index 7c49949c..00000000 --- a/ticker_routes.go +++ /dev/null @@ -1,139 +0,0 @@ -package main - -import ( - "encoding/json" - "log" - "net/http" - - "github.com/gorilla/mux" - "github.com/thrasher-/gocryptotrader/currency/pair" - exchange "github.com/thrasher-/gocryptotrader/exchanges" - "github.com/thrasher-/gocryptotrader/exchanges/ticker" -) - -func GetSpecificTicker(currency, exchangeName, assetType string) (ticker.Price, error) { - var specificTicker ticker.Price - var err error - for i := 0; i < len(bot.exchanges); i++ { - if bot.exchanges[i] != nil { - if bot.exchanges[i].IsEnabled() && bot.exchanges[i].GetName() == exchangeName { - specificTicker, err = bot.exchanges[i].GetTickerPrice( - pair.NewCurrencyPairFromString(currency), - assetType, - ) - break - } - } - } - return specificTicker, err -} - -func jsonTickerResponse(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - currency := vars["currency"] - exchange := vars["exchangeName"] - assetType := vars["assetType"] - - if assetType == "" { - assetType = ticker.Spot - } - response, err := GetSpecificTicker(currency, exchange, assetType) - if err != nil { - log.Printf("Failed to fetch ticker for %s currency: %s\n", exchange, - currency) - return - } - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(response); err != nil { - panic(err) - } -} - -// AllEnabledExchangeCurrencies holds the enabled exchange currencies -type AllEnabledExchangeCurrencies struct { - Data []EnabledExchangeCurrencies `json:"data"` -} - -// EnabledExchangeCurrencies is a sub type for singular exchanges and respective -// currencies -type EnabledExchangeCurrencies struct { - ExchangeName string `json:"exchangeName"` - ExchangeValues []ticker.Price `json:"exchangeValues"` -} - -func GetAllActiveTickers() []EnabledExchangeCurrencies { - var tickerData []EnabledExchangeCurrencies - - for _, individualBot := range bot.exchanges { - if individualBot != nil && individualBot.IsEnabled() { - var individualExchange EnabledExchangeCurrencies - exchangeName := individualBot.GetName() - individualExchange.ExchangeName = exchangeName - log.Println( - "Getting enabled currencies for '" + exchangeName + "'", - ) - currencies := individualBot.GetEnabledCurrencies() - for _, x := range currencies { - currency := x - assetTypes, err := exchange.GetExchangeAssetTypes(exchangeName) - if err != nil { - log.Printf("failed to get %s exchange asset types. Error: %s", - exchangeName, err) - continue - } - var tickerPrice ticker.Price - if len(assetTypes) > 1 { - for y := range assetTypes { - tickerPrice, err = individualBot.UpdateTicker(currency, - assetTypes[y]) - } - } else { - tickerPrice, err = individualBot.UpdateTicker(currency, - assetTypes[0]) - } - - if err != nil { - log.Printf("failed to get %s %s ticker. Error: %s", - currency.Pair().String(), - exchangeName, - err) - continue - } - - individualExchange.ExchangeValues = append( - individualExchange.ExchangeValues, tickerPrice, - ) - } - tickerData = append(tickerData, individualExchange) - } - } - return tickerData -} - -func getAllActiveTickersResponse(w http.ResponseWriter, r *http.Request) { - var response AllEnabledExchangeCurrencies - response.Data = GetAllActiveTickers() - - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(response); err != nil { - panic(err) - } -} - -// ExchangeRoutes denotes the current exchange routes -var ExchangeRoutes = Routes{ - Route{ - "AllActiveExchangesAndCurrencies", - "GET", - "/exchanges/enabled/latest/all", - getAllActiveTickersResponse, - }, - Route{ - "IndividualExchangeAndCurrency", - "GET", - "/exchanges/{exchangeName}/latest/{currency}", - jsonTickerResponse, - }, -} diff --git a/ticker_routes_test.go b/ticker_routes_test.go deleted file mode 100644 index 06ab7d0f..00000000 --- a/ticker_routes_test.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/tools/websocket_client/main.go b/tools/websocket_client/main.go index 509ada13..66e23569 100644 --- a/tools/websocket_client/main.go +++ b/tools/websocket_client/main.go @@ -7,6 +7,7 @@ import ( "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/config" ) var ( @@ -74,7 +75,7 @@ func main() { log.Println("Connected to websocket!") log.Println("Authenticating..") - SendWebsocketAuth("username", "password") + SendWebsocketAuth("blah", "blah") var wsResp WebsocketEventResponse err = WSConn.ReadJSON(&wsResp) @@ -112,9 +113,22 @@ func main() { log.Printf("Fetched config.") + dataJSON, err := common.JSONEncode(&wsResp.Data) + if err != nil { + log.Fatal(err) + } + + var resultCfg config.Config + err = common.JSONDecode(dataJSON, &resultCfg) + if err != nil { + log.Fatal(err) + } + + resultCfg.Name = "TEST" + req = WebsocketEvent{ Event: "SaveConfig", - Data: wsResp.Data, + Data: resultCfg, } log.Println("Saving config..") diff --git a/wallet_routes.go b/wallet_routes.go deleted file mode 100644 index 3ed2e1f3..00000000 --- a/wallet_routes.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "log" - "net/http" - - "github.com/thrasher-/gocryptotrader/exchanges" -) - -// AllEnabledExchangeAccounts holds all enabled accounts info -type AllEnabledExchangeAccounts struct { - Data []exchange.AccountInfo `json:"data"` -} - -// GetCollatedExchangeAccountInfoByCoin collates individual exchange account -// information and turns into into a map string of -// exchange.AccountCurrencyInfo -func GetCollatedExchangeAccountInfoByCoin(accounts []exchange.AccountInfo) map[string]exchange.AccountCurrencyInfo { - result := make(map[string]exchange.AccountCurrencyInfo) - for i := 0; i < len(accounts); i++ { - for j := 0; j < len(accounts[i].Currencies); j++ { - currencyName := accounts[i].Currencies[j].CurrencyName - avail := accounts[i].Currencies[j].TotalValue - onHold := accounts[i].Currencies[j].Hold - - info, ok := result[currencyName] - if !ok { - accountInfo := exchange.AccountCurrencyInfo{CurrencyName: currencyName, Hold: onHold, TotalValue: avail} - result[currencyName] = accountInfo - } else { - info.Hold += onHold - info.TotalValue += avail - result[currencyName] = info - } - } - } - return result -} - -// GetAccountCurrencyInfoByExchangeName returns info for an exchange -func GetAccountCurrencyInfoByExchangeName(accounts []exchange.AccountInfo, exchangeName string) (exchange.AccountInfo, error) { - for i := 0; i < len(accounts); i++ { - if accounts[i].ExchangeName == exchangeName { - return accounts[i], nil - } - } - return exchange.AccountInfo{}, errors.New(exchange.ErrExchangeNotFound) -} - -// GetAllEnabledExchangeAccountInfo returns all the current enabled exchanges -func GetAllEnabledExchangeAccountInfo() AllEnabledExchangeAccounts { - var response AllEnabledExchangeAccounts - for _, individualBot := range bot.exchanges { - if individualBot != nil && individualBot.IsEnabled() { - if !individualBot.GetAuthenticatedAPISupport() { - log.Printf("GetAllEnabledExchangeAccountInfo: Skippping %s due to disabled authenticated API support.", individualBot.GetName()) - continue - } - individualExchange, err := individualBot.GetExchangeAccountInfo() - if err != nil { - log.Printf("Error encountered retrieving exchange account info for %s. Error %s", - individualBot.GetName(), err) - continue - } - response.Data = append(response.Data, individualExchange) - } - } - return response -} - -// SendAllEnabledAccountInfo via get request returns JSON response of account -// info -func SendAllEnabledAccountInfo(w http.ResponseWriter, r *http.Request) { - response := GetAllEnabledExchangeAccountInfo() - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(response); err != nil { - panic(err) - } -} - -// WalletRoutes are current routes specified for queries. -var WalletRoutes = Routes{ - Route{ - "AllEnabledAccountInfo", - "GET", - "/exchanges/enabled/accounts/all", - SendAllEnabledAccountInfo, - }, -} diff --git a/wallet_routes_test.go b/wallet_routes_test.go deleted file mode 100644 index b8f5d9b9..00000000 --- a/wallet_routes_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "testing" -) - -func TestGetCollatedExchangeAccountInfoByCoin(t *testing.T) { - GetCollatedExchangeAccountInfoByCoin(GetAllEnabledExchangeAccountInfo().Data) -} - -func TestGetAccountCurrencyInfoByExchangeName(t *testing.T) { - _, err := GetAccountCurrencyInfoByExchangeName( - GetAllEnabledExchangeAccountInfo().Data, "ANX", - ) - if err == nil { - t.Error( - "Test Failed - Wallet_Routes_Test.go - GetAccountCurrencyInfoByExchangeName", - ) - } -} - -func TestGetAllEnabledExchangeAccountInfo(t *testing.T) { - if value := GetAllEnabledExchangeAccountInfo(); len(value.Data) != 0 { - t.Error( - "Test Failed - Wallet_Routes_Test.go - GetAllEnabledExchangeAccountInfo", - ) - } -} diff --git a/websocket.go b/websocket.go index 8c697e57..fb3bb8dc 100644 --- a/websocket.go +++ b/websocket.go @@ -9,12 +9,15 @@ import ( "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" + "github.com/thrasher-/gocryptotrader/currency" ) +// Const vars for websocket const ( WebsocketResponseSuccess = "OK" ) +// WebsocketRoutes adds ws route to the HTTP server var WebsocketRoutes = Routes{ Route{ "ws", @@ -24,6 +27,7 @@ var WebsocketRoutes = Routes{ }, } +// WebsocketClient stores information related to the websocket client type WebsocketClient struct { ID int Conn *websocket.Conn @@ -31,6 +35,7 @@ type WebsocketClient struct { Authenticated bool } +// WebsocketEvent is the struct used for websocket events type WebsocketEvent struct { Exchange string `json:"exchange,omitempty"` AssetType string `json:"assetType,omitempty"` @@ -38,20 +43,26 @@ type WebsocketEvent struct { Data interface{} } +// WebsocketEventResponse is the struct used for websocket event responses type WebsocketEventResponse struct { Event string `json:"event"` Data interface{} `json:"data"` Error string `json:"error"` } -type WebsocketTickerRequest struct { +// WebsocketOrderbookTickerRequest is a struct used for ticker and orderbook +// requests +type WebsocketOrderbookTickerRequest struct { Exchange string `json:"exchangeName"` Currency string `json:"currency"` AssetType string `json:"assetType"` } +// WebsocketClientHub stores an array of websocket clients var WebsocketClientHub []WebsocketClient +// WebsocketClientHandler upgrades the HTTP connection to a websocket +// compatible one func WebsocketClientHandler(w http.ResponseWriter, r *http.Request) { upgrader := websocket.Upgrader{ WriteBufferSize: 1024, @@ -73,6 +84,7 @@ func WebsocketClientHandler(w http.ResponseWriter, r *http.Request) { log.Println("New websocket client connected.") } +// DisconnectWebsocketClient disconnects a websocket client func DisconnectWebsocketClient(id int, err error) { for i := range WebsocketClientHub { if WebsocketClientHub[i].ID == id { @@ -84,6 +96,7 @@ func DisconnectWebsocketClient(id int, err error) { } } +// SendWebsocketMessage sends a websocket message to a specific client func SendWebsocketMessage(id int, data interface{}) error { for _, x := range WebsocketClientHub { if x.ID == id { @@ -93,6 +106,8 @@ func SendWebsocketMessage(id int, data interface{}) error { return nil } +// BroadcastWebsocketMessage broadcasts a websocket event message to all +// websocket clients func BroadcastWebsocketMessage(evt WebsocketEvent) error { for _, x := range WebsocketClientHub { x.Conn.WriteJSON(evt) @@ -100,29 +115,163 @@ func BroadcastWebsocketMessage(evt WebsocketEvent) error { return nil } +// WebsocketAuth is a struct used for type WebsocketAuth struct { Username string `json:"username"` Password string `json:"password"` } +type wsCommandHandler func(wsClient *websocket.Conn, data interface{}) error + +var wsHandlers = map[string]wsCommandHandler{ + "getconfig": wsGetConfig, + "saveconfig": wsSaveConfig, + "getaccountinfo": wsGetAccountInfo, + "gettickers": wsGetTickers, + "getticker": wsGetTicker, + "getorderbooks": wsGetOrderbooks, + "getorderbook": wsGetOrderbook, + "getexchangerates": wsGetExchangeRates, + "getportfolio": wsGetPortfolio, +} + +func wsGetConfig(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetConfig", + Data: bot.config, + } + return wsClient.WriteJSON(wsResp) +} + +func wsSaveConfig(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "SaveConfig", + } + var cfg config.Config + err := common.JSONDecode(data.([]byte), &cfg) + if err != nil { + wsResp.Error = err.Error() + err = wsClient.WriteJSON(wsResp) + if err != nil { + return err + } + } + + err = bot.config.UpdateConfig(bot.configFile, cfg) + if err != nil { + wsResp.Error = err.Error() + err = wsClient.WriteJSON(wsResp) + if err != nil { + return err + } + } + + setupBotExchanges() + wsResp.Data = WebsocketResponseSuccess + return wsClient.WriteJSON(wsResp) +} + +func wsGetAccountInfo(wsClient *websocket.Conn, data interface{}) error { + accountInfo := GetAllEnabledExchangeAccountInfo() + wsResp := WebsocketEventResponse{ + Event: "GetAccountInfo", + Data: accountInfo, + } + return wsClient.WriteJSON(wsResp) +} + +func wsGetTickers(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetTickers", + } + wsResp.Data = GetAllActiveTickers() + return wsClient.WriteJSON(wsResp) +} + +func wsGetTicker(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetTicker", + } + var tickerReq WebsocketOrderbookTickerRequest + err := common.JSONDecode(data.([]byte), &tickerReq) + if err != nil { + wsResp.Error = err.Error() + wsClient.WriteJSON(wsResp) + return err + } + + result, err := GetSpecificTicker(tickerReq.Currency, + tickerReq.Exchange, tickerReq.AssetType) + + if err != nil { + wsResp.Error = err.Error() + wsClient.WriteJSON(wsResp) + return err + } + wsResp.Data = result + return wsClient.WriteJSON(wsResp) +} + +func wsGetOrderbooks(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetOrderbooks", + } + wsResp.Data = GetAllActiveOrderbooks() + return wsClient.WriteJSON(wsResp) +} + +func wsGetOrderbook(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetOrderbook", + } + var orderbookReq WebsocketOrderbookTickerRequest + err := common.JSONDecode(data.([]byte), &orderbookReq) + if err != nil { + wsResp.Error = err.Error() + wsClient.WriteJSON(wsResp) + return err + } + + result, err := GetSpecificOrderbook(orderbookReq.Currency, + orderbookReq.Exchange, orderbookReq.AssetType) + + if err != nil { + wsResp.Error = err.Error() + wsClient.WriteJSON(wsResp) + return err + } + wsResp.Data = result + return wsClient.WriteJSON(wsResp) +} + +func wsGetExchangeRates(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetExchangeRates", + } + if currency.YahooEnabled { + wsResp.Data = currency.CurrencyStore + } else { + wsResp.Data = currency.CurrencyStoreFixer + } + return wsClient.WriteJSON(wsResp) +} + +func wsGetPortfolio(wsClient *websocket.Conn, data interface{}) error { + wsResp := WebsocketEventResponse{ + Event: "GetPortfolio", + } + wsResp.Data = bot.portfolio.GetPortfolioSummary() + return wsClient.WriteJSON(wsResp) +} + +// WebsocketHandler Handles websocket client requests func WebsocketHandler() { for { for x := range WebsocketClientHub { - msgType, msg, err := WebsocketClientHub[x].Conn.ReadMessage() - if err != nil { - DisconnectWebsocketClient(x, err) - continue - } - - if msgType != websocket.TextMessage { - DisconnectWebsocketClient(x, err) - continue - } - var evt WebsocketEvent - err = common.JSONDecode(msg, &evt) + err := WebsocketClientHub[x].Conn.ReadJSON(&evt) if err != nil { - log.Println(err) + DisconnectWebsocketClient(x, err) continue } @@ -137,6 +286,9 @@ func WebsocketHandler() { continue } + req := common.StringToLower(evt.Event) + log.Printf("Websocket req: %s", req) + if !WebsocketClientHub[x].Authenticated && evt.Event != "auth" { wsResp := WebsocketEventResponse{ Event: "auth", @@ -152,8 +304,8 @@ func WebsocketHandler() { log.Println(err) continue } - hashPW := common.HexEncodeToString(common.GetSHA256([]byte("password"))) - if auth.Username == "username" && auth.Password == hashPW { + hashPW := common.HexEncodeToString(common.GetSHA256([]byte(bot.config.Webserver.AdminUsername))) + if auth.Username == bot.config.Webserver.AdminUsername && auth.Password == hashPW { WebsocketClientHub[x].Authenticated = true wsResp := WebsocketEventResponse{ Event: "auth", @@ -172,97 +324,15 @@ func WebsocketHandler() { continue } } - switch evt.Event { - case "GetConfig": - wsResp := WebsocketEventResponse{ - Event: "GetConfig", - Data: bot.config, - } - SendWebsocketMessage(x, wsResp) + result, ok := wsHandlers[req] + if !ok { + log.Printf("Websocket unsupported event") continue - case "SaveConfig": - wsResp := WebsocketEventResponse{ - Event: "SaveConfig", - } - var cfg config.Config - err := common.JSONDecode(dataJSON, &cfg) - if err != nil { - wsResp.Error = err.Error() - SendWebsocketMessage(x, wsResp) - log.Println(err) - continue - } + } - //Save change the settings - for x := range bot.config.Exchanges { - for i := 0; i < len(cfg.Exchanges); i++ { - if cfg.Exchanges[i].Name == bot.config.Exchanges[x].Name { - bot.config.Exchanges[x].Enabled = cfg.Exchanges[i].Enabled - bot.config.Exchanges[x].APIKey = cfg.Exchanges[i].APIKey - bot.config.Exchanges[x].APISecret = cfg.Exchanges[i].APISecret - bot.config.Exchanges[x].EnabledPairs = cfg.Exchanges[i].EnabledPairs - } - } - } - - //Reload the configuration - err = bot.config.SaveConfig(bot.configFile) - if err != nil { - wsResp.Error = err.Error() - SendWebsocketMessage(x, wsResp) - continue - } - err = bot.config.LoadConfig(bot.configFile) - if err != nil { - wsResp.Error = err.Error() - SendWebsocketMessage(x, wsResp) - continue - } - setupBotExchanges() - wsResp.Data = WebsocketResponseSuccess - SendWebsocketMessage(x, wsResp) - continue - case "GetAccountInfo": - accountInfo := GetAllEnabledExchangeAccountInfo() - wsResp := WebsocketEventResponse{ - Event: "GetAccountInfo", - Data: accountInfo, - } - SendWebsocketMessage(x, wsResp) - continue - case "GetTicker": - wsResp := WebsocketEventResponse{ - Event: "GetTicker", - } - var tickerReq WebsocketTickerRequest - err := common.JSONDecode(dataJSON, &tickerReq) - if err != nil { - wsResp.Error = err.Error() - SendWebsocketMessage(x, wsResp) - log.Println(err) - continue - } - - data, err := GetSpecificTicker(tickerReq.Currency, - tickerReq.Exchange, tickerReq.AssetType) - - if err != nil { - wsResp.Error = err.Error() - SendWebsocketMessage(x, wsResp) - log.Println(err) - continue - } - wsResp.Data = data - SendWebsocketMessage(x, wsResp) - continue - - case "GetTickers": - wsResp := WebsocketEventResponse{ - Event: "GetTickers", - } - tickers := GetAllActiveTickers() - wsResp.Data = tickers - SendWebsocketMessage(x, wsResp) + err = result(WebsocketClientHub[x].Conn, dataJSON) + if err != nil { + log.Printf("Websocket request %s failed. Error %s", evt.Event, err) continue } }