Skip to content

chore: add ohlcv websocket streaming#29739

Open
sahar-fehri wants to merge 20 commits into
mainfrom
chore/ohlcv-ws-streaming-integration
Open

chore: add ohlcv websocket streaming#29739
sahar-fehri wants to merge 20 commits into
mainfrom
chore/ohlcv-ws-streaming-integration

Conversation

@sahar-fehri
Copy link
Copy Markdown
Contributor

@sahar-fehri sahar-fehri commented May 5, 2026

Description

Related to: https://www.notion.so/metamask-consensys/OHLCV-WebSocket-Integration-UI-Implementation-Guide-346f86d67d6880b6a70fc3be0f0c34b9

Wires OHLCVService from @metamask/core-backend into the Engine and creates a useOHLCVRealtime hook that streams live candlestick updates to the advanced chart via the existing realtimeBar prop on AdvancedChart.

Why: The advanced chart currently only renders historical data fetched via the REST OHLCV API. Users see stale candles until they navigate away and back. Real-time streaming via WebSocket keeps the chart live with 5-second heartbeat updates.

How: Follows the exact same Engine wiring pattern as AccountActivityService — messenger, init function, Engine registration. The new useOHLCVRealtime hook subscribes to OHLCVService:barUpdated events, filters by channel, and converts the WS bar format (timestamp in Unix seconds) to the chart's expected format (time in milliseconds).

Manual Test Plan

Prerequisites

  • MetaMask Mobile connected to a wallet with tokens
  • backendWebSocketConnection feature flag enabled

Adding console.log statements to the mobile hook

1. Inside handleBarUpdated — after the channel guard:

const handleBarUpdated = (payload: { channel: string; bar: WSOHLCVBar }) => {
  if (payload.channel === channelRef.current) {
    console.log(                                                        // ← ADD
      `[OHLCV-WS] Bar received — channel=${payload.channel}, close=${payload.bar.close}, ts=${payload.bar.timestamp}`,
    );
    lastMessageTimeRef.current = Date.now();
    ...

2. Inside handleSubscriptionError — first line of the callback:

const handleSubscriptionError = (payload: { channel: string; error: string; operation: string }) => {
  console.log(                                                          // ← ADD
    `[OHLCV-WS] Subscription error on ${payload.channel}: ${payload.error} (${payload.operation})`,
  );
};

3. Inside handleChainStatusChanged — after the chainIds.includes guard:

if (payload.chainIds.includes(chainId)) {
  console.log(                                                          // ← ADD
    `[OHLCV-WS] Chain status changed — chainId=${chainId}, status=${payload.status}`,
  );
  chainDownRef.current = payload.status === 'down';
}

4. Inside pollLatest — first line of the function:

const pollLatest = async () => {
  pollingAbortRef.current?.abort();
  const controller = new AbortController();
  pollingAbortRef.current = controller;

  console.log('[OHLCV-WS] Polling /latest via REST fallback');          // ← ADD
  ...

5. Inside the staleness setInterval — when isStale || chainDown:

if (isStale || chainDownRef.current) {
  console.log(                                                          // ← ADD
    `[OHLCV-WS] Stream stale or chain down — isStale=${isStale}, chainDown=${chainDownRef.current}, elapsed=${elapsed}ms`,
  );
  pollLatest();
}

6. Inside the debounce setTimeout — first line:

debounceTimerRef.current = setTimeout(async () => {
  console.log(                                                          // ← ADD
    `[OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for ${channel}`,
  );
  try {
    await Engine.controllerMessenger.call('OHLCVService:subscribe', { ... });
    ...

7. In the cleanup return function — first line:

return () => {
  console.log(                                                          // ← ADD
    `[OHLCV-WS] Cleanup — channel=${channel}, wasSubscribed=${subscribedRef.current}`,
  );
  cancelledRef.current = true;
  ...

Enabling core logs in the debugger

By default, core OHLCVService logs use projectLogger (the debug package) and won't appear in the React Native debugger. To make them visible, open:

node_modules/@metamask/core-backend/dist/ws/ohlcv/OHLCVService.cjs

Find this line (near the top, around line 30):

const log = (0, logger_1.createModuleLogger)(logger_1.projectLogger, SERVICE_NAME);

Replace with:

const log = (...args) => console.log('[OHLCV-WS]', ...args);

Now all core logs will appear in the debugger with the [OHLCV-WS] prefix, alongside the mobile hook logs. Revert with yarn install when done.


Group A — No Code Changes (Just Tap and Observe)


Scenario 1: Basic WebSocket Subscription

Steps:

  1. Open Token Details for a supported token (e.g., ETH on Base)
  2. Wait for historical chart to load
  3. Observe logs

Expected logs:

[OHLCV-WS] OHLCV-WS: Initializing — registering system-notifications callback
[OHLCV-WS] OHLCV-WS: Resubscribing active channels after reconnect {count: 0}
[OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.15m.eur
[OHLCV-WS] OHLCV-WS: Subscribe succeeded — new WS subscription created {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'}
[OHLCV-WS] Bar received — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, close=1986.69, ts=1778538600

Note: Resubscribing active channels after reconnect {count: 0} appears at app boot because AccountActivityService opened the shared WebSocket first. OHLCVService hears the CONNECTED event and checks for channels to restore — finds zero since no subscription exists yet. This is normal.

Verify: Bars continue arriving every ~5s with updating close prices.


Scenario 2: Navigate Away (Unsubscribe + Grace Period)

Steps:

  1. From Scenario 1, press back to leave Token Details
  2. Wait 3+ seconds
  3. Observe logs

Expected logs:

[OHLCV-WS] Cleanup — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, wasSubscribed=true
[OHLCV-WS] OHLCV-WS: Grace period expired — performing actual WS unsubscribe {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'}
[OHLCV-WS] OHLCV-WS: WS unsubscribe completed {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'}

Verify: No more bar updates after grace period expires.


Scenario 3: Rapid Navigation (Grace Period Cancel)

Steps:

  1. Open Token Details for Token A, wait for subscription
  2. Navigate back
  3. Immediately re-open Token A (within 3 seconds)

Expected logs:

[OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.15m.eur
[OHLCV-WS] OHLCV-WS: Subscribe succeeded — new WS subscription created {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'}
[OHLCV-WS] Cleanup — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, wasSubscribed=true
[OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.15m.eur
[OHLCV-WS] OHLCV-WS: Cancelled grace-period unsubscribe, bumped refCount {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur', refCount: 1}

Verify: Cancelled grace-period unsubscribe, bumped refCount appears — subscription was reused without a server roundtrip.


Scenario 4: Switch Between Tokens

Steps:

  1. Open Token Details for Token A (e.g. ETH on Base), wait for subscription
  2. Navigate back to token list
  3. Open Token B (e.g. MNT on Ethereum)

Expected logs:

[OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.15m.eur
[OHLCV-WS] OHLCV-WS: Subscribe succeeded — new WS subscription created {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'}
[OHLCV-WS] Bar received — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, close=1988.32, ts=1778539500
[OHLCV-WS] Cleanup — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, wasSubscribed=true
[OHLCV-WS] OHLCV-WS: Grace period expired — performing actual WS unsubscribe {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'}
[OHLCV-WS] OHLCV-WS: WS unsubscribe completed {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'}
[OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:1/erc20:0x3c3a...15m.eur
[OHLCV-WS] OHLCV-WS: Subscribe succeeded — new WS subscription created {channel: 'market-data.v1.eip155:1/erc20:0x3c3a...15m.eur'}
[OHLCV-WS] Bar received — channel=market-data.v1.eip155:1/erc20:0x3c3a...15m.eur, close=0.59, ts=1778539500

Verify: Token A fully unsubscribes (grace period expires). Token B gets its own subscription and bars flow.


Scenario 5: Rapid Time Range Switching

Steps:

  1. Open Token Details, wait for bars on default time range (15m)
  2. Rapidly switch between time ranges (e.g. 1H → 1D → 1W → 1H)

Expected logs (showing one switch cycle: 15m → 1h):

[OHLCV-WS] Cleanup — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, wasSubscribed=true
[OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.1h.eur
[OHLCV-WS] OHLCV-WS: Flushing grace-period channel before new subscribe {flushedChannel: '...15m.eur', newChannel: '...1h.eur'}
[OHLCV-WS] OHLCV-WS: Grace period expired — performing actual WS unsubscribe {channel: '...15m.eur'}
[OHLCV-WS] OHLCV-WS: WS unsubscribe completed {channel: '...15m.eur'}
[OHLCV-WS] OHLCV-WS: Subscribe succeeded — new WS subscription created {channel: '...1h.eur'}

This pattern repeats for each switch (1h → 1d → 1h → 15m → 1m). Each time, the old channel is flushed immediately before the new subscribe — no accumulation, no server rejections.

Verify: Every subscribe succeeds (Subscribe succeeded). Flushing grace-period channel appears before each new subscribe. Bars flow on the final time range.


Scenario 6: App Background / Foreground

Steps:

  1. Open Token Details for a supported token, wait for bars to flow
  2. Press home button (send app to background)
  3. Wait ~10 seconds
  4. Bring app back to foreground

Expected logs:

[OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.15m.eur
[OHLCV-WS] OHLCV-WS: Subscribe succeeded — new WS subscription created {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'}
[OHLCV-WS] Bar received — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, close=1987.42, ts=1778540400
[OHLCV-WS] Bar received — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, close=1987.37, ts=1778540400
— app sent to background, then brought back —
[OHLCV-WS] OHLCV-WS: Resubscribing active channels after reconnect {count: 1}
[OHLCV-WS] OHLCV-WS: Resubscription succeeded {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'}
[OHLCV-WS] Bar received — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, close=1987.18, ts=1778540400

Verify: Resubscribing active channels after reconnect {count: 1} appears after foregrounding. Bars resume automatically without user interaction.


Scenario 7: Unsupported Token (No OHLCV Data)

Steps:

  1. Open Token Details for a token with no OHLCV API data

Expected: No WS subscription, falls back to legacy line chart.


Group B — Requires Changing DEV Constants in useOHLCVRealtime.ts

After testing, set both constants back to 0 before committing.


Scenario 8: WebSocket Disconnect → REST Polling Fallback

What this tests: The WebSocket connection drops and stays disconnected. After the staleness threshold (30s) is exceeded, the hook falls back to polling REST.

Code to add

In useOHLCVRealtime.ts, set the DEV constant:

const DEV_SIMULATE_WS_DISCONNECT_AFTER_MS = 10000; // ← ACTIVE

The simulation code in the hook must call disconnect (clean shutdown, not forceReconnection):

if (DEV_SIMULATE_WS_DISCONNECT_AFTER_MS > 0) {
  setTimeout(() => {
    console.log(
      `[OHLCV-WS] DEV: Simulating WS disconnect (no reconnect) after ${DEV_SIMULATE_WS_DISCONNECT_AFTER_MS}ms`,
    );
    Engine.controllerMessenger.call(
      'BackendWebSocketService:disconnect' as never,
    );
  }, DEV_SIMULATE_WS_DISCONNECT_AFTER_MS);
}

How it works

After 10s, calls BackendWebSocketService:disconnect (clean shutdown, no auto-reconnect). The WS stays dead. After 30s with no bars, staleness triggers REST polling every 15s.

Steps to test

  1. Set the constants as shown above
  2. Rebuild / hot-reload the app
  3. Open Token Details for a supported token
  4. Wait for bars to start flowing (~5s)
  5. At 10s, the simulated disconnect fires automatically
  6. Wait ~30s for the staleness threshold
  7. Observe REST fallback polling in logs

Expected logs:

[OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.15m.eur
[OHLCV-WS] OHLCV-WS: Subscribe succeeded — new WS subscription created {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'}
[OHLCV-WS] Bar received — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, close=1985.08, ts=1778540400
[OHLCV-WS] DEV: Simulating WS disconnect (no reconnect) after 10000ms
[OHLCV-WS] Stream stale or chain down — isStale=true, chainDown=false, elapsed=38975ms
[OHLCV-WS] Polling /latest via REST fallback
[OHLCV-WS] Stream stale or chain down — isStale=true, chainDown=false, elapsed=44886ms
[OHLCV-WS] Polling /latest via REST fallback

Verify: After the simulated disconnect, no more Bar received logs. REST polling kicks in every 15s once staleness threshold (30s) is exceeded.


Group C — Requires Editing .cjs in node_modules

After testing, run yarn install in the mobile repo to restore the original file.


Scenario 10: Subscribe Failure / Error Recovery

What this tests: OHLCVService.subscribe() fails. The service catches the error, publishes OHLCVService:subscriptionError, forces reconnection, and REST fallback keeps the chart alive.

Code to add

1. Disable dev simulation constant in useOHLCVRealtime.ts:

const DEV_SIMULATE_WS_DISCONNECT_AFTER_MS = 0;

2. Simulate subscribe failure — open node_modules/@metamask/core-backend/dist/ws/ohlcv/OHLCVService.cjs.

Find the subscribe call (look for BackendWebSocketService:subscribe) and comment it out, then add a throw:

// await __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:subscribe', {
//     channels: [channel],
//     channelType: SUBSCRIPTION_NAMESPACE,
//     callback: (notification) => {
//         __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_handleBarUpdate).call(this, channel, notification);
//     },
// });
throw new Error('DEV: Simulated subscribe failure — invalid channel');

Steps to test

  1. Apply both code changes above
  2. Reload the app
  3. Open Token Details for a supported token
  4. Observe logs

Expected — look for these key logs:

[OHLCV-WS] OHLCV-WS: Resubscribing active channels after reconnect {count: 0}
[OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.15m.eur
[OHLCV-WS] OHLCV-WS: Subscription failed, forcing reconnection {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur', error: Error: Test error ...}
[OHLCV-WS] Subscription error on market-data.v1.eip155:8453/slip44:60.15m.eur: Error: Test error (subscribe)
[OHLCV-WS] OHLCV-WS: Forcing WebSocket reconnection
[OHLCV-WS] OHLCV-WS: Resubscribing active channels after reconnect {count: 0}
[OHLCV-WS] Stream stale or chain down — isStale=true, chainDown=false, elapsed=44219ms
[OHLCV-WS] Polling /latest via REST fallback

Verify: Error is caught, reconnection attempted (Forcing WebSocket reconnection), and REST fallback keeps chart alive after staleness is detected.


Log Reference

All logs use the OHLCV-WS prefix. Filter by OHLCV-WS in Flipper / debugger.

Changelog

CHANGELOG entry: Adds websocket streaming integration for ohlcv data

Related issues

Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-3194?atlOrigin=eyJpIjoiYmQ4N2E3MTlmZTFlNGYyNGFiODUxNzA2YThmM2FkYTkiLCJwIjoiaiJ9
Related: MetaMask/core#8695

Manual testing steps

Feature: my feature name

  Scenario: user [verb for user action]
    Given [describe expected initial app state]

    When user [verb for user action]
    Then [describe expected outcome]

Screenshots/Recordings

Before

After

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Medium Risk
Introduces a new Engine-wired OHLCVService and a real-time data hook that subscribes/polls network resources; failures could impact chart updates and add background network load despite feature-flag gating.

Overview
Adds a new Engine-integrated OHLCVService (messenger + init wiring) and upgrades @metamask/core-backend to a preview build to support OHLCV WebSocket streaming.

Updates Price.advanced to optionally stream live candles into AdvancedChart via the existing realtimeBar prop, gated by a new version-gated remote flag (tokenDetailsOhlcvWsIntegration) and chart readiness/threshold checks; header price/diff now prefers the latest streamed close when available.

Introduces useOHLCVRealtime (with debounce, channel filtering, cleanup/unsubscribe, and staleness/chain-down REST /latest polling fallback) plus new unit tests and adjusted chart tests/mocks, and registers the new feature flag in CI constant resolution and the feature-flag registry.

Reviewed by Cursor Bugbot for commit 1d8190f. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 11, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednpm/​@​metamask/​core-backend@​6.2.1 ⏵ 6.2.2-preview-150787a7d100 +25100100100 +6100

View full report

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 12, 2026

Codecov Report

❌ Patch coverage is 91.07143% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.68%. Comparing base (3751d9a) to head (1d8190f).
⚠️ Report is 59 commits behind head on main.

Files with missing lines Patch % Lines
...onents/UI/Charts/AdvancedChart/useOHLCVRealtime.ts 91.95% 2 Missing and 5 partials ⚠️
...mponents/UI/AssetOverview/Price/Price.advanced.tsx 72.72% 1 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #29739      +/-   ##
==========================================
+ Coverage   81.54%   81.68%   +0.13%     
==========================================
  Files        5343     5389      +46     
  Lines      142128   143476    +1348     
  Branches    32411    32756     +345     
==========================================
+ Hits       115899   117192    +1293     
+ Misses      18299    18266      -33     
- Partials     7930     8018      +88     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread package.json Outdated
"@metamask/connectivity-controller": "^0.1.0",
"@metamask/controller-utils": "^11.18.0",
"@metamask/core-backend": "^6.2.0",
"@metamask/core-backend": "npm:@metamask-previews/core-backend@6.2.2-preview-094d56fb3",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: to be removed after release

@sahar-fehri sahar-fehri marked this pull request as ready for review May 12, 2026 01:15
@sahar-fehri sahar-fehri requested review from a team as code owners May 12, 2026 01:15
Comment thread app/components/UI/Charts/AdvancedChart/useOHLCVRealtime.ts
});

const wsInterval = WS_INTERVAL_BY_TIME_RANGE[timeRange];
// TODO: Check if we want to add a feature flag to gate the WS OHLCV feature
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be a followup

Comment thread app/components/UI/Charts/AdvancedChart/useOHLCVRealtime.ts
@github-actions github-actions Bot added size-XL and removed size-L labels May 12, 2026
@sahar-fehri sahar-fehri requested a review from a team as a code owner May 12, 2026 12:23
Comment thread app/components/UI/Charts/AdvancedChart/useOHLCVRealtime.ts
Comment thread app/components/UI/Charts/AdvancedChart/useOHLCVRealtime.ts
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a24239b. Configure here.

Comment thread app/components/UI/AssetOverview/Price/Price.advanced.tsx Outdated
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeWalletPlatform
  • Selected Performance tags: @PerformanceLaunch, @PerformanceAssetLoading
  • Risk Level: medium
  • AI Confidence: 78%
click to see 🤖 AI reasoning details

E2E Test Selection:
The PR introduces a new OHLCVService (real-time OHLCV candlestick WebSocket service) integrated into the Engine, along with a feature-flagged UI integration in the advanced chart component.

Key findings:

  1. Engine changes are additive: OHLCVService is added as a new stateless service (in STATELESS_NON_CONTROLLER_NAMES), not modifying existing controllers. No initialization order violations detected.
  2. OHLCVService depends on BackendWebSocketService: The messenger delegates BackendWebSocketService actions/events. This is the same WebSocket infrastructure tested by the account-activity WebSocket tests in SmokeWalletPlatform.
  3. Feature flag disabled by default: tokenDetailsOhlcvWsIntegration has inProd: false and enabled: false as production default, so the UI changes won't be active in E2E tests unless explicitly enabled.
  4. Preview package version: @metamask/core-backend updated to 6.2.2-preview-150787a7d - this is a preview/pre-release version which carries some risk for the WebSocket service infrastructure.
  5. SmokeWalletPlatform covers: The account-activity WebSocket connection tests (web-socket-connection.spec.ts) validate that BackendWebSocketService and related WebSocket infrastructure works correctly after login, background/foreground, and lock/unlock cycles. Adding OHLCVService to the Engine could affect these flows if initialization fails.
  6. No direct E2E tests for OHLCV/advanced charts: No spec files found testing the advanced chart or OHLCV functionality directly.

The primary risk is that the Engine initialization with the new OHLCVService (using a preview package) could affect the WebSocket service infrastructure. SmokeWalletPlatform is the appropriate tag as it contains the WebSocket connection tests and covers the wallet platform where asset overview is displayed.

Performance Test Selection:
Two performance concerns arise from this PR: (1) App launch/initialization - the new OHLCVService is initialized during Engine startup, adding a new WebSocket service to the initialization chain. This could affect cold start time and time-to-interactive (@PerformanceLaunch). (2) Asset loading - the useOHLCVRealtime hook adds real-time WebSocket streaming to the asset overview price component, with staleness polling intervals and REST fallback fetches. Even though feature-flagged off by default, the hook infrastructure is loaded and could affect asset loading performance (@PerformanceAssetLoading).

View GitHub Actions results

@sonarqubecloud
Copy link
Copy Markdown

pull Bot pushed a commit to Reality2byte/core that referenced this pull request May 13, 2026
…MetaMask#8695)

## Explanation

### Architecture Overview

```
┌─────────────────┐        messenger         ┌──────────────────────────┐
│   OHLCVService   │ ─── calls actions ────► │  BackendWebSocketService  │
│  (domain logic)  │                          │  (raw WS connection)      │
│                  │ ◄── listens to events ── │                           │
└────────┬────────┘                           └──────────┬───────────────┘
         │                                               │
    publishes events                              actual WebSocket
    to UI consumers                          (connect, auth, reconnect,
                                              heartbeat, JSON framing)
         │
         ▼
┌──────────────────┐
│   Mobile UI       │
│  (React hooks)    │
│  useOHLCVRealtime │
└──────────────────┘
```

### What

- Add `OHLCVService` for real-time OHLCV (candlestick) data streaming
via the backend WebSocket gateway
- Move all WebSocket-related files (`BackendWebSocketService`,
`AccountActivityService`) into a new `src/ws/` directory per code review
feedback

### Why

- Enable real-time chart updates on the Token Details screen without
polling
- Reduce API load by replacing periodic HTTP calls with persistent
WebSocket subscriptions
- Organize WebSocket code into a dedicated `ws/` folder for better
discoverability

### New files

- `src/ws/ohlcv/OHLCVService.ts` — main service with
subscribe/unsubscribe semantics, reference counting, grace-period
unsubscribe, idempotency checks, chain-status forwarding, and automatic
resubscription on reconnect
- `src/ws/ohlcv/OHLCVService.test.ts` — 22 unit tests covering all paths
(100% branch coverage)
- `src/ws/ohlcv/OHLCVService-method-action-types.ts` — auto-generated
messenger action types
- `src/ws/ohlcv/types.ts` — `OHLCVBar` and `OHLCVSubscriptionOptions`
types
- `src/ws/ohlcv/index.ts` — barrel exports

### Modified files

- `src/index.ts` — added exports for `OHLCVService`, its types, and
allowed actions/events; updated import paths to `./ws/`
- `eslint-suppressions.json` — updated paths for moved files, added
suppressions for new test file
- `CHANGELOG.md` — documented new service and exports

### Moved files (no logic changes)

- `src/BackendWebSocketService.ts` → `src/ws/BackendWebSocketService.ts`
- `src/BackendWebSocketService.test.ts` →
`src/ws/BackendWebSocketService.test.ts`
- `src/BackendWebSocketService-method-action-types.ts` →
`src/ws/BackendWebSocketService-method-action-types.ts`
- `src/AccountActivityService.ts` → `src/ws/AccountActivityService.ts`
- `src/AccountActivityService.test.ts` →
`src/ws/AccountActivityService.test.ts`
- `src/AccountActivityService-method-action-types.ts` →
`src/ws/AccountActivityService-method-action-types.ts`
- Only import path updates (`./logger` → `../logger`, `./types` →
`../types`, test helper paths)

### Key design decisions

- **UI-driven lifecycle** — unlike `AccountActivityService`
(auto-subscribes on account change), `OHLCVService` exposes
`subscribe()`/`unsubscribe()` called by the UI when the chart
mounts/unmounts
- **Reference counting** — multiple UI consumers subscribing to the same
assetId/interval/currency share one WebSocket subscription
- **Grace period (3s)** — when all consumers unsubscribe, actual WS
unsubscribe is delayed 3 seconds to absorb rapid navigation (Token A →
Token B → Token A)
- **Idempotency** — uses `channelHasSubscription` before subscribing;
duplicate calls are no-ops (React Strict Mode safe)
- **Chain status** — listens to `system-notifications.v1.market-data.v1`
(auto-subscribed by server) and publishes
`OHLCVService:chainStatusChanged`
- **Disconnect handling** — on WebSocket disconnect, publishes
`chainStatusChanged { status: 'down' }` for all tracked chains,
triggering UI polling fallback
- **Reconnect** — resubscribes all active channels when WebSocket
reconnects (no `sessionId` needed for OHLCV; UI polling fallback covers
the gap)
- **`init()` method** — system notification callback registered in
`init()` (not constructor) to comply with messenger-in-constructor lint
rule

### Events published

- `OHLCVService:barUpdated` — `{ channel, bar: OHLCVBar }` — new candle
data from WebSocket
- `OHLCVService:chainStatusChanged` — `{ chainIds, status, timestamp? }`
— chain up/down (server notification or WS disconnect)
- `OHLCVService:subscriptionError` — `{ channel, error, operation }` —
subscribe or unsubscribe failure

## References

* Related to
https://www.notion.so/metamask-consensys/OHLCV-WebSocket-Integration-UI-Implementation-Guide-346f86d67d6880b6a70fc3be0f0c34b9
* Related to MetaMask/metamask-mobile#29739
* Fixes https://consensyssoftware.atlassian.net/browse/ASSETS-3195

## Checklist

- [ ] I've updated the test suite for new or updated code as appropriate
- [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [ ] I've communicated my changes to consumers by [updating changelogs
for packages I've
changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md)
- [ ] I've introduced [breaking
changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md)
in this PR and have prepared draft pull requests for clients and
consumer packages to resolve them


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds a new WebSocket-driven market-data service with reference
counting, timers, and reconnect resubscription logic, which can affect
subscription lifecycles and event delivery. Also moves existing
WebSocket services into `src/ws/`, so consumers relying on internal
paths (vs package exports) could break if any remain.
> 
> **Overview**
> Adds a new `OHLCVService` to stream real-time OHLCV bars over
WebSocket, exposing `subscribe`/`unsubscribe` via messenger actions,
publishing `barUpdated`/`chainStatusChanged`/`subscriptionError` events,
and handling reconnect resubscription with ref-counting plus a
grace-period unsubscribe (mutex-protected).
> 
> Refactors `core-backend` by moving `BackendWebSocketService` and
`AccountActivityService` (and their tests/action-type files) into
`src/ws/`, updating imports/exports (`src/index.ts`), and updating lint
suppressions; also adds `async-mutex` plus comprehensive unit tests for
the new service and documents the addition in the changelog.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
730af62. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants