Commit 2d3cc40
chore: add ohlcv websocket streaming (#29739)
## **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:
```ts
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:
```ts
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:
```ts
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:
```ts
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`:
```ts
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:
```ts
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:
```ts
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):
```js
const log = (0, logger_1.createModuleLogger)(logger_1.projectLogger, SERVICE_NAME);
```
Replace with:
```js
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:
```ts
const DEV_SIMULATE_WS_DISCONNECT_AFTER_MS = 10000; // ← ACTIVE
```
The simulation code in the hook must call `disconnect` (clean shutdown,
**not** `forceReconnection`):
```ts
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`:
```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:
```js
// 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
3. Reload the app
4. Open Token Details for a supported token
5. 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**
<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`
If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`
(This helps the Release Engineer do their job more quickly and
accurately)
-->
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**
```gherkin
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**
<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->
### **Before**
<!-- [screenshots/recordings] -->
### **After**
<!-- [screenshots/recordings] -->
## **Pre-merge author checklist**
<!--
Every checklist item must be consciously assessed before marking this PR
as
"Ready for review". A checked box means you deliberately considered that
responsibility, not that you literally performed every action listed.
Unchecked boxes are ambiguous: they are not an implicit "N/A" and they
are not
a silent "skip". See `docs/readme/ready-for-review.md` for the full
checklist
semantics.
-->
- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I've included tests if applicable
- [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.
#### 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](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93)
to import wallets with many accounts and tokens
- [ ] I've instrumented key operations with Sentry traces for production
performance metrics
- See [`trace()`](/app/util/trace.ts) for usage and
[`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274)
for an example
For performance guidelines and tooling, see the [Performance
Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers).
## **Pre-merge reviewer checklist**
<!--
Reviewer checklist items follow the same semantics as the author
checklist: an
unchecked box is ambiguous, a checked box means the reviewer consciously
assessed that responsibility. See `docs/readme/ready-for-review.md`.
-->
- [ ] 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.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Introduces a new Engine-integrated WebSocket service and a real-time
data path for price charts, which could impact app lifecycle, network
subscriptions, and chart correctness if misconfigured. Includes a REST
polling fallback and feature-flag gating, reducing blast radius but
still touching core infrastructure.
>
> **Overview**
> Adds real-time OHLCV candlestick streaming to the token details
advanced chart by wiring `OHLCVService` (from `@metamask/core-backend`)
into the Engine/messenger layer and upgrading `@metamask/core-backend`
to `^6.3.0`.
>
> Introduces `useOHLCVRealtime`, which subscribes (debounced) to
`OHLCVService` bar updates and provides a staleness/chain-down HTTP
`/latest` fallback, then feeds updates into `AdvancedChart` via its
existing `realtimeBar` prop.
>
> Gates the behavior behind a new remote, version-gated feature flag
`tokenDetailsOhlcvWsIntegration` (registry + selector + CI constant
mapping) and updates related unit tests/mocks to account for the new
hook and selector.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
fe6f560. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Bernardo Garces Chapero <bernardo.chapero@consensys.net>
Co-authored-by: Prithpal Sooriya <prithpal.sooriya@consensys.net>1 parent 2c34efa commit 2d3cc40
19 files changed
Lines changed: 1059 additions & 12 deletions
File tree
- .github/scripts
- app
- components/UI
- AssetOverview/Price
- Charts/AdvancedChart
- core/Engine
- controllers/core-backend
- messengers
- core-backend
- selectors/featureFlagController/tokenDetailsOhlcvWsIntegration
- tests/feature-flags
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
23 | 24 | | |
24 | 25 | | |
25 | 26 | | |
| |||
Lines changed: 11 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
31 | 38 | | |
32 | 39 | | |
33 | 40 | | |
| |||
89 | 96 | | |
90 | 97 | | |
91 | 98 | | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
92 | 103 | | |
93 | 104 | | |
94 | 105 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| 39 | + | |
39 | 40 | | |
40 | 41 | | |
41 | 42 | | |
| |||
62 | 63 | | |
63 | 64 | | |
64 | 65 | | |
| 66 | + | |
65 | 67 | | |
66 | 68 | | |
67 | 69 | | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
68 | 83 | | |
69 | 84 | | |
70 | 85 | | |
| |||
134 | 149 | | |
135 | 150 | | |
136 | 151 | | |
| 152 | + | |
137 | 153 | | |
138 | 154 | | |
139 | 155 | | |
| |||
293 | 309 | | |
294 | 310 | | |
295 | 311 | | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
296 | 343 | | |
297 | 344 | | |
298 | 345 | | |
| |||
346 | 393 | | |
347 | 394 | | |
348 | 395 | | |
349 | | - | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
350 | 399 | | |
351 | 400 | | |
352 | 401 | | |
353 | | - | |
| 402 | + | |
354 | 403 | | |
355 | 404 | | |
356 | | - | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
357 | 412 | | |
358 | 413 | | |
359 | 414 | | |
| |||
554 | 609 | | |
555 | 610 | | |
556 | 611 | | |
| 612 | + | |
557 | 613 | | |
558 | 614 | | |
559 | 615 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
55 | 59 | | |
56 | 60 | | |
57 | 61 | | |
| |||
0 commit comments