diff --git a/.circleci/config.yml b/.circleci/config.yml index 7e34cf8baa..55baccb028 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,7 @@ version: 2.1 orbs: k8s: circleci/kubernetes@0.7.0 + s3: circleci/aws-s3@1.0.13 commands: git_checkout_from_cache: description: "Git checkout and save cache" @@ -11,8 +12,8 @@ commands: - source-v1-{{ .Branch }}-{{ .Revision }} - source-v1-{{ .Branch }}- - source-v1- - - run: - name: Fetch git tags + - run: + name: Fetch git tags command: | mkdir -p ~/.ssh echo 'github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ== ' >> ~/.ssh/known_hosts @@ -23,7 +24,7 @@ commands: fi - checkout - run: - name: Compress git objects + name: Compress git objects command: git gc - save_cache: name: Git save cache @@ -46,14 +47,43 @@ commands: key: npm-v1-{{ checksum "package.json" }} paths: - "node_modules" - build: description: "Build" steps: - run: name: "yarn build" command: node_modules/gulp/bin/gulp.js build-min - + compress: + description: "Compress" + steps: + - run: + name: "Compress" + command: | + pushd www/ + tar -cvf artifact.tar * + mv artifact.tar ${OLDPWD}/ + - run: + name: "Tag commit id as artifact identifer" + command: echo "${CIRCLE_SHA1}" > artifact-info.txt + upload_artifact: + description: "upload artifact to s3" + steps: + - s3/copy: + from: artifact.tar + to: 's3://${CONTEXT_ARTIFACT_S3_BUCKET}/${CIRCLE_PROJECT_REPONAME}/' + aws-access-key-id: env_CONTEXT_ARTIFACT_S3_AWS_ACCESS_KEY_ID + aws-secret-access-key: env_CONTEXT_ARTIFACT_S3_AWS_SECRET_ACCESS_KEY + aws-region: env_CONTEXT_ARTIFACT_S3_AWS_REGION + arguments: '--metadata "{\"x-amz-artifact-id\": \"${CIRCLE_SHA1}\" }"' + upload_checksum: + description: "upload artifact commit id to s3" + steps: + - s3/copy: + from: artifact-info.txt + to: 's3://${CONTEXT_ARTIFACT_S3_BUCKET}/${CIRCLE_PROJECT_REPONAME}/' + aws-access-key-id: env_CONTEXT_ARTIFACT_S3_AWS_ACCESS_KEY_ID + aws-secret-access-key: env_CONTEXT_ARTIFACT_S3_AWS_SECRET_ACCESS_KEY + aws-region: env_CONTEXT_ARTIFACT_S3_AWS_REGION docker: description: "Build and Push image to docker hub" parameters: @@ -61,13 +91,13 @@ commands: type: string steps: - setup_remote_docker - - run: + - run: name: Building docker image for << parameters.target >> command: | build_tag="${CIRCLE_SHA1}" [ "<< parameters.target >>" == "beta" ] && build_tag="beta-${CIRCLE_SHA1}" docker build -t ${DOCKHUB_ORGANISATION}/binary-static-bot:${build_tag} . - - run: + - run: name: Pushing Image to docker hub command: | build_tag="${CIRCLE_SHA1}" @@ -81,7 +111,7 @@ commands: type: string steps: - k8s/install-kubectl - - run: + - run: name: Deploying to k8s cluster for service binary-bot-beta command: | build_tag="${CIRCLE_SHA1}" @@ -118,7 +148,7 @@ jobs: target: "beta" - k8s_deploy: target: "beta" - + release_production: docker: - image: circleci/node:12.13.0-stretch @@ -126,6 +156,9 @@ jobs: - git_checkout_from_cache - npm_install - build + - compress + - upload_artifact # uploading the built code to s3 to create a backup of key services separate from Kubernetes deployment + - upload_checksum # uploading compressed artifact checksum to cross match artifact fingerprint before actual deployment - docker: target: "production" - k8s_deploy: @@ -135,7 +168,6 @@ workflows: test: jobs: - test - release: jobs: - release_beta: @@ -150,4 +182,4 @@ workflows: ignore: /.*/ tags: only: /^production.*/ - + context: binary-frontend-artifact-upload diff --git a/package-lock.json b/package-lock.json index cd0e2300a2..51ba19a628 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.2.0", + "resolved": "https://registry.npmjs.org/js-interpreter/-/js-interpreter-2.2.0.tgz", + "integrity": "sha512-eZq/kAEjxahuGowG91tWLVgzPuLGGqyakTFF7JPlblV6b/Uu9EOnYxs5EOSVQlKwHoq7MND0gu1zk3OGs5a6Iw==", "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 } } @@ -15338,6 +15330,12 @@ "replace-ext": "^1.0.0" }, "dependencies": { + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true + }, "replace-ext": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", diff --git a/package.json b/package.json index 12cf1bb8fd..bebe152b02 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.2.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..42633ae81c 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.globalObject = this.stateStack[0].scope.object; + this.initFunc_(this, this.globalObject); +}; +/* 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..a2a5c0e3ad 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 => { @@ -121,7 +129,7 @@ export default Engine => checkProposalReady() { const proposals = this.data.get('proposals'); - if (proposals && proposals.size) { + if (proposals && proposals.size === this.proposalTemplates.length) { const isSameWithTemplate = this.proposalTemplates.every(p => this.data.hasIn(['proposals', p.passthrough.uuid]) ); 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();