Skip to content

Implement GET /v1/trades/tx/{tx_hash} endpoint#50

Merged
graphite-app[bot] merged 1 commit into
mainfrom
implement-trades-tx-hash
May 7, 2026
Merged

Implement GET /v1/trades/tx/{tx_hash} endpoint#50
graphite-app[bot] merged 1 commit into
mainfrom
implement-trades-tx-hash

Conversation

@findolor
Copy link
Copy Markdown
Collaborator

@findolor findolor commented Feb 24, 2026

Replaces #39.

Dependent PR

Motivation

See issues:

Solution

Adds the GET /v1/trades/tx/{tx_hash} endpoint that retrieves all trades associated with a given transaction hash. Updates rain.orderbook submodule, adds NotYetIndexed (202) error variant, restructures trades module into a directory with TradesDataSource trait, and includes 6 unit tests.

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)

fix #28

Summary by CodeRabbit

  • New Features
    • New trades endpoints: fetch trades by transaction (fully implemented) and by address (placeholder/returns not implemented) with pagination and aggregated trade summaries.
  • Bug Fixes
    • Transactions still being indexed now return "Not yet indexed" (HTTP 202).
  • Documentation
    • OpenAPI updated for trades endpoints; coding guideline added recommending use of the address macro for Address constants.
  • Tests
    • Unit tests and fixtures added for trade-by-transaction scenarios.

@findolor findolor self-assigned this Feb 24, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 24, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Trades module with a Raindex-backed TradesDataSource, implements GET /v1/trades/tx/{tx_hash} (processing, error mapping, and tests) and a GET /v1/trades/{address} handler (returns Internal/not-implemented), introduces ApiError::NotYetIndexed (202), updates OpenAPI references, refactors order/swap result mappings and test fixtures, and updates docs/submodule pointer.

Changes

Trades endpoint migration

Layer / File(s) Summary
API error
src/error.rs
Adds ApiError::NotYetIndexed(String), maps it to HTTP 202 Accepted and logs at info level for indexing-timeout cases.
Trades module
src/routes/trades/mod.rs
Introduces TradesDataSource trait and RaindexTradesDataSource adapter that calls get_trades_for_transaction and maps raindex timeout to NotYetIndexed or other errors to Internal.
Tx trades handler
src/routes/trades/get_by_tx.rs
Implements GET /v1/trades/tx/{tx_hash} with processing function to convert Raindex trades into TradesByTxResponse; includes unit tests for success, 404, 202, query/internal errors, and unauthenticated access.
Address trades handler
src/routes/trades/get_by_address.rs
Adds annotated GET /v1/trades/{address} handler with auth, pagination, logging, and instrumentation; currently returns an ApiError::Internal (not implemented).
Remove legacy routes file
src/routes/trades.rs
Removes prior flat route handlers and their routes() function; replaced by modular handlers.
OpenAPI docs
src/main.rs
Updates ApiDoc openapi paths to reference nested module handler paths for trades.
Order/Swap mapping & fixtures
src/routes/order/mod.rs, src/routes/swap/mod.rs
Refactors mapping chains to return Vec results inline, enriches trade fixture JSON, and adds mock_trades_list_result() and mock_empty_trades_list_result() test fixtures.
Docs & submodule
AGENTS.md, lib/rain.orderbook
Adds guideline to prefer alloy::primitives::address! for Address constants; updates lib/rain.orderbook submodule commit pointer.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant APIHandler as GET /v1/trades/tx/{tx_hash}
    participant RateLimiter
    participant Auth
    participant RaindexProvider as Raindex Provider
    participant Processor as Data Processor
    participant Response

    Client->>APIHandler: HTTP GET request
    APIHandler->>RateLimiter: Check global rate limit
    RateLimiter-->>APIHandler: ✓ Allowed
    APIHandler->>Auth: Validate authenticated key
    Auth-->>APIHandler: ✓ Authenticated
    APIHandler->>RaindexProvider: Acquire read lock
    RaindexProvider-->>APIHandler: Lock acquired
    APIHandler->>RaindexProvider: get_trades_for_transaction(tx_hash)
    alt Transaction Found & Indexed
        RaindexProvider-->>Processor: RaindexTradesListResult
        Processor->>Processor: Compute IO ratios & totals
        Processor->>Processor: Build TradeByTxEntry list
        Processor-->>APIHandler: TradesByTxResponse
        APIHandler->>Response: 200 OK + JSON
    else Transaction Not Found
        RaindexProvider-->>APIHandler: empty result
        APIHandler->>Response: 404 Not Found
    else Indexing In Progress
        RaindexProvider-->>APIHandler: TransactionIndexingTimeout
        APIHandler->>Response: 202 Accepted (NOT_YET_INDEXED)
    else Other Error
        RaindexProvider-->>APIHandler: Internal error
        APIHandler->>Response: 500 Internal Server Error
    end
    Response-->>Client: HTTP Response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • alastairong1
  • hardyjosh
  • 0xgleb

"I nibble logs and hop through queues,
When indexing sleeps I mind the views.
A 202 hums, 'Hold on, I'm near' —
I'll fetch the trades when the blocks appear. 🥕"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: implementing the GET /v1/trades/tx/{tx_hash} endpoint, which directly aligns with the primary objective from issue #28.
Linked Issues check ✅ Passed All coding requirements from issue #28 are met: endpoint returns TradesByTxResponse when trades found [get_by_tx.rs], returns 404 on NotFound [get_by_tx.rs], returns 202 via NotYetIndexed error variant [error.rs, get_by_tx.rs], and RaindexError::TransactionIndexingTimeout is mapped appropriately [mod.rs].
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the GET /v1/trades/tx/{tx_hash} endpoint or supporting infrastructure. Updates to rain.orderbook submodule, trades module restructuring, TradesDataSource trait, NotYetIndexed error, and test fixtures all support the primary objective without extraneous modifications.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch implement-trades-tx-hash

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.

@findolor findolor requested a review from hardyjosh February 24, 2026 13:11
Copy link
Copy Markdown

@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

🧹 Nitpick comments (4)
src/routes/trades/get_by_tx.rs (2)

83-97: Use B256 directly as the order_cache key instead of String.

The order hash goes through a round-trip: order_hash().to_string()B256::from_str(...) in multiple places (lines 88, 171). The order_cache is keyed by String (line 110) and looked up via .to_string() (line 130). Using B256 as the key eliminates all string conversions, removes the parse-failure error paths (lines 88–91, 171–174), and simplifies the code.

Sketch of the refactor
-    let order_cache: HashMap<
-        String,
-        Option<rain_orderbook_common::raindex_client::orders::RaindexOrder>,
-    > = order_results
+    let order_cache: HashMap<
+        B256,
+        Option<rain_orderbook_common::raindex_client::orders::RaindexOrder>,
+    > = order_results
         .into_iter()
         .collect::<Result<Vec<_>, _>>()?
         .into_iter()
-        .map(|(hash, order)| (hash.to_string(), order))
+        .map(|(hash, order)| (hash, order))
         .collect();

Then look up with the B256 directly instead of converting to string first, and skip the re-parse at line 171.

Also applies to: 109-117, 130-138, 171-174

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

In `@src/routes/trades/get_by_tx.rs` around lines 83 - 97, The code currently
round-trips order_hash() to String and back to B256 and uses order_cache keyed
by String; change order_cache to use B256 keys and stop converting order hashes
to strings: build unique_hashes as Vec<B256> by mapping order_hash() (or its
bytes) directly without parse-from-string (update the unique_hashes block and
remove the B256::from_str error handling), change all lookups/insertions to use
the B256 key (replace .to_string() lookups with direct B256 lookups), and remove
the redundant re-parse at the later lookup (the parse in the block around the
earlier re-parse, e.g., where order_cache is accessed at lines referenced in the
review) so there is no string conversion or parse failure path.

119-127: Repetitive Float::parse("0") with identical error handling.

Float::parse("0".to_string()) is called four times (lines 120, 124, 152, 193) with the same error-mapping boilerplate. A small helper would reduce noise:

Example helper
fn float_zero() -> Result<Float, ApiError> {
    Float::parse("0".to_string()).map_err(|e| {
        tracing::error!(error = %e, "float parse error");
        ApiError::Internal("float parse error".into())
    })
}

Also applies to: 152-155, 193-196

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

In `@src/routes/trades/get_by_tx.rs` around lines 119 - 127, Create a small helper
function (e.g., float_zero -> Result<Float, ApiError>) that encapsulates
Float::parse("0".to_string()) with the current map_err closure (tracing::error!
and ApiError::Internal) and replace the repeated calls initializing total_input,
total_output and any other zero-initialized Float occurrences in get_by_tx (the
variables named total_input, total_output and other similar parse sites) to call
float_zero() instead; ensure you update use sites to handle the Result the same
way as before so behavior/error logging remains identical.
src/routes/trades/mod.rs (1)

28-53: Sequential orderbook iteration — early-return on TradesIndexingTimeout discards partial results.

If orderbook A returns trades and orderbook B hits TradesIndexingTimeout, the already-collected trades from A are discarded. This all-or-nothing behavior seems intentional for consistency, but it's worth documenting this design decision since it might surprise callers. If the number of configured orderbooks grows, the sequential iteration could also become a latency bottleneck — parallel queries with join_all would help there.

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

In `@src/routes/trades/mod.rs` around lines 28 - 53, The current loop over
orderbooks collects trades into all_trades but returns early on
RaindexError::TradesIndexingTimeout, discarding partial results; change the
logic in the loop that calls self.client.get_trades_for_transaction to (a)
record a timeout occurrence instead of immediate return (e.g., set a flag like
saw_timeout) and continue collecting from other orderbooks so all_trades
preserves partial results, and (b) after the loop, if saw_timeout is true return
ApiError::NotYetIndexed (or include both partial results and a NotYetIndexed
indicator per design); additionally consider switching the sequential loop to
parallel queries using futures::future::join_all on calls to
get_trades_for_transaction to reduce latency and document the chosen behavior
around partial results and timeouts.
src/error.rs (1)

61-67: Info-level logging is appropriate here.

Good call using tracing::info! rather than warn! since this is an expected transient state, not an error. However, consider adding a unit test for the NotYetIndexed variant similar to the existing error tests (lines 146–174) to ensure the 202 status and NOT_YET_INDEXED code are correctly returned.

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

In `@src/error.rs` around lines 61 - 67, Add a unit test that mirrors the existing
error tests (lines 146–174) to verify ApiError::NotYetIndexed behavior:
construct or trigger ApiError::NotYetIndexed, call the same conversion/response
code path used by the other error tests, and assert the HTTP status is 202 and
the error code string equals "NOT_YET_INDEXED"; reference the
ApiError::NotYetIndexed variant and the response conversion function used in the
other tests to place the assertion so it fails if the status or code change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/routes/trades/get_by_address.rs`:
- Line 40: The handler currently calls todo!() inside the async closure passed
to run_with_client, which will panic at runtime; replace the todo!() with a
proper error return (e.g., return an Err(ApiError::Internal("not
implemented".into())) or construct and return an appropriate ApiError result
type from the async block) or gate the entire route behind a feature flag so it
is not registered; locate the closure passed to run_with_client in
get_by_address.rs and update it to return a Result type consistent with the
route handler instead of calling todo!(), referencing ApiError::Internal (or the
project's error constructor) to produce a graceful error response.

In `@src/routes/trades/get_by_tx.rs`:
- Around line 152-169: The code computes abs_output from zero -
output_vc.amount() and then divides input_vc.amount() by abs_output to get
actual_io_ratio (and similarly for abs_total_output/total_io_ratio); add an
explicit zero check after computing abs_output and abs_total_output: if either
is zero (or very close to zero), avoid the divide and return a clear,
descriptive result (e.g., set actual_io_ratio/total_io_ratio to "N/A" or "0" or
return ApiError::BadRequest with a descriptive message) instead of relying on
the Float error; update the branches around the Float division/format steps
(references: zero, abs_output, actual_io_ratio, abs_total_output,
total_io_ratio) to short-circuit on zero and log a specific message before
returning the chosen response.

---

Nitpick comments:
In `@src/error.rs`:
- Around line 61-67: Add a unit test that mirrors the existing error tests
(lines 146–174) to verify ApiError::NotYetIndexed behavior: construct or trigger
ApiError::NotYetIndexed, call the same conversion/response code path used by the
other error tests, and assert the HTTP status is 202 and the error code string
equals "NOT_YET_INDEXED"; reference the ApiError::NotYetIndexed variant and the
response conversion function used in the other tests to place the assertion so
it fails if the status or code change.

In `@src/routes/trades/get_by_tx.rs`:
- Around line 83-97: The code currently round-trips order_hash() to String and
back to B256 and uses order_cache keyed by String; change order_cache to use
B256 keys and stop converting order hashes to strings: build unique_hashes as
Vec<B256> by mapping order_hash() (or its bytes) directly without
parse-from-string (update the unique_hashes block and remove the B256::from_str
error handling), change all lookups/insertions to use the B256 key (replace
.to_string() lookups with direct B256 lookups), and remove the redundant
re-parse at the later lookup (the parse in the block around the earlier
re-parse, e.g., where order_cache is accessed at lines referenced in the review)
so there is no string conversion or parse failure path.
- Around line 119-127: Create a small helper function (e.g., float_zero ->
Result<Float, ApiError>) that encapsulates Float::parse("0".to_string()) with
the current map_err closure (tracing::error! and ApiError::Internal) and replace
the repeated calls initializing total_input, total_output and any other
zero-initialized Float occurrences in get_by_tx (the variables named
total_input, total_output and other similar parse sites) to call float_zero()
instead; ensure you update use sites to handle the Result the same way as before
so behavior/error logging remains identical.

In `@src/routes/trades/mod.rs`:
- Around line 28-53: The current loop over orderbooks collects trades into
all_trades but returns early on RaindexError::TradesIndexingTimeout, discarding
partial results; change the logic in the loop that calls
self.client.get_trades_for_transaction to (a) record a timeout occurrence
instead of immediate return (e.g., set a flag like saw_timeout) and continue
collecting from other orderbooks so all_trades preserves partial results, and
(b) after the loop, if saw_timeout is true return ApiError::NotYetIndexed (or
include both partial results and a NotYetIndexed indicator per design);
additionally consider switching the sequential loop to parallel queries using
futures::future::join_all on calls to get_trades_for_transaction to reduce
latency and document the chosen behavior around partial results and timeouts.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d773fed and 5e34ceb.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • AGENTS.md
  • Cargo.toml
  • lib/rain.orderbook
  • src/error.rs
  • src/main.rs
  • src/routes/trades.rs
  • src/routes/trades/get_by_address.rs
  • src/routes/trades/get_by_tx.rs
  • src/routes/trades/mod.rs
💤 Files with no reviewable changes (1)
  • src/routes/trades.rs

Comment thread src/routes/trades/get_by_address.rs Outdated
Comment thread src/routes/trades/get_by_tx.rs Outdated
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
src/routes/trades/mod.rs (1)

11-14: Consider extending the trait for address-based queries to maintain consistency.

The get_trades_by_address handler currently uses direct shared_raindex State access via run_with_client(), while get_trades_by_tx uses the TradesDataSource trait abstraction. Since the address-based handler is still incomplete, adding a corresponding trait method would maintain consistency with the existing data source abstraction pattern once the implementation is finalized.

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

In `@src/routes/trades/mod.rs` around lines 11 - 14, Add a new trait method to
TradesDataSource (e.g., async fn get_trades_by_address(&self, address: H160) ->
Result<RaindexTradesListResult, ApiError>) so address-based queries follow the
same abstraction as get_trades_by_tx, then update all implementations of
TradesDataSource to implement get_trades_by_address and refactor the
get_trades_by_address handler to call
state.run_with_client().get_trades_by_address(...) instead of accessing
shared_raindex directly; ensure the signature and return type match
RaindexTradesListResult/ApiError used by get_trades_by_tx.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/routes/trades/mod.rs`:
- Around line 11-14: Add a new trait method to TradesDataSource (e.g., async fn
get_trades_by_address(&self, address: H160) -> Result<RaindexTradesListResult,
ApiError>) so address-based queries follow the same abstraction as
get_trades_by_tx, then update all implementations of TradesDataSource to
implement get_trades_by_address and refactor the get_trades_by_address handler
to call state.run_with_client().get_trades_by_address(...) instead of accessing
shared_raindex directly; ensure the signature and return type match
RaindexTradesListResult/ApiError used by get_trades_by_tx.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6f67f612-ca02-455d-a43a-e11730689ce0

📥 Commits

Reviewing files that changed from the base of the PR and between 5e34ceb and 59070a6.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • lib/rain.orderbook
  • src/raindex/config.rs
  • src/routes/order/mod.rs
  • src/routes/swap/mod.rs
  • src/routes/trades/get_by_tx.rs
  • src/routes/trades/mod.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/rain.orderbook

Copy link
Copy Markdown

@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

♻️ Duplicate comments (1)
src/routes/trades/get_by_address.rs (1)

39-39: ⚠️ Potential issue | 🔴 Critical

Remove the todo!() panic from this handler.

Any request that reaches this route will abort the task instead of returning an ApiError, so the documented API contract is never honored.

Minimal fix
-        todo!()
+        Err(ApiError::Internal(
+            "get_trades_by_address is not implemented".into(),
+        ))

As per coding guidelines, "Never use expect or unwrap in production code; handle errors gracefully or exit with a message".

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

In `@src/routes/trades/get_by_address.rs` at line 39, The handler in
src/routes/trades/get_by_address.rs currently calls todo!(), which panics;
replace that panic with proper error handling that returns an ApiError through
the handler's Result type (i.e., remove todo!() and return Err(ApiError::... )
or implement the real logic and propagate errors using ?). Locate the
get_by_address route handler (the function containing todo!()) and ensure you
never use unwrap/expect; instead construct or map errors into ApiError (e.g.,
ApiError::internal or an appropriate variant) so the API contract is honored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/routes/trades/get_by_tx.rs`:
- Around line 101-115: The code currently takes only the first pair summary
(result.summary().and_then(|s| s.first())) to populate TradesTotals, which drops
all other pairs; instead iterate over all summaries from result.summary() and
aggregate totals: sum each summary's numeric input and output amounts to produce
total_input_amount and total_output_amount, and compute average_io_ratio as an
appropriate aggregate (e.g., a weighted average using per-pair IO ratios
weighted by input/output or total volumes). Replace uses of
summary.formatted_total_input()/formatted_total_output()/formatted_average_io_ratio()
for the single summary with aggregation over all summaries and then format the
final aggregated values into strings for TradesTotals when building
TradesByTxResponse.

---

Duplicate comments:
In `@src/routes/trades/get_by_address.rs`:
- Line 39: The handler in src/routes/trades/get_by_address.rs currently calls
todo!(), which panics; replace that panic with proper error handling that
returns an ApiError through the handler's Result type (i.e., remove todo!() and
return Err(ApiError::... ) or implement the real logic and propagate errors
using ?). Locate the get_by_address route handler (the function containing
todo!()) and ensure you never use unwrap/expect; instead construct or map errors
into ApiError (e.g., ApiError::internal or an appropriate variant) so the API
contract is honored.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4710cca8-411c-4ffa-9833-46d28a5b6d4a

📥 Commits

Reviewing files that changed from the base of the PR and between 59070a6 and 39f65e4.

📒 Files selected for processing (6)
  • src/error.rs
  • src/main.rs
  • src/routes/order/mod.rs
  • src/routes/trades/get_by_address.rs
  • src/routes/trades/get_by_tx.rs
  • src/routes/trades/mod.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main.rs
  • src/routes/trades/mod.rs
  • src/error.rs

Comment on lines +101 to +115
let summary = result.summary().and_then(|s| s.first()).ok_or_else(|| {
tracing::error!("no pair summary in trades result");
ApiError::Internal("missing pair summary".into())
})?;

Ok(Json(TradesByTxResponse {
tx_hash,
block_number,
timestamp,
sender,
trades: trade_entries,
totals: TradesTotals {
total_input_amount: summary.formatted_total_input().to_string(),
total_output_amount: summary.formatted_total_output().to_string(),
average_io_ratio: summary.formatted_average_io_ratio().to_string(),
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Mar 10, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't derive transaction totals from only the first pair summary.

result.summary() is pair-scoped, so first() makes totals ignore every pair after the first while trades still includes them all. For mixed-pair transactions, total_input_amount, total_output_amount, and average_io_ratio become wrong.

Minimal guard
-    let summary = result.summary().and_then(|s| s.first()).ok_or_else(|| {
-        tracing::error!("no pair summary in trades result");
-        ApiError::Internal("missing pair summary".into())
-    })?;
+    let summaries = result.summary().ok_or_else(|| {
+        tracing::error!("no pair summary in trades result");
+        ApiError::Internal("missing pair summary".into())
+    })?;
+    if summaries.len() != 1 {
+        tracing::error!(
+            summary_count = summaries.len(),
+            "expected exactly one pair summary for tx"
+        );
+        return Err(ApiError::Internal(
+            "mixed-pair transaction summaries are not supported".into(),
+        ));
+    }
+    let summary = &summaries[0];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/trades/get_by_tx.rs` around lines 101 - 115, The code currently
takes only the first pair summary (result.summary().and_then(|s| s.first())) to
populate TradesTotals, which drops all other pairs; instead iterate over all
summaries from result.summary() and aggregate totals: sum each summary's numeric
input and output amounts to produce total_input_amount and total_output_amount,
and compute average_io_ratio as an appropriate aggregate (e.g., a weighted
average using per-pair IO ratios weighted by input/output or total volumes).
Replace uses of
summary.formatted_total_input()/formatted_total_output()/formatted_average_io_ratio()
for the single summary with aggregation over all summaries and then format the
final aggregated values into strings for TradesTotals when building
TradesByTxResponse.

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.

Currently we don't have mixed pair transactions in st0x. if we ever have those we would also need to change the response.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@findolor, understood — that's a useful constraint to know. Since mixed-pair transactions are intentionally out of scope for now, first() is a reasonable simplification.

That said, it might be worth adding a lightweight defensive guard to catch unexpected multi-pair data in production before it silently produces wrong totals:

let summaries = result.summary().ok_or_else(|| {
    tracing::error!("no pair summary in trades result");
    ApiError::Internal("missing pair summary".into())
})?;

if summaries.len() > 1 {
    tracing::warn!(
        summary_count = summaries.len(),
        "unexpected multi-pair summary; only the first pair will be used for totals"
    );
}

let summary = &summaries[0];

This keeps the current single-pair behaviour intact while surfacing the anomaly in logs if it ever occurs. Up to you whether to add it — happy to close this comment if you'd prefer to leave the code as-is for now.


✏️ Learnings added
Learnt from: findolor
Repo: ST0x-Technology/st0x.rest.api PR: 50
File: src/routes/trades/get_by_tx.rs:101-115
Timestamp: 2026-03-10T07:35:34.283Z
Learning: In src/routes/trades/get_by_tx.rs, using result.summary().and_then(|s| s.first()) to derive TradesTotals is intentional: st0x does not currently have mixed-pair transactions, so only one pair summary is expected per transaction. If mixed-pair transactions are ever supported, the TradesByTxResponse structure and this aggregation logic would both need to change. Don't flag single-pair summary assumptions in this file as bugs unless the product scope has changed to include multi-pair transactions.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: findolor
Repo: ST0x-Technology/st0x.rest.api PR: 18
File: src/fairings/rate_limiter.rs:158-203
Timestamp: 2026-02-13T09:37:37.357Z
Learning: When multiple guards or layers write to the same cached rate-limit information, document and enforce the execution order so that the client-visible headers reflect the intended quota (global vs per-key). In this repo, rate-limiter.rs and auth-related guards intentionally write to the same CachedRateLimitInfo entry; ensure the per-key quota is what’s surfaced to clients and that the order is explicit in route signatures. Include tests that verify the final headers reflect the intended quota and that writes are intentional and auditable.

Learnt from: findolor
Repo: ST0x-Technology/st0x.rest.api PR: 38
File: src/routes/swap/mod.rs:115-129
Timestamp: 2026-02-17T09:35:57.431Z
Learning: In Rust server code for swap endpoints, if the response contains a non-empty approvals array, the behavior uses a two-phase flow: first the client sends an approval transaction, then queries the endpoint again to obtain the actual swap calldata. In the approval response, set to = approval.spender() to signal the eventual target, and set data = Bytes::new() (empty) to indicate 'no transaction yet.' Prefer this approach over using Address::ZERO for clarity. This pattern is specific to the swap flow in src/routes/swap/mod.rs and should be followed in similar swap-related endpoints with analogous approval semantics.

Learnt from: findolor
Repo: ST0x-Technology/st0x.rest.api PR: 50
File: src/routes/trades/get_by_tx.rs:152-169
Timestamp: 2026-02-25T07:09:51.723Z
Learning: In trade-processing code (e.g., src/routes/trades/get_by_tx.rs and related files), rely on the guarantee from Rain OrderBookV6 takeOrders4 that all TakeOrderV3 events have non-zero output amounts. Since orders with outputMax.isZero() are skipped and OrderZeroAmount is emitted, division-by-zero checks for IO ratio calculations when processing indexed trades from the API are unnecessary. Apply this guidance to similar files handling indexed trade data, and ensure tests reflect the non-zero assumption.

@findolor findolor requested review from 0xgleb and JuaniRios April 6, 2026 05:35
})
.collect();

let summary = result.summary().and_then(|s| s.first()).ok_or_else(|| {
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.

does doing s.first() get only 1 asset pair and discard the rest? Is this intended or should we display all pairs?

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.

this is fine for us now since we don't have multiple asset pairs. this also requires a new version of the api which we will not be doing atm but could do in the future

Comment thread src/routes/trades/get_by_address.rs Outdated
async move {
tracing::info!(address = ?address, params = ?params, "request received");
let _raindex = shared_raindex.read().await;
todo!()
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.

There are 4 todo! lines in the repo including this one. We should log a todo message instead of panicking the API when this gets called

Comment thread src/routes/trades/mod.rs
Comment on lines +33 to +35
ApiError::NotYetIndexed(format!(
"transaction {tx_hash:#x} not yet indexed after {attempts} attempts"
))
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.

There's already a log on error.rs for this error type, are both always getting triggered? if so one should be removed to not duplicate logs

@alastairong1
Copy link
Copy Markdown
Contributor

LGTM — we independently implemented the same NotYetIndexed → 202 pattern (now reverted). The RaindexError::TradesIndexingTimeout mapping here is the right approach. Approving.

Copy link
Copy Markdown
Collaborator Author


How to use the Graphite Merge Queue

Add the label add-to-gt-merge-queue to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@findolor findolor force-pushed the implement-trades-tx-hash branch from 39f65e4 to 69db573 Compare May 7, 2026 13:28
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/routes/order/mod.rs`:
- Around line 180-185: The fixture trade_json() defines the "orderbook" key
twice, causing the first value to be silently overridden; locate the
trade_json() fixture in src/routes/order/mod.rs and remove the duplicate
"orderbook" entry so the object has a single "orderbook" property (keep the
correct value), ensuring no other keys are duplicated and the resulting JSON
remains consistent with tests that reference orderbook/orderHash/ioRatio.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 566526e9-97bf-4b03-9986-553e990509f1

📥 Commits

Reviewing files that changed from the base of the PR and between 39f65e4 and 69db573.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • AGENTS.md
  • lib/rain.orderbook
  • src/error.rs
  • src/main.rs
  • src/routes/order/mod.rs
  • src/routes/swap/mod.rs
  • src/routes/trades.rs
  • src/routes/trades/get_by_address.rs
  • src/routes/trades/get_by_tx.rs
  • src/routes/trades/mod.rs
💤 Files with no reviewable changes (1)
  • src/routes/trades.rs
✅ Files skipped from review due to trivial changes (3)
  • AGENTS.md
  • lib/rain.orderbook
  • src/main.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/error.rs
  • src/routes/trades/get_by_address.rs
  • src/routes/trades/get_by_tx.rs
  • src/routes/trades/mod.rs

Comment thread src/routes/order/mod.rs
@findolor findolor force-pushed the implement-trades-tx-hash branch from 69db573 to f7ecb76 Compare May 7, 2026 14:34
@findolor findolor requested a review from JuaniRios May 7, 2026 14:37
@findolor findolor force-pushed the implement-trades-tx-hash branch from f7ecb76 to 6029bce Compare May 7, 2026 17:41
@graphite-app
Copy link
Copy Markdown

graphite-app Bot commented May 7, 2026

Merge activity

Replaces #39.

## Dependent PR
- rainlanguage/raindex#2497

## Motivation

See issues:
- #28

## Solution

Adds the `GET /v1/trades/tx/{tx_hash}` endpoint that retrieves all trades associated with a given transaction hash. Updates `rain.orderbook` submodule, adds `NotYetIndexed` (202) error variant, restructures trades module into a directory with `TradesDataSource` trait, and includes 6 unit tests.

## Checks

By submitting this for review, I'm confirming I've done the following:
- [x] made this PR as small as possible
- [x] unit-tested any new functionality
- [x] linked any relevant issues or PRs
- [ ] included screenshots (if this involves a front-end change)

fix #28

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * New trades endpoints: fetch trades by transaction (fully implemented) and by address (placeholder/returns not implemented) with pagination and aggregated trade summaries.
* **Bug Fixes**
  * Transactions still being indexed now return "Not yet indexed" (HTTP 202).
* **Documentation**
  * OpenAPI updated for trades endpoints; coding guideline added recommending use of the address macro for Address constants.
* **Tests**
  * Unit tests and fixtures added for trade-by-transaction scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@graphite-app graphite-app Bot force-pushed the implement-trades-tx-hash branch from 6029bce to c94fa3a Compare May 7, 2026 21:36
@graphite-app graphite-app Bot merged commit c94fa3a into main May 7, 2026
10 checks passed
graphite-app Bot pushed a commit that referenced this pull request May 8, 2026
## Chained PRs
- #50

## Motivation

See issues:
- #29
- #30

Add a paginated endpoint to retrieve all trades for a given owner address across all configured orderbooks, with optional time filtering. Replaces #40, which was based on the old chain (#39) and couldn't merge cleanly. This branch is rebased on top of `implement-trades-tx-hash` (PR #50).

## Solution

- Implement `GET /v1/trades/{address}` with pagination (`page`, `pageSize`) and optional time filters (`startTime`, `endTime`)
- Extend `TradesDataSource` trait with `get_trades_for_owner` method that iterates all configured orderbooks and aggregates trades and total counts
- Wire the route handler to use `RaindexTradesDataSource` via `run_with_client`, converting `TradesPaginationParams` into `PaginationParams` + `TimeFilter` with overflow validation
- Map each `RaindexTrade` into `TradeByAddress` response type (tx_hash, formatted amounts, `TokenRef` for input/output tokens, order_hash, timestamp, block_number)
- Compute pagination metadata (total_pages via `div_ceil`, has_more)
- Update `rain.orderbook` submodule (`ff9578c` → `a612a4f`) to bring in `get_trades_for_owner()`, `RaindexTradesListResult`, `PaginationParams`, `TimeFilter`, `OrderbookIdentifierParams`, and address casing fix
- Update `get_by_tx.rs` mock to implement the new trait method
- Add AGENTS.md submodule rule: never modify submodule code directly
- Unit tests for success (with amount/token assertions), empty results, query failure, 401 without auth, and 500 on bad config

## Checks

By submitting this for review, I'm confirming I've done the following:
- [x] made this PR as small as possible
- [x] unit-tested any new functionality
- [x] linked any relevant issues or PRs
- [ ] included screenshots (if this involves a front-end change)

fix #29

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * Implemented the trade lookup endpoint for wallet addresses with pagination and time-range filtering capabilities.

* **Documentation**
  * Added guidelines for managing external code dependencies.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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.

Implement GET /v1/trades/tx/{tx_hash}

4 participants