Skip to content

-1 balance hotfix and some minor fixes/alterations#73

Merged
theshadow76 merged 6 commits intoChipaDevTeam:masterfrom
sixtysixx:master
Mar 5, 2026
Merged

-1 balance hotfix and some minor fixes/alterations#73
theshadow76 merged 6 commits intoChipaDevTeam:masterfrom
sixtysixx:master

Conversation

@sixtysixx
Copy link
Copy Markdown
Collaborator

@sixtysixx sixtysixx commented Mar 5, 2026

Pull Request

Overview

-1 balance hotfix and some minor fixes/alterations such as unsafe unwraps, and updating pattern for balance gathering

Changes

  • List key changes here.
  • Keep descriptions concise and technical.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation / Examples
  • Performance / Refactoring
  • CI/CD / Build System

Validation

Describe how the changes were tested.

  • Unit tests
  • Integration tests
  • Manual verification

Environment

  • OS: W11 using WSL
  • Python Version: 3.12.12
  • Rust Version: 1.93.1

Checklist

  • Code follows project conventions and style guidelines.
  • Documentation and examples updated if necessary.
  • All tests pass locally.
  • {X] No new warnings introduced.

Screenshots (Optional)

Add relevant visuals if applicable.

Summary by CodeRabbit

  • New Features

    • Added Python language support; expanded state management for trading, balances, server time, and assets.
  • Bug Fixes

    • Improved SSID parsing to reject double-encoded inputs and normalize format.
    • More robust timeout checks and Close-message handling; serialized pending-trade calls to avoid races.
  • Documentation

    • Added build prerequisites note and WebSocket constructor docs.
  • Tests

    • New resilient parsing tests for balance and deals modules.
  • Refactor

    • Unified message/event parsing and streamlined reconnection and error handling.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b6f3c885-f807-4d47-8f37-179cd717e93f

📥 Commits

Reviewing files that changed from the base of the PR and between eb13062 and 05c72b7.

📒 Files selected for processing (2)
  • .serena/project.yml
  • tests/python/pocketoption/test_async_mocked.py
✅ Files skipped from review due to trivial changes (1)
  • tests/python/pocketoption/test_async_mocked.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • .serena/project.yml

📝 Walkthrough

Walkthrough

Resilient message parsing and state management were introduced across PocketOption: rule types replaced with MultiPatternRule, SSID parsing/normalization tightened, a full async thread-safe State and TradeState added, pending-trade call serialization added, WebSocket reconnection logic refactored, and integration tests added for text/binary message handling.

Changes

Cohort / File(s) Summary
Configuration & Docs
\.serena/project.yml, README.md
Added Python to language list and a build prerequisites line mentioning rustc, cargo, and maturin.
Rule & Message Parsing
crates/binary_options_tools/src/pocketoption/modules/balance.rs, crates/binary_options_tools/src/pocketoption/modules/deals.rs
Replaced TwoStepRule/DealsUpdateRule with MultiPatternRule; deals now parse event names from JSON arrays and unify text/binary payload handling; some public constants renamed.
Keep-alive & SSID handling
crates/binary_options_tools/src/pocketoption/modules/keep_alive.rs, crates/binary_options_tools/src/pocketoption/ssid.rs
SSID parsing/normalization rewritten to detect/reject double-encoded JSON and ensure 42["auth",...] wrapping; keep-alive now wraps SID when needed and treats Close messages in routing.
State & Trade Management
crates/binary_options_tools/src/pocketoption/state.rs
Introduced full State and StateBuilder, TradeState with async-safe locks and APIs for balance, assets, server time, opened/closed/pending deals, validators, subscriptions, and temporal data clearing.
Pending Trades Concurrency
crates/binary_options_tools/src/pocketoption/modules/pending_trades.rs
Added call_lock: Arc<Mutex<()>> to PendingTradesHandle and serialize open_pending_order calls via the mutex.
Client & Utils Refactor
crates/binary_options_tools/src/pocketoption/pocket_client.rs, crates/binary_options_tools/src/pocketoption/utils.rs
Connector/error paths migrated to CoreError/CoreResult; pocket_client timeout checks simplified and duplicate docblock added; connector TLS/error mapping updated to core error model.
Core WebSocket Logic
crates/core/src/general/client.rs
Added documented async inits and extracted reconnection/backoff into handle_reconnection_failure(...), centralizing reconnect behavior.
Tests
crates/binary_options_tools/src/pocketoption/modules/mod.rs, crates/binary_options_tools/src/pocketoption/modules/resilient_parsing_tests.rs
Added resilient_parsing_tests with async integration tests verifying BalanceModule and DealsApiModule parsing for text and binary-prefixed messages.
Misc — Formatting & Tests
tests/python/pocketoption/test_async_mocked.py
Minor assertion formatting changes (no behavior change).

Sequence Diagram(s)

sequenceDiagram
    participant Server
    participant WebSocketClient
    participant Module (Balance/Deals)
    participant State

    Server->>WebSocketClient: send "42[...]" or "451-..." message
    WebSocketClient->>Module: deliver Message (text or binary placeholder)
    Module->>Module: MultiPatternRule matches event/pattern
    Module->>State: parse payload → update balance/trades/assets
    State-->>Module: notify waiters (balance_updated/assets_updated)
    Module-->>WebSocketClient: respond/resolve waiting requests
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • Rick-29
  • theshadow76

Poem

🐰 Hop, hop — packets dance and play,
Text and binary find their way,
State keeps order, locks held tight,
SSIDs wrapped, reconnects take flight,
A rabbit cheers: parsing saved the day! 🌿

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title mentions a balance hotfix and minor fixes, but the changeset is substantial with significant refactoring across multiple modules including state management, rule pattern changes, and public API alterations beyond what the title suggests. Consider a more descriptive title such as 'Refactor balance handling and state management' or break this into focused PRs to better represent the scope of changes.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a crucial fix for a negative balance display bug and significantly enhances the client's ability to parse WebSocket messages from the PocketOption platform. The core changes involve making the message processing more resilient to varying Socket.IO message formats and strengthening the session ID (SSID) parsing to prevent common issues. These improvements contribute to a more stable and reliable client, alongside minor code quality enhancements and updated documentation.

Highlights

  • Balance Hotfix & Resilient Parsing: Implemented a hotfix for the -1 balance issue and refactored WebSocket message parsing in the balance and deals modules to robustly handle both legacy (451-) and standard (42) Socket.IO message prefixes, preventing data misinterpretation.
  • SSID Parsing Enhancements: Improved the SSID parsing logic to prevent double-encoded JSON injection and ensure that the raw SSID is consistently formatted for WebSocket communication, enhancing security and reliability.
  • Code Refactoring & Error Handling: Removed several instances of unsafe unwraps and consolidated reconnection logic into a dedicated helper function, improving code readability, maintainability, and error handling across the codebase.
  • Project Configuration & Documentation Updates: Added Python to the project's language configuration and updated the README with clearer instructions for building from source, including necessary toolchain requirements.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • .serena/project.yml
    • Added Python to the list of supported project languages.
  • README.md
    • Updated build instructions for Option C to explicitly list rustc, cargo, and maturin as requirements.
  • crates/binary_options_tools/src/pocketoption/modules/balance.rs
    • Replaced TwoStepRule with MultiPatternRule for more flexible balance update message matching.
    • Simplified the rule definition for balance messages.
  • crates/binary_options_tools/src/pocketoption/modules/deals.rs
    • Removed unused AtomicBool and Ordering imports.
    • Replaced hardcoded 451- prefixes with new EV_ constants for event names.
    • Refactored message parsing logic to dynamically extract event names and data from JSON arrays, supporting both 451- and 42 prefixes.
    • Replaced DealsUpdateRule with MultiPatternRule for deals updates.
    • Removed the custom DealsUpdateRule struct and its implementation.
  • crates/binary_options_tools/src/pocketoption/modules/keep_alive.rs
    • Added logic to ensure SSID is correctly formatted with a 42 prefix for Socket.IO authentication messages.
    • Added Message::Close to the InitRule to handle WebSocket close messages.
  • crates/binary_options_tools/src/pocketoption/modules/mod.rs
    • Added a new module resilient_parsing_tests.
  • crates/binary_options_tools/src/pocketoption/modules/pending_trades.rs
    • Added a comment explaining the purpose of call_lock for pending trade calls.
  • crates/binary_options_tools/src/pocketoption/modules/resilient_parsing_tests.rs
    • Added a new test file with unit tests for resilient parsing of balance and deals messages.
  • crates/binary_options_tools/src/pocketoption/pocket_client.rs
    • Added Rustdoc comments for the new function.
    • Replaced if let Ok(_) = ... with if ... .is_ok() for tokio::time::timeout calls to avoid unsafe unwrap patterns.
    • Consolidated if conditions for assets_updated notification.
  • crates/binary_options_tools/src/pocketoption/ssid.rs
    • Added #[doc(hidden)] attribute to raw and session_raw fields in Demo and Real structs.
    • Improved Ssid::parse function to prevent double-encoded JSON injection and ensure the raw SSID is always in the 42["auth",...] format.
    • Added a new test case test_ssid_rejects_double_encoded_json.
    • Added a test case for parsing only the JSON part of an SSID.
  • crates/binary_options_tools/src/pocketoption/state.rs
    • Updated add_raw_validator, remove_raw_validator, and clear_raw_validators to use expect on RwLock write guards, indicating a poisoned lock.
  • crates/binary_options_tools/src/pocketoption/utils.rs
    • Changed get_connector to return CoreResult instead of ConnectorResult and improved error handling for CONNECTOR.get().
    • Mapped ConnectorError to CoreError::Connection for better error propagation.
  • crates/core/src/general/client.rs
    • Added Rustdoc comments for init functions.
    • Extracted reconnection logic into a new private helper function handle_reconnection_failure to reduce code duplication and improve readability.
Activity
  • The pull request was initiated by sixtysixx.
  • The author provided an overview detailing a hotfix for a -1 balance issue, addressing unsafe unwraps, and updating balance gathering patterns.
  • The changes are categorized as a bug fix, documentation/examples, and performance/refactoring.
  • Validation was performed through unit tests and manual verification.
  • The author confirmed that the code adheres to project conventions, documentation is updated, all tests pass locally, and no new warnings were introduced.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request addresses an issue with balance calculation and introduces several other fixes and alterations. Key changes include refactoring the WebSocket message parsing to be more resilient, replacing unwrap() calls with expect() for improved error reporting, and hardening SSID parsing against potential double-encoding issues. I have one suggestion regarding the new parsing logic in deals.rs to improve its efficiency.

Comment on lines +242 to +246
if let Ok(data) = serde_json::to_string(&arr[1]) {
self.process_text_data(&data, current_expected).await;
expected = ExpectedMessage::None;
continue;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The current implementation serializes the JSON value arr[1] back to a string using serde_json::to_string, only for it to be immediately deserialized from a string inside process_text_data. This serialization-deserialization cycle is inefficient.

A better approach would be to refactor process_text_data to a new function, for example process_json_data, which accepts a &serde_json::Value. This would allow you to deserialize your types directly from the JSON value, avoiding the unnecessary overhead and improving performance. The existing process_text_data could then be updated to parse the string into a Value and call the new function.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/core/src/general/client.rs (1)

269-282: ⚠️ Potential issue | 🔴 Critical

Critical: Match result is discarded, causing function to always return error instead of successful reconnection.

The match expression (lines 269–277) evaluates but its result is immediately discarded. Line 278 then creates a separate Err(ReconnectionAttemptFailure) that becomes the final return value. This means:

  1. Successful reconnection from handle_reconnection_failure never reaches the caller (lines 209–211 expect Ok(websocket) to update write/read)
  2. The function always returns Err(ReconnectionAttemptFailure) regardless of the match outcome

The match result must be returned directly. Either add return in each match arm, or return the entire match expression:

Proposed fix
-        match try_join3(listener_future, sender_future, callback).await {
+        return match try_join3(listener_future, sender_future, callback).await {
             Ok(_) => {
                 Self::handle_reconnection_failure(connector, credentials, config, loops).await
             }
             Err(e) => {
                 warn!("Error in event loop, {e}, reconnecting...");
                 Self::handle_reconnection_failure(connector, credentials, config, loops).await
             }
-        }
-        Err(BinaryOptionsToolsError::ReconnectionAttemptFailure {
-            number: *loops,
-            max: config.get_max_allowed_loops()?,
-        })
+        };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/core/src/general/client.rs` around lines 269 - 282, The match on
try_join3(listener_future, sender_future, callback).await is evaluated but its
result is discarded, causing the subsequent
Err(BinaryOptionsToolsError::ReconnectionAttemptFailure { ... }) to always be
returned; change the code so the match expression is returned (or add explicit
returns in each arm) so that the results from
Self::handle_reconnection_failure(connector, credentials, config, loops).await
propagate to the caller instead of being ignored, and only return
Err(BinaryOptionsToolsError::ReconnectionAttemptFailure { number: *loops, max:
config.get_max_allowed_loops()? }) when appropriate.
🧹 Nitpick comments (10)
crates/core/src/general/client.rs (2)

291-306: Consider logging the connection error for observability.

The connection error from connector.connect() is discarded in the else branch. Logging the specific failure reason would help with debugging reconnection issues.

♻️ Proposed fix
-        if let Ok(websocket) = connector.connect(credentials.clone(), config).await {
-            return Ok(websocket);
-        } else {
+        match connector.connect(credentials.clone(), config).await {
+            Ok(websocket) => return Ok(websocket),
+            Err(e) => {
+                warn!("Connection attempt failed: {e}");
+            }
+        }
+        {
             *loops += 1;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/core/src/general/client.rs` around lines 291 - 306, The reconnection
branch discards the error returned by connector.connect(); capture that Err
value and include it in the warning log so failures are observable. Update the
if-let to match on connector.connect(...).await as Err(e) (or use match) and log
the error (e) along with the existing message (including variables loops and
max_loops from the surrounding scope), then keep the existing sleep and max-loop
handling; ensure BinaryOptionsToolsError::MaxReconnectAttemptsReached remains
returned when loops >= max_loops.

295-300: Minor: Redundant get_sleep_interval() call.

config.get_sleep_interval()? is called twice—once for logging and once for sleeping. Store it in a single variable.

♻️ Proposed fix
         *loops += 1;
         let sleep_interval = config.get_sleep_interval()?;
         let max_loops = config.get_max_allowed_loops()?;
         warn!(
             "Error reconnecting... trying again in {sleep_interval} seconds (try {loops} of {max_loops})"
         );
-        sleep(Duration::from_secs(config.get_sleep_interval()?)).await;
+        sleep(Duration::from_secs(sleep_interval)).await;
         if *loops >= max_loops {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/core/src/general/client.rs` around lines 295 - 300, The code calls
config.get_sleep_interval()? twice; fetch it once into a local variable (e.g.,
let sleep_interval = config.get_sleep_interval()? already present) and reuse
that variable in the warn! message and in the sleep(Duration::from_secs(...)).
Also reuse the existing max_loops variable from config.get_max_allowed_loops()?;
update the warn! invocation and the sleep(...) call to reference sleep_interval
instead of calling config.get_sleep_interval()? again so the function is only
invoked once.
crates/binary_options_tools/src/pocketoption/pocket_client.rs (1)

168-175: Remove the duplicated rustdoc block above new().

This repeats the constructor documentation already present just above and can drift over time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/binary_options_tools/src/pocketoption/pocket_client.rs` around lines
168 - 175, Remove the duplicated rustdoc block that precedes the async
constructor function PocketClient::new(ssid: impl ToString) ->
PocketResult<Self>; keep only the original/first doc comment for the constructor
and delete the repeated comment immediately above the pub async fn new(...)
declaration so the constructor has a single, canonical docstring.
crates/binary_options_tools/src/pocketoption/modules/keep_alive.rs (1)

94-97: Use an exact auth-frame prefix check here.

Line 94 checks only '4', which can let non-auth Socket.IO frames bypass wrapping. Prefer checking for 42["auth", explicitly before deciding not to wrap.

Proposed patch
-                            if !ssid_str.starts_with('4') {
+                            if !ssid_str.starts_with(r#"42["auth","#) {
                                 debug!(target: "InitModule", "SSID does not start with Socket.IO packet type; wrapping in 42[\"auth\",...]");
                                 ssid_str = format!(r#"42["auth",{}]"#, ssid_str);
                             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/binary_options_tools/src/pocketoption/modules/keep_alive.rs` around
lines 94 - 97, The current check on ssid_str uses starts_with('4') and can miss
non-auth Socket.IO frames; change the logic in the keep-alive handling (where
ssid_str is modified) to test for the exact auth-frame prefix like 42["auth",
(e.g., use starts_with with the full prefix string) and only wrap when that
exact prefix is absent; update the conditional around ssid_str and adjust the
debug message (in the same block that currently logs "SSID does not start with
Socket.IO packet type; wrapping in 42[\"auth\",...]") to reflect the
exact-prefix check and wrapping action.
crates/binary_options_tools/src/pocketoption/state.rs (1)

335-342: Avoid quadratic lookup in update_closed_deals.

Line 341 uses ids.contains(id) with a Vec, which becomes O(opened_deals × closed_batch). Use a HashSet for membership checks.

Proposed patch
 use std::{
-    collections::HashMap,
+    collections::{HashMap, HashSet},
     sync::{Arc, RwLock as SyncRwLock},
     time::Instant,
 };
@@
-        let ids: Vec<_> = deals.iter().map(|deal| deal.id).collect();
+        let ids: HashSet<_> = deals.iter().map(|deal| deal.id).collect();
@@
-            .retain(|id, _| !ids.contains(id));
+            .retain(|id, _| !ids.contains(id));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/binary_options_tools/src/pocketoption/state.rs` around lines 335 -
342, The retain call in update_closed_deals is doing a quadratic membership
check by using ids.contains(id) on a Vec; change to build a HashSet of ids
(e.g., let id_set: HashSet<_> = deals.iter().map(|d| d.id).collect()) and then
call retain(|id, _| !id_set.contains(id)) on self.opened_deals to get O(1)
lookups; import std::collections::HashSet and ensure the deal id type implements
Hash + Eq.
crates/binary_options_tools/src/pocketoption/modules/deals.rs (3)

373-384: Comment is slightly incomplete.

The comment mentions only 451- prefix messages, but based on the MultiPatternRule implementation in the context snippet, it matches both 451- and 42 prefixed messages (any Socket.IO format with [ array notation).

📝 Update comment
     fn rule(_: Arc<State>) -> Box<dyn Rule + Send + Sync> {
         // This rule will match messages like:
-        // 451-["updateOpenedDeals",...]
-        // 451-["updateClosedDeals",...]
-        // 451-["successcloseOrder",...]
+        // 42["updateOpenedDeals",...] or 451-["updateOpenedDeals",...]
+        // 42["updateClosedDeals",...] or 451-["updateClosedDeals",...]
+        // 42["successcloseOrder",...] or 451-["successcloseOrder",...]

         Box::new(MultiPatternRule::new(vec![
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/binary_options_tools/src/pocketoption/modules/deals.rs` around lines
373 - 384, The inline comment in the rule() function is incomplete: update the
comment above the Box::new(MultiPatternRule::new(...)) to indicate this rule
matches both "451-" prefixed messages and Socket.IO "42" prefixed messages (any
array-style payload like [...]) for the patterns EV_UPDATE_CLOSED_DEALS,
EV_UPDATE_OPENED_DEALS, and EV_SUCCESS_CLOSE_ORDER; reference the
MultiPatternRule and the EV_* constants so readers know the rule covers both
transport prefixes and array-formatted payloads.

271-339: Consider extracting common waiter notification logic.

The binary message handling (lines 271-339) duplicates the waiter notification pattern found in process_text_data. The pattern of iterating deals, checking waiting_requests, and sending results appears in multiple places.

If this module grows, extracting a helper method like notify_waiters_for_closed_deals(&mut self, deals: Vec<Deal>) would reduce duplication. Not urgent for this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/binary_options_tools/src/pocketoption/modules/deals.rs` around lines
271 - 339, Extract the duplicated waiter-notification logic into a helper method
named notify_waiters_for_closed_deals(&mut self, deals: Vec<Deal>) that: calls
self.state.trade_state.update_closed_deals(deals.clone()).await, iterates over
each deal, removes waiters via self.waiting_requests.remove(&deal.id), logs
(info) the closed deal, and sends Ok(deal.clone()) to each tx; then replace the
repeated blocks inside Message::Binary arms (ExpectedMessage::UpdateClosedDeals
and ExpectedMessage::SuccessCloseOrder fallback) and the similar code in
process_text_data to call notify_waiters_for_closed_deals(self, deals) instead
of duplicating the loop and send logic.

247-258: Redundant placeholder check in else branch.

At line 240, the condition arr.len() >= 2 filters to 1-step messages. In this else branch, arr.len() < 2, meaning arr.len() == 1 (since we know !arr.is_empty() from line 229).

The has_placeholder check on lines 249-251 iterates arr.iter().skip(1), which yields an empty iterator when arr.len() == 1. Thus, has_placeholder is always false in this branch, making the condition at line 253 equivalent to just arr.len() == 1.

♻️ Simplify the condition
                                                    } else {
-                                                        // Check for binary placeholder in the whole array if it's not 1-step
-                                                        let has_placeholder = arr.iter().skip(1).any(|v| {
-                                                            v.as_object().is_some_and(|obj| obj.contains_key("_placeholder"))
-                                                        });
-                                                        
-                                                        if has_placeholder || arr.len() == 1 {
-                                                            tracing::debug!(target: "DealsApiModule", "Detected binary placeholder, waiting for binary payload for {:?}", current_expected);
+                                                        // Event name only, expect binary payload next
+                                                        if arr.len() == 1 {
+                                                            tracing::debug!(target: "DealsApiModule", "Event header only, waiting for binary payload for {:?}", current_expected);
                                                             expected = current_expected;
                                                             continue;
                                                         }
                                                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/binary_options_tools/src/pocketoption/modules/deals.rs` around lines
247 - 258, The else branch after the arr.len() >= 2 check contains a redundant
has_placeholder computation that always evaluates false because
arr.iter().skip(1) is empty when arr.len() == 1; update the logic in the
DealsApiModule handling (the block referencing arr, has_placeholder, expected
and current_expected) to remove the has_placeholder iteration and simply check
arr.len() == 1 (or otherwise only detect the 1-step case) before setting
expected = current_expected and emitting the same tracing::debug message; keep
the existing behavior for setting expected and continuing, but eliminate the
needless iterator and boolean to simplify the code.
crates/binary_options_tools/src/pocketoption/modules/resilient_parsing_tests.rs (2)

71-109: Same flaky pattern applies here.

This test has the same sleep-based synchronization and untracked spawned task issues as the balance test. Apply the same fix pattern for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@crates/binary_options_tools/src/pocketoption/modules/resilient_parsing_tests.rs`
around lines 71 - 109, The test unsafely relies on tokio::time::sleep and an
untracked tokio::spawn for module.run(), making it flaky; change the pattern to
track and await the spawned task or signal completion explicitly instead of
sleeping: capture the JoinHandle from tokio::spawn(async move { let _ =
module.run().await; }) (or have module.run() return when idle), then after
sending msg_tx.send(...).await.unwrap() await that JoinHandle (or use a
oneshot/Notify that module.run() triggers after processing) before reading
state.trade_state.opened_deals.read().await and asserting the UUID; remove the
sleep and untracked spawn so the test deterministically waits for processing of
msg_deal_42.

34-46: Sleep-based synchronization is flaky and spawned task errors are silent.

The tests use tokio::time::sleep(Duration::from_millis(100)) for synchronization, which can lead to intermittent failures under load. Additionally, the spawned task at lines 34-36 is not tracked—if module.run() panics, the test won't detect it.

Consider using a more deterministic synchronization approach and tracking the spawned task:

♻️ Suggested improvement using JoinHandle
-        // Run the module in background
-        tokio::spawn(async move {
-            let _ = module.run().await;
-        });
+        // Run the module in background with handle for error propagation
+        let module_handle = tokio::spawn(async move {
+            module.run().await
+        });

         // Send 451 message
         msg_tx.send(Arc::new(msg_451)).await.unwrap();
         tokio::time::sleep(std::time::Duration::from_millis(100)).await;
         assert_eq!(*state.balance.read().await, Some(dec!(123.45)));

         // Send 42 message
         msg_tx.send(Arc::new(msg_42)).await.unwrap();
         tokio::time::sleep(std::time::Duration::from_millis(100)).await;
         assert_eq!(*state.balance.read().await, Some(dec!(678.90)));
+
+        // Cleanup: drop sender to allow module to exit, then check for panics
+        drop(msg_tx);
+        module_handle.await.expect("Module task panicked").expect("Module run failed");

For more robust synchronization, consider polling state changes with a timeout instead of fixed sleeps.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@crates/binary_options_tools/src/pocketoption/modules/resilient_parsing_tests.rs`
around lines 34 - 46, The test currently uses tokio::spawn without tracking the
JoinHandle and uses fixed tokio::time::sleep for synchronization which is flaky;
change the spawn to capture the JoinHandle returned by tokio::spawn for the task
running module.run() and, after the test actions, await that handle (and
propagate any panic/error) so task failures are visible; replace the fixed
tokio::time::sleep calls after msg_tx.send(Arc::new(msg_451)) and
msg_tx.send(Arc::new(msg_42)) with a deterministic wait that polls state.balance
(using state.balance.read().await in a loop with tokio::time::timeout) until it
equals the expected dec!(123.45) and dec!(678.90) respectively, failing the test
on timeout.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/binary_options_tools/src/pocketoption/ssid.rs`:
- Around line 149-152: The double-encoding check currently only inspects the raw
input via trimmed, missing cases where JSON parsing yields a string (e.g.
42["auth","{...}"]) — update the logic to also inspect parsed when it is a JSON
string: if parsed.is_string(), get the inner string (parsed.as_str()) and run
the same quoted detection (starts_with('"') && ends_with('"') or
starts_with('\'')) and return CoreError::SsidParsing("Invalid SSID format:
double-encoding detected") when true; apply this same additional check to the
other validation block that mirrors lines 155-164 so both paths detect
wrapped-string double-encoding.

---

Outside diff comments:
In `@crates/core/src/general/client.rs`:
- Around line 269-282: The match on try_join3(listener_future, sender_future,
callback).await is evaluated but its result is discarded, causing the subsequent
Err(BinaryOptionsToolsError::ReconnectionAttemptFailure { ... }) to always be
returned; change the code so the match expression is returned (or add explicit
returns in each arm) so that the results from
Self::handle_reconnection_failure(connector, credentials, config, loops).await
propagate to the caller instead of being ignored, and only return
Err(BinaryOptionsToolsError::ReconnectionAttemptFailure { number: *loops, max:
config.get_max_allowed_loops()? }) when appropriate.

---

Nitpick comments:
In `@crates/binary_options_tools/src/pocketoption/modules/deals.rs`:
- Around line 373-384: The inline comment in the rule() function is incomplete:
update the comment above the Box::new(MultiPatternRule::new(...)) to indicate
this rule matches both "451-" prefixed messages and Socket.IO "42" prefixed
messages (any array-style payload like [...]) for the patterns
EV_UPDATE_CLOSED_DEALS, EV_UPDATE_OPENED_DEALS, and EV_SUCCESS_CLOSE_ORDER;
reference the MultiPatternRule and the EV_* constants so readers know the rule
covers both transport prefixes and array-formatted payloads.
- Around line 271-339: Extract the duplicated waiter-notification logic into a
helper method named notify_waiters_for_closed_deals(&mut self, deals: Vec<Deal>)
that: calls self.state.trade_state.update_closed_deals(deals.clone()).await,
iterates over each deal, removes waiters via
self.waiting_requests.remove(&deal.id), logs (info) the closed deal, and sends
Ok(deal.clone()) to each tx; then replace the repeated blocks inside
Message::Binary arms (ExpectedMessage::UpdateClosedDeals and
ExpectedMessage::SuccessCloseOrder fallback) and the similar code in
process_text_data to call notify_waiters_for_closed_deals(self, deals) instead
of duplicating the loop and send logic.
- Around line 247-258: The else branch after the arr.len() >= 2 check contains a
redundant has_placeholder computation that always evaluates false because
arr.iter().skip(1) is empty when arr.len() == 1; update the logic in the
DealsApiModule handling (the block referencing arr, has_placeholder, expected
and current_expected) to remove the has_placeholder iteration and simply check
arr.len() == 1 (or otherwise only detect the 1-step case) before setting
expected = current_expected and emitting the same tracing::debug message; keep
the existing behavior for setting expected and continuing, but eliminate the
needless iterator and boolean to simplify the code.

In `@crates/binary_options_tools/src/pocketoption/modules/keep_alive.rs`:
- Around line 94-97: The current check on ssid_str uses starts_with('4') and can
miss non-auth Socket.IO frames; change the logic in the keep-alive handling
(where ssid_str is modified) to test for the exact auth-frame prefix like
42["auth", (e.g., use starts_with with the full prefix string) and only wrap
when that exact prefix is absent; update the conditional around ssid_str and
adjust the debug message (in the same block that currently logs "SSID does not
start with Socket.IO packet type; wrapping in 42[\"auth\",...]") to reflect the
exact-prefix check and wrapping action.

In
`@crates/binary_options_tools/src/pocketoption/modules/resilient_parsing_tests.rs`:
- Around line 71-109: The test unsafely relies on tokio::time::sleep and an
untracked tokio::spawn for module.run(), making it flaky; change the pattern to
track and await the spawned task or signal completion explicitly instead of
sleeping: capture the JoinHandle from tokio::spawn(async move { let _ =
module.run().await; }) (or have module.run() return when idle), then after
sending msg_tx.send(...).await.unwrap() await that JoinHandle (or use a
oneshot/Notify that module.run() triggers after processing) before reading
state.trade_state.opened_deals.read().await and asserting the UUID; remove the
sleep and untracked spawn so the test deterministically waits for processing of
msg_deal_42.
- Around line 34-46: The test currently uses tokio::spawn without tracking the
JoinHandle and uses fixed tokio::time::sleep for synchronization which is flaky;
change the spawn to capture the JoinHandle returned by tokio::spawn for the task
running module.run() and, after the test actions, await that handle (and
propagate any panic/error) so task failures are visible; replace the fixed
tokio::time::sleep calls after msg_tx.send(Arc::new(msg_451)) and
msg_tx.send(Arc::new(msg_42)) with a deterministic wait that polls state.balance
(using state.balance.read().await in a loop with tokio::time::timeout) until it
equals the expected dec!(123.45) and dec!(678.90) respectively, failing the test
on timeout.

In `@crates/binary_options_tools/src/pocketoption/pocket_client.rs`:
- Around line 168-175: Remove the duplicated rustdoc block that precedes the
async constructor function PocketClient::new(ssid: impl ToString) ->
PocketResult<Self>; keep only the original/first doc comment for the constructor
and delete the repeated comment immediately above the pub async fn new(...)
declaration so the constructor has a single, canonical docstring.

In `@crates/binary_options_tools/src/pocketoption/state.rs`:
- Around line 335-342: The retain call in update_closed_deals is doing a
quadratic membership check by using ids.contains(id) on a Vec; change to build a
HashSet of ids (e.g., let id_set: HashSet<_> = deals.iter().map(|d|
d.id).collect()) and then call retain(|id, _| !id_set.contains(id)) on
self.opened_deals to get O(1) lookups; import std::collections::HashSet and
ensure the deal id type implements Hash + Eq.

In `@crates/core/src/general/client.rs`:
- Around line 291-306: The reconnection branch discards the error returned by
connector.connect(); capture that Err value and include it in the warning log so
failures are observable. Update the if-let to match on
connector.connect(...).await as Err(e) (or use match) and log the error (e)
along with the existing message (including variables loops and max_loops from
the surrounding scope), then keep the existing sleep and max-loop handling;
ensure BinaryOptionsToolsError::MaxReconnectAttemptsReached remains returned
when loops >= max_loops.
- Around line 295-300: The code calls config.get_sleep_interval()? twice; fetch
it once into a local variable (e.g., let sleep_interval =
config.get_sleep_interval()? already present) and reuse that variable in the
warn! message and in the sleep(Duration::from_secs(...)). Also reuse the
existing max_loops variable from config.get_max_allowed_loops()?; update the
warn! invocation and the sleep(...) call to reference sleep_interval instead of
calling config.get_sleep_interval()? again so the function is only invoked once.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d8e8cdc3-4fa7-4351-858d-26495f267951

📥 Commits

Reviewing files that changed from the base of the PR and between 94309a5 and eb13062.

📒 Files selected for processing (13)
  • .serena/project.yml
  • README.md
  • crates/binary_options_tools/src/pocketoption/modules/balance.rs
  • crates/binary_options_tools/src/pocketoption/modules/deals.rs
  • crates/binary_options_tools/src/pocketoption/modules/keep_alive.rs
  • crates/binary_options_tools/src/pocketoption/modules/mod.rs
  • crates/binary_options_tools/src/pocketoption/modules/pending_trades.rs
  • crates/binary_options_tools/src/pocketoption/modules/resilient_parsing_tests.rs
  • crates/binary_options_tools/src/pocketoption/pocket_client.rs
  • crates/binary_options_tools/src/pocketoption/ssid.rs
  • crates/binary_options_tools/src/pocketoption/state.rs
  • crates/binary_options_tools/src/pocketoption/utils.rs
  • crates/core/src/general/client.rs

Comment on lines +149 to 152
// Security: Direct validation to prevent double-JSON injection
if (trimmed.starts_with('"') && trimmed.ends_with('"')) || trimmed.starts_with("'") {
return Err(CoreError::SsidParsing("Invalid SSID format: double-encoding detected".into()));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Double-encoding detection misses the wrapped-string case.

Line 150 validates only the full input. For payloads like 42["auth","{...}"], parsed is still a quoted JSON string, so this path returns a generic JSON parse error instead of the intended "double-encoding detected" error (and likely fails the assertion on Line 386).

Proposed patch
         let parsed = if let Some(stripped) = trimmed.strip_prefix(prefix) {
             stripped.strip_suffix("]").ok_or_else(|| {
                 CoreError::SsidParsing("Error parsing ssid: missing closing bracket".into())
             })?
         } else {
             trimmed
         };
+
+        // Security: also reject double-encoded payload after removing Socket.IO wrapper
+        if (parsed.starts_with('"') && parsed.ends_with('"')) || parsed.starts_with('\'') {
+            return Err(CoreError::SsidParsing(
+                "Invalid SSID format: double-encoding detected".into(),
+            ));
+        }
 
         let mut ssid: Demo = serde_json::from_str(parsed)
             .map_err(|e| CoreError::SsidParsing(format!("JSON parsing error: {e}")))?;

Also applies to: 155-164

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/binary_options_tools/src/pocketoption/ssid.rs` around lines 149 - 152,
The double-encoding check currently only inspects the raw input via trimmed,
missing cases where JSON parsing yields a string (e.g. 42["auth","{...}"]) —
update the logic to also inspect parsed when it is a JSON string: if
parsed.is_string(), get the inner string (parsed.as_str()) and run the same
quoted detection (starts_with('"') && ends_with('"') or starts_with('\'')) and
return CoreError::SsidParsing("Invalid SSID format: double-encoding detected")
when true; apply this same additional check to the other validation block that
mirrors lines 155-164 so both paths detect wrapped-string double-encoding.

@theshadow76 theshadow76 merged commit 22df38d into ChipaDevTeam:master Mar 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants