Skip to content

Add local database command with event fetching functionality#2102

Merged
findolor merged 24 commits intomainfrom
local-db-cli
Oct 4, 2025
Merged

Add local database command with event fetching functionality#2102
findolor merged 24 commits intomainfrom
local-db-cli

Conversation

@findolor
Copy link
Copy Markdown
Collaborator

@findolor findolor commented Aug 28, 2025

Caution

Chained PR - Do not merge before #2101

Motivation

This change introduces local database operations to the CLI, specifically focusing on
blockchain event fetching capabilities.

Solution

Added a new local-db command module with the following components:

  • New CLI command structure: Added local-db fetch-events subcommand that allows users
    to fetch events from blockchain networks and save them to JSON files
  • Dependency injection pattern: Implemented an EventClient trait to enable testable
    code with mock clients for unit testing
  • Hyper RPC client enhancements: Extended the HyperRpcClient to support Arbitrum (chain
    ID 42161) in addition to Base, and exposed the client through SqliteWeb for dependency
    injection
  • Comprehensive testing: Added extensive unit tests covering success cases, error
    handling, and edge cases using mock implementations
  • File management: Added appropriate .gitignore entries to exclude generated event
    output files from version control

Checks

By submitting this for review, I'm confirming I've done the following:

  • made this PR as small as possible
  • unit-tested any new functionality
  • linked any relevant issues or PRs
  • included screenshots (if this involves a front-end change)

Summary by CodeRabbit

  • New Features

    • Added a local-db CLI group with fetch-events and decode-events subcommands.
    • fetch-events: fetch blockchain events for an address over a block range (end defaults to latest) and save pretty JSON (custom path or via RAIN_ORDERBOOK_EVENTS_DIR).
    • decode-events: decode events from a JSON file and write decoded output (defaults when not provided).
    • RPC connections now support Arbitrum (chain ID 42161).
  • Chores

    • Ignore generated event JSON files.
    • Adjusted CLI dependency declarations for builds.

@findolor findolor self-assigned this Aug 28, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 28, 2025

Walkthrough

Adds a LocalDb CLI group with FetchEvents and DecodeEvents commands, an EventClient trait plus SqliteWeb adapter, moves/updates CLI dependencies and .gitignore, and adjusts HyperRpcClient URL selection and a SqliteWeb accessor.

Changes

Cohort / File(s) Summary
CLI Local DB commands
crates/cli/src/commands/local_db/fetch_events.rs, crates/cli/src/commands/local_db/decode_events.rs, crates/cli/src/commands/local_db/mod.rs
Adds FetchEvents and DecodeEvents CLI commands, introduces EventClient trait and SqliteWeb impl usage, implements I/O, pretty JSON serialization, default output-path logic, error mapping, and unit tests with a mock client.
CLI integration & entry
crates/cli/src/commands/mod.rs, crates/cli/src/lib.rs
Adds local_db module, exposes LocalDb enum (variants FetchEvents/DecodeEvents) and wires it into the top-level Orderbook::execute.
CLI crate config & ignore
crates/cli/Cargo.toml, crates/cli/.gitignore
Adds async-trait and moves serde_json to regular dependencies (removed from dev-deps); adds .gitignore entry to ignore src/commands/local_db/events_*.json.
Common RPC & client API
crates/common/src/hyper_rpc.rs, crates/common/src/raindex_client/sqlite_web/mod.rs
HyperRpcClient::new now selects a base URL per chain_id (adds Arbitrum/42161) and appends the token path segment; SqliteWeb exposes pub fn hyper_rpc_client(&self) -> &HyperRpcClient.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant CLI as CLI (Orderbook)
  participant LDB as LocalDb::FetchEvents
  participant SW as SqliteWeb (EventClient)
  participant RPC as HyperRpcClient
  participant FS as FileSystem

  U->>CLI: orderbook local-db fetch-events [args]
  CLI->>LDB: execute()
  alt end_block provided
    Note over LDB: use provided end_block
  else end_block omitted
    LDB->>SW: get_latest_block_number()
    SW->>RPC: latestBlock()
    RPC-->>SW: latest_block
    SW-->>LDB: latest_block
  end
  LDB->>SW: fetch(address, start_block, end_block)
  SW->>RPC: getLogs(address, range)
  RPC-->>SW: events[]
  SW-->>LDB: events[]
  LDB->>LDB: serialize to pretty JSON
  LDB->>FS: write output file (explicit or default)
  FS-->>LDB: ok
  LDB-->>CLI: Result<()>
  CLI-->>U: Done
  rect rgba(200,230,255,0.12)
    Note over LDB,SW: New EventClient abstraction and SqliteWeb usage
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • hardyjosh
  • 0xgleb

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly and concisely summarizes the main change, indicating the addition of a new local database CLI command with event fetching functionality, which aligns directly with the pull request’s primary purpose without extraneous details.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch local-db-cli

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

@findolor findolor requested a review from 0xgleb September 26, 2025 12:19
Base automatically changed from local-db-insert-commands to main October 2, 2025 10:16
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: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1348347 and 029e733.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • crates/cli/.gitignore (1 hunks)
  • crates/cli/Cargo.toml (2 hunks)
  • crates/cli/src/commands/local_db/fetch_events.rs (1 hunks)
  • crates/cli/src/commands/local_db/mod.rs (1 hunks)
  • crates/cli/src/commands/mod.rs (1 hunks)
  • crates/cli/src/lib.rs (3 hunks)
  • crates/common/src/hyper_rpc.rs (1 hunks)
  • crates/common/src/raindex_client/sqlite_web/mod.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
crates/**/{src,tests,benches,examples}/**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

crates/**/{src,tests,benches,examples}/**/*.rs: Rust crates: lint with cargo clippy --workspace --all-targets --all-features -D warnings
Rust crates: ensure cargo test --workspace passes
Rust crates: build with cargo build during development/readiness checks

Files:

  • crates/cli/src/lib.rs
  • crates/cli/src/commands/mod.rs
  • crates/common/src/hyper_rpc.rs
  • crates/cli/src/commands/local_db/mod.rs
  • crates/common/src/raindex_client/sqlite_web/mod.rs
  • crates/cli/src/commands/local_db/fetch_events.rs
🧬 Code graph analysis (2)
crates/common/src/hyper_rpc.rs (1)
crates/common/src/raindex_client/sqlite_web/mod.rs (1)
  • new (64-67)
crates/cli/src/commands/local_db/mod.rs (1)
crates/cli/src/commands/local_db/fetch_events.rs (3)
  • fetch_events (12-17)
  • fetch_events (26-33)
  • fetch_events (185-198)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: git-clean
  • GitHub Check: test
  • GitHub Check: test
  • GitHub Check: copilot-setup-steps

Comment thread crates/cli/src/commands/local_db/fetch_events.rs Outdated
Comment on lines +71 to +77
let output_filename = self
.output_file
.map(PathBuf::from)
.unwrap_or_else(|| Self::default_output_path(end_block));
let mut file = File::create(&output_filename)?;
file.write_all(serde_json::to_string_pretty(&all_events)?.as_bytes())?;

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Oct 2, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Ensure the output directory exists before writing files.

The default path (src/commands/local_db/events_*.json) is unlikely to exist for end users, and even custom paths fail if their parent directories are missing. As written, File::create will error with No such file or directory, breaking the command’s default flow. Create the parent directory before opening the file (or pick a default path that already exists).

         let output_filename = self
             .output_file
             .map(PathBuf::from)
             .unwrap_or_else(|| Self::default_output_path(end_block));
+        if let Some(parent) = output_filename.parent() {
+            if !parent.is_dir() {
+                std::fs::create_dir_all(parent)?;
+            }
+        }
         let mut file = File::create(&output_filename)?;

Also applies to: 87-93

🤖 Prompt for AI Agents
In crates/cli/src/commands/local_db/fetch_events.rs around lines 71-77 (and also
apply the same change for lines 87-93), the code calls File::create on an output
path whose parent directory may not exist; this will fail with "No such file or
directory." Before creating/writing the file, ensure the parent directory exists
by taking the output path (or default_output_path), get its parent directory,
and if Some(parent) call std::fs::create_dir_all(parent) propagating errors if
creation fails; then proceed to create and write the file as before.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

it's fine because either way we should be returning an error to the user. it's best to let the error come from io operations at this point

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.

@findolor, understood. The I/O error message will clearly indicate the issue to users. I'll skip this suggestion.

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment thread crates/common/src/hyper_rpc.rs Outdated
42161 => "https://arbitrum.rpc.hypersync.xyz".to_string(),
_ => return Err(HyperRpcError::UnsupportedChainId { chain_id }),
};
let rpc_url = format!("{}/{}", base_url, api_token);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
let rpc_url = format!("{}/{}", base_url, api_token);
let rpc_url = format!("{base_url}/{api_token}");

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: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45b9bfd and 20a684a.

📒 Files selected for processing (2)
  • crates/cli/src/commands/local_db/fetch_events.rs (1 hunks)
  • crates/common/src/hyper_rpc.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
crates/**/{src,tests,benches,examples}/**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

crates/**/{src,tests,benches,examples}/**/*.rs: Rust crates: lint with cargo clippy --workspace --all-targets --all-features -D warnings
Rust crates: ensure cargo test --workspace passes
Rust crates: build with cargo build during development/readiness checks

Files:

  • crates/common/src/hyper_rpc.rs
  • crates/cli/src/commands/local_db/fetch_events.rs
🧬 Code graph analysis (2)
crates/common/src/hyper_rpc.rs (1)
crates/common/src/raindex_client/sqlite_web/mod.rs (1)
  • new (64-67)
crates/cli/src/commands/local_db/fetch_events.rs (1)
crates/common/src/hyper_rpc.rs (3)
  • get_latest_block_number (100-129)
  • from (241-243)
  • new (80-94)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: Deploy-Docs-Preview
🔇 Additional comments (8)
crates/cli/src/commands/local_db/fetch_events.rs (7)

1-8: LGTM!

All imports are relevant and necessary for the file's functionality. The use of anyhow::Result for application-level error handling aligns with the learnings about anyhow usage.


9-18: LGTM!

The EventClient trait provides a clean abstraction for dependency injection, enabling unit testing with mock implementations. Method signatures and error types are appropriate.


20-34: LGTM!

The trait implementation correctly delegates to the underlying SqliteWeb methods. The self.fetch_events(...) call at line 32 resolves to the inherent method implementation (not the trait method), avoiding recursion. Based on learnings.


36-51: LGTM!

The FetchEvents struct is well-designed for CLI usage with appropriate optional fields. Runtime validation in the execute methods is acceptable, as HyperRpcClient::new will fail for unsupported chain IDs.


54-80: LGTM!

The execute_with_client method correctly handles end block determination, event fetching, and file writing. Error messages provide appropriate context, and the generic design enables testability.


82-85: LGTM!

The execute method correctly instantiates SqliteWeb and delegates to the generic implementation. Error propagation is appropriate.


97-199: LGTM!

The test helpers are well-designed:

  • sample_event provides consistent test data
  • EnvVarGuard ensures proper cleanup and prevents test pollution
  • MockEventClient uses a builder pattern for flexible test scenarios
crates/common/src/hyper_rpc.rs (1)

89-94: LGTM!

The constructor return statement is correct, and the struct fields are properly initialized.

Comment on lines +87 to +94
fn default_output_path(end_block: u64) -> PathBuf {
let filename = format!("events_{}.json", end_block);
if let Ok(dir) = std::env::var("RAIN_ORDERBOOK_EVENTS_DIR") {
Path::new(&dir).join(filename)
} else {
Path::new("src/commands/local_db").join(filename)
}
}
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.

🧹 Nitpick | 🔵 Trivial

Reconsider the fallback output path.

The default path src/commands/local_db/events_*.json is relative to the current working directory, not the crate root. This means:

  • Running the CLI from different directories creates src/commands/local_db/ in the user's working directory
  • The path mimics source code structure, which is confusing for end users
  • Output ends up in unexpected locations unless RAIN_ORDERBOOK_EVENTS_DIR is set

Consider defaulting to the current directory for clarity:

 fn default_output_path(end_block: u64) -> PathBuf {
     let filename = format!("events_{}.json", end_block);
     if let Ok(dir) = std::env::var("RAIN_ORDERBOOK_EVENTS_DIR") {
         Path::new(&dir).join(filename)
     } else {
-        Path::new("src/commands/local_db").join(filename)
+        PathBuf::from(filename)
     }
 }

Or document that users should set RAIN_ORDERBOOK_EVENTS_DIR and error if not set.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn default_output_path(end_block: u64) -> PathBuf {
let filename = format!("events_{}.json", end_block);
if let Ok(dir) = std::env::var("RAIN_ORDERBOOK_EVENTS_DIR") {
Path::new(&dir).join(filename)
} else {
Path::new("src/commands/local_db").join(filename)
}
}
fn default_output_path(end_block: u64) -> PathBuf {
let filename = format!("events_{}.json", end_block);
if let Ok(dir) = std::env::var("RAIN_ORDERBOOK_EVENTS_DIR") {
Path::new(&dir).join(filename)
} else {
PathBuf::from(filename)
}
}
🤖 Prompt for AI Agents
In crates/cli/src/commands/local_db/fetch_events.rs around lines 87-94, the
fallback output path currently uses "src/commands/local_db" which creates
source-like directories in whatever CWD the user runs the CLI from; change the
fallback to the current working directory instead (use std::env::current_dir()
or Path::new(".") and join the filename) so outputs land in the user's CWD, and
optionally add a short comment or doc note explaining that
RAIN_ORDERBOOK_EVENTS_DIR can be set to override the default or decide to return
an error if the env var is required.

Comment on lines +81 to 87
let base_url = match chain_id {
8453 => "https://base.rpc.hypersync.xyz".to_string(),
42161 => "https://arbitrum.rpc.hypersync.xyz".to_string(),
_ => return Err(HyperRpcError::UnsupportedChainId { chain_id }),
};
let rpc_url = format!("{base_url}/{api_token}");
let provider = Arc::new(Self::build_provider(&rpc_url)?);
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.

🛠️ Refactor suggestion | 🟠 Major

❓ Verification inconclusive

LGTM! Arbitrum support added correctly.

The URL construction now supports both Base (8453) and Arbitrum (42161) with consistent base URL patterns. The format string correctly appends the API token as a path segment. As per past review comment.

Verify that test coverage includes the new Arbitrum chain ID:


🏁 Script executed:

#!/bin/bash
# Description: Check if tests cover chain_id 42161 (Arbitrum)
# Expected: Find test cases that construct HyperRpcClient with chain_id 42161

rg -n "HyperRpcClient::new\(42161" crates/common/src/hyper_rpc.rs

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search entire repository for HyperRpcClient::new with 42161 (Arbitrum) to find tests
rg -nP 'HyperRpcClient::new\s*\(\s*42161' -C3

Length of output: 0


Add test for Arbitrum chain_id (42161)

  • Test coverage currently lacks a case for chain_id = 42161. Add a unit test invoking HyperRpcClient::new(42161, api_token) and asserting the constructed URL matches https://arbitrum.rpc.hypersync.xyz/{api_token}.
🤖 Prompt for AI Agents
In crates/common/src/hyper_rpc.rs around lines 81 to 87, add a unit test that
covers chain_id = 42161 by calling HyperRpcClient::new(42161, api_token) and
asserting the constructed RPC URL equals
"https://arbitrum.rpc.hypersync.xyz/{api_token}"; if the constructed URL is not
accessible from the test, add a small test-only getter or make the URL-building
function pub(crate) so the test can verify the exact rpc_url string, then add
the test in the file's #[cfg(test)] module to assert equality with the expected
URL.

@findolor findolor requested a review from 0xgleb October 3, 2025 17:27
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: 2

♻️ Duplicate comments (2)
crates/cli/src/commands/local_db/fetch_events.rs (2)

87-93: Reconsider the fallback output path for better user experience.

The default path src/commands/local_db/events_*.json is relative to the current working directory, not the crate root. This means:

  • Running the CLI from different directories creates src/commands/local_db/ in the user's working directory
  • The path mimics source code structure, which is confusing for end users
  • Output ends up in unexpected locations unless RAIN_ORDERBOOK_EVENTS_DIR is set

This concern was previously raised but remains unresolved. Consider one of these alternatives:

  1. Default to the current directory (.)
  2. Require RAIN_ORDERBOOK_EVENTS_DIR and error if not set
  3. Use a well-known directory like ~/.rain_orderbook/events/

Option 1 (simplest): Default to current directory:

 fn default_output_path(end_block: u64) -> PathBuf {
     let filename = format!("events_{}.json", end_block);
     if let Ok(dir) = std::env::var("RAIN_ORDERBOOK_EVENTS_DIR") {
         Path::new(&dir).join(filename)
     } else {
-        Path::new("src/commands/local_db").join(filename)
+        PathBuf::from(filename)
     }
 }

Option 2: Require the environment variable:

 fn default_output_path(end_block: u64) -> PathBuf {
     let filename = format!("events_{}.json", end_block);
-    if let Ok(dir) = std::env::var("RAIN_ORDERBOOK_EVENTS_DIR") {
-        Path::new(&dir).join(filename)
-    } else {
-        Path::new("src/commands/local_db").join(filename)
-    }
+    let dir = std::env::var("RAIN_ORDERBOOK_EVENTS_DIR")
+        .expect("RAIN_ORDERBOOK_EVENTS_DIR environment variable must be set");
+    Path::new(&dir).join(filename)
 }

12-17: Clarify the fetch method name to avoid ambiguity.

The trait method fetch is generic and could be confused with other fetch operations in the codebase. Past discussion (lines 87-94 comment thread) raised concerns about naming confusion. Consider renaming to fetch_events to match the underlying implementation and improve clarity.

Apply this diff to rename the method:

-    async fn fetch(
+    async fn fetch_events(
         &self,
         address: &str,
         start_block: u64,
         end_block: u64,
     ) -> Result<Vec<LogEntryResponse>, SqliteWebError>;

And update the call site at line 67:

-        let all_events = client
-            .fetch(&self.orderbook_address, self.start_block, end_block)
+        let all_events = client
+            .fetch_events(&self.orderbook_address, self.start_block, end_block)
             .await
             .map_err(|e| anyhow::anyhow!("Failed to fetch events: {}", e))?;

And update the mock implementation at lines 185-190:

-        async fn fetch(
+        async fn fetch_events(
             &self,
             _address: &str,
             _start_block: u64,
             _end_block: u64,
         ) -> Result<Vec<LogEntryResponse>, SqliteWebError> {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 20a684a and d04e241.

📒 Files selected for processing (1)
  • crates/cli/src/commands/local_db/fetch_events.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
crates/**/{src,tests,benches,examples}/**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

crates/**/{src,tests,benches,examples}/**/*.rs: Rust crates: lint with cargo clippy --workspace --all-targets --all-features -D warnings
Rust crates: ensure cargo test --workspace passes
Rust crates: build with cargo build during development/readiness checks

Files:

  • crates/cli/src/commands/local_db/fetch_events.rs
🧬 Code graph analysis (1)
crates/cli/src/commands/local_db/fetch_events.rs (2)
crates/common/src/hyper_rpc.rs (3)
  • get_latest_block_number (100-129)
  • from (241-243)
  • new (80-94)
crates/common/src/raindex_client/sqlite_web/fetch.rs (1)
  • fetch_events (37-50)
🔇 Additional comments (4)
crates/cli/src/commands/local_db/fetch_events.rs (4)

20-34: LGTM! EventClient implementation delegates correctly.

The trait implementation properly delegates to the underlying SqliteWeb methods without introducing recursion, as confirmed by previous review learnings.


36-51: LGTM! FetchEvents struct is well-designed for CLI usage.

The struct appropriately uses optional fields for end_block and output_file, enabling flexible command invocation. Runtime validation of block ranges occurs in execute_with_client.


54-80: LGTM! Core execution logic is well-structured.

The method correctly:

  • Resolves end_block from the latest block when not provided
  • Fetches events with appropriate error context
  • Writes formatted JSON output

Error handling properly wraps underlying errors with user-friendly messages.


97-320: Excellent test coverage with well-structured mocks.

The test suite thoroughly covers:

  • ✅ Success cases with explicit and default end blocks
  • ✅ Error handling for RPC failures
  • ✅ Error handling for event fetching failures
  • ✅ Environment variable integration
  • ✅ Default output path behavior

The MockEventClient and EnvVarGuard are well-designed test utilities that enable clear, isolated testing.

Comment on lines +54 to +64
pub async fn execute_with_client<C: EventClient>(self, client: C) -> Result<()> {
println!("Starting event fetch...");

let end_block = if let Some(end_block) = self.end_block {
end_block
} else {
client
.get_latest_block_number()
.await
.map_err(|e| anyhow::anyhow!("Failed to get latest block number: {}", e))?
};
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.

🧹 Nitpick | 🔵 Trivial

Consider validating block range ordering.

When both start_block and end_block are explicitly provided, there's no validation that start_block <= end_block. This could lead to unexpected behavior or confusing error messages from the underlying client.

Add validation before fetching events:

         let end_block = if let Some(end_block) = self.end_block {
+            if self.start_block > end_block {
+                return Err(anyhow::anyhow!(
+                    "start_block ({}) must be less than or equal to end_block ({})",
+                    self.start_block,
+                    end_block
+                ));
+            }
             end_block
         } else {
             client
                 .get_latest_block_number()
                 .await
                 .map_err(|e| anyhow::anyhow!("Failed to get latest block number: {}", e))?
         };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub async fn execute_with_client<C: EventClient>(self, client: C) -> Result<()> {
println!("Starting event fetch...");
let end_block = if let Some(end_block) = self.end_block {
end_block
} else {
client
.get_latest_block_number()
.await
.map_err(|e| anyhow::anyhow!("Failed to get latest block number: {}", e))?
};
pub async fn execute_with_client<C: EventClient>(self, client: C) -> Result<()> {
println!("Starting event fetch...");
let end_block = if let Some(end_block) = self.end_block {
// Validate that the explicitly provided range is in the correct order
if self.start_block > end_block {
return Err(anyhow::anyhow!(
"start_block ({}) must be less than or equal to end_block ({})",
self.start_block,
end_block
));
}
end_block
} else {
client
.get_latest_block_number()
.await
.map_err(|e| anyhow::anyhow!("Failed to get latest block number: {}", e))?
};
🤖 Prompt for AI Agents
In crates/cli/src/commands/local_db/fetch_events.rs around lines 54 to 64, add
validation that when both self.start_block and end_block are provided the start
is <= end; after resolving end_block (but before calling client to fetch events)
check if let Some(start) = self.start_block and if start > end_block then return
an Err(anyhow::anyhow!("Invalid block range: start_block ({}) is greater than
end_block ({})", start, end_block)); ensure the check uses the same numeric
types as the block values and short-circuits execution before any client calls.

Comment on lines +201 to +319
#[tokio::test]
async fn test_execute_with_client_success_with_explicit_end_block() {
let temp_file = NamedTempFile::new().unwrap();
let temp_path = temp_file.path().to_str().unwrap().to_string();

let fetch_events = FetchEvents {
api_token: "test_token".to_string(),
chain_id: 1,
start_block: 100,
end_block: Some(200),
orderbook_address: "0x123".to_string(),
output_file: Some(temp_path.clone()),
};

let mock_client = MockEventClient::new().with_events(vec![sample_event("0x64")]);

let result = fetch_events.execute_with_client(mock_client).await;
assert!(result.is_ok());

let content = std::fs::read_to_string(&temp_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(parsed[0]["blockNumber"], "0x64");
}

#[tokio::test]
async fn test_execute_with_client_success_with_latest_block() {
let temp_file = NamedTempFile::new().unwrap();
let temp_path = temp_file.path().to_str().unwrap().to_string();

let fetch_events = FetchEvents {
api_token: "test_token".to_string(),
chain_id: 1,
start_block: 100,
end_block: None,
orderbook_address: "0x123".to_string(),
output_file: Some(temp_path.clone()),
};

let mock_client = MockEventClient::new()
.with_latest_block(500)
.with_events(vec![sample_event("0x1f4")]);

let result = fetch_events.execute_with_client(mock_client).await;
assert!(result.is_ok());

let content = std::fs::read_to_string(&temp_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(parsed[0]["blockNumber"], "0x1f4");
}

#[tokio::test]
async fn test_execute_with_client_latest_block_error() {
let fetch_events = FetchEvents {
api_token: "test_token".to_string(),
chain_id: 1,
start_block: 100,
end_block: None,
orderbook_address: "0x123".to_string(),
output_file: Some("test_output.json".to_string()),
};

let mock_client =
MockEventClient::new().with_latest_block_error("RPC connection failed".to_string());

let result = fetch_events.execute_with_client(mock_client).await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Failed to get latest block number"));
}

#[tokio::test]
async fn test_execute_with_client_fetch_events_error() {
let fetch_events = FetchEvents {
api_token: "test_token".to_string(),
chain_id: 1,
start_block: 100,
end_block: Some(200),
orderbook_address: "0x123".to_string(),
output_file: Some("test_output.json".to_string()),
};

let mock_client =
MockEventClient::new().with_events_error("Network connection failed".to_string());

let result = fetch_events.execute_with_client(mock_client).await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Failed to fetch events"));
}

#[tokio::test]
async fn test_execute_with_client_default_output_filename() {
let temp_dir = TempDir::new().unwrap();
let _env_guard = EnvVarGuard::set(
"RAIN_ORDERBOOK_EVENTS_DIR",
temp_dir.path().to_str().unwrap(),
);

let fetch_events = FetchEvents {
api_token: "test_token".to_string(),
chain_id: 1,
start_block: 100,
end_block: Some(200),
orderbook_address: "0x123".to_string(),
output_file: None,
};

let mock_client = MockEventClient::new().with_events(vec![]);

let result = fetch_events.execute_with_client(mock_client).await;
assert!(result.is_ok());

let expected_filename = temp_dir.path().join("events_200.json");
assert!(expected_filename.exists());
}
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.

🧹 Nitpick | 🔵 Trivial

Optional: Consider additional edge case tests.

The current test suite is comprehensive. For even more thorough coverage, you could add:

  1. Invalid block range test: Verify behavior when start_block > end_block (both provided)
  2. File write failure test: Simulate filesystem errors (e.g., read-only directory)

These are nice-to-have improvements rather than requirements, as the core functionality is already well-tested.

Example test for invalid block range:

#[tokio::test]
async fn test_execute_with_client_invalid_block_range() {
    let fetch_events = FetchEvents {
        api_token: "test_token".to_string(),
        chain_id: 1,
        start_block: 200,
        end_block: Some(100), // end < start
        orderbook_address: "0x123".to_string(),
        output_file: Some("test_output.json".to_string()),
    };

    let mock_client = MockEventClient::new().with_events(vec![]);

    let result = fetch_events.execute_with_client(mock_client).await;
    assert!(result.is_err());
    // If validation is added, check error message
}
🤖 Prompt for AI Agents
crates/cli/src/commands/local_db/fetch_events.rs lines 201-319: add two
additional tests to cover the edge cases suggested—(1) an invalid block range
where start_block > end_block: construct a FetchEvents with start_block >
end_block, call execute_with_client with a MockEventClient and assert the call
returns Err (and optionally assert a specific validation error message once
validation is implemented), and (2) a file write failure: create a temporary
directory or path that will fail on write (e.g., create a temp dir, change its
permissions to read-only or point RAIN_ORDERBOOK_EVENTS_DIR to a directory
without write permissions, then call execute_with_client with output_file None
or a filename and assert it returns Err and contains a filesystem/write error);
ensure tests clean up/reset env vars and permissions after running.

@findolor findolor merged commit 55c1e1d into main Oct 4, 2025
19 checks passed
@findolor findolor deleted the local-db-cli branch October 4, 2025 07:49
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Oct 4, 2025

@coderabbitai assess this PR size classification for the totality of the PR with the following criterias and report it in your comment:

S/M/L PR Classification Guidelines:

This guide helps classify merged pull requests by effort and complexity rather than just line count. The goal is to assess the difficulty and scope of changes after they have been completed.

Small (S)

Characteristics:

  • Simple bug fixes, typos, or minor refactoring
  • Single-purpose changes affecting 1-2 files
  • Documentation updates
  • Configuration tweaks
  • Changes that require minimal context to review

Review Effort: Would have taken 5-10 minutes

Examples:

  • Fix typo in variable name
  • Update README with new instructions
  • Adjust configuration values
  • Simple one-line bug fixes
  • Import statement cleanup

Medium (M)

Characteristics:

  • Feature additions or enhancements
  • Refactoring that touches multiple files but maintains existing behavior
  • Breaking changes with backward compatibility
  • Changes requiring some domain knowledge to review

Review Effort: Would have taken 15-30 minutes

Examples:

  • Add new feature or component
  • Refactor common utility functions
  • Update dependencies with minor breaking changes
  • Add new component with tests
  • Performance optimizations
  • More complex bug fixes

Large (L)

Characteristics:

  • Major feature implementations
  • Breaking changes or API redesigns
  • Complex refactoring across multiple modules
  • New architectural patterns or significant design changes
  • Changes requiring deep context and multiple review rounds

Review Effort: Would have taken 45+ minutes

Examples:

  • Complete new feature with frontend/backend changes
  • Protocol upgrades or breaking changes
  • Major architectural refactoring
  • Framework or technology upgrades

Additional Factors to Consider

When deciding between sizes, also consider:

  • Test coverage impact: More comprehensive test changes lean toward larger classification
  • Risk level: Changes to critical systems bump up a size category
  • Team familiarity: Novel patterns or technologies increase complexity

Notes:

  • the assessment must be for the totality of the PR, that means comparing the base branch to the last commit of the PR
  • the assessment output must be exactly one of: S, M or L (single-line comment) in format of: SIZE={S/M/L}
  • do not include any additional text, only the size classification
  • your assessment comment must not include tips or additional sections
  • do NOT tag me or anyone else on your comment

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Oct 4, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the diff statistics for the entire PR
# Comparing base branch (main) to the PR branch (local-db-cli)

# First, fetch the branches
git fetch origin main:main 2>/dev/null || true
git fetch origin local-db-cli:local-db-cli 2>/dev/null || true

# Get the merge base
MERGE_BASE=$(git merge-base main local-db-cli 2>/dev/null || echo "")

if [ -z "$MERGE_BASE" ]; then
    echo "Could not find merge base, trying direct comparison"
    git diff --shortstat main...local-db-cli
    echo "---"
    git diff --numstat main...local-db-cli
else
    echo "Merge base: $MERGE_BASE"
    git diff --shortstat $MERGE_BASE...local-db-cli
    echo "---"
    git diff --numstat $MERGE_BASE...local-db-cli
fi

Length of output: 455


🏁 Script executed:

#!/bin/bash
# Get current branch name and diff against origin/main
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
echo "Current branch: $CURRENT_BRANCH"

# Get the diff statistics against origin/main
echo "=== Diff statistics ==="
git diff --shortstat origin/main

echo ""
echo "=== Detailed file changes ==="
git diff --numstat origin/main | grep -v "^-" | awk '{added+=$1; deleted+=$2; files++} END {print "Files changed:", files; print "Lines added:", added; print "Lines deleted:", deleted; print "Total changes:", added+deleted}'

echo ""
echo "=== Per file breakdown ==="
git diff --numstat origin/main | grep -v "^-"

Length of output: 177


🏁 Script executed:

#!/bin/bash
# List all files mentioned in the PR context and count their lines
echo "=== Counting lines in changed files ==="

FILES=(
  "crates/cli/.gitignore"
  "crates/cli/Cargo.toml"
  "crates/cli/src/commands/local_db/fetch_events.rs"
  "crates/cli/src/commands/local_db/mod.rs"
  "crates/cli/src/commands/local_db/decode_events.rs"
  "crates/cli/src/commands/mod.rs"
  "crates/cli/src/lib.rs"
  "crates/common/src/hyper_rpc.rs"
  "crates/common/src/raindex_client/sqlite_web/mod.rs"
)

total_lines=0
for file in "${FILES[@]}"; do
  if [ -f "$file" ]; then
    lines=$(wc -l < "$file")
    total_lines=$((total_lines + lines))
    echo "$file: $lines lines"
  else
    echo "$file: NOT FOUND"
  fi
done

echo ""
echo "Total lines in all changed files: $total_lines"

# Count lines in the new files specifically
echo ""
echo "=== New files (full content) ==="
wc -l crates/cli/src/commands/local_db/fetch_events.rs 2>/dev/null || echo "fetch_events.rs not found"
wc -l crates/cli/src/commands/local_db/decode_events.rs 2>/dev/null || echo "decode_events.rs not found"
wc -l crates/cli/src/commands/local_db/mod.rs 2>/dev/null || echo "mod.rs not found"

Length of output: 671


SIZE=L


🧠 Learnings used
Learnt from: rouzwelt
PR: rainlanguage/rain.orderbook#0
File: :0-0
Timestamp: 2025-05-21T21:24:42.275Z
Learning: For PR #1884, after each commit review, I should provide a new comment with a PR size report in the format: `TOTAL=number ADDITIONS=number DELETIONS=number`, excluding irrelevant files such as lock files (package-lock.json, cargo.lock, etc.).

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.

3 participants