diff --git a/index.html b/index.html index e3bdc0cb..130dd626 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Deriv V2 + Deriv Bot
diff --git a/package-lock.json b/package-lock.json index 2195baa2..0690f130 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,6 +103,7 @@ "ts-jest": "^29.1.2", "typescript": "^5.2.2", "vite-live-preview": "^0.1.6", + "vite-plugin-static-copy": "^1.0.5", "vite-plugin-svgr": "^4.2.0", "vite-require": "^0.2.3", "vite-tsconfig-paths": "^4.3.2", @@ -19205,6 +19206,24 @@ "magic-string": "^0.30.1" } }, + "node_modules/vite-plugin-static-copy": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-1.0.5.tgz", + "integrity": "sha512-02k0Rox+buYdEOfeilKZSgs1gXfPf9RjVztZEIYZgVIxjsVZi6AXssjzdi+qW6zYt00d3bq+tpP2voVXN2fKLw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "fs-extra": "^11.1.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0" + } + }, "node_modules/vite-plugin-svgr": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.2.0.tgz", diff --git a/package.json b/package.json index 10724499..007a6957 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "ts-jest": "^29.1.2", "typescript": "^5.2.2", "vite-live-preview": "^0.1.6", + "vite-plugin-static-copy": "^1.0.5", "vite-plugin-svgr": "^4.2.0", "vite-require": "^0.2.3", "vite-tsconfig-paths": "^4.3.2", diff --git a/src/app/app-content.jsx b/src/app/app-content.jsx index 61bf5703..c6537584 100644 --- a/src/app/app-content.jsx +++ b/src/app/app-content.jsx @@ -1,8 +1,10 @@ import React from 'react'; import { ToastContainer } from 'react-toastify'; +import { setSmartChartsPublicPath } from '@deriv/deriv-charts'; import { Loader } from '@deriv-com/ui'; +import { getUrlBase } from '@/components/shared'; import TransactionDetailsModal from '@/components/transaction-details'; import { api_base, ApiHelpers, ServerTime } from '@/external/bot-skeleton'; import { useStore } from '@/hooks/useStore'; @@ -57,6 +59,10 @@ const AppContent = () => { } }; + React.useEffect(() => { + setSmartChartsPublicPath(getUrlBase('/js/smartcharts/')); + }, []); + React.useEffect(() => { // Listen for proposal open contract messages to check // if there is any active contract from bot still running diff --git a/src/components/trade-animation/trade-animation.tsx b/src/components/trade-animation/trade-animation.tsx index be12bbca..b2257f4e 100644 --- a/src/components/trade-animation/trade-animation.tsx +++ b/src/components/trade-animation/trade-animation.tsx @@ -31,16 +31,15 @@ const TradeAnimation = observer(({ className, should_show_overlay }: TTradeAnima onStopBotClick, performSelfExclusionCheck, } = run_panel; - const { account_status } = client; + const { account_status, is_logged_in } = client; const cashier_validation = account_status?.cashier_validation; const [shouldDisable, setShouldDisable] = 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(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + if (is_logged_in) performSelfExclusionCheck(); + }, [is_logged_in, performSelfExclusionCheck]); React.useEffect(() => { if (shouldDisable) { diff --git a/src/external/bot-skeleton/scratch/dbot.js b/src/external/bot-skeleton/scratch/dbot.js index 6c628fc0..b733c90c 100644 --- a/src/external/bot-skeleton/scratch/dbot.js +++ b/src/external/bot-skeleton/scratch/dbot.js @@ -27,7 +27,7 @@ class DBot { async initWorkspace(public_path, store, api_helpers_store, is_mobile, is_dark_mode) { await loadBlockly(is_dark_mode); const recent_files = await getSavedWorkspaces(); - api_base.init(); + this.interpreter = Interpreter(); const that = this; window.Blockly.Blocks.trade_definition_tradetype.onchange = function (event) { diff --git a/src/external/bot-skeleton/services/api/active-symbols.js b/src/external/bot-skeleton/services/api/active-symbols.js index 1d426d74..6ea60a4d 100644 --- a/src/external/bot-skeleton/services/api/active-symbols.js +++ b/src/external/bot-skeleton/services/api/active-symbols.js @@ -7,7 +7,7 @@ import PendingPromise from '../../utils/pending-promise'; import { api_base } from './api-base'; export default class ActiveSymbols { - constructor(ws, trading_times) { + constructor(trading_times) { this.active_symbols = []; this.disabled_symbols = config.DISABLED_SYMBOLS; this.disabled_submarkets = config.DISABLED_SUBMARKETS; @@ -15,7 +15,6 @@ export default class ActiveSymbols { this.is_initialised = false; this.processed_symbols = {}; this.trading_times = trading_times; - this.ws = ws; } async retrieveActiveSymbols(is_forced_update = false) { @@ -27,9 +26,14 @@ export default class ActiveSymbols { } this.is_initialised = true; - const active_symbols = api_base?.active_symbols ?? []; - this.active_symbols = active_symbols; + if (api_base.has_active_symbols) { + this.active_symbols = api_base?.active_symbols ?? []; + } else { + await api_base.active_symbols_promise; + this.active_symbols = api_base?.active_symbols ?? []; + } + this.processed_symbols = this.processActiveSymbols(); // TODO: fix need to look into it as the method is not present diff --git a/src/external/bot-skeleton/services/api/api-base.js b/src/external/bot-skeleton/services/api/api-base.js index 9b8fdf64..6bc7e1dd 100644 --- a/src/external/bot-skeleton/services/api/api-base.js +++ b/src/external/bot-skeleton/services/api/api-base.js @@ -2,6 +2,7 @@ import { observer as globalObserver } from '../../utils/observer'; import { doUntilDone, socket_state } from '../tradeEngine/utils/helpers'; import { generateDerivApiInstance, getLoginId, getToken } from './appId'; +import chart_api from './chart-api'; class APIBase { api; @@ -12,26 +13,28 @@ class APIBase { is_running = false; subscriptions = []; time_interval = null; - has_activeSymbols = false; + has_active_symbols = false; is_stopping = false; active_symbols = []; + active_symbols_promise = null; + async init(force_update = false) { + this.toggleRunButton(true); + if (force_update) this.terminate(); + this.api = generateDerivApiInstance(); + if (!this.has_active_symbols) { + this.active_symbols_promise = this.getActiveSymbols(); + } + this.initEventListeners(); + if (this.time_interval) clearInterval(this.time_interval); + this.time_interval = null; + this.getTime(); + if (getLoginId()) { - this.toggleRunButton(true); - if (force_update) this.terminate(); - this.api = generateDerivApiInstance(); - this.initEventListeners(); await this.authorizeAndSubscribe(); - if (this.time_interval) clearInterval(this.time_interval); - this.time_interval = null; - this.getTime(); - } else { - this.api = generateDerivApiInstance(); - if (!this.has_activeSymbols) { - this.getActiveSymbols(); - } } + chart_api.init(); } getConnectionStatus() { @@ -79,10 +82,10 @@ class APIBase { this.api.authorize(this.token); try { const { authorize } = await this.api.expectResponse('authorize'); - if (this.has_activeSymbols) { + if (this.has_active_symbols) { this.toggleRunButton(false); } else { - this.getActiveSymbols(); + this.active_symbols_promise = this.getActiveSymbols(); } await this.subscribe(); this.account_info = authorize; @@ -101,16 +104,19 @@ class APIBase { } 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); - this.active_symbols = active_symbols; - }); + await doUntilDone(() => this.api.send({ active_symbols: 'brief' })).then( + ({ active_symbols = [], error = {} }) => { + const pip_sizes = {}; + if (active_symbols.length) this.has_active_symbols = true; + active_symbols.forEach(({ symbol, pip }) => { + pip_sizes[symbol] = +(+pip).toExponential().substring(3); + }); + this.pip_sizes = pip_sizes; + this.toggleRunButton(false); + this.active_symbols = active_symbols; + return active_symbols || error; + } + ); }; toggleRunButton = toggle => { diff --git a/src/external/bot-skeleton/services/api/api-helpers.js b/src/external/bot-skeleton/services/api/api-helpers.js index de5f93e4..12ccc622 100644 --- a/src/external/bot-skeleton/services/api/api-helpers.js +++ b/src/external/bot-skeleton/services/api/api-helpers.js @@ -9,7 +9,7 @@ class ApiHelpers { constructor(api_helpers_store) { this.trading_times = new TradingTimes(api_helpers_store); this.contracts_for = new ContractsFor(api_helpers_store); - this.active_symbols = new ActiveSymbols(api_helpers_store.ws, this.trading_times); + this.active_symbols = new ActiveSymbols(this.trading_times); this.account_limits = new AccountLimits(api_helpers_store); } diff --git a/src/external/bot-skeleton/services/api/chart-api.js b/src/external/bot-skeleton/services/api/chart-api.js new file mode 100644 index 00000000..15aadc79 --- /dev/null +++ b/src/external/bot-skeleton/services/api/chart-api.js @@ -0,0 +1,25 @@ +import { generateDerivApiInstance, getLoginId, getToken } from './appId'; + +class ChartAPI { + api; + + init = async () => { + this.api = await generateDerivApiInstance(); + if (getLoginId()) { + await this.api.authorize(getToken().token); + } + this.getTime(); + }; + + getTime() { + if (!this.time_interval) { + this.time_interval = setInterval(() => { + this.api.send({ time: 1 }); + }, 30000); + } + } +} + +const chart_api = new ChartAPI(); + +export default chart_api; diff --git a/src/external/bot-skeleton/services/api/contracts-for.js b/src/external/bot-skeleton/services/api/contracts-for.js index 21ab7c82..b4facbdb 100644 --- a/src/external/bot-skeleton/services/api/contracts-for.js +++ b/src/external/bot-skeleton/services/api/contracts-for.js @@ -1,6 +1,8 @@ import { config } from '../../constants/config'; import PendingPromise from '../../utils/pending-promise'; +import { api_base } from './api-base'; + export default class ContractsFor { constructor({ ws, server_time }) { this.cache_age_in_min = 10; @@ -196,7 +198,7 @@ export default class ContractsFor { } this.retrieving_contracts_for[symbol] = new PendingPromise(); - const response = await this.ws.send({ contracts_for: symbol }); + const response = await api_base.api.send({ contracts_for: symbol }); if (response.error) { return []; diff --git a/src/main.tsx b/src/main.tsx index 6300a444..cd67dfc8 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,5 @@ -import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './app/App.tsx'; -ReactDOM.createRoot(document.getElementById('root')!).render( - - - -); +ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/src/pages/chart/chart.tsx b/src/pages/chart/chart.tsx index b80c7c62..218c957d 100644 --- a/src/pages/chart/chart.tsx +++ b/src/pages/chart/chart.tsx @@ -2,13 +2,37 @@ import { useEffect } from 'react'; import classNames from 'classnames'; import { observer } from 'mobx-react-lite'; -import { ChartTitle, setSmartChartsPublicPath, SmartChart } from '@deriv/deriv-charts'; +import { + ActiveSymbolsRequest, + ServerTimeRequest, + TicksHistoryResponse, + TicksStreamRequest, + TradingTimesRequest, +} from '@deriv/api-types'; +import { ChartTitle, SmartChart } from '@deriv/deriv-charts'; -import { getUrlBase } from '@/components/shared'; +import chart_api from '@/external/bot-skeleton/services/api/chart-api'; import { useStore } from '@/hooks/useStore'; import ToolbarWidgets from './toolbar-widgets'; +import '@deriv/deriv-charts/dist/smartcharts.css'; + +type TSubscription = { + [key: string]: null | { + unsubscribe?: () => void; + }; +}; + +type TError = null | { + error?: { + code?: string; + message?: string; + }; +}; + +const subscriptions: TSubscription = {}; + const Chart = observer(({ show_digits_stats }: { show_digits_stats: boolean }) => { const barriers: [] = []; const { common, ui } = useStore(); @@ -23,17 +47,14 @@ const Chart = observer(({ show_digits_stats }: { show_digits_stats: boolean }) = symbol, updateChartType, updateGranularity, - wsForget, - wsForgetStream, - wsSendRequest, - wsSubscribe, + updateSymbol, } = chart_store; const { ui: { is_mobile, is_desktop }, } = useStore(); const { is_drawer_open } = run_panel; const { is_chart_modal_visible } = dashboard; - const is_socket_opened = common.is_socket_opened; + const settings = { assetInformation: false, // ui.is_chart_asset_info_visible, countdown: true, @@ -44,9 +65,51 @@ const Chart = observer(({ show_digits_stats }: { show_digits_stats: boolean }) = }; useEffect(() => { - setSmartChartsPublicPath(getUrlBase('/js/smartcharts/')); + return () => { + chart_api.api.forgetAll('ticks'); + Object.keys(subscriptions).forEach(subscription_id => { + requestForgetStream(subscription_id); + }); + }; }, []); + useEffect(() => { + if (!symbol) updateSymbol(); + }, [symbol, updateSymbol]); + + const requestAPI = (req: ServerTimeRequest | ActiveSymbolsRequest | TradingTimesRequest) => { + return chart_api.api.send(req); + }; + + const requestForget = () => { + return chart_api.api.forgetAll('ticks'); + }; + + const requestForgetStream = (subscription_id: string) => { + subscriptions[subscription_id as string]?.unsubscribe(); + delete subscriptions[subscription_id]; + }; + + const requestSubscribe = async (req: TicksStreamRequest, callback: (data: any) => void) => { + try { + const history = await chart_api.api.send(req); + if (history) callback(history); + if (req.subscribe === 1) { + subscriptions[history?.subscription.id] = chart_api.api + .onMessage() + ?.subscribe(({ data }: { data: TicksHistoryResponse }) => { + callback(data); + }); + } + } catch (e) { + // eslint-disable-next-line no-console + console.log((e as TError)?.error?.message); + } + }; + + if (!symbol) return null; + const is_connection_opened = !!chart_api?.api; + return (
)} chartType={chart_type} isMobile={is_mobile} enabledNavigationWidget={is_desktop} granularity={granularity} - requestAPI={wsSendRequest} - requestForget={wsForget} - requestForgetStream={wsForgetStream} - requestSubscribe={wsSubscribe} + requestAPI={requestAPI} + requestForget={requestForget} + requestForgetStream={requestForgetStream} + requestSubscribe={requestSubscribe} settings={settings} symbol={symbol} topWidgets={() => } - isConnectionOpened={is_socket_opened} + isConnectionOpened={is_connection_opened} getMarketsOrder={getMarketsOrder} isLive leftMargin={80} diff --git a/src/pages/chart/toolbar-widgets.tsx b/src/pages/chart/toolbar-widgets.tsx index 97c85a69..de8252b6 100644 --- a/src/pages/chart/toolbar-widgets.tsx +++ b/src/pages/chart/toolbar-widgets.tsx @@ -1,3 +1,5 @@ +import { memo } from 'react'; + import { ChartMode, DrawTools, Share, StudyLegend, ToolbarWidget, Views } from '@deriv/deriv-charts'; import { isDesktop } from '@/components/shared'; @@ -10,23 +12,30 @@ type TToolbarWidgetsProps = { const ToolbarWidgets = ({ updateChartType, updateGranularity, position }: TToolbarWidgetsProps) => { return ( - - - {isDesktop() && ( - <> - - - - - - )} - + <> +
+ + + {isDesktop() && ( + <> + + + + + + )} + + ); }; -export default ToolbarWidgets; +export default memo(ToolbarWidgets); diff --git a/src/stores/chart-store.ts b/src/stores/chart-store.ts index 1b918864..ac2d7b46 100644 --- a/src/stores/chart-store.ts +++ b/src/stores/chart-store.ts @@ -1,20 +1,13 @@ import { action, computed, makeObservable, observable, reaction } from 'mobx'; -import { - ActiveSymbolsRequest, - ServerTimeRequest, - TicksHistoryRequest, - TicksStreamRequest, - TradingTimesRequest, -} from '@deriv/api-types'; - import { LocalStore } from '@/components/shared'; -import { ServerTime } from '@/external/bot-skeleton'; import RootStore from './root-store'; -export const g_subscribers_map: Partial>> = {}; -let WS: RootStore['ws']; +type TSubscription = { + id: string | null; + subscriber: null | { unsubscribe: () => void }; +}; export default class ChartStore { root_store: RootStore; @@ -34,7 +27,6 @@ export default class ChartStore { }); this.root_store = root_store; - WS = root_store.ws; const { run_panel } = root_store; reaction( @@ -45,6 +37,11 @@ export default class ChartStore { this.restoreFromStorage(); } + subscription: TSubscription = { + id: null, + subscriber: null, + }; + symbol: string | undefined; is_chart_loading: boolean | undefined; chart_type: string | undefined; @@ -126,42 +123,6 @@ export default class ChartStore { } }; - // #region WS - wsSubscribe = (req: TicksStreamRequest, callback: () => void) => { - if (req.subscribe === 1) { - const key = JSON.stringify(req); - const subscriber = WS?.subscribeTicksHistory(req, callback); - g_subscribers_map[key] = subscriber; - } - }; - - wsForget = (req: TicksHistoryRequest) => { - const key = JSON.stringify(req); - if (g_subscribers_map[key]) { - g_subscribers_map[key]?.unsubscribe(); - delete g_subscribers_map[key]; - } - }; - - wsForgetStream = (stream_id: string) => { - WS?.forgetStream(stream_id); - }; - - wsSendRequest = (req: TradingTimesRequest | ActiveSymbolsRequest | ServerTimeRequest) => { - if ('time' in req && req.time) { - return ServerTime.timePromise().then(() => { - return { - msg_type: 'time', - time: ServerTime.get().unix(), - }; - }); - } - if ('active_symbols' in req && req.active_symbols) { - return WS?.activeSymbols(); - } - if (WS?.storage.send) return WS?.storage.send(req); - }; - getMarketsOrder = (active_symbols: { market: string; display_name: string }[]) => { const synthetic_index = 'synthetic_index'; diff --git a/src/stores/load-modal-store.ts b/src/stores/load-modal-store.ts index f0e5fa90..4c78f3fa 100644 --- a/src/stores/load-modal-store.ts +++ b/src/stores/load-modal-store.ts @@ -163,7 +163,7 @@ export default class LoadModalStore implements ILoadModalStore { get selected_strategy(): TStrategy { return ( - this.dashboard_strategies.find((ws: { id: string }) => ws.id === this.selected_strategy_id) ?? + this.dashboard_strategies.find((workspace: { id: string }) => workspace.id === this.selected_strategy_id) ?? this.dashboard_strategies[0] ); } diff --git a/src/stores/root-store.ts b/src/stores/root-store.ts index 34c4a8e7..92b85643 100644 --- a/src/stores/root-store.ts +++ b/src/stores/root-store.ts @@ -43,7 +43,6 @@ export default class RootStore { public data_collection_store: DataCollectionStore; core = {}; - ws = null; constructor(dbot: unknown) { this.dbot = dbot; diff --git a/src/stores/self-exclusion-store.ts b/src/stores/self-exclusion-store.ts index c186d696..8496e23b 100644 --- a/src/stores/self-exclusion-store.ts +++ b/src/stores/self-exclusion-store.ts @@ -67,8 +67,8 @@ export default class SelfExclusionStore { async checkRestriction() { if (api_base.api) { - api_base.api.getSelfExclusion().then(response => { - const { max_losses: maxLosses } = response.get_self_exclusion; + api_base.api.getSelfExclusion().then(({ get_self_exclusion }) => { + const { max_losses: maxLosses } = get_self_exclusion; if (maxLosses) { this.setApiMaxLosses(maxLosses); } diff --git a/src/stores/summary-card-store.ts b/src/stores/summary-card-store.ts index f8a7d390..0aa93a31 100644 --- a/src/stores/summary-card-store.ts +++ b/src/stores/summary-card-store.ts @@ -7,6 +7,7 @@ import { getIndicativePrice, isEqualObject, isMultiplierContract } from '@/compo import { TContractInfo } from '@/components/summary/summary-card.types'; import { getValidationRules, TValidationRuleIndex, TValidationRules } from '@/constants/contract'; import { contract_stages } from '@/constants/contract-stage'; +import { api_base } from '@/external/bot-skeleton'; import { getContractUpdateConfig } from '@/utils/multiplier'; import Validator from '@/utils/tmp/validator'; @@ -212,7 +213,7 @@ export default class SummaryCardStore { const limit_order = this.getLimitOrder(); if (this.contract_info?.contract_id) { - this.root_store.ws.contractUpdate(this.contract_info?.contract_id, limit_order).then(response => { + api_base.api.contractUpdate(this.contract_info?.contract_id, limit_order).then(response => { if (response.error) { this.root_store.run_panel.showContractUpdateErrorDialog(response.error.message); return; diff --git a/vite.config.ts b/vite.config.ts index 254e961e..0ecd3297 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from 'vite'; +import { viteStaticCopy } from 'vite-plugin-static-copy'; import svgr from 'vite-plugin-svgr'; import { viteRequire } from 'vite-require'; import tsconfigPaths from 'vite-tsconfig-paths'; @@ -28,6 +29,18 @@ export default defineConfig({ } }, }, + viteStaticCopy({ + targets: [ + { + src: 'node_modules/@deriv/deriv-charts/dist/*', + dest: 'js/smartcharts', + }, + { + src: 'node_modules/@deriv/deriv-charts/dist/chart/assets/*', + dest: 'assets', + }, + ], + }), ], css: { preprocessorOptions: {