Skip to content
This repository has been archived by the owner on Feb 1, 2024. It is now read-only.

Kelp UI - validation for edit form and autogen secret keys #196

Merged
18 changes: 18 additions & 0 deletions gui/backend/new_secret_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package backend

import (
"fmt"
"net/http"

"github.com/stellar/go/keypair"
)

func (s *APIServer) newSecretKey(w http.ResponseWriter, r *http.Request) {
kp, e := keypair.Random()
if e != nil {
s.writeError(w, fmt.Sprintf("error generating keypair: %s\n", e))
return
}
seed := kp.Seed()
w.Write([]byte(seed))
}
1 change: 1 addition & 0 deletions gui/backend/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func SetRoutes(r *chi.Mux, s *APIServer) {
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.Post("/start", http.HandlerFunc(s.startBot))
r.Post("/stop", http.HandlerFunc(s.stopBot))
Expand Down
65 changes: 65 additions & 0 deletions gui/backend/upsert_bot_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"log"
"net/http"

"github.com/stellar/go/strkey"
"github.com/stellar/kelp/gui/model"
"github.com/stellar/kelp/plugins"
"github.com/stellar/kelp/support/kelpos"
Expand All @@ -25,6 +26,18 @@ type upsertBotConfigResponse struct {
Success bool `json:"success"`
}

type upsertBotConfigResponseErrors struct {
Error string `json:"error"`
Fields upsertBotConfigRequest `json:"fields"`
}

func makeUpsertError(fields upsertBotConfigRequest) *upsertBotConfigResponseErrors {
return &upsertBotConfigResponseErrors{
Error: "There are some errors marked in red inline",
Fields: fields,
}
}

func (s *APIServer) upsertBotConfig(w http.ResponseWriter, r *http.Request) {
bodyBytes, e := ioutil.ReadAll(r.Body)
if e != nil {
Expand All @@ -50,6 +63,11 @@ func (s *APIServer) upsertBotConfig(w http.ResponseWriter, r *http.Request) {
return
}

if errResp := s.validateConfigs(req); errResp != nil {
s.writeJson(w, errResp)
return
}

filenamePair := model.GetBotFilenames(req.Name, req.Strategy)
traderFilePath := fmt.Sprintf("%s/%s", s.configsDir, filenamePair.Trader)
botConfig := req.TraderConfig
Expand All @@ -71,3 +89,50 @@ func (s *APIServer) upsertBotConfig(w http.ResponseWriter, r *http.Request) {

s.writeJson(w, upsertBotConfigResponse{Success: true})
}

func (s *APIServer) validateConfigs(req upsertBotConfigRequest) *upsertBotConfigResponseErrors {
hasError := false
errResp := upsertBotConfigRequest{
TraderConfig: trader.BotConfig{},
StrategyConfig: plugins.BuySellConfig{},
}

if _, e := strkey.Decode(strkey.VersionByteSeed, req.TraderConfig.TradingSecretSeed); e != nil {
errResp.TraderConfig.TradingSecretSeed = "invalid Trader Secret Key"
hasError = true
}

if req.TraderConfig.AssetCodeA == "" || len(req.TraderConfig.AssetCodeA) > 12 {
errResp.TraderConfig.AssetCodeA = "1 - 12 characters"
hasError = true
}

if req.TraderConfig.AssetCodeB == "" || len(req.TraderConfig.AssetCodeB) > 12 {
errResp.TraderConfig.AssetCodeB = "1 - 12 characters"
hasError = true
}

if _, e := strkey.Decode(strkey.VersionByteSeed, req.TraderConfig.SourceSecretSeed); req.TraderConfig.SourceSecretSeed != "" && e != nil {
errResp.TraderConfig.SourceSecretSeed = "invalid Source Secret Key"
hasError = true
}

if len(req.StrategyConfig.Levels) == 0 || hasNewLevel(req.StrategyConfig.Levels) {
errResp.StrategyConfig.Levels = []plugins.StaticLevel{}
hasError = true
}

if hasError {
return makeUpsertError(errResp)
}
return nil
}

func hasNewLevel(levels []plugins.StaticLevel) bool {
for _, l := range levels {
if l.AMOUNT == 0 || l.SPREAD == 0 {
return true
}
}
return false
}
12 changes: 12 additions & 0 deletions gui/web/src/components/_styles/grid.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@
max-width: 66.667%;
}

.col10p {
composes: _col;
flex-basis: 10.0%;
max-width: 10.00%;
}

.col90p {
composes: _col;
flex-basis: 90.0%;
max-width: 90.00%;
}

.colPriceSelector {
composes: _col;
flex-basis: 90%;
Expand Down
33 changes: 27 additions & 6 deletions gui/web/src/components/atoms/Input/Input.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ class Input extends Component {
error: PropTypes.string,
size: PropTypes.string,
disabled: PropTypes.bool,
showError: PropTypes.bool,
onChange: PropTypes.func
};

Expand All @@ -42,10 +41,32 @@ class Input extends Component {
if (this.props.type === "int" || this.props.type === "float") {
// convert back to number so it is set correctly in the state
newEvent = { target: { value: +checked } };
} else if (this.props.type === "int_positive" || this.props.type === "float_positive") {
// ensure it is positive
let val = +checked
if (val === 0) {
// don't allow an update if it's zero
return
} else if (val < 0) {
val = -val
}
newEvent = { target: { value: val } };
} else if (this.props.type === "percent") {
// convert back to representation passed in to complete the abstraction of a % value input
// use event.target.value instead of checked here, because checked modified the value which is itself already modified
newEvent = { target: { value: +event.target.value / 100 } };
} else if (this.props.type === "percent_positive") {
// ensure it is positive
// use event.target.value instead of checked here, because checked modified the value which is itself already modified
let val = +event.target.value
if (val === 0) {
// don't allow an update if it's zero
return
} else if (val < 0) {
val = -val
}
// convert back to representation passed in to complete the abstraction of a % value input
newEvent = { target: { value: val / 100 } };
}
this.props.onChange(newEvent);
}
Expand All @@ -54,11 +75,11 @@ class Input extends Component {
checkType(input) {
if (this.props.type === "string") {
return this.isString(input);
} else if (this.props.type === "int") {
} else if (this.props.type === "int" || this.props.type === "int_positive") {
return this.isInt(input);
} else if (this.props.type === "float") {
} else if (this.props.type === "float" || this.props.type === "float_positive") {
return this.isFloat(input);
} else if (this.props.type === "percent") {
} else if (this.props.type === "percent" || this.props.type === "percent_positive") {
return this.isPercent(input);
}
}
Expand Down Expand Up @@ -129,7 +150,7 @@ class Input extends Component {
}

render() {
const errorActive = this.props.showError ? styles.inputError : null;
const errorActive = this.props.error !== null ? styles.inputError : null;
const inputClassList = classNames(
styles.input,
styles[this.props.size],
Expand Down Expand Up @@ -161,7 +182,7 @@ class Input extends Component {
<p className={suffixClassList}>{this.props.suffix}</p>
)}

{ this.props.showError && (
{ this.props.error !== null && (
<p className={styles.errorMessage}>{this.props.error}</p>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion gui/web/src/components/molecules/BotCard/BotCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Pill from '../../atoms/Pill/Pill';
import RunStatus from '../../atoms/RunStatus/RunStatus';
import chartThumb from '../../../assets/images/chart-thumb.png';
// import chartThumb from '../../../assets/images/chart-thumb.png';
import styles from './BotCard.module.scss';
import PillGroup from '../PillGroup/PillGroup';
import StartStop from '../../atoms/StartStop/StartStop';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ class ErrorMessage extends Component {
<div className={styles.wrapper}>
<p className={styles.title}>Oops, something is not right.
</p>
<p className={styles.text}>Please, review the fields marked in red and try again.
</p>
<p className={styles.text}>{this.props.error}</p>
</div>
);
}
Expand Down
Loading