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}