diff --git a/packages/bot-skeleton/package.json b/packages/bot-skeleton/package.json index 96becac19d3c..b19bf02aea06 100644 --- a/packages/bot-skeleton/package.json +++ b/packages/bot-skeleton/package.json @@ -46,6 +46,8 @@ "immutable": "^3.8.2", "localforage": "^1.9.0", "lz-string": "^1.4.4", + "mobx": "^6.6.1", + "mobx-react": "^7.5.1", "redux": "^4.0.1", "redux-thunk": "^2.2.0", "scratch-blocks": "0.1.0-prerelease.20200917235131", diff --git a/packages/bot-skeleton/src/scratch/dbot-store.js b/packages/bot-skeleton/src/scratch/dbot-store.js index 1d951ec9ebc2..fb7333342df2 100644 --- a/packages/bot-skeleton/src/scratch/dbot-store.js +++ b/packages/bot-skeleton/src/scratch/dbot-store.js @@ -1,3 +1,6 @@ +import { reaction } from 'mobx'; +import { api_base } from '../services/api/api-base'; + class DBotStoreInterface { // TODO here we are suppose to define an interface and implement fields of DBotStore. handleFileChange = () => { @@ -27,6 +30,11 @@ class DBotStore extends DBotStoreInterface { this.handleFileChange = store.handleFileChange; this.startLoading = store.startLoading; this.endLoading = store.endLoading; + + reaction( + () => this.client.loginid, + () => api_base.createNewInstance(this.client.loginid) + ); } static setInstance(store) { diff --git a/packages/bot-skeleton/src/scratch/dbot.js b/packages/bot-skeleton/src/scratch/dbot.js index adee21e1bab2..4df1e4afb380 100644 --- a/packages/bot-skeleton/src/scratch/dbot.js +++ b/packages/bot-skeleton/src/scratch/dbot.js @@ -10,6 +10,7 @@ import { observer as globalObserver } from '../utils/observer'; import ApiHelpers from '../services/api/api-helpers'; import Interpreter from '../services/tradeEngine/utils/interpreter'; import { setColors } from './hooks/colours'; +import { api_base } from '../services/api/api-base'; class DBot { constructor() { @@ -97,7 +98,7 @@ class DBot { window.dispatchEvent(new Event('resize')); window.addEventListener('dragover', DBot.handleDragOver); window.addEventListener('drop', e => DBot.handleDropOver(e, handleFileChange)); - + api_base.init(); // disable overflow el_scratch_div.parentNode.style.overflow = 'hidden'; resolve(); @@ -134,7 +135,7 @@ class DBot { } this.interpreter = Interpreter(); - + api_base.setIsRunning(true); this.interpreter.run(code).catch(error => { globalObserver.emit('Error', error); this.stopBot(); @@ -226,6 +227,7 @@ class DBot { * that trade will be completed first to reflect correct contract status in UI. */ stopBot() { + api_base.setIsRunning(false); if (this.interpreter) { this.interpreter.stop(); } @@ -241,6 +243,10 @@ class DBot { } } + terminateConnection = () => { + api_base.terminate(); + }; + /** * Unselects any selected block before running the bot. */ diff --git a/packages/bot-skeleton/src/services/api/api-base.js b/packages/bot-skeleton/src/services/api/api-base.js new file mode 100644 index 000000000000..fd7c6d96dabd --- /dev/null +++ b/packages/bot-skeleton/src/services/api/api-base.js @@ -0,0 +1,132 @@ +import { observer as globalObserver } from '../../utils/observer'; +import { doUntilDone } from '../tradeEngine/utils/helpers'; +import { generateDerivApiInstance, getLoginId, getToken } from './appId'; + +class APIBase { + api; + token; + account_id; + pip_sizes = {}; + account_info = {}; + is_running = false; + subscriptions = []; + time_interval = null; + has_activeSymbols = false; + + init(force_update = false) { + if (getLoginId()) { + this.toggleRunButton(true); + if (force_update) this.terminate(); + this.api = generateDerivApiInstance(); + this.initEventListeners(); + this.authorizeAndSubscribe(); + if (this.time_interval) clearInterval(this.time_interval); + this.time_interval = null; + this.getTime(); + } + } + + terminate() { + // eslint-disable-next-line no-console + console.log('connection terminated'); + if (this.api) this.api.disconnect(); + } + + initEventListeners() { + if (window) { + window.addEventListener('online', this.reconnectIfNotConnected); + window.addEventListener('focus', this.reconnectIfNotConnected); + } + } + + createNewInstance(account_id) { + if (this.account_id !== account_id) { + this.init(true); + } + } + + reconnectIfNotConnected = () => { + // eslint-disable-next-line no-console + console.log('connection state: ', this.api.connection.readyState); + if (this.api.connection.readyState !== 1) { + // eslint-disable-next-line no-console + console.log('Info: Connection to the server was closed, trying to reconnect.'); + this.init(); + } + }; + + authorizeAndSubscribe() { + const { token, account_id } = getToken(); + if (token) { + this.token = token; + this.account_id = account_id; + this.api + .authorize(this.token) + .then(({ authorize }) => { + if (this.has_activeSymbols) { + this.toggleRunButton(false); + } else { + this.getActiveSymbols(); + } + this.subscribe(); + this.account_info = authorize; + }) + .catch(e => { + globalObserver.emit('Error', e); + }); + } + } + + subscribe() { + doUntilDone(() => this.api.send({ balance: 1, subscribe: 1 })); + doUntilDone(() => this.api.send({ transaction: 1, subscribe: 1 })); + } + + getActiveSymbols = async () => { + doUntilDone(() => this.api.send({ active_symbols: 'brief' })).then(({ active_symbols = [] }) => { + const pip_sizes = {}; + if (active_symbols.length) this.has_activeSymbols = true; + active_symbols.forEach(({ symbol, pip }) => { + pip_sizes[symbol] = +(+pip).toExponential().substring(3); + }); + this.pip_sizes = pip_sizes; + this.toggleRunButton(false); + }); + }; + + toggleRunButton = toggle => { + const run_button = document.querySelector('#db-animation__run-button'); + if (!run_button) return; + run_button.disabled = toggle; + }; + + setIsRunning(toggle = false) { + this.is_running = toggle; + } + + pushSubscription(subscription) { + this.subscriptions.push(subscription); + } + + clearSubscriptions() { + this.subscriptions.forEach(s => s.unsubscribe()); + this.subscriptions = []; + + // Resetting timeout resolvers + const global_timeouts = globalObserver.getState('global_timeouts') ?? []; + + global_timeouts.forEach((_, i) => { + clearTimeout(i); + }); + } + + getTime() { + if (!this.time_interval) { + this.time_interval = setInterval(() => { + this.api.send({ time: 1 }); + }, 30000); + } + } +} + +export const api_base = new APIBase(); diff --git a/packages/bot-skeleton/src/services/api/appId.js b/packages/bot-skeleton/src/services/api/appId.js index ed7417a9d34d..e56a9931fbea 100644 --- a/packages/bot-skeleton/src/services/api/appId.js +++ b/packages/bot-skeleton/src/services/api/appId.js @@ -10,3 +10,19 @@ export const generateDerivApiInstance = () => { }); return deriv_api; }; + +export const getLoginId = () => { + const login_id = localStorage.getItem('active_loginid'); + if (login_id && login_id !== 'null') return login_id; + return null; +}; + +export const getToken = () => { + const active_loginid = getLoginId(); + const client_accounts = JSON.parse(localStorage.getItem('client.accounts')) || undefined; + const active_account = (client_accounts && client_accounts[active_loginid]) || {}; + return { + token: active_account?.token || undefined, + account_id: active_loginid || undefined, + }; +}; diff --git a/packages/bot-skeleton/src/services/api/ticks_service.js b/packages/bot-skeleton/src/services/api/ticks_service.js index 2eaf3e8a50ce..3c6632faca0a 100644 --- a/packages/bot-skeleton/src/services/api/ticks_service.js +++ b/packages/bot-skeleton/src/services/api/ticks_service.js @@ -2,6 +2,7 @@ import { Map } from 'immutable'; import { historyToTicks, getLast } from 'binary-utils'; import { doUntilDone, getUUID } from '../tradeEngine/utils/helpers'; import { observer as globalObserver } from '../../utils/observer'; +import { api_base } from './api-base'; const parseTick = tick => ({ epoch: +tick.epoch, @@ -39,8 +40,7 @@ const updateCandles = (candles, ohlc) => { const getType = isCandle => (isCandle ? 'candles' : 'ticks'); export default class TicksService { - constructor(api) { - this.api = api; + constructor() { this.ticks = new Map(); this.candles = new Map(); this.tickListeners = new Map(); @@ -60,23 +60,13 @@ export default class TicksService { if (!this.active_symbols_promise) { this.active_symbols_promise = new Promise(resolve => { - this.getActiveSymbols().then(active_symbols => { - this.pipSizes = active_symbols - .reduce((s, i) => s.set(i.symbol, +(+i.pip).toExponential().substring(3)), new Map()) - .toObject(); - resolve(this.pipSizes); - }); + this.pipSizes = api_base.pip_sizes; + resolve(this.pipSizes); }); } return this.active_symbols_promise; } - getActiveSymbols = async () => { - await this.api.expectResponse('authorize'); - const active_symbols = await this.api.send({ active_symbols: 'brief' }); - return active_symbols.active_symbols; - }; - request(options) { const { symbol, granularity } = options; @@ -89,7 +79,6 @@ export default class TicksService { if (style === 'candles' && this.candles.hasIn([symbol, Number(granularity)])) { return Promise.resolve(this.candles.getIn([symbol, Number(granularity)])); } - return this.requestStream({ ...options, style }); } @@ -163,7 +152,7 @@ export default class TicksService { ...(tickSubscription || []), ]; - Promise.all(subscription.map(id => doUntilDone(() => this.api.forget(id)))); + Promise.all(subscription.map(id => doUntilDone(() => api_base.api.forget(id)))); this.subscriptions = new Map(); } @@ -195,7 +184,7 @@ export default class TicksService { } observe() { - this.api.onMessage().subscribe(({ data }) => { + const subscription = api_base.api.onMessage().subscribe(({ data }) => { if (data.msg_type === 'tick') { const { tick } = data; const { symbol, id } = tick; @@ -218,6 +207,7 @@ export default class TicksService { } } }); + api_base.pushSubscription(subscription); } requestStream(options) { @@ -260,7 +250,7 @@ export default class TicksService { style, }; return new Promise((resolve, reject) => { - doUntilDone(() => this.api.send(request_object)) + doUntilDone(() => api_base.api.send(request_object), [], api_base) .then(r => { if (style === 'ticks') { const ticks = historyToTicks(r.history); @@ -278,4 +268,27 @@ export default class TicksService { .catch(reject); }); } + + forget = subscription_id => { + if (subscription_id) { + api_base.api.forget(subscription_id); + } + }; + + unsubscribeFromTicksService() { + if (this.ticks_history_promise) { + const { stringified_options } = this.ticks_history_promise; + const { symbol = '' } = JSON.parse(stringified_options); + if (symbol) { + this.forget(this.subscriptions.getIn(['tick', symbol])); + } + } + if (this.candles_promise) { + const { stringified_options } = this.candles_promise; + const { symbol = '' } = JSON.parse(stringified_options); + if (symbol) { + this.forget(this.subscriptions.getIn(['candle', symbol])); + } + } + } } diff --git a/packages/bot-skeleton/src/services/tradeEngine/trade/Balance.js b/packages/bot-skeleton/src/services/tradeEngine/trade/Balance.js index 19f3aad85318..03a01578c671 100644 --- a/packages/bot-skeleton/src/services/tradeEngine/trade/Balance.js +++ b/packages/bot-skeleton/src/services/tradeEngine/trade/Balance.js @@ -1,13 +1,14 @@ import { getFormattedText } from '@deriv/shared'; import { info } from '../utils/broadcast'; import DBotStore from '../../../scratch/dbot-store'; +import { api_base } from '../../api/api-base'; let balance_string = ''; export default Engine => class Balance extends Engine { observeBalance() { - this.api.onMessage().subscribe(({ data }) => { + const subscription = api_base.api.onMessage().subscribe(({ data }) => { if (data.msg_type === 'balance') { const { balance: { balance: b, currency }, @@ -18,6 +19,7 @@ export default Engine => info({ accountID: this.accountInfo.loginid, balance: balance_string }); } }); + api_base.pushSubscription(subscription); } // eslint-disable-next-line class-methods-use-this diff --git a/packages/bot-skeleton/src/services/tradeEngine/trade/OpenContract.js b/packages/bot-skeleton/src/services/tradeEngine/trade/OpenContract.js index 52c610a5f0a0..2c65f1637ae6 100644 --- a/packages/bot-skeleton/src/services/tradeEngine/trade/OpenContract.js +++ b/packages/bot-skeleton/src/services/tradeEngine/trade/OpenContract.js @@ -3,15 +3,16 @@ import { sell, openContractReceived } from './state/actions'; import { contractStatus, contract as broadcastContract } from '../utils/broadcast'; import { doUntilDone } from '../utils/helpers'; import DBotStore from '../../../scratch/dbot-store'; +import { api_base } from '../../api/api-base'; export default Engine => class OpenContract extends Engine { observeOpenContract() { - this.api.onMessage().subscribe(({ data }) => { + const subscription = api_base.api.onMessage().subscribe(({ data }) => { if (data.msg_type === 'proposal_open_contract') { const contract = data.proposal_open_contract; - if (!contract && !this.expectedContractId(contract?.contract_id)) { + if (!contract || !this.expectedContractId(contract?.contract_id)) { return; } @@ -19,7 +20,7 @@ export default Engine => this.data.contract = contract; - broadcastContract({ accountID: this.accountInfo.loginid, ...contract }); + broadcastContract({ accountID: api_base.account_info.loginid, ...contract }); if (this.isSold) { this.contractId = ''; @@ -41,6 +42,7 @@ export default Engine => } } }); + api_base.pushSubscription(subscription); } waitForAfter() { @@ -51,7 +53,13 @@ export default Engine => subscribeToOpenContract(contract_id = this.contractId) { this.contractId = contract_id; - doUntilDone(() => this.api.send({ proposal_open_contract: 1, contract_id, subscribe: 1 })) + const request_object = { + proposal_open_contract: 1, + contract_id, + subscribe: 1, + }; + + doUntilDone(() => api_base.api.send(request_object)) .then(data => { const { populateConfig } = DBotStore.instance; populateConfig(data.proposal_open_contract); @@ -59,7 +67,7 @@ export default Engine => }) .catch(error => { if (error.error.code !== 'AlreadySubscribed') { - doUntilDone(() => this.api.send({ proposal_open_contract: 1, contract_id, subscribe: 1 })).then( + doUntilDone(() => api_base.api.send(request_object)).then( response => (this.openContractId = response.proposal_open_contract.id) ); } diff --git a/packages/bot-skeleton/src/services/tradeEngine/trade/Proposal.js b/packages/bot-skeleton/src/services/tradeEngine/trade/Proposal.js index 17e7914b298a..515d40b9421a 100644 --- a/packages/bot-skeleton/src/services/tradeEngine/trade/Proposal.js +++ b/packages/bot-skeleton/src/services/tradeEngine/trade/Proposal.js @@ -1,6 +1,7 @@ import { localize } from '@deriv/translations'; import { proposalsReady, clearProposals } from './state/actions'; import { tradeOptionToProposal, doUntilDone } from '../utils/helpers'; +import { api_base } from '../../api/api-base'; export default Engine => class Proposal extends Engine { @@ -69,7 +70,7 @@ export default Engine => Promise.all( this.proposal_templates.map(proposal => { - doUntilDone(() => this.api.send(proposal)).catch(error => { + doUntilDone(() => api_base.api.send(proposal)).catch(error => { // We intercept ContractBuyValidationError as user may have specified // e.g. a DIGITUNDER 0 or DIGITOVER 9, while one proposal may be invalid // the other is valid. We will error on Purchase rather than here. @@ -94,7 +95,7 @@ export default Engine => } observeProposals() { - this.api.onMessage().subscribe(response => { + const subscription = api_base.api.onMessage().subscribe(response => { if (response.data.msg_type === 'proposal') { const { passthrough, proposal } = response.data; if ( @@ -108,6 +109,7 @@ export default Engine => } } }); + api_base.pushSubscription(subscription); } unsubscribeProposals() { @@ -128,7 +130,7 @@ export default Engine => return Promise.resolve(); } - return doUntilDone(() => this.api.forget(proposal.id)).then(() => { + return doUntilDone(() => api_base.api.forget(proposal.id)).then(() => { removeForgetProposalById(proposal.id); }); }) diff --git a/packages/bot-skeleton/src/services/tradeEngine/trade/Purchase.js b/packages/bot-skeleton/src/services/tradeEngine/trade/Purchase.js index 800c54201f93..cd3730f840eb 100644 --- a/packages/bot-skeleton/src/services/tradeEngine/trade/Purchase.js +++ b/packages/bot-skeleton/src/services/tradeEngine/trade/Purchase.js @@ -3,6 +3,7 @@ import { BEFORE_PURCHASE } from './state/constants'; import { contractStatus, info, log } from '../utils/broadcast'; import { getUUID, recoverFromError, doUntilDone } from '../utils/helpers'; import { log_types } from '../../../constants/messages'; +import { api_base } from '../../api/api-base'; let delayIndex = 0; let purchase_reference; @@ -41,7 +42,7 @@ export default Engine => buy_price: buy.buy_price, }); }; - const action = () => this.api.send({ buy: id, price: askPrice }); + const action = () => api_base.api.send({ buy: id, price: askPrice }); this.isSold = false; contractStatus({ id: 'contract.purchase_sent', diff --git a/packages/bot-skeleton/src/services/tradeEngine/trade/Sell.js b/packages/bot-skeleton/src/services/tradeEngine/trade/Sell.js index 203cb9d6bdee..d2dc6bb7a3dc 100644 --- a/packages/bot-skeleton/src/services/tradeEngine/trade/Sell.js +++ b/packages/bot-skeleton/src/services/tradeEngine/trade/Sell.js @@ -3,6 +3,7 @@ import { contractStatus, log } from '../utils/broadcast'; import { recoverFromError, doUntilDone } from '../utils/helpers'; import { log_types } from '../../../constants/messages'; import { observer as globalObserver } from '../../../utils/observer'; +import { api_base } from '../../api/api-base'; export default Engine => class Sell extends Engine { @@ -42,9 +43,9 @@ export default Engine => const contract_id = this.contractId; const sellContractAndGetContractInfo = () => { - return doUntilDone(() => this.api.send({ sell: contract_id, price: 0 })) + return doUntilDone(() => api_base.api.send({ sell: contract_id, price: 0 })) .then(sell_response => { - doUntilDone(() => this.api.send({ proposal_open_contract: 1, contract_id })).then( + doUntilDone(() => api_base.api.send({ proposal_open_contract: 1, contract_id })).then( () => sell_response ); }) @@ -70,7 +71,7 @@ export default Engine => // For every other error, check whether the contract is not actually already sold. return doUntilDone(() => - this.api.send({ + api_base.api.send({ proposal_open_contract: 1, contract_id, }) diff --git a/packages/bot-skeleton/src/services/tradeEngine/trade/index.js b/packages/bot-skeleton/src/services/tradeEngine/trade/index.js index 145bfcf67219..0a619a075f0f 100644 --- a/packages/bot-skeleton/src/services/tradeEngine/trade/index.js +++ b/packages/bot-skeleton/src/services/tradeEngine/trade/index.js @@ -15,6 +15,7 @@ import { doUntilDone } from '../utils/helpers'; import { expectInitArg } from '../utils/sanitize'; import { createError } from '../../../utils/error'; import { observer as globalObserver } from '../../../utils/observer'; +import { api_base } from '../../api/api-base'; const watchBefore = store => watchScope({ @@ -64,7 +65,6 @@ const watchScope = ({ store, stopScope, passScope, passFlag }) => { export default class TradeEngine extends Balance(Purchase(Sell(OpenContract(Proposal(Ticks(Total(class {}))))))) { constructor($scope) { super(); - this.api = $scope.api; this.observer = $scope.observer; this.$scope = $scope; this.observe(); @@ -95,7 +95,6 @@ export default class TradeEngine extends Balance(Purchase(Sell(OpenContract(Prop globalObserver.emit('bot.running'); this.tradeOptions = tradeOptions; - this.store.dispatch(start()); this.checkLimits(tradeOptions); this.makeProposals({ ...this.options, ...tradeOptions }); @@ -106,17 +105,13 @@ export default class TradeEngine extends Balance(Purchase(Sell(OpenContract(Prop if (this.token === token) { return Promise.resolve(); } - - doUntilDone(() => this.api.authorize(token)).catch(e => { - this.$scope.observer.emit('Error', e); - }); return new Promise(resolve => { // Try to recover from a situation where API doesn't give us a correct response on // "proposal_open_contract" which would make the bot run forever. When there's a "sell" // event, wait a couple seconds for the API to give us the correct "proposal_open_contract" // response, if there's none after x seconds. Send an explicit request, which _should_ // solve the issue. This is a backup! - this.api.onMessage().subscribe(({ data }) => { + const subscription = api_base.api.onMessage().subscribe(({ data }) => { if (data.msg_type === 'transaction' && data.transaction.action === 'sell') { this.transaction_recovery_timeout = setTimeout(() => { const { contract } = this.data; @@ -124,27 +119,16 @@ export default class TradeEngine extends Balance(Purchase(Sell(OpenContract(Prop const is_open_contract = contract.status === 'open'; if (is_same_contract && is_open_contract) { doUntilDone(() => { - this.api.send({ proposal_open_contract: 1, contract_id: contract.contract_id }); + api_base.api.send({ proposal_open_contract: 1, contract_id: contract.contract_id }); }, ['PriceMoved']); } }, 1500); } - if (data.msg_type === 'authorize') { - this.accountInfo = data; - this.token = token; - - // Only subscribe to balance in browser, not for tests. - if (document) { - doUntilDone(() => this.api.send({ balance: 1, subscribe: 1 })).then(r => { - this.balance = Number(r.balance.balance); - resolve(); - }); - } else { - resolve(); - } - doUntilDone(() => this.api.send({ transaction: 1, subscribe: 1 })); - } + this.accountInfo = api_base.account_info; + this.token = api_base.token; + resolve(); }); + api_base.pushSubscription(subscription); }); } diff --git a/packages/bot-skeleton/src/services/tradeEngine/utils/cliTools.js b/packages/bot-skeleton/src/services/tradeEngine/utils/cliTools.js index bac5c53d459d..63cb063a7dc2 100644 --- a/packages/bot-skeleton/src/services/tradeEngine/utils/cliTools.js +++ b/packages/bot-skeleton/src/services/tradeEngine/utils/cliTools.js @@ -1,11 +1,9 @@ import TicksService from '../../api/ticks_service'; import Observer from '../../../utils/observer'; -import { generateDerivApiInstance } from '../../api/appId'; export const createScope = () => { const observer = new Observer(); - const api = generateDerivApiInstance(); - const ticksService = new TicksService(api); + const ticksService = new TicksService(); const stopped = false; - return { observer, api, ticksService, stopped }; + return { observer, ticksService, stopped }; }; diff --git a/packages/bot-skeleton/src/services/tradeEngine/utils/helpers.js b/packages/bot-skeleton/src/services/tradeEngine/utils/helpers.js index ecb02e769017..8a996f9ec836 100644 --- a/packages/bot-skeleton/src/services/tradeEngine/utils/helpers.js +++ b/packages/bot-skeleton/src/services/tradeEngine/utils/helpers.js @@ -130,13 +130,17 @@ export const shouldThrowError = (error, errors_to_ignore = []) => { return !is_ignorable_error; }; -export const recoverFromError = (promiseFn, recoverFn, errors_to_ignore, delay_index) => { +export const recoverFromError = (promiseFn, recoverFn, errors_to_ignore, delay_index, api_base) => { return new Promise((resolve, reject) => { const promise = promiseFn(); if (promise) { promise.then(resolve).catch(error => { - if (shouldThrowError(error, errors_to_ignore)) { + /** + * if bot is not running there is no point of recovering from error + * `!api_base.is_running` will check the bot status if it is not running it will kick out the control from loop + */ + if (shouldThrowError(error, errors_to_ignore) || (api_base && !api_base.is_running)) { reject(error); return; } @@ -172,7 +176,13 @@ export const recoverFromError = (promiseFn, recoverFn, errors_to_ignore, delay_i }); }; -export const doUntilDone = (promiseFn, errors_to_ignore) => { +/** + * @param {*} promiseFn api call - it could be api call or subscription + * @param {*} errors_to_ignore list of errors to ignore + * @param {*} api_base instance of APIBase class to check if the bot is running or not + * @returns a new promise + */ +export const doUntilDone = (promiseFn, errors_to_ignore, api_base) => { let delay_index = 1; return new Promise((resolve, reject) => { @@ -182,7 +192,7 @@ export const doUntilDone = (promiseFn, errors_to_ignore) => { }; const repeatFn = () => { - recoverFromError(promiseFn, recoverFn, errors_to_ignore, delay_index).then(resolve).catch(reject); + recoverFromError(promiseFn, recoverFn, errors_to_ignore, delay_index, api_base).then(resolve).catch(reject); }; repeatFn(); diff --git a/packages/bot-skeleton/src/services/tradeEngine/utils/interpreter.js b/packages/bot-skeleton/src/services/tradeEngine/utils/interpreter.js index d43cb67f01d0..788d172dbe39 100644 --- a/packages/bot-skeleton/src/services/tradeEngine/utils/interpreter.js +++ b/packages/bot-skeleton/src/services/tradeEngine/utils/interpreter.js @@ -4,6 +4,7 @@ import { createScope } from './cliTools'; import Interface from '../Interface'; import { unrecoverable_errors } from '../../../constants/messages'; import { observer as globalObserver } from '../../../utils/observer'; +import { api_base } from '../../api/api-base'; JSInterpreter.prototype.takeStateSnapshot = function () { const newStateStack = cloneThorough(this.stateStack, undefined, undefined, undefined, true); @@ -180,16 +181,14 @@ const Interpreter = () => { } function terminateSession() { - const { connection } = $scope.api; - if (connection.readyState === 0) { - connection.addEventListener('open', () => connection.close()); - } else if (connection.readyState === 1) { - connection.close(); - } - $scope.stopped = true; $scope.is_error_triggered = false; globalObserver.emit('bot.stop'); + const { ticksService } = $scope; + // Unsubscribe previous ticks_history subscription + ticksService.unsubscribeFromTicksService(); + // Unsubscribe the subscriptions from Proposal, Balance and OpenContract + api_base.clearSubscriptions(); } function run(code) { diff --git a/packages/bot-web-ui/src/components/trade-animation/trade-animation.jsx b/packages/bot-web-ui/src/components/trade-animation/trade-animation.jsx index 738ed174987a..de40ff31ad8c 100644 --- a/packages/bot-web-ui/src/components/trade-animation/trade-animation.jsx +++ b/packages/bot-web-ui/src/components/trade-animation/trade-animation.jsx @@ -85,9 +85,16 @@ const TradeAnimation = ({ info_direction, toggleAnimationInfoModal, cashier_validation, + performSelfExclusionCheck, }) => { const [is_button_disabled, updateIsButtonDisabled] = React.useState(false); const is_unavailable_for_payment_agent = cashier_validation?.includes('WithdrawServiceUnavailableForPA'); + + // perform self-exclusion checks which will be stored under the self-exclusion-store + React.useEffect(() => { + performSelfExclusionCheck(); + }, []); + React.useEffect(() => { if (is_button_disabled) { setTimeout(() => { @@ -95,6 +102,7 @@ const TradeAnimation = ({ }, 1000); } }, [is_button_disabled]); + const status_classes = ['', '', '']; let progress_status = contract_stage - @@ -174,6 +182,7 @@ TradeAnimation.propTypes = { is_stop_button_disabled: PropTypes.bool, onRunButtonClick: PropTypes.func, onStopButtonClick: PropTypes.func, + performSelfExclusionCheck: PropTypes.func, profit: PropTypes.number, should_show_overlay: PropTypes.bool, }; @@ -187,6 +196,7 @@ export default connect(({ summary_card, run_panel, toolbar, ui, client }) => ({ is_stop_button_disabled: run_panel.is_stop_button_disabled, onRunButtonClick: run_panel.onRunButtonClick, onStopButtonClick: run_panel.onStopButtonClick, + performSelfExclusionCheck: run_panel.performSelfExclusionCheck, profit: summary_card.profit, should_show_overlay: run_panel.should_show_overlay, toggleAnimationInfoModal: toolbar.toggleAnimationInfoModal, diff --git a/packages/bot-web-ui/src/stores/app-store.js b/packages/bot-web-ui/src/stores/app-store.js index 2088fa4b7da7..862140f7d1e5 100644 --- a/packages/bot-web-ui/src/stores/app-store.js +++ b/packages/bot-web-ui/src/stores/app-store.js @@ -39,7 +39,7 @@ export default class AppStore { onUnmount() { DBot.terminateBot(); - + DBot.terminateConnection(); if (Blockly.derivWorkspace) { clearInterval(Blockly.derivWorkspace.save_workspace_interval); Blockly.derivWorkspace.dispose(); diff --git a/packages/bot-web-ui/src/stores/run-panel-store.js b/packages/bot-web-ui/src/stores/run-panel-store.js index c0144b8e494a..c35b2390467e 100644 --- a/packages/bot-web-ui/src/stores/run-panel-store.js +++ b/packages/bot-web-ui/src/stores/run-panel-store.js @@ -32,6 +32,7 @@ export default class RunPanelStore { toggleDrawer: action.bound, setActiveTabIndex: action.bound, onCloseDialog: action.bound, + performSelfExclusionCheck: action.bound, showStopMultiplierContractDialog: action.bound, showLoginDialog: action.bound, showRealAccountDialog: action.bound, @@ -130,6 +131,11 @@ export default class RunPanelStore { ); } + async performSelfExclusionCheck() { + const { self_exclusion } = this.root_store; + await self_exclusion.checkRestriction(); + } + async onRunButtonClick() { const { core, summary_card, route_prompt_dialog, self_exclusion } = this.root_store; const { client, ui } = core; @@ -148,7 +154,6 @@ export default class RunPanelStore { */ if (is_ios || isSafari()) this.preloadAudio(); - await self_exclusion.checkRestriction(); if (!self_exclusion.should_bot_run) { self_exclusion.setIsRestricted(true); return;