diff --git a/package-lock.json b/package-lock.json index cd0e2300a2..d51f94522c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3022,10 +3022,8 @@ } }, "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true + "version": "github:aminmarashi/clone#d97b4f0ff3d3afebcaaf4a2ecc9c50fbce914900", + "from": "github:aminmarashi/clone#d97b4f" }, "clone-buffer": { "version": "1.0.0", @@ -9365,24 +9363,18 @@ "dev": true }, "js-interpreter": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/js-interpreter/-/js-interpreter-1.4.6.tgz", - "integrity": "sha1-DHv71+9qU8wbO6FtkGtji/CFUhQ=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/js-interpreter/-/js-interpreter-2.1.0.tgz", + "integrity": "sha512-ZaBpfhtBuWhySGjBwdpuuMH/loUXk5XNVNCOQs6rEImmzxr1NeFfYi4DtdDpB5t4pth3dmL46DcZP8W4SY9SVw==", "dev": true, "requires": { - "acorn": "^4.0.11", - "clone": "github:aminmarashi/clone#d97b4f" + "minimist": "^1.2.0" }, "dependencies": { - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", - "dev": true - }, - "clone": { - "version": "github:aminmarashi/clone#d97b4f0ff3d3afebcaaf4a2ecc9c50fbce914900", - "from": "github:aminmarashi/clone#d97b4f", + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } } diff --git a/package.json b/package.json index 12cf1bb8fd..eda7a5102b 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "jquery": "^3.4.1", "jquery-ui": "1.12.1", "jquery-ui-css": "1.11.4", - "js-interpreter": "^1.4.6", + "js-interpreter": "^2.1.0", "json2csv": "^3.11.5", "lint-staged": "^8.1.7", "loader-utils": "^1.1.0", @@ -105,6 +105,7 @@ "@binary-com/smartcharts": "^0.6.1", "binary-style": "^0.2.4", "blockly": "github:google/blockly#59e5ac6", + "clone": "aminmarashi/clone#d97b4f", "commander": "^2.20.0", "concat-stream": "^2.0.0", "core-js": "^2.6.5", diff --git a/src/botPage/bot/Interpreter.js b/src/botPage/bot/Interpreter.js index 73eaac960d..b0c41397fd 100644 --- a/src/botPage/bot/Interpreter.js +++ b/src/botPage/bot/Interpreter.js @@ -1,14 +1,27 @@ +import clone from 'clone'; import JSInterpreter from 'js-interpreter'; import { observer as globalObserver } from '../../common/utils/observer'; import { createScope } from './CliTools'; import Interface from './Interface'; +/* eslint-disable func-names, no-underscore-dangle */ +JSInterpreter.prototype.takeStateSnapshot = function() { + const newStateStack = clone(this.stateStack, undefined, undefined, undefined, true); + return newStateStack; +}; + +JSInterpreter.prototype.restoreStateSnapshot = function(snapshot) { + this.stateStack = clone(snapshot, undefined, undefined, undefined, true); + this.global = this.stateStack[0].scope; + this.initFunc_(this, this.global); +}; +/* eslint-enable */ + const unrecoverableErrors = [ 'InsufficientBalance', 'CustomLimitsReached', 'OfferingsValidationError', 'InvalidCurrency', - 'ContractBuyValidationError', 'NotDefaultCurrency', 'PleaseAuthenticate', 'FinancialAssessmentRequired', @@ -56,9 +69,9 @@ export default class Interpreter { const pseudoBotIf = interpreter.nativeToPseudo(BotIf); - Object.entries(ticksIf).forEach(([name, f]) => - interpreter.setProperty(pseudoBotIf, name, this.createAsync(interpreter, f)) - ); + Object.entries(ticksIf).forEach(([name, f]) => { + interpreter.setProperty(pseudoBotIf, name, this.createAsync(interpreter, f)); + }); interpreter.setProperty( pseudoBotIf, @@ -169,16 +182,31 @@ export default class Interpreter { } } createAsync(interpreter, func) { - return interpreter.createAsyncFunction((...args) => { + const asyncFunc = (...args) => { const callback = args.pop(); - func(...args.map(arg => interpreter.pseudoToNative(arg))) + // Workaround for unknown number of args + const reversedArgs = args.slice().reverse(); + const firsDefinedArgIdx = reversedArgs.findIndex(arg => arg !== undefined); + + // Remove extra undefined args from end of the args + const functionArgs = firsDefinedArgIdx < 0 ? [] : reversedArgs.slice(firsDefinedArgIdx).reverse(); + // End of workaround + + func(...functionArgs.map(arg => interpreter.pseudoToNative(arg))) .then(rv => { callback(interpreter.nativeToPseudo(rv)); this.loop(); }) .catch(e => this.$scope.observer.emit('Error', e)); - }); + }; + + // TODO: This is a workaround, create issue on original repo, once fixed + // remove this. We don't know how many args are going to be passed, so we + // assume a max of 100. + const MAX_ACCEPTABLE_FUNC_ARGS = 100; + Object.defineProperty(asyncFunc, 'length', { value: MAX_ACCEPTABLE_FUNC_ARGS + 1 }); + return interpreter.createAsyncFunction(asyncFunc); } hasStarted() { return !this.stopped; diff --git a/src/botPage/bot/TradeEngine/Proposal.js b/src/botPage/bot/TradeEngine/Proposal.js index 8fced7acd7..e9203b3379 100644 --- a/src/botPage/bot/TradeEngine/Proposal.js +++ b/src/botPage/bot/TradeEngine/Proposal.js @@ -1,6 +1,7 @@ import { translate } from '../../../common/i18n'; import { tradeOptionToProposal, doUntilDone } from '../tools'; import { proposalsReady, clearProposals } from './state/actions'; +import { TrackJSError } from '../../view/logger'; export default Engine => class Proposal extends Engine { @@ -22,7 +23,8 @@ export default Engine => this.data.get('proposals').forEach(proposal => { if (proposal.contractType === contractType) { if (proposal.error) { - throw Error(proposal.error.error.error.message); + const { error } = proposal.error; + throw new TrackJSError(error.error.code, error.error.message, error); } else { toBuy = proposal; } @@ -30,7 +32,11 @@ export default Engine => }); if (!toBuy) { - throw Error(translate('Selected proposal does not exist')); + throw new TrackJSError( + 'CustomInvalidProposal', + translate('Selected proposal does not exist'), + Array.from(this.data.get('proposals')).map(proposal => proposal[1]) + ); } return { @@ -46,14 +52,12 @@ export default Engine => this.store.dispatch(clearProposals()); } requestProposals() { - this.proposalTemplates.map(proposal => - doUntilDone(() => - this.api - .subscribeToPriceForContractProposal(proposal) - // eslint-disable-next-line consistent-return - .catch(e => { + Promise.all( + this.proposalTemplates.map(proposal => + doUntilDone(() => + this.api.subscribeToPriceForContractProposal(proposal).catch(e => { if (e && e.name === 'RateLimit') { - return Promise.reject(e); + throw e; } const errorCode = e.error && e.error.error && e.error.error.code; @@ -62,18 +66,22 @@ export default Engine => const { uuid } = e.error.echo_req.passthrough; if (!this.data.hasIn(['forgetProposals', uuid])) { + // Add to proposals map with error. Will later be shown to user, see selectProposal. this.data = this.data.setIn(['proposals', uuid], { ...proposal, - contractType: proposal.contract_type, - error : e, + ...proposal.passthrough, + error: e, }); } - } else { - this.$scope.observer.emit('Error', e); + + return null; } + + throw e; }) + ) ) - ); + ).catch(e => this.$scope.observer.emit('Error', e)); } observeProposals() { this.listen('proposal', r => { diff --git a/src/botPage/bot/tools.js b/src/botPage/bot/tools.js index 49f15f96b4..9008c5987c 100644 --- a/src/botPage/bot/tools.js +++ b/src/botPage/bot/tools.js @@ -99,12 +99,29 @@ const getBackoffDelay = (error, delayIndex) => { return linearIncrease * 1000; }; -export const shouldThrowError = (e, types = [], delayIndex = 0) => - e && - (!types - .concat(['CallError', 'WrongResponse', 'GetProposalFailure', 'RateLimit', 'DisconnectError']) - .includes(e.name) || - (e.name !== 'DisconnectError' && delayIndex > maxRetries)); +export const shouldThrowError = (error, types = [], delayIndex = 0) => { + if (!error) { + return false; + } + + const defaultErrors = ['CallError', 'WrongResponse', 'GetProposalFailure', 'RateLimit', 'DisconnectError']; + const authErrors = ['InvalidToken', 'AuthorizationRequired']; + const errors = types.concat(defaultErrors); + + if (authErrors.includes(error.name)) { + // If auth error, reload page. + window.location.reload(); + return true; + } else if (!errors.includes(error.name)) { + // If error is unrecoverable, throw error. + return true; + } else if (error.name !== 'DisconnectError' && delayIndex > maxRetries) { + // If exceeded maxRetries, throw error. + return true; + } + + return false; +}; export const recoverFromError = (f, r, types, delayIndex) => new Promise((resolve, reject) => { diff --git a/src/botPage/view/LogTable.js b/src/botPage/view/LogTable.js index a2a06d4acf..b8f413afb1 100644 --- a/src/botPage/view/LogTable.js +++ b/src/botPage/view/LogTable.js @@ -24,7 +24,7 @@ export default class LogTable extends Component { type : PropTypes.string, timestamp: PropTypes.string, message : PropTypes.string, - }).isRequired, + }), }; constructor() { super();