diff --git a/gui/backend/api_server.go b/gui/backend/api_server.go index a0155fd7d..975ae595f 100644 --- a/gui/backend/api_server.go +++ b/gui/backend/api_server.go @@ -18,23 +18,44 @@ import ( "github.com/stellar/kelp/support/kelpos" ) +// UserData is the json data passed in to represent a user +type UserData struct { + ID string `json:"id"` +} + +// toUser converts to the format understood by kelpos +func (u UserData) toUser() *kelpos.User { + return kelpos.MakeUser(u.ID) +} + +// String is the stringer method +func (u UserData) String() string { + return fmt.Sprintf("UserData[ID=%s]", u.ID) +} + +// kelpErrorDataForUser tracks errors for a given user +type kelpErrorDataForUser struct { + errorMap map[string]KelpError + lock *sync.Mutex +} + // APIServer is an instance of the API service type APIServer struct { - kelpBinPath *kelpos.OSPath - botConfigsPath *kelpos.OSPath - botLogsPath *kelpos.OSPath - kos *kelpos.KelpOS - horizonTestnetURI string - horizonPubnetURI string - ccxtRestUrl string - apiTestNet *horizonclient.Client - apiPubNet *horizonclient.Client - disablePubnet bool - noHeaders bool - quitFn func() - metricsTracker *plugins.MetricsTracker - kelpErrorMap map[string]KelpError - kelpErrorMapLock *sync.Mutex + kelpBinPath *kelpos.OSPath + botConfigsPath *kelpos.OSPath + botLogsPath *kelpos.OSPath + kos *kelpos.KelpOS + horizonTestnetURI string + horizonPubnetURI string + ccxtRestUrl string + apiTestNet *horizonclient.Client + apiPubNet *horizonclient.Client + disablePubnet bool + noHeaders bool + quitFn func() + metricsTracker *plugins.MetricsTracker + kelpErrorsByUser map[string]kelpErrorDataForUser + kelpErrorsByUserLock *sync.Mutex cachedOptionsMetadata metadata } @@ -61,8 +82,6 @@ func MakeAPIServer( return nil, fmt.Errorf("error while loading options metadata when making APIServer: %s", e) } - kelpErrorMap := map[string]KelpError{} - return &APIServer{ kelpBinPath: kelpBinPath, botConfigsPath: botConfigsPath, @@ -78,19 +97,42 @@ func MakeAPIServer( cachedOptionsMetadata: optionsMetadata, quitFn: quitFn, metricsTracker: metricsTracker, - kelpErrorMap: kelpErrorMap, - kelpErrorMapLock: &sync.Mutex{}, + kelpErrorsByUser: map[string]kelpErrorDataForUser{}, + kelpErrorsByUserLock: &sync.Mutex{}, }, nil } -// InitBackend initializes anything required to get the backend ready to serve -func (s *APIServer) InitBackend() error { - // initial load of bots into memory - _, e := s.doListBots() - if e != nil { - return fmt.Errorf("error listing/loading bots: %s", e) +func (s *APIServer) botConfigsPathForUser(userID string) *kelpos.OSPath { + return s.botConfigsPath.Join(userID) +} + +func (s *APIServer) botLogsPathForUser(userID string) *kelpos.OSPath { + return s.botLogsPath.Join(userID) +} + +func (s *APIServer) kelpErrorsForUser(userID string) kelpErrorDataForUser { + s.kelpErrorsByUserLock.Lock() + defer s.kelpErrorsByUserLock.Unlock() + + var kefu kelpErrorDataForUser + if v, ok := s.kelpErrorsByUser[userID]; ok { + kefu = v + } else { + // create new value and insert in map + kefu = kelpErrorDataForUser{ + errorMap: map[string]KelpError{}, + lock: &sync.Mutex{}, + } + s.kelpErrorsByUser[userID] = kefu } + return kefu +} + +// InitBackend initializes anything required to get the backend ready to serve +func (s *APIServer) InitBackend() error { + // do not do an initial load of bots into memory for now since it's based on the user context which we don't have right now + // and we don't want to do it for all users right now return nil } @@ -177,20 +219,37 @@ func (s *APIServer) writeErrorJson(w http.ResponseWriter, message string) { w.Write(marshalledJson) } -func (s *APIServer) addKelpErrorToMap(ke KelpError) { +func (s *APIServer) addKelpErrorToMap(userData UserData, ke KelpError) { key := ke.UUID + kefu := s.kelpErrorsForUser(userData.ID) // need to use a lock because we could encounter a "concurrent map writes" error against the map which is being updated by multiple threads - s.kelpErrorMapLock.Lock() - defer s.kelpErrorMapLock.Unlock() + kefu.lock.Lock() + defer kefu.lock.Unlock() + + kefu.errorMap[key] = ke +} - s.kelpErrorMap[key] = ke +// removeKelpErrorUserDataIfEmpty removes user error data if the underlying map is empty +func (s *APIServer) removeKelpErrorUserDataIfEmpty(userData UserData) { + // issue with this is that someone can hold a reference to this object when it is empty + // and then we remove from parent map and the other thread will add a value, which would result + // in the object having an entry in the map but being orphaned. + // + // We can get creative with timeouts too but that is all an overoptimizationn + // + // We could resolve this by always holding both the higher level lock and the per-user lock to modify + // values inside a user's error map, but that will slow things down + // + // for now we do not remove Kelp error user data even if empty. + + // do nothing } -func (s *APIServer) writeKelpError(w http.ResponseWriter, kerw KelpErrorResponseWrapper) { +func (s *APIServer) writeKelpError(userData UserData, w http.ResponseWriter, kerw KelpErrorResponseWrapper) { w.WriteHeader(http.StatusInternalServerError) log.Printf("writing error: %s\n", kerw.String()) - s.addKelpErrorToMap(kerw.KelpError) + s.addKelpErrorToMap(userData, kerw.KelpError) marshalledJSON, e := json.MarshalIndent(kerw, "", " ") if e != nil { @@ -239,15 +298,15 @@ func (s *APIServer) runKelpCommandBackground(namespace string, cmd string) (*kel return s.kos.Background(namespace, cmdString) } -func (s *APIServer) setupOpsDirectory() error { - e := s.kos.Mkdir(s.botConfigsPath) +func (s *APIServer) setupOpsDirectory(userID string) error { + e := s.kos.Mkdir(s.botConfigsPathForUser(userID)) if e != nil { - return fmt.Errorf("error setting up configs directory (%s): %s\n", s.botConfigsPath, e) + return fmt.Errorf("error setting up configs directory (%s): %s", s.botConfigsPathForUser(userID).Native(), e) } - e = s.kos.Mkdir(s.botLogsPath) + e = s.kos.Mkdir(s.botLogsPathForUser(userID)) if e != nil { - return fmt.Errorf("error setting up logs directory (%s): %s\n", s.botLogsPath, e) + return fmt.Errorf("error setting up logs directory (%s): %s", s.botLogsPathForUser(userID).Native(), e) } return nil diff --git a/gui/backend/autogenerate_bot.go b/gui/backend/autogenerate_bot.go index d019b8835..5a3958839 100644 --- a/gui/backend/autogenerate_bot.go +++ b/gui/backend/autogenerate_bot.go @@ -3,6 +3,7 @@ package backend import ( "encoding/json" "fmt" + "io/ioutil" "log" "net/http" "strings" @@ -28,7 +29,30 @@ var centralizedVolumePrecisionOverride = int8(1) var centralizedMinBaseVolumeOverride = float64(30.0) var centralizedMinQuoteVolumeOverride = float64(10.0) +type autogenerateBotRequest struct { + UserData UserData `json:"user_data"` +} + func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { + bodyBytes, e := ioutil.ReadAll(r.Body) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error reading request input: %s", e)) + return + } + log.Printf("autogenerateBot requestJson: %s\n", string(bodyBytes)) + + var req autogenerateBotRequest + e = json.Unmarshal(bodyBytes, &req) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error unmarshaling json: %s; bodyString = %s", e, string(bodyBytes))) + return + } + userID := req.UserData.ID + if strings.TrimSpace(userID) == "" { + s.writeErrorJson(w, fmt.Sprintf("cannot have empty userID")) + return + } + kp, e := keypair.Random() if e != nil { s.writeError(w, fmt.Sprintf("error generating keypair: %s\n", e)) @@ -37,14 +61,14 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { // make and register bot, which places it in the initial bot state bot := model2.MakeAutogeneratedBot() - e = s.kos.RegisterBot(bot) + e = s.kos.BotDataForUser(req.UserData.toUser()).RegisterBot(bot) if e != nil { // the bot is not registered at this stage so we don't throw a KelpError here s.writeError(w, fmt.Sprintf("error registering bot: %s\n", e)) return } - e = s.setupOpsDirectory() + e = s.setupOpsDirectory(userID) if e != nil { // the bot is not registered at this stage so we don't throw a KelpError here s.writeError(w, fmt.Sprintf("error setting up ops directory: %s\n", e)) @@ -53,7 +77,7 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { filenamePair := bot.Filenames() sampleTrader := s.makeSampleTrader(kp.Seed()) - traderFilePath := s.botConfigsPath.Join(filenamePair.Trader) + traderFilePath := s.botConfigsPathForUser(userID).Join(filenamePair.Trader) log.Printf("writing autogenerated bot config to file: %s\n", traderFilePath.AsString()) e = toml.WriteFile(traderFilePath.Native(), sampleTrader) if e != nil { @@ -63,11 +87,11 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { } sampleBuysell := makeSampleBuysell() - strategyFilePath := s.botConfigsPath.Join(filenamePair.Strategy) + strategyFilePath := s.botConfigsPathForUser(userID).Join(filenamePair.Strategy) log.Printf("writing autogenerated strategy config to file: %s\n", strategyFilePath.AsString()) e = toml.WriteFile(strategyFilePath.Native(), sampleBuysell) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper( errorTypeBot, bot.Name, time.Now().UTC(), @@ -81,7 +105,7 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { go func() { e := s.setupTestnetAccount(kp.Address(), kp.Seed(), bot.Name) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper( errorTypeBot, bot.Name, time.Now().UTC(), @@ -91,9 +115,9 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { return } - e = s.kos.AdvanceBotState(bot.Name, kelpos.InitState()) + e = s.kos.BotDataForUser(req.UserData.toUser()).AdvanceBotState(bot.Name, kelpos.InitState()) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper( errorTypeBot, bot.Name, time.Now().UTC(), @@ -106,7 +130,7 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { botJSON, e := json.Marshal(*bot) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper( errorTypeBot, bot.Name, time.Now().UTC(), diff --git a/gui/backend/delete_bot.go b/gui/backend/delete_bot.go index aec5b1380..c010ad602 100644 --- a/gui/backend/delete_bot.go +++ b/gui/backend/delete_bot.go @@ -1,26 +1,45 @@ package backend import ( + "encoding/json" "fmt" + "io/ioutil" "log" "net/http" + "strings" "time" "github.com/stellar/kelp/gui/model2" "github.com/stellar/kelp/support/kelpos" ) +type deleteBotRequest struct { + UserData UserData `json:"user_data"` + BotName string `json:"bot_name"` +} + func (s *APIServer) deleteBot(w http.ResponseWriter, r *http.Request) { - botName, e := s.parseBotName(r) + bodyBytes, e := ioutil.ReadAll(r.Body) if e != nil { - s.writeError(w, fmt.Sprintf("error in deleteBot: %s\n", e)) + s.writeErrorJson(w, fmt.Sprintf("error when reading request input: %s\n", e)) + return + } + var req deleteBotRequest + e = json.Unmarshal(bodyBytes, &req) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error unmarshaling json: %s; bodyString = %s", e, string(bodyBytes))) + return + } + if strings.TrimSpace(req.UserData.ID) == "" { + s.writeErrorJson(w, fmt.Sprintf("cannot have empty userID")) return } + botName := req.BotName // only stop bot if current state is running - botState, e := s.doGetBotState(botName) + botState, e := s.doGetBotState(req.UserData, botName) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -31,9 +50,9 @@ func (s *APIServer) deleteBot(w http.ResponseWriter, r *http.Request) { } log.Printf("current botState: %s\n", botState) if botState == kelpos.BotStateRunning { - e = s.doStopBot(botName) + e = s.doStopBot(req.UserData, botName) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -45,9 +64,9 @@ func (s *APIServer) deleteBot(w http.ResponseWriter, r *http.Request) { } for { - botState, e := s.doGetBotState(botName) + botState, e := s.doGetBotState(req.UserData, botName) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -66,14 +85,14 @@ func (s *APIServer) deleteBot(w http.ResponseWriter, r *http.Request) { } // unregister bot - s.kos.SafeUnregisterBot(botName) + s.kos.BotDataForUser(req.UserData.toUser()).SafeUnregisterBot(botName) // delete configs botPrefix := model2.GetPrefix(botName) - botConfigPath := s.botConfigsPath.Join(botPrefix) + botConfigPath := s.botConfigsPathForUser(req.UserData.ID).Join(botPrefix) _, e = s.kos.Blocking("rm", fmt.Sprintf("rm %s*", botConfigPath.Unix())) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), diff --git a/gui/backend/fetch_kelp_errors.go b/gui/backend/fetch_kelp_errors.go index fe8a52188..1e3a09213 100644 --- a/gui/backend/fetch_kelp_errors.go +++ b/gui/backend/fetch_kelp_errors.go @@ -2,7 +2,10 @@ package backend import ( "encoding/json" + "fmt" + "io/ioutil" "net/http" + "strings" ) // KelpErrorListResponseWrapper is the outer object that contains the Kelp Errors @@ -10,10 +13,32 @@ type KelpErrorListResponseWrapper struct { KelpErrorList []KelpError `json:"kelp_error_list"` } +type fetchKelpErrorsRequest struct { + UserData UserData `json:"user_data"` +} + func (s *APIServer) fetchKelpErrors(w http.ResponseWriter, r *http.Request) { - kelpErrors := make([]KelpError, len(s.kelpErrorMap)) + bodyBytes, e := ioutil.ReadAll(r.Body) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error when reading request input: %s\n", e)) + return + } + var req fetchKelpErrorsRequest + e = json.Unmarshal(bodyBytes, &req) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error unmarshaling json: %s; bodyString = %s", e, string(bodyBytes))) + return + } + if strings.TrimSpace(req.UserData.ID) == "" { + s.writeErrorJson(w, fmt.Sprintf("cannot have empty userID")) + return + } + userData := req.UserData + + kefu := s.kelpErrorsForUser(userData.ID) + kelpErrors := make([]KelpError, len(kefu.errorMap)) i := 0 - for _, ke := range s.kelpErrorMap { + for _, ke := range kefu.errorMap { kelpErrors[i] = ke i++ } diff --git a/gui/backend/generate_bot_name.go b/gui/backend/generate_bot_name.go index 9afec29ac..62618d3a7 100644 --- a/gui/backend/generate_bot_name.go +++ b/gui/backend/generate_bot_name.go @@ -1,7 +1,9 @@ package backend import ( + "encoding/json" "fmt" + "io/ioutil" "net/http" "strings" @@ -18,8 +20,28 @@ func init() { utils.Shuffle(ocean_animals) } +type genBotNameRequest struct { + UserData UserData `json:"user_data"` +} + func (s *APIServer) generateBotName(w http.ResponseWriter, r *http.Request) { - botName, e := s.doGenerateBotName() + bodyBytes, e := ioutil.ReadAll(r.Body) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error when reading request input: %s\n", e)) + return + } + var req genBotNameRequest + e = json.Unmarshal(bodyBytes, &req) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error unmarshaling json: %s; bodyString = %s", e, string(bodyBytes))) + return + } + if strings.TrimSpace(req.UserData.ID) == "" { + s.writeErrorJson(w, fmt.Sprintf("cannot have empty userID")) + return + } + + botName, e := s.doGenerateBotName(req.UserData) if e != nil { s.writeError(w, fmt.Sprintf("error encountered while generating new bot name: %s", e)) return @@ -29,7 +51,7 @@ func (s *APIServer) generateBotName(w http.ResponseWriter, r *http.Request) { w.Write([]byte(botName)) } -func (s *APIServer) doGenerateBotName() (string, error) { +func (s *APIServer) doGenerateBotName(userData UserData) (string, error) { var botName string startingIdxName := idx_name for { @@ -40,7 +62,7 @@ func (s *APIServer) doGenerateBotName() (string, error) { idx_animals = (idx_animals + 1) % len(ocean_animals) // only check name so we use unique first names for convenience - prefixExists, e := s.prefixExists(strings.ToLower(name)) + prefixExists, e := s.prefixExists(userData, strings.ToLower(name)) if e != nil { return "", fmt.Errorf("error encountered while checking for new bot name prefix: %s", e) } @@ -50,14 +72,14 @@ func (s *APIServer) doGenerateBotName() (string, error) { // if prefix exists and we have reached back to the starting index then we have exhausted all options if idx_name == startingIdxName { - return "", fmt.Errorf("cannot generate name because we ran out of first name combinations...") + return "", fmt.Errorf("cannot generate name because we ran out of first name combinations") } } return botName, nil } -func (s *APIServer) prefixExists(prefix string) (bool, error) { - command := fmt.Sprintf("ls %s | grep %s", s.botConfigsPath.Unix(), prefix) +func (s *APIServer) prefixExists(userData UserData, prefix string) (bool, error) { + command := fmt.Sprintf("ls %s | grep %s", s.botConfigsPathForUser(userData.ID).Unix(), prefix) _, e := s.kos.Blocking("prefix", command) if e != nil { if strings.Contains(e.Error(), "exit status 1") { diff --git a/gui/backend/get_bot_config.go b/gui/backend/get_bot_config.go index a464cd915..e8b092038 100644 --- a/gui/backend/get_bot_config.go +++ b/gui/backend/get_bot_config.go @@ -3,8 +3,10 @@ package backend import ( "encoding/json" "fmt" + "io/ioutil" "log" "net/http" + "strings" "time" "github.com/stellar/go/support/config" @@ -13,6 +15,11 @@ import ( "github.com/stellar/kelp/trader" ) +type getBotConfigRequest struct { + UserData UserData `json:"user_data"` + BotName string `json:"bot_name"` +} + type botConfigResponse struct { Name string `json:"name"` Strategy string `json:"strategy"` @@ -21,18 +28,29 @@ type botConfigResponse struct { } func (s *APIServer) getBotConfig(w http.ResponseWriter, r *http.Request) { - botName, e := s.parseBotName(r) + bodyBytes, e := ioutil.ReadAll(r.Body) if e != nil { - s.writeError(w, fmt.Sprintf("error parsing bot name in getBotConfig: %s\n", e)) + s.writeErrorJson(w, fmt.Sprintf("error when reading request input: %s\n", e)) + return + } + var req getBotConfigRequest + e = json.Unmarshal(bodyBytes, &req) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error unmarshaling json: %s; bodyString = %s", e, string(bodyBytes))) + return + } + if strings.TrimSpace(req.UserData.ID) == "" { + s.writeErrorJson(w, fmt.Sprintf("cannot have empty userID")) return } + botName := req.BotName filenamePair := model2.GetBotFilenames(botName, "buysell") - traderFilePath := s.botConfigsPath.Join(filenamePair.Trader) + traderFilePath := s.botConfigsPathForUser(req.UserData.ID).Join(filenamePair.Trader) var botConfig trader.BotConfig e = config.Read(traderFilePath.Native(), &botConfig) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -41,11 +59,11 @@ func (s *APIServer) getBotConfig(w http.ResponseWriter, r *http.Request) { )) return } - strategyFilePath := s.botConfigsPath.Join(filenamePair.Strategy) + strategyFilePath := s.botConfigsPathForUser(req.UserData.ID).Join(filenamePair.Strategy) var buysellConfig plugins.BuySellConfig e = config.Read(strategyFilePath.Native(), &buysellConfig) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -63,7 +81,7 @@ func (s *APIServer) getBotConfig(w http.ResponseWriter, r *http.Request) { } jsonBytes, e := json.MarshalIndent(response, "", " ") if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), diff --git a/gui/backend/get_bot_info.go b/gui/backend/get_bot_info.go index 798200d1b..a5ebd88c6 100644 --- a/gui/backend/get_bot_info.go +++ b/gui/backend/get_bot_info.go @@ -3,6 +3,7 @@ package backend import ( "encoding/json" "fmt" + "io/ioutil" "log" "net/http" "strconv" @@ -38,22 +39,38 @@ type botInfo struct { SpreadPercent float64 `json:"spread_pct"` } +type getBotInfoRequest struct { + UserData UserData `json:"user_data"` + BotName string `json:"bot_name"` +} + func (s *APIServer) getBotInfo(w http.ResponseWriter, r *http.Request) { - botName, e := s.parseBotName(r) + bodyBytes, e := ioutil.ReadAll(r.Body) if e != nil { - s.writeError(w, fmt.Sprintf("error parsing bot name in getBotInfo: %s\n", e)) + s.writeErrorJson(w, fmt.Sprintf("error when reading request input: %s\n", e)) + return + } + var req getBotInfoRequest + e = json.Unmarshal(bodyBytes, &req) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error unmarshaling json: %s; bodyString = %s", e, string(bodyBytes))) + return + } + if strings.TrimSpace(req.UserData.ID) == "" { + s.writeErrorJson(w, fmt.Sprintf("cannot have empty userID")) return } + botName := req.BotName - s.runGetBotInfoDirect(w, botName) + s.runGetBotInfoDirect(w, req.UserData, botName) } -func (s *APIServer) runGetBotInfoDirect(w http.ResponseWriter, botName string) { +func (s *APIServer) runGetBotInfoDirect(w http.ResponseWriter, userData UserData, botName string) { log.Printf("getBotInfo is invoking logic directly for botName: %s\n", botName) - botState, e := s.doGetBotState(botName) + botState, e := s.doGetBotState(userData, botName) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(userData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -70,11 +87,11 @@ func (s *APIServer) runGetBotInfoDirect(w http.ResponseWriter, botName string) { } filenamePair := model2.GetBotFilenames(botName, buysell) - traderFilePath := s.botConfigsPath.Join(filenamePair.Trader) + traderFilePath := s.botConfigsPathForUser(userData.ID).Join(filenamePair.Trader) var botConfig trader.BotConfig e = config.Read(traderFilePath.Native(), &botConfig) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(userData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -85,7 +102,7 @@ func (s *APIServer) runGetBotInfoDirect(w http.ResponseWriter, botName string) { } e = botConfig.Init() if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(userData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -109,7 +126,7 @@ func (s *APIServer) runGetBotInfoDirect(w http.ResponseWriter, botName string) { account, e := client.AccountDetail(horizonclient.AccountRequest{AccountID: botConfig.TradingAccount()}) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(userData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -122,7 +139,7 @@ func (s *APIServer) runGetBotInfoDirect(w http.ResponseWriter, botName string) { if assetBase == utils.NativeAsset { balanceBase, e = getNativeBalance(account) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(userData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -134,7 +151,7 @@ func (s *APIServer) runGetBotInfoDirect(w http.ResponseWriter, botName string) { } else { balanceBase, e = getCreditBalance(account, assetBase) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(userData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -148,7 +165,7 @@ func (s *APIServer) runGetBotInfoDirect(w http.ResponseWriter, botName string) { if assetQuote == utils.NativeAsset { balanceQuote, e = getNativeBalance(account) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(userData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -160,7 +177,7 @@ func (s *APIServer) runGetBotInfoDirect(w http.ResponseWriter, botName string) { } else { balanceQuote, e = getCreditBalance(account, assetQuote) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(userData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -173,7 +190,7 @@ func (s *APIServer) runGetBotInfoDirect(w http.ResponseWriter, botName string) { offers, e := utils.LoadAllOffers(account.AccountID, client) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(userData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -196,7 +213,7 @@ func (s *APIServer) runGetBotInfoDirect(w http.ResponseWriter, botName string) { Limit: 1, }) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(userData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), diff --git a/gui/backend/get_bot_state.go b/gui/backend/get_bot_state.go index a520ab248..f4dac6da4 100644 --- a/gui/backend/get_bot_state.go +++ b/gui/backend/get_bot_state.go @@ -1,25 +1,45 @@ package backend import ( + "encoding/json" "fmt" + "io/ioutil" "log" "net/http" + "strings" "time" "github.com/stellar/kelp/support/kelpos" ) +type getBotStateRequest struct { + UserData UserData `json:"user_data"` + BotName string `json:"bot_name"` +} + func (s *APIServer) getBotState(w http.ResponseWriter, r *http.Request) { - botName, e := s.parseBotName(r) + bodyBytes, e := ioutil.ReadAll(r.Body) if e != nil { // we do not have the botName so we cannot throw a bot specific error here s.writeError(w, fmt.Sprintf("error in getBotState: %s\n", e)) return } + var req getBotStateRequest + e = json.Unmarshal(bodyBytes, &req) + if e != nil { + s.writeError(w, fmt.Sprintf("error unmarshaling json: %s; bodyString = %s", e, string(bodyBytes))) + return + } + if strings.TrimSpace(req.UserData.ID) == "" { + s.writeError(w, fmt.Sprintf("cannot have empty userID")) + return + } + botName := req.BotName + userData := req.UserData - state, e := s.doGetBotState(botName) + state, e := s.doGetBotState(userData, botName) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(userData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -33,11 +53,12 @@ func (s *APIServer) getBotState(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("%s\n", state))) } -func (s *APIServer) doGetBotState(botName string) (kelpos.BotState, error) { - b, e := s.kos.GetBot(botName) +func (s *APIServer) doGetBotState(userData UserData, botName string) (kelpos.BotState, error) { + ubd := s.kos.BotDataForUser(userData.toUser()) + b, e := ubd.GetBot(botName) if e != nil { return kelpos.InitState(), fmt.Errorf("unable to get bot state: %s", e) } - log.Printf("bots available: %v", s.kos.RegisteredBots()) + log.Printf("bots available for user (%s): %v", userData.String(), ubd.RegisteredBots()) return b.State, nil } diff --git a/gui/backend/get_new_bot_config.go b/gui/backend/get_new_bot_config.go index f4c760fa4..1cb3b0281 100644 --- a/gui/backend/get_new_bot_config.go +++ b/gui/backend/get_new_bot_config.go @@ -3,12 +3,34 @@ package backend import ( "encoding/json" "fmt" + "io/ioutil" "log" "net/http" + "strings" ) +type getNewBotConfigRequest struct { + UserData UserData `json:"user_data"` +} + func (s *APIServer) getNewBotConfig(w http.ResponseWriter, r *http.Request) { - botName, e := s.doGenerateBotName() + bodyBytes, e := ioutil.ReadAll(r.Body) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error when reading request input: %s\n", e)) + return + } + var req getNewBotConfigRequest + e = json.Unmarshal(bodyBytes, &req) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error unmarshaling json: %s; bodyString = %s", e, string(bodyBytes))) + return + } + if strings.TrimSpace(req.UserData.ID) == "" { + s.writeErrorJson(w, fmt.Sprintf("cannot have empty userID")) + return + } + + botName, e := s.doGenerateBotName(req.UserData) if e != nil { s.writeErrorJson(w, fmt.Sprintf("cannot generate a new bot name: %s", e)) return diff --git a/gui/backend/list_bots.go b/gui/backend/list_bots.go index 0001b2eb8..6d8846ea8 100644 --- a/gui/backend/list_bots.go +++ b/gui/backend/list_bots.go @@ -3,6 +3,7 @@ package backend import ( "encoding/json" "fmt" + "io/ioutil" "log" "net/http" "strings" @@ -11,10 +12,30 @@ import ( "github.com/stellar/kelp/support/kelpos" ) +type listBotsRequest struct { + UserData UserData `json:"user_data"` +} + func (s *APIServer) listBots(w http.ResponseWriter, r *http.Request) { log.Printf("listing bots\n") - bots, e := s.doListBots() + bodyBytes, e := ioutil.ReadAll(r.Body) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error when reading request input: %s\n", e)) + return + } + var req listBotsRequest + e = json.Unmarshal(bodyBytes, &req) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error unmarshaling json: %s; bodyString = %s", e, string(bodyBytes))) + return + } + if strings.TrimSpace(req.UserData.ID) == "" { + s.writeErrorJson(w, fmt.Sprintf("cannot have empty userID")) + return + } + + bots, e := s.doListBots(req.UserData) if e != nil { s.writeErrorJson(w, fmt.Sprintf("error encountered while listing bots: %s", e)) return @@ -30,9 +51,9 @@ func (s *APIServer) listBots(w http.ResponseWriter, r *http.Request) { w.Write(botsJSON) } -func (s *APIServer) doListBots() ([]model2.Bot, error) { +func (s *APIServer) doListBots(userData UserData) ([]model2.Bot, error) { bots := []model2.Bot{} - resultBytes, e := s.kos.Blocking("ls", fmt.Sprintf("ls %s | sort", s.botConfigsPath.Unix())) + resultBytes, e := s.kos.Blocking("ls", fmt.Sprintf("ls %s | sort", s.botConfigsPathForUser(userData.ID).Unix())) if e != nil { return bots, fmt.Errorf("error when listing bots: %s", e) } @@ -46,8 +67,9 @@ func (s *APIServer) doListBots() ([]model2.Bot, error) { } log.Printf("bots available: %v", bots) + ubd := s.kos.BotDataForUser(userData.toUser()) for _, bot := range bots { - botState, e := s.kos.QueryBotState(bot.Name) + botState, e := ubd.QueryBotState(bot.Name) if e != nil { return bots, fmt.Errorf("unable to query bot state for bot '%s': %s", bot.Name, e) } @@ -55,7 +77,7 @@ func (s *APIServer) doListBots() ([]model2.Bot, error) { log.Printf("found bot '%s' with state '%s'\n", bot.Name, botState) // if page is reloaded then bot would already be registered, which is ok -- but we upsert here so it doesn't matter if botState != kelpos.InitState() { - s.kos.RegisterBotWithStateUpsert(&bot, botState) + ubd.RegisterBotWithStateUpsert(&bot, botState) } } diff --git a/gui/backend/remove_kelp_errors.go b/gui/backend/remove_kelp_errors.go index 2139f33ec..390ed2162 100644 --- a/gui/backend/remove_kelp_errors.go +++ b/gui/backend/remove_kelp_errors.go @@ -5,10 +5,12 @@ import ( "fmt" "io/ioutil" "net/http" + "strings" ) // RemoveKelpErrorRequest is the outer object that contains the Kelp Error type RemoveKelpErrorRequest struct { + UserData UserData `json:"user_data"` KelpErrorIDs []string `json:"kelp_error_ids"` } @@ -23,16 +25,22 @@ func (s *APIServer) removeKelpErrors(w http.ResponseWriter, r *http.Request) { s.writeErrorJson(w, fmt.Sprintf("error when reading request body input: %s", e)) return } - var kelpErrorRequest RemoveKelpErrorRequest e = json.Unmarshal(bodyBytes, &kelpErrorRequest) if e != nil { s.writeErrorJson(w, fmt.Sprintf("unable to parse kelp error input from request as json: %s", e)) return } + if strings.TrimSpace(kelpErrorRequest.UserData.ID) == "" { + s.writeErrorJson(w, fmt.Sprintf("cannot have empty userID")) + return + } + userData := kelpErrorRequest.UserData // perform the actual action of removing the error - removedMap := s.removeErrorsFromMap(kelpErrorRequest.KelpErrorIDs) + removedMap := s.removeErrorsFromMap(userData, kelpErrorRequest.KelpErrorIDs) + // reduce memory usage to prevent memory leaks in user data (since we remove entries here) + s.removeKelpErrorUserDataIfEmpty(userData) resp := RemoveKelpErrorResponse{RemovedMap: removedMap} bytes, e := json.Marshal(resp) @@ -46,15 +54,16 @@ func (s *APIServer) removeKelpErrors(w http.ResponseWriter, r *http.Request) { w.Write(bytes) } -func (s *APIServer) removeErrorsFromMap(keIDs []string) (removedMap map[string]bool) { +func (s *APIServer) removeErrorsFromMap(userData UserData, keIDs []string) (removedMap map[string]bool) { removedMap = map[string]bool{} - s.kelpErrorMapLock.Lock() - defer s.kelpErrorMapLock.Unlock() + kefu := s.kelpErrorsForUser(userData.ID) + kefu.lock.Lock() + defer kefu.lock.Unlock() for _, uuid := range keIDs { - if _, exists := s.kelpErrorMap[uuid]; exists { - delete(s.kelpErrorMap, uuid) + if _, exists := kefu.errorMap[uuid]; exists { + delete(kefu.errorMap, uuid) removedMap[uuid] = true } else { removedMap[uuid] = false diff --git a/gui/backend/routes.go b/gui/backend/routes.go index 0582d9737..0cfd6dbf7 100644 --- a/gui/backend/routes.go +++ b/gui/backend/routes.go @@ -12,14 +12,14 @@ func SetRoutes(r *chi.Mux, s *APIServer) { r.Get("/version", http.HandlerFunc(s.version)) r.Get("/quit", http.HandlerFunc(s.quit)) r.Get("/serverMetadata", http.HandlerFunc(s.serverMetadata)) - r.Get("/listBots", http.HandlerFunc(s.listBots)) - r.Get("/autogenerate", http.HandlerFunc(s.autogenerateBot)) - r.Get("/genBotName", http.HandlerFunc(s.generateBotName)) - r.Get("/getNewBotConfig", http.HandlerFunc(s.getNewBotConfig)) r.Get("/newSecretKey", http.HandlerFunc(s.newSecretKey)) r.Get("/optionsMetadata", http.HandlerFunc(s.optionsMetadata)) - r.Get("/fetchKelpErrors", http.HandlerFunc(s.fetchKelpErrors)) + r.Post("/listBots", http.HandlerFunc(s.listBots)) + r.Post("/genBotName", http.HandlerFunc(s.generateBotName)) + r.Post("/getNewBotConfig", http.HandlerFunc(s.getNewBotConfig)) + r.Post("/autogenerate", http.HandlerFunc(s.autogenerateBot)) + r.Post("/fetchKelpErrors", http.HandlerFunc(s.fetchKelpErrors)) r.Post("/removeKelpErrors", http.HandlerFunc(s.removeKelpErrors)) r.Post("/start", http.HandlerFunc(s.startBot)) r.Post("/stop", http.HandlerFunc(s.stopBot)) diff --git a/gui/backend/start_bot.go b/gui/backend/start_bot.go index 42ae0a022..50284f855 100644 --- a/gui/backend/start_bot.go +++ b/gui/backend/start_bot.go @@ -1,6 +1,7 @@ package backend import ( + "encoding/json" "fmt" "io/ioutil" "log" @@ -15,17 +16,32 @@ import ( "github.com/stellar/kelp/trader" ) +type startBotRequest struct { + UserData UserData `json:"user_data"` + BotName string `json:"bot_name"` +} + func (s *APIServer) startBot(w http.ResponseWriter, r *http.Request) { - botNameBytes, e := ioutil.ReadAll(r.Body) + bodyBytes, e := ioutil.ReadAll(r.Body) if e != nil { s.writeErrorJson(w, fmt.Sprintf("error when reading request input: %s\n", e)) return } - botName := string(botNameBytes) + var req startBotRequest + e = json.Unmarshal(bodyBytes, &req) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error unmarshaling json: %s; bodyString = %s", e, string(bodyBytes))) + return + } + if strings.TrimSpace(req.UserData.ID) == "" { + s.writeErrorJson(w, fmt.Sprintf("cannot have empty userID")) + return + } + botName := req.BotName - e = s.doStartBot(botName, "buysell", nil, nil) + e = s.doStartBot(req.UserData, botName, "buysell", nil, nil) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -35,9 +51,9 @@ func (s *APIServer) startBot(w http.ResponseWriter, r *http.Request) { return } - e = s.kos.AdvanceBotState(botName, kelpos.BotStateStopped) + e = s.kos.BotDataForUser(req.UserData.toUser()).AdvanceBotState(botName, kelpos.BotStateStopped) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -51,7 +67,7 @@ func (s *APIServer) startBot(w http.ResponseWriter, r *http.Request) { w.Write([]byte("{}")) } -func (s *APIServer) doStartBot(botName string, strategy string, iterations *uint8, maybeFinishCallback func()) error { +func (s *APIServer) doStartBot(userData UserData, botName string, strategy string, iterations *uint8, maybeFinishCallback func()) error { filenamePair := model2.GetBotFilenames(botName, strategy) logPrefix := model2.GetLogPrefix(botName, strategy) @@ -77,24 +93,24 @@ func (s *APIServer) doStartBot(botName string, strategy string, iterations *uint // use relative paths, which is why it seems to work // Note that /mnt/c is unlikely to be valid in windows (but is valid in the linux subsystem) since it's usually prefixed by the // volume (C:\ etc.), which is why relative paths works so well here as it avoids this confusion. - traderRelativeConfigPath, e := s.botConfigsPath.Join(filenamePair.Trader).RelFromPath(s.kos.GetDotKelpWorkingDir()) + traderRelativeConfigPath, e := s.botConfigsPathForUser(userData.ID).Join(filenamePair.Trader).RelFromPath(s.kos.GetDotKelpWorkingDir()) if e != nil { return fmt.Errorf("unable to get relative path of trader config file from basepath: %s", e) } - stratRelativeConfigPath, e := s.botConfigsPath.Join(filenamePair.Strategy).RelFromPath(s.kos.GetDotKelpWorkingDir()) + stratRelativeConfigPath, e := s.botConfigsPathForUser(userData.ID).Join(filenamePair.Strategy).RelFromPath(s.kos.GetDotKelpWorkingDir()) if e != nil { return fmt.Errorf("unable to get relative path of strategy config file from basepath: %s", e) } - logRelativePrefixPath, e := s.botLogsPath.Join(logPrefix).RelFromPath(s.kos.GetDotKelpWorkingDir()) + logRelativePrefixPath, e := s.botLogsPathForUser(userData.ID).Join(logPrefix).RelFromPath(s.kos.GetDotKelpWorkingDir()) if e != nil { return fmt.Errorf("unable to get relative path of log prefix path from basepath: %s", e) } // prevent starting pubnet bots if pubnet is disabled var botConfig trader.BotConfig - traderLoadReadPath := s.botConfigsPath.Join(filenamePair.Trader) + traderLoadReadPath := s.botConfigsPathForUser(userData.ID).Join(filenamePair.Trader) e = config.Read(traderLoadReadPath.Native(), &botConfig) if e != nil { return fmt.Errorf("cannot read bot config at path '%s': %s", traderLoadReadPath.Native(), e) @@ -140,7 +156,7 @@ func (s *APIServer) doStartBot(botName string, strategy string, iterations *uint return } - s.addKelpErrorToMap(makeKelpErrorResponseWrapper( + s.addKelpErrorToMap(userData, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -149,7 +165,7 @@ func (s *APIServer) doStartBot(botName string, strategy string, iterations *uint ).KelpError) // set state to stopped - s.abruptStoppedState(botName) + s.abruptStoppedState(userData, botName) // we don't want to continue because the bot didn't finish correctly return @@ -164,11 +180,11 @@ func (s *APIServer) doStartBot(botName string, strategy string, iterations *uint return nil } -func (s *APIServer) abruptStoppedState(botName string) { +func (s *APIServer) abruptStoppedState(userData UserData, botName string) { // advance state from running to stopping - e := s.kos.AdvanceBotState(botName, kelpos.BotStateRunning) + e := s.kos.BotDataForUser(userData.toUser()).AdvanceBotState(botName, kelpos.BotStateRunning) if e != nil { - s.addKelpErrorToMap(makeKelpErrorResponseWrapper( + s.addKelpErrorToMap(userData, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -179,9 +195,9 @@ func (s *APIServer) abruptStoppedState(botName string) { } // advance state from stopping to stopped - e = s.kos.AdvanceBotState(botName, kelpos.BotStateStopping) + e = s.kos.BotDataForUser(userData.toUser()).AdvanceBotState(botName, kelpos.BotStateStopping) if e != nil { - s.addKelpErrorToMap(makeKelpErrorResponseWrapper( + s.addKelpErrorToMap(userData, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), diff --git a/gui/backend/stop_bot.go b/gui/backend/stop_bot.go index 3e4c84a5e..880b4b252 100644 --- a/gui/backend/stop_bot.go +++ b/gui/backend/stop_bot.go @@ -1,24 +1,43 @@ package backend import ( + "encoding/json" "fmt" + "io/ioutil" "log" "net/http" + "strings" "time" "github.com/stellar/kelp/support/kelpos" ) +type stopBotRequest struct { + UserData UserData `json:"user_data"` + BotName string `json:"bot_name"` +} + func (s *APIServer) stopBot(w http.ResponseWriter, r *http.Request) { - botName, e := s.parseBotName(r) + bodyBytes, e := ioutil.ReadAll(r.Body) if e != nil { - s.writeError(w, fmt.Sprintf("error in stopBot: %s\n", e)) + s.writeErrorJson(w, fmt.Sprintf("error when reading request input: %s\n", e)) + return + } + var req stopBotRequest + e = json.Unmarshal(bodyBytes, &req) + if e != nil { + s.writeErrorJson(w, fmt.Sprintf("error unmarshaling json: %s; bodyString = %s", e, string(bodyBytes))) + return + } + if strings.TrimSpace(req.UserData.ID) == "" { + s.writeErrorJson(w, fmt.Sprintf("cannot have empty userID")) return } + botName := req.BotName - e = s.doStopBot(botName) + e = s.doStopBot(req.UserData, botName) if e != nil { - s.writeKelpError(w, makeKelpErrorResponseWrapper( + s.writeKelpError(req.UserData, w, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -30,8 +49,8 @@ func (s *APIServer) stopBot(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -func (s *APIServer) doStopBot(botName string) error { - e := s.kos.AdvanceBotState(botName, kelpos.BotStateRunning) +func (s *APIServer) doStopBot(userData UserData, botName string) error { + e := s.kos.BotDataForUser(userData.toUser()).AdvanceBotState(botName, kelpos.BotStateRunning) if e != nil { return fmt.Errorf("error advancing bot state: %s", e) } @@ -43,10 +62,10 @@ func (s *APIServer) doStopBot(botName string) error { log.Printf("stopped bot '%s'\n", botName) var numIterations uint8 = 1 - e = s.doStartBot(botName, "delete", &numIterations, func() { - eInner := s.deleteFinishCallback(botName) + e = s.doStartBot(userData, botName, "delete", &numIterations, func() { + eInner := s.deleteFinishCallback(userData, botName) if eInner != nil { - s.addKelpErrorToMap(makeKelpErrorResponseWrapper( + s.addKelpErrorToMap(userData, makeKelpErrorResponseWrapper( errorTypeBot, botName, time.Now().UTC(), @@ -62,10 +81,10 @@ func (s *APIServer) doStopBot(botName string) error { return nil } -func (s *APIServer) deleteFinishCallback(botName string) error { +func (s *APIServer) deleteFinishCallback(userData UserData, botName string) error { log.Printf("deleted offers for bot '%s'\n", botName) - e := s.kos.AdvanceBotState(botName, kelpos.BotStateStopping) + e := s.kos.BotDataForUser(userData.toUser()).AdvanceBotState(botName, kelpos.BotStateStopping) if e != nil { return fmt.Errorf("error advancing bot state when manually attempting to stop bot: %s", e) } diff --git a/gui/backend/upsert_bot_config.go b/gui/backend/upsert_bot_config.go index 681f1b2fe..e45a1bd80 100644 --- a/gui/backend/upsert_bot_config.go +++ b/gui/backend/upsert_bot_config.go @@ -24,6 +24,11 @@ import ( "github.com/stellar/kelp/trader" ) +type upsertBotConfigRequestWrapper struct { + UserData UserData `json:"user_data"` + UpsertBotConfigRequest upsertBotConfigRequest `json:"config_data"` +} + type upsertBotConfigRequest struct { Name string `json:"name"` Strategy string `json:"strategy"` @@ -58,14 +63,20 @@ func (s *APIServer) upsertBotConfig(w http.ResponseWriter, r *http.Request) { } log.Printf("upsertBotConfig requestJson: %s\n", string(bodyBytes)) - var req upsertBotConfigRequest - e = json.Unmarshal(bodyBytes, &req) + var reqWrapper upsertBotConfigRequestWrapper + e = json.Unmarshal(bodyBytes, &reqWrapper) if e != nil { s.writeErrorJson(w, fmt.Sprintf("error unmarshaling json: %s; bodyString = %s", e, string(bodyBytes))) return } + req := reqWrapper.UpsertBotConfigRequest + userID := reqWrapper.UserData.ID + if strings.TrimSpace(userID) == "" { + s.writeErrorJson(w, fmt.Sprintf("cannot have empty userID")) + return + } - botState, e := s.kos.QueryBotState(req.Name) + botState, e := s.kos.BotDataForUser(reqWrapper.UserData.toUser()).QueryBotState(req.Name) if e != nil { s.writeErrorJson(w, fmt.Sprintf("error getting bot state for bot '%s': %s", req.Name, e)) return @@ -88,14 +99,14 @@ func (s *APIServer) upsertBotConfig(w http.ResponseWriter, r *http.Request) { return } - e = s.setupOpsDirectory() + e = s.setupOpsDirectory(userID) if e != nil { s.writeError(w, fmt.Sprintf("error setting up ops directory: %s\n", e)) return } filenamePair := model2.GetBotFilenames(req.Name, req.Strategy) - traderFilePath := s.botConfigsPath.Join(filenamePair.Trader) + traderFilePath := s.botConfigsPathForUser(userID).Join(filenamePair.Trader) botConfig := req.TraderConfig log.Printf("upsert bot config to file: %s\n", traderFilePath.AsString()) e = toml.WriteFile(traderFilePath.Native(), &botConfig) @@ -104,7 +115,7 @@ func (s *APIServer) upsertBotConfig(w http.ResponseWriter, r *http.Request) { return } - strategyFilePath := s.botConfigsPath.Join(filenamePair.Strategy) + strategyFilePath := s.botConfigsPathForUser(userID).Join(filenamePair.Strategy) strategyConfig := req.StrategyConfig log.Printf("upsert strategy config to file: %s\n", strategyFilePath.AsString()) e = toml.WriteFile(strategyFilePath.Native(), &strategyConfig) @@ -114,7 +125,7 @@ func (s *APIServer) upsertBotConfig(w http.ResponseWriter, r *http.Request) { } // check if we need to create new funding accounts and new trustlines - s.reinitBotCheck(req) + s.reinitBotCheck(reqWrapper.UserData, req) s.writeJson(w, upsertBotConfigResponse{Success: true}) } @@ -254,7 +265,7 @@ func hasNewLevel(levels []plugins.StaticLevel) bool { return false } -func (s *APIServer) reinitBotCheck(req upsertBotConfigRequest) { +func (s *APIServer) reinitBotCheck(userData UserData, req upsertBotConfigRequest) { isTestnet := strings.Contains(req.TraderConfig.HorizonURL, "test") bot := &model2.Bot{ Name: req.Name, @@ -264,13 +275,13 @@ func (s *APIServer) reinitBotCheck(req upsertBotConfigRequest) { } // set bot state to initializing so it handles the update - s.kos.RegisterBotWithStateUpsert(bot, kelpos.InitState()) + s.kos.BotDataForUser(userData.toUser()).RegisterBotWithStateUpsert(bot, kelpos.InitState()) // we only want to start initializing bot once it has been created, so we only advance state if everything is completed go func() { tradingKP, e := keypair.Parse(req.TraderConfig.TradingSecretSeed) if e != nil { - s.addKelpErrorToMap(makeKelpErrorResponseWrapper( + s.addKelpErrorToMap(userData, makeKelpErrorResponseWrapper( errorTypeBot, bot.Name, time.Now().UTC(), @@ -286,7 +297,7 @@ func (s *APIServer) reinitBotCheck(req upsertBotConfigRequest) { traderAccount, e := s.checkFundAccount(client, tradingKP.Address(), bot.Name) if e != nil { - s.addKelpErrorToMap(makeKelpErrorResponseWrapper( + s.addKelpErrorToMap(userData, makeKelpErrorResponseWrapper( errorTypeBot, bot.Name, time.Now().UTC(), @@ -303,7 +314,7 @@ func (s *APIServer) reinitBotCheck(req upsertBotConfigRequest) { } e = s.checkAddTrustline(*traderAccount, tradingKP, req.TraderConfig.TradingSecretSeed, bot.Name, isTestnet, assets) if e != nil { - s.addKelpErrorToMap(makeKelpErrorResponseWrapper( + s.addKelpErrorToMap(userData, makeKelpErrorResponseWrapper( errorTypeBot, bot.Name, time.Now().UTC(), @@ -317,7 +328,7 @@ func (s *APIServer) reinitBotCheck(req upsertBotConfigRequest) { if req.TraderConfig.SourceSecretSeed != "" { sourceKP, e := keypair.Parse(req.TraderConfig.SourceSecretSeed) if e != nil { - s.addKelpErrorToMap(makeKelpErrorResponseWrapper( + s.addKelpErrorToMap(userData, makeKelpErrorResponseWrapper( errorTypeBot, bot.Name, time.Now().UTC(), @@ -328,7 +339,7 @@ func (s *APIServer) reinitBotCheck(req upsertBotConfigRequest) { } _, e = s.checkFundAccount(client, sourceKP.Address(), bot.Name) if e != nil { - s.addKelpErrorToMap(makeKelpErrorResponseWrapper( + s.addKelpErrorToMap(userData, makeKelpErrorResponseWrapper( errorTypeBot, bot.Name, time.Now().UTC(), @@ -340,9 +351,9 @@ func (s *APIServer) reinitBotCheck(req upsertBotConfigRequest) { } // advance bot state - e = s.kos.AdvanceBotState(bot.Name, kelpos.InitState()) + e = s.kos.BotDataForUser(userData.toUser()).AdvanceBotState(bot.Name, kelpos.InitState()) if e != nil { - s.addKelpErrorToMap(makeKelpErrorResponseWrapper( + s.addKelpErrorToMap(userData, makeKelpErrorResponseWrapper( errorTypeBot, bot.Name, time.Now().UTC(), diff --git a/gui/web/src/components/screens/NewBot/NewBot.js b/gui/web/src/components/screens/NewBot/NewBot.js index ab67eba0a..44e4c34db 100644 --- a/gui/web/src/components/screens/NewBot/NewBot.js +++ b/gui/web/src/components/screens/NewBot/NewBot.js @@ -72,7 +72,7 @@ class NewBot extends Component { }); var _this = this; - this._asyncRequests["botConfig"] = upsertBotConfig(this.props.baseUrl, JSON.stringify(this.state.configData)).then(resp => { + this._asyncRequests["botConfig"] = upsertBotConfig(this.props.baseUrl, this.state.configData).then(resp => { if (!_this._asyncRequests["botConfig"]) { // if it has been deleted it means we don't want to process the result return diff --git a/gui/web/src/kelp-ops-api/autogenerate.js b/gui/web/src/kelp-ops-api/autogenerate.js index 8f45dfd42..a2c2982e5 100644 --- a/gui/web/src/kelp-ops-api/autogenerate.js +++ b/gui/web/src/kelp-ops-api/autogenerate.js @@ -1,5 +1,12 @@ +import getUserData from "./getUserData"; + export default (baseUrl) => { - return fetch(baseUrl + "/api/v1/autogenerate").then(resp => { + return fetch(baseUrl + "/api/v1/autogenerate", { + method: "POST", + body: JSON.stringify({ + user_data: getUserData(), + }), + }).then(resp => { return resp.json(); }); }; \ No newline at end of file diff --git a/gui/web/src/kelp-ops-api/deleteBot.js b/gui/web/src/kelp-ops-api/deleteBot.js index 1f7e2f918..21f8142f4 100644 --- a/gui/web/src/kelp-ops-api/deleteBot.js +++ b/gui/web/src/kelp-ops-api/deleteBot.js @@ -1,7 +1,12 @@ +import getUserData from "./getUserData"; + export default (baseUrl, botName) => { return fetch(baseUrl + "/api/v1/deleteBot", { method: "POST", - body: botName, + body: JSON.stringify({ + user_data: getUserData(), + bot_name: botName, + }), }).then(resp => { return resp.text(); }); diff --git a/gui/web/src/kelp-ops-api/fetchKelpErrors.js b/gui/web/src/kelp-ops-api/fetchKelpErrors.js index dff6e47b4..f463015ec 100644 --- a/gui/web/src/kelp-ops-api/fetchKelpErrors.js +++ b/gui/web/src/kelp-ops-api/fetchKelpErrors.js @@ -1,5 +1,12 @@ +import getUserData from "./getUserData"; + export default (baseUrl) => { - return fetch(baseUrl + "/api/v1/fetchKelpErrors").then(resp => { + return fetch(baseUrl + "/api/v1/fetchKelpErrors", { + method: "POST", + body: JSON.stringify({ + user_data: getUserData(), + }), + }).then(resp => { return resp.json(); }); }; \ No newline at end of file diff --git a/gui/web/src/kelp-ops-api/genBotName.js b/gui/web/src/kelp-ops-api/genBotName.js index cb324a964..41cea366a 100644 --- a/gui/web/src/kelp-ops-api/genBotName.js +++ b/gui/web/src/kelp-ops-api/genBotName.js @@ -1,5 +1,12 @@ +import getUserData from "./getUserData"; + export default (baseUrl) => { - return fetch(baseUrl + "/api/v1/genBotName").then(resp => { - return resp.text(); + return fetch(baseUrl + "/api/v1/genBotName", { + method: "POST", + body: JSON.stringify({ + user_data: getUserData(), + }), + }).then(resp => { + return resp.jtext(); }); }; \ No newline at end of file diff --git a/gui/web/src/kelp-ops-api/getBotConfig.js b/gui/web/src/kelp-ops-api/getBotConfig.js index e9d3abf3f..884da3018 100644 --- a/gui/web/src/kelp-ops-api/getBotConfig.js +++ b/gui/web/src/kelp-ops-api/getBotConfig.js @@ -1,7 +1,12 @@ +import getUserData from "./getUserData"; + export default (baseUrl, botName) => { return fetch(baseUrl + "/api/v1/getBotConfig", { method: "POST", - body: botName, + body: JSON.stringify({ + user_data: getUserData(), + bot_name: botName, + }), }).then(resp => { return resp.json(); }); diff --git a/gui/web/src/kelp-ops-api/getBotInfo.js b/gui/web/src/kelp-ops-api/getBotInfo.js index a3fe85c20..a3e38b5e4 100644 --- a/gui/web/src/kelp-ops-api/getBotInfo.js +++ b/gui/web/src/kelp-ops-api/getBotInfo.js @@ -1,7 +1,12 @@ +import getUserData from "./getUserData"; + export default (baseUrl, botName, signal) => { return fetch(baseUrl + "/api/v1/getBotInfo", { method: "POST", - body: botName, + body: JSON.stringify({ + user_data: getUserData(), + bot_name: botName, + }), signal: signal, }).then(resp => { return resp.json(); diff --git a/gui/web/src/kelp-ops-api/getNewBotConfig.js b/gui/web/src/kelp-ops-api/getNewBotConfig.js index 4b3ee89b2..ea7758f5d 100644 --- a/gui/web/src/kelp-ops-api/getNewBotConfig.js +++ b/gui/web/src/kelp-ops-api/getNewBotConfig.js @@ -1,5 +1,12 @@ +import getUserData from "./getUserData"; + export default (baseUrl) => { - return fetch(baseUrl + "/api/v1/getNewBotConfig").then(resp => { + return fetch(baseUrl + "/api/v1/getNewBotConfig", { + method: "POST", + body: JSON.stringify({ + user_data: getUserData(), + }), + }).then(resp => { return resp.json(); }); }; \ No newline at end of file diff --git a/gui/web/src/kelp-ops-api/getState.js b/gui/web/src/kelp-ops-api/getState.js index 05093208f..25e9dd941 100644 --- a/gui/web/src/kelp-ops-api/getState.js +++ b/gui/web/src/kelp-ops-api/getState.js @@ -1,7 +1,12 @@ +import getUserData from "./getUserData"; + export default (baseUrl, botName) => { return fetch(baseUrl + "/api/v1/getState", { method: "POST", - body: botName, + body: JSON.stringify({ + user_data: getUserData(), + bot_name: botName, + }), }).then(resp => { return resp.text(); }); diff --git a/gui/web/src/kelp-ops-api/getUserData.js b/gui/web/src/kelp-ops-api/getUserData.js new file mode 100644 index 000000000..72522832c --- /dev/null +++ b/gui/web/src/kelp-ops-api/getUserData.js @@ -0,0 +1,5 @@ +export default () => { + return { + ID: "1347129431489", + }; +}; \ No newline at end of file diff --git a/gui/web/src/kelp-ops-api/listBots.js b/gui/web/src/kelp-ops-api/listBots.js index 56a21b52a..ce456844d 100644 --- a/gui/web/src/kelp-ops-api/listBots.js +++ b/gui/web/src/kelp-ops-api/listBots.js @@ -1,5 +1,12 @@ +import getUserData from "./getUserData"; + export default (baseUrl) => { - return fetch(baseUrl + "/api/v1/listBots").then(resp => { + return fetch(baseUrl + "/api/v1/listBots", { + method: "POST", + body: JSON.stringify({ + user_data: getUserData(), + }), + }).then(resp => { return resp.json(); }); }; \ No newline at end of file diff --git a/gui/web/src/kelp-ops-api/removeKelpErrors.js b/gui/web/src/kelp-ops-api/removeKelpErrors.js index ca6eb31b5..bd9c069db 100644 --- a/gui/web/src/kelp-ops-api/removeKelpErrors.js +++ b/gui/web/src/kelp-ops-api/removeKelpErrors.js @@ -1,11 +1,12 @@ +import getUserData from "./getUserData"; + export default (baseUrl, kelpErrorIDs) => { - const data = { - kelp_error_ids: kelpErrorIDs - }; - return fetch(baseUrl + "/api/v1/removeKelpErrors", { method: "POST", - body: JSON.stringify(data) + body: JSON.stringify({ + user_data: getUserData(), + kelp_error_ids: kelpErrorIDs, + }), }).then(resp => { return resp.json(); }); diff --git a/gui/web/src/kelp-ops-api/start.js b/gui/web/src/kelp-ops-api/start.js index 0dbb26f90..d312ea6cf 100644 --- a/gui/web/src/kelp-ops-api/start.js +++ b/gui/web/src/kelp-ops-api/start.js @@ -1,7 +1,12 @@ +import getUserData from "./getUserData"; + export default (baseUrl, botName) => { return fetch(baseUrl + "/api/v1/start", { method: "POST", - body: botName, + body: JSON.stringify({ + user_data: getUserData(), + bot_name: botName, + }), }).then(resp => { return resp.json(); }); diff --git a/gui/web/src/kelp-ops-api/stop.js b/gui/web/src/kelp-ops-api/stop.js index 352909304..a61b09356 100644 --- a/gui/web/src/kelp-ops-api/stop.js +++ b/gui/web/src/kelp-ops-api/stop.js @@ -1,7 +1,12 @@ +import getUserData from "./getUserData"; + export default (baseUrl, botName) => { return fetch(baseUrl + "/api/v1/stop", { method: "POST", - body: botName, + body: JSON.stringify({ + user_data: getUserData(), + bot_name: botName, + }), }).then(resp => { return resp.text(); }); diff --git a/gui/web/src/kelp-ops-api/upsertBotConfig.js b/gui/web/src/kelp-ops-api/upsertBotConfig.js index a1cfa749c..0afa7d4a9 100644 --- a/gui/web/src/kelp-ops-api/upsertBotConfig.js +++ b/gui/web/src/kelp-ops-api/upsertBotConfig.js @@ -1,7 +1,12 @@ +import getUserData from "./getUserData"; + export default (baseUrl, configData) => { return fetch(baseUrl + "/api/v1/upsertBotConfig", { method: "POST", - body: configData, + body: JSON.stringify({ + user_data: getUserData(), + config_data: configData, + }), }).then(resp => { return resp.json(); }); diff --git a/support/kelpos/kelpos.go b/support/kelpos/kelpos.go index bef3874ee..db773f890 100644 --- a/support/kelpos/kelpos.go +++ b/support/kelpos/kelpos.go @@ -9,19 +9,20 @@ import ( "sync" "github.com/stellar/go/support/errors" - "github.com/stellar/kelp/gui/model2" ) const dotKelpDir = ".kelp" // KelpOS is a struct that manages all subprocesses started by this Kelp process type KelpOS struct { - binDir *OSPath - dotKelpWorkingDir *OSPath - processes map[string]Process - processLock *sync.Mutex - bots map[string]*BotInstance - botLock *sync.Mutex + binDir *OSPath + dotKelpWorkingDir *OSPath + processes map[string]Process + processLock *sync.Mutex + userBotData map[string]*UserBotData + userBotDataLock *sync.Mutex + + // uninitialized silentRegistrations bool } @@ -90,15 +91,25 @@ func makeKelpOS() *KelpOS { dotKelpWorkingDir: dotKelpWorkingDir, processes: map[string]Process{}, processLock: &sync.Mutex{}, - bots: map[string]*BotInstance{}, - botLock: &sync.Mutex{}, + userBotData: map[string]*UserBotData{}, + userBotDataLock: &sync.Mutex{}, } } -// BotInstance is an instance of a given bot along with the metadata -type BotInstance struct { - Bot *model2.Bot - State BotState +// BotDataForUser gets the UserBotData for a given user +func (kos *KelpOS) BotDataForUser(user *User) *UserBotData { + kos.userBotDataLock.Lock() + defer kos.userBotDataLock.Unlock() + + var ubd *UserBotData + if v, ok := kos.userBotData[user.ID]; ok { + ubd = v + } else { + ubd = makeUserBotData(kos, user) + kos.userBotData[user.ID] = ubd + } + + return ubd } // GetKelpOS gets the singleton instance diff --git a/support/kelpos/user.go b/support/kelpos/user.go new file mode 100644 index 000000000..0da662621 --- /dev/null +++ b/support/kelpos/user.go @@ -0,0 +1,25 @@ +package kelpos + +import ( + "fmt" +) + +// User is a struct that represents a user +type User struct { + // for now this is just userID, but later this may be expanded to become host:userID (UUID) for example + ID string +} + +// MakeUser is a factory method to create a User +func MakeUser( + ID string, +) *User { + return &User{ + ID: ID, + } +} + +// String is the standard stringer method +func (u *User) String() string { + return fmt.Sprintf("User[ID=%s]", u.ID) +} diff --git a/support/kelpos/bots.go b/support/kelpos/userBotData.go similarity index 59% rename from support/kelpos/bots.go rename to support/kelpos/userBotData.go index 79760f498..c47d7b27c 100644 --- a/support/kelpos/bots.go +++ b/support/kelpos/userBotData.go @@ -4,22 +4,47 @@ import ( "fmt" "log" "strings" + "sync" "github.com/stellar/kelp/gui/model2" ) +// BotInstance is an instance of a given bot along with the metadata +type BotInstance struct { + Bot *model2.Bot + State BotState +} + +// UserBotData represents the Bot registration map and other items related to a given user +type UserBotData struct { + user *User + kos *KelpOS + bots map[string]*BotInstance + botLock *sync.Mutex +} + +// makeUserBotData is a factory method +func makeUserBotData(kos *KelpOS, user *User) *UserBotData { + return &UserBotData{ + kos: kos, + user: user, + bots: map[string]*BotInstance{}, + botLock: &sync.Mutex{}, + } +} + // RegisterBot registers a new bot, returning an error if one already exists with the same name -func (kos *KelpOS) RegisterBot(bot *model2.Bot) error { - return kos.RegisterBotWithState(bot, InitState()) +func (ubd *UserBotData) RegisterBot(bot *model2.Bot) error { + return ubd.RegisterBotWithState(bot, InitState()) } // SafeUnregisterBot unregister a bot without any errors -func (kos *KelpOS) SafeUnregisterBot(botName string) { - kos.botLock.Lock() - defer kos.botLock.Unlock() +func (ubd *UserBotData) SafeUnregisterBot(botName string) { + ubd.botLock.Lock() + defer ubd.botLock.Unlock() - if _, exists := kos.bots[botName]; exists { - delete(kos.bots, botName) + if _, exists := ubd.bots[botName]; exists { + delete(ubd.bots, botName) log.Printf("unregistered bot with name '%s'", botName) } else { log.Printf("no bot registered with name '%s'", botName) @@ -27,21 +52,21 @@ func (kos *KelpOS) SafeUnregisterBot(botName string) { } // RegisterBotWithState registers a new bot with a given state, returning an error if one already exists with the same name -func (kos *KelpOS) RegisterBotWithState(bot *model2.Bot, state BotState) error { - return kos.registerBotWithState(bot, state, false) +func (ubd *UserBotData) RegisterBotWithState(bot *model2.Bot, state BotState) error { + return ubd.registerBotWithState(bot, state, false) } // RegisterBotWithStateUpsert registers a new bot with a given state, it always registers the bot even if it is already registered, never returning an error -func (kos *KelpOS) RegisterBotWithStateUpsert(bot *model2.Bot, state BotState) { - _ = kos.registerBotWithState(bot, state, true) +func (ubd *UserBotData) RegisterBotWithStateUpsert(bot *model2.Bot, state BotState) { + _ = ubd.registerBotWithState(bot, state, true) } // registerBotWithState registers a new bot with a given state, returning an error if one already exists with the same name -func (kos *KelpOS) registerBotWithState(bot *model2.Bot, state BotState, forceRegister bool) error { - kos.botLock.Lock() - defer kos.botLock.Unlock() +func (ubd *UserBotData) registerBotWithState(bot *model2.Bot, state BotState, forceRegister bool) error { + ubd.botLock.Lock() + defer ubd.botLock.Unlock() - _, exists := kos.bots[bot.Name] + _, exists := ubd.bots[bot.Name] if exists { if !forceRegister { return fmt.Errorf("bot '%s' already registered", bot.Name) @@ -49,7 +74,7 @@ func (kos *KelpOS) registerBotWithState(bot *model2.Bot, state BotState, forceRe log.Printf("bot '%s' already registered, but re-registering with state '%s' because forceRegister was set", bot.Name, state) } - kos.bots[bot.Name] = &BotInstance{ + ubd.bots[bot.Name] = &BotInstance{ Bot: bot, State: state, } @@ -57,11 +82,11 @@ func (kos *KelpOS) registerBotWithState(bot *model2.Bot, state BotState, forceRe } // AdvanceBotState advances the state of the given bot atomically, ensuring the bot is currently at the expected state -func (kos *KelpOS) AdvanceBotState(botName string, expectedCurrentState BotState) error { - kos.botLock.Lock() - defer kos.botLock.Unlock() +func (ubd *UserBotData) AdvanceBotState(botName string, expectedCurrentState BotState) error { + ubd.botLock.Lock() + defer ubd.botLock.Unlock() - b, exists := kos.bots[botName] + b, exists := ubd.bots[botName] if !exists { return fmt.Errorf("bot '%s' is not registered", botName) } @@ -82,11 +107,11 @@ func (kos *KelpOS) AdvanceBotState(botName string, expectedCurrentState BotState } // GetBot fetches the bot state for the given name -func (kos *KelpOS) GetBot(botName string) (*BotInstance, error) { - kos.botLock.Lock() - defer kos.botLock.Unlock() +func (ubd *UserBotData) GetBot(botName string) (*BotInstance, error) { + ubd.botLock.Lock() + defer ubd.botLock.Unlock() - b, exists := kos.bots[botName] + b, exists := ubd.bots[botName] if !exists { return b, fmt.Errorf("bot '%s' does not exist", botName) } @@ -94,8 +119,8 @@ func (kos *KelpOS) GetBot(botName string) (*BotInstance, error) { } // QueryBotState checks to see if the bot is actually running and returns the state accordingly -func (kos *KelpOS) QueryBotState(botName string) (BotState, error) { - if bi, e := kos.GetBot(botName); e == nil { +func (ubd *UserBotData) QueryBotState(botName string) (BotState, error) { + if bi, e := ubd.GetBot(botName); e == nil { // read initializing state from memory because it's hard to figure that out from the logic below if bi.State == BotStateInitializing { return bi.State, nil @@ -104,7 +129,7 @@ func (kos *KelpOS) QueryBotState(botName string) (BotState, error) { prefix := getBotNamePrefix(botName) command := fmt.Sprintf("ps aux | grep trade | grep %s | grep -v grep", prefix) - outputBytes, e := kos.Blocking(fmt.Sprintf("query_bot_state: %s", botName), command) + outputBytes, e := ubd.kos.Blocking(fmt.Sprintf("query_bot_state: %s", botName), command) if e != nil { if strings.Contains(e.Error(), "exit status 1") { return BotStateStopped, nil @@ -120,9 +145,9 @@ func (kos *KelpOS) QueryBotState(botName string) (BotState, error) { } // RegisteredBots returns the list of registered bots -func (kos *KelpOS) RegisteredBots() []string { +func (ubd *UserBotData) RegisteredBots() []string { list := []string{} - for k, _ := range kos.bots { + for k := range ubd.bots { list = append(list, k) } return list