diff --git a/gui/backend/new_secret_key.go b/gui/backend/new_secret_key.go
new file mode 100644
index 000000000..bba379dd0
--- /dev/null
+++ b/gui/backend/new_secret_key.go
@@ -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))
+}
diff --git a/gui/backend/routes.go b/gui/backend/routes.go
index c2ad55007..26df41df0 100644
--- a/gui/backend/routes.go
+++ b/gui/backend/routes.go
@@ -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))
diff --git a/gui/backend/upsert_bot_config.go b/gui/backend/upsert_bot_config.go
index 60c4e676e..c042f8a0b 100644
--- a/gui/backend/upsert_bot_config.go
+++ b/gui/backend/upsert_bot_config.go
@@ -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"
@@ -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 {
@@ -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
@@ -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
+}
diff --git a/gui/web/src/components/_styles/grid.module.scss b/gui/web/src/components/_styles/grid.module.scss
index 02817fc82..2d2594684 100644
--- a/gui/web/src/components/_styles/grid.module.scss
+++ b/gui/web/src/components/_styles/grid.module.scss
@@ -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%;
diff --git a/gui/web/src/components/atoms/Input/Input.js b/gui/web/src/components/atoms/Input/Input.js
index ac59c0d2e..735db4138 100644
--- a/gui/web/src/components/atoms/Input/Input.js
+++ b/gui/web/src/components/atoms/Input/Input.js
@@ -27,7 +27,6 @@ class Input extends Component {
error: PropTypes.string,
size: PropTypes.string,
disabled: PropTypes.bool,
- showError: PropTypes.bool,
onChange: PropTypes.func
};
@@ -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);
}
@@ -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);
}
}
@@ -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],
@@ -161,7 +182,7 @@ class Input extends Component {
{this.props.suffix}
)}
- { this.props.showError && (
+ { this.props.error !== null && (
{this.props.error}
)}
diff --git a/gui/web/src/components/molecules/BotCard/BotCard.js b/gui/web/src/components/molecules/BotCard/BotCard.js
index 40d8c40a4..5dac64e07 100644
--- a/gui/web/src/components/molecules/BotCard/BotCard.js
+++ b/gui/web/src/components/molecules/BotCard/BotCard.js
@@ -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';
diff --git a/gui/web/src/components/molecules/ErrorMessage/ErrorMessage.js b/gui/web/src/components/molecules/ErrorMessage/ErrorMessage.js
index 445b2997b..65f5100d7 100644
--- a/gui/web/src/components/molecules/ErrorMessage/ErrorMessage.js
+++ b/gui/web/src/components/molecules/ErrorMessage/ErrorMessage.js
@@ -8,8 +8,7 @@ class ErrorMessage extends Component {
Oops, something is not right.
-
Please, review the fields marked in red and try again.
-
+
{this.props.error}
);
}
diff --git a/gui/web/src/components/molecules/Form/Form.js b/gui/web/src/components/molecules/Form/Form.js
index c5e85741c..9317e53ee 100644
--- a/gui/web/src/components/molecules/Form/Form.js
+++ b/gui/web/src/components/molecules/Form/Form.js
@@ -17,7 +17,8 @@ import FieldGroup from '../FieldGroup/FieldGroup';
import PriceFeedAsset from '../PriceFeedAsset/PriceFeedAsset';
import PriceFeedFormula from '../PriceFeedFormula/PriceFeedFormula';
import Levels from '../Levels/Levels';
-// import ErrorMessage from '../ErrorMessage/ErrorMessage';
+import ErrorMessage from '../ErrorMessage/ErrorMessage';
+import newSecretKey from '../../../kelp-ops-api/newSecretKey';
class Form extends Component {
constructor(props) {
@@ -36,9 +37,20 @@ class Form extends Component {
this.newLevel = this.newLevel.bind(this);
this.hasNewLevel = this.hasNewLevel.bind(this);
this.removeLevel = this.removeLevel.bind(this);
+ this.newSecret = this.newSecret.bind(this);
+ this.getError = this.getError.bind(this);
this._emptyLevel = this._emptyLevel.bind(this);
this._triggerUpdateLevels = this._triggerUpdateLevels.bind(this);
+ this._fetchDotNotation = this._fetchDotNotation.bind(this);
this._last_fill_tracker_sleep_millis = 1000;
+
+ this._asyncRequests = {};
+ }
+
+ componentWillUnmount() {
+ if (this._asyncRequests["secretKey"]) {
+ delete this._asyncRequests["secretKey"];
+ }
}
setLoadingFormula() {
@@ -61,6 +73,27 @@ class Form extends Component {
}
}
+ _fetchDotNotation(obj, path) {
+ let parts = path.split('.');
+ for (let i = 0; i < parts.length; i++) {
+ obj = obj[parts[i]];
+
+ if (obj === undefined || obj === null || obj === "" || obj === 0) {
+ return null;
+ }
+ }
+
+ return obj
+ }
+
+ getError(fieldKey) {
+ if (!this.props.errorResp) {
+ return null;
+ }
+
+ return this._fetchDotNotation(this.props.errorResp.fields, fieldKey);
+ }
+
save() {
this.setState({
isSaving: true,
@@ -133,8 +166,8 @@ class Form extends Component {
_emptyLevel() {
return {
- amount: "0.00",
- spread: "0.00",
+ amount: 0.00,
+ spread: 0.00,
};
}
@@ -146,15 +179,83 @@ class Form extends Component {
)
}
+ newSecret(field) {
+ var _this = this;
+ this._asyncRequests["secretKey"] = newSecretKey(this.props.baseUrl).then(resp => {
+ if (!_this._asyncRequests["secretKey"]) {
+ // if it has been deleted it means we don't want to process the result
+ return
+ }
+
+ delete _this._asyncRequests["secretKey"];
+ this.props.onChange(field, {target: {value: resp}});
+ });
+ }
+
render() {
// let tradingPlatform = "sdex";
// if (this.props.configData.trader_config.trading_exchange && this.props.configData.trader_config.trading_exchange !== "") {
// tradingPlatform = this.props.configData.trader_config.trading_exchange;
// }
- let network = "TestNet";
- if (!this.props.configData.trader_config.horizon_url.includes("test")) {
- network = "PubNet";
+ let traderSecretKeyInput = (
+ { this.props.onChange("trader_config.trading_secret_seed", event) }}
+ error={this.getError("trader_config.trading_secret_seed")}
+ />
+ );
+ let sourceSecretKeyInput = (
+ { this.props.onChange("trader_config.source_secret_seed", event) }}
+ error={this.getError("trader_config.source_secret_seed")}
+ />
+ );
+ let network = "PubNet";
+ let traderSecretKey = traderSecretKeyInput;
+ let sourceSecretKey = sourceSecretKeyInput;
+ if (this.props.configData.trader_config.horizon_url.includes("test")) {
+ network = "TestNet";
+ traderSecretKey = (
+
+
+ {traderSecretKeyInput}
+
+
+
+
+ );
+ sourceSecretKey = (
+
+
+ {sourceSecretKeyInput}
+
+
+
+
+ );
+ }
+
+ let error = "";
+ if (this.props.errorResp) {
+ error = ();
}
return (
@@ -165,6 +266,8 @@ class Form extends Component {
*/}
+ {error}
+
{ this.props.onChange("name", event) }}
disabled={!this.props.isNew}
+ error={this.getError("name")}
/>
{/* Trader Settings */}
@@ -225,13 +329,7 @@ class Form extends Component {
- { this.props.onChange("trader_config.trading_secret_seed", event) }}
- error="Please enter a valid trader account secret key"
- showError={false}
- />
+ {traderSecretKey}
@@ -245,6 +343,7 @@ class Form extends Component {
value={this.props.configData.trader_config.asset_code_a}
type="string"
onChange={(event) => { this.props.onChange("trader_config.asset_code_a", event) }}
+ error={this.getError("trader_config.asset_code_a")}
/>
@@ -257,6 +356,7 @@ class Form extends Component {
type="string"
onChange={(event) => { this.props.onChange("trader_config.issuer_a", event) }}
disabled={this.props.configData.trader_config.asset_code_a === "XLM"}
+ error={this.getError("trader_config.issuer_a")}
/>
@@ -270,6 +370,7 @@ class Form extends Component {
value={this.props.configData.trader_config.asset_code_b}
type="string"
onChange={(event) => { this.props.onChange("trader_config.asset_code_b", event) }}
+ error={this.getError("trader_config.asset_code_b")}
/>
@@ -282,6 +383,7 @@ class Form extends Component {
type="string"
onChange={(event) => { this.props.onChange("trader_config.issuer_b", event) }}
disabled={this.props.configData.trader_config.asset_code_b === "XLM"}
+ error={this.getError("trader_config.issuer_b")}
/>
@@ -294,11 +396,7 @@ class Form extends Component {
- { this.props.onChange("trader_config.source_secret_seed", event) }}
- />
+ {sourceSecretKey}
@@ -306,8 +404,9 @@ class Form extends Component {
{ this.props.onChange("trader_config.tick_interval_seconds", event) }}
+ error={this.getError("trader_config.tick_interval_seconds")}
/>
@@ -316,8 +415,9 @@ class Form extends Component {
{ this.props.onChange("trader_config.max_tick_delay_millis", event) }}
+ error={this.getError("trader_config.max_tick_delay_millis")}
/>
@@ -340,8 +440,9 @@ class Form extends Component {
{ this.props.onChange("trader_config.delete_cycles_threshold", event) }}
+ error={this.getError("trader_config.delete_cycles_threshold")}
/>
@@ -365,8 +466,9 @@ class Form extends Component {
{
this.props.onChange("trader_config.fill_tracker_sleep_millis", event, {
"trader_config.fill_tracker_sleep_millis": (value) => {
@@ -389,9 +491,11 @@ class Form extends Component {
{ this.props.onChange("trader_config.fill_tracker_delete_cycles_threshold", event) }}/>
+ onChange={(event) => { this.props.onChange("trader_config.fill_tracker_delete_cycles_threshold", event) }}
+ error={this.getError("trader_config.fill_tracker_delete_cycles_threshold")}
+ />
@@ -401,8 +505,9 @@ class Form extends Component {
{ this.props.onChange("trader_config.fee.capacity_trigger", event) }}
+ error={this.getError("trader_config.fee.capacity_trigger")}
/>
@@ -414,8 +519,9 @@ class Form extends Component {
{ this.props.onChange("trader_config.fee.percentile", event) }}
+ error={this.getError("trader_config.fee.percentile")}
/>
@@ -427,8 +533,9 @@ class Form extends Component {
{ this.props.onChange("trader_config.fee.max_op_fee_stroops", event) }}
+ error={this.getError("trader_config.fee.max_op_fee_stroops")}
/>
@@ -442,8 +549,9 @@ class Form extends Component {
{ this.props.onChange("trader_config.centralized_price_precision_override", event) }}
+ error={this.getError("trader_config.centralized_price_precision_override")}
/>
@@ -453,8 +561,9 @@ class Form extends Component {
{ this.props.onChange("trader_config.centralized_volume_precision_override", event) }}
+ error={this.getError("trader_config.centralized_volume_precision_override")}
/>
@@ -467,8 +576,9 @@ class Form extends Component {
{ this.props.onChange("trader_config.centralized_min_base_volume_override", event) }}
+ error={this.getError("trader_config.centralized_min_base_volume_override")}
/>
@@ -478,8 +588,9 @@ class Form extends Component {
{ this.props.onChange("trader_config.centralized_min_quote_volume_override", event) }}
+ error={this.getError("trader_config.centralized_min_quote_volume_override")}
/>
@@ -541,6 +652,7 @@ class Form extends Component {
newLevel={this.newLevel}
hasNewLevel={this.hasNewLevel}
onRemove={(levelIdx) => { this.removeLevel(levelIdx) }}
+ error={this.getError("strategy_config.levels")}
/>
@@ -558,8 +670,9 @@ class Form extends Component {
{ this.props.onChange("strategy_config.price_tolerance", event) }}
+ error={this.getError("strategy_config.price_tolerance")}
/>
@@ -569,8 +682,9 @@ class Form extends Component {
{ this.props.onChange("strategy_config.amount_tolerance", event) }}
+ error={this.getError("strategy_config.amount_tolerance")}
/>
@@ -583,8 +697,9 @@ class Form extends Component {
{ this.props.onChange("strategy_config.rate_offset_percent", event) }}
+ error={this.getError("strategy_config.rate_offset_percent")}
/>
@@ -593,8 +708,9 @@ class Form extends Component {
{ this.props.onChange("strategy_config.rate_offset", event) }}
+ error={this.getError("strategy_config.rate_offset")}
/>
@@ -617,9 +733,8 @@ class Form extends Component {
- {/* */}
-
+ {error}